/* * 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.res.decoder; import android.util.TypedValue; import brut.androlib.AndrolibException; import brut.androlib.res.data.*; import brut.androlib.res.data.value.*; import brut.util.Duo; import brut.util.ExtDataInput; import com.google.common.io.LittleEndianDataInputStream; import org.apache.commons.io.input.CountingInputStream; import java.io.DataInput; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.logging.Logger; public class ARSCDecoder { public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken) throws AndrolibException { return decode(arscStream, findFlagsOffsets, keepBroken, new ResTable()); } public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken, ResTable resTable) throws AndrolibException { try { ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken); ResPackage[] pkgs = decoder.readTableHeader(); return new ARSCData(pkgs, decoder.mFlagsOffsets == null ? null : decoder.mFlagsOffsets.toArray(new FlagsOffset[0]), resTable); } catch (IOException ex) { throw new AndrolibException("Could not decode arsc file", ex); } } private ARSCDecoder(InputStream arscStream, ResTable resTable, boolean storeFlagsOffsets, boolean keepBroken) { arscStream = mCountIn = new CountingInputStream(arscStream); if (storeFlagsOffsets) { mFlagsOffsets = new ArrayList<>(); } else { mFlagsOffsets = null; } // We need to explicitly cast to DataInput as otherwise the constructor is ambiguous. // We choose DataInput instead of InputStream as ExtDataInput wraps an InputStream in // a DataInputStream which is big-endian and ignores the little-endian behavior. mIn = new ExtDataInput((DataInput) new LittleEndianDataInputStream(arscStream)); mResTable = resTable; mKeepBroken = keepBroken; } private ResPackage[] readTableHeader() throws IOException, AndrolibException { nextChunkCheckType(Header.TYPE_TABLE); int packageCount = mIn.readInt(); mTableStrings = StringBlock.read(mIn); ResPackage[] packages = new ResPackage[packageCount]; nextChunk(); for (int i = 0; i < packageCount; i++) { mTypeIdOffset = 0; packages[i] = readTablePackage(); } return packages; } private ResPackage readTablePackage() throws IOException, AndrolibException { checkChunkType(Header.XML_TYPE_PACKAGE); int id = mIn.readInt(); if (id == 0) { // This means we are dealing with a Library Package, we should just temporarily // set the packageId to the next available id . This will be set at runtime regardless, but // for Apktool's use we need a non-zero packageId. // AOSP indicates 0x02 is next, as 0x01 is system and 0x7F is private. id = 2; if (mResTable.getPackageOriginal() == null && mResTable.getPackageRenamed() == null) { mResTable.setSharedLibrary(true); } } String name = mIn.readNullEndedString(128, true); /* typeStrings */mIn.skipInt(); /* lastPublicType */mIn.skipInt(); /* keyStrings */mIn.skipInt(); /* lastPublicKey */mIn.skipInt(); // TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e // which is only in split/new applications. int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4 if (mHeader.headerSize == splitHeaderSize) { mTypeIdOffset = mIn.readInt(); } if (mTypeIdOffset > 0) { LOGGER.warning("Please report this application to Apktool for a fix: https://github.com/iBotPeaches/Apktool/issues/1728"); } mTypeNames = StringBlock.read(mIn); mSpecNames = StringBlock.read(mIn); mResId = id << 24; mPkg = new ResPackage(mResTable, id, name); nextChunk(); boolean flag = true; while (flag) { switch (mHeader.type) { case Header.XML_TYPE_SPEC_TYPE: readTableTypeSpec(); break; case Header.XML_TYPE_LIBRARY: readLibraryType(); break; case Header.XML_TYPE_OVERLAY: readOverlaySpec(); break; case Header.XML_TYPE_STAGED_ALIAS: readStagedAliasSpec(); break; default: flag = false; break; } } return mPkg; } private void readLibraryType() throws AndrolibException, IOException { checkChunkType(Header.XML_TYPE_LIBRARY); int libraryCount = mIn.readInt(); int packageId; String packageName; for (int i = 0; i < libraryCount; i++) { packageId = mIn.readInt(); packageName = mIn.readNullEndedString(128, true); LOGGER.fine(String.format("Decoding Shared Library (%s), pkgId: %d", packageName, packageId)); } while(nextChunk().type == Header.XML_TYPE_TYPE) { readTableTypeSpec(); } } private void readStagedAliasSpec() throws IOException { int count = mIn.readInt(); for (int i = 0; i < count; i++) { LOGGER.fine(String.format("Skipping staged alias stagedId (%h) finalId: %h", mIn.readInt(), mIn.readInt())); } nextChunk(); } private void readOverlaySpec() throws AndrolibException, IOException { checkChunkType(Header.XML_TYPE_OVERLAY); String name = mIn.readNullEndedString(128, true); String actor = mIn.readNullEndedString(128, true); LOGGER.fine(String.format("Overlay name: \"%s\", actor: \"%s\")", name, actor)); while(nextChunk().type == Header.XML_TYPE_OVERLAY_POLICY) { readOverlayPolicySpec(); } } private void readOverlayPolicySpec() throws AndrolibException, IOException { checkChunkType(Header.XML_TYPE_OVERLAY_POLICY); /* policyFlags */mIn.skipInt(); int count = mIn.readInt(); for (int i = 0; i < count; i++) { LOGGER.fine(String.format("Skipping overlay (%h)", mIn.readInt())); } nextChunk(); } private void readTableTypeSpec() throws AndrolibException, IOException { mTypeSpec = readSingleTableTypeSpec(); addTypeSpec(mTypeSpec); int type = nextChunk().type; ResTypeSpec resTypeSpec; while (type == Header.XML_TYPE_SPEC_TYPE) { resTypeSpec = readSingleTableTypeSpec(); addTypeSpec(resTypeSpec); type = nextChunk().type; // We've detected sparse resources, lets record this so we can rebuild in that same format (sparse/not) // with aapt2. aapt1 will ignore this. if (! mResTable.getSparseResources()) { mResTable.setSparseResources(true); } } while (type == Header.XML_TYPE_TYPE) { readTableType(); // skip "TYPE 8 chunks" and/or padding data at the end of this chunk if (mCountIn.getCount() < mHeader.endPosition) { LOGGER.warning("Unknown data detected. Skipping: " + (mHeader.endPosition - mCountIn.getCount()) + " byte(s)"); mCountIn.skip(mHeader.endPosition - mCountIn.getCount()); } type = nextChunk().type; addMissingResSpecs(); } } private ResTypeSpec readSingleTableTypeSpec() throws AndrolibException, IOException { checkChunkType(Header.XML_TYPE_SPEC_TYPE); int id = mIn.readUnsignedByte(); mIn.skipBytes(3); int entryCount = mIn.readInt(); if (mFlagsOffsets != null) { mFlagsOffsets.add(new FlagsOffset(mCountIn.getCount(), entryCount)); } /* flags */mIn.skipBytes(entryCount * 4); mTypeSpec = new ResTypeSpec(mTypeNames.getString(id - 1), mResTable, mPkg, id, entryCount); mPkg.addType(mTypeSpec); return mTypeSpec; } private ResType readTableType() throws IOException, AndrolibException { checkChunkType(Header.XML_TYPE_TYPE); int typeId = mIn.readUnsignedByte() - mTypeIdOffset; if (mResTypeSpecs.containsKey(typeId)) { mResId = (0xff000000 & mResId) | mResTypeSpecs.get(typeId).getId() << 16; mTypeSpec = mResTypeSpecs.get(typeId); } int typeFlags = mIn.readByte(); /* reserved */mIn.skipBytes(2); int entryCount = mIn.readInt(); int entriesStart = mIn.readInt(); mMissingResSpecs = new boolean[entryCount]; Arrays.fill(mMissingResSpecs, true); ResConfigFlags flags = readConfigFlags(); int position = (mHeader.startPosition + entriesStart) - (entryCount * 4); // For some APKs there is a disconnect between the reported size of Configs // If we find a mismatch skip those bytes. if (position != mCountIn.getCount()) { LOGGER.warning("Invalid data detected. Skipping: " + (position - mCountIn.getCount()) + " byte(s)"); mIn.skipBytes(position - mCountIn.getCount()); } if (typeFlags == 1) { LOGGER.fine("Sparse type flags detected: " + mTypeSpec.getName()); } int[] entryOffsets = mIn.readIntArray(entryCount); if (flags.isInvalid) { String resName = mTypeSpec.getName() + flags.getQualifiers(); if (mKeepBroken) { LOGGER.warning("Invalid config flags detected: " + resName); } else { LOGGER.warning("Invalid config flags detected. Dropping resources: " + resName); } } mType = flags.isInvalid && !mKeepBroken ? null : mPkg.getOrCreateConfig(flags); HashMap offsetsToEntryData = new HashMap<>(); for (int offset : entryOffsets) { if (offset == -1 || offsetsToEntryData.containsKey(offset)) { continue; } offsetsToEntryData.put(offset, readEntryData()); } for (int i = 0; i < entryOffsets.length; i++) { if (entryOffsets[i] != -1) { mMissingResSpecs[i] = false; mResId = (mResId & 0xffff0000) | i; EntryData entryData = offsetsToEntryData.get(entryOffsets[i]); readEntry(entryData); } } return mType; } 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(); ResValue value = (flags & ENTRY_FLAG_COMPLEX) == 0 ? readValue() : readComplexEntry(); EntryData entryData = new EntryData(); entryData.mFlags = flags; entryData.mSpecNamesId = specNamesId; entryData.mValue = value; return entryData; } private void readEntry(EntryData entryData) throws AndrolibException { int specNamesId = entryData.mSpecNamesId; ResValue value = entryData.mValue; if (mTypeSpec.isString() && value instanceof ResFileValue) { value = new ResStringValue(value.toString(), ((ResFileValue) value).getRawIntValue()); } if (mType == null) { return; } ResID resId = new ResID(mResId); ResResSpec spec; if (mPkg.hasResSpec(resId)) { spec = mPkg.getResSpec(resId); if (spec.isDummyResSpec()) { removeResSpec(spec); spec = new ResResSpec(resId, mSpecNames.getString(specNamesId), mPkg, mTypeSpec); mPkg.addResSpec(spec); mTypeSpec.addResSpec(spec); } } else { spec = new ResResSpec(resId, mSpecNames.getString(specNamesId), mPkg, mTypeSpec); mPkg.addResSpec(spec); mTypeSpec.addResSpec(spec); } ResResource res = new ResResource(mType, spec, value); try { mType.addResource(res); spec.addResource(res); } catch (AndrolibException ex) { if (mKeepBroken) { mType.addResource(res, true); spec.addResource(res, true); LOGGER.warning(String.format("Duplicate Resource Detected. Ignoring duplicate: %s", res)); } else { throw ex; } } } private ResBagValue readComplexEntry() throws IOException, AndrolibException { int parent = mIn.readInt(); int count = mIn.readInt(); ResValueFactory factory = mPkg.getValueFactory(); Duo[] items = new Duo[count]; ResIntBasedValue resValue; int resId; for (int i = 0; i < count; i++) { resId = mIn.readInt(); resValue = readValue(); if (!(resValue instanceof ResScalarValue)) { resValue = new ResStringValue(resValue.toString(), resValue.getRawIntValue()); } items[i] = new Duo<>(resId, (ResScalarValue) resValue); } return factory.bagFactory(parent, items, mTypeSpec); } private ResIntBasedValue readValue() throws IOException, AndrolibException { /* size */mIn.skipCheckShort((short) 8); /* zero */mIn.skipCheckByte((byte) 0); byte type = mIn.readByte(); int data = mIn.readInt(); return type == TypedValue.TYPE_STRING ? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data) : mPkg.getValueFactory().factory(type, data, null); } private ResConfigFlags readConfigFlags() throws IOException, AndrolibException { int size = mIn.readInt(); int read = 28; if (size < 28) { throw new AndrolibException("Config size < 28"); } boolean isInvalid = false; short mcc = mIn.readShort(); short mnc = mIn.readShort(); 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(); int density = mIn.readUnsignedShort(); byte keyboard = mIn.readByte(); byte navigation = mIn.readByte(); byte inputFlags = mIn.readByte(); /* inputPad0 */mIn.skipBytes(1); short screenWidth = mIn.readShort(); short screenHeight = mIn.readShort(); short sdkVersion = mIn.readShort(); /* minorVersion, now must always be 0 */mIn.skipBytes(2); byte screenLayout = 0; byte uiMode = 0; short smallestScreenWidthDp = 0; if (size >= 32) { screenLayout = mIn.readByte(); uiMode = mIn.readByte(); smallestScreenWidthDp = mIn.readShort(); read = 32; } short screenWidthDp = 0; short screenHeightDp = 0; if (size >= 36) { screenWidthDp = mIn.readShort(); screenHeightDp = mIn.readShort(); read = 36; } char[] localeScript = null; char[] localeVariant = null; if (size >= 48) { localeScript = readScriptOrVariantChar(4).toCharArray(); localeVariant = readScriptOrVariantChar(8).toCharArray(); read = 48; } byte screenLayout2 = 0; byte colorMode = 0; if (size >= 52) { screenLayout2 = mIn.readByte(); colorMode = mIn.readByte(); mIn.skipBytes(2); // reserved padding read = 52; } if (size >= 56) { mIn.skipBytes(4); read = 56; } int exceedingSize = size - KNOWN_CONFIG_BYTES; if (exceedingSize > 0) { byte[] buf = new byte[exceedingSize]; read += exceedingSize; mIn.readFully(buf); BigInteger exceedingBI = new BigInteger(1, buf); if (exceedingBI.equals(BigInteger.ZERO)) { LOGGER.fine(String .format("Config flags size > %d, but exceeding bytes are all zero, so it should be ok.", KNOWN_CONFIG_BYTES)); } else { LOGGER.warning(String.format("Config flags size > %d. Size = %d. Exceeding bytes: 0x%X.", KNOWN_CONFIG_BYTES, size, exceedingBI)); isInvalid = true; } } int remainingSize = size - read; if (remainingSize > 0) { mIn.skipBytes(remainingSize); } return new ResConfigFlags(mcc, mnc, language, country, orientation, touchscreen, density, keyboard, navigation, inputFlags, screenWidth, screenHeight, sdkVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, screenHeightDp, localeScript, localeVariant, screenLayout2, colorMode, isInvalid, size); } private char[] unpackLanguageOrRegion(byte in0, byte in1, char base) { // 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 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 addTypeSpec(ResTypeSpec resTypeSpec) { mResTypeSpecs.put(resTypeSpec.getId(), resTypeSpec); } private void addMissingResSpecs() throws AndrolibException { int resId = mResId & 0xffff0000; for (int i = 0; i < mMissingResSpecs.length; i++) { if (!mMissingResSpecs[i]) { continue; } ResResSpec spec = new ResResSpec(new ResID(resId | i), "APKTOOL_DUMMY_" + Integer.toHexString(i), mPkg, mTypeSpec); // If we already have this resID dont add it again. if (! mPkg.hasResSpec(new ResID(resId | i))) { mPkg.addResSpec(spec); mTypeSpec.addResSpec(spec); if (mType == null) { mType = mPkg.getOrCreateConfig(new ResConfigFlags()); } // We are going to make dummy attributes a null reference (@null) now instead of a boolean false. // This is because aapt2 is much more strict when it comes to what we can put in an application. ResValue value = new ResReferenceValue(mPkg, 0, ""); ResResource res = new ResResource(mType, spec, value); mType.addResource(res); spec.addResource(res); } } } private void removeResSpec(ResResSpec spec) throws AndrolibException { if (mPkg.hasResSpec(spec.getId())) { mPkg.removeResSpec(spec); mTypeSpec.removeResSpec(spec); } } private Header nextChunk() throws IOException { return mHeader = Header.read(mIn, mCountIn); } private void checkChunkType(int expectedType) throws AndrolibException { if (mHeader.type != expectedType) { throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x", expectedType, mHeader.type)); } } private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException { nextChunk(); checkChunkType(expectedType); } private final ExtDataInput mIn; private final ResTable mResTable; private final CountingInputStream mCountIn; private final List mFlagsOffsets; private final boolean mKeepBroken; private Header mHeader; private StringBlock mTableStrings; private StringBlock mTypeNames; private StringBlock mSpecNames; private ResPackage mPkg; private ResTypeSpec mTypeSpec; private ResType mType; private int mResId; private int mTypeIdOffset = 0; private boolean[] mMissingResSpecs; private final HashMap mResTypeSpecs = new HashMap<>(); private final static short ENTRY_FLAG_COMPLEX = 0x0001; private final static short ENTRY_FLAG_PUBLIC = 0x0002; private final static short ENTRY_FLAG_WEAK = 0x0004; public static class Header { public final short type; public final int headerSize; public final int chunkSize; public final int startPosition; public final int endPosition; public Header(short type, int headerSize, int chunkSize, int headerStart) { this.type = type; this.headerSize = headerSize; this.chunkSize = chunkSize; this.startPosition = headerStart; this.endPosition = headerStart + chunkSize; } public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException { short type; int start = countIn.getCount(); try { type = in.readShort(); } catch (EOFException ex) { return new Header(TYPE_NONE, 0, 0, countIn.getCount()); } return new Header(type, in.readShort(), in.readInt(), start); } public final static short TYPE_NONE = -1; public final static short TYPE_STRING_POOL = 0x0001; public final static short TYPE_TABLE = 0x0002; public final static short TYPE_XML = 0x0003; public final static short XML_TYPE_PACKAGE = 0x0200; public final static short XML_TYPE_TYPE = 0x0201; public final static short XML_TYPE_SPEC_TYPE = 0x0202; public final static short XML_TYPE_LIBRARY = 0x0203; public final static short XML_TYPE_OVERLAY = 0x0204; public final static short XML_TYPE_OVERLAY_POLICY = 0x0205; public final static short XML_TYPE_STAGED_ALIAS = 0x0206; } public static class FlagsOffset { public final int offset; public final int count; public FlagsOffset(int offset, int count) { this.offset = offset; this.count = count; } } private class EntryData { public short mFlags; public int mSpecNamesId; public ResValue mValue; } private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); private static final int KNOWN_CONFIG_BYTES = 56; public static class ARSCData { public ARSCData(ResPackage[] packages, FlagsOffset[] flagsOffsets, ResTable resTable) { mPackages = packages; mFlagsOffsets = flagsOffsets; mResTable = resTable; } public FlagsOffset[] getFlagsOffsets() { return mFlagsOffsets; } public ResPackage[] getPackages() { return mPackages; } public ResPackage getOnePackage() throws AndrolibException { if (mPackages.length <= 0) { throw new AndrolibException("Arsc file contains zero packages"); } else if (mPackages.length != 1) { int id = findPackageWithMostResSpecs(); LOGGER.fine("Arsc file contains multiple packages. Using package " + mPackages[id].getName() + " as default."); return mPackages[id]; } return mPackages[0]; } public int findPackageWithMostResSpecs() { int count = mPackages[0].getResSpecCount(); int id = 0; for (int i = 0; i < mPackages.length; i++) { if (mPackages[i].getResSpecCount() >= count) { count = mPackages[i].getResSpecCount(); id = i; } } return id; } public ResTable getResTable() { return mResTable; } private final ResPackage[] mPackages; private final FlagsOffset[] mFlagsOffsets; private final ResTable mResTable; } }