mirror of
https://github.com/revanced/Apktool.git
synced 2025-05-06 08:44:30 +02:00
427 lines
13 KiB
Java
427 lines
13 KiB
Java
/**
|
|
* Copyright 2011 Ryszard Wiśniewski <brut.alll@gmail.com>
|
|
*
|
|
* 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;
|
|
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.mindprod.ledatastream.LEDataInputStream;
|
|
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 {
|
|
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.readTable();
|
|
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) {
|
|
if (storeFlagsOffsets) {
|
|
arscStream = mCountIn = new CountingInputStream(arscStream);
|
|
mFlagsOffsets = new ArrayList<FlagsOffset>();
|
|
} else {
|
|
mCountIn = null;
|
|
mFlagsOffsets = null;
|
|
}
|
|
mIn = new ExtDataInput(new LEDataInputStream(arscStream));
|
|
mResTable = resTable;
|
|
mKeepBroken = keepBroken;
|
|
}
|
|
|
|
private ResPackage[] readTable() 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++) {
|
|
packages[i] = readPackage();
|
|
}
|
|
|
|
return packages;
|
|
}
|
|
|
|
private ResPackage readPackage() throws IOException, AndrolibException {
|
|
checkChunkType(Header.TYPE_PACKAGE);
|
|
int id = (byte) mIn.readInt();
|
|
String name = mIn.readNulEndedString(128, true);
|
|
/*typeNameStrings*/ mIn.skipInt();
|
|
/*typeNameCount*/ mIn.skipInt();
|
|
/*specNameStrings*/ mIn.skipInt();
|
|
/*specNameCount*/ mIn.skipInt();
|
|
|
|
mTypeNames = StringBlock.read(mIn);
|
|
mSpecNames = StringBlock.read(mIn);
|
|
|
|
mResId = id << 24;
|
|
mPkg = new ResPackage(mResTable, id, name);
|
|
|
|
nextChunk();
|
|
while (mHeader.type == Header.TYPE_TYPE) {
|
|
readType();
|
|
}
|
|
|
|
return mPkg;
|
|
}
|
|
|
|
private ResType readType() throws AndrolibException, IOException {
|
|
checkChunkType(Header.TYPE_TYPE);
|
|
byte id = mIn.readByte();
|
|
mIn.skipBytes(3);
|
|
int entryCount = mIn.readInt();
|
|
|
|
mMissingResSpecs = new boolean[entryCount];
|
|
Arrays.fill(mMissingResSpecs, true);
|
|
|
|
if (mFlagsOffsets != null) {
|
|
mFlagsOffsets.add(new FlagsOffset(mCountIn.getCount(), entryCount));
|
|
}
|
|
/*flags*/ mIn.skipBytes(entryCount * 4);
|
|
|
|
mResId = (0xff000000 & mResId) | id << 16;
|
|
mType = new ResType(mTypeNames.getString(id - 1), mResTable, mPkg);
|
|
mPkg.addType(mType);
|
|
|
|
while (nextChunk().type == Header.TYPE_CONFIG) {
|
|
readConfig();
|
|
}
|
|
|
|
addMissingResSpecs();
|
|
|
|
return mType;
|
|
}
|
|
|
|
private ResConfig readConfig() throws IOException, AndrolibException {
|
|
checkChunkType(Header.TYPE_CONFIG);
|
|
/*typeId*/ mIn.skipInt();
|
|
int entryCount = mIn.readInt();
|
|
/*entriesStart*/ mIn.skipInt();
|
|
|
|
ResConfigFlags flags = readConfigFlags();
|
|
int[] entryOffsets = mIn.readIntArray(entryCount);
|
|
|
|
if (flags.isInvalid) {
|
|
String resName = mType.getName() + flags.getQualifiers();
|
|
if (mKeepBroken) {
|
|
LOGGER.warning(
|
|
"Invalid config flags detected: " + resName);
|
|
} else {
|
|
LOGGER.warning(
|
|
"Invalid config flags detected. Dropping resources: " + resName);
|
|
}
|
|
}
|
|
|
|
mConfig = flags.isInvalid && ! mKeepBroken ?
|
|
null : mPkg.getOrCreateConfig(flags);
|
|
|
|
for (int i = 0; i < entryOffsets.length; i++) {
|
|
if (entryOffsets[i] != -1) {
|
|
mMissingResSpecs[i] = false;
|
|
mResId = (mResId & 0xffff0000) | i;
|
|
readEntry();
|
|
}
|
|
}
|
|
|
|
return mConfig;
|
|
}
|
|
|
|
private void readEntry() throws IOException, AndrolibException {
|
|
/*size*/ mIn.skipBytes(2);
|
|
short flags = mIn.readShort();
|
|
int specNamesId = mIn.readInt();
|
|
|
|
ResValue value = (flags & ENTRY_FLAG_COMPLEX) == 0 ?
|
|
readValue() : readComplexEntry();
|
|
|
|
if (mConfig == null) {
|
|
return;
|
|
}
|
|
|
|
ResID resId = new ResID(mResId);
|
|
ResResSpec spec;
|
|
if (mPkg.hasResSpec(resId)) {
|
|
spec = mPkg.getResSpec(resId);
|
|
} else {
|
|
spec = new ResResSpec(
|
|
resId, mSpecNames.getString(specNamesId), mPkg, mType);
|
|
mPkg.addResSpec(spec);
|
|
mType.addResSpec(spec);
|
|
}
|
|
ResResource res = new ResResource(mConfig, spec, value);
|
|
|
|
mConfig.addResource(res);
|
|
spec.addResource(res);
|
|
mPkg.addResource(res);
|
|
}
|
|
|
|
private ResBagValue readComplexEntry() throws IOException,
|
|
AndrolibException {
|
|
int parent = mIn.readInt();
|
|
int count = mIn.readInt();
|
|
|
|
ResValueFactory factory = mPkg.getValueFactory();
|
|
Duo<Integer, ResScalarValue>[] items = new Duo[count];
|
|
for (int i = 0; i < count; i++) {
|
|
items[i] = new Duo<Integer, ResScalarValue>(
|
|
mIn.readInt(), (ResScalarValue) readValue());
|
|
}
|
|
|
|
return factory.bagFactory(parent, items);
|
|
}
|
|
|
|
private ResValue 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)) :
|
|
mPkg.getValueFactory().factory(type, data);
|
|
}
|
|
|
|
private ResConfigFlags readConfigFlags() throws IOException, AndrolibException {
|
|
int size = mIn.readInt();
|
|
if (size < 28) {
|
|
throw new AndrolibException("Config size < 28");
|
|
}
|
|
|
|
boolean isInvalid = false;
|
|
|
|
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()};
|
|
|
|
byte orientation = mIn.readByte();
|
|
byte touchscreen = mIn.readByte();
|
|
short density = mIn.readShort();
|
|
|
|
byte keyboard = mIn.readByte();
|
|
byte navigation = mIn.readByte();
|
|
byte inputFlags = mIn.readByte();
|
|
mIn.skipBytes(1);
|
|
|
|
short screenWidth = mIn.readShort();
|
|
short screenHeight = mIn.readShort();
|
|
|
|
short sdkVersion = mIn.readShort();
|
|
mIn.skipBytes(2);
|
|
|
|
byte screenLayout = 0;
|
|
byte uiMode = 0;
|
|
if (size >= 32) {
|
|
screenLayout = mIn.readByte();
|
|
uiMode = mIn.readByte();
|
|
mIn.skipBytes(2);
|
|
}
|
|
|
|
int exceedingSize = size - KNOWN_CONFIG_BYTES;
|
|
if (exceedingSize > 0) {
|
|
byte[] buf = new byte[exceedingSize];
|
|
mIn.readFully(buf);
|
|
BigInteger exceedingBI = new BigInteger(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. Exceeding bytes: %0" + (exceedingSize * 2) + "X.",
|
|
KNOWN_CONFIG_BYTES, exceedingBI));
|
|
isInvalid = true;
|
|
}
|
|
}
|
|
|
|
return new ResConfigFlags(mcc, mnc, language, country, orientation,
|
|
touchscreen, density, keyboard, navigation, inputFlags,
|
|
screenWidth, screenHeight, sdkVersion, screenLayout, uiMode,
|
|
isInvalid);
|
|
}
|
|
|
|
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),
|
|
String.format("APKTOOL_DUMMY_%04x", i), mPkg, mType);
|
|
mPkg.addResSpec(spec);
|
|
mType.addResSpec(spec);
|
|
|
|
ResValue value = new ResBoolValue(false);
|
|
ResResource res = new ResResource(
|
|
mPkg.getOrCreateConfig(new ResConfigFlags()), spec, value);
|
|
mPkg.addResource(res);
|
|
mConfig.addResource(res);
|
|
spec.addResource(res);
|
|
}
|
|
}
|
|
|
|
private Header nextChunk() throws IOException {
|
|
return mHeader = Header.read(mIn);
|
|
}
|
|
|
|
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<FlagsOffset> mFlagsOffsets;
|
|
private final boolean mKeepBroken;
|
|
|
|
private Header mHeader;
|
|
private StringBlock mTableStrings;
|
|
private StringBlock mTypeNames;
|
|
private StringBlock mSpecNames;
|
|
private ResPackage mPkg;
|
|
private ResType mType;
|
|
private ResConfig mConfig;
|
|
private int mResId;
|
|
private boolean[] mMissingResSpecs;
|
|
|
|
|
|
private final static short ENTRY_FLAG_COMPLEX = 0x0001;
|
|
|
|
|
|
public static class Header {
|
|
public final short type;
|
|
public final int chunkSize;
|
|
|
|
public Header(short type, int size) {
|
|
this.type = type;
|
|
this.chunkSize = size;
|
|
}
|
|
|
|
public static Header read(ExtDataInput in) throws IOException {
|
|
short type;
|
|
try {
|
|
type = in.readShort();
|
|
} catch (EOFException ex) {
|
|
return new Header(TYPE_NONE, 0);
|
|
}
|
|
in.skipBytes(2);
|
|
return new Header(type, in.readInt());
|
|
}
|
|
|
|
public final static short
|
|
TYPE_NONE = -1,
|
|
TYPE_TABLE = 0x0002,
|
|
TYPE_PACKAGE = 0x0200,
|
|
TYPE_TYPE = 0x0202,
|
|
TYPE_CONFIG = 0x0201;
|
|
}
|
|
|
|
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 static final Logger LOGGER =
|
|
Logger.getLogger(ARSCDecoder.class.getName());
|
|
private static final int KNOWN_CONFIG_BYTES = 32;
|
|
|
|
|
|
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 != 1) {
|
|
throw new AndrolibException(
|
|
"Arsc file contains zero or multiple packages");
|
|
}
|
|
return mPackages[0];
|
|
}
|
|
|
|
public ResTable getResTable() {
|
|
return mResTable;
|
|
}
|
|
|
|
private final ResPackage[] mPackages;
|
|
private final FlagsOffset[] mFlagsOffsets;
|
|
private final ResTable mResTable;
|
|
}
|
|
}
|