From f68c4f88d10e32832e9965f761b1409aa1b21a8d Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Fri, 17 Jul 2026 22:51:22 +0200 Subject: [PATCH] feat: add external tool catalog and version detection --- Makefile | 15 +- docs/tickets/{open => closed}/TICKET-038.md | 0 docs/tickets/closed/TICKET-039.md | 515 +++++ include/core/tool_catalog.h | 257 +++ labfy-investigation | Bin 289536 -> 310912 bytes src/core/tool_catalog.c | 872 ++++++++ tests/test_tool_catalog | Bin 0 -> 66224 bytes tests/test_tool_catalog.c | 2017 +++++++++++++++++++ 8 files changed, 3674 insertions(+), 2 deletions(-) rename docs/tickets/{open => closed}/TICKET-038.md (100%) create mode 100644 docs/tickets/closed/TICKET-039.md create mode 100644 include/core/tool_catalog.h create mode 100644 src/core/tool_catalog.c create mode 100755 tests/test_tool_catalog create mode 100644 tests/test_tool_catalog.c diff --git a/Makefile b/Makefile index 88c5ded..79e42a4 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ TEST_TASK_MANAGER := tests/test_task_manager TEST_TOOL_REGISTRY := tests/test_tool_registry TEST_TOOL_PROCESS := tests/test_tool_process TEST_TOOL_TASK := tests/test_tool_task +TEST_TOOL_CATALOG := tests/test_tool_catalog all: $(TARGET) @@ -171,6 +172,13 @@ $(TEST_TOOL_TASK): \ src/core/background_task.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_TOOL_CATALOG): \ + tests/test_tool_catalog.c \ + src/core/tool_catalog.c \ + src/core/tool_registry.c \ + src/core/tool_process.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -187,7 +195,8 @@ test: \ $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ $(TEST_TOOL_PROCESS) \ - $(TEST_TOOL_TASK) + $(TEST_TOOL_TASK) \ + $(TEST_TOOL_CATALOG) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -205,6 +214,7 @@ test: \ @$(TEST_TOOL_REGISTRY) @$(TEST_TOOL_PROCESS) @$(TEST_TOOL_TASK) + @$(TEST_TOOL_CATALOG) @echo "Tous les tests sont valides." %.o: %.c @@ -230,6 +240,7 @@ clean: $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ $(TEST_TOOL_PROCESS) \ - $(TEST_TOOL_TASK) + $(TEST_TOOL_TASK) \ + $(TEST_TOOL_CATALOG) .PHONY: clean run test diff --git a/docs/tickets/open/TICKET-038.md b/docs/tickets/closed/TICKET-038.md similarity index 100% rename from docs/tickets/open/TICKET-038.md rename to docs/tickets/closed/TICKET-038.md diff --git a/docs/tickets/closed/TICKET-039.md b/docs/tickets/closed/TICKET-039.md new file mode 100644 index 0000000..602599c --- /dev/null +++ b/docs/tickets/closed/TICKET-039.md @@ -0,0 +1,515 @@ +# TICKET-039 — Catalogue initial des outils externes et détection de leurs versions + +## Statut + +À faire + +## Priorité + +Haute + +## Objectif + +Créer le catalogue initial des outils externes utilisés par Labfy Investigation et fournir une détection fiable de leur version. + +Ce ticket complète les modules déjà validés : + +```text +ToolRegistry +ToolProcess +ToolTask +``` + +Le catalogue décrit les outils connus par l’application. Le registre décrit leur état sur la machine courante. + +```text +ToolCatalog + description statique des outils connus + ↓ +ToolRegistry + disponibilité, chemin résolu, version détectée + ↓ +ToolProcess + exécution sécurisée de la commande de version +``` + +## Contexte + +`ToolRegistry` permet déjà d’enregistrer un outil, de rechercher son exécutable dans le `PATH`, de conserver son chemin résolu et sa version détectée, et de distinguer les états `UNKNOWN`, `AVAILABLE` et `MISSING`. + +Il manque une source centrale définissant : + +- les outils reconnus par l’application ; +- leur identifiant interne ; +- leur nom affiché ; +- leur exécutable ; +- leur importance ; +- les arguments permettant de demander leur version ; +- la manière de normaliser la sortie obtenue. + +Sans catalogue central, chaque fonctionnalité finirait par enregistrer elle-même ses dépendances. + +## Modules attendus + +```text +include/core/tool_catalog.h +src/core/tool_catalog.c +tests/test_tool_catalog.c +``` + +Le ticket ne doit pas modifier l’API publique de `ToolRegistry`, sauf nécessité démontrée pendant l’implémentation. + +## Responsabilités de ToolCatalog + +Le module doit : + +1. conserver une liste statique et immuable des outils connus ; +2. garantir l’unicité de leurs identifiants ; +3. exposer leurs informations descriptives ; +4. enregistrer l’ensemble du catalogue dans un `ToolRegistry` ; +5. définir les arguments de détection de version ; +6. exécuter ces arguments avec `ToolProcess` ; +7. normaliser la sortie de version ; +8. retourner la version détectée sans modifier directement le registre ; +9. gérer l’annulation ; +10. rester indépendant de GTK. + +Le module ne doit pas installer de dépendance, modifier le `PATH`, utiliser un shell, analyser un résultat OSINT ni modifier l’interface graphique. + +## Catalogue initial + +| Identifiant interne | Nom affiché | Exécutable | Importance | Arguments de version | +|---|---|---|---|---| +| `dns.dig` | `dig` | `dig` | optionnel | `-v` | +| `dns.host` | `host` | `host` | optionnel | `-V` | +| `network.whois` | `whois` | `whois` | optionnel | `--version` | +| `http.curl` | `curl` | `curl` | optionnel | `--version` | +| `tls.openssl` | `OpenSSL` | `openssl` | optionnel | `version` | + +Tous ces outils sont optionnels au niveau de l’application entière. + +Ne pas ajouter encore `nslookup`, `traceroute`, `jq`, `file`, `exiftool`, `nmap`, `subfinder`, `amass`, les outils Python ou les outils installés depuis GitHub. + +## Structure ToolCatalogEntry + +Créer une structure opaque : + +```c +typedef struct ToolCatalogEntry ToolCatalogEntry; +``` + +Elle conserve au minimum : + +```text +identifier +display_name +executable_name +requirement +version_arguments +``` + +Les données du catalogue sont statiques et immuables. Elles ne doivent jamais être libérées par l’appelant. + +## Domaine d’erreur + +```c +#define TOOL_CATALOG_ERROR \ + tool_catalog_error_quark() +``` + +```c +typedef enum +{ + TOOL_CATALOG_ERROR_INVALID_ARGUMENT, + TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND, + TOOL_CATALOG_ERROR_REGISTRATION, + TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED, + TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED, + TOOL_CATALOG_ERROR_TOOL_MISSING, + TOOL_CATALOG_ERROR_INVALID_TOOL_STATE, + TOOL_CATALOG_ERROR_PROCESS, + TOOL_CATALOG_ERROR_VERSION_COMMAND, + TOOL_CATALOG_ERROR_VERSION_OUTPUT, + TOOL_CATALOG_ERROR_CANCELLED +} ToolCatalogError; +``` + +```c +GQuark tool_catalog_error_quark(void); +``` + +## API publique proposée + +### Consultation + +```c +gsize tool_catalog_get_count(void); +``` + +```c +const ToolCatalogEntry *tool_catalog_get_entry( + gsize index +); +``` + +```c +const ToolCatalogEntry *tool_catalog_find( + const char *identifier +); +``` + +### Accesseurs + +```c +const char *tool_catalog_entry_get_identifier( + const ToolCatalogEntry *entry +); +``` + +```c +const char *tool_catalog_entry_get_display_name( + const ToolCatalogEntry *entry +); +``` + +```c +const char *tool_catalog_entry_get_executable_name( + const ToolCatalogEntry *entry +); +``` + +```c +ToolRequirement tool_catalog_entry_get_requirement( + const ToolCatalogEntry *entry +); +``` + +```c +gsize tool_catalog_entry_get_version_argument_count( + const ToolCatalogEntry *entry +); +``` + +```c +const char *tool_catalog_entry_get_version_argument( + const ToolCatalogEntry *entry, + gsize index +); +``` + +### Enregistrement + +```c +gboolean tool_catalog_register_defaults( + ToolRegistry *tool_registry, + GError **error +); +``` + +### Détection d’une version + +```c +gboolean tool_catalog_detect_version( + const ToolRegistry *tool_registry, + const char *identifier, + GCancellable *cancellable, + char **out_version, + GError **error +); +``` + +`out_version` reçoit une chaîne nouvellement allouée, à libérer avec `g_free()`. + +La fonction ne doit pas appeler `tool_registry_set_version()`. + +## Règles d’enregistrement + +`tool_catalog_register_defaults()` doit vérifier avant toute insertion qu’aucun identifiant du catalogue n’existe déjà dans le registre. + +En cas de doublon : + +- aucune entrée ne doit être ajoutée ; +- la fonction retourne `FALSE` ; +- l’erreur vaut `TOOL_CATALOG_ERROR_REGISTRATION`. + +Cette prévalidation évite une insertion partielle, car `ToolRegistry` ne fournit pas de suppression. + +## Validation de la détection + +La fonction doit refuser : + +- un registre `NULL` ; +- un identifiant `NULL` ou vide ; +- un `out_version` égal à `NULL` ; +- un `out_version` pointant déjà vers une chaîne ; +- un `GError` déjà initialisé. + +Au début d’un appel valide : + +```c +*out_version = NULL; +``` + +Cas d’erreur : + +```text +entrée absente du catalogue → TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND +outil absent du registre → TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED +état UNKNOWN → TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED +état MISSING → TOOL_CATALOG_ERROR_TOOL_MISSING +AVAILABLE sans chemin résolu → TOOL_CATALOG_ERROR_INVALID_TOOL_STATE +``` + +## Exécution de la commande de version + +Utiliser exclusivement : + +```c +tool_process_run() +``` + +avec : + +```text +executable_path = chemin résolu du ToolRegistry +arguments = arguments statiques du ToolCatalogEntry +working_directory = NULL +cancellable = paramètre reçu +``` + +Interdictions : + +- `/bin/sh -c` ; +- `system()` ; +- `popen()` ; +- concaténation d’une ligne de commande ; +- redirections interprétées ; +- ajout ou transformation des arguments. + +## Annulation + +Si `ToolProcess` retourne `TOOL_PROCESS_ERROR_CANCELLED`, la fonction doit produire : + +```text +G_IO_ERROR +G_IO_ERROR_CANCELLED +``` + +Aucune version partielle ne doit être retournée. + +## Code de sortie + +Pour une commande de version, un code de sortie non nul est un échec : + +```text +TOOL_CATALOG_ERROR_VERSION_COMMAND +``` + +Une terminaison par signal produit la même catégorie d’erreur. + +La sortie ne doit pas être interprétée lorsque le processus n’a pas terminé normalement avec le code zéro. + +## Sélection de la sortie + +Règle générique : + +1. prendre la première ligne non vide de stdout ; +2. si stdout ne contient rien d’exploitable, essayer stderr ; +3. si aucun flux ne contient de texte exploitable, échouer. + +Cette règle évite une logique spéciale par outil pendant ce premier ticket. + +## Normalisation + +Le catalogue ne doit pas extraire seulement un numéro sémantique. + +La version conservée est la première ligne descriptive exploitable, par exemple : + +```text +DiG 9.20.8 +curl 8.14.1 (x86_64-pc-linux-gnu) +OpenSSL 3.5.1 1 Jul 2025 +``` + +Algorithme : + +1. obtenir les octets du flux ; +2. inspecter au maximum 4096 octets par flux ; +3. convertir les octets invalides avec `g_utf8_make_valid()` ; +4. séparer les lignes ; +5. ignorer les lignes vides ; +6. choisir la première ligne non vide ; +7. retirer espaces et tabulations en début et fin ; +8. retirer `\r` et `\n` ; +9. refuser un résultat vide ; +10. retourner une nouvelle chaîne. + +## Propriété des données + +Les entrées et chaînes du catalogue sont empruntées et statiques. + +La chaîne placée dans `*out_version` appartient à l’appelant et doit être libérée avec `g_free()`. + +`ToolCatalog` ne devient jamais propriétaire du registre. + +## Contraintes de thread + +Le catalogue statique est immuable. + +`tool_catalog_detect_version()` : + +- ne modifie pas le catalogue ; +- ne modifie pas le registre ; +- copie le chemin nécessaire avant l’exécution ; +- ne conserve aucun pointeur vers `ToolInfo` après la préparation. + +Le stockage avec `tool_registry_set_version()` reste la responsabilité du propriétaire du registre, idéalement dans le thread principal. + +## Tests unitaires obligatoires + +Les tests utilisent uniquement de faux exécutables temporaires. + +Ils ne doivent pas dépendre des outils réellement installés. + +### Tests du catalogue + +1. nombre exact d’entrées ; +2. contenu exact des cinq entrées ; +3. index invalide ; +4. recherche par identifiant ; +5. identifiant inconnu ; +6. unicité des identifiants ; +7. arguments de version ; +8. enregistrement complet dans un registre vide ; +9. état initial `UNKNOWN` ; +10. prévalidation d’un doublon sans insertion partielle. + +### Tests de détection + +11. arguments invalides ; +12. entrée inconnue ; +13. outil non enregistré ; +14. outil non vérifié ; +15. outil absent ; +16. version sur stdout ; +17. version sur stderr ; +18. première ligne non vide ; +19. normalisation des espaces et fins de ligne ; +20. sortie non UTF-8 ; +21. sortie vide ; +22. code de sortie non nul ; +23. terminaison par signal ; +24. annulation ; +25. indépendance du registre ; +26. plusieurs détections successives. + +## Noms de tests suggérés + +```text +/tool_catalog/entries +/tool_catalog/invalid_index +/tool_catalog/find +/tool_catalog/unique_entries +/tool_catalog/register_defaults +/tool_catalog/register_duplicate +/tool_catalog/detect_invalid_arguments +/tool_catalog/detect_unknown_entry +/tool_catalog/detect_unregistered_tool +/tool_catalog/detect_unchecked_tool +/tool_catalog/detect_missing_tool +/tool_catalog/version_stdout +/tool_catalog/version_stderr +/tool_catalog/version_first_nonempty_line +/tool_catalog/version_normalization +/tool_catalog/version_non_utf8 +/tool_catalog/version_empty +/tool_catalog/version_nonzero_exit +/tool_catalog/version_signaled +/tool_catalog/version_cancelled +/tool_catalog/version_registry_independence +/tool_catalog/version_successive_runs +``` + +## Makefile + +```make +TEST_TOOL_CATALOG := tests/test_tool_catalog +``` + +```make +$(TEST_TOOL_CATALOG): \ + tests/test_tool_catalog.c \ + src/core/tool_catalog.c \ + src/core/tool_registry.c \ + src/core/tool_process.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +``` + +Ajouter le binaire à `make test` et `make clean`. + +## Vérifications + +```bash +make clean +make +make tests/test_tool_catalog +./tests/test_tool_catalog +make test +git diff --check +``` + +## Vérification mémoire + +```bash +G_DEBUG=gc-friendly \ +G_SLICE=always-malloc \ +valgrind \ + --leak-check=full \ + --show-leak-kinds=all \ + ./tests/test_tool_catalog +``` + +## Critères d’acceptation + +Le ticket est validé lorsque : + +- le catalogue contient exactement les cinq outils prévus ; +- toutes les entrées sont immuables ; +- tous les identifiants sont uniques ; +- le catalogue s’enregistre dans un registre vide ; +- un doublon empêche toute insertion partielle ; +- la détection utilise `ToolProcess` ; +- aucun shell n’est utilisé ; +- stdout et stderr sont pris en charge ; +- la première ligne non vide est normalisée ; +- une sortie invalide devient un UTF-8 valide ; +- l’inspection est limitée à 4096 octets par flux ; +- un code non nul ou un signal est refusé ; +- l’annulation produit `G_IO_ERROR_CANCELLED` ; +- la fonction ne modifie pas directement le registre ; +- les versions successives sont indépendantes ; +- les tests ne dépendent d’aucun outil installé ; +- tous les tests passent ; +- aucune fuite mémoire n’est détectée ; +- le test est intégré au `Makefile`. + +## Démonstration finale + +Après validation : + +1. créer un `ToolRegistry` ; +2. appeler `tool_catalog_register_defaults()` ; +3. appeler `tool_registry_refresh()` ; +4. afficher les outils présents et absents ; +5. détecter la version d’un outil disponible ; +6. stocker explicitement la version avec `tool_registry_set_version()`. + +Cette démonstration restera hors de l’interface GTK principale. + +## Suite prévue + +```text +#040 — Initialisation des dépendances et analyse asynchrone au démarrage +``` + +Ce ticket devra créer le registre global de l’application, enregistrer le catalogue, rafraîchir les disponibilités hors du thread GTK, détecter les versions et publier une tâche dans `TaskManager`. diff --git a/include/core/tool_catalog.h b/include/core/tool_catalog.h new file mode 100644 index 0000000..47e4a02 --- /dev/null +++ b/include/core/tool_catalog.h @@ -0,0 +1,257 @@ +/****************************************************************************** + * @file tool_catalog.h + * @brief Catalogue statique des outils externes connus. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_TOOL_CATALOG_H +#define LABFY_INVESTIGATION_TOOL_CATALOG_H + +#include "core/tool_registry.h" + +#include +#include + +G_BEGIN_DECLS + +/** + * @brief Erreurs produites par ToolCatalog. + */ +typedef enum +{ + /** + * Un argument transmis au module est invalide. + */ + TOOL_CATALOG_ERROR_INVALID_ARGUMENT, + + /** + * L'identifiant demandé n'existe pas dans le catalogue. + */ + TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND, + + /** + * Le catalogue n'a pas pu être enregistré dans le registre. + */ + TOOL_CATALOG_ERROR_REGISTRATION, + + /** + * L'outil existe dans le catalogue, mais pas dans le registre. + */ + TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED, + + /** + * La disponibilité de l'outil n'a pas encore été vérifiée. + */ + TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED, + + /** + * L'outil a été vérifié, mais son exécutable est absent. + */ + TOOL_CATALOG_ERROR_TOOL_MISSING, + + /** + * L'outil est disponible, mais son état interne est incohérent. + */ + TOOL_CATALOG_ERROR_INVALID_TOOL_STATE, + + /** + * ToolProcess n'a pas pu exécuter ou interroger l'outil. + */ + TOOL_CATALOG_ERROR_PROCESS, + + /** + * La commande de version s'est terminée avec un code non nul + * ou à la suite d'un signal. + */ + TOOL_CATALOG_ERROR_VERSION_COMMAND, + + /** + * La commande n'a produit aucune version exploitable. + */ + TOOL_CATALOG_ERROR_VERSION_OUTPUT +} ToolCatalogError; + +/** + * @brief Domaine d'erreur du catalogue d'outils. + */ +#define TOOL_CATALOG_ERROR \ + tool_catalog_error_quark() + +/** + * @brief Entrée opaque et immuable du catalogue. + * + * Les entrées sont conservées dans une table statique interne. + * Elles ne doivent jamais être libérées par l'appelant. + */ +typedef struct ToolCatalogEntry ToolCatalogEntry; + +/** + * @brief Retourne le domaine d'erreur du catalogue. + * + * @return Quark GLib du domaine d'erreur. + */ +GQuark tool_catalog_error_quark(void); + +/** + * @brief Retourne le nombre d'outils connus du catalogue. + * + * @return Nombre d'entrées statiques. + */ +gsize tool_catalog_get_count(void); + +/** + * @brief Retourne une entrée à partir de son index. + * + * Le pointeur retourné est emprunté et statique. + * + * @param index Index de l'entrée. + * + * @return Entrée correspondante, ou NULL si l'index est invalide. + */ +const ToolCatalogEntry *tool_catalog_get_entry( + gsize index +); + +/** + * @brief Recherche une entrée par son identifiant interne. + * + * Le pointeur retourné est emprunté et statique. + * + * @param identifier Identifiant recherché. + * + * @return Entrée correspondante, ou NULL si elle n'existe pas. + */ +const ToolCatalogEntry *tool_catalog_find( + const char *identifier +); + +/** + * @brief Retourne l'identifiant interne d'une entrée. + * + * La chaîne retournée est empruntée et statique. + * + * @param entry Entrée consultée. + * + * @return Identifiant de l'outil, ou NULL. + */ +const char *tool_catalog_entry_get_identifier( + const ToolCatalogEntry *entry +); + +/** + * @brief Retourne le nom affiché d'une entrée. + * + * La chaîne retournée est empruntée et statique. + * + * @param entry Entrée consultée. + * + * @return Nom affiché, ou NULL. + */ +const char *tool_catalog_entry_get_display_name( + const ToolCatalogEntry *entry +); + +/** + * @brief Retourne le nom de l'exécutable recherché dans le PATH. + * + * La chaîne retournée est empruntée et statique. + * + * @param entry Entrée consultée. + * + * @return Nom de l'exécutable, ou NULL. + */ +const char *tool_catalog_entry_get_executable_name( + const ToolCatalogEntry *entry +); + +/** + * @brief Retourne l'importance de la dépendance. + * + * @param entry Entrée consultée. + * + * @return Importance de l'outil. + */ +ToolRequirement tool_catalog_entry_get_requirement( + const ToolCatalogEntry *entry +); + +/** + * @brief Retourne le nombre d'arguments de la commande de version. + * + * Le chemin de l'exécutable n'est pas compté. + * + * @param entry Entrée consultée. + * + * @return Nombre d'arguments. + */ +gsize tool_catalog_entry_get_version_argument_count( + const ToolCatalogEntry *entry +); + +/** + * @brief Retourne un argument de la commande de version. + * + * La chaîne retournée est empruntée et statique. + * + * @param entry Entrée consultée. + * @param index Index de l'argument. + * + * @return Argument correspondant, ou NULL si l'index est invalide. + */ +const char *tool_catalog_entry_get_version_argument( + const ToolCatalogEntry *entry, + gsize index +); + +/** + * @brief Enregistre toutes les entrées du catalogue dans un registre. + * + * Avant toute insertion, la fonction vérifie qu'aucun identifiant du + * catalogue n'existe déjà dans le registre. + * + * En cas de doublon, aucune entrée n'est ajoutée. + * + * @param tool_registry Registre cible. + * @param error Emplacement facultatif pour l'erreur. + * + * @return TRUE si toutes les entrées ont été enregistrées. + */ +gboolean tool_catalog_register_defaults( + ToolRegistry *tool_registry, + GError **error +); + +/** + * @brief Détecte la version d'un outil disponible. + * + * La fonction : + * + * - vérifie que l'outil existe dans le catalogue et le registre ; + * - vérifie que son exécutable est disponible ; + * - exécute sa commande de version avec ToolProcess ; + * - sélectionne la première ligne non vide de stdout ou stderr ; + * - retourne une chaîne UTF-8 normalisée. + * + * La fonction ne modifie pas la version conservée dans ToolRegistry. + * + * En cas d'annulation, l'erreur retournée appartient au domaine + * G_IO_ERROR avec le code G_IO_ERROR_CANCELLED. + * + * @param tool_registry Registre contenant l'outil détecté. + * @param identifier Identifiant de l'entrée du catalogue. + * @param cancellable Objet d'annulation facultatif. + * @param out_version Emplacement recevant une nouvelle chaîne. + * @param error Emplacement facultatif pour l'erreur. + * + * @return TRUE si une version exploitable a été détectée. + */ +gboolean tool_catalog_detect_version( + const ToolRegistry *tool_registry, + const char *identifier, + GCancellable *cancellable, + char **out_version, + GError **error +); + +G_END_DECLS + +#endif diff --git a/labfy-investigation b/labfy-investigation index 65db09d5dfefacdaf72b4bf88036c2b4202ac929..915aaec332f04f539ae54ca369a0db9d29716dd6 100755 GIT binary patch delta 92842 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 delta 79770 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 diff --git a/src/core/tool_catalog.c b/src/core/tool_catalog.c new file mode 100644 index 0000000..021780c --- /dev/null +++ b/src/core/tool_catalog.c @@ -0,0 +1,872 @@ +/****************************************************************************** + * @file tool_catalog.c + * @brief Catalogue statique des outils externes connus. + ******************************************************************************/ + +#include "core/tool_catalog.h" +#include "core/tool_process.h" + +#define TOOL_CATALOG_VERSION_OUTPUT_LIMIT 4096 + +/** + * @struct ToolCatalogEntry + * @brief Description statique d'un outil externe connu. + */ +struct ToolCatalogEntry +{ + const char *identifier; + const char *display_name; + const char *executable_name; + + ToolRequirement requirement; + + const char *const *version_arguments; + gsize version_argument_count; +}; + +/* + * Arguments de détection des versions. + * + * Chaque tableau est terminé par NULL afin de pouvoir être transmis + * directement à ToolProcess. + */ +static const char *const tool_catalog_dig_version_arguments[] = +{ + "-v", + NULL +}; + +static const char *const tool_catalog_host_version_arguments[] = +{ + "-V", + NULL +}; + +static const char *const tool_catalog_whois_version_arguments[] = +{ + "--version", + NULL +}; + +static const char *const tool_catalog_curl_version_arguments[] = +{ + "--version", + NULL +}; + +static const char *const tool_catalog_openssl_version_arguments[] = +{ + "version", + NULL +}; + +/** + * @brief Catalogue statique initial. + */ +static const ToolCatalogEntry tool_catalog_entries[] = +{ + { + .identifier = "dns.dig", + .display_name = "dig", + .executable_name = "dig", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = + tool_catalog_dig_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "dns.host", + .display_name = "host", + .executable_name = "host", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = + tool_catalog_host_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "network.whois", + .display_name = "whois", + .executable_name = "whois", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = + tool_catalog_whois_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "http.curl", + .display_name = "curl", + .executable_name = "curl", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = + tool_catalog_curl_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "tls.openssl", + .display_name = "OpenSSL", + .executable_name = "openssl", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = + tool_catalog_openssl_version_arguments, + .version_argument_count = 1 + } +}; + +/** + * @brief Vérifie qu'une chaîne est définie et non vide. + */ +static gboolean tool_catalog_string_is_valid( + const char *text +) +{ + return text != NULL && + text[0] != '\0'; +} + +/** + * @brief Encapsule une erreur provenant d'un autre module. + */ +static void tool_catalog_set_wrapped_error( + GError **error, + ToolCatalogError error_code, + const char *context_message, + const GError *cause +) +{ + if (cause == NULL || + cause->message == NULL) + { + g_set_error_literal( + error, + TOOL_CATALOG_ERROR, + error_code, + context_message + ); + + return; + } + + g_set_error( + error, + TOOL_CATALOG_ERROR, + error_code, + "%s : %s", + context_message, + cause->message + ); +} + +/** + * @brief Extrait la première ligne non vide d'une sortie. + * + * Au maximum TOOL_CATALOG_VERSION_OUTPUT_LIMIT octets sont inspectés. + * Les séquences UTF-8 invalides sont remplacées. + * + * @param bytes Sortie brute du processus. + * + * @return Nouvelle chaîne normalisée, ou NULL. + */ +static char *tool_catalog_extract_first_nonempty_line( + GBytes *bytes +) +{ + gconstpointer bytes_data = NULL; + + gsize bytes_size = 0; + gsize inspected_size = 0; + gsize line_index = 0; + + char *utf8_text = NULL; + char **lines = NULL; + char *version = NULL; + char *trimmed_line = NULL; + + if (bytes == NULL) + { + return NULL; + } + + bytes_data = g_bytes_get_data( + bytes, + &bytes_size + ); + + if (bytes_data == NULL || + bytes_size == 0) + { + return NULL; + } + + inspected_size = MIN( + bytes_size, + (gsize) TOOL_CATALOG_VERSION_OUTPUT_LIMIT + ); + + utf8_text = g_utf8_make_valid( + bytes_data, + (gssize) inspected_size + ); + + if (utf8_text == NULL) + { + return NULL; + } + + lines = g_strsplit( + utf8_text, + "\n", + -1 + ); + + if (lines == NULL) + { + g_free( + utf8_text + ); + + return NULL; + } + + for (line_index = 0; + lines[line_index] != NULL; + line_index++) + { + /* + * g_strstrip() retire notamment : + * + * - espaces ; + * - tabulations ; + * - retours chariot ; + * - fins de ligne. + */ + trimmed_line = g_strstrip( + lines[line_index] + ); + + if (trimmed_line[0] == '\0') + { + continue; + } + + version = g_strdup( + trimmed_line + ); + + break; + } + + g_strfreev( + lines + ); + + g_free( + utf8_text + ); + + return version; +} + +GQuark tool_catalog_error_quark(void) +{ + return g_quark_from_static_string( + "labfy-investigation-tool-catalog-error" + ); +} + +gsize tool_catalog_get_count(void) +{ + return G_N_ELEMENTS( + tool_catalog_entries + ); +} + +const ToolCatalogEntry *tool_catalog_get_entry( + gsize index +) +{ + if (index >= tool_catalog_get_count()) + { + return NULL; + } + + return &tool_catalog_entries[index]; +} + +const ToolCatalogEntry *tool_catalog_find( + const char *identifier +) +{ + gsize entry_index = 0; + + if (!tool_catalog_string_is_valid( + identifier + )) + { + return NULL; + } + + for (entry_index = 0; + entry_index < tool_catalog_get_count(); + entry_index++) + { + if (g_strcmp0( + tool_catalog_entries[entry_index].identifier, + identifier + ) == 0) + { + return &tool_catalog_entries[entry_index]; + } + } + + return NULL; +} + +const char *tool_catalog_entry_get_identifier( + const ToolCatalogEntry *entry +) +{ + if (entry == NULL) + { + return NULL; + } + + return entry->identifier; +} + +const char *tool_catalog_entry_get_display_name( + const ToolCatalogEntry *entry +) +{ + if (entry == NULL) + { + return NULL; + } + + return entry->display_name; +} + +const char *tool_catalog_entry_get_executable_name( + const ToolCatalogEntry *entry +) +{ + if (entry == NULL) + { + return NULL; + } + + return entry->executable_name; +} + +ToolRequirement tool_catalog_entry_get_requirement( + const ToolCatalogEntry *entry +) +{ + if (entry == NULL) + { + /* + * Valeur neutre et non bloquante pour un appel invalide. + */ + return TOOL_REQUIREMENT_OPTIONAL; + } + + return entry->requirement; +} + +gsize tool_catalog_entry_get_version_argument_count( + const ToolCatalogEntry *entry +) +{ + if (entry == NULL) + { + return 0; + } + + return entry->version_argument_count; +} + +const char *tool_catalog_entry_get_version_argument( + const ToolCatalogEntry *entry, + gsize index +) +{ + if (entry == NULL || + entry->version_arguments == NULL || + index >= entry->version_argument_count) + { + return NULL; + } + + return entry->version_arguments[index]; +} + +gboolean tool_catalog_register_defaults( + ToolRegistry *tool_registry, + GError **error +) +{ + const ToolCatalogEntry *entry = NULL; + + GError *registration_error = NULL; + + gsize entry_index = 0; + + gboolean registration_success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (tool_registry == NULL) + { + g_set_error_literal( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT, + "Le registre fourni au catalogue est invalide." + ); + + return FALSE; + } + + /* + * Prévalidation complète. + * + * Aucun outil ne doit être ajouté lorsqu'au moins un identifiant + * existe déjà dans le registre. + */ + for (entry_index = 0; + entry_index < tool_catalog_get_count(); + entry_index++) + { + entry = tool_catalog_get_entry( + entry_index + ); + + if (entry == NULL) + { + g_set_error_literal( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_REGISTRATION, + "Le catalogue contient une entrée invalide." + ); + + return FALSE; + } + + if (tool_registry_find( + tool_registry, + entry->identifier + ) != NULL) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_REGISTRATION, + "L'outil '%s' existe déjà dans le registre.", + entry->identifier + ); + + return FALSE; + } + } + + /* + * Aucun doublon n'a été trouvé. Les entrées peuvent maintenant + * être enregistrées. + */ + for (entry_index = 0; + entry_index < tool_catalog_get_count(); + entry_index++) + { + entry = tool_catalog_get_entry( + entry_index + ); + + registration_success = + tool_registry_register( + tool_registry, + entry->identifier, + entry->display_name, + entry->executable_name, + entry->requirement, + ®istration_error + ); + + if (!registration_success) + { + tool_catalog_set_wrapped_error( + error, + TOOL_CATALOG_ERROR_REGISTRATION, + "Une entrée du catalogue n'a pas pu être enregistrée", + registration_error + ); + + g_clear_error( + ®istration_error + ); + + return FALSE; + } + } + + return TRUE; +} + +gboolean tool_catalog_detect_version( + const ToolRegistry *tool_registry, + const char *identifier, + GCancellable *cancellable, + char **out_version, + GError **error +) +{ + const ToolCatalogEntry *catalog_entry = NULL; + const ToolInfo *tool_info = NULL; + + ToolAvailability availability; + + const char *resolved_path = NULL; + + char *executable_path = NULL; + char *detected_version = NULL; + + ToolProcessResult *process_result = NULL; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + GError *process_error = NULL; + + gboolean process_success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (tool_registry == NULL || + !tool_catalog_string_is_valid( + identifier + ) || + out_version == NULL || + *out_version != NULL) + { + g_set_error_literal( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT, + "Les arguments de détection de version sont invalides." + ); + + return FALSE; + } + + *out_version = NULL; + + catalog_entry = tool_catalog_find( + identifier + ); + + if (catalog_entry == NULL) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND, + "L'identifiant '%s' n'existe pas dans le catalogue.", + identifier + ); + + return FALSE; + } + + tool_info = tool_registry_find( + tool_registry, + identifier + ); + + if (tool_info == NULL) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED, + "L'outil '%s' n'est pas enregistré dans le registre.", + identifier + ); + + return FALSE; + } + + availability = tool_info_get_availability( + tool_info + ); + + if (availability == + TOOL_AVAILABILITY_UNKNOWN) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED, + "La disponibilité de l'outil '%s' n'a pas été vérifiée.", + identifier + ); + + return FALSE; + } + + if (availability == + TOOL_AVAILABILITY_MISSING) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_MISSING, + "L'outil '%s' est absent de la machine.", + identifier + ); + + return FALSE; + } + + if (availability != + TOOL_AVAILABILITY_AVAILABLE) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_TOOL_STATE, + "L'outil '%s' possède un état de disponibilité invalide.", + identifier + ); + + return FALSE; + } + + resolved_path = tool_info_get_resolved_path( + tool_info + ); + + if (!tool_catalog_string_is_valid( + resolved_path + )) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_TOOL_STATE, + "L'outil '%s' est disponible mais ne possède aucun chemin.", + identifier + ); + + return FALSE; + } + + /* + * La copie permet de ne plus dépendre du ToolInfo pendant + * l'exécution du processus. + */ + executable_path = g_strdup( + resolved_path + ); + + process_success = tool_process_run( + executable_path, + catalog_entry->version_arguments, + NULL, + cancellable, + &process_result, + &process_error + ); + + g_free( + executable_path + ); + + executable_path = NULL; + + if (!process_success) + { + if (process_error != NULL && + g_error_matches( + process_error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_CANCELLED + )) + { + g_set_error( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "%s", + process_error->message != NULL + ? process_error->message + : "La détection de version a été annulée." + ); + } + else + { + tool_catalog_set_wrapped_error( + error, + TOOL_CATALOG_ERROR_PROCESS, + "La commande de version n'a pas pu être exécutée", + process_error + ); + } + + g_clear_error( + &process_error + ); + + tool_process_result_free( + process_result + ); + + return FALSE; + } + + if (process_result == NULL) + { + g_set_error_literal( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_PROCESS, + "La commande de version n'a produit aucun résultat." + ); + + return FALSE; + } + + /* + * Une commande de version doit se terminer normalement avec + * le code zéro. + */ + if (!tool_process_result_exited_normally( + process_result + )) + { + if (tool_process_result_was_signaled( + process_result + )) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_COMMAND, + "La commande de version de '%s' a été terminée " + "par le signal %d.", + identifier, + tool_process_result_get_termination_signal( + process_result + ) + ); + } + else + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_COMMAND, + "La commande de version de '%s' ne s'est pas " + "terminée normalement.", + identifier + ); + } + + tool_process_result_free( + process_result + ); + + return FALSE; + } + + if (tool_process_result_get_exit_status( + process_result + ) != 0) + { + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_COMMAND, + "La commande de version de '%s' a retourné le code %d.", + identifier, + tool_process_result_get_exit_status( + process_result + ) + ); + + tool_process_result_free( + process_result + ); + + return FALSE; + } + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + detected_version = + tool_catalog_extract_first_nonempty_line( + stdout_bytes + ); + + /* + * Certains outils écrivent leur version sur stderr. + */ + if (detected_version == NULL) + { + stderr_bytes = + tool_process_result_ref_stderr( + process_result + ); + + detected_version = + tool_catalog_extract_first_nonempty_line( + stderr_bytes + ); + } + + g_clear_pointer( + &stdout_bytes, + g_bytes_unref + ); + + g_clear_pointer( + &stderr_bytes, + g_bytes_unref + ); + + tool_process_result_free( + process_result + ); + + process_result = NULL; + + if (detected_version == NULL || + detected_version[0] == '\0') + { + g_clear_pointer( + &detected_version, + g_free + ); + + g_set_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_OUTPUT, + "L'outil '%s' n'a produit aucune version exploitable.", + identifier + ); + + return FALSE; + } + + *out_version = detected_version; + + return TRUE; +} diff --git a/tests/test_tool_catalog b/tests/test_tool_catalog new file mode 100755 index 0000000000000000000000000000000000000000..6d1d2d4f12b1ce5be1903084b5f9543abd87d3f7 GIT binary patch literal 66224 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 P-}Enmak7KJ(KY@blf4 +#include +#include +#include + +/** + * @struct ExpectedToolCatalogEntry + * @brief Valeurs attendues pour une entrée du catalogue. + */ +typedef struct +{ + const char *identifier; + const char *display_name; + const char *executable_name; + const char *version_argument; +} ExpectedToolCatalogEntry; + +/** + * @brief Catalogue attendu par les tests. + */ +static const ExpectedToolCatalogEntry expected_catalog_entries[] = +{ + { + .identifier = "dns.dig", + .display_name = "dig", + .executable_name = "dig", + .version_argument = "-v" + }, + { + .identifier = "dns.host", + .display_name = "host", + .executable_name = "host", + .version_argument = "-V" + }, + { + .identifier = "network.whois", + .display_name = "whois", + .executable_name = "whois", + .version_argument = "--version" + }, + { + .identifier = "http.curl", + .display_name = "curl", + .executable_name = "curl", + .version_argument = "--version" + }, + { + .identifier = "tls.openssl", + .display_name = "OpenSSL", + .executable_name = "openssl", + .version_argument = "version" + } +}; + +static char *test_tool_catalog_directory = NULL; + +typedef struct +{ + GCancellable *cancellable; + guint64 delay_microseconds; +} ToolCatalogCancellationData; + +static gpointer test_tool_catalog_cancel_after_delay( + gpointer user_data +) +{ + ToolCatalogCancellationData *cancellation_data = + user_data; + + if (cancellation_data == NULL || + cancellation_data->cancellable == NULL) + { + return NULL; + } + + g_usleep( + cancellation_data->delay_microseconds + ); + + g_cancellable_cancel( + cancellation_data->cancellable + ); + + return NULL; +} + +static ToolRegistry *test_tool_catalog_create_registry( + gboolean refresh_registry +); + +/** + * @brief Crée le faux exécutable curl utilisé par les tests. + * + * @param script_content Contenu complet du script. + */ +static void test_tool_catalog_write_curl_script( + const char *script_content +) +{ + char *curl_path = NULL; + + GError *error = NULL; + + gboolean write_success = FALSE; + int chmod_result = 0; + + g_assert_nonnull( + test_tool_catalog_directory + ); + + g_assert_nonnull( + script_content + ); + + curl_path = + g_build_filename( + test_tool_catalog_directory, + "curl", + NULL + ); + + write_success = + g_file_set_contents( + curl_path, + script_content, + -1, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + write_success + ); + + chmod_result = + g_chmod( + curl_path, + S_IRUSR | + S_IWUSR | + S_IXUSR + ); + + g_assert_cmpint( + chmod_result, + ==, + 0 + ); + + g_free( + curl_path + ); +} + +static void test_tool_catalog_version_stdout(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 1.2.3\\n'\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + detected_version, + ==, + "Fake curl 1.2.3" + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_stderr(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 2.3.4\\n' >&2\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + detected_version, + ==, + "Fake curl 2.3.4" + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_first_nonempty_line(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf '\\n\\nFake curl 3.4.5\\nautre ligne\\n'\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + detected_version, + ==, + "Fake curl 3.4.5" + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_normalization(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf '\\t Fake curl 4.5.6 \\t\\r\\n'\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + detected_version, + ==, + "Fake curl 4.5.6" + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_non_utf8(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl \\377 5.6.7\\n'\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + detected_version + ); + + g_assert_true( + g_utf8_validate( + detected_version, + -1, + NULL + ) + ); + + g_assert_true( + g_str_has_prefix( + detected_version, + "Fake curl " + ) + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_empty(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "exit 0\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_OUTPUT + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Crée un registre contenant le catalogue par défaut. + * + * @param refresh_registry TRUE pour rechercher les exécutables. + * + * @return Nouveau registre. + */ +static ToolRegistry *test_tool_catalog_create_registry( + gboolean refresh_registry +) +{ + ToolRegistry *tool_registry = NULL; + GError *error = NULL; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_true( + tool_catalog_register_defaults( + tool_registry, + &error + ) + ); + + g_assert_no_error( + error + ); + + if (refresh_registry) + { + g_assert_true( + tool_registry_refresh( + tool_registry, + &error + ) + ); + + g_assert_no_error( + error + ); + } + + return tool_registry; +} + +/** + * @brief Vérifie le contenu exact du catalogue statique. + */ +static void test_tool_catalog_entries(void) +{ + const ToolCatalogEntry *catalog_entry = NULL; + const ExpectedToolCatalogEntry *expected_entry = NULL; + + gsize entry_index = 0; + + g_assert_cmpuint( + tool_catalog_get_count(), + ==, + G_N_ELEMENTS(expected_catalog_entries) + ); + + for (entry_index = 0; + entry_index < G_N_ELEMENTS(expected_catalog_entries); + entry_index++) + { + catalog_entry = tool_catalog_get_entry( + entry_index + ); + + expected_entry = + &expected_catalog_entries[entry_index]; + + g_assert_nonnull( + catalog_entry + ); + + g_assert_cmpstr( + tool_catalog_entry_get_identifier( + catalog_entry + ), + ==, + expected_entry->identifier + ); + + g_assert_cmpstr( + tool_catalog_entry_get_display_name( + catalog_entry + ), + ==, + expected_entry->display_name + ); + + g_assert_cmpstr( + tool_catalog_entry_get_executable_name( + catalog_entry + ), + ==, + expected_entry->executable_name + ); + + g_assert_cmpint( + tool_catalog_entry_get_requirement( + catalog_entry + ), + ==, + TOOL_REQUIREMENT_OPTIONAL + ); + + g_assert_cmpuint( + tool_catalog_entry_get_version_argument_count( + catalog_entry + ), + ==, + 1 + ); + + g_assert_cmpstr( + tool_catalog_entry_get_version_argument( + catalog_entry, + 0 + ), + ==, + expected_entry->version_argument + ); + + g_assert_null( + tool_catalog_entry_get_version_argument( + catalog_entry, + 1 + ) + ); + } +} + +/** + * @brief Vérifie les index et accesseurs invalides. + */ +static void test_tool_catalog_invalid_index(void) +{ + g_assert_null( + tool_catalog_get_entry( + tool_catalog_get_count() + ) + ); + + g_assert_null( + tool_catalog_get_entry( + G_MAXSIZE + ) + ); + + g_assert_null( + tool_catalog_entry_get_identifier( + NULL + ) + ); + + g_assert_null( + tool_catalog_entry_get_display_name( + NULL + ) + ); + + g_assert_null( + tool_catalog_entry_get_executable_name( + NULL + ) + ); + + g_assert_cmpint( + tool_catalog_entry_get_requirement( + NULL + ), + ==, + TOOL_REQUIREMENT_OPTIONAL + ); + + g_assert_cmpuint( + tool_catalog_entry_get_version_argument_count( + NULL + ), + ==, + 0 + ); + + g_assert_null( + tool_catalog_entry_get_version_argument( + NULL, + 0 + ) + ); +} + +/** + * @brief Vérifie la recherche des entrées par identifiant. + */ +static void test_tool_catalog_find(void) +{ + const ToolCatalogEntry *catalog_entry = NULL; + + gsize entry_index = 0; + + for (entry_index = 0; + entry_index < G_N_ELEMENTS(expected_catalog_entries); + entry_index++) + { + catalog_entry = tool_catalog_find( + expected_catalog_entries[entry_index].identifier + ); + + g_assert_nonnull( + catalog_entry + ); + + g_assert_cmpstr( + tool_catalog_entry_get_identifier( + catalog_entry + ), + ==, + expected_catalog_entries[entry_index].identifier + ); + } + + g_assert_null( + tool_catalog_find( + "unknown.tool" + ) + ); + + g_assert_null( + tool_catalog_find( + NULL + ) + ); + + g_assert_null( + tool_catalog_find( + "" + ) + ); +} + +/** + * @brief Vérifie l'unicité des identifiants du catalogue. + */ +static void test_tool_catalog_unique_entries(void) +{ + const ToolCatalogEntry *first_entry = NULL; + const ToolCatalogEntry *second_entry = NULL; + + const char *first_identifier = NULL; + const char *second_identifier = NULL; + + gsize first_index = 0; + gsize second_index = 0; + + for (first_index = 0; + first_index < tool_catalog_get_count(); + first_index++) + { + first_entry = tool_catalog_get_entry( + first_index + ); + + g_assert_nonnull( + first_entry + ); + + first_identifier = + tool_catalog_entry_get_identifier( + first_entry + ); + + g_assert_nonnull( + first_identifier + ); + + g_assert_cmpstr( + first_identifier, + !=, + "" + ); + + for (second_index = first_index + 1; + second_index < tool_catalog_get_count(); + second_index++) + { + second_entry = tool_catalog_get_entry( + second_index + ); + + g_assert_nonnull( + second_entry + ); + + second_identifier = + tool_catalog_entry_get_identifier( + second_entry + ); + + g_assert_cmpstr( + first_identifier, + !=, + second_identifier + ); + } + } +} + +/** + * @brief Vérifie l'enregistrement complet dans un registre vide. + */ +static void test_tool_catalog_register_defaults(void) +{ + ToolRegistry *tool_registry = NULL; + + const ToolInfo *tool_info = NULL; + const ExpectedToolCatalogEntry *expected_entry = NULL; + + GError *error = NULL; + + gsize entry_index = 0; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_true( + tool_catalog_register_defaults( + tool_registry, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpuint( + tool_registry_get_count( + tool_registry + ), + ==, + G_N_ELEMENTS(expected_catalog_entries) + ); + + for (entry_index = 0; + entry_index < G_N_ELEMENTS(expected_catalog_entries); + entry_index++) + { + expected_entry = + &expected_catalog_entries[entry_index]; + + tool_info = tool_registry_get_tool( + tool_registry, + entry_index + ); + + g_assert_nonnull( + tool_info + ); + + g_assert_cmpstr( + tool_info_get_identifier( + tool_info + ), + ==, + expected_entry->identifier + ); + + g_assert_cmpstr( + tool_info_get_display_name( + tool_info + ), + ==, + expected_entry->display_name + ); + + g_assert_cmpstr( + tool_info_get_executable_name( + tool_info + ), + ==, + expected_entry->executable_name + ); + + g_assert_cmpint( + tool_info_get_requirement( + tool_info + ), + ==, + TOOL_REQUIREMENT_OPTIONAL + ); + + g_assert_cmpint( + tool_info_get_availability( + tool_info + ), + ==, + TOOL_AVAILABILITY_UNKNOWN + ); + + g_assert_null( + tool_info_get_resolved_path( + tool_info + ) + ); + + g_assert_null( + tool_info_get_detected_version( + tool_info + ) + ); + } + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie qu'un doublon empêche toute insertion partielle. + */ +static void test_tool_catalog_register_duplicate(void) +{ + ToolRegistry *tool_registry = NULL; + + const ToolInfo *existing_tool = NULL; + + GError *error = NULL; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_true( + tool_registry_register( + tool_registry, + "http.curl", + "Outil existant", + "existing_curl", + TOOL_REQUIREMENT_REQUIRED, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_false( + tool_catalog_register_defaults( + tool_registry, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_REGISTRATION + ); + + g_clear_error( + &error + ); + + /* + * Aucun outil du catalogue ne doit avoir été ajouté. + * Seul l'outil préexistant reste présent. + */ + g_assert_cmpuint( + tool_registry_get_count( + tool_registry + ), + ==, + 1 + ); + + existing_tool = tool_registry_find( + tool_registry, + "http.curl" + ); + + g_assert_nonnull( + existing_tool + ); + + g_assert_cmpstr( + tool_info_get_display_name( + existing_tool + ), + ==, + "Outil existant" + ); + + g_assert_cmpstr( + tool_info_get_executable_name( + existing_tool + ), + ==, + "existing_curl" + ); + + g_assert_null( + tool_registry_find( + tool_registry, + "dns.dig" + ) + ); + + g_assert_null( + tool_registry_find( + tool_registry, + "dns.host" + ) + ); + + g_assert_null( + tool_registry_find( + tool_registry, + "network.whois" + ) + ); + + g_assert_null( + tool_registry_find( + tool_registry, + "tls.openssl" + ) + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie les arguments invalides de l'enregistrement. + */ +static void test_tool_catalog_register_invalid_arguments(void) +{ + GError *error = NULL; + + g_assert_false( + tool_catalog_register_defaults( + NULL, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie les arguments invalides de la détection. + */ +static void test_tool_catalog_detect_invalid_arguments(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + tool_registry = + test_tool_catalog_create_registry( + FALSE + ); + + g_assert_false( + tool_catalog_detect_version( + NULL, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + NULL, + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + NULL, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + detected_version = + g_strdup( + "version déjà présente" + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_clear_pointer( + &detected_version, + g_free + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'une entrée inconnue du catalogue. + */ +static void test_tool_catalog_detect_unknown_entry(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "unknown.tool", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil absent du registre. + */ +static void test_tool_catalog_detect_unregistered_tool(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil non encore recherché. + */ +static void test_tool_catalog_detect_unchecked_tool(void) +{ + ToolRegistry *tool_registry = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + tool_registry = + test_tool_catalog_create_registry( + FALSE + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un exécutable absent. + */ +static void test_tool_catalog_detect_missing_tool(void) +{ + ToolRegistry *tool_registry = NULL; + + const ToolInfo *tool_info = NULL; + + char *detected_version = NULL; + char *fake_curl_path = NULL; + + GError *error = NULL; + + fake_curl_path = + g_build_filename( + test_tool_catalog_directory, + "curl", + NULL + ); + + /* + * Garantit que curl est réellement absent du PATH temporaire. + */ + g_remove( + fake_curl_path + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + tool_info = tool_registry_find( + tool_registry, + "http.curl" + ); + + g_assert_nonnull( + tool_info + ); + + g_assert_cmpint( + tool_info_get_availability( + tool_info + ), + ==, + TOOL_AVAILABILITY_MISSING + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_TOOL_MISSING + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + g_free( + fake_curl_path + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_nonzero_exit(void) +{ + ToolRegistry *tool_registry = NULL; + char *detected_version = NULL; + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 7.0.0\\n'\n" + "exit 7\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_COMMAND + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_signaled(void) +{ + ToolRegistry *tool_registry = NULL; + char *detected_version = NULL; + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "kill -TERM $$\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_error( + error, + TOOL_CATALOG_ERROR, + TOOL_CATALOG_ERROR_VERSION_COMMAND + ); + + g_assert_null( + detected_version + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_cancelled(void) +{ + ToolRegistry *tool_registry = NULL; + + GCancellable *cancellable = NULL; + GThread *cancellation_thread = NULL; + + ToolCatalogCancellationData cancellation_data = {0}; + + char *detected_version = NULL; + + gint64 started_at_us = 0; + gint64 elapsed_us = 0; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "while :\n" + "do\n" + " :\n" + "done\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + cancellable = + g_cancellable_new(); + + cancellation_data.cancellable = + cancellable; + + cancellation_data.delay_microseconds = + 200000; + + cancellation_thread = + g_thread_new( + "tool-catalog-cancellation", + test_tool_catalog_cancel_after_delay, + &cancellation_data + ); + + started_at_us = + g_get_monotonic_time(); + + g_assert_false( + tool_catalog_detect_version( + tool_registry, + "http.curl", + cancellable, + &detected_version, + &error + ) + ); + + elapsed_us = + g_get_monotonic_time() - + started_at_us; + + g_thread_join( + cancellation_thread + ); + + g_assert_error( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED + ); + + g_assert_null( + detected_version + ); + + g_assert_cmpint( + elapsed_us, + <, + 3000000 + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_registry_independence(void) +{ + ToolRegistry *tool_registry = NULL; + + const ToolInfo *tool_info = NULL; + + char *detected_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 8.1.0\\n'\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + tool_info = tool_registry_find( + tool_registry, + "http.curl" + ); + + g_assert_nonnull( + tool_info + ); + + g_assert_null( + tool_info_get_detected_version( + tool_info + ) + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + /* + * La détection seule ne doit pas modifier le registre. + */ + g_assert_null( + tool_info_get_detected_version( + tool_info + ) + ); + + g_assert_true( + tool_registry_set_version( + tool_registry, + "http.curl", + detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + tool_info_get_detected_version( + tool_info + ), + ==, + "Fake curl 8.1.0" + ); + + g_free( + detected_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_successive_runs(void) +{ + ToolRegistry *tool_registry = NULL; + + char *first_version = NULL; + char *second_version = NULL; + + GError *error = NULL; + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 1.0.0\\n'\n" + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &first_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + test_tool_catalog_write_curl_script( + "#!/bin/sh\n" + "printf 'Fake curl 2.0.0\\n'\n" + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &second_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpstr( + first_version, + ==, + "Fake curl 1.0.0" + ); + + g_assert_cmpstr( + second_version, + ==, + "Fake curl 2.0.0" + ); + + g_assert_cmpstr( + first_version, + !=, + second_version + ); + + g_free( + first_version + ); + + g_free( + second_version + ); + + tool_registry_free( + tool_registry + ); +} + +static void test_tool_catalog_version_output_limit(void) +{ + ToolRegistry *tool_registry = NULL; + + char *long_line = NULL; + char *script_content = NULL; + char *detected_version = NULL; + + GError *error = NULL; + + long_line = + g_strnfill( + 5000, + 'A' + ); + + script_content = + g_strdup_printf( + "#!/bin/sh\n" + "printf '%s\\n'\n", + long_line + ); + + test_tool_catalog_write_curl_script( + script_content + ); + + tool_registry = + test_tool_catalog_create_registry( + TRUE + ); + + g_assert_true( + tool_catalog_detect_version( + tool_registry, + "http.curl", + NULL, + &detected_version, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + detected_version + ); + + g_assert_cmpuint( + strlen( + detected_version + ), + ==, + 4096 + ); + + g_assert_cmpint( + detected_version[0], + ==, + 'A' + ); + + g_assert_cmpint( + detected_version[4095], + ==, + 'A' + ); + + g_free( + detected_version + ); + + g_free( + script_content + ); + + g_free( + long_line + ); + + tool_registry_free( + tool_registry + ); +} + +int main( + int argc, + char **argv +) +{ + GError *error = NULL; + int test_result = 0; + + g_test_init( + &argc, + &argv, + NULL + ); + + test_tool_catalog_directory = + g_dir_make_tmp( + "labfy-tool-catalog-XXXXXX", + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + test_tool_catalog_directory + ); + + g_assert_true( + g_setenv( + "PATH", + test_tool_catalog_directory, + TRUE + ) + ); + + g_test_add_func( + "/tool_catalog/entries", + test_tool_catalog_entries + ); + + g_test_add_func( + "/tool_catalog/invalid_index", + test_tool_catalog_invalid_index + ); + + g_test_add_func( + "/tool_catalog/find", + test_tool_catalog_find + ); + + g_test_add_func( + "/tool_catalog/unique_entries", + test_tool_catalog_unique_entries + ); + + g_test_add_func( + "/tool_catalog/register_defaults", + test_tool_catalog_register_defaults + ); + + g_test_add_func( + "/tool_catalog/register_duplicate", + test_tool_catalog_register_duplicate + ); + + g_test_add_func( + "/tool_catalog/register_invalid_arguments", + test_tool_catalog_register_invalid_arguments + ); + + g_test_add_func( + "/tool_catalog/detect_invalid_arguments", + test_tool_catalog_detect_invalid_arguments + ); + + g_test_add_func( + "/tool_catalog/detect_unknown_entry", + test_tool_catalog_detect_unknown_entry + ); + + g_test_add_func( + "/tool_catalog/detect_unregistered_tool", + test_tool_catalog_detect_unregistered_tool + ); + + g_test_add_func( + "/tool_catalog/detect_unchecked_tool", + test_tool_catalog_detect_unchecked_tool + ); + + g_test_add_func( + "/tool_catalog/detect_missing_tool", + test_tool_catalog_detect_missing_tool + ); + + g_test_add_func( + "/tool_catalog/version_stdout", + test_tool_catalog_version_stdout + ); + + g_test_add_func( + "/tool_catalog/version_stderr", + test_tool_catalog_version_stderr + ); + + g_test_add_func( + "/tool_catalog/version_first_nonempty_line", + test_tool_catalog_version_first_nonempty_line + ); + + g_test_add_func( + "/tool_catalog/version_normalization", + test_tool_catalog_version_normalization + ); + + g_test_add_func( + "/tool_catalog/version_non_utf8", + test_tool_catalog_version_non_utf8 + ); + + g_test_add_func( + "/tool_catalog/version_empty", + test_tool_catalog_version_empty + ); + + g_test_add_func( + "/tool_catalog/version_nonzero_exit", + test_tool_catalog_version_nonzero_exit + ); + + g_test_add_func( + "/tool_catalog/version_signaled", + test_tool_catalog_version_signaled + ); + + g_test_add_func( + "/tool_catalog/version_cancelled", + test_tool_catalog_version_cancelled + ); + + g_test_add_func( + "/tool_catalog/version_registry_independence", + test_tool_catalog_version_registry_independence + ); + + g_test_add_func( + "/tool_catalog/version_successive_runs", + test_tool_catalog_version_successive_runs + ); + + g_test_add_func( + "/tool_catalog/version_output_limit", + test_tool_catalog_version_output_limit + ); + + test_result = g_test_run(); + + { + char *curl_path = + g_build_filename( + test_tool_catalog_directory, + "curl", + NULL + ); + + g_remove( + curl_path + ); + + g_free( + curl_path + ); + } + + g_rmdir( + test_tool_catalog_directory + ); + + g_clear_pointer( + &test_tool_catalog_directory, + g_free + ); + + return test_result; +}