From b49e77087dc3cdd14b66165c7e646af0c411d44c Mon Sep 17 00:00:00 2001 From: Igor Eisberg <8811086+IgorEisberg@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:54:03 +0300 Subject: [PATCH] refactor: clean up external pull parser and introduce brut.j.xml (#3709) * refactor: clean up external pull parser and introduce brut.j.xml We have no need for an XML pull parser in the project, it was only used for testing, which is now done with XPath. The external xpp3 library from org.ogce is obsolete and has the issue of including javax.xml.namespace.QName which conflicts with the JRE implementation that exists for a very long time now. This makes direct usages of QName produce very obscure NPEs that took me hours to figure out. This patch will allow further optimization that is WIP. The external library was replaced by the basic xmlpull API. The MXSerializer has been cleaned and the features used by apktool have been integrated into the custom implementation, now part of a separate module called brut.j.xml. Writing has been optimized by buffering write operations, inspired by KXmlSerializer used by Android itself. A class XmlPullUtils also written that allows copying from a XmlPullParser into a XmlSerializer with or without an EventHandler. We use it for AndroidManifestPullStreamDecoder (with EventHandler, to allow omitting the uses-sdk tag), and for ResXmlPullStreamDecoder (direct copy, without EventHandler). saveDocument in ResXmlPatcher was tweaked to output proper output - a new line after declaration and a new line after root element's end tag. TL;DR mostly behind the scene refactor, no end user changes. --- brut.apktool/apktool-cli/proguard-rules.pro | 3 - brut.apktool/apktool-lib/build.gradle.kts | 6 +- .../brut/androlib/res/ResourcesDecoder.java | 33 +- .../res/decoder/AXmlResourceParser.java | 4 +- .../AndroidManifestPullStreamDecoder.java | 206 ++- .../res/decoder/ResXmlPullStreamDecoder.java | 38 +- .../androlib/res/util/ExtMXSerializer.java | 76 -- .../androlib/res/util/ExtXmlSerializer.java | 32 - .../brut/androlib/res/xml/ResXmlPatcher.java | 21 +- .../test/java/brut/androlib/TestUtils.java | 55 +- .../aapt2/testapp/AndroidManifest.xml | 2 +- brut.j.dir/build.gradle.kts | 6 +- brut.j.xml/build.gradle.kts | 3 + .../main/java/brut/xmlpull}/MXSerializer.java | 1111 +++++++++-------- .../main/java/brut/xmlpull/XmlPullUtils.java | 122 ++ build.gradle.kts | 2 +- gradle/libs.versions.toml | 4 +- settings.gradle.kts | 2 +- 18 files changed, 887 insertions(+), 839 deletions(-) delete mode 100644 brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java delete mode 100644 brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java create mode 100644 brut.j.xml/build.gradle.kts rename {brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed => brut.j.xml/src/main/java/brut/xmlpull}/MXSerializer.java (63%) create mode 100644 brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java diff --git a/brut.apktool/apktool-cli/proguard-rules.pro b/brut.apktool/apktool-cli/proguard-rules.pro index 27594dfb..5b6b4e34 100644 --- a/brut.apktool/apktool-cli/proguard-rules.pro +++ b/brut.apktool/apktool-cli/proguard-rules.pro @@ -6,9 +6,6 @@ static ** valueOf(java.lang.String); } -# https://github.com/iBotPeaches/Apktool/issues/3602#issuecomment-2117317880 --dontwarn org.xmlpull.mxp1** - # https://github.com/iBotPeaches/Apktool/pull/3670#issuecomment-2296326878 -dontwarn com.google.j2objc.annotations.Weak -dontwarn com.google.j2objc.annotations.RetainedWith diff --git a/brut.apktool/apktool-lib/build.gradle.kts b/brut.apktool/apktool-lib/build.gradle.kts index 6a0c8030..ed25d5e3 100644 --- a/brut.apktool/apktool-lib/build.gradle.kts +++ b/brut.apktool/apktool-lib/build.gradle.kts @@ -25,13 +25,13 @@ tasks { } dependencies { - api(project(":brut.j.dir")) - api(project(":brut.j.util")) api(project(":brut.j.common")) + api(project(":brut.j.util")) + api(project(":brut.j.dir")) + api(project(":brut.j.xml")) implementation(libs.baksmali) implementation(libs.smali) - implementation(libs.xmlpull) implementation(libs.guava) implementation(libs.commons.lang3) implementation(libs.commons.io) diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java index a1749e33..4f8e9818 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java @@ -21,13 +21,12 @@ import brut.androlib.apk.ApkInfo; import brut.androlib.exceptions.AndrolibException; import brut.androlib.res.data.*; import brut.androlib.res.decoder.*; -import brut.androlib.res.util.ExtMXSerializer; -import brut.androlib.res.util.ExtXmlSerializer; import brut.androlib.res.xml.ResValuesXmlSerializable; import brut.androlib.res.xml.ResXmlPatcher; import brut.directory.Directory; import brut.directory.DirectoryException; import brut.directory.FileDirectory; +import brut.xmlpull.MXSerializer; import org.apache.commons.io.IOUtils; import org.xmlpull.v1.XmlSerializer; @@ -75,7 +74,8 @@ public class ResourcesDecoder { } AXmlResourceParser axmlParser = new AndroidManifestResourceParser(mResTable); - ResStreamDecoder fileDecoder = new AndroidManifestPullStreamDecoder(axmlParser, getResXmlSerializer()); + XmlSerializer xmlSerializer = newXmlSerializer(); + ResStreamDecoder fileDecoder = new AndroidManifestPullStreamDecoder(axmlParser, xmlSerializer); Directory inApk, out; InputStream inputStream = null; @@ -157,7 +157,8 @@ public class ResourcesDecoder { decoders.setDecoder("9patch", new Res9patchStreamDecoder()); AXmlResourceParser axmlParser = new AXmlResourceParser(mResTable); - decoders.setDecoder("xml", new ResXmlPullStreamDecoder(axmlParser, getResXmlSerializer())); + XmlSerializer xmlSerializer = newXmlSerializer(); + decoders.setDecoder("xml", new ResXmlPullStreamDecoder(axmlParser, xmlSerializer)); ResFileDecoder fileDecoder = new ResFileDecoder(decoders); Directory in, out, outRes; @@ -170,7 +171,6 @@ public class ResourcesDecoder { throw new AndrolibException(ex); } - ExtMXSerializer xmlSerializer = getResXmlSerializer(); for (ResPackage pkg : mResTable.listMainPackages()) { LOGGER.info("Decoding file-resources..."); @@ -191,20 +191,24 @@ public class ResourcesDecoder { } } - private ExtMXSerializer getResXmlSerializer() { - ExtMXSerializer serial = new ExtMXSerializer(); - serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_INDENTATION, " "); - serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_LINE_SEPARATOR, System.getProperty("line.separator")); - serial.setProperty(ExtXmlSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8"); - serial.setDisabledAttrEscape(true); - return serial; + private XmlSerializer newXmlSerializer() throws AndrolibException { + try { + XmlSerializer serial = new MXSerializer(); + serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true); + serial.setProperty(MXSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8"); + serial.setProperty(MXSerializer.PROPERTY_INDENTATION, " "); + serial.setProperty(MXSerializer.PROPERTY_LINE_SEPARATOR, System.getProperty("line.separator")); + return serial; + } catch (IllegalArgumentException | IllegalStateException ex) { + throw new AndrolibException(ex); + } } private void generateValuesFile(ResValuesFile valuesFile, Directory out, - ExtXmlSerializer serial) throws AndrolibException { + XmlSerializer serial) throws AndrolibException { try { OutputStream outStream = out.getFileOutput(valuesFile.getPath()); - serial.setOutput((outStream), null); + serial.setOutput(outStream, null); serial.startDocument(null, null); serial.startTag(null, "resources"); @@ -216,7 +220,6 @@ public class ResourcesDecoder { } serial.endTag(null, "resources"); - serial.newLine(); serial.endDocument(); serial.flush(); outStream.close(); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java index 5bbe0567..b8deb587 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java @@ -611,12 +611,12 @@ public class AXmlResourceParser implements XmlResourceParser { } @Override - public boolean getFeature(String feature) { + public boolean getFeature(String name) { return false; } @Override - public void setFeature(String name, boolean value) throws XmlPullParserException { + public void setFeature(String name, boolean state) throws XmlPullParserException { throw new XmlPullParserException(E_NOT_SUPPORTED); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java index 34b97271..d3235015 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java @@ -20,122 +20,28 @@ import brut.androlib.exceptions.AndrolibException; import brut.androlib.exceptions.AXmlDecodingException; import brut.androlib.exceptions.RawXmlEncounteredException; import brut.androlib.res.data.ResTable; -import brut.androlib.res.util.ExtXmlSerializer; +import brut.xmlpull.XmlPullUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.wrapper.XmlPullParserWrapper; -import org.xmlpull.v1.wrapper.XmlPullWrapperFactory; -import org.xmlpull.v1.wrapper.XmlSerializerWrapper; -import org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper; +import org.xmlpull.v1.XmlSerializer; import java.io.*; public class AndroidManifestPullStreamDecoder implements ResStreamDecoder { - public AndroidManifestPullStreamDecoder(AXmlResourceParser parser, - ExtXmlSerializer serializer) { - this.mParser = parser; - this.mSerial = serializer; + private final AXmlResourceParser mParser; + private final XmlSerializer mSerial; + + public AndroidManifestPullStreamDecoder(AXmlResourceParser parser, XmlSerializer serial) { + mParser = parser; + mSerial = serial; } @Override - public void decode(InputStream in, OutputStream out) - throws AndrolibException { + public void decode(InputStream in, OutputStream out) throws AndrolibException { try { - XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance(); - XmlPullParserWrapper par = factory.newPullParserWrapper(mParser); - final ResTable resTable = mParser.getResTable(); - - XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) { - final boolean hideSdkInfo = !resTable.getAnalysisMode(); - - @Override - public void event(XmlPullParser pp) - throws XmlPullParserException, IOException { - int type = pp.getEventType(); - - if (type == XmlPullParser.START_TAG) { - if ("manifest".equals(pp.getName())) { - try { - parseManifest(pp); - } catch (AndrolibException ignored) {} - } else if ("uses-sdk".equals(pp.getName())) { - try { - parseUsesSdk(pp); - } catch (AndrolibException ignored) {} - if (hideSdkInfo) { - return; - } - } - } else if (type == XmlPullParser.END_TAG - && "uses-sdk".equals(pp.getName())) { - if (hideSdkInfo) { - return; - } - } - - super.event(pp); - } - - private void parseManifest(XmlPullParser pp) - throws AndrolibException { - for (int i = 0; i < pp.getAttributeCount(); i++) { - String ns = pp.getAttributeNamespace(i); - String name = pp.getAttributeName(i); - String value = pp.getAttributeValue(i); - - if (value.isEmpty()) { - continue; - } - - if (ns.isEmpty()) { - if (name.equals("package")) { - resTable.setPackageRenamed(value); - } - } else if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { - switch (name) { - case "versionCode": - resTable.setVersionCode(value); - break; - case "versionName": - resTable.setVersionName(value); - break; - } - } - } - } - - private void parseUsesSdk(XmlPullParser pp) - throws AndrolibException { - for (int i = 0; i < pp.getAttributeCount(); i++) { - String ns = pp.getAttributeNamespace(i); - String name = pp.getAttributeName(i); - String value = pp.getAttributeValue(i); - - if (value.isEmpty()) { - continue; - } - - if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { - switch (name) { - case "minSdkVersion": - case "targetSdkVersion": - case "maxSdkVersion": - case "compileSdkVersion": - resTable.addSdkInfo(name, value); - break; - } - } - } - } - }; - - par.setInput(in, null); - ser.setOutput(out, null); - - while (par.nextToken() != XmlPullParser.END_DOCUMENT) { - ser.event(par); - } - ser.flush(); + mParser.setInput(in, null); + mSerial.setOutput(out, null); + XmlPullUtils.copy(mParser, mSerial, new EventHandler(mParser.getResTable())); } catch (XmlPullParserException ex) { throw new AXmlDecodingException("Could not decode XML", ex); } catch (IOException ex) { @@ -143,6 +49,90 @@ public class AndroidManifestPullStreamDecoder implements ResStreamDecoder { } } - private final AXmlResourceParser mParser; - private final ExtXmlSerializer mSerial; + private static class EventHandler implements XmlPullUtils.EventHandler { + private final ResTable mResTable; + private final boolean mHideSdkInfo; + + public EventHandler(ResTable resTable) { + mResTable = resTable; + mHideSdkInfo = !resTable.getAnalysisMode(); + } + + @Override + public boolean onEvent(XmlPullParser in, XmlSerializer out) throws XmlPullParserException { + int type = in.getEventType(); + + if (type == XmlPullParser.START_TAG) { + String name = in.getName(); + + if (name.equals("manifest")) { + parseManifest(in); + } else if (name.equals("uses-sdk")) { + parseUsesSdk(in); + + if (mHideSdkInfo) { + return true; + } + } + } else if (type == XmlPullParser.END_TAG) { + String name = in.getName(); + + if (name.equals("uses-sdk")) { + if (mHideSdkInfo) { + return true; + } + } + } + + return false; + } + + private void parseManifest(XmlPullParser in) { + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = in.getAttributeNamespace(i); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + + if (value.isEmpty()) { + continue; + } + if (ns.isEmpty()) { + if (name.equals("package")) { + mResTable.setPackageRenamed(value); + } + } else if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { + switch (name) { + case "versionCode": + mResTable.setVersionCode(value); + break; + case "versionName": + mResTable.setVersionName(value); + break; + } + } + } + } + + private void parseUsesSdk(XmlPullParser in) { + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = in.getAttributeNamespace(i); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + + if (value.isEmpty()) { + continue; + } + if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { + switch (name) { + case "minSdkVersion": + case "targetSdkVersion": + case "maxSdkVersion": + case "compileSdkVersion": + mResTable.addSdkInfo(name, value); + break; + } + } + } + } + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java index 1719b20c..1e90d176 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java @@ -19,45 +19,31 @@ package brut.androlib.res.decoder; import brut.androlib.exceptions.AndrolibException; import brut.androlib.exceptions.AXmlDecodingException; import brut.androlib.exceptions.RawXmlEncounteredException; -import brut.androlib.res.util.ExtXmlSerializer; -import org.xmlpull.v1.XmlPullParser; +import brut.xmlpull.XmlPullUtils; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.wrapper.XmlPullParserWrapper; -import org.xmlpull.v1.wrapper.XmlPullWrapperFactory; -import org.xmlpull.v1.wrapper.XmlSerializerWrapper; -import org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper; +import org.xmlpull.v1.XmlSerializer; import java.io.*; public class ResXmlPullStreamDecoder implements ResStreamDecoder { - public ResXmlPullStreamDecoder(AXmlResourceParser parser, - ExtXmlSerializer serializer) { - this.mParser = parser; - this.mSerial = serializer; + private final AXmlResourceParser mParser; + private final XmlSerializer mSerial; + + public ResXmlPullStreamDecoder(AXmlResourceParser parser, XmlSerializer serial) { + mParser = parser; + mSerial = serial; } @Override - public void decode(InputStream in, OutputStream out) - throws AndrolibException { + public void decode(InputStream in, OutputStream out) throws AndrolibException { try { - XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance(); - XmlPullParserWrapper par = factory.newPullParserWrapper(mParser); - XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory); - - par.setInput(in, null); - ser.setOutput(out, null); - - while (par.nextToken() != XmlPullParser.END_DOCUMENT) { - ser.event(par); - } - ser.flush(); + mParser.setInput(in, null); + mSerial.setOutput(out, null); + XmlPullUtils.copy(mParser, mSerial); } catch (XmlPullParserException ex) { throw new AXmlDecodingException("Could not decode XML", ex); } catch (IOException ex) { throw new RawXmlEncounteredException("Could not decode XML", ex); } } - - private final AXmlResourceParser mParser; - private final ExtXmlSerializer mSerial; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java deleted file mode 100644 index 7114dae4..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * 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 - * - * https://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 brut.androlib.res.util; - -import org.xmlpull.renamed.MXSerializer; - -import java.io.*; - -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(); - } - - @Override - protected void writeAttributeValue(String value, Writer out) throws IOException { - if (mIsDisabledAttrEscape) { - out.write(value == null ? "" : value); - return; - } - super.writeAttributeValue(value, out); - } - - @Override - public void setOutput(OutputStream os, String encoding) throws IOException { - super.setOutput(os, encoding != null ? encoding : mDefaultEncoding); - } - - @Override - public Object getProperty(String name) throws IllegalArgumentException { - if (PROPERTY_DEFAULT_ENCODING.equals(name)) { - return mDefaultEncoding; - } - return super.getProperty(name); - } - - @Override - public void setProperty(String name, Object value) throws IllegalArgumentException, IllegalStateException { - if (PROPERTY_DEFAULT_ENCODING.equals(name)) { - mDefaultEncoding = (String) value; - } else { - super.setProperty(name, value); - } - } - - @Override - public ExtXmlSerializer newLine() throws IOException { - super.out.write(lineSeparator); - return this; - } - - @Override - public void setDisabledAttrEscape(boolean disabled) { - mIsDisabledAttrEscape = disabled; - } - - private String mDefaultEncoding; - private boolean mIsDisabledAttrEscape = false; - -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java deleted file mode 100644 index 3e4592ef..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * 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 - * - * https://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 brut.androlib.res.util; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -public interface ExtXmlSerializer extends XmlSerializer { - - ExtXmlSerializer newLine() throws IOException; - - void setDisabledAttrEscape(boolean disabled); - - String PROPERTY_SERIALIZER_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#serializer-indentation"; - String PROPERTY_SERIALIZER_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator"; - String PROPERTY_DEFAULT_ENCODING = "DEFAULT_ENCODING"; -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java index 5f236c39..ec868901 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java @@ -23,12 +23,15 @@ import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.io.*; import java.util.*; import java.util.logging.Logger; @@ -436,11 +439,19 @@ public final class ResXmlPatcher { private static void saveDocument(File file, Document doc) throws IOException, SAXException, ParserConfigurationException, TransformerException { - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - DOMSource source = new DOMSource(doc); - StreamResult result = new StreamResult(file); - transformer.transform(source, result); + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + byte[] xmlDecl = "".getBytes(StandardCharsets.US_ASCII); + byte[] newLine = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII); + + try (OutputStream output = Files.newOutputStream(file.toPath())) { + output.write(xmlDecl); + output.write(newLine); + transformer.transform(new DOMSource(doc), new StreamResult(output)); + output.write(newLine); + } } private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD"; diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java index 855326ff..e59768de 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java @@ -25,12 +25,16 @@ import brut.directory.Directory; import brut.directory.FileDirectory; import brut.util.OS; import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import java.io.*; import java.net.URL; import java.net.URLDecoder; @@ -40,43 +44,24 @@ import java.util.Map; public abstract class TestUtils { - public static Map parseStringsXml(File file) - throws BrutException { + public static Map parseStringsXml(File file) throws BrutException { try { - XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); - xpp.setInput(new FileReader(file)); + Document doc = getDocumentFromFile(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expression = "/resources/string[@name]"; + NodeList nodes = (NodeList) xPath.evaluate(expression, doc, XPathConstants.NODESET); - int eventType; - String key = null; Map map = new HashMap<>(); - while ((eventType = xpp.next()) != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case XmlPullParser.START_TAG: - if ("string".equals(xpp.getName())) { - int attrCount = xpp.getAttributeCount(); - for (int i = 0; i < attrCount; i++) { - if ("name".equals(xpp.getAttributeName(i))) { - key = xpp.getAttributeValue(i); - break; - } - } - } - break; - case XmlPullParser.END_TAG: - if ("string".equals(xpp.getName())) { - key = null; - } - break; - case XmlPullParser.TEXT: - if (key != null) { - map.put(key, xpp.getText()); - } - break; - } + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node nameAttr = attrs.getNamedItem("name"); + map.put(nameAttr.getNodeValue(), node.getTextContent()); } return map; - } catch (IOException | XmlPullParserException ex) { + } catch (XPathExpressionException ex) { throw new BrutException(ex); } } @@ -84,7 +69,7 @@ public abstract class TestUtils { public static Document getDocumentFromFile(File file) throws BrutException { try { return ResXmlPatcher.loadDocument(file); - } catch (ParserConfigurationException | SAXException | IOException ex) { + } catch (IOException | SAXException | ParserConfigurationException ex) { throw new BrutException(ex); } } diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml index 7a6c57e5..b5020e46 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml @@ -11,5 +11,5 @@ - + diff --git a/brut.j.dir/build.gradle.kts b/brut.j.dir/build.gradle.kts index d4865c80..d6157bc6 100644 --- a/brut.j.dir/build.gradle.kts +++ b/brut.j.dir/build.gradle.kts @@ -1,5 +1,5 @@ dependencies { - implementation(project(":brut.j.common")) - implementation(project(":brut.j.util")) - implementation(libs.commons.io) + implementation(project(":brut.j.common")) + implementation(project(":brut.j.util")) + implementation(libs.commons.io) } diff --git a/brut.j.xml/build.gradle.kts b/brut.j.xml/build.gradle.kts new file mode 100644 index 00000000..96159031 --- /dev/null +++ b/brut.j.xml/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(libs.xmlpull) +} diff --git a/brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java b/brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java similarity index 63% rename from brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java rename to brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java index 96c5f41f..68096dca 100644 --- a/brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java +++ b/brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java @@ -14,14 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.xmlpull.renamed; +package brut.xmlpull; import org.xmlpull.v1.XmlSerializer; import java.io.*; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; /** * Implementation of XmlSerializer interface from XmlPull V1 API. This @@ -30,72 +28,71 @@ import java.util.Set; *

* Implemented features: *

    + *
  • FEATURE_ATTR_VALUE_NO_ESCAPE *
  • 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 + *
  • PROPERTY_DEFAULT_ENCODING + *
  • PROPERTY_INDENTATION + *
  • PROPERTY_LINE_SEPARATOR + *
  • PROPERTY_LOCATION *
* */ 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/"; + public static final String FEATURE_ATTR_VALUE_NO_ESCAPE = "http://xmlpull.org/v1/doc/features.html#attr-value-no-escape"; + public static final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned"; + public static final String PROPERTY_DEFAULT_ENCODING = "http://xmlpull.org/v1/doc/properties.html#default-encoding"; + public static final String PROPERTY_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#indentation"; + public static final String PROPERTY_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#line-separator"; + public static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; + 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"; + private static final String XML_URI = "http://www.w3.org/XML/1998/namespace"; + private static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; // properties/features - protected boolean namesInterned; - protected boolean attributeUseApostrophe; - protected String indentationString = null; // " "; - protected String lineSeparator = "\n"; + private boolean namesInterned; + private boolean attrValueNoEscape; + private String defaultEncoding; + private String indentationString; + private String lineSeparator; - protected String location; - protected Writer out; + private String location; + private Writer writer; - protected int autoDeclaredPrefixes; + private int autoDeclaredPrefixes; - protected int depth = 0; + private 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]; + private String[] elNamespace = new String[2]; + private String[] elName = new String[elNamespace.length]; + private String[] elPrefix = new String[elNamespace.length]; + private 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]; + private int namespaceEnd = 0; + private String[] namespacePrefix = new String[8]; + private String[] namespaceUri = new String[namespacePrefix.length]; - protected boolean finished; - protected boolean pastRoot; - protected boolean setPrefixCalled; - protected boolean startTagIncomplete; + private boolean finished; + private boolean pastRoot; + private boolean setPrefixCalled; + private boolean startTagIncomplete; - protected boolean doIndent; - protected boolean seenTag; + private boolean doIndent; + private boolean seenTag; - protected boolean seenBracket; - protected boolean seenBracketBracket; - - // buffer output if needed 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; + private boolean seenBracket; + private boolean seenBracketBracket; + private static final String[] precomputedPrefixes; static { precomputedPrefixes = new String[32]; // arbitrary number ... for (int i = 0; i < precomputedPrefixes.length; i++) { @@ -112,9 +109,267 @@ public class MXSerializer implements XmlSerializer { } } - protected void reset() { + private String getLocation() { + return location != null ? " @" + location : ""; + } + + private void ensureElementsCapacity() { + int elStackSize = elName.length; + int newSize = (depth >= 7 ? 2 * depth : 8) + 2; + + if (TRACE_SIZING) { + System.err.println(getClass().getName() + " elStackSize " + + elStackSize + " ==> " + newSize); + } + boolean needsCopying = elStackSize > 0; + String[] arr; + // 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; + + int[] iarr = new int[newSize]; + if (needsCopying) { + System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); + } else { + // special initialization + iarr[0] = 0; + } + elNamespaceCount = iarr; + } + + private void ensureNamespacesCapacity() { + int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8; + if (TRACE_SIZING) { + System.err.println(getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize); + } + String[] newNamespacePrefix = new String[newSize]; + 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; + } + + // use buffer to optimize writing + private static final int BUFFER_LEN = 8192; + private final char[] buffer = new char[BUFFER_LEN]; + private int bufidx; + + private void flushBuffer() throws IOException { + if (bufidx > 0) { + writer.write(buffer, 0, bufidx); + writer.flush(); + bufidx = 0; + } + } + + private void write(char ch) throws IOException { + if (bufidx >= BUFFER_LEN) { + flushBuffer(); + } + buffer[bufidx++] = ch; + } + + private void write(char[] buf, int i, int length) throws IOException { + while (length > 0) { + if (bufidx == BUFFER_LEN) { + flushBuffer(); + } + int batch = BUFFER_LEN - bufidx; + if (batch > length) { + batch = length; + } + System.arraycopy(buf, i, buffer, bufidx, batch); + i += batch; + length -= batch; + bufidx += batch; + } + } + + private void write(String str) throws IOException { + write(str, 0, str.length()); + } + + private void write(String str, int i, int length) throws IOException { + while (length > 0) { + if (bufidx == BUFFER_LEN) { + flushBuffer(); + } + int batch = BUFFER_LEN - bufidx; + if (batch > length) { + batch = length; + } + str.getChars(i, i + batch, buffer, bufidx); + i += batch; + length -= batch; + bufidx += batch; + } + } + + // precomputed variables to simplify writing indentation + private static final int MAX_INDENT = 65; + private int offsetNewLine; + private int indentationJump; + private char[] indentationBuf; + private int maxIndentLevel; + private boolean writeLineSeparator; // should end-of-line be written + private 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 changes. + */ + private void rebuildIndentationBuf() { + if (!doIndent) { + return; + } + int bufSize = 0; + offsetNewLine = 0; + if (writeLineSeparator) { + offsetNewLine = lineSeparator.length(); + bufSize += offsetNewLine; + } + maxIndentLevel = 0; + if (writeIndentation) { + indentationJump = indentationString.length(); + maxIndentLevel = MAX_INDENT / indentationJump; + bufSize += maxIndentLevel * indentationJump; + } + if (indentationBuf == null || indentationBuf.length < bufSize) { + indentationBuf = new char[bufSize + 8]; + } + int bufPos = 0; + if (writeLineSeparator) { + 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); + } + } + } + } + + private void writeIndent() throws IOException { + int start = writeLineSeparator ? 0 : offsetNewLine; + int level = Math.min(depth, maxIndentLevel); + + write(indentationBuf, start, ((level - 1) * indentationJump) + offsetNewLine); + } + + // --- public API methods + + @Override + public void setFeature(String name, boolean state) + throws IllegalArgumentException, IllegalStateException { + if (name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + switch (name) { + case FEATURE_ATTR_VALUE_NO_ESCAPE: + attrValueNoEscape = state; + break; + case FEATURE_NAMES_INTERNED: + namesInterned = state; + break; + default: + throw new IllegalStateException("unsupported feature: " + name); + } + } + + @Override + public boolean getFeature(String name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + switch (name) { + case FEATURE_ATTR_VALUE_NO_ESCAPE: + return attrValueNoEscape; + case FEATURE_NAMES_INTERNED: + return namesInterned; + default: + return false; + } + } + + @Override + public void setProperty(String name, Object value) + throws IllegalArgumentException, IllegalStateException { + if (name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + switch (name) { + case PROPERTY_DEFAULT_ENCODING: + defaultEncoding = (String) value; + break; + case PROPERTY_INDENTATION: + indentationString = (String) value; + break; + case PROPERTY_LINE_SEPARATOR: + lineSeparator = (String) value; + break; + case PROPERTY_LOCATION: + location = (String) value; + break; + default: + throw new IllegalStateException("unsupported property: " + name); + } + writeLineSeparator = lineSeparator != null && !lineSeparator.isEmpty(); + writeIndentation = indentationString != null && !indentationString.isEmpty(); + // optimize - do not write when nothing to write ... + doIndent = indentationString != null && (writeLineSeparator || writeIndentation); + // NOTE: when indentationString == null there is no indentation + // (even though writeLineSeparator may be true ...) + rebuildIndentationBuf(); + seenTag = false; // for consistency + } + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + switch (name) { + case PROPERTY_DEFAULT_ENCODING: + return defaultEncoding; + case PROPERTY_INDENTATION: + return indentationString; + case PROPERTY_LINE_SEPARATOR: + return lineSeparator; + case PROPERTY_LOCATION: + return location; + default: + return null; + } + } + + @Override + public void setOutput(Writer writer) { + this.writer = writer; + + // reset state location = null; - out = null; autoDeclaredPrefixes = 0; depth = 0; @@ -148,241 +403,43 @@ public class MXSerializer implements XmlSerializer { seenBracketBracket = false; } - protected void ensureElementsCapacity() { - final int elStackSize = elName.length; - final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; - - if (TRACE_SIZING) { - System.err.println(getClass().getName() + " elStackSize " - + elStackSize + " ==> " + newSize); - } - final boolean needsCopying = elStackSize > 0; - String[] arr; - // 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) { - 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; - } - - @Override - 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); - } - } - - @Override - 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) - 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); - } - } - } - } - - protected void writeIndent() throws IOException { - final int start = writeLineSepartor ? 0 : offsetNewLine; - final int level = Math.min(depth, maxIndentLevel); - - out.write(indentationBuf, start, ((level - 1) * indentationJump) + offsetNewLine); - } - - @Override - public void setProperty(String name, Object value) - throws IllegalArgumentException, IllegalStateException { - if (name == null) { - throw new IllegalArgumentException("property name can not be null"); - } - switch (name) { - case PROPERTY_SERIALIZER_INDENTATION: - indentationString = (String) value; - break; - case PROPERTY_SERIALIZER_LINE_SEPARATOR: - lineSeparator = (String) value; - break; - case PROPERTY_LOCATION: - location = (String) value; - break; - default: - 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 - } - - @Override - public Object getProperty(String name) throws IllegalArgumentException { - if (name == null) { - throw new IllegalArgumentException("property name can not be null"); - } - switch (name) { - case PROPERTY_SERIALIZER_INDENTATION: - return indentationString; - case PROPERTY_SERIALIZER_LINE_SEPARATOR: - return lineSeparator; - case PROPERTY_LOCATION: - return location; - default: - 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; - } - - @Override - public void setOutput(Writer writer) { - reset(); - out = writer; - } - @Override public void setOutput(OutputStream os, String encoding) throws IOException { - if (os == null) + 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); } + if (encoding == null) { + encoding = defaultEncoding; + } + setOutput(encoding != null + ? new OutputStreamWriter(os, encoding) + : new OutputStreamWriter(os)); } @Override - public void startDocument(String encoding, Boolean standalone) - throws IOException { - if (attributeUseApostrophe) { - out.write(""); + if (writeLineSeparator) { + write(lineSeparator); } - out.write("?>"); } @Override @@ -391,15 +448,18 @@ public class MXSerializer implements XmlSerializer { while (depth > 0) { endTag(elNamespace[depth], elName[depth]); } + if (writeLineSeparator) { + write(lineSeparator); + } + flushBuffer(); finished = pastRoot = startTagIncomplete = true; - out.flush(); } @Override public void setPrefix(String prefix, String namespace) throws IOException { - if (startTagIncomplete) + if (startTagIncomplete) { closeStartTag(); - + } if (prefix == null) { prefix = ""; } @@ -428,17 +488,12 @@ public class MXSerializer implements XmlSerializer { setPrefixCalled = true; } - protected String lookupOrDeclarePrefix(String namespace) { - return getPrefix(namespace, true); - } - @Override public String getPrefix(String namespace, boolean generatePrefix) { return getPrefix(namespace, generatePrefix, false); } - protected String getPrefix(String namespace, boolean generatePrefix, - boolean nonEmpty) { + private String getPrefix(String namespace, boolean generatePrefix, boolean nonEmpty) { if (!namesInterned) { // when String is interned we can do much faster namespace stack lookups ... namespace = namespace.intern(); @@ -447,15 +502,15 @@ public class MXSerializer implements XmlSerializer { } if (namespace == null) { throw new IllegalArgumentException("namespace must be not null" + getLocation()); - } else if (namespace.length() == 0) { + } else if (namespace.isEmpty()) { 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.equals(namespaceUri[i])) { - final String prefix = namespacePrefix[i]; - if (nonEmpty && prefix.length() == 0) { + String prefix = namespacePrefix[i]; + if (nonEmpty && prefix.isEmpty()) { continue; } @@ -470,24 +525,6 @@ public class MXSerializer implements XmlSerializer { return generatePrefix(namespace); } - private String generatePrefix(String namespace) { - ++autoDeclaredPrefixes; - // fast lookup uses table that was pre-initialized in static{} .... - final String prefix = autoDeclaredPrefixes < precomputedPrefixes.length - ? precomputedPrefixes[autoDeclaredPrefixes] - : ("n" + autoDeclaredPrefixes).intern(); - - // declare prefix - if (namespaceEnd >= namespacePrefix.length) { - ensureNamespacesCapacity(); - } - namespacePrefix[namespaceEnd] = prefix; - namespaceUri[namespaceEnd] = namespace; - ++namespaceEnd; - - return prefix; - } - @Override public int getDepth() { return depth; @@ -504,8 +541,7 @@ public class MXSerializer implements XmlSerializer { } @Override - public XmlSerializer startTag(String namespace, String name) - throws IOException { + public XmlSerializer startTag(String namespace, String name) throws IOException { if (startTagIncomplete) { closeStartTag(); } @@ -521,20 +557,22 @@ public class MXSerializer implements XmlSerializer { ensureElementsCapacity(); } - if (checkNamesInterned && namesInterned) + if (checkNamesInterned && namesInterned) { checkInterning(namespace); + } elNamespace[depth] = (namesInterned || namespace == null) ? namespace : namespace.intern(); - if (checkNamesInterned && namesInterned) + if (checkNamesInterned && namesInterned) { checkInterning(name); + } elName[depth] = (namesInterned || name == null) ? name : name.intern(); - if (out == null) { + if (writer == null) { throw new IllegalStateException("setOutput() must called set before serialization can start"); } - out.write('<'); + write('<'); if (namespace != null) { - if (namespace.length() > 0) { + if (!namespace.isEmpty()) { // in future make this algo a feature on serializer String prefix = null; if (depth > 0 && (namespaceEnd - elNamespaceCount[depth - 1]) == 1) { @@ -557,13 +595,13 @@ public class MXSerializer implements XmlSerializer { } } if (prefix == null) { - prefix = lookupOrDeclarePrefix(namespace); + prefix = getPrefix(namespace, true, false); } // make sure that default ("") namespace to not print ":" - if (prefix.length() > 0) { + if (!prefix.isEmpty()) { elPrefix[depth] = prefix; - out.write(prefix); - out.write(':'); + write(prefix); + write(':'); } else { elPrefix[depth] = ""; } @@ -571,10 +609,10 @@ public class MXSerializer implements XmlSerializer { // make sure that default namespace can be declared for (int i = namespaceEnd - 1; i >= 0; --i) { if (namespacePrefix[i] == "") { - final String uri = namespaceUri[i]; + String uri = namespaceUri[i]; if (uri == null) { setPrefix("", ""); - } else if (uri.length() > 0) { + } else if (!uri.isEmpty()) { throw new IllegalStateException("start tag can not be written in empty default namespace " + "as default namespace is currently bound to '" + uri + "'" + getLocation()); @@ -587,18 +625,40 @@ public class MXSerializer implements XmlSerializer { } else { elPrefix[depth] = ""; } - out.write(name); + write(name); return this; } + private 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 declarations! + writeNamespaceDeclarations(); + write('>'); + elNamespaceCount[depth] = namespaceEnd; + startTagIncomplete = false; + } + } + @Override - public XmlSerializer attribute(String namespace, String name, String value) - throws IOException { + public XmlSerializer attribute(String namespace, String name, String value) throws IOException { if (!startTagIncomplete) { throw new IllegalArgumentException("startTag() must be called before attribute()" + getLocation()); } - out.write(' '); - if (namespace != null && namespace.length() > 0) { + write(' '); + if (namespace != null && !namespace.isEmpty()) { if (!namesInterned) { namespace = namespace.intern(); } else if (checkNamesInterned) { @@ -610,79 +670,18 @@ public class MXSerializer implements XmlSerializer { // NOTE: attributes such as a='b' are in NO namespace prefix = generatePrefix(namespace); } - out.write(prefix); - out.write(':'); + write(prefix); + write(':'); } - out.write(name); - out.write('='); - out.write(attributeUseApostrophe ? '\'' : '"'); - writeAttributeValue(value, out); - out.write(attributeUseApostrophe ? '\'' : '"'); + write(name); + write("=\""); + writeAttributeValue(value); + write('"'); 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 declarations! - writeNamespaceDeclarations(); - out.write('>'); - elNamespaceCount[depth] = namespaceEnd; - startTagIncomplete = false; - } - } - - protected void writeNamespaceDeclarations() throws IOException { - Set uniqueNamespaces = new HashSet<>(); - for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) { - String prefix = namespacePrefix[i]; - String uri = namespaceUri[i]; - - // Some applications as seen in #2664 have duplicated namespaces. - // AOSP doesn't care, but the parser does. So we filter them out. - if (uniqueNamespaces.contains(prefix + uri)) { - continue; - } - - if (doIndent && uri.length() > 40) { - writeIndent(); - out.write(" "); - } - if (prefix != "") { - out.write(" xmlns:"); - out.write(prefix); - out.write('='); - } else { - out.write(" xmlns="); - } - out.write(attributeUseApostrophe ? '\'' : '"'); - - // NOTE: escaping of namespace value the same way as attributes!!!! - writeAttributeValue(uri, out); - out.write(attributeUseApostrophe ? '\'' : '"'); - - uniqueNamespaces.add(prefix + uri); - } - } - @Override - public XmlSerializer endTag(String namespace, String name) - throws IOException { + public XmlSerializer endTag(String namespace, String name) throws IOException { seenBracket = seenBracketBracket = false; if (namespace != null) { if (!namesInterned) { @@ -700,19 +699,19 @@ public class MXSerializer implements XmlSerializer { } if (startTagIncomplete) { writeNamespaceDeclarations(); - out.write(" />"); // space is added to make it easier to work in XHTML!!! + write(" />"); // space is added to make it easier to work in XHTML!!! } else { if (doIndent && seenTag) { writeIndent(); } - out.write(" 0) { - out.write(startTagPrefix); - out.write(':'); + if (!startTagPrefix.isEmpty()) { + write(startTagPrefix); + write(':'); } - out.write(name); - out.write('>'); + write(name); + write('>'); } --depth; namespaceEnd = elNamespaceCount[depth]; @@ -723,142 +722,209 @@ public class MXSerializer implements XmlSerializer { @Override public XmlSerializer text(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled) + if (startTagIncomplete || setPrefixCalled) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - writeElementContent(text, out); + } + writeElementContent(text); return this; } @Override - public XmlSerializer text(char[] buf, int start, int len) - throws IOException { - if (startTagIncomplete || setPrefixCalled) + public XmlSerializer text(char[] buf, int start, int len) throws IOException { + if (startTagIncomplete || setPrefixCalled) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - writeElementContent(buf, start, len, out); + } + writeElementContent(buf, start, len); return this; } @Override public void cdsect(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - out.write(""); + } + write(""); } @Override public void entityRef(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - out.write('&'); - out.write(text); // escape? - out.write(';'); + } + write('&'); + write(text); + write(';'); } @Override public void processingInstruction(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - out.write(""); + } + write(""); } @Override public void comment(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - out.write(""); + } + write(""); } @Override public void docdecl(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - out.write(""); + } + write(""); } @Override public void ignorableWhitespace(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) + if (startTagIncomplete || setPrefixCalled || seenBracket) { closeStartTag(); - if (doIndent && seenTag) + } + if (doIndent && seenTag) { seenTag = false; - if (text.length() == 0) { + } + if (text.isEmpty()) { throw new IllegalArgumentException("empty string is not allowed for ignorable whitespace" + getLocation()); } - out.write(text); // no escape? + write(text); } @Override public void flush() throws IOException { - if (!finished && startTagIncomplete) + if (!finished && startTagIncomplete) { closeStartTag(); - out.flush(); + } + flushBuffer(); } // --- utility methods - protected void writeAttributeValue(String value, Writer out) - throws IOException { - // .[apostrophe and <, & escaped], - final char quot = attributeUseApostrophe ? '\'' : '"'; - final String quotEntity = attributeUseApostrophe ? "'" : """; + private String generatePrefix(String namespace) { + ++autoDeclaredPrefixes; + // fast lookup uses table that was pre-initialized in static{} .... + String prefix = autoDeclaredPrefixes < precomputedPrefixes.length + ? precomputedPrefixes[autoDeclaredPrefixes] + : ("n" + autoDeclaredPrefixes).intern(); + // declare prefix + if (namespaceEnd >= namespacePrefix.length) { + ensureNamespacesCapacity(); + } + namespacePrefix[namespaceEnd] = prefix; + namespaceUri[namespaceEnd] = namespace; + ++namespaceEnd; + + return prefix; + } + + private void writeNamespaceDeclarations() throws IOException { + Set uniqueNamespaces = new HashSet<>(); + for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) { + String prefix = namespacePrefix[i]; + String uri = namespaceUri[i]; + + // Some applications as seen in #2664 have duplicated namespaces. + // AOSP doesn't care, but the parser does. So we filter them writer. + if (uniqueNamespaces.contains(prefix + uri)) { + continue; + } + + if (doIndent && uri.length() > 40) { + writeIndent(); + write(' '); + } + write(" xmlns"); + if (prefix != "") { + write(':'); + write(prefix); + } + write("=\""); + writeAttributeValue(uri); + write('"'); + + uniqueNamespaces.add(prefix + uri); + } + } + + private void writeAttributeValue(String value) throws IOException { + if (attrValueNoEscape) { + write(value); + return; + } + // .[&, < and " escaped], 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("&"); + if (i > pos) { + write(value.substring(pos, i)); + } + write("&"); pos = i + 1; } if (ch == '<') { - if (i > pos) - out.write(value.substring(pos, i)); - out.write("<"); + if (i > pos) { + write(value.substring(pos, i)); + } + write("<"); pos = i + 1; - } else if (ch == quot) { - if (i > pos) - out.write(value.substring(pos, i)); - out.write(quotEntity); + } else if (ch == '"') { + if (i > pos) { + write(value.substring(pos, i)); + } + write("""); 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(';'); + if (i > pos) { + write(value.substring(pos, i)); + } + write("&#"); + write(Integer.toString(ch)); + write(';'); pos = i + 1; } else { - if (TRACE_ESCAPING) + if (TRACE_ESCAPING) { System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + value.length() + " " + printable(value)); - + } throw new IllegalStateException( "character " + printable(ch) + " (" + Integer.toString(ch) + ") is not allowed in output" + getLocation() + " (attr value=" @@ -866,17 +932,11 @@ public class MXSerializer implements XmlSerializer { } } } - if (pos > 0) { - out.write(value.substring(pos)); - } else { - out.write(value); // this is shortcut to the most common case - } + write(pos > 0 ? value.substring(pos) : value); } - protected void writeElementContent(String text, Writer out) - throws IOException { - - // For some reason, some non-empty, empty characters are surviving this far and getting filtered out + private void writeElementContent(String text) throws IOException { + // For some reason, some non-empty, empty characters are surviving this far and getting filtered writer // So we are left with null, which causes an NPE if (text == null) { return; @@ -898,29 +958,33 @@ public class MXSerializer implements XmlSerializer { if (ch == '&') { if (!(i < text.length() - 3 && text.charAt(i+1) == 'l' && text.charAt(i+2) == 't' && text.charAt(i+3) == ';')) { - if (i > pos) - out.write(text.substring(pos, i)); - out.write("&"); + if (i > pos) { + write(text.substring(pos, i)); + } + write("&"); pos = i + 1; } } else if (ch == '<') { - if (i > pos) - out.write(text.substring(pos, i)); - out.write("<"); + if (i > pos) { + write(text.substring(pos, i)); + } + write("<"); pos = i + 1; } else if (seenBracketBracket && ch == '>') { - if (i > pos) - out.write(text.substring(pos, i)); - out.write(">"); + if (i > pos) { + write(text.substring(pos, i)); + } + 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 (TRACE_ESCAPING) + 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) + ")"); @@ -929,24 +993,17 @@ public class MXSerializer implements XmlSerializer { 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 - } - + write(pos > 0 ? text.substring(pos) : text); } - protected void writeElementContent(char[] buf, int off, int len, Writer out) - throws IOException { + private void writeElementContent(char[] buf, int off, int len) throws IOException { // escape '<', '&', ']]>' - final int end = off + len; + int end = off + len; int pos = off; for (int i = off; i < end; i++) { - final char ch = buf[i]; + char ch = buf[i]; if (ch == ']') { if (seenBracket) { seenBracketBracket = true; @@ -956,31 +1013,32 @@ public class MXSerializer implements XmlSerializer { } else { if (ch == '&') { if (i > pos) { - out.write(buf, pos, i - pos); + write(buf, pos, i - pos); } - out.write("&"); + write("&"); pos = i + 1; } else if (ch == '<') { if (i > pos) { - out.write(buf, pos, i - pos); + write(buf, pos, i - pos); } - out.write("<"); + write("<"); pos = i + 1; } else if (seenBracketBracket && ch == '>') { if (i > pos) { - out.write(buf, pos, i - pos); + write(buf, pos, i - pos); } - out.write(">"); + 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 (TRACE_ESCAPING) + 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()); @@ -992,24 +1050,24 @@ public class MXSerializer implements XmlSerializer { } } if (end > pos) { - out.write(buf, pos, end - pos); + write(buf, pos, end - pos); } } - protected static String printable(String s) { - if (s == null) { + private static String printable(String str) { + if (str == null) { return "null"; } - StringBuffer retval = new StringBuffer(s.length() + 16); + StringBuffer retval = new StringBuffer(str.length() + 16); retval.append("'"); - for (int i = 0; i < s.length(); i++) { - addPrintable(retval, s.charAt(i)); + for (int i = 0; i < str.length(); i++) { + addPrintable(retval, str.charAt(i)); } retval.append("'"); return retval.toString(); } - protected static String printable(char ch) { + private static String printable(char ch) { StringBuffer retval = new StringBuffer(); addPrintable(retval, ch); return retval.toString(); @@ -1017,37 +1075,38 @@ public class MXSerializer implements XmlSerializer { 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").append(ss.substring(ss.length() - 4)); - } else { - retval.append(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) { + String str = "0000" + Integer.toString(ch, 16); + retval.append("\\u").append(str.substring(str.length() - 4)); + } else { + retval.append(ch); + } + break; } } } diff --git a/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java b/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java new file mode 100644 index 00000000..921cf6d0 --- /dev/null +++ b/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * 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 + * + * https://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 brut.xmlpull; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; + +public class XmlPullUtils { + private static final String PROPERTY_XMLDECL_STANDALONE + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone"; + + public interface EventHandler { + boolean onEvent(XmlPullParser in, XmlSerializer out) throws XmlPullParserException; + } + + public static void copy(XmlPullParser in, XmlSerializer out) + throws XmlPullParserException, IOException { + copy(in, out, null); + } + + public static void copy(XmlPullParser in, XmlSerializer out, EventHandler handler) + throws XmlPullParserException, IOException { + Boolean standalone = (Boolean) in.getProperty(PROPERTY_XMLDECL_STANDALONE); + + // Some parsers may have already consumed the event that starts the + // document, so we manually emit that event here for consistency + if (in.getEventType() == XmlPullParser.START_DOCUMENT) { + out.startDocument(in.getInputEncoding(), standalone); + } + + while (true) { + int event = in.nextToken(); + if (event == XmlPullParser.START_DOCUMENT) { + out.startDocument(in.getInputEncoding(), standalone); + continue; + } + if (event == XmlPullParser.END_DOCUMENT) { + out.endDocument(); + break; + } + if (handler != null && handler.onEvent(in, out)) { + continue; + } + switch (event) { + case XmlPullParser.START_TAG: + if (!in.getFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES)) { + int nsStart = in.getNamespaceCount(in.getDepth() - 1); + int nsEnd = in.getNamespaceCount(in.getDepth()); + for (int i = nsStart; i < nsEnd; i++) { + String prefix = in.getNamespacePrefix(i); + String ns = in.getNamespaceUri(i); + out.setPrefix(prefix, ns); + } + } + out.startTag(normalizeNamespace(in.getNamespace()), in.getName()); + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = normalizeNamespace(in.getAttributeNamespace(i)); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + out.attribute(ns, name, value); + } + break; + case XmlPullParser.END_TAG: + out.endTag(normalizeNamespace(in.getNamespace()), in.getName()); + break; + case XmlPullParser.TEXT: + out.text(in.getText()); + break; + case XmlPullParser.CDSECT: + out.cdsect(in.getText()); + break; + case XmlPullParser.ENTITY_REF: + out.entityRef(in.getName()); + break; + case XmlPullParser.IGNORABLE_WHITESPACE: + out.ignorableWhitespace(in.getText()); + break; + case XmlPullParser.PROCESSING_INSTRUCTION: + out.processingInstruction(in.getText()); + break; + case XmlPullParser.COMMENT: + out.comment(in.getText()); + break; + case XmlPullParser.DOCDECL: + out.docdecl(in.getText()); + break; + default: + throw new IllegalStateException("Unknown event: " + event); + } + } + } + + /** + * Some parsers may return an empty string when a namespace in unsupported, + * which can confuse serializers. This method normalizes empty strings to + * be null. + */ + private static String normalizeNamespace(String namespace) { + if (namespace == null || namespace.isEmpty()) { + return null; + } else { + return namespace; + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 0ad1cf69..44d71637 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ subprojects { targetCompatibility = JavaVersion.VERSION_1_8 } - val mavenProjects = arrayOf("apktool-lib", "apktool-cli", "brut.j.common", "brut.j.util", "brut.j.dir") + val mavenProjects = arrayOf("brut.j.common", "brut.j.util", "brut.j.dir", "brut.j.xml", "apktool-lib", "apktool-cli") if (project.name in mavenProjects) { apply(plugin = "maven-publish") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ce26123..efa0bf51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ guava = "33.3.1-jre" junit = "4.13.2" r8 = "8.5.35" smali = "3.0.8" -xmlpull = "1.1.6" +xmlpull = "1.1.3.1" xmlunit = "2.10.0" [libraries] @@ -21,5 +21,5 @@ guava = { module = "com.google.guava:guava", version.ref = "guava" } junit = { module = "junit:junit", version.ref = "junit" } r8 = { module = "com.android.tools:r8", version.ref = "r8" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } -xmlpull = { module = "org.ogce:xpp3", version.ref = "xmlpull" } +xmlpull = { module = "xmlpull:xmlpull", version.ref = "xmlpull" } xmlunit = { module = "org.xmlunit:xmlunit-legacy", version.ref = "xmlunit" } diff --git a/settings.gradle.kts b/settings.gradle.kts index db82a26b..619f8094 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ rootProject.name = "apktool-cli" -include("brut.j.common", "brut.j.util", "brut.j.dir", "brut.apktool:apktool-lib", "brut.apktool:apktool-cli") +include("brut.j.common", "brut.j.util", "brut.j.dir", "brut.j.xml", "brut.apktool:apktool-lib", "brut.apktool:apktool-cli") dependencyResolutionManagement { versionCatalogs {