web-dev-qa-db-fra.com

Échappement de chaîne JSON simple pour C ++?

J'ai un programme très simple qui génère une chaîne JSON simple que je concatène manuellement ensemble et que je génère via le flux std :: cout (la sortie est vraiment aussi simple), mais j'ai des chaînes qui pourraient contenir des guillemets doubles, des accolades et d'autres caractères qui pourraient casser la chaîne JSON. J'ai donc besoin d'une bibliothèque (ou d'une fonction plus précise) pour échapper les chaînes selon le standard JSON, le plus léger possible, rien de plus, rien de moins.

J'ai trouvé quelques bibliothèques qui sont utilisées pour encoder des objets entiers en JSON mais ayant à l'esprit que mon programme est un fichier cpp de 900 lignes, je préfère ne pas compter sur une bibliothèque qui est quelques fois plus grande que mon programme juste pour réaliser quelque chose d'aussi simple que ce.

33
ddinchev

Avertissement

Quelle que soit la solution que vous prenez, gardez à l'esprit que la norme JSON requiert que vous échappiez tous les caractères de contrôle. Cela semble être une idée fausse commune. De nombreux développeurs se trompent.

Tous les caractères de contrôle signifie tout, de '\x00' À '\x1f', Pas seulement ceux avec une courte représentation comme '\x0a' (Également appelé '\n'). Par exemple, vous devez échapper le caractère '\x02' Sous la forme \u0002.

Voir aussi: ECMA-404 The JSON Data Interchange Format , Page 10

Solution simple

Si vous savez avec certitude que votre chaîne d'entrée est encodée en UTF-8, vous pouvez garder les choses simples.

Comme JSON vous permet de tout échapper via \uXXXX, Même " Et \, Une solution simple est:

#include <sstream>
#include <iomanip>

std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        if (*c == '"' || *c == '\\' || ('\x00' <= *c && *c <= '\x1f')) {
            o << "\\u"
              << std::hex << std::setw(4) << std::setfill('0') << (int)*c;
        } else {
            o << *c;
        }
    }
    return o.str();
}

Représentation la plus courte

Pour la représentation la plus courte, vous pouvez utiliser des raccourcis JSON, tels que \" Au lieu de \u0022. La fonction suivante produit la représentation JSON la plus courte d'une chaîne codée UTF-8 s:

#include <sstream>
#include <iomanip>

std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        switch (*c) {
        case '"': o << "\\\""; break;
        case '\\': o << "\\\\"; break;
        case '\b': o << "\\b"; break;
        case '\f': o << "\\f"; break;
        case '\n': o << "\\n"; break;
        case '\r': o << "\\r"; break;
        case '\t': o << "\\t"; break;
        default:
            if ('\x00' <= *c && *c <= '\x1f') {
                o << "\\u"
                  << std::hex << std::setw(4) << std::setfill('0') << (int)*c;
            } else {
                o << *c;
            }
        }
    }
    return o.str();
}

Instruction de commutation pure

Il est également possible de s'entendre avec une instruction switch pure, c'est-à-dire sans if et <iomanip>. Bien que cela soit assez lourd, il peut être préférable du point de vue de la "sécurité par simplicité et pureté":

#include <sstream>

std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        switch (*c) {
        case '\x00': o << "\\u0000"; break;
        case '\x01': o << "\\u0001"; break;
        ...
        case '\x0a': o << "\\n"; break;
        ...
        case '\x1f': o << "\\u001f"; break;
        case '\x22': o << "\\\""; break;
        case '\x5c': o << "\\\\"; break;
        default: o << *c;
        }
    }
    return o.str();
}

Utilisation d'une bibliothèque

Vous voudrez peut-être jeter un œil à https://github.com/nlohmann/json , qui est une bibliothèque C++ efficace uniquement en-tête (licence MIT) qui semble être très bien testée.

Vous pouvez soit appeler directement leur méthode escape_string(), soit prendre leur implémentation de escape_string() comme point de départ pour votre propre implémentation:

https://github.com/nlohmann/json/blob/ec7a1d834773f9fee90d8ae908a0c9933c5646fc/src/json.hpp#L4604-L4697

34
vog

Mise à jour : Ne l'utilisez pas! vog fournit une solution beaucoup plus complète (et tout aussi compacte) ci-dessous: https://stackoverflow.com/a/33799784

Voici un début très simple, cependant il ne gère pas les caractères Unicode invalides. Si vous n'en attendez aucun dans votre sortie, n'hésitez pas à utiliser ceci ...

#include <string>
#include <sstream>

std::string escapeJsonString(const std::string& input) {
    std::ostringstream ss;
    for (auto iter = input.cbegin(); iter != input.cend(); iter++) {
    //C++98/03:
    //for (std::string::const_iterator iter = input.begin(); iter != input.end(); iter++) {
        switch (*iter) {
            case '\\': ss << "\\\\"; break;
            case '"': ss << "\\\""; break;
            case '/': ss << "\\/"; break;
            case '\b': ss << "\\b"; break;
            case '\f': ss << "\\f"; break;
            case '\n': ss << "\\n"; break;
            case '\r': ss << "\\r"; break;
            case '\t': ss << "\\t"; break;
            default: ss << *iter; break;
        }
    }
    return ss.str();
}
34
Milan

J'ai écrit un simple échappement JSON et des fonctions non échappées. Le code est accessible au public dans GitHub . Pour toute personne intéressée, voici le code:

enum State {ESCAPED, UNESCAPED};

std::string escapeJSON(const std::string& input)
{
    std::string output;
    output.reserve(input.length());

    for (std::string::size_type i = 0; i < input.length(); ++i)
    {
        switch (input[i]) {
            case '"':
                output += "\\\"";
                break;
            case '/':
                output += "\\/";
                break;
            case '\b':
                output += "\\b";
                break;
            case '\f':
                output += "\\f";
                break;
            case '\n':
                output += "\\n";
                break;
            case '\r':
                output += "\\r";
                break;
            case '\t':
                output += "\\t";
                break;
            case '\\':
                output += "\\\\";
                break;
            default:
                output += input[i];
                break;
        }

    }

    return output;
}

std::string unescapeJSON(const std::string& input)
{
    State s = UNESCAPED;
    std::string output;
    output.reserve(input.length());

    for (std::string::size_type i = 0; i < input.length(); ++i)
    {
        switch(s)
        {
            case ESCAPED:
                {
                    switch(input[i])
                    {
                        case '"':
                            output += '\"';
                            break;
                        case '/':
                            output += '/';
                            break;
                        case 'b':
                            output += '\b';
                            break;
                        case 'f':
                            output += '\f';
                            break;
                        case 'n':
                            output += '\n';
                            break;
                        case 'r':
                            output += '\r';
                            break;
                        case 't':
                            output += '\t';
                            break;
                        case '\\':
                            output += '\\';
                            break;
                        default:
                            output += input[i];
                            break;
                    }

                    s = UNESCAPED;
                    break;
                }
            case UNESCAPED:
                {
                    switch(input[i])
                    {
                        case '\\':
                            s = ESCAPED;
                            break;
                        default:
                            output += input[i];
                            break;
                    }
                }
        }
    }
    return output;
}
7
mariolpantunes

Vous n'avez pas dit exactement où ces chaînes que vous bricolez sont venant de, à l'origine, donc cela peut ne pas être utile. Mais s'ils vivent tous dans le code, comme @isnullxbh l'a mentionné dans ce commentaire à une réponse à une question différente, une autre option consiste à tirer parti d'une belle fonctionnalité C++ 11: Littéraux de chaîne bruts .

Je ne citerai pas l'explication longue et normative de cppreference, vous pouvez le lire vous-même. Fondamentalement, cependant, les chaînes R apportent au C++ le même type de littéraux délimités par le programmeur, avec absolument aucune restriction sur le contenu, que vous obtenez d'ici- docs dans le Shell, et quelles langues comme Perl utilisent si efficacement. (La citation préfixée utilisant des accolades peut être la plus grande invention de Perl :)

my qstring = q{Quoted 'string'!};
my qqstring = qq{Double "quoted" 'string'!};
my replacedstring = q{Regexps that /totally/! get eaten by your parser.};
replacedstring =~ s{/totally/!}{(won't!)}; 
# Heh. I see the syntax highlighter isn't quite up to the challege, though.

En C++ 11 ou version ultérieure, un littéral de chaîne brut est préfixé d'un R majuscule avant les guillemets doubles, et à l'intérieur des guillemets, la chaîne est précédée d'un délimiteur de forme libre (un ou plusieurs caractères) suivi d'une parenthèse ouvrante.

À partir de là, vous pouvez écrire en toute sécurité littéralement n'importe quoi autre qu'une parenthèse fermante suivie du délimiteur que vous avez choisi. Cette séquence (suivie d'un guillemet double de fermeture) termine le littéral brut, puis vous avez un std::string que vous pouvez faire confiance en toute confiance restera insensible à toute analyse ou traitement de chaîne.

La "brutalité" n'est pas non plus perdue dans les manipulations ultérieures. Donc, en empruntant à la liste des chapitres de Crockford Comment JavaScript fonctionne, ceci est complètement valide:

std::string ch0_to_4 = R"json(
[
    {"number": 0, "chapter": "Read Me First!"},
    {"number": 1, "chapter": "How Names Work"},
    {"number": 2, "chapter": "How Numbers Work"},
    {"number": 3, "chapter": "How Big Integers Work"},
    {"number": 4, "chapter": "How Big Floating Point Works"},)json";

std::string ch5_and_6 = R"json(
    {"number": 5, "chapter": "How Big Rationals Work"},
    {"number": 6, "chapter": "How Booleans Work"})json";

std::string chapters = ch0_to_4 + ch5_and_6 + "\n]";
std::cout << chapters;

La chaîne "chapitres" émergera de std::cout complètement intact:

[
    {"number": 0, "chapter": "Read Me First!"},
    {"number": 1, "chapter": "How Names Work"},
    {"number": 2, "chapter": "How Numbers Work"},
    {"number": 3, "chapter": "How Big Integers Work"},
    {"number": 4, "chapter": "How Big Floating Point Works"},
    {"number": 5, "chapter": "How Big Rationals Work"},
    {"number": 6, "chapter": "How Booleans Work"}
]
0
FeRD