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 a92de989..75d2fe62 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 @@ -280,21 +280,21 @@ public class ARSCDecoder { mHeader.checkForUnreadHeader(mIn); + boolean isOffset16 = (typeFlags & TABLE_TYPE_FLAG_OFFSET16) != 0; + boolean isSparse = (typeFlags & TABLE_TYPE_FLAG_SPARSE) != 0; + // Be sure we don't poison mResTable by marking the application as sparse // Only flag the ResTable as sparse if the main package is not loaded. - if ((typeFlags & TABLE_TYPE_FLAG_SPARSE) != 0 && !mResTable.isMainPkgLoaded()) { + if (isSparse && !mResTable.isMainPkgLoaded()) { mResTable.setSparseResources(true); } - if ((typeFlags & TABLE_TYPE_FLAG_OFFSET16) != 0) { - LOGGER.warning("Please report this application to Apktool for a fix: https://github.com/iBotPeaches/Apktool/issues/3367"); - throw new AndrolibException("Unexpected TYPE_FLAG_OFFSET16"); - } - HashMap entryOffsetMap = new LinkedHashMap<>(); for (int i = 0; i < entryCount; i++) { - if ((typeFlags & TABLE_TYPE_FLAG_SPARSE) != 0) { + if (isSparse) { entryOffsetMap.put(mIn.readUnsignedShort(), mIn.readUnsignedShort()); + } else if (isOffset16) { + entryOffsetMap.put(i, mIn.readUnsignedShort()); } else { entryOffsetMap.put(i, mIn.readInt()); } @@ -310,11 +310,12 @@ public class ARSCDecoder { } mType = flags.isInvalid && !mKeepBroken ? null : mPkg.getOrCreateConfig(flags); + int noEntry = isOffset16 ? NO_ENTRY_OFFSET16 : NO_ENTRY; for (int i : entryOffsetMap.keySet()) { mResId = (mResId & 0xffff0000) | i; int offset = entryOffsetMap.get(i); - if (offset == NO_ENTRY) { + if (offset == noEntry) { mMissingResSpecMap.put(mResId, typeId); continue; } @@ -347,25 +348,35 @@ public class ARSCDecoder { private EntryData readEntryData() throws IOException, AndrolibException { short size = mIn.readShort(); - if (size < 0) { - throw new AndrolibException("Entry size is under 0 bytes."); - } - short flags = mIn.readShort(); - int specNamesId = mIn.readInt(); - if (specNamesId == NO_ENTRY) { - return null; - } boolean isComplex = (flags & ENTRY_FLAG_COMPLEX) != 0; boolean isCompact = (flags & ENTRY_FLAG_COMPACT) != 0; - if (isCompact) { - LOGGER.warning("Please report this application to Apktool for a fix: https://github.com/iBotPeaches/Apktool/issues/3366"); - throw new AndrolibException("Unexpected entry type: compact"); + if (size < 0 && !isCompact) { + throw new AndrolibException("Entry size is under 0 bytes and not compactly packed."); + } + + int specNamesId = mIn.readInt(); + if (specNamesId == NO_ENTRY && !isCompact) { + return null; + } + + // #3366 - In a compactly packed entry, the key index is the size & type is higher 8 bits on flags. + // We assume a size of 8 bytes for compact entries and the specNamesId is the data itself encoded. + ResValue value; + if (isCompact) { + byte type = (byte) ((flags >> 8) & 0xFF); + value = readCompactValue(type, specNamesId); + + // To keep code below happy - we know if compact that the size has the key index encoded. + specNamesId = size; + } else if (isComplex) { + value = readComplexEntry(); + } else { + value = readValue(); } - ResValue value = isComplex ? readComplexEntry() : readValue(); // #2824 - In some applications the res entries are duplicated with the 2nd being malformed. // AOSP skips this, so we will do the same. if (value == null) { @@ -443,6 +454,12 @@ public class ARSCDecoder { return factory.bagFactory(parent, items, mTypeSpec); } + private ResIntBasedValue readCompactValue(byte type, int data) throws AndrolibException { + return type == TypedValue.TYPE_STRING + ? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data) + : mPkg.getValueFactory().factory(type, data, null); + } + private ResIntBasedValue readValue() throws IOException, AndrolibException { int size = mIn.readShort(); if (size < 8) { @@ -686,6 +703,7 @@ public class ARSCDecoder { private static final int KNOWN_CONFIG_BYTES = 64; private static final int NO_ENTRY = 0xFFFFFFFF; + private static final int NO_ENTRY_OFFSET16 = 0xFFFF; private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java index acc8785c..12d3a351 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java @@ -22,6 +22,7 @@ import brut.directory.ExtFile; import brut.directory.FileDirectory; import org.custommonkey.xmlunit.*; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; @@ -155,6 +156,17 @@ public class BaseTest { } } + protected static int getStringEntryCount(Document doc, String key) { + int count = 0; + Element resources = doc.getDocumentElement(); + for (int i = 0; i < resources.getChildNodes().getLength(); i++) { + if (resources.getChildNodes().item(i).getNodeName().equals(key)) { + count++; + } + } + return count; + } + protected static ExtFile sTmpDir; protected static ExtFile sTestOrigDir; protected static ExtFile sTestNewDir; diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java new file mode 100644 index 00000000..5b4ad679 --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib.decode; + +import brut.androlib.*; +import brut.directory.ExtFile; +import brut.common.BrutException; +import brut.util.OS; +import java.io.File; +import java.io.IOException; + +import org.junit.*; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; + +import static org.junit.Assert.*; + +public class CompactResourceTest extends BaseTest { + + @BeforeClass + public static void beforeClass() throws Exception { + TestUtils.cleanFrameworkFile(); + sTmpDir = new ExtFile(OS.createTempDirectory()); + TestUtils.copyResourceDir(CompactResourceTest.class, "decode/issue3366/", sTmpDir); + } + + @AfterClass + public static void afterClass() throws BrutException { + OS.rmdir(sTmpDir); + } + + @Test + public void checkIfDecodeSucceeds() throws BrutException, IOException, ParserConfigurationException, SAXException { + String apk = "issue3366.apk"; + File testApk = new File(sTmpDir, apk); + + // decode issue3366.apk + ApkDecoder apkDecoder = new ApkDecoder(testApk); + sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); + + File outDir = new File(sTmpDir + File.separator + apk + ".out"); + apkDecoder.decode(outDir); + + Document doc = loadDocument(new File(sTestOrigDir + "/res/values/strings.xml")); + assertEquals(1002, getStringEntryCount(doc, "string")); + + Config config = Config.getDefaultConfig(); + LOGGER.info("Building duplicatedex.apk..."); + new ApkBuilder(config, sTestOrigDir).build(testApk); + } +} diff --git a/brut.apktool/apktool-lib/src/test/resources/decode/issue3366/issue3366.apk b/brut.apktool/apktool-lib/src/test/resources/decode/issue3366/issue3366.apk new file mode 100644 index 00000000..0bccfb8b Binary files /dev/null and b/brut.apktool/apktool-lib/src/test/resources/decode/issue3366/issue3366.apk differ