From c7c6863bbd844d334577034be419ce3b7c2d460e Mon Sep 17 00:00:00 2001 From: REAndroid Date: Mon, 1 May 2023 20:27:16 +0200 Subject: [PATCH] pull parser style xml values decoder #18 --- .../java/com/reandroid/apk/ApkDecoder.java | 110 +++++++ .../com/reandroid/apk/ApkJsonDecoder.java | 2 +- .../java/com/reandroid/apk/ApkModule.java | 9 +- .../reandroid/apk/ApkModuleXmlDecoder.java | 283 ++++++------------ src/main/java/com/reandroid/apk/PathMap.java | 2 +- .../java/com/reandroid/apk/PathSanitizer.java | 67 +++-- .../java/com/reandroid/apk/XmlHelper.java | 7 +- .../reandroid/apk/xmldecoder/BagDecoder.java | 11 +- .../apk/xmldecoder/BagDecoderArray.java | 96 ++++++ .../apk/xmldecoder/BagDecoderAttr.java | 94 ++++++ .../apk/xmldecoder/BagDecoderCommon.java | 77 +++++ .../apk/xmldecoder/BagDecoderPlural.java | 91 ++++++ .../apk/xmldecoder/DecoderResTableEntry.java | 38 +++ .../xmldecoder/DecoderResTableEntryMap.java | 52 ++++ .../apk/xmldecoder/DecoderTableEntry.java | 57 ++++ .../reandroid/apk/xmldecoder/EntryWriter.java | 28 ++ .../apk/xmldecoder/EntryWriterElement.java | 86 ++++++ .../apk/xmldecoder/EntryWriterSerializer.java | 61 ++++ .../xmldecoder/ResXmlDocumentSerializer.java | 2 +- .../apk/xmldecoder/XMLArrayDecoder.java | 88 ------ .../apk/xmldecoder/XMLAttrDecoder.java | 80 ----- .../apk/xmldecoder/XMLBagDecoder.java | 33 +- .../apk/xmldecoder/XMLCommonBagDecoder.java | 75 ----- .../apk/xmldecoder/XMLDecodeHelper.java | 84 ++++++ .../apk/xmldecoder/XMLEntryDecoder.java | 135 +++++++++ .../xmldecoder/XMLEntryDecoderDocument.java | 55 ++++ .../xmldecoder/XMLEntryDecoderSerializer.java | 145 +++++++++ .../apk/xmldecoder/XMLPluralsDecoder.java | 82 ----- .../reandroid/arsc/array/TypeBlockArray.java | 32 ++ .../com/reandroid/arsc/base/BlockArray.java | 88 ++++-- .../arsc/container/SpecTypePair.java | 15 +- .../com/reandroid/arsc/group/EntryGroup.java | 19 +- .../java/com/reandroid/xml/XMLElement.java | 2 +- 33 files changed, 1491 insertions(+), 615 deletions(-) create mode 100644 src/main/java/com/reandroid/apk/ApkDecoder.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/BagDecoderArray.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/BagDecoderAttr.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/BagDecoderCommon.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/BagDecoderPlural.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/DecoderTableEntry.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/EntryWriter.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/EntryWriterElement.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java delete mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLArrayDecoder.java delete mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLAttrDecoder.java delete mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLCommonBagDecoder.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java create mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java delete mode 100644 src/main/java/com/reandroid/apk/xmldecoder/XMLPluralsDecoder.java diff --git a/src/main/java/com/reandroid/apk/ApkDecoder.java b/src/main/java/com/reandroid/apk/ApkDecoder.java new file mode 100644 index 0000000..02fbc2a --- /dev/null +++ b/src/main/java/com/reandroid/apk/ApkDecoder.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.block.ApkSignatureBlock; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public abstract class ApkDecoder { + private final Set mDecodedPaths; + private APKLogger apkLogger; + private boolean mLogErrors; + + public ApkDecoder(){ + mDecodedPaths = new HashSet<>(); + } + public final void decodeTo(File outDir) throws IOException{ + reset(); + onDecodeTo(outDir); + } + abstract void onDecodeTo(File outDir) throws IOException; + + boolean containsDecodedPath(String path){ + return mDecodedPaths.contains(path); + } + void addDecodedPath(String path){ + mDecodedPaths.add(path); + } + void writePathMap(File dir, Collection sourceList) throws IOException { + PathMap pathMap = new PathMap(); + pathMap.add(sourceList); + File file = new File(dir, PathMap.JSON_FILE); + pathMap.toJson().write(file); + } + void dumpSignatures(File outDir, ApkSignatureBlock signatureBlock) throws IOException { + if(signatureBlock == null){ + return; + } + logMessage("Dumping signatures ..."); + File dir = new File(outDir, ApkUtil.SIGNATURE_DIR_NAME); + signatureBlock.writeSplitRawToDirectory(dir); + } + void logOrThrow(String message, IOException exception) throws IOException{ + if(isLogErrors()){ + logError(message, exception); + return; + } + if(message == null && exception == null){ + return; + } + if(exception == null){ + exception = new IOException(message); + } + throw exception; + } + private void reset(){ + mDecodedPaths.clear(); + } + + public boolean isLogErrors() { + return mLogErrors; + } + public void setLogErrors(boolean logErrors) { + this.mLogErrors = logErrors; + } + + public void setApkLogger(APKLogger apkLogger) { + this.apkLogger = apkLogger; + } + APKLogger getApkLogger() { + return apkLogger; + } + void logMessage(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + void logError(String msg, Throwable tr) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger == null || (msg == null && tr == null)){ + return; + } + apkLogger.logError(msg, tr); + } + void logVerbose(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/main/java/com/reandroid/apk/ApkJsonDecoder.java b/src/main/java/com/reandroid/apk/ApkJsonDecoder.java index dbc8371..399faf7 100644 --- a/src/main/java/com/reandroid/apk/ApkJsonDecoder.java +++ b/src/main/java/com/reandroid/apk/ApkJsonDecoder.java @@ -41,7 +41,7 @@ public class ApkJsonDecoder { this(apkModule, false); } public void sanitizeFilePaths(){ - PathSanitizer sanitizer = new PathSanitizer(apkModule); + PathSanitizer sanitizer = PathSanitizer.create(apkModule); sanitizer.sanitize(); } public File writeToDirectory(File dir) throws IOException { diff --git a/src/main/java/com/reandroid/apk/ApkModule.java b/src/main/java/com/reandroid/apk/ApkModule.java index ad35893..beb6283 100644 --- a/src/main/java/com/reandroid/apk/ApkModule.java +++ b/src/main/java/com/reandroid/apk/ApkModule.java @@ -120,14 +120,7 @@ public class ApkModule implements ApkFile { } return getAndroidManifestBlock().getSplit(); } - public FrameworkApk initializeAndroidFramework() throws IOException { - if(!hasTableBlock()){ - return null; - } - Integer version = getAndroidFrameworkVersion(); - return initializeAndroidFramework(getTableBlock(false), version); - } - private FrameworkApk initializeAndroidFramework(TableBlock tableBlock, Integer version) throws IOException { + public FrameworkApk initializeAndroidFramework(TableBlock tableBlock, Integer version) throws IOException { if(tableBlock == null || isAndroid(tableBlock)){ return null; } diff --git a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java index 85d006f..0ca8bc7 100644 --- a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java +++ b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java @@ -1,115 +1,97 @@ /* - * 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. - */ + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apk; -import com.reandroid.apk.xmldecoder.ResXmlDocumentSerializer; +import com.reandroid.apk.xmldecoder.*; import com.reandroid.archive.InputSource; -import com.reandroid.apk.xmldecoder.XMLBagDecoder; -import com.reandroid.apk.xmldecoder.XMLNamespaceValidator; -import com.reandroid.archive2.block.ApkSignatureBlock; import com.reandroid.arsc.chunk.PackageBlock; import com.reandroid.arsc.chunk.TableBlock; -import com.reandroid.arsc.chunk.TypeBlock; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; import com.reandroid.arsc.chunk.xml.ResXmlDocument; import com.reandroid.arsc.container.SpecTypePair; -import com.reandroid.arsc.decoder.ValueDecoder; import com.reandroid.arsc.value.*; -import com.reandroid.common.EntryStore; import com.reandroid.json.JSONObject; -import com.reandroid.xml.XMLAttribute; import com.reandroid.xml.XMLDocument; -import com.reandroid.xml.XMLElement; -import com.reandroid.xml.XMLException; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.util.*; +import java.util.function.Predicate; - public class ApkModuleXmlDecoder { +public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate { private final ApkModule apkModule; private final Map> decodedEntries; - private XMLBagDecoder xmlBagDecoder; - private final Set mDecodedPaths; + private ResXmlDocumentSerializer documentSerializer; - private boolean useAndroidSerializer; + private XMLEntryDecoderSerializer entrySerializer; + + public ApkModuleXmlDecoder(ApkModule apkModule){ - this.apkModule=apkModule; + super(); + this.apkModule = apkModule; this.decodedEntries = new HashMap<>(); - this.mDecodedPaths = new HashSet<>(); - this.useAndroidSerializer = true; - } - public void setUseAndroidSerializer(boolean useAndroidSerializer) { - this.useAndroidSerializer = useAndroidSerializer; + super.setApkLogger(apkModule.getApkLogger()); } public void sanitizeFilePaths(){ - sanitizeFilePaths(false); - } - public void sanitizeFilePaths(boolean sanitizeResourceFiles){ - PathSanitizer sanitizer = new PathSanitizer(apkModule, sanitizeResourceFiles); + PathSanitizer sanitizer = PathSanitizer.create(apkModule); sanitizer.sanitize(); } - public void decodeTo(File outDir) - throws IOException, XMLException { + @Override + void onDecodeTo(File outDir) throws IOException{ this.decodedEntries.clear(); logMessage("Decoding ..."); + + if(!apkModule.hasTableBlock()){ + logOrThrow(null, new IOException("Don't have resource table")); + return; + } + decodeUncompressedFiles(outDir); - apkModule.initializeAndroidFramework(); - TableBlock tableBlock=apkModule.getTableBlock(); - decodePackageInfo(outDir, tableBlock); + TableBlock tableBlock = apkModule.getTableBlock(); - xmlBagDecoder=new XMLBagDecoder(tableBlock); + this.entrySerializer = new XMLEntryDecoderSerializer(tableBlock); + this.entrySerializer.setDecodedEntries(this); - decodePublicXml(tableBlock, outDir); - - decodeAndroidManifest(outDir); - - addDecodedPath(TableBlock.FILE_NAME); + decodeAndroidManifest(outDir, apkModule.getAndroidManifestBlock()); + decodeTableBlock(outDir, tableBlock); logMessage("Decoding resource files ..."); - List resFileList=apkModule.listResFiles(); + List resFileList = apkModule.listResFiles(); for(ResFile resFile:resFileList){ decodeResFile(outDir, resFile); } - decodeValues(tableBlock, outDir, tableBlock); + decodeValues(outDir, tableBlock); extractRootFiles(outDir); - writePathMap(outDir); + writePathMap(outDir, apkModule.getApkArchive().listInputSources()); - dumpSignatures(outDir); + dumpSignatures(outDir, apkModule.getApkSignatureBlock()); } - private void dumpSignatures(File outDir) throws IOException { - ApkSignatureBlock signatureBlock = apkModule.getApkSignatureBlock(); - if(signatureBlock == null){ - return; + private void decodeTableBlock(File outDir, TableBlock tableBlock) throws IOException { + try{ + decodePackageInfo(outDir, tableBlock); + decodePublicXml(tableBlock, outDir); + addDecodedPath(TableBlock.FILE_NAME); + }catch (IOException exception){ + logOrThrow("Error decoding resource table", exception); } - logMessage("Dumping signatures ..."); - File dir = new File(outDir, ApkUtil.SIGNATURE_DIR_NAME); - signatureBlock.writeSplitRawToDirectory(dir); - } - private void writePathMap(File dir) throws IOException { - PathMap pathMap = new PathMap(); - pathMap.add(apkModule.getApkArchive()); - File file = new File(dir, PathMap.JSON_FILE); - pathMap.toJson().write(file); } private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException { for(PackageBlock packageBlock:tableBlock.listPackages()){ @@ -123,7 +105,7 @@ import java.util.*; jsonObject.write(packageJsonFile); } private void decodeUncompressedFiles(File outDir) - throws IOException { + throws IOException { File file=new File(outDir, UncompressedFiles.JSON_FILE); UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles(); uncompressedFiles.toJson().write(file); @@ -158,7 +140,7 @@ import java.util.*; addDecodedEntry(entry); } private void decodeResXml(File outDir, ResFile resFile) - throws IOException{ + throws IOException{ Entry entry = resFile.pickOne(); PackageBlock packageBlock = entry.getPackageBlock(); @@ -181,11 +163,8 @@ import java.util.*; } return documentSerializer; } - private TableBlock getTableBlock(){ - return apkModule.getTableBlock(); - } private void decodePublicXml(TableBlock tableBlock, File outDir) - throws IOException{ + throws IOException{ for(PackageBlock packageBlock:tableBlock.listPackages()){ decodePublicXml(packageBlock, outDir); } @@ -207,7 +186,7 @@ import java.util.*; xmlDocument.save(pubXml, false); } private void decodePublicXml(PackageBlock packageBlock, File outDir) - throws IOException { + throws IOException { String packageDirName=getPackageDirName(packageBlock); logMessage("Decoding public.xml: "+packageDirName); File file=new File(outDir, packageDirName); @@ -218,15 +197,14 @@ import java.util.*; resourceIds.loadPackageBlock(packageBlock); resourceIds.writeXml(file); } - private void decodeAndroidManifest(File outDir) - throws IOException { + private void decodeAndroidManifest(File outDir, AndroidManifestBlock manifestBlock) + throws IOException { if(!apkModule.hasAndroidManifestBlock()){ logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME); return; } File file=new File(outDir, AndroidManifestBlock.FILE_NAME); logMessage("Decoding: "+file.getName()); - AndroidManifestBlock manifestBlock=apkModule.getAndroidManifestBlock(); int currentPackageId = manifestBlock.guessCurrentPackageId(); serializeXml(currentPackageId, manifestBlock, file); addDecodedPath(AndroidManifestBlock.FILE_NAME); @@ -235,50 +213,28 @@ import java.util.*; throws IOException { XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(document); namespaceValidator.validate(); - if(useAndroidSerializer){ - ResXmlDocumentSerializer serializer = getDocumentSerializer(); - if(currentPackageId != 0){ - serializer.getDecoder().setCurrentPackageId(currentPackageId); - } - try { - serializer.write(document, outFile); - } catch (XmlPullParserException ex) { - throw new IOException("Error: "+outFile.getName(), ex); - } - }else { - try { - XMLDocument xmlDocument = document.decodeToXml(getTableBlock(), currentPackageId); - xmlDocument.save(outFile, true); - } catch (XMLException ex) { - throw new IOException("Error: "+outFile.getName(), ex); - } + ResXmlDocumentSerializer serializer = getDocumentSerializer(); + if(currentPackageId != 0){ + serializer.getDecoder().setCurrentPackageId(currentPackageId); + } + try { + serializer.write(document, outFile); + } catch (XmlPullParserException ex) { + throw new IOException("Error: "+outFile.getName(), ex); } } private void serializeXml(int currentPackageId, InputSource inputSource, File outFile) - throws IOException { - - if(useAndroidSerializer){ - ResXmlDocumentSerializer serializer = getDocumentSerializer(); - if(currentPackageId != 0){ - serializer.getDecoder().setCurrentPackageId(currentPackageId); - } - try { - serializer.write(inputSource, outFile); - } catch (XmlPullParserException ex) { - throw new IOException("Error: "+outFile.getName(), ex); - } - }else { - try { - ResXmlDocument document = apkModule.loadResXmlDocument(inputSource); - XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(document); - namespaceValidator.validate(); - XMLDocument xmlDocument = document.decodeToXml(getTableBlock(), currentPackageId); - xmlDocument.save(outFile, true); - } catch (XMLException ex) { - throw new IOException("Error: "+outFile.getName(), ex); - } - } - } + throws IOException { + ResXmlDocumentSerializer serializer = getDocumentSerializer(); + if(currentPackageId != 0){ + serializer.getDecoder().setCurrentPackageId(currentPackageId); + } + try { + serializer.write(inputSource, outFile); + } catch (XmlPullParserException ex) { + throw new IOException("Error: "+outFile.getName(), ex); + } + } private void addDecodedEntry(Entry entry){ if(entry.isNull()){ return; @@ -298,71 +254,27 @@ import java.util.*; } return resConfigSet.contains(entry.getResConfig()); } - private void decodeValues(EntryStore entryStore, File outDir, TableBlock tableBlock) throws IOException { + private void decodeValues(File outDir, TableBlock tableBlock) throws IOException { for(PackageBlock packageBlock:tableBlock.listPackages()){ - decodeValues(entryStore, outDir, packageBlock); + decodeValues(outDir, packageBlock); } } - private void decodeValues(EntryStore entryStore, File outDir, PackageBlock packageBlock) throws IOException { + private void decodeValues(File outDir, PackageBlock packageBlock) throws IOException { logMessage("Decoding values: " +packageBlock.getIndex() +"-"+packageBlock.getName()); packageBlock.sortTypes(); - for(SpecTypePair specTypePair: packageBlock.listSpecTypePairs()){ - for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){ - decodeValues(entryStore, outDir, typeBlock); - } + File pkgDir = new File(outDir, getPackageDirName(packageBlock)); + File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME); + + for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){ + decodeValues(resDir, specTypePair); } } - private void decodeValues(EntryStore entryStore, File outDir, TypeBlock typeBlock) throws IOException { - XMLDocument xmlDocument = new XMLDocument("resources"); - XMLElement docElement = xmlDocument.getDocumentElement(); - for(Entry entry :typeBlock.listEntries(true)){ - if(containsDecodedEntry(entry)){ - continue; - } - docElement.addChild(decodeValue(entryStore, entry)); - } - if(docElement.getChildesCount()==0){ - return; - } - File file=new File(outDir, getPackageDirName(typeBlock.getPackageBlock())); - file=new File(file, ApkUtil.RES_DIR_NAME); - file=new File(file, "values"+typeBlock.getQualifiers()); - String type=typeBlock.getTypeName(); - if(!type.endsWith("s")){ - type=type+"s"; - } - file=new File(file, type+".xml"); - xmlDocument.save(file, false); - } - private XMLElement decodeValue(EntryStore entryStore, Entry entry){ - XMLElement element=new XMLElement(XmlHelper.toXMLTagName(entry.getTypeName())); - int resourceId= entry.getResourceId(); - XMLAttribute attribute=new XMLAttribute("name", entry.getName()); - element.addAttribute(attribute); - attribute.setNameId(resourceId); - element.setResourceId(resourceId); - if(!entry.isComplex()){ - ResValue resValue =(ResValue) entry.getTableEntry().getValue(); - if(resValue.getValueType()== ValueType.STRING){ - XmlHelper.setTextContent(element, - resValue.getDataAsPoolString()); - }else { - String value = ValueDecoder.decodeEntryValue(entryStore, - entry.getPackageBlock(), - resValue.getValueType(), - resValue.getData()); - element.setTextContent(value); - } - }else { - ResTableMapEntry mapEntry = (ResTableMapEntry) entry.getTableEntry(); - xmlBagDecoder.decode(mapEntry, element); - return element; - } - return element; + private void decodeValues(File outDir, SpecTypePair specTypePair) throws IOException { + entrySerializer.decode(outDir, specTypePair); } private String getPackageDirName(PackageBlock packageBlock){ String name = ApkUtil.sanitizeForFileName(packageBlock.getName()); @@ -394,29 +306,8 @@ import java.util.*; inputSource.write(outputStream); outputStream.close(); } - private boolean containsDecodedPath(String path){ - return mDecodedPaths.contains(path); - } - private void addDecodedPath(String path){ - mDecodedPaths.add(path); - } - - private void logMessage(String msg) { - APKLogger apkLogger=apkModule.getApkLogger(); - if(apkLogger!=null){ - apkLogger.logMessage(msg); - } - } - private void logError(String msg, Throwable tr) { - APKLogger apkLogger=apkModule.getApkLogger(); - if(apkLogger!=null){ - apkLogger.logError(msg, tr); - } - } - private void logVerbose(String msg) { - APKLogger apkLogger=apkModule.getApkLogger(); - if(apkLogger!=null){ - apkLogger.logVerbose(msg); - } + @Override + public boolean test(Entry entry) { + return !containsDecodedEntry(entry); } } diff --git a/src/main/java/com/reandroid/apk/PathMap.java b/src/main/java/com/reandroid/apk/PathMap.java index aa2a059..b4ec728 100644 --- a/src/main/java/com/reandroid/apk/PathMap.java +++ b/src/main/java/com/reandroid/apk/PathMap.java @@ -118,7 +118,7 @@ public class PathMap implements JSONConvert { } add(archive.listInputSources()); } - public void add(Collection sources){ + public void add(Collection sources){ if(sources==null){ return; } diff --git a/src/main/java/com/reandroid/apk/PathSanitizer.java b/src/main/java/com/reandroid/apk/PathSanitizer.java index bab5b29..a682fd9 100644 --- a/src/main/java/com/reandroid/apk/PathSanitizer.java +++ b/src/main/java/com/reandroid/apk/PathSanitizer.java @@ -1,57 +1,62 @@ - /* - * 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. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apk; import com.reandroid.archive.InputSource; +import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Set; public class PathSanitizer { - private final ApkModule apkModule; + private final Collection sourceList; + private final boolean sanitizeResourceFiles; + private Collection resFileList; private APKLogger apkLogger; private final Set mSanitizedPaths; - private final boolean sanitizeResourceFiles; - public PathSanitizer(ApkModule apkModule, boolean sanitizeResourceFiles){ - this.apkModule = apkModule; - this.apkLogger = apkModule.getApkLogger(); + public PathSanitizer(Collection sourceList, boolean sanitizeResourceFiles){ + this.sourceList = sourceList; this.mSanitizedPaths = new HashSet<>(); this.sanitizeResourceFiles = sanitizeResourceFiles; } - public PathSanitizer(ApkModule apkModule){ - this(apkModule, false); + public PathSanitizer(Collection sourceList){ + this(sourceList, false); } public void sanitize(){ mSanitizedPaths.clear(); logMessage("Sanitizing paths ..."); sanitizeResFiles(); - List sourceList = apkModule.getApkArchive().listInputSources(); for(InputSource inputSource:sourceList){ sanitize(inputSource, 1, false); } logMessage("DONE = "+mSanitizedPaths.size()); } + public void setResourceFileList(Collection resFileList){ + this.resFileList = resFileList; + } private void sanitizeResFiles(){ + Collection resFileList = this.resFileList; + if(resFileList == null){ + return; + } boolean sanitizeRes = this.sanitizeResourceFiles; Set sanitizedPaths = this.mSanitizedPaths; if(sanitizeRes){ logMessage("Sanitizing resource files ..."); } - List resFileList = apkModule.listResFiles(); for(ResFile resFile:resFileList){ if(sanitizeRes){ sanitize(resFile); @@ -122,13 +127,13 @@ public class PathSanitizer { private String getLogTag(){ return "[SANITIZE]: "; } - private void logMessage(String msg){ + void logMessage(String msg){ APKLogger logger = this.apkLogger; if(logger!=null){ logger.logMessage(getLogTag()+msg); } } - private void logVerbose(String msg){ + void logVerbose(String msg){ APKLogger logger = this.apkLogger; if(logger!=null){ logger.logVerbose(getLogTag()+msg); @@ -201,6 +206,14 @@ public class PathSanitizer { || (ch >= 'a' && ch <= 'z'); } + public static PathSanitizer create(ApkModule apkModule){ + PathSanitizer pathSanitizer = new PathSanitizer( + apkModule.getApkArchive().listInputSources()); + pathSanitizer.setApkLogger(apkModule.getApkLogger()); + pathSanitizer.setResourceFileList(apkModule.listResFiles()); + return pathSanitizer; + } + private static final int MAX_NAME_LENGTH = 75; private static final int MAX_PATH_LENGTH = 100; } diff --git a/src/main/java/com/reandroid/apk/XmlHelper.java b/src/main/java/com/reandroid/apk/XmlHelper.java index c519da2..51813e7 100644 --- a/src/main/java/com/reandroid/apk/XmlHelper.java +++ b/src/main/java/com/reandroid/apk/XmlHelper.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +16,10 @@ package com.reandroid.apk; import com.reandroid.arsc.item.StringItem; -import com.reandroid.xml.XMLElement; +import com.reandroid.xml.*; public class XmlHelper { + public static void setTextContent(XMLElement element, StringItem stringItem){ if(stringItem==null){ element.clearChildNodes(); @@ -37,4 +38,6 @@ public class XmlHelper { } return typeName; } + + public static final String RESOURCES_TAG = "resources"; } diff --git a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoder.java index 1fd583b..ba59782 100644 --- a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoder.java +++ b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoder.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +19,9 @@ import com.reandroid.arsc.value.ResTableMapEntry; import com.reandroid.common.EntryStore; import com.reandroid.xml.XMLElement; -abstract class BagDecoder { - private final EntryStore entryStore; +abstract class BagDecoder extends DecoderTableEntry { public BagDecoder(EntryStore entryStore){ - this.entryStore=entryStore; + super(entryStore); } - EntryStore getEntryStore(){ - return entryStore; - } - public abstract void decode(ResTableMapEntry mapEntry, XMLElement parentElement); public abstract boolean canDecode(ResTableMapEntry mapEntry); } diff --git a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderArray.java b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderArray.java new file mode 100644 index 0000000..8da7784 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderArray.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.ApkUtil; +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.*; +import com.reandroid.common.EntryStore; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +class BagDecoderArray extends BagDecoder{ + public BagDecoderArray(EntryStore entryStore) { + super(entryStore); + } + + @Override + public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter writer) throws IOException { + Entry entry = mapEntry.getParentEntry(); + String tag = getTagName(mapEntry); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + + PackageBlock packageBlock = entry.getPackageBlock(); + ResValueMap[] resValueMaps = mapEntry.listResValueMap(); + for(int i = 0; i < resValueMaps.length; i++){ + ResValueMap valueMap = resValueMaps[i]; + String childTag = "item"; + writer.startTag(childTag); + writeText(writer, packageBlock, valueMap); + writer.endTag(childTag); + } + return writer.endTag(tag); + } + private String getTagName(ResTableMapEntry mapEntry){ + ResValueMap[] resValueMaps = mapEntry.listResValueMap(); + Set valueTypes = new HashSet<>(); + for(int i = 0; i < resValueMaps.length; i++){ + valueTypes.add(resValueMaps[i].getValueType()); + } + if(valueTypes.contains(ValueType.STRING)){ + return ApkUtil.TAG_STRING_ARRAY; + } + if(valueTypes.size() == 1 && valueTypes.contains(ValueType.INT_DEC)){ + return ApkUtil.TAG_INTEGER_ARRAY; + } + return XmlHelper.toXMLTagName(mapEntry.getParentEntry().getTypeName()); + } + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return isArrayValue(mapEntry); + } + public static boolean isArrayValue(ResTableMapEntry mapEntry){ + int parentId=mapEntry.getParentId(); + if(parentId!=0){ + return false; + } + ResValueMap[] bagItems = mapEntry.listResValueMap(); + if(bagItems==null||bagItems.length==0){ + return false; + } + int len=bagItems.length; + for(int i=0;i> 16) & 0xffff; + if(high!=0x0100 && high!=0x0200){ + return false; + } + int low = name & 0xffff; + int id = low - 1; + if(id!=i){ + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderAttr.java b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderAttr.java new file mode 100644 index 0000000..93b1070 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderAttr.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.attribute.AttributeBag; +import com.reandroid.arsc.value.attribute.AttributeBagItem; +import com.reandroid.common.EntryStore; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; + +class BagDecoderAttr extends BagDecoder{ + public BagDecoderAttr(EntryStore entryStore){ + super(entryStore); + } + + @Override + public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter writer) throws IOException { + Entry entry = mapEntry.getParentEntry(); + String tag = XmlHelper.toXMLTagName(entry.getTypeName()); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + AttributeBag attributeBag = AttributeBag.create(mapEntry.getValue()); + writeParentAttributes(writer, attributeBag); + + boolean is_flag = attributeBag.isFlag(); + String childTag = is_flag ? "flag" : "enum"; + + AttributeBagItem[] bagItems = attributeBag.getBagItems(); + + EntryStore entryStore = getEntryStore(); + + for(int i=0;i< bagItems.length;i++){ + AttributeBagItem item = bagItems[i]; + if(item.isType()){ + continue; + } + writer.startTag(childTag); + + String name = item.getNameOrHex(entryStore); + writer.attribute("name", name); + int rawVal = item.getData(); + String value; + if(is_flag){ + value = String.format("0x%08x", rawVal); + }else { + value = String.valueOf(rawVal); + } + writer.text(value); + + writer.endTag(childTag); + } + return writer.endTag(tag); + } + + private void writeParentAttributes(EntryWriter writer, AttributeBag attributeBag) throws IOException { + String formats= attributeBag.decodeValueType(); + if(formats!=null){ + writer.attribute("formats", formats); + } + AttributeBagItem item = attributeBag.getMin(); + if(item != null){ + writer.attribute("min", item.getBound().toString()); + } + item = attributeBag.getMax(); + if(item!=null){ + writer.attribute("max", item.getBound().toString()); + } + item = attributeBag.getL10N(); + if(item!=null){ + writer.attribute("l10n", item.getBound().toString()); + } + } + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return AttributeBag.isAttribute(mapEntry); + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderCommon.java b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderCommon.java new file mode 100644 index 0000000..d03f0a1 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderCommon.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.ResValueMap; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; + +class BagDecoderCommon extends BagDecoder{ + public BagDecoderCommon(EntryStore entryStore) { + super(entryStore); + } + + @Override + public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter writer) throws IOException { + Entry entry = mapEntry.getParentEntry(); + String tag = XmlHelper.toXMLTagName(entry.getTypeName()); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + + PackageBlock packageBlock = entry.getPackageBlock(); + + int parentId = mapEntry.getParentId(); + String parent; + if(parentId != 0){ + parent = ValueDecoder.decodeEntryValue(getEntryStore(), + packageBlock, ValueType.REFERENCE, parentId); + }else { + parent = null; + } + if(parent != null){ + writer.attribute("parent", parent); + } + + EntryStore entryStore = getEntryStore(); + ResValueMap[] resValueMaps = mapEntry.listResValueMap(); + for(int i = 0; i < resValueMaps.length; i++){ + ResValueMap valueMap = resValueMaps[i]; + String childTag = "item"; + writer.startTag(childTag); + + String name = ValueDecoder.decodeAttributeName( + entryStore, packageBlock, valueMap.getName()); + writer.attribute("name", name); + + writeText(writer, packageBlock, valueMap); + + writer.endTag(childTag); + } + return writer.endTag(tag); + } + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return mapEntry !=null; + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderPlural.java b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderPlural.java new file mode 100644 index 0000000..912114a --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/BagDecoderPlural.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.*; +import com.reandroid.arsc.value.plurals.PluralsQuantity; +import com.reandroid.common.EntryStore; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; + +class BagDecoderPlural extends BagDecoder{ + public BagDecoderPlural(EntryStore entryStore) { + super(entryStore); + } + + @Override + public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter writer) throws IOException { + Entry entry = mapEntry.getParentEntry(); + String tag = XmlHelper.toXMLTagName(entry.getTypeName()); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + + ResValueMap[] resValueMaps = mapEntry.listResValueMap(); + PackageBlock packageBlock = entry.getPackageBlock(); + for(int i=0; i < resValueMaps.length; i++){ + ResValueMap valueMap = resValueMaps[i]; + String childTag = "item"; + writer.startTag(childTag); + + PluralsQuantity quantity = + PluralsQuantity.valueOf((short) (valueMap.getName() & 0xffff)); + if(quantity == null){ + throw new IOException("Unknown plural quantity: " + valueMap); + } + writer.attribute("quantity", quantity.toString()); + + writeText(writer, packageBlock, valueMap); + + writer.endTag(childTag); + } + return writer.endTag(tag); + } + + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return isResBagPluralsValue(mapEntry); + } + + public static boolean isResBagPluralsValue(ResTableMapEntry valueItem){ + int parentId=valueItem.getParentId(); + if(parentId!=0){ + return false; + } + ResValueMap[] bagItems = valueItem.listResValueMap(); + if(bagItems==null||bagItems.length==0){ + return false; + } + int len=bagItems.length; + for(int i=0;i> 16) & 0xffff; + if(high!=0x0100){ + return false; + } + int low = name & 0xffff; + PluralsQuantity pq=PluralsQuantity.valueOf((short) low); + if(pq==null){ + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java b/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java new file mode 100644 index 0000000..f58980b --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableEntry; +import com.reandroid.common.EntryStore; + +import java.io.IOException; + +public class DecoderResTableEntry extends DecoderTableEntry { + public DecoderResTableEntry(EntryStore entryStore){ + super(entryStore); + } + @Override + public OUTPUT decode(ResTableEntry tableEntry, EntryWriter writer) throws IOException{ + Entry entry = tableEntry.getParentEntry(); + String tag = XmlHelper.toXMLTagName(entry.getTypeName()); + writer.startTag(tag); + writeText(writer, entry.getPackageBlock(), tableEntry.getValue()); + return writer.endTag(tag); + } + +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java b/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java new file mode 100644 index 0000000..f0e7cff --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.common.EntryStore; + +import java.io.IOException; + +class DecoderResTableEntryMap extends DecoderTableEntry { + private final Object[] decoderList; + private final BagDecoderCommon bagDecoderCommon; + + public DecoderResTableEntryMap(EntryStore entryStore) { + super(entryStore); + this.decoderList = new Object[] { + new BagDecoderAttr<>(entryStore), + new BagDecoderPlural<>(entryStore), + new BagDecoderArray<>(entryStore) + }; + + this.bagDecoderCommon = new BagDecoderCommon<>(entryStore); + } + + @Override + public OUTPUT decode(ResTableMapEntry tableEntry, EntryWriter writer) throws IOException { + return getFor(tableEntry).decode(tableEntry, writer); + } + private BagDecoder getFor(ResTableMapEntry mapEntry){ + Object[] decoderList = this.decoderList; + for(int i = 0; i < decoderList.length; i++){ + BagDecoder bagDecoder = (BagDecoder) decoderList[i]; + if(bagDecoder.canDecode(mapEntry)){ + return bagDecoder; + } + } + return bagDecoderCommon; + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/DecoderTableEntry.java b/src/main/java/com/reandroid/apk/xmldecoder/DecoderTableEntry.java new file mode 100644 index 0000000..dc5068e --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/DecoderTableEntry.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.TableEntry; +import com.reandroid.arsc.value.ValueItem; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; + +import java.io.IOException; + +abstract class DecoderTableEntry, OUTPUT> { + private final EntryStore entryStore; + public DecoderTableEntry(EntryStore entryStore){ + this.entryStore = entryStore; + } + public EntryStore getEntryStore() { + return entryStore; + } + public abstract OUTPUT decode(INPUT tableEntry, EntryWriter writer) throws IOException; + + void writeText(EntryWriter writer, PackageBlock packageBlock, ValueItem valueItem) + throws IOException { + + if(valueItem.getValueType() == ValueType.STRING){ + XMLDecodeHelper.writeTextContent(writer, valueItem.getDataAsPoolString()); + }else { + String value = ValueDecoder.decodeEntryValue( + getEntryStore(), + packageBlock, + valueItem.getValueType(), + valueItem.getData()); + + value = XMLDecodeHelper.escapeXmlChars(value); + if(value == null){ + System.err.println("\nNULL: " + valueItem); + } + + writer.text(value); + } + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/EntryWriter.java b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriter.java new file mode 100644 index 0000000..4279b5d --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriter.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import java.io.IOException; + +public interface EntryWriter{ + void setFeature(String name, Object value); + T startTag(String name) throws IOException; + T endTag(String name) throws IOException; + T attribute(String name, String value) throws IOException; + T text(String text) throws IOException; + void comment(String comment) throws IOException; + void flush() throws IOException; +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterElement.java b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterElement.java new file mode 100644 index 0000000..bbc6375 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterElement.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.xml.XMLComment; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; + +public class EntryWriterElement implements EntryWriter { + private XMLElement mCurrentElement; + private XMLElement mResult; + + public EntryWriterElement(){ + } + + public XMLElement getElement() { + return mResult; + } + @Override + public void setFeature(String name, Object value) { + } + @Override + public XMLElement startTag(String name) throws IOException { + XMLElement xmlElement = new XMLElement(name); + XMLElement current = mCurrentElement; + if(current != null){ + current.addChild(xmlElement); + }else { + mResult = null; + } + mCurrentElement = xmlElement; + return xmlElement; + } + @Override + public XMLElement endTag(String name) throws IOException { + XMLElement current = mCurrentElement; + if(current == null){ + throw new IOException("endTag called before startTag"); + } + if(!name.equals(current.getTagName())){ + throw new IOException("Mismatch endTag = " + + name + ", expect = " + current.getTagName()); + } + XMLElement parent = current.getParent(); + if(parent == null){ + mResult = current; + }else { + current = parent; + } + mCurrentElement = parent; + return current; + } + @Override + public XMLElement attribute(String name, String value) { + mCurrentElement.setAttribute(name, value); + return mCurrentElement; + } + @Override + public XMLElement text(String text) throws IOException { + mCurrentElement.setTextContent(text, false); + return mCurrentElement; + } + @Override + public void comment(String comment) throws IOException { + if(comment != null){ + mCurrentElement.addComment(new XMLComment(comment)); + } + } + @Override + public void flush() throws IOException { + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java new file mode 100644 index 0000000..982d703 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + +public class EntryWriterSerializer implements EntryWriter { + private final XmlSerializer xmlSerializer; + public EntryWriterSerializer(XmlSerializer xmlSerializer){ + this.xmlSerializer = xmlSerializer; + } + + public XmlSerializer getXmlSerializer() { + return xmlSerializer; + } + + @Override + public void setFeature(String name, Object value) { + xmlSerializer.setFeature(name, (Boolean)value); + } + @Override + public XmlSerializer startTag(String name) throws IOException { + xmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + return xmlSerializer.startTag(null, name); + } + @Override + public XmlSerializer endTag(String name) throws IOException { + return xmlSerializer.endTag(null, name); + } + @Override + public XmlSerializer attribute(String name, String value) throws IOException { + return xmlSerializer.attribute(null, name, value); + } + @Override + public XmlSerializer text(String text) throws IOException { + return xmlSerializer.text(text); + } + @Override + public void comment(String comment) throws IOException { + xmlSerializer.comment(comment); + } + @Override + public void flush() throws IOException { + xmlSerializer.flush(); + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java b/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java index 3298c12..dbc848f 100644 --- a/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java +++ b/src/main/java/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLArrayDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLArrayDecoder.java deleted file mode 100644 index 0e53313..0000000 --- a/src/main/java/com/reandroid/apk/xmldecoder/XMLArrayDecoder.java +++ /dev/null @@ -1,88 +0,0 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.reandroid.apk.xmldecoder; - -import com.reandroid.apk.ApkUtil; -import com.reandroid.apk.XmlHelper; -import com.reandroid.arsc.decoder.ValueDecoder; -import com.reandroid.arsc.value.ResTableMapEntry; -import com.reandroid.arsc.value.ResValueMap; -import com.reandroid.arsc.value.ValueType; -import com.reandroid.common.EntryStore; -import com.reandroid.xml.XMLElement; - -import java.util.HashSet; -import java.util.Set; - - class XMLArrayDecoder extends BagDecoder{ - public XMLArrayDecoder(EntryStore entryStore) { - super(entryStore); - } - - @Override - public void decode(ResTableMapEntry mapEntry, XMLElement parentElement) { - ResValueMap[] bagItems = mapEntry.listResValueMap(); - EntryStore entryStore=getEntryStore(); - Set valueTypes = new HashSet<>(); - for(int i=0;i> 16) & 0xffff; - if(high!=0x0100 && high!=0x0200){ - return false; - } - int low = name & 0xffff; - int id = low - 1; - if(id!=i){ - return false; - } - } - return true; - } -} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLAttrDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLAttrDecoder.java deleted file mode 100644 index 146a6fc..0000000 --- a/src/main/java/com/reandroid/apk/xmldecoder/XMLAttrDecoder.java +++ /dev/null @@ -1,80 +0,0 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.reandroid.apk.xmldecoder; - -import com.reandroid.arsc.value.ResTableMapEntry; -import com.reandroid.arsc.value.attribute.AttributeBag; -import com.reandroid.arsc.value.attribute.AttributeBagItem; -import com.reandroid.common.EntryStore; -import com.reandroid.xml.XMLElement; - -class XMLAttrDecoder extends BagDecoder{ - public XMLAttrDecoder(EntryStore entryStore){ - super(entryStore); - } - @Override - public void decode(ResTableMapEntry mapEntry, XMLElement parentElement){ - AttributeBag attributeBag=AttributeBag.create(mapEntry.getValue()); - decodeParentAttributes(parentElement, attributeBag); - - boolean is_flag=attributeBag.isFlag(); - String tagName=is_flag?"flag":"enum"; - - AttributeBagItem[] bagItems = attributeBag.getBagItems(); - EntryStore entryStore=getEntryStore(); - for(int i=0;i< bagItems.length;i++){ - AttributeBagItem item=bagItems[i]; - if(item.isType()){ - continue; - } - XMLElement child=new XMLElement(tagName); - String name = item.getNameOrHex(entryStore); - child.setAttribute("name", name); - int rawVal=item.getData(); - String value; - if(is_flag){ - value=String.format("0x%08x", rawVal); - }else { - value=String.valueOf(rawVal); - } - child.setTextContent(value); - parentElement.addChild(child); - } - } - @Override - public boolean canDecode(ResTableMapEntry mapEntry) { - return AttributeBag.isAttribute(mapEntry); - } - - private void decodeParentAttributes(XMLElement element, AttributeBag attributeBag){ - String formats= attributeBag.decodeValueType(); - if(formats!=null){ - element.setAttribute("formats", formats); - } - AttributeBagItem boundItem=attributeBag.getMin(); - if(boundItem!=null){ - element.setAttribute("min", boundItem.getBound().toString()); - } - boundItem=attributeBag.getMax(); - if(boundItem!=null){ - element.setAttribute("max", boundItem.getBound().toString()); - } - boundItem=attributeBag.getL10N(); - if(boundItem!=null){ - element.setAttribute("l10n", boundItem.getBound().toString()); - } - } -} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLBagDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLBagDecoder.java index c639ebf..688eaf8 100644 --- a/src/main/java/com/reandroid/apk/xmldecoder/XMLBagDecoder.java +++ b/src/main/java/com/reandroid/apk/xmldecoder/XMLBagDecoder.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,36 +15,25 @@ */ package com.reandroid.apk.xmldecoder; -import com.reandroid.arsc.array.ResValueMapArray; import com.reandroid.arsc.value.ResTableMapEntry; import com.reandroid.common.EntryStore; import com.reandroid.xml.XMLElement; -import java.util.ArrayList; -import java.util.List; +import java.io.IOException; +@Deprecated public class XMLBagDecoder { - private final EntryStore entryStore; - private final List decoderList; - private final XMLCommonBagDecoder commonBagDecoder; + private final DecoderResTableEntryMap mDocumentDecoder; + private final EntryWriterElement mWriter; public XMLBagDecoder(EntryStore entryStore){ - this.entryStore=entryStore; - this.decoderList=new ArrayList<>(); - this.decoderList.add(new XMLAttrDecoder(entryStore)); - this.decoderList.add(new XMLPluralsDecoder(entryStore)); - this.decoderList.add(new XMLArrayDecoder(entryStore)); - this.commonBagDecoder = new XMLCommonBagDecoder(entryStore); + mDocumentDecoder = new DecoderResTableEntryMap<>(entryStore); + mWriter = new EntryWriterElement(); } public void decode(ResTableMapEntry mapEntry, XMLElement parentElement){ - BagDecoder bagDecoder=getFor(mapEntry); - bagDecoder.decode(mapEntry, parentElement); - } - private BagDecoder getFor(ResTableMapEntry mapEntry){ - for(BagDecoder bagDecoder:decoderList){ - if(bagDecoder.canDecode(mapEntry)){ - return bagDecoder; - } + try { + XMLElement child = mDocumentDecoder.decode(mapEntry, mWriter); + parentElement.addChild(child); + } catch (IOException exception) { } - return commonBagDecoder; } } diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLCommonBagDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLCommonBagDecoder.java deleted file mode 100644 index ad58bb5..0000000 --- a/src/main/java/com/reandroid/apk/xmldecoder/XMLCommonBagDecoder.java +++ /dev/null @@ -1,75 +0,0 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.reandroid.apk.xmldecoder; - -import com.reandroid.apk.XmlHelper; -import com.reandroid.arsc.chunk.PackageBlock; -import com.reandroid.arsc.decoder.ValueDecoder; -import com.reandroid.arsc.value.ResTableMapEntry; -import com.reandroid.arsc.value.ResValueMap; -import com.reandroid.arsc.value.ValueType; -import com.reandroid.common.EntryStore; -import com.reandroid.xml.XMLElement; - -class XMLCommonBagDecoder extends BagDecoder{ - public XMLCommonBagDecoder(EntryStore entryStore) { - super(entryStore); - } - - @Override - public void decode(ResTableMapEntry mapEntry, XMLElement parentElement) { - - PackageBlock currentPackage= mapEntry - .getParentEntry().getPackageBlock(); - - int parentId = mapEntry.getParentId(); - String parent; - if(parentId!=0){ - parent = ValueDecoder.decodeEntryValue(getEntryStore(), - currentPackage, ValueType.REFERENCE, parentId); - }else { - parent=null; - } - if(parent!=null){ - parentElement.setAttribute("parent", parent); - } - int currentPackageId=currentPackage.getId(); - ResValueMap[] bagItems = mapEntry.listResValueMap(); - EntryStore entryStore = getEntryStore(); - for(int i=0;i< bagItems.length;i++){ - ResValueMap item=bagItems[i]; - XMLElement child=new XMLElement("item"); - String name = ValueDecoder.decodeAttributeName( - entryStore, currentPackage, item.getName()); - - child.setAttribute("name", name); - - ValueType valueType = item.getValueType(); - if(valueType == ValueType.STRING){ - XmlHelper.setTextContent(child, item.getDataAsPoolString()); - }else { - String value = ValueDecoder.decode(entryStore, currentPackageId, - item); - child.setTextContent(value); - } - parentElement.addChild(child); - } - } - @Override - public boolean canDecode(ResTableMapEntry mapEntry) { - return mapEntry !=null; - } -} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java new file mode 100644 index 0000000..ca2831d --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.arsc.item.StringItem; +import com.reandroid.xml.*; +import com.reandroid.xml.parser.XMLSpanParser; + +import java.io.IOException; + +public class XMLDecodeHelper { + + public static void writeTextContent(EntryWriter writer, StringItem stringItem) throws IOException { + if(stringItem == null){ + return; + } + if(!stringItem.hasStyle()){ + writer.text(escapeXmlChars(stringItem.get())); + }else { + String xml = stringItem.getXml(); + XMLElement element = parseSpanSafe(xml); + if(element != null){ + writeElement(writer, element); + }else { + // TODO: throw or investigate the reason + writer.text(xml); + } + } + } + public static void writeElement(EntryWriter writer, XMLElement element) throws IOException { + writer.startTag(element.getTagName()); + for(XMLAttribute xmlAttribute : element.listAttributes()){ + writer.attribute(xmlAttribute.getName(), xmlAttribute.getValue()); + } + for(XMLNode xmlNode : element.getChildNodes()){ + if(xmlNode instanceof XMLText){ + writer.text(((XMLText)xmlNode).getText(false)); + }else if(xmlNode instanceof XMLElement){ + writeElement(writer, (XMLElement) xmlNode); + } + } + writer.endTag(element.getTagName()); + } + private static XMLElement parseSpanSafe(String spanText){ + if(spanText==null){ + return null; + } + try { + XMLSpanParser spanParser = new XMLSpanParser(); + return spanParser.parse(spanText); + } catch (XMLException ignored) { + return null; + } + } + public static String escapeXmlChars(String str){ + if(str == null){ + return null; + } + if(str.indexOf('&') < 0 && str.indexOf('<') < 0 && str.indexOf('>') < 0){ + return str; + } + str=str.replaceAll("&", "&"); + str=str.replaceAll("<", "<"); + str=str.replaceAll(">", ">"); + str=str.replaceAll("&", "&"); + str=str.replaceAll("<", "<"); + str=str.replaceAll(">", ">"); + return str; + } + +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java new file mode 100644 index 0000000..ad9fc1f --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.value.*; +import com.reandroid.common.EntryStore; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Predicate; + +public class XMLEntryDecoder{ + private final Object mLock = new Object(); + private final DecoderResTableEntry decoderEntry; + private final DecoderResTableEntryMap decoderEntryMap; + private Predicate mDecodedEntries; + + public XMLEntryDecoder(EntryStore entryStore){ + this.decoderEntry = new DecoderResTableEntry<>(entryStore); + this.decoderEntryMap = new DecoderResTableEntryMap<>(entryStore); + } + + public void setDecodedEntries(Predicate decodedEntries) { + this.mDecodedEntries = decodedEntries; + } + + private boolean shouldDecode(Entry entry){ + if(entry == null || entry.isNull()){ + return false; + } + if(this.mDecodedEntries != null){ + return mDecodedEntries.test(entry); + } + return true; + } + + public OUTPUT decode(EntryWriter writer, Entry entry) throws IOException{ + if(!shouldDecode(entry)){ + return null; + } + synchronized (mLock){ + TableEntry tableEntry = entry.getTableEntry(); + if(tableEntry instanceof ResTableMapEntry){ + return decoderEntryMap.decode((ResTableMapEntry) tableEntry, writer); + } + return decoderEntry.decode((ResTableEntry) tableEntry, writer); + } + } + public int decode(EntryWriter writer, Collection entryList) throws IOException { + int count = 0; + for(Entry entry : entryList){ + OUTPUT output = decode(writer, entry); + if(output != null){ + count ++; + } + } + return count; + } + public int decode(EntryWriter writer, ResConfig resConfig, Collection entryGroupList) throws IOException { + int count = 0; + for(EntryGroup entryGroup : entryGroupList){ + OUTPUT output = decode(writer, entryGroup.getEntry(resConfig)); + if(output != null){ + count ++; + } + } + return count; + } + public int decode(EntryWriter writer, TypeBlock typeBlock) throws IOException { + Iterator iterator = typeBlock.getEntryArray() + .iterator(true); + int count = 0; + while (iterator.hasNext()){ + Entry entry = iterator.next(); + OUTPUT output = decode(writer, entry); + if(output != null){ + count++; + } + } + return count; + } + + void deleteIfZero(int decodeCount, File file){ + if(decodeCount > 0){ + return; + } + file.delete(); + File dir = file.getParentFile(); + if(isEmptyDirectory(dir)){ + dir.delete(); + } + } + private boolean isEmptyDirectory(File dir){ + if(dir == null || !dir.isDirectory()){ + return false; + } + File[] files = dir.listFiles(); + return files == null || files.length == 0; + } + File toOutXmlFile(File resDirectory, TypeBlock typeBlock){ + String path = toValuesXml(typeBlock); + return new File(resDirectory, path); + } + String toValuesXml(TypeBlock typeBlock){ + StringBuilder builder = new StringBuilder(); + char sepChar = File.separatorChar; + builder.append("values"); + builder.append(typeBlock.getQualifiers()); + builder.append(sepChar); + String type = typeBlock.getTypeName(); + builder.append(type); + if(!type.endsWith("s")){ + builder.append('s'); + } + builder.append(".xml"); + return builder.toString(); + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java new file mode 100644 index 0000000..9313dbf --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.value.Entry; +import com.reandroid.common.EntryStore; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; + +import java.io.IOException; +import java.util.Collection; + +public class XMLEntryDecoderDocument extends XMLEntryDecoder{ + private final EntryWriterElement entryWriterElement; + public XMLEntryDecoderDocument(EntryStore entryStore) { + super(entryStore); + this.entryWriterElement = new EntryWriterElement(); + } + + public XMLElement decode(Entry entry) throws IOException { + return super.decode(this.entryWriterElement, entry); + } + + public XMLDocument decode(XMLDocument xmlDocument, Collection entryList) + throws IOException { + + if(xmlDocument == null){ + xmlDocument = new XMLDocument(XmlHelper.RESOURCES_TAG); + } + XMLElement docElement = xmlDocument.getDocumentElement(); + + if(docElement == null){ + docElement = new XMLElement(XmlHelper.RESOURCES_TAG); + xmlDocument.setDocumentElement(docElement); + } + for(Entry entry : entryList){ + docElement.addChild(decode(entry)); + } + return xmlDocument; + } +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java new file mode 100644 index 0000000..1286009 --- /dev/null +++ b/src/main/java/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.android.org.kxml2.io.KXmlSerializer; +import com.reandroid.apk.XmlHelper; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.common.EntryStore; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class XMLEntryDecoderSerializer extends XMLEntryDecoder implements Closeable { + private final EntryWriterSerializer entryWriterSerializer; + private Closeable mClosable; + private boolean mStart; + + public XMLEntryDecoderSerializer(EntryStore entryStore, XmlSerializer serializer) { + super(entryStore); + this.entryWriterSerializer = new EntryWriterSerializer(serializer); + } + public XMLEntryDecoderSerializer(EntryStore entryStore) { + this(entryStore, new KXmlSerializer()); + } + + public int decode(File resDirectory, SpecTypePair specTypePair) throws IOException { + int count; + if(specTypePair.hasDuplicateResConfig(true)){ + count = decodeDuplicateConfigs(resDirectory, specTypePair); + }else { + count = decodeUniqueConfigs(resDirectory, specTypePair); + } + return count; + } + private int decodeDuplicateConfigs(File resDirectory, SpecTypePair specTypePair) throws IOException { + List resConfigList = specTypePair.listResConfig(); + Collection entryGroupList = specTypePair + .createEntryGroups(true).values(); + int total = 0; + for(ResConfig resConfig : resConfigList){ + TypeBlock typeBlock = resConfig.getParentInstance(TypeBlock.class); + File outXml = toOutXmlFile(resDirectory, typeBlock); + total += decode(outXml, resConfig, entryGroupList); + } + return total; + } + private int decodeUniqueConfigs(File resDirectory, SpecTypePair specTypePair) throws IOException { + int total = 0; + Iterator itr = specTypePair.iteratorNonEmpty(); + while (itr.hasNext()){ + TypeBlock typeBlock = itr.next(); + File outXml = toOutXmlFile(resDirectory, typeBlock); + total += decode(outXml, typeBlock); + } + return total; + } + public int decode(File outXmlFile, ResConfig resConfig, Collection entryGroupList) throws IOException { + setOutput(outXmlFile); + int count = decode(resConfig, entryGroupList); + close(); + deleteIfZero(count, outXmlFile); + return count; + } + public int decode(File outXmlFile, TypeBlock typeBlock) throws IOException { + setOutput(outXmlFile); + int count = super.decode(entryWriterSerializer, typeBlock); + close(); + deleteIfZero(count, outXmlFile); + return count; + } + public int decode(ResConfig resConfig, Collection entryGroupList) throws IOException { + return super.decode(entryWriterSerializer, resConfig, entryGroupList); + } + public void setOutput(File file) throws IOException { + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + setOutput(new FileOutputStream(file)); + } + public void setOutput(OutputStream outputStream) throws IOException { + close(); + getXmlSerializer().setOutput(outputStream, StandardCharsets.UTF_8.name()); + this.mClosable = outputStream; + start(); + } + public void setOutput(Writer writer) throws IOException { + close(); + getXmlSerializer().setOutput(writer); + this.mClosable = writer; + start(); + } + + private void start() throws IOException { + if(!mStart){ + XmlSerializer xmlSerializer = getXmlSerializer(); + xmlSerializer.startDocument("utf-8", null); + xmlSerializer.startTag(null, XmlHelper.RESOURCES_TAG); + mStart = true; + } + } + private void end() throws IOException { + if(mStart){ + XmlSerializer xmlSerializer = getXmlSerializer(); + xmlSerializer.endTag(null, XmlHelper.RESOURCES_TAG); + xmlSerializer.endDocument(); + xmlSerializer.flush(); + mStart = false; + } + } + private XmlSerializer getXmlSerializer(){ + return entryWriterSerializer.getXmlSerializer(); + } + + @Override + public void close() throws IOException { + Closeable closeable = this.mClosable; + end(); + if(closeable != null){ + closeable.close(); + } + this.mClosable = null; + } + +} diff --git a/src/main/java/com/reandroid/apk/xmldecoder/XMLPluralsDecoder.java b/src/main/java/com/reandroid/apk/xmldecoder/XMLPluralsDecoder.java deleted file mode 100644 index f6326bc..0000000 --- a/src/main/java/com/reandroid/apk/xmldecoder/XMLPluralsDecoder.java +++ /dev/null @@ -1,82 +0,0 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.reandroid.apk.xmldecoder; - -import com.reandroid.apk.XmlHelper; -import com.reandroid.arsc.decoder.ValueDecoder; -import com.reandroid.arsc.value.*; -import com.reandroid.arsc.value.plurals.PluralsQuantity; -import com.reandroid.common.EntryStore; -import com.reandroid.xml.XMLElement; - -class XMLPluralsDecoder extends BagDecoder{ - public XMLPluralsDecoder(EntryStore entryStore) { - super(entryStore); - } - @Override - public void decode(ResTableMapEntry mapEntry, XMLElement parentElement) { - ResValueMap[] bagItems = mapEntry.listResValueMap(); - int len=bagItems.length; - EntryStore entryStore=getEntryStore(); - for(int i=0;i> 16) & 0xffff; - if(high!=0x0100){ - return false; - } - int low = name & 0xffff; - PluralsQuantity pq=PluralsQuantity.valueOf((short) low); - if(pq==null){ - return false; - } - } - return true; - } -} diff --git a/src/main/java/com/reandroid/arsc/array/TypeBlockArray.java b/src/main/java/com/reandroid/arsc/array/TypeBlockArray.java index cbe12cb..3d991c8 100755 --- a/src/main/java/com/reandroid/arsc/array/TypeBlockArray.java +++ b/src/main/java/com/reandroid/arsc/array/TypeBlockArray.java @@ -32,6 +32,7 @@ import com.reandroid.json.JSONObject; import java.io.IOException; import java.util.*; +import java.util.function.Predicate; public class TypeBlockArray extends BlockArray implements JSONConvert, Comparator { @@ -236,6 +237,27 @@ public class TypeBlockArray extends BlockArray } }; } + public Iterator iteratorNonEmpty(){ + return super.iterator(NON_EMPTY_TESTER); + } + public boolean hasDuplicateResConfig(boolean ignoreEmpty){ + Set uniqueHashSet = new HashSet<>(); + Iterator itr; + if(ignoreEmpty){ + itr = iteratorNonEmpty(); + }else { + itr = iterator(true); + } + while (itr.hasNext()){ + Integer hash = itr.next() + .getResConfig().hashCode(); + if(uniqueHashSet.contains(hash)){ + return true; + } + uniqueHashSet.add(hash); + } + return false; + } private SpecBlock getSpecBlock(){ SpecTypePair parent = getParent(SpecTypePair.class); if(parent != null){ @@ -383,4 +405,14 @@ public class TypeBlockArray extends BlockArray public int compare(TypeBlock typeBlock1, TypeBlock typeBlock2) { return typeBlock1.compareTo(typeBlock2); } + + private static final Predicate NON_EMPTY_TESTER = new Predicate() { + @Override + public boolean test(TypeBlock typeBlock) { + if(typeBlock == null || typeBlock.isNull()){ + return false; + } + return !typeBlock.isEmpty(); + } + }; } diff --git a/src/main/java/com/reandroid/arsc/base/BlockArray.java b/src/main/java/com/reandroid/arsc/base/BlockArray.java index e079421..b050be7 100755 --- a/src/main/java/com/reandroid/arsc/base/BlockArray.java +++ b/src/main/java/com/reandroid/arsc/base/BlockArray.java @@ -1,21 +1,22 @@ - /* - * 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. - */ +/* + * 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.arsc.base; import java.util.*; +import java.util.function.Predicate; public abstract class BlockArray extends BlockContainer implements BlockArrayCreator { @@ -273,6 +274,9 @@ public abstract class BlockArray extends BlockContainer impl public Iterator iterator(boolean skipNullBlock) { return new BlockIterator(skipNullBlock); } + public Iterator iterator(Predicate tester) { + return new PredicateIterator(tester); + } public boolean contains(Object block){ T[] items=elementData; if(block==null || items==null){ @@ -437,14 +441,62 @@ public abstract class BlockArray extends BlockContainer impl if(!mSkipNullBlock || isFinished()){ return; } - T item=BlockArray.this.get(mCursor); - while (item==null||item.isNull()){ + T item = BlockArray.this.get(mCursor); + while (item == null || item.isNull()){ mCursor++; - item=BlockArray.this.get(mCursor); + item = BlockArray.this.get(mCursor); if(mCursor>=mMaxSize){ break; } } } } + + + private class PredicateIterator implements Iterator { + private int mCursor; + private final int mMaxSize; + private final Predicate mTester; + PredicateIterator(Predicate tester){ + this.mTester = tester; + mCursor = 0; + mMaxSize = BlockArray.this.childesCount(); + } + @Override + public boolean hasNext() { + checkCursor(); + return hasItems(); + } + @Override + public T next() { + if(hasItems()){ + T item=BlockArray.this.get(mCursor); + mCursor++; + checkCursor(); + return item; + } + return null; + } + private boolean hasItems(){ + return mCursor < mMaxSize; + } + private void checkCursor(){ + if(mTester == null){ + return; + } + while (hasItems() && !test(BlockArray.this.get(getCursor()))){ + mCursor++; + } + } + private int getCursor(){ + return mCursor; + } + private boolean test(T item){ + Predicate tester = mTester; + if(tester != null){ + return tester.test(item); + } + return true; + } + } } diff --git a/src/main/java/com/reandroid/arsc/container/SpecTypePair.java b/src/main/java/com/reandroid/arsc/container/SpecTypePair.java index bc24bab..8d37035 100755 --- a/src/main/java/com/reandroid/arsc/container/SpecTypePair.java +++ b/src/main/java/com/reandroid/arsc/container/SpecTypePair.java @@ -74,11 +74,11 @@ public class SpecTypePair extends BlockContainer return createEntryGroups(false); } public Map createEntryGroups(boolean skipNullEntries){ - Map map = new HashMap<>(); - for(TypeBlock typeBlock:listTypeBlocks()){ + Map map = new LinkedHashMap<>(); + for(TypeBlock typeBlock : listTypeBlocks()){ EntryArray entryArray = typeBlock.getEntryArray(); - for(Entry entry:entryArray.listItems(skipNullEntries)){ - if(entry==null){ + for(Entry entry : entryArray.listItems(skipNullEntries)){ + if(entry == null){ continue; } int id = entry.getResourceId(); @@ -177,6 +177,13 @@ public class SpecTypePair extends BlockContainer return mTypeBlockArray.listResConfig(); } + public Iterator iteratorNonEmpty(){ + return mTypeBlockArray.iteratorNonEmpty(); + } + public boolean hasDuplicateResConfig(boolean ignoreEmpty){ + return mTypeBlockArray.hasDuplicateResConfig(ignoreEmpty); + } + public byte getTypeId(){ return mSpecBlock.getTypeId(); } diff --git a/src/main/java/com/reandroid/arsc/group/EntryGroup.java b/src/main/java/com/reandroid/arsc/group/EntryGroup.java index 872fc95..97c1f15 100755 --- a/src/main/java/com/reandroid/arsc/group/EntryGroup.java +++ b/src/main/java/com/reandroid/arsc/group/EntryGroup.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,6 +32,23 @@ public class EntryGroup extends ItemGroup { super(ARRAY_CREATOR, String.valueOf(resId)); this.resourceId=resId; } + public Entry getEntry(ResConfig resConfig){ + Entry[] items = getItems(); + if(items == null || resConfig == null){ + return null; + } + int length = items.length; + for(int i=0; i