adapt AOSP xml serializer

This commit is contained in:
REAndroid 2023-04-18 15:16:59 +02:00
parent dd5233f13c
commit 092383e3e0
6 changed files with 909 additions and 41 deletions

View File

@ -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("&amp;");
break;
case '>' :
append("&gt;");
break;
case '<' :
append("&lt;");
break;
default:
if(c == quot){
append(c == '"' ? "&quot;" : "&apos;");
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("<!DOCTYPE");
append(dd);
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("<?xml version='1.0' ");
if(encoding != null){
this.encoding = encoding;
if(encoding.toLowerCase(Locale.US).startsWith("utf")){
unicode = true;
}
}
if(this.encoding != null){
append("encoding='");
append(this.encoding);
append("' ");
}
if(standalone != null){
append("standalone='");
append(standalone ? "yes" : "no");
append("' ");
}
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("</{"+namespace+"}"+name+"> does not match start");
}
if(pending){
check(true);
depth--;
}
else {
if(indent[depth + 1]){
append('\r');
append('\n');
appendSpace(2 * depth);
}
append("</");
String prefix = elementStack[depth * 3 + 1];
if(!prefix.isEmpty()){
append(prefix);
append(':');
}
append(name);
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("]]>", "]]]]><![CDATA[>");
append("<![CDATA[");
for (int i = 0; i < data.length(); ++i){
char ch = data.charAt(i);
boolean allowedInCdata = (ch >= 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("<![CDATA[");
} else {
reportInvalidCharacter(ch);
}
}
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("<!--");
append(comment);
append("-->");
}
@Override
public void processingInstruction(String pi)
throws IOException {
check(false);
append("<?");
append(pi);
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);
}
}

View File

@ -528,6 +528,9 @@ public class ApkModule implements ApkFile {
if(inputSource==null){ if(inputSource==null){
throw new FileNotFoundException("No such file in apk: " + path); throw new FileNotFoundException("No such file in apk: " + path);
} }
return loadResXmlDocument(inputSource);
}
public ResXmlDocument loadResXmlDocument(InputSource inputSource) throws IOException{
ResXmlDocument resXmlDocument = new ResXmlDocument(); ResXmlDocument resXmlDocument = new ResXmlDocument();
resXmlDocument.setApkFile(this); resXmlDocument.setApkFile(this);
resXmlDocument.readBytes(inputSource.openStream()); resXmlDocument.readBytes(inputSource.openStream());

View File

@ -15,6 +15,7 @@
*/ */
package com.reandroid.apk; package com.reandroid.apk;
import com.reandroid.apk.xmldecoder.ResXmlDocumentSerializer;
import com.reandroid.archive.InputSource; import com.reandroid.archive.InputSource;
import com.reandroid.apk.xmldecoder.XMLBagDecoder; import com.reandroid.apk.xmldecoder.XMLBagDecoder;
import com.reandroid.apk.xmldecoder.XMLNamespaceValidator; import com.reandroid.apk.xmldecoder.XMLNamespaceValidator;
@ -32,10 +33,12 @@ import com.reandroid.xml.XMLAttribute;
import com.reandroid.xml.XMLDocument; import com.reandroid.xml.XMLDocument;
import com.reandroid.xml.XMLElement; import com.reandroid.xml.XMLElement;
import com.reandroid.xml.XMLException; import com.reandroid.xml.XMLException;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.*; import java.util.*;
public class ApkModuleXmlDecoder { public class ApkModuleXmlDecoder {
@ -43,13 +46,22 @@ import java.util.*;
private final Map<Integer, Set<ResConfig>> decodedEntries; private final Map<Integer, Set<ResConfig>> decodedEntries;
private XMLBagDecoder xmlBagDecoder; private XMLBagDecoder xmlBagDecoder;
private final Set<String> mDecodedPaths; private final Set<String> mDecodedPaths;
private ResXmlDocumentSerializer documentSerializer;
private boolean useAndroidSerializer;
public ApkModuleXmlDecoder(ApkModule apkModule){ public ApkModuleXmlDecoder(ApkModule apkModule){
this.apkModule=apkModule; this.apkModule=apkModule;
this.decodedEntries = new HashMap<>(); this.decodedEntries = new HashMap<>();
this.mDecodedPaths = new HashSet<>(); this.mDecodedPaths = new HashSet<>();
this.useAndroidSerializer = true;
}
public void setUseAndroidSerializer(boolean useAndroidSerializer) {
this.useAndroidSerializer = useAndroidSerializer;
} }
public void sanitizeFilePaths(){ public void sanitizeFilePaths(){
PathSanitizer sanitizer = new PathSanitizer(apkModule); sanitizeFilePaths(false);
}
public void sanitizeFilePaths(boolean sanitizeResourceFiles){
PathSanitizer sanitizer = new PathSanitizer(apkModule, sanitizeResourceFiles);
sanitizer.sanitize(); sanitizer.sanitize();
} }
public void decodeTo(File outDir) public void decodeTo(File outDir)
@ -66,14 +78,14 @@ import java.util.*;
decodePublicXml(tableBlock, outDir); decodePublicXml(tableBlock, outDir);
decodeAndroidManifest(tableBlock, outDir); decodeAndroidManifest(outDir);
addDecodedPath(TableBlock.FILE_NAME); addDecodedPath(TableBlock.FILE_NAME);
logMessage("Decoding resource files ..."); logMessage("Decoding resource files ...");
List<ResFile> resFileList=apkModule.listResFiles(); List<ResFile> resFileList=apkModule.listResFiles();
for(ResFile resFile:resFileList){ for(ResFile resFile:resFileList){
decodeResFile(tableBlock, outDir, resFile); decodeResFile(outDir, resFile);
} }
decodeValues(tableBlock, outDir, tableBlock); decodeValues(tableBlock, outDir, tableBlock);
@ -104,10 +116,10 @@ import java.util.*;
UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles(); UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles();
uncompressedFiles.toJson().write(file); uncompressedFiles.toJson().write(file);
} }
private void decodeResFile(EntryStore entryStore, File outDir, ResFile resFile) private void decodeResFile(File outDir, ResFile resFile)
throws IOException, XMLException { throws IOException{
if(resFile.isBinaryXml()){ if(resFile.isBinaryXml()){
decodeResXml(entryStore, outDir, resFile); decodeResXml(outDir, resFile);
}else { }else {
decodeResRaw(outDir, resFile); decodeResRaw(outDir, resFile);
} }
@ -133,28 +145,32 @@ import java.util.*;
addDecodedEntry(entry); addDecodedEntry(entry);
} }
private void decodeResXml(EntryStore entryStore, File outDir, ResFile resFile) private void decodeResXml(File outDir, ResFile resFile)
throws IOException, XMLException{ throws IOException{
Entry entry =resFile.pickOne(); Entry entry = resFile.pickOne();
PackageBlock packageBlock= entry.getPackageBlock(); PackageBlock packageBlock = entry.getPackageBlock();
ResXmlDocument resXmlDocument = apkModule.loadResXmlDocument(
resFile.getInputSource().getName());
File pkgDir=new File(outDir, getPackageDirName(packageBlock)); File pkgDir = new File(outDir, getPackageDirName(packageBlock));
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
String path = alias.replace('/', File.separatorChar); String path = alias.replace('/', File.separatorChar);
path=path.replace('/', File.separatorChar); path = path.replace('/', File.separatorChar);
File file=new File(pkgDir, path); File file = new File(pkgDir, path);
logVerbose("Decoding: "+path); logVerbose("Decoding: " + path);
XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(resXmlDocument); serializeXml(packageBlock.getId(), resFile.getInputSource(), file);
namespaceValidator.validate();
XMLDocument xmlDocument= resXmlDocument.decodeToXml(entryStore, packageBlock.getId());
xmlDocument.save(file, true);
resFile.setFilePath(alias); resFile.setFilePath(alias);
addDecodedEntry(entry);
addDecodedEntry(resFile.pickOne()); }
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) private void decodePublicXml(TableBlock tableBlock, File outDir)
throws IOException{ throws IOException{
@ -190,8 +206,8 @@ import java.util.*;
resourceIds.loadPackageBlock(packageBlock); resourceIds.loadPackageBlock(packageBlock);
resourceIds.writeXml(file); resourceIds.writeXml(file);
} }
private void decodeAndroidManifest(EntryStore entryStore, File outDir) private void decodeAndroidManifest(File outDir)
throws IOException, XMLException { throws IOException {
if(!apkModule.hasAndroidManifestBlock()){ if(!apkModule.hasAndroidManifestBlock()){
logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME); logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME);
return; return;
@ -199,13 +215,58 @@ import java.util.*;
File file=new File(outDir, AndroidManifestBlock.FILE_NAME); File file=new File(outDir, AndroidManifestBlock.FILE_NAME);
logMessage("Decoding: "+file.getName()); logMessage("Decoding: "+file.getName());
AndroidManifestBlock manifestBlock=apkModule.getAndroidManifestBlock(); AndroidManifestBlock manifestBlock=apkModule.getAndroidManifestBlock();
XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(manifestBlock); int currentPackageId = manifestBlock.guessCurrentPackageId();
namespaceValidator.validate(); serializeXml(currentPackageId, manifestBlock, file);
int currentPackageId= manifestBlock.guessCurrentPackageId();
XMLDocument xmlDocument=manifestBlock.decodeToXml(entryStore, currentPackageId);
xmlDocument.save(file, true);
addDecodedPath(AndroidManifestBlock.FILE_NAME); 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){ private void addDecodedEntry(Entry entry){
if(entry.isNull()){ if(entry.isNull()){
return; return;

View File

@ -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;
}
}

View File

@ -33,6 +33,7 @@ public class ResXmlPullParser implements XmlResourceParser {
private final ParserEventList mEventList = new ParserEventList(); private final ParserEventList mEventList = new ParserEventList();
private ResXmlDocument mDocument; private ResXmlDocument mDocument;
private boolean mDocumentCreatedHere; private boolean mDocumentCreatedHere;
private DocumentLoadedListener documentLoadedListener;
public ResXmlPullParser(Decoder decoder){ public ResXmlPullParser(Decoder decoder){
this.mDecoder = decoder; this.mDecoder = decoder;
@ -345,18 +346,7 @@ public class ResXmlPullParser implements XmlResourceParser {
} }
@Override @Override
public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException { public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException {
synchronized (this){ loadResXmlDocument(inputStream);
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;
}
} }
@Override @Override
public String getInputEncoding() { public String getInputEncoding() {
@ -617,4 +607,31 @@ public class ResXmlPullParser implements XmlResourceParser {
return null; 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);
}
} }

View File

@ -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<nsCount; i++){
String prefix = parser.getNamespacePrefix(i);
String namespace = parser.getNamespaceUri(i);
serializer.setPrefix(prefix, namespace);
}
serializer.startTag(parser.getNamespace(), parser.getName());
int attrCount = parser.getAttributeCount();
for(int i=0; i<attrCount; i++){
serializer.attribute(parser.getAttributeNamespace(i),
parser.getAttributeName(i),
parser.getAttributeValue(i));
}
}
private void onText() throws IOException{
serializer.text(parser.getText());
}
private void onComment() throws IOException{
serializer.comment(parser.getText());
}
private void onEndTag() throws IOException{
serializer.endTag(parser.getNamespace(), parser.getName());
}
private void onEndDocument() throws IOException{
serializer.endDocument();
}
}