Développement

Découverte de l'éditeur hexadécimal ImHex et de son éditeur de pattern

ImHex est un éditeur hexadécimal assez puissant disposant de nombreuses fonctionnalités très intéressantes. Je vous en avais déjà rapidement parlé dans mon premier article sur DICOM il y a quelques mois maintenant, mais aujourd'hui on va revenir plus en détail sur cet outil et plus précisément sur son éditeur de pattern.

Dans cet article je vais vous présenter les principales fonctionnalités que j'utilise dans ImHex et je vais vous expliquer pas à pas comment écrire vos propres fichiers de pattern afin de prendre en charge des formats binaires qui ne sont pas encore supportés par ImHex. Pour cela, je vous introduirai le format Obsidian Project File qui nous servira d'exemple et pour lequel on écrira un pattern.

Vous êtes prêts ? Alors c'est parti ! 😁️

C'est quoi un éditeur hexadécimal ?

Je vais commencer par vous expliquer rapidement ce qu'est l'hexadécimal et un éditeur hexadécimal car ce n'est pas forcément évident pour tout le monde.

Le système hexadécimal

L'hexadécimal est une façon de représenter un nombre.

Au quotidien, nous les humains, on compte plutôt en décimal (base 10), on utilise pour cela 10 symboles (la plupart du temps les chiffres arabes de 0 à 9) ce qui nous permet d'exprimer 10 nombres différents avec un seul caractère.

Les ordinateurs eux comptent en binaire (base 2) et n'utilisent donc que 2 symboles. Généralement on représente ça avec les chiffres arabes 0 et 1, mais ça peut être un signal électrique à l'état haut ou bas, un creux ou une bosse sur un CD, un trou ou l'absence de trou sur une carte perforée,...

Et donc il existe l'hexadécimal qui permet de compter en base 16 et qui nécessite donc... 16 symboles différents. On représente les nombres hexadécimaux avec les chiffres arabes de 0 à 9 puis on utilise ensuite les lettres latines de A à F.

Si je veux compter de 0 à 31 en hexadécimal ça me donnera donc :

0x0 (0) 0x1 (1) 0x2 (2) 0x3 (3) 0x4 (4) 0x5 (5) 0x6 (6) 0x7 (7)
0x8 (8) 0x9 (9) 0xA (10) 0xB (11) 0xC (12) 0xD (13) 0xE (14) 0xF (15)
0x10 (16) 0x11 (17) 0x12 (18) 0x13 (19) 0x14 (20) 0x15 (21) 0x16 (22) 0x17 (23)
0x18 (24) 0x19 (25) 0x1A (26) 0x1B (27) 0x1C (28) 0x1D (29) 0x1E (30) 0x1F (31)

Note

NOTATION HEXADÉCIMALE : afin de différencier les nombres en écriture décimale (base 10) de ceux en hexadécimal (base 16), on rajoute souvent le préfixe "0x" devant le nombre (par exemple "0x42", "0xAF",...). On peut également retrouver d'autres notations comme l'ajout d'un "$" ou d'un "h" devant ou derrière le nombre hexadécimal. Ne soyez donc pas surpris si vous retrouvez ce genre de notations dans une documentation.

Pour ma part j'ai une préférence pour le préfix "0x", c'est donc cette notation que j'utiliserai tout au long de l'article. De la même manière, j'exprime les nombres binaires en les préfixant par "0b".

Il existe également tout un tas d'autres bases qu'on utilise dans des contextes particuliers, comme par exemple l'octal qui est utilisé pour exprimer les permissions des fichiers Unix (vous savez le fameux "chmod 777" 😅️)... Mais bref ça dépasse le périmètre de cet article ! 😄️

Prenons un exemple pour finir d'illustrer tout ça. Le nombre décimal 42 peut s'exprimer :

  • 0b101010 en binaire,
  • 0x2A en hexadécimal,
  • ou encore 0o52 en octal [promis j'arrête avec l'octal 😆️].

Pourquoi s'embêter à compter en base 16 me direz-vous ?

Eh bah par ce que c'est super pratique en informatique (et en plus ça rime ! 😛️). Aujourd'hui quasiment tous les systèmes informatiques encodent leurs données dans des octets, soit des mots de 8 bits (une série de 8 "0" et "1"). Et il se trouve qu'on peut exprimer toutes les valeurs possibles d'un octet (0-255) avec seulement 2 caractères en hexadécimal (0x00-0xFF) et que ça a en plus l'avantage de bien tomber puisque la valeur maximale exprimable avec 2 caractères en hexadécimal (0xFF) correspond justement à la valeur maximale exprimable sur un octet (0b11111111) contrairement à la base décimale où il faut s'arrêter à 255 et pas 999.

Si vous voulez en apprendre plus sur l'hexadécimal, je vous invite à consulter Wikipédia :

Les éditeurs hexadécimaux

Un éditeur hexadécimal (ou éditeur hexa) est un outil permettant de visualiser et d'éditer des fichiers binaires. Généralement ces éditeurs présentent à minima une vue où les données binaires sont représentées en hexadécimal, accompagné le plus souvent d'une colonne donnant une représentation sous forme de texte de la donnée.

Voici par exemple ce que peut afficher la commande "xxd" qui n'est pas à proprement parler un éditeur, mais qui permet tout de même d'afficher le contenu d'un fichier binaire en hexadécimal dans le terminal :

$ xxd project.wprj

00000000: 5750 524a 0001 4558 414d 504c 4520 2020  WPRJ..EXAMPLE
00000010: 0100 0000 3300 0000 6a01 0000 009d 0000  ....3...j.......
00000020: 006b 0100 0001 0800 0000 5900 0001 6100  .k........Y...a.
00000030: 0000 4835 cb3b 0e83 300c 00d0 bb78 6d8d  ..H5.;..0....xm.
00000040: ecc4 40c8 da2b b4ec 261f 4a55 4105 74aa  ..@..+..&.JUA.t.
00000050: 7a77 b2b0 3ee9 fde0 b32e af14 f63e addb  zw..>........>..
00000060: b4cc e081 2b82 ebc9 8fef 148b 9152 ce24  ....+........R.$
00000070: 0e3b ab82 a261 4097 4dc4 ae6e 2237 39b4  .;...a@.M..n"79.
[...]

| col1  | col2                                    | col3          |

Comme vous pouvez le voir ici on a 3 colonnes que je vous ai annoté col1, col2 et col3.

La première colonne (col1) donne l'adresse (on parle aussi d'offset) du premier octet de la ligne. Ainsi, sur la première ligne le 0x57 est à l'adresse 0x00, soit au début du fichier, sur la seconde ligne; l'octet qui a pour valeur 0x01 se trouve à l'adresse 0x10, et ainsi de suite. Les adresses sont exprimées en hexadécimal.

La seconde colonne (col2) est la plus importante : c'est celle qui nous affiche les données de notre fichier en écriture hexadécimale. Chaque octet du fichier correspond à 2 caractères, ainsi 0x57 est le premier octet du fichier, 0x50 le second, etc.

Et enfin la troisième colonne (col3) donne une représentation ASCII, c'est-à-dire sous forme de texte, de chacun des octets du fichier. Ainsi on peut voir que le 0x57 du début du fichier correspond au caractère "W", 0x50 à "P", etc. Il n'est pas rare de trouver des morceaux de texte lisibles à l'intérieur d'un fichier binaire, cette vue permet donc de les repérer et de les interpréter d'un coup d'œil. Notez que les valeurs qui ne correspondent pas à des caractères affichables sont représentées par un point ".".

Présentation rapide de ImHex

ImHex est donc un éditeur hexadécimal. Mais un éditeur hexadécimal kilékrôbo 🤩️ et qui propose un tas de fonctionnalités super cool. Je ne vais pas pouvoir tout aborder ici, mais je vais vous présenter rapidement celles qui me sont les plus utiles actuellement. Je vais à chaque fois vous pointer la documentation associée pour que vous puissiez trouver plus de détails.

Pour commencer voici une capture d'écran pour que vous puissiez voir à quoi ça ressemble (il s'agit du layout par défaut).

Capture d'écran de l'éditeur hexadécimal ImHex

Capture d'écran de l'éditeur hexadécimal ImHex

Éditeur hexadécimal

En haut à gauche on retrouve bien sûr la partie « Hex editor ». J'ai pas grand-chose de plus à en dire que ce que j'ai déjà expliqué dans la partie précédente. Même si elle est pas mal configurable, elle reste assez « standard » pour ce genre d'outil.

Inspecteur de données

Juste à côté on a le « Data Inspector ». Ça aussi c'est une fonctionnalité plutôt standard dans les éditeurs hexa. Ça s'utilise en pointant un octet dans la vue hexadécimale et ça nous donne plein d'interprétations possibles pour l'octet en question... et les octets suivants quand on considère des données codées sur plusieurs octets.

Ça va par exemple nous indiquer à quoi correspond la valeur si on la considère comme un entier/flottant 8/16/24/32 bits, signé/non-signé, big/little endian,... Ou encore en quel caractère ASCII/Unicode ça pourrait se traduire, et le tout en gérant plusieurs encodages. Et ça va même encore plus loin puisque ImHex peut également nous interpréter la valeur comme une couleur, une heure, une date, un UUID,...

Bref, ça fait le café !

Inspecteur de patterns

En bas à gauche on retrouve la section « Pattern Data ». Quand ImHex sait comment interpréter le fichier qu'on a ouvert, cette section permet de naviguer à l'intérieur dudit fichier, un peu de la même façon qu'on peut naviguer dans un document HTML avec l'inspecteur d'un navigateur Web.

Capture d'écran de l'inspecteur HTML de Firefox

Capture d'écran de l'inspecteur HTML de Firefox

Ici on va pouvoir parcourir les différentes sections, structures et champs imbriqués dans le fichier. Et pour chaque élément on va avoir son nom, où il commence, où il finit, sa taille, son type et son interprétation.

Sur la capture on peut par exemple voir qu'on est dans un fichier de type « ObsidianProjectFile », qui contient une structure « header », dont le premier champ s'appelle « magic » et qu'il s'agit d'une string de 4 caractères qui a pour valeur "WPRJ".

C'est vraiment super puissant, et c'est cette partie qui m'a énormément aidé lors de mon analyse du format DICOM dont je vous avais parlé il y a quelque temps.

Éditeur de patterns

En haut à droite on retrouve le « Pattern editor ». Il s'agit d'un éditeur de texte dans lequel on peut écrire un petit programme dans un langage spécifique servant à décrire la structure du fichier ouvert. C'est grâce à ça que ImHex est en mesure de nous faire naviguer dans les structures de données du fichier via l'inspecteur dont on vient juste de parler.

ImHex est fourni de base avec pas mal de ces programmes et est capable de sélectionner automatiquement le bon lorsque l'on ouvre un fichier. Et on peut bien entendu les modifier ou écrire les nôtres... Ça tombe bien, on va justement reparler de ça dans pas bien longtemps ! 😉️

En bas de l'éditeur de pattern on retrouve divers outils qui y sont liés comme la console ou les sections... On en reparlera le moment venu. 😉️

Traitement de données

Toujours en haut à droite se trouve également l'onglet « Data Processor » que je vous ai recollé tout à droite de la capture. Il s'agit d'un éditeur nodal permettant d'effectuer des traitements sur les données contenues dans le fichier.

Sur la capture d'écran j'ai par exemple confectionné un petit traitement qui va chercher l'emplacement et la taille d'une image se trouvant en pièce jointe d'un projet Obsidian (je vous expliquerais de quoi il s'agit bientôt 😉️) et qui l'affiche grâce à un nœud « Image Visualizer ». Ça nous permet de voir que l'image représente un damier blanc et fuchsia (oui c'est pas très beau, mais ça a l'avantage de pas prendre trop de place 😛️).

Autres outils

ImHex dispose de nombreux autres outils planqués dans les menus (sans même parler des plugins) mais on n'en parlera pas ici, sinon je vais jamais réussir à terminer cet article ! 😅️

Écrire son propre pattern

Si on veut analyser un format de fichier pas (encore ?) pris en charge par ImHex, il est intéressant d'écrire un petit script pour permettre à ImHex de comprendre sa structure afin qu'il mette tout ça en couleur et qu'on puisse naviguer facilement dans le fichier.

Le langage pour écrire ce script est appelé « Pattern Language » dans la doc de ImHex. Il s'agit d'un DSL (Domain Specific Language) dont la syntaxe est inspirée de C++ et de Rust.

Dans la suite de cet article on va écrire un pattern pour les fichiers Obsidian Project, qui est un format non pris en charge par ImHex et que je connais bien pour l'avoir conçu il y a une décennie... 😅️

Présentation du format Obsidian Project

Wanadev Project File est un format de fichier binaire créé et utilisé par Wanadev pour enregistrer les projets issus des logiciels et configurateurs 3D qu'elle développe, comme Kazaplan et bien d'autres. Le format a été créé en novembre 2015 puis a été open sourcé en février 2016 sous le nom d'Obsidian Project File, accompagné de la bibliothèque JavaScript et des outils en ligne de commande associés.

Ce format a été pensé pour être facilement utilisable (et performant) en JavaScript dans un navigateur Web sur le PC de monsieur et madame Tout-le-Monde (donc des machines grand public, peu puissantes et potentiellement vieillissantes). Il est composé :

  • d'un entête binaire de 51 octets [oui je sais... 😅️] contenant divers champs permettant d'identifier le format, sa version, le type de projet auquel on a à faire et diverses informations sur les sections suivantes du fichier.

  • On a ensuite les sections :

    • « Metadata » (un objet clef/valeur avec diverses informations utiles au projet),
    • « Project » (une sérialisation des structures de données du projet)
    • et « BlobIndex » (la liste des pièces jointes du projet avec quelques métadonnées).

    Ces trois sections sont en JSON (car c'est le format le plus simple et le plus efficace en JavaScript) qui peut optionnellement être compressé (en pratique c'est en fait toujours le cas, mais ça peut se désactiver au besoin).

  • Et enfin on a la section « Blobs » qui contient une concaténation de toutes les pièces jointes du projet (le plus souvent des images ou des modèles 3D).

    Cette section n'est pas compressée pour des raisons de performance : on peut adresser les fichiers contenus dans cette section directement en mémoire sans avoir à effectuer le moindre calcul ni à copier les données ; et puis comme le plus souvent il s'agit d'images déjà compressées, ça serait de toute façon assez peu pertinent.

    Cette section est la seule qui peut avoir une taille nulle, lorsque le projet n'a aucune pièce jointe.

Structure d'un fichier Obsidian Project

Structure d'un fichier Obsidian Project

Voici quelques liens pour plus de détail sur le format de fichier, la bibliothèque JavaScript et les outils associés :

Je vous mets également un exemple de fichier au format Obsidian Project que vous pouvez ouvrir avec ImHex si vous voulez voir à quoi ça ressemble :

Création du pattern : directives

Maintenant qu'on sait à quoi ressemble le format de fichier Obsidian Project, on va pouvoir se lancer dans l'écriture du pattern qui permettra à ImHex de l'analyser. Pour ce faire on va ouvrir un fichier Obsidian Project dans ImHex (vous pouvez télécharger celui d'exemple que je vous ai fourni dans la section précédente), puis on va taper notre code dans l'onglet « Pattern editor ».

Et on va commencer par définir les trois directives suivantes :

#pragma description Obsidian Project File
#pragma magic [57 50 52 4A] @ 0
#pragma endian big

La première directive contient une description destinée aux humains et qui sera affichée dans l'interface de ImHex.

La seconde directive permet d'expliquer à ImHex comment reconnaître un fichier Obsidian Project via son magic number. Ici on lui explique que le fichier doit commencer (@ 0) par les octets 0x57 0x50 0x52 0x4A (soit "WPRJ" en hexadécimal).

Enfin, la dernière directive indique que tous les nombres seront encodés en big endian (par défaut ImHex les considère en little endian), ce qui nous permettra d'éviter de le définir explicitement à chaque fois.

Création du pattern : le fichier

On va maintenant rajouter les 2 lignes suivantes :

struct ObsidianProjectFile {};
ObsidianProjectFile file @ 0;

Ici on a défini une structure nommée "ObsidianProjectFile" qui va englober toutes nos données, puis on indique à ImHex que cette structure est présente dans le fichier à partir de l'adresse 0 et on nome cette « instance » "file".

On peut à présent cliquer sur le petit bouton « play » situé en bas à gauche de l'éditeur de pattern. Il ne va pas se passer grand-chose car pour le moment on a seulement défini une structure englobant tout le fichier sans plus de détail, mais si tout s'est bien passé, vous devriez voir la ligne suivante apparaître dans la console :

I: Pattern exited with code: 0
Capture d'écran de l'éditeur de pattern de ImHex

Éditeur de pattern de ImHex

Création du pattern : l'entête

On va à présent définir une nouvelle structure de donnée que l'on va nommer « Header » pour décrire l'entête du fichier :

struct Header {
    char magic[4];
    u16 version;
    char type[10];
    u8 metadataFormat;
    u32 metadataOffset;
    u32 metadataLength;
    u8 projectFormat;
    u32 projectOffset;
    u32 projectLength;
    u8 blobIndexFormat;
    u32 blobIndexOffset;
    u32 blobIndexLength;
    u32 blobsOffset;
    u32 blobsLength;
};

Ici on retrouve simplement tous les champs qu'on a vus un peu plus tôt quand j'ai expliqué à quoi ressemblait le format Obsidian File donc je n'entre pas plus dans les détails car c'est assez transparent.

Et maintenant on va modifier notre structure "ObsidianProjectFile" pour y déclarer notre entête :

struct ObsidianProjectFile {
    Header header;
};

Si on clique à nouveau sur le petit bouton play, cette fois-ci on devrait commencer à voir quelques trucs intéressants apparaitre : de la couleur dans l'éditeur hexadécimal, et une arborescence de structures et de champs dans l'inspecteur qui se trouve en dessous :

Capture d'écran : première mise en couleur

Première mise en couleur

À ce stade une petite amélioration peut encore être apportée à la déclaration de notre entête : la valeur stockée dans les champs "metadataFormat", "projectFormat" et "blobIndexFormat" ont une signification particulière :

  • 0x00 signifie que la section correspondante est encodée en JSON,
  • 0x01 signifie que la section est encodée au format JSON+deflate (JSON compressé).

On peut apporter cette information pour faciliter l'interprétation des données. Pour ce faire, on va déclarer un petit enum qui nous permettra d'expliciter tout ça :

enum SectionFormats: u8 {
    JSON = 0x00,
    JSONDeflate = 0x01,
};

Il ne nous reste plus qu'à remplacer le type "u8" des champs de l'entête par notre enum :

struct Header {
    ...
    SectionFormats metadataFormat;
    ...
    SectionFormats projectFormat;
    ...
    SectionFormats blobIndexFormat;
    ...
};

Et voici ce que ça donne dans l'inspecteur si on relance notre petit script :

Création du pattern : les autres sections du fichier

Attaquons-nous à présent à la section suivante : Metadata.

Commençons par définir une structure de donnée pour la contenir :

struct MetadataSection {
    u8 metadata[0];
};

Ici on déclare donc la structure "MetadataScection" qui contient un unique champ : "metadata" qui est un tableau d'octet de taille... qu'on va mettre à 0 pour le moment et on y reviendra un peu plus tard... 😅️

Ajoutons à présent notre structure dans la déclaration du fichier Obsidian ("ObsidianProjectFile") :

struct ObsidianProjectFile {
    Header header;
    MetadataSection metadata @ header.metadataOffset;  // <-----
}

On déclare donc un élément de type "MetadataSection" qui commence à l'adresse indiquée dans le header du fichier déclaré juste au-dessus ("@ header.metadataOffset").

Et maintenant revenons-en à la taille de la section Metadata. Si on a pu récupérer l'adresse de la section ici, on devrait aussi être en mesure de récupérer sa taille pour notre champ "MetadataSection.metadata" qu'on a déclaré un peu plus tôt non ?

Et bah oui ! Il est possible depuis une structure de remonter à la structure parente via le mot clef "parent". On peut donc modifier notre structure "MetadataSection" de la manière suivante :

struct MetadataSection {
    u8 metadata[parent.header.metadataLength];
};

Si on exécute notre code, la section Metadata devrait à présent être mise en couleur... C'est cool, mais il y a toutefois un petit détail qui me chiffonne :

Comme vous pouvez le constater, on voit "metadata" apparaitre deux fois de manière imbriquée. La première ligne correspond à la structure "MetadataSection" (que l'on a déclaré sous ce nom dans la structure du fichier) et celle du dessous au champ "metadata" de la structure elle-même.

C'est pas bien grave en soi, mais ça serait quand même mieux sans ce doublon ! Heureusement ImHex a prévu le coup ! 😁️

Il est en effet possible d'ajouter l'attribut [[inline]] à la déclaration de notre tableau d'u8 pour qu'il intègre le contenu du champ directement dans la structure parente :

struct MetadataSection {
    u8 metadata[parent.header.metadataLength] [[inline]];
};

Si on exécute cette version du code, on peut constater que cette fois-ci le doublon a disparu :

Maintenant qu'on a vu comment ça marchait pour la section Metadata, on peut déclarer les suivantes en se basant sur le même modèle, ce qui nous donne le script suivant :

#pragma description Obsidian Project File
#pragma magic [57 50 52 4A] @ 0
#pragma endian big

enum SectionFormats: u8 {
    JSON = 0x00,
    JSONDeflate = 0x01,
};

struct Header {
    char magic[4];
    u16 version;
    char type[10];
    SectionFormats metadataFormat;
    u32 metadataOffset;
    u32 metadataLength;
    SectionFormats projectFormat;
    u32 projectOffset;
    u32 projectLength;
    SectionFormats blobIndexFormat;
    u32 blobIndexOffset;
    u32 blobIndexLength;
    u32 blobsOffset;
    u32 blobsLength;
};

struct MetadataSection {
    u8 metadata[parent.header.metadataLength] [[inline]];
};

struct ProjectSection {
    u8 project[parent.header.projectLength] [[inline]];
};

struct BlobIndexSection {
    u8 blobIndex[parent.header.blobIndexLength] [[inline]];
};

struct Blobs {
    u8 blobs[parent.header.blobsLength] [[inline]];
};

struct ObsidianProjectFile {
    Header header;
    MetadataSection metadata @ header.metadataOffset;
    ProjectSection project @ header.projectOffset;
    BlobIndexSection blobIndex @ header.blobIndexOffset;
    Blobs blobs @ header.blobsOffset;
};

ObsidianProjectFile file @ 0;

Et voilà, avec tout ça vous avez de quoi analyser plus facilement des fichiers Obsidian File avec ImHex ! 😁️

Bonus : décompresser les sections compressées

On aurait pu s'arrêter là, mais il y a un dernier truc sympa qu'on pourrait faire : afficher une version décompressée des sections Metadata, Project et BlobIndex lorsqu'elles sont compressées.

Ça tombe bien, ImHex fournit justement une bibliothèque pour décompresser des données au format zlib/gzip : "hex.dec". Pour l'utiliser il faut tout simplement l'importer en rajoutant la ligne suivante en haut du script :

import hex.dec;

Ensuite on va modifier notre structure de donnée "MetadataSection" de la façon suivante :

struct MetadataSection {
    u8 metadata[parent.header.metadataLength] [[inline]];
    // ↓↓↓↓
    if (parent.header.metadataFormat == SectionFormats::JSONDeflate) {
        std::mem::Section decompressed = std::mem::create_section("MetadataSection (decompressed)");
        hex::dec::zlib_decompress(metadata, decompressed, -15);
    }
    // ↑↑↑↑
};

Pour commencer on wrap tout dans un if qui nous permettra d'effectuer l'opération de décompression uniquement lorsque la section est effectivement compressée.

Ensuite, on crée une section en mémoire que l'on va appeler "decompressed" et qui contiendra... bah nos données décompressées. On notera que la fonction "std::mem::create_section()" prend une string en paramètre. Il s'agit du nom que l'on donne à la section et qui est destiné aux humains ; il sera affiché dans l'interface.

Enfin, on appelle la fonction "hex::dec::zlib_decompress()" pour décompresser notre section. En premier paramètre il faut lui fournir la donnée compressée, en second paramètre l'endroit où vont être écrites les données, et le dernier paramètre correspond au "windowBits" (a.k.a. window size) de la zlib...

À vrai dire j'ai un peu galéré pour trouver la bonne valeur à passer pour ce 3ème paramètre... À l'époque où j'ai créé le format, je me suis contenté d'utiliser la fonction "zlib.DeflareRaw()" de Node.js, qui n'indique nulle part à quel paramétrage de la zlib ça correspond vraiment. La doc indique seulement :

Compress data using deflate, and do not append a zlib header.

Dans ces cas-là la meilleure chose à faire est généralement d'aller voir le code source... mais ça ne m'a pas beaucoup aidé. Le code des bindings de la zlib de Node.js n'est en effet pas très explicite et utilises des constantes qui proviennent des tréfonds de Node (internal binding).

Bref, j'ai laissé tomber cette piste et suis allé voir si la doc de la zlib pourrait m'éclairer davantage, et heureusement ça a été le cas ! Voici donc ce que nous apprend la doc :

The windowBits parameter is the base two logarithm of the window size (the size of the history buffer). It should be in the range 8..15 for this version of the library. [...] windowBits can also be –8..–15 for raw deflate. In this case, -windowBits determines the window size. deflate() will then generate raw deflate data with no zlib header or trailer, and will not compute a check value.

Au moins avec ça j'ai compris que je devais fournir une valeur entre -9 et -15 (malgré le fait que la doc de ImHex indique qu'il s'agit d'un paramètre de type u64, soit entier 64 bits non signé... 😑️).

Et pour trouver la valeur exacte à passer... bah j'ai tâtonné jusqu'à ce que ça fonctionne... 😅️

Enfin bref, après cette aventure, on peut ajouter des implémentations similaires pour les deux autres sections. Une fois ceci fait, on peut cliquer sur le bouton play et nous rendre dans l'onglet « Sections » :

Ici on y retrouve les sections que l'on a créées et sur la ligne de chacune des sections se trouvent 2 boutons. Le premier permet de visualiser le contenu de la section dans un éditeur hexadécimal, et le second permet d'enregistrer le contenu de la section dans un fichier.

Si on clique sur le premier bouton, on peut enfin voir en clair le contenu de section correspondante, ce qui est plutôt pratique quand on analyse un fichier :

Fichier de pattern complet

Voici une version complète du fichier de pattern que l'on vient d'écrire :

#pragma description Obsidian Project File
#pragma magic [57 50 52 4A] @ 0
#pragma endian big

import hex.dec;

enum SectionFormats: u8 {
    JSON = 0x00,
    JSONDeflate = 0x01,
};

struct Header {
    char magic[4];
    u16 version;
    char type[10];
    SectionFormats metadataFormat;
    u32 metadataOffset;
    u32 metadataLength;
    SectionFormats projectFormat;
    u32 projectOffset;
    u32 projectLength;
    SectionFormats blobIndexFormat;
    u32 blobIndexOffset;
    u32 blobIndexLength;
    u32 blobsOffset;
    u32 blobsLength;
};

struct MetadataSection {
    u8 metadata[parent.header.metadataLength] [[inline]];
    if (parent.header.metadataFormat == SectionFormats::JSONDeflate) {
        std::mem::Section decompressed = std::mem::create_section("MetadataSection (decompressed)");
        hex::dec::zlib_decompress(metadata, decompressed, -15);
    }
};

struct ProjectSection {
    u8 project[parent.header.projectLength] [[inline]];
    if (parent.header.projectFormat == SectionFormats::JSONDeflate) {
        std::mem::Section decompressed = std::mem::create_section("ProjectSection (decompressed)");
        hex::dec::zlib_decompress(project, decompressed, -15);
    }
};

struct BlobIndexSection {
    u8 blobIndex[parent.header.blobIndexLength] [[inline]];
    if (parent.header.blobIndexFormat == SectionFormats::JSONDeflate) {
        std::mem::Section decompressed = std::mem::create_section("BlobIndexSection (decompressed)");
        hex::dec::zlib_decompress(blobIndex, decompressed, -15);
    }
};

struct Blobs {
    u8 blobs[parent.header.blobsLength] [[inline]];
};

struct ObsidianProjectFile {
    Header header;
    MetadataSection metadata @ header.metadataOffset;
    ProjectSection project @ header.projectOffset;
    BlobIndexSection blobIndex @ header.blobIndexOffset;
    Blobs blobs @ header.blobsOffset;
};

ObsidianProjectFile file @ 0;

Vous pouvez également télécharger directement le fichier en cliquant sur le bouton ci-dessous :

Prise en compte automatique du pattern

Il nous reste une dernière petite chose à faire : permettre à ImHex de charger tout seul notre fichier de pattern quand on ouvre un fichier Obsidian Project. Pour ce faire il va suffire d'enregistrer notre pattern dans un fichier nommé "obsidian-file.hexpat" et de le placer au bon endroit.

Sous Linux le « bon endroit » est "~/.var/app/net.werwolv.ImHex/data/imhex/patterns/", mais le plus simple c'est de passer par le menu « Help → About » puis d'aller dans l'onglet « Directories ». Là vous trouverez la liste des différents dossiers pris en compte, et vous pourrez même cliquer directement sur celui qui vous intéresse pour ouvrir votre navigateur de fichier ! 😁️

Capture d'écran : Liste des dossiers dans la fenêtre « À propos » de ImHex

Liste des dossiers dans la fenêtre « À propos » de ImHex

Une fois le fichier placé dans ce répertoire, ImHex l'utilisera automatiquement à l'ouverture d'un fichier Obsidian File, qu'il pourra reconnaître grâce au "#pragma magic" qu'on avait écrit en haut du script.

Capture d'écran : Chargement automatique du pattern à l'ouverture de ImHex

Chargement automatique du pattern à l'ouverture de ImHex

Conclusion

ImHex est un éditeur hexadécimal super puissant, et clairement son système de pattern y est pour beaucoup (en tout cas pour mon usage). Je referais peut-être plus tard d'autres articles sur son Data Processor (l'éditeur nodal qu'on peut voir dans la première capture d'écran) et sur ses autres fonctionnalités si jamais je venais à les utiliser (n'hésitez pas à me dire si d'autres articles à ce sujet vous intéressent).

Au final écrire des pattern pour ImHex n'est pas très compliqué mais il est vrai que la doc manque d'exemples. Heureusement on peut se servir des innombrables patterns déjà dispo pour voir comment ça marche !

Je vous mets ci-dessous quelques liens utiles pour aller plus loin :

À Bientôt pour de nouveaux articles ! 😃️