Sur les traces des calculatrices: Qalculate


Auparavant, nous avons effectué des révisions de code de grands packages mathématiques, par exemple, Scilab et Octave, où les calculatrices sont restées à l'écart en tant que petits utilitaires, dans lesquels il est difficile de faire des erreurs en raison de leur petite base de code. Nous avions tort de ne pas leur avoir prêté attention. Le cas de la publication du code source de la calculatrice Windows a montré qu'en fait, tout le monde était intéressé à discuter des types d'erreurs qui s'y cachent. De plus, le nombre d'erreurs était largement suffisant pour écrire un article à ce sujet. Mes collègues et moi, nous avons décidé d'explorer le code d'un certain nombre de calculatrices populaires, et il s'est avéré que le code de la calculatrice Windows n'était pas si mauvais (spoiler).

Présentation


Calculez! est une calculatrice de bureau multi-plateforme multi-usages. Il est simple à utiliser, mais offre une puissance et une polyvalence normalement réservées aux packages mathématiques complexes, ainsi que des outils utiles pour les besoins quotidiens (tels que la conversion de devises et le calcul du pourcentage). Le projet se compose de deux composants: libqalculate (bibliothèque et CLI) et qalculate-gtk (GTK + UI). L'étude ne portait que sur le code libqalculate.

Pour comparer facilement le projet avec Windows Calculator, que nous avons récemment vérifié, je cite la sortie de l'utilitaire Cloc pour libqalculate:


En le considérant subjectivement, il contient plus d'erreurs et elles sont plus critiques que dans le code de la calculatrice Windows. Néanmoins, je recommanderais de tirer des conclusions par vous-même, après avoir lu cet aperçu du code.

À propos, voici un lien vers un article sur la vérification de la calculatrice de Microsoft: " Counting Bugs in Windows Calculator ".

L'outil d'analyse est l'analyseur de code statique PVS-Studio . Il s'agit d'un ensemble de solutions pour le contrôle de la qualité du code, la recherche de bogues et les vulnérabilités potentielles. Les langages pris en charge incluent: C, C ++, C # et Java. Vous pouvez exécuter l'analyseur sur Windows, Linux et macOS.

Copiez-collez et Typos à nouveau!


V523 L' instruction 'then' est équivalente à l'instruction 'else'. Number.cc 4018

bool Number::square() { .... if(mpfr_cmpabs(i_value->internalLowerFloat(), i_value->internalUpperFloat()) > 0) { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } else { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } .... } 

Le code est absolument le même dans les blocs if et else . Les fragments de code adjacents sont très similaires à celui-ci, mais différentes fonctions y sont utilisées: internalLowerFloat () et internalUpperFloat () . Il est sûr de supposer qu'un développeur a copié le code et oublié de corriger le nom de la fonction ici.

V501 Il existe des sous-expressions identiques '! Mtr2.number (). IsReal ()' à gauche et à droite de '||' opérateur. BuiltinFunctions.cc 6274

 int IntegrateFunction::calculate(....) { .... if(!mtr2.isNumber() || !mtr2.number().isReal() || !mtr.isNumber() || !mtr2.number().isReal()) b_unknown_precision = true; .... } 

Dans ce cas, des expressions dupliquées sont apparues en raison du fait qu'à un endroit mtr2 a été écrit au lieu de mtr. Ainsi, un appel de la fonction mtr.number (). IsReal () est absent dans la condition.

V501 Il existe des sous-expressions identiques «vargs [1] .representNonPositive ()» à gauche et à droite de «||» opérateur. BuiltinFunctions.cc 5785



Nous n'aurions jamais trouvé de défauts dans ce code manuellement! Mais voilà. De plus, dans le fichier d'origine, ces fragments sont écrits sur une seule ligne. L'analyseur a détecté une expression dupliquée vargs [1] .representNonPositive () , qui peut indiquer une faute de frappe ou, par conséquent, une erreur potentielle.

Voici la liste complète des endroits suspects, que l'on peut à peine décoder.

  • V501 Il existe des sous-expressions identiques «vargs [1] .representNonPositive ()» à gauche et à droite de «||» opérateur. BuiltinFunctions.cc 5788
  • V501 Il existe des sous-expressions identiques «ajouter» à gauche et à droite de l'opérateur «&&». MathStructure.cc 1780
  • V501 Il existe des sous-expressions identiques «ajouter» à gauche et à droite de l'opérateur «&&». MathStructure.cc 2043
  • V501 Il existe des sous-expressions identiques '(* v_subs [v_order [1]]). ReprésenteNégatif (vrai)' à gauche et à droite de l'opérateur '&&'. MathStructure.cc 5569

Boucle avec condition incorrecte


V534 Il est probable qu'une mauvaise variable soit comparée à l'intérieur de l'opérateur 'for'. Pensez à revoir «i». MathStructure.cc 28741

 bool MathStructure::isolate_x_sub(....) { .... for(size_t i = 0; i < mvar->size(); i++) { if((*mvar)[i].contains(x_var)) { mvar2 = &(*mvar)[i]; if(mvar->isMultiplication()) { for(size_t i2 = 0; i < mvar2->size(); i2++) { if((*mvar2)[i2].contains(x_var)) {mvar2 = &(*mvar2)[i2]; break;} } } break; } } .... } 

Dans la boucle interne, la variable i2 représente un compteur, mais en raison d'une faute de frappe, une erreur a été commise - la variable i de la boucle externe est utilisée dans la condition de sortie de la boucle.

Redondance ou erreur?


V590 Envisagez d'inspecter cette expression. L'expression est excessive ou contient une erreur d'impression. Number.cc 6564

 bool Number::add(const Number &o, MathOperation op) { .... if(i1 >= COMPARISON_RESULT_UNKNOWN && (i2 == COMPARISON_RESULT_UNKNOWN || i2 != COMPARISON_RESULT_LESS)) return false; .... } 

Il y a 3 ans après avoir eu un aperçu de ce code, j'ai écrit une feuille de triche pour moi et d'autres développeurs: " Expressions logiques en C / C ++. Erreurs commises par des professionnels ". Quand je tombe sur un tel code, je m'assure que la note n'est pas devenu moins pertinent. Vous pouvez consulter l'article, trouver un modèle d'erreur correspondant au code et découvrir toutes les nuances.

Dans le cas de cet exemple, nous irons à la section «Expression == || ! = "Et découvrez que l'expression i2 == COMPARISON_RESULT_UNKNOWN n'affecte rien.

Déréférencer les pointeurs non contrôlés


V595 Le pointeur 'o_data' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 1108, 1112. DataSet.cc 1108

 string DataObjectArgument::subprintlong() const { string str = _("an object from"); str += " \""; str += o_data->title(); // <= str += "\""; DataPropertyIter it; DataProperty *o = NULL; if(o_data) { // <= o = o_data->getFirstProperty(&it); } .... } 

Dans une fonction, le pointeur o_data est déréférencé à la fois sans et avec une vérification. Cela peut être du code redondant ou une erreur potentielle. Je me penche vers ce dernier.

Il y a deux endroits similaires:

  • V595 Le pointeur 'o_assumption' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 229, 230. Variable.cc 229
  • V595 Le pointeur 'i_value' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 3412, 3427. Number.cc 3412

gratuit () ou supprimer []?


V611 La mémoire a été allouée à l'aide de l'opérateur "nouveau" mais a été libérée à l'aide de la fonction "libre". Pensez à inspecter les logiques d'opération derrière la variable «remcopy». Number.cc 8123

 string Number::print(....) const { .... while(!exact && precision2 > 0) { if(try_infinite_series) { remcopy = new mpz_t[1]; // <= mpz_init_set(*remcopy, remainder); } mpz_mul_si(remainder, remainder, base); mpz_tdiv_qr(remainder, remainder2, remainder, d); exact = (mpz_sgn(remainder2) == 0); if(!started) { started = (mpz_sgn(remainder) != 0); } if(started) { mpz_mul_si(num, num, base); mpz_add(num, num, remainder); } if(try_infinite_series) { if(started && first_rem_check == 0) { remainders.push_back(remcopy); } else { if(started) first_rem_check--; mpz_clear(*remcopy); free(remcopy); // <= } } .... } .... } 

La mémoire du tableau remcopy est allouée et libérée de différentes manières, ce qui est une grave erreur.

Changements perdus


 bool expand_partial_fractions(MathStructure &m, ....) { .... if(b_poly && !mquo.isZero()) { MathStructure m = mquo; if(!mrem.isZero()) { m += mrem; m.last() *= mtest[i]; m.childrenUpdated(); } expand_partial_fractions(m, eo, false); return true; } .... } 

La variable m dans la fonction est passée par référence, ce qui signifie sa modification. Cependant, l'analyseur a détecté que le code contient la variable avec le même nom, qui chevauche la portée du paramètre de la fonction, permettant la perte de modifications.

Pointeurs étranges


V774 Le pointeur «cu» a été utilisé après la libération de la mémoire. Calculator.cc 3595

 MathStructure Calculator::convertToBestUnit(....) { .... CompositeUnit *cu = new CompositeUnit("", "...."); cu->add(....); Unit *u = getBestUnit(cu, false, eo.local_currency_conversion); if(u == cu) { delete cu; // <= return mstruct_new; } delete cu; // <= if(eo.approximation == APPROXIMATION_EXACT && cu->hasApproximateRelationTo(u, true)) { // <= if(!u->isRegistered()) delete u; return mstruct_new; } .... } 

L'analyseur avertit que le code appelle une méthode de l'objet cu juste après la désallocation de la mémoire. Mais quand on essaie de s'y attaquer, le code s'avère encore plus étrange. Premièrement, appeler delete cu se produit toujours - à la fois dans la condition et après cela. Deuxièmement, le code après la condition implique que les pointeurs u et cu ne sont pas égaux, ce qui signifie qu'après la suppression de l'objet cu , il est tout à fait logique d'utiliser l'objet u . Très probablement, une faute de frappe a été faite dans le code et l'auteur du code voulait utiliser uniquement la variable u .

Utilisation de la fonction de recherche


V797 La fonction 'find' est utilisée comme si elle retournait un type booléen. La valeur de retour de la fonction doit probablement être comparée à std :: string :: npos. Unit.cc 404

 MathStructure &AliasUnit::convertFromFirstBaseUnit(....) const { if(i_exp != 1) mexp /= i_exp; ParseOptions po; if(isApproximate() && suncertainty.empty() && precision() == -1) { if(sinverse.find(DOT) || svalue.find(DOT)) po.read_precision = READ_PRECISION_WHEN_DECIMALS; else po.read_precision = ALWAYS_READ_PRECISION; } .... } 

Même si le code peut être compilé avec succès, il semble suspect, car la fonction find renvoie le numéro du type std :: string :: size_type . La condition sera vraie si le point est trouvé dans n'importe quelle partie de la chaîne sauf si le point est au début. C'est un étrange chèque. Je ne suis pas sûr mais, peut-être, ce code devrait être réécrit comme suit:

 if( sinverse.find(DOT) != std::string::npos || svalue.find(DOT) != std::string::npos) { po.read_precision = READ_PRECISION_WHEN_DECIMALS; } 

Fuite potentielle de mémoire


V701 realloc () fuite possible: lorsque realloc () échoue dans l'allocation de mémoire, le «tampon» du pointeur d'origine est perdu. Pensez à affecter realloc () à un pointeur temporaire. util.cc 703

 char *utf8_strdown(const char *str, int l) { #ifdef HAVE_ICU .... outlength = length + 4; buffer = (char*) realloc(buffer, outlength * sizeof(char)); // <= .... #else return NULL; #endif } 

Lorsque vous travaillez avec la fonction realloc () , il est recommandé d'utiliser un tampon intermédiaire, car dans le cas où il est impossible d'allouer de la mémoire, le pointeur vers l'ancienne zone de mémoire sera irrémédiablement perdu.

Conclusion


Le Qalculate! Le projet arrive en tête de liste des meilleures calculatrices gratuites, alors qu'il contient de nombreuses erreurs graves. D'un autre côté, nous n'avons pas encore vérifié ses concurrents. Nous allons essayer de parcourir toutes les calculatrices populaires.

En ce qui concerne la comparaison avec la qualité de la calculatrice du monde Windows, l'utilitaire de Microsoft semble jusqu'à présent plus fiable et bien travaillé.

Vérifiez votre propre «calculatrice» - téléchargez PVS-Studio et essayez-le pour votre projet. :-)

Source: https://habr.com/ru/post/fr443656/


All Articles