lucidiot's cybrecluster

Rendre Kaitai Struct compatible avec Windows 2000 et XP

Lucidiot Informatique 2022-06-19
J'ai fait revenir un projet open-source 10 ans en arrière pour pouvoir lire des fichiers binaires.


Dans l'article précédent, j'ai utilisé un schéma Kaitai Struct pour réécrire un utilitaire pour Microsoft Train Simulator compatible avec Windows 2000 et XP. Le problème, c'est que le code que Kaitai génère n'est pas vraiment compatible avec ces versions de Windows. Dans cet article, je vais détailler un peu comment j'ai pu faire pour contourner ce problème.

Utiliser Kaitai dans un projet C

Pour commencer, il faut bien comprendre comment Kaitai Struct fonctionne. D'abord, le compilateur est écrit en Scala, ce qui rend possible de le compiler pour la JVM mais aussi pour JavaScript ; c'est cette dernière qui permet de faire fonctionner l'IDE en ligne. Le compilateur va lire un fichier .ksy, le valider et générer du code dans le langage que l'on souhaite (Java, C, C++, C#, Python, PHP, etc.).

Sous Windows 2000 et XP, je pourrais essayer d'exécuter le compilateur en installant Java ; la dernière version officiellement supportée sous 2000 est Java 6, et sous XP Java 7, mais Java 8 peut y être installé quand même en ignorant un avertissement. Scala peut se compiler vers Java 6, donc je pourrais sans problème exécuter ksc et ksv, le compilateur et le visualiseur (utile pour développer de nouveaux schémas).

Je préfère cela dit plutôt utiliser l'IDE de Kaitai, qui ne me demande même pas d'installer Java et surtout qui fournit une interface plus confortable pour écrire et tester des schémas. C'est assez lourd puisque c'est un gros paquet de JavaScript dans le navigateur, mais je peux éviter la plupart de la charge sur le système en écrivant le texte en YAML sous un éditeur de texte classique, puis en copiant vers l'IDE seulement au moment où je veux vraiment tester. Depuis l'IDE, un menu permet de générer du code dans tous les langages supportés par Kaitai Struct, donc je peux y générer le fichier C# qui m'intéresse.

Une fois qu'on a ce code généré, on peut l'intégrer à un projet de code existant. On va devoir ajouter une nouvelle dépendance, appelée le runtime de Kaitai Struct. Le code généré est relativement concis et utilise en fait un grand nombre de fonctions définies dans ces runtimes, pour éviter de générer trop de code illisible et faciliter la maintenance.

Le runtime C# est appelé kaitai_struct_csharp_runtime, et les développeurs normaux utiliseraient NuGet, le gestionnaire de paquets de .NET, pour l'installer via KaitaiStruct.Runtime.CSharp. Sur la page NuGet, on peut voir la liste des paquets dont dépend ce paquet, ainsi que des versions de .NET avec lesquelles il est compatible. Il n'y a aucune dépendance particulière, mais il est compilé pour .NET Framework 4.5 et .NET Standard 1.3. .NET Standard est une spécification de .NET, et pas une implémentation particulière ; si on compile vers .NET Standard, on compile automatiquement vers toute une variété d'implémentations de .NET Standard. On a par conséquent diverses autres implémentations prises en charge, telles que Mono ou Xamarin pour iOS.

NuGet n'existait pas à l'époque de Windows 2000 et Windows XP, donc je ne vais pas pouvoir l'utiliser. Il va me falloir utiliser une méthode un peu plus « barbare » en incorporant le runtime directement dans mon projet C# comme si c'était mon propre code, juste une sous-partie de mon programme. Je pourrais intégrer la DLL, qui est la version compilée du runtime, mais cela implique que je ne puisse pas appliquer de modifications pour résoudre les incompatibilités qui vont survenir avec ce ciblage de Windows 2000 et Windows XP.

Rétrograder vers .NET 2.0

Je développe sous Windows XP, donc il est assez logique de cibler Windows XP, sinon je vais avoir un peu mal à tester mon programme ou tout simplement l'utiliser pour résoudre mon problème initial. Mais je vise aussi un peu plus bas avec Windows 2000 : je veux être compatible avec un système qui a en fait apporté une grande partie des innovations qui ont conduit à Windows XP, bien qu'il aie été beaucoup ignoré, de la même façon que Windows Vista a apporté beaucoup des innovations pour lesquelles Windows 7 a reçu les éloges, pendant qu'on le roule dans la boue. On peut voir à quel point Windows 2000 est globalement oublié quand on voit que les informations de compatibilité pour Windows 95, 98, XP ou même Vista sont souvent disponibles, mais rarement Windows NT ou Windows 2000. Windows NT est lui aussi tombé dans l'oubli, mais il est beaucoup plus difficile de le cibler en développant en C# que Windows 2000, qui reste plus proche de XP.

Si on ne vise que du développement sous Windows XP, on peut utiliser le .NET Framework 4.0, mais Windows 2000 nous restreint au .NET Framework 2.0. Une version plus ancienne du .NET Framework implique une version plus ancienne du langage C#, ce qui peut introduire des problèmes d'incompatibilité ; par exemple, les constructeurs de classes ne peuvent pas avoir de paramètres par défaut en C# 2.0, qui est la version prise en charge par le .NET Framework 2.0. Le .NET Framework 4.0 fournit C# 4.0, et le runtime de Kaitai Struct se compile sous .NET Framework 4.5, qui fournit C# 5.0. On a donc trois versions majeures de différence ! Il est donc très probable qu'on aie des problèmes en essayant d'intégrer le runtime.

Tout d'abord, j'ai dû créer un fichier .sln, une solution Visual Studio, qui sert à regrouper différents projets en C# ou d'autres langages. Visual Studio 2005, la dernière version de Visual Studio qui fonctionne complètement sous Windows 2000 et XP et me garantit donc une compatibilité totale, n'aime pas trop travailler avec seulement un projet C# (.csproj) sans solution.

Ensuite, il m'a fallu réécrire presque entièrement le fichier .csproj. Ce fichier a probablement été créé avec dotnet, l'outil en ligne de commande pour créer des projets .NET y compris sous Mac et Linux, ou avec une version assez récente de Visual Studio. Il contenait des propriétés spécifiques aux packages NuGet, mais surtout il ne contenait même plus d'instructions à Visual Studio sur comment le compiler, ou quels fichiers compiler ; beaucoup de choses que les outils .NET modernes peuvent détecter presque tout seuls aujourd'hui, mais dont dépend absolument VS2005.

Maintenant que Visual Studio 2005 arrive enfin à comprendre que ce projet C# existe, on peut essayer de le compiler. Et on se retrouve avec une seule erreur :

Error   1   The type or namespace name 'Linq' does not exist in the namespace 'System' (are you missing an assembly reference?)

LINQ est une fonctionnalité introduite dans la version 3.5 du .NET Framework, en 2007. LINQ fournit une syntaxe similaire à SQL, et assez étrange, pour manipuler des structures de données en C#. Cette syntaxe est suffisamment étrange pour qu'on ne la rencontre que rarement dans du code C#, et que moult ne connaissent pas forcément son existence, mais l'API qui fait fonctionner cette extension de C#, définie dans System.Linq, est quant à elle régulièrement utilisée. L'équivalent de cette API dans le langage Java est le concept de Stream, qui n'est arrivé qu'en 2014. Lorsque je développe en C# dans un contexte où j'ai accès à LINQ, je n'hésite pas à en abuser, mais pour viser Windows 2000, je n'ai pas d'autre choix que de l'abandonner.

System.Linq est de nos jours ajouté par défaut quand on crée un fichier C# avec Visual Studio, parce que beaucoup de développeurs vont l'utiliser, parfois même sans le savoir, tout comme System.Collections.Generic ou juste System tout court. Mais il peut arriver qu'il ne soit pas utilisé du tout, et donc que Visual Studio aie ajouté une dépendance pour rien. J'ai retiré using System.Linq; du runtime, et d'un seul coup, l'erreur a disparu pour laisser place à une autre :

Error   1   'System.IO.Compression.DeflateStream' does not contain a definition for 'CopyTo'

La bonne nouvelle, c'est qu'effectivement, LINQ n'était pas du tout utilisé. On n'aura donc pas à tout réimplémenter nous-mêmes... On va devoir quand même réécrire une méthode particulière : System.IO.Stream.CopyTo, une méthode dont héritent tous les Stream de .NET, n'est disponible qu'à partir de .NET 4.0. On parle de classes permettant de lire des flux de données, pas de l'équivalent Java de LINQ. Comme d'habitude, StackOverflow vient à notre secours et on peut juste ajouter une nouvelle méthode pour fournir la même fonctionnalité.

Et c'est après cette réimplémentation que le runtime de Kaitai Struct se compile avec succès sous .NET Framework 2.0. Ça ne demandait pas tant que ça d'efforts finalement -- le runtime officiel pourrait sans problèmes se rétrograder aussi et me fournir une DLL compilée, mais la réponse automatique de tous les projets open-source face à ça est que c'est toujours trop compliqué de maintenir ce genre de compatibilité, même si on leur donne la preuve de sa simplicité, ou que c'est de ma responsabilité de me mettre à jour. Vous pouvez consulter toutes les différences entre le runtime officiel et mon fork pour vous faire votre propre idée.

Rétrograder le code généré

Il nous reste une dernière épine dans le pied : le code généré par le compilateur de Kaitai Struct peut lui aussi utiliser des fonctionnalités de C# 3, 4 ou 5.0, puisque le runtime dépendra de toute façon de ça. Ce serait assez gênant de devoir rétrograder ce code généré à la main à chaque fois qu'on veut modifier le schéma. Je pourrais m'amuser à essayer de compiler ma propre version du compilateur de Kaitai Struct pour modifier le générateur, mais cela impliquerait de coder en Scala et surtout de préparer un environnement de développement pour du Scala. Je n'ai pas envie de passer autant de temps à savoir si je peux coder en Scala sous Windows XP, alors je vais plutôt essayer de modifier le code généré moi-même.

J'ai pu identifier deux types de problèmes dans le code généré qui nous empêchent de le compiler en C# 2.0 : d'abord, les constructeurs de toutes les classes ont des valeurs par défaut pour leurs paramètres. Cette fonctionnalité a été introduite dans C# 4.0. Le compilateur utilise toujours la même forme de constructeur pour toutes les classes qu'il génère, donc on pourrait probablement faire ce remplacement automatiquement :

// La syntaxe "= null" n'est pas compatible avec C# 2.0
public Something(KaitaiStream p__io, SomeParentClass p__parent = null, SomeRootClass p__root = null) : base(p__io)
{
  // ...
}

// On pourrait remplacer ça par trois constructeurs équivalents :
public Something(KaitaiStream p__io) : this(p__io, null) { }
public Something(KaitaiStream p__io, SomeParentClass p__parent) : this(p__io, p__parent, null) { }
public Something(KaitaiStream p__io, SomeParentClass p__parent, SomeRootClass p__root) : base(p__io)
{
 // ...
}

L'autre fonctionnalité incompatible a été introduite dans C# 3.0 : le mot-clé var. Ce mot-clé est un raccourci pour éviter d'avoir à écrire deux fois le type d'une variable, par exemple var truc = new UnNomDeClasseTresLong() au lieu de UnNomDeClasseTresLong truc = new UnNomDeClasseTresLong(). Kaitai s'en sert de deux façons, et on aura donc deux remplacements légèrement différents :

// C# 3.0
var something = new Something(...);
var number = 3;

// C# 2.0
Something something = new Something(...);
int number = 3;

L'avantage d'avoir du code généré, c'est que c'est assez répétitif et que toutes ces lignes vont toujours obéir au même motif. Au même motif qui pourrait être exprimé sous la forme d'une... expression. Et c'est reparti, encore des expressions régulières !

Après avoir généré mon code C# et enregistré dans un fichier, je peux exécuter une commande sed dans mon shell Cygwin :

sed -i 's/^\(\s*\)public \(\w+\)(KaitaiStream p__io, \([\w.]+\) p__parent = null, \([\w.]+\) p__root = null) : base(p__io)/\1public \2(KaitaiStream p__io) : this(p__io, null) { }\n\1public \2(KaitaiStream p__io, \3 p__parent) : this(p__io, p__parent, null) { }\n\1public \2(KaitaiStream p__io, \3 p__parent, \4 p__root) : base(p__io)/;s/var \(.+\) = new \([\w.]+\)/\2 \1 = new \2/;s/var \(.+\) = \([0-9]+\)/int \1 = \2/' GeneratedFile.cs

Cette commande exécute sur mon fichier C# trois commandes s, c'est-à-dire des substitutions. Chaque substitution contient donc deux expressions régulières, une pour trouver le texte à substituer, et une pour générer le texte de remplacement. Voilà les trois paires d'expressions sous une forme plus lisible :

^\(\s*\)public \(\w+\)(KaitaiStream p__io, \([\w.]+\) p__parent = null, \([\w.]+\) p__root = null) : base(p__io)
\1public \2(KaitaiStream p__io) : this(p__io, null) { }\n\1public \2(KaitaiStream p__io, \3 p__parent) : this(p__io, p__parent, null) { }\n\1public \2(KaitaiStream p__io, \3 p__parent, \4 p__root) : base(p__io)/

var \(.+\) = new \([\w.]+\)
\2 \1 = new \2/

var \(.+\) = \([0-9]+\)
int \1 = \2

La première substitution, qui découpe les constructeurs en trois, est la plus complexe. Notons d'abord que, si vous n'avez jamais utilisé sed, les groupes de capture ont un comportement particulier en raison de leur définition dans POSIX : () ne correspond qu'à de simples parenthèses, et il faut écrire \(\) pour avoir un vrai groupe de capture. Je pourrais inverser ce comportement avec sed -E, mais c'est une extension fournie uniquement par les outils GNU, et non définie dans POSIX, et j'essaie quand je peux de rester aussi proche que possible de POSIX pour une compatibilité maximale. Qui sait, peut-être que quelqu'un d'encore plus fou que moi essaie de compiler pour C# 2.0 sur un système Multics ?

On commence par un groupe de capture qui ne récupère que des espaces, car je vais avoir besoin de connaître l'indentation du constructeur pour pouvoir la respecter en ajoutant de nouvelles lignes. Le second groupe de capture me permet de récupérer le nom de la classe qu'on construit. Ensuite, je sais que l'argument p__io est toujours le même et toujours du même type, donc il est là sans aucune modification particulière. Pour p__parent et p__root, je récupère là par contre le type de chaque argument dans des groupes de capture aussi.

Pour le remplacement, je crée donc trois lignes ; on peut voir les nouveaux sauts de ligne avec \n. Au début de chaque ligne, j'utilise \1 pour ajouter l'indentation. Tant qu'on y est, autant garder du code un peu propre. J'ai ensuite le nom de la classe à disposition dans \2 et le type des deux arguments optionnels dans \3 et \4, donc je les utilise pour écrire mes trois constructeurs comme je les avais décrits un peu plus haut.

Pour le mot-clé var, je commence par remplacer tous les usages où on voit un mot-clé new ; ce qui se trouve après le new sera le type de ma variable, donc je peux juste remplacer var par ce type. Pour le cas où ce n'est pas un new mais juste un nombre entier, j'utilise bêtement int.

Avec cette commande sed, je n'ai donc qu'une étape supplémentaire assez rapide à faire pour pouvoir obtenir du code compatible avec Windows 2000 et XP sans efforts.

Le processus en résumé

Pour utiliser Kaitai Struct avec des projets C# visant Windows 2000 et XP, je suis donc à peu de choses près ces étapes :

  1. Écrire le schéma en YAML dans Notepad++, tout en prenant des notes pour l'article Brainshit qui finira inévitablement par sortir.
  2. Copier le schéma dans l'IDE Kaitai Struct et y envoyer un fichier binaire pour le tester.
  3. Recommencer les deux premières étapes jusqu'à ce que le parsing soit satisfaisant.
  4. Utiliser l'IDE pour compiler dans du code C#.
  5. Copier le code C# dans un fichier.
  6. Appliquer la commande sed sur le fichier pour le rendre compatible avec C# 2.0.
  7. Incorporer ce fichier C# dans un projet C# de Visual Studio 2005, configuré pour .NET Framework 2.0.
  8. Ajouter à la solution Visual Studio le fork de kaitai_struct_csharp_runtime, en utilisant un sous-module Git.
  9. Écrire du code utilisant ce nouveau code généré pour faire quelque chose d'intéressant !

Conclusion

Je n'ai pas précisé qu'avant d'utiliser une commande sed, j'utilisais en fait la fonction de recherche et remplacement de Notepad++, qui prend en charge les expressions régulières. Cet article n'était à la base qu'un pauvre fichier texte qui traînait sur mon disque dur, comprenant les quelques expressions à copier-coller dans la boîte de dialogue de Notepad++ pour pouvoir faire la conversion...

Avoir ce fichier étrange ouvert dans le même éditeur où j'écris habituellement des articles pour Brainshit en Markdown m'a donné envie de le transformer aussi en article, pour documenter un peu tout ce que j'ai dû vraiment mettre en place pour jouer avec des données binaires sous XP.

J'ai au moins deux autres exemples intéressants d'utilisation de Kaitai Struct sous XP, peut-être même trois. Le premier demande d'abord encore beaucoup d'autres articles pour avoir du contexte. le second demande que je réalise tout simplement le projet, ce qui implique que je répare d'abord du matériel dont j'ai besoin, et le dernier demande encore beaucoup de recherches et ne sera peut-être pas forcément nécessaire. Vous devriez donc peut-être voir Kaitai réapparaître dans mes projets à l'avenir, si je ne change pas complètement mes plans entre temps...


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !