diff --git a/.gitmodules b/.gitmodules index 88f350f4..3448c611 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "edxp-core/src/main/cpp/external/Dobby"] path = edxp-core/src/main/cpp/external/Dobby url = https://github.com/jmpews/Dobby.git -[submodule "apk-parser"] - path = apk-parser - url = https://github.com/jaredrummler/APKParser.git diff --git a/edxp-common/build.gradle b/edxp-common/build.gradle index 9a804dda..55b22170 100644 --- a/edxp-common/build.gradle +++ b/edxp-common/build.gradle @@ -25,7 +25,6 @@ dependencies { api project(':xposed-bridge') compileOnly project(':dexmaker') compileOnly 'com.android.support:support-annotations:28.0.0' - implementation project(':apk-parser:library') } diff --git a/edxp-common/src/main/java/com/elderdrivers/riru/edxp/_hooker/impl/HandleBindApp.java b/edxp-common/src/main/java/com/elderdrivers/riru/edxp/_hooker/impl/HandleBindApp.java index 21103279..2cb2696d 100644 --- a/edxp-common/src/main/java/com/elderdrivers/riru/edxp/_hooker/impl/HandleBindApp.java +++ b/edxp-common/src/main/java/com/elderdrivers/riru/edxp/_hooker/impl/HandleBindApp.java @@ -12,11 +12,12 @@ import android.content.res.XResources; import com.elderdrivers.riru.edxp.config.ConfigManager; import com.elderdrivers.riru.edxp.util.Hookers; +import com.elderdrivers.riru.edxp.util.MetaDataReader; import com.elderdrivers.riru.edxp.util.Utils; -import com.jaredrummler.apkparser.ApkParser; import java.io.File; import java.io.IOException; +import java.util.Map; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; @@ -61,11 +62,12 @@ public class HandleBindApp extends XC_MethodHook { boolean isModule = false; int xposedminversion = -1; boolean xposedsharedprefs = false; - try (ApkParser ap = ApkParser.create(new File(appInfo.sourceDir))){ - isModule = ap.getApkMeta().metaData.containsKey("xposedmodule"); + try { + Map metaData = MetaDataReader.getMetaData(new File(appInfo.sourceDir)); + isModule = metaData.containsKey("xposedmodule"); if (isModule) { - xposedminversion = Integer.parseInt(ap.getApkMeta().metaData.get("xposedminversion")); - xposedsharedprefs = ap.getApkMeta().metaData.containsKey("xposedsharedprefs"); + xposedminversion = (Integer) metaData.get("xposedminversion"); + xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } } catch (NumberFormatException | IOException e) { Hookers.logE("ApkParser fails", e); diff --git a/settings.gradle b/settings.gradle index 0bdf3c55..ae4c3cf5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':edxp-core', ':xposed-bridge', ':hiddenapi-stubs', ':dexmaker', ':dalvikdx', ':edxp-common', ':edxp-yahfa', ':edxp-sandhook', ':edxp-service', ':apk-parser:library', ':sandhook-hooklib', ':sandhook-annotation' \ No newline at end of file +include ':edxp-core', ':xposed-bridge', ':hiddenapi-stubs', ':dexmaker', ':dalvikdx', ':edxp-common', ':edxp-yahfa', ':edxp-sandhook', ':edxp-service', ':sandhook-hooklib', ':sandhook-annotation' \ No newline at end of file diff --git a/xposed-bridge/build.gradle b/xposed-bridge/build.gradle index be861ede..c87e99b7 100644 --- a/xposed-bridge/build.gradle +++ b/xposed-bridge/build.gradle @@ -43,7 +43,6 @@ preBuild.doLast { } dependencies { - compileOnly project(':apk-parser:library') compileOnly files(project(":dexmaker").tasks.getByName("makeJarRelease").outputs) compileOnly files(project(":hiddenapi-stubs").tasks.getByName("makeStubJar").outputs) } diff --git a/xposed-bridge/src/main/java/com/elderdrivers/riru/edxp/util/MetaDataReader.java b/xposed-bridge/src/main/java/com/elderdrivers/riru/edxp/util/MetaDataReader.java new file mode 100644 index 00000000..3ac3326b --- /dev/null +++ b/xposed-bridge/src/main/java/com/elderdrivers/riru/edxp/util/MetaDataReader.java @@ -0,0 +1,107 @@ +package com.elderdrivers.riru.edxp.util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarFile; + +import pxb.android.axml.AxmlReader; +import pxb.android.axml.AxmlVisitor; +import pxb.android.axml.NodeVisitor; + +public class MetaDataReader { + private final HashMap metaData = new HashMap<>(); + + public static Map getMetaData(File apk) throws IOException { + return new MetaDataReader(apk).metaData; + } + + private MetaDataReader(File apk) throws IOException { + JarFile zip = new JarFile(apk); + InputStream is = zip.getInputStream(zip.getEntry("AndroidManifest.xml")); + byte[] bytes = getBytesFromInputStream(is); + AxmlReader reader = new AxmlReader(bytes); + reader.accept(new AxmlVisitor() { + @Override + public NodeVisitor child(String ns, String name) { + NodeVisitor child = super.child(ns, name); + return new ManifestTagVisitor(child); + } + }); + } + + public static byte[] getBytesFromInputStream(InputStream inputStream) throws IOException { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + byte[] b = new byte[1024]; + int n; + while ((n = inputStream.read(b)) != -1) { + bos.write(b, 0, n); + } + byte[] data = bos.toByteArray(); + return data; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private class ManifestTagVisitor extends NodeVisitor { + public ManifestTagVisitor(NodeVisitor child) { + super(child); + } + + @Override + public NodeVisitor child(String ns, String name) { + NodeVisitor child = super.child(ns, name); + if ("application".equals(name)) { + return new ApplicationTagVisitor(child); + } + return child; + } + + private class ApplicationTagVisitor extends NodeVisitor { + public ApplicationTagVisitor(NodeVisitor child) { + super(child); + } + + @Override + public NodeVisitor child(String ns, String name) { + NodeVisitor child = super.child(ns, name); + if("meta-data".equals(name)) { + return new MetaDataVisitor(child); + } + return child; + } + } + } + + private class MetaDataVisitor extends NodeVisitor { + public String name = null; + public Object value = null; + public MetaDataVisitor(NodeVisitor child) { + super(child); + } + + @Override + public void attr(String ns, String name, int resourceId, int type, Object obj) { + if (type == 3 && "name".equals(name)) { + this.name = (String)obj; + } + if ("value".equals(name) ) { + value = obj; + } + super.attr(ns, name, resourceId, type, obj); + } + + @Override + public void end() { + if(name != null && value != null) { + metaData.put(name, value); + } + super.end(); + } + } +} diff --git a/xposed-bridge/src/main/java/de/robv/android/xposed/PendingHooks.java b/xposed-bridge/src/main/java/de/robv/android/xposed/PendingHooks.java index fbd81ce4..ff4bf842 100644 --- a/xposed-bridge/src/main/java/de/robv/android/xposed/PendingHooks.java +++ b/xposed-bridge/src/main/java/de/robv/android/xposed/PendingHooks.java @@ -1,17 +1,11 @@ package de.robv.android.xposed; -import com.elderdrivers.riru.edxp.config.EdXpConfigGlobal; -import com.jaredrummler.apkparser.utils.Utils; - import java.lang.reflect.Member; -import java.lang.reflect.Modifier; -import java.util.HashSet; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import static de.robv.android.xposed.XposedBridge.hookMethodNative; -import static de.robv.android.xposed.XposedBridge.log; public final class PendingHooks { diff --git a/xposed-bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/xposed-bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java index d1a95ddb..b6771acf 100644 --- a/xposed-bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java +++ b/xposed-bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java @@ -1,17 +1,14 @@ package de.robv.android.xposed; import android.annotation.SuppressLint; -import android.app.ActivityThread; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.os.Environment; import android.preference.PreferenceManager; import android.util.Log; import com.android.internal.util.XmlUtils; -import com.jaredrummler.apkparser.ApkParser; +import com.elderdrivers.riru.edxp.util.MetaDataReader; import org.xmlpull.v1.XmlPullParserException; @@ -76,11 +73,12 @@ public final class XSharedPreferences implements SharedPreferences { boolean isModule = false; int xposedminversion = -1; boolean xposedsharedprefs = false; - try (ApkParser ap = ApkParser.create(new File(m))) { - isModule = ap.getApkMeta().metaData.containsKey("xposedmodule"); - if(isModule) { - xposedminversion = Integer.parseInt(ap.getApkMeta().metaData.get("xposedminversion")); - xposedsharedprefs = ap.getApkMeta().metaData.containsKey("xposedsharedprefs"); + try { + Map metaData = MetaDataReader.getMetaData(new File(m)); + isModule = metaData.containsKey("xposedmodule"); + if (isModule) { + xposedminversion = (Integer) metaData.get("xposedminversion"); + xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } } catch (NumberFormatException | IOException e) { Log.w(TAG, "Apk parser fails: " + e); diff --git a/xposed-bridge/src/main/java/pxb/android/ResConst.java b/xposed-bridge/src/main/java/pxb/android/ResConst.java new file mode 100644 index 00000000..be0d87a8 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/ResConst.java @@ -0,0 +1,33 @@ +/* + * 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; + +public interface ResConst { + int RES_STRING_POOL_TYPE = 0x0001; + int RES_TABLE_TYPE = 0x0002; + int RES_TABLE_PACKAGE_TYPE = 0x0200; + int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; + int RES_TABLE_TYPE_TYPE = 0x0201; + + int RES_XML_TYPE = 0x0003; + int RES_XML_RESOURCE_MAP_TYPE = 0x0180; + int RES_XML_END_NAMESPACE_TYPE = 0x0101; + int RES_XML_END_ELEMENT_TYPE = 0x0103; + int RES_XML_START_NAMESPACE_TYPE = 0x0100; + int RES_XML_START_ELEMENT_TYPE = 0x0102; + int RES_XML_CDATA_TYPE = 0x0104; + +} diff --git a/xposed-bridge/src/main/java/pxb/android/StringItem.java b/xposed-bridge/src/main/java/pxb/android/StringItem.java new file mode 100644 index 00000000..7f99ecba --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/StringItem.java @@ -0,0 +1,61 @@ +/* + * 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; + +public class StringItem { + public String data; + public int dataOffset; + public int index; + + public StringItem() { + super(); + } + + public StringItem(String data) { + super(); + this.data = data; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + StringItem other = (StringItem) obj; + if (data == null) { + if (other.data != null) + return false; + } else if (!data.equals(other.data)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((data == null) ? 0 : data.hashCode()); + return result; + } + + public String toString() { + return String.format("S%04d %s", index, data); + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/StringItems.java b/xposed-bridge/src/main/java/pxb/android/StringItems.java new file mode 100644 index 00000000..edf026a6 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/StringItems.java @@ -0,0 +1,163 @@ +/* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("serial") +public class StringItems extends ArrayList { + private static final int UTF8_FLAG = 0x00000100; + + + public static String[] read(ByteBuffer in) throws IOException { + int trunkOffset = in.position() - 8; + int stringCount = in.getInt(); + int styleOffsetCount = in.getInt(); + int flags = in.getInt(); + int stringDataOffset = in.getInt(); + int stylesOffset = in.getInt(); + int offsets[] = new int[stringCount]; + String strings[] = new String[stringCount]; + for (int i = 0; i < stringCount; i++) { + offsets[i] = in.getInt(); + } + + int base = trunkOffset + stringDataOffset; + for (int i = 0; i < offsets.length; i++) { + in.position(base + offsets[i]); + String s; + + if (0 != (flags & UTF8_FLAG)) { + u8length(in); // ignored + int u8len = u8length(in); + int start = in.position(); + int blength = u8len; + while (in.get(start + blength) != 0) { + blength++; + } + s = new String(in.array(), start, blength, "UTF-8"); + } else { + int length = u16length(in); + s = new String(in.array(), in.position(), length * 2, "UTF-16LE"); + } + strings[i] = s; + } + return strings; + } + + static int u16length(ByteBuffer in) { + int length = in.getShort() & 0xFFFF; + if (length > 0x7FFF) { + length = ((length & 0x7FFF) << 8) | (in.getShort() & 0xFFFF); + } + return length; + } + + static int u8length(ByteBuffer in) { + int len = in.get() & 0xFF; + if ((len & 0x80) != 0) { + len = ((len & 0x7F) << 8) | (in.get() & 0xFF); + } + return len; + } + + byte[] stringData; + + public int getSize() { + return 5 * 4 + this.size() * 4 + stringData.length + 0;// TODO + } + + public void prepare() throws IOException { + for (StringItem s : this) { + if (s.data.length() > 0x7FFF) { + useUTF8 = false; + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int i = 0; + int offset = 0; + baos.reset(); + Map map = new HashMap(); + for (StringItem item : this) { + item.index = i++; + String stringData = item.data; + Integer of = map.get(stringData); + if (of != null) { + item.dataOffset = of; + } else { + item.dataOffset = offset; + map.put(stringData, offset); + if (useUTF8) { + int length = stringData.length(); + byte[] data = stringData.getBytes("UTF-8"); + int u8lenght = data.length; + + if (length > 0x7F) { + offset++; + baos.write((length >> 8) | 0x80); + } + baos.write(length); + + if (u8lenght > 0x7F) { + offset++; + baos.write((u8lenght >> 8) | 0x80); + } + baos.write(u8lenght); + baos.write(data); + baos.write(0); + offset += 3 + u8lenght; + } else { + int length = stringData.length(); + byte[] data = stringData.getBytes("UTF-16LE"); + if (length > 0x7FFF) { + int x = (length >> 16) | 0x8000; + baos.write(x); + baos.write(x >> 8); + offset += 2; + } + baos.write(length); + baos.write(length >> 8); + baos.write(data); + baos.write(0); + baos.write(0); + offset += 4 + data.length; + } + } + } + // TODO + stringData = baos.toByteArray(); + } + + private boolean useUTF8 = true; + + public void write(ByteBuffer out) throws IOException { + out.putInt(this.size()); + out.putInt(0);// TODO style count + out.putInt(useUTF8 ? UTF8_FLAG : 0); + out.putInt(7 * 4 + this.size() * 4); + out.putInt(0); + for (StringItem item : this) { + out.putInt(item.dataOffset); + } + out.put(stringData); + // TODO + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/ArscDumper.java b/xposed-bridge/src/main/java/pxb/android/arsc/ArscDumper.java new file mode 100644 index 00000000..1e5760d2 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/ArscDumper.java @@ -0,0 +1,73 @@ +/* + * 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 java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import pxb.android.axml.Util; + +/** + * dump an arsc file + * + * @author bob + * + */ +public class ArscDumper { + public static void dump(List pkgs) { + for (int x = 0; x < pkgs.size(); x++) { + Pkg pkg = pkgs.get(x); + + System.out.println(String.format(" Package %d id=%d name=%s typeCount=%d", x, pkg.id, pkg.name, + pkg.types.size())); + for (Type type : pkg.types.values()) { + System.out.println(String.format(" type %d %s", type.id - 1, type.name)); + + int resPrefix = pkg.id << 24 | type.id << 16; + for (int i = 0; i < type.specs.length; i++) { + ResSpec spec = type.getSpec(i); + System.out.println(String.format(" spec 0x%08x 0x%08x %s", resPrefix | spec.id, spec.flags, + spec.name)); + } + for (int i = 0; i < type.configs.size(); i++) { + Config config = type.configs.get(i); + System.out.println(" config"); + + List entries = new ArrayList(config.resources.values()); + for (int j = 0; j < entries.size(); j++) { + ResEntry entry = entries.get(j); + System.out.println(String.format(" resource 0x%08x %-20s: %s", + resPrefix | entry.spec.id, entry.spec.name, entry.value)); + } + } + } + } + } + + public static void main(String... args) throws IOException { + if (args.length == 0) { + System.err.println("asrc-dump file.arsc"); + return; + } + byte[] data = Util.readFile(new File(args[0])); + List pkgs = new ArscParser(data).parse(); + + dump(pkgs); + + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/ArscParser.java b/xposed-bridge/src/main/java/pxb/android/arsc/ArscParser.java new file mode 100644 index 00000000..721c1406 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/ArscParser.java @@ -0,0 +1,317 @@ +/* + * 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 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; + +import pxb.android.ResConst; +import pxb.android.StringItems; + +/** + * + * Read the resources.arsc inside an Android apk. + * + * Usage: + * + *
+ * 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
+ * 
+ * + * The format of arsc is described here (gingerbread) + *
    + *
  • frameworks/base/libs/utils/ResourceTypes.cpp
  • + *
  • frameworks/base/include/utils/ResourceTypes.h
  • + *
+ * and the cmd line aapt d resources abc.apk is also good for debug + * (available in android sdk) + * + *

+ * Todos: + *

    + * TODO add support to read styled strings + *
+ * + *

+ * Thanks to the the following projects + *

    + *
  • android4me https://code.google.com/p/android4me/
  • + *
  • Apktool https://code.google.com/p/android-apktool
  • + *
  • Android http://source.android.com/
  • + *
+ * + * @author bob + * + */ +public class ArscParser implements ResConst { + + /* 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(); + } + } + + /** + * 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; + public static final int TYPE_STRING = 0x03; + + private int fileSize = -1; + private ByteBuffer in; + private String[] keyNamesX; + private Pkg pkg; + private List pkgs = new ArrayList(); + private String[] strings; + private String[] typeNamesX; + + public ArscParser(byte[] b) { + this.in = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); + } + + public List 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 + } + while (in.hasRemaining()) { + Chunk chunk = new Chunk(); + switch (chunk.type) { + case RES_STRING_POOL_TYPE: + strings = StringItems.read(in); + break; + case RES_TABLE_PACKAGE_TYPE: + readPackage(in); + } + in.position(chunk.location + chunk.size); + } + return pkgs; + } + + // 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 readEntry(Config config, ResSpec spec) { + int size = in.getShort(); + + 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++) { + Map.Entry 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 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); + in.position(chunk.location + chunk.size); + } + + out: while (in.hasRemaining()) { + Chunk chunk = new Chunk(); + switch (chunk.type) { + case RES_TABLE_TYPE_SPEC_TYPE: { + 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: { + 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(); + + 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); + + int[] entrys = new int[entryCount]; + for (int i = 0; i < entryCount; i++) { + entrys[i] = in.getInt(); + } + 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); + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/ArscWriter.java b/xposed-bridge/src/main/java/pxb/android/arsc/ArscWriter.java new file mode 100644 index 00000000..082c42f9 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/ArscWriter.java @@ -0,0 +1,400 @@ +/* + * 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 java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import pxb.android.ResConst; +import pxb.android.StringItem; +import pxb.android.StringItems; +import pxb.android.axml.Util; + +/** + * Write pkgs to an arsc file + * + * @see ArscParser + * @author bob + * + */ +public class ArscWriter implements ResConst { + private static class PkgCtx { + Map keyNames = new HashMap(); + StringItems keyNames0 = new StringItems(); + public int keyStringOff; + int offset; + Pkg pkg; + int pkgSize; + List typeNames = new ArrayList(); + + StringItems typeNames0 = new StringItems(); + int typeStringOff; + + public void addKeyName(String name) { + if (keyNames.containsKey(name)) { + return; + } + StringItem stringItem = new StringItem(name); + keyNames.put(name, stringItem); + keyNames0.add(stringItem); + } + + public void addTypeName(int id, String name) { + while (typeNames.size() <= id) { + typeNames.add(null); + } + + StringItem item = typeNames.get(id); + if (item == null) { + typeNames.set(id, new StringItem(name)); + } else { + throw new RuntimeException(); + } + } + } + + private static void D(String fmt, Object... args) { + + } + + private List ctxs = new ArrayList(5); + private List pkgs; + private Map strTable = new TreeMap(); + private StringItems strTable0 = new StringItems(); + + public ArscWriter(List pkgs) { + this.pkgs = pkgs; + } + + public static void main(String... args) throws IOException { + if (args.length < 2) { + System.err.println("asrc-write-test in.arsc out.arsc"); + return; + } + byte[] data = Util.readFile(new File(args[0])); + List pkgs = new ArscParser(data).parse(); + // ArscDumper.dump(pkgs); + byte[] data2 = new ArscWriter(pkgs).toByteArray(); + // ArscDumper.dump(new ArscParser(data2).parse()); + Util.writeFile(data2, new File(args[1])); + } + + private void addString(String str) { + if (strTable.containsKey(str)) { + return; + } + StringItem stringItem = new StringItem(str); + strTable.put(str, stringItem); + strTable0.add(stringItem); + } + + private int count() { + + int size = 0; + + size += 8 + 4;// chunk, pkgcount + { + int stringSize = strTable0.getSize(); + if (stringSize % 4 != 0) { + stringSize += 4 - stringSize % 4; + } + size += 8 + stringSize;// global strings + } + for (PkgCtx ctx : ctxs) { + ctx.offset = size; + int pkgSize = 0; + pkgSize += 8 + 4 + 256;// chunk,pid+name + pkgSize += 4 * 4; + + ctx.typeStringOff = pkgSize; + { + int stringSize = ctx.typeNames0.getSize(); + if (stringSize % 4 != 0) { + stringSize += 4 - stringSize % 4; + } + pkgSize += 8 + stringSize;// type names + } + + ctx.keyStringOff = pkgSize; + + { + int stringSize = ctx.keyNames0.getSize(); + if (stringSize % 4 != 0) { + stringSize += 4 - stringSize % 4; + } + pkgSize += 8 + stringSize;// key names + } + + for (Type type : ctx.pkg.types.values()) { + type.wPosition = size + pkgSize; + pkgSize += 8 + 4 + 4 + 4 * type.specs.length; // trunk,id,entryCount, + // configs + + for (Config config : type.configs) { + config.wPosition = pkgSize + size; + int configBasePostion = pkgSize; + pkgSize += 8 + 4 + 4 + 4; // trunk,id,entryCount,entriesStart + int size0 = config.id.length; + if (size0 % 4 != 0) { + size0 += 4 - size0 % 4; + } + pkgSize += size0;// config + + if (pkgSize - configBasePostion > 0x0038) { + throw new RuntimeException("config id too big"); + } else { + pkgSize = configBasePostion + 0x0038; + } + + pkgSize += 4 * config.entryCount;// offset + config.wEntryStart = pkgSize - configBasePostion; + int entryBase = pkgSize; + for (ResEntry e : config.resources.values()) { + e.wOffset = pkgSize - entryBase; + pkgSize += 8;// size,flag,keyString + if (e.value instanceof BagValue) { + BagValue big = (BagValue) e.value; + pkgSize += 8 + big.map.size() * 12; + } else { + pkgSize += 8; + } + } + config.wChunkSize = pkgSize - configBasePostion; + } + } + ctx.pkgSize = pkgSize; + size += pkgSize; + } + + return size; + } + + private List prepare() throws IOException { + for (Pkg pkg : pkgs) { + PkgCtx ctx = new PkgCtx(); + ctx.pkg = pkg; + ctxs.add(ctx); + + for (Type type : pkg.types.values()) { + ctx.addTypeName(type.id - 1, type.name); + for (ResSpec spec : type.specs) { + ctx.addKeyName(spec.name); + } + for (Config config : type.configs) { + for (ResEntry e : config.resources.values()) { + Object object = e.value; + if (object instanceof BagValue) { + travelBagValue((BagValue) object); + } else { + travelValue((Value) object); + } + } + } + } + ctx.keyNames0.prepare(); + ctx.typeNames0.addAll(ctx.typeNames); + ctx.typeNames0.prepare(); + } + strTable0.prepare(); + return ctxs; + } + + public byte[] toByteArray() throws IOException { + prepare(); + int size = count(); + ByteBuffer out = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + write(out, size); + return out.array(); + } + + private void travelBagValue(BagValue bag) { + for (Map.Entry e : bag.map) { + travelValue(e.getValue()); + } + } + + private void travelValue(Value v) { + if (v.raw != null) { + addString(v.raw); + } + } + + private void write(ByteBuffer out, int size) throws IOException { + out.putInt(RES_TABLE_TYPE | (0x000c << 16)); + out.putInt(size); + out.putInt(ctxs.size()); + + { + int stringSize = strTable0.getSize(); + int padding = 0; + if (stringSize % 4 != 0) { + padding = 4 - stringSize % 4; + } + out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16)); + out.putInt(stringSize + padding + 8); + strTable0.write(out); + out.put(new byte[padding]); + } + + for (PkgCtx pctx : ctxs) { + if (out.position() != pctx.offset) { + throw new RuntimeException(); + } + final int basePosition = out.position(); + out.putInt(RES_TABLE_PACKAGE_TYPE | (0x011c << 16)); + out.putInt(pctx.pkgSize); + out.putInt(pctx.pkg.id); + int p = out.position(); + out.put(pctx.pkg.name.getBytes("UTF-16LE")); + out.position(p + 256); + + out.putInt(pctx.typeStringOff); + out.putInt(pctx.typeNames0.size()); + + out.putInt(pctx.keyStringOff); + out.putInt(pctx.keyNames0.size()); + + { + if (out.position() - basePosition != pctx.typeStringOff) { + throw new RuntimeException(); + } + int stringSize = pctx.typeNames0.getSize(); + int padding = 0; + if (stringSize % 4 != 0) { + padding = 4 - stringSize % 4; + } + out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16)); + out.putInt(stringSize + padding + 8); + pctx.typeNames0.write(out); + out.put(new byte[padding]); + } + + { + if (out.position() - basePosition != pctx.keyStringOff) { + throw new RuntimeException(); + } + int stringSize = pctx.keyNames0.getSize(); + int padding = 0; + if (stringSize % 4 != 0) { + padding = 4 - stringSize % 4; + } + out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16)); + out.putInt(stringSize + padding + 8); + pctx.keyNames0.write(out); + out.put(new byte[padding]); + } + + for (Type t : pctx.pkg.types.values()) { + D("[%08x]write spec", out.position(), t.name); + if (t.wPosition != out.position()) { + throw new RuntimeException(); + } + out.putInt(RES_TABLE_TYPE_SPEC_TYPE | (0x0010 << 16)); + out.putInt(4 * 4 + 4 * t.specs.length);// size + + out.putInt(t.id); + out.putInt(t.specs.length); + for (ResSpec spec : t.specs) { + out.putInt(spec.flags); + } + + for (Config config : t.configs) { + D("[%08x]write config", out.position()); + int typeConfigPosition = out.position(); + if (config.wPosition != typeConfigPosition) { + throw new RuntimeException(); + } + out.putInt(RES_TABLE_TYPE_TYPE | (0x0038 << 16)); + out.putInt(config.wChunkSize);// size + + out.putInt(t.id); + out.putInt(t.specs.length); + out.putInt(config.wEntryStart); + + D("[%08x]write config ids", out.position()); + out.put(config.id); + + int size0 = config.id.length; + int padding = 0; + if (size0 % 4 != 0) { + padding = 4 - size0 % 4; + } + out.put(new byte[padding]); + + out.position(typeConfigPosition + 0x0038); + + D("[%08x]write config entry offsets", out.position()); + for (int i = 0; i < config.entryCount; i++) { + ResEntry entry = config.resources.get(i); + if (entry == null) { + out.putInt(-1); + } else { + out.putInt(entry.wOffset); + } + } + + if (out.position() - typeConfigPosition != config.wEntryStart) { + throw new RuntimeException(); + } + D("[%08x]write config entrys", out.position()); + for (ResEntry e : config.resources.values()) { + D("[%08x]ResTable_entry", out.position()); + boolean isBag = e.value instanceof BagValue; + out.putShort((short) (isBag ? 16 : 8)); + int flag = e.flag; + if (isBag) { // add complex flag + flag |= ArscParser.ENTRY_FLAG_COMPLEX; + } else { // remove + flag &= ~ArscParser.ENTRY_FLAG_COMPLEX; + } + out.putShort((short) flag); + out.putInt(pctx.keyNames.get(e.spec.name).index); + if (isBag) { + BagValue bag = (BagValue) e.value; + out.putInt(bag.parent); + out.putInt(bag.map.size()); + for (Map.Entry entry : bag.map) { + out.putInt(entry.getKey()); + writeValue(entry.getValue(), out); + } + } else { + writeValue((Value) e.value, out); + } + } + } + } + } + } + + private void writeValue(Value value, ByteBuffer out) { + out.putShort((short) 8); + out.put((byte) 0); + out.put((byte) value.type); + if (value.type == ArscParser.TYPE_STRING) { + out.putInt(strTable.get(value.raw).index); + } else { + out.putInt(value.data); + } + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/BagValue.java b/xposed-bridge/src/main/java/pxb/android/arsc/BagValue.java new file mode 100644 index 00000000..f75ddd08 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/BagValue.java @@ -0,0 +1,70 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class BagValue { + public List> map = new ArrayList>(); + public final int parent; + + public BagValue(int parent) { + this.parent = parent; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof BagValue)) + return false; + BagValue other = (BagValue) obj; + if (map == null) { + if (other.map != null) + return false; + } else if (!map.equals(other.map)) + return false; + if (parent != other.parent) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((map == null) ? 0 : map.hashCode()); + result = prime * result + parent; + return result; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("{bag%08x", parent)); + for (Map.Entry e : map) { + sb.append(",").append(String.format("0x%08x", e.getKey())); + sb.append("="); + sb.append(e.getValue()); + } + + return sb.append("}").toString(); + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/Config.java b/xposed-bridge/src/main/java/pxb/android/arsc/Config.java new file mode 100644 index 00000000..66ec1048 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/Config.java @@ -0,0 +1,34 @@ +/* + * 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 java.util.Map; +import java.util.TreeMap; + +public class Config { + public final int entryCount; + public final byte[] id; + public Map resources = new TreeMap(); + /* package */int wChunkSize; + /* package */int wEntryStart; + /* package */int wPosition; + + public Config(byte[] id, int entryCount) { + super(); + this.id = id; + this.entryCount = entryCount; + } +} \ No newline at end of file diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/Pkg.java b/xposed-bridge/src/main/java/pxb/android/arsc/Pkg.java new file mode 100644 index 00000000..909003cb --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/Pkg.java @@ -0,0 +1,54 @@ +/* + * 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 java.util.TreeMap; + +public class Pkg { + public final int id; + public String name; + public TreeMap types = new TreeMap(); + + public Pkg(int id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Type getType(int tid, String name, int entrySize) { + Type type = types.get(tid); + if (type != null) { + if (name != null) { + if (type.name == null) { + type.name = name; + } else if (!name.endsWith(type.name)) { + throw new RuntimeException(); + } + if (type.specs.length != entrySize) { + throw new RuntimeException(); + } + } + } else { + type = new Type(); + type.id = tid; + type.name = name; + type.specs = new ResSpec[entrySize]; + types.put(tid, type); + } + return type; + } + +} \ No newline at end of file diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/ResEntry.java b/xposed-bridge/src/main/java/pxb/android/arsc/ResEntry.java new file mode 100644 index 00000000..bb45007e --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/ResEntry.java @@ -0,0 +1,35 @@ +/* + * 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; + +public class ResEntry { + public final int flag; + + public final ResSpec spec; + /** + * {@link BagValue} or {@link Value} + */ + public Object value; + + /* package */int wOffset; + + public ResEntry(int flag, ResSpec spec) { + super(); + this.flag = flag; + this.spec = spec; + } + +} \ No newline at end of file diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/ResSpec.java b/xposed-bridge/src/main/java/pxb/android/arsc/ResSpec.java new file mode 100644 index 00000000..92193a2b --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/ResSpec.java @@ -0,0 +1,36 @@ +/* + * 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; + +public class ResSpec { + public int flags; + public final int id; + public String name; + + public ResSpec(int id) { + super(); + this.id = id; + } + + public void updateName(String s) { + String name = this.name; + if (name == null) { + this.name = s; + } else if (!s.equals(name)) { + throw new RuntimeException(); + } + } +} \ No newline at end of file diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/Type.java b/xposed-bridge/src/main/java/pxb/android/arsc/Type.java new file mode 100644 index 00000000..5f35d3d3 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/Type.java @@ -0,0 +1,44 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +public class Type { + public List configs = new ArrayList(); + public int id; + public String name; + public ResSpec[] specs; + /* package */int wPosition; + + public void addConfig(Config config) { + if (config.entryCount != specs.length) { + throw new RuntimeException(); + } + configs.add(config); + } + + public ResSpec getSpec(int resId) { + ResSpec res = specs[resId]; + if (res == null) { + res = new ResSpec(resId); + specs[resId] = res; + } + return res; + } + +} \ No newline at end of file diff --git a/xposed-bridge/src/main/java/pxb/android/arsc/Value.java b/xposed-bridge/src/main/java/pxb/android/arsc/Value.java new file mode 100644 index 00000000..56f90dba --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/arsc/Value.java @@ -0,0 +1,37 @@ +/* + * 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; + +public class Value { + public final int data; + public String raw; + public final int type; + + public Value(int type, int data, String raw) { + super(); + this.type = type; + this.data = data; + this.raw = raw; + } + + public String toString() { + if (type == 0x03) { + return raw; + } + return String.format("{t=0x%02x d=0x%08x}", type, data); + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/Axml.java b/xposed-bridge/src/main/java/pxb/android/axml/Axml.java new file mode 100644 index 00000000..49879f7a --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/Axml.java @@ -0,0 +1,142 @@ +/* + * 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.axml; + +import java.util.ArrayList; +import java.util.List; + +public class Axml extends AxmlVisitor { + + public static class Node extends NodeVisitor { + public static class Attr { + public String ns, name; + public int resourceId, type; + public Object value; + + public void accept(NodeVisitor nodeVisitor) { + nodeVisitor.attr(ns, name, resourceId, type, value); + } + } + + public static class Text { + public int ln; + public String text; + + public void accept(NodeVisitor nodeVisitor) { + nodeVisitor.text(ln, text); + } + } + + public List attrs = new ArrayList(); + public List children = new ArrayList(); + public Integer ln; + public String ns, name; + public Text text; + + public void accept(NodeVisitor nodeVisitor) { + NodeVisitor nodeVisitor2 = nodeVisitor.child(ns, name); + acceptB(nodeVisitor2); + nodeVisitor2.end(); + } + + public void acceptB(NodeVisitor nodeVisitor) { + if (text != null) { + text.accept(nodeVisitor); + } + for (Attr a : attrs) { + a.accept(nodeVisitor); + } + if (ln != null) { + nodeVisitor.line(ln); + } + for (Node c : children) { + c.accept(nodeVisitor); + } + } + + @Override + public void attr(String ns, String name, int resourceId, int type, Object obj) { + Attr attr = new Attr(); + attr.name = name; + attr.ns = ns; + attr.resourceId = resourceId; + attr.type = type; + attr.value = obj; + attrs.add(attr); + } + + @Override + public NodeVisitor child(String ns, String name) { + Node node = new Node(); + node.name = name; + node.ns = ns; + children.add(node); + return node; + } + + @Override + public void line(int ln) { + this.ln = ln; + } + + @Override + public void text(int lineNumber, String value) { + Text text = new Text(); + text.ln = lineNumber; + text.text = value; + this.text = text; + } + } + + public static class Ns { + public int ln; + public String prefix, uri; + + public void accept(AxmlVisitor visitor) { + visitor.ns(prefix, uri, ln); + } + } + + public List firsts = new ArrayList(); + public List nses = new ArrayList(); + + public void accept(final AxmlVisitor visitor) { + for (Ns ns : nses) { + ns.accept(visitor); + } + for (Node first : firsts) { + first.accept(visitor); + } + } + + @Override + public NodeVisitor child(String ns, String name) { + Node node = new Node(); + node.name = name; + node.ns = ns; + firsts.add(node); + return node; + } + + @Override + public void ns(String prefix, String uri, int ln) { + Ns ns = new Ns(); + ns.prefix = prefix; + ns.uri = uri; + ns.ln = ln; + nses.add(ns); + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/AxmlParser.java b/xposed-bridge/src/main/java/pxb/android/axml/AxmlParser.java new file mode 100644 index 00000000..d3802451 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/AxmlParser.java @@ -0,0 +1,274 @@ +/* + * 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.axml; + +import static pxb.android.axml.NodeVisitor.TYPE_INT_BOOLEAN; +import static pxb.android.axml.NodeVisitor.TYPE_STRING; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import pxb.android.ResConst; +import pxb.android.StringItems; + +/** + * a class to read android axml + * + * @author Panxiaobo + */ +public class AxmlParser implements ResConst { + + public static final int END_FILE = 7; + public static final int END_NS = 5; + public static final int END_TAG = 3; + public static final int START_FILE = 1; + public static final int START_NS = 4; + public static final int START_TAG = 2; + public static final int TEXT = 6; + // private int attrName[]; + // private int attrNs[]; + // private int attrResId[]; + // private int attrType[]; + // private Object attrValue[]; + + private int attributeCount; + + private IntBuffer attrs; + + private int classAttribute; + private int fileSize = -1; + private int idAttribute; + private ByteBuffer in; + private int lineNumber; + private int nameIdx; + private int nsIdx; + + private int prefixIdx; + + private int[] resourceIds; + + private String[] strings; + + private int styleAttribute; + + private int textIdx; + + public AxmlParser(byte[] data) { + this(ByteBuffer.wrap(data)); + } + + public AxmlParser(ByteBuffer in) { + super(); + this.in = in.order(ByteOrder.LITTLE_ENDIAN); + } + + public int getAttrCount() { + return attributeCount; + } + + public int getAttributeCount() { + return attributeCount; + } + + public String getAttrName(int i) { + int idx = attrs.get(i * 5 + 1); + return strings[idx]; + + } + + public String getAttrNs(int i) { + int idx = attrs.get(i * 5 + 0); + return idx >= 0 ? strings[idx] : null; + } + + String getAttrRawString(int i) { + int idx = attrs.get(i * 5 + 2); + if (idx >= 0) { + return strings[idx]; + } + return null; + } + + public int getAttrResId(int i) { + if (resourceIds != null) { + int idx = attrs.get(i * 5 + 1); + if (idx >= 0 && idx < resourceIds.length) { + return resourceIds[idx]; + } + } + return -1; + } + + public int getAttrType(int i) { + return attrs.get(i * 5 + 3) >> 24; + } + + public Object getAttrValue(int i) { + int v = attrs.get(i * 5 + 4); + + if (i == idAttribute) { + return ValueWrapper.wrapId(v, getAttrRawString(i)); + } else if (i == styleAttribute) { + return ValueWrapper.wrapStyle(v, getAttrRawString(i)); + } else if (i == classAttribute) { + return ValueWrapper.wrapClass(v, getAttrRawString(i)); + } + + switch (getAttrType(i)) { + case TYPE_STRING: + return strings[v]; + case TYPE_INT_BOOLEAN: + return v != 0; + default: + return v; + } + } + + public int getLineNumber() { + return lineNumber; + } + + public String getName() { + return strings[nameIdx]; + } + + public String getNamespacePrefix() { + return strings[prefixIdx]; + } + + public String getNamespaceUri() { + return nsIdx >= 0 ? strings[nsIdx] : null; + } + + public String getText() { + return strings[textIdx]; + } + + public int next() throws IOException { + if (fileSize < 0) { + int type = in.getInt() & 0xFFFF; + if (type != RES_XML_TYPE) { + throw new RuntimeException(); + } + fileSize = in.getInt(); + return START_FILE; + } + int event = -1; + for (int p = in.position(); p < fileSize; p = in.position()) { + int type = in.getInt() & 0xFFFF; + int size = in.getInt(); + switch (type) { + case RES_XML_START_ELEMENT_TYPE: { + { + lineNumber = in.getInt(); + in.getInt();/* skip, 0xFFFFFFFF */ + nsIdx = in.getInt(); + nameIdx = in.getInt(); + int flag = in.getInt();// 0x00140014 ? + if (flag != 0x00140014) { + throw new RuntimeException(); + } + } + + attributeCount = in.getShort() & 0xFFFF; + idAttribute = (in.getShort() & 0xFFFF) - 1; + classAttribute = (in.getShort() & 0xFFFF) - 1; + styleAttribute = (in.getShort() & 0xFFFF) - 1; + + attrs = in.asIntBuffer(); + + // attrResId = new int[attributeCount]; + // attrName = new int[attributeCount]; + // attrNs = new int[attributeCount]; + // attrType = new int[attributeCount]; + // attrValue = new Object[attributeCount]; + // for (int i = 0; i < attributeCount; i++) { + // int attrNsIdx = in.getInt(); + // int attrNameIdx = in.getInt(); + // int raw = in.getInt(); + // int aValueType = in.getInt() >>> 24; + // int aValue = in.getInt(); + // Object value = null; + // switch (aValueType) { + // case TYPE_STRING: + // value = strings[aValue]; + // break; + // case TYPE_INT_BOOLEAN: + // value = aValue != 0; + // break; + // default: + // value = aValue; + // } + // int resourceId = attrNameIdx < this.resourceIds.length ? + // resourceIds[attrNameIdx] : -1; + // attrNs[i] = attrNsIdx; + // attrName[i] = attrNameIdx; + // attrType[i] = aValueType; + // attrResId[i] = resourceId; + // attrValue[i] = value; + // } + event = START_TAG; + } + break; + case RES_XML_END_ELEMENT_TYPE: { + in.position(p + size); + event = END_TAG; + } + break; + case RES_XML_START_NAMESPACE_TYPE: + lineNumber = in.getInt(); + in.getInt();/* 0xFFFFFFFF */ + prefixIdx = in.getInt(); + nsIdx = in.getInt(); + event = START_NS; + break; + case RES_XML_END_NAMESPACE_TYPE: + in.position(p + size); + event = END_NS; + break; + case RES_STRING_POOL_TYPE: + strings = StringItems.read(in); + in.position(p + size); + continue; + case RES_XML_RESOURCE_MAP_TYPE: + int count = size / 4 - 2; + resourceIds = new int[count]; + for (int i = 0; i < count; i++) { + resourceIds[i] = in.getInt(); + } + in.position(p + size); + continue; + case RES_XML_CDATA_TYPE: + lineNumber = in.getInt(); + in.getInt();/* 0xFFFFFFFF */ + textIdx = in.getInt(); + + in.getInt();/* 00000008 00000000 */ + in.getInt(); + + event = TEXT; + break; + default: + throw new RuntimeException("Unsupported type: " + type); + } + in.position(p + size); + return event; + } + return END_FILE; + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/AxmlReader.java b/xposed-bridge/src/main/java/pxb/android/axml/AxmlReader.java new file mode 100644 index 00000000..11c17613 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/AxmlReader.java @@ -0,0 +1,94 @@ +/* + * 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.axml; + +import static pxb.android.axml.AxmlParser.END_FILE; +import static pxb.android.axml.AxmlParser.END_NS; +import static pxb.android.axml.AxmlParser.END_TAG; +import static pxb.android.axml.AxmlParser.START_FILE; +import static pxb.android.axml.AxmlParser.START_NS; +import static pxb.android.axml.AxmlParser.START_TAG; +import static pxb.android.axml.AxmlParser.TEXT; + +import java.io.IOException; +import java.util.Stack; + +import de.robv.android.xposed.XposedBridge; + +/** + * a class to read android axml + * + * @author Panxiaobo + */ +public class AxmlReader { + public static final NodeVisitor EMPTY_VISITOR = new NodeVisitor() { + + @Override + public NodeVisitor child(String ns, String name) { + return this; + } + + }; + final AxmlParser parser; + + public AxmlReader(byte[] data) { + super(); + this.parser = new AxmlParser(data); + } + + public void accept(final AxmlVisitor av) throws IOException { + Stack nvs = new Stack(); + NodeVisitor tos = av; + while (true) { + int type = parser.next(); + switch (type) { + case START_FILE: + break; + case START_TAG: + nvs.push(tos); + tos = tos.child(parser.getNamespaceUri(), parser.getName()); + if (tos != null) { + if (tos != EMPTY_VISITOR) { + tos.line(parser.getLineNumber()); + for (int i = 0; i < parser.getAttrCount(); i++) { + tos.attr(parser.getAttrNs(i), parser.getAttrName(i), parser.getAttrResId(i), + parser.getAttrType(i), parser.getAttrValue(i)); + } + } + } else { + tos = EMPTY_VISITOR; + } + break; + case END_TAG: + tos.end(); + tos = nvs.pop(); + break; + case START_NS: + av.ns(parser.getNamespacePrefix(), parser.getNamespaceUri(), parser.getLineNumber()); + break; + case END_NS: + break; + case TEXT: + tos.text(parser.getLineNumber(), parser.getText()); + break; + case END_FILE: + return; + default: + XposedBridge.log("Unsupported tag: " + type); + } + } + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/AxmlVisitor.java b/xposed-bridge/src/main/java/pxb/android/axml/AxmlVisitor.java new file mode 100644 index 00000000..3856d9d7 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/AxmlVisitor.java @@ -0,0 +1,47 @@ +/* + * 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.axml; + +/** + * visitor to visit an axml + * + * @author Panxiaobo + */ +public class AxmlVisitor extends NodeVisitor { + + public AxmlVisitor() { + super(); + + } + + public AxmlVisitor(NodeVisitor av) { + super(av); + } + + /** + * create a ns + * + * @param prefix + * @param uri + * @param ln + */ + public void ns(String prefix, String uri, int ln) { + if (nv != null && nv instanceof AxmlVisitor) { + ((AxmlVisitor) nv).ns(prefix, uri, ln); + } + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/AxmlWriter.java b/xposed-bridge/src/main/java/pxb/android/axml/AxmlWriter.java new file mode 100644 index 00000000..d5daeedf --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/AxmlWriter.java @@ -0,0 +1,442 @@ +/* + * 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.axml; + +import static pxb.android.ResConst.RES_STRING_POOL_TYPE; +import static pxb.android.ResConst.RES_XML_CDATA_TYPE; +import static pxb.android.ResConst.RES_XML_END_ELEMENT_TYPE; +import static pxb.android.ResConst.RES_XML_END_NAMESPACE_TYPE; +import static pxb.android.ResConst.RES_XML_RESOURCE_MAP_TYPE; +import static pxb.android.ResConst.RES_XML_START_ELEMENT_TYPE; +import static pxb.android.ResConst.RES_XML_START_NAMESPACE_TYPE; +import static pxb.android.ResConst.RES_XML_TYPE; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.TreeSet; + +import pxb.android.StringItem; +import pxb.android.StringItems; + +/** + * a class to write android axml + * + * @author Panxiaobo + */ +public class AxmlWriter extends AxmlVisitor { + static final Comparator ATTR_CMP = new Comparator() { + + @Override + public int compare(Attr a, Attr b) { + int x = a.resourceId - b.resourceId; + if (x == 0) { + x = a.name.data.compareTo(b.name.data); + if (x == 0) { + boolean aNsIsnull = a.ns == null; + boolean bNsIsnull = b.ns == null; + if (aNsIsnull) { + if (bNsIsnull) { + x = 0; + } else { + x = -1; + } + } else { + if (bNsIsnull) { + x = 1; + } else { + x = a.ns.data.compareTo(b.ns.data); + } + } + + } + } + return x; + } + }; + + static class Attr { + + public int index; + public StringItem name; + public StringItem ns; + public int resourceId; + public int type; + public Object value; + public StringItem raw; + + public Attr(StringItem ns, StringItem name, int resourceId) { + super(); + this.ns = ns; + this.name = name; + this.resourceId = resourceId; + } + + public void prepare(AxmlWriter axmlWriter) { + ns = axmlWriter.updateNs(ns); + if (this.name != null) { + if (resourceId != -1) { + this.name = axmlWriter.updateWithResourceId(this.name, this.resourceId); + } else { + this.name = axmlWriter.update(this.name); + } + } + if (value instanceof StringItem) { + value = axmlWriter.update((StringItem) value); + } + if (raw != null) { + raw = axmlWriter.update(raw); + } + } + + } + + static class NodeImpl extends NodeVisitor { + private Set attrs = new TreeSet(ATTR_CMP); + private List children = new ArrayList(); + private int line; + private StringItem name; + private StringItem ns; + private StringItem text; + private int textLineNumber; + Attr id; + Attr style; + Attr clz; + + public NodeImpl(String ns, String name) { + super(null); + this.ns = ns == null ? null : new StringItem(ns); + this.name = name == null ? null : new StringItem(name); + } + + @Override + public void attr(String ns, String name, int resourceId, int type, Object value) { + if (name == null) { + throw new RuntimeException("name can't be null"); + } + Attr a = new Attr(ns == null ? null : new StringItem(ns), new StringItem(name), resourceId); + a.type = type; + + if (value instanceof ValueWrapper) { + ValueWrapper valueWrapper = (ValueWrapper) value; + if (valueWrapper.raw != null) { + a.raw = new StringItem(valueWrapper.raw); + } + a.value = valueWrapper.ref; + switch (valueWrapper.type) { + case ValueWrapper.CLASS: + clz = a; + break; + case ValueWrapper.ID: + id = a; + break; + case ValueWrapper.STYLE: + style = a; + break; + } + } else if (type == TYPE_STRING) { + StringItem raw = new StringItem((String) value); + a.raw = raw; + a.value = raw; + + } else { + a.raw = null; + a.value = value; + } + + attrs.add(a); + } + + @Override + public NodeVisitor child(String ns, String name) { + NodeImpl child = new NodeImpl(ns, name); + this.children.add(child); + return child; + } + + @Override + public void end() { + } + + @Override + public void line(int ln) { + this.line = ln; + } + + public int prepare(AxmlWriter axmlWriter) { + ns = axmlWriter.updateNs(ns); + name = axmlWriter.update(name); + + int attrIndex = 0; + for (Attr attr : attrs) { + attr.index = attrIndex++; + attr.prepare(axmlWriter); + } + + text = axmlWriter.update(text); + int size = 24 + 36 + attrs.size() * 20;// 24 for end tag,36+x*20 for + // start tag + for (NodeImpl child : children) { + size += child.prepare(axmlWriter); + } + if (text != null) { + size += 28; + } + return size; + } + + @Override + public void text(int ln, String value) { + this.text = new StringItem(value); + this.textLineNumber = ln; + } + + void write(ByteBuffer out) throws IOException { + // start tag + out.putInt(RES_XML_START_ELEMENT_TYPE | (0x0010 << 16)); + out.putInt(36 + attrs.size() * 20); + out.putInt(line); + out.putInt(0xFFFFFFFF); + out.putInt(ns != null ? this.ns.index : -1); + out.putInt(name.index); + out.putInt(0x00140014);// TODO + out.putShort((short) this.attrs.size()); + out.putShort((short) (id == null ? 0 : id.index + 1)); + out.putShort((short) (clz == null ? 0 : clz.index + 1)); + out.putShort((short) (style == null ? 0 : style.index + 1)); + for (Attr attr : attrs) { + out.putInt(attr.ns == null ? -1 : attr.ns.index); + out.putInt(attr.name.index); + out.putInt(attr.raw != null ? attr.raw.index : -1); + out.putInt((attr.type << 24) | 0x000008); + Object v = attr.value; + if (v instanceof StringItem) { + out.putInt(((StringItem) attr.value).index); + } else if (v instanceof Boolean) { + out.putInt(Boolean.TRUE.equals(v) ? -1 : 0); + } else if (v instanceof Float) { + out.putInt(Float.floatToIntBits((float) v)); + } else { + out.putInt((Integer) attr.value); + } + } + + if (this.text != null) { + out.putInt(RES_XML_CDATA_TYPE | (0x0010 << 16)); + out.putInt(28); + out.putInt(textLineNumber); + out.putInt(0xFFFFFFFF); + out.putInt(text.index); + out.putInt(0x00000008); + out.putInt(0x00000000); + } + + // children + for (NodeImpl child : children) { + child.write(out); + } + + // end tag + out.putInt(RES_XML_END_ELEMENT_TYPE | (0x0010 << 16)); + out.putInt(24); + out.putInt(-1); + out.putInt(0xFFFFFFFF); + out.putInt(ns != null ? this.ns.index : -1); + out.putInt(name.index); + } + } + + static class Ns { + int ln; + StringItem prefix; + StringItem uri; + + public Ns(StringItem prefix, StringItem uri, int ln) { + super(); + this.prefix = prefix; + this.uri = uri; + this.ln = ln; + } + } + + private List firsts = new ArrayList(3); + + private Map nses = new HashMap(); + + private List otherString = new ArrayList(); + + private Map resourceId2Str = new HashMap(); + + private List resourceIds = new ArrayList(); + + private List resourceString = new ArrayList(); + + private StringItems stringItems = new StringItems(); + + // TODO add style support + // private List styleItems = new ArrayList(); + + @Override + public NodeVisitor child(String ns, String name) { + NodeImpl first = new NodeImpl(ns, name); + this.firsts.add(first); + return first; + } + + @Override + public void end() { + } + + @Override + public void ns(String prefix, String uri, int ln) { + nses.put(uri, new Ns(prefix == null ? null : new StringItem(prefix), new StringItem(uri), ln)); + } + + private int prepare() throws IOException { + int size = 0; + + for (NodeImpl first : firsts) { + size += first.prepare(this); + } + { + int a = 0; + for (Map.Entry e : nses.entrySet()) { + Ns ns = e.getValue(); + if (ns == null) { + ns = new Ns(null, new StringItem(e.getKey()), 0); + e.setValue(ns); + } + if (ns.prefix == null) { + ns.prefix = new StringItem(String.format("axml_auto_%02d", a++)); + } + ns.prefix = update(ns.prefix); + ns.uri = update(ns.uri); + } + } + + size += nses.size() * 24 * 2; + + this.stringItems.addAll(resourceString); + resourceString = null; + this.stringItems.addAll(otherString); + otherString = null; + this.stringItems.prepare(); + int stringSize = this.stringItems.getSize(); + if (stringSize % 4 != 0) { + stringSize += 4 - stringSize % 4; + } + size += 8 + stringSize; + size += 8 + resourceIds.size() * 4; + return size; + } + + public byte[] toByteArray() throws IOException { + + int size = 8 + prepare(); + ByteBuffer out = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + + out.putInt(RES_XML_TYPE | (0x0008 << 16)); + out.putInt(size); + + int stringSize = this.stringItems.getSize(); + int padding = 0; + if (stringSize % 4 != 0) { + padding = 4 - stringSize % 4; + } + out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16)); + out.putInt(stringSize + padding + 8); + this.stringItems.write(out); + out.put(new byte[padding]); + + out.putInt(RES_XML_RESOURCE_MAP_TYPE | (0x0008 << 16)); + out.putInt(8 + this.resourceIds.size() * 4); + for (Integer i : resourceIds) { + out.putInt(i); + } + + Stack stack = new Stack(); + for (Map.Entry e : this.nses.entrySet()) { + Ns ns = e.getValue(); + stack.push(ns); + out.putInt(RES_XML_START_NAMESPACE_TYPE | (0x0010 << 16)); + out.putInt(24); + out.putInt(-1); + out.putInt(0xFFFFFFFF); + out.putInt(ns.prefix.index); + out.putInt(ns.uri.index); + } + + for (NodeImpl first : firsts) { + first.write(out); + } + + while (stack.size() > 0) { + Ns ns = stack.pop(); + out.putInt(RES_XML_END_NAMESPACE_TYPE | (0x0010 << 16)); + out.putInt(24); + out.putInt(ns.ln); + out.putInt(0xFFFFFFFF); + out.putInt(ns.prefix.index); + out.putInt(ns.uri.index); + } + return out.array(); + } + + StringItem update(StringItem item) { + if (item == null) + return null; + int i = this.otherString.indexOf(item); + if (i < 0) { + StringItem copy = new StringItem(item.data); + this.otherString.add(copy); + return copy; + } else { + return this.otherString.get(i); + } + } + + StringItem updateNs(StringItem item) { + if (item == null) { + return null; + } + String ns = item.data; + if (!this.nses.containsKey(ns)) { + this.nses.put(ns, null); + } + return update(item); + } + + StringItem updateWithResourceId(StringItem name, int resourceId) { + String key = name.data + resourceId; + StringItem item = this.resourceId2Str.get(key); + if (item != null) { + return item; + } else { + StringItem copy = new StringItem(name.data); + resourceIds.add(resourceId); + resourceString.add(copy); + resourceId2Str.put(key, copy); + return copy; + } + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/DumpAdapter.java b/xposed-bridge/src/main/java/pxb/android/axml/DumpAdapter.java new file mode 100644 index 00000000..b68fee2e --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/DumpAdapter.java @@ -0,0 +1,116 @@ +/* + * 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.axml; + +import java.util.HashMap; +import java.util.Map; + +/** + * dump axml to stdout + * + * @author Panxiaobo + */ +public class DumpAdapter extends AxmlVisitor { + protected int deep; + protected Map nses; + + public DumpAdapter() { + this(null); + } + + public DumpAdapter(NodeVisitor nv) { + this(nv, 0, new HashMap()); + } + + public DumpAdapter(NodeVisitor nv, int x, Map nses) { + super(nv); + this.deep = x; + this.nses = nses; + } + + @Override + public void attr(String ns, String name, int resourceId, int type, Object obj) { + for (int i = 0; i < deep; i++) { + System.out.print(" "); + } + if (ns != null) { + System.out.print(String.format("%s:", getPrefix(ns))); + } + System.out.print(name); + if (resourceId != -1) { + System.out.print(String.format("(%08x)", resourceId)); + } + if (obj instanceof String) { + System.out.print(String.format("=[%08x]\"%s\"", type, obj)); + } else if (obj instanceof Boolean) { + System.out.print(String.format("=[%08x]\"%b\"", type, obj)); + } else if (obj instanceof ValueWrapper) { + ValueWrapper w = (ValueWrapper) obj; + System.out.print(String.format("=[%08x]@%08x, raw: \"%s\"", type, w.ref, w.raw)); + } else if (type == TYPE_REFERENCE) { + System.out.print(String.format("=[%08x]@%08x", type, obj)); + } else { + System.out.print(String.format("=[%08x]%08x", type, obj)); + } + System.out.println(); + super.attr(ns, name, resourceId, type, obj); + } + + @Override + public NodeVisitor child(String ns, String name) { + for (int i = 0; i < deep; i++) { + System.out.print(" "); + } + System.out.print("<"); + if (ns != null) { + System.out.print(getPrefix(ns) + ":"); + } + System.out.println(name); + NodeVisitor nv = super.child(ns, name); + if (nv != null) { + return new DumpAdapter(nv, deep + 1, nses); + } + return null; + } + + protected String getPrefix(String uri) { + if (nses != null) { + String prefix = nses.get(uri); + if (prefix != null) { + return prefix; + } + } + return uri; + } + + @Override + public void ns(String prefix, String uri, int ln) { + System.out.println(prefix + "=" + uri); + this.nses.put(uri, prefix); + super.ns(prefix, uri, ln); + } + + @Override + public void text(int ln, String value) { + for (int i = 0; i < deep + 1; i++) { + System.out.print(" "); + } + System.out.print("T: "); + System.out.println(value); + super.text(ln, value); + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/NodeVisitor.java b/xposed-bridge/src/main/java/pxb/android/axml/NodeVisitor.java new file mode 100644 index 00000000..fe6179bc --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/NodeVisitor.java @@ -0,0 +1,97 @@ +/* + * 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.axml; + +public abstract class NodeVisitor { + + public static final int TYPE_FIRST_INT = 0x10; + public static final int TYPE_INT_BOOLEAN = 0x12; + public static final int TYPE_INT_HEX = 0x11; + public static final int TYPE_REFERENCE = 0x01; + public static final int TYPE_STRING = 0x03; + protected NodeVisitor nv; + + public NodeVisitor() { + super(); + } + + public NodeVisitor(NodeVisitor nv) { + super(); + this.nv = nv; + } + + /** + * add attribute to the node + * + * @param ns + * @param name + * @param resourceId + * @param type + * {@link #TYPE_STRING} or others + * @param obj + * a string for {@link #TYPE_STRING} ,and Integer for others + */ + public void attr(String ns, String name, int resourceId, int type, Object obj) { + if (nv != null) { + nv.attr(ns, name, resourceId, type, obj); + } + } + + /** + * create a child node + * + * @param ns + * @param name + * @return + */ + public NodeVisitor child(String ns, String name) { + if (nv != null) { + return nv.child(ns, name); + } + return null; + } + + /** + * end the visit + */ + public void end() { + if (nv != null) { + nv.end(); + } + } + + /** + * line number in the .xml + * + * @param ln + */ + public void line(int ln) { + if (nv != null) { + nv.line(ln); + } + } + + /** + * the node text + * + * @param value + */ + public void text(int lineNumber, String value) { + if (nv != null) { + nv.text(lineNumber, value); + } + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/Util.java b/xposed-bridge/src/main/java/pxb/android/axml/Util.java new file mode 100644 index 00000000..992f39d5 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/Util.java @@ -0,0 +1,78 @@ +/* + * 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.axml; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +public class Util { + public static byte[] readFile(File in) throws IOException { + InputStream is = new FileInputStream(in); + byte[] xml = new byte[is.available()]; + is.read(xml); + is.close(); + return xml; + } + + public static byte[] readIs(InputStream is) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + copy(is, os); + return os.toByteArray(); + } + + public static void writeFile(byte[] data, File out) throws IOException { + FileOutputStream fos = new FileOutputStream(out); + fos.write(data); + fos.close(); + } + + public static Map readProguardConfig(File config) throws IOException { + Map clzMap = new HashMap(); + BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(config), "utf8")); + try { + for (String ln = r.readLine(); ln != null; ln = r.readLine()) { + if (ln.startsWith("#") || ln.startsWith(" ")) { + continue; + } + // format a.pt.Main -> a.a.a: + int i = ln.indexOf("->"); + if (i > 0) { + clzMap.put(ln.substring(0, i).trim(), ln.substring(i + 2, ln.length() - 1).trim()); + } + } + } finally { + r.close(); + } + return clzMap; + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + byte[] xml = new byte[10 * 1024]; + for (int c = is.read(xml); c > 0; c = is.read(xml)) { + os.write(xml, 0, c); + } + } + +} diff --git a/xposed-bridge/src/main/java/pxb/android/axml/ValueWrapper.java b/xposed-bridge/src/main/java/pxb/android/axml/ValueWrapper.java new file mode 100644 index 00000000..c9da562b --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/axml/ValueWrapper.java @@ -0,0 +1,34 @@ +package pxb.android.axml; + +public class ValueWrapper { + + public static final int ID = 1; + public static final int STYLE = 2; + public static final int CLASS = 3; + public final int type; + public final String raw; + public final int ref; + + private ValueWrapper(int type, int ref, String raw) { + super(); + this.type = type; + this.raw = raw; + this.ref = ref; + } + + public ValueWrapper replaceRaw(String raw) { + return new ValueWrapper(type, ref, raw); + } + + public static ValueWrapper wrapId(int ref, String raw) { + return new ValueWrapper(ID, ref, raw); + } + + public static ValueWrapper wrapStyle(int ref, String raw) { + return new ValueWrapper(STYLE, ref, raw); + } + + public static ValueWrapper wrapClass(int ref, String raw) { + return new ValueWrapper(CLASS, ref, raw); + } +} diff --git a/xposed-bridge/src/main/java/pxb/android/package.html b/xposed-bridge/src/main/java/pxb/android/package.html new file mode 100644 index 00000000..ede50bf2 --- /dev/null +++ b/xposed-bridge/src/main/java/pxb/android/package.html @@ -0,0 +1,2 @@ +

Source code of axml project.

+

Revision 2394d02d86bb - Apr 22, 2014

\ No newline at end of file