Bonjour,
je me suis amusé à implémenter une fonction robuste pour lire un entier (type int). L'automate s'implémente assez facilement en suivant un algo dont le squelette ressemblerait à :
1 2 3 4 5 6 7 8 9 10
| tant qu'on a un signe + ou -
si c'est - alors
on change le signe final du nombre
nombre=0
tant qu'on a un chiffre
nombre = nombre x 10 + chiffre
si on a autre chose que la fin de la chaine alors
renvoyer une erreur |
On peut étoffer tout ça avec une gestion de l'overflow.
Pour coder ça on va avoir besoin d'un code de retour pour dire :
* tout c'est bien passé, la chaine ne contenait qu'un int valide
* on a rencontré un caractère invalide
* tout se passait bien, mais si on continuait on obtenait un entier qui ne tenait pas dans un int : overflow
* on nous a passé NULL comme chaine à traiter ! (bon spécifique à c)
Ce serait pas mal aussi que si on le désire on puisse obtenir l'endroit où on c'est arrêté (la fon de la chaine ou le caractère invalide ou le chiffre qui provoque l'overflow)
Un prototype pourrait être :
pi_res_t parse_int(char* str, int* integer, char** ptr);
str étant la chaine à parser
integer pointant vers la variable user qui va recevoir ce qu'on parse
ptr un pointeur sur un pointeur de char qui s'il n'est pas nul pointera vers le premier caractère non traité
le retour pi_res_t sera un enum pour différencier les différents cas vu plus haut
typedef enum {PI_OK, PI_ERANGE, PI_EOVERFLOW, PI_ENULLARG} pi_res_t;
PI_OK : tout c'est bien passé
PI_ERANGE : on a rencontré un caractère invalide
PI_EOVERFLOW : le traitement du chiffre suivant provoque un overflow
PI_ENULLARG : str vaut NULL !
En gros on l'utiliserait ainsi :
1 2 3 4 5
| char* test="--16384!"
char* ptr_premier_car_non_traite=NULL;
int integer;
pi_res_t res;
res =parse_int(test,&integer, &ptr_premier_char_non_traite); |
Dans ce cas, res contiendrait PI_ERANGE (car le ! n'est pas un caractère valide)
integer vaudrait 16384 (on retourne quand même ce qu'on a pu parser)
et ptr_premier_char_non_traite pointerait vers ce fameux '!' dans test, en particulier ptr_premier_char_non_traite - test donne la position de ce caractère.
Si on n'a pas besoin de cette information on peut passe NULL pour en avertir la fonction :
res=parse_int(test&integer,NULL)
Tout est identique sauf qu'on a pas d'info sur où se trouve ce caractère.
Passons à l'implémentation:
La première partie de notre fonction est super simple : si on nous passe une chaine NULL on s'arrête de suite :
1 2 3 4 5 6
| if (str==NULL) {
if (endchar!=NULL)
*endchar=NULL;
*integer=0;
return PI_ENULLARG;
} |
Classique ... on convient juste que si on nous donne NULL, alors on renvoie l'entier 0, si l'utilisateur désire savoir où ça plante on lui renvoie NULL et la fonction elle même renvoie le code adéquat.
Passons aux choses sérieuse en commençant par un petit détour concernant la gestion de l'overflow. En c, un int 32bits peut représenter les entier de - 2^(31) = -2147483648 jusqu'à 2^(31)-1 = 2147483647. Un int non signé quant à lui peut représenter les entiers de 0 à 4294967295 = (2^32-1).
On va poser MAXINTPOS=2147483648.
Comme nous parsons le signe indépendemment du nombre, nous pouvons utiliser un booléen pour indiquer le signe du nombre parsé, puis construire un nombre positif sachant que le résultat final sera ce nombre fois le signe parsé (+1 ou -1).
Nous allons donc utiliser des entiers non signés et vérifier lors de la construction du nombre que ce nombre est bien représentable comme un int. Nous vérifions d'abord si l'opération elle-même n'a pas provoqué un overflow sur les entiers non signés (1), puis si ce nombre peut être représenté avec un int (2) :
(1) : avec a et b non signés, a=b*10 provoque un overflow si b est différent de a/10 (ok ... je n'en suis pas sûr, c'est à prouver voire à remplacer par quelquechose de plus simple)
(2) : a non signé, a n'est représentable avec un int si a>=MAXINTPOS, respectivement -a ne l'est pas si a>MAXINTPOS
ok, donc dans la suite nous aurons besoin de :
1 2 3 4 5 6
| char* ptr=str;
unsigned int number=0;
unsigned int tmp_number;
unsigned char digit;
int is_neg=0;
int overflow=0; |
ptr sera le pointeur sur le caractère en cours de traitement
tmp_number le résultat intermédiaire nous permettant de vérifier s'il y a eu un overflow ou non
digit la valeur décimale du caractère
is_neg le booléen indiquant si on parse un entier positif ou négatif
overflow le booléen qui indique si on a eu un overflow pendant la conversion
On aura aussi au préalable définit notre MAXINTPOS, par exemple :
#define MAXINTPOS ((unsigned int) (1<<(sizeof(int)*8-1)))
La première tâche est donc de traiter le signe :
1 2 3 4 5
| while ( (*ptr=='-') || (*ptr=='+') ) {
if (*ptr=='-')
is_neg=!is_neg;
++ptr;
} |
Rien de particulièrement difficile : on change le signe si on rencontre un '-'.
Soit on ne rentre pas dans la boucle (il n'y a pas de signe), soit on rentre (car il y a au moins un signe) et quand on en ressort il y autre chose qu'un signe.
On peut donc embrayer sur la construction du nombre, j'ai essayé de bien la détailler. Tant qu'on trouve des chiffres et qu'il n'y a pas eu d'overflow, on lit le chiffre, on construit temporairement le nombre que ça donnerait s'il n'y a pas d'overflow c'est ok sinon on sort immédiatement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| while ( (*ptr>='0') && (*ptr<='9') && (!overflow) ) {
digit=*ptr - '0';
tmp_number=number*10+digit;
if ( ( ((tmp_number-digit)/10)!=number)
||
( ( is_neg) && (tmp_number>MAXINTPOS) )
||
( (!is_neg) && (tmp_number>MAXINTPOS-1) )
){
overflow=1;
} else {
number=tmp_number;
++ptr;
} |
Voilà donc en sortant de cette boucle (si on y est rentré) on est certain d'avoir parsé un nombre qui entre dans un int. Si overflow vaut 1, il nous reste des chiffres dans le buffer mais on ne peut aller plus loin car sinon ça ne tient plus dans un int, sinon soit le caractère est '\0' dans ce cas tout est ok, soit c'est autre chose et on a un caractère invalide.
Si on ne rentre pas dans cette boucle c'est qu'on a pas à faire à un nombre. Le plus simple savoir si on y est rentré est de soit mettre inconditionnellement un booléen à 1 dans le boucle, ou de comparer le pointeur ptr avant et après la boucle pour savoir si on a avancé. Choisissons la seconde solution :
1 2
| char *before_num_parse=ptr;
while ( (*ptr>='0') && (*ptr<='9') && (!overflow) ) { |
Donc après cette boucle, soit before_num_parse==ptr et dans ce cas il n'y a pas de nombre : on renvoie 0 avec une erreur sur caractère invalide,
soit les pointeurs sont différents et on a bien un nombre, dans ce cas on renvoie ce nombre multiplié par le signe transformé en int , s'il y a eu overflow on renvoie l'erreur overflow, si le dernier caractère n'est pas '\0' on renvoie une erreur carcatère invalide et sinon tout est bon.
Mais dans tous les cas de figure, si l'utilisateur veut s'avoir où on s'arrête on peut lui dire.
Ce qui donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (endchar!=NULL)
*endchar=ptr;
if (before_num_parse==ptr) {
*integer = 0;
return PI_ERANGE;
} else {
if (is_neg)
*integer=- (int)number;
else
*integer= (int)number;
if (overflow)
return PI_EOVERFLOW;
else if (*ptr!=0)
return PI_ERANGE;
else return PI_OK;
} |
Bon ... tout y est 
C'est le type d'exercice qui se complique si on cherche à trouver une solution robuste.
Cette fonction n'est pas sensible aux attaques de type buffer overflow, elle prend en compte tous les cas de figure et permet à l'utilisateur d'agir en conséquence en cas d'erreur.
On peut évidemment étendre cette fonction pour qu'elle accepte les caractères d'espacement facilement, pour pouvoir parser des entiers signés plus longs (long et long long, avec peut-être une restriction au niveau du check des overflows). On peut aussi l'étendre avec une fonctionnalité du type try parse en autorisant un pointeur sur int est NULL par exemple.
Je vais poster le code complet dans la section source avec un petit jeu d'essai. Je ne commente pas le code (je sais honte sur moi!) mais j'y ajouterai un lien vers cette discussion.
Je suis ouvert à tout commentaire ou correction 
sources disponibles ici : sources C
Partager