2019-10-16 07:49:58 +02:00
|
|
|
/*
|
2019-07-13 18:19:41 +02:00
|
|
|
* Copyright (C) 2019 Ryszard Wiśniewski <brut.alll@gmail.com>
|
|
|
|
* Copyright (C) 2019 Connor Tumbleson <connor.tumbleson@gmail.com>
|
2012-09-20 03:27:35 +02:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
* http://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;
|
2018-03-05 16:28:50 +01:00
|
|
|
import brut.androlib.Androlib;
|
2012-09-20 03:27:35 +02:00
|
|
|
import brut.androlib.AndrolibException;
|
|
|
|
import brut.androlib.res.data.*;
|
|
|
|
import brut.androlib.res.data.value.*;
|
|
|
|
import brut.util.Duo;
|
|
|
|
import brut.androlib.res.data.ResTable;
|
|
|
|
import brut.util.ExtDataInput;
|
2016-08-05 15:17:19 +02:00
|
|
|
import com.google.common.io.LittleEndianDataInputStream;
|
2012-09-20 03:27:35 +02:00
|
|
|
import java.io.*;
|
|
|
|
import java.math.BigInteger;
|
|
|
|
import java.util.*;
|
|
|
|
import java.util.logging.Logger;
|
|
|
|
import org.apache.commons.io.input.CountingInputStream;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @author Ryszard Wiśniewski <brut.alll@gmail.com>
|
|
|
|
*/
|
|
|
|
public class ARSCDecoder {
|
2014-10-05 19:09:57 +02:00
|
|
|
public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken)
|
2014-02-10 02:01:57 +01:00
|
|
|
throws AndrolibException {
|
|
|
|
return decode(arscStream, findFlagsOffsets, keepBroken, new ResTable());
|
|
|
|
}
|
|
|
|
|
2014-10-05 19:09:57 +02:00
|
|
|
public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken,
|
|
|
|
ResTable resTable)
|
2014-02-10 02:01:57 +01:00
|
|
|
throws AndrolibException {
|
|
|
|
try {
|
2014-10-05 19:09:57 +02:00
|
|
|
ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken);
|
2015-12-08 03:48:57 +01:00
|
|
|
ResPackage[] pkgs = decoder.readTableHeader();
|
2014-10-05 19:09:57 +02:00
|
|
|
return new ARSCData(pkgs, decoder.mFlagsOffsets == null
|
|
|
|
? null
|
|
|
|
: decoder.mFlagsOffsets.toArray(new FlagsOffset[0]), resTable);
|
2014-02-10 02:01:57 +01:00
|
|
|
} catch (IOException ex) {
|
|
|
|
throw new AndrolibException("Could not decode arsc file", ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-05 19:09:57 +02:00
|
|
|
private ARSCDecoder(InputStream arscStream, ResTable resTable, boolean storeFlagsOffsets, boolean keepBroken) {
|
2016-04-29 15:13:16 +02:00
|
|
|
arscStream = mCountIn = new CountingInputStream(arscStream);
|
2014-02-10 02:01:57 +01:00
|
|
|
if (storeFlagsOffsets) {
|
|
|
|
mFlagsOffsets = new ArrayList<FlagsOffset>();
|
|
|
|
} else {
|
|
|
|
mFlagsOffsets = null;
|
|
|
|
}
|
2016-08-05 15:17:19 +02:00
|
|
|
// 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));
|
2014-02-10 02:01:57 +01:00
|
|
|
mResTable = resTable;
|
|
|
|
mKeepBroken = keepBroken;
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
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++) {
|
2017-12-13 19:16:48 +01:00
|
|
|
mTypeIdOffset = 0;
|
2015-12-08 03:48:57 +01:00
|
|
|
packages[i] = readTablePackage();
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
return packages;
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
private ResPackage readTablePackage() throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
checkChunkType(Header.TYPE_PACKAGE);
|
2018-03-05 16:28:50 +01:00
|
|
|
int id = mIn.readInt();
|
2014-12-25 16:56:25 +01:00
|
|
|
|
|
|
|
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;
|
2015-05-05 14:43:48 +02:00
|
|
|
if (mResTable.getPackageOriginal() == null && mResTable.getPackageRenamed() == null) {
|
|
|
|
mResTable.setSharedLibrary(true);
|
|
|
|
}
|
2014-12-25 16:56:25 +01:00
|
|
|
}
|
|
|
|
|
2014-05-13 00:11:28 +02:00
|
|
|
String name = mIn.readNullEndedString(128, true);
|
2015-12-08 03:48:57 +01:00
|
|
|
/* typeStrings */mIn.skipInt();
|
|
|
|
/* lastPublicType */mIn.skipInt();
|
|
|
|
/* keyStrings */mIn.skipInt();
|
|
|
|
/* lastPublicKey */mIn.skipInt();
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2017-12-13 19:16:48 +01:00
|
|
|
// 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");
|
|
|
|
}
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
mTypeNames = StringBlock.read(mIn);
|
|
|
|
mSpecNames = StringBlock.read(mIn);
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
mResId = id << 24;
|
|
|
|
mPkg = new ResPackage(mResTable, id, name);
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
nextChunk();
|
2014-12-25 16:56:25 +01:00
|
|
|
while (mHeader.type == Header.TYPE_LIBRARY) {
|
|
|
|
readLibraryType();
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
while (mHeader.type == Header.TYPE_SPEC_TYPE) {
|
|
|
|
readTableTypeSpec();
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
return mPkg;
|
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-12-25 16:56:25 +01:00
|
|
|
private void readLibraryType() throws AndrolibException, IOException {
|
|
|
|
checkChunkType(Header.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.info(String.format("Decoding Shared Library (%s), pkgId: %d", packageName, packageId));
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
while(nextChunk().type == Header.TYPE_TYPE) {
|
|
|
|
readTableTypeSpec();
|
2014-12-25 16:56:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-16 00:04:23 +01:00
|
|
|
private void readTableTypeSpec() throws AndrolibException, IOException {
|
2015-12-14 14:03:09 +01:00
|
|
|
mTypeSpec = readSingleTableTypeSpec();
|
|
|
|
addTypeSpec(mTypeSpec);
|
|
|
|
|
|
|
|
int type = nextChunk().type;
|
|
|
|
ResTypeSpec resTypeSpec;
|
|
|
|
|
|
|
|
while (type == Header.TYPE_SPEC_TYPE) {
|
|
|
|
resTypeSpec = readSingleTableTypeSpec();
|
|
|
|
addTypeSpec(resTypeSpec);
|
|
|
|
type = nextChunk().type;
|
2018-02-16 00:04:23 +01:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
2015-12-14 14:03:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
while (type == Header.TYPE_TYPE) {
|
|
|
|
readTableType();
|
2017-10-20 23:10:32 +02:00
|
|
|
|
2016-09-17 19:13:19 +02:00
|
|
|
// skip "TYPE 8 chunks" and/or padding data at the end of this chunk
|
2017-07-26 22:21:59 +02:00
|
|
|
if (mCountIn.getCount() < mHeader.endPosition) {
|
2017-12-13 19:16:48 +01:00
|
|
|
LOGGER.warning("Unknown data detected. Skipping: " + (mHeader.endPosition - mCountIn.getCount()) + " byte(s)");
|
2016-09-17 19:13:19 +02:00
|
|
|
mCountIn.skip(mHeader.endPosition - mCountIn.getCount());
|
|
|
|
}
|
2017-10-20 23:10:32 +02:00
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
type = nextChunk().type;
|
|
|
|
|
|
|
|
addMissingResSpecs();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private ResTypeSpec readSingleTableTypeSpec() throws AndrolibException, IOException {
|
2015-12-08 03:48:57 +01:00
|
|
|
checkChunkType(Header.TYPE_SPEC_TYPE);
|
2016-08-02 05:26:00 +02:00
|
|
|
int id = mIn.readUnsignedByte();
|
2014-02-10 02:01:57 +01:00
|
|
|
mIn.skipBytes(3);
|
|
|
|
int entryCount = mIn.readInt();
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
if (mFlagsOffsets != null) {
|
|
|
|
mFlagsOffsets.add(new FlagsOffset(mCountIn.getCount(), entryCount));
|
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
/* flags */mIn.skipBytes(entryCount * 4);
|
|
|
|
mTypeSpec = new ResTypeSpec(mTypeNames.getString(id - 1), mResTable, mPkg, id, entryCount);
|
2015-12-08 03:48:57 +01:00
|
|
|
mPkg.addType(mTypeSpec);
|
|
|
|
return mTypeSpec;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
private ResType readTableType() throws IOException, AndrolibException {
|
|
|
|
checkChunkType(Header.TYPE_TYPE);
|
2017-12-13 19:16:48 +01:00
|
|
|
int typeId = mIn.readUnsignedByte() - mTypeIdOffset;
|
2015-12-14 14:03:09 +01:00
|
|
|
if (mResTypeSpecs.containsKey(typeId)) {
|
|
|
|
mResId = (0xff000000 & mResId) | mResTypeSpecs.get(typeId).getId() << 16;
|
|
|
|
mTypeSpec = mResTypeSpecs.get(typeId);
|
|
|
|
}
|
|
|
|
|
2017-08-23 20:14:48 +02:00
|
|
|
int typeFlags = mIn.readByte();
|
|
|
|
/* reserved */mIn.skipBytes(2);
|
2014-02-10 02:01:57 +01:00
|
|
|
int entryCount = mIn.readInt();
|
2016-04-29 15:13:16 +02:00
|
|
|
int entriesStart = mIn.readInt();
|
2015-12-14 14:03:09 +01:00
|
|
|
mMissingResSpecs = new boolean[entryCount];
|
|
|
|
Arrays.fill(mMissingResSpecs, true);
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
ResConfigFlags flags = readConfigFlags();
|
2016-04-29 15:13:16 +02:00
|
|
|
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()) {
|
2017-12-13 19:16:48 +01:00
|
|
|
LOGGER.warning("Invalid data detected. Skipping: " + (position - mCountIn.getCount()) + " byte(s)");
|
2016-04-29 15:13:16 +02:00
|
|
|
mIn.skipBytes(position - mCountIn.getCount());
|
|
|
|
}
|
2017-08-23 20:43:57 +02:00
|
|
|
|
|
|
|
if (typeFlags == 1) {
|
|
|
|
LOGGER.info("Sparse type flags detected: " + mTypeSpec.getName());
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
int[] entryOffsets = mIn.readIntArray(entryCount);
|
|
|
|
|
|
|
|
if (flags.isInvalid) {
|
2015-12-08 03:48:57 +01:00
|
|
|
String resName = mTypeSpec.getName() + flags.getQualifiers();
|
2014-02-10 02:01:57 +01:00
|
|
|
if (mKeepBroken) {
|
|
|
|
LOGGER.warning("Invalid config flags detected: " + resName);
|
|
|
|
} else {
|
2014-10-05 19:09:57 +02:00
|
|
|
LOGGER.warning("Invalid config flags detected. Dropping resources: " + resName);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
mType = flags.isInvalid && !mKeepBroken ? null : mPkg.getOrCreateConfig(flags);
|
2017-12-08 22:38:31 +01:00
|
|
|
HashMap<Integer, EntryData> offsetsToEntryData = new HashMap<Integer, EntryData>();
|
2017-10-20 23:10:32 +02:00
|
|
|
|
|
|
|
for (int offset : entryOffsets) {
|
2017-12-08 22:38:31 +01:00
|
|
|
if (offset == -1 || offsetsToEntryData.containsKey(offset)) {
|
|
|
|
continue;
|
|
|
|
}
|
2017-10-20 23:10:32 +02:00
|
|
|
|
2017-12-08 22:38:31 +01:00
|
|
|
offsetsToEntryData.put(offset, readEntryData());
|
2017-10-20 23:10:32 +02:00
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
|
|
|
|
for (int i = 0; i < entryOffsets.length; i++) {
|
|
|
|
if (entryOffsets[i] != -1) {
|
|
|
|
mMissingResSpecs[i] = false;
|
|
|
|
mResId = (mResId & 0xffff0000) | i;
|
2017-10-20 23:10:32 +02:00
|
|
|
EntryData entryData = offsetsToEntryData.get(entryOffsets[i]);
|
|
|
|
readEntry(entryData);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
return mType;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2017-10-20 23:10:32 +02:00
|
|
|
|
|
|
|
private EntryData readEntryData() throws IOException, AndrolibException {
|
2016-04-29 15:13:16 +02:00
|
|
|
short size = mIn.readShort();
|
|
|
|
if (size < 0) {
|
|
|
|
throw new AndrolibException("Entry size is under 0 bytes.");
|
|
|
|
}
|
2017-10-20 23:10:32 +02:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
short flags = mIn.readShort();
|
|
|
|
int specNamesId = mIn.readInt();
|
2014-10-05 19:09:57 +02:00
|
|
|
ResValue value = (flags & ENTRY_FLAG_COMPLEX) == 0 ? readValue() : readComplexEntry();
|
2017-10-20 23:10:32 +02:00
|
|
|
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;
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
if (mTypeSpec.isString() && value instanceof ResFileValue) {
|
2015-07-07 22:42:43 +02:00
|
|
|
value = new ResStringValue(value.toString(), ((ResFileValue) value).getRawIntValue());
|
2015-05-14 20:22:43 +02:00
|
|
|
}
|
2015-12-08 03:48:57 +01:00
|
|
|
if (mType == null) {
|
2014-02-10 02:01:57 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ResID resId = new ResID(mResId);
|
|
|
|
ResResSpec spec;
|
|
|
|
if (mPkg.hasResSpec(resId)) {
|
|
|
|
spec = mPkg.getResSpec(resId);
|
2015-12-14 14:03:09 +01:00
|
|
|
|
|
|
|
if (spec.isDummyResSpec()) {
|
|
|
|
removeResSpec(spec);
|
|
|
|
|
|
|
|
spec = new ResResSpec(resId, mSpecNames.getString(specNamesId), mPkg, mTypeSpec);
|
|
|
|
mPkg.addResSpec(spec);
|
|
|
|
mTypeSpec.addResSpec(spec);
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
} else {
|
2015-12-08 03:48:57 +01:00
|
|
|
spec = new ResResSpec(resId, mSpecNames.getString(specNamesId), mPkg, mTypeSpec);
|
2014-02-10 02:01:57 +01:00
|
|
|
mPkg.addResSpec(spec);
|
2015-12-08 03:48:57 +01:00
|
|
|
mTypeSpec.addResSpec(spec);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2015-12-08 03:48:57 +01:00
|
|
|
ResResource res = new ResResource(mType, spec, value);
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2016-02-15 17:13:49 +01:00
|
|
|
try {
|
2016-02-16 14:18:26 +01:00
|
|
|
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.toString()));
|
|
|
|
} else {
|
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
mPkg.addResource(res);
|
|
|
|
}
|
|
|
|
|
2015-12-08 03:48:57 +01:00
|
|
|
private ResBagValue readComplexEntry() throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
int parent = mIn.readInt();
|
|
|
|
int count = mIn.readInt();
|
|
|
|
|
|
|
|
ResValueFactory factory = mPkg.getValueFactory();
|
|
|
|
Duo<Integer, ResScalarValue>[] items = new Duo[count];
|
2015-07-07 22:42:43 +02:00
|
|
|
ResIntBasedValue resValue;
|
2015-05-14 20:22:43 +02:00
|
|
|
int resId;
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
for (int i = 0; i < count; i++) {
|
2015-05-14 20:22:43 +02:00
|
|
|
resId = mIn.readInt();
|
|
|
|
resValue = readValue();
|
|
|
|
|
2015-07-07 22:42:43 +02:00
|
|
|
if (resValue instanceof ResScalarValue) {
|
2015-05-14 20:22:43 +02:00
|
|
|
items[i] = new Duo<Integer, ResScalarValue>(resId, (ResScalarValue) resValue);
|
2015-07-07 22:42:43 +02:00
|
|
|
} else {
|
|
|
|
resValue = new ResStringValue(resValue.toString(), resValue.getRawIntValue());
|
2015-05-14 20:22:43 +02:00
|
|
|
items[i] = new Duo<Integer, ResScalarValue>(resId, (ResScalarValue) resValue);
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2019-01-29 09:12:35 +01:00
|
|
|
return factory.bagFactory(parent, items, mTypeSpec);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2015-07-07 22:42:43 +02:00
|
|
|
private ResIntBasedValue readValue() throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
/* size */mIn.skipCheckShort((short) 8);
|
|
|
|
/* zero */mIn.skipCheckByte((byte) 0);
|
|
|
|
byte type = mIn.readByte();
|
|
|
|
int data = mIn.readInt();
|
|
|
|
|
2014-10-05 19:09:57 +02:00
|
|
|
return type == TypedValue.TYPE_STRING
|
2015-07-07 22:42:43 +02:00
|
|
|
? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data)
|
2014-10-05 19:09:57 +02:00
|
|
|
: mPkg.getValueFactory().factory(type, data, null);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2015-12-14 14:10:14 +01:00
|
|
|
private ResConfigFlags readConfigFlags() throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
int size = mIn.readInt();
|
2015-11-17 07:31:17 +01:00
|
|
|
int read = 28;
|
2015-03-22 13:09:00 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
if (size < 28) {
|
|
|
|
throw new AndrolibException("Config size < 28");
|
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
boolean isInvalid = false;
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
short mcc = mIn.readShort();
|
|
|
|
short mnc = mIn.readShort();
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2015-02-17 18:48:39 +01:00
|
|
|
char[] language = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a');
|
|
|
|
char[] country = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0');
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
byte orientation = mIn.readByte();
|
|
|
|
byte touchscreen = mIn.readByte();
|
2013-03-31 16:05:07 +02:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
int density = mIn.readUnsignedShort();
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
byte keyboard = mIn.readByte();
|
|
|
|
byte navigation = mIn.readByte();
|
|
|
|
byte inputFlags = mIn.readByte();
|
2013-02-13 04:12:17 +01:00
|
|
|
/* inputPad0 */mIn.skipBytes(1);
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
short screenWidth = mIn.readShort();
|
|
|
|
short screenHeight = mIn.readShort();
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
short sdkVersion = mIn.readShort();
|
2013-02-13 04:12:17 +01:00
|
|
|
/* minorVersion, now must always be 0 */mIn.skipBytes(2);
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
byte screenLayout = 0;
|
|
|
|
byte uiMode = 0;
|
|
|
|
short smallestScreenWidthDp = 0;
|
|
|
|
if (size >= 32) {
|
|
|
|
screenLayout = mIn.readByte();
|
|
|
|
uiMode = mIn.readByte();
|
|
|
|
smallestScreenWidthDp = mIn.readShort();
|
2015-03-22 13:09:00 +01:00
|
|
|
read = 32;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
short screenWidthDp = 0;
|
|
|
|
short screenHeightDp = 0;
|
|
|
|
if (size >= 36) {
|
|
|
|
screenWidthDp = mIn.readShort();
|
|
|
|
screenHeightDp = mIn.readShort();
|
2015-03-22 13:09:00 +01:00
|
|
|
read = 36;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2015-04-11 19:39:38 +02:00
|
|
|
char[] localeScript = null;
|
|
|
|
char[] localeVariant = null;
|
2015-02-19 16:05:00 +01:00
|
|
|
if (size >= 48) {
|
2015-04-11 19:39:38 +02:00
|
|
|
localeScript = readScriptOrVariantChar(4).toCharArray();
|
|
|
|
localeVariant = readScriptOrVariantChar(8).toCharArray();
|
2015-03-22 13:09:00 +01:00
|
|
|
read = 48;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2015-10-08 13:58:11 +02:00
|
|
|
byte screenLayout2 = 0;
|
2017-08-23 23:03:51 +02:00
|
|
|
byte colorMode = 0;
|
2015-10-08 13:58:11 +02:00
|
|
|
if (size >= 52) {
|
|
|
|
screenLayout2 = mIn.readByte();
|
2017-08-23 23:03:51 +02:00
|
|
|
colorMode = mIn.readByte();
|
2017-08-23 19:53:30 +02:00
|
|
|
mIn.skipBytes(2); // reserved padding
|
2015-10-08 13:58:11 +02:00
|
|
|
read = 52;
|
|
|
|
}
|
|
|
|
|
2016-04-18 14:06:52 +02:00
|
|
|
if (size >= 56) {
|
|
|
|
mIn.skipBytes(4);
|
|
|
|
read = 56;
|
|
|
|
}
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
int exceedingSize = size - KNOWN_CONFIG_BYTES;
|
|
|
|
if (exceedingSize > 0) {
|
|
|
|
byte[] buf = new byte[exceedingSize];
|
2015-05-22 16:07:15 +02:00
|
|
|
read += exceedingSize;
|
2014-02-10 02:01:57 +01:00
|
|
|
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 {
|
2016-07-21 14:29:06 +02:00
|
|
|
LOGGER.warning(String.format("Config flags size > %d. Size = %d. Exceeding bytes: 0x%X.",
|
|
|
|
KNOWN_CONFIG_BYTES, size, exceedingBI));
|
2014-02-10 02:01:57 +01:00
|
|
|
isInvalid = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-22 13:09:00 +01:00
|
|
|
int remainingSize = size - read;
|
|
|
|
if (remainingSize > 0) {
|
|
|
|
mIn.skipBytes(remainingSize);
|
|
|
|
}
|
|
|
|
|
2015-02-19 16:05:00 +01:00
|
|
|
return new ResConfigFlags(mcc, mnc, language, country,
|
2014-02-10 02:01:57 +01:00
|
|
|
orientation, touchscreen, density, keyboard, navigation,
|
|
|
|
inputFlags, screenWidth, screenHeight, sdkVersion,
|
|
|
|
screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp,
|
2017-08-23 19:53:30 +02:00
|
|
|
screenHeightDp, localeScript, localeVariant, screenLayout2,
|
2017-08-23 23:03:51 +02:00
|
|
|
colorMode, isInvalid, size);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2015-02-17 18:48:39 +01:00
|
|
|
private char[] unpackLanguageOrRegion(byte in0, byte in1, char base) throws AndrolibException {
|
2015-02-19 16:05:00 +01:00
|
|
|
// 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;
|
2015-02-17 18:48:39 +01:00
|
|
|
}
|
2015-02-19 16:05:00 +01:00
|
|
|
string.append((char) ch);
|
2015-02-17 18:48:39 +01:00
|
|
|
}
|
2015-02-19 16:05:00 +01:00
|
|
|
mIn.skipBytes(length);
|
|
|
|
|
|
|
|
return string.toString();
|
2015-02-17 18:48:39 +01:00
|
|
|
}
|
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
private void addTypeSpec(ResTypeSpec resTypeSpec) {
|
|
|
|
mResTypeSpecs.put(resTypeSpec.getId(), resTypeSpec);
|
|
|
|
}
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
private void addMissingResSpecs() throws AndrolibException {
|
|
|
|
int resId = mResId & 0xffff0000;
|
|
|
|
|
|
|
|
for (int i = 0; i < mMissingResSpecs.length; i++) {
|
|
|
|
if (!mMissingResSpecs[i]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2016-03-07 20:33:07 +01:00
|
|
|
ResResSpec spec = new ResResSpec(new ResID(resId | i), "APKTOOL_DUMMY_" + Integer.toHexString(i), mPkg, mTypeSpec);
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
// 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());
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
ResValue value = new ResBoolValue(false, 0, null);
|
|
|
|
ResResource res = new ResResource(mType, spec, value);
|
|
|
|
|
|
|
|
mPkg.addResource(res);
|
|
|
|
mType.addResource(res);
|
|
|
|
spec.addResource(res);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2015-12-14 14:03:09 +01:00
|
|
|
private void removeResSpec(ResResSpec spec) throws AndrolibException {
|
|
|
|
if (mPkg.hasResSpec(spec.getId())) {
|
|
|
|
mPkg.removeResSpec(spec);
|
|
|
|
mTypeSpec.removeResSpec(spec);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private Header nextChunk() throws IOException {
|
2016-04-29 15:13:16 +02:00
|
|
|
return mHeader = Header.read(mIn, mCountIn);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void checkChunkType(int expectedType) throws AndrolibException {
|
|
|
|
if (mHeader.type != expectedType) {
|
2014-10-05 19:09:57 +02:00
|
|
|
throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x",
|
2014-02-10 02:01:57 +01:00
|
|
|
expectedType, mHeader.type));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-14 14:10:14 +01:00
|
|
|
private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException {
|
2014-02-10 02:01:57 +01:00
|
|
|
nextChunk();
|
|
|
|
checkChunkType(expectedType);
|
|
|
|
}
|
|
|
|
|
|
|
|
private final ExtDataInput mIn;
|
|
|
|
private final ResTable mResTable;
|
|
|
|
private final CountingInputStream mCountIn;
|
|
|
|
private final List<FlagsOffset> mFlagsOffsets;
|
|
|
|
private final boolean mKeepBroken;
|
|
|
|
|
|
|
|
private Header mHeader;
|
|
|
|
private StringBlock mTableStrings;
|
|
|
|
private StringBlock mTypeNames;
|
|
|
|
private StringBlock mSpecNames;
|
|
|
|
private ResPackage mPkg;
|
2015-12-08 03:48:57 +01:00
|
|
|
private ResTypeSpec mTypeSpec;
|
2014-02-10 02:01:57 +01:00
|
|
|
private ResType mType;
|
|
|
|
private int mResId;
|
2017-12-13 19:16:48 +01:00
|
|
|
private int mTypeIdOffset = 0;
|
2014-02-10 02:01:57 +01:00
|
|
|
private boolean[] mMissingResSpecs;
|
2016-08-02 05:26:00 +02:00
|
|
|
private HashMap<Integer, ResTypeSpec> mResTypeSpecs = new HashMap<>();
|
2014-02-10 02:01:57 +01:00
|
|
|
|
|
|
|
private final static short ENTRY_FLAG_COMPLEX = 0x0001;
|
2017-07-26 22:21:59 +02:00
|
|
|
private final static short ENTRY_FLAG_PUBLIC = 0x0002;
|
|
|
|
private final static short ENTRY_FLAG_WEAK = 0x0004;
|
2014-02-10 02:01:57 +01:00
|
|
|
|
|
|
|
public static class Header {
|
|
|
|
public final short type;
|
2015-12-07 14:34:56 +01:00
|
|
|
public final int headerSize;
|
2014-02-10 02:01:57 +01:00
|
|
|
public final int chunkSize;
|
2016-04-29 15:13:16 +02:00
|
|
|
public final int startPosition;
|
|
|
|
public final int endPosition;
|
2014-02-10 02:01:57 +01:00
|
|
|
|
2016-04-29 15:13:16 +02:00
|
|
|
public Header(short type, int headerSize, int chunkSize, int headerStart) {
|
2014-02-10 02:01:57 +01:00
|
|
|
this.type = type;
|
2015-12-07 14:34:56 +01:00
|
|
|
this.headerSize = headerSize;
|
|
|
|
this.chunkSize = chunkSize;
|
2016-04-29 15:13:16 +02:00
|
|
|
this.startPosition = headerStart;
|
|
|
|
this.endPosition = headerStart + chunkSize;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
2016-04-29 15:13:16 +02:00
|
|
|
public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException {
|
2014-02-10 02:01:57 +01:00
|
|
|
short type;
|
2016-04-29 15:13:16 +02:00
|
|
|
int start = countIn.getCount();
|
2014-02-10 02:01:57 +01:00
|
|
|
try {
|
|
|
|
type = in.readShort();
|
|
|
|
} catch (EOFException ex) {
|
2016-04-29 15:13:16 +02:00
|
|
|
return new Header(TYPE_NONE, 0, 0, countIn.getCount());
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
2016-04-29 15:13:16 +02:00
|
|
|
return new Header(type, in.readShort(), in.readInt(), start);
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public final static short TYPE_NONE = -1, TYPE_TABLE = 0x0002,
|
2015-12-08 03:48:57 +01:00
|
|
|
TYPE_PACKAGE = 0x0200, TYPE_TYPE = 0x0201, TYPE_SPEC_TYPE = 0x0202, TYPE_LIBRARY = 0x0203;
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public static class FlagsOffset {
|
|
|
|
public final int offset;
|
|
|
|
public final int count;
|
|
|
|
|
|
|
|
public FlagsOffset(int offset, int count) {
|
|
|
|
this.offset = offset;
|
|
|
|
this.count = count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-08 22:38:31 +01:00
|
|
|
private class EntryData {
|
|
|
|
public short mFlags;
|
|
|
|
public int mSpecNamesId;
|
|
|
|
public ResValue mValue;
|
|
|
|
}
|
|
|
|
|
2014-10-05 19:09:57 +02:00
|
|
|
private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName());
|
2016-04-18 14:06:52 +02:00
|
|
|
private static final int KNOWN_CONFIG_BYTES = 56;
|
2014-02-10 02:01:57 +01:00
|
|
|
|
|
|
|
public static class ARSCData {
|
|
|
|
|
2014-10-05 19:09:57 +02:00
|
|
|
public ARSCData(ResPackage[] packages, FlagsOffset[] flagsOffsets, ResTable resTable) {
|
2014-02-10 02:01:57 +01:00
|
|
|
mPackages = packages;
|
|
|
|
mFlagsOffsets = flagsOffsets;
|
|
|
|
mResTable = resTable;
|
|
|
|
}
|
|
|
|
|
|
|
|
public FlagsOffset[] getFlagsOffsets() {
|
|
|
|
return mFlagsOffsets;
|
|
|
|
}
|
|
|
|
|
|
|
|
public ResPackage[] getPackages() {
|
|
|
|
return mPackages;
|
|
|
|
}
|
|
|
|
|
|
|
|
public ResPackage getOnePackage() throws AndrolibException {
|
2013-10-12 22:40:06 +02:00
|
|
|
if (mPackages.length <= 0) {
|
2014-10-05 19:09:57 +02:00
|
|
|
throw new AndrolibException("Arsc file contains zero packages");
|
2013-10-12 22:40:06 +02:00
|
|
|
} else if (mPackages.length != 1) {
|
|
|
|
int id = findPackageWithMostResSpecs();
|
2014-10-05 19:09:57 +02:00
|
|
|
LOGGER.info("Arsc file contains multiple packages. Using package "
|
|
|
|
+ mPackages[id].getName() + " as default.");
|
|
|
|
|
2013-10-12 22:40:06 +02:00
|
|
|
return mPackages[id];
|
2014-02-10 02:01:57 +01:00
|
|
|
}
|
|
|
|
return mPackages[0];
|
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2013-10-12 22:40:06 +02:00
|
|
|
public int findPackageWithMostResSpecs() {
|
2015-12-08 03:48:57 +01:00
|
|
|
int count = mPackages[0].getResSpecCount();
|
2013-10-12 22:40:06 +02:00
|
|
|
int id = 0;
|
|
|
|
|
|
|
|
for (int i = 0; i < mPackages.length; i++) {
|
|
|
|
if (mPackages[i].getResSpecCount() >= count) {
|
|
|
|
count = mPackages[i].getResSpecCount();
|
|
|
|
id = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
public ResTable getResTable() {
|
|
|
|
return mResTable;
|
|
|
|
}
|
2013-02-13 04:12:17 +01:00
|
|
|
|
2014-02-10 02:01:57 +01:00
|
|
|
private final ResPackage[] mPackages;
|
|
|
|
private final FlagsOffset[] mFlagsOffsets;
|
|
|
|
private final ResTable mResTable;
|
|
|
|
}
|
2013-03-17 15:37:46 +01:00
|
|
|
}
|