From 4793f75496e171c20656ea83bbfaa2f02383b927 Mon Sep 17 00:00:00 2001 From: IgorEisberg Date: Mon, 21 Sep 2026 23:42:48 +0300 Subject: [PATCH] fix: yaml edge case --- .../meta/ApkInfoSerializationTest.java | 6 +- .../src/test/resources/meta/serialization.yml | 4 + .../src/main/java/brut/yaml/YamlUtils.java | 204 ++++++++++-------- 3 files changed, 121 insertions(+), 93 deletions(-) diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/meta/ApkInfoSerializationTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/meta/ApkInfoSerializationTest.java index b1d899d7f9..66904cc05a 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/meta/ApkInfoSerializationTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/meta/ApkInfoSerializationTest.java @@ -47,12 +47,16 @@ private void check(ApkInfo apkInfo) { assertEquals(1, apkInfo.getVersionInfo().getVersionCode()); assertEquals("1.0", apkInfo.getVersionInfo().getVersionName()); assertNotNull(apkInfo.getDoNotCompress()); - assertEquals(6, apkInfo.getDoNotCompress().size()); + assertEquals(10, apkInfo.getDoNotCompress().size()); assertEquals("assets/0byte_file.jpg", apkInfo.getDoNotCompress().get(0)); assertEquals("arsc", apkInfo.getDoNotCompress().get(1)); assertEquals("png", apkInfo.getDoNotCompress().get(2)); assertEquals("mp3", apkInfo.getDoNotCompress().get(3)); assertEquals("stored.file", apkInfo.getDoNotCompress().get(4)); assertEquals("surrogate/pair/😊.test", apkInfo.getDoNotCompress().get(5)); + assertEquals("'double-quoted", apkInfo.getDoNotCompress().get(6)); + assertEquals("\"single-quoted", apkInfo.getDoNotCompress().get(7)); + assertEquals("\\\"single-quoted-with-\\tab", apkInfo.getDoNotCompress().get(8)); + assertEquals("\"double-quoted-with-\tab", apkInfo.getDoNotCompress().get(9)); } } diff --git a/brut.apktool/apktool-lib/src/test/resources/meta/serialization.yml b/brut.apktool/apktool-lib/src/test/resources/meta/serialization.yml index ecbf720402..acaac5d53b 100644 --- a/brut.apktool/apktool-lib/src/test/resources/meta/serialization.yml +++ b/brut.apktool/apktool-lib/src/test/resources/meta/serialization.yml @@ -15,3 +15,7 @@ doNotCompress: - mp3 - stored.file - surrogate/pair/😊.test +- "'double-quoted" +- '"single-quoted' +- '\"single-quoted-with-\tab' +- "\"double-quoted-with-\tab" diff --git a/brut.j.yaml/src/main/java/brut/yaml/YamlUtils.java b/brut.j.yaml/src/main/java/brut/yaml/YamlUtils.java index 46016a40f4..ec3281a572 100644 --- a/brut.j.yaml/src/main/java/brut/yaml/YamlUtils.java +++ b/brut.j.yaml/src/main/java/brut/yaml/YamlUtils.java @@ -32,106 +32,126 @@ public static String escapeString(String str) { if (len == 0) { return "\"\""; } - // Scan for characters that force quoting. - boolean quote = false; - for (int i = 0; i < len; i++) { - char ch = str.charAt(i); - // No-Break Space, Line Separator, and Paragraph Separator are printable but must be escaped. - if (ch == '\u00A0' || ch == '\u2028' || ch == '\u2029') { - quote = true; + // If starts with a quote, quote the rest using the opposite quote character. + char ch = str.charAt(0); + char quote; + if (ch == '\'') { + quote = '"'; + } else { + quote = ch == '"' ? '\'' : 0; + // Scan for characters that force double-quoting. + for (int i = 0; i < len; i++) { + ch = str.charAt(i); + // No-Break Space, Line Separator, and Paragraph Separator are printable but must be escaped. + if (ch == '\u00A0' || ch == '\u2028' || ch == '\u2029') { + quote = '"'; + break; + } + if (TextUtils.isPrintableChar(ch)) { + continue; + } + // Is this a high surrogate followed by a valid low surrogate? + if (Character.isHighSurrogate(ch) && i + 1 < len && Character.isLowSurrogate(str.charAt(i + 1))) { + i++; + continue; + } + // A non-printable character must be escaped. + quote = '"'; break; } - if (TextUtils.isPrintableChar(ch)) { - continue; - } - // Is this a high surrogate followed by a valid low surrogate? - if (Character.isHighSurrogate(ch) && i + 1 < len && Character.isLowSurrogate(str.charAt(i + 1))) { - i++; - continue; - } - // A non-printable character must be escaped. - quote = true; - break; } - // If nothing forced quoting, return the string as-is. - if (!quote) { - return str; - } - // Quoting is needed: escape \, ", and non-printable characters. - StringBuilder sb = new StringBuilder(len * 2 + 2); - sb.append('"'); - for (int i = 0; i < len; i++) { - char ch = str.charAt(i); - switch (ch) { - case '\\': - sb.append("\\\\"); - continue; - case '"': - sb.append("\\\""); - continue; - case '\0': // Null - sb.append("\\0"); - continue; - case '\u0007': // Bell - sb.append("\\a"); - continue; - case '\b': // Backspace - sb.append("\\b"); - continue; - case '\t': // Character Tabulation - sb.append("\\t"); - continue; - case '\n': // Line Feed - sb.append("\\n"); - continue; - case '\u000B': // Line Tabulation - sb.append("\\v"); - continue; - case '\f': // Form Feed - sb.append("\\f"); - continue; - case '\r': // Carriage Return - sb.append("\\r"); - continue; - case '\u001B': // Escape - sb.append("\\e"); - continue; - case '\u0085': // Next Line - sb.append("\\N"); - continue; - case '\u00A0': // No-Break Space - sb.append("\\_"); - continue; - case '\u2028': // Line Separator - sb.append("\\L"); - continue; - case '\u2029': // Paragraph Separator - sb.append("\\P"); + if (quote == '"') { + // Double-quote: escape \, ", and non-printable characters. + StringBuilder sb = new StringBuilder(len * 2 + 2); + sb.append('"'); + for (int i = 0; i < len; i++) { + ch = str.charAt(i); + switch (ch) { + case '\\': + sb.append("\\\\"); + continue; + case '"': + sb.append("\\\""); + continue; + case '\0': // Null + sb.append("\\0"); + continue; + case '\u0007': // Bell + sb.append("\\a"); + continue; + case '\b': // Backspace + sb.append("\\b"); + continue; + case '\t': // Character Tabulation + sb.append("\\t"); + continue; + case '\n': // Line Feed + sb.append("\\n"); + continue; + case '\u000B': // Line Tabulation + sb.append("\\v"); + continue; + case '\f': // Form Feed + sb.append("\\f"); + continue; + case '\r': // Carriage Return + sb.append("\\r"); + continue; + case '\u001B': // Escape + sb.append("\\e"); + continue; + case '\u0085': // Next Line + sb.append("\\N"); + continue; + case '\u00A0': // No-Break Space + sb.append("\\_"); + continue; + case '\u2028': // Line Separator + sb.append("\\L"); + continue; + case '\u2029': // Paragraph Separator + sb.append("\\P"); + continue; + } + if (TextUtils.isPrintableChar(ch)) { + sb.append(ch); continue; + } + // Is this a high surrogate followed by a valid low surrogate? + if (Character.isHighSurrogate(ch) && i + 1 < len) { + char low = str.charAt(i + 1); + if (Character.isLowSurrogate(low)) { + sb.append(ch).append(low); + i++; + continue; + } + // fallthrough + } + // Java-style Unicode escape the non-printable character. + sb.append("\\u") + .append(Character.forDigit(ch >>> 12, 16)) + .append(Character.forDigit(ch >>> 8 & 0xF, 16)) + .append(Character.forDigit(ch >>> 4 & 0xF, 16)) + .append(Character.forDigit(ch & 0xF, 16)); } - if (TextUtils.isPrintableChar(ch)) { - sb.append(ch); - continue; - } - // Is this a high surrogate followed by a valid low surrogate? - if (Character.isHighSurrogate(ch) && i + 1 < len) { - char low = str.charAt(i + 1); - if (Character.isLowSurrogate(low)) { - sb.append(ch).append(low); - i++; - continue; + sb.append('"'); + str = sb.toString(); + } else if (quote == '\'') { + // Single-quote: escape ' by doubling it. + StringBuilder sb = new StringBuilder(len * 2 + 2); + sb.append('\''); + for (int i = 0; i < len; i++) { + ch = str.charAt(i); + if (ch == '\'') { + sb.append("''"); + } else { + sb.append(ch); } - // fallthrough } - // Java-style Unicode escape the non-printable character. - sb.append("\\u") - .append(Character.forDigit(ch >>> 12, 16)) - .append(Character.forDigit(ch >>> 8 & 0xF, 16)) - .append(Character.forDigit(ch >>> 4 & 0xF, 16)) - .append(Character.forDigit(ch & 0xF, 16)); + sb.append('\''); + str = sb.toString(); } - sb.append('"'); - return sb.toString(); + return str; } public static String unescapeString(String str) {