From ce9645864d9a93705717c408ecb3b2432e4f3502 Mon Sep 17 00:00:00 2001 From: nitram84 <58364572+nitram84@users.noreply.github.com> Date: Sun, 9 Jul 2023 21:05:29 +0200 Subject: [PATCH] fix(res): support formatted=false for strings (PR #1947) --- .../main/java/jadx/core/xmlgen/ResXmlGen.java | 5 + .../core/xmlgen/StringFormattedCheck.java | 104 ++++++++++++++++++ .../java/jadx/core/xmlgen/ResXmlGenTest.java | 22 ++++ 3 files changed, 131 insertions(+) create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/StringFormattedCheck.java diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index befdc4d95..a5816ac98 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -250,6 +250,11 @@ public class ResXmlGen { cw.add(' ').add(attrName).add("=\"").add(attrValue).add('"'); } } + + if (itemTag.equals("string") && valueStr.contains("%") && StringFormattedCheck.hasMultipleNonPositionalSubstitutions(valueStr)) { + cw.add(" formatted=\"false\""); + } + if (valueStr.equals("")) { cw.add(" />"); } else { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/StringFormattedCheck.java b/jadx-core/src/main/java/jadx/core/xmlgen/StringFormattedCheck.java new file mode 100644 index 000000000..dd317fad3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/StringFormattedCheck.java @@ -0,0 +1,104 @@ +package jadx.core.xmlgen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/* + * This class contains source code form https://github.com/iBotPeaches/Apktool/ + * see: + * https://github.com/iBotPeaches/Apktool/blob/master/brut.apktool/apktool-lib/src/main/java/brut/ + * androlib/res/xml/ResXmlEncoders.java + */ +public class StringFormattedCheck { + + public static boolean hasMultipleNonPositionalSubstitutions(String str) { + Duo, List> tuple = findSubstitutions(str, 4); + return !tuple.m1.isEmpty() && tuple.m1.size() + tuple.m2.size() > 1; + } + + @SuppressWarnings("checkstyle:ClassTypeParameterName") + private static class Duo { + public final T1 m1; + public final T2 m2; + + public Duo(T1 t1, T2 t2) { + this.m1 = t1; + this.m2 = t2; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("unchecked") + final Duo other = (Duo) obj; + if (!Objects.equals(this.m1, other.m1)) { + return false; + } + return Objects.equals(this.m2, other.m2); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 71 * hash + (this.m1 != null ? this.m1.hashCode() : 0); + hash = 71 * hash + (this.m2 != null ? this.m2.hashCode() : 0); + return hash; + } + } + + /** + * It returns a tuple of: + * - a list of offsets of non positional substitutions. non-pos is defined as any "%" which isn't + * "%%" nor "%\d+\$" + * - a list of offsets of positional substitutions + */ + @SuppressWarnings({ "checkstyle:NeedBraces", "checkstyle:EmptyStatement" }) + private static Duo, List> findSubstitutions(String str, int nonPosMax) { + if (nonPosMax == -1) { + nonPosMax = Integer.MAX_VALUE; + } + int pos; + int pos2 = 0; + List nonPositional = new ArrayList<>(); + List positional = new ArrayList<>(); + + if (str == null) { + return new Duo<>(nonPositional, positional); + } + + int length = str.length(); + + while ((pos = str.indexOf('%', pos2)) != -1) { + pos2 = pos + 1; + if (pos2 == length) { + nonPositional.add(pos); + break; + } + char c = str.charAt(pos2++); + if (c == '%') { + continue; + } + if (c >= '0' && c <= '9' && pos2 < length) { + while ((c = str.charAt(pos2++)) >= '0' && c <= '9' && pos2 < length) + ; + if (c == '$') { + positional.add(pos); + continue; + } + } + + nonPositional.add(pos); + if (nonPositional.size() >= nonPosMax) { + break; + } + } + + return new Duo<>(nonPositional, positional); + } +} diff --git a/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java b/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java index e0a11366b..c5296f550 100644 --- a/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java +++ b/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java @@ -149,6 +149,28 @@ class ResXmlGenTest { + "", files.get(0).getText().toString()); } + @Test + void testStringFormattedFalse() { + ResourceStorage resStorage = new ResourceStorage(); + ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "string", "app_name", ""); + re.setSimpleValue(new RawValue(3, 0)); + re.setNamedValues(Lists.list()); + resStorage.add(re); + + BinaryXMLStrings strings = new BinaryXMLStrings(); + strings.put(0, "%s at %s"); + ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + List files = resXmlGen.makeResourcesXml(); + + assertEquals(1, files.size()); + assertEquals("res/values/strings.xml", files.get(0).getFileName()); + assertEquals("\n" + + "\n" + + " %s at %s\n" + + "", files.get(0).getText().toString()); + } + @Test void testArrayEscape() { ResourceStorage resStorage = new ResourceStorage();