lucidiot's cybrecluster

Extended Well-Known Binary avec Kaitai Struct

Lucidiot Informatique 2022-03-13
La dernière étape dans le traitement du WKB, une variante non-standard bien plus simple que le standard.


On arrive enfin au bout du Well-Known Binary : la troisième et dernière variante, l'Extended Well-Known Binary ou EWKB. Cette variante n'est pas définie par un standard, ce que je trouve finalement assez positif après le dernier combat contre le WKB ISO. En échange, on se retrouve du coup avec une documentation très succinte sur le site de PostGIS, le système d'information géographique qui a créé l'EWKB, et rien de plus.

J'ai essayé de faire quelques recherches sur l'EWKB pour essayer de comprendre un peu plus son histoire, peut-être retrouver des échanges de mails sur des mailing lists ou des articles de blogs faits par des développeurs impliqués dans PostGIS, et je n'ai pas trouvé grand chose de très intéressant. Le résultat de recherche qui documentait probablement le plus ce format était mon propre article introduisant le Well-Known Binary en général et toutes ses variantes...

Il y a quand même deux bonnes nouvelles : premièrement, on sait grâce à la documentation que l'EWKB ne rajoute pas de nouvelles géométries par rapport au WKB standard. Oui, on repart avec le WKB standard et pas l'énormité du WKB ISO ! Deuxièmement, on a cette fois à notre disposition un grand nombre d'implémentations pour générer ou interpréter des géométries en EWKB et on peut donc bien plus facilement apprendre et tester son travail.

Extensions au WKB standard

Le WKB étendu rajoute concrètement trois fonctionnalités : la profondeur ou dimension Z, la mesure ou dimension M, et le SRID.

On a déjà vu le SRID à plusieurs reprises en parlant des WKB et WKT, mais pour rappel c'est un identifiant qui indique le système de coordonnées qu'on utilise. Cela permet par exemple de convertir entre des coordonnées utilisées par des cartes spécifiques à un pays, comme par exemple le Réseau Géodésique Français 1993 ou RGF93, vers le bon vieux World Geodetic System 1984, les coordonnées GPS classiques.

Le RGF93 est encore utilisé aujourd'hui en France par l'IGN, ce qui montre que les SRID sont toujours aussi importants parce que tout le monde ne parle pas avec les mêmes systèmes de coordonnées. Il n'est pas aisé de passer au WGS84 ici, car le RGF93 a une précision de deux centimètres. Une telle précision avec des coordonnées GPS pourrait facilement résulter en des problèmes liés aux nombres à virgule flottante, une création qui ne fait que causer des problèmes aux informaticiens et non-informaticiens !

Enfin bref, après l'exécution de quelques commandes ST_AsEWKB sur un serveur PostgreSQL avec l'extension PostGIS installée, on peut commencer à regarder à quoi ressemble concrètement l'EWKB. Tout d'abord, pour un point classique à deux dimensions et sans SRID, il n'y a pas de différence :

01 01 00 00 00 71 3D 0A 57 28 FC 23 41 0A D7 A3 70 A5 3D 08 41 Boutisme Type X Y Coordonné e Coordonné e

Ensuite, pour un point à quatre dimensions sans SRID, on voit apparaître deux nombres à virgule flottante supplémentaire et le type change vers une valeur que nous ne connaissons pas :

01 01 00 00 C0 71 3D 0A 57 28 FC 23 41 0A D7 A3 70 A5 3D 08 41 Boutisme Type X Y 00 00 00 00 00 70 75 40 00 00 00 00 00 00 45 40 Z Mesure Coordonné e Coordonné e Coordonné e

Enfin, avec un point à deux dimensions qui a un SRID, on voit apparaître le SRID sous forme de nombre entier avant les coordonnées et le type change aussi différemment.

01 01 00 00 20 AB 6B 00 00 71 3D 0A 57 28 FC 23 41 0A D7 A3 70 A5 3D 08 41 Boutisme Type SRID X Y Coordonné e Coordonné e

Structure du type

Le plus important dans l'EWKB, et le moins bien documenté, semble donc être des modifications sur le type. Si on écrit le type en binaire au lieu d'hexadécimal, on peut le déchiffrer en comparant plusieurs binaires générés par PostGIS :

00000001 00000000 00000000 1 1 1 00000 Type Z M SRID Type

Les 4 octets qui étaient assignés au type ne sont plus seulement destinés au type ; on a maintenant 4 données différentes. 3 bits sont utilisés pour spécifier trois valeurs booléennes. Ces valeurs indiquent si la géométrie comporte une dimension Z, une dimension M ou un SRID. Les 29 bits restants sont utilisés pour le type de géométrie.

L'ordre des bits apparaît ici comme assez étrange. C'est normal : nous sommes en petit-boutisme. Pour lire ce champ correctement, on doit réordonner les octets pour former une structure en gros-boutisme :

1 1 1 00000 00000000 00000000 00000001 Z M SRID Type

J'ai utilisé le petit-boutisme dans mon premier schéma car tous les exemples que j'ai pu générer avec PostGIS utilisent le petit-boutisme. Je ne sais pas comment forcer le gros-boutisme, ni pourquoi ça utilise un format qui n'est plus celui le plus couramment utilisé par les processeurs, donc un format qui est plus lent à comprendre pour les humains et aussi pour les machines.

Ce sacrifice des trois bits ayant les poids les plus forts signifient que les types de géométrie ne peuvent plus s'étendre que de 0 à 536 870 911 (229–1) au lieu de 0 à 4 294 967 296 (232–1) Sachant que le type le plus élevé dans du WKB ISO est 1 000 005 et qu'il est déprécié, on est larges.

Lecture de bits avec Kaitai Struct

Il y a deux façons de lire des bits dans Kaitai Struct : la méthode classique est d'utiliser des opérateurs bit-à-bit (bitwise en anglais) comme &, >> ou <<, et la méthode plus récente utilise des champs de type bit.

Champs à l'échelle des bits

Quand on avait traité la courbe NURBS, on avait choisi d'ignorer un unique bit dont l'usage n'était pas clairement spécifié et qui posait beaucoup de problèmes avec Kaitai Struct. Mais dans le cas présent, les nouvelles propriétés du type de géométrie sont bien plus faciles à traiter. La différence réside dans le fait que nous savons ici que tout reste aligné sur des octets : il n'y a pas de décalage où une valeur qui devrait normalement occuper un octet se retrouverait à cheval entre deux octets.

Tout reste bien propre, et on peut donc demander à Kaitai Struct de lire quatre octets, puis de traiter séparément les trois premiers. Pour cela, on peut utiliser le type b, qui spécifie des valeurs non-signées mesurées en bits. b1 permet d'indiquer une valeur n'occupant qu'un seul bit. On va en utiliser trois, il nous reste donc 29 bits, donc on utilisera b29 pour le type de géométrie.

On pourrait donc imaginer un type dans Kaitai Struct qui contiendrait les 4 valeurs, ainsi que la propriété bit-endian pour indiquer le boutisme à utiliser pour la lecture des bits eux-mêmes, puisqu'il est à définir distinctement du boutisme par octet :

seq:
  - id: has_z
    type: b1
  - id: has_m
    type: b1
  - id: has_srid
    type: b1
  - id: type
    type: b29
    enum: geometry_type
meta:
  bit-endian:
    switch-on: _parent._parent.endianness
    cases:
      "0": be
      "1": le

Cependant, le boutisme à l'échelle des bits est plus compliqué que ça. D'abord, switch-on n'y est pas pris en charge, donc cette syntaxe nous produira une belle erreur. Ensuite, l'ordre des champs n'est plus le bon quand on passe en petit-boutisme. L'explication à ce problème implique un schéma complexe dans la documentation officielle.

J'ai fait pas mal d'expérimentation, et ça a notamment inclus la création d'un type geometry_header qui effectuerait une sorte d'abstraction par dessus ce traitement complexe pour essayer de simplifier le reste du parseur. Mais le problème de l'impossibilité de réordonner facilement tous les champs et de changer le boutisme en fonction d'un autre champ a vraiment trop complexifié le parseur. Il m'est apparu assez clair que la solution alternative est de traiter ces flags booléens comme on le ferait dans un langage tel que C++, avec des opérateurs travaillant sur les bits.

Opérateurs bit-à-bit

On va utiliser un seul opérateur pour pouvoir transformer plusieurs fois le nombre à 4 octets qui contient plusieurs informations en plusieurs valeurs qui nous intéressent : l'opérateur et (&). On va d'abord juste changer notre champ de type de géométrie : ce n'est plus un type correspondant à l'énumération geometry_type, mais un « type brut », qu'il faudra traiter ailleurs.

- id: raw_type
  type: u4

Pour créer des valeurs calculées à partir de champs dans Kaitai Struct, on peut utiliser instances. C'est une propriété comme seq ou meta sur les types et elle permet de définir des expressions dans le langage d'expression de Kaitai pour faire un peu ce qu'on veut. On va définir quatre instances, pour les quatre valeurs qu'on recherche.

On peut commencer par définir l'instance type, qui sera cette fois le véritable type :

instances:
  type:
    enum: geometry_type
    value: 'raw_type & 0x1fffffff'

On indique que notre instance correspond à l'énumération geometry_type, ce qui nous permet de conserver les jolis noms de géométries qu'on a interprétés depuis les identifiants originaux. value est l'expression utilisée pour obtenir la valeur de notre type. Pour extraire une valeur du champ, on va devoir « supprimer » les autres valeurs. Pour le type, on peut mettre les trois bits de poids les plus forts à zéro, ce qui supprimera les trois booléens et laissera le type seul dans une valeur de type u4.

Pour garder des bits en permanence à zéro, on peut utiliser l'opérateur &, qui effectue une opération ET bit par bit sur le nombre. La valeur 0x1fffffff correspond en fait à 4 octets où tous les bits sont à 1 sauf les trois premiers ; l'opération ET va ainsi toujours renvoyer zéro pour les trois premiers bits, et renvoyer la valeur du bit qui se trouve dans raw_type pour tous les autres. On récupère alors immédiatement le type seul.

Pour les trois booléens, on va utiliser le même principe, avec un petit détail en plus :

has_z:
  value: 'raw_type & 0x80000000 != 0'
has_m:
  value: 'raw_type & 0x40000000 != 0'
has_srid:
  value: 'raw_type & 0x20000000 != 0'

Les masques, c'est-à-dire les valeurs hexadécimales qu'on utilise dans l'opérateur &, sont ici définis pour n'avoir rien d'autre que le seul bit qui nous intéresse à chaque fois. On a donc une valeur qui a véritablement deux possibilités : soit tous les bits sont à 0, soit un des bits est à 1. Le plus rapide donc est de juste voir si ce n'est pas zéro ; si oui, on renvoie true. Cette comparaison avec != fait que Kaitai Struct va correctement interpréter que l'instance est un booléen et pas un étrange nombre à 4 octets, ce qui simplifiera ensuite notre utilisation de ces attributs.

Si on avait eu des nombres qui auraient été de véritables nombres, on aurait dû utiliser en plus du reste les opérateurs de décalage de bits, ou bit-shifting en anglais, pour décaler les bits vers la droite et les placer tout à droite des 4 octets pour qu'ils deviennent lisibles.

Un gros avantage de cette lecture avec des opérateurs bit-à-bit par rapport à la méthode plus récemment introduite de champs de type bit est que nous n'avons pas du tout à nous préoccuper du boutisme. Toute la gestion du boutisme qu'on a fait au tout début de notre travail sur le WKB s'applique et suffit à nous donner un nombre qu'on peut toujours traiter en gros-boutisme sans se poser de questions.

Modification des en-têtes

Nous faisons de la métamétaprogrammation depuis le début de cette série, mais j'ai utilisé directement le YAML original de Kaitai Struct pour expliquer le traitement des bits. On peut maintenant passer à la pratique et mettre en œuvre cette technique dans le script jq.

Nous avons une méthode wkb_seq qui prend une référence de type de Kaitai et construit une séquence de champs comprenant le nombre entier pour geometry_type et un champ appelé data avec le type spécifié. Puisqu'elle ne génère qu'une séquence et pas des types entiers, on ne peut pas y ajouter directement les instances, donc il va falloir la modifier pour qu'elle renvoie des structures complètes de types avec une séquence et des instances. On va par conséquent renommer cette fonction en wkb_wrap, pour indiquer qu'elle entoure un type avec les attributs d'en-tête de WKB.

def wkb_wrap: {
  seq: [
    {
      id: "raw_type",
      type: "u4"
    },
    {
      id: "data",
      type: .
    }
  ],
  instances: {
    type: {
      value: "raw_type & 0x1fffffff",
      enum: "geometry_type"
    },
    has_z: {
      value: "(raw_type & 0x80000000) >> 31 == 1"
    },
    has_m: {
      value: "(raw_type & 0x40000000) >> 30 == 1"
    },
    has_srid: {
      value: "(raw_type & 0x20000000) >> 29 == 1"
    }
  }
};

Il nous faut du coup mettre à jour toutes les anciennes utilisations de wkb_seq. Cela concerne le grand type geometry, ainsi que wkb_point, wkb_line_string et wkb_polygon :

types: {
  geometry: (
    ({
      "switch-on": "type",
      cases: {
        # ...
      }
    }
    | wkb_wrap
    + {
      types: {
        # ...
        wkb_point: ("point" | wkb_wrap | endianize),
        wkb_line_string: ("line_string" | wkb_wrap | endianize),
        wkb_polygon: ("polygon" | wkb_wrap | endianize)
      }
    }) | endianize
  )
}

Pour les types wkb_*, le fonctionnement est plutôt simple ; on envoie directement le type entièrement construit à endianize. Pour geometry, on utilise l'addition d'objets pour ajouter au type construit par wkb_wrap une propriété types: qui contient les sous-types qu'on voulait ajouter, avant d'envoyer toute cette structure à endianize.

Notez que le switch-on: type n'a pas changé sur la géométrie, car nous avons pris soin de garder le nom type pour le type lisible de la géométrie. C'est une instance, ou autrement dit une propriété calculée, mais on y accède exactement de la même manière que pour un champ classique.

Propriétés optionnelles

Il nous faut maintenant faire quelques modifications pour rajouter des propriétés optionnelles : juste après le type de géométrie, nous devons pouvoir lire le SRID s'il est utilisé, mais sinon ne pas lire d'octet du tout et s'attendre au début immédiat des données de la géométrie. Et si nous essayons de lire un point dans n'importe quel contexte, il nous faut y inclure un ou deux nombres à virgule flottante supplémentaire selon l'état de has_z et has_m.

Il y a une façon assez simple de faire cela dans Kaitai, et si vous avez osé essayer de comprendre ce que j'ai fait avec le WKB ISO, ça va vous faire vous poser des questions. Je reviendrai sur ça plus tard. On va utiliser if, qui permet d'exclure complètement un champ si une condition n'est pas remplie. Dans la plupart des langages, le champ aura du coup une valeur nulle, et on ne lira aucun octet.

Pour la lecture du SRID, vu qu'il succède immédiatement au type et qu'il n'est pas spécifique à une géométrie en particulier, on peut l'ajouter dans wkb_wrap, pour le faire succéder immédiatement au type de géométrie si has_srid est actif :

{
  id: "raw_type",
  type: "u4"
},
{
  id: "srid",
  type: "u4",
  if: "has_srid"
},
{
  id: "data",
  type: .
}

Il n'y a rien de plus à faire de particulier, car has_srid est défini dans le contexte de ce type et est déjà un booléen. Avec ça, le SRID est immédiatement lu en tant que nombre entier non-signé de 4 octets et est accessible partout.

La deuxième modification à faire est d'ajouter les coordonnées Z et M aux points lorsque c'est pertinent. Puisque l'EWKB ne définit pas de géométries plus complexes, il n'y aura aucun autre endroit où on aura besoin de rajouter ces champs. La difficulté ici est d'accéder aux instances qu'on a défini dans le champ parent. Kaitai Struct nous fournit pour cela une variable spéciale nommée _parent.

On pourrait essayer d'utiliser _parent.has_z ou _parent.has_m :

- id: z
  type: f8
  if: _parent.has_z
- id: m
  type: f8
  if: _parent.has_m

Mais on se retrouve alors avec une erreur assez étrange à comprendre :

io.kaitai.struct.precompile.ErrorInInput: ewkb: /types/geometry/types/body/types/point/seq/2/if: don't know how to call method 'has_z' of object type 'CalcKaitaiStructType'

Le point est utilisé comme enfant de plusieurs parents différents. On l'utilise directement dans le corps de geometry, mais aussi dans wkb_point et line_string ; du coup, Kaitai ne peut plus vérifier si le parent contient bien une propriété has_z. Le parent est seulement reconnu comme étant de type « type de Kaitai Struct », ce qui ne va pas nous être très utile.

On va pour l'instant ignorer wkb_point et line_string et se concentrer sur le cas le plus simple, quand on veut traiter un simple point seul. On peut alors utiliser _parent.as<body>.has_z : .as<> permet de faire un transtypage, ou cast, en le type body, qui est le type créé par endianize pour contenir les champs qu'on a construit dans wkb_wrap. Kaitai ne se plaint alors plus du tout et nous laisse accéder à has_z et has_m sans problèmes. On peut donc mettre à jour le point dans le script :

point: {
  seq: [
    {
      id: "x",
      type: "f8"
    },
    {
      id: "y",
      type: "f8"
    },
    {
      id: "z",
      type: "f8",
      if: "_parent.as<body>.has_z"
    },
    {
      id: "m",
      type: "f8",
      if: "_parent.as<body>.has_m"
    }
  ]
}

On a échangé nos mamans

Je ne m'attendais pas à un jour faire référence à une émission télévisée de ce genre dans un article.

Si on essaie de lire n'importe quel type de géométrie qui ne soit pas directement un point, on va immédiatement se heurter à des erreurs à l'exécution que le compilateur de Kaitai Struct a laissé passer. Le transtypage est vraiment le moyen de dire au compilateur « fais-moi confiance », donc on arrive inévitablement à ce genre de problèmes si on ne fait pas attention.

Le problème réside dans le fait que le _parent du point n'est pas toujours le body. point peut être contenu dans un line_string, auquel cas on trouvera has_z dans _parent._parent, ou encore dans un polygon, où on aurait alors _parent._parent._parent. Pour pouvoir gérer cette situation de façon propre, Kaitai nous permet de faire croire à un champ qu'il a un parent différent. Voici un exemple simplifié :

seq:
  - id: point
    type: point
    parent: _parent

Dans ce type imaginaire qui contient un point, on utilise la propriété parent pour indiquer quel est le parent du point. On peut mettre n'importe quelle instance valide là-dedans et ça sera utilisé en tant que variable _parent dans le type point. Ici, on utilise _parent comme parent. Ça ne fait pas rien : le parent de notre type devient le parent du type enfant, ce qui rend ce type imaginaire complètement invisible au point. Le point n'a plus aucune idée qu'il se trouve dans un line_string ou un polygon ; tout ce qu'il voit, c'est la géométrie entière, et il a donc exactement le même contexte partout.

On n'a que deux types à modifier pour gérer ce cas, line_string et polygon. Quand on utilise un ensemble de points ou une collection de géométries, le point est précédé par un en-tête rien qu'à lui et a du coup aussi pour parent un body classique.

geometry: {
  # ...
  types: {
    # ...
    line_string: ({ id: "points", type: "point", parent: "_parent" } | counted),
    polygon: ({ id: "rings", type: "line_string", parent: "_parent" } | counted)
  }
}

Remarques

Notez qu'on aurait aussi pu utiliser _root.geometry.body.has_z pour toujours accéder aux attributs de la géométrie racine pour notre point, peu importe où on se trouve, mais on ne peut pas garantir que l'en-tête de géométrie ne soit pas différent.

Les implémentations des EWKB interdisent des dimensions incohérentes, par exemple une collection de géométries qui contient à la fois des points 2D et 3D, ou des géométries avec des SRID différents. Cependant, d'un point de vue purement technique, rien n'empêche à ce genre d'incohérences d'apparaître dans les données binaires. Par conséquent, il est préférable de laisser chaque point accéder à l'en-tête qui le correspond vraiment.

On peut citer trois raisons pour lesquelles on peut choisir d'accepter ces incohérences interdites par la spécification :

  1. Ça fait moins de travail !
  2. On peut déléguer ces vérifications au programme qui utilise notre lecteur de géométries, qui pourrait peut-être choisir justement de nettoyer les géométries au lieu de les rejeter complètement pour réparer des données corrompues, ou qui pourrait au moins afficher un message d'erreur assez clair.
  3. La validation des valeurs des champs dans Kaitai Struct a une issue GitHub, et elle n'est pour l'instant pas implémentée, donc on ne peut pas la faire de façon très explicite.

J'ai mentionné plus haut que le if pourrait conduire à se poser des questions sur ma réalisation du WKB ISO, où j'ai plutôt utilisé le if des scripts jq pour générer des types distincts pour chaque combinaison de dimensions que la spécification autorisait. Peut-être que j'aurais pu en fait factoriser tous ces types et laisser Kaitai Struct traiter certains champs comme optionnels.

J'avais en fait essayé de faire ça au départ, mais le nombre de géométries et leur complexité a fait que je me suis retrouvé bloqué à plusieurs reprises, parfois parce que Kaitai ne pouvait pas gérer un champ optionnel à certains endroits. C'est la même limitation qui m'a empêché de gérer les géométries vides. J'ai donc laissé tomber et j'ai utilisé à la place un nombre incalculable de champs et beaucoup trop de fonctions jq. J'avais aussi expérimenté avec les types paramétriques sans grand succès ; il m'aurait fallu des types vraiment génériques, comme les classes génériques qu'on retrouve dans un bon nombre de langages, pour pouvoir faire quelque chose d'intéressant.

Script final

Script jq générant un schéma Kaitai Struct pour l'Extended Well-Known Binary

#!/usr/bin/jq
# Generates a Kaitai Struct-compatible JSON structure to parse
# the Extended Well-Known Binary.
# Usage: oq -n -o simpleyaml -f wkb.jq  wkb.ksy

def endianize: {
  seq: [
    {
      id: "endianness",
      type: "u1"
    },
    {
      id: "body",
      type: "body"
    }
  ],
  types: {
    "body": (. + {
      meta: {
        endian: {
          "switch-on": "_parent.endianness",
          cases: {
            "0": "be",
            "1": "le"
          }
        }
      }
    })
  }
};

def counted: {
  seq: [
    {
      id: "count",
      type: "u4"
    },
    . + {
      repeat: "expr",
      "repeat-expr": "count"
    }
  ]
};

def wkb_wrap: {
  seq: [
    {
      id: "raw_type",
      type: "u4"
    },
    {
      id: "srid",
      type: "u4",
      if: "has_srid"
    },
    {
      id: "data",
      type: .
    }
  ],
  instances: {
    type: {
      value: "raw_type & 0x1fffffff",
      enum: "geometry_type"
    },
    has_z: {
      value: "(raw_type & 0x80000000) >> 31 == 1"
    },
    has_m: {
      value: "(raw_type & 0x40000000) >> 30 == 1"
    },
    has_srid: {
      value: "(raw_type & 0x20000000) >> 29 == 1"
    }
  }
};

{
  meta: {
    id: "ewkb",
    title: "Extended Well-Known Binary",
    application: "PostGIS",
    license: "AGPL-3.0-or-later"
  },
  "doc-ref": [
    "https://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT",
    "http://libgeos.org/specifications/wkb/"
  ],
  seq: [
    {
      id: "geometry",
      type: "geometry"
    }
  ],
  enums: {
    geometry_type: {
      "0": "geometry",
      "1": "point",
      "2": "line_string",
      "3": "polygon",
      "4": "multi_point",
      "5": "multi_line_string",
      "6": "multi_polygon",
      "7": "geometry_collection"
    }
  },
  types: {
    geometry: (
      ({
        "switch-on": "type",
        cases: {
          "geometry_type::point": "point",
          "geometry_type::line_string": "line_string",
          "geometry_type::polygon": "polygon",
          "geometry_type::multi_point": "multi_point",
          "geometry_type::multi_line_string": "multi_line_string",
          "geometry_type::multi_polygon": "multi_polygon",
          "geometry_type::geometry_collection": "geometry_collection"
        }
      }
      | wkb_wrap
      + {
        types: {
          point: {
            seq: [
              {
                id: "x",
                type: "f8"
              },
              {
                id: "y",
                type: "f8"
              },
              {
                id: "z",
                type: "f8",
                if: "_parent.as.has_z"
              },
              {
                id: "m",
                type: "f8",
                if: "_parent.as.has_m"
              }
            ]
          },
          line_string: ({ id: "points", type: "point", parent: "_parent" } | counted),
          polygon: ({ id: "rings", type: "line_string", parent: "_parent" } | counted),
          wkb_point: ("point" | wkb_wrap | endianize),
          wkb_line_string: ("line_string" | wkb_wrap | endianize),
          wkb_polygon: ("polygon" | wkb_wrap | endianize),
          multi_point: ({ id: "points", type: "wkb_point" } | counted),
          multi_line_string: ({ id: "line_strings", type: "wkb_line_string" } | counted),
          multi_polygon: ({ id: "polygons", type: "wkb_polygon" } | counted),
          geometry_collection: ({ id: "geometries", type: "geometry" } | counted)
        }
      }) | endianize
    )
  }
}

Conclusion

On arrive enfin au bout du Well-Known Binary tout entier. Comme d'habitude, ces recherches se sont étalées sur bien plus de temps que prévu mais elles m'ont permis d'apprendre pas mal de choses sur des sujets sur lesquels j'étais curieux.

J'envisage toujours de pouvoir peut-être plus tard utiliser ces schémas Kaitai Struct pour développer des applications particulières qui concerneraient mes explorations d'OpenStreetMap et des réseaux Wi-Fi, mais vu que le projet n'en est qu'à l'état de simple idée, et vu la file d'attente d'articles que je veux écrire ou de projets à terminer, on ne devrait pas revoir le WKB de si tôt. Je prendrai peut-être le temps plus tard de revenir cela dit sur le WKT, pour y traiter à nouveau les variantes ISO et EWKB dans mon bon vieux script m4.

Cette série d'articles était initialement juste un projet un peu hasard, que je savais comme utilisant beaucoup Kaitai Struct, et que j'ai voulu traiter rapidement pour faire une « simple » introduction à ce système de lecture de fichiers binaires et à l'utilisation que j'en fais. Ça n'a bien entendu pas du tout été simple, mais pour pouvoir vraiment parler de ce que j'ai commencé il y a déjà plusieurs mois et qui m'intéresse bien plus, il va encore me falloir introduire d'autres choses. Préparez-vous donc à une nouvelle petite série d'articles, cette fois avec beaucoup moins de code et beaucoup plus de prose, tout ça seulement pour introduire encore une autre série…


Commentaires

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