From 092383e3e0c458ad55cbc6642d6ef184fdff61b7 Mon Sep 17 00:00:00 2001 From: REAndroid Date: Tue, 18 Apr 2023 15:16:59 +0200 Subject: [PATCH] adapt AOSP xml serializer --- .../android/org/kxml2/io/KXmlSerializer.java | 561 ++++++++++++++++++ .../java/com/reandroid/apk/ApkModule.java | 3 + .../reandroid/apk/ApkModuleXmlDecoder.java | 119 +++- .../xmldecoder/ResXmlDocumentSerializer.java | 125 ++++ .../arsc/chunk/xml/ResXmlPullParser.java | 41 +- .../reandroid/xml/XmlParserToSerializer.java | 101 ++++ 6 files changed, 909 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/android/org/kxml2/io/KXmlSerializer.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java create mode 100644 src/main/java/com/reandroid/xml/XmlParserToSerializer.java diff --git a/src/main/java/com/android/org/kxml2/io/KXmlSerializer.java b/src/main/java/com/android/org/kxml2/io/KXmlSerializer.java new file mode 100644 index 0000000..7ecd1b1 --- /dev/null +++ b/src/main/java/com/android/org/kxml2/io/KXmlSerializer.java @@ -0,0 +1,561 @@ +/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. */ + +package com.android.org.kxml2.io; + +import java.io.*; +import java.util.Arrays; +import java.util.Locale; +import org.xmlpull.v1.*; + +public class KXmlSerializer implements XmlSerializer { + + private static final int BUFFER_LEN = 8192; + private final char[] mText = new char[BUFFER_LEN]; + private int mPos; + private Writer writer; + private boolean pending; + private int auto; + private int depth; + private String[] elementStack = new String[12]; + private int[] nspCounts = new int[4]; + private String[] nspStack = new String[8]; + private boolean[] indent = new boolean[4]; + private boolean firstAttributeWritten; + private int indentAttributeReference; + private boolean unicode; + private String encoding; + + private void append(char c) throws IOException { + if(mPos >= BUFFER_LEN){ + flushBuffer(); + } + mText[mPos++] = c; + } + + private void append(String str, int i, int length) throws IOException { + while (length > 0){ + if(mPos == BUFFER_LEN){ + flushBuffer(); + } + int batch = BUFFER_LEN - mPos; + if(batch > length){ + batch = length; + } + str.getChars(i, i + batch, mText, mPos); + i += batch; + length -= batch; + mPos += batch; + } + } + + private void appendSpace(int length) throws IOException { + while (length > 0){ + if(mPos == BUFFER_LEN){ + flushBuffer(); + } + int batch = BUFFER_LEN - mPos; + if(batch > length){ + batch = length; + } + Arrays.fill(mText, mPos, mPos + batch, ' '); + length -= batch; + mPos += batch; + } + } + + private void append(String str) throws IOException { + append(str, 0, str.length()); + } + + private void flushBuffer() throws IOException { + if(mPos > 0){ + writer.write(mText, 0, mPos); + writer.flush(); + mPos = 0; + } + } + + private void check(boolean close) throws IOException { + if(!pending) + return; + + depth++; + pending = false; + + if(indent.length <= depth){ + boolean[] hlp = new boolean[depth + 4]; + System.arraycopy(indent, 0, hlp, 0, depth); + indent = hlp; + } + indent[depth] = indent[depth - 1]; + + for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++){ + append(" xmlns"); + if(!nspStack[i * 2].isEmpty()){ + append(':'); + append(nspStack[i * 2]); + } + else if(getNamespace().isEmpty() && !nspStack[i * 2 + 1].isEmpty()) + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + append("=\""); + writeEscaped(nspStack[i * 2 + 1], '"'); + append('"'); + } + + if(nspCounts.length <= depth + 1){ + int[] hlp = new int[depth + 8]; + System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); + nspCounts = hlp; + } + + nspCounts[depth + 1] = nspCounts[depth]; + if(close){ + append(" />"); + } else { + append('>'); + } + } + + private void writeEscaped(String s, int quot) throws IOException { + for (int i = 0; i < s.length(); i++){ + char c = s.charAt(i); + switch (c){ + case '\n': + case '\r': + case '\t': + if(quot == -1) + append(c); + else + append("&#"+((int) c)+';'); + break; + case '&' : + append("&"); + break; + case '>' : + append(">"); + break; + case '<' : + append("<"); + break; + default: + if(c == quot){ + append(c == '"' ? """ : "'"); + break; + } + boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); + if(allowedInXml){ + if(unicode || c < 127){ + append(c); + } else { + append("&#" + ((int) c) + ";"); + } + } else if(Character.isHighSurrogate(c) && i < s.length() - 1){ + writeSurrogate(c, s.charAt(i + 1)); + ++i; + } else { + reportInvalidCharacter(c); + } + } + } + } + private static void reportInvalidCharacter(char ch){ + throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")"); + } + @Override + public void docdecl(String dd) throws IOException { + append("'); + } + @Override + public void endDocument() throws IOException { + while (depth > 0){ + endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); + } + flush(); + } + @Override + public void entityRef(String name) throws IOException { + check(false); + append('&'); + append(name); + append(';'); + } + @Override + public boolean getFeature(String name){ + return "http://xmlpull.org/v1/doc/features.html#indent-output" + .equals(name) && indent[depth]; + } + @Override + public String getPrefix(String namespace, boolean create){ + try { + return getPrefix(namespace, false, create); + } + catch (IOException e){ + throw new RuntimeException(e.toString()); + } + } + private String getPrefix(String namespace, boolean includeDefault, boolean create) + throws IOException { + int[] nspCounts = this.nspCounts; + int depth = this.depth; + String[] nspStack = this.nspStack; + + for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0;i -= 2){ + if(nspStack[i + 1].equals(namespace) + && (includeDefault + || !nspStack[i].isEmpty())){ + String cand = nspStack[i]; + for (int j = i + 2; j < nspCounts[depth + 1] * 2; j++){ + if(nspStack[j].equals(cand)){ + cand = null; + break; + } + } + if(cand != null){ + return cand; + } + } + } + if(!create){ + return null; + } + + String prefix; + + if(namespace.isEmpty()) { + prefix = ""; + }else { + do { + prefix = "n" + (auto++); + for (int i = nspCounts[depth + 1] * 2 - 2;i >= 0;i -= 2){ + if(prefix.equals(nspStack[i])){ + prefix = null; + break; + } + } + } + while (prefix == null); + } + + boolean p = pending; + pending = false; + setPrefix(prefix, namespace); + pending = p; + return prefix; + } + + @Override + public Object getProperty(String name){ + throw new RuntimeException("Unsupported property: "+name); + } + @Override + public void ignorableWhitespace(String s) throws IOException { + text(s); + } + @Override + public void setFeature(String name, boolean value){ + if("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)){ + indent[depth] = value; + firstAttributeWritten = false; + }else { + throw new RuntimeException("Unsupported Feature: "+name); + } + } + @Override + public void setProperty(String name, Object value){ + throw new RuntimeException("Unsupported Property:" + value); + } + @Override + public void setPrefix(String prefix, String namespace) + throws IOException { + + check(false); + if(prefix == null) { + prefix = ""; + } + if(namespace == null) { + namespace = ""; + } + String defined = getPrefix(namespace, true, false); + if(prefix.equals(defined)) { + return; + } + + int pos = (nspCounts[depth + 1]++) << 1; + + if(nspStack.length < pos + 1){ + String[] hlp = new String[nspStack.length + 16]; + System.arraycopy(nspStack, 0, hlp, 0, pos); + nspStack = hlp; + } + + nspStack[pos++] = prefix; + nspStack[pos] = namespace; + } + + public void setOutput(Writer writer){ + this.writer = writer; + nspCounts[0] = 2; + nspCounts[1] = 2; + nspStack[0] = ""; + nspStack[1] = ""; + nspStack[2] = "xml"; + nspStack[3] = "http://www.w3.org/XML/1998/namespace"; + pending = false; + auto = 0; + depth = 0; + + unicode = false; + } + @Override + public void setOutput(OutputStream os, String encoding) + throws IOException { + if(os == null) { + throw new IllegalArgumentException("os == null"); + } + setOutput(encoding == null + ? new OutputStreamWriter(os) + : new OutputStreamWriter(os, encoding)); + this.encoding = encoding; + if(encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")){ + unicode = true; + } + } + @Override + public void startDocument(String encoding, Boolean standalone) throws IOException { + append(""); + } + @Override + public XmlSerializer startTag(String namespace, String name) + throws IOException { + check(false); + firstAttributeWritten = false; + indentAttributeReference = 0; + if(indent[depth]){ + append('\r'); + append('\n'); + int spaceLength = 2 * depth; + appendSpace(spaceLength); + indentAttributeReference = spaceLength; + } + int esp = depth * 3; + if(elementStack.length < esp + 3){ + String[] hlp = new String[elementStack.length + 12]; + System.arraycopy(elementStack, 0, hlp, 0, esp); + elementStack = hlp; + } + String prefix = namespace == null? + "" : getPrefix(namespace, true, true); + + if(namespace != null && namespace.isEmpty()){ + for (int i = nspCounts[depth]; i < nspCounts[depth + 1]; i++){ + if(nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()){ + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + } + } + } + elementStack[esp++] = namespace; + elementStack[esp++] = prefix; + elementStack[esp] = name; + append('<'); + indentAttributeReference += 1; + if(!prefix.isEmpty()){ + append(prefix); + append(':'); + indentAttributeReference += prefix.length() + 1; + } + append(name); + indentAttributeReference += name.length(); + pending = true; + return this; + } + @Override + public XmlSerializer attribute(String namespace, String name, String value) + throws IOException { + if(!pending) { + throw new IllegalStateException("illegal position for attribute"); + } + if(namespace == null) { + namespace = ""; + } + String prefix = namespace.isEmpty() ? + "" : getPrefix(namespace, false, true); + attributeIndent(); + append(' '); + if(!prefix.isEmpty()){ + append(prefix); + append(':'); + } + append(name); + append('='); + char q = value.indexOf('"') == -1 ? '"' : '\''; + append(q); + writeEscaped(value, q); + append(q); + firstAttributeWritten = true; + return this; + } + @Override + public void flush() throws IOException { + check(false); + flushBuffer(); + } + @Override + public XmlSerializer endTag(String namespace, String name)throws IOException { + if(!pending) { + depth--; + } + if((namespace == null + && elementStack[depth * 3] != null) + || (namespace != null + && !namespace.equals(elementStack[depth * 3])) + || !elementStack[depth * 3 + 2].equals(name)) { + throw new IllegalArgumentException(" does not match start"); + } + + if(pending){ + check(true); + depth--; + } + else { + if(indent[depth + 1]){ + append('\r'); + append('\n'); + appendSpace(2 * depth); + } + append("'); + } + + nspCounts[depth + 1] = nspCounts[depth]; + return this; + } + @Override + public String getNamespace(){ + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; + } + @Override + public String getName(){ + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; + } + @Override + public int getDepth(){ + return pending ? depth + 1 : depth; + } + @Override + public XmlSerializer text(String text) throws IOException { + check(false); + indent[depth] = false; + writeEscaped(text, -1); + return this; + } + @Override + public XmlSerializer text(char[] text, int start, int len) + throws IOException { + text(new String(text, start, len)); + return this; + } + @Override + public void cdsect(String data) throws IOException { + check(false); + data = data.replace("]]>", "]]]]>"); + append("= 0x20 && ch <= 0xd7ff) || + (ch == '\t' || ch == '\n' || ch == '\r') || + (ch >= 0xe000 && ch <= 0xfffd); + if(allowedInCdata){ + append(ch); + } else if(Character.isHighSurrogate(ch) && i < data.length() - 1){ + // Character entities aren't valid in CDATA, so break out for this. + append("]]>"); + writeSurrogate(ch, data.charAt(++i)); + append(""); + } + + private void writeSurrogate(char high, char low) throws IOException { + if(!Character.isLowSurrogate(low)){ + throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) + + " U+" + Integer.toHexString((int) low) + ")"); + } + int codePoint = Character.toCodePoint(high, low); + append("&#" + codePoint + ";"); + } + @Override + public void comment(String comment) throws IOException { + check(false); + append(""); + } + @Override + public void processingInstruction(String pi) + throws IOException { + check(false); + append(""); + } + + private void attributeIndent() throws IOException { + if(!firstAttributeWritten || !indent[depth]){ + return; + } + int length = this.indentAttributeReference; + if(length <= 0){ + return; + } + append('\r'); + append('\n'); + appendSpace(length); + } +} diff --git a/src/main/java/com/reandroid/apk/ApkModule.java b/src/main/java/com/reandroid/apk/ApkModule.java index c409e19..bb8b64f 100644 --- a/src/main/java/com/reandroid/apk/ApkModule.java +++ b/src/main/java/com/reandroid/apk/ApkModule.java @@ -528,6 +528,9 @@ public class ApkModule implements ApkFile { if(inputSource==null){ throw new FileNotFoundException("No such file in apk: " + path); } + return loadResXmlDocument(inputSource); + } + public ResXmlDocument loadResXmlDocument(InputSource inputSource) throws IOException{ ResXmlDocument resXmlDocument = new ResXmlDocument(); resXmlDocument.setApkFile(this); resXmlDocument.readBytes(inputSource.openStream()); diff --git a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java index b2acb62..51af36a 100644 --- a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java +++ b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java @@ -15,6 +15,7 @@ */ package com.reandroid.apk; +import com.reandroid.apk.xmldecoder.ResXmlDocumentSerializer; import com.reandroid.archive.InputSource; import com.reandroid.apk.xmldecoder.XMLBagDecoder; import com.reandroid.apk.xmldecoder.XMLNamespaceValidator; @@ -32,10 +33,12 @@ import com.reandroid.xml.XMLAttribute; import com.reandroid.xml.XMLDocument; import com.reandroid.xml.XMLElement; import com.reandroid.xml.XMLException; +import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.*; public class ApkModuleXmlDecoder { @@ -43,13 +46,22 @@ import java.util.*; private final Map> decodedEntries; private XMLBagDecoder xmlBagDecoder; private final Set mDecodedPaths; + private ResXmlDocumentSerializer documentSerializer; + private boolean useAndroidSerializer; public ApkModuleXmlDecoder(ApkModule apkModule){ this.apkModule=apkModule; this.decodedEntries = new HashMap<>(); this.mDecodedPaths = new HashSet<>(); + this.useAndroidSerializer = true; + } + public void setUseAndroidSerializer(boolean useAndroidSerializer) { + this.useAndroidSerializer = useAndroidSerializer; } public void sanitizeFilePaths(){ - PathSanitizer sanitizer = new PathSanitizer(apkModule); + sanitizeFilePaths(false); + } + public void sanitizeFilePaths(boolean sanitizeResourceFiles){ + PathSanitizer sanitizer = new PathSanitizer(apkModule, sanitizeResourceFiles); sanitizer.sanitize(); } public void decodeTo(File outDir) @@ -66,14 +78,14 @@ import java.util.*; decodePublicXml(tableBlock, outDir); - decodeAndroidManifest(tableBlock, outDir); + decodeAndroidManifest(outDir); addDecodedPath(TableBlock.FILE_NAME); logMessage("Decoding resource files ..."); List resFileList=apkModule.listResFiles(); for(ResFile resFile:resFileList){ - decodeResFile(tableBlock, outDir, resFile); + decodeResFile(outDir, resFile); } decodeValues(tableBlock, outDir, tableBlock); @@ -104,10 +116,10 @@ import java.util.*; UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles(); uncompressedFiles.toJson().write(file); } - private void decodeResFile(EntryStore entryStore, File outDir, ResFile resFile) - throws IOException, XMLException { + private void decodeResFile(File outDir, ResFile resFile) + throws IOException{ if(resFile.isBinaryXml()){ - decodeResXml(entryStore, outDir, resFile); + decodeResXml(outDir, resFile); }else { decodeResRaw(outDir, resFile); } @@ -133,28 +145,32 @@ import java.util.*; addDecodedEntry(entry); } - private void decodeResXml(EntryStore entryStore, File outDir, ResFile resFile) - throws IOException, XMLException{ - Entry entry =resFile.pickOne(); - PackageBlock packageBlock= entry.getPackageBlock(); - ResXmlDocument resXmlDocument = apkModule.loadResXmlDocument( - resFile.getInputSource().getName()); + private void decodeResXml(File outDir, ResFile resFile) + throws IOException{ + Entry entry = resFile.pickOne(); + PackageBlock packageBlock = entry.getPackageBlock(); - File pkgDir=new File(outDir, getPackageDirName(packageBlock)); + File pkgDir = new File(outDir, getPackageDirName(packageBlock)); String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); String path = alias.replace('/', File.separatorChar); - path=path.replace('/', File.separatorChar); - File file=new File(pkgDir, path); + path = path.replace('/', File.separatorChar); + File file = new File(pkgDir, path); - logVerbose("Decoding: "+path); - XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(resXmlDocument); - namespaceValidator.validate(); - XMLDocument xmlDocument= resXmlDocument.decodeToXml(entryStore, packageBlock.getId()); - xmlDocument.save(file, true); + logVerbose("Decoding: " + path); + serializeXml(packageBlock.getId(), resFile.getInputSource(), file); resFile.setFilePath(alias); - - addDecodedEntry(resFile.pickOne()); + addDecodedEntry(entry); + } + private ResXmlDocumentSerializer getDocumentSerializer(){ + if(documentSerializer == null){ + documentSerializer = new ResXmlDocumentSerializer(apkModule); + documentSerializer.setValidateXmlNamespace(true); + } + return documentSerializer; + } + private TableBlock getTableBlock(){ + return apkModule.getTableBlock(); } private void decodePublicXml(TableBlock tableBlock, File outDir) throws IOException{ @@ -190,8 +206,8 @@ import java.util.*; resourceIds.loadPackageBlock(packageBlock); resourceIds.writeXml(file); } - private void decodeAndroidManifest(EntryStore entryStore, File outDir) - throws IOException, XMLException { + private void decodeAndroidManifest(File outDir) + throws IOException { if(!apkModule.hasAndroidManifestBlock()){ logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME); return; @@ -199,13 +215,58 @@ import java.util.*; File file=new File(outDir, AndroidManifestBlock.FILE_NAME); logMessage("Decoding: "+file.getName()); AndroidManifestBlock manifestBlock=apkModule.getAndroidManifestBlock(); - XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(manifestBlock); - namespaceValidator.validate(); - int currentPackageId= manifestBlock.guessCurrentPackageId(); - XMLDocument xmlDocument=manifestBlock.decodeToXml(entryStore, currentPackageId); - xmlDocument.save(file, true); + int currentPackageId = manifestBlock.guessCurrentPackageId(); + serializeXml(currentPackageId, manifestBlock, file); addDecodedPath(AndroidManifestBlock.FILE_NAME); } + private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile) + throws IOException { + XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(document); + namespaceValidator.validate(); + if(useAndroidSerializer){ + ResXmlDocumentSerializer serializer = getDocumentSerializer(); + if(currentPackageId != 0){ + serializer.getDecoder().setCurrentPackageId(currentPackageId); + } + try { + serializer.write(document, outFile); + } catch (XmlPullParserException ex) { + throw new IOException("Error: "+outFile.getName(), ex); + } + }else { + try { + XMLDocument xmlDocument = document.decodeToXml(getTableBlock(), currentPackageId); + xmlDocument.save(outFile, true); + } catch (XMLException ex) { + throw new IOException("Error: "+outFile.getName(), ex); + } + } + } + private void serializeXml(int currentPackageId, InputSource inputSource, File outFile) + throws IOException { + + if(useAndroidSerializer){ + ResXmlDocumentSerializer serializer = getDocumentSerializer(); + if(currentPackageId != 0){ + serializer.getDecoder().setCurrentPackageId(currentPackageId); + } + try { + serializer.write(inputSource, outFile); + } catch (XmlPullParserException ex) { + throw new IOException("Error: "+outFile.getName(), ex); + } + }else { + try { + ResXmlDocument document = apkModule.loadResXmlDocument(inputSource); + XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(document); + namespaceValidator.validate(); + XMLDocument xmlDocument = document.decodeToXml(getTableBlock(), currentPackageId); + xmlDocument.save(outFile, true); + } catch (XMLException ex) { + throw new IOException("Error: "+outFile.getName(), ex); + } + } + } private void addDecodedEntry(Entry entry){ if(entry.isNull()){ return; diff --git a/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java b/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java new file mode 100644 index 0000000..5dc0ca8 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java @@ -0,0 +1,125 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * 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 com.reandroid.apk.xmldecoder; + +import com.android.org.kxml2.io.KXmlSerializer; +import com.reandroid.apk.ApkModule; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.chunk.xml.ResXmlPullParser; +import com.reandroid.arsc.decoder.Decoder; +import com.reandroid.xml.XmlParserToSerializer; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class ResXmlDocumentSerializer implements ResXmlPullParser.DocumentLoadedListener{ + private final Object mLock = new Object(); + private final ResXmlPullParser parser; + private final XmlSerializer serializer; + private final XmlParserToSerializer parserToSerializer; + private boolean validateXmlNamespace; + public ResXmlDocumentSerializer(ResXmlPullParser parser){ + this.parser = parser; + this.serializer = new KXmlSerializer(); + this.parserToSerializer = new XmlParserToSerializer(parser, serializer); + } + public ResXmlDocumentSerializer(Decoder decoder){ + this(new ResXmlPullParser(decoder)); + } + public ResXmlDocumentSerializer(ApkModule apkModule){ + this(createDecoder(apkModule)); + } + + public void write(InputSource inputSource, File file) + throws IOException, XmlPullParserException { + write(inputSource.openStream(), file); + } + public void write(InputSource inputSource, OutputStream outputStream) + throws IOException, XmlPullParserException { + write(inputSource.openStream(), outputStream); + inputSource.disposeInputSource(); + } + public void write(InputStream inputStream, OutputStream outputStream) + throws IOException, XmlPullParserException { + synchronized (mLock){ + this.parser.setInput(inputStream, null); + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + this.serializer.setOutput(writer); + this.parserToSerializer.write(); + writer.close(); + outputStream.close(); + } + } + public void write(InputStream inputStream, File file) + throws IOException, XmlPullParserException { + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + write(inputStream, outputStream); + } + public void write(ResXmlDocument xmlDocument, File file) + throws IOException, XmlPullParserException { + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + write(xmlDocument, outputStream); + } + public void write(ResXmlDocument xmlDocument, OutputStream outputStream) + throws IOException, XmlPullParserException { + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + write(xmlDocument, writer); + writer.close(); + outputStream.close(); + } + public void write(ResXmlDocument xmlDocument, Writer writer) + throws IOException, XmlPullParserException { + synchronized (mLock){ + this.parser.setResXmlDocument(xmlDocument); + this.serializer.setOutput(writer); + this.parserToSerializer.write(); + writer.flush(); + } + } + public Decoder getDecoder(){ + return parser.getDecoder(); + } + + public void setValidateXmlNamespace(boolean validateXmlNamespace) { + this.validateXmlNamespace = validateXmlNamespace; + } + @Override + public ResXmlDocument onDocumentLoaded(ResXmlDocument resXmlDocument) { + if(!validateXmlNamespace){ + return resXmlDocument; + } + XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(resXmlDocument); + namespaceValidator.validate(); + return resXmlDocument; + } + + private static Decoder createDecoder(ApkModule apkModule){ + Decoder decoder = Decoder.create(apkModule.getTableBlock()); + decoder.setApkFile(apkModule); + return decoder; + } +} diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java index 191db15..0731cef 100644 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java @@ -33,6 +33,7 @@ public class ResXmlPullParser implements XmlResourceParser { private final ParserEventList mEventList = new ParserEventList(); private ResXmlDocument mDocument; private boolean mDocumentCreatedHere; + private DocumentLoadedListener documentLoadedListener; public ResXmlPullParser(Decoder decoder){ this.mDecoder = decoder; @@ -345,18 +346,7 @@ public class ResXmlPullParser implements XmlResourceParser { } @Override public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException { - synchronized (this){ - ResXmlDocument xmlDocument = new ResXmlDocument(); - try { - xmlDocument.readBytes(inputStream); - } catch (IOException exception) { - XmlPullParserException pullParserException = new XmlPullParserException(exception.getMessage()); - pullParserException.initCause(exception); - throw pullParserException; - } - setResXmlDocument(xmlDocument); - this.mDocumentCreatedHere = true; - } + loadResXmlDocument(inputStream); } @Override public String getInputEncoding() { @@ -617,4 +607,31 @@ public class ResXmlPullParser implements XmlResourceParser { return null; } + public void setDocumentLoadedListener(DocumentLoadedListener documentLoadedListener) { + this.documentLoadedListener = documentLoadedListener; + } + + private void loadResXmlDocument(InputStream inputStream) throws XmlPullParserException { + synchronized (this){ + ResXmlDocument xmlDocument = new ResXmlDocument(); + try { + xmlDocument.readBytes(inputStream); + } catch (IOException exception) { + XmlPullParserException pullParserException = new XmlPullParserException(exception.getMessage()); + pullParserException.initCause(exception); + throw pullParserException; + } + DocumentLoadedListener listener = this.documentLoadedListener; + if(listener != null){ + xmlDocument = listener.onDocumentLoaded(xmlDocument); + } + setResXmlDocument(xmlDocument); + this.mDocumentCreatedHere = true; + } + } + + public static interface DocumentLoadedListener{ + public ResXmlDocument onDocumentLoaded(ResXmlDocument resXmlDocument); + } + } diff --git a/src/main/java/com/reandroid/xml/XmlParserToSerializer.java b/src/main/java/com/reandroid/xml/XmlParserToSerializer.java new file mode 100644 index 0000000..8d1c36f --- /dev/null +++ b/src/main/java/com/reandroid/xml/XmlParserToSerializer.java @@ -0,0 +1,101 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * 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 com.reandroid.xml; + +import android.content.res.XmlResourceParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + +public class XmlParserToSerializer { + private final XmlSerializer serializer; + private final XmlResourceParser parser; + public XmlParserToSerializer(XmlResourceParser parser, XmlSerializer serializer){ + this.parser = parser; + this.serializer = serializer; + } + public void write() throws IOException, XmlPullParserException { + XmlResourceParser parser = this.parser; + int event = parser.next(); + while (nextEvent(event)){ + event = parser.next(); + } + close(); + } + private void close(){ + parser.close(); + } + private boolean nextEvent(int event) throws IOException, XmlPullParserException { + boolean hasNext = true; + switch (event){ + case XmlResourceParser.START_DOCUMENT: + onStartDocument(); + break; + case XmlResourceParser.START_TAG: + onStartTag(); + break; + case XmlResourceParser.TEXT: + onText(); + break; + case XmlResourceParser.COMMENT: + onComment(); + break; + case XmlResourceParser.END_TAG: + onEndTag(); + break; + case XmlResourceParser.END_DOCUMENT: + onEndDocument(); + hasNext = false; + break; + } + return hasNext; + } + + private void onStartDocument() throws IOException{ + serializer.startDocument("utf-8", null); + } + private void onStartTag() throws IOException, XmlPullParserException { + XmlResourceParser parser = this.parser; + XmlSerializer serializer = this.serializer; + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + int nsCount = parser.getNamespaceCount(parser.getDepth()); + for(int i=0; i