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 65db09d..915aaec 100755 Binary files a/labfy-investigation and b/labfy-investigation differ 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 0000000..6d1d2d4 Binary files /dev/null and b/tests/test_tool_catalog differ diff --git a/tests/test_tool_catalog.c b/tests/test_tool_catalog.c new file mode 100644 index 0000000..67eccb7 --- /dev/null +++ b/tests/test_tool_catalog.c @@ -0,0 +1,2017 @@ +/****************************************************************************** + * @file test_tool_catalog.c + * @brief Tests unitaires du catalogue des outils externes. + ******************************************************************************/ + +#include "core/tool_catalog.h" + +#include +#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; +}