88 votes

Comment déséchafauder une chaîne littérale en Java ?

Je traite un code source Java en utilisant Java. J'extrais les chaînes de caractères et les envoie à une fonction qui prend une chaîne de caractères. Le problème est que je dois transmettre la version non encodée de la chaîne de caractères à la fonction (c'est-à-dire que je dois convertir la version non encodée de la chaîne de caractères en une version non encodée). \n à un saut de ligne, et \\ à un seul \ etc).

Existe-t-il une fonction dans l'API Java qui fait cela ? Si ce n'est pas le cas, puis-je obtenir cette fonction à partir d'une bibliothèque ? Il est évident que le compilateur Java doit effectuer cette conversion.

Au cas où quelqu'un voudrait le savoir, j'essaie de désobfusquer des chaînes de caractères dans des fichiers Java obfusqués décompilés.

110voto

tchrist Points 47116

Le problème

El org.apache.commons.lang.StringEscapeUtils.unescapeJava() donnée ici comme une autre réponse est en réalité très peu utile.

  • Il oublie \0 pour la nullité.
  • Il ne gère pas l'octal du tout .
  • Il ne peut pas gérer les sortes d'évasions admises par la java.util.regex.Pattern.compile() et tout ce qui l'utilise, y compris \a , \e et surtout \cX .
  • Il ne prend pas en charge les points de code Unicode logiques par numéro, seulement l'UTF-16.
  • Cela ressemble à du code UCS-2, et non à du code UTF-16 : ils utilisent le code déprécié de l'UE. charAt au lieu de l'interface codePoint promulguant ainsi l'illusion qu'une interface Java char est garantie de contenir un caractère Unicode. Ce n'est pas le cas. Ils s'en tirent uniquement parce qu'aucun substitut UTF-16 ne cherchera ce qu'ils recherchent.

La solution

J'ai écrit une chaîne unescaper qui résout la question de l'OP sans toutes les irritations du code Apache.

/*
 *
 * unescape_perl_string()
 *
 *      Tom Christiansen <tchrist@perl.com>
 *      Sun Nov 28 12:55:24 MST 2010
 *
 * It's completely ridiculous that there's no standard
 * unescape_java_string function.  Since I have to do the
 * damn thing myself, I might as well make it halfway useful
 * by supporting things Java was too stupid to consider in
 * strings:
 * 
 *   => "?" items  are additions to Java string escapes
 *                 but normal in Java regexes
 *
 *   => "!" items  are also additions to Java regex escapes
 *   
 * Standard singletons: ?\a ?\e \f \n \r \t
 * 
 *      NB: \b is unsupported as backspace so it can pass-through
 *          to the regex translator untouched; I refuse to make anyone
 *          doublebackslash it as doublebackslashing is a Java idiocy
 *          I desperately wish would die out.  There are plenty of
 *          other ways to write it:
 *
 *              \cH, \12, \012, \x08 \x{8}, \u0008, \U00000008
 *
 * Octal escapes: \0 \0N \0NN \N \NN \NNN
 *    Can range up to !\777 not \377
 *    
 *      TODO: add !\o{NNNNN}
 *          last Unicode is 4177777
 *          maxint is 37777777777
 *
 * Control chars: ?\cX
 *      Means: ord(X) ^ ord('@')
 *
 * Old hex escapes: \xXX
 *      unbraced must be 2 xdigits
 *
 * Perl hex escapes: !\x{XXX} braced may be 1-8 xdigits
 *       NB: proper Unicode never needs more than 6, as highest
 *           valid codepoint is 0x10FFFF, not maxint 0xFFFFFFFF
 *
 * Lame Java escape: \[IDIOT JAVA PREPROCESSOR]uXXXX must be
 *                   exactly 4 xdigits;
 *
 *       I can't write XXXX in this comment where it belongs
 *       because the damned Java Preprocessor can't mind its
 *       own business.  Idiots!
 *
 * Lame Python escape: !\UXXXXXXXX must be exactly 8 xdigits
 * 
 * TODO: Perl translation escapes: \Q \U \L \E \[IDIOT JAVA PREPROCESSOR]u \l
 *       These are not so important to cover if you're passing the
 *       result to Pattern.compile(), since it handles them for you
 *       further downstream.  Hm, what about \[IDIOT JAVA PREPROCESSOR]u?
 *
 */

public final static
String unescape_perl_string(String oldstr) {

    /*
     * In contrast to fixing Java's broken regex charclasses,
     * this one need be no bigger, as unescaping shrinks the string
     * here, where in the other one, it grows it.
     */

    StringBuffer newstr = new StringBuffer(oldstr.length());

    boolean saw_backslash = false;

    for (int i = 0; i < oldstr.length(); i++) {
        int cp = oldstr.codePointAt(i);
        if (oldstr.codePointAt(i) > Character.MAX_VALUE) {
            i++; /****WE HATES UTF-16! WE HATES IT FOREVERSES!!!****/
        }

        if (!saw_backslash) {
            if (cp == '\\') {
                saw_backslash = true;
            } else {
                newstr.append(Character.toChars(cp));
            }
            continue; /* switch */
        }

        if (cp == '\\') {
            saw_backslash = false;
            newstr.append('\\');
            newstr.append('\\');
            continue; /* switch */
        }

        switch (cp) {

            case 'r':  newstr.append('\r');
                       break; /* switch */

            case 'n':  newstr.append('\n');
                       break; /* switch */

            case 'f':  newstr.append('\f');
                       break; /* switch */

            /* PASS a \b THROUGH!! */
            case 'b':  newstr.append("\\b");
                       break; /* switch */

            case 't':  newstr.append('\t');
                       break; /* switch */

            case 'a':  newstr.append('\007');
                       break; /* switch */

            case 'e':  newstr.append('\033');
                       break; /* switch */

            /*
             * A "control" character is what you get when you xor its
             * codepoint with '@'==64.  This only makes sense for ASCII,
             * and may not yield a "control" character after all.
             *
             * Strange but true: "\c{" is ";", "\c}" is "=", etc.
             */
            case 'c':   {
                if (++i == oldstr.length()) { die("trailing \\c"); }
                cp = oldstr.codePointAt(i);
                /*
                 * don't need to grok surrogates, as next line blows them up
                 */
                if (cp > 0x7f) { die("expected ASCII after \\c"); }
                newstr.append(Character.toChars(cp ^ 64));
                break; /* switch */
            }

            case '8':
            case '9': die("illegal octal digit");
                      /* NOTREACHED */

    /*
     * may be 0 to 2 octal digits following this one
     * so back up one for fallthrough to next case;
     * unread this digit and fall through to next case.
     */
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7': --i;
                      /* FALLTHROUGH */

            /*
             * Can have 0, 1, or 2 octal digits following a 0
             * this permits larger values than octal 377, up to
             * octal 777.
             */
            case '0': {
                if (i+1 == oldstr.length()) {
                    /* found \0 at end of string */
                    newstr.append(Character.toChars(0));
                    break; /* switch */
                }
                i++;
                int digits = 0;
                int j;
                for (j = 0; j <= 2; j++) {
                    if (i+j == oldstr.length()) {
                        break; /* for */
                    }
                    /* safe because will unread surrogate */
                    int ch = oldstr.charAt(i+j);
                    if (ch < '0' || ch > '7') {
                        break; /* for */
                    }
                    digits++;
                }
                if (digits == 0) {
                    --i;
                    newstr.append('\0');
                    break; /* switch */
                }
                int value = 0;
                try {
                    value = Integer.parseInt(
                                oldstr.substring(i, i+digits), 8);
                } catch (NumberFormatException nfe) {
                    die("invalid octal value for \\0 escape");
                }
                newstr.append(Character.toChars(value));
                i += digits-1;
                break; /* switch */
            } /* end case '0' */

            case 'x':  {
                if (i+2 > oldstr.length()) {
                    die("string too short for \\x escape");
                }
                i++;
                boolean saw_brace = false;
                if (oldstr.charAt(i) == '{') {
                        /* ^^^^^^ ok to ignore surrogates here */
                    i++;
                    saw_brace = true;
                }
                int j;
                for (j = 0; j < 8; j++) {

                    if (!saw_brace && j == 2) {
                        break;  /* for */
                    }

                    /*
                     * ASCII test also catches surrogates
                     */
                    int ch = oldstr.charAt(i+j);
                    if (ch > 127) {
                        die("illegal non-ASCII hex digit in \\x escape");
                    }

                    if (saw_brace && ch == '}') { break; /* for */ }

                    if (! ( (ch >= '0' && ch <= '9')
                                ||
                            (ch >= 'a' && ch <= 'f')
                                ||
                            (ch >= 'A' && ch <= 'F')
                          )
                       )
                    {
                        die(String.format(
                            "illegal hex digit #%d '%c' in \\x", ch, ch));
                    }

                }
                if (j == 0) { die("empty braces in \\x{} escape"); }
                int value = 0;
                try {
                    value = Integer.parseInt(oldstr.substring(i, i+j), 16);
                } catch (NumberFormatException nfe) {
                    die("invalid hex value for \\x escape");
                }
                newstr.append(Character.toChars(value));
                if (saw_brace) { j++; }
                i += j-1;
                break; /* switch */
            }

            case 'u': {
                if (i+4 > oldstr.length()) {
                    die("string too short for \\u escape");
                }
                i++;
                int j;
                for (j = 0; j < 4; j++) {
                    /* this also handles the surrogate issue */
                    if (oldstr.charAt(i+j) > 127) {
                        die("illegal non-ASCII hex digit in \\u escape");
                    }
                }
                int value = 0;
                try {
                    value = Integer.parseInt( oldstr.substring(i, i+j), 16);
                } catch (NumberFormatException nfe) {
                    die("invalid hex value for \\u escape");
                }
                newstr.append(Character.toChars(value));
                i += j-1;
                break; /* switch */
            }

            case 'U': {
                if (i+8 > oldstr.length()) {
                    die("string too short for \\U escape");
                }
                i++;
                int j;
                for (j = 0; j < 8; j++) {
                    /* this also handles the surrogate issue */
                    if (oldstr.charAt(i+j) > 127) {
                        die("illegal non-ASCII hex digit in \\U escape");
                    }
                }
                int value = 0;
                try {
                    value = Integer.parseInt(oldstr.substring(i, i+j), 16);
                } catch (NumberFormatException nfe) {
                    die("invalid hex value for \\U escape");
                }
                newstr.append(Character.toChars(value));
                i += j-1;
                break; /* switch */
            }

            default:   newstr.append('\\');
                       newstr.append(Character.toChars(cp));
           /*
            * say(String.format(
            *       "DEFAULT unrecognized escape %c passed through",
            *       cp));
            */
                       break; /* switch */

        }
        saw_backslash = false;
    }

    /* weird to leave one at the end */
    if (saw_backslash) {
        newstr.append('\\');
    }

    return newstr.toString();
}

/*
 * Return a string "U+XX.XXX.XXXX" etc, where each XX set is the
 * xdigits of the logical Unicode code point. No bloody brain-damaged
 * UTF-16 surrogate crap, just true logical characters.
 */
 public final static
 String uniplus(String s) {
     if (s.length() == 0) {
         return "";
     }
     /* This is just the minimum; sb will grow as needed. */
     StringBuffer sb = new StringBuffer(2 + 3 * s.length());
     sb.append("U+");
     for (int i = 0; i < s.length(); i++) {
         sb.append(String.format("%X", s.codePointAt(i)));
         if (s.codePointAt(i) > Character.MAX_VALUE) {
             i++; /****WE HATES UTF-16! WE HATES IT FOREVERSES!!!****/
         }
         if (i+1 < s.length()) {
             sb.append(".");
         }
     }
     return sb.toString();
 }

private static final
void die(String foa) {
    throw new IllegalArgumentException(foa);
}

private static final
void say(String what) {
    System.out.println(what);
}

Si cela peut aider d'autres personnes, vous êtes le bienvenu - sans conditions. Si vous l'améliorez, j'aimerais que vous m'envoyiez vos améliorations, mais vous n'êtes certainement pas obligé de le faire.

3 votes

Pourquoi votre routine s'appelle "unescape_ perl _chaîne ? De plus, le fait de faire tout le désencadrement supplémentaire pour des choses non définies par la spécification n'est-il pas un bug puisque Java lui-même n'interpréterait pas un littéral de cette façon ? Je m'assure juste que je ne manque rien ici - le code est assez complexe pour que je m'inquiète un peu de tous les bits supplémentaires.

0 votes

@tchrist Avez-vous une idée si les problèmes que vous avez décrits avec l'approche d'Apache sont toujours valables ou s'ils ont été corrigés ?

1 votes

@tchrist Merci. Je viens d'essayer votre méthode soignée avec un fichier texte contenant foo\\bar et il a retourné foo\\bar . Je me serais attendu à ce que ce soit foo\bar . S'agit-il d'un bug ou est-ce que je comprends mal l'idée derrière la méthode ?

52voto

polygenelubricants Points 136838

Vous pouvez utiliser String unescapeJava(String) méthode de StringEscapeUtils de Langage Apache Commons .

Voici un extrait d'exemple :

    String in = "a\\tb\\n\\\"c\\\"";

    System.out.println(in);
    // a\tb\n\"c\"

    String out = StringEscapeUtils.unescapeJava(in);

    System.out.println(out);
    // a    b
    // "c"

La classe utilitaire dispose de méthodes permettant d'échapper et de déséchapper des chaînes de caractères pour Java, Java script, HTML, XML et SQL. Elle possède également des surcharges qui permettent d'écrire directement dans un fichier de type java.io.Writer .


Mises en garde

On dirait que StringEscapeUtils gère les échappatoires Unicode avec un u mais pas les échappements octaux, ni les échappements Unicode avec des éléments étrangers. u s.

    /* Unicode escape test #1: PASS */

    System.out.println(
        "\u0030"
    ); // 0
    System.out.println(
        StringEscapeUtils.unescapeJava("\\u0030")
    ); // 0
    System.out.println(
        "\u0030".equals(StringEscapeUtils.unescapeJava("\\u0030"))
    ); // true

    /* Octal escape test: FAIL */

    System.out.println(
        "\45"
    ); // %
    System.out.println(
        StringEscapeUtils.unescapeJava("\\45")
    ); // 45
    System.out.println(
        "\45".equals(StringEscapeUtils.unescapeJava("\\45"))
    ); // false

    /* Unicode escape test #2: FAIL */

    System.out.println(
        "\uu0030"
    ); // 0
    System.out.println(
        StringEscapeUtils.unescapeJava("\\uu0030")
    ); // throws NestableRuntimeException:
       //   Unable to parse unicode value: u003

Une citation du JLS :

Les échappatoires octaux sont fournis pour la compatibilité avec le C, mais ne peuvent exprimer que des valeurs Unicode. \u0000 par le biais de \u00FF Il est donc préférable d'utiliser des échappatoires Unicode.

Si votre chaîne de caractères peut contenir des échappatoires octaux, vous voudrez peut-être les convertir d'abord en échappatoires Unicode, ou utiliser une autre approche.

Les éléments étrangers u est également documenté comme suit :

Le langage de programmation Java spécifie une méthode standard de transformation d'un programme écrit en Unicode en ASCII qui transforme un programme en une forme pouvant être traitée par des outils basés sur l'ASCII. La transformation implique la conversion de tous les échappements Unicode dans le texte source du programme en ASCII par l'ajout d'un caractère supplémentaire de type u -par exemple, \uxxxx devient \uuxxxx -tout en convertissant simultanément les caractères non ASCII du texte source en caractères d'échappement Unicode contenant un seul u chacun.

Cette version transformée est également acceptable pour un compilateur du langage de programmation Java et représente exactement le même programme. La source Unicode exacte peut être restaurée ultérieurement à partir de cette forme ASCII en convertissant chaque séquence d'échappement où de multiples u sont présents dans une séquence de caractères Unicode avec un caractère de moins. u tout en convertissant simultanément chaque séquence d'échappement en une seule séquence de type u au caractère Unicode unique correspondant.

Si votre chaîne peut contenir des échappatoires Unicode avec des éléments étrangers u vous devrez peut-être aussi prétraiter cette information avant de l'utiliser. StringEscapeUtils .

Vous pouvez également essayer d'écrire votre propre chaîne littérale Java unescaper à partir de zéro, en veillant à respecter les spécifications exactes de JLS.

Références

0 votes

J'ai également trouvé cette bibliothèque (après l'avoir postée). Je rencontre des problèmes avec les valeurs octales. J'essaie actuellement de les convertir à la main.

0 votes

Euh, convertir les octaux en unicode n'est pas trivial. Seulement 0-127 peut être facilement mis en correspondance. C'est bien ça ?

0 votes

@ziggystar : On dirait que JLS dit 0-255 à la place (voir citation). Le plus grand échappement octal est \377 .

20voto

Udo Klimaschewski Points 2671

J'ai rencontré un problème similaire, je n'étais pas satisfait des solutions présentées et j'ai mis en œuvre celle-ci moi-même.

Également disponible en tant que Gist sur Github :

/**
 * Unescapes a string that contains standard Java escape sequences.
 * <ul>
 * <li><strong>&#92;b &#92;f &#92;n &#92;r &#92;t &#92;" &#92;'</strong> :
 * BS, FF, NL, CR, TAB, double and single quote.</li>
 * <li><strong>&#92;X &#92;XX &#92;XXX</strong> : Octal character
 * specification (0 - 377, 0x00 - 0xFF).</li>
 * <li><strong>&#92;uXXXX</strong> : Hexadecimal based Unicode character.</li>
 * </ul>
 * 
 * @param st
 *            A string optionally containing standard java escape sequences.
 * @return The translated string.
 */
public String unescapeJavaString(String st) {

    StringBuilder sb = new StringBuilder(st.length());

    for (int i = 0; i < st.length(); i++) {
        char ch = st.charAt(i);
        if (ch == '\\') {
            char nextChar = (i == st.length() - 1) ? '\\' : st
                    .charAt(i + 1);
            // Octal escape?
            if (nextChar >= '0' && nextChar <= '7') {
                String code = "" + nextChar;
                i++;
                if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
                        && st.charAt(i + 1) <= '7') {
                    code += st.charAt(i + 1);
                    i++;
                    if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
                            && st.charAt(i + 1) <= '7') {
                        code += st.charAt(i + 1);
                        i++;
                    }
                }
                sb.append((char) Integer.parseInt(code, 8));
                continue;
            }
            switch (nextChar) {
            case '\\':
                ch = '\\';
                break;
            case 'b':
                ch = '\b';
                break;
            case 'f':
                ch = '\f';
                break;
            case 'n':
                ch = '\n';
                break;
            case 'r':
                ch = '\r';
                break;
            case 't':
                ch = '\t';
                break;
            case '\"':
                ch = '\"';
                break;
            case '\'':
                ch = '\'';
                break;
            // Hex Unicode: u????
            case 'u':
                if (i >= st.length() - 5) {
                    ch = 'u';
                    break;
                }
                int code = Integer.parseInt(
                        "" + st.charAt(i + 2) + st.charAt(i + 3)
                                + st.charAt(i + 4) + st.charAt(i + 5), 16);
                sb.append(Character.toChars(code));
                i += 5;
                continue;
            }
            i++;
        }
        sb.append(ch);
    }
    return sb.toString();
}

3 votes

Pour plus de précautions, faites une vérification de nullité au début, et retournez null dans ce cas. Mais merci !

10voto

Lasse Espeholt Points 11944

Voir ceci de http://commons.apache.org/lang/ :

StringEscapeUtils

StringEscapeUtils.unescapeJava(String str)

9voto

DaoWen Points 14480

Je sais que cette question est ancienne, mais je voulais une solution qui n'implique pas de bibliothèques en dehors de celles incluses dans JRE6 (c'est-à-dire qu'Apache Commons n'est pas acceptable), et j'ai trouvé une solution simple en utilisant le module intégré java.io.StreamTokenizer :

import java.io.*;

// ...

String literal = "\"Has \\\"\\\\\\\t\\\" & isn\\\'t \\\r\\\n on 1 line.\"";
StreamTokenizer parser = new StreamTokenizer(new StringReader(literal));
String result;
try {
  parser.nextToken();
  if (parser.ttype == '"') {
    result = parser.sval;
  }
  else {
    result = "ERROR!";
  }
}
catch (IOException e) {
  result = e.toString();
}
System.out.println(result);

Sortie :

Has "\  " & isn't
 on 1 line.

1 votes

@UdoKlimaschewski - Vous avez raison. Vous pouvez regarder à la source pour voir quels échappements il supporte réellement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X