rendez-vous sur ArraKISS
Archives ContactATOM
whoami@ybad.name
# find . -iname

    Écrire un générateur de site en C

    Dans cette série de billets, je souhaite présenter comment je me suis remis au C et quels choix j’ai dû faire pour composer staticook, le générateur de site que j’utilise désormais. Ce sera le point de vue d’un apprenti, pas d’un expert. ;)

    Un peu d’histoire

    La génération de sites statiques, c’est un peu mon dada. J’ai dû en utiliser et en bricoler déjà 3 avant staticook. Il y a eu une version en shell qui convertissait du txt2tags en HTML, une autre en python avec du markdown, puis une autre en shell. Sur le principe, rien de très nouveau donc. Le défi principal était donc de le faire dans un autre langage.

    Le fonctionnement du générateur

    Le générateur doit traiter une hiérarchie de site web. Il s’agit bêtement d’un dossier contenant tout un site, sauf qu’au lieu d’avoir les pages déjà écrites en HTML, elles les sont en markdown.

    Il faut pouvoir mettre chaque page dans un modèle afin qu’elles aient toutes le même entête, le même pied de page, la même apparence. Par conséquent, il faut prévoir ces modèles de façon à pouvoir facilement en modifier quelques éléments : le titre de la page, un menu de navigation, un système de tags…

    Ça serait bien de pouvoir configurer facilement l’outil.

    On veut aussi avoir un flux de nouveautés : ça sera un flux ATOM.

    Une page de recherche ça serait pas mal pour s’organiser : on l’appellera page d’archives.

    Un génère aussi un fichier sitemap pour les moteurs de recherche.

    On s’assurera de ne pas travailler pour rien : il faudra traiter seulement les nouveaux fichiers ou ceux modifiés entre deux générations.

    Rester aussi sûr que possible : on utilisera donc les outils disponibles sur OpenBSD pour ça. Ainsi, ça permettra de mettre en place une ferme de sites.

    En somme, on a besoin de :

    Voilà le menu ^^ On va l’étudier dans l’ordre d’apparition, mais pas dans l’ordre de difficulté.

    Let’s go!

    C’est donc parti pour écrire du code, essayer, changer d’avis, réessayer, lire Stack Overflow, lire les pages man, réessayer, puis tout rechanger après avoir eu une nouvelle idée. Ça va être fun! ^^

    Notez que j’utilise souvent des fonctions commençant par “e” : “ereallocarray, estrdup, estrlcat”… C’est l’équivalent de la même fonction sans le “e” initial, mais avec gestion d’erreur. Si vous voyez que j’utilise une fonction que vous ne connaissez pas, jetez un oeil au fichier utils.c pour vous faire un avis.

    Liste récursive du site

    En bon gros débutant, j’utilise mes 10 doigts pour chercher sur le web des explications. Ce faisant, on peut lire des choses à propos de fts_. Afin de bien comprendre, j’ouvre le manuel de fts_open.

    Hum, ça semble pas mal, ça permet de récupérer dans un tableau tout le contenu d’un dossier de façon récursive. Sauf que, euh, je suis débutant, c’est un peu raide de m’en servir sans exemple. Il y a heureusement rosettacode qui propose.

    En l’évoquant sur mastodon, on me fait remarquer qu’il existe aussi scandir. Cette dernière fonction semble hyper intéressante puisqu’elle retourne un tableau qu’il sera facile de parcourir ensuite. Elle est plus simple à comprendre que fts_open, et j’aime comprendre le code que j’utilise. Elle s’utilisera ainsi :

    struct dirent **namelist;
    int n;
    
    if (n = scandir(".", &namelist, NULL, alphasort);
    if (n < 0)
        err(1, "scandir");
    for (int i=0; i<=n; i++) {
    	printf("%s\n", namelist[n]->d_name);
    	free(namelist[n]);
    }
    free(namelist);
    

    Il faut bien penser à appeler free sur le contenu du tableau et sur le tableau lui-même.

    Pas mal du tout en effet. Toutefois, on peut faire mieux car :

    C’est donc parti pour tout écrire à la main. Je regarde rapidement ce qu’ils font chez suckless (et un peu le code de scandir.

    on va suivre la démarche suivante:

    Voici la fonction qui va se charger de lister le contenu d’un dossier et de prendre les décisions qui vont bien:

    int
    cook_dir(const char *path, time_t lastmod, ChainList *pl, ChainList *config, char *ignorelist)
    

    C’est un peu long comme déclaration. Il s’agit après tout de la fonction la plus importante du programme. Voici les paramètres avec quelques explications :

    On commence par déclarer les variables dont on aura besoin:

    struct stat sb;    /* pour avoir le temps de modification */
    struct dirent *dp; /* le contenu d'un répertoire */
    DIR *dirp;         /* le descripteur du dossier */
    
    /* chemins complet pour accéder au fichier rencontré
     * et l'équivalent avec l'extension .html
     */
    char srcp[PATH_MAX] = {0}, htmlp[PATH_MAX] = {0};
    /* La liste des liens pour le menu */
    char *navlinks = NULL
    /* un pointeur qui servira à repérer l'extension */
    char *dot = NULL;
    /* listes de fichiers markdown et de dossiers */
    char **mdlist = NULL, **dirlist = NULL; 
    /* nombre de fichiers markdown et de dossiers */
    int nmd = 0, ndir = 0; 
    /* nombre de pages créées.
    int nc = 0;
    

    Ça fait beaucoup de monde ^^

    On peut maintenant lire le dossier. Tout d’abord, on l’ouvre:

    if ((dirp = opendir(path)) == NULL)
    	err(1, "opendir");
    

    Ensuite, on démarre la boucle qui va traiter le contenu du dossier, élément par élément :

    while ((dp = readdir(dirp)) != NULL) {
    

    Si c’est le dossier actuel et parent, on s’en fiche:

    if ((strcmp(dp->d_name, ".") == 0) || (strcmp(dp->d_name, "..") == 0)) 
    	continue; /* skip self and parent */
    

    On va enregistrer dans la variable srcp le chemin complet pour accéder au fichier actuel. En effet, dp->d_name ne contient que le nom du fichier, pas son chemin d’accès.

    On utilise pour cela la fonction vestrlcat qui correspond à la fonction strlcat avec une liste d’arguments variables. Elle permet de mettre dans srcp tout ce qu’on indique ensuite jusqu’au NULL, sans jamais dépasser la taille de srcp et en étant sûr que srcp se termine toujours par un \0. Vous pouvez la regarder dans utils.c.

    vestrlcat(srcp, sizeof(srcp), path, "/", dp->d_name, NULL);
    

    On vérifie si on doit ignorer ce fichier. On utilise une fonction toignore. On lui passe en argument srcp+1 car on ne veut pas du “.” au début du chemin d’accès : l’utilisateur a configuré la liste ignorelist avec des chemins relatifs à la racine de son site. Autrement dit, on veut “/bla/blabla.md” et pas “./bla/blabla.md”

    if (toignore(srcp+1, ignorelist)) {
    	srcp[0] = '\0';
    	continue;
    }
    

    Commencent maintenant les choses intéressantes. On va regarder si le document actuel est un dossier. Si oui, alors on ajoute son nom dans le tableau dirlist après en avoir augmenté la taille.

    if (dp->d_type == DT_DIR) {
    	ndir++;
    	dirlist = ereallocarray(dirlist, ndir, sizeof(char*));
    	dirlist[ndir-1] = estrdup(dp->d_name);
    

    ereallocarray, c’est juste reallocarray avec gestion d’erreurs. On en reste là, on traitera le contenu de ce tableau à la fin, en passant chaque dossier à notre fonction cook_dir.

    L’autre cas possible, c’est d’avoir un fichier:

    	} else {
    

    hé hé hé :)

    Deux sortes de fichiers nous intéressent:

    Pour les repérer, on va se fier à leur extension. Bien sûr, un utilisateur peut avoir enregistré du markdown dans un fichier “.html”. Le seul risque encouru est qu’il obtienne une page moche. Il n’y a de toute façon pas de raisons de faire ça.

    Seulement voilà, il existe aussi des fichiers sans extensions. C’est là que la variable dot va nous permettre de faire la différence:

    dot = strrchr(dp->d_name, '.');
    if (dot != NULL) {
    

    Si on a trouvé un point, alors on peut continuer en regardant la nature de l’extension. La fonction strrchr permet de chercher à partir de la fin du nom de fichier s’il y a un “.” et nous retourne l’emplacement de ce point dans la chaîne de caractère. On va donc pouvoir comparer le contenu de dot qui contient la fin du nom de fichier à partir du “.”.

    Si on a une page html, on va l’ajouter à la liste des pages du site pl. On regardera plus tard comment est écrite dict_add_val.

    if (strcmp(dot, ".html") == 0) {
    	dict_add_val(pl, "path", srcp);
    

    Sinon, on a peut-être une page markdown:

    } else if (strcmp(dot, ext) == 0) {
    

    Dans ce cas, on doit obtenir le chemin vers le page html correspondante. On remplace donc l’extension. Pour ça, on doit faire une copie de srcp car on en aura besoin plus tard.

    char *srcpcpy = estrdup(srcp);
    

    Ensuite, on va terminer la chaîne de caractères au niveau du point final :

    dot = strrchr(srcpcpy, '.');
    *dot = '\0'; /* remove extension */
    

    Reste à mettre à la place l’extension “.html”, puis de libérer la mémoire de la copie réalisée :

    vestrlcat(htmlp, sizeof(htmlp), srcpcpy, ".html", NULL);
    free(srcpcpy);
    

    On va ajouter cette page à la liste des fichiers markdown. On traitera cette liste plus tard seulement si besoin

    nmd++;
    mdlist = ereallocarray(mdlist, nmd, sizeof(char*));
    mdlist[nmd-1] = estrdup(dp->d_name);
    

    On vérifie si le fichier markdown a été créé ou modifiée depuis la dernière génération du site. Il faudra alors traîter tous les fichiers markdown pour rafraîchir le menu de chacun.

    if (stat(srcp, &sb) != 0)
    	err(1,"stat");
    if (sb.st_mtime > lastmod) {
    	save_recent(htmlp);
    	nc++; 
    

    On ne fait qu’enregisrer la page dans la liste des fichiers récents. (save_recent). Ça ne sera utile que pour la génération du flux ATOM plus tard. Ce qui est important, c’est surtout qu’on enregistre qu’il y a une nouvelle page : ça permettra de savoir ensuite qu’il faut traîter les pages markdown. C’est justement ce qu’on va faire :

    for (int i=0; i < nmd; i++) {
    	if (nc > 0) {
    	...
    		render_page(html, htmlp, navlinks, config);
    
    	...
    	free(mdlist[i]);
    	...
    }
    free(mdlist);
    

    On en profite pour libérer la mémoire du tableau au fur et à mesure.

    On fait de même pour les répertoires :

    for (int i=0; i < ndir; i++) {
    	vestrlcat(srcp, sizeof(srcp), path, "/", dirlist[i], NULL);
    	nc += cook_dir(srcp, lastmod, pl, config, ignorelist);
    	free(dirlist[i]);
    	srcp[0] = '\0'; /* "empty" srcp
    }
    free(dirlist);
    

    Et voilà! :)

    Convertir du markdown

    Pour commencer, j’ai voulu laisser le choix à chacun la meilleure façon de convertir des fichiers markdown. J’ai donc défini dans la configuration la possibilité d’indiquer la commande voulue pour ça : on pouvait appeler discount, smu, ou bien le Markdown.pl d’origine. À chacun de voir.

    Pour récupérer le retour de ces commandes, il fallait utiliser popen(). Cette fonction a l’avantage de retourner un descripteur de fichier, un peu comme si on avait ouvert un fichier à lire:

    char *
    get_cmd_output(const char *cmd)
    {
    	FILE *fp = NULL;
        char *res = NULL;
    
        if ((fp = popen(cmd, "r")) == NULL)
            err(1, "%s: %s", Error opening pipe for cmd", cmd);
    
        res = get_stream_txt(fp);
        if (pclose(fp) != 0)
            err(1, "%s %s", cmd, "not found or exited with error status");
    	
    	return res;
    }
    

    En gros, popen exécute la commande, puis on lit le descripteur retourné comme si c’était un fichier avec la fonction suivante:

    char *
    get_stream_txt(FILE *fp)
    {
    	char *buf = NULL;
    	size_t s;
    	size_t len = 0;
    	size_t bsize = 2 * BUFSIZ;
    
    	buf = ecalloc(bsize, sizeof(char));
    	while((s = fread(buf + len, 1, BUFSIZ, fp))) {
    		len += s;
    		if(BUFSIZ + len + 1 > bsize) {
    			bsize += BUFSIZ;
    			buf = ereallocarray(buf, bsize, sizeof(char));
    		}
    	}
    	buf[len] = '\0';
    
    	return(buf);
    }
    

    Cette dernière enregistre dans un buffer buf le texte retourné et augmente sa taille si besoin.

    if(BUFSIZ + len + 1 > bsize) {
    	bsize += BUFSIZ;
    	buf = ereallocarray(buf, bsize, sizeof(char));
    }
    

    À la fin, on s’assure que la chaîne de caractère obtenue est correctement terminée :

    buf[len] = '\0';
    

    Cela dit, ça ne me plaisait pas. Je voulais pouvoir utiliser pledge dans staticook. Avec popen, je devais autoriser les promesses “proc” et “exec”, et donc staticook à éxécuter n’importe quelle commande. Quitte à vouloir faire sécurisé, autant ne rien faire.

    Il fallait donc que staticook se débrouille tout seul pour convertir du markdown.

    Il fallait donc utiliser une bibliothèque.

    J’ai espéré pouvoir utiliser smu pour ça, mais cela revenait à en réécrire la majeure partie.

    J’ai donc commencé à me pencher sur lowdown, mais sans être convaincu : la version disponible sur mon système n’était pas la dernière, j’avais des soucis au niveau du rendu qui ne correspondait pas à ce que j’attendais. Cela dit, le développeur a été de très bon conseil, merci à lui !

    Finalement, je me suis tourné vers le standard cmark très facile d’utilisation:

    char *
    gen_html(const char *path)
    {
    	char *in = NULL, *out = NULL;
    	in = get_file_txt(path);
    	out = cmark_markdown_to_html(in, strlen(in), 
    		CMARK_OPT_DEFAULT |
    		CMARK_OPT_SMART |
    		CMARK_OPT_UNSAFE); /* markdown let write html */
    
    	free(in);
    	return out;
    }
    

    Cette fonction charge dans la variable in le contenu du fichier situé à l’emplacement path. Ça signifie que si le fichier est trop gros, le programme va s’arrêter en affichant une erreur. Cependant, si le contenu d’un article ne peut pas tenir en mémoire, alors le système utilisé est vraiment limité… Je suis sûr qu’on a tous déjà copié/collé une très grande quantité de texte sans voir notre machine planter.

    Ensuite, on appelle la fonction cmark_markdown_to_html qui s’utilise très facilement. Il suffit de lui passer la chaîne de caractère à convertir in, lui indiquer sa taille strlen(in) puis quelques options. J’ai choisi d’intégrer l’option UNSAFE car initialement, le markdown permettait d’écrire du html de façon transparente dans le markdown. C’est une option que je devrais peut-être ajouter pour laisser l’utilisateur décider.

    Système de template

    Cette partie est celle par laquelle j’ai commencé en écrivant staticook, et c’est celle dont je suis le plus content. Un modèle (template) devait contenir des variables dont le contenu pourrait être modifié à ma guise selon les besoins : titre d’une page, liste de tags, menu de navigation…

    J’ai regardé le code de saait qui fait des choses semblables. Ce dernier utilise une notation avec des pointeurs qui fonctionne bien, mais avec laquelle je ne suis pas à l’aise.

    J’ai donc pris le parti d’écrire un sytème de template à ma façon.

    Tout d’abord, il fallait être en mesure de repérer une variable qui serait à remplacer. J’ai choisi d’encadrer une variable par un caractère peu utilisé, le %. Ça aurait pu être un “$” ou je ne sais quoi d’autre. C’est d’ailleurs configurable dans le fichier config.h, c’est la variable tpldelim.

    Ce caractère entoure une variable : ça permet de détecter quand elle commence et quand elle termine.

    Tout d’abord, j’ai imaginé la chose suivante: lire le modèle caractère par caractère et recopier tant que je ne rencontrais pas de délimiteur.

    Lorsqu’un délimiteur est rencontré, j’enregistre jusqu’à tomber sur un autre délimiteur marquant la fin. À ce moment, on remplace par l’équivalent de la variable, ou bien on recopie ce qu’on a enregistré s’il n’y a pas d’équivalences. Si on ne rencontre pas de délimiteur de fin, on recopie aussi.

    Ça marchait bien, mais ça m’a semblé peu efficace.

    J’ai donc recommencé la même chose, mais en traitant le modèle ligne par ligne avec getline. Ça fonctionnait bien, mais il y avait de la mémoire automatiquement allouée qu’il ne fallait pas oublier de libérer. Ce n’est pas un problème ceci dit, mais j’ai trouvé qu’il y avait plus efficace.

    Après tout, il était possible qu’un fichier ne contienne pas du tout de variables à remplacer.

    Il m’a paru plus pertinent de découper le fichier ni caractère par caractère, ni ligne par ligne, mais par bloc de texte d’un délimiteur “%” à un autre “%”. La fonction [strsep](http://man.openbsd.org/strsep) était toute trouvée. Il s’agit de strtok améliorée qui permet d’éviter les écueils d’un double délimiteur ( “bla%%bla” poserait problème).

    On va déjà faire une copie de notre texte:

    char *work = estrdup(str);
    

    On commence par faire une boucle tant qu’on trouve des bouts de textes avec le délimiteur. On enregistre dans found ce qu’il y a avant le délimiteur, work contient ce qu’il y a après:

    while((found = strsep(&work, tpldelim)) != NULL) {
    

    On augmente d’1 le nombre de délimiteurs rencontrés:

    n++;
    

    Ensuite, on se demande si le nombre de délimiteurs rencontrés est pair ou impair:

    if (n % 2) {
    

    Si c’est impair, alors on peut copier le texte sans demander son reste. Sinon, il faut vérifier s’il y a équivalence avec une variable et le cas échéant en recopier la valeur. S’il n’y a pas d’équivalence, on recopie le texte tel qu’il était.

    } else {
    	/* on regarde s'il y a une équivalence */
    	varval = dict_get_key(tvl, found);
    	if (varval != NULL) {
    		/* write variable equivalent */
    		efprintf(fp, "%s", varval);
    	} else {
    		/* no equivalence: write with tpldelim */
    		if (work == NULL)
    			efprintf(fp, "%s%s", tpldelim, found);
    		else
    			efprintf(fp, "%s%s%s", tpldelim, found, tpldelim);
    	}
    

    On fait appel à une fonction dict_get_key qui permet de savoir si dans la liste des variables d’un template il y a une équivalence. Dit plus simplement, si dans une structure on a:

    a["AUTHOR"] = "batman;
    a["ADRESSE"] = "5 rue du slip";
    a["AGE"] = "55";
    a["SEXE"] = "oui";
    

    Alors appeler dict_get_key(a, "AUTHOR") retourne “batman”.

    Me voilà donc obligé de vous parler des listes chainées dont j’abuse ici et par la suite pour la configuration.

    Les listes chaînées

    Ce concept n’est pas très original en C, mais je le trouve tellement pratique! En C, on dispose de plusieurs types, par exemple des entiers (int), des caractères (char) ou des tableaux de caractères se terminant par un “\0” qu’on appelle “string”. Avec ça, on peut tout faire. Ce qui nous manquerait, on l’obtient en créant de nouvelles structures.

    Une liste chaînée fait un peu penser à un tableau dont on ignore la taille. Chaque objet dans le tableau sait où se trouve le suivant. Ça permet de le parcourir de proche en proche.

    Une liste chaînée ChainList est déclarée ainsi :

    typedef struct ChainList {
    	void *first;
    } ChainList;
    

    Tout ce qu’elle contient, c’est l’adresse vers le premier objet de cette liste. C’est tout. Puisque cet objet connaîtra l’adresse de l’objet suivant, on n’a pas besoin de plus. Libre à chacun de créer le type de structure qu’il souhaite. Pour ma part, j’ai voulu me servir d’une sorte de dictionnaire pour associer une clé (key) à une valeur (value):

    typedef struct Dict{
    	char *key;
    	char *value;
    	struct Dict *next;
    } Dict;
    

    Il faut juste indiquer un pointeur vers le prochain objet qui est du même type que celui-ci : un Dict.

    De cette façon on peut jouer avec les listes très facilement.

    Déjà, voyons comment en créer une :

    ChainList *
    list_create(void)
    {
    	ChainList *nl = malloc(sizeof(ChainList));
    
    	if (nl == NULL)
    		err(1, "malloc ChainList");
    	nl->first = NULL;
    
    	return nl;
    }
    

    Bien, rien de très original, on ne fait qu’allouer de la mémoire pour la structure souhaitée et retourner le pointeur correspondant afin de s’en servir ensuite.

    Avant d’aller plus loin, voyons aussi comment libérer la mémoire du contenu d’une liste. Le contenu sera des structures Dict, il faut donc libérer les key et les value après avoir récupéré l’adresse de l’élément suivant:

    void
    list_dict_free(ChainList *l)
    {
        /* adresse du premier objet de la liste */
    	Dict *itm = l->first;
    	Dict *nextitm;
    
    
    	while (itm != NULL) {
    		/* adresse de l'objet suivant */
    		nextitm = itm->next;
    		/* on libère la mémoire */
    		free(itm->key);
    		free(itm->value);
    		free(itm);
    		/* on passe au suivant */
    		itm = nextitm;
    	}
    	free(l);
    }
    

    Ici, je présente comment obtenir la valeur correspondant à une clé. On ne fait que suivre les éléments de la liste les uns après les autres en partant du premier (->first). Dès qu’une clé correspond à celle demandée (strcmp), on en retourne la valeur. Dans le pire des cas, il faudra parcourir tout le tableau. Pour staticook, ça n’a pas d’importance car il y a une dizaine de tests à faire, ça va très vite. S’il y en avait davantage, il faudrait considérer l’utilisation d’une table de hashage plutôt.

    char *
    dict_get_key(ChainList *l, const char *key)
    {
    	Dict *d = l->first;
    
    
    	while (d != NULL) {
    		if ((strcmp(d->key, key)) == 0)
    			return d->value;
    		d = d->next;
    	}
    
    	return NULL;
    }
    

    Pour finir, je présente comment ajouter une nouvelle paire clé-valeur.

    void
    dict_add_val(ChainList *l, const char *key, const char *value)
    {
    	/* add new Dict in l */
    	Dict *nv = malloc(sizeof(Dict));
    
    	if (nv == NULL)
    		err(1, "malloc nv");
    
    	if ((key != NULL) && (value != NULL)) {
    		nv->next = l->first;
    		l->first = nv;
    		nv->key = estrdup(key);
    		nv->value = estrdup(value);
    	} else {
    		warnx("dict_add_val:invalid key or value");
    		warn("key:%s, value:%s", key, value);
    	}
    }
    

    Configuration

    Aaah, la configuration. J’ai voulu au départ faire comme la plupart des projets suckless, c’est à dire configurer en recompilant avec le fichier config.h. Ça marchait très bien, et c’était même très pratique : toutes les variables étaient globales donc accessibles de partout. Toutefois, l’intérêt devenait limité si on voulait gérer plusieurs sites voire une ferme de sites. Cela supposait compiler un binaire de staticook pour chaque site.

    J’ai donc souhaité rendre staticook plus “user friendly” comme on dit.

    Après avoir cherché un peu, je vois qu’OpenBSD utilise des fichiers avec l’extension “.y” pour ça : je ne connais pas. J’ai trouvé par la suite une bibliothèque Config-Parser-C qui faisait ça. Mais ça me semblait trop pour juste ce petit projet. J’ai donc décidé d’utiliser ce que j’avais déjà : charger la configuration dans un Dict (voir le paragraphe précédent) puisque ce n’est qu’une simple association de mot-clé avec une valeur donnée. Bien sûr, quelques éléments de configuration restent disponbiles dans le config.h, mais ce sont des points qui ne devraient pas trop varier et reste très spécifiques à un utilisateur, autrement dit un admin qui gère une ferme de sites.

    C’est parti, on déclare notre fonction qui prend en argument le chemin vers le fichier de configuration:

    ChainList *
    load_config(const char *conf_path)
    

    ensuite, on déclare quelques variables:

    /* Le contenu du fichier de config */
    char *conftxt = NULL;
    /* une ligne du fichier */
    char *line = NULL;
    /* les paires clé/valeur */
    char *key = NULL, *value = NULL;
    /* la configuration est une liste chaînée de Dict*/
    ChainList *config = list_create();
    

    On récupère le contenu du fichier de configuration:

    conftxt = get_file_txt(conf_path);
    

    Et ensuite, va faire une boucle ligne après ligne. Ici, j’utilise encore strsep, mais il existe auss getline ou getdelim pour ça, mais ces dernières s’appliquent sur un fichier directement.

    while ((line = strsep(&conftxt, "\n")) != NULL) {
    

    Premier cas de figure, on a une ligne vide (donc on s’en fiche):

    if (!(strlen(line) > 0))
    	continue;
    

    Ou alors, on a une ligne commentée:

    if (line[0] == '#')
    	continue;
    

    Sinon, on essaye de récupérer la clé, c’est ce qui est avant un “=”:

    key = strsep(&line, "=");
    

    La valeur sera ce qui reste après le “=”, qui se trouve maintenant dans line. On enregistre donc ces paires dans le dictionnaire:

    key = trim(key);
    value = trim(line);
    dict_add_val(config, key, value);
    

    Et on libère la mémoire avant de retourner la configuration ;)

    free(conftxt);
    return config;
    

    Vous avez peut-être remarqué que j’appelle une fonction trim qui me sert à retirer les espaces en début et en fin de chaîne de caractère. On trouve des exemples sur StackOverflow, mais pour une fois j’ai écrit cette fonction tout seul comme un grand, et j’en suis assez content car ça demande moins de lignes de code que ce que j’ai vu en ligne:

    char *
    trim(const char *str)
    {
    	int begin = 0;
    	int end = strlen(str);
    	char *out = NULL;
    
    
    	while (isspace(str[begin]))
    		begin++;
    
    	while (isspace(str[end-1]))
    		end--;
    	/* +1 for ending \0 */
    	out = ecalloc(end - begin + 1, sizeof(char));
    	strlcpy(out, str + begin, end - begin +1 );
    
    	return out;
    }
    

    On retourne une copie de la chaine de caractère passée en argument sans les espaces de début et de fin.

    Tout d’abord, on compte le nombre d’espaces au début :

    while (isspace(str[begin]))
    	begin++;
    

    Pareil pour la fin de la chaîne:

    while (isspace(str[end-1]))
    	end--;
    

    Et enfin, on copie juste le bout de la chaîne qui nous intéresse, c’est à dire après avoir passé les espaces du début (str + begin) et jusqu’à la fin moins les derniers espaces (end - begin + 1). Le “+1” sert à copier un “\0” final, c’est strlcpy qui assure cette partie.

    out = ecalloc(end - begin + 1, sizeof(char));
    strlcpy(out, str + begin, end - begin +1 );
    

    Un flux ATOM

    Générer un flux ATOM et vérifier sa validité, c’est juste écrire du texte dans un fichier. Le problème qui se posait ici, c’était de garder une trace des pages récentes, entre deux générations du site. On peut penser à une base de données, avec du sqlite par exemple, qui permet même de garder en mémoire le titre des pages, leur date de création…

    J’ai choisi une solution plus minimaliste : enregistrer dans un fichier texte le chemin des pages. Tant pis, il faudra retrouver le titre et la date de création à chaque fois.

    Pour enregistrer une page, j’utilise cette fonction (on l’a déjà rencontrée):

    void
    save_recent(const char *path)
    {
    	puts(path);
    	FILE *rlfp = NULL;
    
    
    	rlfp = efopen(recentlist, "a");
    
    	efprintf(rlfp, "%s\n", path);
    	efclose(rlfp);
    }
    

    On ne fait qu’ajouter une ligne au fichier recentlist, définit dans le config.h.

    Cependant, si on laisse les choses ainsi, on risque d’avoir un fichier qui va grossir indéfiniment. De plus, seuls les dernières pages nous intéressent, c’est à dire la fin du fichier.

    C’est ce que j’ai fait au début : lire le fichier à partir de la fin, caractère après caractère, enregistrer le tout dans un tableau et arrêter au bout d’un certain nombre de lignes repérables par “\n”, puis inverser l’ordre du tableau pour récupérer le contenu à l’endroit. Ça marchait. C’était compliqué.

    À la place, avant de générer le flux atom et de lire les lignes les plus récentes, on fait du ménage : on appelle la fonction clean_recent:

    void
    clean_recent(void)
    {
    	FILE *rlfp = NULL;
    	int l = 0, toskip = 0, skipped = 0;
    	unsigned long idx = 0;
    	char *allrecent = NULL;
    
    
    	rlfp = efopen(recentlist, "r");
    	l = count_lines(rlfp);
    	efclose(rlfp);
    
    	if (l > maxitem) {
    		toskip = l - maxitem;
    
    		allrecent = get_file_txt(recentlist);
    		while (skipped < toskip) {
    			if (allrecent[idx] == '\n') {
    				skipped++;
    			}
    			idx++;
    		}
    
    		rlfp = efopen(recentlist, "w");
    		while (idx <= strlen(allrecent)) {
    			fputc(allrecent[idx], rlfp);
    			idx++;
    		}
    
    		efclose(rlfp);
    		free(allrecent);
    	}
    }
    

    On commence par ouvrir le fichier et en compter le nombre de lignes avec la fonction suivante qui lit le fichier caractère par caractère et compte le nombre de “\n” rencontrés

    int
    count_lines(FILE *fp)
    {
    	unsigned int l = 0;
    	char ch = 0;
    	rewind(fp);
    	/* on lit un caractère tant qu'on n'est pas à la fin */
    	while ((ch = fgetc(fp)) != EOF) {
    		if(ch == '\n')
    			l++; /* une nouvelle ligne! */
    	}
    	rewind(fp); /* on rembobine */
    	return l;
    }
    

    Ensuite, si le nombre de lignes est trop grand, on calcule le nombre de lignes dont on peut se débarrasser :

    toskip = l - maxitem;
    

    On va donc recopier notre liste seulement après avoir passé les premières lignes :

    while (skipped < toskip) {
    	if (allrecent[idx] == '\n') {
    		skipped++;
    	}
    	idx++;
    }
    

    On peut maintenant écraser notre liste en recopiant la fin de la liste:

    rlfp = efopen(recentlist, "w");
    while (idx <= strlen(allrecent)) {
    	fputc(allrecent[idx], rlfp);
    	idx++;
    }
    

    Maintenant, quand on va lire la liste des pages récentes, il n’en reste que le nombre souhaité.

    Liste des pages du site

    Que ce soit pour générer le sitemap.xml ou créer la page d’archives, il faut avoir une liste des pages existantes. Au départ, j’ai voulu faire un tableau. Il fallait en augmenter la taille au besoin. Jusque-là ça va. Finalement, il était nécessaire de le parcourir, or, on ignore au départ quelle sera sa taille. Certes, ça se calcule, mais j’ai préféré utiliser une structure dont je me sers pour les modèles : une liste chaînée. La liste chaînée déjà définie contient des dictionnaires (clé -> valeur). Au lieu de redéfinir une nouvelle structure, c’est cette dernière que j’utilise, seul le champ “valeur” me sert.

    unveil, pledge

    C’est la partie la plus facile, mais qui pourtant me parait une des plus importante si quelqu’un souhaite un jour utiliser staticook pour proposer un service d’hébergement : il faut que ça soit sûr.

    Tout d’abord, on s’assure de n’avoir accès et ne pouvoir modifier (“rwc”) que le dossier que l’on souhaite traiter avec unveil :

    if (unveil(argv[1], "rwc") == -1)
    	err(1, "unveil");
    

    Ensuite, on fait des promesses avec pledge pour s’assurer ne faire que du flux de texte (stdio, en gros), et pouvoir libre/créer/écrire dans des fichiers (rpath cpath wpath):

    if (pledge("stdio rpath cpath wpath", NULL) == -1)
    	err(1, "pledge");
    

    Petite anecdote de la fin

    J’ai tout développé pendant le confinement de 2020, sur mon serveur (un apu2) dans une session tmux car je voulais pouvoir reprendre mon travail malgré les interruptions répétées de la vie et de mon petit droïde :)

    vi aura suffit à rédiger ce code et ces pages.