/*
* Copyright (c) 2009-2013 Panxiaobo
*
* 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 pxb.android.arsc;
import pxb.android.ResConst;
import pxb.android.StringItems;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Read the resources.arsc inside an Android apk.
* <p/>
* Usage:
* <p/>
* <pre>
* byte[] oldArscFile= ... ; //
* List<Pkg> pkgs = new ArscParser(oldArscFile).parse(); // read the file
* modify(pkgs); // do what you want here
* byte[] newArscFile = new ArscWriter(pkgs).toByteArray(); // build a new file
* </pre>
* <p/>
* The format of arsc is described here (gingerbread)
* <ul>
* <li>frameworks/base/libs/utils/ResourceTypes.cpp</li>
* <li>frameworks/base/include/utils/ResourceTypes.h</li>
* </ul>
* and the cmd line <code>aapt d resources abc.apk</code> is also good for debug
* (available in android sdk)
* <p/>
* <p/>
* Todos:
* <ul>
* TODO add support to read styled strings
* </ul>
* <p/>
* <p/>
* Thanks to the the following projects
* <ul>
* <li>android4me https://code.google.com/p/android4me/</li>
* <li>Apktool https://code.google.com/p/android-apktool</li>
* <li>Android http://source.android.com/</li>
* </ul>
*
* @author bob
*/
@SuppressWarnings("UnusedDeclaration")
public class ArscParser implements ResConst {
public static final int TYPE_STRING = 0x03;
/**
* If set, this resource has been declared public, so libraries are allowed
* to reference it.
*/
static final int ENGRY_FLAG_PUBLIC = 0x0002;
/**
* If set, this is a complex entry, holding a set of name/value mappings. It
* is followed by an array of ResTable_map structures.
*/
final static short ENTRY_FLAG_COMPLEX = 0x0001;
private static final boolean DEBUG = false;
private int fileSize = -1;
private ByteBuffer in;
private String[] keyNamesX;
private Pkg pkg;
private List<Pkg> pkgs = new ArrayList<>();
private String[] strings;
private String[] typeNamesX;
public ArscParser(byte[] b) {
this.in = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
}
private static void D(String fmt, Object... args) {
if (DEBUG) {
System.out.println(String.format(fmt, args));
}
}
public List<Pkg> parse() throws IOException {
if (fileSize < 0) {
Chunk head = new Chunk();
if (head.type != RES_TABLE_TYPE) {
throw new RuntimeException();
}
fileSize = head.size;
in.getInt();// packagecount
}
boolean flag = false;
while (in.hasRemaining()) {
Chunk chunk = new Chunk();
switch (chunk.type) {
case RES_STRING_POOL_TYPE:
strings = StringItems.read(in);
if (DEBUG) {
for (int i = 0; i < strings.length; i++) {
D("STR [%08x] %s", i, strings[i]);
}
}
break;
case RES_TABLE_PACKAGE_TYPE:
readPackage(in);
default:
flag = true;
}
if (flag) {
break;
}
in.position(chunk.location + chunk.size);
}
return pkgs;
}
private void readEntry(Config config, ResSpec spec) {
D("[%08x]read ResTable_entry", in.position());
int size = in.getShort();
D("ResTable_entry %d", size);
int flags = in.getShort(); // ENTRY_FLAG_PUBLIC
int keyStr = in.getInt();
spec.updateName(keyNamesX[keyStr]);
ResEntry resEntry = new ResEntry(flags, spec);
if (0 != (flags & ENTRY_FLAG_COMPLEX)) {
int parent = in.getInt();
int count = in.getInt();
BagValue bag = new BagValue(parent);
for (int i = 0; i < count; i++) {
@SuppressWarnings("unchecked")
Map.Entry<Integer, Value> entry = new AbstractMap.SimpleEntry(in.getInt(), readValue());
bag.map.add(entry);
}
resEntry.value = bag;
} else {
resEntry.value = readValue();
}
config.resources.put(spec.id, resEntry);
}
// private void readConfigFlags() {
// int size = in.getInt();
// if (size < 28) {
// throw new RuntimeException();
// }
// short mcc = in.getShort();
// short mnc = in.getShort();
//
// char[] language = new char[] { (char) in.get(), (char) in.get() };
// char[] country = new char[] { (char) in.get(), (char) in.get() };
//
// byte orientation = in.get();
// byte touchscreen = in.get();
// short density = in.getShort();
//
// byte keyboard = in.get();
// byte navigation = in.get();
// byte inputFlags = in.get();
// byte inputPad0 = in.get();
//
// short screenWidth = in.getShort();
// short screenHeight = in.getShort();
//
// short sdkVersion = in.getShort();
// short minorVersion = in.getShort();
//
// byte screenLayout = 0;
// byte uiMode = 0;
// short smallestScreenWidthDp = 0;
// if (size >= 32) {
// screenLayout = in.get();
// uiMode = in.get();
// smallestScreenWidthDp = in.getShort();
// }
//
// short screenWidthDp = 0;
// short screenHeightDp = 0;
//
// if (size >= 36) {
// screenWidthDp = in.getShort();
// screenHeightDp = in.getShort();
// }
//
// short layoutDirection = 0;
// if (size >= 38 && sdkVersion >= 17) {
// layoutDirection = in.getShort();
// }
//
// }
private void readPackage(ByteBuffer in) throws IOException {
int pid = in.getInt() % 0xFF;
String name;
{
int nextPisition = in.position() + 128 * 2;
StringBuilder sb = new StringBuilder(32);
for (int i = 0; i < 128; i++) {
int s = in.getShort();
if (s == 0) {
break;
} else {
sb.append((char) s);
}
}
name = sb.toString();
in.position(nextPisition);
}
pkg = new Pkg(pid, name);
pkgs.add(pkg);
int typeStringOff = in.getInt();
int typeNameCount = in.getInt();
int keyStringOff = in.getInt();
int specNameCount = in.getInt();
{
Chunk chunk = new Chunk();
if (chunk.type != RES_STRING_POOL_TYPE) {
throw new RuntimeException();
}
typeNamesX = StringItems.read(in);
in.position(chunk.location + chunk.size);
}
{
Chunk chunk = new Chunk();
if (chunk.type != RES_STRING_POOL_TYPE) {
throw new RuntimeException();
}
keyNamesX = StringItems.read(in);
if (DEBUG) {
for (int i = 0; i < keyNamesX.length; i++) {
D("STR [%08x] %s", i, keyNamesX[i]);
}
}
in.position(chunk.location + chunk.size);
}
out:
while (in.hasRemaining()) {
Chunk chunk = new Chunk();
switch (chunk.type) {
case RES_TABLE_TYPE_SPEC_TYPE: {
D("[%08x]read spec", in.position() - 8);
int tid = in.get() & 0xFF;
in.get(); // res0
in.getShort();// res1
int entryCount = in.getInt();
Type t = pkg.getType(tid, typeNamesX[tid - 1], entryCount);
for (int i = 0; i < entryCount; i++) {
t.getSpec(i).flags = in.getInt();
}
}
break;
case RES_TABLE_TYPE_TYPE: {
D("[%08x]read config", in.position() - 8);
int tid = in.get() & 0xFF;
in.get(); // res0
in.getShort();// res1
int entryCount = in.getInt();
Type t = pkg.getType(tid, typeNamesX[tid - 1], entryCount);
int entriesStart = in.getInt();
D("[%08x]read config id", in.position());
int p = in.position();
int size = in.getInt();
// readConfigFlags();
byte[] data = new byte[size];
in.position(p);
in.get(data);
Config config = new Config(data, entryCount);
in.position(chunk.location + chunk.headSize);
D("[%08x]read config entry offset", in.position());
int[] entrys = new int[entryCount];
for (int i = 0; i < entryCount; i++) {
entrys[i] = in.getInt();
}
D("[%08x]read config entrys", in.position());
for (int i = 0; i < entrys.length; i++) {
if (entrys[i] != -1) {
in.position(chunk.location + entriesStart + entrys[i]);
ResSpec spec = t.getSpec(i);
readEntry(config, spec);
}
}
t.addConfig(config);
}
break;
default:
break out;
}
in.position(chunk.location + chunk.size);
}
}
private Object readValue() {
int size1 = in.getShort();// 8
int zero = in.get();// 0
int type = in.get() & 0xFF; // TypedValue.*
int data = in.getInt();
String raw = null;
if (type == TYPE_STRING) {
raw = strings[data];
}
return new Value(type, data, raw);
}
/* pkg */class Chunk {
public final int headSize;
public final int location;
public final int size;
public final int type;
public Chunk() {
location = in.position();
type = in.getShort() & 0xFFFF;
headSize = in.getShort() & 0xFFFF;
size = in.getInt();
D("[%08x]type: %04x, headsize: %04x, size:%08x", location, type, headSize, size);
}
}
}