From ba345ac6b948c5e795722693072dbe241094e95e Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sun, 8 Jul 2012 10:58:43 -0500 Subject: [PATCH] increased v1.4.8, merging more of yyj fixes in hopes to fix this arsc error --- apktool-cli/pom.xml | 2 +- apktool-lib/pom.xml | 2 +- .../src/main/java/brut/androlib/Androlib.java | 4 +- .../main/java/brut/androlib/ApkDecoder.java | 10 + .../brut/androlib/res/AndrolibResources.java | 38 +- .../androlib/res/data/ResConfigFlags.java | 4 +- .../java/brut/androlib/res/data/ResTable.java | 14 + .../res/data/value/ResPluralsValue.java | 6 +- .../res/data/value/ResScalarValue.java | 4 +- .../androlib/res/decoder/ARSCDecoder.java | 7 +- .../res/decoder/AXmlResourceParser.java | 182 ++- .../androlib/res/decoder/ResFileDecoder.java | 26 +- .../androlib/res/decoder/StringBlock.java | 22 + .../res/decoder/XmlPullStreamDecoder.java | 89 +- .../androlib/res/util/ExtMXSerializer.java | 127 +- .../androlib/res/util/ExtXmlSerializer.java | 2 + .../java/brut/androlib/src/SmaliDecoder.java | 2 +- .../xmlpull/mxp1_serializer/MXSerializer.java | 1160 +++++++++++++++++ 18 files changed, 1638 insertions(+), 63 deletions(-) create mode 100644 apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java diff --git a/apktool-cli/pom.xml b/apktool-cli/pom.xml index c22844b4..c14f26f4 100644 --- a/apktool-cli/pom.xml +++ b/apktool-cli/pom.xml @@ -3,7 +3,7 @@ brut.apktool apktool-cli - 1.4.7-SNAPSHOT + 1.4.8-SNAPSHOT jar diff --git a/apktool-lib/pom.xml b/apktool-lib/pom.xml index 1f0a470b..a4c75052 100644 --- a/apktool-lib/pom.xml +++ b/apktool-lib/pom.xml @@ -3,7 +3,7 @@ brut.apktool apktool-lib - 1.4.7-SNAPSHOT + 1.4.8-SNAPSHOT jar diff --git a/apktool-lib/src/main/java/brut/androlib/Androlib.java b/apktool-lib/src/main/java/brut/androlib/Androlib.java index 077eecf6..a367c505 100644 --- a/apktool-lib/src/main/java/brut/androlib/Androlib.java +++ b/apktool-lib/src/main/java/brut/androlib/Androlib.java @@ -93,7 +93,7 @@ public class Androlib { } public void decodeManifestFull(ExtFile apkFile, File outDir, - ResTable resTable) throws AndrolibException { + ResTable resTable) throws AndrolibException { mAndRes.decodeManifest(resTable, apkFile, outDir); } @@ -523,4 +523,4 @@ public class Androlib { new String[]{"AndroidManifest.xml", "res"}; private final static String[] APK_MANIFEST_FILENAMES = new String[]{"AndroidManifest.xml"}; -} \ No newline at end of file +} diff --git a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index 8e0c5be1..6dee5c05 100644 --- a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -217,6 +217,7 @@ public class ApkDecoder { meta.put("isFrameworkApk", Boolean.valueOf(mAndrolib.isFrameworkApk(getResTable()))); putUsesFramework(meta); + putSdkInfo(meta); } mAndrolib.writeMetaFile(mOutDir, meta); @@ -246,6 +247,15 @@ public class ApkDecoder { meta.put("usesFramework", uses); } + private void putSdkInfo(Map meta) + throws AndrolibException { + Map info = getResTable().getSdkInfo(); + if (info.size() > 0) { + meta.put("sdkInfo", info); + } + + } + private final Androlib mAndrolib; private ExtFile mApkFile; diff --git a/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java b/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java index f770ddd3..199bc309 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java +++ b/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java @@ -124,9 +124,8 @@ final public class AndrolibResources { out = new FileDirectory(outDir); LOGGER.info("Decoding AndroidManifest.xml with only framework resources..."); - fileDecoder.decode( - inApk, "AndroidManifest.xml", out, "AndroidManifest.xml", - "xml"); + fileDecoder.decodeManifest( + inApk, "AndroidManifest.xml", out, "AndroidManifest.xml"); } catch (DirectoryException ex) { throw new AndrolibException(ex); @@ -148,9 +147,9 @@ final public class AndrolibResources { out = new FileDirectory(outDir); LOGGER.info("Decoding AndroidManifest.xml with resources..."); - fileDecoder.decode( - inApk, "AndroidManifest.xml", out, "AndroidManifest.xml", - "xml"); + + fileDecoder.decodeManifest( + inApk, "AndroidManifest.xml", out, "AndroidManifest.xml"); if (inApk.containsDir("res")) { in = inApk.getDir("res"); @@ -183,6 +182,14 @@ final public class AndrolibResources { } } + public void setSdkInfo(Map map) { + if(map != null) { + mMinSdkVersion = map.get("minSdkVersion"); + mTargetSdkVersion = map.get("targetSdkVersion"); + mMaxSdkVersion = map.get("maxSdkVersion"); + } + } + public void aaptPackage(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include, boolean update, boolean framework) throws AndrolibException { @@ -193,6 +200,18 @@ final public class AndrolibResources { if (update) { cmd.add("-u"); } + if (mMinSdkVersion != null) { + cmd.add("--min-sdk-version"); + cmd.add(mMinSdkVersion); + } + if (mTargetSdkVersion != null) { + cmd.add("--target-sdk-version"); + cmd.add(mTargetSdkVersion); + } + if (mMaxSdkVersion != null) { + cmd.add("--max-sdk-version"); + cmd.add(mMaxSdkVersion); + } cmd.add("-F"); cmd.add(apkFile.getAbsolutePath()); @@ -541,4 +560,9 @@ final public class AndrolibResources { private final static Logger LOGGER = Logger.getLogger(AndrolibResources.class.getName()); -} \ No newline at end of file + + private String mMinSdkVersion = null; + private String mMaxSdkVersion = null; + private String mTargetSdkVersion = null; + +} diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java b/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java index e10ae9e0..bd235509 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java +++ b/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java @@ -196,7 +196,9 @@ public class ResConfigFlags { case UI_MODE_TYPE_DESK: ret.append("-desk"); break; - + case UI_MODE_TYPE_TELEVISION: + ret.append("-television"); + break; case UI_MODE_TYPE_APPLIANCE: ret.append("-appliance"); break; diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java b/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java index 1d2fecb0..58949285 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java +++ b/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java @@ -39,6 +39,8 @@ public class ResTable { private String mFrameTag; + private Map mSdkInfo = new LinkedHashMap(); + public ResTable() { mAndRes = null; } @@ -120,4 +122,16 @@ public class ResTable { public void setFrameTag(String tag) { mFrameTag = tag; } + + public Map getSdkInfo() { + return mSdkInfo; + } + + public void addSdkInfo(String key, String value) { + mSdkInfo.put(key, value); + } + + public void clearSdkInfo() { + mSdkInfo.clear(); + } } diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java index 60b4a5cd..39930619 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java +++ b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java @@ -16,9 +16,10 @@ package brut.androlib.res.data.value; +import brut.androlib.res.xml.ResValuesXmlSerializable; +import brut.androlib.res.xml.ResXmlEncoders; import brut.androlib.AndrolibException; import brut.androlib.res.data.ResResource; -import brut.androlib.res.xml.ResValuesXmlSerializable; import brut.util.Duo; import java.io.IOException; import org.xmlpull.v1.XmlSerializer; @@ -63,7 +64,8 @@ public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializ } serializer.endTag(null, "plurals"); } - + + private final ResScalarValue[] mItems; diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java index 92d95344..0764d98b 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java +++ b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java @@ -18,9 +18,9 @@ package brut.androlib.res.data.value; import brut.androlib.res.xml.ResValuesXmlSerializable; import brut.androlib.res.xml.ResXmlEncodable; +import brut.androlib.res.xml.ResXmlEncoders; import brut.androlib.AndrolibException; import brut.androlib.res.data.ResResource; -import brut.androlib.res.xml.ResXmlEncoders; import java.io.IOException; import org.xmlpull.v1.XmlSerializer; @@ -54,7 +54,7 @@ public abstract class ResScalarValue extends ResValue } return encodeAsResXml(); } - + public String encodeAsResXmlValueExt() throws AndrolibException { String rawValue = mRawValue; if (rawValue != null) { diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java index ddb22949..661cf688 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java @@ -139,8 +139,7 @@ public class ARSCDecoder { checkChunkType(Header.TYPE_CONFIG); /*typeId*/ mIn.skipInt(); int entryCount = mIn.readInt(); - /*entriesStart*/ - mIn.skipInt(); + /*entriesStart*/ mIn.skipInt(); ResConfigFlags flags = readConfigFlags(); int[] entryOffsets = mIn.readIntArray(entryCount); @@ -248,13 +247,13 @@ public class ARSCDecoder { byte keyboard = mIn.readByte(); byte navigation = mIn.readByte(); byte inputFlags = mIn.readByte(); - mIn.skipBytes(1); + /*inputPad0*/ mIn.skipBytes(1); short screenWidth = mIn.readShort(); short screenHeight = mIn.readShort(); short sdkVersion = mIn.readShort(); - mIn.skipBytes(2); + /*minorVersion, now must always be 0*/ mIn.skipBytes(2); byte screenLayout = 0; byte uiMode = 0; diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java index 11a55b97..cd7b848a 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java @@ -17,9 +17,14 @@ package brut.androlib.res.decoder; import android.content.res.XmlResourceParser; + +import java.io.BufferedWriter; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.Writer; + import org.xmlpull.v1.XmlPullParserException; import android.util.TypedValue; import brut.androlib.AndrolibException; @@ -812,7 +817,9 @@ public class AXmlResourceParser implements XmlResourceParser { } int event = m_event; - resetEventInfo(); + if (event != START_DOCUMENT) {//keep m_lineNumber + resetEventInfo(); + } while (true) { if (m_decreaseDepth) { @@ -834,47 +841,49 @@ public class AXmlResourceParser implements XmlResourceParser { chunkType = CHUNK_XML_START_TAG; } else { chunkType = m_reader.readInt(); - } - if (chunkType == CHUNK_RESOURCEIDS) { - int chunkSize = m_reader.readInt(); - if (chunkSize < 8 || (chunkSize % 4) != 0) { - throw new IOException("Invalid resource ids size (" + chunkSize + ")."); + if (chunkType == CHUNK_RESOURCEIDS) { + int chunkSize = m_reader.readInt(); + if (chunkSize < 8 || (chunkSize % 4) != 0) { + throw new IOException("Invalid resource ids size (" + chunkSize + ")."); + } + m_resourceIDs = m_reader.readIntArray(chunkSize / 4 - 2); + continue; } - m_resourceIDs = m_reader.readIntArray(chunkSize / 4 - 2); - continue; - } - if (chunkType < CHUNK_XML_FIRST || chunkType > CHUNK_XML_LAST) { - throw new IOException("Invalid chunk type (" + chunkType + ")."); - } - - // Fake START_DOCUMENT event. - if (chunkType == CHUNK_XML_START_TAG && event == -1) { - m_event = START_DOCUMENT; - break; - } - - // Common header. - /*chunkSize*/ m_reader.skipInt(); - int lineNumber = m_reader.readInt(); - /*0xFFFFFFFF*/ m_reader.skipInt(); - - if (chunkType == CHUNK_XML_START_NAMESPACE - || chunkType == CHUNK_XML_END_NAMESPACE) { - if (chunkType == CHUNK_XML_START_NAMESPACE) { - int prefix = m_reader.readInt(); - int uri = m_reader.readInt(); - m_namespaces.push(prefix, uri); - } else { - /*prefix*/ m_reader.skipInt(); - /*uri*/ m_reader.skipInt(); - m_namespaces.pop(); + if (chunkType < CHUNK_XML_FIRST || chunkType > CHUNK_XML_LAST) { + throw new IOException("Invalid chunk type (" + chunkType + ")."); } - continue; + + // Common header. + /*chunkSize*/ m_reader.skipInt(); + int lineNumber = m_reader.readInt(); + /*0xFFFFFFFF*/ m_reader.skipInt(); + + // Fake START_DOCUMENT event. + if (chunkType == CHUNK_XML_START_TAG && event == -1) { + m_event = START_DOCUMENT; + m_lineNumber = lineNumber; + break; + } + + if (chunkType == CHUNK_XML_START_NAMESPACE + || chunkType == CHUNK_XML_END_NAMESPACE) { + if (chunkType == CHUNK_XML_START_NAMESPACE) { + int prefix = m_reader.readInt(); + int uri = m_reader.readInt(); + m_namespaces.push(prefix, uri); + } else { + /*prefix*/ m_reader.skipInt(); + /*uri*/ m_reader.skipInt(); + m_namespaces.pop(); + } + continue; + } + + m_lineNumber = lineNumber; } - m_lineNumber = lineNumber; if (chunkType == CHUNK_XML_START_TAG) { m_namespaceUri = m_reader.readInt(); @@ -893,6 +902,13 @@ public class AXmlResourceParser implements XmlResourceParser { } m_namespaces.increaseDepth(); m_event = START_TAG; + m_strings.touch(m_name, m_name); + for(int i = 0; i array.length) + max = array.length; + if(min < 0) + min = 0; + StringBuffer sb = new StringBuffer("["); + int i = min; + while(true) { + sb.append(array[i]); + i++; + if(i < max) { + sb.append(", "); + } else { + sb.append("]"); + break; + } + } + return sb.toString(); + } + + private boolean compareAttr(int[] attr1, int[] attr2) { + //TODO: sort Attrs + /* + * ATTRIBUTE_IX_VALUE_TYPE == TYPE_STRING : ATTRIBUTE_IX_VALUE_STRING + * : ATTRIBUTE_IX_NAMESPACE_URI + * ATTRIBUTE_IX_NAMESPACE_URI : ATTRIBUTE_IX_NAME + * id + * + */ + if(attr1[ATTRIBUTE_IX_VALUE_TYPE] == TypedValue.TYPE_STRING && + attr1[ATTRIBUTE_IX_VALUE_TYPE] == attr2[ATTRIBUTE_IX_VALUE_TYPE] && + //(m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) || + // m_strings.touch(attr2[ATTRIBUTE_IX_VALUE_STRING], m_name)) && + //m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) && + attr1[ATTRIBUTE_IX_VALUE_STRING] != attr2[ATTRIBUTE_IX_VALUE_STRING]) { + return (attr1[ATTRIBUTE_IX_VALUE_STRING] < attr2[ATTRIBUTE_IX_VALUE_STRING]); + } else if ((attr1[ATTRIBUTE_IX_NAMESPACE_URI] == attr2[ATTRIBUTE_IX_NAMESPACE_URI]) && (attr1[ATTRIBUTE_IX_NAMESPACE_URI] != -1) && + //(m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) || + // m_strings.touch(attr2[ATTRIBUTE_IX_NAME], m_name)) && + //m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) && + (attr1[ATTRIBUTE_IX_NAME] != attr2[ATTRIBUTE_IX_NAME])) { + return (attr1[ATTRIBUTE_IX_NAME] < attr2[ATTRIBUTE_IX_NAME]); + //} else if (attr1[ATTRIBUTE_IX_NAMESPACE_URI] < attr2[ATTRIBUTE_IX_NAMESPACE_URI]) { + // return true; + } else { + return false; + } + } + + private void sortAttrs() { + int attributeCount = m_attributes.length/ATTRIBUTE_LENGHT; + int tmp1[][] = new int[attributeCount][]; + int tmp2[] = null; + for (int i = 0; i < attributeCount;i++) { + tmp1[i] = new int[ATTRIBUTE_LENGHT+1]; + for(int j = 0; j < ATTRIBUTE_LENGHT; j++) { + tmp1[i][j] = m_attributes[i*ATTRIBUTE_LENGHT+j]; + } + tmp1[i][ATTRIBUTE_LENGHT] = i; + if(DBG) { + try { + if (dbgOut == null) { + dbgOut = new BufferedWriter(new FileWriter("C:\\res.log",false)); + } + dbgOut.write("Namespace: " + getAttributeNamespace (i) + + ", Name: " + getAttributeName (i)+ + ", Value: " + getAttributeValue (i) + ", Array: " + + formatArray(tmp1[i], 0, ATTRIBUTE_LENGHT) + "\n"); + dbgOut.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + for (int j = 1; j < attributeCount;j++) { + for (int i = 1; i < attributeCount;i++) { + if(compareAttr(tmp1[i], tmp1[i-1])) { + tmp2 = tmp1[i-1]; + tmp1[i-1] = tmp1[i]; + tmp1[i] = tmp2; + } + } + } + for (int i = 0; i < attributeCount;i++) { + for(int j = 0; j < ATTRIBUTE_LENGHT; j++) { + m_attributes[i*ATTRIBUTE_LENGHT+j] = tmp1[i][j]; + } + } + } + + private void setFirstError(AndrolibException error) { if (mFirstError == null) { mFirstError = error; } @@ -964,4 +1070,6 @@ public class AXmlResourceParser implements XmlResourceParser { CHUNK_XML_END_TAG = 0x00100103, CHUNK_XML_TEXT = 0x00100104, CHUNK_XML_LAST = 0x00100104; + private Writer dbgOut = null; + private final static boolean DBG = false; } \ No newline at end of file diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java index b0c9ca48..e16f9b59 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java @@ -116,6 +116,30 @@ public class ResFileDecoder { } } + public void decodeManifest(Directory inDir, String inFileName, Directory outDir, + String outFileName) throws AndrolibException { + InputStream in = null; + OutputStream out = null; + try { + in = inDir.getFileInput(inFileName); + out = outDir.getFileOutput(outFileName); + ((XmlPullStreamDecoder)mDecoders.getDecoder("xml")).decodeManifest(in, out); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } finally { + try{ + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } catch (IOException ex) { + throw new AndrolibException(ex); + } + } + } + private final static Logger LOGGER = Logger.getLogger(ResFileDecoder.class.getName()); -} \ No newline at end of file +} diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java index fd6c7a4c..f2e5024f 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java @@ -22,6 +22,7 @@ import brut.util.ExtDataInput; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.*; +import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,6 +54,10 @@ public class StringBlock { StringBlock block = new StringBlock(); block.m_isUTF8 = (flags & UTF8_FLAG) != 0; block.m_stringOffsets = reader.readIntArray(stringCount); + block.m_stringOwns = new int[stringCount]; + for (int i=0;i= m_stringOwns.length) { + return false; + } + if(m_stringOwns[index] == -1) { + m_stringOwns[index] = own; + return true; + } else if (m_stringOwns[index] == own) { + return true; + } else { + return false; + } + } + private int[] m_stringOffsets; private byte[] m_strings; private int[] m_styleOffsets; private int[] m_styles; private boolean m_isUTF8; + private int[] m_stringOwns; private static final CharsetDecoder UTF16LE_DECODER = Charset.forName("UTF-16LE").newDecoder(); private static final CharsetDecoder UTF8_DECODER = diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java index b6829203..dfb6a8e0 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java @@ -17,10 +17,15 @@ package brut.androlib.res.decoder; import brut.androlib.AndrolibException; +import brut.androlib.res.AndrolibResources; +import brut.androlib.res.data.ResTable; import brut.androlib.res.util.ExtXmlSerializer; import java.io.*; +import java.util.logging.Logger; + import org.xmlpull.v1.*; import org.xmlpull.v1.wrapper.*; +import org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper; /** * @author Ryszard Wiśniewski @@ -37,7 +42,74 @@ public class XmlPullStreamDecoder implements ResStreamDecoder { try { XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance(); XmlPullParserWrapper par = factory.newPullParserWrapper(mParser); - XmlSerializerWrapper ser = factory.newSerializerWrapper(mSerial); + final ResTable resTable = ((AXmlResourceParser)mParser).getAttrDecoder().getCurrentPackage().getResTable(); + final boolean optimizeForManifest = mOptimizeForManifest; + XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory){ + boolean hideSdkInfo = false; + @Override + public void event(XmlPullParser pp) throws XmlPullParserException, IOException { + int type = pp.getEventType(); + int newLine = pp.getLineNumber(); + if ((!optimizeForManifest) || newLine != 0) { + ((ExtXmlSerializer)xs).setLineNumber(newLine, type); + super.event(pp); + } else { + if (type == XmlPullParser.START_TAG) { + if ("uses-sdk".equalsIgnoreCase(pp.getName())) { + + //TODO: parse uses-sdk( and some others?) + /* + * (--version-code) + * (--version-name) + * (debuggable) + * (...) + * --min-sdk-version + * --target-sdk-version + * --max-sdk-version + */ + try { + hideSdkInfo = parseAttr(pp); + if(hideSdkInfo) { + return; + } + } catch (AndrolibException e) {} + } + LOGGER.warning("Found generated line but parse failed, output it in xml."); + } else if (hideSdkInfo && type == XmlPullParser.END_TAG && + "uses-sdk".equalsIgnoreCase(pp.getName())) { + return; + } + //((ExtXmlSerializer)xs).setLineNumber(newLine, type); + super.event(pp); + } + } + + private boolean parseAttr(XmlPullParser pp) throws AndrolibException { + ResTable restable = resTable; + for (int i = 0; i < pp.getAttributeCount(); i++) { + final String a_ns = "http://schemas.android.com/apk/res/android"; + String ns = pp.getAttributeNamespace (i); + if (a_ns.equalsIgnoreCase(ns)) { + String name = pp.getAttributeName (i); + String value = pp.getAttributeValue (i); + if (name != null && value != null) { + if (name.equalsIgnoreCase("minSdkVersion") || + name.equalsIgnoreCase("targetSdkVersion") || + name.equalsIgnoreCase("maxSdkVersion")) { + restable.addSdkInfo(name, value); + } else { + restable.clearSdkInfo(); + return false;//Found unknown flags + } + } + } else { + resTable.clearSdkInfo(); + return false;//Found unknown flags + } + } + return true; + } + };//factory.newSerializerWrapper(mSerial); par.setInput(in, null); ser.setOutput(out, null); @@ -54,6 +126,21 @@ public class XmlPullStreamDecoder implements ResStreamDecoder { } } + public void decodeManifest(InputStream in, OutputStream out) + throws AndrolibException { + mOptimizeForManifest = true; + try { + decode(in, out); + } finally { + mOptimizeForManifest = false; + } + } + private final XmlPullParser mParser; private final ExtXmlSerializer mSerial; + + private boolean mOptimizeForManifest = false; + + private final static Logger LOGGER = + Logger.getLogger(XmlPullStreamDecoder.class.getName()); } diff --git a/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java b/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java index 3beed1f6..cd9a83a0 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java +++ b/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java @@ -17,7 +17,10 @@ package brut.androlib.res.util; import java.io.*; + import org.xmlpull.mxp1_serializer.MXSerializer; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; /** * @author Ryszard Wiśniewski @@ -26,9 +29,11 @@ public class ExtMXSerializer extends MXSerializer implements ExtXmlSerializer { @Override public void startDocument(String encoding, Boolean standalone) throws IOException, IllegalArgumentException, IllegalStateException { - super.startDocument(encoding != null ? encoding : mDefaultEncoding, - standalone); - this.newLine(); + if (!enableLineOpt || mNewLine >= 1) { + super.startDocument(encoding != null ? encoding : mDefaultEncoding, + standalone); + this.newLine(); + } } @Override @@ -66,13 +71,129 @@ public class ExtMXSerializer extends MXSerializer implements ExtXmlSerializer { public ExtXmlSerializer newLine() throws IOException { super.out.write(lineSeparator); + mCurLine ++; + dbg("Nline: " + mCurLine); return this; } + //XmlPullParser.START_TAG(" 1) { + mNewLine = 1; + } else { + mNewLine = 0; + } + } else { + mNewLine = newLine; + if (!startTagIncomplete) {//XmlPullParser.START_TAG("<") + dbg(", old event:"+XmlPullParser.TYPES[mLastEvent]); + if(mLastEvent != XmlPullParser.END_TAG) { + moveToLine (newLine); + } else { + moveToLine (newLine - 1); + } + } + } + mLastEvent = event; + } + return this; + } + + //XmlPullParser.END_TAG(" />") and XmlPullParser.START_TAG("><") + @Override + protected void writeNamespaceDeclarations() throws IOException { + super.writeNamespaceDeclarations(); + if (enableLineOpt) { + if (mLastEvent == XmlPullParser.END_TAG) { + moveToLine (mNewLine); + } else { + moveToLine (mNewLine - 1); + } + } + } + + private ExtXmlSerializer moveToLine(int newLine) throws IOException { + int addLines = newLine - mCurLine; + dbg(", addLines: " + addLines); + for (; addLines > 0; addLines --) { + newLine(); + } + return this; + } + + @Override + protected void reset() { + super.reset(); + mCurLine = 1; + mLastEvent = XmlPullParser.START_DOCUMENT; + enableLineOpt = false; + } + public void setDisabledAttrEscape(boolean disabled) { mIsDisabledAttrEscape = disabled; } + @Override + public XmlSerializer text(String text) throws IOException { + if (enableLineOpt) { + mCurLine += (getTextLineNum(text) - 1); + } + return super.text(text); + } + + private int getTextLineNum(String text) { + String str = "." + text + "."; + int linenum = str.split("\\n").length + str.split("\\r").length + - str.split("\\n\\r").length;//(Unix(LF)-1) + (Mac(CR)-1) - (Win(CRLF)-1) + 1 + return linenum; + } + + @Override + public XmlSerializer text(char[] buf, int start, int len) + throws IOException { + if (enableLineOpt) { + mCurLine += (getTextLineNum(new String(buf, start, len)) - 1); + } + return super.text(buf, start, len); + } + + @Override + public void ignorableWhitespace(String text) throws IOException { + if (enableLineOpt) { + mCurLine += (getTextLineNum(text) - 1); + } + super.ignorableWhitespace(text); + } + private String mDefaultEncoding; private boolean mIsDisabledAttrEscape = false; + private int mCurLine; + private int mNewLine; + private int mLastEvent; + private boolean enableLineOpt = false; + private final static boolean DBG = false; + + public ExtXmlSerializer dbg(String str) throws IOException { + if(DBG) + super.out.write(""); + return this; + } } diff --git a/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java b/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java index 3c8ffed6..8fc75f8b 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java +++ b/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java @@ -26,6 +26,8 @@ public interface ExtXmlSerializer extends XmlSerializer { public ExtXmlSerializer newLine() throws IOException; public void setDisabledAttrEscape(boolean disabled); + public ExtXmlSerializer setLineNumber(int newLine, int event) throws IOException; + public ExtXmlSerializer dbg(String str) throws IOException; public static final String PROPERTY_SERIALIZER_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#serializer-indentation"; diff --git a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java index c243883f..92eb3f50 100644 --- a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java @@ -44,7 +44,7 @@ public class SmaliDecoder { baksmali.disassembleDexFile(mApkFile.getAbsolutePath(), new DexFile(mApkFile), false, mOutDir.getAbsolutePath(), null, null, null, false, true, true, true, false, false, - mDebug ? main.FULLMERGE : 0, false, false, null); + mDebug ? main.ALLPRE : 0, false, false, null); } catch (IOException ex) { throw new AndrolibException(ex); } diff --git a/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java b/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java new file mode 100644 index 00000000..47af7c2b --- /dev/null +++ b/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java @@ -0,0 +1,1160 @@ +/** + * Copyright 2011 Ryszard Wiśniewski + * + * 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 org.xmlpull.mxp1_serializer; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.xmlpull.v1.XmlSerializer; + +/** + * Implementation of XmlSerializer interface from XmlPull V1 API. + * This implementation is optimzied for performance and low memory footprint. + * + *

Implemented features:

    + *
  • FEATURE_NAMES_INTERNED - when enabled all returned names + * (namespaces, prefixes) will be interned and it is required that + * all names passed as arguments MUST be interned + *
  • FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE + *
+ *

Implemented properties:

    + *
  • PROPERTY_SERIALIZER_INDENTATION + *
  • PROPERTY_SERIALIZER_LINE_SEPARATOR + *
+ * + */ +public class MXSerializer implements XmlSerializer { + protected final static String XML_URI = "http://www.w3.org/XML/1998/namespace"; + protected final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; + private static final boolean TRACE_SIZING = false; + private static final boolean TRACE_ESCAPING = false; + + protected final String FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE = + "http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe"; + protected final String FEATURE_NAMES_INTERNED = + "http://xmlpull.org/v1/doc/features.html#names-interned"; + protected final String PROPERTY_SERIALIZER_INDENTATION = + "http://xmlpull.org/v1/doc/properties.html#serializer-indentation"; + protected final String PROPERTY_SERIALIZER_LINE_SEPARATOR = + "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator"; + protected final static String PROPERTY_LOCATION = + "http://xmlpull.org/v1/doc/properties.html#location"; + + // properties/features + protected boolean namesInterned; + protected boolean attributeUseApostrophe; + protected String indentationString = null; //" "; + protected String lineSeparator = "\n"; + + protected String location; + protected Writer out; + + protected int autoDeclaredPrefixes; + + protected int depth = 0; + + // element stack + protected String elNamespace[] = new String[ 2 ]; + protected String elName[] = new String[ elNamespace.length ]; + protected String elPrefix[] = new String[ elNamespace.length ]; + protected int elNamespaceCount[] = new int[ elNamespace.length ]; + + //namespace stack + protected int namespaceEnd = 0; + protected String namespacePrefix[] = new String[ 8 ]; + protected String namespaceUri[] = new String[ namespacePrefix.length ]; + + protected boolean finished; + protected boolean pastRoot; + protected boolean setPrefixCalled; + protected boolean startTagIncomplete; + + protected boolean doIndent; + protected boolean seenTag; + + protected boolean seenBracket; + protected boolean seenBracketBracket; + + // buffer output if neede to write escaped String see text(String) + private static final int BUF_LEN = Runtime.getRuntime().freeMemory() > 1000000L ? 8*1024 : 256; + protected char buf[] = new char[ BUF_LEN ]; + + + protected static final String precomputedPrefixes[]; + + static { + precomputedPrefixes = new String[32]; //arbitrary number ... + for (int i = 0; i < precomputedPrefixes.length; i++) + { + precomputedPrefixes[i] = ("n"+i).intern(); + } + } + + private boolean checkNamesInterned = false; + + private void checkInterning(String name) { + if(namesInterned && name != name.intern()) { + throw new IllegalArgumentException( + "all names passed as arguments must be interned" + +"when NAMES INTERNED feature is enabled"); + } + } + + protected void reset() { + location = null; + out = null; + autoDeclaredPrefixes = 0; + depth = 0; + + // nullify references on all levels to allow it to be GCed + for (int i = 0; i < elNamespaceCount.length; i++) + { + elName[ i ] = null; + elPrefix[ i ] = null; + elNamespace[ i ] = null; + elNamespaceCount[ i ] = 2; + } + + + namespaceEnd = 0; + + + //NOTE: no need to intern() as all literal strings and string-valued constant expressions + //are interned. String literals are defined in 3.10.5 of the Java Language Specification + // just checking ... + //assert "xmlns" == "xmlns".intern(); + //assert XMLNS_URI == XMLNS_URI.intern(); + + //TODO: how to prevent from reporting this namespace? + // this is special namespace declared for consistensy with XML infoset + namespacePrefix[ namespaceEnd ] = "xmlns"; + namespaceUri[ namespaceEnd ] = XMLNS_URI; + ++namespaceEnd; + + namespacePrefix[ namespaceEnd ] = "xml"; + namespaceUri[ namespaceEnd ] = XML_URI; + ++namespaceEnd; + + finished = false; + pastRoot = false; + setPrefixCalled = false; + startTagIncomplete = false; + //doIntent is not changed + seenTag = false; + + seenBracket = false; + seenBracketBracket = false; + } + + + protected void ensureElementsCapacity() { + final int elStackSize = elName.length; + //assert (depth + 1) >= elName.length; + // we add at least one extra slot ... + final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; // = lucky 7 + 1 //25 + if(TRACE_SIZING) { + System.err.println( + getClass().getName()+" elStackSize "+elStackSize+" ==> "+newSize); + } + final boolean needsCopying = elStackSize > 0; + String[] arr = null; + // reuse arr local variable slot + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elName, 0, arr, 0, elStackSize); + elName = arr; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elPrefix, 0, arr, 0, elStackSize); + elPrefix = arr; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elNamespace, 0, arr, 0, elStackSize); + elNamespace = arr; + + final int[] iarr = new int[newSize]; + if(needsCopying) { + System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); + } else { + // special initialization + iarr[0] = 0; + } + elNamespaceCount = iarr; + } + + protected void ensureNamespacesCapacity() { //int size) { + //int namespaceSize = namespacePrefix != null ? namespacePrefix.length : 0; + //assert (namespaceEnd >= namespacePrefix.length); + + //if(size >= namespaceSize) { + //int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25 + final int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8; + if(TRACE_SIZING) { + System.err.println( + getClass().getName()+" namespaceSize "+namespacePrefix.length+" ==> "+newSize); + } + final String[] newNamespacePrefix = new String[newSize]; + final String[] newNamespaceUri = new String[newSize]; + if(namespacePrefix != null) { + System.arraycopy( + namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd); + System.arraycopy( + namespaceUri, 0, newNamespaceUri, 0, namespaceEnd); + } + namespacePrefix = newNamespacePrefix; + namespaceUri = newNamespaceUri; + + // TODO use hashes for quick namespace->prefix lookups + // if( ! allStringsInterned ) { + // int[] newNamespacePrefixHash = new int[newSize]; + // if(namespacePrefixHash != null) { + // System.arraycopy( + // namespacePrefixHash, 0, newNamespacePrefixHash, 0, namespaceEnd); + // } + // namespacePrefixHash = newNamespacePrefixHash; + // } + //prefixesSize = newSize; + // ////assert nsPrefixes.length > size && nsPrefixes.length == newSize + //} + } + + + public void setFeature(String name, + boolean state) throws IllegalArgumentException, IllegalStateException + { + if(name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + if(FEATURE_NAMES_INTERNED.equals(name)) { + namesInterned = state; + } else if(FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) { + attributeUseApostrophe = state; + } else { + throw new IllegalStateException("unsupported feature "+name); + } + } + + public boolean getFeature(String name) throws IllegalArgumentException + { + if(name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + if(FEATURE_NAMES_INTERNED.equals(name)) { + return namesInterned; + } else if(FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) { + return attributeUseApostrophe; + } else { + return false; + } + } + + // precomputed variables to simplify writing indentation + protected int offsetNewLine; + protected int indentationJump; + protected char[] indentationBuf; + protected int maxIndentLevel; + protected boolean writeLineSepartor; //should end-of-line be written + protected boolean writeIndentation; // is indentation used? + + /** + * For maximum efficiency when writing indents the required output is pre-computed + * This is internal function that recomputes buffer after user requested chnages. + */ + protected void rebuildIndentationBuf() { + if(doIndent == false) return; + final int maxIndent = 65; //hardcoded maximum indentation size in characters + int bufSize = 0; + offsetNewLine = 0; + if(writeLineSepartor) { + offsetNewLine = lineSeparator.length(); + bufSize += offsetNewLine; + } + maxIndentLevel = 0; + if(writeIndentation) { + indentationJump = indentationString.length(); + maxIndentLevel = maxIndent / indentationJump; + bufSize += maxIndentLevel * indentationJump; + } + if(indentationBuf == null || indentationBuf.length < bufSize) { + indentationBuf = new char[bufSize + 8]; + } + int bufPos = 0; + if(writeLineSepartor) { + for (int i = 0; i < lineSeparator.length(); i++) + { + indentationBuf[ bufPos++ ] = lineSeparator.charAt(i); + } + } + if(writeIndentation) { + for (int i = 0; i < maxIndentLevel; i++) + { + for (int j = 0; j < indentationString.length(); j++) + { + indentationBuf[ bufPos++ ] = indentationString.charAt(j); + } + } + } + } + + // if(doIndent) writeIndent(); + protected void writeIndent() throws IOException { + final int start = writeLineSepartor ? 0 : offsetNewLine; + final int level = (depth > maxIndentLevel) ? maxIndentLevel : depth; + out.write( indentationBuf, start, ( (level - 1) * indentationJump) + offsetNewLine); + } + + public void setProperty(String name, + Object value) throws IllegalArgumentException, IllegalStateException + { + if(name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + if(PROPERTY_SERIALIZER_INDENTATION.equals(name)) { + indentationString = (String)value; + } else if(PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) { + lineSeparator = (String)value; + } else if(PROPERTY_LOCATION.equals(name)) { + location = (String) value; + } else { + throw new IllegalStateException("unsupported property "+name); + } + writeLineSepartor = lineSeparator != null && lineSeparator.length() > 0; + writeIndentation = indentationString != null && indentationString.length() > 0; + // optimize - do not write when nothing to write ... + doIndent = indentationString != null && (writeLineSepartor || writeIndentation); + //NOTE: when indentationString == null there is no indentation + // (even though writeLineSeparator may be true ...) + rebuildIndentationBuf(); + seenTag = false; // for consistency + } + + public Object getProperty(String name) throws IllegalArgumentException + { + if(name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + if(PROPERTY_SERIALIZER_INDENTATION.equals(name)) { + return indentationString; + } else if(PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) { + return lineSeparator; + } else if(PROPERTY_LOCATION.equals(name)) { + return location; + } else { + return null; + } + } + + private String getLocation() { + return location != null ? " @"+location : ""; + } + + // this is special method that can be accessed directly to retrieve Writer serializer is using + public Writer getWriter() + { + return out; + } + + public void setOutput(Writer writer) + { + reset(); + out = writer; + } + + public void setOutput(OutputStream os, String encoding) throws IOException + { + if(os == null) throw new IllegalArgumentException("output stream can not be null"); + reset(); + if(encoding != null) { + out = new OutputStreamWriter(os, encoding); + } else { + out = new OutputStreamWriter(os); + } + } + + public void startDocument (String encoding, Boolean standalone) throws IOException + { + char apos = attributeUseApostrophe ? '\'' : '"'; + if(attributeUseApostrophe) { + out.write(""); + } + + public void endDocument() throws IOException + { + // close all unclosed tag; + while(depth > 0) { + endTag(elNamespace[ depth ], elName[ depth ]); + } + //assert depth == 0; + //assert startTagIncomplete == false; + finished = pastRoot = startTagIncomplete = true; + out.flush(); + } + + public void setPrefix(String prefix, String namespace) throws IOException + { + if(startTagIncomplete) closeStartTag(); + //assert prefix != null; + //assert namespace != null; + if (prefix == null) { + prefix = ""; + } + if(!namesInterned) { + prefix = prefix.intern(); //will throw NPE if prefix==null + } else if(checkNamesInterned) { + checkInterning(prefix); + } else if(prefix == null) { + throw new IllegalArgumentException("prefix must be not null"+getLocation()); + } + + //check that prefix is not duplicated ... + for (int i = elNamespaceCount[ depth ]; i < namespaceEnd; i++) + { + if(prefix == namespacePrefix[ i ]) { + throw new IllegalStateException("duplicated prefix "+printable(prefix)+getLocation()); + } + } + + if(!namesInterned) { + namespace = namespace.intern(); + } else if(checkNamesInterned) { + checkInterning(namespace); + } else if(namespace == null) { + throw new IllegalArgumentException("namespace must be not null"+getLocation()); + } + + if(namespaceEnd >= namespacePrefix.length) { + ensureNamespacesCapacity(); + } + namespacePrefix[ namespaceEnd ] = prefix; + namespaceUri[ namespaceEnd ] = namespace; + ++namespaceEnd; + setPrefixCalled = true; + } + + protected String lookupOrDeclarePrefix( String namespace ) { + return getPrefix(namespace, true); + } + + public String getPrefix(String namespace, boolean generatePrefix) + { + return getPrefix(namespace, generatePrefix, false); + } + + protected String getPrefix(String namespace, boolean generatePrefix, boolean nonEmpty) + { + //assert namespace != null; + if(!namesInterned) { + // when String is interned we can do much faster namespace stack lookups ... + namespace = namespace.intern(); + } else if(checkNamesInterned) { + checkInterning(namespace); + //assert namespace != namespace.intern(); + } + if(namespace == null) { + throw new IllegalArgumentException("namespace must be not null"+getLocation()); + } else if(namespace.length() == 0) { + throw new IllegalArgumentException("default namespace cannot have prefix"+getLocation()); + } + + // first check if namespace is already in scope + for (int i = namespaceEnd - 1; i >= 0 ; --i) + { + if(namespace == namespaceUri[ i ]) { + final String prefix = namespacePrefix[ i ]; + if(nonEmpty && prefix.length() == 0) continue; + // now check that prefix is still in scope + for (int p = namespaceEnd - 1; p > i ; --p) + { + if(prefix == namespacePrefix[ p ]) + continue; // too bad - prefix is redeclared with different namespace + } + return prefix; + } + } + + // so not found it ... + if(!generatePrefix) { + return null; + } + return generatePrefix(namespace); + } + + private String generatePrefix(String namespace) { + //assert namespace == namespace.intern(); + while(true) { + ++autoDeclaredPrefixes; + //fast lookup uses table that was pre-initialized in static{} .... + final String prefix = autoDeclaredPrefixes < precomputedPrefixes.length + ? precomputedPrefixes[autoDeclaredPrefixes] : ("n"+autoDeclaredPrefixes).intern(); + // make sure this prefix is not declared in any scope (avoid hiding in-scope prefixes)! + for (int i = namespaceEnd - 1; i >= 0 ; --i) + { + if(prefix == namespacePrefix[ i ]) { + continue; // prefix is already declared - generate new and try again + } + } + // declare prefix + + if(namespaceEnd >= namespacePrefix.length) { + ensureNamespacesCapacity(); + } + namespacePrefix[ namespaceEnd ] = prefix; + namespaceUri[ namespaceEnd ] = namespace; + ++namespaceEnd; + + return prefix; + } + } + + public int getDepth() + { + return depth; + } + + public String getNamespace () + { + return elNamespace[depth]; + } + + public String getName() + { + return elName[depth]; + } + + public XmlSerializer startTag (String namespace, String name) throws IOException + { + if(startTagIncomplete) { + closeStartTag(); + } + seenBracket = seenBracketBracket = false; + ++depth; + if(doIndent && depth > 0 && seenTag) { + writeIndent(); + } + seenTag = true; + setPrefixCalled = false; + startTagIncomplete = true; + if( (depth + 1) >= elName.length) { + ensureElementsCapacity(); + } + ////assert namespace != null; + + if(checkNamesInterned && namesInterned) checkInterning(namespace); + elNamespace[ depth ] = (namesInterned || namespace == null) ? namespace : namespace.intern(); + //assert name != null; + //elName[ depth ] = name; + if(checkNamesInterned && namesInterned) checkInterning(name); + elName[ depth ] = (namesInterned || name == null) ? name : name.intern(); + if(out == null) { + throw new IllegalStateException("setOutput() must called set before serialization can start"); + } + out.write('<'); + if(namespace != null) { + if(namespace.length() > 0) { + //ALEK: in future make this algo a feature on serializer + String prefix = null; + if(depth > 0 && (namespaceEnd - elNamespaceCount[depth-1]) == 1) { + // if only one prefix was declared un-declare it if the prefix is already declared on parent el with the same URI + String uri = namespaceUri[namespaceEnd-1]; + if(uri == namespace || uri.equals(namespace)) { + String elPfx = namespacePrefix[namespaceEnd-1]; + // 2 == to skip predefined namesapces (xml and xmlns ...) + for(int pos = elNamespaceCount[depth-1] - 1; pos >= 2; --pos ) { + String pf = namespacePrefix[pos]; + if(pf == elPfx || pf.equals(elPfx)) { + String n = namespaceUri[pos]; + if(n == uri || n.equals(uri)) { + --namespaceEnd; //un-declare namespace: this is kludge! + prefix = elPfx; + } + break; + } + } + } + } + if(prefix == null) { + prefix = lookupOrDeclarePrefix( namespace ); + } + //assert prefix != null; + // make sure that default ("") namespace to not print ":" + if(prefix.length() > 0) { + elPrefix[ depth ] = prefix; + out.write(prefix); + out.write(':'); + } else { + elPrefix[ depth ] = ""; + } + } else { + // make sure that default namespace can be declared + for (int i = namespaceEnd - 1; i >= 0 ; --i) { + if(namespacePrefix[ i ] == "") { + final String uri = namespaceUri[ i ]; + if(uri == null) { + // declare default namespace + setPrefix("", ""); + } else if(uri.length() > 0) { + throw new IllegalStateException( + "start tag can not be written in empty default namespace "+ + "as default namespace is currently bound to '"+uri+"'"+getLocation()); + } + break; + } + } + elPrefix[ depth ] = ""; + } + } else { + elPrefix[ depth ] = ""; + } + out.write(name); + return this; + } + + public XmlSerializer attribute (String namespace, String name, + String value) throws IOException + { + if(!startTagIncomplete) { + throw new IllegalArgumentException("startTag() must be called before attribute()"+getLocation()); + } + //assert setPrefixCalled == false; + out.write(' '); + ////assert namespace != null; + if(namespace != null && namespace.length() > 0) { + //namespace = namespace.intern(); + if(!namesInterned) { + namespace = namespace.intern(); + } else if(checkNamesInterned) { + checkInterning(namespace); + } + String prefix = getPrefix( namespace, false, true ); + //assert( prefix != null); + //if(prefix.length() == 0) { + if(prefix == null) { + // needs to declare prefix to hold default namespace + //NOTE: attributes such as a='b' are in NO namespace + prefix = generatePrefix(namespace); + } + out.write(prefix); + out.write(':'); + // if(prefix.length() > 0) { + // out.write(prefix); + // out.write(':'); + // } + } + //assert name != null; + out.write(name); + out.write('='); + //assert value != null; + out.write( attributeUseApostrophe ? '\'' : '"'); + writeAttributeValue(value, out); + out.write( attributeUseApostrophe ? '\'' : '"'); + return this; + } + + protected void closeStartTag() throws IOException { + if(finished) { + throw new IllegalArgumentException("trying to write past already finished output"+getLocation()); + } + if(seenBracket) { + seenBracket = seenBracketBracket = false; + } + if( startTagIncomplete || setPrefixCalled ) { + if(setPrefixCalled) { + throw new IllegalArgumentException( + "startTag() must be called immediately after setPrefix()"+getLocation()); + } + if(!startTagIncomplete) { + throw new IllegalArgumentException("trying to close start tag that is not opened"+getLocation()); + } + + // write all namespace delcarations! + writeNamespaceDeclarations(); + out.write('>'); + elNamespaceCount[ depth ] = namespaceEnd; + startTagIncomplete = false; + } + } + + protected void writeNamespaceDeclarations() throws IOException + { + //int start = elNamespaceCount[ depth - 1 ]; + for (int i = elNamespaceCount[ depth - 1 ]; i < namespaceEnd; i++) + { + if(doIndent && namespaceUri[ i ].length() > 40) { + writeIndent(); + out.write(" "); + } + if(namespacePrefix[ i ] != "") { + out.write(" xmlns:"); + out.write(namespacePrefix[ i ]); + out.write('='); + } else { + out.write(" xmlns="); + } + out.write( attributeUseApostrophe ? '\'' : '"'); + + //NOTE: escaping of namespace value the same way as attributes!!!! + writeAttributeValue(namespaceUri[ i ], out); + + out.write( attributeUseApostrophe ? '\'' : '"'); + } + } + + public XmlSerializer endTag(String namespace, String name) throws IOException + { + // check that level is valid + ////assert namespace != null; + //if(namespace != null) { + // namespace = namespace.intern(); + //} + seenBracket = seenBracketBracket = false; + if(namespace != null) { + if(!namesInterned) { + namespace = namespace.intern(); + } else if(checkNamesInterned) { + checkInterning(namespace); + } + } + + if(namespace != elNamespace[ depth ]) + { + throw new IllegalArgumentException( + "expected namespace "+printable(elNamespace[ depth ]) + +" and not "+printable(namespace)+getLocation()); + } + if(name == null) { + throw new IllegalArgumentException("end tag name can not be null"+getLocation()); + } + if(checkNamesInterned && namesInterned) { + checkInterning(name); + } + String startTagName = elName[ depth ]; + if((!namesInterned && !name.equals(startTagName)) + || (namesInterned && name != startTagName )) + { + throw new IllegalArgumentException( + "expected element name "+printable(elName[ depth ])+" and not "+printable(name)+getLocation()); + } + if(startTagIncomplete) { + writeNamespaceDeclarations(); + out.write(" />"); //space is added to make it easier to work in XHTML!!! + --depth; + } else { + //assert startTagIncomplete == false; + if(doIndent && seenTag) { writeIndent(); } + out.write(" 0) { + out.write(startTagPrefix); + out.write(':'); + } + + // if(namespace != null && namespace.length() > 0) { + // //TODO prefix should be alredy known from matching start tag ... + // final String prefix = lookupOrDeclarePrefix( namespace ); + // //assert( prefix != null); + // if(prefix.length() > 0) { + // out.write(prefix); + // out.write(':'); + // } + // } + out.write(name); + out.write('>'); + --depth; + } + namespaceEnd = elNamespaceCount[ depth ]; + startTagIncomplete = false; + seenTag = true; + return this; + } + + public XmlSerializer text (String text) throws IOException + { + //assert text != null; + if(startTagIncomplete || setPrefixCalled) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + writeElementContent(text, out); + return this; + } + + public XmlSerializer text (char [] buf, int start, int len) throws IOException + { + if(startTagIncomplete || setPrefixCalled) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + writeElementContent(buf, start, len, out); + return this; + } + + public void cdsect (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + out.write(""); + } + + public void entityRef (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + out.write('&'); + out.write(text); //escape? + out.write(';'); + } + + public void processingInstruction (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + out.write(""); + } + + public void comment (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + out.write(""); + } + + public void docdecl (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + out.write(""); + } + + public void ignorableWhitespace (String text) throws IOException + { + if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag(); + if(doIndent && seenTag) seenTag = false; + if(text.length() == 0) { + throw new IllegalArgumentException( + "empty string is not allowed for ignorable whitespace"+getLocation()); + } + out.write(text); //no escape? + } + + public void flush () throws IOException + { + if(!finished && startTagIncomplete) closeStartTag(); + out.flush(); + } + + // --- utility methods + + protected void writeAttributeValue(String value, Writer out) throws IOException + { + //.[apostrophe and <, & escaped], + final char quot = attributeUseApostrophe ? '\'' : '"'; + final String quotEntity = attributeUseApostrophe ? "'" : """; + + int pos = 0; + for (int i = 0; i < value.length(); i++) + { + char ch = value.charAt(i); + if(ch == '&') { + if(i > pos) out.write(value.substring(pos, i)); + out.write("&"); + pos = i + 1; + } if(ch == '<') { + if(i > pos) out.write(value.substring(pos, i)); + out.write("<"); + pos = i + 1; + }else if(ch == quot) { + if(i > pos) out.write(value.substring(pos, i)); + out.write(quotEntity); + pos = i + 1; + } else if(ch < 32) { + //in XML 1.0 only legal character are #x9 | #xA | #xD + // and they must be escaped otherwise in attribute value they are normalized to spaces + if(ch == 13 || ch == 10 || ch == 9) { + if(i > pos) out.write(value.substring(pos, i)); + out.write("&#"); + out.write(Integer.toString(ch)); + out.write(';'); + pos = i + 1; + } else { + if(TRACE_ESCAPING) System.err.println(getClass().getName()+" DEBUG ATTR value.len="+value.length()+" "+printable(value)); + + throw new IllegalStateException( + //"character "+Integer.toString(ch)+" is not allowed in output"+getLocation()); + "character "+printable(ch)+" ("+Integer.toString(ch)+") is not allowed in output"+getLocation() + +" (attr value="+printable(value)+")"); + // in XML 1.1 legal are [#x1-#xD7FF] + // if(ch > 0) { + // if(i > pos) out.write(text.substring(pos, i)); + // out.write("&#"); + // out.write(Integer.toString(ch)); + // out.write(';'); + // pos = i + 1; + // } else { + // throw new IllegalStateException( + // "character zero is not allowed in XML 1.1 output"+getLocation()); + // } + } + } + } + if(pos > 0) { + out.write(value.substring(pos)); + } else { + out.write(value); // this is shortcut to the most common case + } + + } + + protected void writeElementContent(String text, Writer out) throws IOException + { + // esccape '<', '&', ']]>', <32 if necessary + int pos = 0; + for (int i = 0; i < text.length(); i++) + { + //TODO: check if doing char[] text.getChars() would be faster than getCharAt(i) ... + char ch = text.charAt(i); + if(ch == ']') { + if(seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else { + if(ch == '&') { + if(i > pos) out.write(text.substring(pos, i)); + out.write("&"); + pos = i + 1; + } else if(ch == '<') { + if(i > pos) out.write(text.substring(pos, i)); + out.write("<"); + pos = i + 1; + } else if(seenBracketBracket && ch == '>') { + if(i > pos) out.write(text.substring(pos, i)); + out.write(">"); + pos = i + 1; + } else if(ch < 32) { + //in XML 1.0 only legal character are #x9 | #xA | #xD + if( ch == 9 || ch == 10 || ch == 13) { + // pass through + + // } else if(ch == 13) { //escape + // if(i > pos) out.write(text.substring(pos, i)); + // out.write("&#"); + // out.write(Integer.toString(ch)); + // out.write(';'); + // pos = i + 1; + } else { + if(TRACE_ESCAPING) System.err.println(getClass().getName()+" DEBUG TEXT value.len="+text.length()+" "+printable(text)); + throw new IllegalStateException( + "character "+Integer.toString(ch)+" is not allowed in output"+getLocation() + +" (text value="+printable(text)+")"); + // in XML 1.1 legal are [#x1-#xD7FF] + // if(ch > 0) { + // if(i > pos) out.write(text.substring(pos, i)); + // out.write("&#"); + // out.write(Integer.toString(ch)); + // out.write(';'); + // pos = i + 1; + // } else { + // throw new IllegalStateException( + // "character zero is not allowed in XML 1.1 output"+getLocation()); + // } + } + } + if(seenBracket) { + seenBracketBracket = seenBracket = false; + } + + } + } + if(pos > 0) { + out.write(text.substring(pos)); + } else { + out.write(text); // this is shortcut to the most common case + } + + + + } + + protected void writeElementContent(char[] buf, int off, int len, Writer out) throws IOException + { + // esccape '<', '&', ']]>' + final int end = off + len; + int pos = off; + for (int i = off; i < end; i++) + { + final char ch = buf[i]; + if(ch == ']') { + if(seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else { + if(ch == '&') { + if(i > pos) { + out.write(buf, pos, i - pos); + } + out.write("&"); + pos = i + 1; + } else if(ch == '<') { + if(i > pos) { + out.write(buf, pos, i - pos); + } + out.write("<"); + pos = i + 1; + + } else if(seenBracketBracket && ch == '>') { + if(i > pos) { + out.write(buf, pos, i - pos); + } + out.write(">"); + pos = i + 1; + } else if(ch < 32) { + //in XML 1.0 only legal character are #x9 | #xA | #xD + if( ch == 9 || ch == 10 || ch == 13) { + // pass through + + + // } else if(ch == 13 ) { //if(ch == '\r') { + // if(i > pos) { + // out.write(buf, pos, i - pos); + // } + // out.write("&#"); + // out.write(Integer.toString(ch)); + // out.write(';'); + // pos = i + 1; + } else { + if(TRACE_ESCAPING) System.err.println( + getClass().getName()+" DEBUG TEXT value.len=" + +len+" "+printable(new String(buf,off,len))); + throw new IllegalStateException( + "character "+printable(ch)+" ("+Integer.toString(ch)+") is not allowed in output"+getLocation()); + // in XML 1.1 legal are [#x1-#xD7FF] + // if(ch > 0) { + // if(i > pos) out.write(text.substring(pos, i)); + // out.write("&#"); + // out.write(Integer.toString(ch)); + // out.write(';'); + // pos = i + 1; + // } else { + // throw new IllegalStateException( + // "character zero is not allowed in XML 1.1 output"+getLocation()); + // } + } + } + if(seenBracket) { + seenBracketBracket = seenBracket = false; + } + // assert seenBracketBracket == seenBracket == false; + } + } + if(end > pos) { + out.write(buf, pos, end - pos); + } + } + + /** simple utility method -- good for debugging */ + protected static final String printable(String s) { + if(s == null) return "null"; + StringBuffer retval = new StringBuffer(s.length() + 16); + retval.append("'"); + char ch; + for (int i = 0; i < s.length(); i++) { + addPrintable(retval, s.charAt(i)); + } + retval.append("'"); + return retval.toString(); + } + + protected static final String printable(char ch) { + StringBuffer retval = new StringBuffer(); + addPrintable(retval, ch); + return retval.toString(); + } + + private static void addPrintable(StringBuffer retval, char ch) + { + switch (ch) + { + case '\b': + retval.append("\\b"); + break; + case '\t': + retval.append("\\t"); + break; + case '\n': + retval.append("\\n"); + break; + case '\f': + retval.append("\\f"); + break; + case '\r': + retval.append("\\r"); + break; + case '\"': + retval.append("\\\""); + break; + case '\'': + retval.append("\\\'"); + break; + case '\\': + retval.append("\\\\"); + break; + default: + if (ch < 0x20 || ch > 0x7e) { + final String ss = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + ss.substring(ss.length() - 4, ss.length())); + } else { + retval.append(ch); + } + } + } + +} +