diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java index 05986ae6..a56ec464 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java @@ -26,9 +26,7 @@ public class ResConfigFlags { public final short mnc; public final char[] language; - public final char[] country; - - public final short layoutDirection; + public final char[] region; public final byte orientation; public final byte touchscreen; @@ -50,6 +48,9 @@ public class ResConfigFlags { public final short screenWidthDp; public final short screenHeightDp; + private final char[] localeScript; + private final char[] localeVariant; + public final boolean isInvalid; private final String mQualifiers; @@ -58,8 +59,7 @@ public class ResConfigFlags { mcc = 0; mnc = 0; language = new char[] { '\00', '\00' }; - country = new char[] { '\00', '\00' }; - layoutDirection = SCREENLAYOUT_LAYOUTDIR_ANY; + region = new char[] { '\00', '\00' }; orientation = ORIENTATION_ANY; touchscreen = TOUCHSCREEN_ANY; density = DENSITY_DEFAULT; @@ -74,17 +74,20 @@ public class ResConfigFlags { smallestScreenWidthDp = 0; screenWidthDp = 0; screenHeightDp = 0; + localeScript = new char[] { '\00', '\00', '\00', '\00' }; + localeVariant = new char[] { '\00', '\00', '\00', '\00', '\00', '\00', '\00', '\00' }; isInvalid = false; mQualifiers = ""; } public ResConfigFlags(short mcc, short mnc, char[] language, - char[] country, short layoutDirection, byte orientation, + char[] region, byte orientation, byte touchscreen, int density, byte keyboard, byte navigation, byte inputFlags, short screenWidth, short screenHeight, short sdkVersion, byte screenLayout, byte uiMode, short smallestScreenWidthDp, short screenWidthDp, - short screenHeightDp, boolean isInvalid) { + short screenHeightDp, char[] localeScript, char[] localeVariant, + boolean isInvalid) { if (orientation < 0 || orientation > 3) { LOGGER.warning("Invalid orientation value: " + orientation); orientation = 0; @@ -114,8 +117,7 @@ public class ResConfigFlags { this.mcc = mcc; this.mnc = mnc; this.language = language; - this.country = country; - this.layoutDirection = layoutDirection; + this.region = region; this.orientation = orientation; this.touchscreen = touchscreen; this.density = density; @@ -130,6 +132,8 @@ public class ResConfigFlags { this.smallestScreenWidthDp = smallestScreenWidthDp; this.screenWidthDp = screenWidthDp; this.screenHeightDp = screenHeightDp; + this.localeScript = localeScript; + this.localeVariant = localeVariant; this.isInvalid = isInvalid; mQualifiers = generateQualifiers(); } @@ -155,12 +159,8 @@ public class ResConfigFlags { ret.append("-mnc00"); } } - if (language[0] != '\00') { - ret.append('-').append(language); - if (country[0] != '\00') { - ret.append("-r").append(country); - } - } + ret.append(getLocaleString()); + switch (screenLayout & MASK_LAYOUTDIR) { case SCREENLAYOUT_LAYOUTDIR_RTL: ret.append("-ldrtl"); @@ -369,6 +369,51 @@ public class ResConfigFlags { return 0; } + private String getLocaleString() { + StringBuilder sb = new StringBuilder(); + + // check for old style non BCP47 tags + // allows values-xx-rXX, values-xx, values-xxx-rXX + // denies values-xxx, anything else + if (language[0] != '\00' && localeScript.length == 0 && localeVariant.length == 0 && + (region.length != 3 && language.length != 3) || + (language.length == 3 && region.length == 2 && region[0] != '\00' && + localeScript.length == 0 && localeVariant.length == 0)) { + + sb.append("-").append(language); + if (region[0] != '\00') { + sb.append("-r").append(region); + } + } else { // BCP47 + if (language[0] == '\00' && region[0] == '\00') { + return sb.toString(); // early return, no language or region + } + sb.append("-b+"); + if (language[0] != '\00') { + sb.append(language); + } + if (localeScript.length == 4) { + sb.append("+").append(localeScript); + } + if ((region.length == 2 || region.length == 3) && region[0] != '\00') { + sb.append("+").append(region); + } + if (localeVariant.length >= 5) { + sb.append("+").append(toUpper(localeVariant)); + } + } + return sb.toString(); + } + + private String toUpper(char[] character) { + StringBuilder sb = new StringBuilder(); + for (char ch: character) { + sb.append(Character.toUpperCase(ch)); + } + return sb.toString(); + } + + @Override public String toString() { return !getQualifiers().equals("") ? getQualifiers() : "[DEFAULT]"; diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java index 622e9c30..57caf4ad 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java @@ -256,8 +256,8 @@ public class ARSCDecoder { short mcc = mIn.readShort(); short mnc = mIn.readShort(); - char[] language = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; - char[] country = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; + char[] language = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a'); + char[] country = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0'); byte orientation = mIn.readByte(); byte touchscreen = mIn.readByte(); @@ -291,9 +291,11 @@ public class ARSCDecoder { screenHeightDp = mIn.readShort(); } - short layoutDirection = 0; - if (size >= 38) { - layoutDirection = mIn.readShort(); + char[] localeScript = {'\00'}; + char[] localeVariant = {'\00'}; + if (size >= 48) { + localeScript = this.readScriptOrVariantChar(4).toCharArray(); + localeVariant = this.readScriptOrVariantChar(8).toCharArray(); } int exceedingSize = size - KNOWN_CONFIG_BYTES; @@ -313,11 +315,40 @@ public class ARSCDecoder { } } - return new ResConfigFlags(mcc, mnc, language, country, layoutDirection, + return new ResConfigFlags(mcc, mnc, language, country, orientation, touchscreen, density, keyboard, navigation, inputFlags, screenWidth, screenHeight, sdkVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, - screenHeightDp, isInvalid); + screenHeightDp, localeScript, localeVariant, isInvalid); + } + + private char[] unpackLanguageOrRegion(byte in0, byte in1, char base) throws AndrolibException { + // check high bit, if so we have a packed 3 letter code + if (((in0 >> 7) & 1) == 1) { + int first = in1 & 0x1F; + int second = ((in1 & 0xE0) >> 5) + ((in0 & 0x03) << 3); + int third = (in0 & 0x7C) >> 2; + + // since this function handles languages & regions, we add the value(s) to the base char + // which is usually 'a' or '0' depending on language or region. + return new char[] { (char) (first + base), (char) (second + base), (char) (third + base) }; + } + return new char[] { (char) in0, (char) in1 }; + } + + private String readScriptOrVariantChar(int length) throws AndrolibException, IOException { + StringBuilder string = new StringBuilder(16); + + while(length-- != 0) { + short ch = mIn.readByte(); + if (ch == 0) { + break; + } + string.append((char) ch); + } + mIn.skipBytes(length); + + return string.toString(); } private void addMissingResSpecs() throws AndrolibException { @@ -416,7 +447,7 @@ public class ARSCDecoder { } private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); - private static final int KNOWN_CONFIG_BYTES = 38; + private static final int KNOWN_CONFIG_BYTES = 48; public static class ARSCData { diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java index 4ee4e694..11f7c10a 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java @@ -182,6 +182,51 @@ public class BuildAndDecodeTest { compareValuesFiles("values-watch/strings.xml"); } + @Test + public void packed3CharsTest() throws BrutException, IOException { + compareValuesFiles("values-ast-rES/strings.xml"); + } + + @Test + public void rightToLeftTest() throws BrutException, IOException { + compareValuesFiles("values-ldrtl/strings.xml"); + } + + @Test + public void scriptBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+en+Latn+US/strings.xml"); + } + + @Test + public void threeLetterLangBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+ast/strings.xml"); + } + + @Test + public void twoLetterLangBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-en-rUS/strings.xml"); + } + + @Test + public void variantBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+en+US+POSIX/strings.xml"); + } + + @Test + public void fourpartBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+ast+Latn+IT+AREVELA/strings.xml"); + } + + @Test + public void RegionLocaleBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+en+Latn+419/strings.xml"); + } + + @Test + public void numericalRegionBcp47Test() throws BrutException, IOException { + compareValuesFiles("values-b+eng+419/strings.xml"); + } + @Test public void drawableNoDpiTest() throws BrutException, IOException { compareResFolder("drawable-nodpi"); diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml new file mode 100644 index 00000000..35eaf5ef --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml @@ -0,0 +1,4 @@ + + + test1 + \ No newline at end of file