Journal Code natif et Node.js - parser et préprocesseur XML

Posté par  (site web personnel) . Licence CC By‑SA.
10
1
sept.
2017

Histoire de diversifier mon activité, et aussi parce que j'aime bien me lancer des défis, j'ai décidé de me mettre à Node.js. Cependant, compte tenu de ma faible appétence pour Javascript, lui préférant de loin C++, j'ai bien entendu cherché un moyen de coder pour Node.js avec mon langage fétiche. Et c'est tout à fait possible, grâce aux addons. Bon, ce n'était pas vraiment une surprise, n'ayant jusqu'à présent jamais rencontré d'environnement d'exécution logiciel qui ne puisse s'interfacer, à minima, avec du C.

Me voici donc en train de coder mon premier addon pour Node.js, et de découvrir en passant les joies de la programmation asynchrone. Ce faisant, je me suis, comme à mon habitude, crée quelques bibliothèques pour me faciliter le développement des prochains addons, et j'avais acquis suffisamment d'aisance dans le domaine pour proposer avec sérénité cette nouvelle compétence à mes clients .

Néanmoins, une chose me chagrinait. Par rapport à mon environnement de développement C++ usuel, celui nécessité pour le développement d'un addon faisait un peu usine à gaz. Entre les fichiers package.json et binding.gyp, la commande node(-pre)-gyp pour générer les fichiers automatisant la génération de l'addon proprement dit, les dépendances aux différentes bibliothèques (node, v8, libuv…), je ne pouvais plus utiliser les outils que j'avais amoureusement développés pour me faciliter la gestion mes projets C++.

J'ai donc décidé de développer un addon universel qui aurait pour rôle d'officier comme wrapper pour des bibliothèques dynamiques qui contiendraient le véritable code des mes différents addons. Ces bibliothèques seront totalement indépendants de Node.js, le wrapper, commun à toutes ces bibliothèques, se chargeant de faire le lien entre ces bibliothèques et Node.js.

Pour me faire la main, j'ai développé un addon Node.js proposant un parser et un préprocesseur XML. Je l'ai placé sur npm, où vous le trouverez à l'adresse http://www.npmjs.com/package/xppq. Comme tout paquet présent sur npm, vous pouvez l'installer en lançant npm install xppq. N'hésitez pas, tout ce petit monde est publié sous licence libre, et il y en a pour tout les goûts : GNU/Linux et autres systèmes POSIX, dont macOS, ainsi que Windows, que ce soit sur plateforme x86 ou ARM. Oui, ça peut même être installé sur Raspberry Pi et consorts…

À noter que, excepté pour Windows, l'installation de cet addon nécessite un environnement de développement C++. Ben oui, c'est quand même du natif !

Une fois installé, vous pouvez le tester en lançant npm explore xppq -- node test.js. Vous obtiendrez le résultat du parsage (?) d'un fichier XML de démonstration après passage par le préprocesseur, à savoir :

Start tag: 'SomeTag'
 Attribute: 'AnAttribute' = 'SomeAttributeValue'
 Start tag: 'SomeOtherTag'
  Attribute: 'AnotherAttribute' = 'AnotherAttributeValue'
  Value:     'TagValue'
 End tag:   'SomeOtherTag'
 Start tag: 'YetAnotherTag'
  Attribute: 'YetAnotherAttribute' = 'YetAnotherAttributeValue'
  Value:     'Some macro content !'
 End tag:   'YetAnotherTag'
End tag:   'SomeTag'

En lançant npm explore xppq -- node test.js 0 (notez le 0 à la fin), le contenu du fichier de démonstration s'affichera, à savoir :

<?xml version="1.0" encoding="UTF-8"?>
<SomeTag xmlns:xpp="http://q37.info/ns/xpp/" AnAttribute="SomeAttributeValue">
 <SomeOtherTag AnotherAttribute="AnotherAttributeValue">TagValue</SomeOtherTag>
 <xpp:define name="SomeMacro">
  <xpp:bloc>Some macro content !</xpp:bloc>
 </xpp:define>
 <YetAnotherTag YetAnotherAttribute="YetAnotherAttributeValue">
  <xpp:expand select="SomeMacro"/>
 </YetAnotherTag>
</SomeTag>

En remplaçant le 0 par un chiffre entre 1 et 4, différents tests seront lancés, comme le piping du préprocesseur, ou l'application d'un callback sur la sortie du préprocesseur, le préprocesseur étant en fait implémenté sous forme de stream Node.js.

Voici encore un aperçu du contenu du fichier XPPq.js, qui charge le wrapper, lui-même chargeant la bibliothèques dynamique contenant le parser et le préprocesseur, avec l’encapsulation permettant d'y accéder à partir de Javascript :

"use strict"

var affix = "xppq";

var njsq = null;
var componentPath = null;
var componentFilename = null;
var path = require("path");

njsq = require('njsq');
componentPath = __dirname;

componentFilename = path.join(componentPath, affix + "njs").replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/ /g, "\\ ");
njsq.register(componentFilename);
module.exports = njsq;

module.exports.returnArgument = (text) => { return njsq._wrapper( 0, text ) };

const stream = require('stream');

class Stream extends stream.Readable {
    constructor(stream, options) {
        super(options);
        stream.on('readable', () => { var chunk = stream.read(); if (chunk == null) njsq._wrapper(5, stream); else njsq._wrapper(4, stream, chunk); });
        njsq._wrapper( 7, stream, this );
    }
    _read(size) {
        njsq._wrapper( 6, this );
    }
}


// If modified, modify also 'parser.cpp'.
var tokens = {
    ERROR: 0,
    START_TAG: 1,
    ATTRIBUTE: 2,
    VALUE: 3,
    END_TAG: 4
};

module.exports = njsq;
module.exports.Stream = Stream;
module.exports.parse = (stream, callback) => { stream.on('readable', () => { var chunk = stream.read(); if ( chunk == null ) njsq._wrapper( 2, stream ); else njsq._wrapper(1, stream, chunk); } ); njsq._wrapper(3, stream, callback) };
module.exports.tokens = tokens;

Et encore le fichier test.js, qui montre des exemples de mise en œuvre du parser et du préprocesseur :

"use strict"

const fs = require('fs');
const stream = require('stream');
const xppq = require('./XPPq.js');
var indentLevel = 0;

function write(text) {
    process.stdout.write(text);
}

function indent(level) {
    while (level--)
        write(' ');
}

function callback(token, tag, attribute, value) {
    switch (token) {
        case xppq.tokens.ERROR:
            write(">>> ERROR:  '" + value + "'\n");
            break;
        case xppq.tokens.START_TAG:
            indent(indentLevel);
            write("Start tag: '" + tag + "'\n");
            indentLevel++;
            break;
        case xppq.tokens.ATTRIBUTE:
            indent(indentLevel);
            write("Attribute: '" + attribute + "' = '" + value + "'\n");
            break;
        case xppq.tokens.VALUE:
            indent(indentLevel);
            write("Value:     '" + value.trim() + "'\n");
            break;
        case xppq.tokens.END_TAG:
            indentLevel--;
            indent(indentLevel);
            write("End tag:   '" + tag + "'\n");
            break;
        default:
            throw ("Unknown token !!!");
            break;
    }
}

const file = __dirname + '/demo.xml';
var test = 4;   // Default test id.
var arg = process.argv[2];

if (arg != undefined)
    test = Number(arg);

console.log( xppq.componentInfo() );
console.log( xppq.wrapperInfo());
console.log( xppq.returnArgument('Basic test : this text comes from the addon (native code), and is written from Javascript.' ) );
console.log( '     ---------------' );

switch (test) {
    case 0:
        console.log("No treatment ; to see the original file.\n");
        fs.createReadStream(file).pipe(process.stdout);
        break;
    case 1:
        console.log("Piping the preprocessing stream.\n");
        new xppq.Stream(fs.createReadStream(file)).on('error', (err) => console.error('\n>>> ERROR : ' + err + '\n')).pipe(process.stdout);
        break;
    case 2:
        console.log("Using the preprocessing stream with a callback, which transforms to lower case.\n");
        new xppq.Stream(fs.createReadStream(file)).on('data', (chunk) => write(chunk.toString().toLowerCase())).on('error', (err) => console.error('\n>>> ERROR : ' + err + '\n'));
        break;
    case 3:
        console.log("XML parsing WITHOUT preprocessing.\n");
        xppq.parse(fs.createReadStream(file), callback);
        break;
    case 4:
        console.log("XML parsing WITH preprocessing.\n");
        xppq.parse(new xppq.Stream(fs.createReadStream(file)).on('error', (err) => console.error('>>> ERROR : ' + err)), callback);
        break;
    default:
        console.error("'" + arg + "' is not a valid test id ; must be '0' to '4'.");
        break;
}

Compte tenu de la nature Libre de Node.js, ainsi que du paquet npm présenté ici, celui-ci intéressera peut-être quelques développeurs Node.js de passage. Par ailleurs, je suis preneur de tous commentaires concernant ce paquet, notamment touchant à sa conformité aux règles de l'art (à part le fait qu'il soit codé en C++ au lieu de Javascript, évidemment).

Pour terminer, le traditionnel badge npm du paquet, donnant accès à toutes les informations le concernant :

NPM

  • # Si je peux me permettre...

    Posté par  . Évalué à 7.

    D'abord, merci de contribuer du code, c'est sympa. Pour le feedback :

    1) Il y a genre aucun commentaires dans le code, dur pour qui que ce soit de t'aider si ils ne peuvent pas comprendre ce que le code fait
    2) Les noms de variables/methodes/etc… sont imbitables pour le commun des mortels (aucune idée de ce que lcl, qCDTOR, etc… sont)
    3) Exposer un parser écrit en C++ à des documents venant de sources inconnues ( = internet) est super dangereux, est-ce que tu as fais des passes de fuzzing sur ce parser au minimum ?

    • [^] # Mais je t'en prie...

      Posté par  (site web personnel) . Évalué à 1.

      D'abord, merci de contribuer du code, c'est sympa. 
      

      Merci de t'y intéresser :-).

      Pour le feedback :
      1) Il y a genre aucun commentaires dans le code, dur pour qui que ce soit de t'aider si ils ne peuvent pas comprendre ce que le code fait
      

      En fait, je ne cherche pas de contributeurs, car cela poserait des problèmes de licence. Le code est, par défaut, disponible sous AGPL, mais c'est sous une autre licence, moins contraignante, qu'il est disponible pour mes clients. Je peux publier ce code sous n'importe quelle licence, car j'en suis l'unique auteur. Mais s'il contient des contributions de tiers, cela sera beaucoup plus compliqué…

      En fait, bien plus que des contributeurs, c'est des utilisateurs que je recherche, qui puisse me remonter les éventuels problèmes. C'est d'ailleurs pour pour cela que j'ai migré mon code sur github, qui est l'une, voire la plus populaire des forges logiciels, pour que les utilisateurs disposent d'outils qui leurs sont familiers.

      Ceci dit, je pense que ce qui manque à mon code, c'est bien plus une documentation expliquant les principes généraux que des commentaires. Mais, comme c'est moi qui ai écrit le code et que je l'utilise tous les jours, je ne suis probablement pas objectif…

      2) Les noms de variables/methodes/etc… sont imbitables pour le commun des mortels (aucune idée de ce que lcl, qCDTOR, etc… sont)
      

      En effet, d'où ce que j'écrivais ci-dessus, de la nécessité d'avoir une documentation explicative. J'envisage de publier une série d'utilitaires mettant chacun en avant une fonctionnalité bien précise de mes bibliothèques, et d'en écrire la documentation correspondante à ce moment-là. Mais, compte tenu du travail que cela représente, il faudrait qu'il y ai une réelle demande à ce niveau-là, ce dont je ne suis pas convaincu, pour me lancer.

      3) Exposer un parser écrit en C++ à des documents venant de sources inconnues ( = internet) est super dangereux, est-ce que tu as fais des passes de fuzzing sur ce parser au minimum ?
      

      Bon, je vais me faire huer, mais la vérité est que j'écris très peu de tests en tant que tels. Ce n'est pas que j'y sois opposé, et je conseille fortement d'en écrire, mais le fait est, et ça fait un paquet d'années que je développe, que j'ai très bien pu m'en passer jusqu'à présent. Ceci dit, je factorise mon code très en amont du développement, ce qui explique peut-être cela. Le préprocesseur, et le parser que lequel il s'appuie, qui font l'objet de ce journal, sont récents en tant que addon Node.js, mais le code C++ qu'il y a derrière existe depuis probablement presque dix ans, et est mis en œuvre dans tous les développements que j'ai réalisés depuis. C'est donc du code qui est utilisé très intensivement. Certes, ça ne garantit rien en soi, mais, plus un code a d'utilisateurs, plus rapidement les problèmes sont détectés (et, à priori, corrigés), et c'est pour cela que je cherche à accroître le nombre de ses utilisateurs…

      Pour nous émanciper des géants du numérique : Zelbinium !

      • [^] # Re: Mais je t'en prie...

        Posté par  . Évalué à 9.

        Bon, je vais me faire huer, mais la vérité est que j'écris très peu de tests en tant que tels. Ce n'est pas que j'y sois opposé, et je conseille fortement d'en écrire, mais le fait est, et ça fait un paquet d'années que je développe, que j'ai très bien pu m'en passer jusqu'à présent. Ceci dit, je factorise mon code très en amont du développement, ce qui explique peut-être cela. Le préprocesseur, et le parser que lequel il s'appuie, qui font l'objet de ce journal, sont récents en tant que addon Node.js, mais le code C++ qu'il y a derrière existe depuis probablement presque dix ans, et est mis en œuvre dans tous les développements que j'ai réalisés depuis. C'est donc du code qui est utilisé très intensivement. Certes, ça ne garantit rien en soi, mais, plus un code a d'utilisateurs, plus rapidement les problèmes sont détectés (et, à priori, corrigés), et c'est pour cela que je cherche à accroître le nombre de ses utilisateurs…

        Alors oui tu vas te faire totalement huer car quand il s'agit de sécurité, ce que tu décris ne garantis absolument, mais vraiment absolument, rien, et en tant que personne qui a passé les 15 dernières années dans la sécurité, je peux t'assurer que ta logique est complètement fausse et à l'encontre de la réalité.

        Windows, ou Linux, ou Office, ou Chrome, … cela fait des annéees qu'énormémenent de gens les utilisent, il y a des tonnes et des tonnes de tests, et des failles sont encore trouvées régulièrement. Ce que tu fais là, c'est du kamikaze et cela met en grand danger tes utilisateurs.

        Vraiment, il faut soit que tu fasses un véritable effort pour tester ton code, soit que tu dises clairement à tes utilisateurs de ne pas exposer ton projet à des fichiers venant d'internet. Faire autre chose est honnètement irresponsable.

        • [^] # Re: Mais je t'en prie...

          Posté par  (site web personnel) . Évalué à 3.

          Comme indiqué plus haut, je factorise mon code à outrance, ce qui fait qu'une grosse partie est commune à toutes les applications que je développe. Lors du développement de ce code, dont le cœur l'a été il y a plus de dix ans, j'avais à l'esprit les problématiques de sécurité, et j'ai fait mon maximum pour éviter les failles. Ce qui, je le concède volontiers, ne garantit en rien qu'il n'y en ai pas. Toujours est-il que ce code est déployé chez de nombreux clients, dans certains cas dans le cadre d'applications client/serveur accessibles au grand public et avec une fréquentation élevée.

          Jusqu'à présent, aucun client ne m'a remonté de problèmes de sécurité. Cela peut arriver demain, auquel cas, après correction de la faille, je développerais une batterie de tests pour tester la correction, et pour tenter de détecter d'autres failles similaires. Mais, à ce jour, je ne vais perdre de temps à développer des tests pour tenter de déceler des failles dont on n'a jamais détecté la moindre trace…

          Quand au code que je mets à disposition sous licence Libre, comme celui faisant l'objet de ce journal, j'attire l'attention sur la section 15 et la section 16 du fichier LICENCE fournit avec les sources. Partant de là, libre à l'utilisateur de, au choix,

          • auditer mon code (tâche ardue, mais pas impossible ; c'est de l'Open Source après tout),
          • le soumettre à une batterie de tests,
          • me demander de développer une batterie de tests.

          Pour la dernière option, cela sera bien entendu réalisé contre rémunération, sauf s'il parvient à me fournir un test case mettant en évidence une faille qui justifie le développement de cette batterie de tests.

          Encore une fois, même si moi-même je ne le fais pas, je conseille fortement tout un chacun de soumettre son code à un maximum de tests. Mais, concernant mon code, avec l'historique qu'il a, le gain que m'apporterait ces tests par rapport au temps que je passerais à les développer n'est pas rentable, à aucun point de vue, en particulier, mais pas seulement, du point de vue financier. D'autant plus que, et ton commentaire va également dans ce sens, aucune batterie de test ne peut garantir l'absence de failles.

          Pour résumer, compte tenu de l'historique de mon code, de la manière dont il se comporte chez moi (j'utilise quotidiennement plusieurs logiciels que j'ai développés et qui contiennent donc ce code) et chez mes clients, je considère qu'il n'est pas plus vulnérable que la majorité des codes réalisés avec un minimum de sérieux et, de ce fait, probablement soumis à une batterie de tests. Mais je suis tout à fait disposé à réviser mon jugement, pour peu que l'on me soumette un test case prouvant mon erreur, et non pas seulement des considérations théoriques.

          Quoi qu'il en soit, je te remercie pour tes remarques qui m'ont données l'occasion de m'expliquer davantage sur le sujet.

          Pour nous émanciper des géants du numérique : Zelbinium !

      • [^] # Re: Mais je t'en prie...

        Posté par  (site web personnel) . Évalué à 6.

        En fait, je ne cherche pas de contributeurs, car cela poserait des problèmes de licence.

        Les commentaires de code, je les écris avant tout pour la personne que je serai dans 6 mois lorsque je devrai repasser dans le code.
        Sur des projets de plusieurs dizaines/centaines de milliers de lignes, il faut une sacrée dose de confiance en soi pour s'en passer.
        De plus, en ne prenant pas soin de rendre ton code intelligible, tu envoies le signal : "après moi, le déluge".

  • # Syntaxe bizarre

    Posté par  . Évalué à 6.

    Je suis tombe sur ce code dans ton fichier src/parser.cpp :

    void parser::OnData( sclnjs::sCaller &Caller )
    {
    qRH
        sclnjs::rRStream This;
        sclnjs::rBuffer Chunk;
    qRB
        tol::Init( This, Chunk );
        Caller.GetArgument( This, Chunk );
    
        rRack_ &Rack = *(rRack_ *)This.Get( "_rack" );
    
        Rack.OFlow << Chunk;
    qRR
    qRT
    qRE
    }

    Est ce que tu peux expliquer ce que sont qRH, qRB, qRR, qRT, qRE et d'ou ils viennent ?
    (Je suis au boulot et je peux pas charger ca dans un IDE pour tester :D )

    • [^] # Re: Syntaxe bizarre

      Posté par  (site web personnel) . Évalué à 1.

      Je suis tombe sur ce code dans ton fichier src/parser.cpp :
      
      (...)
      
      Est ce que tu peux expliquer ce que sont qRH, qRB, qRR, qRT, qRE et d'ou ils viennent ?
      (Je suis au boulot et je peux pas charger ca dans un IDE pour tester :D )
      

      Ce sont des macros qui datent de l'époque où je codais en C, lorsque j'avais implémenté un équivalent des exceptions, qui n'existaient pas alors, à l'aide de la bibliothèque setjmp.

      Grosso modo, qRB correspond à try, qRR à catch, et qRT à l'accolade fermant du bloc du catch. Les autres macros servent à gérer les variables utilisées pour la gestion des erreurs.

      Elles sont définis ici et les version d'origines, basées sur setjmp.h, sont visibles .

      D'un point de vue algorithmique, comme elles sont uniquement dédiées à la gestion des erreurs, elles peuvent être ignorées.

      Pour nous émanciper des géants du numérique : Zelbinium !

  • # Essai en ligne.

    Posté par  (site web personnel) . Évalué à 1.

    Pour ceux qui ne connaissent pas (c'était mon cas il n'y pas encore bien longtemps), il est possible d'essayer le paquet en ligne, grâce à RunKit.

    À titre d'exemple, copiez le code ci-dessous dans le champ de saisi, puis cliquez sur le bouton vert (run) situé en-bas à droite de ce même champ :

    var xppq = require("xppq");
    var str = require("string-to-stream");
    
    var xml = '<?xml version="1.0" encoding="UTF-8"?> <SomeTag xmlns:xpp="http://q37.info/ns/xpp/" AnAttribute="SomeAttributeValue">  <SomeOtherTag AnotherAttribute="AnotherAttributeValue">TagValue</SomeOtherTag>  <xpp:define name="SomeMacro">   <xpp:bloc>Some macro content !</xpp:bloc>  </xpp:define>  <YetAnotherTag YetAnotherAttribute="YetAnotherAttributeValue">   <xpp:expand select="SomeMacro"/>  </YetAnotherTag> </SomeTag>';
    
    function write(text) {
        console.log(text);
    }
    
    write( xppq.componentInfo() );
    write( xppq.wrapperInfo() );
    
    function callback(token, tag, attribute, value) {
        switch (token) {
            case xppq.tokens.ERROR:
                write(">>> ERROR:  '" + value + "'\n");
                break;
            case xppq.tokens.START_TAG:
                write("Start tag: '" + tag + "'\n");
                break;
            case xppq.tokens.ATTRIBUTE:
                write("Attribute: '" + attribute + "' = '" + value + "'\n");
                break;
            case xppq.tokens.VALUE:
                write("Value:     '" + value.trim() + "'\n");
                break;
            case xppq.tokens.END_TAG:
                write("End tag:   '" + tag + "'\n");
                break;
            default:
                throw ("Unknown token !!!");
                break;
        }
    }
    
    xppq.parse(new xppq.Stream( str(xml)).on('error', (err) => console.error('>>> ERROR : ' + err)), callback);

    (Pour ceux qui connaissent RunKit, pourquoi process.stdout.write(...) ne fonctionne-t-il pas, et y a-t-il moyen d'avoir quelques chose d'équivalent, mis à part console ?)

    Pour nous émanciper des géants du numérique : Zelbinium !

    • [^] # Re: Essai en ligne.

      Posté par  (site web personnel) . Évalué à 2.

      (Pour ceux qui connaissent RunKit, pourquoi process.stdout.write(…) ne fonctionne-t-il pas, et y a-t-il moyen d'avoir quelques chose d'équivalent, mis à part console ?)

      Expérimente avec des petits exemples pour éclaircir les comportements de type “buffering” et le caractère bloquant ou non des appels. Cf.
      https://nodejs.org/api/process.html#process_a_note_on_process_i_o

      Je suis condamné à programmer un peu avec NodeJS par mon travail et mon retour d'expérience est assez mauvais. Cela concerne en particulier les flux d'entrée (input streams) dont l'interface est déguelasse. Ces flux ont deux modes de fonctionnement appelés “flowing” ou “paused” et l'interface de ces flux a des comportements très différents dépendant et du mode de fonctionnement engagé et de la façon dont a été créé le flux. En un mot, cela n'a aucun sens de regrouper tous ces machins derrière une unique class d'objets.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.