From 3700ecb717121a0af690d5820c072e56450653d9 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 2 Jan 2015 19:50:09 +0300 Subject: [PATCH] core: add resources methods to jadx API --- .../src/main/java/jadx/cli/JadxCLIArgs.java | 18 +- .../main/java/jadx/api/DefaultJadxArgs.java | 7 +- .../src/main/java/jadx/api/IJadxArgs.java | 4 +- .../main/java/jadx/api/JadxDecompiler.java | 88 +++-- .../src/main/java/jadx/api/ResourceFile.java | 63 ++++ .../src/main/java/jadx/api/ResourceType.java | 50 +++ .../main/java/jadx/api/ResourcesLoader.java | 143 ++++++++ .../jadx/core/xmlgen/BinaryXMLParser.java | 332 ++++++++++-------- .../java/jadx/tests/api/IntegrationTest.java | 5 + 9 files changed, 525 insertions(+), 185 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/api/ResourceFile.java create mode 100644 jadx-core/src/main/java/jadx/api/ResourceType.java create mode 100644 jadx-core/src/main/java/jadx/api/ResourcesLoader.java diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 232667295..2efb20fed 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -32,6 +32,12 @@ public final class JadxCLIArgs implements IJadxArgs { @Parameter(names = {"-f", "--fallback"}, description = "make simple dump (using goto instead of 'if', 'for', etc)") protected boolean fallbackMode = false; + @Parameter(names = {"-r", "--no-res"}, description = "do not decode resources") + protected boolean skipResources = false; + + @Parameter(names = {"-s", "--no-src"}, description = "do not decompile source code") + protected boolean skipSources = false; + @Parameter(names = {"--show-bad-code"}, description = "show inconsistent code (incorrectly decompiled)") protected boolean showInconsistentCode = false; @@ -47,9 +53,6 @@ public final class JadxCLIArgs implements IJadxArgs { @Parameter(names = {"-h", "--help"}, description = "print this help", help = true) protected boolean printHelp = false; - @Parameter(names = {"-x", "--xml"}, description = "try to decode the AndroidManifest.xml") - protected boolean xmlTest = false; - private final List input = new ArrayList(1); private File outputDir; @@ -168,8 +171,13 @@ public final class JadxCLIArgs implements IJadxArgs { } @Override - public boolean isXMLTest() { - return xmlTest; + public boolean isSkipResources() { + return skipResources; + } + + @Override + public boolean isSkipSources() { + return skipSources; } @Override diff --git a/jadx-core/src/main/java/jadx/api/DefaultJadxArgs.java b/jadx-core/src/main/java/jadx/api/DefaultJadxArgs.java index 66bcdfd0e..114b8d6e4 100644 --- a/jadx-core/src/main/java/jadx/api/DefaultJadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/DefaultJadxArgs.java @@ -40,7 +40,12 @@ public class DefaultJadxArgs implements IJadxArgs { } @Override - public boolean isXMLTest() { + public boolean isSkipResources() { + return false; + } + + @Override + public boolean isSkipSources() { return false; } } diff --git a/jadx-core/src/main/java/jadx/api/IJadxArgs.java b/jadx-core/src/main/java/jadx/api/IJadxArgs.java index 32ea4fd6e..424118163 100644 --- a/jadx-core/src/main/java/jadx/api/IJadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/IJadxArgs.java @@ -17,5 +17,7 @@ public interface IJadxArgs { boolean isVerbose(); - boolean isXMLTest(); + boolean isSkipResources(); + + boolean isSkipSources(); } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index f59501765..11389de7b 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -2,6 +2,7 @@ package jadx.api; import jadx.core.Jadx; import jadx.core.ProcessClass; +import jadx.core.codegen.CodeWriter; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; @@ -55,6 +56,9 @@ public final class JadxDecompiler { private RootNode root; private List passes; private List classes; + private List resources; + + private BinaryXMLParser xmlParser; public JadxDecompiler() { this(new DefaultJadxArgs()); @@ -82,6 +86,8 @@ public final class JadxDecompiler { void reset() { ClassInfo.clearCache(); classes = null; + resources = null; + xmlParser = null; root = null; } @@ -108,27 +114,21 @@ public final class JadxDecompiler { parse(); } - public void parseAndSaveXML() { - if (this.args.isXMLTest()) { - InputFile inf = inputFiles.get(0); - try { - byte[] buffer = InputFile.loadXMLBuffer(inf.getFile()); - if (buffer != null) { - File out = new File(outDir, "AndroidManifest.xml"); - BinaryXMLParser bxp = new BinaryXMLParser(root); - bxp.parse(buffer, out); - } - } catch (Exception e) { - LOG.info("Decompiling AndroidManifest.xml failed!", e); - } - } + public void save() { + save(!args.isSkipSources(), !args.isSkipResources()); } - public void save() { - parseAndSaveXML(); + public void saveSources() { + save(true, false); + } + public void saveResources() { + save(false, true); + } + + private void save(boolean saveSources, boolean saveResources) { try { - ExecutorService ex = getSaveExecutor(); + ExecutorService ex = getSaveExecutor(saveSources, saveResources); ex.shutdown(); ex.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { @@ -137,6 +137,10 @@ public final class JadxDecompiler { } public ExecutorService getSaveExecutor() { + return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources()); + } + + private ExecutorService getSaveExecutor(boolean saveSources, boolean saveResources) { if (root == null) { throw new JadxRuntimeException("No loaded files"); } @@ -145,14 +149,31 @@ public final class JadxDecompiler { LOG.info("processing ..."); ExecutorService executor = Executors.newFixedThreadPool(threadsCount); - for (final JavaClass cls : getClasses()) { - executor.execute(new Runnable() { - @Override - public void run() { - cls.decompile(); - SaveCode.save(outDir, args, cls.getClassNode()); - } - }); + if (saveSources) { + for (final JavaClass cls : getClasses()) { + executor.execute(new Runnable() { + @Override + public void run() { + cls.decompile(); + SaveCode.save(outDir, args, cls.getClassNode()); + } + }); + } + } + if (saveResources) { + for (final ResourceFile resourceFile : getResources()) { + executor.execute(new Runnable() { + @Override + public void run() { + if (ResourceType.isSupportedForUnpack(resourceFile.getType())) { + CodeWriter cw = resourceFile.getContent(); + if (cw != null) { + cw.save(new File(outDir, resourceFile.getName())); + } + } + } + }); + } } return executor; } @@ -172,6 +193,16 @@ public final class JadxDecompiler { return classes; } + public List getResources() { + if (resources == null) { + if (root == null) { + return Collections.emptyList(); + } + resources = new ResourcesLoader(this).load(inputFiles); + } + return resources; + } + public List getPackages() { List classList = getClasses(); if (classList.isEmpty()) { @@ -232,6 +263,13 @@ public final class JadxDecompiler { return root; } + BinaryXMLParser getXmlParser() { + if (xmlParser == null) { + xmlParser = new BinaryXMLParser(root); + } + return xmlParser; + } + JavaClass findJavaClass(ClassNode cls) { if (cls == null) { return null; diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java new file mode 100644 index 000000000..8a05ad83e --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -0,0 +1,63 @@ +package jadx.api; + +import jadx.core.codegen.CodeWriter; + +import java.io.File; + +public class ResourceFile { + + public static final class ZipRef { + private final File zipFile; + private final String entryName; + + public ZipRef(File zipFile, String entryName) { + this.zipFile = zipFile; + this.entryName = entryName; + } + + public File getZipFile() { + return zipFile; + } + + public String getEntryName() { + return entryName; + } + + @Override + public String toString() { + return "ZipRef{" + zipFile + ", '" + entryName + "'}"; + } + } + + private final JadxDecompiler decompiler; + private final String name; + private final ResourceType type; + private ZipRef zipRef; + + ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { + this.decompiler = decompiler; + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public ResourceType getType() { + return type; + } + + public CodeWriter getContent() { + return ResourcesLoader.loadContent(decompiler, zipRef, type); + } + + void setZipRef(ZipRef zipRef) { + this.zipRef = zipRef; + } + + @Override + public String toString() { + return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}"; + } +} diff --git a/jadx-core/src/main/java/jadx/api/ResourceType.java b/jadx-core/src/main/java/jadx/api/ResourceType.java new file mode 100644 index 000000000..e27a59740 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/ResourceType.java @@ -0,0 +1,50 @@ +package jadx.api; + +public enum ResourceType { + CODE(".dex", ".class"), + MANIFEST("AndroidManifest.xml"), + XML(".xml"), // TODO binary or not? + ARSC(".arsc"), // TODO decompile !!! + FONT(".ttf"), + IMG(".png", ".gif", ".jpg"), + LIB(".so"), + UNKNOWN; + + private String[] exts; + + ResourceType(String... exts) { + this.exts = exts; + } + + public String[] getExts() { + return exts; + } + + public static ResourceType getFileType(String fileName) { + for (ResourceType type : ResourceType.values()) { + for (String ext : type.getExts()) { + if (fileName.endsWith(ext)) { + return type; + } + } + } + return UNKNOWN; + } + + public static boolean isSupportedForUnpack(ResourceType type) { + switch (type) { + case CODE: + case ARSC: + case LIB: + case XML: + case FONT: + case IMG: + case UNKNOWN: + return false; + + case MANIFEST: + return true; + } + return false; + } +} diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java new file mode 100644 index 000000000..a114ffa29 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -0,0 +1,143 @@ +package jadx.api; + +import jadx.api.ResourceFile.ZipRef; +import jadx.core.codegen.CodeWriter; +import jadx.core.utils.files.InputFile; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// TODO: move to core package +final class ResourcesLoader { + private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); + + private static final int READ_BUFFER_SIZE = 8 * 1024; + private static final int LOAD_SIZE_LIMIT = 500 * 1024; + + private JadxDecompiler jadxRef; + + ResourcesLoader(JadxDecompiler jadxRef) { + this.jadxRef = jadxRef; + } + + List load(List inputFiles) { + List list = new ArrayList(inputFiles.size()); + for (InputFile file : inputFiles) { + loadFile(list, file.getFile()); + } + return list; + } + + static CodeWriter loadContent(JadxDecompiler jadxRef, ZipRef zipRef, ResourceType type) { + if (zipRef == null) { + return null; + } + ZipFile zipFile = null; + InputStream inputStream = null; + try { + zipFile = new ZipFile(zipRef.getZipFile()); + ZipEntry entry = zipFile.getEntry(zipRef.getEntryName()); + if (entry != null) { + if (entry.getSize() > LOAD_SIZE_LIMIT) { + return new CodeWriter().add("File too big, size: " + + String.format("%.2f KB", entry.getSize() / 1024.)); + } + inputStream = new BufferedInputStream(zipFile.getInputStream(entry)); + return decode(jadxRef, type, inputStream); + } else { + LOG.warn("Zip entry not found: {}", zipRef); + } + } catch (IOException e) { + LOG.error("Error load: " + zipRef, e); + } finally { + try { + if (zipFile != null) { + zipFile.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } catch (Exception e) { + LOG.debug("Error close zip file: " + zipRef, e); + } + } + return null; + } + + private static CodeWriter decode(JadxDecompiler jadxRef, ResourceType type, + InputStream inputStream) throws IOException { + switch (type) { + case MANIFEST: + case XML: + return jadxRef.getXmlParser().parse(inputStream); + } + return loadToCodeWriter(inputStream); + } + + private void loadFile(List list, File file) { + if (file == null) { + return; + } + ZipFile zip = null; + try { + zip = new ZipFile(file); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + addEntry(list, file, entry); + } + } catch (IOException e) { + LOG.debug("Not a zip file: " + file.getAbsolutePath()); + } finally { + if (zip != null) { + try { + zip.close(); + } catch (Exception e) { + LOG.error("Zip file close error: " + file.getAbsolutePath(), e); + } + } + } + } + + private void addEntry(List list, File zipFile, ZipEntry entry) { + if (entry.isDirectory()) { + return; + } + String name = entry.getName(); + ResourceType type = ResourceType.getFileType(name); + ResourceFile rf = new ResourceFile(jadxRef, name, type); + rf.setZipRef(new ZipRef(zipFile, name)); + list.add(rf); + // LOG.debug("Add resource entry: {}, size: {}", name, entry.getSize()); + } + + private static CodeWriter loadToCodeWriter(InputStream is) throws IOException { + CodeWriter cw = new CodeWriter(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE); + byte[] buffer = new byte[READ_BUFFER_SIZE]; + int count; + try { + while ((count = is.read(buffer)) != -1) { + baos.write(buffer, 0, count); + } + } finally { + try { + is.close(); + } catch (Exception ignore) { + } + } + cw.add(baos.toString("UTF-8")); + return cw; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java index 26f5ff8fb..fa88be0a7 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -5,11 +5,12 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; -import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; @@ -32,9 +33,11 @@ public class BinaryXMLParser { private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); - private byte[] bytes; + private static final Charset STRING_CHARSET = Charset.forName("UTF-16LE"); + + private CodeWriter writer; + private InputStream input; private String[] strings; - private int count; private String nsPrefix = "ERROR"; private String nsURI = "ERROR"; @@ -43,9 +46,9 @@ public class BinaryXMLParser { private boolean firstElement; private boolean wasOneLiner = false; - private CodeWriter writer; private Map styleMap = new HashMap(); private Map localStyleMap = new HashMap(); + private final ManifestAttributes attributes; public BinaryXMLParser(RootNode root) { @@ -70,30 +73,34 @@ public class BinaryXMLParser { } } - public void parse(byte[] xmlBytes, File out) { - LOG.debug("Decoding AndroidManifest.xml, output: {}", out); - + public synchronized CodeWriter parse(InputStream inputStream) { writer = new CodeWriter(); writer.add(""); - bytes = xmlBytes; - count = 0; + input = inputStream; firstElement = true; - decode(); - writer.save(out); + try { + decode(); + } catch (IOException e) { + LOG.debug("Binary xml decode failed", e); + CodeWriter cw = new CodeWriter(); + cw.add("Error decode binary xml"); + cw.startLine(Utils.getStackTrace(e)); + return cw; + } + writer.finish(); + return writer; } - void decode() { - if (cInt16(bytes, count) != 0x0003) { + void decode() throws IOException { + if (cInt16() != 0x0003) { die("Version is not 3"); } - if (cInt16(bytes, count) != 0x0008) { + if (cInt16() != 0x0008) { die("Size of header is not 8"); } - if (cInt32(bytes, count) != bytes.length) { - die("Size of manifest doesn't match"); - } - while ((count + 2) <= bytes.length) { - int type = cInt16(bytes, count); + cInt32(); + while (input.available() != 0) { + int type = cInt16(); switch (type) { case 0x0001: parseStringPool(); @@ -124,105 +131,106 @@ public class BinaryXMLParser { } } - private void parseStringPool() { - if (cInt16(bytes, count) != 0x001c) { + private void parseStringPool() throws IOException { + if (cInt16() != 0x001c) { die("Header header size not 28"); } - int hsize = cInt32(bytes, count); - int stringCount = cInt32(bytes, count); - int styleCount = cInt32(bytes, count); - int flags = cInt32(bytes, count); - int stringsStart = cInt32(bytes, count); - int stylesStart = cInt32(bytes, count); + int hsize = cInt32(); + int stringCount = cInt32(); + int styleCount = cInt32(); + int flags = cInt32(); + int stringsStart = cInt32(); + int stylesStart = cInt32(); int[] stringsOffsets = new int[stringCount]; for (int i = 0; i < stringCount; i++) { - stringsOffsets[i] = cInt32(bytes, count); + stringsOffsets[i] = cInt32(); } strings = new String[stringCount]; for (int i = 0; i < stringCount; i++) { int off = 8 + stringsStart + stringsOffsets[i]; - int strlen = cInt16(bytes, off); + int strlen = cInt16(); byte[] str = new byte[strlen * 2]; - System.arraycopy(bytes, count, str, 0, strlen * 2); - count += strlen * 2; - strings[i] = new String(str, Charset.forName("UTF-16LE")); - count += 2; + readToArray(str); + strings[i] = new String(str, STRING_CHARSET); + cInt16(); } } - private void parseResourceMap() { - if (cInt16(bytes, count) != 0x8) { + private void parseResourceMap() throws IOException { + if (cInt16() != 0x8) { die("Header size of resmap is not 8!"); } - int rhsize = cInt32(bytes, count); + int rhsize = cInt32(); int[] ids = new int[(rhsize - 8) / 4]; for (int i = 0; i < ids.length; i++) { - ids[i] = cInt32(bytes, count); + ids[i] = cInt32(); } } - private void parseNameSpace() { - if (cInt16(bytes, count) != 0x0010) { + private void parseNameSpace() throws IOException { + if (cInt16() != 0x10) { die("NAMESPACE header is not 0x0010"); } - if (cInt32(bytes, count) != 0x18) { + if (cInt32() != 0x18) { die("NAMESPACE header chunk is not 0x18 big"); } - int beginLineNumber = cInt32(bytes, count); - int comment = cInt32(bytes, count); - int beginPrefix = cInt32(bytes, count); + int beginLineNumber = cInt32(); + int comment = cInt32(); + int beginPrefix = cInt32(); nsPrefix = strings[beginPrefix]; - int beginURI = cInt32(bytes, count); + int beginURI = cInt32(); nsURI = strings[beginURI]; } - private void parseNameSpaceEnd() { - if (cInt16(bytes, count) != 0x0010) { + private void parseNameSpaceEnd() throws IOException { + if (cInt16() != 0x10) { die("NAMESPACE header is not 0x0010"); } - if (cInt32(bytes, count) != 0x18) { + if (cInt32() != 0x18) { die("NAMESPACE header chunk is not 0x18 big"); } - int endLineNumber = cInt32(bytes, count); - int comment = cInt32(bytes, count); - int endPrefix = cInt32(bytes, count); + int endLineNumber = cInt32(); + int comment = cInt32(); + int endPrefix = cInt32(); nsPrefix = strings[endPrefix]; - int endURI = cInt32(bytes, count); + int endURI = cInt32(); nsURI = strings[endURI]; } - private void parseElement() { + private void parseElement() throws IOException { if (firstElement) { firstElement = false; } else { writer.incIndent(); } - if (cInt16(bytes, count) != 0x0010) { + if (cInt16() != 0x10) { die("ELEMENT HEADER SIZE is not 0x10"); } - count += 4; // TODO: Check element chunk size - int elementBegLineNumber = cInt32(bytes, count); - int comment = cInt32(bytes, count); - int startNS = cInt32(bytes, count); - int startNSName = cInt32(bytes, count); // actually is elementName... + // TODO: Check element chunk size + cInt32(); + int elementBegLineNumber = cInt32(); + int comment = cInt32(); + int startNS = cInt32(); + int startNSName = cInt32(); // actually is elementName... if (!wasOneLiner && !"ERROR".equals(currentTag) && !currentTag.equals(strings[startNSName])) { writer.add(">"); } wasOneLiner = false; currentTag = strings[startNSName]; writer.startLine("<").add(strings[startNSName]); - int attributeStart = cInt16(bytes, count); + writer.attachSourceLine(elementBegLineNumber); + int attributeStart = cInt16(); if (attributeStart != 0x14) { die("startNS's attributeStart is not 0x14"); } - int attributeSize = cInt16(bytes, count); + int attributeSize = cInt16(); if (attributeSize != 0x14) { die("startNS's attributeSize is not 0x14"); } - int attributeCount = cInt16(bytes, count); - int idIndex = cInt16(bytes, count); - int classIndex = cInt16(bytes, count); - int styleIndex = cInt16(bytes, count); + int attributeCount = cInt16(); + int idIndex = cInt16(); + int classIndex = cInt16(); + int styleIndex = cInt16(); if ("manifest".equals(strings[startNSName])) { writer.add(" xmlns:\"").add(nsURI).add("\""); } @@ -230,71 +238,7 @@ public class BinaryXMLParser { writer.add(" "); } for (int i = 0; i < attributeCount; i++) { - int attributeNS = cInt32(bytes, count); - int attributeName = cInt32(bytes, count); - int attributeRawValue = cInt32(bytes, count); - int attrValSize = cInt16(bytes, count); - if (attrValSize != 0x08) { - die("attrValSize != 0x08 not supported"); - } - if (cInt8(bytes, count) != 0) { - die("res0 is not 0"); - } - int attrValDataType = cInt8(bytes, count); - int attrValData = cInt32(bytes, count); - if (attributeNS != -1) { - writer.add(nsPrefix).add(':'); - } - String attrName = strings[attributeName]; - writer.add(attrName).add("=\""); - String decodedAttr = attributes.decode(attrName, attrValData); - if (decodedAttr != null) { - writer.add(decodedAttr); - } else { - switch (attrValDataType) { - case 0x3: - writer.add(strings[attrValData]); - break; - - case 0x10: - writer.add(String.valueOf(attrValData)); - break; - - case 0x12: - // FIXME: What to do, when data is always -1? - if (attrValData == 0) { - writer.add("false"); - } else if (attrValData == 1 || attrValData == -1) { - writer.add("true"); - } else { - writer.add("UNKNOWN_BOOLEAN_TYPE"); - } - break; - - case 0x1: - String name = styleMap.get(attrValData); - if (name != null) { - writer.add("@*"); - if (attributeNS != -1) { - writer.add(nsPrefix).add(':'); - } - writer.add("style/").add(name.replaceAll("_", ".")); - } else { - FieldNode field = localStyleMap.get(attrValData); - if (field != null) { - String cls = field.getParentClass().getShortName().toLowerCase(); - writer.add("@").add(cls).add("/").add(field.getName()); - } else { - writer.add("0x").add(Integer.toHexString(attrValData)); - } - } - break; - - default: - writer.add("UNKNOWN_DATA_TYPE_" + attrValDataType); - break; - } - } + parseAttribute(i); writer.add('"'); if ((i + 1) < attributeCount) { writer.add(" "); @@ -302,22 +246,95 @@ public class BinaryXMLParser { } } - private void parseElementEnd() { - if (cInt16(bytes, count) != 0x0010) { - die("ELEMENT END header is not 0x0010"); + private void parseAttribute(int i) throws IOException { + int attributeNS = cInt32(); + int attributeName = cInt32(); + int attributeRawValue = cInt32(); + int attrValSize = cInt16(); + if (attrValSize != 0x08) { + die("attrValSize != 0x08 not supported"); } - if (cInt32(bytes, count) != 0x18) { + if (cInt8() != 0) { + die("res0 is not 0"); + } + int attrValDataType = cInt8(); + int attrValData = cInt32(); + if (attributeNS != -1) { + writer.add(nsPrefix).add(':'); + } + String attrName = strings[attributeName]; + writer.add(attrName).add("=\""); + String decodedAttr = attributes.decode(attrName, attrValData); + if (decodedAttr != null) { + writer.add(decodedAttr); + } else { + decodeAttribute(attributeNS, attrValDataType, attrValData); + } + } + + private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData) { + switch (attrValDataType) { + case 0x3: + writer.add(strings[attrValData]); + break; + + case 0x10: + writer.add(String.valueOf(attrValData)); + break; + + case 0x12: + // FIXME: What to do, when data is always -1? + if (attrValData == 0) { + writer.add("false"); + } else if (attrValData == 1 || attrValData == -1) { + writer.add("true"); + } else { + writer.add("UNKNOWN_BOOLEAN_TYPE"); + } + break; + + case 0x1: + String name = styleMap.get(attrValData); + if (name != null) { + writer.add("@*"); + if (attributeNS != -1) { + writer.add(nsPrefix).add(':'); + } + writer.add("style/").add(name.replaceAll("_", ".")); + } else { + FieldNode field = localStyleMap.get(attrValData); + if (field != null) { + String cls = field.getParentClass().getShortName().toLowerCase(); + writer.add("@").add(cls).add("/").add(field.getName()); + } else { + writer.add("0x").add(Integer.toHexString(attrValData)); + } + } + break; + + default: + writer.add("UNKNOWN_DATA_TYPE_" + attrValDataType); + break; + } + } + + private void parseElementEnd() throws IOException { + if (cInt16() != 0x10) { + die("ELEMENT END header is not 0x10"); + } + if (cInt32() != 0x18) { die("ELEMENT END header chunk is not 0x18 big"); } - int endLineNumber = cInt32(bytes, count); - int comment = cInt32(bytes, count); - int elementNS = cInt32(bytes, count); - int elementName = cInt32(bytes, count); + int endLineNumber = cInt32(); + int comment = cInt32(); + int elementNS = cInt32(); + int elementName = cInt32(); if (currentTag.equals(strings[elementName])) { writer.add(" />"); wasOneLiner = true; } else { writer.startLine("