diff --git a/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java b/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java index c42d5ecb..bb17c19c 100644 --- a/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java +++ b/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java @@ -16,15 +16,15 @@ package com.reandroid.apk; import com.reandroid.apk.xmlencoder.EncodeMaterials; -import com.reandroid.apk.xmlencoder.XMLEncodeSource; +import com.reandroid.apk.xmlencoder.XMLParseEncodeSource; import com.reandroid.apk.xmlencoder.XMLTableBlockEncoder; import com.reandroid.archive.FileInputSource; import com.reandroid.arsc.chunk.PackageBlock; import com.reandroid.arsc.chunk.TableBlock; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; import com.reandroid.arsc.value.Entry; -import com.reandroid.xml.source.XMLFileSource; -import com.reandroid.xml.source.XMLSource; +import com.reandroid.xml.source.XMLFileParserSource; +import com.reandroid.xml.source.XMLParserSource; import java.io.File; import java.io.IOException; @@ -83,10 +83,11 @@ private void encodeManifestXml(File mainDirectory) { encodeMaterials.setCurrentPackage(packageBlock); tableBlock.setCurrentPackage(packageBlock); } - XMLSource xmlSource = - new XMLFileSource(AndroidManifestBlock.FILE_NAME, file); - XMLEncodeSource xmlEncodeSource = - new XMLEncodeSource(encodeMaterials, xmlSource); + XMLParserSource xmlSource = + new XMLFileParserSource(AndroidManifestBlock.FILE_NAME, file); + XMLParseEncodeSource xmlEncodeSource = + new XMLParseEncodeSource(tableBlock.pickOne(), xmlSource); + xmlEncodeSource.setApkLogger(getApkLogger()); getApkModule().add(xmlEncodeSource); } private void scanResFilesDirectory(File mainDirectory) { @@ -104,13 +105,16 @@ private void encodeResFile(File resFilesDirectory, File file){ String path = ApkUtil.toArchivePath(resFilesDirectory, file); logVerbose(path); Entry entry = getEntry(path); - EncodeMaterials encodeMaterials = getEncodeMaterials(); + if(entry == null){ + logMessage("Un registered file: " + file); + return; + } if(file.getName().endsWith(".xml")){ - XMLSource xmlSource = - new XMLFileSource(path, file); - XMLEncodeSource xmlEncodeSource = - new XMLEncodeSource(encodeMaterials, xmlSource); - xmlEncodeSource.setEntry(entry); + XMLParserSource xmlSource = + new XMLFileParserSource(path, file); + XMLParseEncodeSource xmlEncodeSource = + new XMLParseEncodeSource(entry.getPackageBlock(), xmlSource); + xmlEncodeSource.setApkLogger(getApkLogger()); getApkModule().add(xmlEncodeSource); }else { FileInputSource inputSource = new FileInputSource(file, path); diff --git a/src/main/java/com/reandroid/apk/xmlencoder/FilePathEncoder.java b/src/main/java/com/reandroid/apk/xmlencoder/FilePathEncoder.java index 6f281315..83dee6dc 100644 --- a/src/main/java/com/reandroid/apk/xmlencoder/FilePathEncoder.java +++ b/src/main/java/com/reandroid/apk/xmlencoder/FilePathEncoder.java @@ -15,6 +15,7 @@ */ package com.reandroid.apk.xmlencoder; +import com.reandroid.apk.APKLogger; import com.reandroid.archive.APKArchive; import com.reandroid.archive.FileInputSource; import com.reandroid.archive.InputSource; @@ -22,8 +23,8 @@ import com.reandroid.apk.UncompressedFiles; import com.reandroid.arsc.model.ResourceEntry; import com.reandroid.arsc.value.Entry; -import com.reandroid.xml.source.XMLFileSource; -import com.reandroid.xml.source.XMLSource; +import com.reandroid.xml.source.XMLFileParserSource; +import com.reandroid.xml.source.XMLParserSource; import java.io.File; import java.util.List; @@ -32,6 +33,7 @@ public class FilePathEncoder { private final EncodeMaterials materials; private APKArchive apkArchive; private UncompressedFiles uncompressedFiles; + private APKLogger mLogger; public FilePathEncoder(EncodeMaterials encodeMaterials){ this.materials =encodeMaterials; } @@ -92,8 +94,10 @@ private InputSource createRawFileInputSource(String path, File resFile){ return new FileInputSource(resFile, path); } private InputSource createXMLEncodeInputSource(String path, File resFile){ - XMLSource xmlSource = new XMLFileSource(path, resFile); - return new XMLEncodeSource(materials, xmlSource); + XMLParserSource xmlSource = new XMLFileParserSource(path, resFile); + XMLParseEncodeSource encodeSource = new XMLParseEncodeSource(materials.getCurrentPackage(), xmlSource); + encodeSource.setApkLogger(mLogger); + return encodeSource; } private boolean isXmlFile(File resFile){ String name=resFile.getName(); @@ -113,4 +117,7 @@ private void addUncompressedFiles(String path){ uncompressedFiles.addPath(path); } } + public void setApkLogger(APKLogger logger){ + this.mLogger = logger; + } } diff --git a/src/main/java/com/reandroid/apk/xmlencoder/XMLParseEncodeSource.java b/src/main/java/com/reandroid/apk/xmlencoder/XMLParseEncodeSource.java new file mode 100644 index 00000000..40d0f99d --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmlencoder/XMLParseEncodeSource.java @@ -0,0 +1,93 @@ +package com.reandroid.apk.xmlencoder; + +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.CrcOutputStream; +import com.reandroid.archive.ByteInputSource; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.chunk.xml.ResXmlPullSerializer; +import com.reandroid.arsc.util.IOUtil; +import com.reandroid.xml.XmlParserToSerializer; +import com.reandroid.xml.source.XMLParserSource; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.OutputStream; + +public class XMLParseEncodeSource extends ByteInputSource { + private final PackageBlock packageBlock; + private final XMLParserSource parserSource; + private ResXmlDocument mResXmlDocument; + private APKLogger mLogger; + + public XMLParseEncodeSource(PackageBlock packageBlock, XMLParserSource parserSource) { + super(new byte[0], parserSource.getPath()); + this.packageBlock = packageBlock; + this.parserSource = parserSource; + } + @Override + public long getLength() throws IOException { + return getResXmlDocument().countBytes(); + } + @Override + public long getCrc() throws IOException{ + ResXmlDocument resXmlDocument = getResXmlDocument(); + CrcOutputStream outputStream=new CrcOutputStream(); + resXmlDocument.writeBytes(outputStream); + return outputStream.getCrcValue(); + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getResXmlDocument().writeBytes(outputStream); + } + @Override + public byte[] getBytes() { + try { + return getResXmlDocument().getBytes(); + } catch (IOException ignored) { + } + //should not reach here + return new byte[0]; + } + @Override + public void disposeInputSource(){ + mResXmlDocument = null; + } + + public PackageBlock getPackageBlock() { + return packageBlock; + } + public XMLParserSource getParserSource() { + return parserSource; + } + public ResXmlDocument getResXmlDocument() throws IOException{ + if(mResXmlDocument == null){ + try { + mResXmlDocument = encode(); + } catch (XmlPullParserException ex) { + throw new IOException(ex.getMessage()); + } + } + return mResXmlDocument; + } + private ResXmlDocument encode() throws XmlPullParserException, IOException { + logVerbose("Encoding: " + getParserSource().getPath()); + XmlPullParser parser = getParserSource().getParser(); + ResXmlPullSerializer serializer = new ResXmlPullSerializer(); + serializer.setCurrentPackage(getPackageBlock()); + XmlParserToSerializer parserToSerializer = new XmlParserToSerializer(parser, serializer); + parserToSerializer.write(); + IOUtil.close(parser); + return serializer.getResultDocument(); + } + public void setApkLogger(APKLogger logger){ + this.mLogger = logger; + } + private void logVerbose(String msg){ + APKLogger logger = this.mLogger; + if(logger != null){ + logger.logVerbose(msg); + } + } +} diff --git a/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java b/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java index 6bcadcf5..786695cf 100644 --- a/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java +++ b/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java @@ -176,6 +176,7 @@ private void encodeValues(List pubXmlFileList) throws XMLException { File resDir = toResDirectory(pubXmlFile); encodeResDir(resDir); FilePathEncoder filePathEncoder = new FilePathEncoder(encodeMaterials); + filePathEncoder.setApkLogger(getApkLogger()); filePathEncoder.setApkArchive(getApkModule().getApkArchive()); filePathEncoder.setUncompressedFiles(getApkModule().getUncompressedFiles()); filePathEncoder.encodePackageResDir(resDir); diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullSerializer.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullSerializer.java index 8eee0165..76c324fe 100644 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullSerializer.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullSerializer.java @@ -17,14 +17,10 @@ import com.reandroid.arsc.chunk.PackageBlock; import com.reandroid.arsc.chunk.TableBlock; -import com.reandroid.arsc.coder.EncodeResult; -import com.reandroid.arsc.coder.ReferenceString; -import com.reandroid.arsc.coder.ValueCoder; -import com.reandroid.arsc.coder.XmlSanitizer; +import com.reandroid.arsc.coder.*; import com.reandroid.arsc.model.ResourceEntry; import com.reandroid.arsc.model.ResourceLibrary; -import com.reandroid.arsc.value.AttributeDataFormat; -import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.*; import com.reandroid.arsc.value.attribute.AttributeBag; import org.xmlpull.v1.XmlSerializer; @@ -37,9 +33,22 @@ public class ResXmlPullSerializer implements XmlSerializer { private ResXmlDocument mDocument; private ResXmlElement mCurrentElement; private boolean mEndDocument; + private StringBuilder mCurrentText; + private boolean mValidateValues; public ResXmlPullSerializer(){ } + + public ResXmlDocument getResultDocument() { + return mDocument; + } + + public void setValidateValues(boolean validateValues) { + this.mValidateValues = validateValues; + } + public boolean isValidateValues() { + return mValidateValues; + } public PackageBlock getCurrentPackage(){ return mCurrentPackage; } @@ -49,10 +58,6 @@ public void setCurrentPackage(PackageBlock packageBlock){ mDocument.setPackageBlock(packageBlock); } } - public ResXmlDocument getDocument() { - return mDocument; - } - public void setDocument(ResXmlDocument document) { this.mDocument = document; if(document == null){ @@ -200,13 +205,19 @@ public String getName() { } @Override - public XmlSerializer startTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { + public ResXmlPullSerializer startTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { + flushText(); ResXmlElement element = getCurrentElement(); String prefix = null; int i = name.indexOf(':'); if(i > 0){ prefix = name.substring(0, i); name = name.substring(i + 1); + }else { + ResXmlNamespace xmlNamespace = element.getNamespaceByUri(namespace); + if(xmlNamespace != null){ + prefix = xmlNamespace.getPrefix(); + } } if(element.getTag() == null){ element.setTag(name); @@ -220,7 +231,7 @@ public XmlSerializer startTag(String namespace, String name) throws IOException, } @Override - public XmlSerializer attribute(String namespace, String name, String value) throws IOException, IllegalArgumentException, IllegalStateException { + public ResXmlPullSerializer attribute(String namespace, String name, String value) throws IOException, IllegalArgumentException, IllegalStateException { ResXmlElement element = mCurrentElement; String prefix = null; int i = name.indexOf(':'); @@ -240,21 +251,21 @@ public XmlSerializer attribute(String namespace, String name, String value) thro } @Override - public XmlSerializer endTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { + public ResXmlPullSerializer endTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { + flushText(); mCurrentElement.calculatePositions(); mCurrentElement = mCurrentElement.getParentResXmlElement(); return this; } @Override - public XmlSerializer text(String text) throws IOException, IllegalArgumentException, IllegalStateException { - ResXmlElement element = mCurrentElement; - element.addResXmlText(text); + public ResXmlPullSerializer text(String text) throws IOException, IllegalArgumentException, IllegalStateException { + appendText(text); return this; } @Override - public XmlSerializer text(char[] buf, int start, int len) throws IOException, IllegalArgumentException, IllegalStateException { + public ResXmlPullSerializer text(char[] buf, int start, int len) throws IOException, IllegalArgumentException, IllegalStateException { return text(new String(buf, start, len)); } @@ -265,7 +276,17 @@ public void cdsect(String text) throws IOException, IllegalArgumentException, Il @Override public void entityRef(String text) throws IOException, IllegalArgumentException, IllegalStateException { - + String decoded; + if("lt".equals(text)){ + decoded = "<"; + }else if("gt".equals(text)){ + decoded = ">"; + }else if("amp".equals(text)){ + decoded = "&"; + }else { + decoded = text; + } + appendText(decoded); } @Override @@ -275,7 +296,8 @@ public void processingInstruction(String text) throws IOException, IllegalArgume @Override public void comment(String text) throws IOException, IllegalArgumentException, IllegalStateException { - + ResXmlElement current = getCurrentElement(); + current.setComment(text); } @Override @@ -293,54 +315,99 @@ public void flush() throws IOException { } - private void encode(ResXmlAttribute attribute, String uri, String prefix, String name, String value){ + private void flushText(){ + if(mCurrentText == null){ + return; + } + String text = mCurrentText.toString(); + mCurrentText = null; + if(isIndent(text)){ + return; + } + ResXmlElement element = getCurrentElement(); + element.addResXmlText(text); + } + private void appendText(String text){ + if(text == null){ + return; + } + StringBuilder builder = mCurrentText; + if(builder == null){ + builder = new StringBuilder(); + mCurrentText = builder; + } + builder.append(text); + } + private void encode(ResXmlAttribute attribute, String uri, String prefix, String name, String value) throws IOException { attribute.setNamespace(uri, prefix); - Entry attrEntry = null; - if(prefix != null){ - ResourceEntry resourceEntry = getAttributeName(prefix, name); - if(resourceEntry == null){ - throw new ResourceEncodeException("Unknown attribute '" + prefix + ":" + name); + ResourceEntry attrResource = null; + Entry attrEntry; + EncodeResult encodeResult = ValueCoder.encodeUnknownResourceId(name); + if(encodeResult != null){ + attribute.setName(name, encodeResult.value); + }else if(prefix != null){ + attrResource = getTableBlock().getAttrResource(prefix, name); + if(attrResource == null){ + throw new IOException("Unknown attribute name '" + prefix + ":" + name + "'"); } - attribute.setName(resourceEntry.getName(), resourceEntry.getResourceId()); + attribute.setName(attrResource.getName(), attrResource.getResourceId()); attribute.setNamespace(uri, prefix); - attrEntry = resourceEntry.get(); }else { attribute.setName(name, 0); } - EncodeResult encodeResult = encodeReference(value); + encodeResult = encodeReference(value); if(encodeResult != null){ - attribute.setTypeAndData(encodeResult.valueType, encodeResult.value); + attribute.setValue(encodeResult); return; } - if(attrEntry != null){ - AttributeBag attributeBag = AttributeBag.create(attrEntry.getResValueMapArray()); + AttributeDataFormat[] formats = null; + if(attrResource != null){ + attrResource = attrResource.resolveReference(); + attrEntry = attrResource.get(); + AttributeBag attributeBag = AttributeBag.create(attrEntry); if(attributeBag != null){ - encodeResult = attributeBag.encodeEnumOrFlagValue(value); - if(encodeResult!=null){ - attribute.setTypeAndData(encodeResult.valueType, encodeResult.value); - return; + formats = attributeBag.getFormats(); + if(attributeBag.isEnumOrFlag()){ + encodeResult = attributeBag.encodeEnumOrFlagValue(value); + if(encodeResult == null){ + // Could be decoded as hex or integer + encodeResult = ValueCoder.encode(value, CommonType.INTEGER.valueTypes()); + } + if(encodeResult == null && formats != null){ + encodeResult = ValueCoder.encode(value, formats); + } + if(encodeResult == null){ + if(isValidateValues()){ + String msg = "Invalid attribute enum/flag/value '" + name + "=\"" + value + "\"'"; + throw new IOException(msg); + } + encodeResult = ValueCoder.encode(value); + } + if(encodeResult != null){ + attribute.setValue(encodeResult); + return; + } } } } - if(attrEntry != null){ - AttributeDataFormat[] formats = attrEntry.getResTableMapEntry().getAttributeTypeFormats(); + boolean allowString = attrResource == null; + if(formats != null){ encodeResult = ValueCoder.encode(value, formats); + if(encodeResult == null){ + allowString = AttributeDataFormat.contains(formats, ValueType.STRING); + } } if(encodeResult != null){ - attribute.setTypeAndData(encodeResult.valueType, encodeResult.value); + attribute.setValue(encodeResult); return; } - attribute.setValueAsString(XmlSanitizer.unEscapeUnQuote(value)); - } - private ResourceEntry getAttributeName(String prefix, String name){ - TableBlock tableBlock = getTableBlock(); - ResourceEntry resourceEntry = tableBlock.getResource(prefix, "attr", name); - if(resourceEntry == null){ - resourceEntry = tableBlock.getResource(null, "attr", name); + if(!allowString && isValidateValues()){ + throw new IOException("Invalid attribute value " + + name + "=\"" + value + "\""); } - return resourceEntry; + attribute.setValueAsString(XmlSanitizer.unEscapeUnQuote(value)); } - private EncodeResult encodeReference(String text){ + private EncodeResult encodeReference(String text) throws IOException { EncodeResult encodeResult = ValueCoder.encodeUnknownResourceId(text); if(encodeResult != null){ return encodeResult; @@ -351,7 +418,7 @@ private EncodeResult encodeReference(String text){ } encodeResult = referenceString.encode(getTableBlock()); if(encodeResult == null){ - throw new ResourceEncodeException("Unknown reference: " + text); + throw new IOException("Unknown reference: " + text); } return encodeResult; } @@ -359,10 +426,19 @@ private TableBlock getTableBlock(){ PackageBlock packageBlock = getCurrentPackage(); return packageBlock.getTableBlock(); } - - public static class ResourceEncodeException extends IllegalArgumentException{ - public ResourceEncodeException(String msg){ - super(msg); + private static boolean isIndent(String text){ + if(text.length() == 0){ + return true; + } + char[] chars = text.toCharArray(); + if(chars[0] != '\n'){ + return false; + } + for(int i = 1; i < chars.length; i++){ + if(chars[i] != ' '){ + return false; + } } + return true; } } diff --git a/src/main/java/com/reandroid/xml/source/XMLFileParserSource.java b/src/main/java/com/reandroid/xml/source/XMLFileParserSource.java new file mode 100644 index 00000000..61058d5f --- /dev/null +++ b/src/main/java/com/reandroid/xml/source/XMLFileParserSource.java @@ -0,0 +1,44 @@ +/* + * 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.source; + +import com.reandroid.xml.XMLParserFactory; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; + +public class XMLFileParserSource implements XMLParserSource{ + private final String path; + private final File file; + + public XMLFileParserSource(String path, File file){ + this.path = path; + this.file = file; + } + + @Override + public XmlPullParser getParser() throws XmlPullParserException { + return XMLParserFactory.newPullParser(getFile()); + } + @Override + public String getPath() { + return path; + } + public File getFile() { + return file; + } +} diff --git a/src/main/java/com/reandroid/xml/source/XMLParserSource.java b/src/main/java/com/reandroid/xml/source/XMLParserSource.java new file mode 100644 index 00000000..786a41b4 --- /dev/null +++ b/src/main/java/com/reandroid/xml/source/XMLParserSource.java @@ -0,0 +1,25 @@ +/* + * 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.source; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + + +public interface XMLParserSource { + XmlPullParser getParser() throws XmlPullParserException; + String getPath(); +}