URL: https://linuxfr.org/users/forc3/journaux/flex-mon-ami Title: Flex mon ami ! Authors: forc3 Date: 2005年01月31日T13:59:53+01:00 Tags: Score: 0 ** toutes mes excuses pour le formattage de code foireux ** mais je n'obtiens pas les tabulations avec les balises code ** Donc j'utilise pre mais du coup les inf et sup des include sont pas ** affichés. J'espère que c'est un problème du à la css que j'utilise... ** Les lignes vides sont une des conséquences aussi... De retour avec de nouvelles aventures à propos de flex, c'est outil magnifique qui vous fait gagner du temps, de la sécurité, et de la maintenabilité. Mais passons cette description qui bien sûr ne satisfera pas tout le monde. Mais je m'en cogne. Je viens juste ici vous livrez un peu de mon expérience à avec outil. Un peu de 'background', j'ai toujours trouvé l'écriture de parser très très lourd. Je me suis rendu compte très vite que pour faire quelquechose de maintenable c'était impossible, car personne n'avait le courage de replonger le nez dans ses lignes, même moi l'auteur détestait me rendre compte que je devais y retourner. Un beau jour, au gré de mes balades internet, il y a un moment je tombe sur flex, et plus tard bison. Etant déjà grand utilisateur de perl pour ses expressions rationnelles, j'ai trouvé l'approche très intéressante de flex. Aimant la performance et ne supportant pas savoir que ma chaine de caractère va être dupliquée X fois en mémoire juste pour matcher un simple motif, j'ai vite abandonné perl pour parser quoi que ce soit. L'énorme avantage de flex c'est qu'il ne fonctionne pas par ligne. C'est beaucoup plus simple de trouver des liens web dans une seule ligne qu'en Perl. Et c'est en plus beaucoup plus efficace... Une fois la maîtrise de l'outil, je me disais que c'était bien la classe mais j'aurais vraiment voulu essayer d'étendre ses possibilités autre part que sur des fichiers ... Sur des socket ca serait fort intéressant. C'est à la suite du developpement d'un serveur IMAP dont le parseur est écrit intégralement en flex/bison, que me suis heurté au problème spécifique des sockets et de la programmation réseau. C'est à dire, que tous les gens qui me parlent ne pensent qu'a tenter de faire des overflow sur mes buffer, et qu'ils prennent un malin plaisir à ouvrir des connexions sans rien n'y faire. C'est constation faite, je trouvais insoutenable de me voir ainsi à la merci de n'importe qui. Il me fallait une solution. C'est une mission pour moi ! Je vais donc vous présenter ici ma méthode pour rendre flex, utilisable comme moteur principal d'une application utilisant un protocole bien défini. Au hasard HTTP... Cependant avant de passer au HTTP, je vais le faire sur un exemple très simple. Un protocole utilisant des headers déjà très courant sur internet ;p. Récapitulons ce que nous cherchons à faire: - lire un flux de données, avec un timeout - lire un flux de données en controlant leur taille. Ce qui veut dire que notre serveur ne dois attendre des données que pendant un certains temps, et doit pouvoir vérifier qu'il n'a pas reçu trop de données... Afin de rendre possible ces deux choses nous avons besoin de rendre notre fonction flex capable d'attendre des données seulement pendant une durée définie, et capable de savoir combien de données il me reste à lire. Pour la première chose c'est très simple, il suffit de passer le fd en O_NONBLOCK et d'appeler poll dessus avant de faire un read(). Pour la seconde chose nous allons utilisez ce que le protocole ont prévus pour cela, c'est à dire le Content-Length... Pour bien comprendre ce que nous allons faire, il faut juste noter que nous allons faire avec flex un parseur capable de s'arréter s'il n'a rien lu, et surtout d'extraire du flux lui même la quantité de données à lire. Pour les paranoiaques comme moi, on notera qu'il est très important de fixer une limite à la quantité des données à lire, il est hors de question d'accepter toutes les valeurs... Assez de blah blah, entrons dans le code. Première chose on redéfini la fonction de lecture de flex ! #define YY_INPUT(buf, result, max_size) \ £spaces£ £/spaces£do { \ £spaces£ £/spaces£int ret = 0; \ £spaces£ £/spaces£struct pollfd fds; \ £spaces£ £/spaces£fds.fd = 0; \ £spaces£ £/spaces£fds.events = POLLIN; \ £spaces£ £/spaces£if ( ! poll(&fds, 1, 5000) ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£YY_FATAL_ERROR("input in flex scanner failed timeout reading") \ £spaces£ £/spaces£goto skip_reading; \ £spaces£ £/spaces£} \ £spaces£ £/spaces£if ( fds.revents & ( POLLOUT | POLLERR | POLLHUP ) ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£goto skip_reading; \ £spaces£ £/spaces£} \ £spaces£ £/spaces£if ( (result = read( 0, (char *) buf, max_size )) < 0 ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£YY_FATAL_ERROR( "input in flex scanner failed" ) \ £spaces£ £/spaces£} \ skip_reading: \ £spaces£ £/spaces£} while (0) Ainsi flex va utiliser ce code pour lire des données, les attentifs auront notés que ce code ne contient pas le passage du fd en O_NONBLOCK, enfait c'est fait avant d'appeler yylex(). La différence fondamentale entre le faire et ne pas le faire c'est que le read sera bloquant ou pas, par contre il n'y aura aucune différence sur la fonction poll(). Dans certains cas, il est possible de ne pouvoir lire des données après la sortie d'une fonction de monitoring de fd (poll(), select(), kqueue(), sigio, epoll() etc). Donc dans un cas le read retourner -1 et errno a EAGAIN, alors que dans l'autre il va bloquer jusqu'à obtenir des données. Ce sont vos tests qui décideront de la validité de mettre le fd en O_NONBLOCK, c'est pour cela que ce n'est pas inclu dans la macro. Autre problème, la fonction YY_FATAL_ERROR, sort définitivement du lexer. Moi je ne veux pas qu'un timeout me fasse sortir, donc je redifini la fonction YY_FATAL_ERROR. #define YY_FATAL_ERROR(msg) fatal_error(msg); J'aurais bien sur inseré cette ligne avant la macro YY_INPUT... Le code de ma fonction fatal_error est le suivant static void fatal_error(const char *msg) { £spaces£ £/spaces£printf("fatal error: %s\n", msg); £spaces£ £/spaces£fatal = 1; } Vous aurez compris, je pense, que ma variable fatal est du type extern volatile int, et est utilisée dans la boucle principale du programme appelant la fonction yylex(). C'est un exemple, a vous d'adapter en fonction de vos besoins/idées. Maintenant passons à l'étape, lecture de données bornée. Flex nous mets à disposition la fonction input() qui va appeler notre fonction de lecture si besoin est, et nous retourner le caractère suivant. C'est elle qui va être au coeur de notre gestion de donnée... Et voici comment: T_CONTENT_SIZE ^content-size:\ [0-9]+$ T_END_OF_HEADER \n\n %% {T_CONTENT_SIZE} { £spaces£ £/spaces£bytes = atoi((const char *) &yytext[14]); £spaces£ £/spaces£if ( bytes < 0 ) £spaces£ £/spaces£bytes = 0; £spaces£ £/spaces£printf("+ CS %d\n", bytes); } {T_END_OF_HEADER} { £spaces£ £/spaces£already_read = 0; £spaces£ £/spaces£int ret; £spaces£ £/spaces£buffer = malloc(sizeof(char) * bytes); £spaces£ £/spaces£if ( buffer ) { £spaces£ £/spaces£for ( already_read = 0; already_read < bytes; already_read++ ) { £spaces£ £/spaces£ret = input(); £spaces£ £/spaces£if ( (ret == EOF) || (ret == 0) || fatal ) { £spaces£ £/spaces£printf("error: end of input after reading only %d bytes.\n", already_read); £spaces£ £/spaces£yyterminate(); £spaces£ £/spaces£break; £spaces£ £/spaces£} £spaces£ £/spaces£buffer[already_read] = ret; £spaces£ £/spaces£} £spaces£ £/spaces£} } Alors, nous utilisons content-size, pour le fun, changer le en content-length ca marche autant ... Ensuite nous utilisons le motif T_END_OF_HEADER afin de marquer le passage entre headers et data. C'est très proche du HTTP... C'est voulu. Dans le flux lorsque nous trouvons T_CONTENT_SIZE, nous récupérons la valeur, (il faut checker errno avec atoi pour différencier de 0 et de 0 à cause d'une erreur, ne faites pas comme moi ...) nous la stockons dans bytes. Après à la fin de headers après T_END_OF_HEADER, nous allouons assez de mémoire pour recevoir les données. Une simple boucle permet de remplir le buffer tout justement alloué. Remarquer bien que nous ne lisons pas plus que bytes... Nous n'écrasons rien. (On peut/_doit_ tout a fait prévoir une vérification plus solide de bytes, en la bornant à un certain maximum...) Nous testons tous les valeur de retour de input() afin d'agir en conséquence et de gérer une éventuelle déconnexion, ou erreur de lecture. Libre à vous encore une fois d'appeler yyterminate() ou pas. Voici le code entier du programme compilable de cette manière: - flex -Cr -i test.lex - gcc lex.yy.c -o input_test test.lex { #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <fcntl.h> #include <sys/types.h> #include <poll.h> static unsigned long bytes; static unsigned long already_read; static char *buffer; static int fatal; static void fatal_error(const char *msg); #define YY_FATAL_ERROR(msg) fatal_error(msg); #define YY_INPUT(buf, result, max_size) \ £spaces£ £/spaces£do { \ £spaces£ £/spaces£int ret = 0; \ £spaces£ £/spaces£struct pollfd fds; \ £spaces£ £/spaces£fds.fd = 0; \ £spaces£ £/spaces£fds.events = POLLIN; \ £spaces£ £/spaces£if ( ! poll(&fds, 1, 5000) ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£YY_FATAL_ERROR("input in flex scanner failed timeout reading") \ £spaces£ £/spaces£goto skip_reading; \ £spaces£ £/spaces£} \ £spaces£ £/spaces£if ( fds.revents & ( POLLOUT | POLLERR | POLLHUP ) ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£goto skip_reading; \ £spaces£ £/spaces£} \ £spaces£ £/spaces£if ( (result = read( 0, (char *) buf, max_size )) < 0 ) { \ £spaces£ £/spaces£fatal = 1; \ £spaces£ £/spaces£result = 0; \ £spaces£ £/spaces£YY_FATAL_ERROR( "input in flex scanner failed" ) \ £spaces£ £/spaces£} \ skip_reading: \ £spaces£ £/spaces£} while (0) %} T_CONTENT_TYPE ^content-type:\ [^ \n]+$ T_CONTENT_SIZE ^content-size:\ [0-9]+$ T_END_OF_HEADER \n\n %% {T_CONTENT_TYPE} { £spaces£ £/spaces£printf("+ CT %.*s\n", yyleng - 14, &yytext[14]); } {T_CONTENT_SIZE} { £spaces£ £/spaces£bytes = atoi((const char *) &yytext[14]); £spaces£ £/spaces£if ( bytes < 0 ) £spaces£ £/spaces£bytes = 0; £spaces£ £/spaces£printf("+ CS %d\n", bytes); } {T_END_OF_HEADER} { £spaces£ £/spaces£already_read = 0; £spaces£ £/spaces£int ret; £spaces£ £/spaces£buffer = malloc(sizeof(char) * bytes); £spaces£ £/spaces£if ( buffer ) { £spaces£ £/spaces£for ( already_read = 0; already_read < bytes; already_read++ ) { £spaces£ £/spaces£ret = input(); £spaces£ £/spaces£if ( (ret == EOF) || (ret == 0) || fatal ) { £spaces£ £/spaces£printf("error: end of input after reading only %d bytes.\n", already_read); £spaces£ £/spaces£yyterminate(); £spaces£ £/spaces£break; £spaces£ £/spaces£} £spaces£ £/spaces£buffer[already_read] = ret; £spaces£ £/spaces£} £spaces£ £/spaces£} } %% static void fatal_error(const char *msg) { £spaces£ £/spaces£printf("fatal error: %s\n", msg); £spaces£ £/spaces£fatal = 1; } int main() { £spaces£ £/spaces£fatal = 0; £spaces£ £/spaces£buffer = 0; £spaces£ £/spaces£yyin = stdin; £spaces£ £/spaces£yylex(); £spaces£ £/spaces£printf("found %d bytes of data:\n%.*s\n", already_read, already_read, buffer); £spaces£ £/spaces£if ( buffer ) £spaces£ £/spaces£free(buffer); £spaces£ £/spaces£return 0; } En espérant que ça puisse vous vous inciter à coder vos parsers avec des outils fait pour. Ca nous éviterait d'avoir un nombre incalculable d'overflow dans les moindres petites fonctions d'analyses de flux/caractères... Le libre en aurait bien besoin...