diff --git a/doc/Architecture.md b/doc/Architecture.md index 586508bd..603ecf95 100644 --- a/doc/Architecture.md +++ b/doc/Architecture.md @@ -1,5 +1,5 @@ #### directory structure -`src/arsc_parser/` - Java .arsc parser I found somewhere, with fixes (should eventually get replaced by C code) +`src/ARSCLib/` - Java .arsc library used to parse binary xml resources in apks `doc/` - documentation `src/api-impl/` - Java code implementing the android APIs `src/api-impl-jni/` - C code implementing things which it doesn't make sense to do in Java (ideally this would be most things) diff --git a/meson.build b/meson.build index 9421efc8..f787d288 100644 --- a/meson.build +++ b/meson.build @@ -104,10 +104,10 @@ executable('android-translation-layer', [ '-rdynamic' ]) -# hax_arsc_parser.dex (named as classes2.dex so it works inside a jar) -subdir('src/arsc_parser') -hax_arsc_parser_dex = custom_target('hax_arsc_parser.dex', build_by_default: true, input: [hax_arsc_parser_jar], output: ['classes2.dex'], - command: ['dx', '--verbose', '--dex', '--output='+join_paths(builddir_base, 'classes2.dex'), hax_arsc_parser_jar.full_path()]) +# hax_arsc_lib.dex (named as classes2.dex so it works inside a jar) +subdir('src/ARSCLib') +hax_arsc_lib_dex = custom_target('hax_arsc_lib.dex', build_by_default: true, input: [hax_arsc_lib_jar], output: ['classes2.dex'], + command: ['dx', '--verbose', '--dex', '--min-sdk-version', '26', '--output='+join_paths(builddir_base, 'classes2.dex'), hax_arsc_lib_jar.full_path()]) # hax.dex (named as classes.dex so it works inside a jar) subdir('src/api-impl') @@ -115,9 +115,9 @@ hax_dex = custom_target('hax.dex', build_by_default: true, input: [hax_jar], out command: ['dx', '--verbose', '--dex', '--output='+join_paths(builddir_base, 'classes.dex'), hax_jar.full_path()]) # api-impl.jar -custom_target('api-impl.jar', build_by_default: true, input: [hax_dex, hax_arsc_parser_dex], output: ['api-impl.jar'], +custom_target('api-impl.jar', build_by_default: true, input: [hax_dex, hax_arsc_lib_dex], output: ['api-impl.jar'], install: true, install_dir : get_option('libdir') / 'java/dex/android_translation_layer', - command: ['jar', '-cvf', join_paths(builddir_base, 'api-impl.jar'), '-C', builddir_base, hax_dex, '-C', builddir_base, hax_arsc_parser_dex]) + command: ['jar', '-cvf', join_paths(builddir_base, 'api-impl.jar'), '-C', builddir_base, hax_dex, '-C', builddir_base, hax_arsc_lib_dex]) diff --git a/src/ARSCLib/LICENSE b/src/ARSCLib/LICENSE new file mode 100644 index 00000000..a84d2702 --- /dev/null +++ b/src/ARSCLib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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. diff --git a/src/ARSCLib/README.md b/src/ARSCLib/README.md new file mode 100644 index 00000000..cf06edc4 --- /dev/null +++ b/src/ARSCLib/README.md @@ -0,0 +1,6 @@ +taken from https://github.com/REAndroid/ARSCLib +licensed under Apache 2.0 + +# ARSCLib +A tool for decoding Android resources.arsc file using java, for decoding binary XML resources from apk files. + diff --git a/src/ARSCLib/android/content/res/XmlResourceParser.java b/src/ARSCLib/android/content/res/XmlResourceParser.java new file mode 100644 index 00000000..55ac439a --- /dev/null +++ b/src/ARSCLib/android/content/res/XmlResourceParser.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * 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 android.content.res; + +import android.util.AttributeSet; +import org.xmlpull.v1.XmlPullParser; + +public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { + String getAttributeNamespace (int index); + public void close(); +} + diff --git a/src/ARSCLib/android/util/AttributeSet.java b/src/ARSCLib/android/util/AttributeSet.java new file mode 100644 index 00000000..7c73df73 --- /dev/null +++ b/src/ARSCLib/android/util/AttributeSet.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * 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 android.util; + +public interface AttributeSet { + public int getAttributeCount(); + default String getAttributeNamespace (int index) { + return null; + } + public String getAttributeName(int index); + public String getAttributeValue(int index); + public String getAttributeValue(String namespace, String name); + public String getPositionDescription(); + public int getAttributeNameResource(int index); + public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue); + public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue); + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue); + public int getAttributeIntValue(String namespace, String attribute, int defaultValue); + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue); + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue); + public int getAttributeListValue(int index, String[] options, int defaultValue); + public boolean getAttributeBooleanValue(int index, boolean defaultValue); + public int getAttributeResourceValue(int index, int defaultValue); + public int getAttributeIntValue(int index, int defaultValue); + public int getAttributeUnsignedIntValue(int index, int defaultValue); + public float getAttributeFloatValue(int index, float defaultValue); + public String getIdAttribute(); + public String getClassAttribute(); + public int getIdAttributeResourceValue(int defaultValue); + public int getStyleAttribute(); +} diff --git a/src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java b/src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java new file mode 100644 index 00000000..c8d6d137 --- /dev/null +++ b/src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java @@ -0,0 +1,2137 @@ +/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. */ + +// Contributors: Paul Hackenberger (unterminated entity handling in relaxed mode) + +package com.android.org.kxml2.io; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + + +public class KXmlParser implements XmlPullParser, Closeable { + + private static final String PROPERTY_XMLDECL_VERSION + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-version"; + private static final String PROPERTY_XMLDECL_STANDALONE + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone"; + private static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; + private static final String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed"; + + private static final Map DEFAULT_ENTITIES = new HashMap(); + static { + DEFAULT_ENTITIES.put("lt", "<"); + DEFAULT_ENTITIES.put("gt", ">"); + DEFAULT_ENTITIES.put("amp", "&"); + DEFAULT_ENTITIES.put("apos", "'"); + DEFAULT_ENTITIES.put("quot", "\""); + } + + private static final int ELEMENTDECL = 11; + private static final int ENTITYDECL = 12; + private static final int ATTLISTDECL = 13; + private static final int NOTATIONDECL = 14; + private static final int PARAMETER_ENTITY_REF = 15; + private static final char[] START_COMMENT = { '<', '!', '-', '-' }; + private static final char[] END_COMMENT = { '-', '-', '>' }; + private static final char[] COMMENT_DOUBLE_DASH = { '-', '-' }; + private static final char[] START_CDATA = { '<', '!', '[', 'C', 'D', 'A', 'T', 'A', '[' }; + private static final char[] END_CDATA = { ']', ']', '>' }; + private static final char[] START_PROCESSING_INSTRUCTION = { '<', '?' }; + private static final char[] END_PROCESSING_INSTRUCTION = { '?', '>' }; + private static final char[] START_DOCTYPE = { '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E' }; + private static final char[] SYSTEM = { 'S', 'Y', 'S', 'T', 'E', 'M' }; + private static final char[] PUBLIC = { 'P', 'U', 'B', 'L', 'I', 'C' }; + private static final char[] START_ELEMENT = { '<', '!', 'E', 'L', 'E', 'M', 'E', 'N', 'T' }; + private static final char[] START_ATTLIST = { '<', '!', 'A', 'T', 'T', 'L', 'I', 'S', 'T' }; + private static final char[] START_ENTITY = { '<', '!', 'E', 'N', 'T', 'I', 'T', 'Y' }; + private static final char[] START_NOTATION = { '<', '!', 'N', 'O', 'T', 'A', 'T', 'I', 'O', 'N' }; + private static final char[] EMPTY = new char[] { 'E', 'M', 'P', 'T', 'Y' }; + private static final char[] ANY = new char[]{ 'A', 'N', 'Y' }; + private static final char[] NDATA = new char[]{ 'N', 'D', 'A', 'T', 'A' }; + private static final char[] NOTATION = new char[]{ 'N', 'O', 'T', 'A', 'T', 'I', 'O', 'N' }; + private static final char[] REQUIRED = new char[] { 'R', 'E', 'Q', 'U', 'I', 'R', 'E', 'D' }; + private static final char[] IMPLIED = new char[] { 'I', 'M', 'P', 'L', 'I', 'E', 'D' }; + private static final char[] FIXED = new char[] { 'F', 'I', 'X', 'E', 'D' }; + + static final private String UNEXPECTED_EOF = "Unexpected EOF"; + static final private String ILLEGAL_TYPE = "Wrong event type"; + static final private int XML_DECLARATION = 998; + + // general + private String location; + + private String version; + private Boolean standalone; + private String rootElementName; + private String systemId; + private String publicId; + + /** + * True if the {@code } contents are handled. The DTD defines + * entity values and default attribute values. These values are parsed at + * inclusion time and may contain both tags and entity references. + * + *

If this is false, the user must {@link #defineEntityReplacementText + * define entity values manually}. Such entity values are literal strings + * and will not be parsed. There is no API to define default attributes + * manually. + */ + private boolean processDocDecl; + private boolean processNsp; + private boolean relaxed; + private boolean keepNamespaceAttributes; + + /** + * If non-null, the contents of the read buffer must be copied into this + * string builder before the read buffer is overwritten. This is used to + * capture the raw DTD text while parsing the DTD. + */ + private StringBuilder bufferCapture; + + /** + * Entities defined in or for this document. This map is created lazily. + */ + private Map documentEntities; + + /** + * Default attributes in this document. The outer map's key is the element + * name; the inner map's key is the attribute name. Both keys should be + * without namespace adjustments. This map is created lazily. + */ + private Map> defaultAttributes; + + + private int depth; + private String[] elementStack = new String[16]; + private String[] nspStack = new String[8]; + private int[] nspCounts = new int[4]; + + // source + + private Reader reader; + private String encoding; + private ContentSource nextContentSource; + private char[] buffer = new char[8192]; + private int position = 0; + private int limit = 0; + + /* + * Track the number of newlines and columns preceding the current buffer. To + * compute the line and column of a position in the buffer, compute the line + * and column in the buffer and add the preceding values. + */ + private int bufferStartLine; + private int bufferStartColumn; + + // the current token + + private int type; + private boolean isWhitespace; + private String namespace; + private String prefix; + private String name; + private String text; + + private boolean degenerated; + private int attributeCount; + + // true iff. we've encountered the START_TAG of an XML element at depth == 0; + private boolean parsedTopLevelStartTag; + + /* + * The current element's attributes arranged in groups of 4: + * i + 0 = attribute namespace URI + * i + 1 = attribute namespace prefix + * i + 2 = attribute qualified name (may contain ":", as in "html:h1") + * i + 3 = attribute value + */ + private String[] attributes = new String[16]; + + private String error; + + private boolean unresolved; + + + /** + * Retains namespace attributes like {@code xmlns="http://foo"} or {@code xmlns:foo="http:foo"} + * in pulled elements. Most applications will only be interested in the effective namespaces of + * their elements, so these attributes aren't useful. But for structure preserving wrappers like + * DOM, it is necessary to keep the namespace data around. + */ + public void keepNamespaceAttributes() { + this.keepNamespaceAttributes = true; + } + + private boolean adjustNsp() throws XmlPullParserException { + boolean any = false; + + for (int i = 0; i < attributeCount << 2; i += 4) { + String attrName = attributes[i + 2]; + int cut = attrName.indexOf(':'); + String prefix; + + if (cut != -1) { + prefix = attrName.substring(0, cut); + attrName = attrName.substring(cut + 1); + } else if (attrName.equals("xmlns")) { + prefix = attrName; + attrName = null; + } else { + continue; + } + + if (!prefix.equals("xmlns")) { + any = true; + } else { + int j = (nspCounts[depth]++) << 1; + + nspStack = ensureCapacity(nspStack, j + 2); + nspStack[j] = attrName; + nspStack[j + 1] = attributes[i + 3]; + + if (attrName != null && attributes[i + 3].isEmpty()) { + checkRelaxed("illegal empty namespace"); + } + + if (keepNamespaceAttributes) { + // explicitly set the namespace for unprefixed attributes + // such as xmlns="http://foo" + attributes[i] = "http://www.w3.org/2000/xmlns/"; + any = true; + } else { + System.arraycopy( + attributes, + i + 4, + attributes, + i, + ((--attributeCount) << 2) - i); + + i -= 4; + } + } + } + + if (any) { + for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { + + String attrName = attributes[i + 2]; + int cut = attrName.indexOf(':'); + + if (cut == 0 && !relaxed) { + throw new RuntimeException( + "illegal attribute name: " + attrName + " at " + this); + } else if (cut != -1) { + String attrPrefix = attrName.substring(0, cut); + + attrName = attrName.substring(cut + 1); + + String attrNs = getNamespace(attrPrefix); + + if (attrNs == null && !relaxed) { + throw new RuntimeException( + "Undefined Prefix: " + attrPrefix + " in " + this); + } + + attributes[i] = attrNs; + attributes[i + 1] = attrPrefix; + attributes[i + 2] = attrName; + } + } + } + + int cut = name.indexOf(':'); + + if (cut == 0) { + checkRelaxed("illegal tag name: " + name); + } + + if (cut != -1) { + prefix = name.substring(0, cut); + name = name.substring(cut + 1); + } + + this.namespace = getNamespace(prefix); + + if (this.namespace == null) { + if (prefix != null) { + checkRelaxed("undefined prefix: " + prefix); + } + this.namespace = NO_NAMESPACE; + } + + return any; + } + + private String[] ensureCapacity(String[] arr, int required) { + if (arr.length >= required) { + return arr; + } + String[] bigger = new String[required + 16]; + System.arraycopy(arr, 0, bigger, 0, arr.length); + return bigger; + } + + private void checkRelaxed(String errorMessage) throws XmlPullParserException { + if (!relaxed) { + throw new XmlPullParserException(errorMessage, this, null); + } + if (error == null) { + error = "Error: " + errorMessage; + } + } + + public int next() throws XmlPullParserException, IOException { + return next(false); + } + + public int nextToken() throws XmlPullParserException, IOException { + return next(true); + } + + private int next(boolean justOneToken) throws IOException, XmlPullParserException { + if (reader == null) { + throw new XmlPullParserException("setInput() must be called first.", this, null); + } + + if (type == END_TAG) { + depth--; + } + + // degenerated needs to be handled before error because of possible + // processor expectations(!) + + if (degenerated) { + degenerated = false; + type = END_TAG; + return type; + } + + if (error != null) { + if (justOneToken) { + text = error; + type = COMMENT; + error = null; + return type; + } else { + error = null; + } + } + + type = peekType(false); + + if (type == XML_DECLARATION) { + readXmlDeclaration(); + type = peekType(false); + } + + text = null; + isWhitespace = true; + prefix = null; + name = null; + namespace = null; + attributeCount = -1; + boolean throwOnResolveFailure = !justOneToken; + + while (true) { + switch (type) { + + /* + * Return immediately after encountering a start tag, end tag, or + * the end of the document. + */ + case START_TAG: + parseStartTag(false, throwOnResolveFailure); + return type; + case END_TAG: + readEndTag(); + return type; + case END_DOCUMENT: + return type; + + /* + * Return after any text token when we're looking for a single + * token. Otherwise concatenate all text between tags. + */ + case ENTITY_REF: + if (justOneToken) { + StringBuilder entityTextBuilder = new StringBuilder(); + readEntity(entityTextBuilder, true, throwOnResolveFailure, ValueContext.TEXT); + text = entityTextBuilder.toString(); + break; + } + // fall-through + case TEXT: + text = readValue('<', !justOneToken, throwOnResolveFailure, ValueContext.TEXT); + if (depth == 0 && isWhitespace) { + type = IGNORABLE_WHITESPACE; + } + break; + case CDSECT: + read(START_CDATA); + text = readUntil(END_CDATA, true); + break; + + /* + * Comments, processing instructions and declarations are returned + * when we're looking for a single token. Otherwise they're skipped. + */ + case COMMENT: + String commentText = readComment(justOneToken); + if (justOneToken) { + text = commentText; + } + break; + case PROCESSING_INSTRUCTION: + read(START_PROCESSING_INSTRUCTION); + String processingInstruction = readUntil(END_PROCESSING_INSTRUCTION, justOneToken); + if (justOneToken) { + text = processingInstruction; + } + break; + case DOCDECL: + readDoctype(justOneToken); + if (parsedTopLevelStartTag) { + throw new XmlPullParserException("Unexpected token", this, null); + } + break; + + default: + throw new XmlPullParserException("Unexpected token", this, null); + } + + if (depth == 0 && (type == ENTITY_REF || type == TEXT || type == CDSECT)) { + throw new XmlPullParserException("Unexpected token", this, null); + } + + if (justOneToken) { + return type; + } + + if (type == IGNORABLE_WHITESPACE) { + text = null; + } + + /* + * We've read all that we can of a non-empty text block. Always + * report this as text, even if it was a CDATA block or entity + * reference. + */ + int peek = peekType(false); + if (text != null && !text.isEmpty() && peek < TEXT) { + type = TEXT; + return type; + } + + type = peek; + } + } + + /** + * Reads text until the specified delimiter is encountered. Consumes the + * text and the delimiter. + * + * @param returnText true to return the read text excluding the delimiter; + * false to return null. + */ + private String readUntil(char[] delimiter, boolean returnText) + throws IOException, XmlPullParserException { + int start = position; + StringBuilder result = null; + + if (returnText && text != null) { + result = new StringBuilder(); + result.append(text); + } + + search: + while (true) { + if (position + delimiter.length > limit) { + if (start < position && returnText) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + } + if (!fillBuffer(delimiter.length)) { + checkRelaxed(UNEXPECTED_EOF); + type = COMMENT; + return null; + } + start = position; + } + + // TODO: replace with Arrays.equals(buffer, position, delimiter, 0, delimiter.length) + // when the VM has better method inlining + for (int i = 0; i < delimiter.length; i++) { + if (buffer[position + i] != delimiter[i]) { + position++; + continue search; + } + } + + break; + } + + int end = position; + position += delimiter.length; + + if (!returnText) { + return null; + } else if (result == null) { + return new String(buffer, start, end - start); + } else { + result.append(buffer, start, end - start); + return result.toString(); + } + } + + /** + * Returns true if an XML declaration was read. + */ + private void readXmlDeclaration() throws IOException, XmlPullParserException { + if (bufferStartLine != 0 || bufferStartColumn != 0 || position != 0) { + checkRelaxed("processing instructions must not start with xml"); + } + + read(START_PROCESSING_INSTRUCTION); + parseStartTag(true, true); + + if (attributeCount < 1 || !"version".equals(attributes[2])) { + checkRelaxed("version expected"); + } + + version = attributes[3]; + + int pos = 1; + + if (pos < attributeCount && "encoding".equals(attributes[2 + 4])) { + encoding = attributes[3 + 4]; + pos++; + } + + if (pos < attributeCount && "standalone".equals(attributes[4 * pos + 2])) { + String st = attributes[3 + 4 * pos]; + if ("yes".equals(st)) { + standalone = Boolean.TRUE; + } else if ("no".equals(st)) { + standalone = Boolean.FALSE; + } else { + checkRelaxed("illegal standalone value: " + st); + } + pos++; + } + + if (pos != attributeCount) { + checkRelaxed("unexpected attributes in XML declaration"); + } + + isWhitespace = true; + text = null; + } + + private String readComment(boolean returnText) throws IOException, XmlPullParserException { + read(START_COMMENT); + + if (relaxed) { + return readUntil(END_COMMENT, returnText); + } + + String commentText = readUntil(COMMENT_DOUBLE_DASH, returnText); + if (peekCharacter() != '>') { + throw new XmlPullParserException("Comments may not contain --", this, null); + } + position++; + return commentText; + } + + /** + * Read the document's DTD. Although this parser is non-validating, the DTD + * must be parsed to capture entity values and default attribute values. + */ + private void readDoctype(boolean saveDtdText) throws IOException, XmlPullParserException { + read(START_DOCTYPE); + + int startPosition = -1; + if (saveDtdText) { + bufferCapture = new StringBuilder(); + startPosition = position; + } + try { + skip(); + rootElementName = readName(); + readExternalId(true, true); + skip(); + if (peekCharacter() == '[') { + readInternalSubset(); + } + skip(); + } finally { + if (saveDtdText) { + bufferCapture.append(buffer, 0, position); + bufferCapture.delete(0, startPosition); + text = bufferCapture.toString(); + bufferCapture = null; + } + } + + read('>'); + skip(); + } + + /** + * Reads an external ID of one of these two forms: + * SYSTEM "quoted system name" + * PUBLIC "quoted public id" "quoted system name" + * + * If the system name is not required, this also supports lone public IDs of + * this form: + * PUBLIC "quoted public id" + * + * Returns true if any ID was read. + */ + private boolean readExternalId(boolean requireSystemName, boolean assignFields) + throws IOException, XmlPullParserException { + skip(); + int c = peekCharacter(); + + if (c == 'S') { + read(SYSTEM); + } else if (c == 'P') { + read(PUBLIC); + skip(); + if (assignFields) { + publicId = readQuotedId(true); + } else { + readQuotedId(false); + } + } else { + return false; + } + + skip(); + + if (!requireSystemName) { + int delimiter = peekCharacter(); + if (delimiter != '"' && delimiter != '\'') { + return true; // no system name! + } + } + + if (assignFields) { + systemId = readQuotedId(true); + } else { + readQuotedId(false); + } + return true; + } + + private static final char[] SINGLE_QUOTE = new char[] { '\'' }; + private static final char[] DOUBLE_QUOTE = new char[] { '"' }; + + /** + * Reads a quoted string, performing no entity escaping of the contents. + */ + private String readQuotedId(boolean returnText) throws IOException, XmlPullParserException { + int quote = peekCharacter(); + char[] delimiter; + if (quote == '"') { + delimiter = DOUBLE_QUOTE; + } else if (quote == '\'') { + delimiter = SINGLE_QUOTE; + } else { + throw new XmlPullParserException("Expected a quoted string", this, null); + } + position++; + return readUntil(delimiter, returnText); + } + + private void readInternalSubset() throws IOException, XmlPullParserException { + read('['); + + while (true) { + skip(); + if (peekCharacter() == ']') { + position++; + return; + } + + int declarationType = peekType(true); + switch (declarationType) { + case ELEMENTDECL: + readElementDeclaration(); + break; + + case ATTLISTDECL: + readAttributeListDeclaration(); + break; + + case ENTITYDECL: + readEntityDeclaration(); + break; + + case NOTATIONDECL: + readNotationDeclaration(); + break; + + case PROCESSING_INSTRUCTION: + read(START_PROCESSING_INSTRUCTION); + readUntil(END_PROCESSING_INSTRUCTION, false); + break; + + case COMMENT: + readComment(false); + break; + + case PARAMETER_ENTITY_REF: + throw new XmlPullParserException( + "Parameter entity references are not supported", this, null); + + default: + throw new XmlPullParserException("Unexpected token", this, null); + } + } + } + + /** + * Read an element declaration. This contains a name and a content spec. + * + * + * + */ + private void readElementDeclaration() throws IOException, XmlPullParserException { + read(START_ELEMENT); + skip(); + readName(); + readContentSpec(); + skip(); + read('>'); + } + + /** + * Read an element content spec. This is a regular expression-like pattern + * of names or other content specs. The following operators are supported: + * sequence: (a,b,c) + * choice: (a|b|c) + * optional: a? + * one or more: a+ + * any number: a* + * + * The special name '#PCDATA' is permitted but only if it is the first + * element of the first group: + * (#PCDATA|a|b) + * + * The top-level element must be either a choice, a sequence, or one of the + * special names EMPTY and ANY. + */ + private void readContentSpec() throws IOException, XmlPullParserException { + // this implementation is very lenient; it scans for balanced parens only + skip(); + int c = peekCharacter(); + if (c == '(') { + int depth = 0; + do { + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } else if (c == -1) { + throw new XmlPullParserException( + "Unterminated element content spec", this, null); + } + position++; + c = peekCharacter(); + } while (depth > 0); + + if (c == '*' || c == '?' || c == '+') { + position++; + } + } else if (c == EMPTY[0]) { + read(EMPTY); + } else if (c == ANY[0]) { + read(ANY); + } else { + throw new XmlPullParserException("Expected element content spec", this, null); + } + } + + /** + * Reads an attribute list declaration such as the following: + * + * + * Each attribute has a name, type and default. + * + * Types are one of the built-in types (CDATA, ID, IDREF, IDREFS, ENTITY, + * ENTITIES, NMTOKEN, or NMTOKENS), an enumerated type "(list|of|options)" + * or NOTATION followed by an enumerated type. + * + * The default is either #REQUIRED, #IMPLIED, #FIXED, a quoted value, or + * #FIXED with a quoted value. + */ + private void readAttributeListDeclaration() throws IOException, XmlPullParserException { + read(START_ATTLIST); + skip(); + String elementName = readName(); + + while (true) { + skip(); + int c = peekCharacter(); + if (c == '>') { + position++; + return; + } + + // attribute name + String attributeName = readName(); + + // attribute type + skip(); + if (position + 1 >= limit && !fillBuffer(2)) { + throw new XmlPullParserException("Malformed attribute list", this, null); + } + if (buffer[position] == NOTATION[0] && buffer[position + 1] == NOTATION[1]) { + read(NOTATION); + skip(); + } + c = peekCharacter(); + if (c == '(') { + position++; + while (true) { + skip(); + readName(); + skip(); + c = peekCharacter(); + if (c == ')') { + position++; + break; + } else if (c == '|') { + position++; + } else { + throw new XmlPullParserException("Malformed attribute type", this, null); + } + } + } else { + readName(); + } + + // default value + skip(); + c = peekCharacter(); + if (c == '#') { + position++; + c = peekCharacter(); + if (c == 'R') { + read(REQUIRED); + } else if (c == 'I') { + read(IMPLIED); + } else if (c == 'F') { + read(FIXED); + } else { + throw new XmlPullParserException("Malformed attribute type", this, null); + } + skip(); + c = peekCharacter(); + } + if (c == '"' || c == '\'') { + position++; + // TODO: does this do escaping correctly? + String value = readValue((char) c, true, true, ValueContext.ATTRIBUTE); + if (peekCharacter() == c) { + position++; + } + defineAttributeDefault(elementName, attributeName, value); + } + } + } + + private void defineAttributeDefault(String elementName, String attributeName, String value) { + if (defaultAttributes == null) { + defaultAttributes = new HashMap>(); + } + Map elementAttributes = defaultAttributes.get(elementName); + if (elementAttributes == null) { + elementAttributes = new HashMap(); + defaultAttributes.put(elementName, elementAttributes); + } + elementAttributes.put(attributeName, value); + } + + /** + * Read an entity declaration. The value of internal entities are inline: + * + * + * The values of external entities must be retrieved by URL or path: + * + * + * + * + * Entities may be general or parameterized. Parameterized entities are + * marked by a percent sign. Such entities may only be used in the DTD: + * + */ + private void readEntityDeclaration() throws IOException, XmlPullParserException { + read(START_ENTITY); + boolean generalEntity = true; + + skip(); + if (peekCharacter() == '%') { + generalEntity = false; + position++; + skip(); + } + + String name = readName(); + + skip(); + int quote = peekCharacter(); + String entityValue; + if (quote == '"' || quote == '\'') { + position++; + entityValue = readValue((char) quote, true, false, ValueContext.ENTITY_DECLARATION); + if (peekCharacter() == quote) { + position++; + } + } else if (readExternalId(true, false)) { + /* + * Map external entities to the empty string. This is dishonest, + * but it's consistent with Android's Expat pull parser. + */ + entityValue = ""; + skip(); + if (peekCharacter() == NDATA[0]) { + read(NDATA); + skip(); + readName(); + } + } else { + throw new XmlPullParserException("Expected entity value or external ID", this, null); + } + + if (generalEntity && processDocDecl) { + if (documentEntities == null) { + documentEntities = new HashMap(); + } + documentEntities.put(name, entityValue.toCharArray()); + } + + skip(); + read('>'); + } + + private void readNotationDeclaration() throws IOException, XmlPullParserException { + read(START_NOTATION); + skip(); + readName(); + if (!readExternalId(false, false)) { + throw new XmlPullParserException( + "Expected external ID or public ID for notation", this, null); + } + skip(); + read('>'); + } + + private void readEndTag() throws IOException, XmlPullParserException { + read('<'); + read('/'); + name = readName(); // TODO: pass the expected name in as a hint? + skip(); + read('>'); + + int sp = (depth - 1) * 4; + + if (depth == 0) { + checkRelaxed("read end tag " + name + " with no tags open"); + type = COMMENT; + return; + } + + if (name.equals(elementStack[sp + 3])) { + namespace = elementStack[sp]; + prefix = elementStack[sp + 1]; + name = elementStack[sp + 2]; + } else if (!relaxed) { + throw new XmlPullParserException( + "expected: /" + elementStack[sp + 3] + " read: " + name, this, null); + } + } + + /** + * Returns the type of the next token. + */ + private int peekType(boolean inDeclaration) throws IOException, XmlPullParserException { + if (position >= limit && !fillBuffer(1)) { + return END_DOCUMENT; + } + + switch (buffer[position]) { + case '&': + return ENTITY_REF; // & + case '<': + if (position + 3 >= limit && !fillBuffer(4)) { + throw new XmlPullParserException("Dangling <", this, null); + } + + switch (buffer[position + 1]) { + case '/': + return END_TAG; // = limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + + int c = buffer[position]; + + if (xmldecl) { + if (c == '?') { + position++; + read('>'); + return; + } + } else { + if (c == '/') { + degenerated = true; + position++; + skip(); + read('>'); + break; + } else if (c == '>') { + position++; + break; + } + } + + String attrName = readName(); + + int i = (attributeCount++) * 4; + attributes = ensureCapacity(attributes, i + 4); + attributes[i] = ""; + attributes[i + 1] = null; + attributes[i + 2] = attrName; + + skip(); + if (position >= limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + + if (buffer[position] == '=') { + position++; + + skip(); + if (position >= limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + char delimiter = buffer[position]; + + if (delimiter == '\'' || delimiter == '"') { + position++; + } else if (relaxed) { + delimiter = ' '; + } else { + throw new XmlPullParserException("attr value delimiter missing!", this, null); + } + + attributes[i + 3] = readValue(delimiter, true, throwOnResolveFailure, + ValueContext.ATTRIBUTE); + + if (delimiter != ' ' && peekCharacter() == delimiter) { + position++; // end quote + } + } else if (relaxed) { + attributes[i + 3] = attrName; + } else { + checkRelaxed("Attr.value missing f. " + attrName); + attributes[i + 3] = attrName; + } + } + + int sp = depth++ * 4; + if (depth == 1) { + parsedTopLevelStartTag = true; + } + elementStack = ensureCapacity(elementStack, sp + 4); + elementStack[sp + 3] = name; + + if (depth >= nspCounts.length) { + int[] bigger = new int[depth + 4]; + System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length); + nspCounts = bigger; + } + + nspCounts[depth] = nspCounts[depth - 1]; + + if (processNsp) { + adjustNsp(); + } else { + namespace = ""; + } + + // For consistency with Expat, add default attributes after fixing namespaces. + if (defaultAttributes != null) { + Map elementDefaultAttributes = defaultAttributes.get(name); + if (elementDefaultAttributes != null) { + for (Map.Entry entry : elementDefaultAttributes.entrySet()) { + if (getAttributeValue(null, entry.getKey()) != null) { + continue; // an explicit value overrides the default + } + + int i = (attributeCount++) * 4; + attributes = ensureCapacity(attributes, i + 4); + attributes[i] = ""; + attributes[i + 1] = null; + attributes[i + 2] = entry.getKey(); + attributes[i + 3] = entry.getValue(); + } + } + } + + elementStack[sp] = namespace; + elementStack[sp + 1] = prefix; + elementStack[sp + 2] = name; + } + + /** + * Reads an entity reference from the buffer, resolves it, and writes the + * resolved entity to {@code out}. If the entity cannot be read or resolved, + * {@code out} will contain the partial entity reference. + */ + private void readEntity(StringBuilder out, boolean isEntityToken, boolean throwOnResolveFailure, + ValueContext valueContext) throws IOException, XmlPullParserException { + int start = out.length(); + + if (buffer[position++] != '&') { + throw new AssertionError(); + } + + out.append('&'); + + while (true) { + int c = peekCharacter(); + + if (c == ';') { + out.append(';'); + position++; + break; + + } else if (c >= 128 + || (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_' + || c == '-' + || c == '#') { + position++; + out.append((char) c); + + } else if (relaxed) { + // intentionally leave the partial reference in 'out' + return; + + } else { + throw new XmlPullParserException("unterminated entity ref", this, null); + } + } + + String code = out.substring(start + 1, out.length() - 1); + + if (isEntityToken) { + name = code; + } + + if (code.startsWith("#")) { + try { + int c = code.startsWith("#x") + ? Integer.parseInt(code.substring(2), 16) + : Integer.parseInt(code.substring(1)); + out.delete(start, out.length()); + out.appendCodePoint(c); + unresolved = false; + return; + } catch (NumberFormatException notANumber) { + throw new XmlPullParserException("Invalid character reference: &" + code); + } catch (IllegalArgumentException invalidCodePoint) { + throw new XmlPullParserException("Invalid character reference: &" + code); + } + } + + if (valueContext == ValueContext.ENTITY_DECLARATION) { + // keep the unresolved &code; in the text to resolve later + return; + } + + String defaultEntity = DEFAULT_ENTITIES.get(code); + if (defaultEntity != null) { + out.delete(start, out.length()); + unresolved = false; + out.append(defaultEntity); + return; + } + + char[] resolved; + if (documentEntities != null && (resolved = documentEntities.get(code)) != null) { + out.delete(start, out.length()); + unresolved = false; + if (processDocDecl) { + pushContentSource(resolved); // parse the entity as XML + } else { + out.append(resolved); // include the entity value as text + } + return; + } + + /* + * The parser skipped an external DTD, and now we've encountered an + * unknown entity that could have been declared there. Map it to the + * empty string. This is dishonest, but it's consistent with Android's + * old ExpatPullParser. + */ + if (systemId != null) { + out.delete(start, out.length()); + return; + } + + // keep the unresolved entity "&code;" in the text for relaxed clients + unresolved = true; + if (throwOnResolveFailure) { + checkRelaxed("unresolved: &" + code + ";"); + } + } + + /** + * Where a value is found impacts how that value is interpreted. For + * example, in attributes, "\n" must be replaced with a space character. In + * text, "]]>" is forbidden. In entity declarations, named references are + * not resolved. + */ + enum ValueContext { + ATTRIBUTE, + TEXT, + ENTITY_DECLARATION + } + + /** + * Returns the current text or attribute value. This also has the side + * effect of setting isWhitespace to false if a non-whitespace character is + * encountered. + * + * @param delimiter {@code <} for text, {@code "} and {@code '} for quoted + * attributes, or a space for unquoted attributes. + */ + private String readValue(char delimiter, boolean resolveEntities, boolean throwOnResolveFailure, + ValueContext valueContext) throws IOException, XmlPullParserException { + + /* + * This method returns all of the characters from the current position + * through to an appropriate delimiter. + * + * If we're lucky (which we usually are), we'll return a single slice of + * the buffer. This fast path avoids allocating a string builder. + * + * There are 6 unlucky characters we could encounter: + * - "&": entities must be resolved. + * - "%": parameter entities are unsupported in entity values. + * - "<": this isn't permitted in attributes unless relaxed. + * - "]": this requires a lookahead to defend against the forbidden + * CDATA section delimiter "]]>". + * - "\r": If a "\r" is followed by a "\n", we discard the "\r". If it + * isn't followed by "\n", we replace "\r" with either a "\n" + * in text nodes or a space in attribute values. + * - "\n": In attribute values, "\n" must be replaced with a space. + * + * We could also get unlucky by needing to refill the buffer midway + * through the text. + */ + + int start = position; + StringBuilder result = null; + + // if a text section was already started, prefix the start + if (valueContext == ValueContext.TEXT && text != null) { + result = new StringBuilder(); + result.append(text); + } + + while (true) { + + /* + * Make sure we have at least a single character to read from the + * buffer. This mutates the buffer, so save the partial result + * to the slow path string builder first. + */ + if (position >= limit) { + if (start < position) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + } + if (!fillBuffer(1)) { + return result != null ? result.toString() : ""; + } + start = position; + } + + char c = buffer[position]; + + if (c == delimiter + || (delimiter == ' ' && (c <= ' ' || c == '>')) + || c == '&' && !resolveEntities) { + break; + } + + if (c != '\r' + && (c != '\n' || valueContext != ValueContext.ATTRIBUTE) + && c != '&' + && c != '<' + && (c != ']' || valueContext != ValueContext.TEXT) + && (c != '%' || valueContext != ValueContext.ENTITY_DECLARATION)) { + isWhitespace &= (c <= ' '); + position++; + continue; + } + + /* + * We've encountered an unlucky character! Convert from fast + * path to slow path if we haven't done so already. + */ + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + + if (c == '\r') { + if ((position + 1 < limit || fillBuffer(2)) && buffer[position + 1] == '\n') { + position++; + } + c = (valueContext == ValueContext.ATTRIBUTE) ? ' ' : '\n'; + + } else if (c == '\n') { + c = ' '; + + } else if (c == '&') { + isWhitespace = false; // TODO: what if the entity resolves to whitespace? + readEntity(result, false, throwOnResolveFailure, valueContext); + start = position; + continue; + + } else if (c == '<') { + if (valueContext == ValueContext.ATTRIBUTE) { + checkRelaxed("Illegal: \"<\" inside attribute value"); + } + isWhitespace = false; + + } else if (c == ']') { + if ((position + 2 < limit || fillBuffer(3)) + && buffer[position + 1] == ']' && buffer[position + 2] == '>') { + checkRelaxed("Illegal: \"]]>\" outside CDATA section"); + } + isWhitespace = false; + + } else if (c == '%') { + throw new XmlPullParserException("This parser doesn't support parameter entities", + this, null); + + } else { + throw new AssertionError(); + } + + position++; + result.append(c); + start = position; + } + + if (result == null) { + return new String(buffer, start, position - start); + } else { + result.append(buffer, start, position - start); + return result.toString(); + } + } + + private void read(char expected) throws IOException, XmlPullParserException { + int c = peekCharacter(); + if (c != expected) { + checkRelaxed("expected: '" + expected + "' actual: '" + ((char) c) + "'"); + if (c == -1) { + return; // On EOF, don't move position beyond limit + } + } + position++; + } + + private void read(char[] chars) throws IOException, XmlPullParserException { + if (position + chars.length > limit && !fillBuffer(chars.length)) { + checkRelaxed("expected: '" + new String(chars) + "' but was EOF"); + return; + } + + // TODO: replace with Arrays.equals(buffer, position, delimiter, 0, delimiter.length) + // when the VM has better method inlining + for (int i = 0; i < chars.length; i++) { + if (buffer[position + i] != chars[i]) { + checkRelaxed("expected: \"" + new String(chars) + "\" but was \"" + + new String(buffer, position, chars.length) + "...\""); + } + } + + position += chars.length; + } + + private int peekCharacter() throws IOException, XmlPullParserException { + if (position < limit || fillBuffer(1)) { + return buffer[position]; + } + return -1; + } + + /** + * Returns true once {@code limit - position >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException, XmlPullParserException { + // If we've exhausted the current content source, remove it + while (nextContentSource != null) { + if (position < limit) { + throw new XmlPullParserException("Unbalanced entity!", this, null); + } + popContentSource(); + if (limit - position >= minimum) { + return true; + } + } + + // Before clobbering the old characters, update where buffer starts + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + bufferStartLine++; + bufferStartColumn = 0; + } else { + bufferStartColumn++; + } + } + + if (bufferCapture != null) { + bufferCapture.append(buffer, 0, position); + } + + if (limit != position) { + limit -= position; + System.arraycopy(buffer, position, buffer, 0, limit); + } else { + limit = 0; + } + + position = 0; + int total; + while ((total = reader.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + if (limit >= minimum) { + return true; + } + } + return false; + } + + /** + * Returns an element or attribute name. This is always non-empty for + * non-relaxed parsers. + */ + private String readName() throws IOException, XmlPullParserException { + if (position >= limit && !fillBuffer(1)) { + checkRelaxed("name expected"); + return ""; + } + + int start = position; + StringBuilder result = null; + + // read the first character + char c = buffer[position]; + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_' + || c == ':' + || c >= '\u00c0' // TODO: check the XML spec + || relaxed) { + position++; + } else { + checkRelaxed("name expected"); + return ""; + } + + while (true) { + /* + * Make sure we have at least a single character to read from the + * buffer. This mutates the buffer, so save the partial result + * to the slow path string builder first. + */ + if (position >= limit) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + if (!fillBuffer(1)) { + return result.toString(); + } + start = position; + } + + // read another character + c = buffer[position]; + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '_' + || c == '-' + || c == ':' + || c == '.' + || c >= '\u00b7') { // TODO: check the XML spec + position++; + continue; + } + + // we encountered a non-name character. done! + if (result == null) { + return new String(buffer, start, position - start); + } else { + result.append(buffer, start, position - start); + return result.toString(); + } + } + } + + private void skip() throws IOException, XmlPullParserException { + while (position < limit || fillBuffer(1)) { + int c = buffer[position]; + if (c > ' ') { + break; + } + position++; + } + } + + // public part starts here... + + public void setInput(Reader reader) throws XmlPullParserException { + this.reader = reader; + + type = START_DOCUMENT; + parsedTopLevelStartTag = false; + name = null; + namespace = null; + degenerated = false; + attributeCount = -1; + encoding = null; + version = null; + standalone = null; + + if (reader == null) { + return; + } + + position = 0; + limit = 0; + bufferStartLine = 0; + bufferStartColumn = 0; + depth = 0; + documentEntities = null; + } + + public void setInput(InputStream is, String charset) throws XmlPullParserException { + position = 0; + limit = 0; + boolean detectCharset = (charset == null); + + if (is == null) { + throw new IllegalArgumentException("is == null"); + } + + try { + if (detectCharset) { + // read the four bytes looking for an indication of the encoding in use + int firstFourBytes = 0; + while (limit < 4) { + int i = is.read(); + if (i == -1) { + break; + } + firstFourBytes = (firstFourBytes << 8) | i; + buffer[limit++] = (char) i; + } + + if (limit == 4) { + switch (firstFourBytes) { + case 0x00000FEFF: // UTF-32BE BOM + charset = "UTF-32BE"; + limit = 0; + break; + + case 0x0FFFE0000: // UTF-32LE BOM + charset = "UTF-32LE"; + limit = 0; + break; + + case 0x0000003c: // '<' in UTF-32BE + charset = "UTF-32BE"; + buffer[0] = '<'; + limit = 1; + break; + + case 0x03c000000: // '<' in UTF-32LE + charset = "UTF-32LE"; + buffer[0] = '<'; + limit = 1; + break; + + case 0x0003c003f: // "') { + String s = new String(buffer, 0, limit); + int i0 = s.indexOf("encoding"); + if (i0 != -1) { + while (s.charAt(i0) != '"' && s.charAt(i0) != '\'') { + i0++; + } + char deli = s.charAt(i0++); + int i1 = s.indexOf(deli, i0); + charset = s.substring(i0, i1); + } + break; + } + } + break; + + default: + // handle a byte order mark followed by something other than + * is still at character 0. + */ + if (!detectCharset && peekCharacter() == 0xfeff) { + limit--; + System.arraycopy(buffer, 1, buffer, 0, limit); + } + } catch (Exception e) { + throw new XmlPullParserException("Invalid stream or encoding: " + e, this, e); + } + } + + public void close() throws IOException { + if (reader != null) { + reader.close(); + } + } + + public boolean getFeature(String feature) { + if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) { + return processNsp; + } else if (FEATURE_RELAXED.equals(feature)) { + return relaxed; + } else if (FEATURE_PROCESS_DOCDECL.equals(feature)) { + return processDocDecl; + } else { + return false; + } + } + + public String getInputEncoding() { + return encoding; + } + + public void defineEntityReplacementText(String entity, String value) + throws XmlPullParserException { + if (processDocDecl) { + throw new IllegalStateException( + "Entity replacement text may not be defined with DOCTYPE processing enabled."); + } + if (reader == null) { + throw new IllegalStateException( + "Entity replacement text must be defined after setInput()"); + } + if (documentEntities == null) { + documentEntities = new HashMap(); + } + documentEntities.put(entity, value.toCharArray()); + } + + public Object getProperty(String property) { + if (property.equals(PROPERTY_XMLDECL_VERSION)) { + return version; + } else if (property.equals(PROPERTY_XMLDECL_STANDALONE)) { + return standalone; + } else if (property.equals(PROPERTY_LOCATION)) { + return location != null ? location : reader.toString(); + } else { + return null; + } + } + + /** + * Returns the root element's name if it was declared in the DTD. This + * equals the first tag's name for valid documents. + */ + public String getRootElementName() { + return rootElementName; + } + + /** + * Returns the document's system ID if it was declared. This is typically a + * string like {@code http://www.w3.org/TR/html4/strict.dtd}. + */ + public String getSystemId() { + return systemId; + } + + /** + * Returns the document's public ID if it was declared. This is typically a + * string like {@code -//W3C//DTD HTML 4.01//EN}. + */ + public String getPublicId() { + return publicId; + } + + public int getNamespaceCount(int depth) { + if (depth > this.depth) { + throw new IndexOutOfBoundsException(); + } + return nspCounts[depth]; + } + + public String getNamespacePrefix(int pos) { + return nspStack[pos * 2]; + } + + public String getNamespaceUri(int pos) { + return nspStack[(pos * 2) + 1]; + } + + public String getNamespace(String prefix) { + if ("xml".equals(prefix)) { + return "http://www.w3.org/XML/1998/namespace"; + } + if ("xmlns".equals(prefix)) { + return "http://www.w3.org/2000/xmlns/"; + } + + for (int i = (getNamespaceCount(depth) << 1) - 2; i >= 0; i -= 2) { + if (prefix == null) { + if (nspStack[i] == null) { + return nspStack[i + 1]; + } + } else if (prefix.equals(nspStack[i])) { + return nspStack[i + 1]; + } + } + return null; + } + + public int getDepth() { + return depth; + } + + public String getPositionDescription() { + StringBuilder buf = new StringBuilder(type < TYPES.length ? TYPES[type] : "unknown"); + buf.append(' '); + + if (type == START_TAG || type == END_TAG) { + if (degenerated) { + buf.append("(empty) "); + } + buf.append('<'); + if (type == END_TAG) { + buf.append('/'); + } + + if (prefix != null) { + buf.append("{" + namespace + "}" + prefix + ":"); + } + buf.append(name); + + int cnt = attributeCount * 4; + for (int i = 0; i < cnt; i += 4) { + buf.append(' '); + if (attributes[i + 1] != null) { + buf.append("{" + attributes[i] + "}" + attributes[i + 1] + ":"); + } + buf.append(attributes[i + 2] + "='" + attributes[i + 3] + "'"); + } + + buf.append('>'); + } else if (type == IGNORABLE_WHITESPACE) { + ; + } else if (type != TEXT) { + buf.append(getText()); + } else if (isWhitespace) { + buf.append("(whitespace)"); + } else { + String text = getText(); + if (text.length() > 16) { + text = text.substring(0, 16) + "..."; + } + buf.append(text); + } + + buf.append("@" + getLineNumber() + ":" + getColumnNumber()); + if (location != null) { + buf.append(" in "); + buf.append(location); + } else if (reader != null) { + buf.append(" in "); + buf.append(reader.toString()); + } + return buf.toString(); + } + + public int getLineNumber() { + int result = bufferStartLine; + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + result++; + } + } + return result + 1; // the first line is '1' + } + + public int getColumnNumber() { + int result = bufferStartColumn; + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + result = 0; + } else { + result++; + } + } + return result + 1; // the first column is '1' + } + + public boolean isWhitespace() throws XmlPullParserException { + if (type != TEXT && type != IGNORABLE_WHITESPACE && type != CDSECT) { + throw new XmlPullParserException(ILLEGAL_TYPE, this, null); + } + return isWhitespace; + } + + public String getText() { + if (type < TEXT || (type == ENTITY_REF && unresolved)) { + return null; + } else if (text == null) { + return ""; + } else { + return text; + } + } + + public char[] getTextCharacters(int[] poslen) { + String text = getText(); + if (text == null) { + poslen[0] = -1; + poslen[1] = -1; + return null; + } + char[] result = text.toCharArray(); + poslen[0] = 0; + poslen[1] = result.length; + return result; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + public String getPrefix() { + return prefix; + } + + public boolean isEmptyElementTag() throws XmlPullParserException { + if (type != START_TAG) { + throw new XmlPullParserException(ILLEGAL_TYPE, this, null); + } + return degenerated; + } + + public int getAttributeCount() { + return attributeCount; + } + + public String getAttributeType(int index) { + return "CDATA"; + } + + public boolean isAttributeDefault(int index) { + return false; + } + + public String getAttributeNamespace(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[index * 4]; + } + + public String getAttributeName(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 2]; + } + + public String getAttributePrefix(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 1]; + } + + public String getAttributeValue(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 3]; + } + + public String getAttributeValue(String namespace, String name) { + for (int i = (attributeCount * 4) - 4; i >= 0; i -= 4) { + if (attributes[i + 2].equals(name) + && (namespace == null || attributes[i].equals(namespace))) { + return attributes[i + 3]; + } + } + + return null; + } + + public int getEventType() throws XmlPullParserException { + return type; + } + + // utility methods to make XML parsing easier ... + + public int nextTag() throws XmlPullParserException, IOException { + next(); + if (type == TEXT && isWhitespace) { + next(); + } + + if (type != END_TAG && type != START_TAG) { + throw new XmlPullParserException("unexpected type", this, null); + } + + return type; + } + + public void require(int type, String namespace, String name) + throws XmlPullParserException, IOException { + if (type != this.type + || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) { + throw new XmlPullParserException( + "expected: " + TYPES[type] + " {" + namespace + "}" + name, this, null); + } + } + + public String nextText() throws XmlPullParserException, IOException { + if (type != START_TAG) { + throw new XmlPullParserException("precondition: START_TAG", this, null); + } + + next(); + + String result; + if (type == TEXT) { + result = getText(); + next(); + } else { + result = ""; + } + + if (type != END_TAG) { + throw new XmlPullParserException("END_TAG expected", this, null); + } + + return result; + } + + public void setFeature(String feature, boolean value) throws XmlPullParserException { + if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) { + processNsp = value; + } else if (XmlPullParser.FEATURE_PROCESS_DOCDECL.equals(feature)) { + processDocDecl = value; + } else if (FEATURE_RELAXED.equals(feature)) { + relaxed = value; + } else { + throw new XmlPullParserException("unsupported feature: " + feature, this, null); + } + } + + public void setProperty(String property, Object value) throws XmlPullParserException { + if (property.equals(PROPERTY_LOCATION)) { + location = String.valueOf(value); + } else { + throw new XmlPullParserException("unsupported property: " + property); + } + } + static class ContentSource { + private final ContentSource next; + private final char[] buffer; + private final int position; + private final int limit; + ContentSource(ContentSource next, char[] buffer, int position, int limit) { + this.next = next; + this.buffer = buffer; + this.position = position; + this.limit = limit; + } + } + private void pushContentSource(char[] newBuffer) { + nextContentSource = new ContentSource(nextContentSource, buffer, position, limit); + buffer = newBuffer; + position = 0; + limit = newBuffer.length; + } + + /** + * Replaces the current exhausted buffer with the next buffer in the chain. + */ + private void popContentSource() { + buffer = nextContentSource.buffer; + position = nextContentSource.position; + limit = nextContentSource.limit; + nextContentSource = nextContentSource.next; + } +} diff --git a/src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java b/src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java new file mode 100644 index 00000000..53aa6f69 --- /dev/null +++ b/src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java @@ -0,0 +1,565 @@ +/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. */ + +package com.android.org.kxml2.io; + +import java.io.*; +import java.util.Arrays; +import java.util.Locale; +import org.xmlpull.v1.*; + +public class KXmlSerializer implements XmlSerializer { + + private static final int BUFFER_LEN = 8192; + private final char[] mText = new char[BUFFER_LEN]; + private int mPos; + private Writer writer; + private boolean pending; + private int auto; + private int depth; + private String[] elementStack = new String[12]; + private int[] nspCounts = new int[4]; + private String[] nspStack = new String[8]; + private boolean[] indent = new boolean[4]; + private boolean firstAttributeWritten; + private int indentAttributeReference; + private boolean unicode; + private String encoding; + + private void append(char c) throws IOException { + if(mPos >= BUFFER_LEN){ + flushBuffer(); + } + mText[mPos++] = c; + } + + private void append(String str, int i, int length) throws IOException { + while (length > 0){ + if(mPos == BUFFER_LEN){ + flushBuffer(); + } + int batch = BUFFER_LEN - mPos; + if(batch > length){ + batch = length; + } + str.getChars(i, i + batch, mText, mPos); + i += batch; + length -= batch; + mPos += batch; + } + } + + private void appendSpace(int length) throws IOException { + while (length > 0){ + if(mPos == BUFFER_LEN){ + flushBuffer(); + } + int batch = BUFFER_LEN - mPos; + if(batch > length){ + batch = length; + } + Arrays.fill(mText, mPos, mPos + batch, ' '); + length -= batch; + mPos += batch; + } + } + + private void append(String str) throws IOException { + append(str, 0, str.length()); + } + + private void flushBuffer() throws IOException { + if(mPos > 0){ + writer.write(mText, 0, mPos); + writer.flush(); + mPos = 0; + } + } + + private void check(boolean close) throws IOException { + if(!pending) + return; + + depth++; + pending = false; + + if(indent.length <= depth){ + boolean[] hlp = new boolean[depth + 4]; + System.arraycopy(indent, 0, hlp, 0, depth); + indent = hlp; + } + indent[depth] = indent[depth - 1]; + + for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++){ + append(" xmlns"); + if(!nspStack[i * 2].isEmpty()){ + append(':'); + append(nspStack[i * 2]); + } + else if(getNamespace().isEmpty() && !nspStack[i * 2 + 1].isEmpty()) + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + append("=\""); + writeEscaped(nspStack[i * 2 + 1], '"'); + append('"'); + } + + if(nspCounts.length <= depth + 1){ + int[] hlp = new int[depth + 8]; + System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); + nspCounts = hlp; + } + + nspCounts[depth + 1] = nspCounts[depth]; + if(close){ + append(" />"); + } else { + append('>'); + } + } + + private void writeEscaped(String s, int quot) throws IOException { + for (int i = 0; i < s.length(); i++){ + char c = s.charAt(i); + switch (c){ + case '\n': + case '\r': + case '\t': + if(quot == -1) + append(c); + else + append("&#"+((int) c)+';'); + break; + case '&' : + append("&"); + break; + case '>' : + append(">"); + break; + case '<' : + append("<"); + break; + default: + if(c == quot){ + append(c == '"' ? """ : "'"); + break; + } + boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); + if(allowedInXml){ + if(unicode || c < 127){ + append(c); + } else { + append("&#" + ((int) c) + ";"); + } + } else if(Character.isHighSurrogate(c) && i < s.length() - 1){ + writeSurrogate(c, s.charAt(i + 1)); + ++i; + } else { + reportInvalidCharacter(c); + } + } + } + } + private static void reportInvalidCharacter(char ch){ + throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")"); + } + @Override + public void docdecl(String dd) throws IOException { + append("'); + } + @Override + public void endDocument() throws IOException { + while (depth > 0){ + endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); + } + flush(); + } + @Override + public void entityRef(String name) throws IOException { + check(false); + append('&'); + append(name); + append(';'); + } + @Override + public boolean getFeature(String name){ + return "http://xmlpull.org/v1/doc/features.html#indent-output" + .equals(name) && indent[depth]; + } + @Override + public String getPrefix(String namespace, boolean create){ + try { + return getPrefix(namespace, false, create); + } + catch (IOException e){ + throw new RuntimeException(e.toString()); + } + } + private String getPrefix(String namespace, boolean includeDefault, boolean create) + throws IOException { + int[] nspCounts = this.nspCounts; + int depth = this.depth; + String[] nspStack = this.nspStack; + + for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0;i -= 2){ + if(nspStack[i + 1].equals(namespace) + && (includeDefault + || !nspStack[i].isEmpty())){ + String cand = nspStack[i]; + for (int j = i + 2; j < nspCounts[depth + 1] * 2; j++){ + if(nspStack[j].equals(cand)){ + cand = null; + break; + } + } + if(cand != null){ + return cand; + } + } + } + if(!create){ + return null; + } + + String prefix; + + if(namespace.isEmpty()) { + prefix = ""; + }else { + do { + prefix = "n" + (auto++); + for (int i = nspCounts[depth + 1] * 2 - 2;i >= 0;i -= 2){ + if(prefix.equals(nspStack[i])){ + prefix = null; + break; + } + } + } + while (prefix == null); + } + + boolean p = pending; + pending = false; + setPrefix(prefix, namespace); + pending = p; + return prefix; + } + + @Override + public Object getProperty(String name){ + throw new RuntimeException("Unsupported property: "+name); + } + @Override + public void ignorableWhitespace(String s) throws IOException { + text(s); + } + @Override + public void setFeature(String name, boolean value){ + if("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)){ + indent[depth] = value; + firstAttributeWritten = false; + }else { + throw new RuntimeException("Unsupported Feature: "+name); + } + } + @Override + public void setProperty(String name, Object value){ + throw new RuntimeException("Unsupported Property:" + value); + } + @Override + public void setPrefix(String prefix, String namespace) + throws IOException { + + check(false); + if(prefix == null) { + prefix = ""; + } + if(namespace == null) { + namespace = ""; + } + String defined = getPrefix(namespace, true, false); + if(prefix.equals(defined)) { + return; + } + + int pos = (nspCounts[depth + 1]++) << 1; + + if(nspStack.length < pos + 1){ + String[] hlp = new String[nspStack.length + 16]; + System.arraycopy(nspStack, 0, hlp, 0, pos); + nspStack = hlp; + } + + nspStack[pos++] = prefix; + nspStack[pos] = namespace; + } + + public void setOutput(Writer writer){ + this.writer = writer; + nspCounts[0] = 2; + nspCounts[1] = 2; + nspStack[0] = ""; + nspStack[1] = ""; + nspStack[2] = "xml"; + nspStack[3] = "http://www.w3.org/XML/1998/namespace"; + pending = false; + auto = 0; + depth = 0; + + unicode = false; + } + @Override + public void setOutput(OutputStream os, String encoding) + throws IOException { + if(os == null) { + throw new IllegalArgumentException("os == null"); + } + setOutput(encoding == null + ? new OutputStreamWriter(os) + : new OutputStreamWriter(os, encoding)); + this.encoding = encoding; + if(encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")){ + unicode = true; + } + } + @Override + public void startDocument(String encoding, Boolean standalone) throws IOException { + append(""); + } + @Override + public XmlSerializer startTag(String namespace, String name) + throws IOException { + check(false); + firstAttributeWritten = false; + indentAttributeReference = 0; + if(indent[depth]){ + append('\r'); + append('\n'); + int spaceLength = 2 * depth; + appendSpace(spaceLength); + indentAttributeReference = spaceLength; + } + int esp = depth * 3; + if(elementStack.length < esp + 3){ + String[] hlp = new String[elementStack.length + 12]; + System.arraycopy(elementStack, 0, hlp, 0, esp); + elementStack = hlp; + } + String prefix = namespace == null? + "" : getPrefix(namespace, true, true); + + if(namespace != null && namespace.isEmpty()){ + for (int i = nspCounts[depth]; i < nspCounts[depth + 1]; i++){ + if(nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()){ + throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); + } + } + } + elementStack[esp++] = namespace; + elementStack[esp++] = prefix; + elementStack[esp] = name; + append('<'); + indentAttributeReference += 1; + if(!prefix.isEmpty()){ + append(prefix); + append(':'); + indentAttributeReference += prefix.length() + 1; + } + append(name); + int len = name.length(); + if(len > 20){ + len = 20; + } + indentAttributeReference += len; + pending = true; + return this; + } + @Override + public XmlSerializer attribute(String namespace, String name, String value) + throws IOException { + if(!pending) { + throw new IllegalStateException("illegal position for attribute"); + } + if(namespace == null) { + namespace = ""; + } + String prefix = namespace.isEmpty() ? + "" : getPrefix(namespace, false, true); + attributeIndent(); + append(' '); + if(!prefix.isEmpty()){ + append(prefix); + append(':'); + } + append(name); + append('='); + char q = value.indexOf('"') == -1 ? '"' : '\''; + append(q); + writeEscaped(value, q); + append(q); + firstAttributeWritten = true; + return this; + } + @Override + public void flush() throws IOException { + check(false); + flushBuffer(); + } + @Override + public XmlSerializer endTag(String namespace, String name)throws IOException { + if(!pending) { + depth--; + } + if((namespace == null + && elementStack[depth * 3] != null) + || (namespace != null + && !namespace.equals(elementStack[depth * 3])) + || !elementStack[depth * 3 + 2].equals(name)) { + throw new IllegalArgumentException(" does not match start"); + } + + if(pending){ + check(true); + depth--; + } + else { + if(indent[depth + 1]){ + append('\r'); + append('\n'); + appendSpace(2 * depth); + } + append("'); + } + + nspCounts[depth + 1] = nspCounts[depth]; + return this; + } + @Override + public String getNamespace(){ + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; + } + @Override + public String getName(){ + return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; + } + @Override + public int getDepth(){ + return pending ? depth + 1 : depth; + } + @Override + public XmlSerializer text(String text) throws IOException { + check(false); + indent[depth] = false; + writeEscaped(text, -1); + return this; + } + @Override + public XmlSerializer text(char[] text, int start, int len) + throws IOException { + text(new String(text, start, len)); + return this; + } + @Override + public void cdsect(String data) throws IOException { + check(false); + data = data.replace("]]>", "]]]]>"); + append("= 0x20 && ch <= 0xd7ff) || + (ch == '\t' || ch == '\n' || ch == '\r') || + (ch >= 0xe000 && ch <= 0xfffd); + if(allowedInCdata){ + append(ch); + } else if(Character.isHighSurrogate(ch) && i < data.length() - 1){ + // Character entities aren't valid in CDATA, so break out for this. + append("]]>"); + writeSurrogate(ch, data.charAt(++i)); + append(""); + } + + private void writeSurrogate(char high, char low) throws IOException { + if(!Character.isLowSurrogate(low)){ + throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) + + " U+" + Integer.toHexString((int) low) + ")"); + } + int codePoint = Character.toCodePoint(high, low); + append("&#" + codePoint + ";"); + } + @Override + public void comment(String comment) throws IOException { + check(false); + append(""); + } + @Override + public void processingInstruction(String pi) + throws IOException { + check(false); + append(""); + } + + private void attributeIndent() throws IOException { + if(!firstAttributeWritten || !indent[depth]){ + return; + } + int length = this.indentAttributeReference; + if(length <= 0){ + return; + } + append('\r'); + append('\n'); + appendSpace(length); + } +} diff --git a/src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java b/src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java new file mode 100644 index 00000000..715df45c --- /dev/null +++ b/src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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.android.org.kxml2.io; + +// Taken from libcore.internal.StringPool + +class LibCoreStringPool { + + private final String[] pool = new String[512]; + + public LibCoreStringPool() { + } + + private static boolean contentEquals(String s, char[] chars, int start, int length) { + if (s.length() != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (chars[start + i] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns a string equal to {@code new String(array, start, length)}. + */ + public String get(char[] array, int start, int length) { + // Compute an arbitrary hash of the content + int hashCode = 0; + for (int i = start; i < start + length; i++) { + hashCode = (hashCode * 31) + array[i]; + } + + // Pick a bucket using Doug Lea's supplemental secondaryHash function (from HashMap) + hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12); + hashCode ^= (hashCode >>> 7) ^ (hashCode >>> 4); + int index = hashCode & (pool.length - 1); + + String pooled = pool[index]; + if (pooled != null && contentEquals(pooled, array, start, length)) { + return pooled; + } + + String result = new String(array, start, length); + pool[index] = result; + return result; + } + +} diff --git a/src/ARSCLib/com/reandroid/apk/APKLogger.java b/src/ARSCLib/com/reandroid/apk/APKLogger.java new file mode 100644 index 00000000..2ad571ee --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/APKLogger.java @@ -0,0 +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. + */ +package com.reandroid.apk; + +public interface APKLogger { + void logMessage(String msg); + void logError(String msg, Throwable tr); + void logVerbose(String msg); +} diff --git a/src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java b/src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java new file mode 100644 index 00000000..e05cf98d --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java @@ -0,0 +1,219 @@ + /* + * 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 java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class AndroidFrameworks { + private static Map resource_paths; + private static FrameworkApk mCurrent; + + public static void setCurrent(FrameworkApk current){ + synchronized (AndroidFrameworks.class){ + mCurrent = current; + } + } + public static FrameworkApk getCurrent(){ + FrameworkApk current = mCurrent; + if(current==null){ + return null; + } + if(current.isDestroyed()){ + mCurrent = null; + return null; + } + return current; + } + public static FrameworkApk getLatest() throws IOException { + Map pathMap = getResourcePaths(); + synchronized (AndroidFrameworks.class){ + int latest = getHighestVersion(); + FrameworkApk current = getCurrent(); + if(current!=null && latest==current.getVersionCode()){ + return current; + } + String path = pathMap.get(latest); + if(path == null){ + throw new IOException("Could not get latest framework"); + } + return loadResource(latest); + } + } + public static FrameworkApk getBestMatch(int version) throws IOException { + Map pathMap = getResourcePaths(); + synchronized (AndroidFrameworks.class){ + int best = getBestMatchVersion(version); + FrameworkApk current = getCurrent(); + if(current!=null && best==current.getVersionCode()){ + return current; + } + String path = pathMap.get(best); + if(path == null){ + throw new IOException("Could not get framework for version = "+version); + } + return loadResource(best); + } + } + public static void destroyCurrent(){ + synchronized (AndroidFrameworks.class){ + FrameworkApk current = mCurrent; + if(current==null){ + return; + } + current.destroy(); + } + } + private static int getHighestVersion() { + Map pathMap = getResourcePaths(); + int highest = 0; + for(int id:pathMap.keySet()){ + if(highest==0){ + highest = id; + continue; + } + if(id>highest){ + highest = id; + } + } + return highest; + } + private static int getBestMatchVersion(int version) { + Map pathMap = getResourcePaths(); + if(pathMap.containsKey(version)){ + return version; + } + int highest = 0; + int best = 0; + int prevDifference = 0; + for(int id:pathMap.keySet()){ + if(highest==0){ + highest = id; + best = id; + prevDifference = version*2 + 1000; + continue; + } + if(id>highest){ + highest = id; + } + int diff = id-version; + if(diff<0){ + diff=-diff; + } + if(diffbest)){ + best = id; + prevDifference = diff; + } + } + return best; + } + public static FrameworkApk loadResource(int version) throws IOException { + String path = getResourcePath(version); + if(path == null){ + throw new IOException("No resource found for version: "+version); + } + String simpleName = toSimpleName(path); + return FrameworkApk.loadApkBuffer(simpleName, AndroidFrameworks.class.getResourceAsStream(path)); + } + private static String getResourcePath(int version){ + return getResourcePaths().get(version); + } + private static Map getResourcePaths(){ + if(resource_paths!=null){ + return resource_paths; + } + synchronized (AndroidFrameworks.class){ + resource_paths = scanAvailableResourcePaths(); + return resource_paths; + } + } + private static Map scanAvailableResourcePaths(){ + Map results = new HashMap<>(); + int maxSearch = 50; + for(int version=20; version0){ + i++; + path = path.substring(i); + } + i = path.lastIndexOf('.'); + if(i>=0){ + path = path.substring(0, i); + } + return path; + } + private static int parseVersion(String name){ + int i = name.lastIndexOf('/'); + if(i<0){ + i = name.lastIndexOf(File.separatorChar); + } + if(i>0){ + i++; + name = name.substring(i); + } + i = name.lastIndexOf('-'); + if(i>=0){ + i++; + name = name.substring(i); + } + i = name.indexOf('.'); + if(i>=0){ + name = name.substring(0, i); + } + return Integer.parseInt(name); + } + private static boolean isAvailable(String path){ + InputStream inputStream = AndroidFrameworks.class.getResourceAsStream(path); + if(inputStream==null){ + return false; + } + closeQuietly(inputStream); + return true; + } + private static void closeQuietly(InputStream stream){ + if(stream == null){ + return; + } + try { + stream.close(); + } catch (IOException ignored) { + } + } + private static String toResourcePath(int version){ + return ANDROID_RESOURCE_DIRECTORY + ANDROID_PACKAGE + + '-' + version + +FRAMEWORK_EXTENSION; + } + private static final String ANDROID_RESOURCE_DIRECTORY = "/frameworks/android/"; + private static final String ANDROID_PACKAGE = "android"; + private static final String FRAMEWORK_EXTENSION = ".apk"; +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkBundle.java b/src/ARSCLib/com/reandroid/apk/ApkBundle.java new file mode 100644 index 00000000..8550c52f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkBundle.java @@ -0,0 +1,216 @@ +/* + * 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.APKArchive; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.pool.builder.StringPoolMerger; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.*; + +public class ApkBundle { + private final Map mModulesMap; + private APKLogger apkLogger; + public ApkBundle(){ + this.mModulesMap=new HashMap<>(); + } + + public ApkModule mergeModules() throws IOException { + List moduleList=getApkModuleList(); + if(moduleList.size()==0){ + throw new FileNotFoundException("Nothing to merge, empty modules"); + } + ApkModule result = new ApkModule(generateMergedModuleName(), new APKArchive()); + result.setAPKLogger(apkLogger); + result.setLoadDefaultFramework(false); + + mergeStringPools(result); + + ApkModule base=getBaseModule(); + if(base==null){ + base=getLargestTableModule(); + } + result.merge(base); + ApkSignatureBlock signatureBlock = null; + for(ApkModule module:moduleList){ + ApkSignatureBlock asb = module.getApkSignatureBlock(); + if(module==base){ + if(asb != null){ + signatureBlock = asb; + } + continue; + } + if(signatureBlock == null){ + signatureBlock = asb; + } + result.merge(module); + } + + result.setApkSignatureBlock(signatureBlock); + + if(result.hasTableBlock()){ + TableBlock tableBlock=result.getTableBlock(); + tableBlock.sortPackages(); + tableBlock.refresh(); + } + result.getApkArchive().autoSortApkFiles(); + return result; + } + private void mergeStringPools(ApkModule mergedModule) throws IOException { + if(!hasOneTableBlock() || mergedModule.hasTableBlock()){ + return; + } + logMessage("Merging string pools ... "); + TableBlock createdTable = new TableBlock(); + BlockInputSource inputSource= + new BlockInputSource<>(TableBlock.FILE_NAME, createdTable); + mergedModule.getApkArchive().add(inputSource); + + StringPoolMerger poolMerger = new StringPoolMerger(); + + for(ApkModule apkModule:getModules()){ + if(!apkModule.hasTableBlock()){ + continue; + } + TableStringPool stringPool = apkModule.getVolatileTableStringPool(); + poolMerger.add(stringPool); + } + + poolMerger.mergeTo(createdTable.getTableStringPool()); + + logMessage("Merged string pools="+poolMerger.getMergedPools() + +", style="+poolMerger.getMergedStyleStrings() + +", strings="+poolMerger.getMergedStrings()); + } + private String generateMergedModuleName(){ + Set moduleNames=mModulesMap.keySet(); + String merged="merged"; + int i=1; + String name=merged; + while (moduleNames.contains(name)){ + name=merged+"_"+i; + i++; + } + return name; + } + private ApkModule getLargestTableModule(){ + ApkModule apkModule=null; + int chunkSize=0; + for(ApkModule module:getApkModuleList()){ + if(!module.hasTableBlock()){ + continue; + } + TableBlock tableBlock=module.getTableBlock(); + int size=tableBlock.getHeaderBlock().getChunkSize(); + if(apkModule==null || size>chunkSize){ + chunkSize=size; + apkModule=module; + } + } + return apkModule; + } + public ApkModule getBaseModule(){ + for(ApkModule module:getApkModuleList()){ + if(module.isBaseModule()){ + return module; + } + } + return null; + } + public List getApkModuleList(){ + return new ArrayList<>(mModulesMap.values()); + } + public void loadApkDirectory(File dir) throws IOException{ + loadApkDirectory(dir, false); + } + public void loadApkDirectory(File dir, boolean recursive) throws IOException { + if(!dir.isDirectory()){ + throw new FileNotFoundException("No such directory: "+dir); + } + List apkList; + if(recursive){ + apkList = ApkUtil.recursiveFiles(dir, ".apk"); + }else { + apkList = ApkUtil.listFiles(dir, ".apk"); + } + if(apkList.size()==0){ + throw new FileNotFoundException("No '*.apk' files in directory: "+dir); + } + logMessage("Found apk files: "+apkList.size()); + for(File file:apkList){ + logVerbose("Loading: "+file.getName()); + String name = ApkUtil.toModuleName(file); + ApkModule module = ApkModule.loadApkFile(file, name); + module.setAPKLogger(apkLogger); + addModule(module); + } + } + public void addModule(ApkModule apkModule){ + apkModule.setLoadDefaultFramework(false); + String name = apkModule.getModuleName(); + mModulesMap.remove(name); + mModulesMap.put(name, apkModule); + } + public boolean containsApkModule(String moduleName){ + return mModulesMap.containsKey(moduleName); + } + public ApkModule removeApkModule(String moduleName){ + return mModulesMap.remove(moduleName); + } + public ApkModule getApkModule(String moduleName){ + return mModulesMap.get(moduleName); + } + public List listModuleNames(){ + return new ArrayList<>(mModulesMap.keySet()); + } + public int countModules(){ + return mModulesMap.size(); + } + public Collection getModules(){ + return mModulesMap.values(); + } + private boolean hasOneTableBlock(){ + for(ApkModule apkModule:getModules()){ + if(apkModule.hasTableBlock()){ + return true; + } + } + return false; + } + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + private void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkDecoder.java b/src/ARSCLib/com/reandroid/apk/ApkDecoder.java new file mode 100644 index 00000000..02fbc2a0 --- /dev/null +++ b/src/ARSCLib/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/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java b/src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java new file mode 100644 index 00000000..d146cfa7 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java @@ -0,0 +1,233 @@ +/* + * 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 com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +public class ApkJsonDecoder { + private final ApkModule apkModule; + private final Set decodedPaths; + private final boolean splitTypes; + public ApkJsonDecoder(ApkModule apkModule, boolean splitTypes){ + this.apkModule = apkModule; + this.splitTypes = splitTypes; + this.decodedPaths = new HashSet<>(); + } + public ApkJsonDecoder(ApkModule apkModule){ + this(apkModule, false); + } + public void sanitizeFilePaths(){ + PathSanitizer sanitizer = PathSanitizer.create(apkModule); + sanitizer.sanitize(); + } + public File writeToDirectory(File dir) throws IOException { + this.decodedPaths.clear(); + writeUncompressed(dir); + writeManifest(dir); + writeTable(dir); + //writeResourceIds(dir); + //writePublicXml(dir); + writeResources(dir); + writeRootFiles(dir); + writePathMap(dir); + dumpSignatures(dir); + return new File(dir, apkModule.getModuleName()); + } + private void dumpSignatures(File outDir) throws IOException { + ApkSignatureBlock signatureBlock = apkModule.getApkSignatureBlock(); + if(signatureBlock == null){ + return; + } + apkModule.logMessage("Dumping signatures ..."); + File dir = toSignatureDir(outDir); + signatureBlock.writeSplitRawToDirectory(dir); + } + private void writePathMap(File dir) throws IOException { + PathMap pathMap = new PathMap(); + pathMap.add(apkModule.getApkArchive()); + File file = toPathMapJsonFile(dir); + pathMap.toJson().write(file); + } + private void writeUncompressed(File dir) throws IOException { + File file=toUncompressedJsonFile(dir); + UncompressedFiles uncompressedFiles=new UncompressedFiles(); + uncompressedFiles.addCommonExtensions(); + uncompressedFiles.addPath(apkModule.getApkArchive()); + uncompressedFiles.toJson().write(file); + } + private void writeResources(File dir) throws IOException { + for(ResFile resFile:apkModule.listResFiles()){ + writeResource(dir, resFile); + } + } + private void writeResource(File dir, ResFile resFile) throws IOException { + if(resFile.isBinaryXml()){ + writeResourceJson(dir, resFile); + } + } + private void writeResourceJson(File dir, ResFile resFile) throws IOException { + InputSource inputSource= resFile.getInputSource(); + String path=inputSource.getAlias(); + File file=toResJson(dir, path); + ResXmlDocument resXmlDocument =new ResXmlDocument(); + resXmlDocument.readBytes(inputSource.openStream()); + JSONObject jsonObject= resXmlDocument.toJson(); + jsonObject.write(file); + addDecoded(path); + } + private void writeRootFiles(File dir) throws IOException { + for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){ + writeRootFile(dir, inputSource); + } + } + private void writeRootFile(File dir, InputSource inputSource) throws IOException { + String path=inputSource.getAlias(); + if(hasDecoded(path)){ + return; + } + File file=toRootFile(dir, path); + File parent=file.getParentFile(); + if(parent!=null && !parent.exists()){ + parent.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); + addDecoded(path); + } + private void writeTable(File dir) throws IOException { + if(!splitTypes){ + writeTableSingle(dir); + return; + } + writeTableSplit(dir); + } + private void writeTableSplit(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + File splitDir= toJsonTableSplitDir(dir); + TableBlockJson tableBlockJson=new TableBlockJson(tableBlock); + tableBlockJson.writeJsonFiles(splitDir); + addDecoded(TableBlock.FILE_NAME); + } + private void writeTableSingle(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + File file= toJsonTableFile(dir); + tableBlock.toJson().write(file); + addDecoded(TableBlock.FILE_NAME); + } + private void writeResourceIds(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + ResourceIds resourceIds=new ResourceIds(); + resourceIds.loadTableBlock(tableBlock); + JSONObject jsonObject= resourceIds.toJson(); + File file=toResourceIds(dir); + jsonObject.write(file); + } + private void writePublicXml(File dir) throws IOException { + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + ResourceIds resourceIds=new ResourceIds(); + resourceIds.loadTableBlock(tableBlock); + File file=toResourceIdsXml(dir); + resourceIds.writeXml(file); + } + private void writeManifest(File dir) throws IOException { + if(!apkModule.hasAndroidManifestBlock()){ + return; + } + AndroidManifestBlock manifestBlock = apkModule.getAndroidManifestBlock(); + File file = toJsonManifestFile(dir); + manifestBlock.toJson().write(file); + addDecoded(AndroidManifestBlock.FILE_NAME); + } + private boolean hasDecoded(String path){ + return decodedPaths.contains(path); + } + private void addDecoded(String path){ + this.decodedPaths.add(path); + } + private File toJsonTableFile(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(file, name); + } + private File toJsonTableSplitDir(File dir){ + File file=new File(dir, apkModule.getModuleName()); + return new File(file, ApkUtil.SPLIT_JSON_DIRECTORY); + } + private File toResourceIds(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = "resource-ids.json"; + return new File(file, name); + } + private File toResourceIdsXml(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = "public.xml"; + return new File(file, name); + } + private File toSignatureDir(File dir){ + dir = new File(dir, apkModule.getModuleName()); + return new File(dir, ApkUtil.SIGNATURE_DIR_NAME); + } + private File toPathMapJsonFile(File dir){ + File file = new File(dir, apkModule.getModuleName()); + return new File(file, PathMap.JSON_FILE); + } + private File toUncompressedJsonFile(File dir){ + File file = new File(dir, apkModule.getModuleName()); + return new File(file, UncompressedFiles.JSON_FILE); + } + private File toJsonManifestFile(File dir){ + File file=new File(dir, apkModule.getModuleName()); + String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(file, name); + } + private File toResJson(File dir, String path){ + File file=new File(dir, apkModule.getModuleName()); + file=new File(file, ApkUtil.RES_JSON_NAME); + path=path + ApkUtil.JSON_FILE_EXTENSION; + path=path.replace('/', File.separatorChar); + return new File(file, path); + } + private File toRootFile(File dir, String path){ + File file=new File(dir, apkModule.getModuleName()); + file=new File(file, ApkUtil.ROOT_NAME); + path=path.replace('/', File.separatorChar); + return new File(file, path); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java b/src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java new file mode 100644 index 00000000..1adb0f80 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java @@ -0,0 +1,209 @@ +/* + * 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.APKArchive; +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.json.JSONArray; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +public class ApkJsonEncoder { + private APKArchive apkArchive; + private APKLogger apkLogger; + public ApkJsonEncoder(){ + } + public ApkModule scanDirectory(File moduleDir){ + this.apkArchive=new APKArchive(); + String moduleName=moduleDir.getName(); + scanManifest(moduleDir); + scanTable(moduleDir); + scanResJsonDirs(moduleDir); + scanRootDirs(moduleDir); + ApkModule module=new ApkModule(moduleName, apkArchive); + module.setLoadDefaultFramework(false); + module.setAPKLogger(apkLogger); + loadUncompressed(module, moduleDir); + //applyResourceId(module, moduleDir); + restorePathMap(moduleDir, module); + restoreSignatures(moduleDir, module); + return module; + } + private void restoreSignatures(File dir, ApkModule apkModule){ + File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME); + if(!sigDir.isDirectory()){ + return; + } + logMessage("Loading signatures ..."); + ApkSignatureBlock signatureBlock = new ApkSignatureBlock(); + try { + signatureBlock.scanSplitFiles(sigDir); + apkModule.setApkSignatureBlock(signatureBlock); + } catch (IOException exception){ + logError("Failed to load signatures: ", exception); + } + } + private void restorePathMap(File dir, ApkModule apkModule){ + File file = new File(dir, PathMap.JSON_FILE); + if(!file.isFile()){ + return; + } + logMessage("Restoring file path ..."); + PathMap pathMap = new PathMap(); + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + } catch (FileNotFoundException exception) { + logError("Failed to load path-map", exception); + return; + } + JSONArray jsonArray = new JSONArray(inputStream); + pathMap.fromJson(jsonArray); + pathMap.restore(apkModule); + } + private void applyResourceId(ApkModule apkModule, File moduleDir) { + if(!apkModule.hasTableBlock()){ + return; + } + File pubXml=toResourceIdsXml(moduleDir); + if(!pubXml.isFile()){ + return; + } + ResourceIds resourceIds=new ResourceIds(); + try { + resourceIds.fromXml(pubXml); + resourceIds.applyTo(apkModule.getTableBlock()); + } catch (IOException exception) { + throw new IllegalArgumentException(exception.getMessage()); + } + } + private void loadUncompressed(ApkModule module, File moduleDir){ + File jsonFile=toUncompressedJsonFile(moduleDir); + UncompressedFiles uf= module.getUncompressedFiles(); + try { + uf.fromJson(jsonFile); + } catch (IOException ignored) { + } + } + private void scanRootDirs(File moduleDir){ + File rootDir=toRootDir(moduleDir); + List jsonFileList=ApkUtil.recursiveFiles(rootDir); + for(File file:jsonFileList){ + scanRootFile(rootDir, file); + } + } + private void scanRootFile(File rootDir, File file){ + String path=ApkUtil.toArchivePath(rootDir, file); + FileInputSource inputSource=new FileInputSource(file, path); + apkArchive.add(inputSource); + } + private void scanResJsonDirs(File moduleDir){ + File resJsonDir=toResJsonDir(moduleDir); + List jsonFileList=ApkUtil.recursiveFiles(resJsonDir); + for(File file:jsonFileList){ + scanResJsonFile(resJsonDir, file); + } + } + private void scanResJsonFile(File resJsonDir, File file){ + JsonXmlInputSource inputSource=JsonXmlInputSource.fromFile(resJsonDir, file); + apkArchive.add(inputSource); + } + private void scanManifest(File moduleDir){ + File file=toJsonManifestFile(moduleDir); + if(!file.isFile()){ + return; + } + JsonManifestInputSource inputSource=JsonManifestInputSource.fromFile(moduleDir, file); + inputSource.setAPKLogger(apkLogger); + apkArchive.add(inputSource); + } + private void scanTable(File moduleDir) { + boolean splitFound=scanTableSplitJson(moduleDir); + if(splitFound){ + return; + } + scanTableSingleJson(moduleDir); + } + private boolean scanTableSplitJson(File moduleDir) { + File dir=toJsonTableSplitDir(moduleDir); + if(!dir.isDirectory()){ + return false; + } + SplitJsonTableInputSource inputSource=new SplitJsonTableInputSource(dir); + inputSource.setAPKLogger(apkLogger); + apkArchive.add(inputSource); + return true; + } + private void scanTableSingleJson(File moduleDir) { + File file=toJsonTableFile(moduleDir); + if(!file.isFile()){ + return; + } + SingleJsonTableInputSource inputSource= SingleJsonTableInputSource.fromFile(moduleDir, file); + inputSource.setAPKLogger(apkLogger); + apkArchive.add(inputSource); + } + private File toJsonTableFile(File dir){ + String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(dir, name); + } + private File toJsonManifestFile(File dir){ + String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION; + return new File(dir, name); + } + private File toResourceIdsXml(File dir){ + String name = "public.xml"; + return new File(dir, name); + } + private File toUncompressedJsonFile(File dir){ + return new File(dir, UncompressedFiles.JSON_FILE); + } + private File toJsonTableSplitDir(File dir){ + return new File(dir, ApkUtil.SPLIT_JSON_DIRECTORY); + } + private File toResJsonDir(File dir){ + return new File(dir, ApkUtil.RES_JSON_NAME); + } + private File toRootDir(File dir){ + return new File(dir, ApkUtil.ROOT_NAME); + } + + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + private void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkModule.java b/src/ARSCLib/com/reandroid/apk/ApkModule.java new file mode 100644 index 00000000..14a98765 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkModule.java @@ -0,0 +1,838 @@ +/* + * 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.*; +import com.reandroid.archive2.Archive; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.archive2.writer.ApkWriter; +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.array.PackageArray; +import com.reandroid.arsc.chunk.Chunk; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +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.Decoder; +import com.reandroid.arsc.group.StringGroup; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.util.FrameworkTable; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; +import com.reandroid.xml.XMLException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.zip.ZipEntry; + +public class ApkModule implements ApkFile { + private final String moduleName; + private final APKArchive apkArchive; + private boolean loadDefaultFramework = true; + private boolean mDisableLoadFramework = false; + private TableBlock mTableBlock; + private AndroidManifestBlock mManifestBlock; + private final UncompressedFiles mUncompressedFiles; + private APKLogger apkLogger; + private Decoder mDecoder; + private ApkType mApkType; + private ApkSignatureBlock apkSignatureBlock; + private Integer preferredFramework; + + public ApkModule(String moduleName, APKArchive apkArchive){ + this.moduleName=moduleName; + this.apkArchive=apkArchive; + this.mUncompressedFiles=new UncompressedFiles(); + this.mUncompressedFiles.addPath(apkArchive); + } + + public ApkSignatureBlock getApkSignatureBlock() { + return apkSignatureBlock; + } + public void setApkSignatureBlock(ApkSignatureBlock apkSignatureBlock) { + this.apkSignatureBlock = apkSignatureBlock; + } + + public boolean hasSignatureBlock(){ + return getApkSignatureBlock() != null; + } + + public void dumpSignatureInfoFiles(File directory) throws IOException{ + ApkSignatureBlock apkSignatureBlock = getApkSignatureBlock(); + if(apkSignatureBlock == null){ + throw new IOException("Don't have signature block"); + } + apkSignatureBlock.writeSplitRawToDirectory(directory); + } + public void dumpSignatureBlock(File file) throws IOException{ + ApkSignatureBlock apkSignatureBlock = getApkSignatureBlock(); + if(apkSignatureBlock == null){ + throw new IOException("Don't have signature block"); + } + apkSignatureBlock.writeRaw(file); + } + + public void scanSignatureInfoFiles(File directory) throws IOException{ + if(!directory.isDirectory()){ + throw new IOException("No such directory: " + directory); + } + ApkSignatureBlock apkSignatureBlock = this.apkSignatureBlock; + if(apkSignatureBlock == null){ + apkSignatureBlock = new ApkSignatureBlock(); + } + apkSignatureBlock.scanSplitFiles(directory); + setApkSignatureBlock(apkSignatureBlock); + } + public void loadSignatureBlock(File file) throws IOException{ + if(!file.isFile()){ + throw new IOException("No such file: " + file); + } + ApkSignatureBlock apkSignatureBlock = this.apkSignatureBlock; + if(apkSignatureBlock == null){ + apkSignatureBlock = new ApkSignatureBlock(); + } + apkSignatureBlock.read(file); + setApkSignatureBlock(apkSignatureBlock); + } + + public String getSplit(){ + if(!hasAndroidManifestBlock()){ + return null; + } + return getAndroidManifestBlock().getSplit(); + } + public FrameworkApk initializeAndroidFramework(TableBlock tableBlock, Integer version) throws IOException { + if(tableBlock == null || isAndroid(tableBlock)){ + return null; + } + List frameWorkList = tableBlock.getFrameWorks(); + for(TableBlock frameWork:frameWorkList){ + if(isAndroid(frameWork)){ + ApkFile apkFile = frameWork.getApkFile(); + if(apkFile instanceof FrameworkApk){ + return (FrameworkApk) apkFile; + } + return null; + } + } + logMessage("Initializing android framework ..."); + FrameworkApk frameworkApk; + if(version==null){ + logMessage("Can not read framework version, loading latest"); + frameworkApk = AndroidFrameworks.getLatest(); + }else { + logMessage("Loading android framework for version: " + version); + frameworkApk = AndroidFrameworks.getBestMatch(version); + } + FrameworkTable frameworkTable = frameworkApk.getTableBlock(); + tableBlock.addFramework(frameworkTable); + logMessage("Initialized framework: "+frameworkApk.getName()); + return frameworkApk; + } + private boolean isAndroid(TableBlock tableBlock){ + if(tableBlock instanceof FrameworkTable){ + FrameworkTable frameworkTable = (FrameworkTable) tableBlock; + return frameworkTable.isAndroid(); + } + return false; + } + + public FrameworkApk initializeAndroidFramework(XmlPullParser parser) throws IOException { + Map manifestAttributes; + try { + manifestAttributes = XmlHelper.readAttributes(parser, AndroidManifestBlock.TAG_manifest); + } catch (XmlPullParserException ex) { + throw new IOException(ex); + } + if(manifestAttributes == null){ + throw new IOException("Invalid AndroidManifest, missing element: '" + + AndroidManifestBlock.TAG_manifest + "'"); + } + return initializeAndroidFramework(manifestAttributes); + } + public FrameworkApk initializeAndroidFramework(Map manifestAttributes) throws IOException { + String coreApp = manifestAttributes.get(AndroidManifestBlock.NAME_coreApp); + String packageName = manifestAttributes.get(AndroidManifestBlock.NAME_PACKAGE); + if("true".equals(coreApp) && "android".equals(packageName)){ + logMessage("Looks framework itself, skip loading frameworks"); + return null; + } + String compileSdkVersion = manifestAttributes.get(AndroidManifestBlock.NAME_compileSdkVersion); + if(compileSdkVersion == null){ + logMessage("Missing attribute: '" + AndroidManifestBlock.NAME_compileSdkVersion + "', skip loading frameworks"); + return null; + } + int version; + try{ + version = Integer.parseInt(compileSdkVersion); + }catch (NumberFormatException exception){ + logMessage("NumberFormatException on reading: '" + + AndroidManifestBlock.NAME_compileSdkVersion + "=\"" + + compileSdkVersion +"\"' : " + exception.getMessage()); + return null; + } + TableBlock tableBlock = getTableBlock(false); + return initializeAndroidFramework(tableBlock, version); + } + public FrameworkApk initializeAndroidFramework(XMLDocument xmlDocument) throws IOException { + TableBlock tableBlock = getTableBlock(false); + if(isAndroidCoreApp(xmlDocument)){ + logMessage("Looks framework itself, skip loading frameworks"); + return null; + } + Integer version = readCompileVersionCode(xmlDocument); + return initializeAndroidFramework(tableBlock, version); + } + private boolean isAndroidCoreApp(XMLDocument manifestDocument){ + XMLElement root = manifestDocument.getDocumentElement(); + if(root == null){ + return false; + } + if(!"android".equals(root.getAttributeValue("package"))){ + return false; + } + String coreApp = root.getAttributeValue("coreApp"); + return "true".equals(coreApp); + } + private Integer readCompileVersionCode(XMLDocument manifestDocument) { + XMLElement root = manifestDocument.getDocumentElement(); + String versionString = readVersionCodeString(root); + if(versionString==null){ + return null; + } + try{ + return Integer.parseInt(versionString); + }catch (NumberFormatException exception){ + logMessage("NumberFormatException on manifest version reading: '" + +versionString+"': "+exception.getMessage()); + return null; + } + } + private String readVersionCodeString(XMLElement manifestRoot){ + String versionString = manifestRoot.getAttributeValue("android:compileSdkVersion"); + if(versionString!=null){ + return versionString; + } + versionString = manifestRoot.getAttributeValue("platformBuildVersionCode"); + if(versionString!=null){ + return versionString; + } + for(XMLElement element:manifestRoot.listChildElements()){ + if(AndroidManifestBlock.TAG_uses_sdk.equals(element.getTagName())){ + versionString = element.getAttributeValue("android:targetSdkVersion"); + if(versionString!=null){ + return versionString; + } + } + } + return null; + } + + public void setPreferredFramework(Integer version) throws IOException { + if(version!=null && version.equals(preferredFramework)){ + return; + } + this.preferredFramework = version; + if(version == null || mTableBlock==null){ + return; + } + logMessage("Initializing preferred framework: " + version); + mTableBlock.clearFrameworks(); + FrameworkApk frameworkApk = AndroidFrameworks.getBestMatch(version); + AndroidFrameworks.setCurrent(frameworkApk); + mTableBlock.addFramework(frameworkApk.getTableBlock()); + logMessage("Initialized framework: " + frameworkApk.getVersionCode()); + } + + public Integer getAndroidFrameworkVersion(){ + if(preferredFramework != null){ + return preferredFramework; + } + if(!hasAndroidManifestBlock()){ + return null; + } + AndroidManifestBlock manifestBlock = getAndroidManifestBlock(); + Integer version = manifestBlock.getCompileSdkVersion(); + if(version == null){ + version = manifestBlock.getPlatformBuildVersionCode(); + } + if(version == null){ + version = manifestBlock.getTargetSdkVersion(); + } + return version; + } + public void removeResFilesWithEntry(int resourceId) { + removeResFilesWithEntry(resourceId, null, true); + } + public void removeResFilesWithEntry(int resourceId, ResConfig resConfig, boolean trimEntryArray) { + List removedList = removeResFiles(resourceId, resConfig); + SpecTypePair specTypePair = null; + for(Entry entry:removedList){ + if(entry == null || entry.isNull()){ + continue; + } + if(trimEntryArray && specTypePair==null){ + specTypePair = entry.getTypeBlock().getParentSpecTypePair(); + } + entry.setNull(true); + } + if(specTypePair!=null){ + specTypePair.removeNullEntries(resourceId); + } + } + public List removeResFiles(int resourceId) { + return removeResFiles(resourceId, null); + } + public List removeResFiles(int resourceId, ResConfig resConfig) { + List results = new ArrayList<>(); + if(resourceId == 0 && resConfig==null){ + return results; + } + List resFileList = listResFiles(resourceId, resConfig); + APKArchive archive = getApkArchive(); + for(ResFile resFile:resFileList){ + results.addAll(resFile.getEntryList()); + String path = resFile.getFilePath(); + archive.remove(path); + } + return results; + } + public XMLDocument decodeXMLFile(String path) throws IOException, XMLException { + ResXmlDocument resXmlDocument = loadResXmlDocument(path); + AndroidManifestBlock manifestBlock = getAndroidManifestBlock(); + int pkgId = manifestBlock.guessCurrentPackageId(); + return resXmlDocument.decodeToXml(getTableBlock(), pkgId); + } + public List listDexFiles(){ + List results=new ArrayList<>(); + for(InputSource source:getApkArchive().listInputSources()){ + if(DexFileInputSource.isDexName(source.getAlias())){ + results.add(new DexFileInputSource(source.getAlias(), source)); + } + } + DexFileInputSource.sort(results); + return results; + } + public boolean isBaseModule(){ + if(!hasAndroidManifestBlock()){ + return false; + } + AndroidManifestBlock manifestBlock; + try { + manifestBlock=getAndroidManifestBlock(); + return manifestBlock.getMainActivity()!=null; + } catch (Exception ignored) { + return false; + } + } + public String getModuleName(){ + return moduleName; + } + public void writeApk(File file) throws IOException { + writeApk(file, null); + } + public void writeApk(File file, WriteProgress progress) throws IOException { + writeApk(file, progress, null); + } + public void writeApk(File file, WriteProgress progress, WriteInterceptor interceptor) throws IOException { + APKArchive archive = getApkArchive(); + UncompressedFiles uf = getUncompressedFiles(); + uf.apply(archive); + ApkWriter apkWriter = new ApkWriter(file, archive.listInputSources()); + apkWriter.setAPKLogger(getApkLogger()); + apkWriter.setWriteProgress(progress); + apkWriter.setApkSignatureBlock(getApkSignatureBlock()); + apkWriter.write(); + apkWriter.close(); + } + public void uncompressNonXmlResFiles() { + for(ResFile resFile:listResFiles()){ + if(resFile.isBinaryXml()){ + continue; + } + resFile.getInputSource().setMethod(ZipEntry.STORED); + } + } + public UncompressedFiles getUncompressedFiles(){ + return mUncompressedFiles; + } + public void removeDir(String dirName){ + getApkArchive().removeDir(dirName); + } + public void validateResourcesDir() { + List resFileList = listResFiles(); + Set existPaths=new HashSet<>(); + List sourceList = getApkArchive().listInputSources(); + for(InputSource inputSource:sourceList){ + existPaths.add(inputSource.getAlias()); + } + for(ResFile resFile:resFileList){ + String path=resFile.getFilePath(); + String pathNew=resFile.validateTypeDirectoryName(); + if(pathNew==null || pathNew.equals(path)){ + continue; + } + if(existPaths.contains(pathNew)){ + continue; + } + existPaths.remove(path); + existPaths.add(pathNew); + resFile.setFilePath(pathNew); + if(resFile.getInputSource().getMethod() == ZipEntry.STORED){ + getUncompressedFiles().replacePath(path, pathNew); + } + logVerbose("Dir validated: '"+path+"' -> '"+pathNew+"'"); + } + TableStringPool stringPool= getTableBlock().getStringPool(); + stringPool.refreshUniqueIdMap(); + getTableBlock().refresh(); + } + public void setResourcesRootDir(String dirName) { + List resFileList = listResFiles(); + Set existPaths=new HashSet<>(); + List sourceList = getApkArchive().listInputSources(); + for(InputSource inputSource:sourceList){ + existPaths.add(inputSource.getAlias()); + } + for(ResFile resFile:resFileList){ + String path=resFile.getFilePath(); + String pathNew=ApkUtil.replaceRootDir(path, dirName); + if(existPaths.contains(pathNew)){ + continue; + } + existPaths.remove(path); + existPaths.add(pathNew); + resFile.setFilePath(pathNew); + if(resFile.getInputSource().getMethod() == ZipEntry.STORED){ + getUncompressedFiles().replacePath(path, pathNew); + } + logVerbose("Root changed: '"+path+"' -> '"+pathNew+"'"); + } + TableStringPool stringPool= getTableBlock().getStringPool(); + stringPool.refreshUniqueIdMap(); + getTableBlock().refresh(); + } + public List listResFiles() { + return listResFiles(0, null); + } + public List listResFiles(int resourceId, ResConfig resConfig) { + List results=new ArrayList<>(); + TableBlock tableBlock=getTableBlock(); + if (tableBlock==null){ + return results; + } + TableStringPool stringPool= tableBlock.getStringPool(); + for(InputSource inputSource:getApkArchive().listInputSources()){ + String name=inputSource.getAlias(); + StringGroup groupTableString = stringPool.get(name); + if(groupTableString==null){ + continue; + } + for(TableString tableString:groupTableString.listItems()){ + List entryList = filterResFileEntries( + tableString.listReferencedResValueEntries(), resourceId, resConfig); + if(entryList.size()==0){ + continue; + } + ResFile resFile = new ResFile(inputSource, entryList); + results.add(resFile); + } + } + return results; + } + private List filterResFileEntries(List entryList, int resourceId, ResConfig resConfig){ + if(resourceId == 0 && resConfig == null || entryList.size()==0){ + return entryList; + } + List results = new ArrayList<>(); + for(Entry entry:entryList){ + if(entry==null || entry.isNull()){ + continue; + } + if(resourceId!=0 && resourceId!=entry.getResourceId()){ + continue; + } + if(resConfig!=null && !resConfig.equals(entry.getResConfig())){ + continue; + } + results.add(entry); + } + return results; + } + public String getPackageName(){ + if(hasAndroidManifestBlock()){ + return getAndroidManifestBlock().getPackageName(); + } + if(!hasTableBlock()){ + return null; + } + TableBlock tableBlock=getTableBlock(); + PackageArray pkgArray = tableBlock.getPackageArray(); + PackageBlock pkg = pkgArray.get(0); + if(pkg==null){ + return null; + } + return pkg.getName(); + } + public void setPackageName(String name) { + String old=getPackageName(); + if(hasAndroidManifestBlock()){ + getAndroidManifestBlock().setPackageName(name); + } + if(!hasTableBlock()){ + return; + } + TableBlock tableBlock=getTableBlock(); + PackageArray pkgArray = tableBlock.getPackageArray(); + for(PackageBlock pkg:pkgArray.listItems()){ + if(pkgArray.childesCount()==1){ + pkg.setName(name); + continue; + } + String pkgName=pkg.getName(); + if(pkgName.startsWith(old)){ + pkgName=pkgName.replace(old, name); + pkg.setName(pkgName); + } + } + } + public boolean hasAndroidManifestBlock(){ + return mManifestBlock!=null + || getApkArchive().getInputSource(AndroidManifestBlock.FILE_NAME)!=null; + } + public boolean hasTableBlock(){ + return mTableBlock!=null + || getApkArchive().getInputSource(TableBlock.FILE_NAME)!=null; + } + public void destroy(){ + getApkArchive().clear(); + AndroidManifestBlock manifestBlock = this.mManifestBlock; + if(manifestBlock!=null){ + manifestBlock.destroy(); + this.mManifestBlock = null; + } + TableBlock tableBlock = this.mTableBlock; + if(tableBlock!=null){ + tableBlock.destroy(); + this.mTableBlock = null; + } + } + public void setManifest(AndroidManifestBlock manifestBlock){ + APKArchive archive = getApkArchive(); + if(manifestBlock==null){ + mManifestBlock = null; + archive.remove(AndroidManifestBlock.FILE_NAME); + return; + } + manifestBlock.setApkFile(this); + BlockInputSource source = + new BlockInputSource<>(AndroidManifestBlock.FILE_NAME, manifestBlock); + archive.add(source); + mManifestBlock = manifestBlock; + } + public void setTableBlock(TableBlock tableBlock){ + APKArchive archive = getApkArchive(); + if(tableBlock == null){ + mTableBlock = null; + archive.remove(TableBlock.FILE_NAME); + return; + } + tableBlock.setApkFile(this); + BlockInputSource source = + new BlockInputSource<>(TableBlock.FILE_NAME, tableBlock); + archive.add(source); + source.setMethod(ZipEntry.STORED); + getUncompressedFiles().addPath(source); + mTableBlock = tableBlock; + } + @Override + public AndroidManifestBlock getAndroidManifestBlock() { + if(mManifestBlock!=null){ + return mManifestBlock; + } + APKArchive archive=getApkArchive(); + InputSource inputSource = archive.getInputSource(AndroidManifestBlock.FILE_NAME); + if(inputSource==null){ + return null; + } + InputStream inputStream = null; + try { + inputStream = inputSource.openStream(); + AndroidManifestBlock manifestBlock=AndroidManifestBlock.load(inputStream); + inputStream.close(); + BlockInputSource blockInputSource=new BlockInputSource<>(inputSource.getName(),manifestBlock); + blockInputSource.setSort(inputSource.getSort()); + blockInputSource.setMethod(inputSource.getMethod()); + archive.add(blockInputSource); + manifestBlock.setApkFile(this); + TableBlock tableBlock = this.mTableBlock; + if(tableBlock != null){ + int packageId = manifestBlock.guessCurrentPackageId(); + if(packageId != 0){ + manifestBlock.setPackageBlock(tableBlock.pickOne(packageId)); + }else { + manifestBlock.setPackageBlock(tableBlock.pickOne()); + } + } + mManifestBlock = manifestBlock; + onManifestBlockLoaded(manifestBlock); + } catch (IOException exception) { + throw new IllegalArgumentException(exception); + } + return mManifestBlock; + } + private void onManifestBlockLoaded(AndroidManifestBlock manifestBlock){ + initializeApkType(manifestBlock); + } + public TableBlock getTableBlock(boolean initFramework) { + if(mTableBlock==null){ + if(!hasTableBlock()){ + return null; + } + try { + mTableBlock = loadTableBlock(); + if(initFramework && loadDefaultFramework){ + Integer version = getAndroidFrameworkVersion(); + initializeAndroidFramework(mTableBlock, version); + } + } catch (IOException exception) { + throw new IllegalArgumentException(exception); + } + } + return mTableBlock; + } + @Override + public TableBlock getTableBlock() { + return getTableBlock(!mDisableLoadFramework); + } + @Override + public ResXmlDocument loadResXmlDocument(String path) throws IOException{ + InputSource inputSource = getApkArchive().getInputSource(path); + if(inputSource==null){ + throw new FileNotFoundException("No such file in apk: " + path); + } + return loadResXmlDocument(inputSource); + } + public ResXmlDocument loadResXmlDocument(InputSource inputSource) throws IOException{ + ResXmlDocument resXmlDocument = new ResXmlDocument(); + resXmlDocument.setApkFile(this); + resXmlDocument.readBytes(inputSource.openStream()); + return resXmlDocument; + } + @Override + public Decoder getDecoder(){ + return mDecoder; + } + @Override + public void setDecoder(Decoder decoder){ + this.mDecoder = decoder; + } + public ApkType getApkType(){ + if(mApkType!=null){ + return mApkType; + } + return initializeApkType(mManifestBlock); + } + public void setApkType(ApkType apkType){ + this.mApkType = apkType; + } + private ApkType initializeApkType(AndroidManifestBlock manifestBlock){ + if(mApkType!=null){ + return mApkType; + } + ApkType apkType = null; + if(manifestBlock!=null){ + apkType = manifestBlock.guessApkType(); + } + if(apkType != null){ + mApkType = apkType; + }else { + apkType = ApkType.UNKNOWN; + } + return apkType; + } + + // If we need TableStringPool only, this loads pool without + // loading packages and other chunk blocks for faster and less memory usage + public TableStringPool getVolatileTableStringPool() throws IOException{ + if(mTableBlock!=null){ + return mTableBlock.getStringPool(); + } + InputSource inputSource = getApkArchive() + .getInputSource(TableBlock.FILE_NAME); + if(inputSource==null){ + throw new IOException("Module don't have: "+TableBlock.FILE_NAME); + } + if((inputSource instanceof ZipEntrySource) + ||(inputSource instanceof FileInputSource)){ + InputStream inputStream = inputSource.openStream(); + TableStringPool stringPool = TableStringPool.readFromTable(inputStream); + inputStream.close(); + return stringPool; + } + return getTableBlock().getStringPool(); + } + TableBlock loadTableBlock() throws IOException { + APKArchive archive=getApkArchive(); + InputSource inputSource = archive.getInputSource(TableBlock.FILE_NAME); + if(inputSource==null){ + throw new IOException("Entry not found: "+TableBlock.FILE_NAME); + } + TableBlock tableBlock; + if(inputSource instanceof SplitJsonTableInputSource){ + tableBlock=((SplitJsonTableInputSource)inputSource).getTableBlock(); + }else if(inputSource instanceof SingleJsonTableInputSource){ + tableBlock=((SingleJsonTableInputSource)inputSource).getTableBlock(); + }else if(inputSource instanceof BlockInputSource){ + Chunk block = ((BlockInputSource) inputSource).getBlock(); + tableBlock = (TableBlock) block; + }else { + InputStream inputStream = inputSource.openStream(); + tableBlock = TableBlock.load(inputStream); + inputStream.close(); + } + BlockInputSource blockInputSource=new BlockInputSource<>(inputSource.getName(), tableBlock); + blockInputSource.setMethod(inputSource.getMethod()); + blockInputSource.setSort(inputSource.getSort()); + archive.add(blockInputSource); + tableBlock.setApkFile(this); + return tableBlock; + } + public APKArchive getApkArchive() { + return apkArchive; + } + public void setLoadDefaultFramework(boolean loadDefaultFramework) { + this.loadDefaultFramework = loadDefaultFramework; + this.mDisableLoadFramework = !loadDefaultFramework; + } + + public void merge(ApkModule module) throws IOException { + if(module==null||module==this){ + return; + } + logMessage("Merging: "+module.getModuleName()); + mergeDexFiles(module); + mergeTable(module); + mergeFiles(module); + getUncompressedFiles().merge(module.getUncompressedFiles()); + } + private void mergeTable(ApkModule module) { + if(!module.hasTableBlock()){ + return; + } + logMessage("Merging resource table: "+module.getModuleName()); + TableBlock exist; + if(!hasTableBlock()){ + exist=new TableBlock(); + BlockInputSource inputSource=new BlockInputSource<>(TableBlock.FILE_NAME, exist); + getApkArchive().add(inputSource); + }else{ + exist=getTableBlock(); + } + TableBlock coming=module.getTableBlock(); + exist.merge(coming); + } + private void mergeFiles(ApkModule module) { + APKArchive archiveExist = getApkArchive(); + APKArchive archiveComing = module.getApkArchive(); + Map comingAlias=ApkUtil.toAliasMap(archiveComing.listInputSources()); + Map existAlias=ApkUtil.toAliasMap(archiveExist.listInputSources()); + UncompressedFiles uncompressedFiles = module.getUncompressedFiles(); + for(InputSource inputSource:comingAlias.values()){ + if(existAlias.containsKey(inputSource.getAlias())||existAlias.containsKey(inputSource.getName())){ + continue; + } + if(DexFileInputSource.isDexName(inputSource.getName())){ + continue; + } + if (inputSource.getAlias().startsWith("lib/")){ + uncompressedFiles.removePath(inputSource.getAlias()); + } + logVerbose("Added: "+inputSource.getAlias()); + archiveExist.add(inputSource); + } + } + private void mergeDexFiles(ApkModule module){ + UncompressedFiles uncompressedFiles=module.getUncompressedFiles(); + List existList=listDexFiles(); + List comingList=module.listDexFiles(); + APKArchive archive=getApkArchive(); + int index=0; + if(existList.size()>0){ + index=existList.get(existList.size()-1).getDexNumber(); + if(index==0){ + index=2; + }else { + index++; + } + } + for(DexFileInputSource source:comingList){ + uncompressedFiles.removePath(source.getAlias()); + String name= DexFileInputSource.getDexName(index); + DexFileInputSource add=new DexFileInputSource(name, source.getInputSource()); + archive.add(add); + logMessage("Added ["+module.getModuleName()+"] " + +source.getAlias()+" -> "+name); + index++; + if(index==1){ + index=2; + } + } + } + APKLogger getApkLogger(){ + return apkLogger; + } + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } + @Override + public String toString(){ + return getModuleName(); + } + public static ApkModule loadApkFile(File apkFile) throws IOException { + return loadApkFile(apkFile, ApkUtil.DEF_MODULE_NAME); + } + public static ApkModule loadApkFile(File apkFile, String moduleName) throws IOException { + Archive archive = new Archive(apkFile); + ApkModule apkModule = new ApkModule(moduleName, archive.createAPKArchive()); + apkModule.setApkSignatureBlock(archive.getApkSignatureBlock()); + return apkModule; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java b/src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java new file mode 100644 index 00000000..1881bfdd --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java @@ -0,0 +1,320 @@ +/* + * 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.*; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.value.*; +import com.reandroid.identifiers.PackageIdentifier; +import com.reandroid.json.JSONObject; +import com.reandroid.xml.XMLDocument; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.function.Predicate; + +public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate { + private final ApkModule apkModule; + private final Map> decodedEntries; + + private ResXmlDocumentSerializer documentSerializer; + private XMLEntryDecoderSerializer entrySerializer; + + + public ApkModuleXmlDecoder(ApkModule apkModule){ + super(); + this.apkModule = apkModule; + this.decodedEntries = new HashMap<>(); + super.setApkLogger(apkModule.getApkLogger()); + } + public void sanitizeFilePaths(){ + PathSanitizer sanitizer = PathSanitizer.create(apkModule); + sanitizer.sanitize(); + } + @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); + + TableBlock tableBlock = apkModule.getTableBlock(); + + this.entrySerializer = new XMLEntryDecoderSerializer(tableBlock); + this.entrySerializer.setDecodedEntries(this); + + decodeAndroidManifest(outDir, apkModule.getAndroidManifestBlock()); + decodeTableBlock(outDir, tableBlock); + + logMessage("Decoding resource files ..."); + List resFileList = apkModule.listResFiles(); + for(ResFile resFile:resFileList){ + decodeResFile(outDir, resFile); + } + decodeValues(outDir, tableBlock); + + extractRootFiles(outDir); + + writePathMap(outDir, apkModule.getApkArchive().listInputSources()); + + dumpSignatures(outDir, apkModule.getApkSignatureBlock()); + } + 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); + } + } + private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException { + for(PackageBlock packageBlock:tableBlock.listPackages()){ + decodePackageInfo(outDir, packageBlock); + } + } + private void decodePackageInfo(File outDir, PackageBlock packageBlock) throws IOException { + File pkgDir = new File(outDir, getPackageDirName(packageBlock)); + File packageJsonFile = new File(pkgDir, PackageBlock.JSON_FILE_NAME); + JSONObject jsonObject = packageBlock.toJson(false); + jsonObject.write(packageJsonFile); + } + private void decodeUncompressedFiles(File outDir) + throws IOException { + File file=new File(outDir, UncompressedFiles.JSON_FILE); + UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles(); + uncompressedFiles.toJson().write(file); + } + private void decodeResFile(File outDir, ResFile resFile) + throws IOException{ + if(resFile.isBinaryXml()){ + decodeResXml(outDir, resFile); + }else { + decodeResRaw(outDir, resFile); + } + addDecodedPath(resFile.getFilePath()); + } + private void decodeResRaw(File outDir, ResFile resFile) + throws IOException { + Entry entry = resFile.pickOne(); + PackageBlock packageBlock= entry.getPackageBlock(); + + File pkgDir=new File(outDir, getPackageDirName(packageBlock)); + String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); + String path = alias.replace('/', File.separatorChar); + File file=new File(pkgDir, path); + File dir=file.getParentFile(); + if(!dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + resFile.getInputSource().write(outputStream); + outputStream.close(); + resFile.setFilePath(alias); + + addDecodedEntry(entry); + } + private void decodeResXml(File outDir, ResFile resFile) + throws IOException{ + Entry entry = resFile.pickOne(); + PackageBlock packageBlock = entry.getPackageBlock(); + + File pkgDir = new File(outDir, getPackageDirName(packageBlock)); + String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); + String path = alias.replace('/', File.separatorChar); + path = path.replace('/', File.separatorChar); + File file = new File(pkgDir, path); + + logVerbose("Decoding: " + path); + serializeXml(packageBlock.getId(), resFile.getInputSource(), file); + + resFile.setFilePath(alias); + addDecodedEntry(entry); + } + private ResXmlDocumentSerializer getDocumentSerializer(){ + if(documentSerializer == null){ + documentSerializer = new ResXmlDocumentSerializer(apkModule); + documentSerializer.setValidateXmlNamespace(true); + } + return documentSerializer; + } + private void decodePublicXml(TableBlock tableBlock, File outDir) + throws IOException{ + for(PackageBlock packageBlock:tableBlock.listPackages()){ + decodePublicXml(packageBlock, outDir); + } + if(tableBlock.getPackageArray().childesCount()==0){ + decodeEmptyTable(outDir); + } + } + private void decodeEmptyTable(File outDir) throws IOException { + logMessage("Decoding empty table ..."); + String pkgName = apkModule.getPackageName(); + if(pkgName==null){ + return; + } + File pkgDir = new File(outDir, "0-"+pkgName); + File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME); + File values = new File(resDir, "values"); + File pubXml = new File(values, ApkUtil.FILE_NAME_PUBLIC_XML); + XMLDocument xmlDocument = new XMLDocument("resources"); + xmlDocument.save(pubXml, false); + } + private void decodePublicXml(PackageBlock packageBlock, File outDir) + throws IOException { + String packageDirName=getPackageDirName(packageBlock); + logMessage("Decoding public.xml: "+packageDirName); + File file=new File(outDir, packageDirName); + file=new File(file, ApkUtil.RES_DIR_NAME); + file=new File(file, "values"); + file=new File(file, ApkUtil.FILE_NAME_PUBLIC_XML); + PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.load(packageBlock); + packageIdentifier.writePublicXml(file); + } + 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()); + int currentPackageId = manifestBlock.guessCurrentPackageId(); + serializeXml(currentPackageId, manifestBlock, file); + addDecodedPath(AndroidManifestBlock.FILE_NAME); + } + private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile) + throws IOException { + XMLNamespaceValidator.validateNamespaces(document); + 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 { + 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; + } + int resourceId= entry.getResourceId(); + Set resConfigSet=decodedEntries.get(resourceId); + if(resConfigSet==null){ + resConfigSet=new HashSet<>(); + decodedEntries.put(resourceId, resConfigSet); + } + resConfigSet.add(entry.getResConfig()); + } + private boolean containsDecodedEntry(Entry entry){ + Set resConfigSet=decodedEntries.get(entry.getResourceId()); + if(resConfigSet==null){ + return false; + } + return resConfigSet.contains(entry.getResConfig()); + } + private void decodeValues(File outDir, TableBlock tableBlock) throws IOException { + for(PackageBlock packageBlock:tableBlock.listPackages()){ + decodeValues(outDir, packageBlock); + } + } + private void decodeValues(File outDir, PackageBlock packageBlock) throws IOException { + logMessage("Decoding values: " + + getPackageDirName(packageBlock)); + + packageBlock.sortTypes(); + + 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(File outDir, SpecTypePair specTypePair) throws IOException { + entrySerializer.decode(outDir, specTypePair); + } + private String getPackageDirName(PackageBlock packageBlock){ + String name = ApkUtil.sanitizeForFileName(packageBlock.getName()); + if(name==null){ + name="package"; + } + TableBlock tableBlock = packageBlock.getTableBlock(); + int index = packageBlock.getIndex(); + String prefix; + if(index < 10 && tableBlock.countPackages() > 10){ + prefix = "0" + index; + }else { + prefix = Integer.toString(index); + } + return prefix + "-" + name; + } + private void extractRootFiles(File outDir) throws IOException { + logMessage("Extracting root files"); + File rootDir = new File(outDir, "root"); + for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){ + if(containsDecodedPath(inputSource.getAlias())){ + continue; + } + extractRootFiles(rootDir, inputSource); + addDecodedPath(inputSource.getAlias()); + } + } + private void extractRootFiles(File rootDir, InputSource inputSource) throws IOException { + String path=inputSource.getAlias(); + path=path.replace(File.separatorChar, '/'); + File file=new File(rootDir, path); + File dir=file.getParentFile(); + if(!dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); + } + @Override + public boolean test(Entry entry) { + return !containsDecodedEntry(entry); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java b/src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java new file mode 100644 index 00000000..f4be9b7e --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java @@ -0,0 +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. + */ +package com.reandroid.apk; + +import com.reandroid.archive.APKArchive; +import com.reandroid.archive.FileInputSource; +import com.reandroid.apk.xmlencoder.RESEncoder; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.json.JSONArray; +import com.reandroid.xml.XMLException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; + +public class ApkModuleXmlEncoder { + private final RESEncoder resEncoder; + public ApkModuleXmlEncoder(){ + this.resEncoder = new RESEncoder(); + } + public ApkModuleXmlEncoder(ApkModule module, TableBlock tableBlock){ + this.resEncoder = new RESEncoder(module, tableBlock); + } + public void scanDirectory(File mainDirectory) throws IOException, XMLException { + loadUncompressedFiles(mainDirectory); + resEncoder.scanDirectory(mainDirectory); + File rootDir=new File(mainDirectory, "root"); + scanRootDir(rootDir); + restorePathMap(mainDirectory); + restoreSignatures(mainDirectory); + sortFiles(); + TableStringPool tableStringPool = getApkModule().getTableBlock().getTableStringPool(); + tableStringPool.removeUnusedStrings(); + } + private void restoreSignatures(File dir) throws IOException { + File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME); + if(!sigDir.isDirectory()){ + return; + } + ApkModule apkModule = getApkModule(); + apkModule.logMessage("Loading signatures ..."); + ApkSignatureBlock signatureBlock = new ApkSignatureBlock(); + signatureBlock.scanSplitFiles(sigDir); + apkModule.setApkSignatureBlock(signatureBlock); + } + private void restorePathMap(File dir) throws IOException{ + File file = new File(dir, PathMap.JSON_FILE); + if(!file.isFile()){ + return; + } + PathMap pathMap = new PathMap(); + JSONArray jsonArray = new JSONArray(file); + pathMap.fromJson(jsonArray); + pathMap.restore(getApkModule()); + } + public ApkModule getApkModule(){ + return resEncoder.getApkModule(); + } + + private void scanRootDir(File rootDir){ + APKArchive archive=getApkModule().getApkArchive(); + List rootFileList=ApkUtil.recursiveFiles(rootDir); + for(File file:rootFileList){ + String path=ApkUtil.toArchivePath(rootDir, file); + FileInputSource inputSource=new FileInputSource(file, path); + archive.add(inputSource); + } + } + private void sortFiles(){ + APKArchive archive = getApkModule().getApkArchive(); + archive.autoSortApkFiles(); + } + private void loadUncompressedFiles(File mainDirectory) throws IOException { + File file=new File(mainDirectory, UncompressedFiles.JSON_FILE); + UncompressedFiles uncompressedFiles = getApkModule().getUncompressedFiles(); + uncompressedFiles.fromJson(file); + } + public void setApkLogger(APKLogger apkLogger) { + this.resEncoder.setAPKLogger(apkLogger); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ApkUtil.java b/src/ARSCLib/com/reandroid/apk/ApkUtil.java new file mode 100644 index 00000000..d01bec3a --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ApkUtil.java @@ -0,0 +1,200 @@ +/* + * 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.util.*; + +public class ApkUtil { + public static String sanitizeForFileName(String name){ + if(name==null){ + return null; + } + StringBuilder builder = new StringBuilder(); + char[] chars = name.toCharArray(); + boolean skipNext = true; + int length = 0; + int lengthMax = MAX_FILE_NAME_LENGTH; + for(int i=0;i=lengthMax){ + break; + } + char ch = chars[i]; + if(isGoodFileNameSymbol(ch)){ + if(!skipNext){ + builder.append(ch); + length++; + } + skipNext=true; + continue; + } + if(!isGoodFileNameChar(ch)){ + skipNext = true; + continue; + } + builder.append(ch); + length++; + skipNext=false; + } + if(length==0){ + return null; + } + return builder.toString(); + } + private static boolean isGoodFileNameSymbol(char ch){ + return ch == '.' + || ch == '+' + || ch == '-' + || ch == '_' + || ch == '#'; + } + private static boolean isGoodFileNameChar(char ch){ + return (ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'Z') + || (ch >= 'a' && ch <= 'z'); + } + public static int parseHex(String hex){ + long l=Long.decode(hex); + return (int) l; + } + public static String replaceRootDir(String path, String dirName){ + int i=path.indexOf('/')+1; + path=path.substring(i); + if(dirName != null && dirName.length()>0){ + if(!dirName.endsWith("/")){ + dirName=dirName+"/"; + } + path=dirName+path; + } + return path; + } + public static String toArchiveResourcePath(File dir, File file){ + String path = toArchivePath(dir, file); + if(path.endsWith(ApkUtil.JSON_FILE_EXTENSION)){ + int i2=path.length()- ApkUtil.JSON_FILE_EXTENSION.length(); + path=path.substring(0, i2); + } + return path; + } + public static String toArchivePath(File dir, File file){ + String dirPath = dir.getAbsolutePath()+File.separator; + String path = file.getAbsolutePath().substring(dirPath.length()); + path=path.replace(File.separatorChar, '/'); + return path; + } + public static List recursiveFiles(File dir, String ext){ + List results=new ArrayList<>(); + if(dir.isFile()){ + if(hasExtension(dir, ext)){ + results.add(dir); + } + return results; + } + if(!dir.isDirectory()){ + return results; + } + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isFile()){ + if(!hasExtension(file, ext)){ + continue; + } + results.add(file); + continue; + } + results.addAll(recursiveFiles(file, ext)); + } + return results; + } + public static List recursiveFiles(File dir){ + return recursiveFiles(dir, null); + } + public static List listDirectories(File dir){ + List results=new ArrayList<>(); + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isDirectory()){ + results.add(file); + } + } + return results; + } + public static List listFiles(File dir, String ext){ + List results=new ArrayList<>(); + File[] files=dir.listFiles(); + if(files==null){ + return results; + } + for(File file:files){ + if(file.isFile()){ + if(!hasExtension(file, ext)){ + continue; + } + results.add(file); + } + } + return results; + } + private static boolean hasExtension(File file, String ext){ + if(ext==null){ + return true; + } + String name=file.getName().toLowerCase(); + ext=ext.toLowerCase(); + return name.endsWith(ext); + } + public static String toModuleName(File file){ + String name=file.getName(); + int i=name.lastIndexOf('.'); + if(i>0){ + name=name.substring(0,i); + } + return name; + } + public static Map toAliasMap(Collection sourceList){ + Map results=new HashMap<>(); + for(InputSource inputSource:sourceList){ + results.put(inputSource.getAlias(), inputSource); + } + return results; + } + public static final String JSON_FILE_EXTENSION=".json"; + public static final String RES_JSON_NAME="res-json"; + public static final String ROOT_NAME="root"; + public static final String SPLIT_JSON_DIRECTORY="resources"; + public static final String DEF_MODULE_NAME="base"; + public static final String NAME_value_type="value_type"; + public static final String NAME_data="data"; + public static final String RES_DIR_NAME="res"; + public static final String FILE_NAME_PUBLIC_XML ="public.xml"; + + public static final String TAG_STRING_ARRAY = "string-array"; + public static final String TAG_INTEGER_ARRAY = "integer-array"; + + public static final String SIGNATURE_FILE_NAME = "signatures" + ApkSignatureBlock.FILE_EXT; + public static final String SIGNATURE_DIR_NAME = "signatures"; + + private static final int MAX_FILE_NAME_LENGTH = 50; +} diff --git a/src/ARSCLib/com/reandroid/apk/BlockInputSource.java b/src/ARSCLib/com/reandroid/apk/BlockInputSource.java new file mode 100644 index 00000000..c5018c9a --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/BlockInputSource.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; + +import com.reandroid.archive.ByteInputSource; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.chunk.Chunk; + +import java.io.IOException; +import java.io.OutputStream; + +public class BlockInputSource> extends ByteInputSource{ + private final T mBlock; + public BlockInputSource(String name, T block) { + super(new byte[0], name); + this.mBlock=block; + } + public T getBlock() { + mBlock.refresh(); + return mBlock; + } + @Override + public long getLength() throws IOException{ + Block block = getBlock(); + return block.countBytes(); + } + @Override + public long getCrc() throws IOException{ + Block block = getBlock(); + CrcOutputStream outputStream=new CrcOutputStream(); + block.writeBytes(outputStream); + return outputStream.getCrcValue(); + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getBlock().writeBytes(outputStream); + } + @Override + public byte[] getBytes() { + return getBlock().getBytes(); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/CrcOutputStream.java b/src/ARSCLib/com/reandroid/apk/CrcOutputStream.java new file mode 100644 index 00000000..b99d5f3f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/CrcOutputStream.java @@ -0,0 +1,53 @@ + /* + * 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 java.io.IOException; +import java.io.OutputStream; +import java.util.zip.CRC32; + +public class CrcOutputStream extends OutputStream { + private final CRC32 crc; + private long length; + private long mCheckSum; + public CrcOutputStream() { + super(); + this.crc = new CRC32(); + } + public long getLength(){ + return length; + } + public long getCrcValue(){ + if(mCheckSum==0){ + mCheckSum=crc.getValue(); + } + return mCheckSum; + } + @Override + public void write(int b) throws IOException { + this.crc.update(b); + length=length+1; + } + @Override + public void write(byte[] b) throws IOException { + this.write(b, 0, b.length); + } + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.crc.update(b, off, len); + length=length+len; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/DexFileInputSource.java b/src/ARSCLib/com/reandroid/apk/DexFileInputSource.java new file mode 100644 index 00000000..195d6990 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/DexFileInputSource.java @@ -0,0 +1,66 @@ + /* + * 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.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DexFileInputSource extends RenamedInputSource implements Comparable{ + public DexFileInputSource(String name, InputSource inputSource){ + super(name, inputSource); + } + public int getDexNumber(){ + return getDexNumber(getAlias()); + } + @Override + public int compareTo(DexFileInputSource source) { + return Integer.compare(getDexNumber(), source.getDexNumber()); + } + public static void sort(List sourceList){ + sourceList.sort(new Comparator() { + @Override + public int compare(DexFileInputSource s1, DexFileInputSource s2) { + return s1.compareTo(s2); + } + }); + } + public static boolean isDexName(String name){ + return getDexNumber(name)>=0; + } + static String getDexName(int i){ + if(i==0){ + return "classes.dex"; + } + return "classes"+i+".dex"; + } + static int getDexNumber(String name){ + Matcher matcher=PATTERN.matcher(name); + if(!matcher.find()){ + return -1; + } + String num=matcher.group(1); + if(num.length()==0){ + return 0; + } + return Integer.parseInt(num); + } + private static final Pattern PATTERN=Pattern.compile("^classes([0-9]*)\\.dex$"); + +} diff --git a/src/ARSCLib/com/reandroid/apk/FileMagic.java b/src/ARSCLib/com/reandroid/apk/FileMagic.java new file mode 100644 index 00000000..f8acba2f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/FileMagic.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; + +import com.reandroid.archive.InputSource; + +import java.io.IOException; +import java.io.InputStream; + +public class FileMagic { + + public static String getExtensionFromMagic(InputSource inputSource) throws IOException { + byte[] magic=readFileMagic(inputSource); + if(magic==null){ + return null; + } + if(isPng(magic)){ + return ".png"; + } + if(isJpeg(magic)){ + return ".jpg"; + } + if(isWebp(magic)){ + return ".webp"; + } + if(isTtf(magic)){ + return ".ttf"; + } + return null; + } + + private static boolean isJpeg(byte[] magic){ + return compareMagic(MAGIC_JPG, magic); + } + private static boolean isPng(byte[] magic){ + return compareMagic(MAGIC_PNG, magic); + } + private static boolean isWebp(byte[] magic){ + return compareMagic(MAGIC_WEBP, magic); + } + private static boolean isTtf(byte[] magic){ + return compareMagic(MAGIC_TTF, magic); + } + private static boolean compareMagic(byte[] magic, byte[] readMagic){ + if(magic==null || readMagic==null){ + return false; + } + int max=magic.length; + if(max>readMagic.length){ + max=readMagic.length; + } + if(max==0){ + return false; + } + for(int i=0;i blockInputSource=new BlockInputSource<>(inputSource.getName(), frameworkTable); + blockInputSource.setMethod(inputSource.getMethod()); + blockInputSource.setSort(inputSource.getSort()); + archive.add(blockInputSource); + return frameworkTable; + } + public void optimize(){ + synchronized (mLock){ + if(mOptimizing){ + return; + } + if(!hasTableBlock()){ + mOptimizing = false; + return; + } + FrameworkTable frameworkTable = getTableBlock(); + if(frameworkTable.isOptimized()){ + mOptimizing = false; + return; + } + FrameworkOptimizer optimizer = new FrameworkOptimizer(this); + optimizer.optimize(); + mOptimizing = false; + } + } + public String getName(){ + if(isDestroyed()){ + return "destroyed"; + } + String pkg = getPackageName(); + if(pkg==null){ + return ""; + } + return pkg + "-" + getVersionCode(); + } + @Override + public int hashCode(){ + return Objects.hash(getClass(), getName()); + } + @Override + public boolean equals(Object obj){ + if(obj==this){ + return true; + } + if(getClass()!=obj.getClass()){ + return false; + } + FrameworkApk other = (FrameworkApk) obj; + return getName().equals(other.getName()); + } + @Override + public String toString(){ + return getName(); + } + public static FrameworkApk loadApkFile(File apkFile) throws IOException { + Archive archive = new Archive(apkFile); + APKArchive apkArchive = new APKArchive(archive.mapEntrySource()); + return new FrameworkApk(apkArchive); + } + public static FrameworkApk loadApkFile(File apkFile, String moduleName) throws IOException { + Archive archive = new Archive(apkFile); + APKArchive apkArchive = new APKArchive(archive.mapEntrySource()); + return new FrameworkApk(moduleName, apkArchive); + } + public static boolean isFramework(ApkModule apkModule) { + if(!apkModule.hasAndroidManifestBlock()){ + return false; + } + return isFramework(apkModule.getAndroidManifestBlock()); + } + public static boolean isFramework(AndroidManifestBlock manifestBlock){ + ResXmlElement root = manifestBlock.getManifestElement(); + ResXmlAttribute attribute = root.getStartElement() + .searchAttributeByName(AndroidManifestBlock.NAME_coreApp); + if(attribute==null || attribute.getValueType()!= ValueType.INT_BOOLEAN){ + return false; + } + return attribute.getValueAsBoolean(); + } + public static FrameworkApk loadApkBuffer(InputStream inputStream) throws IOException{ + return loadApkBuffer("framework", inputStream); + } + public static FrameworkApk loadApkBuffer(String moduleName, InputStream inputStream) throws IOException { + APKArchive archive = new APKArchive(); + FrameworkApk frameworkApk = new FrameworkApk(moduleName, archive); + Map inputSourceMap = InputSourceUtil.mapInputStreamAsBuffer(inputStream); + ByteInputSource source = inputSourceMap.get(TableBlock.FILE_NAME); + FrameworkTable tableBlock = new FrameworkTable(); + if(source!=null){ + tableBlock.readBytes(source.openStream()); + } + frameworkApk.setTableBlock(tableBlock); + + AndroidManifestBlock manifestBlock = new AndroidManifestBlock(); + source = inputSourceMap.get(AndroidManifestBlock.FILE_NAME); + if(source!=null){ + manifestBlock.readBytes(source.openStream()); + } + frameworkApk.setManifest(manifestBlock); + archive.addAll(inputSourceMap.values()); + return frameworkApk; + } + public static void optimize(File in, File out, APKLogger apkLogger) throws IOException{ + FrameworkApk frameworkApk = FrameworkApk.loadApkFile(in); + frameworkApk.setAPKLogger(apkLogger); + frameworkApk.optimize(); + frameworkApk.writeApk(out); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/FrameworkOptimizer.java b/src/ARSCLib/com/reandroid/apk/FrameworkOptimizer.java new file mode 100644 index 00000000..a02f7ccc --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/FrameworkOptimizer.java @@ -0,0 +1,297 @@ +/* + * 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.APKArchive; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlAttribute; +import com.reandroid.arsc.chunk.xml.ResXmlElement; +import com.reandroid.arsc.chunk.xml.ResXmlNode; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.pool.ResXmlStringPool; +import com.reandroid.arsc.util.FrameworkTable; +import com.reandroid.arsc.value.*; + +import java.io.IOException; +import java.util.*; +import java.util.zip.ZipEntry; + + public class FrameworkOptimizer { + private final ApkModule frameworkApk; + private APKLogger apkLogger; + private boolean mOptimizing; + public FrameworkOptimizer(ApkModule frameworkApk){ + this.frameworkApk = frameworkApk; + this.apkLogger = frameworkApk.getApkLogger(); + } + public void optimize(){ + if(mOptimizing){ + return; + } + mOptimizing = true; + if(!frameworkApk.hasTableBlock()){ + logMessage("Don't have: "+TableBlock.FILE_NAME); + mOptimizing = false; + return; + } + FrameworkTable frameworkTable = getFrameworkTable(); + AndroidManifestBlock manifestBlock = null; + if(frameworkApk.hasAndroidManifestBlock()){ + manifestBlock = frameworkApk.getAndroidManifestBlock(); + } + optimizeTable(frameworkTable, manifestBlock); + UncompressedFiles uncompressedFiles = frameworkApk.getUncompressedFiles(); + uncompressedFiles.clearExtensions(); + uncompressedFiles.clearPaths(); + clearFiles(frameworkApk.getApkArchive()); + logMessage("Optimized"); + } + private void clearFiles(APKArchive archive){ + int count = archive.entriesCount(); + if(count==2){ + return; + } + logMessage("Removing files from: "+count); + InputSource tableSource = archive.getInputSource(TableBlock.FILE_NAME); + InputSource manifestSource = archive.getInputSource(AndroidManifestBlock.FILE_NAME); + archive.clear(); + if(tableSource!=null){ + tableSource.setMethod(ZipEntry.DEFLATED); + } + if(manifestSource!=null){ + manifestSource.setMethod(ZipEntry.DEFLATED); + } + archive.add(tableSource); + archive.add(manifestSource); + count = count - archive.entriesCount(); + logMessage("Removed files: "+count); + } + private void optimizeTable(FrameworkTable table, AndroidManifestBlock manifestBlock){ + if(table.isOptimized()){ + return; + } + logMessage("Optimizing ..."); + int prev = table.countBytes(); + int version = 0; + String name = "framework"; + if(manifestBlock !=null){ + Integer code = manifestBlock.getVersionCode(); + if(code!=null){ + version = code; + } + name = manifestBlock.getPackageName(); + compressManifest(manifestBlock); + backupManifestValue(manifestBlock, table); + } + logMessage("Optimizing table ..."); + table.optimize(name, version); + long diff=prev - table.countBytes(); + long percent=(diff*100L)/prev; + logMessage("Table size reduced by: "+percent+" %"); + mOptimizing = false; + } + + private FrameworkTable getFrameworkTable(){ + TableBlock tableBlock = frameworkApk.getTableBlock(); + if(tableBlock instanceof FrameworkTable){ + return (FrameworkTable) tableBlock; + } + FrameworkTable frameworkTable = toFramework(tableBlock); + frameworkApk.setTableBlock(frameworkTable); + return frameworkTable; + } + private FrameworkTable toFramework(TableBlock tableBlock){ + logMessage("Converting to framework ..."); + BlockReader reader = new BlockReader(tableBlock.getBytes()); + FrameworkTable frameworkTable = new FrameworkTable(); + try { + frameworkTable.readBytes(reader); + } catch (IOException exception) { + logError("Error re-loading framework: ", exception); + } + return frameworkTable; + } + private void compressManifest(AndroidManifestBlock manifestBlock){ + logMessage("Compressing manifest ..."); + int prev = manifestBlock.countBytes(); + ResXmlElement manifest = manifestBlock.getResXmlElement(); + List removeList = getManifestElementToRemove(manifest); + for(ResXmlNode node:removeList){ + manifest.removeNode(node); + } + ResXmlElement application = manifestBlock.getApplicationElement(); + if(application!=null){ + removeList = application.listXmlNodes(); + for(ResXmlNode node:removeList){ + application.removeNode(node); + } + } + ResXmlStringPool stringPool = manifestBlock.getStringPool(); + stringPool.removeUnusedStrings(); + manifestBlock.refresh(); + long diff=prev - manifestBlock.countBytes(); + long percent=(diff*100L)/prev; + logMessage("Manifest size reduced by: "+percent+" %"); + } + private List getManifestElementToRemove(ResXmlElement manifest){ + List results = new ArrayList<>(); + for(ResXmlNode node:manifest.listXmlNodes()){ + if(!(node instanceof ResXmlElement)){ + continue; + } + ResXmlElement element = (ResXmlElement)node; + if(AndroidManifestBlock.TAG_application.equals(element.getTag())){ + continue; + } + results.add(element); + } + return results; + } + private void backupManifestValue(AndroidManifestBlock manifestBlock, TableBlock tableBlock){ + logMessage("Backup manifest values ..."); + ResXmlElement application = manifestBlock.getApplicationElement(); + ResXmlAttribute iconAttribute = null; + int iconReference = 0; + if(application!=null){ + ResXmlAttribute attribute = application + .searchAttributeByResourceId(AndroidManifestBlock.ID_icon); + if(attribute!=null && attribute.getValueType()==ValueType.REFERENCE){ + iconAttribute = attribute; + iconReference = attribute.getData(); + } + } + + ResXmlElement element = manifestBlock.getResXmlElement(); + backupAttributeValues(tableBlock, element); + + if(iconAttribute!=null){ + iconAttribute.setTypeAndData(ValueType.REFERENCE, iconReference); + } + } + private void backupAttributeValues(TableBlock tableBlock, ResXmlElement element){ + if(element==null){ + return; + } + for(ResXmlAttribute attribute: element.listAttributes()){ + backupAttributeValues(tableBlock, attribute); + } + for(ResXmlElement child: element.listElements()){ + backupAttributeValues(tableBlock, child); + } + } + private void backupAttributeValues(TableBlock tableBlock, ResXmlAttribute attribute){ + if(attribute==null){ + return; + } + ValueType valueType = attribute.getValueType(); + if(valueType!=ValueType.REFERENCE && valueType!=ValueType.ATTRIBUTE){ + return; + } + int reference = attribute.getData(); + Entry entry = getEntryWithValue(tableBlock, reference); + if(entry == null || isReferenceEntry(entry) || entry.isComplex()){ + return; + } + ResTableEntry resTableEntry = (ResTableEntry) entry.getTableEntry(); + ResValue resValue = resTableEntry.getValue(); + valueType = resValue.getValueType(); + if(valueType==ValueType.STRING){ + String value = resValue.getValueAsString(); + attribute.setValueAsString(value); + }else { + int data = resValue.getData(); + attribute.setTypeAndData(valueType, data); + } + } + private Entry getEntryWithValue(TableBlock tableBlock, int resourceId){ + Set circularReference = new HashSet<>(); + return getEntryWithValue(tableBlock, resourceId, circularReference); + } + private Entry getEntryWithValue(TableBlock tableBlock, int resourceId, Set circularReference){ + if(circularReference.contains(resourceId)){ + return null; + } + circularReference.add(resourceId); + EntryGroup entryGroup = tableBlock.getEntryGroup(resourceId); + Entry entry = entryGroup.pickOne(); + if(entry==null){ + return null; + } + if(isReferenceEntry(entry)){ + return getEntryWithValue( + tableBlock, + ((ResValue)entry.getTableEntry().getValue()).getData(), + circularReference); + } + if(!entry.isNull()){ + return entry; + } + Iterator itr = entryGroup.iterator(true); + while (itr.hasNext()){ + entry = itr.next(); + if(!isReferenceEntry(entry)){ + if(!entry.isNull()){ + return entry; + } + } + } + return null; + } + private boolean isReferenceEntry(Entry entry){ + if(entry==null || entry.isNull()){ + return false; + } + TableEntry tableEntry = entry.getTableEntry(); + if(tableEntry instanceof CompoundEntry){ + return false; + } + if(!(tableEntry instanceof ResTableEntry)){ + return false; + } + ResTableEntry resTableEntry = (ResTableEntry) tableEntry; + ResValue resValue = resTableEntry.getValue(); + + ValueType valueType = resValue.getValueType(); + + return valueType == ValueType.REFERENCE + || valueType == ValueType.ATTRIBUTE; + } + + APKLogger getApkLogger(){ + return apkLogger; + } + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/JsonManifestInputSource.java b/src/ARSCLib/com/reandroid/apk/JsonManifestInputSource.java new file mode 100644 index 00000000..31731063 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/JsonManifestInputSource.java @@ -0,0 +1,36 @@ + /* + * 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.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; + +import java.io.File; + +public class JsonManifestInputSource extends JsonXmlInputSource { + public JsonManifestInputSource(InputSource inputSource) { + super(inputSource); + } + AndroidManifestBlock newInstance(){ + return new AndroidManifestBlock(); + } + public static JsonManifestInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new JsonManifestInputSource(fileInputSource); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/JsonXmlInputSource.java b/src/ARSCLib/com/reandroid/apk/JsonXmlInputSource.java new file mode 100644 index 00000000..7989744e --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/JsonXmlInputSource.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; + +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.json.JSONException; +import com.reandroid.json.JSONObject; + +import java.io.*; + +public class JsonXmlInputSource extends InputSource { + private final InputSource inputSource; + private APKLogger apkLogger; + public JsonXmlInputSource(InputSource inputSource) { + super(inputSource.getAlias()); + this.inputSource=inputSource; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getResXmlBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + ResXmlDocument resXmlDocument = getResXmlBlock(); + return new ByteArrayInputStream(resXmlDocument.getBytes()); + } + @Override + public long getLength() throws IOException{ + ResXmlDocument resXmlDocument = getResXmlBlock(); + return resXmlDocument.countBytes(); + } + private ResXmlDocument getResXmlBlock() throws IOException{ + logVerbose("From json: "+getAlias()); + ResXmlDocument resXmlDocument =newInstance(); + InputStream inputStream=inputSource.openStream(); + try{ + JSONObject jsonObject=new JSONObject(inputStream); + resXmlDocument.fromJson(jsonObject); + }catch (JSONException ex){ + throw new IOException(inputSource.getAlias()+": "+ex.getMessage(), ex); + } + return resXmlDocument; + } + ResXmlDocument newInstance(){ + return new ResXmlDocument(); + } + void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } + + public static JsonXmlInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new JsonXmlInputSource(fileInputSource); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/PathMap.java b/src/ARSCLib/com/reandroid/apk/PathMap.java new file mode 100644 index 00000000..36b86ba9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/PathMap.java @@ -0,0 +1,206 @@ +/* + * 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.archive.ZipArchive; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.util.*; + +public class PathMap implements JSONConvert { + private final Object mLock = new Object(); + private final Map mNameAliasMap; + private final Map mAliasNameMap; + + public PathMap(){ + this.mNameAliasMap = new HashMap<>(); + this.mAliasNameMap = new HashMap<>(); + } + + public void restore(ApkModule apkModule){ + restoreResFile(apkModule.listResFiles()); + restore(apkModule.getApkArchive().listInputSources()); + } + public List restoreResFile(Collection files){ + List results = new ArrayList<>(); + if(files == null){ + return results; + } + for(ResFile resFile:files){ + String alias = restoreResFile(resFile); + if(alias==null){ + continue; + } + results.add(alias); + } + return results; + } + public String restoreResFile(ResFile resFile){ + InputSource inputSource = resFile.getInputSource(); + String alias = restore(inputSource); + if(alias==null){ + return null; + } + resFile.setFilePath(alias); + return alias; + } + public List restore(Collection sources){ + List results = new ArrayList<>(); + if(sources == null){ + return results; + } + for(InputSource inputSource:sources){ + String alias = restore(inputSource); + if(alias==null){ + continue; + } + results.add(alias); + } + return results; + } + public String restore(InputSource inputSource){ + if(inputSource==null){ + return null; + } + String name = inputSource.getName(); + String alias = getName(name); + if(alias==null){ + name = inputSource.getAlias(); + alias = getName(name); + } + if(alias==null || alias.equals(inputSource.getAlias())){ + return null; + } + inputSource.setAlias(alias); + return alias; + } + + public String getAlias(String name){ + synchronized (mLock){ + return mNameAliasMap.get(name); + } + } + public String getName(String alias){ + synchronized (mLock){ + return mAliasNameMap.get(alias); + } + } + public int size(){ + synchronized (mLock){ + return mNameAliasMap.size(); + } + } + public void clear(){ + synchronized (mLock){ + mNameAliasMap.clear(); + mAliasNameMap.clear(); + } + } + public void add(ZipArchive archive){ + if(archive == null){ + return; + } + add(archive.listInputSources()); + } + public void add(Collection sources){ + if(sources==null){ + return; + } + for(InputSource inputSource:sources){ + add(inputSource); + } + } + public void add(InputSource inputSource){ + if(inputSource==null){ + return; + } + add(inputSource.getName(), inputSource.getAlias()); + } + public void add(String name, String alias){ + if(name==null || alias==null){ + return; + } + if(name.equals(alias)){ + return; + } + synchronized (mLock){ + mNameAliasMap.remove(name); + mNameAliasMap.put(name, alias); + mAliasNameMap.remove(alias); + mAliasNameMap.put(alias, name); + } + } + + private void add(JSONObject json){ + if(json==null){ + return; + } + add(json.optString(NAME_name), json.optString(NAME_alias)); + } + + @Override + public JSONArray toJson() { + JSONArray jsonArray = new JSONArray(); + Map nameMap = this.mNameAliasMap; + List nameList = toSortedList(nameMap.keySet()); + for(String name:nameList){ + JSONObject jsonObject = new JSONObject(); + jsonObject.put(NAME_name, name); + jsonObject.put(NAME_alias, nameMap.get(name)); + jsonArray.put(jsonObject); + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + clear(); + if(json==null){ + return; + } + int length = json.length(); + for(int i=0;i sourceList; + private final boolean sanitizeResourceFiles; + private Collection resFileList; + private APKLogger apkLogger; + private final Set mSanitizedPaths; + public PathSanitizer(Collection sourceList, boolean sanitizeResourceFiles){ + this.sourceList = sourceList; + this.mSanitizedPaths = new HashSet<>(); + this.sanitizeResourceFiles = sanitizeResourceFiles; + } + public PathSanitizer(Collection sourceList){ + this(sourceList, false); + } + public void sanitize(){ + mSanitizedPaths.clear(); + logMessage("Sanitizing paths ..."); + sanitizeResFiles(); + 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 ..."); + } + for(ResFile resFile:resFileList){ + if(sanitizeRes){ + sanitize(resFile); + }else { + sanitizedPaths.add(resFile.getFilePath()); + } + } + } + private void sanitize(ResFile resFile){ + InputSource inputSource = resFile.getInputSource(); + String replace = sanitize(inputSource, 3, true); + if(replace==null){ + return; + } + resFile.setFilePath(replace); + } + private String sanitize(InputSource inputSource, int depth, boolean fixedDepth){ + String name = inputSource.getName(); + if(mSanitizedPaths.contains(name)){ + return null; + } + mSanitizedPaths.add(name); + String alias = inputSource.getAlias(); + if(shouldIgnore(alias)){ + return null; + } + String replace = sanitize(alias, depth, fixedDepth); + if(alias.equals(replace)){ + return null; + } + inputSource.setAlias(replace); + logVerbose("REN: '"+alias+"' -> '"+replace+"'"); + return replace; + } + + private String sanitize(String name, int depth, boolean fixedDepth){ + StringBuilder builder = new StringBuilder(); + String[] nameSplit = name.split("/"); + + boolean pathIsLong = name.length() >= MAX_PATH_LENGTH; + int length = nameSplit.length; + for(int i=0;i=depth)){ + split = createUniqueName(name); + appendPathName(builder, split); + break; + } + if(fixedDepth && i>=(depth-1)){ + if(i < length-1){ + split = createUniqueName(name); + } + appendPathName(builder, split); + break; + } + appendPathName(builder, split); + } + return builder.toString(); + } + private boolean shouldIgnore(String path){ + return path.startsWith("lib/") && path.endsWith(".so"); + } + + public void setApkLogger(APKLogger apkLogger) { + this.apkLogger = apkLogger; + } + private String getLogTag(){ + return "[SANITIZE]: "; + } + void logMessage(String msg){ + APKLogger logger = this.apkLogger; + if(logger!=null){ + logger.logMessage(getLogTag()+msg); + } + } + void logVerbose(String msg){ + APKLogger logger = this.apkLogger; + if(logger!=null){ + logger.logVerbose(getLogTag()+msg); + } + } + + private static void appendPathName(StringBuilder builder, String name){ + if(builder.length()>0){ + builder.append('/'); + } + builder.append(name); + } + private static String createUniqueName(String name){ + int hash = name.hashCode(); + return "alias_" + HexUtil.toHexNoPrefix8(hash); + } + private static boolean isGoodSimpleName(String name){ + if(name==null){ + return false; + } + String alias = sanitizeSimpleName(name); + return name.equals(alias); + } + public static String sanitizeSimpleName(String name){ + if(name==null){ + return null; + } + StringBuilder builder = new StringBuilder(); + char[] chars = name.toCharArray(); + boolean skipNext = true; + int length = 0; + int lengthMax = MAX_NAME_LENGTH; + for(int i=0;i=lengthMax){ + break; + } + char ch = chars[i]; + if(isGoodFileNameSymbol(ch)){ + if(!skipNext){ + builder.append(ch); + length++; + } + skipNext=true; + continue; + } + if(!isGoodFileNameChar(ch)){ + skipNext = true; + continue; + } + builder.append(ch); + length++; + skipNext=false; + } + if(length==0){ + return null; + } + return builder.toString(); + } + + private static boolean isGoodFileNameSymbol(char ch){ + return ch == '.' + || ch == '+' + || ch == '-' + || ch == '#'; + } + private static boolean isGoodFileNameChar(char ch){ + return ch == '_' + || (ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'Z') + || (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/ARSCLib/com/reandroid/apk/RenamedInputSource.java b/src/ARSCLib/com/reandroid/apk/RenamedInputSource.java new file mode 100644 index 00000000..8e1545c4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/RenamedInputSource.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; + +import com.reandroid.archive.InputSource; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class RenamedInputSource extends InputSource { + private final T inputSource; + public RenamedInputSource(String name, T input){ + super(name); + this.inputSource=input; + super.setMethod(input.getMethod()); + super.setSort(input.getSort()); + } + public T getInputSource() { + return inputSource; + } + @Override + public void close(InputStream inputStream) throws IOException { + getInputSource().close(inputStream); + } + @Override + public long getLength() throws IOException { + return getInputSource().getLength(); + } + @Override + public long getCrc() throws IOException { + return getInputSource().getCrc(); + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getInputSource().write(outputStream); + } + @Override + public InputStream openStream() throws IOException { + return getInputSource().openStream(); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ResFile.java b/src/ARSCLib/com/reandroid/apk/ResFile.java new file mode 100644 index 00000000..8ba6435b --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ResFile.java @@ -0,0 +1,188 @@ +/* + * 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.apk.xmlencoder.XMLEncodeSource; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.header.InfoHeader; +import com.reandroid.arsc.value.*; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class ResFile { + private final List entryList; + private final InputSource inputSource; + private boolean mBinXml; + private boolean mBinXmlChecked; + private String mFileExtension; + private boolean mFileExtensionChecked; + private Entry mSelectedEntry; + public ResFile(InputSource inputSource, List entryList){ + this.inputSource=inputSource; + this.entryList = entryList; + } + public List getEntryList(){ + return entryList; + } + public String validateTypeDirectoryName(){ + Entry entry =pickOne(); + if(entry ==null){ + return null; + } + String path=getFilePath(); + String root=""; + int i=path.indexOf('/'); + if(i>0){ + i++; + root=path.substring(0, i); + path=path.substring(i); + } + String name=path; + i=path.lastIndexOf('/'); + if(i>0){ + i++; + name=path.substring(i); + } + TypeBlock typeBlock= entry.getTypeBlock(); + String typeName=typeBlock.getTypeName()+typeBlock.getResConfig().getQualifiers(); + return root+typeName+"/"+name; + } + public Entry pickOne(){ + if(mSelectedEntry ==null){ + mSelectedEntry =selectOne(); + } + return mSelectedEntry; + } + private Entry selectOne(){ + List entryList = this.entryList; + if(entryList.size()==0){ + return null; + } + for(Entry entry :entryList){ + if(!entry.isNull() && entry.isDefault()){ + return entry; + } + } + for(Entry entry :entryList){ + if(!entry.isNull()){ + return entry; + } + } + for(Entry entry :entryList){ + if(entry.isDefault()){ + return entry; + } + } + return entryList.get(0); + } + public String getFilePath(){ + return getInputSource().getAlias(); + } + public void setFilePath(String filePath){ + getInputSource().setAlias(filePath); + for(Entry entry : entryList){ + TableEntry tableEntry = entry.getTableEntry(); + if(!(tableEntry instanceof ResTableEntry)){ + continue; + } + ResValue resValue = ((ResTableEntry) tableEntry).getValue(); + resValue.setValueAsString(filePath); + } + } + public InputSource getInputSource() { + return inputSource; + } + public boolean isBinaryXml(){ + if(mBinXmlChecked){ + return mBinXml; + } + mBinXmlChecked = true; + InputSource inputSource = getInputSource(); + if((inputSource instanceof XMLEncodeSource) + || (inputSource instanceof JsonXmlInputSource)){ + mBinXml=true; + }else{ + try { + mBinXml = ResXmlDocument.isResXmlBlock(inputSource.getBytes(InfoHeader.INFO_MIN_SIZE)); + } catch (IOException ignored) { + } + } + return mBinXml; + } + public File buildOutFile(File dir){ + String path=getFilePath(); + path=path.replace('/', File.separatorChar); + return new File(dir, path); + } + public String buildPath(){ + return buildPath(null); + } + public String buildPath(String parent){ + Entry entry = pickOne(); + StringBuilder builder = new StringBuilder(); + if(parent!=null){ + builder.append(parent); + if(!parent.endsWith("/")){ + builder.append('/'); + } + } + TypeBlock typeBlock = entry.getTypeBlock(); + builder.append(typeBlock.getTypeName()); + builder.append(typeBlock.getQualifiers()); + builder.append('/'); + builder.append(entry.getName()); + String ext = getFileExtension(); + if(ext!=null){ + builder.append(ext); + } + return builder.toString(); + } + private String getFileExtension(){ + if(!mFileExtensionChecked){ + mFileExtensionChecked=true; + mFileExtension=readFileExtension(); + } + return mFileExtension; + } + private String readFileExtension(){ + if(isBinaryXml()){ + return ".xml"; + } + String path=getFilePath(); + int i=path.lastIndexOf('.'); + if(i>0){ + return path.substring(i); + } + try { + String magicExt=FileMagic.getExtensionFromMagic(getInputSource()); + if(magicExt!=null){ + return magicExt; + } + } catch (IOException ignored) { + } + return null; + } + @Override + public String toString(){ + return getFilePath(); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/ResourceIds.java b/src/ARSCLib/com/reandroid/apk/ResourceIds.java new file mode 100644 index 00000000..7b43723f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/ResourceIds.java @@ -0,0 +1,845 @@ +/* + * 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.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.util.ResNameMap; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; +import com.reandroid.xml.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/**Use {@link com.reandroid.identifiers.TableIdentifier} */ +@Deprecated + public class ResourceIds { + private final Table mTable; + public ResourceIds(Table table){ + this.mTable=table; + } + public ResourceIds(){ + this(new Table()); + } + public Table getTable(){ + return mTable; + } + public int applyTo(TableBlock tableBlock){ + return mTable.applyTo(tableBlock); + } + public void fromJson(JSONObject jsonObject){ + mTable.fromJson(jsonObject); + } + public JSONObject toJson(){ + return mTable.toJson(); + } + public void loadTableBlock(TableBlock tableBlock){ + for(PackageBlock packageBlock:tableBlock.listPackages()){ + loadPackageBlock(packageBlock); + } + } + public void loadPackageBlock(PackageBlock packageBlock){ + Collection entryGroupList = packageBlock.listEntryGroup(); + String name= packageBlock.getName(); + for(EntryGroup entryGroup:entryGroupList){ + Table.Package.Type.Entry entry= Table.Package.Type.Entry.fromEntryGroup(entryGroup); + mTable.add(entry); + if(name==null){ + continue; + } + Table.Package.Type type=entry.type; + if(type!=null && type.mPackage!=null){ + type.mPackage.name=name; + name=null; + } + } + } + + public void writeXml(File file) throws IOException { + mTable.writeXml(file); + } + public void writeXml(OutputStream outputStream) throws IOException { + mTable.writeXml(outputStream); + } + public void fromXml(File file) throws IOException { + mTable.fromXml(file); + } + public void fromXml(InputStream inputStream) throws IOException { + mTable.fromXml(inputStream); + } + public void fromXml(XMLDocument xmlDocument) throws IOException { + mTable.fromXml(xmlDocument); + } + + public XMLDocument toXMLDocument(){ + return mTable.toXMLDocument(); + } + + public static class Table implements Comparator{ + public final Map packageMap; + public Table(){ + this.packageMap = new HashMap<>(); + } + public int applyTo(TableBlock tableBlock){ + int renameCount=0; + for(PackageBlock packageBlock : tableBlock.listPackages()){ + Package pkg=getPackage((byte) packageBlock.getId()); + if(pkg!=null){ + renameCount+=pkg.applyTo(packageBlock); + } + } + if(renameCount>0){ + tableBlock.refresh(); + } + return renameCount; + } + public void add(Package pkg){ + Package exist=this.packageMap.get(pkg.id); + if(exist!=null){ + exist.merge(pkg); + return; + } + this.packageMap.put(pkg.id, pkg); + } + public Package add(Package.Type.Entry entry){ + if(entry==null){ + return null; + } + byte pkgId=entry.getPackageId(); + Package pkg = packageMap.get(pkgId); + if(pkg==null){ + pkg=new Package(pkgId); + packageMap.put(pkgId, pkg); + } + pkg.add(entry); + return pkg; + } + public Package.Type.Entry getEntry(int resourceId){ + byte packageId = (byte) ((resourceId>>24) & 0xff); + byte typeId = (byte) ((resourceId>>16) & 0xff); + short entryId = (short) (resourceId & 0xff); + Package pkg = getPackage(packageId); + if(pkg == null){ + return null; + } + return getEntry(packageId, typeId, entryId); + } + public Package getPackage(byte packageId){ + return packageMap.get(packageId); + } + public Package.Type getType(byte packageId, byte typeId){ + Package pkg=getPackage(packageId); + if(pkg==null){ + return null; + } + return pkg.getType(typeId); + } + public Package.Type.Entry getEntry(byte packageId, byte typeId, short entryId){ + Package pkg=getPackage(packageId); + if(pkg==null){ + return null; + } + return pkg.getEntry(typeId, entryId); + } + public List listPackages(){ + List results=new ArrayList<>(packageMap.values()); + results.sort(this); + return results; + } + public List listEntries(){ + List results=new ArrayList<>(countEntries()); + for(Package pkg:packageMap.values()){ + results.addAll(pkg.listEntries()); + } + return results; + } + int countEntries(){ + int result=0; + for(Package pkg:packageMap.values()){ + result+=pkg.countEntries(); + } + return result; + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + JSONArray jsonArray=new JSONArray(); + for(Package pkg: packageMap.values()){ + jsonArray.put(pkg.toJson()); + } + jsonObject.put("packages", jsonArray); + return jsonObject; + } + public void fromJson(JSONObject jsonObject){ + JSONArray jsonArray= jsonObject.optJSONArray("packages"); + if(jsonArray!=null){ + int length= jsonArray.length(); + for(int i=0;i"); + writer.write('\n'); + writer.write(""); + } + private void writeEnd(Writer writer) throws IOException{ + writer.write('\n'); + writer.write(""); + } + public void fromXml(File file) throws IOException { + FileInputStream inputStream=new FileInputStream(file); + fromXml(inputStream); + inputStream.close(); + } + public void fromXml(InputStream inputStream) throws IOException { + try { + fromXml(XMLDocument.load(inputStream)); + } catch (XMLException ex) { + throw new IOException(ex.getMessage(), ex); + } + } + public void fromXml(XMLDocument xmlDocument) { + XMLElement documentElement = xmlDocument.getDocumentElement(); + int count=documentElement.getChildesCount(); + for(int i=0;i, Comparator{ + public final byte id; + public String name; + public final Map typeMap; + private final ResNameMap mEntryNameMap; + public Package(byte id){ + this.id = id; + this.typeMap = new HashMap<>(); + this.mEntryNameMap = new ResNameMap<>(); + } + public void loadEntryMap(){ + mEntryNameMap.clear(); + for(Type type:typeMap.values()){ + String typeName=type.getName(); + for(Type.Entry entry: type.entryMap.values()){ + mEntryNameMap.add(typeName, entry.getName(), entry.getResourceId()); + } + } + } + public Integer getResourceId(String typeName, String name){ + return mEntryNameMap.get(typeName, name); + } + public Type.Entry getEntry(String typeName, String name){ + Type type=getType(typeName); + if(type==null){ + return null; + } + return type.getEntry(name); + } + private Type getType(String typeName){ + for(Type type:typeMap.values()){ + if(type.getName().equals(typeName)){ + return type; + } + } + return null; + } + public int getIdInt(){ + return 0xff & id; + } + public int applyTo(PackageBlock packageBlock){ + int renameCount=0; + Map map = packageBlock.getEntriesGroupMap(); + for(Map.Entry entry:map.entrySet()){ + byte typeId=Table.toTypeId(entry.getKey()); + Type type=typeMap.get(typeId); + if(type==null){ + continue; + } + EntryGroup entryGroup=entry.getValue(); + if(type.applyTo(entryGroup)){ + renameCount++; + } + } + if(renameCount>0){ + cleanSpecStringPool(packageBlock); + } + return renameCount; + } + private void cleanSpecStringPool(PackageBlock packageBlock){ + SpecStringPool specStringPool = packageBlock.getSpecStringPool(); + specStringPool.refreshUniqueIdMap(); + specStringPool.removeUnusedStrings(); + packageBlock.refresh(); + } + public void merge(Package pkg){ + if(pkg==this||pkg==null){ + return; + } + if(pkg.id!=this.id){ + throw new DuplicateException("Different package id: "+this.id+"!="+pkg.id); + } + if(pkg.name!=null){ + this.name = pkg.name; + } + for(Type type:pkg.typeMap.values()){ + add(type); + } + } + public Type getType(byte typeId){ + return typeMap.get(typeId); + } + public void add(Type type){ + Byte typeId= type.id;; + Type exist=this.typeMap.get(typeId); + if(exist!=null){ + exist.merge(type); + return; + } + type.mPackage=this; + this.typeMap.put(typeId, type); + } + public Package.Type.Entry getEntry(byte typeId, short entryId){ + Package.Type type=getType(typeId); + if(type==null){ + return null; + } + return type.getEntry(entryId); + } + public void add(Type.Entry entry){ + if(entry==null){ + return; + } + if(entry.getPackageId()!=this.id){ + throw new DuplicateException("Different package id: "+entry); + } + byte typeId=entry.getTypeId(); + Type type=typeMap.get(typeId); + if(type==null){ + type=new Type(typeId); + type.mPackage=this; + typeMap.put(typeId, type); + } + type.add(entry); + } + public String getHexId(){ + return HexUtil.toHex2(id); + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", this.getIdInt()); + if(this.name!=null){ + jsonObject.put("name", this.name); + } + JSONArray jsonArray=new JSONArray(); + for(Type type:typeMap.values()){ + jsonArray.put(type.toJson()); + } + jsonObject.put("types", jsonArray); + return jsonObject; + } + @Override + public int compareTo(Package pkg) { + return Integer.compare(getIdInt(), pkg.getIdInt()); + } + @Override + public int compare(Type t1, Type t2) { + return t1.compareTo(t2); + } + public void toXMLElements(XMLElement documentElement){ + int count = documentElement.getChildesCount(); + for(Type type:listTypes()){ + type.toXMLElements(documentElement); + } + XMLElement firstElement = documentElement.getChildAt(count); + if(firstElement!=null){ + XMLComment comment = new XMLComment( + "packageName=\""+this.name+"\""); + firstElement.addComment(comment); + } + } + void setPackageName(XMLComment xmlComment){ + if(xmlComment==null){ + return; + } + String pkgName = xmlComment.getCommentText(); + if(pkgName==null || !pkgName.contains("packageName")){ + return; + } + int i = pkgName.indexOf('"'); + if(i>0){ + i++; + pkgName=pkgName.substring(i); + }else { + return; + } + i = pkgName.indexOf('"'); + if(i>0){ + pkgName=pkgName.substring(0, i); + }else { + return; + } + this.name=pkgName.trim(); + } + public List listEntries(){ + List results=new ArrayList<>(countEntries()); + for(Package.Type type:typeMap.values()){ + results.addAll(type.listEntries()); + } + return results; + } + public List listTypes(){ + List results=new ArrayList<>(typeMap.values()); + results.sort(this); + return results; + } + int countEntries(){ + int results=0; + for(Type type:typeMap.values()){ + results+=type.countEntries(); + } + return results; + } + public void writeXml(String indent, Writer writer) throws IOException{ + writeComment(indent, writer); + for(Type type:listTypes()){ + type.writeXml(indent, writer); + } + } + private void writeComment(String indent, Writer writer) throws IOException{ + String name = this.name; + if(name == null){ + return; + } + writer.write('\n'); + writer.write(indent); + writer.write(""); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Package aPackage = (Package) o; + return id == aPackage.id; + } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString(){ + return getHexId() + ", types=" + typeMap.size(); + } + + public static Package fromJson(JSONObject jsonObject){ + Package pkg=new Package((byte) jsonObject.getInt("id")); + pkg.name = jsonObject.optString("name", null); + JSONArray jsonArray = jsonObject.optJSONArray("types"); + int length = jsonArray.length(); + for(int i=0;i, Comparator{ + public final byte id; + public String name; + public String nameAlias; + public Package mPackage; + public final Map entryMap; + public Type(byte id){ + this.id = id; + this.entryMap = new HashMap<>(); + } + public Entry getEntry(String entryName){ + for(Entry entry:entryMap.values()){ + if(entry.getName().equals(entryName)){ + return entry; + } + } + return null; + } + public int getIdInt(){ + return 0xff & id; + } + public boolean applyTo(EntryGroup entryGroup){ + boolean renamed=false; + Entry entry=entryMap.get(entryGroup.getEntryId()); + if(entry!=null){ + if(entry.applyTo(entryGroup)){ + renamed=true; + } + } + return renamed; + } + public String getName() { + if(nameAlias!=null){ + return nameAlias; + } + return name; + } + + public byte getId(){ + return id; + } + public byte getPackageId(){ + if(mPackage!=null){ + return mPackage.id; + } + return 0; + } + public void merge(Type type){ + if(type==this||type==null){ + return; + } + if(this.id!= type.id){ + throw new DuplicateException("Different type ids: "+id+"!="+type.id); + } + String n=type.getName(); + if(n!=null){ + this.name=n; + } + for(Entry entry:type.entryMap.values()){ + Short entryId=entry.getEntryId(); + Entry existEntry=this.entryMap.get(entryId); + if(existEntry != null && Objects.equals(existEntry.getName(), entry.getName())){ + continue; + } + this.entryMap.remove(entryId); + entry.type=this; + this.entryMap.put(entryId, entry); + } + } + public Entry getEntry(short entryId){ + return entryMap.get(entryId); + } + public String getHexId(){ + return HexUtil.toHex2(id); + } + public void add(Entry entry){ + if(entry==null){ + return; + } + if(entry.getTypeId()!=this.id){ + throw new DuplicateException("Different type id: "+entry); + } + short key=entry.getEntryId(); + Entry exist=entryMap.get(key); + if(exist!=null){ + if(Objects.equals(exist.getName(), entry.getName())){ + return; + } + /* Developer may have a reason adding duplicate + resource ids , lets ignore rather than throw + */ + // throw new DuplicateException("Duplicate entry exist: "+exist+", entry: "+entry); + return; + } + if(getName() == null){ + this.name = entry.getTypeName(); + } + entry.type=this; + entryMap.put(key, entry); + } + + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", getIdInt()); + jsonObject.put("name", getName()); + JSONArray jsonArray=new JSONArray(); + for(Entry entry: entryMap.values()){ + jsonArray.put(entry.toJson()); + } + jsonObject.put("entries", jsonArray); + return jsonObject; + } + public void toXMLElements(XMLElement documentElement){ + for(Entry entry:listEntries()){ + documentElement.addChild(entry.toXMLElement()); + } + } + public List listEntries(){ + List results=new ArrayList<>(entryMap.values()); + results.sort(this); + return results; + } + int countEntries(){ + return entryMap.size(); + } + public void writeXml(String indent, Writer writer) throws IOException{ + for(Entry entry:listEntries()){ + entry.writeXml(indent, writer); + } + } + @Override + public int compareTo(Type type) { + return Integer.compare(getIdInt(), type.getIdInt()); + } + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.compareTo(entry2); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Type that = (Type) o; + return id == that.id; + } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getHexId()); + String n=getName(); + if(n !=null){ + builder.append(" ").append(n); + } + builder.append(", entries=").append(entryMap.size()); + return builder.toString(); + } + + public static Type fromJson(JSONObject jsonObject){ + Type type = new Type((byte) jsonObject.getInt("id")); + type.name = jsonObject.optString("name", null); + JSONArray jsonArray = jsonObject.optJSONArray("entries"); + if(jsonArray!=null){ + int length=jsonArray.length(); + for(int i=0;i{ + public int resourceId; + public String typeName; + public String name; + public String nameAlias; + public Type type; + public Entry(int resourceId, String typeName, String name){ + this.resourceId = resourceId; + this.typeName = typeName; + this.name = name; + } + public Entry(int resourceId, String name){ + this(resourceId, null, name); + } + public boolean applyTo(EntryGroup entryGroup){ + return entryGroup.renameSpec(this.getName()); + } + public String getName() { + if(nameAlias!=null){ + return nameAlias; + } + return name; + } + public String getTypeName(){ + if(this.type!=null){ + return this.type.getName(); + } + return this.typeName; + } + public byte getPackageId(){ + if(this.type!=null){ + Package pkg=this.type.mPackage; + if(pkg!=null){ + return pkg.id; + } + } + return (byte) ((resourceId>>24) & 0xff); + } + public byte getTypeId(){ + if(this.type!=null){ + return this.type.id; + } + return (byte) ((resourceId>>16) & 0xff); + } + public short getEntryId(){ + return (short) (resourceId & 0xffff); + } + public int getEntryIdInt(){ + return resourceId & 0xffff; + } + public int getResourceId(){ + return ((getPackageId() & 0xff)<<24) + | ((getTypeId() & 0xff)<<16) + | (getEntryId() & 0xffff); + } + public String getHexId(){ + return HexUtil.toHex8(getResourceId()); + } + + public void writeXml(String indent, Writer writer) throws IOException{ + writer.write('\n'); + writer.write(indent); + writer.write(""); + } + @Override + public int compareTo(Entry entry) { + return Integer.compare(getEntryIdInt(), entry.getEntryIdInt()); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Entry that = (Entry) o; + return getResourceId() == that.getResourceId(); + } + @Override + public int hashCode() { + return Objects.hash(getResourceId()); + } + public JSONObject toJson(){ + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", getResourceId()); + jsonObject.put("name", getName()); + return jsonObject; + } + public XMLElement toXMLElement(){ + XMLElement element=new XMLElement("public"); + element.setResourceId(getResourceId()); + element.addAttribute(new XMLAttribute("id", getHexId())); + element.addAttribute(new XMLAttribute("type", getTypeName())); + element.addAttribute(new XMLAttribute("name", getName())); + return element; + } + @Override + public String toString(){ + return toXMLElement().toText(false); + } + public static Entry fromEntryGroup(EntryGroup entryGroup){ + return new Entry(entryGroup.getResourceId(), + entryGroup.getTypeName(), + entryGroup.getSpecName()); + } + public static Entry fromJson(JSONObject jsonObject){ + return new Entry(jsonObject.getInt("id"), + jsonObject.optString("type", null), + jsonObject.getString("name")); + } + public static Entry fromXml(XMLElement element){ + return new Entry( + ApkUtil.parseHex(element.getAttributeValue("id")), + element.getAttributeValue("type"), + element.getAttributeValue("name")); + } + } + } + + } + private static short toEntryId(int resourceId){ + int i=resourceId&0xffff; + return (short) i; + } + static byte toTypeId(int resourceId){ + int i=resourceId>>16; + i=i&0xff; + return (byte) i; + } + static byte toPackageId(int resourceId){ + int i=resourceId>>24; + i=i&0xff; + return (byte) i; + } + static int toResourceId(byte pkgId, byte typeId, short entryId){ + return (pkgId & 0xff)<<24 + | (typeId & 0xff)<<16 + | (entryId & 0xffff); + } + } + public static class DuplicateException extends IllegalArgumentException{ + public DuplicateException(String message){ + super(message); + } + public DuplicateException(String message, final Throwable cause) { + super(message, cause); + } + public DuplicateException(Throwable cause) { + super(cause.getMessage(), cause); + } + } + + public static final String JSON_FILE_NAME ="resource-ids.json"; +} diff --git a/src/ARSCLib/com/reandroid/apk/SingleJsonTableInputSource.java b/src/ARSCLib/com/reandroid/apk/SingleJsonTableInputSource.java new file mode 100644 index 00000000..1cc1129c --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/SingleJsonTableInputSource.java @@ -0,0 +1,99 @@ +/* + * 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.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.json.JSONException; +import com.reandroid.json.JSONObject; + +import java.io.*; + +public class SingleJsonTableInputSource extends InputSource { + private final InputSource inputSource; + private TableBlock mCache; + private APKLogger apkLogger; + public SingleJsonTableInputSource(InputSource inputSource) { + super(inputSource.getAlias()); + this.inputSource=inputSource; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getTableBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + TableBlock tableBlock = getTableBlock(); + return new ByteArrayInputStream(tableBlock.getBytes()); + } + @Override + public long getLength() throws IOException{ + TableBlock tableBlock = getTableBlock(); + return tableBlock.countBytes(); + } + @Override + public long getCrc() throws IOException { + CrcOutputStream outputStream=new CrcOutputStream(); + this.write(outputStream); + return outputStream.getCrcValue(); + } + public TableBlock getTableBlock() throws IOException{ + if(mCache != null){ + return mCache; + } + logMessage("Building resources table: " + inputSource.getAlias()); + TableBlock tableBlock=newInstance(); + InputStream inputStream = inputSource.openStream(); + try{ + StringPoolBuilder poolBuilder = new StringPoolBuilder(); + JSONObject jsonObject = new JSONObject(inputStream); + poolBuilder.build(jsonObject); + poolBuilder.apply(tableBlock); + tableBlock.fromJson(jsonObject); + }catch (JSONException ex){ + throw new IOException(inputSource.getAlias(), ex); + } + mCache = tableBlock; + return tableBlock; + } + TableBlock newInstance(){ + return new TableBlock(); + } + public static SingleJsonTableInputSource fromFile(File rootDir, File jsonFile){ + String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile); + FileInputSource fileInputSource=new FileInputSource(jsonFile, path); + return new SingleJsonTableInputSource(fileInputSource); + } + void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + private void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/SplitJsonTableInputSource.java b/src/ARSCLib/com/reandroid/apk/SplitJsonTableInputSource.java new file mode 100644 index 00000000..0f8618b6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/SplitJsonTableInputSource.java @@ -0,0 +1,78 @@ + /* + * 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.arsc.chunk.TableBlock; + +import java.io.*; + +public class SplitJsonTableInputSource extends InputSource { + private final File dir; + private TableBlock mCache; + private APKLogger apkLogger; + public SplitJsonTableInputSource(File dir) { + super(TableBlock.FILE_NAME); + this.dir=dir; + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getTableBlock().writeBytes(outputStream); + } + @Override + public InputStream openStream() throws IOException { + TableBlock tableBlock = getTableBlock(); + return new ByteArrayInputStream(tableBlock.getBytes()); + } + @Override + public long getLength() throws IOException{ + TableBlock tableBlock = getTableBlock(); + return tableBlock.countBytes(); + } + @Override + public long getCrc() throws IOException { + CrcOutputStream outputStream=new CrcOutputStream(); + this.write(outputStream); + return outputStream.getCrcValue(); + } + public TableBlock getTableBlock() throws IOException { + if(mCache!=null){ + return mCache; + } + TableBlockJsonBuilder builder=new TableBlockJsonBuilder(); + TableBlock tableBlock=builder.scanDirectory(dir); + mCache=tableBlock; + return tableBlock; + } + void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/StringPoolBuilder.java b/src/ARSCLib/com/reandroid/apk/StringPoolBuilder.java new file mode 100644 index 00000000..21825824 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/StringPoolBuilder.java @@ -0,0 +1,157 @@ +/* + * 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.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.value.ValueHeader; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONException; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; + +public class StringPoolBuilder { + private final Map> mSpecNameMap; + private final Set mTableStrings; + private int mCurrentPackageId; + private JSONArray mStyledStrings; + public StringPoolBuilder(){ + this.mSpecNameMap = new HashMap<>(); + this.mTableStrings = new HashSet<>(); + } + public void apply(TableBlock tableBlock){ + applyTableString(tableBlock.getTableStringPool()); + for(int pkgId:mSpecNameMap.keySet()){ + PackageBlock packageBlock=tableBlock.getPackageArray().getOrCreate(pkgId); + applySpecString(packageBlock.getSpecStringPool()); + } + } + private void applyTableString(TableStringPool stringPool){ + stringPool.fromJson(mStyledStrings); + stringPool.addStrings(getTableString()); + stringPool.refresh(); + } + private void applySpecString(SpecStringPool stringPool){ + int pkgId = stringPool.getPackageBlock().getId(); + stringPool.addStrings(getSpecString(pkgId)); + stringPool.refresh(); + } + public void scanDirectory(File resourcesDir) throws IOException { + mCurrentPackageId=0; + List pkgDirList=ApkUtil.listDirectories(resourcesDir); + for(File dir:pkgDirList){ + File pkgFile=new File(dir, PackageBlock.JSON_FILE_NAME); + scanFile(pkgFile); + List jsonFileList=ApkUtil.recursiveFiles(dir, ".json"); + for(File file:jsonFileList){ + if(file.equals(pkgFile)){ + continue; + } + scanFile(file); + } + } + } + public void scanFile(File jsonFile) throws IOException { + try{ + FileInputStream inputStream=new FileInputStream(jsonFile); + JSONObject jsonObject=new JSONObject(inputStream); + build(jsonObject); + }catch (JSONException ex){ + throw new IOException(jsonFile+": "+ex.getMessage()); + } + } + public void build(JSONObject jsonObject){ + scan(jsonObject); + } + public Set getTableString(){ + return mTableStrings; + } + public Set getSpecString(int pkgId){ + return mSpecNameMap.get(pkgId); + } + private void scan(JSONObject jsonObject){ + if(jsonObject.has(ValueHeader.NAME_entry_name)){ + addSpecName(jsonObject.optString(ValueHeader.NAME_entry_name)); + } + if(jsonObject.has(ApkUtil.NAME_value_type)){ + if(ValueType.STRING.name().equals(jsonObject.getString(ApkUtil.NAME_value_type))){ + String data= jsonObject.optString(ApkUtil.NAME_data, ""); + addTableString(data); + } + return; + }else if(jsonObject.has(PackageBlock.NAME_package_id)){ + mCurrentPackageId = jsonObject.getInt(PackageBlock.NAME_package_id); + } + Set keyList = jsonObject.keySet(); + for(String key:keyList){ + Object obj=jsonObject.get(key); + if(obj instanceof JSONObject){ + scan((JSONObject) obj); + continue; + } + if(obj instanceof JSONArray){ + JSONArray jsonArray = (JSONArray) obj; + if(TableBlock.NAME_styled_strings.equals(key)){ + this.mStyledStrings = jsonArray; + }else { + scan(jsonArray); + } + } + } + } + private void scan(JSONArray jsonArray){ + if(jsonArray==null){ + return; + } + for(Object obj:jsonArray.getArrayList()){ + if(obj instanceof JSONObject){ + scan((JSONObject) obj); + continue; + } + if(obj instanceof JSONArray){ + scan((JSONArray) obj); + } + } + } + private void addTableString(String name){ + if(name==null){ + return; + } + mTableStrings.add(name); + } + private void addSpecName(String name){ + if(name==null){ + return; + } + int pkgId=mCurrentPackageId; + if(pkgId==0){ + throw new IllegalArgumentException("Current package id is 0"); + } + Set specNames=mSpecNameMap.get(pkgId); + if(specNames==null){ + specNames=new HashSet<>(); + mSpecNameMap.put(pkgId, specNames); + } + specNames.add(name); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/TableBlockJson.java b/src/ARSCLib/com/reandroid/apk/TableBlockJson.java new file mode 100644 index 00000000..bf48d615 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/TableBlockJson.java @@ -0,0 +1,95 @@ +/* + * 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.arsc.BuildInfo; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.StagedAlias; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +public class TableBlockJson { + private final TableBlock tableBlock; + public TableBlockJson(TableBlock tableBlock){ + this.tableBlock=tableBlock; + } + public void writeJsonFiles(File outDir) throws IOException { + for(PackageBlock packageBlock: tableBlock.listPackages()){ + writePackageJsonFiles(outDir, packageBlock); + } + } + private void writePackageJsonFiles(File rootDir, PackageBlock packageBlock) throws IOException { + File pkgDir = new File(rootDir, getDirName(packageBlock)); + + writePackageJson(pkgDir, packageBlock); + + for(SpecTypePair specTypePair: packageBlock.listSpecTypePairs()){ + for(TypeBlock typeBlock:specTypePair.getTypeBlockArray().listItems()){ + writeTypeJsonFiles(pkgDir, typeBlock); + } + } + } + private void writePackageJson(File packageDirectory, PackageBlock packageBlock) throws IOException { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put(BuildInfo.NAME_arsc_lib_version, BuildInfo.getVersion()); + + jsonObject.put(PackageBlock.NAME_package_id, packageBlock.getId()); + jsonObject.put(PackageBlock.NAME_package_name, packageBlock.getName()); + StagedAlias stagedAlias=StagedAlias + .mergeAll(packageBlock.getStagedAliasList().getChildes()); + if(stagedAlias!=null){ + jsonObject.put(PackageBlock.NAME_staged_aliases, + stagedAlias.getStagedAliasEntryArray().toJson()); + } + + File packageFile = new File(packageDirectory, PackageBlock.JSON_FILE_NAME); + jsonObject.write(packageFile); + } + private void writeTypeJsonFiles(File packageDirectory, TypeBlock typeBlock) throws IOException { + File file=new File(packageDirectory, + getFileName(typeBlock) + ApkUtil.JSON_FILE_EXTENSION); + JSONObject jsonObject = typeBlock.toJson(); + jsonObject.write(file); + } + private String getFileName(TypeBlock typeBlock){ + StringBuilder builder=new StringBuilder(); + builder.append(String.format("%03d-", typeBlock.getIndex())); + builder.append(HexUtil.toHex2(typeBlock.getTypeId())); + String name= typeBlock.getTypeName(); + builder.append('-').append(name); + builder.append(typeBlock.getResConfig().getQualifiers()); + return builder.toString(); + } + private String getDirName(PackageBlock packageBlock){ + StringBuilder builder=new StringBuilder(); + builder.append(HexUtil.toHex2((byte) packageBlock.getId())); + builder.append("-"); + builder.append(packageBlock.getIndex()); + String name= ApkUtil.sanitizeForFileName(packageBlock.getName()); + if(name!=null){ + builder.append('-'); + builder.append(name); + } + return builder.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/TableBlockJsonBuilder.java b/src/ARSCLib/com/reandroid/apk/TableBlockJsonBuilder.java new file mode 100644 index 00000000..1880496f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/TableBlockJsonBuilder.java @@ -0,0 +1,89 @@ + /* + * 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.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.StagedAlias; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; + +public class TableBlockJsonBuilder { + private final StringPoolBuilder poolBuilder; + public TableBlockJsonBuilder(){ + poolBuilder=new StringPoolBuilder(); + } + public TableBlock scanDirectory(File resourcesDir) throws IOException { + if(!resourcesDir.isDirectory()){ + throw new IOException("No such directory: "+resourcesDir); + } + List pkgDirList=ApkUtil.listDirectories(resourcesDir); + if(pkgDirList.size()==0){ + throw new IOException("No package sub directory found in : "+resourcesDir); + } + TableBlock tableBlock=new TableBlock(); + poolBuilder.scanDirectory(resourcesDir); + poolBuilder.apply(tableBlock); + for(File pkgDir:pkgDirList){ + scanPackageDirectory(tableBlock, pkgDir); + } + tableBlock.sortPackages(); + tableBlock.refresh(); + return tableBlock; + } + private void scanPackageDirectory(TableBlock tableBlock, File pkgDir) throws IOException{ + File pkgFile=new File(pkgDir, PackageBlock.JSON_FILE_NAME); + if(!pkgFile.isFile()){ + throw new IOException("Invalid package directory! Package file missing: "+pkgFile); + } + FileInputStream inputStream=new FileInputStream(pkgFile); + JSONObject jsonObject=new JSONObject(inputStream); + PackageBlock pkg=tableBlock.getPackageArray() + .getOrCreate(jsonObject.getInt(PackageBlock.NAME_package_id)); + pkg.setName(jsonObject.optString(PackageBlock.NAME_package_name)); + if(jsonObject.has(PackageBlock.NAME_staged_aliases)){ + JSONArray stagedJson = jsonObject.getJSONArray(PackageBlock.NAME_staged_aliases); + StagedAlias stagedAlias = new StagedAlias(); + stagedAlias.getStagedAliasEntryArray().fromJson(stagedJson); + pkg.getStagedAliasList().add(stagedAlias); + } + List typeFileList = ApkUtil.listFiles(pkgDir, ApkUtil.JSON_FILE_EXTENSION); + typeFileList.remove(pkgFile); + for(File typeFile:typeFileList){ + loadType(pkg, typeFile); + } + pkg.sortTypes(); + } + private void loadType(PackageBlock packageBlock, File typeJsonFile) throws IOException{ + FileInputStream inputStream=new FileInputStream(typeJsonFile); + JSONObject jsonObject=new JSONObject(inputStream); + JSONObject configObj=jsonObject.getJSONObject(TypeBlock.NAME_config); + ResConfig resConfig=new ResConfig(); + resConfig.fromJson(configObj); + TypeBlock typeBlock=packageBlock.getSpecTypePairArray() + .getOrCreate( + ((byte)(0xff & jsonObject.getInt(TypeBlock.NAME_id))) + , resConfig); + typeBlock.fromJson(jsonObject); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/UncompressedFiles.java b/src/ARSCLib/com/reandroid/apk/UncompressedFiles.java new file mode 100644 index 00000000..cdc3422c --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/UncompressedFiles.java @@ -0,0 +1,232 @@ + /* + * 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.archive.ZipArchive; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.ZipEntry; + +public class UncompressedFiles implements JSONConvert { + private final Set mPathList; + private final Set mExtensionList; + private String mResRawDir; + public UncompressedFiles(){ + this.mPathList=new HashSet<>(); + this.mExtensionList=new HashSet<>(); + } + public void setResRawDir(String resRawDir){ + this.mResRawDir=resRawDir; + } + public void apply(ZipArchive archive){ + for(InputSource inputSource:archive.listInputSources()){ + apply(inputSource); + } + } + public void apply(InputSource inputSource){ + if(isUncompressed(inputSource.getAlias()) || isUncompressed(inputSource.getName())){ + inputSource.setMethod(ZipEntry.STORED); + }else { + inputSource.setMethod(ZipEntry.DEFLATED); + } + } + public boolean isUncompressed(String path){ + if(path==null){ + return false; + } + if(containsPath(path)||containsExtension(path)||isResRawDir(path)){ + return true; + } + String extension=getExtension(path); + return containsExtension(extension); + } + private boolean isResRawDir(String path){ + String dir=mResRawDir; + if(dir==null||dir.length()==0){ + return false; + } + return path.startsWith(dir); + } + public boolean containsExtension(String extension){ + if(extension==null){ + return false; + } + if(mExtensionList.contains(extension)){ + return true; + } + if(!extension.startsWith(".")){ + return mExtensionList.contains("."+extension); + } + return mExtensionList.contains(extension.substring(1)); + } + public boolean containsPath(String path){ + path=sanitizePath(path); + if(path==null){ + return false; + } + return mPathList.contains(path); + } + public void addPath(ZipArchive zipArchive){ + for(InputSource inputSource: zipArchive.listInputSources()){ + addPath(inputSource); + } + } + public void addPath(InputSource inputSource){ + if(inputSource.getMethod()!=ZipEntry.STORED){ + return; + } + addPath(inputSource.getAlias()); + } + public void addPath(String path){ + path=sanitizePath(path); + if(path==null){ + return; + } + mPathList.add(path); + } + public void removePath(String path){ + path=sanitizePath(path); + if(path==null){ + return; + } + mPathList.remove(path); + } + public void replacePath(String path, String rep){ + path=sanitizePath(path); + rep=sanitizePath(rep); + if(path==null||rep==null){ + return; + } + if(!mPathList.contains(path)){ + return; + } + mPathList.remove(path); + mPathList.add(rep); + } + public void addCommonExtensions(){ + for(String ext:COMMON_EXTENSIONS){ + addExtension(ext); + } + } + public void addExtension(String extension){ + if(extension==null || extension.length()==0){ + return; + } + mExtensionList.add(extension); + } + public void clearPaths(){ + mPathList.clear(); + } + public void clearExtensions(){ + mExtensionList.clear(); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(NAME_extensions, new JSONArray(mExtensionList)); + jsonObject.put(NAME_paths, new JSONArray(mPathList)); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + clearExtensions(); + clearPaths(); + if(json==null){ + return; + } + JSONArray extensions = json.optJSONArray(NAME_extensions); + if(extensions!=null){ + int length = extensions.length(); + for(int i=0;i0){ + i++; + path=path.substring(i); + } + i = path.lastIndexOf('.'); + if(i>0){ + return path.substring(i); + } + return null; + } + + public static final String JSON_FILE = "uncompressed-files.json"; + public static final String NAME_paths = "paths"; + public static final String NAME_extensions = "extensions"; + public static String[] COMMON_EXTENSIONS=new String[]{ + ".png", + ".jpg", + ".mp3", + ".mp4", + ".wav", + ".webp", + }; +} diff --git a/src/ARSCLib/com/reandroid/apk/XmlHelper.java b/src/ARSCLib/com/reandroid/apk/XmlHelper.java new file mode 100644 index 00000000..4f2c2e08 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/XmlHelper.java @@ -0,0 +1,95 @@ +/* + * 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.arsc.item.StringItem; +import com.reandroid.xml.*; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.Closeable; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class XmlHelper { + + public static Map readAttributes(XmlPullParser parser, String elementName) throws IOException, XmlPullParserException { + if(!findElement(parser, elementName)){ + return null; + } + return mapAttributes(parser); + } + public static Map mapAttributes(XmlPullParser parser){ + Map map = new HashMap<>(); + int count = parser.getAttributeCount(); + for(int i = 0; i < count; i++){ + String name = parser.getAttributeName(i); + int index = name.indexOf(':'); + if(index > 0 && index < name.length() && !name.startsWith("xmlns:")){ + index++; + name = name.substring(index); + } + map.put(name, + parser.getAttributeValue(i)); + } + return map; + } + private static boolean findElement(XmlPullParser parser, String elementName) throws IOException, XmlPullParserException { + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT){ + if(event != XmlPullParser.START_TAG){ + continue; + } + if(elementName.equals(parser.getName())){ + return true; + } + } + return false; + } + + public static void setTextContent(XMLElement element, StringItem stringItem){ + if(stringItem==null){ + element.clearChildNodes(); + return; + } + if(!stringItem.hasStyle()){ + element.setTextContent(stringItem.get()); + }else { + element.setSpannableText(stringItem.getXml()); + } + } + public static String toXMLTagName(String typeName){ + // e.g ^attr-private + if(typeName.length()>0 && typeName.charAt(0)=='^'){ + typeName = typeName.substring(1); + } + return typeName; + } + + public static void closeSilent(Object obj){ + if(!(obj instanceof Closeable)){ + return; + } + Closeable closeable = (Closeable) obj; + try { + closeable.close(); + } catch (IOException ignored) { + } + } + + public static final String RESOURCES_TAG = "resources"; +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoder.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoder.java new file mode 100644 index 00000000..ba597822 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoder.java @@ -0,0 +1,27 @@ +/* + * 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 com.reandroid.xml.XMLElement; + +abstract class BagDecoder extends DecoderTableEntry { + public BagDecoder(EntryStore entryStore){ + super(entryStore); + } + public abstract boolean canDecode(ResTableMapEntry mapEntry); +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderArray.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderArray.java new file mode 100644 index 00000000..61d40a97 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderArray.java @@ -0,0 +1,122 @@ +/* + * 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 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.enableIndent(true); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + + PackageBlock packageBlock = entry.getPackageBlock(); + EntryStore entryStore = getEntryStore(); + ResValueMap[] resValueMaps = mapEntry.listResValueMap(); + boolean zero_name = isZeroNameArray(resValueMaps); + for(int i = 0; i < resValueMaps.length; i++){ + ResValueMap valueMap = resValueMaps[i]; + String childTag = "item"; + writer.enableIndent(true); + writer.startTag(childTag); + if(zero_name){ + String name = ValueDecoder.decodeAttributeName( + entryStore, packageBlock, valueMap.getName()); + writer.attribute("name", name); + } + 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[] valueMapList = mapEntry.listResValueMap(); + if(valueMapList == null || valueMapList.length == 0){ + return false; + } + if(isIndexedArray(valueMapList)){ + return true; + } + return isZeroNameArray(valueMapList); + } + private static boolean isIndexedArray(ResValueMap[] resValueMapList){ + int length = resValueMapList.length; + for(int i = 0; i < length; i++){ + ResValueMap valueMap = resValueMapList[i]; + int name = valueMap.getName(); + int high = (name >> 16) & 0xffff; + if(high!=0x0100 && high!=0x0200){ + return false; + } + int low = name & 0xffff; + int id = low - 1; + if(id!=i){ + return false; + } + } + return true; + } + private static boolean isZeroNameArray(ResValueMap[] resValueMapList){ + int length = resValueMapList.length; + for(int i = 0; i < length; i++){ + if(!isZeroName(resValueMapList[i])){ + return false; + } + } + return true; + } + private static boolean isZeroName(ResValueMap resValueMap){ + return resValueMap.getName() == 0; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderAttr.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderAttr.java new file mode 100644 index 00000000..af2a0958 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderAttr.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.XmlHelper; +import com.reandroid.arsc.array.CompoundItemArray; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.*; +import com.reandroid.arsc.value.attribute.AttributeBag; +import com.reandroid.arsc.value.attribute.AttributeBagItem; +import com.reandroid.common.EntryStore; + +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.enableIndent(true); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + writeParentAttributes(writer, mapEntry.getValue()); + ResValueMap formatsMap = mapEntry.getByType(AttributeType.FORMATS); + + AttributeDataFormat bagType = AttributeDataFormat.typeOfBag(formatsMap.getData()); + + ResValueMap[] bagItems = mapEntry.listResValueMap(); + + + for(int i = 0; i < bagItems.length; i++){ + ResValueMap item = bagItems[i]; + AttributeType attributeType = item.getAttributeType(); + if(attributeType != null){ + continue; + } + writer.enableIndent(true); + writer.startTag(bagType.getName()); + + String name = item.decodeName(); + writer.attribute("name", name); + int rawVal = item.getData(); + String value; + if(item.getValueType() == ValueType.INT_HEX){ + value = HexUtil.toHex8(rawVal); + }else { + value = Integer.toString(rawVal); + } + writer.text(value); + + writer.endTag(bagType.getName()); + } + return writer.endTag(tag); + } + + private void writeParentAttributes(EntryWriter writer, CompoundItemArray itemArray) throws IOException { + for(ResValueMap valueMap : itemArray.getChildes()){ + AttributeType type = valueMap.getAttributeType(); + if(type == null){ + continue; + } + String value; + if(type == AttributeType.FORMATS){ + value = AttributeDataFormat.toString( + AttributeDataFormat.decodeValueTypes(valueMap.getData())); + }else { + value = Integer.toString(valueMap.getData()); + } + if(value == null){ + continue; + } + writer.attribute(type.getName(), value); + } + } + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return AttributeBag.isAttribute(mapEntry); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderCommon.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderCommon.java new file mode 100644 index 00000000..232b2503 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderCommon.java @@ -0,0 +1,79 @@ +/* + * 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.enableIndent(true); + 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.enableIndent(true); + writer.startTag(childTag); + + String name = ValueDecoder.decodeAttributeName( + entryStore, packageBlock, valueMap.getName()); + writer.attribute("name", name); + + writeText(writer, valueMap); + + writer.endTag(childTag); + } + return writer.endTag(tag); + } + @Override + public boolean canDecode(ResTableMapEntry mapEntry) { + return mapEntry !=null; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderPlural.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderPlural.java new file mode 100644 index 00000000..2831589d --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/BagDecoderPlural.java @@ -0,0 +1,90 @@ +/* + * 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.value.*; +import com.reandroid.arsc.value.plurals.PluralsQuantity; +import com.reandroid.common.EntryStore; + +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.enableIndent(true); + 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.enableIndent(true); + writer.startTag(childTag); + + AttributeType quantity = valueMap.getAttributeType(); + if(quantity == null || !quantity.isPlural()){ + throw new IOException("Unknown plural quantity: " + valueMap); + } + writer.attribute("quantity", quantity.getName()); + + 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/ARSCLib/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java new file mode 100644 index 00000000..45982a60 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderResTableEntry.java @@ -0,0 +1,45 @@ +/* + * 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.enableIndent(true); + writer.startTag(tag); + writer.attribute("name", entry.getName()); + if(!isId(tag)){ + writeText(writer, entry.getPackageBlock(), tableEntry.getValue()); + } + return writer.endTag(tag); + } + + private boolean isId(String tag){ + return "id".equals(tag); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java new file mode 100644 index 00000000..f0e7cff2 --- /dev/null +++ b/src/ARSCLib/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/ARSCLib/com/reandroid/apk/xmldecoder/DecoderTableEntry.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderTableEntry.java new file mode 100644 index 00000000..3255d716 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/DecoderTableEntry.java @@ -0,0 +1,60 @@ +/* + * 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.*; +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()); + writer.text(value); + } + } + void writeText(EntryWriter writer, ResValueMap attributeValue) + throws IOException { + if(attributeValue.getValueType() == ValueType.STRING){ + XMLDecodeHelper.writeTextContent(writer, attributeValue.getDataAsPoolString()); + }else { + String value = ValueDecoder.decode(getEntryStore(), + attributeValue.getPackageBlock().getId(), + attributeValue); + writer.text(value); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriter.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriter.java new file mode 100644 index 00000000..20bd77c8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriter.java @@ -0,0 +1,29 @@ +/* + * 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; + void enableIndent(boolean enable); +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterElement.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterElement.java new file mode 100644 index 00000000..ad14a221 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterElement.java @@ -0,0 +1,108 @@ +/* + * 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; + private boolean mEnableIndent; + + public EntryWriterElement(){ + } + + public XMLElement getElement() { + return mResult; + } + @Override + public void setFeature(String name, Object value) { + if(!FEATURE_INDENT.equals(name)){ + return; + } + boolean state = false; + if(value instanceof Boolean){ + state = (Boolean)value; + } + mEnableIndent = state; + } + @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; + if(mEnableIndent){ + xmlElement.setIndent(2); + xmlElement.setIndentScale(1.0f); + }else { + xmlElement.setIndent(0); + xmlElement.setIndentScale(0.0f); + } + 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 { + } + @Override + public void enableIndent(boolean enable){ + setFeature(FEATURE_INDENT, enable); + } + + private static final String FEATURE_INDENT = "http://xmlpull.org/v1/doc/features.html#indent-output"; +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java new file mode 100644 index 00000000..163688b2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/EntryWriterSerializer.java @@ -0,0 +1,71 @@ +/* + * 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) { + if(value == null){ + value = false; + }else if(!(value instanceof Boolean)){ + return; + } + xmlSerializer.setFeature(name, (Boolean)value); + } + @Override + public XmlSerializer startTag(String name) throws IOException { + 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(); + } + @Override + public void enableIndent(boolean enable){ + setFeature(FEATURE_INDENT, enable); + } + + private static final String FEATURE_INDENT = "http://xmlpull.org/v1/doc/features.html#indent-output"; +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java new file mode 100644 index 00000000..eb5db63c --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apk.xmldecoder; + +import com.android.org.kxml2.io.KXmlSerializer; +import com.reandroid.apk.ApkModule; +import com.reandroid.archive.InputSource; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.chunk.xml.ResXmlPullParser; +import com.reandroid.arsc.decoder.Decoder; +import com.reandroid.xml.XmlParserToSerializer; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class ResXmlDocumentSerializer implements ResXmlPullParser.DocumentLoadedListener{ + private final Object mLock = new Object(); + private final ResXmlPullParser parser; + private final XmlSerializer serializer; + private final XmlParserToSerializer parserToSerializer; + private boolean validateXmlNamespace; + private String mCurrentPath; + public ResXmlDocumentSerializer(ResXmlPullParser parser){ + this.parser = parser; + this.serializer = new KXmlSerializer(); + this.parserToSerializer = new XmlParserToSerializer(parser, serializer); + this.parser.setDocumentLoadedListener(this); + } + public ResXmlDocumentSerializer(Decoder decoder){ + this(new ResXmlPullParser(decoder)); + } + public ResXmlDocumentSerializer(ApkModule apkModule){ + this(createDecoder(apkModule)); + } + + public void write(InputSource inputSource, File file) + throws IOException, XmlPullParserException { + write(inputSource.openStream(), file); + } + public void write(InputSource inputSource, OutputStream outputStream) + throws IOException, XmlPullParserException { + write(inputSource.openStream(), outputStream); + inputSource.disposeInputSource(); + } + public void write(InputStream inputStream, OutputStream outputStream) + throws IOException, XmlPullParserException { + synchronized (mLock){ + this.parser.setInput(inputStream, null); + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + this.serializer.setOutput(writer); + try{ + this.parserToSerializer.write(); + }catch (Exception ex){ + throw getError(ex); + } + writer.close(); + outputStream.close(); + mCurrentPath = null; + } + } + public void write(InputStream inputStream, File file) + throws IOException, XmlPullParserException { + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + mCurrentPath = String.valueOf(file); + FileOutputStream outputStream = new FileOutputStream(file); + write(inputStream, outputStream); + } + public void write(ResXmlDocument xmlDocument, File file) + throws IOException, XmlPullParserException { + mCurrentPath = String.valueOf(file); + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + write(xmlDocument, outputStream); + } + public void write(ResXmlDocument xmlDocument, OutputStream outputStream) + throws IOException, XmlPullParserException { + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + write(xmlDocument, writer); + writer.close(); + outputStream.close(); + } + public void write(ResXmlDocument xmlDocument, Writer writer) + throws IOException, XmlPullParserException { + synchronized (mLock){ + this.parser.setResXmlDocument(xmlDocument); + this.serializer.setOutput(writer); + this.parserToSerializer.write(); + writer.flush(); + } + } + public Decoder getDecoder(){ + return parser.getDecoder(); + } + + public void setValidateXmlNamespace(boolean validateXmlNamespace) { + this.validateXmlNamespace = validateXmlNamespace; + } + @Override + public ResXmlDocument onDocumentLoaded(ResXmlDocument resXmlDocument) { + if(!validateXmlNamespace){ + return resXmlDocument; + } + XMLNamespaceValidator.validateNamespaces(resXmlDocument); + return resXmlDocument; + } + private IOException getError(Exception exception){ + String path = mCurrentPath; + if(exception instanceof IOException){ + String msg = path + ":" + exception.getMessage(); + IOException ioException = new IOException(msg); + ioException.setStackTrace(exception.getStackTrace()); + Throwable cause = ioException.getCause(); + if(cause != null){ + ioException.initCause(cause); + } + return ioException; + } + String msg = path + ":" + exception.getClass() + ":" + exception.getMessage(); + IOException otherException = new IOException(msg); + otherException.setStackTrace(exception.getStackTrace()); + Throwable cause = otherException.getCause(); + if(cause != null){ + otherException.initCause(cause); + } + return otherException; + } + + private static Decoder createDecoder(ApkModule apkModule){ + Decoder decoder = Decoder.create(apkModule.getTableBlock()); + decoder.setApkFile(apkModule); + return decoder; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLBagDecoder.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLBagDecoder.java new file mode 100644 index 00000000..688eaf87 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLBagDecoder.java @@ -0,0 +1,39 @@ +/* + * 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 com.reandroid.xml.XMLElement; + +import java.io.IOException; + +@Deprecated +public class XMLBagDecoder { + private final DecoderResTableEntryMap mDocumentDecoder; + private final EntryWriterElement mWriter; + public XMLBagDecoder(EntryStore entryStore){ + mDocumentDecoder = new DecoderResTableEntryMap<>(entryStore); + mWriter = new EntryWriterElement(); + } + public void decode(ResTableMapEntry mapEntry, XMLElement parentElement){ + try { + XMLElement child = mDocumentDecoder.decode(mapEntry, mWriter); + parentElement.addChild(child); + } catch (IOException exception) { + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLDecodeHelper.java new file mode 100644 index 00000000..55a24f5a --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLDecodeHelper.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.arsc.decoder.ValueDecoder; +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()){ + String text = stringItem.get(); + text = ValueDecoder.escapeSpecialCharacter(text); + text = ValueDecoder.quoteWhitespace(text); + writer.text(text); + }else { + String xml = stringItem.getXml(); + XMLElement element = parseSpanSafe(xml); + if(element != null){ + writeParsedSpannable(writer, element); + }else { + // TODO: throw or investigate the reason + writer.text(xml); + } + } + } + public static void writeParsedSpannable(EntryWriter writer, XMLElement spannableParent) throws IOException { + for(XMLNode xmlNode : spannableParent.getChildNodes()){ + if(xmlNode instanceof XMLText){ + String text = ((XMLText)xmlNode).getText(true); + writer.enableIndent(false); + writer.text(ValueDecoder.escapeSpecialCharacter(text)); + }else if(xmlNode instanceof XMLElement){ + writeElement(writer, (XMLElement) xmlNode); + } + } + } + private static void writeElement(EntryWriter writer, XMLElement element) throws IOException { + writer.enableIndent(false); + writer.startTag(element.getTagName()); + for(XMLAttribute xmlAttribute : element.listAttributes()){ + writer.attribute(xmlAttribute.getName(), xmlAttribute.getValue()); + } + for(XMLNode xmlNode : element.getChildNodes()){ + if(xmlNode instanceof XMLText){ + String text = ((XMLText)xmlNode).getText(true); + writer.text(text); + }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; + } + } + +} diff --git a/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoder.java new file mode 100644 index 00000000..ad9fc1f0 --- /dev/null +++ b/src/ARSCLib/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/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java new file mode 100644 index 00000000..9313dbfc --- /dev/null +++ b/src/ARSCLib/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/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java new file mode 100644 index 00000000..1286009c --- /dev/null +++ b/src/ARSCLib/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/ARSCLib/com/reandroid/apk/xmldecoder/XMLNamespaceValidator.java b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLNamespaceValidator.java new file mode 100644 index 00000000..1f42220f --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmldecoder/XMLNamespaceValidator.java @@ -0,0 +1,107 @@ +/* + * 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.xml.*; + +import java.util.Collection; + +public class XMLNamespaceValidator { + private static final String URI_ANDROID = "http://schemas.android.com/apk/res/android"; + private static final String URI_APP = "http://schemas.android.com/apk/res-auto"; + private static final String PREFIX_ANDROID = "android"; + private static final String PREFIX_APP = "app"; + private final ResXmlDocument xmlBlock; + public XMLNamespaceValidator(ResXmlDocument xmlBlock){ + this.xmlBlock=xmlBlock; + } + public void validate(){ + validateNamespaces(xmlBlock); + } + + public static boolean isValid(ResXmlAttribute attribute){ + int resourceId = attribute.getNameResourceID(); + if(resourceId == 0){ + return attribute.getUri() == null; + } + if(isAndroid(toPackageId(resourceId))){ + return isValidAndroidNamespace(attribute); + }else { + return isValidAppNamespace(attribute); + } + } + public static void validateNamespaces(ResXmlDocument resXmlDocument){ + validateNamespaces(resXmlDocument.getResXmlElement()); + } + public static void validateNamespaces(ResXmlElement element){ + validateNamespaces(element.listAttributes()); + for(ResXmlElement child : element.listElements()){ + validateNamespaces(child); + } + } + + private static void validateNamespaces(Collection attributeList){ + for(ResXmlAttribute attribute : attributeList){ + validateNamespace(attribute); + } + } + private static void validateNamespace(ResXmlAttribute attribute){ + int resourceId = attribute.getNameResourceID(); + if(resourceId == 0){ + attribute.setNamespaceReference(-1); + return; + } + if(isAndroid(toPackageId(resourceId))){ + if(!isValidAndroidNamespace(attribute)){ + attribute.setNamespace(URI_ANDROID, PREFIX_ANDROID); + } + }else { + if(!isValidAppNamespace(attribute)){ + attribute.setNamespace(URI_APP, PREFIX_APP); + } + } + } + + private static boolean isValidAppNamespace(ResXmlAttribute attribute){ + String uri = attribute.getUri(); + String prefix = attribute.getNamePrefix(); + if(URI_ANDROID.equals(uri) || PREFIX_ANDROID.equals(prefix)){ + return false; + } + if(isEmpty(uri) || isEmpty(prefix)){ + return false; + } + return true; + } + private static boolean isValidAndroidNamespace(ResXmlAttribute attribute){ + return URI_ANDROID.equals(attribute.getUri()) + && PREFIX_ANDROID.equals(attribute.getNamePrefix()); + } + + private static boolean isAndroid(int pkgId){ + return pkgId==1; + } + private static int toPackageId(int resId){ + return (resId >> 24 & 0xFF); + } + private static boolean isEmpty(String str){ + if(str==null){ + return true; + } + str=str.trim(); + return str.length()==0; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeException.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeException.java new file mode 100644 index 00000000..c997bba6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeException.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.apk.xmlencoder; + +public class EncodeException extends IllegalArgumentException{ + public EncodeException(String message){ + super(message); + } + public EncodeException(String message, Throwable cause){ + super(message, cause); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeMaterials.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeMaterials.java new file mode 100644 index 00000000..46f3ad47 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeMaterials.java @@ -0,0 +1,433 @@ +/* + * 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.xmlencoder; + +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.FrameworkApk; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.item.SpecString; +import com.reandroid.arsc.util.FrameworkTable; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.util.ResNameMap; +import com.reandroid.arsc.value.Entry; +import com.reandroid.identifiers.PackageIdentifier; +import com.reandroid.identifiers.ResourceIdentifier; +import com.reandroid.identifiers.TableIdentifier; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; + +public class EncodeMaterials { + private PackageBlock currentPackage; + private final Set frameworkTables = new HashSet<>(); + private APKLogger apkLogger; + private boolean mForceCreateNamespaces = true; + private Set mFrameworkPackageNames; + private final ResNameMap mLocalResNameMap = new ResNameMap<>(); + private final TableIdentifier tableIdentifier = new TableIdentifier(); + private PackageIdentifier currentPackageIdentifier; + private Integer mMainPackageId; + public EncodeMaterials(){ + } + public void setMainPackageId(Integer mainPackageId){ + this.mMainPackageId = mainPackageId; + } + public PackageBlock pickMainPackageBlock(TableBlock tableBlock){ + if(mMainPackageId != null){ + return tableBlock.pickOne(mMainPackageId); + } + return tableBlock.pickOne(); + } + public TableIdentifier getTableIdentifier(){ + return tableIdentifier; + } + public void setEntryName(Entry entry, String name){ + PackageBlock packageBlock = entry.getPackageBlock(); + SpecString specString = packageBlock + .getSpecStringPool().getOrCreate(name); + entry.setSpecReference(specString); + } + public SpecString getSpecString(String name){ + return currentPackage.getSpecStringPool() + .get(name) + .get(0); + } + public Entry getAttributeBlock(String refString){ + String type = "attr"; + Entry entry = getAttributeBlock(type, refString); + if(entry == null){ + type = "^attr-private"; + entry = getAttributeBlock(type, refString); + } + return entry; + } + private Entry getAttributeBlock(String type, String refString){ + String packageName = null; + String name = refString; + int i=refString.lastIndexOf(':'); + if(i>=0){ + packageName=refString.substring(0, i); + name=refString.substring(i+1); + } + if(EncodeUtil.isEmpty(packageName) + || packageName.equals(getCurrentPackageName()) + || !isFrameworkPackageName(packageName)){ + + return getLocalEntry(type, name); + } + return getFrameworkEntry(type, name); + } + public int resolveReference(String refString){ + if("@null".equals(refString)){ + return 0; + } + Matcher matcher = ValueDecoder.PATTERN_REFERENCE.matcher(refString); + if(!matcher.find()){ + ValueDecoder.EncodeResult ref = ValueDecoder.encodeHexReference(refString); + if(ref!=null){ + return ref.value; + } + ref = ValueDecoder.encodeNullReference(refString); + if(ref!=null){ + return ref.value; + } + throw new EncodeException( + "Not proper reference string: '"+refString+"'"); + } + String prefix=matcher.group(1); + String packageName = matcher.group(2); + if(packageName!=null && packageName.endsWith(":")){ + packageName=packageName.substring(0, packageName.length()-1); + } + String type = matcher.group(4); + String name = matcher.group(5); + if(isLocalPackageName(packageName)){ + return resolveLocalResourceId(packageName, type, name); + } + + if(EncodeUtil.isEmpty(packageName) + || packageName.equals(getCurrentPackageName()) + || !isFrameworkPackageName(packageName)){ + return resolveLocalResourceId(type, name); + } + return resolveFrameworkResourceId(packageName, type, name); + } + private int resolveLocalResourceId(String packageName, String type, String name){ + ResourceIdentifier ri = tableIdentifier.get(packageName, type, name); + if(ri != null){ + return ri.getResourceId(); + } + EntryGroup entryGroup=getLocalEntryGroup(type, name); + if(entryGroup!=null){ + return entryGroup.getResourceId(); + } + throw new EncodeException("Local entry not found: " + + "package=" + packageName + + ", type=" + type + + ", name=" + name); + } + public int resolveLocalResourceId(String type, String name){ + PackageIdentifier pi = this.currentPackageIdentifier; + if(pi != null){ + ResourceIdentifier ri = pi.getResourceIdentifier(type, name); + if(ri != null){ + return ri.getResourceId(); + } + } + EntryGroup entryGroup=getLocalEntryGroup(type, name); + if(entryGroup!=null){ + return entryGroup.getResourceId(); + } + throw new EncodeException("Local entry not found: " + + "type="+type+ + ", name="+name); + } + public int resolveFrameworkResourceId(String packageName, String type, String name){ + Entry entry = getFrameworkEntry(packageName, type, name); + if(entry !=null){ + return entry.getResourceId(); + } + throw new EncodeException("Framework entry not found: " + + "package="+packageName+ + ", type="+type+ + ", name="+name); + } + public int resolveFrameworkResourceId(int packageId, String type, String name){ + Entry entry = getFrameworkEntry(packageId, type, name); + if(entry !=null){ + return entry.getResourceId(); + } + throw new EncodeException("Framework entry not found: " + + "packageId=" + HexUtil.toHex2((byte) packageId)+ + ", type="+type+ + ", name="+name); + } + public EntryGroup getLocalEntryGroup(String type, String name){ + for(EntryGroup entryGroup : currentPackage.listEntryGroup()){ + if(type.equals(entryGroup.getTypeName()) && + name.equals(entryGroup.getSpecName())){ + return entryGroup; + } + } + for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){ + for(EntryGroup entryGroup : packageBlock.listEntryGroup()){ + if(type.equals(entryGroup.getTypeName()) && + name.equals(entryGroup.getSpecName())){ + return entryGroup; + } + } + } + return null; + } + public Entry getLocalEntry(String type, String name){ + Entry entry =mLocalResNameMap.get(type, name); + if(entry !=null){ + return entry; + } + loadLocalEntryMap(type); + entry =mLocalResNameMap.get(type, name); + if(entry !=null){ + return entry; + } + entry = searchLocalEntry(type, name); + if(entry !=null){ + mLocalResNameMap.add(type, name, entry); + } + return entry; + } + private Entry searchLocalEntry(String type, String name){ + for(EntryGroup entryGroup : currentPackage.listEntryGroup()){ + if(type.equals(entryGroup.getTypeName()) && + name.equals(entryGroup.getSpecName())){ + return entryGroup.pickOne(); + } + } + SpecTypePair specTypePair=currentPackage.getSpecTypePair(type); + if(specTypePair!=null){ + for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){ + for(Entry entry :typeBlock.listEntries(true)){ + if(name.equals(entry.getName())){ + return entry; + } + } + break; + } + } + for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){ + if(packageBlock==currentPackage){ + continue; + } + specTypePair=packageBlock.getSpecTypePair(type); + if(specTypePair!=null){ + for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){ + for(Entry entry :typeBlock.listEntries(true)){ + if(name.equals(entry.getName())){ + return entry; + } + } + break; + } + } + } + return null; + } + private void loadLocalEntryMap(String type){ + ResNameMap localMap = mLocalResNameMap; + for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){ + SpecTypePair specTypePair=packageBlock.getSpecTypePair(type); + if(specTypePair!=null){ + for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){ + for(Entry entry :typeBlock.listEntries(true)){ + localMap.add(entry.getTypeName(), + entry.getName(), entry); + } + } + } + } + } + public Entry getFrameworkEntry(String type, String name){ + for(FrameworkTable table:frameworkTables){ + Entry entry = table.searchEntry(type, name); + if(entry !=null){ + return entry; + } + } + return null; + } + private boolean isFrameworkPackageName(String packageName){ + return getFrameworkPackageNames().contains(packageName); + } + private Set getFrameworkPackageNames(){ + if(mFrameworkPackageNames!=null){ + return mFrameworkPackageNames; + } + Set results=new HashSet<>(); + for(FrameworkTable table:frameworkTables){ + for(PackageBlock packageBlock:table.listPackages()){ + results.add(packageBlock.getName()); + } + } + mFrameworkPackageNames=results; + return results; + } + public Entry getFrameworkEntry(String packageName, String type, String name){ + for(FrameworkTable table:frameworkTables){ + for(PackageBlock packageBlock:table.listPackages()){ + if(packageName.equals(packageBlock.getName())){ + Entry entry = table.searchEntry(type, name); + if(entry !=null){ + return entry; + } + } + } + } + return null; + } + public Entry getFrameworkEntry(int packageId, String type, String name){ + for(FrameworkTable table:frameworkTables){ + for(PackageBlock packageBlock:table.listPackages()){ + if(packageId==packageBlock.getId()){ + Entry entry = table.searchEntry(type, name); + if(entry !=null){ + return entry; + } + } + } + } + return null; + } + public EncodeMaterials setForceCreateNamespaces(boolean force) { + this.mForceCreateNamespaces = force; + return this; + } + public EncodeMaterials setCurrentPackage(PackageBlock currentPackage) { + this.currentPackage = currentPackage; + onCurrentPackageChanged(currentPackage); + return this; + } + public EncodeMaterials setCurrentLocalPackage(PackageIdentifier packageIdentifier) { + this.currentPackageIdentifier = packageIdentifier; + return this; + } + private void onCurrentPackageChanged(PackageBlock currentPackage){ + if(currentPackage == null){ + return; + } + PackageIdentifier pi = tableIdentifier.getByPackage(currentPackage); + if(pi != null){ + this.currentPackageIdentifier = pi; + } + } + private boolean isLocalPackageName(String packageName){ + if(packageName == null){ + return false; + } + for(PackageIdentifier pi : tableIdentifier.getPackages()){ + if(packageName.equals(pi.getName())){ + return true; + } + } + return false; + } + private boolean isUniquePackageNames(){ + Set names = new HashSet<>(); + for(PackageIdentifier pi : tableIdentifier.getPackages()){ + names.add(pi.getName()); + } + return names.size() == tableIdentifier.getPackages().size(); + } + private boolean isUniquePackageIds(){ + Set ids = new HashSet<>(); + for(PackageIdentifier pi : tableIdentifier.getPackages()){ + ids.add(pi.getId()); + } + return ids.size() == tableIdentifier.getPackages().size(); + } + public EncodeMaterials addFramework(FrameworkApk frameworkApk) { + if(frameworkApk!=null){ + addFramework(frameworkApk.getTableBlock()); + } + return this; + } + public EncodeMaterials addFramework(FrameworkTable frameworkTable) { + frameworkTable.loadResourceNameMap(); + this.frameworkTables.add(frameworkTable); + this.mFrameworkPackageNames=null; + return this; + } + public EncodeMaterials setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + return this; + } + + public PackageBlock getCurrentPackage() { + return currentPackage; + } + public boolean isForceCreateNamespaces() { + return mForceCreateNamespaces; + } + + public String getCurrentPackageName(){ + return currentPackage.getName(); + } + public int getCurrentPackageId(){ + return currentPackage.getId(); + } + + public void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + public void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + public void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } + public static EncodeMaterials create(TableBlock tableBlock){ + PackageBlock packageBlock = tableBlock.pickOne(); + if(packageBlock==null){ + throw new EncodeException("No packages found on table block"); + } + return create(packageBlock); + } + public static EncodeMaterials create(PackageBlock packageBlock){ + EncodeMaterials encodeMaterials = new EncodeMaterials(); + + TableBlock tableBlock = packageBlock.getTableBlock(); + encodeMaterials.getTableIdentifier().load(tableBlock); + encodeMaterials.setCurrentPackage(packageBlock); + + for(TableBlock frameworkTable:tableBlock.getFrameWorks()){ + if(frameworkTable instanceof FrameworkTable){ + encodeMaterials.addFramework((FrameworkTable) frameworkTable); + } + } + return encodeMaterials; + } + +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeUtil.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeUtil.java new file mode 100644 index 00000000..6c3724ad --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/EncodeUtil.java @@ -0,0 +1,156 @@ + /* + * 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.xmlencoder; + + import com.reandroid.apk.ApkUtil; + + import java.io.File; + import java.util.Comparator; + import java.util.List; + import java.util.regex.Matcher; + import java.util.regex.Pattern; + + public class EncodeUtil { + public static void sortStrings(List stringList){ + Comparator cmp=new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } + }; + stringList.sort(cmp); + } + public static boolean isPublicXml(File file){ + if(!ApkUtil.FILE_NAME_PUBLIC_XML.equals(file.getName())){ + return false; + } + File dir = file.getParentFile(); + return dir!=null && dir.getName().equals("values"); + } + public static void sortPublicXml(List fileList){ + Comparator cmp=new Comparator() { + @Override + public int compare(File f1, File f2) { + String n1=f1.getAbsolutePath(); + String n2=f2.getAbsolutePath(); + return n1.compareTo(n2); + } + }; + fileList.sort(cmp); + } + public static void sortValuesXml(List fileList){ + Comparator cmp=new Comparator() { + @Override + public int compare(File f1, File f2) { + String n1=getValuesXmlCompare(f1); + String n2=getValuesXmlCompare(f2); + return n1.compareTo(n2); + } + }; + fileList.sort(cmp); + } + private static String getValuesXmlCompare(File file){ + String name=file.getName().toLowerCase(); + if(name.equals("public.xml")){ + return "0"; + } + if(name.equals("ids.xml")){ + return "1"; + } + if(name.contains("attr")){ + return "2"; + } + return "3 "+name; + } + public static boolean isEmpty(String text){ + if(text==null){ + return true; + } + text=text.trim(); + return text.length()==0; + } + public static String getQualifiersFromValuesXml(File valuesXml){ + String dirName=valuesXml.getParentFile().getName(); + int i=dirName.indexOf('-'); + if(i>0){ + return dirName.substring(i); + } + return ""; + } + public static String getEntryPathFromResFile(File resFile){ + File typeDir=resFile.getParentFile(); + File resDir=typeDir.getParentFile(); + return resDir.getName() + +"/"+typeDir.getName() + +"/"+resFile.getName(); + } + public static String getEntryNameFromResFile(File resFile){ + String name=resFile.getName(); + String ninePatch=".9.png"; + if(name.endsWith(ninePatch)){ + return name.substring(0, name.length()-ninePatch.length()); + } + int i=name.lastIndexOf('.'); + if(i>0){ + name = name.substring(0, i); + } + return name; + } + public static String getQualifiersFromResFile(File resFile){ + String name=resFile.getParentFile().getName(); + int i=name.indexOf('-'); + if(i>0){ + return name.substring(i); + } + return ""; + } + public static String getTypeNameFromResFile(File resFile){ + String name=resFile.getParentFile().getName(); + int i=name.indexOf('-'); + if(i>0){ + name=name.substring(0, i); + } + if(!name.equals("plurals") && name.endsWith("s")){ + name=name.substring(0, name.length()-1); + } + return name; + } + public static String getTypeNameFromValuesXml(File valuesXml){ + String name=valuesXml.getName(); + name=name.substring(0, name.length()-4); + if(!name.equals("plurals") && name.endsWith("s")){ + name=name.substring(0, name.length()-1); + } + return name; + } + public static String sanitizeType(String type){ + if(type.startsWith("^attr")){ + return type; + } + Matcher matcher=PATTERN_TYPE.matcher(type); + if(!matcher.find()){ + return ""; + } + return matcher.group(1); + } + public static final String NULL_PACKAGE_NAME = "NULL_PACKAGE_NAME"; + private static final Pattern PATTERN_TYPE=Pattern.compile("^([a-z]+)[^a-z]*.*$"); + + public static final String URI_ANDROID = "http://schemas.android.com/apk/res/android"; + public static final String URI_APP = "http://schemas.android.com/apk/res-auto"; + public static final String PREFIX_ANDROID = "android"; + public static final String PREFIX_APP = "app"; +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/FilePathEncoder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/FilePathEncoder.java new file mode 100644 index 00000000..36bd1441 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/FilePathEncoder.java @@ -0,0 +1,119 @@ +/* + * 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.xmlencoder; + +import com.reandroid.archive.APKArchive; +import com.reandroid.archive.FileInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.apk.ApkUtil; +import com.reandroid.apk.UncompressedFiles; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.value.Entry; +import com.reandroid.xml.source.XMLFileSource; +import com.reandroid.xml.source.XMLSource; + +import java.io.File; +import java.util.List; + +public class FilePathEncoder { + private final EncodeMaterials materials; + private APKArchive apkArchive; + private UncompressedFiles uncompressedFiles; + public FilePathEncoder(EncodeMaterials encodeMaterials){ + this.materials =encodeMaterials; + } + + public void setApkArchive(APKArchive apkArchive) { + this.apkArchive = apkArchive; + } + public void setUncompressedFiles(UncompressedFiles uncompressedFiles){ + this.uncompressedFiles=uncompressedFiles; + } + public void encodeResDir(File resDir){ + materials.logVerbose("Scanning file list: " + +resDir.getParentFile().getName() + +File.separator+resDir.getName()); + List dirList = ApkUtil.listDirectories(resDir); + for(File dir:dirList){ + if(dir.getName().startsWith("values")){ + continue; + } + encodeTypeDir(dir); + } + } + public void encodeTypeDir(File dir){ + List fileList = ApkUtil.listFiles(dir, null); + for(File file:fileList){ + encodeFileEntry(file); + } + } + public InputSource encodeFileEntry(File resFile){ + String type = EncodeUtil.getTypeNameFromResFile(resFile); + PackageBlock packageBlock = materials.getCurrentPackage(); + int typeId=packageBlock + .getTypeStringPool().idOf(type); + String qualifiers = EncodeUtil.getQualifiersFromResFile(resFile); + TypeBlock typeBlock = packageBlock.getOrCreateTypeBlock((byte)typeId, qualifiers); + String name = EncodeUtil.getEntryNameFromResFile(resFile); + int resourceId=materials.resolveLocalResourceId(type, name); + + Entry entry = typeBlock + .getOrCreateEntry((short) (0xffff & resourceId)); + + String path = EncodeUtil.getEntryPathFromResFile(resFile); + entry.setValueAsString(path); + materials.setEntryName(entry, name); + InputSource inputSource=createInputSource(path, resFile); + if(inputSource instanceof XMLEncodeSource){ + ((XMLEncodeSource)inputSource).setEntry(entry); + } + addInputSource(inputSource); + return inputSource; + } + private InputSource createInputSource(String path, File resFile){ + if(isXmlFile(resFile)){ + return createXMLEncodeInputSource(path, resFile); + } + addUncompressedFiles(path); + return createRawFileInputSource(path, resFile); + } + 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); + } + private boolean isXmlFile(File resFile){ + String name=resFile.getName(); + if(!name.endsWith(".xml")){ + return false; + } + String type=EncodeUtil.getTypeNameFromResFile(resFile); + return !type.equals("raw"); + } + private void addInputSource(InputSource inputSource){ + if(inputSource!=null && this.apkArchive!=null){ + apkArchive.add(inputSource); + } + } + private void addUncompressedFiles(String path){ + if(uncompressedFiles!=null){ + uncompressedFiles.addPath(path); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/RESEncoder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/RESEncoder.java new file mode 100644 index 00000000..0b3417e9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/RESEncoder.java @@ -0,0 +1,337 @@ +/* + * 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.xmlencoder; + +import com.reandroid.apk.*; +import com.reandroid.archive.APKArchive; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.identifiers.PackageIdentifier; +import com.reandroid.identifiers.ResourceIdentifier; +import com.reandroid.identifiers.TableIdentifier; +import com.reandroid.xml.XMLException; +import com.reandroid.xml.XMLParserFactory; +import com.reandroid.xml.source.XMLFileSource; +import com.reandroid.xml.source.XMLSource; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.zip.ZipEntry; + +public class RESEncoder { + private APKLogger apkLogger; + private final TableBlock tableBlock; + private final Set parsedFiles = new HashSet<>(); + private final ApkModule apkModule; + public RESEncoder(){ + this(new ApkModule("encoded", + new APKArchive()), new TableBlock()); + } + public RESEncoder(ApkModule module, TableBlock block){ + this.apkModule = module; + this.tableBlock = block; + if(!module.hasTableBlock()){ + module.setLoadDefaultFramework(false); + BlockInputSource inputSource = + new BlockInputSource<>(TableBlock.FILE_NAME, block); + inputSource.setMethod(ZipEntry.STORED); + this.apkModule.setTableBlock(tableBlock); + } + } + public TableBlock getTableBlock(){ + return tableBlock; + } + public ApkModule getApkModule(){ + return apkModule; + } + public void scanDirectory(File mainDir) throws IOException, XMLException { + scanResourceFiles(mainDir); + } + private void scanResourceFiles(File mainDir) throws IOException, XMLException { + List pubXmlFileList = searchPublicXmlFiles(mainDir); + if(pubXmlFileList.size()==0){ + throw new IOException("No .*/values/" + + ApkUtil.FILE_NAME_PUBLIC_XML+" file found in '"+mainDir); + } + preloadStringPool(pubXmlFileList); + EncodeMaterials encodeMaterials = new EncodeMaterials(); + encodeMaterials.setAPKLogger(apkLogger); + + TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier(); + tableIdentifier.loadPublicXml(pubXmlFileList); + tableIdentifier.initialize(this.tableBlock); + + excludeIds(pubXmlFileList); + File manifestFile = initializeFrameworkFromManifest(encodeMaterials, pubXmlFileList); + + encodeAttrs(encodeMaterials, pubXmlFileList); + + encodeValues(encodeMaterials, pubXmlFileList); + + tableBlock.refresh(); + + PackageBlock packageBlock = encodeMaterials.pickMainPackageBlock(this.tableBlock); + if(manifestFile != null){ + if(packageBlock != null){ + encodeMaterials.setCurrentPackage(packageBlock); + } + XMLSource xmlSource = + new XMLFileSource(AndroidManifestBlock.FILE_NAME, manifestFile); + XMLEncodeSource xmlEncodeSource = + new XMLEncodeSource(encodeMaterials, xmlSource); + getApkModule().getApkArchive().add(xmlEncodeSource); + } + } + private File initializeFrameworkFromManifest(EncodeMaterials encodeMaterials, List pubXmlFileList) throws IOException { + for(File pubXmlFile:pubXmlFileList){ + addParsedFiles(pubXmlFile); + File manifestFile = toAndroidManifest(pubXmlFile); + if(!manifestFile.isFile()){ + continue; + } + initializeFrameworkFromManifest(encodeMaterials, manifestFile); + return manifestFile; + } + return null; + } + private void encodeValues(EncodeMaterials encodeMaterials, List pubXmlFileList) throws XMLException, IOException { + logMessage("Encoding values ..."); + TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier(); + + for(File pubXmlFile:pubXmlFileList){ + addParsedFiles(pubXmlFile); + PackageIdentifier packageIdentifier = tableIdentifier.getByTag(pubXmlFile); + + PackageBlock packageBlock = packageIdentifier.getPackageBlock(); + + encodeMaterials.setCurrentPackage(packageBlock); + + File resDir=toResDirectory(pubXmlFile); + encodeResDir(encodeMaterials, resDir); + FilePathEncoder filePathEncoder = new FilePathEncoder(encodeMaterials); + filePathEncoder.setApkArchive(getApkModule().getApkArchive()); + filePathEncoder.setUncompressedFiles(getApkModule().getUncompressedFiles()); + filePathEncoder.encodeResDir(resDir); + + packageBlock.sortTypes(); + packageBlock.refresh(); + } + } + private void encodeAttrs(EncodeMaterials encodeMaterials, List pubXmlFileList) throws XMLException { + logMessage("Encoding attrs ..."); + TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier(); + + for(File pubXmlFile:pubXmlFileList){ + addParsedFiles(pubXmlFile); + PackageIdentifier packageIdentifier = tableIdentifier.getByTag(pubXmlFile); + + PackageBlock packageBlock = packageIdentifier.getPackageBlock(); + encodeMaterials.setCurrentPackage(packageBlock); + + ValuesEncoder valuesEncoder = new ValuesEncoder(encodeMaterials); + File fileAttrs = toAttr(pubXmlFile); + if(fileAttrs.isFile()){ + valuesEncoder.encodeValuesXml(fileAttrs); + packageBlock.sortTypes(); + packageBlock.refresh(); + addParsedFiles(fileAttrs); + } + } + } + private void excludeIds(List pubXmlFileList){ + for(File pubXmlFile:pubXmlFileList){ + addParsedFiles(pubXmlFile); + File fileIds = toId(pubXmlFile); + if(fileIds.isFile()){ + addParsedFiles(fileIds); + } + } + } + private void initializeFrameworkFromManifest(EncodeMaterials encodeMaterials, File manifestFile) throws IOException { + XmlPullParser parser; + try { + parser = XMLParserFactory.newPullParser(manifestFile); + } catch (XmlPullParserException ex) { + throw new IOException(ex); + } + FrameworkApk frameworkApk = getApkModule().initializeAndroidFramework(parser); + encodeMaterials.addFramework(frameworkApk); + initializeMainPackageId(encodeMaterials, parser); + XmlHelper.closeSilent(parser); + } + private void initializeMainPackageId(EncodeMaterials encodeMaterials, XmlPullParser parser) throws IOException { + Map applicationAttributes; + try { + applicationAttributes = XmlHelper.readAttributes(parser, AndroidManifestBlock.TAG_application); + } catch (XmlPullParserException ex) { + throw new IOException(ex); + } + if(applicationAttributes == null){ + return; + } + String iconReference = applicationAttributes.get(AndroidManifestBlock.NAME_icon); + if(iconReference == null){ + return; + } + logMessage("Set main package id from manifest: " + iconReference); + ValueDecoder.ReferenceString ref = ValueDecoder.parseReference(iconReference); + if(ref == null){ + logMessage("Something wrong on : " + AndroidManifestBlock.NAME_icon); + return; + } + TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier(); + ResourceIdentifier resourceIdentifier; + if(ref.packageName != null){ + resourceIdentifier = tableIdentifier.get(ref.packageName, ref.type, ref.name); + }else { + resourceIdentifier = tableIdentifier.get(ref.type, ref.name); + } + if(resourceIdentifier == null){ + logMessage("WARN: failed to resolve: " + ref); + return; + } + int packageId = resourceIdentifier.getPackageId(); + encodeMaterials.setMainPackageId(packageId); + logMessage("Main package id initialized: id = " + + HexUtil.toHex2((byte)packageId) + ", from: " + ref ); + } + private void preloadStringPool(List pubXmlFileList){ + logMessage("Loading string pool ..."); + ValuesStringPoolBuilder poolBuilder=new ValuesStringPoolBuilder(); + for(File pubXml:pubXmlFileList){ + File resDir=toResDirectory(pubXml); + List valuesDirList = listValuesDir(resDir); + for(File dir:valuesDirList){ + logVerbose(poolBuilder.size()+" building pool: "+dir.getName()); + poolBuilder.scanValuesDirectory(dir); + } + } + poolBuilder.addTo(tableBlock.getTableStringPool()); + } + + private void encodeResDir(EncodeMaterials materials, File resDir) throws XMLException { + + List valuesDirList = listValuesDir(resDir); + for(File valuesDir:valuesDirList){ + encodeValuesDir(materials, valuesDir); + } + } + private void encodeValuesDir(EncodeMaterials materials, File valuesDir) throws XMLException { + ValuesEncoder valuesEncoder = new ValuesEncoder(materials); + List xmlFiles = ApkUtil.listFiles(valuesDir, ".xml"); + EncodeUtil.sortValuesXml(xmlFiles); + for(File file:xmlFiles){ + if(isAlreadyParsed(file)){ + continue; + } + addParsedFiles(file); + valuesEncoder.encodeValuesXml(file); + } + } + private File toAndroidManifest(File pubXmlFile){ + File resDirectory = toResDirectory(pubXmlFile); + File packageDirectory = resDirectory.getParentFile(); + File root = packageDirectory.getParentFile(); + return new File(root, AndroidManifestBlock.FILE_NAME); + } + private File toPackageDirectory(File pubXmlFile){ + return toResDirectory(pubXmlFile) + .getParentFile(); + } + private File toResDirectory(File pubXmlFile){ + return pubXmlFile + .getParentFile() + .getParentFile(); + } + private File toId(File pubXmlFile){ + return new File(pubXmlFile.getParentFile(), "ids.xml"); + } + private File toAttr(File pubXmlFile){ + return new File(pubXmlFile.getParentFile(), "attrs.xml"); + } + private List listValuesDir(File resDir){ + List results=new ArrayList<>(); + File def=new File(resDir, "values"); + results.add(def); + File[] dirList=resDir.listFiles(); + if(dirList!=null){ + for(File dir:dirList){ + if(def.equals(dir) || !dir.isDirectory()){ + continue; + } + if(dir.getName().startsWith("values-")){ + results.add(dir); + } + } + } + return results; + } + private List searchPublicXmlFiles(File mainDir){ + logVerbose("Searching public.xml: "+mainDir); + List dirList=ApkUtil.listDirectories(mainDir); + List xmlFiles = new ArrayList<>(); + for(File dir:dirList){ + if(dir.getName().equals("root")){ + continue; + } + xmlFiles.addAll( + ApkUtil.recursiveFiles(dir, ApkUtil.FILE_NAME_PUBLIC_XML)); + } + List results = new ArrayList<>(); + for(File file:xmlFiles){ + if(!EncodeUtil.isPublicXml(file)){ + continue; + } + if(toAndroidManifest(file).isFile()){ + results.add(file); + } + } + EncodeUtil.sortPublicXml(results); + return results; + } + + private boolean isAlreadyParsed(File file){ + return parsedFiles.contains(file); + } + private void addParsedFiles(File file){ + parsedFiles.add(file); + } + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + this.apkModule.setAPKLogger(logger); + } + private void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesEncoder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesEncoder.java new file mode 100644 index 00000000..95e40929 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesEncoder.java @@ -0,0 +1,238 @@ +/* + * 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.xmlencoder; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.value.Entry; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; +import com.reandroid.xml.XMLException; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class ValuesEncoder { + private final EncodeMaterials materials; + private final Map xmlEncodersMap; + private final Map xmlBagEncodersMap; + private final XMLValuesEncoderCommon commonEncoder; + private final XMLValuesEncoderBag bagCommonEncoder; + public ValuesEncoder(EncodeMaterials materials){ + this.materials=materials; + Map map = new HashMap<>(); + map.put("id", new XMLValuesEncoderId(materials)); + map.put("string", new XMLValuesEncoderString(materials)); + XMLValuesEncoderDimen encoderDimen=new XMLValuesEncoderDimen(materials); + map.put("dimen", encoderDimen); + map.put("fraction", encoderDimen); + map.put("color", new XMLValuesEncoderColor(materials)); + map.put("integer", new XMLValuesEncoderInteger(materials)); + + this.xmlEncodersMap=map; + this.commonEncoder=new XMLValuesEncoderCommon(materials); + + Map mapBag=new HashMap<>(); + XMLValuesEncoderAttr encoderAttr = new XMLValuesEncoderAttr(materials); + mapBag.put("attr", encoderAttr); + mapBag.put("^attr-private", encoderAttr); + mapBag.put("plurals", new XMLValuesEncoderPlurals(materials)); + mapBag.put("array", new XMLValuesEncoderArray(materials)); + mapBag.put("style", new XMLValuesEncoderStyle(materials)); + this.xmlBagEncodersMap=mapBag; + this.bagCommonEncoder=new XMLValuesEncoderBag(materials); + + } + public void encodeValuesXml(File valuesXmlFile) throws XMLException { + if(valuesXmlFile.getName().equals("public.xml")){ + return; + } + String simpleName = valuesXmlFile.getParentFile().getName() + +File.separator+valuesXmlFile.getName(); + materials.logVerbose("Encoding: "+simpleName); + + String type = EncodeUtil.getTypeNameFromValuesXml(valuesXmlFile); + String qualifiers = EncodeUtil.getQualifiersFromValuesXml(valuesXmlFile); + XMLDocument xmlDocument = XMLDocument.load(valuesXmlFile); + encodeValuesXml(type, qualifiers, xmlDocument); + } + public void encodeValue(String qualifiers, XMLElement element){ + String type = getType(element, null); + if(type == null){ + throw new EncodeException("Can not determine type: " + element); + } + encodeValue(type, qualifiers, element); + } + public void encodeValue(String type, String qualifiers, XMLElement element){ + boolean is_bag = isBag(element); + encodeValue(is_bag, type, qualifiers, element); + } + public void encodeValue(boolean is_bag, String type, String qualifiers, XMLElement element){ + PackageBlock packageBlock = getEncodeMaterials().getCurrentPackage(); + Entry entry = packageBlock + .getOrCreate(qualifiers, type, element.getAttributeValue("name")); + encodeValue(is_bag, entry, element); + } + public void encodeValue(Entry entry, XMLElement element){ + boolean is_bag = isBag(element); + encodeValue(is_bag, entry, element); + } + public void encodeValue(boolean is_bag, Entry entry, XMLElement element){ + XMLValuesEncoder encoder; + String type = entry.getTypeName(); + if(is_bag){ + encoder = getBagEncoder(type); + }else{ + encoder = getEncoder(type); + } + encoder.encodeValue(entry, element); + } + public void encodeValues(String type, String qualifiers, XMLDocument xmlDocument){ + type = getType(xmlDocument, type); + boolean is_bag = isBag(xmlDocument, type); + encodeValues(is_bag, type, qualifiers, xmlDocument); + } + public void encodeValues(boolean is_bag, String type, String qualifiers, XMLDocument xmlDocument){ + XMLValuesEncoder encoder; + if(is_bag){ + encoder = getBagEncoder(type); + }else{ + encoder = getEncoder(type); + } + encoder.encode(type, qualifiers, xmlDocument); + } + public EncodeMaterials getEncodeMaterials(){ + return materials; + } + private void encodeValuesXml(String type, String qualifiers, XMLDocument xmlDocument) { + type=getType(xmlDocument, type); + XMLValuesEncoder encoder; + if(isBag(xmlDocument, type)){ + encoder = getBagEncoder(type); + }else{ + encoder=getEncoder(type); + } + encoder.encode(type, qualifiers, xmlDocument); + } + private boolean isBag(XMLElement element){ + if(element.hasChildElements()){ + return true; + } + return element.getAttributeCount() > 1; + } + private boolean isBag(XMLDocument xmlDocument, String type){ + if(type.startsWith("attr")){ + return true; + } + if(type.startsWith("^attr")){ + return true; + } + if(type.startsWith("style")){ + return true; + } + if(type.startsWith("plurals")){ + return true; + } + if(type.startsWith("array")){ + return true; + } + if(type.startsWith("string")){ + return false; + } + XMLElement documentElement=xmlDocument.getDocumentElement(); + int count=documentElement.getChildesCount(); + for(int i=0;i0){ + return true; + } + } + return false; + } + private boolean hasNameAttributes(XMLDocument xmlDocument){ + XMLElement documentElement=xmlDocument.getDocumentElement(); + int count=documentElement.getChildesCount(); + for(int i=0;i0){ + XMLElement child = element.getChildAt(0); + if(child.getAttributeValue("name") != null){ + return true; + } + } + } + return false; + } + private String getType(XMLDocument xmlDocument, String def){ + XMLElement documentElement=xmlDocument.getDocumentElement(); + if(documentElement.getChildesCount()==0){ + return def; + } + XMLElement first=documentElement.getChildAt(0); + String type=first.getAttributeValue("type"); + if(type==null){ + type=first.getTagName(); + } + if(type==null){ + return def; + } + if(type.endsWith("-array")){ + return "array"; + } + if(type.startsWith("attr-private")){ + return "^attr-private"; + } + if(type.equals("item")){ + return def; + } + return type; + } + private String getType(XMLElement first, String def){ + String type = first.getAttributeValue("type"); + if(type == null){ + type = first.getTagName(); + } + if(type == null){ + return def; + } + if(type.endsWith("-array")){ + return "array"; + } + if(type.startsWith("attr-private")){ + return "^attr-private"; + } + if(type.equals("item")){ + return def; + } + return type; + } + private XMLValuesEncoder getEncoder(String type){ + type=EncodeUtil.sanitizeType(type); + XMLValuesEncoder encoder=xmlEncodersMap.get(type); + if(encoder!=null){ + return encoder; + } + return commonEncoder; + } + private XMLValuesEncoderBag getBagEncoder(String type){ + type=EncodeUtil.sanitizeType(type); + XMLValuesEncoderBag encoder=xmlBagEncodersMap.get(type); + if(encoder!=null){ + return encoder; + } + return bagCommonEncoder; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesStringPoolBuilder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesStringPoolBuilder.java new file mode 100644 index 00000000..39ecbc66 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/ValuesStringPoolBuilder.java @@ -0,0 +1,176 @@ +/* + * 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.xmlencoder; + +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.array.StyleArray; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.item.StyleItem; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; +import com.reandroid.xml.XMLSpanInfo; +import com.reandroid.xml.XMLSpannable; + +import java.io.File; +import java.util.*; + +public class ValuesStringPoolBuilder { + private final Set stringList; + private final Set styleList; + public ValuesStringPoolBuilder(){ + this.stringList=new HashSet<>(); + this.styleList=new HashSet<>(); + } + public void addTo(TableStringPool stringPool){ + if(stringPool.getStringsArray().childesCount()==0){ + buildWithStyles(stringPool); + } + stringPool.addStrings(stringList); + stringList.clear(); + styleList.clear(); + stringPool.refresh(); + } + private void buildWithStyles(TableStringPool stringPool){ + List spannableList = buildSpannable(); + if(spannableList.size()==0){ + return; + } + StringArray stringsArray = stringPool.getStringsArray(); + StyleArray styleArray = stringPool.getStyleArray(); + + int stylesCount = spannableList.size(); + stringsArray.setChildesCount(stylesCount); + styleArray.setChildesCount(stylesCount); + + List tagList = + new ArrayList<>(XMLSpannable.tagList(spannableList)); + EncodeUtil.sortStrings(tagList); + Map tagsMap = + stringPool.insertStrings(tagList); + + List textList = XMLSpannable.toTextList(spannableList); + + for(int i=0;i buildSpannable(){ + List results=new ArrayList<>(); + Set removeList=new HashSet<>(); + for(String text:styleList){ + XMLSpannable spannable=XMLSpannable.parse(text); + if(spannable!=null){ + results.add(spannable); + removeList.add(text); + }else { + stringList.add(text); + } + } + stringList.removeAll(removeList); + XMLSpannable.sort(results); + return results; + } + public void scanValuesDirectory(File dir){ + addStringsFile(new File(dir, "strings.xml")); + addBagsFile(new File(dir, "plurals.xml")); + } + public int size(){ + return stringList.size(); + } + private void addStringsFile(File file) { + if(file==null||!file.isFile()){ + return; + } + try { + XMLDocument xmlDocument = XMLDocument.load(file); + addStrings(xmlDocument); + } catch (Exception ignored) { + } + } + private void addBagsFile(File file) { + if(file==null||!file.isFile()){ + return; + } + try { + XMLDocument xmlDocument = XMLDocument.load(file); + addBagStrings(xmlDocument); + } catch (Exception ignored) { + } + } + private void addBagStrings(XMLDocument xmlDocument){ + if(xmlDocument == null){ + return; + } + XMLElement documentElement = xmlDocument.getDocumentElement(); + if(documentElement==null){ + return; + } + int count = documentElement.getChildesCount(); + for(int i=0;i0 && text.charAt(0)!='@'){ + stringList.add(text); + } + } + private void addStyleElement(XMLElement element){ + styleList.add(element.buildTextContent(false)); + } + +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLEncodeSource.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLEncodeSource.java new file mode 100644 index 00000000..2c703b12 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLEncodeSource.java @@ -0,0 +1,113 @@ +/* + * 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.xmlencoder; + +import com.reandroid.archive.ByteInputSource; +import com.reandroid.apk.CrcOutputStream; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.value.Entry; +import com.reandroid.xml.XMLException; +import com.reandroid.xml.source.XMLSource; + +import java.io.IOException; +import java.io.OutputStream; + +public class XMLEncodeSource extends ByteInputSource { + private final EncodeMaterials encodeMaterials; + private final XMLSource xmlSource; + private ResXmlDocument resXmlDocument; + private Entry mEntry; + public XMLEncodeSource(EncodeMaterials encodeMaterials, XMLSource xmlSource, Entry entry){ + super(new byte[0], xmlSource.getPath()); + this.encodeMaterials = encodeMaterials; + this.xmlSource = xmlSource; + this.mEntry = entry; + } + public XMLEncodeSource(EncodeMaterials encodeMaterials, XMLSource xmlSource){ + this(encodeMaterials, xmlSource, null); + } + + public XMLSource getXmlSource() { + return xmlSource; + } + public Entry getEntry(){ + return mEntry; + } + public void setEntry(Entry entry) { + this.mEntry = entry; + } + + @Override + public long getLength() throws IOException{ + return getResXmlBlock().countBytes(); + } + @Override + public long getCrc() throws IOException{ + ResXmlDocument resXmlDocument = getResXmlBlock(); + CrcOutputStream outputStream=new CrcOutputStream(); + resXmlDocument.writeBytes(outputStream); + return outputStream.getCrcValue(); + } + @Override + public long write(OutputStream outputStream) throws IOException { + return getResXmlBlock().writeBytes(outputStream); + } + @Override + public byte[] getBytes() { + try { + return getResXmlBlock().getBytes(); + } catch (IOException ignored) { + } + //should not reach here + return new byte[0]; + } + public ResXmlDocument getResXmlBlock() throws IOException{ + if(resXmlDocument !=null){ + return resXmlDocument; + } + try { + XMLFileEncoder xmlFileEncoder=new XMLFileEncoder(encodeMaterials); + xmlFileEncoder.setCurrentPath(xmlSource.getPath()); + EncodeMaterials encodeMaterials = this.encodeMaterials; + encodeMaterials.logVerbose("Encoding xml: " + xmlSource.getPath()); + PackageBlock currentPackage = encodeMaterials.getCurrentPackage(); + PackageBlock packageBlock = getEntryPackageBlock(); + if(packageBlock != null && packageBlock != currentPackage){ + encodeMaterials.setCurrentPackage(packageBlock); + } + resXmlDocument = xmlFileEncoder.encode(xmlSource.getXMLDocument()); + } catch (XMLException ex) { + throw new EncodeException("XMLException on: '"+xmlSource.getPath() + +"'\n '"+ex.getMessage()+"'"); + } + return resXmlDocument; + } + private PackageBlock getEntryPackageBlock(){ + Entry entry = getEntry(); + if(entry != null){ + return entry.getPackageBlock(); + } + return null; + } + @Override + public void disposeInputSource(){ + this.xmlSource.disposeXml(); + if(this.resXmlDocument !=null){ + resXmlDocument =null; + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLFileEncoder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLFileEncoder.java new file mode 100644 index 00000000..442549f8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLFileEncoder.java @@ -0,0 +1,257 @@ +/* + * 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.xmlencoder; + +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.chunk.xml.*; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.AttributeDataFormat; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.arsc.value.attribute.AttributeBag; +import com.reandroid.xml.*; + +import java.io.File; +import java.io.InputStream; + +public class XMLFileEncoder { + private final EncodeMaterials materials; + private ResXmlDocument resXmlDocument; + private String mCurrentPath; + public XMLFileEncoder(EncodeMaterials materials){ + this.materials=materials; + } + + // Just for logging purpose + public void setCurrentPath(String path) { + this.mCurrentPath = path; + } + public ResXmlDocument encode(String xmlString){ + try { + return encode(XMLDocument.load(xmlString)); + } catch (XMLException ex) { + materials.logMessage(ex.getMessage()); + } + return null; + } + public ResXmlDocument encode(InputStream inputStream){ + try { + return encode(XMLDocument.load(inputStream)); + } catch (XMLException ex) { + materials.logMessage(ex.getMessage()); + } + return null; + } + public ResXmlDocument encode(File xmlFile){ + setCurrentPath(xmlFile.getAbsolutePath()); + try { + return encode(XMLDocument.load(xmlFile)); + } catch (XMLException ex) { + materials.logMessage(ex.getMessage()); + } + return null; + } + public ResXmlDocument encode(XMLDocument xmlDocument){ + resXmlDocument = new ResXmlDocument(); + resXmlDocument.setPackageBlock(materials.getCurrentPackage()); + buildIdMap(xmlDocument); + buildElement(xmlDocument); + resXmlDocument.refresh(); + return resXmlDocument; + } + public ResXmlDocument getResXmlBlock(){ + return resXmlDocument; + } + private void buildElement(XMLDocument xmlDocument){ + XMLElement element = xmlDocument.getDocumentElement(); + ResXmlElement resXmlElement = resXmlDocument.createRootElement(element.getTagName()); + buildElement(element, resXmlElement); + } + private void buildElement(XMLElement element, ResXmlElement resXmlElement){ + ensureNamespaces(element, resXmlElement); + resXmlElement.setTag(element.getTagName()); + buildAttributes(element, resXmlElement); + for(XMLNode node:element.getChildNodes()){ + if(node instanceof XMLText){ + resXmlElement.addResXmlText(((XMLText)node).getText(true)); + }else if(node instanceof XMLComment){ + resXmlElement.setComment(((XMLComment)node).getCommentText()); + }else if(node instanceof XMLElement){ + XMLElement child=(XMLElement) node; + ResXmlElement childXml=resXmlElement.createChildElement(); + buildElement(child, childXml); + } + } + } + private void buildAttributes(XMLElement element, ResXmlElement resXmlElement){ + for(XMLAttribute attribute:element.listAttributes()){ + if(attribute instanceof SchemaAttr){ + continue; + } + if(SchemaAttr.looksSchema(attribute.getName(), attribute.getValue())){ + continue; + } + String name=attribute.getNameWoPrefix(); + int resourceId=decodeUnknownAttributeHex(name); + Entry entry =null; + if(resourceId==0){ + entry =getAttributeBlock(attribute); + if(entry !=null){ + resourceId= entry.getResourceId(); + }else if(attribute.getNamePrefix()!=null){ + throw new EncodeException("No resource found for attribute: " + + attribute.getName() + ", at file "+mCurrentPath); + } + } + ResXmlAttribute xmlAttribute = + resXmlElement.createAttribute(name, resourceId); + String prefix=attribute.getNamePrefix(); + if(prefix!=null){ + ResXmlStartNamespace ns = resXmlElement.getStartNamespaceByPrefix(prefix); + if(ns==null){ + ns=forceCreateNamespace(resXmlElement, resourceId, prefix); + } + if(ns==null){ + throw new EncodeException("Namespace not found: " + +attribute.toString() + +", path="+mCurrentPath); + } + xmlAttribute.setNamespaceReference(ns.getUriReference()); + } + + String valueText=attribute.getValue(); + + if(ValueDecoder.isReference(valueText)){ + if(valueText.startsWith("?")){ + xmlAttribute.setValueType(ValueType.ATTRIBUTE); + }else { + xmlAttribute.setValueType(ValueType.REFERENCE); + } + xmlAttribute.setData(materials.resolveReference(valueText)); + continue; + } + if(entry !=null){ + AttributeBag attributeBag=AttributeBag + .create((ResValueMapArray) entry.getTableEntry().getValue()); + + ValueDecoder.EncodeResult encodeResult = + attributeBag.encodeEnumOrFlagValue(valueText); + if(encodeResult!=null){ + xmlAttribute.setValueType(encodeResult.valueType); + xmlAttribute.setData(encodeResult.value); + continue; + } + if(attributeBag.isEqualType(AttributeDataFormat.STRING)) { + xmlAttribute.setValueAsString(ValueDecoder + .unEscapeSpecialCharacter(valueText)); + continue; + } + } + + if(EncodeUtil.isEmpty(valueText)) { + if(valueText == null){ + valueText = ""; + } + xmlAttribute.setValueAsString(valueText); + }else{ + ValueDecoder.EncodeResult encodeResult = + ValueDecoder.encodeGuessAny(valueText); + if(encodeResult!=null){ + xmlAttribute.setValueType(encodeResult.valueType); + xmlAttribute.setData(encodeResult.value); + }else { + xmlAttribute.setValueAsString(ValueDecoder + .unEscapeSpecialCharacter(valueText)); + } + } + } + resXmlElement.calculatePositions(); + } + private void ensureNamespaces(XMLElement element, ResXmlElement resXmlElement){ + for(XMLAttribute attribute:element.listAttributes()){ + String prefix = SchemaAttr.getPrefix(attribute.getName()); + if(prefix==null){ + continue; + } + String uri=attribute.getValue(); + resXmlElement.getOrCreateNamespace(uri, prefix); + } + } + private void buildIdMap(XMLDocument xmlDocument){ + ResIdBuilder idBuilder=new ResIdBuilder(); + XMLElement element= xmlDocument.getDocumentElement(); + searchResIds(idBuilder, element); + idBuilder.buildTo(resXmlDocument.getResXmlIDMap()); + } + private void searchResIds(ResIdBuilder idBuilder, XMLElement element){ + for(XMLAttribute attribute : element.listAttributes()){ + addResourceId(idBuilder, attribute); + } + int count=element.getChildesCount(); + for(int i=0;i>24) & 0xff; + String uri; + if(pkgId == 1){ + uri = EncodeUtil.URI_ANDROID; + }else { + uri=EncodeUtil.URI_APP; + } + ResXmlElement root = resXmlElement.getRootResXmlElement(); + ResXmlStartNamespace ns = root.getOrCreateNamespace(uri, prefix); + materials.logVerbose("Force created ns: "+prefix+":"+uri); + return ns; + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoder.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoder.java new file mode 100644 index 00000000..c3c07fad --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoder.java @@ -0,0 +1,124 @@ +/* + * 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.xmlencoder; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; + +class XMLValuesEncoder { + private final EncodeMaterials materials; + XMLValuesEncoder(EncodeMaterials materials){ + this.materials=materials; + } + public void encode(String type, String qualifiers, XMLDocument xmlDocument){ + XMLElement documentElement = xmlDocument.getDocumentElement(); + TypeBlock typeBlock = getTypeBlock(type, qualifiers); + + int count = documentElement.getChildesCount(); + + typeBlock.getEntryArray().ensureSize(count); + + for(int i=0;i 1){ + return false; + } + String text = element.getTextContent(); + if(!ValueDecoder.isReference(text)){ + return false; + } + encodeReferenceValue(entry, text); + return true; + } + void encodeChildes(XMLElement element, ResTableMapEntry mapEntry){ + throw new EncodeException("Unimplemented bag type encoder: " + +element.getTagName()); + + } + int getChildesCount(XMLElement element){ + return element.getChildesCount(); + } + + @Override + void encodeNullValue(Entry entry){ + // Nothing to do + } + + Integer decodeUnknownAttributeHex(String name){ + if(name.length() == 0 || (name.charAt(0) !='@' && name.charAt(0) != '?')){ + return null; + } + name = name.substring(1); + if(!ValueDecoder.isHex(name)){ + return null; + } + return ValueDecoder.parseHex(name); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderColor.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderColor.java new file mode 100644 index 00000000..88cb856a --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderColor.java @@ -0,0 +1,36 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; + +class XMLValuesEncoderColor extends XMLValuesEncoder{ + XMLValuesEncoderColor(EncodeMaterials materials) { + super(materials); + } + @Override + void encodeStringValue(Entry entry, String value){ + ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeColor(value); + if(encodeResult!=null){ + entry.setValueAsRaw(encodeResult.valueType, encodeResult.value); + }else { + // If reaches here the value might be + // file path e.g. res/color/something.xml + entry.setValueAsString(value); + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderCommon.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderCommon.java new file mode 100644 index 00000000..dc774f39 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderCommon.java @@ -0,0 +1,39 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; + + class XMLValuesEncoderCommon extends XMLValuesEncoder{ + XMLValuesEncoderCommon(EncodeMaterials materials) { + super(materials); + } + @Override + void encodeStringValue(Entry entry, String value){ + if(ValueDecoder.isReference(value)){ + entry.setValueAsReference(getMaterials().resolveReference(value)); + }else { + ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeGuessAny(value); + if(encodeResult!=null){ + entry.setValueAsRaw(encodeResult.valueType, encodeResult.value); + }else { + entry.setValueAsString(ValueDecoder + .unEscapeSpecialCharacter(value)); + } + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderDimen.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderDimen.java new file mode 100644 index 00000000..38cd6dcf --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderDimen.java @@ -0,0 +1,40 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; + + class XMLValuesEncoderDimen extends XMLValuesEncoder{ + XMLValuesEncoderDimen(EncodeMaterials materials) { + super(materials); + } + @Override + void encodeStringValue(Entry entry, String value){ + ValueDecoder.EncodeResult encodeResult = + ValueDecoder.encodeDimensionOrFloat(value); + if(encodeResult==null){ + encodeResult=ValueDecoder.encodeHexOrInt(value); + } + if(encodeResult!=null){ + entry.setValueAsRaw(encodeResult.valueType, encodeResult.value); + }else { + getMaterials().logMessage("Encoding as string dimen value: "+value); + entry.setValueAsString(value); + } + + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderId.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderId.java new file mode 100644 index 00000000..805d4e34 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderId.java @@ -0,0 +1,47 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.ValueHeader; +import com.reandroid.arsc.value.Entry; + + class XMLValuesEncoderId extends XMLValuesEncoder{ + public XMLValuesEncoderId(EncodeMaterials materials) { + super(materials); + } + + @Override + void encodeStringValue(Entry entry, String value){ + entry.setValueAsBoolean(false); + setVisibility(entry); + } + @Override + void encodeNullValue(Entry entry){ + entry.setValueAsBoolean(false); + setVisibility(entry); + } + @Override + void encodeBooleanValue(Entry entry, String value){ + super.encodeBooleanValue(entry, value); + setVisibility(entry); + } + private void setVisibility(Entry entry){ + ValueHeader valueHeader = entry.getHeader(); + valueHeader.setWeak(true); + valueHeader.setPublic(true); + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderInteger.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderInteger.java new file mode 100644 index 00000000..d10080b8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderInteger.java @@ -0,0 +1,42 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueType; + +class XMLValuesEncoderInteger extends XMLValuesEncoder{ + XMLValuesEncoderInteger(EncodeMaterials materials) { + super(materials); + } + @Override + void encodeStringValue(Entry entry, String value){ + value=value.trim(); + if(ValueDecoder.isInteger(value)){ + entry.setValueAsRaw(ValueType.INT_DEC, ValueDecoder.parseInteger(value)); + }else if(ValueDecoder.isHex(value)){ + entry.setValueAsRaw(ValueType.INT_HEX, ValueDecoder.parseHex(value)); + }else { + ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeDimensionOrFloat(value); + if(encodeResult!=null){ + entry.setValueAsRaw(encodeResult.valueType, encodeResult.value); + }else { + throw new EncodeException("Unknown value for type : '"+value+"'"); + } + } + } +} diff --git a/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderPlurals.java b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderPlurals.java new file mode 100644 index 00000000..bbc9e8d8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/apk/xmlencoder/XMLValuesEncoderPlurals.java @@ -0,0 +1,59 @@ + /* + * 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.xmlencoder; + +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.AttributeType; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.ResValueMap; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.arsc.value.plurals.PluralsQuantity; +import com.reandroid.xml.XMLElement; + +class XMLValuesEncoderPlurals extends XMLValuesEncoderBag{ + XMLValuesEncoderPlurals(EncodeMaterials materials) { + super(materials); + } + @Override + void encodeChildes(XMLElement parentElement, ResTableMapEntry resValueBag){ + int count = parentElement.getChildesCount(); + ResValueMapArray itemArray = resValueBag.getValue(); + for(int i=0;i entriesMap){ + super(entriesMap); + } + public APKArchive(){ + super(); + } + + public void refresh(){ + List inputSourceList = listInputSources(); + applySort(inputSourceList); + set(inputSourceList); + } + public void autoSortApkFiles(){ + List inputSourceList = listInputSources(); + autoSortApkFiles(inputSourceList); + set(inputSourceList); + } + public long writeApk(File outApk) throws IOException{ + ZipSerializer serializer=new ZipSerializer(listInputSources()); + return serializer.writeZip(outApk); + } + public long writeApk(OutputStream outputStream) throws IOException{ + ZipSerializer serializer=new ZipSerializer(listInputSources()); + return serializer.writeZip(outputStream); + } + public static APKArchive loadZippedApk(File zipFile) throws IOException { + return loadZippedApk(new ZipFile(zipFile)); + } + public static APKArchive loadZippedApk(ZipFile zipFile) { + Map entriesMap = InputSourceUtil.mapZipFileSources(zipFile); + return new APKArchive(entriesMap); + } + public static void repackApk(File apkFile) throws IOException{ + APKArchive apkArchive =loadZippedApk(apkFile); + apkArchive.writeApk(apkFile); + } + public static void applySort(List sourceList){ + Comparator cmp=new Comparator() { + @Override + public int compare(InputSource in1, InputSource in2) { + return Integer.compare(in1.getSort(), in2.getSort()); + } + }; + sourceList.sort(cmp); + } + public static void autoSortApkFiles(List sourceList){ + Comparator cmp=new Comparator() { + @Override + public int compare(InputSource in1, InputSource in2) { + return getSortName(in1).compareTo(getSortName(in2)); + } + }; + sourceList.sort(cmp); + int i=0; + for(InputSource inputSource:sourceList){ + inputSource.setSort(i); + i++; + } + } + private static String getSortName(InputSource inputSource){ + String name=inputSource.getAlias(); + StringBuilder builder=new StringBuilder(); + if(name.equals("AndroidManifest.xml")){ + builder.append("0 "); + }else if(name.startsWith("META-INF/")){ + builder.append("1 "); + }else if(name.equals("resources.arsc")){ + builder.append("2 "); + }else if(name.startsWith("classes")){ + builder.append("3 "); + }else if(name.startsWith("res/")){ + builder.append("4 "); + }else if(name.startsWith("lib/")){ + builder.append("5 "); + }else if(name.startsWith("assets/")){ + builder.append("6 "); + }else { + builder.append("7 "); + } + builder.append(name.toLowerCase()); + return builder.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive/ByteInputSource.java b/src/ARSCLib/com/reandroid/archive/ByteInputSource.java new file mode 100644 index 00000000..6acfe730 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/ByteInputSource.java @@ -0,0 +1,46 @@ + /* + * 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.archive; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ByteInputSource extends InputSource { + private byte[] inBytes; + public ByteInputSource(byte[] inBytes, String name) { + super(name); + this.inBytes=inBytes; + } + @Override + public long write(OutputStream outputStream) throws IOException { + byte[] bts=getBytes(); + outputStream.write(bts); + return bts.length; + } + @Override + public InputStream openStream() throws IOException { + return new ByteArrayInputStream(getBytes()); + } + public byte[] getBytes() { + return inBytes; + } + @Override + public void disposeInputSource(){ + inBytes=new byte[0]; + } +} diff --git a/src/ARSCLib/com/reandroid/archive/FileInputSource.java b/src/ARSCLib/com/reandroid/archive/FileInputSource.java new file mode 100644 index 00000000..bfb67796 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/FileInputSource.java @@ -0,0 +1,48 @@ +/* + * 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.archive; + +import com.reandroid.common.FileChannelInputStream; + +import java.io.*; + +public class FileInputSource extends InputSource { + private final File file; + public FileInputSource(File file, String name){ + super(name); + this.file=file; + } + @Override + public byte[] getBytes(int length) throws IOException{ + return FileChannelInputStream.read(getFile(), length); + } + @Override + public long getLength() { + return getFile().length(); + } + @Override + public void close(InputStream inputStream) throws IOException { + inputStream.close(); + } + @Override + public FileChannelInputStream openStream() throws IOException { + return new FileChannelInputStream(this.file); + } + public File getFile(){ + return file; + } + +} diff --git a/src/ARSCLib/com/reandroid/archive/InputSource.java b/src/ARSCLib/com/reandroid/archive/InputSource.java new file mode 100644 index 00000000..e29d37c3 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/InputSource.java @@ -0,0 +1,132 @@ + /* + * 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.archive; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +public abstract class InputSource { + private final String name; + private String alias; + private long mCrc; + private long mLength; + private int method = ZipEntry.DEFLATED; + private int sort; + public InputSource(String name){ + this.name = name; + this.alias = InputSourceUtil.sanitize(name); + } + public byte[] getBytes(int length) throws IOException{ + InputStream inputStream = openStream(); + byte[] bytes = new byte[length]; + inputStream.read(bytes, 0, length); + close(inputStream); + return bytes; + } + public void disposeInputSource(){ + } + public int getSort() { + return sort; + } + public void setSort(int sort) { + this.sort = sort; + } + public int getMethod() { + return method; + } + public void setMethod(int method) { + this.method = method; + } + + public String getAlias(){ + if(alias!=null){ + return alias; + } + return getName(); + } + public void setAlias(String alias) { + this.alias = alias; + } + public void close(InputStream inputStream) throws IOException { + inputStream.close(); + } + public long write(OutputStream outputStream) throws IOException { + return write(outputStream, openStream()); + } + private long write(OutputStream outputStream, InputStream inputStream) throws IOException { + long result=0; + byte[] buffer=new byte[1024 * 1000]; + int len; + while ((len=inputStream.read(buffer))>0){ + outputStream.write(buffer, 0, len); + result+=len; + } + close(inputStream); + return result; + } + public String getName(){ + return name; + } + public long getLength() throws IOException{ + if(mLength==0){ + calculateCrc(); + } + return mLength; + } + public long getCrc() throws IOException{ + if(mCrc==0){ + calculateCrc(); + } + return mCrc; + } + public abstract InputStream openStream() throws IOException; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof InputSource)) { + return false; + } + InputSource that = (InputSource) o; + return getName().equals(that.getName()); + } + @Override + public int hashCode() { + return getName().hashCode(); + } + @Override + public String toString(){ + return getClass().getSimpleName()+": "+getName(); + } + private void calculateCrc() throws IOException { + InputStream inputStream=openStream(); + long length=0; + CRC32 crc = new CRC32(); + int bytesRead; + byte[] buffer = new byte[1024*64]; + while((bytesRead = inputStream.read(buffer)) != -1) { + crc.update(buffer, 0, bytesRead); + length+=bytesRead; + } + close(inputStream); + mCrc=crc.getValue(); + mLength=length; + } +} diff --git a/src/ARSCLib/com/reandroid/archive/InputSourceUtil.java b/src/ARSCLib/com/reandroid/archive/InputSourceUtil.java new file mode 100644 index 00000000..49d283eb --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/InputSourceUtil.java @@ -0,0 +1,151 @@ + /* + * 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.archive; + +import java.io.*; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + + public class InputSourceUtil { + + public static String toRelative(File rootDir, File file){ + int len=rootDir.getAbsolutePath().length(); + String path=file.getAbsolutePath(); + path=path.substring(len); + path=sanitize(path); + return path; + } + public static String sanitize(String path){ + path=path.replace('\\', '/'); + while (path.startsWith("./")){ + path=path.substring(2); + } + while (path.startsWith("/")){ + path=path.substring(1); + } + return path; + } + + public static Map mapZipFileSources(ZipFile zipFile){ + Map results=new LinkedHashMap<>(); + Enumeration entriesEnum = zipFile.entries(); + int i=0; + while (entriesEnum.hasMoreElements()){ + ZipEntry zipEntry = entriesEnum.nextElement(); + if(zipEntry.isDirectory()){ + continue; + } + ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry); + source.setSort(i); + source.setMethod(zipEntry.getMethod()); + results.put(source.getName(), source); + i++; + } + return results; + } + public static Map mapInputStreamAsBuffer(InputStream inputStream) throws IOException { + Map results = new LinkedHashMap<>(); + ZipInputStream zin = new ZipInputStream(inputStream); + ZipEntry zipEntry; + int i=0; + while ((zipEntry=zin.getNextEntry())!=null){ + if(zipEntry.isDirectory()){ + continue; + } + byte[] buffer = loadBuffer(zin); + String name = sanitize(zipEntry.getName()); + ByteInputSource source = new ByteInputSource(buffer, name); + source.setSort(i); + source.setMethod(zipEntry.getMethod()); + results.put(name, source); + i++; + } + zin.close(); + return results; + } + private static byte[] loadBuffer(InputStream in) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buff=new byte[40960]; + int len; + while((len=in.read(buff))>0){ + outputStream.write(buff, 0, len); + } + outputStream.close(); + return outputStream.toByteArray(); + } + public static List listZipFileSources(ZipFile zipFile){ + List results=new ArrayList<>(); + Enumeration entriesEnum = zipFile.entries(); + int i=0; + while (entriesEnum.hasMoreElements()){ + ZipEntry zipEntry = entriesEnum.nextElement(); + if(zipEntry.isDirectory()){ + continue; + } + ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry); + source.setSort(i); + results.add(source); + } + return results; + } + public static List listDirectory(File dir){ + List results=new ArrayList<>(); + recursiveDirectory(results, dir, dir); + return results; + } + private static void recursiveDirectory(List results, File rootDir, File dir){ + if(dir.isFile()){ + String name; + if(rootDir.equals(dir)){ + name=dir.getName(); + }else { + name=toRelative(rootDir, dir); + } + results.add(new FileInputSource(dir, name)); + return; + } + File[] childFiles=dir.listFiles(); + if(childFiles==null){ + return; + } + for(File file:childFiles){ + recursiveDirectory(results, rootDir, file); + } + } + public static List sortString(List stringList){ + Comparator cmp=new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } + }; + stringList.sort(cmp); + return stringList; + } + + public static List sort(List sourceList){ + Comparator cmp=new Comparator() { + @Override + public int compare(InputSource in1, InputSource in2) { + return Integer.compare(in1.getSort(), in2.getSort()); + } + }; + sourceList.sort(cmp); + return sourceList; + } +} diff --git a/src/ARSCLib/com/reandroid/archive/WriteInterceptor.java b/src/ARSCLib/com/reandroid/archive/WriteInterceptor.java new file mode 100644 index 00000000..ad80d55a --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/WriteInterceptor.java @@ -0,0 +1,20 @@ + /* + * 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.archive; + +public interface WriteInterceptor { + InputSource onWriteArchive(InputSource inputSource); +} diff --git a/src/ARSCLib/com/reandroid/archive/WriteProgress.java b/src/ARSCLib/com/reandroid/archive/WriteProgress.java new file mode 100644 index 00000000..8c6438b4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/WriteProgress.java @@ -0,0 +1,20 @@ + /* + * 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.archive; + +public interface WriteProgress { + void onCompressFile(String path, int mode, long writtenBytes); +} diff --git a/src/ARSCLib/com/reandroid/archive/ZipAlign.java b/src/ARSCLib/com/reandroid/archive/ZipAlign.java new file mode 100644 index 00000000..8b687e04 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/ZipAlign.java @@ -0,0 +1,333 @@ +/* + This class is copied from "apksigner" and I couldn't find the + original repo/author to credit here. + */ + +package com.reandroid.archive; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + + +public class ZipAlign { + private static final int ZIP_ENTRY_HEADER_LEN = 30; + private static final int ZIP_ENTRY_VERSION = 20; + private static final int ZIP_ENTRY_USES_DATA_DESCR = 0x0008; + private static final int ZIP_ENTRY_DATA_DESCRIPTOR_LEN = 16; + private static final int ALIGNMENT_4 = 4; + private static final int ALIGNMENT_PAGE = 4096; + + private static class XEntry { + public final ZipEntry entry; + public final long headerOffset; + public final int flags; + public final int padding; + + public XEntry(ZipEntry entry, long headerOffset, int flags, int padding) { + this.entry = entry; + this.headerOffset = headerOffset; + this.flags = flags; + this.padding = padding; + } + } + + + private static class FilterOutputStreamEx extends FilterOutputStream { + private long totalWritten = 0; + public FilterOutputStreamEx(OutputStream out) { + super(out); + } + @Override + public void write(byte[] b) throws IOException { + out.write(b); + totalWritten += b.length; + } + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + totalWritten += len; + } + @Override + public void write(int b) throws IOException { + out.write(b); + totalWritten += 1; + } + @Override + public void close() throws IOException { + super.close(); + } + public void writeInt(long v) throws IOException { + write((int) (v & 0xff)); + write((int) ((v >>> 8) & 0xff)); + write((int) ((v >>> 16) & 0xff)); + write((int) ((v >>> 24) & 0xff)); + } + public void writeShort(int v) throws IOException { + write((v) & 0xff); + write((v >>> 8) & 0xff); + } + } + + private File mInputFile; + private int mAlignment; + private File mOutputFile; + private ZipFile mZipFile; + private RandomAccessFile mRafInput; + private FilterOutputStreamEx mOutputStream; + private final List mXEntries = new ArrayList<>(); + private long mInputFileOffset = 0; + private int mTotalPadding = 0; + + public void zipAlign(File input, File output) throws IOException { + zipAlign(input, output, ALIGNMENT_4); + } + public void zipAlign(File input, File output, int alignment) throws IOException { + mInputFile = input; + mAlignment = alignment; + mOutputFile = output; + openFiles(); + copyAllEntries(); + buildCentralDirectory(); + closeFiles(); + } + private void openFiles() throws IOException { + mZipFile = new ZipFile(mInputFile); + mRafInput = new RandomAccessFile(mInputFile, "r"); + mOutputStream = new FilterOutputStreamEx(new BufferedOutputStream(new FileOutputStream(mOutputFile), 32 * 1024)); + } + private void copyAllEntries() throws IOException { + final int entryCount = mZipFile.size(); + if (entryCount == 0) { + return; + } + final Enumeration entries = mZipFile.entries(); + while (entries.hasMoreElements()) { + final ZipEntry entry = (ZipEntry) entries.nextElement(); + final String name = entry.getName(); + + int flags = entry.getMethod() == ZipEntry.STORED ? 0 : 1 << 3; + flags |= 1 << 11; + + final long outputEntryHeaderOffset = mOutputStream.totalWritten; + + final int inputEntryHeaderSize = ZIP_ENTRY_HEADER_LEN + (entry.getExtra() != null ? entry.getExtra().length : 0) + + name.getBytes(StandardCharsets.UTF_8).length; + final long inputEntryDataOffset = mInputFileOffset + inputEntryHeaderSize; + + final int padding; + + if (entry.getMethod() != ZipEntry.STORED) { + padding = 0; + } else { + int alignment = mAlignment; + if (name.startsWith("lib/") && name.endsWith(".so")) { + alignment = ALIGNMENT_PAGE; + } + long newOffset = inputEntryDataOffset + mTotalPadding; + padding = (int) ((alignment - (newOffset % alignment)) % alignment); + mTotalPadding += padding; + } + + final XEntry xentry = new XEntry(entry, outputEntryHeaderOffset, flags, padding); + mXEntries.add(xentry); + byte[] extra = entry.getExtra(); + if (extra == null) { + extra = new byte[padding]; + } else { + byte[] newExtra = new byte[extra.length + padding]; + System.arraycopy(extra, 0, newExtra, 0, extra.length); + Arrays.fill(newExtra, extra.length, newExtra.length, (byte) 0); + extra = newExtra; + } + entry.setExtra(extra); + mOutputStream.writeInt(ZipOutputStream.LOCSIG); + mOutputStream.writeShort(ZIP_ENTRY_VERSION); + mOutputStream.writeShort(flags); + mOutputStream.writeShort(entry.getMethod()); + + int modDate; + int time; + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date(entry.getTime())); + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + modDate = 0x21; + time = 0; + } else { + modDate = cal.get(Calendar.DATE); + modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate; + modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate; + time = cal.get(Calendar.SECOND) >> 1; + time = (cal.get(Calendar.MINUTE) << 5) | time; + time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; + } + + mOutputStream.writeShort(time); + mOutputStream.writeShort(modDate); + + mOutputStream.writeInt(entry.getCrc()); + mOutputStream.writeInt(entry.getCompressedSize()); + mOutputStream.writeInt(entry.getSize()); + + mOutputStream.writeShort(entry.getName().getBytes(StandardCharsets.UTF_8).length); + mOutputStream.writeShort(entry.getExtra().length); + mOutputStream.write(entry.getName().getBytes(StandardCharsets.UTF_8)); + mOutputStream.write(entry.getExtra(), 0, entry.getExtra().length); + + mInputFileOffset += inputEntryHeaderSize; + + final long sizeToCopy; + if ((flags & ZIP_ENTRY_USES_DATA_DESCR) != 0) { + sizeToCopy = (entry.isDirectory() ? 0 : entry.getCompressedSize()) + ZIP_ENTRY_DATA_DESCRIPTOR_LEN; + } else { + sizeToCopy = entry.isDirectory() ? 0 : entry.getCompressedSize(); + } + + if (sizeToCopy > 0) { + mRafInput.seek(mInputFileOffset); + + long totalSizeCopied = 0; + final byte[] buf = new byte[32 * 1024]; + while (totalSizeCopied < sizeToCopy) { + int read = mRafInput.read(buf, 0, (int) Math.min(32 * 1024, sizeToCopy - totalSizeCopied)); + if (read <= 0) { + break; + } + mOutputStream.write(buf, 0, read); + totalSizeCopied += read; + } + } + + mInputFileOffset += sizeToCopy; + } + } + + private void buildCentralDirectory() throws IOException { + final long centralDirOffset = mOutputStream.totalWritten; + final int entryCount = mXEntries.size(); + for (int i = 0; i < entryCount; i++) { + XEntry xentry = mXEntries.get(i); + final ZipEntry entry = xentry.entry; + int modDate; + int time; + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date(entry.getTime())); + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + modDate = 0x21; + time = 0; + } else { + modDate = cal.get(Calendar.DATE); + modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate; + modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate; + time = cal.get(Calendar.SECOND) >> 1; + time = (cal.get(Calendar.MINUTE) << 5) | time; + time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; + } + + mOutputStream.writeInt(ZipFile.CENSIG); // CEN header signature + mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version made by + mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version needed to extract + mOutputStream.writeShort(xentry.flags); // general purpose bit flag + mOutputStream.writeShort(entry.getMethod()); // compression method + mOutputStream.writeShort(time); + mOutputStream.writeShort(modDate); + mOutputStream.writeInt(entry.getCrc()); // crc-32 + mOutputStream.writeInt(entry.getCompressedSize()); // compressed size + mOutputStream.writeInt(entry.getSize()); // uncompressed size + final byte[] nameBytes = entry.getName().getBytes(StandardCharsets.UTF_8); + mOutputStream.writeShort(nameBytes.length); + mOutputStream.writeShort(entry.getExtra() != null ? entry.getExtra().length - xentry.padding : 0); + final byte[] commentBytes; + if (entry.getComment() != null) { + commentBytes = entry.getComment().getBytes(StandardCharsets.UTF_8); + mOutputStream.writeShort(Math.min(commentBytes.length, 0xffff)); + } else { + commentBytes = null; + mOutputStream.writeShort(0); + } + mOutputStream.writeShort(0); // starting disk number + mOutputStream.writeShort(0); // internal file attributes (unused) + mOutputStream.writeInt(0); // external file attributes (unused) + mOutputStream.writeInt(xentry.headerOffset); // relative offset of local header + mOutputStream.write(nameBytes); + if (entry.getExtra() != null) { + mOutputStream.write(entry.getExtra(), 0, entry.getExtra().length - xentry.padding); + } + if (commentBytes != null) { + mOutputStream.write(commentBytes, 0, Math.min(commentBytes.length, 0xffff)); + } + } + final long centralDirSize = mOutputStream.totalWritten - centralDirOffset; + + mOutputStream.writeInt(ZipFile.ENDSIG); // END record signature + mOutputStream.writeShort(0); // number of this disk + mOutputStream.writeShort(0); // central directory start disk + mOutputStream.writeShort(entryCount); // number of directory entries on disk + mOutputStream.writeShort(entryCount); // total number of directory entries + mOutputStream.writeInt(centralDirSize); // length of central directory + mOutputStream.writeInt(centralDirOffset); // offset of central directory + mOutputStream.writeShort(0); + mOutputStream.flush(); + } + + private void closeFiles() throws IOException { + try { + mZipFile.close(); + } finally { + try { + mRafInput.close(); + } finally { + mOutputStream.close(); + } + } + + } + + public static void align4(File inFile) throws IOException{ + align(inFile, ALIGNMENT_4); + } + public static void align4(File inFile, File outFile) throws IOException{ + align(inFile, outFile, ALIGNMENT_4); + } + public static void align(File inFile, int alignment) throws IOException{ + File tmp=toTmpFile(inFile); + tmp.delete(); + align(inFile, tmp, alignment); + inFile.delete(); + tmp.renameTo(inFile); + } + public static void align(File inFile, File outFile, int alignment) throws IOException{ + ZipAlign zipAlign=new ZipAlign(); + File dir=outFile.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + zipAlign.zipAlign(inFile, outFile, alignment); + } + private static File toTmpFile(File file){ + String name=file.getName()+".align.tmp"; + File dir=file.getParentFile(); + if(dir==null){ + return new File(name); + } + return new File(dir, name); + } +} + diff --git a/src/ARSCLib/com/reandroid/archive/ZipArchive.java b/src/ARSCLib/com/reandroid/archive/ZipArchive.java new file mode 100644 index 00000000..80cc6bd5 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/ZipArchive.java @@ -0,0 +1,124 @@ +/* + * 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.archive; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; + +public class ZipArchive { + private final Map mEntriesMap; + public ZipArchive(Map entriesMap){ + this.mEntriesMap=entriesMap; + } + public ZipArchive(){ + this(new LinkedHashMap<>()); + } + + public int size(){ + return mEntriesMap.size(); + } + public void extract(File outDir) throws IOException { + for(InputSource inputSource:listInputSources()){ + extract(outDir, inputSource); + } + } + private void extract(File outDir, InputSource inputSource) throws IOException { + File file=toOutFile(outDir, inputSource.getAlias()); + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); + inputSource.disposeInputSource(); + } + private File toOutFile(File outDir, String path){ + path=path.replace('/', File.separatorChar); + return new File(outDir, path); + } + public void removeDir(String dirName){ + if(!dirName.endsWith("/")){ + dirName=dirName+"/"; + } + for(InputSource inputSource:listInputSources()){ + if(inputSource.getName().startsWith(dirName)){ + remove(inputSource.getName()); + } + } + } + public void removeAll(Pattern patternAlias){ + for(InputSource inputSource:listInputSources()){ + Matcher matcher = patternAlias.matcher(inputSource.getAlias()); + if(matcher.matches()){ + mEntriesMap.remove(inputSource.getName()); + } + } + } + public void clear(){ + mEntriesMap.clear(); + } + public int entriesCount(){ + return mEntriesMap.size(); + } + public InputSource remove(String name){ + InputSource inputSource=mEntriesMap.remove(name); + if(inputSource==null){ + return null; + } + return inputSource; + } + public void addArchive(File archiveFile) throws IOException { + ZipFile zipFile=new ZipFile(archiveFile); + add(zipFile); + } + public void addDirectory(File dir){ + addAll(InputSourceUtil.listDirectory(dir)); + } + public void add(ZipFile zipFile){ + List sourceList = InputSourceUtil.listZipFileSources(zipFile); + this.addAll(sourceList); + } + public void set(Collection inputSourceList){ + clear(); + addAll(inputSourceList); + } + public void addAll(Collection inputSourceList){ + for(InputSource inputSource:inputSourceList){ + add(inputSource); + } + } + public void add(InputSource inputSource){ + if(inputSource==null){ + return; + } + String name=inputSource.getName(); + Map map=mEntriesMap; + map.remove(name); + map.put(name, inputSource); + } + public List listInputSources(){ + return new ArrayList<>(mEntriesMap.values()); + } + public InputSource getInputSource(String name){ + return mEntriesMap.get(name); + } +} diff --git a/src/ARSCLib/com/reandroid/archive/ZipEntrySource.java b/src/ARSCLib/com/reandroid/archive/ZipEntrySource.java new file mode 100644 index 00000000..41bc0967 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/ZipEntrySource.java @@ -0,0 +1,36 @@ + /* + * 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.archive; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ZipEntrySource extends InputSource { + private final ZipFile zipFile; + private final ZipEntry zipEntry; + public ZipEntrySource(ZipFile zipFile, ZipEntry zipEntry){ + super(zipEntry.getName()); + this.zipFile=zipFile; + this.zipEntry=zipEntry; + super.setMethod(zipEntry.getMethod()); + } + @Override + public InputStream openStream() throws IOException { + return zipFile.getInputStream(zipEntry); + } +} diff --git a/src/ARSCLib/com/reandroid/archive/ZipSerializer.java b/src/ARSCLib/com/reandroid/archive/ZipSerializer.java new file mode 100644 index 00000000..f462ecba --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive/ZipSerializer.java @@ -0,0 +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. + */ +package com.reandroid.archive; + +import java.io.*; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ZipSerializer { + private final List mSourceList; + private WriteProgress writeProgress; + private WriteInterceptor writeInterceptor; + public ZipSerializer(List sourceList){ + this.mSourceList=sourceList; + } + + public void setWriteInterceptor(WriteInterceptor writeInterceptor) { + this.writeInterceptor = writeInterceptor; + } + public void setWriteProgress(WriteProgress writeProgress){ + this.writeProgress=writeProgress; + } + public long writeZip(File outZip) throws IOException{ + File dir=outZip.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + File tmp=toTmpFile(outZip); + FileOutputStream fileOutputStream=new FileOutputStream(tmp); + long length= writeZip(fileOutputStream); + fileOutputStream.close(); + outZip.delete(); + tmp.renameTo(outZip); + return length; + } + private File toTmpFile(File file){ + File dir=file.getParentFile(); + String name=file.getName()+".tmp"; + return new File(dir, name); + } + public long writeZip(OutputStream outputStream) throws IOException{ + long length=0; + WriteProgress progress=writeProgress; + ZipOutputStream zipOutputStream=new ZipOutputStream(outputStream); + for(InputSource inputSource:mSourceList){ + inputSource = interceptWrite(inputSource); + if(inputSource==null){ + continue; + } + if(progress!=null){ + progress.onCompressFile(inputSource.getAlias(), inputSource.getMethod(), length); + } + length+=write(zipOutputStream, inputSource); + zipOutputStream.closeEntry(); + inputSource.disposeInputSource(); + } + zipOutputStream.close(); + return length; + } + private long write(ZipOutputStream zipOutputStream, InputSource inputSource) throws IOException{ + ZipEntry zipEntry=createZipEntry(inputSource); + zipOutputStream.putNextEntry(zipEntry); + return inputSource.write(zipOutputStream); + } + private ZipEntry createZipEntry(InputSource inputSource) throws IOException { + String name=inputSource.getAlias(); + ZipEntry zipEntry=new ZipEntry(name); + int method = inputSource.getMethod(); + zipEntry.setMethod(method); + if(method==ZipEntry.STORED){ + zipEntry.setCrc(inputSource.getCrc()); + zipEntry.setSize(inputSource.getLength()); + } + return zipEntry; + } + private InputSource interceptWrite(InputSource inputSource){ + WriteInterceptor interceptor=writeInterceptor; + if(interceptor!=null){ + return interceptor.onWriteArchive(inputSource); + } + return inputSource; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/Archive.java b/src/ARSCLib/com/reandroid/archive2/Archive.java new file mode 100644 index 00000000..295fa15b --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/Archive.java @@ -0,0 +1,129 @@ +/* + * 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.archive2; + +import com.reandroid.archive.APKArchive; +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.block.*; +import com.reandroid.archive2.io.ArchiveEntrySource; +import com.reandroid.archive2.io.ZipFileInput; +import com.reandroid.archive2.io.ArchiveUtil; +import com.reandroid.archive2.io.ZipInput; +import com.reandroid.archive2.model.LocalFileDirectory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; + +public class Archive { + private final ZipInput zipInput; + private final List entryList; + private final EndRecord endRecord; + private final ApkSignatureBlock apkSignatureBlock; + public Archive(ZipInput zipInput) throws IOException { + this.zipInput = zipInput; + LocalFileDirectory lfd = new LocalFileDirectory(); + lfd.visit(zipInput); + List localFileHeaderList = lfd.getHeaderList(); + List centralEntryHeaderList = lfd.getCentralFileDirectory().getHeaderList(); + List entryList = new ArrayList<>(localFileHeaderList.size()); + for(int i=0;i mapEntrySource(){ + Map map = new LinkedHashMap<>(); + ZipInput zipInput = this.zipInput; + List entryList = this.entryList; + for(int i=0; i getEntryList() { + return entryList; + } + + public ApkSignatureBlock getApkSignatureBlock() { + return apkSignatureBlock; + } + public EndRecord getEndRecord() { + return endRecord; + } + + // for test + public void extract(File dir) throws IOException { + for(ArchiveEntry archiveEntry:getEntryList()){ + if(archiveEntry.isDirectory()){ + continue; + } + extract(dir, archiveEntry); + } + } + private void extract(File dir, ArchiveEntry archiveEntry) throws IOException{ + File out = toFile(dir, archiveEntry); + File parent = out.getParentFile(); + if(!parent.exists()){ + parent.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(out); + ArchiveUtil.writeAll(openInputStream(archiveEntry), outputStream); + outputStream.close(); + } + private File toFile(File dir, ArchiveEntry archiveEntry){ + String name = archiveEntry.getName().replace('/', File.separatorChar); + return new File(dir, name); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/ArchiveEntry.java b/src/ARSCLib/com/reandroid/archive2/ArchiveEntry.java new file mode 100644 index 00000000..e6856b55 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/ArchiveEntry.java @@ -0,0 +1,123 @@ +/* + * 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.archive2; + +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.arsc.util.HexUtil; + +import java.util.zip.ZipEntry; + +public class ArchiveEntry extends ZipEntry { + private final CentralEntryHeader centralEntryHeader; + private final LocalFileHeader localFileHeader; + public ArchiveEntry(LocalFileHeader lfh, CentralEntryHeader ceh){ + super(lfh.getFileName()); + this.localFileHeader = lfh; + this.centralEntryHeader = ceh; + } + public ArchiveEntry(String name){ + this(new LocalFileHeader(name), new CentralEntryHeader(name)); + } + public ArchiveEntry(){ + this(new LocalFileHeader(), new CentralEntryHeader()); + } + + public long getDataSize(){ + if(getMethod() == ZipEntry.STORED){ + return getSize(); + } + return getCompressedSize(); + } + + @Override + public int getMethod(){ + return localFileHeader.getMethod(); + } + @Override + public void setMethod(int method){ + localFileHeader.setMethod(method); + centralEntryHeader.setMethod(method); + } + @Override + public long getSize() { + return centralEntryHeader.getSize(); + } + @Override + public void setSize(long size) { + centralEntryHeader.setSize(size); + localFileHeader.setSize(size); + } + @Override + public long getCrc() { + return centralEntryHeader.getCrc(); + } + @Override + public void setCrc(long crc) { + centralEntryHeader.setCrc(crc); + localFileHeader.setCrc(crc); + } + @Override + public long getCompressedSize() { + return centralEntryHeader.getCompressedSize(); + } + @Override + public void setCompressedSize(long csize) { + centralEntryHeader.setCompressedSize(csize); + localFileHeader.setCompressedSize(csize); + } + public long getFileOffset() { + return localFileHeader.getFileOffset(); + } + @Override + public String getName(){ + return centralEntryHeader.getFileName(); + } + public void setName(String name){ + centralEntryHeader.setFileName(name); + localFileHeader.setFileName(name); + } + @Override + public String getComment(){ + return centralEntryHeader.getComment(); + } + @Override + public void setComment(String name){ + centralEntryHeader.setComment(name); + } + @Override + public boolean isDirectory() { + return this.getName().endsWith("/"); + } + public CentralEntryHeader getCentralEntryHeader(){ + return centralEntryHeader; + } + public LocalFileHeader getLocalFileHeader() { + return localFileHeader; + } + public boolean matches(CentralEntryHeader centralEntryHeader){ + if(centralEntryHeader==null){ + return false; + } + return false; + } + + @Override + public String toString(){ + return "["+ getFileOffset()+"] " + getName() + getComment() + + HexUtil.toHex(" 0x", getCrc(), 8); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/ZipSignature.java b/src/ARSCLib/com/reandroid/archive2/ZipSignature.java new file mode 100644 index 00000000..c9526dde --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/ZipSignature.java @@ -0,0 +1,41 @@ +/* + * 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.archive2; + +public enum ZipSignature { + CENTRAL_FILE(0X02014B50), + LOCAL_FILE(0X04034B50), + DATA_DESCRIPTOR(0X08074B50), + END_RECORD(0X06054B50); + + private final int value; + + ZipSignature(int value){ + this.value = value; + } + public int getValue() { + return value; + } + public static ZipSignature valueOf(int value){ + for(ZipSignature signature:VALUES){ + if(value == signature.getValue()){ + return signature; + } + } + return null; + } + private static final ZipSignature[] VALUES = values(); +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/ApkSignatureBlock.java b/src/ARSCLib/com/reandroid/archive2/block/ApkSignatureBlock.java new file mode 100644 index 00000000..4c2c2898 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/ApkSignatureBlock.java @@ -0,0 +1,155 @@ +/* + * 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.archive2.block; + + +import com.reandroid.archive2.block.pad.SchemePadding; +import com.reandroid.arsc.io.BlockReader; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class ApkSignatureBlock extends LengthPrefixedList + implements Comparator { + public ApkSignatureBlock(SignatureFooter signatureFooter){ + super(true); + setBottomBlock(signatureFooter); + } + public ApkSignatureBlock(){ + this(new SignatureFooter()); + } + public List getSignatures(){ + return super.getElements(); + } + public int countSignatures(){ + return super.getElementsCount(); + } + public void sortSignatures(){ + sort(this); + } + public void updatePadding(){ + SchemePadding schemePadding = getOrCreateSchemePadding(); + schemePadding.setPadding(0); + sortSignatures(); + refresh(); + int size = countBytes(); + int alignment = 4096; + int padding = (alignment - (size % alignment)) % alignment; + schemePadding.setPadding(padding); + refresh(); + } + private SchemePadding getOrCreateSchemePadding(){ + SignatureInfo signatureInfo = getSignature(SignatureId.PADDING); + if(signatureInfo == null){ + signatureInfo = new SignatureInfo(); + signatureInfo.setId(SignatureId.PADDING); + signatureInfo.setSignatureScheme(new SchemePadding()); + add(signatureInfo); + } + SignatureScheme scheme = signatureInfo.getSignatureScheme(); + if(!(scheme instanceof SchemePadding)){ + scheme = new SchemePadding(); + signatureInfo.setSignatureScheme(scheme); + } + return (SchemePadding) scheme; + } + public SignatureInfo getSignature(SignatureId signatureId){ + for(SignatureInfo signatureInfo:getSignatures()){ + if(signatureInfo.getId().equals(signatureId)){ + return signatureInfo; + } + } + return null; + } + public SignatureFooter getSignatureFooter(){ + return (SignatureFooter) getBottomBlock(); + } + @Override + public SignatureInfo newInstance() { + return new SignatureInfo(); + } + @Override + protected void onRefreshed(){ + SignatureFooter footer = getSignatureFooter(); + footer.updateMagic(); + super.onRefreshed(); + footer.setSignatureSize(getDataSize()); + } + + public void writeRaw(File file) throws IOException{ + refresh(); + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + writeBytes(outputStream); + outputStream.close(); + } + public List writeSplitRawToDirectory(File dir) throws IOException{ + refresh(); + List signatureInfoList = getElements(); + List writtenFiles = new ArrayList<>(signatureInfoList.size()); + for(SignatureInfo signatureInfo:signatureInfoList){ + File file = signatureInfo.writeRawToDirectory(dir); + writtenFiles.add(file); + } + return writtenFiles; + } + public void read(File file) throws IOException { + super.readBytes(new BlockReader(file)); + } + public void scanSplitFiles(File dir) throws IOException { + if(!dir.isDirectory()){ + throw new IOException("No such directory"); + } + FileFilter filter = new FileFilter() { + @Override + public boolean accept(File file) { + if(!file.isFile()){ + return false; + } + String name = file.getName().toLowerCase(); + return name.endsWith(SignatureId.FILE_EXT_RAW); + } + }; + File[] files = dir.listFiles(filter); + if(files == null){ + return; + } + for(File file:files){ + addSplitRaw(file); + } + sortSignatures(); + } + public SignatureInfo addSplitRaw(File signatureInfoFile) throws IOException { + SignatureInfo signatureInfo = new SignatureInfo(); + signatureInfo.read(signatureInfoFile); + add(signatureInfo); + return signatureInfo; + } + @Override + public int compare(SignatureInfo info1, SignatureInfo info2) { + return info1.getId().compareTo(info2.getId()); + } + + public static final String FILE_EXT = ".sig"; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/BottomBlock.java b/src/ARSCLib/com/reandroid/archive2/block/BottomBlock.java new file mode 100644 index 00000000..2ae5aeaf --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/BottomBlock.java @@ -0,0 +1,36 @@ +/* + * 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.archive2.block; + +import com.reandroid.arsc.container.BlockList; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; + +// General purpose block to consume the remaining bytes of BlockReader +public class BottomBlock extends BlockList { + public BottomBlock(){ + super(); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + while (reader.isAvailable()){ + LengthPrefixedBytes prefixedBytes = new LengthPrefixedBytes(false); + prefixedBytes.readBytes(reader); + this.add(prefixedBytes); + } + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/CentralEntryHeader.java b/src/ARSCLib/com/reandroid/archive2/block/CentralEntryHeader.java new file mode 100644 index 00000000..1a6b7c07 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/CentralEntryHeader.java @@ -0,0 +1,189 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.ZipSignature; +import com.reandroid.arsc.util.HexUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +public class CentralEntryHeader extends CommonHeader { + private String mComment; + public CentralEntryHeader(){ + super(OFFSET_fileName, ZipSignature.CENTRAL_FILE, OFFSET_general_purpose); + } + public CentralEntryHeader(String name){ + this(); + setFileName(name); + } + + @Override + int readComment(InputStream inputStream) throws IOException { + int commentLength = getCommentLength(); + if(commentLength==0){ + mComment = ""; + return 0; + } + setCommentLength(commentLength); + byte[] bytes = getBytesInternal(); + int read = inputStream.read(bytes, getOffsetComment(), commentLength); + if(read != commentLength){ + throw new IOException("Stream ended before reading comment: read=" + +read+", name length="+commentLength); + } + mComment = null; + return commentLength; + } + + public int getVersionExtract(){ + return getShortUnsigned(OFFSET_versionExtract); + } + public void setVersionExtract(int value){ + putShort(OFFSET_versionExtract, value); + } + public String getComment(){ + if(mComment == null){ + mComment = decodeComment(); + } + return mComment; + } + public void setComment(String comment){ + if(comment==null){ + comment=""; + } + byte[] strBytes = ZipStringEncoding.encodeString(isUtf8(), comment); + int length = strBytes.length; + setCommentLength(length); + if(length==0){ + mComment = comment; + return; + } + byte[] bytes = getBytesInternal(); + System.arraycopy(strBytes, 0, bytes, getOffsetComment(), length); + mComment = comment; + } + + + @Override + public int getCommentLength(){ + return getShortUnsigned(OFFSET_commentLength); + } + public void setCommentLength(int value){ + int length = getOffsetComment() + value; + setBytesLength(length, false); + putShort(OFFSET_commentLength, value); + } + public long getLocalRelativeOffset(){ + return getIntegerUnsigned(OFFSET_localRelativeOffset); + } + public void setLocalRelativeOffset(long offset){ + putInteger(OFFSET_localRelativeOffset, offset); + } + @Override + void onUtf8Changed(boolean oldValue){ + String str = mComment; + if(str != null){ + setComment(str); + } + } + + public boolean matches(LocalFileHeader localFileHeader){ + if(localFileHeader==null){ + return false; + } + return getCrc() == localFileHeader.getCrc() + && Objects.equals(getFileName(), localFileHeader.getFileName()); + } + + @Override + public String toString(){ + if(countBytes()0){ + builder.append("name=").append(str); + appendOnce = true; + } + str = getComment(); + if(str.length()>0){ + if(appendOnce){ + builder.append(", "); + } + builder.append("comment=").append(str); + appendOnce = true; + } + if(appendOnce){ + builder.append(", "); + } + builder.append("SIG=").append(getSignature()); + builder.append(", versionMadeBy=").append(HexUtil.toHex4((short) getVersionMadeBy())); + builder.append(", versionExtract=").append(HexUtil.toHex4((short) getVersionExtract())); + builder.append(", GP={").append(getGeneralPurposeFlag()).append("}"); + builder.append(", method=").append(getMethod()); + builder.append(", date=").append(getDate()); + builder.append(", crc=").append(HexUtil.toHex8(getCrc())); + builder.append(", cSize=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + builder.append(", fileNameLength=").append(getFileNameLength()); + builder.append(", extraLength=").append(getExtraLength()); + builder.append(", commentLength=").append(getCommentLength()); + builder.append(", offset=").append(getLocalRelativeOffset()); + return builder.toString(); + } + + + public static CentralEntryHeader fromLocalFileHeader(LocalFileHeader lfh){ + CentralEntryHeader ceh = new CentralEntryHeader(); + ceh.setSignature(ZipSignature.CENTRAL_FILE); + ceh.setVersionMadeBy(0x0300); + long offset = lfh.getFileOffset() - lfh.countBytes(); + ceh.setLocalRelativeOffset(offset); + ceh.getGeneralPurposeFlag().setValue(lfh.getGeneralPurposeFlag().getValue()); + ceh.setMethod(lfh.getMethod()); + ceh.setDosTime(lfh.getDosTime()); + ceh.setCrc(lfh.getCrc()); + ceh.setCompressedSize(lfh.getCompressedSize()); + ceh.setSize(lfh.getSize()); + ceh.setFileName(lfh.getFileName()); + ceh.setExtra(lfh.getExtra()); + return ceh; + } + private static final int OFFSET_signature = 0; + private static final int OFFSET_versionMadeBy = 4; + private static final int OFFSET_versionExtract = 6; + private static final int OFFSET_general_purpose = 8; + private static final int OFFSET_method = 10; + private static final int OFFSET_dos_time = 12; + private static final int OFFSET_dos_date = 14; + private static final int OFFSET_crc = 16; + private static final int OFFSET_compressed_size = 20; + private static final int OFFSET_size = 24; + private static final int OFFSET_fileNameLength = 28; + private static final int OFFSET_extraLength = 30; + private static final int OFFSET_commentLength = 32; + private static final int OFFSET_diskStart = 34; + private static final int OFFSET_internalFileAttributes = 36; + private static final int OFFSET_externalFileAttributes = 38; + private static final int OFFSET_localRelativeOffset = 42; + private static final int OFFSET_fileName = 46; + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/CertificateBlock.java b/src/ARSCLib/com/reandroid/archive2/block/CertificateBlock.java new file mode 100644 index 00000000..1894f70e --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/CertificateBlock.java @@ -0,0 +1,54 @@ +/* + * 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.archive2.block; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class CertificateBlock extends LengthPrefixedBytes{ + public CertificateBlock() { + super(false); + } + + public X509Certificate getCertificate(){ + return generateCertificate(getByteArray().toArray()); + } + public static X509Certificate generateCertificate(byte[] encodedForm){ + CertificateFactory factory = getCertFactory(); + if(factory == null){ + return null; + } + try{ + // TODO: cert bytes could be in DER format ? + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(encodedForm)); + }catch (CertificateException ignored){ + return null; + } + } + private static CertificateFactory getCertFactory() { + if (sCertFactory == null) { + try { + sCertFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException ignored) { + } + } + return sCertFactory; + } + + private static CertificateFactory sCertFactory = null; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/CertificateBlockList.java b/src/ARSCLib/com/reandroid/archive2/block/CertificateBlockList.java new file mode 100644 index 00000000..958cf13f --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/CertificateBlockList.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.archive2.block; + + +public class CertificateBlockList extends LengthPrefixedList{ + public CertificateBlockList() { + super(false); + } + @Override + public CertificateBlock newInstance() { + return new CertificateBlock(); + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/CommonHeader.java b/src/ARSCLib/com/reandroid/archive2/block/CommonHeader.java new file mode 100644 index 00000000..db31b279 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/CommonHeader.java @@ -0,0 +1,419 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.ZipSignature; +import com.reandroid.arsc.util.HexUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.zip.ZipEntry; + +public abstract class CommonHeader extends ZipHeader { + private final int offsetFileName; + private final int offsetGeneralPurpose; + private final GeneralPurposeFlag generalPurposeFlag; + private String mFileName; + private long mFileOffset; + public CommonHeader(int offsetFileName, ZipSignature expectedSignature, int offsetGeneralPurpose){ + super(offsetFileName, expectedSignature); + this.offsetFileName = offsetFileName; + this.offsetGeneralPurpose = offsetGeneralPurpose; + this.generalPurposeFlag = new GeneralPurposeFlag(this, offsetGeneralPurpose); + } + public long getFileOffset() { + return mFileOffset; + } + public void setFileOffset(long fileOffset){ + this.mFileOffset = fileOffset; + } + public long getDataSize(){ + if(getMethod() == ZipEntry.STORED){ + return getSize(); + } + return getCompressedSize(); + } + public void setDataSize(long size){ + if(getMethod() == ZipEntry.STORED){ + setSize(size); + } + setCompressedSize(size); + } + + @Override + int readNext(InputStream inputStream) throws IOException { + int read = 0; + read += readFileName(inputStream); + read += readExtra(inputStream); + read += readComment(inputStream); + mFileName = null; + return read; + } + private int readFileName(InputStream inputStream) throws IOException { + int fileNameLength = getFileNameLength(); + if(fileNameLength==0){ + mFileName = ""; + return 0; + } + setFileNameLength(fileNameLength); + byte[] bytes = getBytesInternal(); + int read = inputStream.read(bytes, offsetFileName, fileNameLength); + if(read != fileNameLength){ + throw new IOException("Stream ended before reading file name: read=" + +read+", name length="+fileNameLength); + } + mFileName = null; + return fileNameLength; + } + private int readExtra(InputStream inputStream) throws IOException { + int extraLength = getExtraLength(); + if(extraLength==0){ + return 0; + } + setExtraLength(extraLength); + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + int read = inputStream.read(bytes, offset, extraLength); + if(read != extraLength){ + throw new IOException("Stream ended before reading extra bytes: read=" + + read +", extra length="+extraLength); + } + return extraLength; + } + int readComment(InputStream inputStream) throws IOException { + return 0; + } + public int getVersionMadeBy(){ + return getShortUnsigned(OFFSET_versionMadeBy); + } + public void setVersionMadeBy(int value){ + putShort(OFFSET_versionMadeBy, value); + } + public int getPlatform(){ + return getByteUnsigned(OFFSET_platform); + } + public void setPlatform(int value){ + getBytesInternal()[OFFSET_platform] = (byte) value; + } + public GeneralPurposeFlag getGeneralPurposeFlag() { + return generalPurposeFlag; + } + public int getMethod(){ + return getShortUnsigned(offsetGeneralPurpose + 2); + } + public void setMethod(int value){ + putShort(offsetGeneralPurpose + 2, value); + GeneralPurposeFlag gpf = getGeneralPurposeFlag(); + //gpf.setHasDataDescriptor(value != ZipEntry.STORED); + } + public long getDosTime(){ + return getIntegerUnsigned(offsetGeneralPurpose + 4); + } + public void setDosTime(long value){ + putInteger(offsetGeneralPurpose + 4, value); + } + public Date getDate(){ + return dosToJavaDate(getDosTime()); + } + public void setDate(Date date){ + setDate(date==null ? 0L : date.getTime()); + } + public void setDate(long date){ + setDosTime(javaToDosTime(date)); + } + public long getCrc(){ + return getIntegerUnsigned(offsetGeneralPurpose + 8); + } + public void setCrc(long value){ + putInteger(offsetGeneralPurpose + 8, value); + } + public long getCompressedSize(){ + return getIntegerUnsigned(offsetGeneralPurpose + 12); + } + public void setCompressedSize(long value){ + putInteger(offsetGeneralPurpose + 12, value); + } + public long getSize(){ + return getIntegerUnsigned(offsetGeneralPurpose + 16); + } + public void setSize(long value){ + putInteger(offsetGeneralPurpose + 16, value); + } + public int getFileNameLength(){ + return getShortUnsigned(offsetGeneralPurpose + 20); + } + private void setFileNameLength(int value){ + int length = offsetFileName + value + getExtraLength() + getCommentLength(); + super.setBytesLength(length, false); + putShort(offsetGeneralPurpose + 20, value); + } + public int getExtraLength(){ + return getShortUnsigned(offsetGeneralPurpose + 22); + } + public void setExtraLength(int value){ + int length = offsetFileName + getFileNameLength() + value + getCommentLength(); + super.setBytesLength(length, false); + putShort(offsetGeneralPurpose + 22, value); + } + public byte[] getExtra(){ + int length = getExtraLength(); + byte[] result = new byte[length]; + if(length==0){ + return result; + } + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + System.arraycopy(bytes, offset, result, 0, length); + return result; + } + public void setExtra(byte[] extra){ + if(extra == null){ + extra = new byte[0]; + } + int length = extra.length; + setExtraLength(length); + if(length == 0){ + return; + } + putBytes(extra, 0, getOffsetExtra(), length); + } + public int getCommentLength(){ + return 0; + } + int getOffsetComment(){ + return offsetFileName + getFileNameLength() + getExtraLength(); + } + private int getOffsetExtra(){ + return offsetFileName + getFileNameLength(); + } + + public String getFileName(){ + if(mFileName == null){ + mFileName = decodeFileName(); + } + return mFileName; + } + public void setFileName(String fileName){ + if(fileName==null){ + fileName=""; + } + byte[] nameBytes; + if(getGeneralPurposeFlag().getUtf8()){ + nameBytes = fileName.getBytes(StandardCharsets.UTF_8); + }else { + nameBytes = fileName.getBytes(); + } + int length = nameBytes.length; + setFileNameLength(length); + if(length==0){ + mFileName = fileName; + return; + } + byte[] bytes = getBytesInternal(); + System.arraycopy(nameBytes, 0, bytes, offsetFileName, length); + mFileName = fileName; + } + public boolean isUtf8(){ + return getGeneralPurposeFlag().getUtf8(); + } + public boolean hasDataDescriptor(){ + return getGeneralPurposeFlag().hasDataDescriptor(); + } + private String decodeFileName(){ + int length = getFileNameLength(); + byte[] bytes = getBytesInternal(); + int offset = offsetFileName; + int max = bytes.length - offset; + if(max<=0){ + return ""; + } + if(length>max){ + length = max; + } + return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(), + getBytesInternal(), offset, length); + } + public String decodeComment(){ + int length = getExtraLength(); + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + int max = bytes.length - offset; + if(max<=0){ + return ""; + } + if(length>max){ + length = max; + } + return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(), + getBytesInternal(), offset, length); + } + void onUtf8Changed(boolean oldValue){ + String str = mFileName; + if(str != null){ + setFileName(str); + } + } + + @Override + public String toString(){ + if(countBytes()0){ + builder.append("name=").append(str); + appendOnce = true; + } + if(appendOnce){ + builder.append(", "); + } + builder.append("SIG=").append(getSignature()); + builder.append(", versionMadeBy=").append(HexUtil.toHex4((short) getVersionMadeBy())); + builder.append(", platform=").append(HexUtil.toHex2((byte) getPlatform())); + builder.append(", GP={").append(getGeneralPurposeFlag()).append("}"); + builder.append(", method=").append(getMethod()); + builder.append(", date=").append(getDate()); + builder.append(", crc=").append(HexUtil.toHex8(getCrc())); + builder.append(", cSize=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + builder.append(", fileNameLength=").append(getFileNameLength()); + builder.append(", extraLength=").append(getExtraLength()); + return builder.toString(); + } + + private static Date dosToJavaDate(final long dosTime) { + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + private static long javaToDosTime(long javaTime) { + int date; + int time; + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date(javaTime)); + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + date = 0x21; + time = 0; + } else { + date = cal.get(Calendar.DATE); + date = (cal.get(Calendar.MONTH) + 1 << 5) | date; + date = ((cal.get(Calendar.YEAR) - 1980) << 9) | date; + time = cal.get(Calendar.SECOND) >> 1; + time = (cal.get(Calendar.MINUTE) << 5) | time; + time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; + } + return ((long) date << 16) | time; + } + + public static class GeneralPurposeFlag { + private final CommonHeader localFileHeader; + private final int offset; + public GeneralPurposeFlag(CommonHeader commonHeader, int offset){ + this.localFileHeader = commonHeader; + this.offset = offset; + } + + public boolean getEncryption(){ + return this.localFileHeader.getBit(offset, 0); + } + public void setEncryption(boolean flag){ + this.localFileHeader.putBit(offset, 0, flag); + } + public boolean hasDataDescriptor(){ + return this.localFileHeader.getBit(offset, 3); + } + public void setHasDataDescriptor(boolean flag){ + this.localFileHeader.putBit(offset, 3, flag); + } + public boolean getStrongEncryption(){ + return this.localFileHeader.getBit(offset, 6); + } + public void setStrongEncryption(boolean flag){ + this.localFileHeader.putBit(offset, 6, flag); + } + public boolean getUtf8(){ + return this.localFileHeader.getBit(offset + 1, 3); + } + public void setUtf8(boolean flag){ + setUtf8(flag, true); + } + private void setUtf8(boolean flag, boolean notify){ + boolean oldUtf8 = getUtf8(); + if(oldUtf8 == flag){ + return; + } + this.localFileHeader.putBit(offset +1, 3, flag); + if(notify){ + this.localFileHeader.onUtf8Changed(oldUtf8); + } + } + + public int getValue(){ + return this.localFileHeader.getShortUnsigned(offset); + } + public void setValue(int value){ + if(value == getValue()){ + return; + } + boolean oldUtf8 = getUtf8(); + this.localFileHeader.putShort(offset, value); + if(oldUtf8 != getUtf8()){ + this.localFileHeader.onUtf8Changed(oldUtf8); + } + } + public void initDefault(){ + setUtf8(false, false); + } + + @Override + public String toString(){ + return "Enc="+ getEncryption() + +", Descriptor="+ hasDataDescriptor() + +", StrongEnc="+ getStrongEncryption() + +", UTF8="+ getUtf8(); + } + } + + private static final int OFFSET_versionMadeBy = 4; + private static final int OFFSET_platform = 5; + + private static final int OFFSET_general_purpose = 6; + + private static final int OFFSET_method = 8; + private static final int OFFSET_dos_time = 10; + private static final int OFFSET_crc = 14; + private static final int OFFSET_compressed_size = 18; + private static final int OFFSET_size = 22; + private static final int OFFSET_fileNameLength = 26; + private static final int OFFSET_extraLength = 28; + + private static final int OFFSET_fileName = 30; + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/DataDescriptor.java b/src/ARSCLib/com/reandroid/archive2/block/DataDescriptor.java new file mode 100644 index 00000000..2aa0cf2a --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/DataDescriptor.java @@ -0,0 +1,67 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.ZipSignature; +import com.reandroid.arsc.util.HexUtil; + +public class DataDescriptor extends ZipHeader{ + public DataDescriptor() { + super(MIN_LENGTH, ZipSignature.DATA_DESCRIPTOR); + } + public long getCrc(){ + return getIntegerUnsigned(OFFSET_crc); + } + public void setCrc(long value){ + putInteger(OFFSET_crc, value); + } + public long getCompressedSize(){ + return getIntegerUnsigned(OFFSET_compressed_size); + } + public void setCompressedSize(long value){ + putInteger(OFFSET_compressed_size, value); + } + public long getSize(){ + return getIntegerUnsigned(OFFSET_size); + } + public void setSize(long value){ + putInteger(OFFSET_size, value); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(getSignature()); + builder.append(", crc=").append(HexUtil.toHex8(getCrc())); + builder.append(", compressed=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + return builder.toString(); + } + public static DataDescriptor fromLocalFile(LocalFileHeader lfh){ + DataDescriptor dataDescriptor = new DataDescriptor(); + dataDescriptor.setSignature(ZipSignature.DATA_DESCRIPTOR); + dataDescriptor.setSize(lfh.getSize()); + dataDescriptor.setCompressedSize(lfh.getCompressedSize()); + dataDescriptor.setCrc(lfh.getCrc()); + return dataDescriptor; + } + + private static final int OFFSET_crc = 4; + private static final int OFFSET_compressed_size = 8; + private static final int OFFSET_size = 12; + + public static final int MIN_LENGTH = 16; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/EndRecord.java b/src/ARSCLib/com/reandroid/archive2/block/EndRecord.java new file mode 100644 index 00000000..49a4c0ff --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/EndRecord.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.archive2.block; + +import com.reandroid.archive2.ZipSignature; +import com.reandroid.arsc.util.HexUtil; + +public class EndRecord extends ZipHeader{ + public EndRecord() { + super(MIN_LENGTH, ZipSignature.END_RECORD); + } + public int getNumberOfDisk(){ + return getShortUnsigned(OFFSET_numberOfDisk); + } + public void setNumberOfDisk(int value){ + putShort(OFFSET_numberOfDisk, value); + } + public int getCentralDirectoryStartDisk(){ + return getShortUnsigned(OFFSET_centralDirectoryStartDisk); + } + public void setCentralDirectoryStartDisk(int value){ + putShort(OFFSET_centralDirectoryStartDisk, value); + } + public int getNumberOfDirectories(){ + return getShortUnsigned(OFFSET_numberOfDirectories); + } + public void setNumberOfDirectories(int value){ + putShort(OFFSET_numberOfDirectories, value); + } + public int getTotalNumberOfDirectories(){ + return getShortUnsigned(OFFSET_totalNumberOfDirectories); + } + public void setTotalNumberOfDirectories(int value){ + putShort(OFFSET_totalNumberOfDirectories, value); + } + public long getLengthOfCentralDirectory(){ + return getIntegerUnsigned(OFFSET_lengthOfCentralDirectory); + } + public void setLengthOfCentralDirectory(long value){ + putInteger(OFFSET_lengthOfCentralDirectory, value); + } + public long getOffsetOfCentralDirectory(){ + return getIntegerUnsigned(OFFSET_offsetOfCentralDirectory); + } + public void setOffsetOfCentralDirectory(int value){ + putInteger(OFFSET_offsetOfCentralDirectory, value); + } + public int getLastShort(){ + return getShortUnsigned(OFFSET_lastShort); + } + public void getLastShort(int value){ + putShort(OFFSET_lastShort, value); + } + + + @Override + public String toString(){ + if(countBytes() extends FixedBlockContainer + implements BlockCreator { + private final Block numberBlock; + private final BlockList elements; + private final SingleBlockContainer bottomContainer; + public LengthPrefixedList(boolean is_long){ + super(3); + Block numberBlock; + if(is_long){ + numberBlock = new LongItem(); + }else { + numberBlock = new IntegerItem(); + } + this.numberBlock = numberBlock; + this.elements = new BlockList<>(); + this.bottomContainer = new SingleBlockContainer<>(); + addChild(0, this.numberBlock); + addChild(1, this.elements); + addChild(2, this.bottomContainer); + } + public long getDataSize(){ + Block numberBlock = this.numberBlock; + if(numberBlock instanceof LongItem){ + return ((LongItem)numberBlock).get(); + } + return ((IntegerItem)numberBlock).get(); + } + public void setDataSize(long dataSize){ + Block numberBlock = this.numberBlock; + if(numberBlock instanceof LongItem){ + ((LongItem)numberBlock).set(dataSize); + }else { + ((IntegerItem)numberBlock).set((int) dataSize); + } + } + public int getElementsCount(){ + return getElements().size(); + } + public List getElements() { + return elements.getChildes(); + } + public T add(T element){ + this.elements.add(element); + return element; + } + public boolean remove(T element){ + return this.elements.remove(element); + } + public void sort(Comparator comparator){ + this.elements.sort(comparator); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + if(!reader.isAvailable()){ + return; + } + numberBlock.readBytes(reader); + int totalSize = (int) getDataSize(); + if(totalSize <= 0){ + return; + } + BlockReader chunkReader = reader.create(totalSize); + readElements(chunkReader); + bottomContainer.readBytes(chunkReader); + reader.offset(totalSize); + } + private void readElements(BlockReader reader) throws IOException{ + int preserve = bottomContainer.countBytes() + 4; + while (reader.available() > preserve){ + int position = reader.getPosition(); + T element = newInstance(); + element = add(element); + element.readBytes(reader); + if(position == reader.getPosition()){ + break; + } + } + } + @Override + protected void onRefreshed(){ + int size = countBytes() - numberBlock.countBytes(); + setDataSize(size); + } + public Block getBottomBlock(){ + return bottomContainer.getItem(); + } + public void setBottomBlock(Block block){ + bottomContainer.setItem(block); + } + + + @Override + public String toString(){ + return "size=" + numberBlock + ", count=" + getElementsCount(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/LocalFileHeader.java b/src/ARSCLib/com/reandroid/archive2/block/LocalFileHeader.java new file mode 100644 index 00000000..f7fa7c09 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/LocalFileHeader.java @@ -0,0 +1,93 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +import java.io.IOException; +import java.io.InputStream; + +public class LocalFileHeader extends CommonHeader { + private DataDescriptor dataDescriptor; + public LocalFileHeader(){ + super(OFFSET_fileName, ZipSignature.LOCAL_FILE, OFFSET_general_purpose); + } + public LocalFileHeader(String name){ + this(); + setFileName(name); + } + + public void mergeZeroValues(CentralEntryHeader ceh){ + if(getCrc()==0){ + setCrc(ceh.getCrc()); + } + if(getSize()==0){ + setSize(ceh.getSize()); + } + if(getCompressedSize()==0){ + setCompressedSize(ceh.getCompressedSize()); + } + if(getGeneralPurposeFlag().getValue()==0){ + getGeneralPurposeFlag().setValue(ceh.getGeneralPurposeFlag().getValue()); + } + } + + public DataDescriptor getDataDescriptor() { + return dataDescriptor; + } + public void setDataDescriptor(DataDescriptor dataDescriptor){ + this.dataDescriptor = dataDescriptor; + getGeneralPurposeFlag().setHasDataDescriptor(dataDescriptor!=null); + } + + public static LocalFileHeader fromCentralEntryHeader(CentralEntryHeader ceh){ + LocalFileHeader lfh = new LocalFileHeader(); + lfh.setSignature(ZipSignature.LOCAL_FILE); + lfh.setVersionMadeBy(ceh.getVersionMadeBy()); + lfh.getGeneralPurposeFlag().setValue(ceh.getGeneralPurposeFlag().getValue()); + lfh.setMethod(ceh.getMethod()); + lfh.setDosTime(ceh.getDosTime()); + lfh.setCrc(ceh.getCrc()); + lfh.setCompressedSize(ceh.getCompressedSize()); + lfh.setSize(ceh.getSize()); + lfh.setFileName(ceh.getFileName()); + lfh.setExtra(ceh.getExtra()); + return lfh; + } + + public static LocalFileHeader read(InputStream inputStream) throws IOException { + LocalFileHeader localFileHeader = new LocalFileHeader(); + localFileHeader.readBytes(inputStream); + if(localFileHeader.isValidSignature()){ + return localFileHeader; + } + return null; + } + private static final int OFFSET_signature = 0; + private static final int OFFSET_versionMadeBy = 4; + private static final int OFFSET_platform = 5; + private static final int OFFSET_general_purpose = 6; + private static final int OFFSET_method = 8; + private static final int OFFSET_dos_time = 10; + private static final int OFFSET_crc = 14; + private static final int OFFSET_compressed_size = 18; + private static final int OFFSET_size = 22; + private static final int OFFSET_fileNameLength = 26; + private static final int OFFSET_extraLength = 28; + + private static final int OFFSET_fileName = 30; + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/SignatureFooter.java b/src/ARSCLib/com/reandroid/archive2/block/SignatureFooter.java new file mode 100644 index 00000000..3594021c --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/SignatureFooter.java @@ -0,0 +1,74 @@ +/* + * 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.archive2.block; + +import com.reandroid.arsc.item.ByteArray; + +import java.io.IOException; +import java.io.InputStream; + +public class SignatureFooter extends ZipBlock{ + public SignatureFooter() { + super(MIN_SIZE); + setMagic(APK_SIG_BLOCK_MAGIC); + } + @Override + public int readBytes(InputStream inputStream) throws IOException { + setBytesLength(MIN_SIZE, false); + byte[] bytes = getBytesInternal(); + return inputStream.read(bytes, 0, bytes.length); + } + public long getSignatureSize(){ + return getLong(OFFSET_size); + } + public void setSignatureSize(long size){ + int minLength = MIN_SIZE; + if(countBytes() < minLength){ + setBytesLength(minLength, false); + } + putLong(OFFSET_size, size); + } + public byte[] getMagic() { + return getBytes(OFFSET_magic, APK_SIG_BLOCK_MAGIC.length, false); + } + public void setMagic(byte[] magic){ + if(magic == null){ + magic = new byte[0]; + } + int length = OFFSET_magic + magic.length; + setBytesLength(length, false); + putBytes(magic, 0, OFFSET_magic, magic.length); + } + public boolean isValid(){ + return getSignatureSize() > MIN_SIZE + && ByteArray.equals(APK_SIG_BLOCK_MAGIC, getMagic()); + } + public void updateMagic(){ + setMagic(APK_SIG_BLOCK_MAGIC); + } + @Override + public String toString(){ + return getSignatureSize() + " ["+new String(getMagic())+"]"; + } + + public static final int MIN_SIZE = 24; + + private static final int OFFSET_size = 0; + private static final int OFFSET_magic = 8; + + private static final byte[] APK_SIG_BLOCK_MAGIC = + new byte[]{'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2'}; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/SignatureId.java b/src/ARSCLib/com/reandroid/archive2/block/SignatureId.java new file mode 100644 index 00000000..33560ef7 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/SignatureId.java @@ -0,0 +1,116 @@ +/* + * 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.archive2.block; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.util.HexUtil; + +import java.util.Objects; + +public class SignatureId implements Comparable{ + private final String name; + private final int id; + private final int sort; + + private SignatureId(String name, int id, int sort) { + this.name = name; + this.id = id; + this.sort = sort; + } + public String name() { + return name; + } + public int getId() { + return id; + } + public String toFileName() { + if (this.name != null) { + return name + FILE_EXT_RAW; + } + return HexUtil.toHex8(id) + FILE_EXT_RAW; + } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SignatureId that = (SignatureId) obj; + return id == that.id; + } + @Override + public int compareTo(SignatureId signatureId) { + return Integer.compare(sort, signatureId.sort); + } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString() { + String name = this.name; + if (name != null) { + return name; + } + return "UNKNOWN(" + HexUtil.toHex8(id) + ")"; + } + public static SignatureId valueOf(String name) { + if (name == null) { + return null; + } + String ext = FILE_EXT_RAW; + if (name.endsWith(ext)) { + name = name.substring(0, name.length() - ext.length()); + } + for (SignatureId signatureId : VALUES) { + if (name.equalsIgnoreCase(signatureId.name())) { + return signatureId; + } + } + if (ValueDecoder.isHex(name)) { + return new SignatureId(null, ValueDecoder.parseHex(name), 99); + } + return null; + } + public static SignatureId valueOf(int id) { + for (SignatureId signatureId : VALUES) { + if (id == signatureId.getId()) { + return signatureId; + } + } + return new SignatureId(null, id, 99); + } + + public static SignatureId[] values() { + return VALUES.clone(); + } + + public static final SignatureId V2 = new SignatureId("V2", 0x7109871A, 0); + public static final SignatureId V3 = new SignatureId("V3", 0xF05368C0, 1); + public static final SignatureId V31 = new SignatureId("V31", 0x1B93AD61, 2); + public static final SignatureId STAMP_V1 = new SignatureId("STAMP_V1", 0x2B09189E, 3); + public static final SignatureId STAMP_V2 = new SignatureId("STAMP_V2", 0x6DFF800D, 4); + public static final SignatureId PADDING = new SignatureId("PADDING", 0x42726577, 9999); + public static final SignatureId NULL = new SignatureId("NULL", 0x0, 999); + + private static final SignatureId[] VALUES = new SignatureId[]{ + V2, V3, V31, STAMP_V1, STAMP_V2, PADDING, NULL + }; + + public static final String FILE_EXT_RAW = ".signature.info.bin"; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/SignatureInfo.java b/src/ARSCLib/com/reandroid/archive2/block/SignatureInfo.java new file mode 100644 index 00000000..94f59bac --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/SignatureInfo.java @@ -0,0 +1,114 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.block.pad.SchemePadding; +import com.reandroid.archive2.block.stamp.SchemeStampV1; +import com.reandroid.archive2.block.stamp.SchemeStampV2; +import com.reandroid.archive2.block.v2.SchemeV2; +import com.reandroid.archive2.block.v3.SchemeV3; +import com.reandroid.archive2.block.v3.SchemeV31; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.container.SingleBlockContainer; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.IntegerItem; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +public class SignatureInfo extends LengthPrefixedBlock implements BlockLoad { + private final IntegerItem idItem; + private final SingleBlockContainer schemeContainer; + public SignatureInfo() { + super(2, true); + this.idItem = new IntegerItem(); + this.schemeContainer = new SingleBlockContainer<>(); + addChild(this.idItem); + addChild(this.schemeContainer); + this.idItem.setBlockLoad(this); + } + public int getIdValue(){ + return idItem.get(); + } + public SignatureId getId(){ + return SignatureId.valueOf(getIdValue()); + } + public void setId(int id){ + idItem.set(id); + } + public void setId(SignatureId signatureId){ + setId(signatureId == null? 0 : signatureId.getId()); + } + public SignatureScheme getSignatureScheme(){ + return schemeContainer.getItem(); + } + public void setSignatureScheme(SignatureScheme signatureScheme){ + schemeContainer.setItem(signatureScheme); + } + + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender == this.idItem){ + onIdLoaded(); + } + } + private void onIdLoaded(){ + SignatureId signatureId = getId(); + SignatureScheme scheme; + if(signatureId == SignatureId.V2){ + scheme = new SchemeV2(); + }else if(signatureId == SignatureId.V3){ + scheme = new SchemeV3(); + }else if(signatureId == SignatureId.V31){ + scheme = new SchemeV31(); + }else if(signatureId == SignatureId.STAMP_V1){ + scheme = new SchemeStampV1(); + }else if(signatureId == SignatureId.STAMP_V2){ + scheme = new SchemeStampV2(); + }else if(signatureId == SignatureId.PADDING){ + scheme = new SchemePadding(); + }else { + scheme = new UnknownScheme(signatureId); + } + schemeContainer.setItem(scheme); + } + public void writeRaw(File file) throws IOException{ + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + writeBytes(outputStream); + outputStream.close(); + } + public File writeRawToDirectory(File dir) throws IOException{ + String name = getIndex() + "_" + getId().toFileName(); + File file = new File(dir, name); + writeRaw(file); + return file; + } + public void read(File file) throws IOException { + super.readBytes(new BlockReader(file)); + } + @Override + public String toString() { + return getId() + ", scheme: " + getSignatureScheme(); + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/SignatureScheme.java b/src/ARSCLib/com/reandroid/archive2/block/SignatureScheme.java new file mode 100644 index 00000000..555179a6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/SignatureScheme.java @@ -0,0 +1,37 @@ +/* + * 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.archive2.block; + +import com.reandroid.arsc.container.ExpandableBlockContainer; + +public class SignatureScheme extends ExpandableBlockContainer { + private final SignatureId signatureId; + public SignatureScheme(int childesCount, SignatureId signatureId){ + super(childesCount); + this.signatureId = signatureId; + } + public SignatureId getSignatureId() { + return signatureId; + } + + public SignatureInfo getSignatureInfo(){ + return getParent(SignatureInfo.class); + } + @Override + public String toString(){ + return "id=" + getSignatureId(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/UnknownScheme.java b/src/ARSCLib/com/reandroid/archive2/block/UnknownScheme.java new file mode 100644 index 00000000..172f8e45 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/UnknownScheme.java @@ -0,0 +1,39 @@ +/* + * 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.archive2.block; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.ByteArray; + +import java.io.IOException; + +// General purpose block to consume the specified bytes of BlockReader +// TODO: No class should override this, implement all like SchemeV2 +public class UnknownScheme extends SignatureScheme{ + private final ByteArray byteArray; + public UnknownScheme(SignatureId signatureId) { + super(1, signatureId); + this.byteArray = new ByteArray(); + addChild(byteArray); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + SignatureInfo signatureInfo = getSignatureInfo(); + int size = (int) signatureInfo.getDataSize() - 4; + byteArray.setSize(size); + super.onReadBytes(reader); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/ZipBlock.java b/src/ARSCLib/com/reandroid/archive2/block/ZipBlock.java new file mode 100644 index 00000000..8669aad0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/ZipBlock.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.archive2.block; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.BlockItem; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class ZipBlock extends BlockItem { + public ZipBlock(int bytesLength) { + super(bytesLength); + } + + public void putBytes(byte[] bytes, int offset, int putOffset, int length){ + if(length<=0 || bytes.length==0){ + return; + } + int size = putOffset + length; + setBytesLength(size, false); + System.arraycopy(bytes, offset, getBytesInternal(), putOffset, length); + } + @Override + public abstract int readBytes(InputStream inputStream) throws IOException; + + // should not use this method + @Override + public void onReadBytes(BlockReader blockReader) throws IOException { + this.readBytes((InputStream) blockReader); + } + + byte[] getBytes(int offset, int length, boolean strict){ + byte[] bytes = getBytesInternal(); + if(strict){ + if(offset<0 || offset>=bytes.length || (offset + length)>bytes.length){ + return null; + } + } + if(offset < 0){ + offset = 0; + } + int available = bytes.length - offset; + if(length<=0 || available <=0){ + return new byte[0]; + } + if(length > available){ + length = available; + } + byte[] result = new byte[length]; + System.arraycopy(getBytesInternal(), offset, result, 0, length); + return result; + } + long getLong(int offset){ + return getLong(getBytesInternal(), offset); + } + void putLong(int offset, long value){ + putLong(getBytesInternal(), offset, value); + } + long getIntegerUnsigned(int offset){ + return getInteger(offset) & 0x00000000ffffffffL; + } + void putBit(int offset, int bitIndex, boolean bit){ + putBit(getBytesInternal(), offset, bitIndex, bit); + } + boolean getBit(int offset, int bitIndex){ + return getBit(getBytesInternal(), offset, bitIndex); + } + int getByteUnsigned(int offset){ + return getBytesInternal()[offset] & 0xff; + } + int getShortUnsigned(int offset){ + return getShort(getBytesInternal(), offset) & 0xffff; + } + int getInteger(int offset){ + return getInteger(getBytesInternal(), offset); + } + void putInteger(int offset, int value){ + putInteger(getBytesInternal(), offset, value); + } + void putInteger(int offset, long value){ + putInteger(getBytesInternal(), offset, (int) value); + } + void putShort(int offset, int value){ + putShort(getBytesInternal(), offset, (short) value); + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/ZipHeader.java b/src/ARSCLib/com/reandroid/archive2/block/ZipHeader.java new file mode 100644 index 00000000..c6c89f29 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/ZipHeader.java @@ -0,0 +1,88 @@ +/* + * 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.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +import java.io.IOException; +import java.io.InputStream; + +public class ZipHeader extends ZipBlock{ + private final ZipSignature expectedSignature; + private final int minByteLength; + public ZipHeader(int minByteLength, ZipSignature expectedSignature) { + super(minByteLength); + this.minByteLength = minByteLength; + this.expectedSignature = expectedSignature; + } + @Override + public int readBytes(InputStream inputStream) throws IOException { + int read = readBasic(inputStream); + ZipSignature sig=getSignature(); + if(sig != getExpectedSignature()){ + return read; + } + read += readNext(inputStream); + return read; + } + private int readBasic(InputStream inputStream) throws IOException { + setBytesLength(getMinByteLength(), false); + byte[] bytes = getBytesInternal(); + int beginLength = bytes.length; + int read = inputStream.read(bytes, 0, beginLength); + if(read != beginLength){ + setBytesLength(read, false); + if(getSignature()==expectedSignature){ + setSignature(0); + } + return read; + } + return read; + } + int readNext(InputStream inputStream) throws IOException { + return 0; + } + public boolean isValidSignature(){ + return getSignature() == getExpectedSignature(); + } + ZipSignature getExpectedSignature(){ + return expectedSignature; + } + int getMinByteLength() { + return minByteLength; + } + + public ZipSignature getSignature(){ + return ZipSignature.valueOf(getSignatureValue()); + } + public int getSignatureValue(){ + if(countBytes()<4){ + return 0; + } + return getInteger(OFFSET_signature); + } + public void setSignature(int value){ + if(countBytes()<4){ + return; + } + putInteger(OFFSET_signature, value); + } + public void setSignature(ZipSignature signature){ + setSignature(signature == null ? 0:signature.getValue()); + } + + private static final int OFFSET_signature = 0; +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/ZipStringEncoding.java b/src/ARSCLib/com/reandroid/archive2/block/ZipStringEncoding.java new file mode 100644 index 00000000..44296af9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/ZipStringEncoding.java @@ -0,0 +1,221 @@ +/* + * 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.archive2.block; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + + +public class ZipStringEncoding { + + private static final char REPLACEMENT = '?'; + private static final byte[] REPLACEMENT_BYTES = { (byte) REPLACEMENT }; + private static final String REPLACEMENT_STRING = String.valueOf(REPLACEMENT); + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static ByteBuffer encodeFully(final CharsetEncoder enc, final CharBuffer cb, final ByteBuffer out) { + ByteBuffer o = out; + while (cb.hasRemaining()) { + final CoderResult result = enc.encode(cb, o, false); + if (result.isOverflow()) { + final int increment = estimateIncrementalEncodingSize(enc, cb.remaining()); + o = growBufferBy(o, increment); + } + } + return o; + } + private static CharBuffer encodeSurrogate(final CharBuffer cb, final char c) { + cb.position(0).limit(6); + cb.put('%'); + cb.put('U'); + + cb.put(HEX_CHARS[(c >> 12) & 0x0f]); + cb.put(HEX_CHARS[(c >> 8) & 0x0f]); + cb.put(HEX_CHARS[(c >> 4) & 0x0f]); + cb.put(HEX_CHARS[c & 0x0f]); + cb.flip(); + return cb; + } + + private static int estimateIncrementalEncodingSize(final CharsetEncoder enc, final int charCount) { + return (int) Math.ceil(charCount * enc.averageBytesPerChar()); + } + private static int estimateInitialBufferSize(final CharsetEncoder enc, final int charChount) { + final float first = enc.maxBytesPerChar(); + final float rest = (charChount - 1) * enc.averageBytesPerChar(); + return (int) Math.ceil(first + rest); + } + + private final Charset charset; + private final boolean useReplacement; + private final CharsetEncoder mEncoder; + private final CharsetDecoder mDecoder; + + ZipStringEncoding(final Charset charset, final boolean useReplacement) { + this.charset = charset; + this.useReplacement = useReplacement; + mEncoder = newEncoder(); + mDecoder = newDecoder(); + } + + public boolean canEncode(final String name) { + final CharsetEncoder enc = newEncoder(); + return enc.canEncode(name); + } + public String decode(byte[] data, int offset, int length) throws IOException { + return mDecoder.decode(ByteBuffer.wrap(data, offset, length)).toString(); + } + public byte[] encode(final String text) { + final CharsetEncoder enc = mEncoder; + + final CharBuffer cb = CharBuffer.wrap(text); + CharBuffer tmp = null; + ByteBuffer out = ByteBuffer.allocate(estimateInitialBufferSize(enc, cb.remaining())); + + while (cb.hasRemaining()) { + final CoderResult res = enc.encode(cb, out, false); + + if (res.isUnmappable() || res.isMalformed()) { + final int spaceForSurrogate = estimateIncrementalEncodingSize(enc, 6 * res.length()); + if (spaceForSurrogate > out.remaining()) { + int charCount = 0; + for (int i = cb.position() ; i < cb.limit(); i++) { + charCount += !enc.canEncode(cb.get(i)) ? 6 : 1; + } + final int totalExtraSpace = estimateIncrementalEncodingSize(enc, charCount); + out = growBufferBy(out, totalExtraSpace - out.remaining()); + } + if (tmp == null) { + tmp = CharBuffer.allocate(6); + } + for (int i = 0; i < res.length(); ++i) { + out = encodeFully(enc, encodeSurrogate(tmp, cb.get()), out); + } + + } else if (res.isOverflow()) { + final int increment = estimateIncrementalEncodingSize(enc, cb.remaining()); + out = growBufferBy(out, increment); + + } else if (res.isUnderflow() || res.isError()) { + break; + } + } + // tell the encoder we are done + enc.encode(cb, out, true); + // may have caused underflow, but that's been ignored traditionally + + out.limit(out.position()); + out.rewind(); + return out.array(); + } + + public Charset getCharset() { + return charset; + } + + private CharsetDecoder newDecoder() { + if (!useReplacement) { + return this.charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + return charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_STRING); + } + + private CharsetEncoder newEncoder() { + if (useReplacement) { + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_BYTES); + } + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + + private static final String UTF8 = UTF_8.name(); + + + private static ZipStringEncoding getZipEncoding(final String name) { + Charset cs = Charset.defaultCharset(); + if (name != null) { + try { + cs = Charset.forName(name); + } catch (final UnsupportedCharsetException e) { + } + } + final boolean useReplacement = isUTF8(cs.name()); + return new ZipStringEncoding(cs, useReplacement); + } + + static ByteBuffer growBufferBy(final ByteBuffer buffer, final int increment) { + buffer.limit(buffer.position()); + buffer.rewind(); + + final ByteBuffer on = ByteBuffer.allocate(buffer.capacity() + increment); + + on.put(buffer); + return on; + } + + private static boolean isUTF8(final String charsetName) { + final String actual = charsetName != null ? charsetName : Charset.defaultCharset().name(); + if (UTF_8.name().equalsIgnoreCase(actual)) { + return true; + } + return UTF_8.aliases().stream().anyMatch(alias -> alias.equalsIgnoreCase(actual)); + } + + public static String decode(boolean isUtf8, byte[] bytes, int offset, int length){ + if(isUtf8){ + return decodeUtf8(bytes, offset, length); + } + return decodeDefault(bytes, offset, length); + } + private static String decodeUtf8(byte[] bytes, int offset, int length){ + try { + return UTF8_ENCODING.decode(bytes, offset, length); + } catch (IOException exception) { + return new String(bytes, offset, length); + } + } + private static String decodeDefault(byte[] bytes, int offset, int length){ + return new String(bytes, offset, length); + } + public static byte[] encodeString(boolean isUtf8, String text){ + if(text==null || text.length()==0){ + return new byte[0]; + } + if(isUtf8){ + return UTF8_ENCODING.encode(text); + } + return DEFAULT_ENCODING.encode(text); + } + + private static final ZipStringEncoding UTF8_ENCODING = getZipEncoding(UTF8); + private static final ZipStringEncoding DEFAULT_ENCODING = getZipEncoding(Charset.defaultCharset().name()); + +} + diff --git a/src/ARSCLib/com/reandroid/archive2/block/pad/SchemePadding.java b/src/ARSCLib/com/reandroid/archive2/block/pad/SchemePadding.java new file mode 100644 index 00000000..f726ef33 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/pad/SchemePadding.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.archive2.block.pad; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.SignatureInfo; +import com.reandroid.archive2.block.SignatureScheme; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.ByteArray; + +import java.io.IOException; + +public class SchemePadding extends SignatureScheme { + private final ByteArray byteArray; + public SchemePadding() { + super(1, SignatureId.PADDING); + this.byteArray = new ByteArray(); + addChild(this.byteArray); + } + public int getPadding(){ + return byteArray.size(); + } + public void setPadding(int padding){ + byteArray.setSize(padding); + } + public byte[] getPaddingBytes() { + return byteArray.getBytes(); + } + public void setPadding(byte[] bytes){ + byteArray.set(bytes); + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + SignatureInfo signatureInfo = getSignatureInfo(); + int size = (int) signatureInfo.getDataSize() - 4; + byteArray.setSize(size); + super.onReadBytes(reader); + } + @Override + public String toString(){ + return "padding = " + getPadding(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV1.java b/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV1.java new file mode 100644 index 00000000..aa2b4f39 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV1.java @@ -0,0 +1,26 @@ +/* + * 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.archive2.block.stamp; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.UnknownScheme; + +// TODO: implement structure +public class SchemeStampV1 extends UnknownScheme { + public SchemeStampV1() { + super(SignatureId.STAMP_V1); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV2.java b/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV2.java new file mode 100644 index 00000000..ceb8504c --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/stamp/SchemeStampV2.java @@ -0,0 +1,26 @@ +/* + * 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.archive2.block.stamp; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.UnknownScheme; + +// TODO: implement structure +public class SchemeStampV2 extends UnknownScheme { + public SchemeStampV2() { + super(SignatureId.STAMP_V2); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v2/SchemeV2.java b/src/ARSCLib/com/reandroid/archive2/block/v2/SchemeV2.java new file mode 100644 index 00000000..c7e4c235 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v2/SchemeV2.java @@ -0,0 +1,29 @@ +/* + * 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.archive2.block.v2; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.SignatureScheme; + +public class SchemeV2 extends SignatureScheme { + private final V2SignedDataList signedDataList; + public SchemeV2(){ + super(1, SignatureId.V2); + this.signedDataList = new V2SignedDataList(); + addChild(this.signedDataList); + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signature.java b/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signature.java new file mode 100644 index 00000000..f73ee339 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signature.java @@ -0,0 +1,24 @@ +/* + * 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.archive2.block.v2; + +import com.reandroid.archive2.block.LengthPrefixedBytes; + +public class V2Signature extends LengthPrefixedBytes { + public V2Signature() { + super(false); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedData.java b/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedData.java new file mode 100644 index 00000000..8eb53f1f --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedData.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.archive2.block.v2; + +import com.reandroid.archive2.block.BottomBlock; +import com.reandroid.archive2.block.LengthPrefixedBlock; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; + +public class V2SignedData extends LengthPrefixedBlock { + private final V2Signer signer; + private final BottomBlock unknown; + public V2SignedData() { + super(2, false); + this.signer = new V2Signer(); + this.unknown = new BottomBlock(); + addChild(this.signer); + addChild(this.unknown); + } + public void onReadBytes(BlockReader reader) throws IOException { + super.onReadBytes(reader); + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedDataList.java b/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedDataList.java new file mode 100644 index 00000000..5767ea39 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v2/V2SignedDataList.java @@ -0,0 +1,29 @@ +/* + * 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.archive2.block.v2; + +import com.reandroid.archive2.block.LengthPrefixedList; + +public class V2SignedDataList extends LengthPrefixedList { + public V2SignedDataList() { + super(false); + } + + @Override + public V2SignedData newInstance() { + return new V2SignedData(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signer.java b/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signer.java new file mode 100644 index 00000000..cc99578b --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v2/V2Signer.java @@ -0,0 +1,51 @@ +/* + * 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.archive2.block.v2; + +import com.reandroid.archive2.block.BottomBlock; +import com.reandroid.archive2.block.CertificateBlock; +import com.reandroid.archive2.block.CertificateBlockList; +import com.reandroid.archive2.block.LengthPrefixedBlock; + +import java.util.List; + +public class V2Signer extends LengthPrefixedBlock { + private final V2Signature v2Signature; + private final CertificateBlockList certificateBlockList; + private final BottomBlock unknown; + public V2Signer() { + super(3, false); + this.v2Signature = new V2Signature(); + this.certificateBlockList = new CertificateBlockList(); + this.unknown = new BottomBlock(); + addChild(this.v2Signature); + addChild(this.certificateBlockList); + addChild(this.unknown); + } + public List getCertificateBlockList(){ + return certificateBlockList.getElements(); + } + public void addCertificateBlock(CertificateBlock certificateBlock){ + certificateBlockList.add(certificateBlock); + } + public void removeCertificateBlock(CertificateBlock certificateBlock){ + certificateBlockList.remove(certificateBlock); + } + @Override + public String toString(){ + return super.toString()+", sig="+v2Signature+", certs="+certificateBlockList; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV3.java b/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV3.java new file mode 100644 index 00000000..5b9c9d8e --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV3.java @@ -0,0 +1,26 @@ +/* + * 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.archive2.block.v3; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.UnknownScheme; + +// TODO: implement structure +public class SchemeV3 extends UnknownScheme { + public SchemeV3() { + super(SignatureId.V3); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV31.java b/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV31.java new file mode 100644 index 00000000..40433481 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/block/v3/SchemeV31.java @@ -0,0 +1,26 @@ +/* + * 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.archive2.block.v3; + +import com.reandroid.archive2.block.SignatureId; +import com.reandroid.archive2.block.UnknownScheme; + +// TODO: implement structure +public class SchemeV31 extends UnknownScheme { + public SchemeV31() { + super(SignatureId.V31); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ArchiveEntrySource.java b/src/ARSCLib/com/reandroid/archive2/io/ArchiveEntrySource.java new file mode 100644 index 00000000..cf4ece94 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ArchiveEntrySource.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.archive2.io; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.ArchiveEntry; +import com.reandroid.archive2.block.LocalFileHeader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; + +public class ArchiveEntrySource extends InputSource { + private final ZipInput zipInput; + private final ArchiveEntry archiveEntry; + public ArchiveEntrySource(ZipInput zipInput, ArchiveEntry archiveEntry){ + super(archiveEntry.getName()); + this.zipInput = zipInput; + this.archiveEntry = archiveEntry; + setMethod(archiveEntry.getMethod()); + } + + @Override + public byte[] getBytes(int length) throws IOException { + if(getMethod() != ZipEntry.STORED){ + return super.getBytes(length); + } + FileChannel fileChannel = getFileChannel(); + byte[] bytes = new byte[length]; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + fileChannel.read(byteBuffer); + return bytes; + } + + public FileChannel getFileChannel() throws IOException { + FileChannel fileChannel = getZipSource().getFileChannel(); + fileChannel.position(getFileOffset()); + return fileChannel; + } + public ZipInput getZipSource(){ + return zipInput; + } + public ArchiveEntry getArchiveEntry() { + return archiveEntry; + } + public long getFileOffset(){ + return getArchiveEntry().getFileOffset(); + } + @Override + public long getLength() throws IOException{ + return getArchiveEntry().getDataSize(); + } + @Override + public long getCrc() throws IOException{ + return getArchiveEntry().getCrc(); + } + @Override + public InputStream openStream() throws IOException { + ArchiveEntry archiveEntry = getArchiveEntry(); + LocalFileHeader lfh = archiveEntry.getLocalFileHeader(); + InputStream inputStream = getZipSource().getInputStream( + archiveEntry.getFileOffset(), archiveEntry.getDataSize()); + if(lfh.getSize() == lfh.getCompressedSize()){ + return inputStream; + } + return new InflaterInputStream(inputStream, + new Inflater(true), 512); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ArchiveUtil.java b/src/ARSCLib/com/reandroid/archive2/io/ArchiveUtil.java new file mode 100644 index 00000000..35f3477e --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ArchiveUtil.java @@ -0,0 +1,50 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ArchiveUtil { + + public static void writeAll(InputStream inputStream, OutputStream outputStream) throws IOException{ + int bufferLength = 1024 * 1000; + byte[] buffer = new byte[bufferLength]; + int read; + while ((read = inputStream.read(buffer, 0, bufferLength))>0){ + outputStream.write(buffer, 0, read); + } + } + public static void skip(InputStream inputStream, long amount) throws IOException { + if(amount==0){ + return; + } + int bufferLength = 1024*1024*100; + if(bufferLength>amount){ + bufferLength = (int) amount; + } + byte[] buffer = new byte[bufferLength]; + int read; + long remain = amount; + while (remain > 0 && (read = inputStream.read(buffer, 0, bufferLength))>0){ + remain = remain - read; + if(remain extends InputStream { + private final T inputStream; + private final CRC32 crc; + private long size; + private long mCheckSum; + private boolean mFinished; + public CountingInputStream(T inputStream, boolean disableCrc){ + this.inputStream = inputStream; + CRC32 crc32; + if(disableCrc){ + crc32 = null; + }else { + crc32 = new CRC32(); + } + this.crc = crc32; + } + public CountingInputStream(T inputStream){ + this(inputStream, false); + } + public T getInputStream() { + return inputStream; + } + public long getSize() { + return size; + } + public long getCrc() { + return mCheckSum; + } + @Override + public int read(byte[] bytes, int offset, int length) throws IOException{ + if(mFinished){ + return -1; + } + length = inputStream.read(bytes, offset, length); + if(length < 0){ + onFinished(); + return length; + } + this.size += length; + if(this.crc != null){ + this.crc.update(bytes, offset, length); + } + return length; + } + @Override + public int read(byte[] bytes) throws IOException{ + return this.read(bytes, 0, bytes.length); + } + @Override + public int read() throws IOException { + throw new IOException("Why one byte ?"); + } + @Override + public long skip(long amount) throws IOException { + if(mFinished){ + return 0; + } + if(amount <= 0){ + return amount; + } + InputStream inputStream = this.inputStream; + if(inputStream instanceof CountingInputStream){ + return inputStream.skip(amount); + } + long remaining = amount; + int len = 1024 * 1000; + if(remaining < len){ + len = (int) remaining; + } + final byte[] buffer = new byte[len]; + int read; + while (true){ + read = inputStream.read(buffer, 0, len); + if(read < 0){ + onFinished(); + break; + } + remaining = remaining - read; + if(remaining <= 0){ + break; + } + if(remaining < len){ + len = (int) remaining; + } + } + return amount - remaining; + } + @Override + public void close() throws IOException{ + if(!mFinished){ + onFinished(); + } + inputStream.close(); + } + private void onFinished(){ + this.mFinished = true; + if(this.crc!=null){ + this.mCheckSum = this.crc.getValue(); + } + } + @Override + public String toString(){ + if(!mFinished || crc==null){ + return "[" + size + "]: " + inputStream.getClass().getSimpleName(); + } + return "[size=" + size +", crc=" + HexUtil.toHex8(mCheckSum) + "]: " + inputStream.getClass().getSimpleName(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/CountingOutputStream.java b/src/ARSCLib/com/reandroid/archive2/io/CountingOutputStream.java new file mode 100644 index 00000000..0ae2a2b7 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/CountingOutputStream.java @@ -0,0 +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. + */ +package com.reandroid.archive2.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.CRC32; + +public class CountingOutputStream extends OutputStream { + private final T outputStream; + private CRC32 crc; + private long size; + public CountingOutputStream(T outputStream, boolean disableCrc){ + this.outputStream = outputStream; + CRC32 crc32; + if(disableCrc){ + crc32 = null; + }else { + crc32 = new CRC32(); + } + this.crc = crc32; + } + public CountingOutputStream(T outputStream){ + this(outputStream, false); + } + + public void disableCrc(boolean disableCrc) { + if(!disableCrc){ + if(crc == null){ + this.crc = new CRC32(); + } + }else{ + this.crc = null; + } + } + + public void reset(){ + this.crc = new CRC32(); + this.size = 0L; + } + public T getOutputStream() { + return outputStream; + } + public long getSize() { + return size; + } + public long getCrc() { + if(crc != null){ + return crc.getValue(); + } + return 0; + } + @Override + public void write(byte[] bytes, int offset, int length) throws IOException{ + if(length == 0){ + return; + } + outputStream.write(bytes, offset, length); + this.size += length; + if(this.crc != null){ + this.crc.update(bytes, offset, length); + } + } + @Override + public void write(byte[] bytes) throws IOException{ + this.write(bytes, 0, bytes.length); + } + @Override + public void write(int i) throws IOException { + this.write(new byte[]{(byte) i}, 0, 1); + } + @Override + public void close() throws IOException{ + outputStream.close(); + } + @Override + public void flush() throws IOException { + outputStream.flush(); + } + @Override + public String toString(){ + return "[" + size + "]: " + outputStream.getClass().getSimpleName(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/FileChannelOutputStream.java b/src/ARSCLib/com/reandroid/archive2/io/FileChannelOutputStream.java new file mode 100644 index 00000000..81411e24 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/FileChannelOutputStream.java @@ -0,0 +1,47 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +public class FileChannelOutputStream extends OutputStream { + private final FileChannel fileChannel; + public FileChannelOutputStream(FileChannel fileChannel){ + this.fileChannel = fileChannel; + } + @Override + public void write(byte[] bytes) throws IOException { + write(bytes, 0, bytes.length); + } + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + long position = fileChannel.position(); + length = fileChannel.write(ByteBuffer.wrap(bytes, offset, length)); + fileChannel.position(position + length); + } + @Override + public void write(int i) throws IOException { + byte b = (byte) (i & 0xff); + write(new byte[]{b}); + } + @Override + public void close(){ + + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/RandomStream.java b/src/ARSCLib/com/reandroid/archive2/io/RandomStream.java new file mode 100644 index 00000000..62a8e47d --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/RandomStream.java @@ -0,0 +1,30 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.FileChannel; + +public interface RandomStream extends Channel { + long position() throws IOException; + void position(long pos) throws IOException; + @Override + void close() throws IOException; + @Override + boolean isOpen(); + FileChannel getFileChannel() throws IOException; +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ReadOnlyStream.java b/src/ARSCLib/com/reandroid/archive2/io/ReadOnlyStream.java new file mode 100644 index 00000000..6b895472 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ReadOnlyStream.java @@ -0,0 +1,24 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.io.InputStream; + +public interface ReadOnlyStream extends RandomStream{ + long getLength() throws IOException; + InputStream getInputStream(long offset, long length) throws IOException; +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/SlicedInputStream.java b/src/ARSCLib/com/reandroid/archive2/io/SlicedInputStream.java new file mode 100644 index 00000000..c01d9af1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/SlicedInputStream.java @@ -0,0 +1,107 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.io.InputStream; + +public class SlicedInputStream extends InputStream{ + private final InputStream inputStream; + private final long mOffset; + private final long mLength; + private long mCount; + private boolean mFinished; + private boolean mStarted; + public SlicedInputStream(InputStream inputStream, long offset, long length){ + this.inputStream = inputStream; + this.mOffset = offset; + this.mLength = length; + } + @Override + public int read(byte[] bytes, int off, int len) throws IOException{ + if(mFinished){ + return -1; + } + checkStarted(); + long remain = mLength - mCount; + if(remain <= 0){ + onFinished(); + return -1; + } + boolean finishNext = false; + if(len > remain){ + len = (int) remain; + finishNext = true; + } + int read = inputStream.read(bytes, off, len); + mCount += read; + if(finishNext){ + onFinished(); + } + return read; + } + @Override + public int read(byte[] bytes) throws IOException{ + return this.read(bytes, 0, bytes.length); + } + @Override + public int read() throws IOException { + if(mFinished){ + return -1; + } + checkStarted(); + long remain = mLength - mCount; + if(remain <= 0){ + onFinished(); + return -1; + } + int result = inputStream.read(); + mCount = mCount + 1; + if(remain == 1){ + onFinished(); + } + return result; + } + @Override + public long skip(long n) throws IOException{ + checkStarted(); + long amount = inputStream.skip(n); + if(amount>0){ + mCount += amount; + } + return amount; + } + @Override + public void close() throws IOException { + onFinished(); + } + private void onFinished() throws IOException { + mFinished = true; + inputStream.close(); + } + private void checkStarted() throws IOException { + if(mStarted){ + return; + } + mStarted = true; + inputStream.skip(mOffset); + mCount = 0; + } + @Override + public String toString(){ + return "["+mOffset+","+mLength+"] "+mCount; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/WriteOnlyStream.java b/src/ARSCLib/com/reandroid/archive2/io/WriteOnlyStream.java new file mode 100644 index 00000000..f4f78f63 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/WriteOnlyStream.java @@ -0,0 +1,26 @@ +/* + * 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.archive2.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface WriteOnlyStream extends RandomStream{ + void write(ReadOnlyStream readStream, long length) throws IOException; + void write(InputStream inputStream) throws IOException; + OutputStream getOutputStream() throws IOException; +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ZipFileInput.java b/src/ARSCLib/com/reandroid/archive2/io/ZipFileInput.java new file mode 100644 index 00000000..4f9b3006 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ZipFileInput.java @@ -0,0 +1,124 @@ +/* + * 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.archive2.io; + +import com.reandroid.common.FileChannelInputStream; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; + +public class ZipFileInput extends ZipInput { + private final File file; + private FileChannel fileChannel; + private InputStream mCurrentInputStream; + public ZipFileInput(File file){ + this.file = file; + } + + public File getFile(){ + return file; + } + + @Override + public long position() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + return fileChannel.position(); + } + return 0; + } + @Override + public void position(long pos) throws IOException { + getFileChannel().position(pos); + } + @Override + public long getLength(){ + return this.file.length(); + } + @Override + public InputStream getInputStream(long offset, long length) throws IOException { + closeCurrentInputStream(); + FileChannel fileChannel = getFileChannel(); + fileChannel.position(offset); + mCurrentInputStream = new FileChannelInputStream(fileChannel, length); + return mCurrentInputStream; + } + + @Override + public byte[] getFooter(int minLength) throws IOException { + long position = getLength(); + if(minLength>position){ + minLength = (int) position; + } + position = position - minLength; + FileChannel fileChannel = getFileChannel(); + fileChannel.position(position); + ByteBuffer buffer = ByteBuffer.allocate(minLength); + fileChannel.read(buffer); + return buffer.array(); + } + @Override + public FileChannel getFileChannel() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + return fileChannel; + } + synchronized (this){ + fileChannel = FileChannel.open(this.file.toPath(), StandardOpenOption.READ); + this.fileChannel = fileChannel; + return fileChannel; + } + } + @Override + public void close() throws IOException { + closeCurrentInputStream(); + closeChannel(); + } + @Override + public boolean isOpen(){ + FileChannel fileChannel = this.fileChannel; + if(fileChannel == null){ + return false; + } + synchronized (this){ + return fileChannel.isOpen(); + } + } + private void closeChannel() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel == null){ + return; + } + synchronized (this){ + fileChannel.close(); + this.fileChannel = null; + } + } + private void closeCurrentInputStream() throws IOException { + InputStream current = this.mCurrentInputStream; + if(current == null){ + return; + } + current.close(); + mCurrentInputStream = null; + } + @Override + public String toString(){ + return "File: " + this.file; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ZipFileOutput.java b/src/ARSCLib/com/reandroid/archive2/io/ZipFileOutput.java new file mode 100644 index 00000000..56407955 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ZipFileOutput.java @@ -0,0 +1,127 @@ +/* + * 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.archive2.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; + +public class ZipFileOutput extends ZipOutput{ + private final File file; + private FileChannel fileChannel; + private FileChannelOutputStream outputStream; + public ZipFileOutput(File file) throws IOException { + initFile(file); + this.file = file; + } + public File getFile() { + return file; + } + public void write(FileChannel input, long length) throws IOException{ + FileChannel fileChannel = getFileChannel(); + long pos = fileChannel.position(); + length = fileChannel.transferFrom(input, pos, length); + fileChannel.position(pos + length); + } + + @Override + public long position() throws IOException { + return getFileChannel().position(); + } + @Override + public void position(long pos) throws IOException { + getFileChannel().position(pos); + } + @Override + public void close() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + fileChannel.close(); + } + } + @Override + public boolean isOpen() { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + return fileChannel.isOpen(); + } + return false; + } + @Override + public FileChannel getFileChannel() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + return fileChannel; + } + synchronized (this){ + fileChannel = FileChannel.open(this.file.toPath(), StandardOpenOption.WRITE); + this.fileChannel = fileChannel; + return fileChannel; + } + } + @Override + public void write(ReadOnlyStream readStream, long length) throws IOException { + FileChannel input = readStream.getFileChannel(); + if(input != null){ + write(input, length); + return; + } + write(readStream.getInputStream(readStream.position(), length)); + } + @Override + public void write(InputStream inputStream) throws IOException { + FileChannel fileChannel = getFileChannel(); + long pos = fileChannel.position(); + int bufferLength = 1024 * 1000 * 100; + byte[] buffer = new byte[bufferLength]; + long result = 0; + int read; + while ((read = inputStream.read(buffer, 0, bufferLength)) > 0){ + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, read); + fileChannel.write(byteBuffer); + result += read; + } + inputStream.close(); + fileChannel.position(pos + result); + } + @Override + public FileChannelOutputStream getOutputStream() throws IOException { + FileChannelOutputStream outputStream = this.outputStream; + if(outputStream == null){ + outputStream = new FileChannelOutputStream(getFileChannel()); + this.outputStream = outputStream; + } + return outputStream; + } + + + private static void initFile(File file) throws IOException{ + if(file.isDirectory()){ + throw new IOException("Not file: " + file); + } + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + if(file.exists()){ + file.delete(); + } + file.createNewFile(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ZipInput.java b/src/ARSCLib/com/reandroid/archive2/io/ZipInput.java new file mode 100644 index 00000000..080501d4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ZipInput.java @@ -0,0 +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. + */ +package com.reandroid.archive2.io; + +import java.io.IOException; + +public abstract class ZipInput implements ReadOnlyStream { + public abstract byte[] getFooter(int minLength) throws IOException; +} diff --git a/src/ARSCLib/com/reandroid/archive2/io/ZipOutput.java b/src/ARSCLib/com/reandroid/archive2/io/ZipOutput.java new file mode 100644 index 00000000..b925a9ba --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/io/ZipOutput.java @@ -0,0 +1,20 @@ +/* + * 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.archive2.io; + +public abstract class ZipOutput implements WriteOnlyStream{ + +} diff --git a/src/ARSCLib/com/reandroid/archive2/model/CentralFileDirectory.java b/src/ARSCLib/com/reandroid/archive2/model/CentralFileDirectory.java new file mode 100644 index 00000000..e309ba4d --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/model/CentralFileDirectory.java @@ -0,0 +1,133 @@ +/* + * 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.archive2.model; + +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.EndRecord; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.archive2.block.SignatureFooter; +import com.reandroid.archive2.io.ZipInput; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class CentralFileDirectory { + private final List headerList; + private EndRecord endRecord; + private SignatureFooter signatureFooter; + public CentralFileDirectory(){ + this.headerList = new ArrayList<>(); + } + public CentralEntryHeader get(LocalFileHeader lfh){ + String name = lfh.getFileName(); + CentralEntryHeader ceh = get(lfh.getIndex()); + if(ceh!=null && Objects.equals(ceh.getFileName() , name)){ + return ceh; + } + return get(name); + } + public CentralEntryHeader get(String name){ + if(name == null){ + name = ""; + } + for(CentralEntryHeader ceh:getHeaderList()){ + if(name.equals(ceh.getFileName())){ + return ceh; + } + } + return null; + } + public CentralEntryHeader get(int i){ + if(i<0 || i>=headerList.size()){ + return null; + } + return headerList.get(i); + } + public int count(){ + return headerList.size(); + } + public List getHeaderList() { + return headerList; + } + + public SignatureFooter getSignatureFooter() { + return signatureFooter; + } + public EndRecord getEndRecord() { + return endRecord; + } + public void visit(ZipInput zipInput) throws IOException { + byte[] footer = zipInput.getFooter(SignatureFooter.MIN_SIZE + EndRecord.MAX_LENGTH); + EndRecord endRecord = findEndRecord(footer); + int length = (int) endRecord.getLengthOfCentralDirectory(); + int endLength = endRecord.countBytes(); + if(footer.length < (length + endLength)){ + footer = zipInput.getFooter(SignatureFooter.MIN_SIZE + length + endLength); + } + int offset = footer.length - length - endLength; + this.endRecord = endRecord; + loadCentralFileHeaders(footer, offset, length); + this.signatureFooter = tryFindSignatureFooter(footer, endRecord); + } + private void loadCentralFileHeaders(byte[] footer, int offset, int length) throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(footer, offset, length); + loadCentralFileHeaders(inputStream); + } + private void loadCentralFileHeaders(InputStream inputStream) throws IOException { + List headerList = this.headerList; + CentralEntryHeader ceh = new CentralEntryHeader(); + ceh.readBytes(inputStream); + while (ceh.isValidSignature()){ + headerList.add(ceh); + ceh = new CentralEntryHeader(); + ceh.readBytes(inputStream); + } + inputStream.close(); + } + private EndRecord findEndRecord(byte[] footer) throws IOException{ + int length = footer.length; + int minLength = EndRecord.MIN_LENGTH; + int start = length - minLength; + for(int offset=start; offset>=0; offset--){ + EndRecord endRecord = new EndRecord(); + endRecord.putBytes(footer, offset, 0, minLength); + if(endRecord.isValidSignature()){ + return endRecord; + } + } + throw new IOException("Failed to find end record"); + } + private SignatureFooter tryFindSignatureFooter(byte[] footer, EndRecord endRecord) throws IOException { + int lenCd = (int) endRecord.getLengthOfCentralDirectory(); + int endLength = endRecord.countBytes(); + int length = SignatureFooter.MIN_SIZE; + int offset = footer.length - endLength - lenCd - length; + if(offset < 0){ + return null; + } + ByteArrayInputStream inputStream = new ByteArrayInputStream(footer, offset, length); + SignatureFooter signatureFooter = new SignatureFooter(); + signatureFooter.readBytes(inputStream); + if(signatureFooter.isValid()){ + return signatureFooter; + } + return null; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/model/LocalFileDirectory.java b/src/ARSCLib/com/reandroid/archive2/model/LocalFileDirectory.java new file mode 100644 index 00000000..b359603a --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/model/LocalFileDirectory.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.archive2.model; + +import com.reandroid.archive2.block.*; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.archive2.io.ZipInput; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class LocalFileDirectory { + private final CentralFileDirectory centralFileDirectory; + private final List headerList; + private ApkSignatureBlock apkSignatureBlock; + public LocalFileDirectory(CentralFileDirectory centralFileDirectory){ + this.centralFileDirectory = centralFileDirectory; + this.headerList = new ArrayList<>(); + } + public LocalFileDirectory(){ + this(new CentralFileDirectory()); + } + public void visit(ZipInput zipInput) throws IOException { + getCentralFileDirectory().visit(zipInput); + visitLocalFile(zipInput); + visitApkSigBlock(zipInput); + } + private void visitLocalFile(ZipInput zipInput) throws IOException { + List headerList = this.getHeaderList(); + long offset; + int index = 0; + CentralFileDirectory centralFileDirectory = getCentralFileDirectory(); + long length = zipInput.getLength(); + InputStream inputStream = zipInput.getInputStream(0, length); + for(CentralEntryHeader ceh : centralFileDirectory.getHeaderList()){ + offset = ceh.getLocalRelativeOffset(); + inputStream.reset(); + offset = inputStream.skip(offset); + LocalFileHeader lfh = LocalFileHeader.read(inputStream); + if(lfh == null){ + throw new IOException("Error reading LFH at " + + offset + ", for CEH = " + ceh.getFileName()); + } + offset = offset + lfh.countBytes(); + lfh.setFileOffset(offset); + ceh.setFileOffset(offset); + lfh.mergeZeroValues(ceh); + inputStream.skip(lfh.getDataSize()); + DataDescriptor dataDescriptor = null; + if(lfh.hasDataDescriptor()){ + dataDescriptor = new DataDescriptor(); + int read = dataDescriptor.readBytes(inputStream); + if(read != dataDescriptor.countBytes()){ + dataDescriptor = null; + } + } + lfh.setDataDescriptor(dataDescriptor); + lfh.setIndex(index); + headerList.add(lfh); + index++; + } + } + private void visitApkSigBlock(ZipInput zipInput) throws IOException{ + CentralFileDirectory cfd = getCentralFileDirectory(); + SignatureFooter footer = cfd.getSignatureFooter(); + if(footer == null || !footer.isValid()){ + return; + } + EndRecord endRecord = cfd.getEndRecord(); + long length = footer.getSignatureSize() + 8; + long offset = endRecord.getOffsetOfCentralDirectory() - length; + ApkSignatureBlock apkSignatureBlock = new ApkSignatureBlock(footer); + apkSignatureBlock.readBytes(new BlockReader(zipInput.getInputStream(offset, length))); + this.apkSignatureBlock = apkSignatureBlock; + } + public ApkSignatureBlock getApkSigBlock() { + return apkSignatureBlock; + } + public CentralFileDirectory getCentralFileDirectory() { + return centralFileDirectory; + } + public List getHeaderList() { + return headerList; + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/ApkWriter.java b/src/ARSCLib/com/reandroid/archive2/writer/ApkWriter.java new file mode 100644 index 00000000..79adfd3d --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/ApkWriter.java @@ -0,0 +1,212 @@ +/* + * 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.archive2.writer; + +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.RenamedInputSource; +import com.reandroid.archive.InputSource; +import com.reandroid.archive.WriteProgress; +import com.reandroid.archive2.ZipSignature; +import com.reandroid.archive2.block.ApkSignatureBlock; +import com.reandroid.archive2.block.EndRecord; +import com.reandroid.archive2.io.ArchiveEntrySource; +import com.reandroid.archive2.io.ZipFileOutput; +import com.reandroid.arsc.chunk.TableBlock; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ApkWriter extends ZipFileOutput { + private final Object mLock = new Object(); + private final Collection sourceList; + private ZipAligner zipAligner; + private ApkSignatureBlock apkSignatureBlock; + private APKLogger apkLogger; + private WriteProgress writeProgress; + + public ApkWriter(File file, Collection sourceList) throws IOException { + super(file); + this.sourceList = sourceList; + this.zipAligner = ZipAligner.apkAligner(); + } + public void write()throws IOException { + synchronized (mLock){ + List outputList = buildOutputEntry(); + logMessage("Buffering compress changed files ..."); + BufferFileInput buffer = writeBuffer(outputList); + buffer.unlock(); + align(outputList); + writeApk(outputList); + buffer.close(); + + writeSignatureBlock(); + + writeCEH(outputList); + this.close(); + logMessage("Written to: " + getFile().getName()); + } + } + public void setApkSignatureBlock(ApkSignatureBlock apkSignatureBlock) { + this.apkSignatureBlock = apkSignatureBlock; + } + public ZipAligner getZipAligner() { + return zipAligner; + } + public void setZipAligner(ZipAligner zipAligner) { + this.zipAligner = zipAligner; + } + + private void writeCEH(List outputList) throws IOException{ + logMessage("Writing CEH ..."); + EndRecord endRecord = new EndRecord(); + endRecord.setSignature(ZipSignature.END_RECORD); + long offset = position(); + endRecord.setOffsetOfCentralDirectory((int) offset); + endRecord.setNumberOfDirectories(outputList.size()); + endRecord.setTotalNumberOfDirectories(outputList.size()); + for(OutputSource outputSource:outputList){ + outputSource.writeCEH(this); + } + long len = position() - offset; + endRecord.setLengthOfCentralDirectory(len); + endRecord.writeBytes(getOutputStream()); + } + private void writeApk(List outputList) throws IOException{ + logMessage("Writing files: " + outputList.size()); + for(OutputSource outputSource:outputList){ + outputSource.writeApk( this); + } + } + private void writeSignatureBlock() throws IOException { + ApkSignatureBlock signatureBlock = this.apkSignatureBlock; + if(signatureBlock == null){ + return; + } + logMessage("Writing signature block ..."); + long offset = position(); + int alignment = 4096; + int filesPadding = (int) ((alignment - (offset % alignment)) % alignment); + OutputStream outputStream = getOutputStream(); + if(filesPadding > 0){ + outputStream.write(new byte[filesPadding]); + } + logMessage("files padding = " + filesPadding); + signatureBlock.updatePadding(); + signatureBlock.writeBytes(outputStream); + } + private BufferFileInput writeBuffer(List outputList) throws IOException { + File bufferFile = getBufferFile(); + BufferFileOutput output = new BufferFileOutput(bufferFile); + BufferFileInput input = new BufferFileInput(bufferFile); + OutputSource tableSource = null; + for(OutputSource outputSource:outputList){ + InputSource inputSource = outputSource.getInputSource(); + if(tableSource == null && TableBlock.FILE_NAME.equals(inputSource.getAlias())){ + tableSource = outputSource; + continue; + } + onCompressFileProgress(inputSource.getAlias(), + inputSource.getMethod(), + output.position()); + outputSource.makeBuffer(input, output); + } + if(tableSource != null){ + tableSource.makeBuffer(input, output); + } + output.close(); + return input; + } + private void align(List outputList){ + ZipAligner aligner = getZipAligner(); + if(aligner!=null){ + aligner.reset(); + logMessage("Zip align ..."); + } + for(OutputSource outputSource:outputList){ + outputSource.align(aligner); + } + } + private File getBufferFile(){ + File file = getFile(); + File dir = file.getParentFile(); + String name = file.getAbsolutePath(); + name = "tmp" + name.hashCode(); + File bufFile; + if(dir != null){ + bufFile = new File(dir, name); + }else { + bufFile = new File(name); + } + bufFile.deleteOnExit(); + return bufFile; + } + private List buildOutputEntry(){ + Collection sourceList = this.sourceList; + List results = new ArrayList<>(sourceList.size()); + for(InputSource inputSource:sourceList){ + results.add(toOutputSource(inputSource)); + } + return results; + } + private OutputSource toOutputSource(InputSource inputSource){ + if(inputSource instanceof ArchiveEntrySource){ + return new ArchiveOutputSource(inputSource); + } + if(inputSource instanceof RenamedInputSource){ + InputSource renamed = ((RenamedInputSource) inputSource).getInputSource(); + if(renamed instanceof ArchiveEntrySource){ + return new RenamedArchiveSource((RenamedInputSource) inputSource); + } + } + return new OutputSource(inputSource); + } + + public void setWriteProgress(WriteProgress writeProgress){ + this.writeProgress = writeProgress; + } + private void onCompressFileProgress(String path, int mode, long writtenBytes) { + if(writeProgress!=null){ + writeProgress.onCompressFile(path, mode, writtenBytes); + } + } + + APKLogger getApkLogger(){ + return apkLogger; + } + public void setAPKLogger(APKLogger logger) { + this.apkLogger = logger; + } + private void logMessage(String msg) { + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + private void logError(String msg, Throwable tr) { + if(apkLogger!=null){ + apkLogger.logError(msg, tr); + } + } + private void logVerbose(String msg) { + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/ArchiveOutputSource.java b/src/ARSCLib/com/reandroid/archive2/writer/ArchiveOutputSource.java new file mode 100644 index 00000000..b6edaf22 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/ArchiveOutputSource.java @@ -0,0 +1,53 @@ +/* + * 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.archive2.writer; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.archive2.io.ArchiveEntrySource; +import com.reandroid.archive2.io.ZipFileInput; +import com.reandroid.archive2.io.ZipInput; + + +public class ArchiveOutputSource extends OutputSource{ + public ArchiveOutputSource(InputSource inputSource){ + super(inputSource); + } + + ArchiveEntrySource getArchiveSource(){ + return (ArchiveEntrySource) super.getInputSource(); + } + @Override + EntryBuffer makeFromEntry(){ + ArchiveEntrySource entrySource = getArchiveSource(); + ZipInput zip = entrySource.getZipSource(); + if(!(zip instanceof ZipFileInput)){ + return null; + } + LocalFileHeader lfh = entrySource.getArchiveEntry().getLocalFileHeader(); + if(lfh.getMethod() != getInputSource().getMethod()){ + return null; + } + return new EntryBuffer((ZipFileInput) zip, + lfh.getFileOffset(), + lfh.getDataSize()); + } + @Override + public LocalFileHeader createLocalFileHeader(){ + ArchiveEntrySource source = getArchiveSource(); + return source.getArchiveEntry().getLocalFileHeader(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/BufferFileInput.java b/src/ARSCLib/com/reandroid/archive2/writer/BufferFileInput.java new file mode 100644 index 00000000..61ca375f --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/BufferFileInput.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.archive2.writer; + +import com.reandroid.archive2.io.ZipFileInput; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; + +public class BufferFileInput extends ZipFileInput { + private boolean unlocked; + public BufferFileInput(File file){ + super(file); + } + + public void unlock(){ + this.unlocked = true; + } + + @Override + public FileChannel getFileChannel() throws IOException { + if(unlocked){ + return super.getFileChannel(); + } + throw new IOException("File locked!"); + } + @Override + public void close() throws IOException { + super.close(); + if(unlocked){ + File file = super.getFile(); + if(file.isFile()){ + file.delete(); + } + unlocked = false; + } + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/BufferFileOutput.java b/src/ARSCLib/com/reandroid/archive2/writer/BufferFileOutput.java new file mode 100644 index 00000000..8eae2b4b --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/BufferFileOutput.java @@ -0,0 +1,27 @@ +/* + * 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.archive2.writer; + +import com.reandroid.archive2.io.ZipFileOutput; + +import java.io.File; +import java.io.IOException; + +public class BufferFileOutput extends ZipFileOutput { + public BufferFileOutput(File file) throws IOException { + super(file); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/EntryBuffer.java b/src/ARSCLib/com/reandroid/archive2/writer/EntryBuffer.java new file mode 100644 index 00000000..59b6670b --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/EntryBuffer.java @@ -0,0 +1,40 @@ +/* + * 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.archive2.writer; + +import com.reandroid.archive2.io.ZipFileInput; + +public class EntryBuffer { + private final ZipFileInput zipFileInput; + private final long offset; + private final long length; + public EntryBuffer(ZipFileInput zipFileInput, long offset, long length){ + this.zipFileInput = zipFileInput; + this.offset = offset; + this.length = length; + } + + public ZipFileInput getZipFileInput() { + return zipFileInput; + } + public long getOffset() { + return offset; + } + public long getLength() { + return length; + } + +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/OutputSource.java b/src/ARSCLib/com/reandroid/archive2/writer/OutputSource.java new file mode 100644 index 00000000..f2b3efae --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/OutputSource.java @@ -0,0 +1,162 @@ +/* + * 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.archive2.writer; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.ZipSignature; +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.DataDescriptor; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.archive2.io.CountingOutputStream; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.ZipEntry; + +class OutputSource { + private final InputSource inputSource; + private LocalFileHeader lfh; + private EntryBuffer entryBuffer; + + OutputSource(InputSource inputSource){ + this.inputSource = inputSource; + } + void align(ZipAligner aligner){ + LocalFileHeader lfh = getLocalFileHeader(); + if(aligner == null){ + lfh.setExtra(null); + }else { + aligner.align(getInputSource(), lfh); + } + } + void makeBuffer(BufferFileInput input, BufferFileOutput output) throws IOException { + EntryBuffer entryBuffer = this.entryBuffer; + if(entryBuffer != null){ + return; + } + entryBuffer = makeFromEntry(); + if(entryBuffer != null){ + this.entryBuffer = entryBuffer; + return; + } + this.entryBuffer = writeBuffer(input, output); + } + private EntryBuffer writeBuffer(BufferFileInput input, BufferFileOutput output) throws IOException { + long offset = output.position(); + writeBufferFile(output); + long length = output.position() - offset; + return new EntryBuffer(input, offset, length); + } + EntryBuffer makeFromEntry(){ + return null; + } + + void writeApk(ApkWriter apkWriter) throws IOException{ + EntryBuffer entryBuffer = this.entryBuffer; + FileChannel input = entryBuffer.getZipFileInput().getFileChannel(); + input.position(entryBuffer.getOffset()); + LocalFileHeader lfh = getLocalFileHeader(); + writeLFH(lfh, apkWriter); + writeData(input, entryBuffer.getLength(), apkWriter); + writeDD(lfh.getDataDescriptor(), apkWriter); + } + void writeCEH(ApkWriter apkWriter) throws IOException{ + LocalFileHeader lfh = getLocalFileHeader(); + CentralEntryHeader ceh = CentralEntryHeader.fromLocalFileHeader(lfh); + ceh.writeBytes(apkWriter.getOutputStream()); + } + private void writeLFH(LocalFileHeader lfh, ApkWriter apkWriter) throws IOException{ + lfh.writeBytes(apkWriter.getOutputStream()); + } + private void writeData(FileChannel input, long length, ApkWriter apkWriter) throws IOException{ + long offset = apkWriter.position(); + LocalFileHeader lfh = getLocalFileHeader(); + lfh.setFileOffset(offset); + apkWriter.write(input, length); + } + void writeDD(DataDescriptor dataDescriptor, ApkWriter apkWriter) throws IOException{ + if(dataDescriptor == null){ + return; + } + dataDescriptor.writeBytes(apkWriter.getOutputStream()); + } + private void writeBufferFile(BufferFileOutput output) throws IOException { + LocalFileHeader lfh = getLocalFileHeader(); + + InputSource inputSource = getInputSource(); + OutputStream rawStream = output.getOutputStream(); + + CountingOutputStream rawCounter = new CountingOutputStream<>(rawStream); + CountingOutputStream deflateCounter = null; + + if(inputSource.getMethod() != ZipEntry.STORED){ + DeflaterOutputStream deflaterInputStream = + new DeflaterOutputStream(rawCounter, new Deflater(Deflater.BEST_SPEED, true), true); + deflateCounter = new CountingOutputStream<>(deflaterInputStream, false); + } + if(deflateCounter != null){ + rawCounter.disableCrc(true); + inputSource.write(deflateCounter); + deflateCounter.close(); + rawCounter.close(); + }else { + inputSource.write(rawCounter); + } + + lfh.setCompressedSize(rawCounter.getSize()); + + if(deflateCounter != null){ + lfh.setMethod(ZipEntry.DEFLATED); + lfh.setCrc(deflateCounter.getCrc()); + lfh.setSize(deflateCounter.getSize()); + }else { + lfh.setSize(rawCounter.getSize()); + lfh.setMethod(ZipEntry.STORED); + lfh.setCrc(rawCounter.getCrc()); + } + + inputSource.disposeInputSource(); + } + + InputSource getInputSource() { + return inputSource; + } + LocalFileHeader getLocalFileHeader(){ + if(lfh == null){ + lfh = createLocalFileHeader(); + lfh.setFileName(getInputSource().getAlias()); + clearAlignment(lfh); + } + return lfh; + } + LocalFileHeader createLocalFileHeader(){ + InputSource inputSource = getInputSource(); + LocalFileHeader lfh = new LocalFileHeader(); + lfh.setSignature(ZipSignature.LOCAL_FILE); + lfh.getGeneralPurposeFlag().initDefault(); + lfh.setFileName(inputSource.getAlias()); + lfh.setMethod(inputSource.getMethod()); + return lfh; + } + private void clearAlignment(LocalFileHeader lfh){ + lfh.getGeneralPurposeFlag().setHasDataDescriptor(false); + lfh.setDataDescriptor(null); + lfh.setExtra(null); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/RenamedArchiveSource.java b/src/ARSCLib/com/reandroid/archive2/writer/RenamedArchiveSource.java new file mode 100644 index 00000000..1db6b0fc --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/RenamedArchiveSource.java @@ -0,0 +1,30 @@ +/* + * 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.archive2.writer; + +import com.reandroid.apk.RenamedInputSource; +import com.reandroid.archive2.io.ArchiveEntrySource; + +public class RenamedArchiveSource extends ArchiveOutputSource{ + public RenamedArchiveSource(RenamedInputSource inputSource) { + super(inputSource); + } + @Override + ArchiveEntrySource getArchiveSource(){ + return (ArchiveEntrySource) + ((RenamedInputSource)super.getInputSource()).getInputSource(); + } +} diff --git a/src/ARSCLib/com/reandroid/archive2/writer/ZipAligner.java b/src/ARSCLib/com/reandroid/archive2/writer/ZipAligner.java new file mode 100644 index 00000000..65fa12a1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/archive2/writer/ZipAligner.java @@ -0,0 +1,111 @@ +/* + * 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.archive2.writer; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive2.block.DataDescriptor; +import com.reandroid.archive2.block.LocalFileHeader; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; + +public class ZipAligner { + private final Map alignmentMap; + private int defaultAlignment; + private boolean enableDataDescriptor; + private long mCurrentOffset; + + public ZipAligner(){ + alignmentMap = new HashMap<>(); + } + + public void setFileAlignment(Pattern patternFileName, int alignment){ + if(patternFileName == null){ + return; + } + alignmentMap.remove(patternFileName); + if(alignment > 1){ + alignmentMap.put(patternFileName, alignment); + } + } + public void clearFileAlignment(){ + alignmentMap.clear(); + } + public void setDefaultAlignment(int defaultAlignment) { + if(defaultAlignment <= 0){ + defaultAlignment = 1; + } + this.defaultAlignment = defaultAlignment; + } + public void setEnableDataDescriptor(boolean enableDataDescriptor) { + this.enableDataDescriptor = enableDataDescriptor; + } + + void reset(){ + mCurrentOffset = 0; + } + void align(InputSource inputSource, LocalFileHeader lfh){ + lfh.setExtra(null); + int padding; + if(inputSource.getMethod() != ZipEntry.STORED){ + padding = 0; + createDataDescriptor(lfh); + }else { + int alignment = getAlignment(inputSource.getAlias()); + long dataOffset = mCurrentOffset + lfh.countBytes(); + padding = (int) ((alignment - (dataOffset % alignment)) % alignment); + } + lfh.setExtra(new byte[padding]); + mCurrentOffset += lfh.getDataSize() + lfh.countBytes(); + DataDescriptor dataDescriptor = lfh.getDataDescriptor(); + if(dataDescriptor!=null){ + mCurrentOffset += dataDescriptor.countBytes(); + } + } + private void createDataDescriptor(LocalFileHeader lfh){ + DataDescriptor dataDescriptor; + if(enableDataDescriptor){ + dataDescriptor = DataDescriptor.fromLocalFile(lfh); + }else { + dataDescriptor = null; + } + lfh.setDataDescriptor(dataDescriptor); + } + private int getAlignment(String name){ + for(Map.Entry entry:alignmentMap.entrySet()){ + Matcher matcher = entry.getKey().matcher(name); + if(matcher.matches()){ + return entry.getValue(); + } + } + return defaultAlignment; + } + + public static ZipAligner apkAligner(){ + ZipAligner zipAligner = new ZipAligner(); + zipAligner.setDefaultAlignment(ALIGNMENT_4); + Pattern patternNativeLib = Pattern.compile("^lib/.+\\.so$"); + zipAligner.setFileAlignment(patternNativeLib, ALIGNMENT_PAGE); + zipAligner.setEnableDataDescriptor(true); + return zipAligner; + } + + private static final int ALIGNMENT_4 = 4; + private static final int ALIGNMENT_PAGE = 4096; +} diff --git a/src/ARSCLib/com/reandroid/arsc/ApkFile.java b/src/ARSCLib/com/reandroid/arsc/ApkFile.java new file mode 100644 index 00000000..afb664d9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/ApkFile.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.arsc; + +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.decoder.Decoder; + +import java.io.IOException; + +public interface ApkFile { + AndroidManifestBlock getAndroidManifestBlock(); + TableBlock getTableBlock(); + ResXmlDocument loadResXmlDocument(String path) throws IOException; + Decoder getDecoder(); + void setDecoder(Decoder decoder); + + enum ApkType { + BASE, + SPLIT, + CORE, + UNKNOWN + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/BuildInfo.java b/src/ARSCLib/com/reandroid/arsc/BuildInfo.java new file mode 100755 index 00000000..1b1aa7b9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/BuildInfo.java @@ -0,0 +1,58 @@ + /* + * 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; + +import java.io.InputStream; +import java.util.Properties; + +public class BuildInfo { + private static Properties sProperties; + + public static String getName(){ + Properties properties=getProperties(); + return properties.getProperty("lib.name", "ARSCLib"); + } + public static String getVersion(){ + Properties properties=getProperties(); + return properties.getProperty("lib.version", ""); + } + public static String getRepo(){ + Properties properties=getProperties(); + return properties.getProperty("lib.repo", "https://github.com/REAndroid"); + } + public static String getDescription(){ + Properties properties=getProperties(); + return properties.getProperty("lib.description", "Failed to load properties"); + } + + private static Properties getProperties(){ + if(sProperties==null){ + sProperties=loadProperties(); + } + return sProperties; + } + private static Properties loadProperties(){ + InputStream inputStream=BuildInfo.class.getResourceAsStream("/arsclib.properties"); + Properties properties=new Properties(); + try{ + properties.load(inputStream); + }catch (Exception ignored){ + } + return properties; + } + + public static final String NAME_arsc_lib_version="arsc_lib_version"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/CompoundItemArray.java b/src/ARSCLib/com/reandroid/arsc/array/CompoundItemArray.java new file mode 100644 index 00000000..30983370 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/CompoundItemArray.java @@ -0,0 +1,113 @@ +/* + * 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.array; + +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.value.AttributeType; +import com.reandroid.arsc.value.AttributeDataFormat; +import com.reandroid.arsc.value.ResValueMap; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; + +public abstract class CompoundItemArray extends BlockArray implements JSONConvert { + public CompoundItemArray(){ + super(); + } + public AttributeDataFormat[] getFormats(){ + ResValueMap formatsMap = getByType(AttributeType.FORMATS); + if(formatsMap != null){ + return AttributeDataFormat.decodeValueTypes(formatsMap.getData()); + } + return null; + } + public boolean containsType(AttributeType attributeType){ + for(T valueMap : getChildes()){ + if(attributeType == valueMap.getAttributeType()){ + return true; + } + } + return false; + } + public T getByType(AttributeType attributeType){ + if(attributeType == null){ + return null; + } + for(T valueMap : getChildes()){ + if(attributeType == valueMap.getAttributeType()){ + return valueMap; + } + } + return null; + } + public T getByName(int name){ + for(T resValueMap : getChildes()){ + if(resValueMap != null && name == resValueMap.getName()){ + return resValueMap; + } + } + return null; + } + @Override + protected void onRefreshed() { + } + public void onRemoved(){ + for(T resValueMap : getChildes()){ + resValueMap.onRemoved(); + } + } + @Override + public void clearChildes(){ + this.onRemoved(); + super.clearChildes(); + } + @Override + public JSONArray toJson() { + JSONArray jsonArray=new JSONArray(); + if(isNull()){ + return jsonArray; + } + T[] childes = getChildes(); + for(int i = 0; i < childes.length; i++){ + jsonArray.put(i, childes[i].toJson()); + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json){ + clearChildes(); + if(json==null){ + return; + } + int count=json.length(); + ensureSize(count); + for(int i=0;i mapArray){ + if(mapArray == null || mapArray == this){ + return; + } + clearChildes(); + int count = mapArray.childesCount(); + ensureSize(count); + for(int i=0;i implements JSONConvert { + public EntryArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart){ + super(offsets, itemCount, itemStart); + } + public void linkTableStringsInternal(TableStringPool tableStringPool){ + Iterator itr = iterator(true); + while (itr.hasNext()){ + Entry entry = itr.next(); + entry.linkTableStringsInternal(tableStringPool); + } + } + public void linkSpecStringsInternal(SpecStringPool specStringPool){ + Iterator itr = iterator(true); + while (itr.hasNext()){ + Entry entry = itr.next(); + entry.linkSpecStringsInternal(specStringPool); + } + } + public int getHighestEntryId(){ + if(isSparse()){ + return ((SparseOffsetsArray) getOffsetArray()).getHighestId(); + } + return childesCount(); + } + public int getEntryId(int index){ + OffsetArray offsetArray = getOffsetArray(); + if(offsetArray instanceof SparseOffsetsArray){ + return ((SparseOffsetsArray) offsetArray).getIdx(index); + } + return index; + } + public int getEntryIndex(int entryId){ + OffsetArray offsetArray = getOffsetArray(); + if(offsetArray instanceof SparseOffsetsArray){ + return ((SparseOffsetsArray) offsetArray).indexOf(entryId); + } + return entryId; + } + public boolean isSparse(){ + return super.getOffsetArray() instanceof SparseOffsetsArray; + } + public void destroy(){ + for(Entry entry:listItems()){ + if(entry!=null){ + entry.setNull(true); + } + } + clearChildes(); + } + public Boolean hasComplexEntry(){ + Iterator itr = iterator(true); + while (itr.hasNext()){ + Entry entry = itr.next(); + if(entry.isComplex()){ + return true; + } + ResValue resValue = entry.getResValue(); + ValueType valueType = resValue.getValueType(); + if(valueType == null || valueType == ValueType.REFERENCE + || valueType == ValueType.NULL){ + continue; + } + return false; + } + return null; + } + public boolean isEmpty(){ + return !iterator(true).hasNext(); + } + + public Entry getOrCreate(short entryId){ + int id = 0xffff & entryId; + Entry entry = getEntry(id); + if(entry != null){ + return entry; + } + boolean sparse = isSparse(); + int count; + if(sparse){ + count = childesCount() + 1; + }else { + count = id + 1; + } + ensureSize(count); + if(!sparse){ + refreshCount(); + return super.get(id); + } + SparseOffsetsArray offsetsArray = (SparseOffsetsArray) getOffsetArray(); + offsetsArray.ensureArraySize(count); + int index = count - 1; + offsetsArray.setIdx(index, id); + refreshCount(); + return super.get(index); + } + public Entry get(short entryId){ + return getEntry(entryId); + } + public Entry getEntry(short entryId){ + return getEntry(0xffff & entryId); + } + public Entry getEntry(int entryId){ + int index = getEntryIndex(entryId); + return super.get(index); + } + /** + * It is allowed to have duplicate entry name therefore it is not recommend to use this. + */ + public Entry getEntry(String entryName){ + if(entryName == null){ + return null; + } + Iterator itr = iterator(true); + while (itr.hasNext()){ + Entry entry = itr.next(); + if(entryName.equals(entry.getName())){ + return entry; + } + } + return null; + } + @Override + public Entry newInstance() { + return new Entry(); + } + @Override + public Entry[] newInstance(int len) { + return new Entry[len]; + } + + /** + * To be removed, use getEntry(String entryName) + */ + @Deprecated + public Entry searchByEntryName(String entryName){ + return getEntry(entryName); + } + @Override + public JSONArray toJson() { + JSONArray jsonArray=new JSONArray(); + int index=0; + String name_id = Entry.NAME_id; + for(Entry entry : listItems(true)){ + JSONObject childObject = entry.toJson(); + if(childObject==null){ + continue; + } + childObject.put(name_id, entry.getId()); + jsonArray.put(index, childObject); + index++; + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + clearChildes(); + if(isSparse()){ + fromJsonSparse(json); + }else { + fromJsonNonSparse(json); + } + refreshCountAndStart(); + } + private void fromJsonNonSparse(JSONArray json){ + int length=json.length(); + ensureSize(length); + String name_id = Entry.NAME_id; + for(int i=0;i itr = entryArray.iterator(true); + while (itr.hasNext()){ + Entry comingBlock = itr.next(); + Entry existingBlock = getOrCreate((short) comingBlock.getId()); + existingBlock.merge(comingBlock); + } + } + private void mergeNonSparse(EntryArray entryArray){ + ensureSize(entryArray.childesCount()); + Iterator itr = entryArray.iterator(true); + while (itr.hasNext()){ + Entry comingBlock = itr.next(); + Entry existingBlock = super.get(comingBlock.getIndex()); + existingBlock.merge(comingBlock); + } + } + @Override + public String toString(){ + return getClass().getSimpleName()+": size="+childesCount(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/LibraryInfoArray.java b/src/ARSCLib/com/reandroid/arsc/array/LibraryInfoArray.java new file mode 100755 index 00000000..79e1ae58 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/LibraryInfoArray.java @@ -0,0 +1,107 @@ + /* + * 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.array; + +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.value.LibraryInfo; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.IOException; + +public class LibraryInfoArray extends BlockArray implements JSONConvert { + private final IntegerItem mInfoCount; + public LibraryInfoArray(IntegerItem infoCount){ + this.mInfoCount=infoCount; + } + public LibraryInfo getOrCreate(int pkgId){ + LibraryInfo info=getById(pkgId); + if(info!=null){ + return info; + } + int index=childesCount(); + ensureSize(index+1); + info=get(index); + info.setPackageId(pkgId); + return info; + } + public LibraryInfo getById(int pkgId){ + for(LibraryInfo info:listItems()){ + if(pkgId==info.getPackageId()){ + return info; + } + } + return null; + } + @Override + public LibraryInfo newInstance() { + return new LibraryInfo(); + } + @Override + public LibraryInfo[] newInstance(int len) { + return new LibraryInfo[len]; + } + @Override + protected void onRefreshed() { + mInfoCount.set(childesCount()); + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException { + setChildesCount(mInfoCount.get()); + super.onReadBytes(reader); + } + @Override + public JSONArray toJson() { + JSONArray jsonArray=new JSONArray(); + int i=0; + for(LibraryInfo libraryInfo:listItems()){ + JSONObject jsonObject= libraryInfo.toJson(); + if(jsonObject==null){ + continue; + } + jsonArray.put(i, jsonObject); + i++; + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + clearChildes(); + if(json==null){ + return; + } + int length= json.length(); + ensureSize(length); + for (int i=0;i extends BlockArray implements BlockLoad { + private final OffsetArray mOffsets; + private final IntegerItem mItemStart; + private final IntegerItem mItemCount; + private final ByteArray mEnd4Block; + private byte mEnd4Type; + public OffsetBlockArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart){ + super(); + this.mOffsets=offsets; + this.mItemCount=itemCount; + this.mItemStart=itemStart; + this.mEnd4Block=new ByteArray(); + mItemCount.setBlockLoad(this); + } + OffsetArray getOffsetArray(){ + return mOffsets; + } + void setEndBytes(byte b){ + this.mEnd4Type=b; + this.mEnd4Block.fill(b); + } + @Override + public void clearChildes(){ + super.clearChildes(); + mOffsets.clear(); + mItemStart.set(0); + mItemCount.set(0); + mEnd4Block.clear(); + } + @Override + public int countBytes(){ + int result=super.countBytes(); + int endCount=mEnd4Block.countBytes(); + return result+endCount; + } + @Override + public void onCountUpTo(BlockCounter counter){ + super.onCountUpTo(counter); + if(counter.FOUND){ + return; + } + mEnd4Block.onCountUpTo(counter); + } + @Override + public byte[] getBytes(){ + byte[] results=super.getBytes(); + if(results==null){ + return null; + } + byte[] endBytes=mEnd4Block.getBytes(); + results=addBytes(results, endBytes); + return results; + } + @Override + public int onWriteBytes(OutputStream stream) throws IOException { + int result=super.onWriteBytes(stream); + if(result==0){ + return 0; + } + result+=mEnd4Block.writeBytes(stream); + return result; + } + @Override + protected void onRefreshed() { + int count=childesCount(); + OffsetArray offsetArray = this.mOffsets; + offsetArray.setSize(count); + T[] childes=getChildes(); + int sum=0; + if(childes!=null){ + int max=childes.length; + for(int i=0;imaxPos){ + maxPos=pos; + } + } + reader.seek(maxPos); + refreshEnd4Block(reader, mEnd4Block); + } + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender==mItemCount){ + int count=mItemCount.get(); + setChildesCount(count); + mOffsets.setSize(count); + } + } + + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append(": count = "); + int s= childesCount(); + builder.append(s); + int count=mItemCount.get(); + if(s!=count){ + builder.append(", countValue="); + builder.append(count); + } + builder.append(", start="); + builder.append(mItemStart.get()); + return builder.toString(); + } + +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/PackageArray.java b/src/ARSCLib/com/reandroid/arsc/array/PackageArray.java new file mode 100755 index 00000000..5fa3aca8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/PackageArray.java @@ -0,0 +1,173 @@ +/* + * 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.array; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Iterator; + +public class PackageArray extends BlockArray + implements BlockLoad, JSONConvert, Comparator { + private final IntegerItem mPackageCount; + public PackageArray(IntegerItem packageCount){ + this.mPackageCount=packageCount; + mPackageCount.setBlockLoad(this); + } + public void destroy(){ + Iterator itr = iterator(true); + while (itr.hasNext()){ + PackageBlock packageBlock=itr.next(); + packageBlock.destroy(); + } + clearChildes(); + } + public PackageBlock pickOne(){ + return pickOne(getChildes(), 0); + } + public PackageBlock pickOne(int packageId){ + return pickOne(getChildes(), packageId); + } + private PackageBlock pickOne(PackageBlock[] items, int packageId){ + if(items==null||items.length==0){ + return null; + } + if(items.length==1){ + return items[0]; + } + PackageBlock largest=null; + for(PackageBlock packageBlock:items){ + if(packageBlock == null){ + continue; + } + if(packageId!=0 && packageId!=packageBlock.getId()){ + continue; + } + if(largest==null){ + largest=packageBlock; + }else if(packageBlock.getEntriesGroupMap().size() > + largest.getEntriesGroupMap().size()){ + largest=packageBlock; + } + } + return largest; + } + public void sort(){ + for(PackageBlock packageBlock:listItems()){ + packageBlock.sortTypes(); + } + sort(this); + } + public PackageBlock getOrCreate(byte pkgId){ + return getOrCreate(0xff & pkgId); + } + public PackageBlock getOrCreate(int pkgId){ + PackageBlock packageBlock = getPackageBlockById(pkgId); + if(packageBlock != null){ + return packageBlock; + } + packageBlock = createNext(); + packageBlock.setId(pkgId); + packageBlock.setName("PACKAGE NAME"); + return packageBlock; + } + public PackageBlock getPackageBlockById(byte pkgId){ + return getPackageBlockById(0xff & pkgId); + } + public PackageBlock getPackageBlockById(int pkgId){ + Iterator itr=iterator(true); + while (itr.hasNext()){ + PackageBlock packageBlock=itr.next(); + if(packageBlock.getId()==pkgId){ + return packageBlock; + } + } + return null; + } + @Override + public PackageBlock newInstance() { + return new PackageBlock(); + } + + @Override + public PackageBlock[] newInstance(int len) { + return new PackageBlock[len]; + } + + @Override + protected void onRefreshed() { + refreshPackageCount(); + } + private void refreshPackageCount(){ + mPackageCount.set(childesCount()); + } + + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender != mPackageCount){ + return; + } + setChildesCount(mPackageCount.get()); + } + @Override + public JSONArray toJson() { + JSONArray jsonArray=new JSONArray(); + int i=0; + for(PackageBlock packageBlock:listItems()){ + JSONObject jsonObject= packageBlock.toJson(); + if(jsonObject==null){ + continue; + } + jsonArray.put(i, jsonObject); + i++; + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + int length= json.length(); + clearChildes(); + ensureSize(length); + for (int i=0;i { + public ResValueMapArray(){ + super(); + } + @Override + public ResValueMap newInstance() { + return new ResValueMap(); + } + + @Override + public ResValueMap[] newInstance(int len) { + return new ResValueMap[len]; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/ResXmlAttributeArray.java b/src/ARSCLib/com/reandroid/arsc/array/ResXmlAttributeArray.java new file mode 100755 index 00000000..a1820115 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/ResXmlAttributeArray.java @@ -0,0 +1,142 @@ + /* + * 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.array; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.chunk.xml.ResXmlAttribute; +import com.reandroid.arsc.item.ShortItem; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.Comparator; + +public class ResXmlAttributeArray extends BlockArray + implements Comparator, JSONConvert { + private final HeaderBlock mHeaderBlock; + private final ShortItem mAttributeStart; + private final ShortItem mAttributeCount; + private final ShortItem mAttributesUnitSize; + public ResXmlAttributeArray(HeaderBlock headerBlock, + ShortItem attributeStart, + ShortItem attributeCount, + ShortItem attributesUnitSize){ + this.mHeaderBlock=headerBlock; + this.mAttributeStart=attributeStart; + this.mAttributeCount=attributeCount; + this.mAttributesUnitSize=attributesUnitSize; + } + public void setAttributesUnitSize(int size){ + ResXmlAttribute[] attributes=getChildes(); + for(int i=0;i { + private final HeaderBlock mHeaderBlock; + private final Map mResIdMap; + private boolean mUpdated; + public ResXmlIDArray(HeaderBlock headerBlock){ + super(); + this.mHeaderBlock=headerBlock; + this.mResIdMap=new HashMap<>(); + } + public void addResourceId(int index, int resId){ + if(index<0){ + return; + } + ensureSize(index+1); + ResXmlID xmlID=get(index); + if(xmlID!=null){ + xmlID.set(resId); + } + } + public ResXmlID getOrCreate(int resId){ + updateIdMap(); + ResXmlID xmlID=mResIdMap.get(resId); + if(xmlID!=null){ + return xmlID; + } + xmlID=new ResXmlID(resId); + add(xmlID); + mUpdated=true; + mResIdMap.put(resId, xmlID); + return xmlID; + } + public ResXmlID getByResId(int resId){ + updateIdMap(); + return mResIdMap.get(resId); + } + public void refreshIdMap(){ + mUpdated = false; + updateIdMap(); + } + private void updateIdMap(){ + if(mUpdated){ + return; + } + mUpdated=true; + mResIdMap.clear(); + ResXmlID[] allChildes=getChildes(); + if(allChildes==null||allChildes.length==0){ + return; + } + int max=allChildes.length; + for(int i=0;i { + public ResXmlStringArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + super(offsets, itemCount, itemStart, is_utf8); + } + @Override + List listUnusedStringsToRemove(){ + List results=new ArrayList<>(); + ResXmlIDMap idMap = getResXmlIDMap(); + int lastIndex = -1; + if(idMap!=null){ + lastIndex = idMap.countId(); + } + for(ResXmlString item:listItems()){ + if(item == null + || item.hasReference() + || item.getIndex() result){ + result = id; + } + } + if(result == NO_ENTRY){ + result = 0; + } + return result; + } + public int indexOf(int idx){ + int size = super.size(); + for(int i=0; i>> 16) & 0xffff; + return value * 4; + } + @Override + public void setOffset(int index, int offset){ + int value; + if(offset == NO_ENTRY){ + value = 0; + }else { + int idx = getAt(index); + idx = idx & 0xffff; + offset = offset & 0xffff; + offset = offset / 4; + offset = offset << 16; + value = offset | idx; + } + super.put(index, value); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/SpecBlockArray.java b/src/ARSCLib/com/reandroid/arsc/array/SpecBlockArray.java new file mode 100755 index 00000000..22f918ba --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/SpecBlockArray.java @@ -0,0 +1,39 @@ + /* + * 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.array; + +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.chunk.SpecBlock; + +public class SpecBlockArray extends BlockArray { + public SpecBlockArray(){ + super(); + } + @Override + public SpecBlock newInstance() { + return new SpecBlock(); + } + + @Override + public SpecBlock[] newInstance(int len) { + return new SpecBlock[len]; + } + + @Override + protected void onRefreshed() { + + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/SpecStringArray.java b/src/ARSCLib/com/reandroid/arsc/array/SpecStringArray.java new file mode 100755 index 00000000..82e2c1b9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/SpecStringArray.java @@ -0,0 +1,33 @@ +/* + * 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.array; + +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.SpecString; + +public class SpecStringArray extends StringArray { + public SpecStringArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + super(offsets, itemCount, itemStart, is_utf8); + } + @Override + public SpecString newInstance() { + return new SpecString(isUtf8()); + } + @Override + public SpecString[] newInstance(int len) { + return new SpecString[len]; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/SpecTypePairArray.java b/src/ARSCLib/com/reandroid/arsc/array/SpecTypePairArray.java new file mode 100755 index 00000000..903061f1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/SpecTypePairArray.java @@ -0,0 +1,360 @@ +/* + * 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.array; + +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.SpecBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.group.StringGroup; +import com.reandroid.arsc.item.TypeString; +import com.reandroid.arsc.pool.TypeStringPool; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.util.*; + +public class SpecTypePairArray extends BlockArray + implements JSONConvert, Comparator { + public SpecTypePairArray(){ + super(); + } + + public void sort(){ + for(SpecTypePair specTypePair:listItems()){ + specTypePair.sortTypes(); + } + sort(this); + } + public void removeEmptyPairs(){ + List allPairs=new ArrayList<>(listItems()); + boolean foundEmpty=false; + for(SpecTypePair typePair:allPairs){ + typePair.removeEmptyTypeBlocks(); + if(typePair.isEmpty()){ + super.remove(typePair, false); + foundEmpty=true; + } + } + if(foundEmpty){ + trimNullBlocks(); + } + } + public boolean isEmpty(){ + Iterator iterator=iterator(true); + while (iterator.hasNext()){ + SpecTypePair pair=iterator.next(); + if(!pair.isEmpty()){ + return false; + } + } + return true; + } + public Entry getOrCreateEntry(byte typeId, short entryId, String qualifiers){ + TypeBlock typeBlock=getOrCreateTypeBlock(typeId, qualifiers); + return typeBlock.getOrCreateEntry(entryId); + } + public Entry getEntry(byte typeId, short entryId, String qualifiers){ + TypeBlock typeBlock=getTypeBlock(typeId, qualifiers); + if(typeBlock==null){ + return null; + } + return typeBlock.getEntry(entryId); + } + public TypeBlock getOrCreateTypeBlock(byte typeId, String qualifiers){ + SpecTypePair pair=getOrCreate(typeId); + return pair.getOrCreateTypeBlock(qualifiers); + } + public TypeBlock getTypeBlock(byte typeId, String qualifiers){ + SpecTypePair pair= getSpecTypePair(typeId); + if(pair==null){ + return null; + } + return pair.getTypeBlock(qualifiers); + } + public TypeBlock getOrCreate(byte typeId, ResConfig resConfig){ + SpecTypePair pair=getOrCreate(typeId); + return pair.getTypeBlockArray().getOrCreate(resConfig); + } + public SpecTypePair getOrCreate(byte typeId){ + SpecTypePair pair = getSpecTypePair(typeId); + if(pair!=null){ + return pair; + } + pair = createNext(); + pair.setTypeId(typeId); + return pair; + } + public SpecTypePair getOrCreate(String typeName){ + SpecTypePair specTypePair = getSpecTypePair(typeName); + if(specTypePair != null){ + return specTypePair; + } + TypeString typeString = getOrCreateTypeString(typeName); + byte id = (byte) typeString.getId(); + specTypePair = createNext(); + specTypePair.setTypeId(id); + return specTypePair; + } + public TypeBlock getOrCreateTypeBlock(String typeName, ResConfig resConfig){ + return getOrCreate(typeName).getOrCreateTypeBlock(resConfig); + } + public TypeBlock getOrCreateTypeBlock(String typeName, String qualifiers){ + return getOrCreate(typeName).getOrCreateTypeBlock(qualifiers); + } + public SpecTypePair getSpecTypePair(int typeId){ + return getSpecTypePair((byte) typeId); + } + public SpecTypePair getSpecTypePair(byte typeId){ + SpecTypePair[] items = getChildes(); + if(items == null){ + return null; + } + int length = items.length; + for(int i = 0; i < length; i++){ + SpecTypePair specTypePair = items[i]; + if(specTypePair != null && specTypePair.getTypeId() == typeId){ + return specTypePair; + } + } + return null; + } + public SpecTypePair getSpecTypePair(String typeName){ + if(typeName == null){ + return null; + } + Iterator itr = iterator(true); + while (itr.hasNext()){ + SpecTypePair specTypePair = itr.next(); + if(specTypePair.isEqualTypeName(typeName)){ + return specTypePair; + } + } + return null; + } + public Entry getAnyEntry(byte typeId, short entryId){ + if(typeId == 0){ + return null; + } + SpecTypePair specTypePair = getSpecTypePair(typeId); + if(specTypePair != null){ + return specTypePair.getAnyEntry(entryId); + } + return null; + } + public Entry getAnyEntry(String typeName, String entryName){ + SpecTypePair specTypePair = getSpecTypePair(typeName); + if(specTypePair != null){ + return specTypePair.getAnyEntry(entryName); + } + return null; + } + public Entry getEntry(String qualifiers, String typeName, String entryName){ + ResConfig resConfig = new ResConfig(); + resConfig.parseQualifiers(qualifiers); + return getEntry(resConfig, typeName, entryName); + } + public Entry getEntry(ResConfig resConfig, String typeName, String entryName){ + SpecTypePair specTypePair = getSpecTypePair(typeName); + if(specTypePair != null){ + return specTypePair.getEntry(resConfig, entryName); + } + return null; + } + public EntryGroup getEntryGroup(String typeName, String entryName){ + SpecTypePair specTypePair = getSpecTypePair(typeName); + if(specTypePair != null){ + return specTypePair.getEntryGroup(entryName); + } + return null; + } + @Override + public SpecTypePair newInstance() { + return new SpecTypePair(); + } + @Override + public SpecTypePair[] newInstance(int len) { + return new SpecTypePair[len]; + } + @Override + protected void onRefreshed() { + + } + @Override + protected void onPreRefreshRefresh(){ + validateEntryCounts(); + } + + private void validateEntryCounts(){ + Map entryCountMap=mapHighestEntryCount(); + for(Map.Entry entry:entryCountMap.entrySet()){ + byte id=entry.getKey(); + int count=entry.getValue(); + SpecTypePair pair= getSpecTypePair(id); + pair.getSpecBlock().setEntryCount(count); + pair.getTypeBlockArray().setEntryCount(count); + } + } + private Map mapHighestEntryCount(){ + Map results=new HashMap<>(); + SpecTypePair[] childes=getChildes(); + for (SpecTypePair pair:childes){ + int count=pair.getHighestEntryCount(); + byte id=pair.getTypeId(); + Integer exist=results.get(id); + if(exist==null || count>exist){ + results.put(id, count); + } + } + return results; + } + public int getSmallestTypeId(){ + SpecTypePair[] childes=getChildes(); + if(childes==null){ + return 0; + } + int result=0; + boolean firstFound=false; + for (int i=0;iresult){ + result=id; + } + } + return result; + } + private TypeString getOrCreateTypeString(String typeName){ + TypeStringPool typeStringPool = getTypeStringPool(); + if(typeStringPool == null){ + return null; + } + StringGroup group = typeStringPool.get(typeName); + if(group != null){ + return group.get(0); + } + int id = typeStringPool.getLastId() + 1; + return typeStringPool.getOrCreate(id, typeName); + } + private TypeStringPool getTypeStringPool(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock != null){ + return packageBlock.getTypeStringPool(); + } + return null; + } + private PackageBlock getPackageBlock(){ + return getParentInstance(PackageBlock.class); + } + @Override + public JSONArray toJson() { + return toJson(false); + } + @Override + public void fromJson(JSONArray json) { + if(json==null){ + return; + } + int length = json.length(); + for (int i=0;i + implements BlockLoad, JSONConvert { + private final IntegerItem count; + public StagedAliasEntryArray(IntegerItem count){ + super(); + this.count=count; + this.count.setBlockLoad(this); + } + public boolean contains(StagedAliasEntry aliasEntry){ + StagedAliasEntry[] childes=getChildes(); + if(childes==null){ + return false; + } + for(int i=0;i extends OffsetBlockArray implements JSONConvert { + private boolean mUtf8; + + public StringArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + super(offsets, itemCount, itemStart); + this.mUtf8=is_utf8; + setEndBytes((byte)0x00); + } + public List toStringList(){ + return new AbstractList() { + @Override + public String get(int i) { + T item=StringArray.this.get(i); + if(item==null){ + return null; + } + return item.getHtml(); + } + @Override + public int size() { + return childesCount(); + } + }; + } + public List removeUnusedStrings(){ + List unusedList = listUnusedStringsToRemove(); + remove(unusedList); + for(T item:unusedList){ + item.onRemoved(); + } + return unusedList; + } + List listUnusedStringsToRemove(){ + return listUnusedStrings(); + } + public List listUnusedStrings(){ + List results=new ArrayList<>(); + for(T item:listItems()){ + if(!item.hasReference()){ + results.add(item); + } + } + return results; + } + public void setUtf8(boolean is_utf8){ + if(mUtf8==is_utf8){ + return; + } + mUtf8=is_utf8; + T[] childes=getChildes(); + if(childes!=null){ + int max=childes.length; + for(int i=0;i implements JSONConvert { + public StyleArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart) { + super(offsets, itemCount, itemStart); + setEndBytes(END_BYTE); + } + @Override + public void clearChildes(){ + for(StyleItem styleItem:listItems()){ + styleItem.onRemoved(); + } + super.clearChildes(); + } + @Override + void refreshEnd4Block(BlockReader reader, ByteArray end4Block) throws IOException { + end4Block.clear(); + if(reader.available()<4){ + return; + } + IntegerItem integerItem=new IntegerItem(); + while (reader.available()>=4){ + int pos=reader.getPosition(); + integerItem.readBytes(reader); + if(integerItem.get()!=0xFFFFFFFF){ + reader.seek(pos); + break; + } + end4Block.add(integerItem.getBytes()); + } + } + @Override + void refreshEnd4Block(ByteArray end4Block) { + super.refreshEnd4Block(end4Block); + if(childesCount()==0){ + return; + } + end4Block.ensureArraySize(8); + end4Block.fill(END_BYTE); + } + @Override + protected void refreshChildes(){ + // Not required + } + @Override + public StyleItem newInstance() { + return new StyleItem(); + } + @Override + public StyleItem[] newInstance(int len) { + return new StyleItem[len]; + } + + @Override + public JSONArray toJson() { + if(childesCount()==0){ + return null; + } + return null; + } + @Override + public void fromJson(JSONArray json) { + + } + public void merge(StyleArray styleArray){ + if(styleArray==null||styleArray==this){ + return; + } + if(childesCount()!=0){ + return; + } + int count=styleArray.childesCount(); + ensureSize(count); + for(int i=0;i { + public TableStringArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + super(offsets, itemCount, itemStart, is_utf8); + } + @Override + public TableString newInstance() { + return new TableString(isUtf8()); + } + @Override + public TableString[] newInstance(int len) { + return new TableString[len]; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/array/TypeBlockArray.java b/src/ARSCLib/com/reandroid/arsc/array/TypeBlockArray.java new file mode 100755 index 00000000..3d991c8e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/TypeBlockArray.java @@ -0,0 +1,418 @@ +/* + * 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.array; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.base.BlockArray; +import com.reandroid.arsc.chunk.SpecBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.TypeHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.TypeString; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +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 { + private byte mTypeId; + private Boolean mHasComplexEntry; + + public TypeBlockArray(){ + super(); + } + + public Boolean hasComplexEntry(){ + if(mHasComplexEntry != null){ + return mHasComplexEntry; + } + for(TypeBlock typeBlock : listItems(true)){ + Boolean hasComplex = typeBlock.getEntryArray().hasComplexEntry(); + if(hasComplex != null){ + mHasComplexEntry = hasComplex; + } + } + return mHasComplexEntry; + } + public void destroy(){ + for(TypeBlock typeBlock:listItems()){ + if(typeBlock!=null){ + typeBlock.destroy(); + } + } + clearChildes(); + } + public void sort(){ + sort(this); + } + public boolean removeNullEntries(int startId){ + boolean result = true; + for(TypeBlock typeBlock:listItems()){ + boolean removed = typeBlock.removeNullEntries(startId); + result = result && removed; + } + return result; + } + public void removeEmptyBlocks(){ + List allTypes=new ArrayList<>(listItems()); + boolean foundEmpty=false; + for(TypeBlock typeBlock:allTypes){ + if(typeBlock.isEmpty()){ + super.remove(typeBlock, false); + foundEmpty=true; + } + } + if(foundEmpty){ + trimNullBlocks(); + } + } + public Entry getOrCreateEntry(short entryId, String qualifiers){ + TypeBlock typeBlock=getOrCreate(qualifiers); + return typeBlock.getOrCreateEntry(entryId); + } + public boolean isEmpty(){ + for(TypeBlock typeBlock:listItems()){ + if(typeBlock!=null && !typeBlock.isEmpty()){ + return false; + } + } + return true; + } + public Entry getEntry(short entryId, String qualifiers){ + TypeBlock typeBlock=getTypeBlock(qualifiers); + if(typeBlock==null){ + return null; + } + return typeBlock.getEntry(entryId); + } + public Entry getEntry(ResConfig resConfig, String entryName){ + TypeBlock typeBlock = getTypeBlock(resConfig); + if(typeBlock != null){ + return typeBlock.getEntry(entryName); + } + return null; + } + public TypeBlock getOrCreate(ResConfig resConfig){ + return getOrCreate(resConfig, false); + } + public TypeBlock getOrCreate(ResConfig resConfig, boolean sparse){ + TypeBlock typeBlock = getTypeBlock(resConfig, sparse); + if(typeBlock != null){ + return typeBlock; + } + byte id = getTypeId(); + typeBlock = createNext(sparse); + typeBlock.setTypeId(id); + ResConfig config = typeBlock.getResConfig(); + config.copyFrom(resConfig); + return typeBlock; + } + public TypeBlock getOrCreate(String qualifiers){ + TypeBlock typeBlock=getTypeBlock(qualifiers); + if(typeBlock!=null){ + return typeBlock; + } + typeBlock=createNext(); + ResConfig config=typeBlock.getResConfig(); + config.parseQualifiers(qualifiers); + return typeBlock; + } + public TypeBlock getTypeBlock(String qualifiers){ + TypeBlock[] items=getChildes(); + if(items==null){ + return null; + } + int max=items.length; + for(int i=0;i listResConfig(){ + return new AbstractList() { + @Override + public ResConfig get(int i) { + TypeBlock typeBlock=TypeBlockArray.this.get(i); + if(typeBlock!=null){ + return typeBlock.getResConfig(); + } + return null; + } + + @Override + public int size() { + return TypeBlockArray.this.childesCount(); + } + }; + } + 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){ + return parent.getSpecBlock(); + } + return null; + } + @Override + protected boolean remove(TypeBlock block, boolean trim){ + if(block==null){ + return false; + } + block.cleanEntries(); + return super.remove(block, trim); + } + @Override + public TypeBlock newInstance() { + byte id = getTypeId(); + TypeBlock typeBlock = new TypeBlock(false); + typeBlock.setTypeId(id); + return typeBlock; + } + @Override + public TypeBlock[] newInstance(int len) { + return new TypeBlock[len]; + } + public TypeBlock createNext(boolean sparse){ + byte id = getTypeId(); + TypeBlock typeBlock = new TypeBlock(sparse); + typeBlock.setTypeId(id); + add(typeBlock); + return typeBlock; + } + @Override + protected void onRefreshed() { + + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + boolean readOk=true; + while (readOk){ + readOk=readTypeBlockArray(reader); + } + } + private boolean readTypeBlockArray(BlockReader reader) throws IOException{ + HeaderBlock headerBlock=reader.readHeaderBlock(); + if(headerBlock==null){ + return false; + } + ChunkType chunkType=headerBlock.getChunkType(); + if(chunkType!=ChunkType.TYPE){ + return false; + } + TypeHeader typeHeader = reader.readTypeHeader(); + int id = getTypeId(); + if(id!=0 && typeHeader.getId().unsignedInt() != id){ + return false; + } + int pos=reader.getPosition(); + TypeBlock typeBlock=createNext(); + typeBlock.readBytes(reader); + return reader.getPosition()>pos; + } + public int getHighestEntryCount(){ + int result=0; + for(TypeBlock typeBlock:getChildes()){ + int high = typeBlock.getEntryArray().getHighestEntryId(); + if(high > result){ + result = high; + } + } + return result; + } + public void setEntryCount(int count){ + for(TypeBlock typeBlock:getChildes()){ + if(!typeBlock.isSparse()){ + typeBlock.setEntryCount(count); + } + } + } + public TypeString getTypeString(){ + for(TypeBlock typeBlock:getChildes()){ + TypeString typeString=typeBlock.getTypeString(); + if(typeString!=null){ + return typeString; + } + } + return null; + } + @Override + public JSONArray toJson() { + JSONArray jsonArray=new JSONArray(); + int i=0; + for(TypeBlock typeBlock:listItems()){ + JSONObject jsonObject= typeBlock.toJson(); + if(jsonObject==null){ + continue; + } + jsonArray.put(i, jsonObject); + i++; + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + if(json == null){ + return; + } + int length = json.length(); + for(int i = 0; i < length; i++){ + JSONObject jsonObject = json.getJSONObject(i); + TypeBlock typeBlock = createNext( + jsonObject.optBoolean(TypeBlock.NAME_is_sparse, false)); + typeBlock.fromJson(jsonObject); + } + } + public void merge(TypeBlockArray typeBlockArray){ + if(typeBlockArray == null || typeBlockArray == this){ + return; + } + for(TypeBlock typeBlock:typeBlockArray.listItems()){ + TypeBlock exist = getOrCreate( + typeBlock.getResConfig(), typeBlock.isSparse()); + exist.merge(typeBlock); + } + } + /** + * TOBEREMOVED + * + * It's mistake to have this method + * + */ + @Deprecated + public Entry searchByEntryName(String entryName){ + if(entryName==null){ + return null; + } + TypeBlock[] childes = getChildes(); + if(childes==null || childes.length==0){ + return null; + } + return childes[0].getEntry(entryName); + } + @Override + 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/ARSCLib/com/reandroid/arsc/array/TypeStringArray.java b/src/ARSCLib/com/reandroid/arsc/array/TypeStringArray.java new file mode 100755 index 00000000..1c6de643 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/array/TypeStringArray.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.arsc.array; + +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.TypeString; + +public class TypeStringArray extends StringArray { + private int lastCreateIndex; + public TypeStringArray(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + super(offsets, itemCount, itemStart, is_utf8); + } + @Override + public TypeString newInstance() { + TypeString typeString=new TypeString(isUtf8()); + //create default name + this.lastCreateIndex++; + typeString.set("type-"+lastCreateIndex); + return typeString; + } + @Override + public TypeString[] newInstance(int len) { + return new TypeString[len]; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/base/Block.java b/src/ARSCLib/com/reandroid/arsc/base/Block.java new file mode 100755 index 00000000..d66e8b41 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/base/Block.java @@ -0,0 +1,134 @@ + /* + * 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 com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class Block { + private int mIndex=-1; + private Block mParent; + private boolean mNull; + private BlockLoad mBlockLoad; + public abstract byte[] getBytes(); + public abstract int countBytes(); + public final int countUpTo(Block block){ + BlockCounter counter=new BlockCounter(block); + onCountUpTo(counter); + return counter.COUNT; + } + public abstract void onCountUpTo(BlockCounter counter); + public final void readBytes(BlockReader reader) throws IOException{ + onReadBytes(reader); + notifyBlockLoad(reader); + } + public final void setBlockLoad(BlockLoad blockLoad){ + mBlockLoad=blockLoad; + } + public void notifyBlockLoad() throws IOException { + notifyBlockLoad(null); + } + private void notifyBlockLoad(BlockReader reader) throws IOException{ + BlockLoad blockLoad=mBlockLoad; + if(blockLoad!=null){ + blockLoad.onBlockLoaded(reader, this); + } + } + protected void onReadBytes(BlockReader reader) throws IOException{ + } + public final int writeBytes(OutputStream stream) throws IOException{ + if(isNull()){ + return 0; + } + return onWriteBytes(stream); + } + protected abstract int onWriteBytes(OutputStream stream) throws IOException; + public boolean isNull(){ + return mNull; + } + public void setNull(boolean is_null){ + mNull=is_null; + } + public final int getIndex(){ + return mIndex; + } + public final void setIndex(int index){ + int old=mIndex; + if(index==old){ + return; + } + mIndex=index; + if(old!=-1 && index!=-1){ + onIndexChanged(old, index); + } + } + public void onIndexChanged(int oldIndex, int newIndex){ + + } + public final void setParent(Block parent){ + if(parent==this){ + return; + } + mParent=parent; + } + public final Block getParent(){ + return mParent; + } + public final T getParent(Class parentClass){ + Block parent = getParent(); + while (parent!=null){ + if(parent.getClass() == parentClass){ + return (T) parent; + } + parent = parent.getParent(); + } + return null; + } + public final T getParentInstance(Class parentClass){ + Block parent = getParent(); + while (parent!=null){ + if(parentClass.isInstance(parent)){ + return (T) parent; + } + parent = parent.getParent(); + } + return null; + } + + + protected static byte[] addBytes(byte[] bts1, byte[] bts2){ + boolean empty1=(bts1==null || bts1.length==0); + boolean empty2=(bts2==null || bts2.length==0); + if(empty1 && empty2){ + return null; + } + if(empty1){ + return bts2; + } + if(empty2){ + return bts1; + } + int len=bts1.length+bts2.length; + byte[] result=new byte[len]; + int start=bts1.length; + System.arraycopy(bts1, 0, result, 0, start); + System.arraycopy(bts2, 0, result, start, bts2.length); + return result; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/base/BlockArray.java b/src/ARSCLib/com/reandroid/arsc/base/BlockArray.java new file mode 100755 index 00000000..b050be70 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/base/BlockArray.java @@ -0,0 +1,502 @@ +/* + * 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 { + private T[] elementData; + public BlockArray(){ + elementData= newInstance(0); + } + + public void removeAllNull(int start){ + removeAll(start, true); + } + public void removeAll(int start){ + removeAll(start, false); + } + private void removeAll(int start, boolean check_null){ + List removeList = subList(start); + if(removeList.size()==0 || (check_null && !isAllNull(removeList))){ + return; + } + T[] itemArray = this.elementData; + for(T item:removeList){ + if(item==null){ + continue; + } + if(!item.isNull()){ + item.setNull(true); + } + int index = item.getIndex(); + if(index>=0 && itemArray[index]==item){ + item.setIndex(-1); + item.setParent(null); + itemArray[index] = null; + } + } + setChildesCount(start); + } + public List subList(int start){ + return subList(start, -1); + } + public List subList(int start, int count){ + T[] items = this.elementData; + if(items==null){ + return new ArrayList<>(); + } + int length = items.length; + if(start>=length){ + return new ArrayList<>(); + } + if(start < 0){ + start=0; + } + int end = count; + if(end < 0){ + end = items.length; + }else { + end = start + count; + if(end > length){ + end=length; + } + } + List results = new ArrayList<>(end - start); + for(int i=start; i listItems(){ + return listItems(false); + } + public Collection listItems(boolean skipNullBlocks){ + return new AbstractCollection() { + @Override + public Iterator iterator(){ + return BlockArray.this.iterator(skipNullBlocks); + } + @Override + public boolean contains(Object o){ + return BlockArray.this.contains(o); + } + @Override + public int size() { + return BlockArray.this.childesCount(); + } + }; + } + @Override + public T[] getChildes(){ + return elementData; + } + public void ensureSize(int size){ + if(size<= childesCount()){ + return; + } + setChildesCount(size); + } + public void setChildesCount(int count){ + if(count<0){ + count=0; + } + if(count==0){ + clearChildes(); + return; + } + int diff = count - childesCount(); + if(diff==0){ + return; + } + changeSize(diff); + } + public void clearChildes(){ + T[] allChildes=elementData; + if(allChildes==null || allChildes.length==0){ + return; + } + int max=allChildes.length; + for(int i=0;i0){ + System.arraycopy(old, 0, update, 0, oldLen); + } + boolean foundNull=false; + for(int i=0;i comparator){ + T[] data=this.elementData; + if(comparator==null || data==null || data.length<2){ + return; + } + Arrays.sort(data, 0, data.length, comparator); + for(int i=0;i= index; i--){ + T exist = childes[i]; + childes[i] = null; + int newIndex = i + 1; + childes[newIndex] = exist; + exist.setIndex(newIndex); + } + childes[index] = item; + item.setParent(this); + item.setIndex(index); + } + public void setItem(int index, T item){ + ensureSize(index+1); + elementData[index]=item; + item.setIndex(index); + item.setParent(this); + } + public void add(T block){ + if(block==null){ + return; + } + T[] old=elementData; + int index=old.length; + elementData= newInstance(index+1); + if(index>0){ + System.arraycopy(old, 0, elementData, 0, index); + } + elementData[index]=block; + block.setIndex(index); + block.setParent(this); + } + public final int countNonNull(){ + return countNonNull(true); + } + public final int childesCount(){ + return elementData.length; + } + public final T createNext(){ + T block=newInstance(); + add(block); + return block; + } + public final T get(int i){ + if(i >= childesCount() || i<0){ + return null; + } + return elementData[i]; + } + public int indexOf(Object block){ + T[] items=elementData; + if(items==null){ + return -1; + } + int len=items.length; + for(int i=0;i iterator() { + return iterator(false); + } + 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){ + return false; + } + int len=items.length; + for(int i=0;i blockList){ + T[] items=elementData; + if(items==null || items.length==0){ + return; + } + int len=items.length; + for(T block:blockList){ + if(block==null){ + continue; + } + int i=block.getIndex(); + if(i<0 || i>=len){ + continue; + } + if(items[i]!=block){ + continue; + } + items[i]=null; + } + trimNullBlocks(); + } + public boolean remove(T block){ + return remove(block, true); + } + protected boolean remove(T block, boolean trim){ + T[] items=elementData; + if(block==null||items==null){ + return false; + } + boolean found=false; + int len=items.length; + for(int i=0;isize){ + end=size; + }else { + end=index; + } + if(end>0){ + System.arraycopy(old, 0, update, 0, end); + } + for(int i=end;i extends BlockCreator{ + T[] newInstance(int len); +} diff --git a/src/ARSCLib/com/reandroid/arsc/base/BlockContainer.java b/src/ARSCLib/com/reandroid/arsc/base/BlockContainer.java new file mode 100755 index 00000000..4e52ec67 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/base/BlockContainer.java @@ -0,0 +1,156 @@ + /* + * 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 com.reandroid.arsc.container.BlockList; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class BlockContainer extends Block{ + public BlockContainer(){ + super(); + } + + protected void onPreRefreshRefresh(){ + + } + protected abstract void onRefreshed(); + public final void refresh(){ + if(isNull()){ + return; + } + onPreRefreshRefresh(); + refreshChildes(); + onRefreshed(); + } + protected void refreshChildes(){ + T[] childes=getChildes(); + if(childes!=null){ + int max=childes.length; + for(int i=0;i container=(BlockContainer)item; + container.refresh(); + }else if(item instanceof BlockList){ + BlockList blockList=(BlockList)item; + blockList.refresh(); + } + } + } + } + @Override + public void onCountUpTo(BlockCounter counter){ + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + T[] childes=getChildes(); + if(childes==null){ + return; + } + int max=childes.length; + for(int i=0;i { + T newInstance(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/Chunk.java b/src/ARSCLib/com/reandroid/arsc/chunk/Chunk.java new file mode 100755 index 00000000..c31124ed --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/Chunk.java @@ -0,0 +1,78 @@ + /* + * 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.chunk; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.container.ExpandableBlockContainer; +import com.reandroid.arsc.container.SingleBlockContainer; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; + +public abstract class Chunk extends ExpandableBlockContainer { + private final T mHeaderBlock; + protected final SingleBlockContainer firstPlaceHolder; + protected Chunk(T headerBlock, int initialChildesCount) { + super(initialChildesCount+2); + this.mHeaderBlock = headerBlock; + this.firstPlaceHolder = new SingleBlockContainer<>(); + addChild(headerBlock); + addChild(firstPlaceHolder); + } + public SingleBlockContainer getFirstPlaceHolder() { + return firstPlaceHolder; + } + void setHeaderLoaded(HeaderBlock.HeaderLoaded headerLoaded){ + getHeaderBlock().setHeaderLoaded(headerLoaded); + } + public final T getHeaderBlock(){ + return mHeaderBlock; + } + @Override + protected final void onRefreshed() { + getHeaderBlock().refreshHeader(); + onChunkRefreshed(); + } + protected abstract void onChunkRefreshed(); + public void onChunkLoaded(){ + + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + HeaderBlock headerBlock=reader.readHeaderBlock(); + checkInvalidChunk(headerBlock); + BlockReader chunkReader = reader.create(headerBlock.getChunkSize()); + super.onReadBytes(chunkReader); + reader.offset(headerBlock.getChunkSize()); + chunkReader.close(); + onChunkLoaded(); + } + void checkInvalidChunk(HeaderBlock headerBlock) throws IOException { + ChunkType chunkType = headerBlock.getChunkType(); + if(chunkType==null || chunkType==ChunkType.NULL){ + throw new IOException("Invalid chunk: "+headerBlock); + } + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append(": "); + builder.append(getHeaderBlock()); + return builder.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/ChunkType.java b/src/ARSCLib/com/reandroid/arsc/chunk/ChunkType.java new file mode 100755 index 00000000..70a58a6d --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/ChunkType.java @@ -0,0 +1,95 @@ +/* + * 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.chunk; + +import com.reandroid.arsc.util.HexUtil; + +public enum ChunkType { + NULL((short)0x0000), + + STRING((short)0x0001), + TABLE((short)0x0002), + XML((short)0x0003), + + XML_START_NAMESPACE((short)0x0100), + XML_END_NAMESPACE((short)0x0101), + XML_START_ELEMENT((short)0x0102), + XML_END_ELEMENT((short)0x0103), + XML_CDATA((short)0x0104), + XML_LAST_CHUNK((short)0x017f), + XML_RESOURCE_MAP((short)0x0180), + + PACKAGE((short)0x0200), + TYPE((short)0x0201), + SPEC((short)0x0202), + LIBRARY((short)0x0203), + OVERLAYABLE((short)0x0204), + OVERLAYABLE_POLICY((short)0x0205), + STAGED_ALIAS((short)0x0206); + + public final short ID; + ChunkType(short id) { + this.ID = id; + } + + @Override + public String toString(){ + return name() + "(" + HexUtil.toHex4(ID) + ")"; + } + + public static ChunkType get(short id){ + ChunkType[] all=values(); + for(ChunkType t:all){ + if(t.ID ==id){ + return t; + } + } + return null; + } + + public static ChunkType getTable(short id){ + for(ChunkType t:table_chunk_types){ + if(t.ID ==id){ + return t; + } + } + return null; + } + public static ChunkType getXml(short id){ + for(ChunkType t:xml_chunk_types){ + if(t.ID ==id){ + return t; + } + } + return null; + } + + private static final ChunkType[] table_chunk_types=new ChunkType[]{ + PACKAGE, + TYPE, + SPEC, + LIBRARY + }; + private static final ChunkType[] xml_chunk_types=new ChunkType[]{ + XML_START_NAMESPACE, + XML_END_NAMESPACE, + XML_START_ELEMENT, + XML_END_ELEMENT, + XML_CDATA, + XML_LAST_CHUNK, + XML_RESOURCE_MAP + }; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/LibraryBlock.java b/src/ARSCLib/com/reandroid/arsc/chunk/LibraryBlock.java new file mode 100755 index 00000000..aeca72d9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/LibraryBlock.java @@ -0,0 +1,76 @@ + /* + * 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.chunk; + +import com.reandroid.arsc.array.LibraryInfoArray; +import com.reandroid.arsc.header.LibraryHeader; +import com.reandroid.arsc.value.LibraryInfo; + +import java.util.Collection; + + public class LibraryBlock extends Chunk { + private final LibraryInfoArray mLibraryInfoArray; + public LibraryBlock() { + super(new LibraryHeader(),1); + LibraryHeader header = getHeaderBlock(); + this.mLibraryInfoArray = new LibraryInfoArray(header.getCount()); + + addChild(mLibraryInfoArray); + } + public LibraryInfoArray getLibraryInfoArray(){ + return mLibraryInfoArray; + } + public void addLibraryInfo(LibraryBlock libraryBlock){ + if(libraryBlock==null){ + return; + } + for(LibraryInfo info:libraryBlock.getLibraryInfoArray().listItems()){ + addLibraryInfo(info); + } + } + public void addLibraryInfo(LibraryInfo info){ + if(info==null){ + return; + } + getLibraryInfoArray().add(info); + getHeaderBlock().getCount().set(mLibraryInfoArray.childesCount()); + } + public Collection listLibraryInfo(){ + return getLibraryInfoArray().listItems(); + } + @Override + public boolean isNull(){ + return mLibraryInfoArray.childesCount()==0; + } + public int getLibraryCount(){ + return mLibraryInfoArray.childesCount(); + } + public void setLibraryCount(int count){ + getHeaderBlock().getCount().set(count); + mLibraryInfoArray.setChildesCount(count); + } + @Override + protected void onChunkRefreshed() { + getHeaderBlock().getCount().set(mLibraryInfoArray.childesCount()); + } + + public void merge(LibraryBlock libraryBlock){ + if(libraryBlock==null||libraryBlock==this){ + return; + } + getLibraryInfoArray().merge(libraryBlock.getLibraryInfoArray()); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/MainChunk.java b/src/ARSCLib/com/reandroid/arsc/chunk/MainChunk.java new file mode 100644 index 00000000..0782fb90 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/MainChunk.java @@ -0,0 +1,11 @@ +package com.reandroid.arsc.chunk; + +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.pool.StringPool; + +public interface MainChunk { + StringPool getStringPool(); + ApkFile getApkFile(); + void setApkFile(ApkFile apkFile); + TableBlock getTableBlock(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/Overlayable.java b/src/ARSCLib/com/reandroid/arsc/chunk/Overlayable.java new file mode 100644 index 00000000..ae8bbbcf --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/Overlayable.java @@ -0,0 +1,169 @@ + /* + * 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.chunk; + + import com.reandroid.arsc.container.BlockList; + import com.reandroid.arsc.header.HeaderBlock; + import com.reandroid.arsc.header.OverlayableHeader; + import com.reandroid.arsc.io.BlockReader; + import com.reandroid.arsc.item.ByteArray; + import com.reandroid.json.JSONArray; + import com.reandroid.json.JSONConvert; + import com.reandroid.json.JSONObject; + + import java.io.IOException; + import java.util.List; + + /** + * Replica of struct "ResTable_overlayable_header" as on AOSP androidfw/ResourceTypes.h + * We didn't test this class with resource table, if someone found a resource/apk please + * create issue on https://github.com/REAndroid/ARSCLib + * */ + public class Overlayable extends Chunk implements JSONConvert { + private final BlockList policyList; + private final ByteArray extraBytes; + + public Overlayable() { + super(new OverlayableHeader(), 2); + this.policyList = new BlockList<>(); + this.extraBytes = new ByteArray(); + addChild(this.policyList); + addChild(this.extraBytes); + } + + public OverlayablePolicy get(int flags){ + for(OverlayablePolicy policy:listOverlayablePolicies()){ + if(flags==policy.getFlags()){ + return policy; + } + } + return null; + } + public void addOverlayablePolicy(OverlayablePolicy overlayablePolicy){ + this.policyList.add(overlayablePolicy); + } + public List listOverlayablePolicies() { + return policyList.getChildes(); + } + public ByteArray getExtraBytes() { + return extraBytes; + } + + public String getName(){ + return getHeaderBlock().getName().get(); + } + public void setName(String str){ + getHeaderBlock().getName().set(str); + } + public String getActor(){ + return getHeaderBlock().getActor().get(); + } + public void setActor(String str){ + getHeaderBlock().getActor().set(str); + } + + @Override + protected void onChunkRefreshed() { + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException { + HeaderBlock headerBlock = reader.readHeaderBlock(); + checkInvalidChunk(headerBlock); + + int size = headerBlock.getChunkSize(); + BlockReader chunkReader = reader.create(size); + headerBlock = getHeaderBlock(); + headerBlock.readBytes(chunkReader); + + readOverlayablePlolicies(chunkReader); + readExtraBytes(chunkReader); + + reader.offset(size); + chunkReader.close(); + onChunkLoaded(); + } + private void readOverlayablePlolicies(BlockReader reader) throws IOException { + HeaderBlock headerBlock = reader.readHeaderBlock(); + BlockList policyList = this.policyList; + while (headerBlock!=null && headerBlock.getChunkType()==ChunkType.OVERLAYABLE_POLICY){ + OverlayablePolicy policy = new OverlayablePolicy(); + policyList.add(policy); + policy.readBytes(reader); + headerBlock = reader.readHeaderBlock(); + } + } + private void readExtraBytes(BlockReader reader) throws IOException { + int remaining = reader.available(); + this.extraBytes.setSize(remaining); + this.extraBytes.readBytes(reader); + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(NAME_name, getName()); + jsonObject.put(NAME_actor, getActor()); + JSONArray jsonArray = new JSONArray(); + for(OverlayablePolicy policy:listOverlayablePolicies()){ + jsonArray.put(policy.toJson()); + } + jsonObject.put(NAME_policies, jsonArray); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setName(json.optString(NAME_name)); + setActor(json.optString(NAME_actor)); + JSONArray jsonArray = json.getJSONArray(NAME_policies); + int length = jsonArray.length(); + BlockList policyList = this.policyList; + for(int i=0;i implements BlockLoad, + JSONConvert { + private final IntegerArray tableRefArray; + public OverlayablePolicy(){ + super(new OverlayablePolicyHeader(), 1); + this.tableRefArray = new IntegerArray(); + + addChild(this.tableRefArray); + + getHeaderBlock().getEntryCount().setBlockLoad(this); + } + @Override + public boolean isNull() { + return getTableReferenceCount()==0; + } + public int getTableReferenceCount(){ + return getTableRefArray().size(); + } + + public Collection listTableReferences(){ + return getTableRefArray().toList(); + } + public IntegerArray getTableRefArray() { + return tableRefArray; + } + public int getFlags() { + return getHeaderBlock().getFlags().get(); + } + public void setFlags(int flags){ + getHeaderBlock().getFlags().set(flags); + } + public void setFlags(PolicyFlag[] policyFlags){ + setFlags(PolicyFlag.sum(policyFlags)); + } + public void addFlag(PolicyFlag policyFlag){ + int i = policyFlag==null? 0 : policyFlag.getFlagValue(); + setFlags(getFlags() | i); + } + public PolicyFlag[] getPolicyFlags(){ + return PolicyFlag.valuesOf(getFlags()); + } + @Override + protected void onChunkRefreshed() { + getHeaderBlock().getEntryCount().set(getTableRefArray().size()); + } + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + IntegerItem entryCount = getHeaderBlock().getEntryCount(); + if(sender==entryCount){ + this.tableRefArray.setSize(entryCount.get()); + } + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(NAME_flags, getFlags()); + JSONArray jsonArray = new JSONArray(); + for(Integer reference:listTableReferences()){ + jsonArray.put(reference); + } + jsonObject.put(NAME_references, jsonArray); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setFlags(json.getInt(NAME_flags)); + JSONArray jsonArray = json.getJSONArray(NAME_references); + IntegerArray integerArray = getTableRefArray(); + int length = jsonArray.length(); + integerArray.setSize(length); + for(int i=0;i + implements ParentChunk, + JSONConvert, + Comparable { + + private final TypeStringPool mTypeStringPool; + private final SpecStringPool mSpecStringPool; + + private final PackageBody mBody; + + private final Map mEntriesGroup; + private boolean entryGroupMapLocked; + + public PackageBlock() { + super(new PackageHeader(), 3); + PackageHeader header = getHeaderBlock(); + + this.mTypeStringPool=new TypeStringPool(false, header.getTypeIdOffsetItem()); + this.mSpecStringPool=new SpecStringPool(true); + + this.mBody = new PackageBody(); + + this.mEntriesGroup = new HashMap<>(); + this.entryGroupMapLocked = true; + + addChild(mTypeStringPool); + addChild(mSpecStringPool); + addChild(mBody); + } + public void linkTableStringsInternal(TableStringPool tableStringPool){ + for(SpecTypePair specTypePair : listSpecTypePairs()){ + specTypePair.linkTableStringsInternal(tableStringPool); + } + } + public void linkSpecStringsInternal(SpecStringPool specStringPool){ + for(SpecTypePair specTypePair : listSpecTypePairs()){ + specTypePair.linkSpecStringsInternal(specStringPool); + } + } + public void destroy(){ + getEntriesGroupMap().clear(); + getPackageBody().destroy(); + getTypeStringPool().destroy(); + getSpecStringPool().destroy(); + setId(0); + setName(""); + } + public Entry getEntry(String qualifiers, String type, String name){ + return getSpecTypePairArray().getEntry(qualifiers, type, name); + } + public Entry getEntry(ResConfig resConfig, String type, String name){ + return getSpecTypePairArray().getEntry(resConfig, type, name); + } + public Entry getOrCreate(String qualifiers, String type, String name){ + return getOrCreate(ResConfig.parse(qualifiers), type, name); + } + public Entry getOrCreate(ResConfig resConfig, String typeName, String name){ + SpecTypePair specTypePair = getOrCreateSpecTypePair(typeName); + TypeBlock typeBlock = specTypePair.getOrCreateTypeBlock(resConfig); + return typeBlock.getOrCreateEntry(name); + } + public TypeBlock getOrCreateTypeBlock(String qualifiers, String typeName){ + SpecTypePair specTypePair = getOrCreateSpecTypePair(typeName); + return specTypePair.getOrCreateTypeBlock(qualifiers); + } + public TypeBlock getOrCreateTypeBlock(ResConfig resConfig, String typeName){ + SpecTypePair specTypePair = getOrCreateSpecTypePair(typeName); + return specTypePair.getOrCreateTypeBlock(resConfig); + } + public SpecTypePair getOrCreateSpecTypePair(String typeName){ + return getSpecTypePairArray().getOrCreate(typeName); + } + public int getTypeIdOffset(){ + return getHeaderBlock().getTypeIdOffset(); + } + public BlockList getUnknownChunkList(){ + return mBody.getUnknownChunkList(); + } + + public StagedAliasEntry searchByStagedResId(int stagedResId){ + for(StagedAlias stagedAlias:getStagedAliasList().getChildes()){ + StagedAliasEntry entry=stagedAlias.getStagedAliasEntryArray() + .searchByStagedResId(stagedResId); + if(entry!=null){ + return entry; + } + } + return null; + } + public List listStagedAlias(){ + return getStagedAliasList().getChildes(); + } + public StagedAliasList getStagedAliasList(){ + return mBody.getStagedAliasList(); + } + public OverlayableList getOverlayableList(){ + return mBody.getOverlayableList(); + } + public BlockList getOverlayablePolicyList(){ + return mBody.getOverlayablePolicyList(); + } + public void sortTypes(){ + getSpecTypePairArray().sort(); + } + + public void removeEmpty(){ + getSpecTypePairArray().removeEmptyPairs(); + } + public boolean isEmpty(){ + return getSpecTypePairArray().isEmpty(); + } + public int getId(){ + return getHeaderBlock().getPackageId().get(); + } + public void setId(byte id){ + setId(0xff & id); + } + public void setId(int id){ + getHeaderBlock().getPackageId().set(id); + } + public String getName(){ + return getHeaderBlock().getPackageName().get(); + } + public void setName(String name){ + getHeaderBlock().getPackageName().set(name); + } + public TableBlock getTableBlock(){ + Block parent=getParent(); + while(parent!=null){ + if(parent instanceof TableBlock){ + return (TableBlock)parent; + } + parent=parent.getParent(); + } + return null; + } + public TypeStringPool getTypeStringPool(){ + return mTypeStringPool; + } + @Override + public SpecStringPool getSpecStringPool(){ + return mSpecStringPool; + } + @Override + public TableBlock getMainChunk(){ + return getTableBlock(); + } + @Override + public PackageBlock getPackageBlock(){ + return this; + } + public PackageBody getPackageBody() { + return mBody; + } + public SpecTypePairArray getSpecTypePairArray(){ + return mBody.getSpecTypePairArray(); + } + public Collection listLibraryInfo(){ + return getLibraryBlock().listLibraryInfo(); + } + + public void addLibrary(LibraryBlock libraryBlock){ + if(libraryBlock==null){ + return; + } + for(LibraryInfo info:libraryBlock.getLibraryInfoArray().listItems()){ + addLibraryInfo(info); + } + } + public void addLibraryInfo(LibraryInfo info){ + getLibraryBlock().addLibraryInfo(info); + } + public LibraryBlock getLibraryBlock(){ + return mBody.getLibraryBlock(); + } + public Set listResourceIds(){ + return getEntriesGroupMap().keySet(); + } + public Entry getOrCreateEntry(byte typeId, short entryId, String qualifiers){ + return getSpecTypePairArray().getOrCreateEntry(typeId, entryId, qualifiers); + } + + public Entry getAnyEntry(int resourceId){ + int packageId = (resourceId >> 24) & 0xff; + if(packageId != getId()){ + return null; + } + byte typeId = (byte) ((resourceId >> 16) & 0xff); + short entryId = (short) (resourceId & 0xffff); + return getSpecTypePairArray().getAnyEntry(typeId, entryId); + } + public Entry getEntry(byte typeId, short entryId, String qualifiers){ + return getSpecTypePairArray().getEntry(typeId, entryId, qualifiers); + } + public TypeBlock getOrCreateTypeBlock(byte typeId, String qualifiers){ + return getSpecTypePairArray().getOrCreateTypeBlock(typeId, qualifiers); + } + public TypeBlock getTypeBlock(byte typeId, String qualifiers){ + return getSpecTypePairArray().getTypeBlock(typeId, qualifiers); + } + + private void unlockEntryGroup() { + synchronized (this){ + if(!this.entryGroupMapLocked){ + return; + } + entryGroupMapLocked = false; + Map map = this.mEntriesGroup; + map.clear(); + createEntryGroupMap(map); + } + } + private void createEntryGroupMap(Map map){ + map.clear(); + for(SpecTypePair specTypePair : listSpecTypePairs()){ + map.putAll(specTypePair.createEntryGroups(true)); + } + } + public Map getEntriesGroupMap(){ + unlockEntryGroup(); + return mEntriesGroup; + } + public Collection listEntryGroup(){ + return getEntriesGroupMap().values(); + } + + /** + * Searches entries by resource id from local map, then if not find + * search by alias resource id + * */ + public EntryGroup getEntryGroup(int resourceId){ + if(resourceId==0){ + return null; + } + EntryGroup entryGroup=getEntriesGroupMap().get(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + StagedAliasEntry stagedAliasEntry = searchByStagedResId(resourceId); + if(stagedAliasEntry!=null){ + return getEntriesGroupMap() + .get(stagedAliasEntry.getFinalizedResId()); + } + return null; + } + public void updateEntry(Entry entry){ + if(this.entryGroupMapLocked){ + return; + } + if(entry == null || entry.isNull()){ + return; + } + int resourceId = entry.getResourceId(); + Map map = getEntriesGroupMap(); + EntryGroup group = map.get(resourceId); + if(group == null){ + group = new EntryGroup(resourceId); + map.put(resourceId, group); + } + group.add(entry); + } + public void removeEntryGroup(Entry entry){ + if(entry == null){ + return; + } + int resourceId = entry.getResourceId(); + Map map = getEntriesGroupMap(); + EntryGroup group = map.get(resourceId); + if(group == null){ + return; + } + group.remove(entry); + if(group.size() == 0){ + map.remove(resourceId); + } + } + public List listEntries(byte typeId, int entryId){ + List results=new ArrayList<>(); + for(SpecTypePair pair:listSpecTypePair(typeId)){ + results.addAll(pair.listEntries(entryId)); + } + return results; + } + public List listSpecTypePair(byte typeId){ + List results = new ArrayList<>(); + for(SpecTypePair specTypePair : listSpecTypePairs()){ + if(typeId == specTypePair.getTypeId()){ + results.add(specTypePair); + } + } + return results; + } + public SpecTypePair getSpecTypePair(String typeName){ + return getSpecTypePairArray().getSpecTypePair(typeName); + } + public SpecTypePair getSpecTypePair(int typeId){ + return getSpecTypePairArray().getSpecTypePair((byte) typeId); + } + public EntryGroup getEntryGroup(String typeName, String entryName){ + return getSpecTypePairArray().getEntryGroup(typeName, entryName); + } + public Collection listSpecTypePairs(){ + return getSpecTypePairArray().listItems(); + } + /** + * Use listSpecTypePairs() + * */ + @Deprecated + public Collection listAllSpecTypePair(){ + return listSpecTypePairs(); + } + + private void refreshTypeStringPoolOffset(){ + int pos=countUpTo(mTypeStringPool); + getHeaderBlock().getTypeStringPoolOffset().set(pos); + } + private void refreshTypeStringPoolCount(){ + getHeaderBlock().getTypeStringPoolCount().set(mTypeStringPool.countStrings()); + } + private void refreshSpecStringPoolOffset(){ + int pos=countUpTo(mSpecStringPool); + getHeaderBlock().getSpecStringPoolOffset().set(pos); + } + private void refreshSpecStringCount(){ + getHeaderBlock().getSpecStringPoolCount().set(mSpecStringPool.countStrings()); + } + private void refreshTypeIdOffset(){ + // TODO: find solution + //int largest=getSpecTypePairArray().getHighestTypeId(); + //int count=getTypeStringPool().countStrings(); + //getHeaderBlock().getTypeIdOffset().set(count-largest); + getHeaderBlock().getTypeIdOffsetItem().set(0); + } + public void onEntryAdded(Entry entry){ + updateEntry(entry); + } + @Override + public void onChunkLoaded() { + } + + @Override + protected void onChunkRefreshed() { + refreshTypeStringPoolOffset(); + refreshTypeStringPoolCount(); + refreshSpecStringPoolOffset(); + refreshSpecStringCount(); + refreshTypeIdOffset(); + } + + @Override + public JSONObject toJson() { + return toJson(true); + } + public JSONObject toJson(boolean addTypes) { + JSONObject jsonObject=new JSONObject(); + + jsonObject.put(BuildInfo.NAME_arsc_lib_version, BuildInfo.getVersion()); + + jsonObject.put(NAME_package_id, getId()); + jsonObject.put(NAME_package_name, getName()); + jsonObject.put(NAME_specs, getSpecTypePairArray().toJson(!addTypes)); + LibraryInfoArray libraryInfoArray = getLibraryBlock().getLibraryInfoArray(); + if(libraryInfoArray.childesCount()>0){ + jsonObject.put(NAME_libraries,libraryInfoArray.toJson()); + } + StagedAlias stagedAlias = + StagedAlias.mergeAll(getStagedAliasList().getChildes()); + if(stagedAlias!=null){ + jsonObject.put(NAME_staged_aliases, + stagedAlias.getStagedAliasEntryArray().toJson()); + } + JSONArray jsonArray = getOverlayableList().toJson(); + if(jsonArray!=null){ + jsonObject.put(NAME_overlaybles, jsonArray); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setId(json.getInt(NAME_package_id)); + setName(json.getString(NAME_package_name)); + getSpecTypePairArray().fromJson(json.optJSONArray(NAME_specs)); + LibraryInfoArray libraryInfoArray = getLibraryBlock().getLibraryInfoArray(); + libraryInfoArray.fromJson(json.optJSONArray(NAME_libraries)); + if(json.has(NAME_staged_aliases)){ + StagedAlias stagedAlias=new StagedAlias(); + stagedAlias.getStagedAliasEntryArray() + .fromJson(json.getJSONArray(NAME_staged_aliases)); + getStagedAliasList().add(stagedAlias); + } + if(json.has(NAME_overlaybles)){ + getOverlayableList().fromJson(json.getJSONArray(NAME_overlaybles)); + } + } + public void merge(PackageBlock packageBlock){ + if(packageBlock==null||packageBlock==this){ + return; + } + if(getId()!=packageBlock.getId()){ + throw new IllegalArgumentException("Can not merge different id packages: " + +getId()+"!="+packageBlock.getId()); + } + setName(packageBlock.getName()); + getLibraryBlock().merge(packageBlock.getLibraryBlock()); + mergeSpecStringPool(packageBlock); + getSpecTypePairArray().merge(packageBlock.getSpecTypePairArray()); + getOverlayableList().merge(packageBlock.getOverlayableList()); + getStagedAliasList().merge(packageBlock.getStagedAliasList()); + } + private void mergeSpecStringPool(PackageBlock coming){ + this.getSpecStringPool().addStrings( + coming.getSpecStringPool().toStringList()); + } + + @Override + public int compareTo(PackageBlock pkg) { + return Integer.compare(getId(), pkg.getId()); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(super.toString()); + builder.append(", id="); + builder.append(HexUtil.toHex2((byte) getId())); + builder.append(", name="); + builder.append(getName()); + int libCount=getLibraryBlock().getLibraryCount(); + if(libCount>0){ + builder.append(", libraries="); + builder.append(libCount); + } + return builder.toString(); + } + + public static final String NAME_package_id = "package_id"; + public static final String NAME_package_name = "package_name"; + public static final String JSON_FILE_NAME = "package.json"; + private static final String NAME_specs = "specs"; + public static final String NAME_libraries = "libraries"; + public static final String NAME_staged_aliases = "staged_aliases"; + public static final String NAME_overlaybles = "overlaybles"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/ParentChunk.java b/src/ARSCLib/com/reandroid/arsc/chunk/ParentChunk.java new file mode 100644 index 00000000..325810a1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/ParentChunk.java @@ -0,0 +1,24 @@ +/* + * 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.chunk; + +import com.reandroid.arsc.pool.StringPool; + +public interface ParentChunk { + StringPool getSpecStringPool(); + MainChunk getMainChunk(); + PackageBlock getPackageBlock(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/SpecBlock.java b/src/ARSCLib/com/reandroid/arsc/chunk/SpecBlock.java new file mode 100755 index 00000000..65873ead --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/SpecBlock.java @@ -0,0 +1,163 @@ +/* + * 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.chunk; + +import com.reandroid.arsc.array.TypeBlockArray; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.header.SpecHeader; +import com.reandroid.arsc.item.*; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.util.List; + +public class SpecBlock extends Chunk implements JSONConvert { + private final SpecFlagsArray specFlagsArray; + public SpecBlock() { + super(new SpecHeader(), 1); + SpecHeader header = getHeaderBlock(); + this.specFlagsArray = new SpecFlagsArray(header.getEntryCount()); + addChild(specFlagsArray); + } + public void destroy(){ + setParent(null); + getSpecFlagsArray().clear(); + } + public SpecFlag getSpecFlag(int id){ + return getSpecFlagsArray().getFlag(id); + } + public SpecFlagsArray getSpecFlagsArray(){ + return specFlagsArray; + } + public List listSpecFlags(){ + return specFlagsArray.toList(); + } + public byte getTypeId(){ + return getHeaderBlock().getId().get(); + } + public int getId(){ + return getHeaderBlock().getId().unsignedInt(); + } + public void setId(int id){ + setTypeId((byte) (0xff & id)); + } + public void setTypeId(byte id){ + getHeaderBlock().getId().set(id); + getTypeBlockArray().setTypeId(id); + } + public TypeBlockArray getTypeBlockArray(){ + SpecTypePair specTypePair=getSpecTypePair(); + if(specTypePair!=null){ + return specTypePair.getTypeBlockArray(); + } + return null; + } + SpecTypePair getSpecTypePair(){ + return getParent(SpecTypePair.class); + } + public int getEntryCount() { + return specFlagsArray.size(); + } + public void setEntryCount(int count){ + specFlagsArray.setSize(count); + specFlagsArray.refresh(); + } + @Override + protected void onChunkRefreshed() { + specFlagsArray.refresh(); + } + + public void merge(SpecBlock specBlock){ + if(specBlock == null || specBlock==this){ + return; + } + this.getSpecFlagsArray().merge(specBlock.getSpecFlagsArray()); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(super.toString()); + TypeBlockArray typeBlockArray=getTypeBlockArray(); + if(typeBlockArray!=null){ + builder.append(", typesCount="); + builder.append(typeBlockArray.childesCount()); + } + return builder.toString(); + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(TypeBlock.NAME_id, getId()); + jsonObject.put(NAME_spec_flags, getSpecFlagsArray().toJson()); + return jsonObject; + } + + @Override + public void fromJson(JSONObject json) { + setId(json.getInt(TypeBlock.NAME_id)); + getSpecFlagsArray().fromJson(json.optJSONArray(NAME_spec_flags)); + } + + public enum Flag{ + SPEC_PUBLIC((byte) 0x40), + SPEC_STAGED_API((byte) 0x20); + + private final byte flag; + Flag(byte flag) { + this.flag = flag; + } + public byte getFlag() { + return flag; + } + public static boolean isPublic(byte flag){ + return (SPEC_PUBLIC.flag & flag) == SPEC_PUBLIC.flag; + } + public static boolean isStagedApi(byte flag){ + return (SPEC_STAGED_API.flag & flag) == SPEC_STAGED_API.flag; + } + public static String toString(byte flagValue){ + StringBuilder builder = new StringBuilder(); + boolean appendOnce = false; + int sum = 0; + int flagValueInt = flagValue & 0xff; + for(Flag flag:values()){ + int flagInt = flag.flag & 0xff; + if((flagInt & flagValueInt) != flagInt){ + continue; + } + if(appendOnce){ + builder.append('|'); + } + builder.append(flag); + appendOnce = true; + sum = sum | flagInt; + } + if(sum != flagValueInt){ + if(appendOnce){ + builder.append('|'); + } + builder.append(HexUtil.toHex2((byte) flagValueInt)); + } + return builder.toString(); + } + } + + public static final String NAME_spec = "spec"; + public static final String NAME_spec_flags = "spec_flags"; + public static final String NAME_flag = "flag"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/StagedAlias.java b/src/ARSCLib/com/reandroid/arsc/chunk/StagedAlias.java new file mode 100644 index 00000000..ed1421f6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/StagedAlias.java @@ -0,0 +1,83 @@ + /* + * 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.chunk; + + import com.reandroid.arsc.array.StagedAliasEntryArray; + import com.reandroid.arsc.header.StagedAliasHeader; + import com.reandroid.arsc.value.StagedAliasEntry; + + import java.util.Collection; + + public class StagedAlias extends Chunk { + private final StagedAliasEntryArray stagedAliasEntryArray; + public StagedAlias() { + super(new StagedAliasHeader(), 1); + StagedAliasHeader header = getHeaderBlock(); + + stagedAliasEntryArray = new StagedAliasEntryArray(header.getCount()); + addChild(stagedAliasEntryArray); + } + public void merge(StagedAlias stagedAlias){ + if(stagedAlias==null||stagedAlias==this){ + return; + } + StagedAliasEntryArray exist = getStagedAliasEntryArray(); + for(StagedAliasEntry entry:stagedAlias.listStagedAliasEntry()){ + if(!exist.contains(entry)){ + exist.add(entry); + } + } + } + public StagedAliasEntryArray getStagedAliasEntryArray() { + return stagedAliasEntryArray; + } + public Collection listStagedAliasEntry(){ + return getStagedAliasEntryArray().listItems(); + } + public int getStagedAliasEntryCount(){ + return getStagedAliasEntryArray().childesCount(); + } + @Override + public boolean isNull(){ + return getStagedAliasEntryCount()==0; + } + @Override + protected void onChunkRefreshed() { + getHeaderBlock().getCount().set(getStagedAliasEntryCount()); + } + @Override + public String toString(){ + return getClass().getSimpleName()+ + ": count="+getStagedAliasEntryCount(); + } + public static StagedAlias mergeAll(Collection stagedAliasList){ + if(stagedAliasList.size()==0){ + return null; + } + StagedAlias result=new StagedAlias(); + for(StagedAlias stagedAlias:stagedAliasList){ + if(stagedAlias.isNull()){ + continue; + } + result.merge(stagedAlias); + } + if(!result.isNull()){ + result.refresh(); + return result; + } + return null; + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/TableBlock.java b/src/ARSCLib/com/reandroid/arsc/chunk/TableBlock.java new file mode 100755 index 00000000..e56de1c6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/TableBlock.java @@ -0,0 +1,420 @@ +/* + * 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.chunk; + +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.BuildInfo; +import com.reandroid.arsc.array.PackageArray; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.InfoHeader; +import com.reandroid.arsc.header.TableHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.value.*; +import com.reandroid.common.EntryStore; +import com.reandroid.common.ReferenceResolver; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.*; +import java.util.*; +import java.util.function.Predicate; + +public class TableBlock extends Chunk + implements MainChunk, JSONConvert, EntryStore { + private final TableStringPool mTableStringPool; + private final PackageArray mPackageArray; + private final List mFrameWorks; + private ApkFile mApkFile; + private ReferenceResolver referenceResolver; + + public TableBlock() { + super(new TableHeader(), 2); + TableHeader header = getHeaderBlock(); + this.mTableStringPool = new TableStringPool(true); + this.mPackageArray = new PackageArray(header.getPackageCount()); + this.mFrameWorks = new ArrayList<>(); + addChild(mTableStringPool); + addChild(mPackageArray); + } + + public void linkTableStringsInternal(TableStringPool tableStringPool){ + for(PackageBlock packageBlock : listPackages()){ + packageBlock.linkTableStringsInternal(tableStringPool); + } + } + public List resolveReference(int referenceId){ + return resolveReference(referenceId, null); + } + public List resolveReferenceWithConfig(int referenceId, ResConfig resConfig){ + ReferenceResolver resolver = this.referenceResolver; + if(resolver == null){ + resolver = new ReferenceResolver(this); + this.referenceResolver = resolver; + } + return resolver.resolveWithConfig(referenceId, resConfig); + } + public List resolveReference(int referenceId, Predicate filter){ + ReferenceResolver resolver = this.referenceResolver; + if(resolver == null){ + resolver = new ReferenceResolver(this); + this.referenceResolver = resolver; + } + return resolver.resolveAll(referenceId, filter); + } + public void destroy(){ + getPackageArray().destroy(); + getStringPool().destroy(); + clearFrameworks(); + refresh(); + } + public int countPackages(){ + return getPackageArray().childesCount(); + } + + public PackageBlock pickOne(){ + return getPackageArray().pickOne(); + } + public PackageBlock pickOne(int packageId){ + return getPackageArray().pickOne(packageId); + } + public void sortPackages(){ + getPackageArray().sort(); + } + public Collection listPackages(){ + return getPackageArray().listItems(); + } + @Override + public TableStringPool getStringPool() { + return mTableStringPool; + } + @Override + public ApkFile getApkFile(){ + return mApkFile; + } + @Override + public void setApkFile(ApkFile apkFile){ + this.mApkFile = apkFile; + } + @Override + public TableBlock getTableBlock() { + return this; + } + + public TableStringPool getTableStringPool(){ + return mTableStringPool; + } + public PackageBlock getPackageBlockById(int pkgId){ + return getPackageArray().getPackageBlockById(pkgId); + } + public PackageArray getPackageArray(){ + return mPackageArray; + } + + private void refreshPackageCount(){ + int count = getPackageArray().childesCount(); + getHeaderBlock().getPackageCount().set(count); + } + @Override + protected void onChunkRefreshed() { + refreshPackageCount(); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + TableHeader tableHeader = getHeaderBlock(); + tableHeader.readBytes(reader); + if(tableHeader.getChunkType()!=ChunkType.TABLE){ + throw new IOException("Not resource table: "+tableHeader); + } + boolean stringPoolLoaded=false; + InfoHeader infoHeader = reader.readHeaderBlock(); + PackageArray packageArray=mPackageArray; + packageArray.clearChildes(); + while(infoHeader!=null && reader.isAvailable()){ + ChunkType chunkType=infoHeader.getChunkType(); + if(chunkType==ChunkType.STRING){ + if(!stringPoolLoaded){ + mTableStringPool.readBytes(reader); + stringPoolLoaded=true; + } + }else if(chunkType==ChunkType.PACKAGE){ + PackageBlock packageBlock=packageArray.createNext(); + packageBlock.readBytes(reader); + }else { + UnknownChunk unknownChunk=new UnknownChunk(); + unknownChunk.readBytes(reader); + addChild(unknownChunk); + } + infoHeader=reader.readHeaderBlock(); + } + reader.close(); + } + + public void readBytes(File file) throws IOException{ + BlockReader reader=new BlockReader(file); + super.readBytes(reader); + } + public void readBytes(InputStream inputStream) throws IOException{ + BlockReader reader=new BlockReader(inputStream); + super.readBytes(reader); + } + public final int writeBytes(File file) throws IOException{ + if(isNull()){ + throw new IOException("Can NOT save null block"); + } + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + OutputStream outputStream=new FileOutputStream(file); + int length = super.writeBytes(outputStream); + outputStream.close(); + return length; + } + + public Entry getAnyEntry(int resourceId){ + if(resourceId == 0 || ((resourceId >> 16) & 0xff) == 0){ + return null; + } + Entry result = null; + for(PackageBlock packageBlock : listPackages()){ + Entry entry = packageBlock.getAnyEntry(resourceId); + if(entry == null){ + continue; + } + if(!entry.isNull()){ + return entry; + } + if(result == null){ + result = entry; + } + } + if(result != null){ + return result; + } + for(TableBlock tableBlock : getFrameWorks()){ + Entry entry = tableBlock.getAnyEntry(resourceId); + if(entry == null){ + continue; + } + if(!entry.isNull()){ + return entry; + } + if(result == null){ + result = entry; + } + } + return result; + } + public EntryGroup search(int resourceId){ + if(resourceId==0){ + return null; + } + EntryGroup entryGroup = searchLocal(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + for(TableBlock tableBlock:getFrameWorks()){ + entryGroup = tableBlock.search(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + } + return null; + } + private EntryGroup searchLocal(int resourceId){ + if(resourceId==0){ + return null; + } + int aliasId = searchResourceIdAlias(resourceId); + for(PackageBlock packageBlock:listPackages()){ + EntryGroup entryGroup = packageBlock.getEntryGroup(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + entryGroup = packageBlock.getEntryGroup(aliasId); + if(entryGroup!=null){ + return entryGroup; + } + } + return null; + } + @Override + public Collection getEntryGroups(int resourceId) { + List results = new ArrayList<>(); + EntryGroup entryGroup = searchLocal(resourceId); + if(entryGroup!=null){ + results.add(entryGroup); + } + for(TableBlock framework:getFrameWorks()){ + results.addAll(framework.getEntryGroups(resourceId)); + } + return results; + } + @Override + public EntryGroup getEntryGroup(int resourceId) { + return search(resourceId); + } + @Override + public Collection getPackageBlocks(int packageId) { + List results=new ArrayList<>(); + PackageBlock packageBlock = getPackageBlockById(packageId); + if(packageBlock!=null){ + results.add(packageBlock); + } + for(TableBlock tableBlock:getFrameWorks()){ + results.addAll(tableBlock.getPackageBlocks(packageId)); + } + return results; + } + public int searchResourceIdAlias(int resourceId){ + for(PackageBlock packageBlock:listPackages()){ + StagedAliasEntry stagedAliasEntry = + packageBlock.searchByStagedResId(resourceId); + if(stagedAliasEntry!=null){ + return stagedAliasEntry.getFinalizedResId(); + } + } + return 0; + } + public List getFrameWorks(){ + return mFrameWorks; + } + public boolean isAndroid(){ + PackageBlock packageBlock = pickOne(); + if(packageBlock == null){ + return false; + } + return "android".equals(packageBlock.getName()) + && packageBlock.getId() == 0x01; + } + public boolean hasFramework(){ + return getFrameWorks().size() != 0; + } + public void addFramework(TableBlock tableBlock){ + if(tableBlock==null||tableBlock==this){ + return; + } + for(TableBlock frm:tableBlock.getFrameWorks()){ + if(frm==this || frm==tableBlock || tableBlock.equals(frm)){ + return; + } + } + mFrameWorks.add(tableBlock); + } + public void removeFramework(TableBlock tableBlock){ + mFrameWorks.remove(tableBlock); + } + public void clearFrameworks(){ + mFrameWorks.clear(); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + + jsonObject.put(BuildInfo.NAME_arsc_lib_version, BuildInfo.getVersion()); + + jsonObject.put(NAME_packages, getPackageArray().toJson()); + JSONArray jsonArray = getStringPool().toJson(); + if(jsonArray!=null){ + jsonObject.put(NAME_styled_strings, jsonArray); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + getPackageArray().fromJson(json.getJSONArray(NAME_packages)); + refresh(); + } + public void merge(TableBlock tableBlock){ + if(tableBlock==null||tableBlock==this){ + return; + } + if(countPackages()==0 && getStringPool().countStrings()==0){ + getStringPool().merge(tableBlock.getStringPool()); + } + getPackageArray().merge(tableBlock.getPackageArray()); + refresh(); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(super.toString()); + builder.append(", packages="); + int pkgCount=mPackageArray.childesCount(); + builder.append(pkgCount); + return builder.toString(); + } + + @Deprecated + public static TableBlock loadWithAndroidFramework(InputStream inputStream) throws IOException{ + return load(inputStream); + } + public static TableBlock load(File file) throws IOException{ + return load(new FileInputStream(file)); + } + public static TableBlock load(InputStream inputStream) throws IOException{ + TableBlock tableBlock=new TableBlock(); + tableBlock.readBytes(inputStream); + return tableBlock; + } + + public static boolean isResTableBlock(File file){ + if(file==null){ + return false; + } + boolean result=false; + try { + InputStream inputStream=new FileInputStream(file); + result=isResTableBlock(inputStream); + inputStream.close(); + } catch (IOException ignored) { + } + return result; + } + public static boolean isResTableBlock(InputStream inputStream){ + try { + HeaderBlock headerBlock= BlockReader.readHeaderBlock(inputStream); + return isResTableBlock(headerBlock); + } catch (IOException ignored) { + return false; + } + } + public static boolean isResTableBlock(BlockReader blockReader){ + if(blockReader==null){ + return false; + } + try { + HeaderBlock headerBlock = blockReader.readHeaderBlock(); + return isResTableBlock(headerBlock); + } catch (IOException ignored) { + return false; + } + } + public static boolean isResTableBlock(HeaderBlock headerBlock){ + if(headerBlock==null){ + return false; + } + ChunkType chunkType=headerBlock.getChunkType(); + return chunkType==ChunkType.TABLE; + } + public static final String FILE_NAME="resources.arsc"; + + private static final String NAME_packages="packages"; + public static final String NAME_styled_strings="styled_strings"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/TypeBlock.java b/src/ARSCLib/com/reandroid/arsc/chunk/TypeBlock.java new file mode 100755 index 00000000..74bb3a60 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/TypeBlock.java @@ -0,0 +1,376 @@ +/* + * 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.chunk; + +import com.reandroid.arsc.array.EntryArray; +import com.reandroid.arsc.array.OffsetArray; +import com.reandroid.arsc.array.SparseOffsetsArray; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.header.TypeHeader; +import com.reandroid.arsc.item.*; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.pool.TypeStringPool; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class TypeBlock extends Chunk + implements JSONConvert, Comparable { + + private final EntryArray mEntryArray; + private TypeString mTypeString; + public TypeBlock(boolean sparse) { + super(new TypeHeader(sparse), 2); + TypeHeader header = getHeaderBlock(); + + OffsetArray entryOffsets; + if(sparse){ + entryOffsets = new SparseOffsetsArray(); + }else { + entryOffsets = new OffsetArray(); + } + this.mEntryArray = new EntryArray(entryOffsets, + header.getCount(), header.getEntriesStart()); + + addChild(entryOffsets); + addChild(mEntryArray); + } + public void linkTableStringsInternal(TableStringPool tableStringPool){ + EntryArray entryArray = getEntryArray(); + entryArray.linkTableStringsInternal(tableStringPool); + } + public void linkSpecStringsInternal(SpecStringPool specStringPool){ + EntryArray entryArray = getEntryArray(); + entryArray.linkSpecStringsInternal(specStringPool); + } + public boolean isSparse(){ + return getHeaderBlock().isSparse(); + } + public void destroy(){ + getEntryArray().destroy(); + setId(0); + setParent(null); + } + public boolean removeNullEntries(int startId){ + startId = 0x0000ffff & startId; + EntryArray entryArray = getEntryArray(); + entryArray.removeAllNull(startId); + return entryArray.childesCount() == startId; + } + public PackageBlock getPackageBlock(){ + SpecTypePair specTypePair = getParent(SpecTypePair.class); + if(specTypePair!=null){ + return specTypePair.getPackageBlock(); + } + return null; + } + public String getTypeName(){ + TypeString typeString=getTypeString(); + if(typeString==null){ + return null; + } + return typeString.get(); + } + public TypeString getTypeString(){ + if(mTypeString!=null){ + if(mTypeString.getId()==getTypeId()){ + return mTypeString; + } + mTypeString=null; + } + PackageBlock packageBlock=getPackageBlock(); + if(packageBlock==null){ + return null; + } + TypeStringPool typeStringPool=packageBlock.getTypeStringPool(); + mTypeString=typeStringPool.getById(getId()); + return mTypeString; + } + public byte getTypeId(){ + return getHeaderBlock().getId().get(); + } + public int getId(){ + return getHeaderBlock().getId().unsignedInt(); + } + public void setId(int id){ + setTypeId((byte) (0xff & id)); + } + public void setTypeId(byte id){ + getHeaderBlock().getId().set(id); + } + public void setTypeName(String name){ + TypeStringPool typeStringPool=getTypeStringPool(); + int id= getId(); + TypeString typeString=typeStringPool.getById(id); + if(typeString==null){ + typeString=typeStringPool.getOrCreate(id, name); + } + typeString.set(name); + } + private TypeStringPool getTypeStringPool(){ + PackageBlock packageBlock=getPackageBlock(); + if(packageBlock!=null){ + return packageBlock.getTypeStringPool(); + } + return null; + } + public void setEntryCount(int count){ + IntegerItem entryCount = getHeaderBlock().getCount(); + if(count == entryCount.get()){ + return; + } + entryCount.set(count); + onSetEntryCount(count); + } + public boolean isEmpty(){ + return getEntryArray().isEmpty(); + } + public boolean isDefault(){ + return getResConfig().isDefault(); + } + public String getQualifiers(){ + return getResConfig().getQualifiers(); + } + public void setQualifiers(String qualifiers){ + getResConfig().parseQualifiers(qualifiers); + } + public int countNonNullEntries(){ + return getEntryArray().countNonNull(); + } + public SpecTypePair getParentSpecTypePair(){ + return getParent(SpecTypePair.class); + } + public void cleanEntries(){ + PackageBlock packageBlock=getPackageBlock(); + List allEntries=listEntries(true); + for(Entry entry :allEntries){ + if(packageBlock!=null){ + packageBlock.removeEntryGroup(entry); + } + entry.setNull(true); + } + } + public void removeEntry(Entry entry){ + PackageBlock packageBlock=getPackageBlock(); + if(packageBlock!=null){ + packageBlock.removeEntryGroup(entry); + } + entry.setNull(true); + } + public Entry getOrCreateEntry(String name){ + Entry entry = getEntryArray().getEntry(name); + if(entry != null){ + return entry; + } + SpecTypePair specTypePair = getParentSpecTypePair(); + Entry exist = specTypePair.getAnyEntry(name); + int id; + if(exist!=null){ + id = exist.getId(); + }else { + id = specTypePair.getHighestEntryCount(); + } + SpecString specString = getPackageBlock() + .getSpecStringPool().getOrCreate(name); + entry = getOrCreateEntry((short) id); + if(entry.isNull()){ + Boolean hasComplex = hasComplexEntry(); + if(hasComplex != null){ + entry.ensureComplex(hasComplex); + } + } + entry.setSpecReference(specString.getIndex()); + return entry; + } + public Entry getOrCreateEntry(short entryId){ + return getEntryArray().getOrCreate(entryId); + } + public Entry getEntry(short entryId){ + return getEntryArray().getEntry(entryId); + } + /** + * It is allowed to have duplicate entry name therefore it is not recommend to use this. + */ + public Entry getEntry(String entryName){ + return getEntryArray().getEntry(entryName); + } + public Boolean hasComplexEntry(){ + SpecTypePair specTypePair = getParentSpecTypePair(); + if(specTypePair != null){ + return specTypePair.hasComplexEntry(); + } + return null; + } + public ResConfig getResConfig(){ + return getHeaderBlock().getConfig(); + } + public EntryArray getEntryArray(){ + return mEntryArray; + } + public List listEntries(){ + return listEntries(false); + } + public List listEntries(boolean skipNullBlock){ + List results=new ArrayList<>(); + Iterator itr = getEntryArray().iterator(skipNullBlock); + while (itr.hasNext()){ + Entry block=itr.next(); + results.add(block); + } + return results; + } + public Entry getEntry(int entryId){ + return getEntryArray().get(entryId); + } + + private void onSetEntryCount(int count) { + getEntryArray().setChildesCount(count); + } + @Override + protected void onChunkRefreshed() { + getEntryArray().refreshCountAndStart(); + } + @Override + protected void onPreRefreshRefresh(){ + getHeaderBlock().getConfig().refresh(); + super.onPreRefreshRefresh(); + } + /* + * method Block.addBytes is inefficient for large size byte array + * so let's override here because this block is the largest + */ + @Override + public byte[] getBytes(){ + ByteArrayOutputStream os=new ByteArrayOutputStream(); + try { + writeBytes(os); + os.close(); + } catch (IOException ignored) { + } + return os.toByteArray(); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + if(isSparse()){ + jsonObject.put(NAME_is_sparse, true); + } + jsonObject.put(NAME_id, getId()); + jsonObject.put(NAME_name, getTypeName()); + jsonObject.put(NAME_config, getResConfig().toJson()); + jsonObject.put(NAME_entries, getEntryArray().toJson()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setId(json.getInt(NAME_id)); + String name = json.optString(NAME_name); + if(name!=null){ + setTypeName(name); + } + getEntryArray() + .fromJson(json.getJSONArray(NAME_entries)); + getResConfig() + .fromJson(json.getJSONObject(NAME_config)); + } + public void merge(TypeBlock typeBlock){ + if(typeBlock==null||typeBlock==this){ + return; + } + if(getTypeId() != typeBlock.getTypeId()){ + throw new IllegalArgumentException("Can not merge different id types: " + +getTypeId()+"!="+typeBlock.getTypeId()); + } + setTypeName(typeBlock.getTypeName()); + getEntryArray().merge(typeBlock.getEntryArray()); + } + @Override + public int compareTo(TypeBlock typeBlock) { + int id1 = getId(); + int id2 = typeBlock.getId(); + if(id1 != id2){ + return Integer.compare(id1, id2); + } + String q1 = (isSparse() ? "1" : "0") + + getResConfig().getQualifiers(); + String q2 = (typeBlock.isSparse() ? "1" : "0") + + typeBlock.getResConfig().getQualifiers(); + return q1.compareTo(q2); + } + public boolean isEqualTypeName(String typeName){ + return isEqualTypeName(getTypeName(), typeName); + } + + /** + * To be removed, use getEntry(String entryName) + */ + @Deprecated + public Entry searchByEntryName(String entryName){ + return getEntryArray().getEntry(entryName); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getTypeName()); + builder.append('{'); + builder.append(getHeaderBlock()); + builder.append('}'); + return builder.toString(); + } + + public static boolean isEqualTypeName(String name1, String name2){ + if(name1 == null){ + return name2 == null; + } + if(name2 == null){ + return false; + } + if(name1.equals(name2)){ + return true; + } + return trimTypeName(name1).equals(trimTypeName(name2)); + } + private static String trimTypeName(String typeName){ + while (typeName.length() > 0 && isWildTypeNamePrefix(typeName.charAt(0))){ + typeName = typeName.substring(1); + } + return typeName; + } + private static boolean isWildTypeNamePrefix(char ch){ + switch (ch){ + case '^': + case '*': + case '+': + return true; + default: + return false; + } + } + + public static final String NAME_name = "name"; + public static final String NAME_config = "config"; + public static final String NAME_id = "id"; + public static final String NAME_entries = "entries"; + public static final String NAME_is_sparse = "is_sparse"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/UnknownChunk.java b/src/ARSCLib/com/reandroid/arsc/chunk/UnknownChunk.java new file mode 100644 index 00000000..aaa4b2e2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/UnknownChunk.java @@ -0,0 +1,98 @@ + /* + * 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.chunk; + + import com.reandroid.arsc.header.HeaderBlock; + import com.reandroid.arsc.item.ByteArray; + + import java.io.*; + + /** + * This class can load any valid chunk, aimed to + * handle any future android changes + * */ +public class UnknownChunk extends Chunk implements HeaderBlock.HeaderLoaded { + private final ByteArray body; + public UnknownChunk() { + super(new HeaderBlock(INITIAL_CHUNK_TYPE), 1); + this.body = new ByteArray(); + addChild(body); + setHeaderLoaded(this); + } + public ByteArray getBody(){ + return body; + } + @Override + public void onChunkTypeLoaded(short type) { + } + @Override + public void onHeaderSizeLoaded(int headerSize) { + } + @Override + public void onChunkSizeLoaded(int headerSize, int chunkSize) { + getBody().setSize(chunkSize - headerSize); + } + + @Override + void checkInvalidChunk(HeaderBlock headerBlock) throws IOException { + } + @Override + protected void onChunkRefreshed() { + } + @Override + public byte[] getBytes(){ + ByteArrayOutputStream os=new ByteArrayOutputStream(); + try { + writeBytes(os); + os.close(); + } catch (IOException ignored) { + } + return os.toByteArray(); + } + public int readBytes(File file) throws IOException{ + FileInputStream inputStream=new FileInputStream(file); + int result=readBytes(inputStream); + inputStream.close(); + return result; + } + public int readBytes(InputStream inputStream) throws IOException{ + int result; + result=getHeaderBlock().readBytes(inputStream); + result+=getBody().readBytes(inputStream); + super.notifyBlockLoad(); + return result; + } + public final int writeBytes(File file) throws IOException{ + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + if(dir.mkdirs()){ + throw new IOException("Can not create directory: "+dir); + } + } + OutputStream outputStream=new FileOutputStream(file); + int length = super.writeBytes(outputStream); + outputStream.close(); + return length; + } + @Override + public String toString(){ + return getHeaderBlock() + +" {Body="+getBody().size()+"}"; + } + + private static final short INITIAL_CHUNK_TYPE = 0x0000; + + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/AndroidManifestBlock.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/AndroidManifestBlock.java new file mode 100644 index 00000000..e492ffe9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/AndroidManifestBlock.java @@ -0,0 +1,495 @@ + /* + * 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.chunk.xml; + +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.value.ValueType; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class AndroidManifestBlock extends ResXmlDocument { + private int mGuessedPackageId; + public AndroidManifestBlock(){ + super(); + super.getStringPool().setUtf8(false); + } + public ApkFile.ApkType guessApkType(){ + if(isSplit()){ + return ApkFile.ApkType.SPLIT; + } + Boolean core = isCoreApp(); + if(core!=null && core){ + return ApkFile.ApkType.CORE; + } + if(getMainActivity()!=null){ + return ApkFile.ApkType.BASE; + } + return null; + } + public Boolean isCoreApp(){ + ResXmlElement manifest = getManifestElement(); + if(manifest == null){ + return null; + } + ResXmlAttribute attribute = manifest.searchAttributeByName(NAME_coreApp); + if(attribute == null){ + return null; + } + if(attribute.getValueType() != ValueType.INT_BOOLEAN){ + return null; + } + return attribute.getValueAsBoolean(); + } + public boolean isSplit(){ + ResXmlElement manifest = getManifestElement(); + if(manifest == null){ + return false; + } + return manifest.searchAttributeByName(NAME_split)!=null; + } + public String getSplit(){ + ResXmlElement manifest = getManifestElement(); + if(manifest == null){ + return null; + } + ResXmlAttribute attribute = manifest.searchAttributeByName(NAME_split); + if(attribute!=null){ + return attribute.getValueAsString(); + } + return null; + } + public void setSplit(String split, boolean forceCreate){ + ResXmlElement manifest = getManifestElement(); + if(manifest == null){ + return; + } + ResXmlAttribute attribute; + if(forceCreate){ + attribute = manifest.getOrCreateAttribute(NAME_split, 0); + }else { + attribute = manifest.searchAttributeByName(NAME_split); + if(attribute==null){ + return; + } + } + attribute.setValueAsString(split); + } + // TODO: find a better way + public int guessCurrentPackageId(){ + if(mGuessedPackageId == 0){ + mGuessedPackageId = ((getIconResourceId()>>24) & 0xff); + } + return mGuessedPackageId; + } + public int getIconResourceId(){ + ResXmlElement applicationElement = getApplicationElement(); + if(applicationElement==null){ + return 0; + } + ResXmlAttribute iconAttribute=applicationElement.searchAttributeByResourceId(ID_icon); + if(iconAttribute==null || iconAttribute.getValueType() != ValueType.REFERENCE){ + return 0; + } + return iconAttribute.getData(); + } + public void setIconResourceId(int resourceId){ + ResXmlElement applicationElement = getApplicationElement(); + if(applicationElement==null){ + return; + } + ResXmlAttribute iconAttribute = + applicationElement.getOrCreateAndroidAttribute(NAME_icon, ID_icon); + iconAttribute.setValueType(ValueType.REFERENCE); + iconAttribute.setData(resourceId); + } + public boolean isDebuggable(){ + ResXmlElement application=getApplicationElement(); + if(application==null){ + return false; + } + ResXmlAttribute attribute = application + .searchAttributeByResourceId(ID_debuggable); + if(attribute==null){ + return false; + } + return attribute.getValueAsBoolean(); + } + public void setDebuggable(boolean debuggable){ + ResXmlElement application=getApplicationElement(); + if(application==null){ + return; + } + ResXmlAttribute attribute = application + .searchAttributeByResourceId(ID_debuggable); + if(debuggable){ + if(attribute==null){ + attribute=application.createAndroidAttribute(NAME_debuggable, ID_debuggable); + } + attribute.setValueAsBoolean(true); + }else if(attribute!=null) { + application.removeAttribute(attribute); + } + } + public ResXmlElement getMainActivity(){ + for(ResXmlElement activity:listActivities()){ + for(ResXmlElement intentFilter:activity.listElements(TAG_intent_filter)){ + for(ResXmlElement action:intentFilter.listElements(TAG_action)){ + ResXmlAttribute attribute = action.searchAttributeByResourceId(ID_name); + if(attribute==null){ + continue; + } + if(VALUE_android_intent_action_MAIN.equals(attribute.getValueAsString())){ + return activity; + } + } + } + } + return null; + } + public List listActivities(){ + return listActivities(true); + } + public List listActivities(boolean includeActivityAlias){ + ResXmlElement application=getApplicationElement(); + if(application==null){ + return new ArrayList<>(); + } + List results = application.listElements(TAG_activity); + if(includeActivityAlias){ + results.addAll(application.listElements(TAG_activity_alias)); + } + return results; + } + public List listApplicationElementsByTag(String tag){ + ResXmlElement application=getApplicationElement(); + if(application==null){ + return new ArrayList<>(); + } + return application.listElements(tag); + } + public List getUsesPermissions(){ + List results=new ArrayList<>(); + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return results; + } + List permissionList = manifestElement.listElements(TAG_uses_permission); + for(ResXmlElement permission:permissionList){ + ResXmlAttribute nameAttr = permission.searchAttributeByResourceId(ID_name); + if(nameAttr==null||nameAttr.getValueType()!=ValueType.STRING){ + continue; + } + String val=nameAttr.getValueAsString(); + if(val!=null){ + results.add(val); + } + } + return results; + } + public ResXmlElement getUsesPermission(String permissionName){ + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return null; + } + List permissionList = manifestElement.listElements(TAG_uses_permission); + for(ResXmlElement permission:permissionList){ + ResXmlAttribute nameAttr = permission.searchAttributeByResourceId(ID_name); + if(nameAttr==null || nameAttr.getValueType()!=ValueType.STRING){ + continue; + } + String val=nameAttr.getValueAsString(); + if(val==null){ + continue; + } + if(val.equals(permissionName)){ + return permission; + } + } + return null; + } + public ResXmlElement addUsesPermission(String permissionName){ + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return null; + } + ResXmlElement exist = getUsesPermission(permissionName); + if(exist!=null){ + return exist; + } + ResXmlElement result = manifestElement.createChildElement(TAG_uses_permission); + ResXmlAttribute attr = result.getOrCreateAndroidAttribute(NAME_name, ID_name); + attr.setValueAsString(permissionName); + int i = manifestElement.lastIndexOf(TAG_uses_permission); + i++; + manifestElement.changeIndex(result, i); + return result; + } + public String getPackageName(){ + ResXmlElement manifest=getManifestElement(); + if(manifest==null){ + return null; + } + ResXmlAttribute attribute = manifest.searchAttributeByName(NAME_PACKAGE); + if(attribute==null || attribute.getValueType()!=ValueType.STRING){ + return null; + } + return attribute.getValueAsString(); + } + public boolean setPackageName(String packageName){ + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return false; + } + ResXmlAttribute attribute= manifestElement.searchAttributeByName(NAME_PACKAGE); + if(attribute==null){ + return false; + } + attribute.setValueAsString(packageName); + return true; + } + public Integer getPlatformBuildVersionCode(){ + ResXmlElement manifest = getManifestElement(); + if(manifest==null){ + return null; + } + ResXmlAttribute attribute = manifest.searchAttributeByName(NAME_platformBuildVersionCode); + if(attribute==null || attribute.getValueType()!=ValueType.INT_DEC){ + return null; + } + return attribute.getData(); + } + public Integer getTargetSdkVersion(){ + ResXmlElement manifest = getManifestElement(); + if(manifest==null){ + return null; + } + ResXmlElement usesSdk = manifest.getElementByTagName(TAG_uses_sdk); + if(usesSdk==null){ + return null; + } + ResXmlAttribute attribute = usesSdk.searchAttributeByResourceId(ID_targetSdkVersion); + if(attribute==null || attribute.getValueType()!=ValueType.INT_DEC){ + return null; + } + return attribute.getData(); + } + public Integer getCompileSdkVersion(){ + return getManifestAttributeInt(ID_compileSdkVersion); + } + public void setCompileSdkVersion(int val){ + setManifestAttributeInt(NAME_compileSdkVersion, ID_compileSdkVersion, val); + } + public String getCompileSdkVersionCodename(){ + return getManifestAttributeString(ID_compileSdkVersionCodename); + } + public boolean setCompileSdkVersionCodename(String val){ + ResXmlElement manifest=getManifestElement(); + if(manifest==null){ + return false; + } + ResXmlAttribute attribute = manifest.searchAttributeByResourceId(ID_compileSdkVersionCodename); + if(attribute==null){ + return false; + } + attribute.setValueAsString(val); + return true; + } + public Integer getVersionCode(){ + return getManifestAttributeInt(ID_versionCode); + } + public void setVersionCode(int val){ + setManifestAttributeInt(NAME_versionCode, ID_versionCode, val); + } + public String getVersionName(){ + return getManifestAttributeString(ID_versionName); + } + public boolean setVersionName(String packageName){ + return setManifestAttributeString(NAME_versionName, ID_versionName, packageName); + } + private String getManifestAttributeString(int resourceId){ + ResXmlElement manifest=getManifestElement(); + if(manifest==null){ + return null; + } + ResXmlAttribute attribute = manifest.searchAttributeByResourceId(resourceId); + if(attribute==null || attribute.getValueType()!=ValueType.STRING){ + return null; + } + return attribute.getValueAsString(); + } + private boolean setManifestAttributeString(String attributeName, int resourceId, String value){ + ResXmlElement manifestElement=getOrCreateManifestElement(); + ResXmlAttribute attribute = manifestElement + .getOrCreateAndroidAttribute(attributeName, resourceId); + attribute.setValueAsString(value); + return true; + } + private void setManifestAttributeInt(String attributeName, int resourceId, int value){ + ResXmlElement manifestElement=getOrCreateManifestElement(); + ResXmlAttribute attribute = manifestElement + .getOrCreateAndroidAttribute(attributeName, resourceId); + attribute.setTypeAndData(ValueType.INT_DEC, value); + } + private Integer getManifestAttributeInt(int resourceId){ + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return null; + } + ResXmlAttribute attribute= manifestElement.searchAttributeByResourceId(resourceId); + if(attribute==null || attribute.getValueType()!=ValueType.INT_DEC){ + return null; + } + return attribute.getData(); + } + public ResXmlElement getApplicationElement(){ + ResXmlElement manifestElement=getManifestElement(); + if(manifestElement==null){ + return null; + } + return manifestElement.getElementByTagName(TAG_application); + } + public ResXmlElement getManifestElement(){ + ResXmlElement manifestElement=getResXmlElement(); + if(manifestElement==null){ + return null; + } + if(!TAG_manifest.equals(manifestElement.getTag())){ + return null; + } + return manifestElement; + } + private ResXmlElement getOrCreateManifestElement(){ + ResXmlElement manifestElement=getResXmlElement(); + if(manifestElement==null){ + manifestElement=createRootElement(TAG_manifest); + } + if(!TAG_manifest.equals(manifestElement.getTag())){ + manifestElement.setTag(TAG_manifest); + } + return manifestElement; + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append("{"); + builder.append(NAME_PACKAGE).append("=").append(getPackageName()); + builder.append(", ").append(NAME_versionCode).append("=").append(getVersionCode()); + builder.append(", ").append(NAME_versionName).append("=").append(getVersionName()); + builder.append(", ").append(NAME_compileSdkVersion).append("=").append(getCompileSdkVersion()); + builder.append(", ").append(NAME_compileSdkVersionCodename).append("=").append(getCompileSdkVersionCodename()); + + List allPermissions= getUsesPermissions(); + builder.append(", PERMISSIONS["); + boolean appendOnce=false; + for(String permissions:allPermissions){ + if(appendOnce){ + builder.append(", "); + } + builder.append(permissions); + appendOnce=true; + } + builder.append("]"); + builder.append("}"); + return builder.toString(); + } + public static boolean isAndroidManifestBlock(ResXmlDocument xmlBlock){ + if(xmlBlock==null){ + return false; + } + ResXmlElement root = xmlBlock.getResXmlElement(); + if(root==null){ + return false; + } + return TAG_manifest.equals(root.getTag()); + } + public static AndroidManifestBlock load(File file) throws IOException { + return load(new FileInputStream(file)); + } + public static AndroidManifestBlock load(InputStream inputStream) throws IOException { + AndroidManifestBlock manifestBlock=new AndroidManifestBlock(); + manifestBlock.readBytes(inputStream); + return manifestBlock; + } + public static final String TAG_action = "action"; + public static final String TAG_activity = "activity"; + public static final String TAG_activity_alias = "activity-alias"; + public static final String TAG_application = "application"; + public static final String TAG_category = "category"; + public static final String TAG_data = "data"; + public static final String TAG_intent_filter = "intent-filter"; + public static final String TAG_manifest = "manifest"; + public static final String TAG_meta_data = "meta-data"; + public static final String TAG_package = "package"; + public static final String TAG_permission = "permission"; + public static final String TAG_provider = "provider"; + public static final String TAG_receiver = "receiver"; + public static final String TAG_service = "service"; + public static final String TAG_uses_feature = "uses-feature"; + public static final String TAG_uses_library = "uses-library"; + public static final String TAG_uses_permission = "uses-permission"; + public static final String TAG_uses_sdk = "uses-sdk"; + + public static final String NAME_compileSdkVersion = "compileSdkVersion"; + public static final String NAME_compileSdkVersionCodename = "compileSdkVersionCodename"; + public static final String NAME_installLocation="installLocation"; + public static final String NAME_PACKAGE = "package"; + public static final String NAME_split = "split"; + public static final String NAME_coreApp = "coreApp"; + public static final String NAME_platformBuildVersionCode = "platformBuildVersionCode"; + public static final String NAME_platformBuildVersionName = "platformBuildVersionName"; + public static final String NAME_versionCode = "versionCode"; + public static final String NAME_versionName = "versionName"; + public static final String NAME_name = "name"; + public static final String NAME_extractNativeLibs = "extractNativeLibs"; + public static final String NAME_isSplitRequired = "isSplitRequired"; + public static final String NAME_value = "value"; + public static final String NAME_resource = "resource"; + public static final String NAME_debuggable = "debuggable"; + public static final String NAME_icon = "icon"; + public static final String NAME_label = "label"; + public static final String NAME_theme = "theme"; + public static final String NAME_id = "id"; + + public static final int ID_name = 0x01010003; + public static final int ID_compileSdkVersion = 0x01010572; + public static final int ID_targetSdkVersion = 0x01010270; + public static final int ID_compileSdkVersionCodename = 0x01010573; + public static final int ID_authorities = 0x01010018; + public static final int ID_host = 0x01010028; + public static final int ID_configChanges = 0x0101001f; + public static final int ID_screenOrientation = 0x0101001e; + public static final int ID_extractNativeLibs = 0x010104ea; + public static final int ID_isSplitRequired = 0x01010591; + public static final int ID_value = 0x01010024; + public static final int ID_resource = 0x01010025; + public static final int ID_versionCode = 0x0101021b; + public static final int ID_versionName = 0x0101021c; + public static final int ID_debuggable = 0x0101000f; + public static final int ID_icon = 0x01010002; + public static final int ID_label = 0x01010001; + public static final int ID_theme = 0x01010000; + public static final int ID_id = 0x010100d0; + + public static final String VALUE_android_intent_action_MAIN = "android.intent.action.MAIN"; + + public static final String FILE_NAME="AndroidManifest.xml"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/BaseXmlChunk.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/BaseXmlChunk.java new file mode 100755 index 00000000..f91b3629 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/BaseXmlChunk.java @@ -0,0 +1,205 @@ + /* + * 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.chunk.xml; + + import com.reandroid.arsc.chunk.ChunkType; + import com.reandroid.arsc.base.Block; + import com.reandroid.arsc.chunk.Chunk; + import com.reandroid.arsc.header.XmlNodeHeader; + import com.reandroid.arsc.item.IntegerItem; + import com.reandroid.arsc.item.ResXmlString; + import com.reandroid.arsc.pool.ResXmlStringPool; + + import java.util.HashSet; + import java.util.Set; + + class BaseXmlChunk extends Chunk { + private final IntegerItem mNamespaceReference; + private final IntegerItem mStringReference; + + BaseXmlChunk(ChunkType chunkType, int initialChildesCount) { + super(new XmlNodeHeader(chunkType), initialChildesCount+2); + + this.mNamespaceReference=new IntegerItem(-1); + this.mStringReference=new IntegerItem(-1); + + addChild(mNamespaceReference); + addChild(mStringReference); + } + void onRemoved(){ + ResXmlStringPool stringPool = getStringPool(); + if(stringPool==null){ + return; + } + stringPool.removeReference(getHeaderBlock().getCommentReference()); + stringPool.removeReference(mNamespaceReference); + stringPool.removeReference(mStringReference); + } + void linkStringReferences(){ + linkStringReference(getHeaderBlock().getCommentReference()); + linkStringReference(mNamespaceReference); + linkStringReference(mStringReference); + } + private void linkStringReference(IntegerItem item){ + ResXmlString xmlString = getResXmlString(item.get()); + if(xmlString!=null){ + xmlString.addReferenceIfAbsent(item); + } + } + void unLinkStringReference(IntegerItem item){ + ResXmlString xmlString = getResXmlString(item.get()); + if(xmlString!=null){ + xmlString.removeReference(item); + } + } + public void setLineNumber(int val){ + getHeaderBlock().getLineNumber().set(val); + } + public int getLineNumber(){ + return getHeaderBlock().getLineNumber().get(); + } + public void setCommentReference(int val){ + if(val == getCommentReference()){ + return; + } + IntegerItem comment=getHeaderBlock().getCommentReference(); + unLinkStringReference(comment); + getHeaderBlock().getCommentReference().set(val); + linkStringReference(comment); + } + public int getCommentReference(){ + return getHeaderBlock().getCommentReference().get(); + } + public void setNamespaceReference(int val){ + if(val == getNamespaceReference()){ + return; + } + unLinkStringReference(mNamespaceReference); + mNamespaceReference.set(val); + linkStringReference(mNamespaceReference); + } + public int getNamespaceReference(){ + return mNamespaceReference.get(); + } + public void setStringReference(int val){ + if(val == getStringReference()){ + return; + } + unLinkStringReference(mStringReference); + mStringReference.set(val); + linkStringReference(mStringReference); + } + public int getStringReference(){ + return mStringReference.get(); + } + public ResXmlString setString(String str){ + ResXmlStringPool pool = getStringPool(); + if(pool==null){ + return null; + } + ResXmlString xmlString = pool.getOrCreate(str); + setStringReference(xmlString.getIndex()); + return xmlString; + } + public ResXmlStringPool getStringPool(){ + Block parent=getParent(); + while (parent!=null){ + if(parent instanceof ResXmlDocument){ + return ((ResXmlDocument)parent).getStringPool(); + } + if(parent instanceof ResXmlElement){ + return ((ResXmlElement)parent).getStringPool(); + } + parent=parent.getParent(); + } + return null; + } + public ResXmlString getResXmlString(int ref){ + if(ref<0){ + return null; + } + ResXmlStringPool stringPool=getStringPool(); + if(stringPool!=null){ + return stringPool.get(ref); + } + return null; + } + ResXmlString getOrCreateResXmlString(String str){ + ResXmlStringPool stringPool=getStringPool(); + if(stringPool!=null){ + return stringPool.getOrCreate(str); + } + return null; + } + String getString(int ref){ + ResXmlString xmlString=getResXmlString(ref); + if(xmlString!=null){ + return xmlString.get(); + } + return null; + } + ResXmlString getOrCreateString(String str){ + ResXmlStringPool stringPool=getStringPool(); + if(stringPool==null){ + return null; + } + return stringPool.getOrCreate(str); + } + + public String getName(){ + return getString(getStringReference()); + } + public String getUri(){ + return getString(getNamespaceReference()); + } + public String getComment(){ + return getString(getCommentReference()); + } + public void setComment(String comment){ + if(comment==null||comment.length()==0){ + setCommentReference(-1); + }else { + String old=getComment(); + if(comment.equals(old)){ + return; + } + ResXmlString xmlString = getOrCreateResXmlString(comment); + setCommentReference(xmlString.getIndex()); + } + } + public ResXmlElement getParentResXmlElement(){ + return getParent(ResXmlElement.class); + } + @Override + protected void onChunkRefreshed() { + + } + @Override + public String toString(){ + ChunkType chunkType=getHeaderBlock().getChunkType(); + if(chunkType==null){ + return super.toString(); + } + StringBuilder builder=new StringBuilder(); + builder.append(chunkType.toString()); + builder.append(": line="); + builder.append(getLineNumber()); + builder.append(" {"); + builder.append(getName()); + builder.append("}"); + return builder.toString(); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEvent.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEvent.java new file mode 100644 index 00000000..11d46cc8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEvent.java @@ -0,0 +1,59 @@ + /* + * 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.chunk.xml; + +import org.xmlpull.v1.XmlPullParser; + +public class ParserEvent { + private final int event; + private final ResXmlNode xmlNode; + private final String comment; + private final boolean endComment; + public ParserEvent(int event, ResXmlNode xmlNode, String comment, boolean endComment){ + this.event = event; + this.xmlNode = xmlNode; + this.comment = comment; + this.endComment = endComment; + } + public ParserEvent(int event, ResXmlNode xmlNode){ + this(event, xmlNode, null, false); + } + public int getEvent() { + return event; + } + public ResXmlNode getXmlNode() { + return xmlNode; + } + public String getComment() { + return comment; + } + public boolean isEndComment() { + return endComment; + } + + public static final int START_DOCUMENT = XmlPullParser.START_DOCUMENT; + public static final int END_DOCUMENT = XmlPullParser.END_DOCUMENT; + public static final int START_TAG = XmlPullParser.START_TAG; + public static final int END_TAG = XmlPullParser.END_TAG; + public static final int TEXT = XmlPullParser.TEXT; + public static final int CDSECT = XmlPullParser.CDSECT; + public static final int ENTITY_REF = XmlPullParser.ENTITY_REF; + public static final int IGNORABLE_WHITESPACE = XmlPullParser.IGNORABLE_WHITESPACE; + public static final int PROCESSING_INSTRUCTION = XmlPullParser.PROCESSING_INSTRUCTION; + public static final int COMMENT = XmlPullParser.COMMENT; + public static final int DOCDECL = XmlPullParser.DOCDECL; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEventList.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEventList.java new file mode 100644 index 00000000..f3832045 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ParserEventList.java @@ -0,0 +1,112 @@ +/* + * 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.chunk.xml; + + +import com.reandroid.arsc.decoder.ValueDecoder; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class ParserEventList implements Iterator { + private final List eventList; + private int index; + private ParserEvent mCurrent; + private int type = -1; + public ParserEventList(){ + this.eventList = new ArrayList<>(); + } + public void clear(){ + this.eventList.clear(); + reset(); + } + public int getType(){ + return type; + } + public String getText(){ + if(type == ParserEvent.COMMENT){ + return mCurrent.getComment(); + } + if(type == ParserEvent.START_TAG || type == ParserEvent.END_TAG){ + return getElement().getTag(); + } + if(type == ParserEvent.TEXT){ + String text = ((ResXmlTextNode)getXmlNode()).getText(); + if(text == null){ + text = ""; + } + return ValueDecoder.escapeSpecialCharacter(text); + } + return null; + } + public int getLineNumber(){ + if(type!=ParserEvent.COMMENT + && type!=ParserEvent.START_TAG + && type!=ParserEvent.END_TAG){ + return 0; + } + ResXmlNode xmlNode = getXmlNode(); + if(mCurrent.isEndComment() || type==ParserEvent.END_TAG){ + return ((ResXmlElement)xmlNode).getEndLineNumber(); + } + if(type==ParserEvent.TEXT){ + return ((ResXmlTextNode)xmlNode).getLineNumber(); + } + return ((ResXmlElement)xmlNode).getStartLineNumber(); + } + public ResXmlNode getXmlNode(){ + return mCurrent.getXmlNode(); + } + public ResXmlElement getElement(){ + return (ResXmlElement) mCurrent.getXmlNode(); + } + @Override + public ParserEvent next(){ + if(!hasNext()){ + return null; + } + ParserEvent event = get(index); + index++; + mCurrent = event; + type = event.getEvent(); + return event; + } + @Override + public boolean hasNext(){ + return index < size(); + } + public int size(){ + return eventList.size(); + } + public int getIndex() { + return index; + } + public void reset(){ + index = 0; + mCurrent = null; + type = -1; + } + void add(ParserEvent parserEvent){ + if(parserEvent==null){ + return; + } + eventList.add(parserEvent); + } + private ParserEvent get(int i){ + return eventList.get(i); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResIdBuilder.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResIdBuilder.java new file mode 100644 index 00000000..43fe3377 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResIdBuilder.java @@ -0,0 +1,78 @@ + /* + * 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.chunk.xml; + +import com.reandroid.arsc.array.ResXmlIDArray; +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.item.ResXmlID; +import com.reandroid.arsc.item.ResXmlString; +import com.reandroid.arsc.pool.ResXmlStringPool; + +import java.util.*; + +public class ResIdBuilder implements Comparator { + private final Map mIdNameMap; + public ResIdBuilder(){ + this.mIdNameMap=new HashMap<>(); + } + public void buildTo(ResXmlIDMap resXmlIDMap){ + ResXmlStringPool stringPool = resXmlIDMap.getXmlStringPool(); + StringArray xmlStringsArray = stringPool.getStringsArray(); + ResXmlIDArray xmlIDArray = resXmlIDMap.getResXmlIDArray(); + List idList=getSortedIds(); + int size = idList.size(); + xmlStringsArray.ensureSize(size); + xmlIDArray.ensureSize(size); + for(int i=0;i getSortedIds(){ + List results=new ArrayList<>(mIdNameMap.keySet()); + results.sort(this); + return results; + } + @Override + public int compare(Integer i1, Integer i2) { + return i1.compareTo(i2); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java new file mode 100755 index 00000000..7ebd6447 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java @@ -0,0 +1,510 @@ +/* + * 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.chunk.xml; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.*; +import com.reandroid.arsc.pool.ResXmlStringPool; +import com.reandroid.arsc.pool.StringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.AttributeValue; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueItem; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; +import com.reandroid.json.JSONObject; +import com.reandroid.xml.XMLAttribute; +import com.reandroid.xml.XMLException; + +import java.io.IOException; +import java.util.Objects; + +public class ResXmlAttribute extends ValueItem implements AttributeValue, Comparable{ + private ReferenceItem mNSReference; + private ReferenceItem mNameReference; + private ReferenceItem mNameIdReference; + private ReferenceItem mValueStringReference; + public ResXmlAttribute(int attributeUnitSize) { + super(attributeUnitSize, OFFSET_SIZE); + byte[] bts = getBytesInternal(); + putInteger(bts, OFFSET_NS, -1); + putInteger(bts, OFFSET_NAME, -1); + putInteger(bts, OFFSET_STRING, -1); + } + public ResXmlAttribute() { + this(20); + } + + public String getUri(){ + return getString(getNamespaceReference()); + } + public String getFullName(){ + String name=getName(); + if(name==null){ + return null; + } + String prefix=getNamePrefix(); + if(prefix==null){ + return name; + } + return prefix+":"+name; + } + public String getName(){ + return getString(getNameReference()); + } + public String getNamePrefix(){ + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement==null){ + return null; + } + ResXmlStartNamespace startNamespace=xmlElement.getStartNamespaceByUriRef(getNamespaceReference()); + if(startNamespace==null){ + return null; + } + return startNamespace.getPrefix(); + } + // WARN! Careful this is not real value + public String getValueString(){ + return getString(getValueStringReference()); + } + @Override + public int getNameResourceID(){ + ResXmlID xmlID = getResXmlID(); + if(xmlID != null){ + return xmlID.get(); + } + return 0; + } + @Override + public void setNameResourceID(int resourceId){ + ResXmlIDMap xmlIDMap=getResXmlIDMap(); + if(xmlIDMap==null){ + return; + } + ResXmlID xmlID = xmlIDMap.getOrCreate(resourceId); + setNameReference(xmlID.getIndex()); + } + @Override + public Entry resolveName(){ + return resolve(getNameResourceID()); + } + + public void setName(String name, int resourceId){ + if(Objects.equals(name, getName()) && resourceId==getNameResourceID()){ + return; + } + unlink(mNameReference); + unLinkNameId(getResXmlID()); + ResXmlString xmlString = getOrCreateAttributeName(name, resourceId); + if(xmlString==null){ + return; + } + setNameReference(xmlString.getIndex()); + mNameReference = link(OFFSET_NAME); + linkNameId(); + } + private void linkStartNameSpace(){ + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement==null){ + return; + } + ResXmlStartNamespace startNamespace=xmlElement.getStartNamespaceByUriRef(getNamespaceReference()); + if(startNamespace==null){ + return; + } + startNamespace.addAttributeReference(this); + } + private void unLinkStartNameSpace(){ + ResXmlElement xmlElement = getParentResXmlElement(); + if(xmlElement == null){ + return; + } + ResXmlStartNamespace startNamespace = + xmlElement.getStartNamespaceByUriRef(getNamespaceReference()); + if(startNamespace == null){ + return; + } + startNamespace.removeAttributeReference(this); + } + private ResXmlString getOrCreateAttributeName(String name, int resourceId){ + ResXmlStringPool stringPool = getStringPool(); + if(stringPool==null){ + return null; + } + return stringPool.getOrCreateAttribute(resourceId, name); + } + public ResXmlElement getParentResXmlElement(){ + return getParent(ResXmlElement.class); + } + public int getAttributesUnitSize(){ + return OFFSET_SIZE + super.getSize(); + } + public void setAttributesUnitSize(int size){ + int eight = size - OFFSET_SIZE; + super.setSize(eight); + } + private String getString(int ref){ + if(ref<0){ + return null; + } + StringPool stringPool = getStringPool(); + if(stringPool == null){ + return null; + } + StringItem stringItem = stringPool.get(ref); + if(stringItem == null){ + return null; + } + return stringItem.getHtml(); + } + private ResXmlID getResXmlID(){ + ResXmlIDMap xmlIDMap = getResXmlIDMap(); + if(xmlIDMap == null){ + return null; + } + return xmlIDMap.getResXmlIDArray().get(getNameReference()); + } + private ResXmlIDMap getResXmlIDMap(){ + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement!=null){ + return xmlElement.getResXmlIDMap(); + } + return null; + } + + int getNamespaceReference(){ + return getInteger(getBytesInternal(), OFFSET_NS); + } + public void setNamespace(String uri, String prefix){ + if(uri == null || prefix == null){ + setNamespaceReference(-1); + return; + } + ResXmlElement parentElement = getParentResXmlElement(); + if(parentElement == null){ + return; + } + ResXmlStartNamespace ns = parentElement.getOrCreateNamespace(uri, prefix); + setNamespaceReference(ns.getUriReference()); + } + public void setNamespaceReference(int ref){ + if(ref == getNamespaceReference()){ + return; + } + unlink(mNSReference); + putInteger(getBytesInternal(), OFFSET_NS, ref); + mNSReference = link(OFFSET_NS); + linkStartNameSpace(); + } + int getNameReference(){ + return getInteger(getBytesInternal(), OFFSET_NAME); + } + void setNameReference(int ref){ + if(ref == getNameReference()){ + return; + } + unLinkNameId(getResXmlID()); + unlink(mNameReference); + putInteger(getBytesInternal(), OFFSET_NAME, ref); + mNameReference = link(OFFSET_NAME); + linkNameId(); + } + int getValueStringReference(){ + return getInteger(getBytesInternal(), OFFSET_STRING); + } + void setValueStringReference(int ref){ + if(ref == getValueStringReference() && mValueStringReference!=null){ + return; + } + StringPool stringPool = getStringPool(); + if(stringPool == null){ + return; + } + StringItem stringItem = stringPool.get(ref); + unlink(mValueStringReference); + if(stringItem!=null){ + ref = stringItem.getIndex(); + } + putInteger(getBytesInternal(), OFFSET_STRING, ref); + ReferenceItem referenceItem = null; + if(stringItem!=null){ + referenceItem = new ReferenceBlock<>(this, OFFSET_STRING); + stringItem.addReference(referenceItem); + } + mValueStringReference = referenceItem; + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException { + super.onReadBytes(reader); + super.onDataLoaded(); + linkAll(); + linkStartNameSpace(); + } + @Override + public void onRemoved(){ + super.onRemoved(); + unLinkStartNameSpace(); + unlinkAll(); + } + @Override + protected void onUnlinkDataString(ReferenceItem referenceItem){ + unlink(referenceItem); + } + @Override + protected void onDataChanged(){ + if(getValueType()==ValueType.STRING){ + setValueStringReference(getData()); + }else { + setValueStringReference(-1); + } + } + @Override + public ResXmlDocument getParentChunk() { + ResXmlElement element = getParentResXmlElement(); + if(element!=null){ + return element.getParentDocument(); + } + return null; + } + + private void linkNameId(){ + ResXmlID xmlID = getResXmlID(); + if(xmlID==null){ + return; + } + unLinkNameId(xmlID); + ReferenceItem referenceItem = new ReferenceBlock<>(this, OFFSET_NAME); + xmlID.addReference(referenceItem); + mNameIdReference = referenceItem; + } + private void unLinkNameId(ResXmlID xmlID){ + ReferenceItem referenceItem = mNameIdReference; + if(referenceItem==null || xmlID == null){ + return; + } + xmlID.removeReference(referenceItem); + mNameIdReference = null; + if(xmlID.hasReference()){ + return; + } + ResXmlIDMap xmlIDMap = getResXmlIDMap(); + if(xmlIDMap == null){ + return; + } + xmlIDMap.removeSafely(xmlID); + } + private void linkAll(){ + unlink(mNSReference); + mNSReference = link(OFFSET_NS); + unlink(mNameReference); + mNameReference = link(OFFSET_NAME); + unlink(mValueStringReference); + mValueStringReference = link(OFFSET_STRING); + + linkNameId(); + } + private void unlinkAll(){ + unlink(mNSReference); + unlink(mNameReference); + unlink(mValueStringReference); + mNSReference = null; + mNameReference = null; + mValueStringReference = null; + + unLinkNameId(getResXmlID()); + } + private ReferenceItem link(int offset){ + if(offset<0){ + return null; + } + StringPool stringPool = getStringPool(); + if(stringPool == null){ + return null; + } + int ref = getInteger(getBytesInternal(), offset); + StringItem stringItem = stringPool.get(ref); + if(stringItem == null){ + return null; + } + ReferenceItem referenceItem = new ReferenceBlock<>(this, offset); + stringItem.addReference(referenceItem); + return referenceItem; + } + private void unlink(ReferenceItem reference){ + if(reference == null){ + return; + } + ResXmlStringPool stringPool = getStringPool(); + if(stringPool==null){ + return; + } + stringPool.removeReference(reference); + } + @Override + public ResXmlStringPool getStringPool(){ + StringPool stringPool = super.getStringPool(); + if(stringPool instanceof ResXmlStringPool){ + return (ResXmlStringPool) stringPool; + } + return null; + } + @Override + public int compareTo(ResXmlAttribute other) { + int id1=getNameResourceID(); + int id2=other.getNameResourceID(); + if(id1==0 && id2!=0){ + return 1; + } + if(id2==0 && id1!=0){ + return -1; + } + if(id1!=0){ + return Integer.compare(id1, id2); + } + String name1=getName(); + if(name1==null){ + name1=""; + } + String name2=other.getName(); + if(name2==null){ + name2=""; + } + return name1.compareTo(name2); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject= new JSONObject(); + jsonObject.put(NAME_name, getName()); + jsonObject.put(NAME_id, getNameResourceID()); + jsonObject.put(NAME_namespace_uri, getUri()); + ValueType valueType=getValueType(); + jsonObject.put(NAME_value_type, valueType.name()); + if(valueType==ValueType.STRING){ + jsonObject.put(NAME_data, getValueAsString()); + }else if(valueType==ValueType.INT_BOOLEAN){ + jsonObject.put(NAME_data, getValueAsBoolean()); + }else { + jsonObject.put(NAME_data, getData()); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + String name = json.optString(NAME_name, ""); + int id = json.optInt(NAME_id, 0); + setName(name, id); + String uri= json.optString(NAME_namespace_uri, null); + if(uri!=null){ + ResXmlStartNamespace ns = getParentResXmlElement().getStartNamespaceByUri(uri); + if(ns==null){ + ns = getParentResXmlElement().getRootResXmlElement() + .getOrCreateNamespace(uri, ""); + } + setNamespaceReference(ns.getUriReference()); + } + ValueType valueType=ValueType.fromName(json.getString(NAME_value_type)); + if(valueType==ValueType.STRING){ + setValueAsString(json.optString(NAME_data, "")); + }else if(valueType==ValueType.INT_BOOLEAN){ + setValueAsBoolean(json.getBoolean(NAME_data)); + }else { + setValueType(valueType); + setData(json.getInt(NAME_data)); + } + } + public XMLAttribute decodeToXml(EntryStore entryStore, int currentPackageId) throws XMLException { + int resourceId=getNameResourceID(); + String name; + if(resourceId==0){ + name=getName(); + }else { + EntryGroup group = entryStore.getEntryGroup(resourceId); + if(group==null){ + //Lets ignore such error until XML encoder implemented + //throw new XMLException("Failed to decode attribute name: " + //HexUtil.toHex8("@0x", resourceId)); + name = HexUtil.toHex8("@0x", resourceId); + }else { + name = group.getSpecName(); + } + } + String prefix = getNamePrefix(); + if(prefix!=null){ + name=prefix+":"+name; + } + ValueType valueType = getValueType(); + int raw = getData(); + String value = ValueDecoder.decode(entryStore, currentPackageId, (AttributeValue) this); + XMLAttribute attribute = new XMLAttribute(name, value); + attribute.setNameId(resourceId); + if(valueType==ValueType.REFERENCE||valueType==ValueType.ATTRIBUTE){ + attribute.setValueId(raw); + } + return attribute; + } + @Override + public String toString(){ + String fullName = getFullName(); + if(fullName!=null ){ + int id=getNameResourceID(); + if(id!=0){ + fullName=fullName+"(@"+ HexUtil.toHex8(id)+")"; + } + String valStr; + ValueType valueType=getValueType(); + if(valueType==ValueType.STRING){ + valStr=getValueAsString(); + }else if (valueType==ValueType.INT_BOOLEAN){ + valStr = String.valueOf(getValueAsBoolean()); + }else if (valueType==ValueType.INT_DEC){ + valStr = String.valueOf(getData()); + }else { + valStr = "["+valueType+"] " + HexUtil.toHex8(getData()); + } + if(valStr!=null){ + return fullName+"=\""+valStr+"\""; + } + return fullName+"["+valueType+"]=\""+ getData()+"\""; + } + StringBuilder builder= new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append(": "); + builder.append(getIndex()); + builder.append("{NamespaceReference=").append(getNamespaceReference()); + builder.append(", NameReference=").append(getNameReference()); + builder.append(", ValueStringReference=").append(getValueStringReference()); + builder.append(", ValueSize=").append(getSize()); + builder.append(", ValueTypeByte=").append(getType() & 0xff); + builder.append(", Data=").append(getData()); + builder.append("}"); + return builder.toString(); + } + + + + public static final String NAME_id = "id"; + public static final String NAME_name = "name"; + public static final String NAME_namespace_uri = "namespace_uri"; + + private static final int OFFSET_NS = 0; + private static final int OFFSET_NAME = 4; + private static final int OFFSET_STRING = 8; + + private static final int OFFSET_SIZE = 12; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlDocument.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlDocument.java new file mode 100755 index 00000000..1a5c280e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlDocument.java @@ -0,0 +1,449 @@ +/* + * 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.chunk.xml; + +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.chunk.*; +import com.reandroid.arsc.container.SingleBlockContainer; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.InfoHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.pool.ResXmlStringPool; +import com.reandroid.arsc.pool.StringPool; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; +import com.reandroid.common.FileChannelInputStream; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; +import com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLElement; +import com.reandroid.xml.XMLException; + +import java.io.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ResXmlDocument extends Chunk + implements MainChunk, ParentChunk, JSONConvert { + private final ResXmlStringPool mResXmlStringPool; + private final ResXmlIDMap mResXmlIDMap; + private ResXmlElement mResXmlElement; + private final SingleBlockContainer mResXmlElementContainer; + private ApkFile mApkFile; + private PackageBlock mPackageBlock; + public ResXmlDocument() { + super(new HeaderBlock(ChunkType.XML),3); + this.mResXmlStringPool=new ResXmlStringPool(true); + this.mResXmlIDMap=new ResXmlIDMap(); + this.mResXmlElement=new ResXmlElement(); + this.mResXmlElementContainer=new SingleBlockContainer<>(); + this.mResXmlElementContainer.setItem(mResXmlElement); + addChild(mResXmlStringPool); + addChild(mResXmlIDMap); + addChild(mResXmlElementContainer); + } + public void destroy(){ + ResXmlElement root = getResXmlElement(); + if(root!=null){ + root.clearChildes(); + setResXmlElement(null); + } + getResXmlIDMap().destroy(); + getStringPool().destroy(); + refresh(); + } + public void setAttributesUnitSize(int size, boolean setToAll){ + ResXmlElement root = getResXmlElement(); + if(root!=null){ + root.setAttributesUnitSize(size, setToAll); + } + } + public ResXmlElement createRootElement(String tag){ + int lineNo=1; + ResXmlElement resXmlElement=new ResXmlElement(); + resXmlElement.newStartElement(lineNo); + + setResXmlElement(resXmlElement); + + if(tag!=null){ + resXmlElement.setTag(tag); + } + return resXmlElement; + } + void linkStringReferences(){ + ResXmlElement element=getResXmlElement(); + if(element!=null){ + element.linkStringReferences(); + } + } + /* + * method Block.addBytes is inefficient for large size byte array + * so let's override here because this block is the largest + */ + @Override + public byte[] getBytes(){ + ByteArrayOutputStream os=new ByteArrayOutputStream(); + try { + writeBytes(os); + os.close(); + } catch (IOException ignored) { + } + return os.toByteArray(); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + HeaderBlock headerBlock=reader.readHeaderBlock(); + if(headerBlock==null){ + return; + } + BlockReader chunkReader=reader.create(headerBlock.getChunkSize()); + headerBlock=getHeaderBlock(); + headerBlock.readBytes(chunkReader); + // android/aapt2 accepts 0x0000 (NULL) chunk type as XML, it could + // be android's bug and might be fixed in the future until then lets fix it ourselves + headerBlock.setType(ChunkType.XML); + while (chunkReader.isAvailable()){ + boolean readOk=readNext(chunkReader); + if(!readOk){ + break; + } + } + reader.offset(headerBlock.getChunkSize()); + chunkReader.close(); + onChunkLoaded(); + } + @Override + public void onChunkLoaded(){ + super.onChunkLoaded(); + linkStringReferences(); + } + private boolean readNext(BlockReader reader) throws IOException { + if(!reader.isAvailable()){ + return false; + } + int position=reader.getPosition(); + HeaderBlock headerBlock=reader.readHeaderBlock(); + if(headerBlock==null){ + return false; + } + ChunkType chunkType=headerBlock.getChunkType(); + if(chunkType==ChunkType.STRING){ + mResXmlStringPool.readBytes(reader); + }else if(chunkType==ChunkType.XML_RESOURCE_MAP){ + mResXmlIDMap.readBytes(reader); + }else if(isElementChunk(chunkType)){ + mResXmlElementContainer.readBytes(reader); + return reader.isAvailable(); + }else { + throw new IOException("Unexpected chunk "+headerBlock); + } + return reader.isAvailable() && position!=reader.getPosition(); + } + private boolean isElementChunk(ChunkType chunkType){ + if(chunkType==ChunkType.XML_START_ELEMENT){ + return true; + } + if(chunkType==ChunkType.XML_END_ELEMENT){ + return true; + } + if(chunkType==ChunkType.XML_START_NAMESPACE){ + return true; + } + if(chunkType==ChunkType.XML_END_NAMESPACE){ + return true; + } + if(chunkType==ChunkType.XML_CDATA){ + return true; + } + if(chunkType==ChunkType.XML_LAST_CHUNK){ + return true; + } + return false; + } + @Override + public ResXmlStringPool getStringPool(){ + return mResXmlStringPool; + } + @Override + public ApkFile getApkFile(){ + return mApkFile; + } + @Override + public void setApkFile(ApkFile apkFile){ + this.mApkFile = apkFile; + } + @Override + public PackageBlock getPackageBlock(){ + ApkFile apkFile = this.mApkFile; + PackageBlock packageBlock = this.mPackageBlock; + if(apkFile == null || packageBlock != null){ + return packageBlock; + } + TableBlock tableBlock = apkFile.getTableBlock(); + if(tableBlock != null){ + return tableBlock.pickOne(); + } + return null; + } + public void setPackageBlock(PackageBlock packageBlock) { + this.mPackageBlock = packageBlock; + } + @Override + public TableBlock getTableBlock(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock != null){ + TableBlock tableBlock = packageBlock.getTableBlock(); + if(tableBlock != null){ + return tableBlock; + } + } + ApkFile apkFile = getApkFile(); + if(apkFile != null){ + return apkFile.getTableBlock(); + } + return null; + } + @Override + public StringPool getSpecStringPool() { + return null; + } + @Override + public MainChunk getMainChunk(){ + return this; + } + public ResXmlIDMap getResXmlIDMap(){ + return mResXmlIDMap; + } + public ResXmlElement getResXmlElement(){ + return mResXmlElement; + } + public void setResXmlElement(ResXmlElement resXmlElement){ + this.mResXmlElement=resXmlElement; + this.mResXmlElementContainer.setItem(resXmlElement); + } + @Override + protected void onChunkRefreshed() { + + } + public void readBytes(File file) throws IOException{ + BlockReader reader=new BlockReader(file); + super.readBytes(reader); + } + public void readBytes(InputStream inputStream) throws IOException{ + BlockReader reader=new BlockReader(inputStream); + super.readBytes(reader); + } + public final int writeBytes(File file) throws IOException{ + if(isNull()){ + throw new IOException("Can NOT save null block"); + } + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + OutputStream outputStream=new FileOutputStream(file); + int length = super.writeBytes(outputStream); + outputStream.close(); + return length; + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(ResXmlDocument.NAME_element, getResXmlElement().toJson()); + JSONArray pool = getStringPool().toJson(); + if(pool!=null){ + jsonObject.put(ResXmlDocument.NAME_styled_strings, pool); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + onFromJson(json); + ResXmlElement xmlElement=getResXmlElement(); + xmlElement.fromJson(json.optJSONObject(ResXmlDocument.NAME_element)); + refresh(); + } + public XMLDocument decodeToXml() throws XMLException { + ApkFile apkFile = getApkFile(); + if(apkFile == null){ + throw new XMLException("Null parent apk file"); + } + int currentPackageId = 0; + AndroidManifestBlock manifestBlock; + if(this instanceof AndroidManifestBlock){ + manifestBlock = ((AndroidManifestBlock)this); + }else { + manifestBlock = apkFile.getAndroidManifestBlock(); + } + if(manifestBlock!=null){ + currentPackageId = manifestBlock.guessCurrentPackageId(); + } + TableBlock tableBlock = apkFile.getTableBlock(); + return decodeToXml(tableBlock, currentPackageId); + } + public XMLDocument decodeToXml(EntryStore entryStore, int currentPackageId) throws XMLException { + XMLDocument xmlDocument = new XMLDocument(); + XMLElement xmlElement = getResXmlElement() + .decodeToXml(entryStore, currentPackageId); + xmlDocument.setDocumentElement(xmlElement); + return xmlDocument; + } + private void onFromJson(JSONObject json){ + List attributeList=recursiveAttributes(json.optJSONObject(ResXmlDocument.NAME_element)); + buildResourceIds(attributeList); + Set allStrings=recursiveStrings(json.optJSONObject(ResXmlDocument.NAME_element)); + ResXmlStringPool stringPool = getStringPool(); + stringPool.addStrings(allStrings); + stringPool.refresh(); + } + private void buildResourceIds(List attributeList){ + ResIdBuilder builder=new ResIdBuilder(); + for(JSONObject attribute:attributeList){ + int id=attribute.getInt(ResXmlAttribute.NAME_id); + if(id==0){ + continue; + } + String name=attribute.getString(ResXmlAttribute.NAME_name); + builder.add(id, name); + } + builder.buildTo(getResXmlIDMap()); + } + private List recursiveAttributes(JSONObject elementJson){ + List results = new ArrayList<>(); + if(elementJson==null){ + return results; + } + JSONArray attributes = elementJson.optJSONArray(ResXmlElement.NAME_attributes); + if(attributes != null){ + int length = attributes.length(); + for(int i=0; i recursiveStrings(JSONObject elementJson){ + Set results = new HashSet<>(); + if(elementJson==null){ + return results; + } + results.add(elementJson.optString(ResXmlElement.NAME_namespace_uri)); + results.add(elementJson.optString(ResXmlElement.NAME_name)); + JSONArray namespaces=elementJson.optJSONArray(ResXmlElement.NAME_namespaces); + if(namespaces != null){ + int length = namespaces.length(); + for(int i=0; i, + Comparator { + private final BlockList mStartNamespaceList; + private final SingleBlockContainer mStartElementContainer; + private final BlockList mBody; + private final SingleBlockContainer mEndElementContainer; + private final BlockList mEndNamespaceList; + private int mLevel; + public ResXmlElement() { + super(5); + this.mStartNamespaceList = new BlockList<>(); + this.mStartElementContainer= new SingleBlockContainer<>(); + this.mBody = new BlockList<>(); + this.mEndElementContainer = new SingleBlockContainer<>(); + this.mEndNamespaceList = new BlockList<>(); + addChild(0, mStartNamespaceList); + addChild(1, mStartElementContainer); + addChild(2, mBody); + addChild(3, mEndElementContainer); + addChild(4, mEndNamespaceList); + } + public void changeIndex(ResXmlElement element, int index){ + int i = 0; + for(ResXmlNode xmlNode:mBody.getChildes()){ + if(i == index){ + element.setIndex(i); + i++; + } + if(xmlNode==element){ + continue; + } + xmlNode.setIndex(i); + i++; + } + mBody.sort(this); + } + public int lastIndexOf(String tagName){ + List elementList = listElements(tagName); + int i = elementList.size(); + if(i==0){ + return -1; + } + i--; + return elementList.get(i).getIndex(); + } + public int indexOf(String tagName){ + ResXmlElement element = getElementByTagName(tagName); + if(element!=null){ + return element.getIndex(); + } + return -1; + } + public int indexOf(ResXmlElement element){ + int index = 0; + for(ResXmlNode xmlNode:mBody.getChildes()){ + if(xmlNode==element){ + return index; + } + index++; + } + return -1; + } + public void setAttributesUnitSize(int size, boolean setToAll){ + ResXmlStartElement startElement = getStartElement(); + startElement.setAttributesUnitSize(size); + if(setToAll){ + for(ResXmlElement child:listElements()){ + child.setAttributesUnitSize(size, setToAll); + } + } + } + public String getStartComment(){ + ResXmlStartElement start = getStartElement(); + if(start!=null){ + return start.getComment(); + } + return null; + } + String getEndComment(){ + ResXmlEndElement end = getEndElement(); + if(end!=null){ + return end.getComment(); + } + return null; + } + public int getStartLineNumber(){ + ResXmlStartElement start = getStartElement(); + if(start!=null){ + return start.getLineNumber(); + } + return 0; + } + public int getEndLineNumber(){ + ResXmlEndElement end = getEndElement(); + if(end!=null){ + return end.getLineNumber(); + } + return 0; + } + public void setComment(String comment){ + getStartElement().setComment(comment); + } + public void calculatePositions(){ + ResXmlStartElement start = getStartElement(); + if(start!=null){ + start.calculatePositions(); + } + } + public ResXmlAttribute newAttribute(){ + return getStartElement().newAttribute(); + } + @Override + void onRemoved(){ + for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ + startNamespace.onRemoved(); + } + ResXmlStartElement start = getStartElement(); + if(start != null){ + start.onRemoved(); + } + for(ResXmlNode xmlNode : listXmlNodes()){ + xmlNode.onRemoved(); + } + } + @Override + void linkStringReferences(){ + for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ + startNamespace.linkStringReferences(); + } + ResXmlStartElement start = getStartElement(); + if(start != null){ + start.linkStringReferences(); + } + for(ResXmlNode xmlNode : getXmlNodes()){ + xmlNode.linkStringReferences(); + } + } + public ResXmlElement createChildElement(){ + return createChildElement(null); + } + public ResXmlElement createChildElement(String tag){ + int lineNo=getStartElement().getLineNumber()+1; + ResXmlElement resXmlElement=new ResXmlElement(); + resXmlElement.newStartElement(lineNo); + + addElement(resXmlElement); + + if(tag!=null){ + resXmlElement.setTag(tag); + } + return resXmlElement; + } + public ResXmlAttribute getOrCreateAndroidAttribute(String name, int resourceId){ + return getOrCreateAttribute(NS_ANDROID_URI, NS_ANDROID_PREFIX, name, resourceId); + } + public ResXmlAttribute getOrCreateAttribute(String uri, String prefix, String name, int resourceId){ + ResXmlAttribute attribute=searchAttribute(name, resourceId); + if(attribute==null){ + attribute = createAttribute(name, resourceId); + if(uri!=null){ + ResXmlElement root = getRootResXmlElement(); + ResXmlStartNamespace ns = root.getOrCreateNamespace(uri, prefix); + attribute.setNamespaceReference(ns.getUriReference()); + } + } + return attribute; + } + public ResXmlAttribute getOrCreateAttribute(String name, int resourceId){ + ResXmlAttribute attribute=searchAttribute(name, resourceId); + if(attribute==null){ + attribute=createAttribute(name, resourceId); + } + return attribute; + } + public ResXmlAttribute createAndroidAttribute(String name, int resourceId){ + ResXmlAttribute attribute=createAttribute(name, resourceId); + ResXmlStartNamespace ns = getOrCreateNamespace(NS_ANDROID_URI, NS_ANDROID_PREFIX); + attribute.setNamespaceReference(ns.getUriReference()); + return attribute; + } + public ResXmlAttribute createAttribute(String name, int resourceId){ + ResXmlAttribute attribute=new ResXmlAttribute(); + addAttribute(attribute); + attribute.setName(name, resourceId); + return attribute; + } + public void addAttribute(ResXmlAttribute attribute){ + getStartElement().getResXmlAttributeArray().add(attribute); + } + public ResXmlElement getElementByTagName(String name){ + if(name==null){ + return null; + } + for(ResXmlElement child:listElements()){ + if(name.equals(child.getTag())||name.equals(child.getTagName())){ + return child; + } + } + return null; + } + private ResXmlAttribute searchAttribute(String name, int resourceId){ + if(resourceId==0){ + return searchAttributeByName(name); + } + return searchAttributeByResourceId(resourceId); + } + // Searches attribute with resource id = 0 + public ResXmlAttribute searchAttributeByName(String name){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.searchAttributeByName(name); + } + return null; + } + public ResXmlAttribute searchAttributeByResourceId(int resourceId){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.searchAttributeByResourceId(resourceId); + } + return null; + } + public void setTag(String tag){ + ResXmlStringPool pool = getStringPool(); + if(pool==null){ + return; + } + ensureStartEndElement(); + ResXmlStartElement start=getStartElement(); + String prefix=null; + String name=tag; + int i=tag.lastIndexOf(':'); + if(i>=0){ + prefix=tag.substring(0,i); + i++; + name=tag.substring(i); + } + start.setName(name); + ResXmlStartNamespace ns = getStartNamespaceByPrefix(prefix); + if(ns!=null){ + start.setNamespaceReference(ns.getUriReference()); + } + } + public String getTagName(){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getTagName(); + } + return null; + } + public String getTag(){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getName(); + } + return null; + } + public String getTagUri(){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getUri(); + } + return null; + } + public String getTagPrefix(){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getPrefix(); + } + return null; + } + public int getAttributeCount() { + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getResXmlAttributeArray().childesCount(); + } + return 0; + } + public ResXmlAttribute getAttributeAt(int index){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getResXmlAttributeArray().get(index); + } + return null; + } + public Collection listAttributes(){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.listResXmlAttributes(); + } + return new ArrayList<>(); + } + public ResXmlStringPool getStringPool(){ + Block parent=getParent(); + while (parent!=null){ + if(parent instanceof ResXmlDocument){ + return ((ResXmlDocument)parent).getStringPool(); + } + if(parent instanceof ResXmlElement){ + return ((ResXmlElement)parent).getStringPool(); + } + parent=parent.getParent(); + } + return null; + } + public ResXmlIDMap getResXmlIDMap(){ + ResXmlDocument resXmlDocument = getParentDocument(); + if(resXmlDocument!=null){ + return resXmlDocument.getResXmlIDMap(); + } + return null; + } + public ResXmlDocument getParentDocument(){ + return getParentInstance(ResXmlDocument.class); + } + + @Override + public int getDepth(){ + ResXmlElement parent = getParentResXmlElement(); + if(parent != null){ + return parent.getDepth() + 1; + } + return 0; + } + @Override + void addEvents(ParserEventList parserEventList){ + String comment = getStartComment(); + if(comment!=null){ + parserEventList.add( + new ParserEvent(ParserEvent.COMMENT, this, comment, false)); + } + parserEventList.add(new ParserEvent(ParserEvent.START_TAG, this)); + for(ResXmlNode xmlNode:getXmlNodes()){ + xmlNode.addEvents(parserEventList); + } + comment = getEndComment(); + if(comment!=null){ + parserEventList.add( + new ParserEvent(ParserEvent.COMMENT, this, comment, true)); + } + parserEventList.add(new ParserEvent(ParserEvent.END_TAG, this)); + } + public int getLevel(){ + return mLevel; + } + private void setLevel(int level){ + mLevel = level; + } + public void addElement(ResXmlElement element){ + mBody.add(element); + } + public boolean removeAttribute(ResXmlAttribute resXmlAttribute){ + if(resXmlAttribute != null){ + resXmlAttribute.onRemoved(); + } + return getStartElement().getResXmlAttributeArray().remove(resXmlAttribute); + } + public boolean removeElement(ResXmlElement element){ + if(element !=null && element.getParent()!=null){ + element.onRemoved(); + } + return mBody.remove(element); + } + public boolean removeNode(ResXmlNode node){ + if(node instanceof ResXmlElement){ + return removeElement((ResXmlElement) node); + } + return mBody.remove(node); + } + public int countElements(){ + int result = 0; + for(ResXmlNode xmlNode: getXmlNodes()){ + if(xmlNode instanceof ResXmlElement){ + result++; + } + } + return result; + } + public void clearChildes(){ + List copyOfNodeList=new ArrayList<>(mBody.getChildes()); + for(ResXmlNode xmlNode:copyOfNodeList){ + if(xmlNode==null){ + continue; + } + xmlNode.onRemoved(); + mBody.remove(xmlNode); + } + } + public ResXmlNode getResXmlNode(int position){ + return mBody.get(position); + } + public int countResXmlNodes(){ + return mBody.size(); + } + public boolean hasText(){ + for(ResXmlNode xmlNode : getXmlNodes()){ + if(xmlNode instanceof ResXmlTextNode){ + return true; + } + } + return false; + } + public boolean hasElement(){ + for(ResXmlNode xmlNode : getXmlNodes()){ + if(xmlNode instanceof ResXmlElement){ + return true; + } + } + return false; + } + public List listXmlNodes(){ + return new ArrayList<>(getXmlNodes()); + } + private List getXmlNodes(){ + return mBody.getChildes(); + } + public List listXmlText(){ + List results=new ArrayList<>(); + for(ResXmlNode xmlNode: getXmlNodes()){ + if(xmlNode instanceof ResXmlTextNode){ + results.add(((ResXmlTextNode) xmlNode).getResXmlText()); + } + } + return results; + } + public List listXmlTextNodes(){ + List results=new ArrayList<>(); + for(ResXmlNode xmlNode: getXmlNodes()){ + if(xmlNode instanceof ResXmlTextNode){ + results.add((ResXmlTextNode) xmlNode); + } + } + return results; + } + public List listElements(){ + List results=new ArrayList<>(); + for(ResXmlNode xmlNode: getXmlNodes()){ + if(xmlNode instanceof ResXmlElement){ + results.add((ResXmlElement) xmlNode); + } + } + return results; + } + public List listElements(String name){ + List results=new ArrayList<>(); + if(name==null){ + return results; + } + for(ResXmlElement element:listElements()){ + if(name.equals(element.getTag())||name.equals(element.getTagName())){ + results.add(element); + } + } + return results; + } + public ResXmlElement getRootResXmlElement(){ + ResXmlElement parent = getParentResXmlElement(); + if(parent != null){ + return parent.getRootResXmlElement(); + } + return this; + } + public ResXmlElement getParentResXmlElement(){ + return getParentInstance(ResXmlElement.class); + } + public ResXmlStartNamespace getStartNamespaceByUriRef(int uriRef){ + if(uriRef<0){ + return null; + } + for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ + if(uriRef==ns.getUriReference()){ + return ns; + } + } + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement!=null){ + return xmlElement.getStartNamespaceByUriRef(uriRef); + } + return null; + } + public ResXmlStartNamespace getNamespace(String uri, String prefix){ + if(uri == null || prefix == null){ + return null; + } + for(ResXmlStartNamespace ns : mStartNamespaceList.getChildes()){ + if(uri.equals(ns.getUri()) && prefix.equals(ns.getPrefix())){ + return ns; + } + } + ResXmlElement xmlElement = getParentResXmlElement(); + if(xmlElement != null){ + return xmlElement.getNamespace(uri, prefix); + } + return null; + } + public ResXmlStartNamespace getOrCreateNamespace(String uri, String prefix){ + ResXmlStartNamespace exist = getNamespace(uri, prefix); + if(exist != null){ + return exist; + } + return getRootResXmlElement().createNamespace(uri, prefix); + } + public ResXmlStartNamespace createNamespace(String uri, String prefix){ + ResXmlStartNamespace startNamespace = new ResXmlStartNamespace(); + ResXmlEndNamespace endNamespace = new ResXmlEndNamespace(); + startNamespace.setEnd(endNamespace); + + addStartNamespace(startNamespace); + addEndNamespace(endNamespace); + ResXmlStringPool stringPool = getStringPool(); + ResXmlString xmlString = stringPool.createNew(uri); + startNamespace.setUriReference(xmlString.getIndex()); + startNamespace.setPrefix(prefix); + + return startNamespace; + } + public ResXmlStartNamespace getStartNamespaceByUri(String uri){ + if(uri==null){ + return null; + } + for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ + if(uri.equals(ns.getUri())){ + return ns; + } + } + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement!=null){ + return xmlElement.getStartNamespaceByUri(uri); + } + return null; + } + public ResXmlStartNamespace getStartNamespaceByPrefix(String prefix){ + if(prefix==null){ + return null; + } + for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ + if(prefix.equals(ns.getPrefix())){ + return ns; + } + } + ResXmlElement xmlElement=getParentResXmlElement(); + if(xmlElement!=null){ + return xmlElement.getStartNamespaceByPrefix(prefix); + } + return null; + } + public List getStartNamespaceList(){ + return mStartNamespaceList.getChildes(); + } + public int getNamespaceCount(){ + return mStartNamespaceList.size(); + } + public ResXmlStartNamespace getNamespace(int index){ + return mStartNamespaceList.get(index); + } + public void addStartNamespace(ResXmlStartNamespace item){ + mStartNamespaceList.add(item); + } + private List getEndNamespaceList(){ + return mEndNamespaceList.getChildes(); + } + public void addEndNamespace(ResXmlEndNamespace item){ + mEndNamespaceList.add(item); + } + void removeNamespace(ResXmlStartNamespace startNamespace){ + if(startNamespace == null){ + return; + } + startNamespace.onRemoved(); + mStartNamespaceList.remove(startNamespace); + mEndNamespaceList.remove(startNamespace.getEnd()); + } + + ResXmlStartElement newStartElement(int lineNo){ + ResXmlStartElement startElement=new ResXmlStartElement(); + setStartElement(startElement); + + ResXmlEndElement endElement=new ResXmlEndElement(); + startElement.setResXmlEndElement(endElement); + + setEndElement(endElement); + endElement.setResXmlStartElement(startElement); + + startElement.setLineNumber(lineNo); + endElement.setLineNumber(lineNo); + + return startElement; + } + + public ResXmlStartElement getStartElement(){ + return mStartElementContainer.getItem(); + } + private void setStartElement(ResXmlStartElement item){ + mStartElementContainer.setItem(item); + } + + private ResXmlEndElement getEndElement(){ + return mEndElementContainer.getItem(); + } + private void setEndElement(ResXmlEndElement item){ + mEndElementContainer.setItem(item); + } + + public void addResXmlTextNode(ResXmlTextNode xmlTextNode){ + mBody.add(xmlTextNode); + } + public void addResXmlText(ResXmlText xmlText){ + if(xmlText!=null){ + addResXmlTextNode(new ResXmlTextNode(xmlText)); + } + } + public void addResXmlText(String text){ + if(text==null){ + return; + } + ResXmlTextNode xmlTextNode=new ResXmlTextNode(); + addResXmlTextNode(xmlTextNode); + xmlTextNode.setText(text); + } + + private boolean isBalanced(){ + return isElementBalanced() && isNamespaceBalanced(); + } + private boolean isNamespaceBalanced(){ + return (mStartNamespaceList.size()==mEndNamespaceList.size()); + } + private boolean isElementBalanced(){ + return (hasStartElement() && hasEndElement()); + } + private boolean hasStartElement(){ + return mStartElementContainer.hasItem(); + } + private boolean hasEndElement(){ + return mEndElementContainer.hasItem(); + } + + private void linkStartEnd(){ + linkStartEndElement(); + linkStartEndNameSpaces(); + } + private void linkStartEndElement(){ + ResXmlStartElement start=getStartElement(); + ResXmlEndElement end=getEndElement(); + if(start==null || end==null){ + return; + } + start.setResXmlEndElement(end); + end.setResXmlStartElement(start); + } + private void ensureStartEndElement(){ + ResXmlStartElement start=getStartElement(); + ResXmlEndElement end=getEndElement(); + if(start!=null && end!=null){ + return; + } + if(start==null){ + start=new ResXmlStartElement(); + setStartElement(start); + } + if(end==null){ + end=new ResXmlEndElement(); + setEndElement(end); + } + linkStartEndElement(); + } + private void linkStartEndNameSpaces(){ + if(!isNamespaceBalanced()){ + return; + } + int max=mStartNamespaceList.size(); + for(int i=0;i0 && getLevel()==0){ + onFinishedUnexpected(reader); + return; + } + onFinishedSuccess(reader, headerBlock); + } + private void onFinishedSuccess(BlockReader reader, HeaderBlock headerBlock) throws IOException{ + + } + private void onFinishedUnexpected(BlockReader reader) throws IOException{ + StringBuilder builder=new StringBuilder(); + builder.append("Unexpected finish reading: reader=").append(reader.toString()); + HeaderBlock header = reader.readHeaderBlock(); + if(header!=null){ + builder.append(", next header="); + builder.append(header.toString()); + } + throw new IOException(builder.toString()); + } + private void onStartElement(BlockReader reader) throws IOException{ + if(hasStartElement()){ + ResXmlElement childElement=new ResXmlElement(); + addElement(childElement); + childElement.setLevel(getLevel()+1); + childElement.readBytes(reader); + }else{ + ResXmlStartElement startElement=new ResXmlStartElement(); + setStartElement(startElement); + startElement.readBytes(reader); + } + } + private void onEndElement(BlockReader reader) throws IOException{ + if(hasEndElement()){ + multipleEndElement(reader); + return; + } + ResXmlEndElement endElement=new ResXmlEndElement(); + setEndElement(endElement); + endElement.readBytes(reader); + } + private void onStartNamespace(BlockReader reader) throws IOException{ + ResXmlStartNamespace startNamespace=new ResXmlStartNamespace(); + addStartNamespace(startNamespace); + startNamespace.readBytes(reader); + } + private void onEndNamespace(BlockReader reader) throws IOException{ + ResXmlEndNamespace endNamespace=new ResXmlEndNamespace(); + addEndNamespace(endNamespace); + endNamespace.readBytes(reader); + } + private void onXmlText(BlockReader reader) throws IOException{ + ResXmlText xmlText=new ResXmlText(); + addResXmlText(xmlText); + xmlText.readBytes(reader); + } + + private void unknownChunk(BlockReader reader, HeaderBlock headerBlock) throws IOException{ + throw new IOException("Unknown chunk: "+headerBlock.toString()); + } + private void multipleEndElement(BlockReader reader) throws IOException{ + throw new IOException("Multiple end element: "+reader.toString()); + } + private void unexpectedChunk(BlockReader reader, HeaderBlock headerBlock) throws IOException{ + throw new IOException("Unexpected chunk: "+headerBlock.toString()); + } + private void unBalancedFinish(BlockReader reader) throws IOException{ + if(!isNamespaceBalanced()){ + throw new IOException("Unbalanced namespace: start=" + +mStartNamespaceList.size()+", end="+mEndNamespaceList.size()); + } + + if(!isElementBalanced()){ + // Should not happen unless corrupted file, auto corrected above + StringBuilder builder=new StringBuilder(); + builder.append("Unbalanced element: start="); + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + builder.append(startElement); + }else { + builder.append("null"); + } + builder.append(", end="); + ResXmlEndElement endElement=getEndElement(); + if(endElement!=null){ + builder.append(endElement); + }else { + builder.append("null"); + } + throw new IOException(builder.toString()); + } + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(NAME_node_type, NAME_element); + ResXmlStartElement start = getStartElement(); + jsonObject.put(NAME_line, start.getLineNumber()); + int i=0; + JSONArray nsList=new JSONArray(); + for(ResXmlStartNamespace namespace:getStartNamespaceList()){ + JSONObject ns=new JSONObject(); + ns.put(NAME_namespace_uri, namespace.getUri()); + ns.put(NAME_namespace_prefix, namespace.getPrefix()); + nsList.put(i, ns); + i++; + } + if(i>0){ + jsonObject.put(NAME_namespaces, nsList); + } + jsonObject.put(NAME_name, start.getName()); + String comment=start.getComment(); + if(comment!=null){ + jsonObject.put(NAME_comment, comment); + } + String uri=start.getUri(); + if(uri!=null){ + jsonObject.put(NAME_namespace_uri, uri); + } + JSONArray attrArray=start.getResXmlAttributeArray().toJson(); + jsonObject.put(NAME_attributes, attrArray); + i=0; + JSONArray childes=new JSONArray(); + for(ResXmlNode xmlNode: getXmlNodes()){ + childes.put(i, xmlNode.toJson()); + i++; + } + if(i>0){ + jsonObject.put(NAME_childes, childes); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + ResXmlStartElement start = getStartElement(); + int lineNo=json.optInt(NAME_line, 1); + if(start==null){ + start = newStartElement(lineNo); + }else { + start.setLineNumber(lineNo); + } + JSONArray nsArray = json.optJSONArray(NAME_namespaces); + if(nsArray!=null){ + int length=nsArray.length(); + for(int i=0;i"); + for(ResXmlText xmlText : listXmlText()){ + builder.append(xmlText.getText()); + } + builder.append(""); + }else { + builder.append("/>"); + } + return builder.toString(); + } + return "NULL"; + } + static ResXmlElement newResXmlElement(String tag){ + ResXmlElement resXmlElement=new ResXmlElement(); + ResXmlStartElement startElement=new ResXmlStartElement(); + resXmlElement.setStartElement(startElement); + ResXmlEndElement endElement=new ResXmlEndElement(); + resXmlElement.setEndElement(endElement); + resXmlElement.setTag(tag); + return resXmlElement; + } + + public static final String NS_ANDROID_URI = "http://schemas.android.com/apk/res/android"; + public static final String NS_ANDROID_PREFIX = "android"; + + static final String NAME_element = "element"; + static final String NAME_name = "name"; + static final String NAME_comment = "comment"; + static final String NAME_text = "text"; + static final String NAME_namespaces = "namespaces"; + static final String NAME_namespace_uri = "namespace_uri"; + static final String NAME_namespace_prefix = "namespace_prefix"; + private static final String NAME_line = "line"; + static final String NAME_attributes = "attributes"; + static final String NAME_childes = "childes"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndElement.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndElement.java new file mode 100755 index 00000000..ea32e125 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndElement.java @@ -0,0 +1,32 @@ + /* + * 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.chunk.xml; + + import com.reandroid.arsc.chunk.ChunkType; + + public class ResXmlEndElement extends BaseXmlChunk { + private ResXmlStartElement mResXmlStartElement; + public ResXmlEndElement(){ + super(ChunkType.XML_END_ELEMENT, 0); + } + + public void setResXmlStartElement(ResXmlStartElement element){ + mResXmlStartElement=element; + } + public ResXmlStartElement getResXmlStartElement(){ + return mResXmlStartElement; + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndNamespace.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndNamespace.java new file mode 100755 index 00000000..22edb1b6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlEndNamespace.java @@ -0,0 +1,30 @@ + /* + * 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.chunk.xml; + + import com.reandroid.arsc.chunk.ChunkType; + + public class ResXmlEndNamespace extends ResXmlNamespace{ + public ResXmlEndNamespace() { + super(ChunkType.XML_END_NAMESPACE); + } + public ResXmlStartNamespace getStart(){ + return (ResXmlStartNamespace) getPair(); + } + public void setStart(ResXmlStartNamespace namespace){ + setPair(namespace); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlIDMap.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlIDMap.java new file mode 100755 index 00000000..c1f78c66 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlIDMap.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.arsc.chunk.xml; + + import com.reandroid.arsc.chunk.ChunkType; + import com.reandroid.arsc.array.ResXmlIDArray; + import com.reandroid.arsc.chunk.Chunk; + import com.reandroid.arsc.header.HeaderBlock; + import com.reandroid.arsc.item.ResXmlID; + import com.reandroid.arsc.item.ResXmlString; + import com.reandroid.arsc.pool.ResXmlStringPool; + + import java.util.Collection; + + public class ResXmlIDMap extends Chunk { + private final ResXmlIDArray mResXmlIDArray; + public ResXmlIDMap() { + super(new HeaderBlock(ChunkType.XML_RESOURCE_MAP), 1); + this.mResXmlIDArray=new ResXmlIDArray(getHeaderBlock()); + addChild(mResXmlIDArray); + } + void removeSafely(ResXmlID resXmlID){ + if(resXmlID==null + || resXmlID.getParent()==null + || resXmlID.getIndex()<0 + || resXmlID.hasReference()){ + return; + } + ResXmlString xmlString = resXmlID.getResXmlString(); + if(xmlString == null + || xmlString.getParent()==null + || xmlString.getIndex()<0 + || xmlString.hasReference()){ + return; + } + ResXmlStringPool stringPool = getXmlStringPool(); + if(stringPool == null){ + return; + } + resXmlID.set(0); + ResXmlIDArray idArray = getResXmlIDArray(); + idArray.remove(resXmlID); + stringPool.removeString(xmlString); + } + public int countId(){ + return getResXmlIDArray().childesCount(); + } + public void destroy(){ + getResXmlIDArray().clearChildes(); + } + public ResXmlIDArray getResXmlIDArray(){ + return mResXmlIDArray; + } + + public Collection listResXmlID(){ + return getResXmlIDArray().listItems(); + } + public void addResourceId(int index, int resId){ + getResXmlIDArray().addResourceId(index, resId); + } + public ResXmlID getResXmlID(int ref){ + return getResXmlIDArray().get(ref); + } + public ResXmlID getOrCreate(int resId){ + return getResXmlIDArray().getOrCreate(resId); + } + public ResXmlID getByResId(int resId){ + return getResXmlIDArray().getByResId(resId); + } + @Override + protected void onChunkRefreshed() { + + } + ResXmlStringPool getXmlStringPool(){ + ResXmlDocument resXmlDocument = getParentInstance(ResXmlDocument.class); + if(resXmlDocument!=null){ + return resXmlDocument.getStringPool(); + } + return null; + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNamespace.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNamespace.java new file mode 100644 index 00000000..123af290 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNamespace.java @@ -0,0 +1,87 @@ + /* + * 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.chunk.xml; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.ResXmlString; + +abstract class ResXmlNamespace extends BaseXmlChunk{ + private ResXmlNamespace mPair; + ResXmlNamespace(ChunkType chunkType) { + super(chunkType, 0); + } + @Override + public String getUri(){ + return getString(getUriReference()); + } + public void setUri(String uri){ + ResXmlString xmlString = getOrCreateString(uri); + if(xmlString==null){ + throw new IllegalArgumentException("Null ResXmlString, add to parent element first"); + } + setUriReference(xmlString.getIndex()); + } + public String getPrefix(){ + return getString(getPrefixReference()); + } + public void setPrefix(String prefix){ + ResXmlString xmlString = getOrCreateString(prefix); + if(xmlString==null){ + throw new IllegalArgumentException("Null ResXmlString, add to parent element first"); + } + setPrefixReference(xmlString.getIndex()); + } + public int getUriReference(){ + return getStringReference(); + } + public void setUriReference(int ref){ + setStringReference(ref); + ResXmlNamespace pair=getPair(); + if(pair!=null && pair.getUriReference()!=ref){ + pair.setUriReference(ref); + } + } + public int getPrefixReference(){ + return getNamespaceReference(); + } + public void setPrefixReference(int ref){ + setNamespaceReference(ref); + ResXmlNamespace pair=getPair(); + if(pair!=null && pair.getPrefixReference()!=ref){ + pair.setPrefixReference(ref); + } + } + ResXmlNamespace getPair(){ + return mPair; + } + void setPair(ResXmlNamespace pair){ + if(pair==this){ + return; + } + this.mPair=pair; + if(pair !=null && pair.getPair()!=this){ + pair.setPair(this); + } + } + @Override + public String toString(){ + String uri=getUri(); + if(uri==null){ + return super.toString(); + } + return "xmlns:"+getPrefix()+"=\""+getUri()+"\""; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNode.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNode.java new file mode 100644 index 00000000..344fbea0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlNode.java @@ -0,0 +1,32 @@ +/* + * 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.chunk.xml; + +import com.reandroid.arsc.container.FixedBlockContainer; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +public abstract class ResXmlNode extends FixedBlockContainer implements JSONConvert { + ResXmlNode(int childesCount) { + super(childesCount); + } + abstract void onRemoved(); + abstract void linkStringReferences(); + public abstract int getDepth(); + abstract void addEvents(ParserEventList parserEventList); + + public static final String NAME_node_type = "node_type"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java new file mode 100644 index 00000000..e1c7baf1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java @@ -0,0 +1,731 @@ +/* + * 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.chunk.xml; + +import android.content.res.XmlResourceParser; +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.decoder.Decoder; +import com.reandroid.arsc.value.ValueType; +import org.xmlpull.v1.XmlPullParserException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class ResXmlPullParser implements XmlResourceParser { + private Decoder mDecoder; + private final ParserEventList mEventList = new ParserEventList(); + private ResXmlDocument mDocument; + private boolean mDocumentCreatedHere; + private DocumentLoadedListener documentLoadedListener; + private boolean processNamespaces; + private boolean reportNamespaceAttrs; + private boolean mIsTagStared; + + public ResXmlPullParser(Decoder decoder){ + this.mDecoder = decoder; + this.processNamespaces = false; + this.reportNamespaceAttrs = false; + } + public ResXmlPullParser(){ + this(null); + } + public synchronized ResXmlPullParser getParser(){ + if(isBusy()){ + return new ResXmlPullParser(getDecoder()); + } + closeDocument(); + return this; + } + public synchronized boolean isBusy() { + return !mEventList.hasNext(); + } + public synchronized void setResXmlDocument(ResXmlDocument xmlDocument){ + closeDocument(); + this.mDocument = xmlDocument; + initDefaultFeatures(); + initializeDecoder(xmlDocument); + xmlDocument.addEvents(mEventList); + } + public ResXmlDocument getResXmlDocument() { + return mDocument; + } + + public void setDecoder(Decoder decoder) { + this.mDecoder = decoder; + } + public Decoder getDecoder(){ + return mDecoder; + } + private void initializeDecoder(ResXmlDocument xmlDocument){ + Decoder decoder = this.mDecoder; + if(decoder!=null){ + if(decoder.getApkFile()==null){ + decoder.setApkFile(xmlDocument.getApkFile()); + } + return; + } + ApkFile apkFile = xmlDocument.getApkFile(); + if(apkFile!=null){ + decoder = apkFile.getDecoder(); + if(decoder!=null){ + this.mDecoder = decoder; + return; + } + } + mDecoder = Decoder.create(xmlDocument); + } + + public void closeDocument(){ + mEventList.clear(); + mIsTagStared = false; + destroyDocument(); + } + private void destroyDocument(){ + if(!mDocumentCreatedHere){ + return; + } + mDocumentCreatedHere = false; + if(this.mDocument == null){ + return; + } + this.mDocument.destroy(); + this.mDocument = null; + } + + @Override + public void close(){ + closeDocument(); + } + @Override + public int getAttributeCount() { + ResXmlElement element = getCurrentElement(); + if(element == null){ + return 0; + } + int count = element.getAttributeCount(); + if(isCountNamespacesAsAttribute()){ + count += element.getNamespaceCount(); + } + return count; + } + @Override + public String getAttributeName(int index) { + if(isCountNamespacesAsAttribute()){ + int nsCount = getNamespaceCountInternal(); + if(index < nsCount){ + return getNamespaceAttributeName(index); + } + } + return decodeAttributeName(getResXmlAttributeAt(index)); + } + @Override + public String getAttributeValue(int index) { + if(isCountNamespacesAsAttribute()){ + int nsCount = getNamespaceCountInternal(); + if(index < nsCount){ + return getNamespaceAttributeValue(index); + } + } + return decodeAttributeValue(getResXmlAttributeAt(index)); + } + @Override + public String getAttributeValue(String namespace, String name) { + return decodeAttributeValue(getAttribute(namespace, name)); + } + @Override + public String getPositionDescription() { + return null; + } + @Override + public int getAttributeNameResource(int index) { + ResXmlAttribute attribute = getResXmlAttributeAt(index); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + return 0; + } + @Override + public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + List list = Arrays.asList(options); + int index = list.indexOf(decodeAttributeValue(xmlAttribute)); + if(index==-1){ + return defaultValue; + } + return index; + } + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null || xmlAttribute.getValueType() != ValueType.INT_BOOLEAN){ + return defaultValue; + } + return xmlAttribute.getValueAsBoolean(); + } + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.ATTRIBUTE + ||valueType==ValueType.REFERENCE + ||valueType==ValueType.DYNAMIC_ATTRIBUTE + ||valueType==ValueType.DYNAMIC_REFERENCE){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC + ||valueType==ValueType.INT_HEX){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.FLOAT){ + return Float.intBitsToFloat(xmlAttribute.getData()); + } + return defaultValue; + } + + @Override + public int getAttributeListValue(int index, String[] options, int defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + List list = Arrays.asList(options); + int i = list.indexOf(decodeAttributeValue(xmlAttribute)); + if(i==-1){ + return defaultValue; + } + return index; + } + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null || xmlAttribute.getValueType() != ValueType.INT_BOOLEAN){ + return defaultValue; + } + return xmlAttribute.getValueAsBoolean(); + } + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.ATTRIBUTE + ||valueType==ValueType.REFERENCE + ||valueType==ValueType.DYNAMIC_ATTRIBUTE + ||valueType==ValueType.DYNAMIC_REFERENCE){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeIntValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null){ + return defaultValue; + } + return xmlAttribute.getData(); + } + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + return xmlAttribute.getData(); + } + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + ResXmlAttribute xmlAttribute = getResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.FLOAT){ + return Float.intBitsToFloat(xmlAttribute.getData()); + } + return defaultValue; + } + + @Override + public String getIdAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getIdAttribute(); + if(attribute!=null){ + return attribute.getName(); + } + } + return null; + } + @Override + public String getClassAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getClassAttribute(); + if(attribute!=null){ + return attribute.getName(); + } + } + return null; + } + @Override + public int getIdAttributeResourceValue(int defaultValue) { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getIdAttribute(); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + } + return 0; + } + @Override + public int getStyleAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getStyleAttribute(); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + } + return 0; + } + + @Override + public void setFeature(String name, boolean state) throws XmlPullParserException { + boolean changed; + if(FEATURE_PROCESS_NAMESPACES.equals(name)) { + changed = processNamespaces != state; + processNamespaces = state; + }else if(FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + changed = reportNamespaceAttrs != state; + reportNamespaceAttrs = state; + }else { + throw new XmlPullParserException("Unsupported feature: " + name); + } + if(changed && mIsTagStared){ + throw new XmlPullParserException("Feature changed during parsing: " + + name + ", state=" + state); + } + } + + @Override + public boolean getFeature(String name) { + if(FEATURE_PROCESS_NAMESPACES.equals(name)) { + return processNamespaces; + }else if(FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return reportNamespaceAttrs; + } + return false; + } + @Override + public void setProperty(String name, Object value) throws XmlPullParserException { + } + @Override + public Object getProperty(String name) { + return null; + } + @Override + public void setInput(Reader in) throws XmlPullParserException { + InputStream inputStream = getFromLock(in); + if(inputStream == null){ + throw new XmlPullParserException("Can't parse binary xml from reader"); + } + setInput(inputStream, null); + } + @Override + public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException { + loadResXmlDocument(inputStream); + } + @Override + public String getInputEncoding() { + // Not applicable but let not return null + return "UTF-8"; + } + @Override + public void defineEntityReplacementText(String entityName, String replacementText) throws XmlPullParserException { + } + @Override + public int getNamespaceCount(int depth) throws XmlPullParserException { + if(isCountNamespacesAsAttribute()){ + return 0; + } + ResXmlElement element = getCurrentElement(); + while(element!=null && element.getDepth()>depth){ + element=element.getParentResXmlElement(); + } + if(element!=null){ + return element.getNamespaceCount(); + } + return 0; + } + @Override + public String getNamespacePrefix(int pos) throws XmlPullParserException { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getNamespace(pos).getPrefix(); + } + return null; + } + @Override + public String getNamespaceUri(int pos) throws XmlPullParserException { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getNamespace(pos).getUri(); + } + return null; + } + @Override + public String getNamespace(String prefix) { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + ResXmlStartNamespace startNamespace = element.getStartNamespaceByPrefix(prefix); + if(startNamespace!=null){ + return startNamespace.getUri(); + } + } + return null; + } + @Override + public int getDepth() { + int event = mEventList.getType(); + if(event == START_TAG || event == END_TAG || event == TEXT){ + return mEventList.getXmlNode().getDepth(); + } + return 0; + } + @Override + public int getLineNumber() { + return mEventList.getLineNumber(); + } + @Override + public int getColumnNumber() { + return 0; + } + @Override + public boolean isWhitespace() throws XmlPullParserException { + String text = getText(); + if(text == null){ + return true; + } + text = text.trim(); + return text.length() == 0; + } + @Override + public String getText() { + return mEventList.getText(); + } + @Override + public char[] getTextCharacters(int[] holderForStartAndLength) { + String text = getText(); + if (text == null) { + holderForStartAndLength[0] = -1; + holderForStartAndLength[1] = -1; + return null; + } + char[] result = text.toCharArray(); + holderForStartAndLength[0] = 0; + holderForStartAndLength[1] = result.length; + return result; + } + @Override + public String getNamespace() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTagUri(); + } + return null; + } + @Override + public String getName() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTag(); + } + return null; + } + @Override + public String getPrefix() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTagPrefix(); + } + return null; + } + @Override + public boolean isEmptyElementTag() throws XmlPullParserException { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.countResXmlNodes() == 0 && element.getAttributeCount()==0; + } + return true; + } + @Override + public String getAttributeNamespace(int index) { + if(processNamespaces){ + return null; + } + ResXmlAttribute attribute = getResXmlAttributeAt(index); + if(attribute != null){ + return attribute.getUri(); + } + return null; + } + @Override + public String getAttributePrefix(int index) { + if(processNamespaces){ + return null; + } + ResXmlAttribute attribute = getResXmlAttributeAt(index); + if(attribute != null){ + return attribute.getNamePrefix(); + } + return null; + } + @Override + public String getAttributeType(int index) { + return "CDATA"; + } + @Override + public boolean isAttributeDefault(int index) { + return false; + } + private String decodeAttributeName(ResXmlAttribute attribute){ + if(attribute == null){ + return null; + } + String name; + int resourceId = attribute.getNameResourceID(); + if(resourceId == 0 || mDecoder==null){ + name = attribute.getName(); + }else { + name = mDecoder.decodeResourceName(attribute.getNameResourceID(), true); + if(processNamespaces){ + name = attribute.getNamePrefix() + ":" + name; + } + } + return name; + } + private String decodeAttributeValue(ResXmlAttribute attribute){ + if(attribute==null){ + return null; + } + return mDecoder.decodeAttributeValue(attribute); + } + public ResXmlAttribute getResXmlAttributeAt(int index){ + index = getRealAttributeIndex(index); + ResXmlElement element = getCurrentElement(); + if(element == null){ + return null; + } + return element.getAttributeAt(index); + } + public ResXmlAttribute getAttribute(String namespace, String name) { + ResXmlElement element = getCurrentElement(); + if(element == null){ + return null; + } + for(ResXmlAttribute attribute:element.listAttributes()){ + if(Objects.equals(namespace, attribute.getUri()) + && Objects.equals(name, attribute.getName())){ + return attribute; + } + } + return null; + } + private ResXmlStartElement getResXmlStartElement(){ + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getStartElement(); + } + return null; + } + public ResXmlElement getCurrentElement() { + int type = mEventList.getType(); + if(type==START_TAG||type==END_TAG){ + return mEventList.getElement(); + } + return null; + } + private int getRealAttributeIndex(int index){ + if(isCountNamespacesAsAttribute()){ + index = index - getNamespaceCountInternal(); + } + return index; + } + private int getNamespaceCountInternal(){ + ResXmlElement element = getCurrentElement(); + if(element != null){ + return element.getNamespaceCount(); + } + return 0; + } + private boolean isCountNamespacesAsAttribute(){ + return processNamespaces & reportNamespaceAttrs; + } + private String getNamespaceAttributeName(int index){ + ResXmlStartNamespace namespace = getCurrentElement() + .getNamespace(index); + String prefix = namespace.getPrefix(); + if(processNamespaces){ + prefix = "xmlns:" + prefix; + } + return prefix; + } + private String getNamespaceAttributeValue(int index){ + ResXmlStartNamespace namespace = getCurrentElement() + .getNamespace(index); + return namespace.getUri(); + } + @Override + public int getEventType() throws XmlPullParserException { + return mEventList.getType(); + } + @Override + public int next() throws XmlPullParserException, IOException { + mEventList.next(); + int type = mEventList.getType(); + if(type == START_TAG){ + mIsTagStared = true; + } + return type; + } + @Override + public int nextToken() throws XmlPullParserException, IOException { + return next(); + } + @Override + public void require(int type, String namespace, String name) throws XmlPullParserException, IOException { + if (type != this.getEventType() + || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) { + throw new XmlPullParserException( + "expected: " + TYPES[type] + " {" + namespace + "}" + name, this, null); + } + } + @Override + public String nextText() throws XmlPullParserException, IOException { + int event = getEventType(); + if (event != START_TAG) { + throw new XmlPullParserException("precondition: START_TAG", this, null); + } + while (event!=TEXT && event!=END_TAG && event!=END_DOCUMENT){ + event=next(); + } + if(event==TEXT){ + return getText(); + } + return ""; + } + @Override + public int nextTag() throws XmlPullParserException, IOException { + int event = getEventType(); + if (event != START_TAG) { + throw new XmlPullParserException("precondition: START_TAG", this, null); + } + event = next(); + while (event!=START_TAG && event!=END_DOCUMENT){ + event=next(); + } + return event; + } + + private static InputStream getFromLock(Reader reader){ + try{ + Field field = Reader.class.getDeclaredField("lock"); + field.setAccessible(true); + Object obj = field.get(reader); + if(obj instanceof InputStream){ + return (InputStream) obj; + } + }catch (Throwable ignored){ + } + return null; + } + + public void setDocumentLoadedListener(DocumentLoadedListener documentLoadedListener) { + this.documentLoadedListener = documentLoadedListener; + } + + private void loadResXmlDocument(InputStream inputStream) throws XmlPullParserException { + synchronized (this){ + ResXmlDocument xmlDocument = new ResXmlDocument(); + try { + xmlDocument.readBytes(inputStream); + } catch (IOException exception) { + XmlPullParserException pullParserException = new XmlPullParserException(exception.getMessage()); + pullParserException.initCause(exception); + throw pullParserException; + } + DocumentLoadedListener listener = this.documentLoadedListener; + if(listener != null){ + xmlDocument = listener.onDocumentLoaded(xmlDocument); + } + setResXmlDocument(xmlDocument); + this.mDocumentCreatedHere = true; + } + } + private void initDefaultFeatures(){ + processNamespaces = true; + reportNamespaceAttrs = true; + } + + public static interface DocumentLoadedListener{ + public ResXmlDocument onDocumentLoaded(ResXmlDocument resXmlDocument); + } + +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java new file mode 100755 index 00000000..730f748a --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java @@ -0,0 +1,298 @@ + /* + * 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.chunk.xml; + + import com.reandroid.arsc.chunk.ChunkType; + import com.reandroid.arsc.array.ResXmlAttributeArray; + import com.reandroid.arsc.item.ResXmlString; + import com.reandroid.arsc.item.ShortItem; + + import java.util.Collection; + import java.util.Set; + + public class ResXmlStartElement extends BaseXmlChunk { + private final ShortItem mAttributeStart; + private final ShortItem mAttributeUnitSize; + private final ShortItem mAttributeCount; + private final ShortItem mIdAttributePosition; + private final ShortItem mClassAttributePosition; + private final ShortItem mStyleAttributePosition; + private final ResXmlAttributeArray mAttributeArray; + private ResXmlEndElement mResXmlEndElement; + public ResXmlStartElement() { + super(ChunkType.XML_START_ELEMENT, 7); + mAttributeStart = new ShortItem(ATTRIBUTES_DEFAULT_START); + mAttributeUnitSize = new ShortItem(ATTRIBUTES_UNIT_SIZE); + mAttributeCount = new ShortItem(); + mIdAttributePosition = new ShortItem(); + mClassAttributePosition = new ShortItem(); + mStyleAttributePosition = new ShortItem(); + mAttributeArray = new ResXmlAttributeArray(getHeaderBlock(), + mAttributeStart, + mAttributeCount, + mAttributeUnitSize); + addChild(mAttributeStart); + addChild(mAttributeUnitSize); + addChild(mAttributeCount); + addChild(mIdAttributePosition); + addChild(mClassAttributePosition); + addChild(mStyleAttributePosition); + addChild(mAttributeArray); + } + public ResXmlAttribute getIdAttribute(){ + return getResXmlAttributeArray().get(mIdAttributePosition.unsignedInt()-1); + } + public ResXmlAttribute getClassAttribute(){ + return getResXmlAttributeArray().get(mClassAttributePosition.unsignedInt()-1); + } + public ResXmlAttribute getStyleAttribute(){ + return getResXmlAttributeArray().get(mStyleAttributePosition.unsignedInt()-1); + } + void setAttributesUnitSize(int size){ + mAttributeArray.setAttributesUnitSize(size); + } + public ResXmlAttribute newAttribute(){ + ResXmlAttributeArray attributeArray = getResXmlAttributeArray(); + return attributeArray.createNext(); + } + @Override + void linkStringReferences(){ + super.linkStringReferences(); + ResXmlEndElement end = getResXmlEndElement(); + if(end!=null){ + end.linkStringReferences(); + } + } + @Override + void onRemoved(){ + super.onRemoved(); + ResXmlEndElement end = getResXmlEndElement(); + if(end!=null){ + end.onRemoved(); + } + for(ResXmlAttribute attr:listResXmlAttributes()){ + attr.onRemoved(); + } + } + @Override + protected void onPreRefreshRefresh(){ + sortAttributes(); + } + private void sortAttributes(){ + ResXmlAttributeArray array = getResXmlAttributeArray(); + + ResXmlAttribute idAttribute=array.get(mIdAttributePosition.get()-1); + ResXmlAttribute classAttribute=array.get(mClassAttributePosition.get()-1); + ResXmlAttribute styleAttribute=array.get(mStyleAttributePosition.get()-1); + + array.sortAttributes(); + if(idAttribute!=null){ + mIdAttributePosition.set((short) (idAttribute.getIndex()+1)); + } + if(classAttribute!=null){ + mClassAttributePosition.set((short) (classAttribute.getIndex()+1)); + // In case obfuscation + if(!ATTRIBUTE_NAME_CLASS.equals(classAttribute.getName())){ + classAttribute.setName(ATTRIBUTE_NAME_CLASS, 0); + } + } + if(styleAttribute!=null){ + mStyleAttributePosition.set((short) (styleAttribute.getIndex()+1)); + // In case obfuscation + if(!ATTRIBUTE_NAME_STYLE.equals(styleAttribute.getName())){ + styleAttribute.setName(ATTRIBUTE_NAME_STYLE, 0); + } + } + } + void calculatePositions(){ + ResXmlAttribute idAttribute=getAttribute(ATTRIBUTE_RESOURCE_ID_id); + ResXmlAttribute classAttribute=getNoIdAttribute(ATTRIBUTE_NAME_CLASS); + ResXmlAttribute styleAttribute=getNoIdAttribute(ATTRIBUTE_NAME_STYLE); + + if(idAttribute!=null){ + mIdAttributePosition.set((short) (idAttribute.getIndex()+1)); + } + if(classAttribute!=null){ + mClassAttributePosition.set((short) (classAttribute.getIndex()+1)); + } + if(styleAttribute!=null){ + mStyleAttributePosition.set((short) (styleAttribute.getIndex()+1)); + } + } + public ResXmlAttribute getAttribute(int resourceId){ + for(ResXmlAttribute attribute:listResXmlAttributes()){ + if(resourceId==attribute.getNameResourceID()){ + return attribute; + } + } + return null; + } + private ResXmlAttribute getNoIdAttribute(String name){ + for(ResXmlAttribute attribute:listResXmlAttributes()){ + if(attribute.getNameResourceID()!=0){ + continue; + } + if(name.equals(attribute.getName())){ + return attribute; + } + } + return null; + } + public ResXmlAttribute getAttribute(String uri, String name){ + if(name==null){ + return null; + } + for(ResXmlAttribute attribute:listResXmlAttributes()){ + if(name.equals(attribute.getName())||name.equals(attribute.getFullName())){ + if(uri!=null){ + if(uri.equals(attribute.getUri())){ + return attribute; + } + continue; + } + return attribute; + } + } + return null; + } + // Searches attribute with resource id = 0 + public ResXmlAttribute searchAttributeByName(String name){ + if(name==null){ + return null; + } + for(ResXmlAttribute attribute:listResXmlAttributes()){ + if(name.equals(attribute.getName()) || name.equals(attribute.getFullName())){ + return attribute; + } + } + return null; + } + public ResXmlAttribute searchAttributeByResourceId(int resourceId){ + if(resourceId==0){ + return null; + } + for(ResXmlAttribute attribute:listResXmlAttributes()){ + if(resourceId==attribute.getNameResourceID()){ + return attribute; + } + } + return null; + } + public String getTagName(){ + String prefix=getPrefix(); + String name=getName(); + if(prefix==null){ + return name; + } + return prefix+":"+name; + } + public void setName(String name){ + setString(name); + ResXmlEndElement endElement = getResXmlEndElement(); + if(endElement!=null){ + endElement.setString(name); + } + } + public Collection listResXmlAttributes(){ + return getResXmlAttributeArray().listItems(); + } + public ResXmlAttributeArray getResXmlAttributeArray(){ + return mAttributeArray; + } + + public String getUri(){ + int uriRef=getNamespaceReference(); + if(uriRef<0){ + return null; + } + ResXmlElement parentElement=getParentResXmlElement(); + ResXmlStartNamespace startNamespace=parentElement.getStartNamespaceByUriRef(uriRef); + if(startNamespace!=null){ + return startNamespace.getUri(); + } + return null; + } + public String getPrefix(){ + int uriRef=getNamespaceReference(); + if(uriRef<0){ + return null; + } + ResXmlElement parentElement=getParentResXmlElement(); + ResXmlStartNamespace startNamespace=parentElement.getStartNamespaceByUriRef(uriRef); + if(startNamespace!=null){ + return startNamespace.getPrefix(); + } + return null; + } + public void setResXmlEndElement(ResXmlEndElement element){ + mResXmlEndElement=element; + } + public ResXmlEndElement getResXmlEndElement(){ + return mResXmlEndElement; + } + + @Override + protected void onChunkRefreshed() { + refreshAttributeStart(); + refreshAttributeCount(); + } + private void refreshAttributeStart(){ + int start=countUpTo(mAttributeArray); + start=start-getHeaderBlock().getHeaderSize(); + mAttributeStart.set((short)start); + } + private void refreshAttributeCount(){ + int count=mAttributeArray.childesCount(); + mAttributeCount.set((short)count); + } + + @Override + public String toString(){ + String txt=getTagName(); + if(txt==null){ + return super.toString(); + } + StringBuilder builder=new StringBuilder(); + builder.append(txt); + ResXmlAttribute[] allAttr=mAttributeArray.getChildes(); + if(allAttr!=null){ + for(int i=0;i10){ + break; + } + builder.append(" "); + builder.append(allAttr[i].toString()); + } + } + return builder.toString(); + } + + private static final short ATTRIBUTES_UNIT_SIZE=20; + private static final short ATTRIBUTES_DEFAULT_START=20; + /* + * Find another way to mark an attribute is class, device actually relies on + * value of mClassAttributePosition */ + private static final String ATTRIBUTE_NAME_CLASS="class"; + /* + * Find another way to mark an attribute is style, device actually relies on + * value of mStyleAttributePosition */ + private static final String ATTRIBUTE_NAME_STYLE="style"; + /* + * Resource id value of attribute 'android:id' + * instead of relying on hardcoded value, we should find another way to + * mark an attribute is 'id' */ + private static final int ATTRIBUTE_RESOURCE_ID_id =0x010100d0; + } diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartNamespace.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartNamespace.java new file mode 100755 index 00000000..35705379 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlStartNamespace.java @@ -0,0 +1,90 @@ +/* + * 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.chunk.xml; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.xml.SchemaAttr; +import com.reandroid.xml.XMLAttribute; + +import java.util.HashSet; +import java.util.Set; + +public class ResXmlStartNamespace extends ResXmlNamespace { + private final Set mReferencedAttributes; + + public ResXmlStartNamespace() { + super(ChunkType.XML_START_NAMESPACE); + this.mReferencedAttributes = new HashSet<>(); + } + public ResXmlEndNamespace getEnd(){ + return (ResXmlEndNamespace) getPair(); + } + public void setEnd(ResXmlEndNamespace namespace){ + setPair(namespace); + } + @Override + void linkStringReferences(){ + super.linkStringReferences(); + ResXmlEndNamespace end = getEnd(); + if(end!=null){ + end.linkStringReferences(); + } + } + @Override + void onRemoved(){ + ResXmlEndNamespace end = getEnd(); + if(end!=null){ + end.onRemoved(); + } + mReferencedAttributes.clear(); + } + public boolean hasReferencedAttributes(){ + return mReferencedAttributes.size()>0; + } + public void clearReferencedAttributes(){ + mReferencedAttributes.clear(); + } + public Set getReferencedAttributes(){ + return mReferencedAttributes; + } + void addAttributeReference(ResXmlAttribute attribute){ + if(attribute!=null){ + mReferencedAttributes.add(attribute); + } + } + void removeAttributeReference(ResXmlAttribute attribute){ + if(attribute!=null){ + mReferencedAttributes.remove(attribute); + } + } + public XMLAttribute decodeToXml(){ + String uri=getUri(); + String prefix=getPrefix(); + if(isEmpty(uri) || isEmpty(prefix)){ + return null; + } + SchemaAttr schemaAttr=new SchemaAttr(prefix, uri); + schemaAttr.setLineNumber(getLineNumber()); + return schemaAttr; + } + private boolean isEmpty(String txt){ + if(txt==null){ + return true; + } + txt=txt.trim(); + return txt.length()==0; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlText.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlText.java new file mode 100755 index 00000000..7780561f --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlText.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.arsc.chunk.xml; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.ResXmlString; +import com.reandroid.arsc.pool.ResXmlStringPool; + +public class ResXmlText extends BaseXmlChunk { + private final IntegerItem mReserved; + public ResXmlText() { + super(ChunkType.XML_CDATA, 1); + this.mReserved=new IntegerItem(); + addChild(mReserved); + setStringReference(0); + } + public String getText(){ + ResXmlString xmlString=getResXmlString(getTextReference()); + if(xmlString!=null){ + return xmlString.getHtml(); + } + return null; + } + public int getTextReference(){ + return getNamespaceReference(); + } + public void setTextReference(int ref){ + setNamespaceReference(ref); + } + public void setText(String text){ + ResXmlStringPool stringPool=getStringPool(); + if(stringPool==null){ + return; + } + ResXmlString resXmlString = stringPool.getOrCreate(text); + int ref=resXmlString.getIndex(); + setTextReference(ref); + } + @Override + public String toString(){ + String txt=getText(); + if(txt!=null){ + return txt; + } + return super.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java new file mode 100644 index 00000000..d50918b3 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java @@ -0,0 +1,111 @@ +/* + * 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.chunk.xml; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.json.JSONObject; +import com.reandroid.xml.XMLText; + +public class ResXmlTextNode extends ResXmlNode { + private final ResXmlText resXmlText; + public ResXmlTextNode(ResXmlText resXmlText) { + super(1); + this.resXmlText = resXmlText; + addChild(0, resXmlText); + } + public ResXmlTextNode() { + this(new ResXmlText()); + } + public ResXmlText getResXmlText() { + return resXmlText; + } + public int getLineNumber(){ + return getResXmlText().getLineNumber(); + } + public String getComment() { + return getResXmlText().getComment(); + } + @Override + public int getDepth(){ + ResXmlElement parent = getParentResXmlElement(); + if(parent!=null){ + return parent.getDepth() + 1; + } + return 0; + } + @Override + void addEvents(ParserEventList parserEventList){ + String comment = getComment(); + if(comment!=null){ + parserEventList.add( + new ParserEvent(ParserEvent.COMMENT, this, comment, false)); + } + parserEventList.add(new ParserEvent(ParserEvent.TEXT, this)); + } + public ResXmlElement getParentResXmlElement(){ + return getResXmlText().getParentResXmlElement(); + } + + public void setLineNumber(int lineNumber){ + getResXmlText().setLineNumber(lineNumber); + } + public String getText(){ + return getResXmlText().getText(); + } + public void setText(String text){ + getResXmlText().setText(text); + } + public int getTextReference(){ + return getResXmlText().getTextReference(); + } + public void setTextReference(int ref){ + getResXmlText().setTextReference(ref); + } + @Override + void onRemoved(){ + getResXmlText().onRemoved(); + } + @Override + void linkStringReferences(){ + getResXmlText().linkStringReferences(); + } + @Override + public String toString(){ + String txt=getText(); + if(txt!=null){ + return txt; + } + return super.toString(); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(NAME_node_type, NAME_text); + jsonObject.put(NAME_text, getText()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setText(json.optString(NAME_text, null)); + } + public XMLText decodeToXml() { + XMLText xmlText=new XMLText(ValueDecoder.escapeSpecialCharacter(getText())); + xmlText.setLineNumber(getLineNumber()); + return xmlText; + } + + public static final String NAME_text="text"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/BlockList.java b/src/ARSCLib/com/reandroid/arsc/container/BlockList.java new file mode 100755 index 00000000..daef4bf0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/BlockList.java @@ -0,0 +1,137 @@ + /* + * 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.container; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockContainer; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class BlockList extends Block { + private final List mItems; + public BlockList(){ + super(); + mItems=new ArrayList<>(); + } + public void clearChildes(){ + ArrayList childList = new ArrayList<>(getChildes()); + for(T child:childList){ + remove(child); + } + } + public void sort(Comparator comparator){ + mItems.sort(comparator); + } + public boolean remove(T item){ + if(item!=null){ + item.setParent(null); + item.setIndex(-1); + } + return mItems.remove(item); + } + public void add(T item){ + if(item==null){ + return; + } + item.setIndex(mItems.size()); + item.setParent(this); + mItems.add(item); + } + public T get(int i){ + if(i>=mItems.size() || i<0){ + return null; + } + return mItems.get(i); + } + public int size(){ + return mItems.size(); + } + public List getChildes(){ + return mItems; + } + public final void refresh(){ + if(isNull()){ + return; + } + refreshChildes(); + } + private void refreshChildes(){ + for(T item:getChildes()){ + if(item instanceof BlockContainer){ + BlockContainer container=(BlockContainer)item; + container.refresh(); + }else if(item instanceof BlockList){ + BlockList blockList=(BlockList)item; + blockList.refresh(); + } + } + } + @Override + public byte[] getBytes() { + byte[] results=null; + for(T item:mItems){ + if(item!=null){ + results=addBytes(results, item.getBytes()); + } + } + return results; + } + @Override + public int countBytes() { + int result=0; + for(T item:mItems){ + result+=item.countBytes(); + } + return result; + } + + @Override + public void onCountUpTo(BlockCounter counter) { + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + for(T item:mItems){ + if(counter.FOUND){ + break; + } + item.onCountUpTo(counter); + } + } + @Override + protected int onWriteBytes(OutputStream stream) throws IOException { + int result=0; + for(T item:mItems){ + result+=item.writeBytes(stream); + } + return result; + } + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + for(T item:mItems){ + item.readBytes(reader); + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/ExpandableBlockContainer.java b/src/ARSCLib/com/reandroid/arsc/container/ExpandableBlockContainer.java new file mode 100755 index 00000000..8b3d7189 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/ExpandableBlockContainer.java @@ -0,0 +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. + */ +package com.reandroid.arsc.container; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockContainer; + +public class ExpandableBlockContainer extends BlockContainer { + private Block[] mChildes; + private int mCursor; + public ExpandableBlockContainer(int initialSize){ + super(); + this.mChildes=new Block[initialSize]; + } + public final void addChild(Block block){ + if(block==null){ + return; + } + int index=mCursor; + ensureCount(index+1); + mChildes[index]=block; + block.setIndex(index); + block.setParent(this); + mCursor++; + } + private void ensureCount(int count){ + if(count<= childesCount()){ + return; + } + Block[] old=mChildes; + mChildes=new Block[count]; + for(int i=0;i { + private final Block[] mChildes; + public FixedBlockContainer(int childesCount){ + super(); + mChildes=new Block[childesCount]; + } + public void addChild(int index, Block block){ + mChildes[index]=block; + block.setIndex(index); + block.setParent(this); + } + @Override + protected void onRefreshed(){ + } + @Override + public int childesCount() { + return mChildes.length; + } + @Override + public Block[] getChildes() { + return mChildes; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/PackageBody.java b/src/ARSCLib/com/reandroid/arsc/container/PackageBody.java new file mode 100755 index 00000000..04b02115 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/PackageBody.java @@ -0,0 +1,147 @@ + /* + * 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.container; + +import com.reandroid.arsc.chunk.*; +import com.reandroid.arsc.array.SpecTypePairArray; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.SpecHeader; +import com.reandroid.arsc.header.TypeHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.list.OverlayableList; +import com.reandroid.arsc.list.StagedAliasList; + +import java.io.IOException; + +public class PackageBody extends FixedBlockContainer { + + private final SpecTypePairArray mSpecTypePairArray; + private final LibraryBlock mLibraryBlock; + private final StagedAliasList mStagedAliasList; + private final OverlayableList mOverlayableList; + private final BlockList mOverlayablePolicyList; + private final BlockList mUnknownChunkList; + public PackageBody(){ + super(6); + this.mSpecTypePairArray = new SpecTypePairArray(); + this.mLibraryBlock = new LibraryBlock(); + this.mStagedAliasList = new StagedAliasList(); + this.mOverlayableList = new OverlayableList(); + this.mOverlayablePolicyList = new BlockList<>(); + this.mUnknownChunkList = new BlockList<>(); + + addChild(0, mSpecTypePairArray); + addChild(1, mLibraryBlock); + addChild(2, mStagedAliasList); + addChild(3, mOverlayableList); + addChild(4, mOverlayablePolicyList); + addChild(5, mUnknownChunkList); + } + public void destroy(){ + getSpecTypePairArray().clearChildes(); + getLibraryBlock().getLibraryInfoArray().clearChildes(); + getStagedAliasList().clearChildes(); + getOverlayableList().clearChildes(); + getOverlayablePolicyList().clearChildes(); + getUnknownChunkList().clearChildes(); + } + public OverlayableList getOverlayableList() { + return mOverlayableList; + } + public BlockList getOverlayablePolicyList() { + return mOverlayablePolicyList; + } + public StagedAliasList getStagedAliasList() { + return mStagedAliasList; + } + public LibraryBlock getLibraryBlock(){ + return mLibraryBlock; + } + public SpecTypePairArray getSpecTypePairArray() { + return mSpecTypePairArray; + } + public BlockList getUnknownChunkList(){ + return mUnknownChunkList; + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + boolean readOk=true; + while (readOk){ + readOk=readNextBlock(reader); + } + } + private boolean readNextBlock(BlockReader reader) throws IOException { + HeaderBlock headerBlock = reader.readHeaderBlock(); + if(headerBlock==null){ + return false; + } + int pos=reader.getPosition(); + ChunkType chunkType=headerBlock.getChunkType(); + if(chunkType==ChunkType.SPEC){ + readSpecBlock(reader); + }else if(chunkType==ChunkType.TYPE){ + readTypeBlock(reader); + }else if(chunkType==ChunkType.LIBRARY){ + readLibraryBlock(reader); + }else if(chunkType==ChunkType.OVERLAYABLE){ + readOverlayable(reader); + }else if(chunkType==ChunkType.OVERLAYABLE_POLICY){ + readOverlayablePolicy(reader); + }else if(chunkType==ChunkType.STAGED_ALIAS){ + readStagedAlias(reader); + }else { + readUnknownChunk(reader); + } + return pos!=reader.getPosition(); + } + private void readSpecBlock(BlockReader reader) throws IOException{ + SpecHeader specHeader = reader.readSpecHeader(); + SpecTypePair specTypePair = mSpecTypePairArray.getOrCreate(specHeader.getId().get()); + specTypePair.getSpecBlock().readBytes(reader); + } + private void readTypeBlock(BlockReader reader) throws IOException{ + TypeHeader typeHeader = reader.readTypeHeader(); + SpecTypePair specTypePair = mSpecTypePairArray.getOrCreate(typeHeader.getId().get()); + TypeBlock typeBlock = specTypePair.getTypeBlockArray().createNext(typeHeader.isSparse()); + typeBlock.readBytes(reader); + } + private void readLibraryBlock(BlockReader reader) throws IOException{ + LibraryBlock libraryBlock=new LibraryBlock(); + libraryBlock.readBytes(reader); + mLibraryBlock.addLibraryInfo(libraryBlock); + } + private void readStagedAlias(BlockReader reader) throws IOException{ + StagedAlias stagedAlias = new StagedAlias(); + stagedAlias.readBytes(reader); + mStagedAliasList.add(stagedAlias); + } + private void readOverlayable(BlockReader reader) throws IOException{ + Overlayable overlayable = new Overlayable(); + overlayable.readBytes(reader); + mOverlayableList.add(overlayable); + } + private void readOverlayablePolicy(BlockReader reader) throws IOException{ + OverlayablePolicy overlayablePolicy = new OverlayablePolicy(); + overlayablePolicy.readBytes(reader); + mOverlayablePolicyList.add(overlayablePolicy); + } + private void readUnknownChunk(BlockReader reader) throws IOException{ + UnknownChunk unknownChunk = new UnknownChunk(); + unknownChunk.readBytes(reader); + mUnknownChunkList.add(unknownChunk); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/ResValueContainer.java b/src/ARSCLib/com/reandroid/arsc/container/ResValueContainer.java new file mode 100755 index 00000000..a9b4a97b --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/ResValueContainer.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.arsc.container; + +import com.reandroid.arsc.base.BlockContainer; +import com.reandroid.arsc.value.ValueItem; + +public class ResValueContainer extends BlockContainer { + private final ValueItem[] mChildes; + public ResValueContainer(){ + super(); + mChildes=new ValueItem[1]; + } + @Override + protected void onRefreshed(){ + } + @Override + public int childesCount() { + return mChildes.length; + } + @Override + public ValueItem[] getChildes() { + return mChildes; + } + public void setResValue(ValueItem resValue){ + ValueItem old=getResValue(); + if(old!=null){ + old.setIndex(-1); + old.setParent(null); + } + mChildes[0]=resValue; + if(resValue==null){ + return; + } + resValue.setIndex(0); + resValue.setParent(this); + } + public ValueItem getResValue(){ + if(mChildes.length==0){ + return null; + } + return mChildes[0]; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/SingleBlockContainer.java b/src/ARSCLib/com/reandroid/arsc/container/SingleBlockContainer.java new file mode 100755 index 00000000..1135e84a --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/SingleBlockContainer.java @@ -0,0 +1,122 @@ + /* + * 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.container; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockContainer; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.OutputStream; + +public class SingleBlockContainer extends BlockContainer { + private T mItem; + public SingleBlockContainer(){ + super(); + } + @Override + protected void refreshChildes(){ + if(mItem!=null){ + if(mItem instanceof BlockContainer){ + ((BlockContainer)mItem).refresh(); + } + } + } + @Override + protected void onRefreshed() { + + } + public T getItem() { + return mItem; + } + public void setItem(T item) { + if(item==null){ + if(mItem!=null){ + mItem.setIndex(-1); + mItem.setParent(null); + } + mItem=null; + return; + } + this.mItem = item; + item.setIndex(getIndex()); + item.setParent(this); + } + public boolean hasItem(){ + return this.mItem!=null; + } + @Override + public byte[] getBytes() { + if(mItem!=null){ + return mItem.getBytes(); + } + return null; + } + @Override + public int countBytes() { + if(mItem!=null){ + return mItem.countBytes(); + } + return 0; + } + + @Override + public void onCountUpTo(BlockCounter counter) { + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + if(mItem!=null){ + mItem.onCountUpTo(counter); + } + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + if(mItem!=null){ + mItem.readBytes(reader); + } + } + @Override + public int onWriteBytes(OutputStream stream) throws IOException { + if(mItem!=null){ + return mItem.writeBytes(stream); + } + return 0; + } + + @Override + public int childesCount() { + return hasItem()?0:1; + } + + @Override + public T[] getChildes() { + return null; + } + + @Override + public String toString(){ + if(mItem!=null){ + return mItem.toString(); + } + return getClass().getSimpleName()+": EMPTY"; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/container/SpecTypePair.java b/src/ARSCLib/com/reandroid/arsc/container/SpecTypePair.java new file mode 100755 index 00000000..69f9cbe3 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/container/SpecTypePair.java @@ -0,0 +1,383 @@ +/* + * 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.container; + +import com.reandroid.arsc.array.EntryArray; +import com.reandroid.arsc.chunk.*; +import com.reandroid.arsc.array.TypeBlockArray; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockContainer; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.TypeHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.TypeString; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.*; + +public class SpecTypePair extends BlockContainer + implements JSONConvert, Comparable{ + private final Block[] mChildes; + private final SpecBlock mSpecBlock; + private final TypeBlockArray mTypeBlockArray; + + public SpecTypePair(SpecBlock specBlock, TypeBlockArray typeBlockArray){ + this.mSpecBlock = specBlock; + this.mTypeBlockArray = typeBlockArray; + + this.mChildes = new Block[]{specBlock, typeBlockArray}; + + specBlock.setIndex(0); + typeBlockArray.setIndex(1); + + specBlock.setParent(this); + typeBlockArray.setParent(this); + } + public SpecTypePair(){ + this(new SpecBlock(), new TypeBlockArray()); + } + + public Boolean hasComplexEntry(){ + return getTypeBlockArray().hasComplexEntry(); + } + public void linkTableStringsInternal(TableStringPool tableStringPool){ + for(TypeBlock typeBlock:listTypeBlocks()){ + typeBlock.linkTableStringsInternal(tableStringPool); + } + } + public void linkSpecStringsInternal(SpecStringPool specStringPool){ + for(TypeBlock typeBlock:listTypeBlocks()){ + typeBlock.linkSpecStringsInternal(specStringPool); + } + } + public Map createEntryGroups(){ + return createEntryGroups(false); + } + public Map createEntryGroups(boolean skipNullEntries){ + Map map = new LinkedHashMap<>(); + for(TypeBlock typeBlock : listTypeBlocks()){ + EntryArray entryArray = typeBlock.getEntryArray(); + for(Entry entry : entryArray.listItems(skipNullEntries)){ + if(entry == null){ + continue; + } + int id = entry.getResourceId(); + EntryGroup entryGroup = map.get(id); + if(entryGroup == null){ + entryGroup = new EntryGroup(id); + map.put(id, entryGroup); + } + entryGroup.add(entry); + } + } + return map; + } + public EntryGroup createEntryGroup(int id){ + id = 0xffff & id; + EntryGroup entryGroup = null; + for(TypeBlock typeBlock:listTypeBlocks()){ + Entry entry = typeBlock.getEntry(id); + if(entry == null){ + continue; + } + if(entryGroup == null){ + entryGroup = new EntryGroup(entry.getResourceId()); + } + entryGroup.add(entry); + } + return entryGroup; + } + public EntryGroup getEntryGroup(String entryName){ + EntryGroup entryGroup = null; + for(TypeBlock typeBlock:listTypeBlocks()){ + Entry entry = typeBlock.getEntry(entryName); + if(entry == null){ + continue; + } + if(entryGroup == null){ + entryGroup = new EntryGroup(entry.getResourceId()); + } + entryGroup.add(entry); + } + return entryGroup; + } + public void destroy(){ + getSpecBlock().destroy(); + getTypeBlockArray().destroy(); + } + public Entry getAnyEntry(short entryId){ + Entry result = null; + TypeBlock[] types = getTypeBlockArray().getChildes(); + for(int i = 0; i < types.length; i++){ + TypeBlock typeBlock = types[i]; + if(typeBlock == null){ + continue; + } + Entry entry = typeBlock.getEntry(entryId); + if(entry == null){ + continue; + } + if(!entry.isNull()){ + return entry; + } + if(result == null){ + result = entry; + } + } + return result; + } + public Entry getAnyEntry(String name){ + TypeBlock[] types = getTypeBlockArray().getChildes(); + for(int i = 0; i < types.length; i++){ + TypeBlock typeBlock = types[i]; + if(typeBlock == null){ + continue; + } + Entry entry = typeBlock.getEntry(name); + if(entry != null){ + return entry; + } + } + return null; + } + public Entry getEntry(ResConfig resConfig, String entryName){ + return getTypeBlockArray().getEntry(resConfig, entryName); + } + public void sortTypes(){ + getTypeBlockArray().sort(); + } + public boolean removeNullEntries(int startId){ + startId = 0x0000ffff & startId; + boolean removed = getTypeBlockArray().removeNullEntries(startId); + if(!removed){ + return false; + } + getSpecBlock().setEntryCount(startId); + return true; + } + public void removeEmptyTypeBlocks(){ + getTypeBlockArray().removeEmptyBlocks(); + } + public boolean isEmpty(){ + return getTypeBlockArray().isEmpty(); + } + public int countTypeBlocks(){ + return getTypeBlockArray().childesCount(); + } + public Entry getOrCreateEntry(short entryId, String qualifiers){ + return getTypeBlockArray().getOrCreateEntry(entryId, qualifiers); + } + public Entry getEntry(short entryId, String qualifiers){ + return getTypeBlockArray().getEntry(entryId, qualifiers); + } + public TypeBlock getOrCreateTypeBlock(String qualifiers){ + return getTypeBlockArray().getOrCreate(qualifiers); + } + public TypeBlock getOrCreateTypeBlock(ResConfig resConfig){ + return getTypeBlockArray().getOrCreate(resConfig); + } + public TypeBlock getTypeBlock(String qualifiers){ + return getTypeBlockArray().getTypeBlock(qualifiers); + } + public TypeBlock getTypeBlock(ResConfig resConfig){ + return getTypeBlockArray().getTypeBlock(resConfig); + } + public List listResConfig(){ + return mTypeBlockArray.listResConfig(); + } + + public Iterator iteratorNonEmpty(){ + return mTypeBlockArray.iteratorNonEmpty(); + } + public boolean hasDuplicateResConfig(boolean ignoreEmpty){ + return mTypeBlockArray.hasDuplicateResConfig(ignoreEmpty); + } + + public byte getTypeId(){ + return mSpecBlock.getTypeId(); + } + public int getId(){ + return mSpecBlock.getId(); + } + public void setTypeId(byte id){ + mSpecBlock.setTypeId(id); + mTypeBlockArray.setTypeId(id); + } + public String getTypeName(){ + TypeString typeString = getTypeString(); + if(typeString!=null){ + return typeString.get(); + } + return null; + } + public boolean isEqualTypeName(String typeName){ + return TypeBlock.isEqualTypeName(getTypeName(), typeName); + } + /** + * TOBEREMOVED + * + * It is allowed to have duplicate entry name therefore it is not recommend to use this. + * Lets depreciate to warn developer + */ + @Deprecated + public Entry searchByEntryName(String entryName){ + return getTypeBlockArray().searchByEntryName(entryName); + } + public SpecBlock getSpecBlock(){ + return mSpecBlock; + } + public TypeBlockArray getTypeBlockArray(){ + return mTypeBlockArray; + } + public PackageBlock getPackageBlock(){ + return getParent(PackageBlock.class); + } + public List listEntries(int entryId){ + List results=new ArrayList<>(); + Iterator itr = mTypeBlockArray.iterator(true); + while (itr.hasNext()){ + TypeBlock typeBlock=itr.next(); + Entry entry = typeBlock.getEntry(entryId); + if(entry ==null|| entry.isNull()){ + continue; + } + results.add(entry); + } + return results; + } + public List listEntries(String entryName){ + List results = new ArrayList<>(); + Iterator itr = mTypeBlockArray.iterator(true); + while (itr.hasNext()){ + TypeBlock typeBlock = itr.next(); + Entry entry = typeBlock.getEntry(entryName); + if(entry != null){ + results.add(entry); + } + } + return results; + } + public Collection listTypeBlocks(){ + return getTypeBlockArray().listItems(); + } + + @Override + protected void onRefreshed() { + + } + @Override + public int childesCount() { + return mChildes.length; + } + @Override + public Block[] getChildes() { + return mChildes; + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException { + HeaderBlock headerBlock=reader.readHeaderBlock(); + if(headerBlock==null){ + return; + } + ChunkType chunkType=headerBlock.getChunkType(); + if(chunkType == ChunkType.TYPE){ + readTypeBlock(reader); + return; + } + if(chunkType!=ChunkType.SPEC){ + readUnexpectedNonSpecBlock(reader, headerBlock); + } + mSpecBlock.readBytes(reader); + } + private void readTypeBlock(BlockReader reader) throws IOException { + TypeHeader typeHeader = reader.readTypeHeader(); + TypeBlock typeBlock = mTypeBlockArray.createNext(typeHeader.isSparse()); + typeBlock.readBytes(reader); + } + private void readUnexpectedNonSpecBlock(BlockReader reader, HeaderBlock headerBlock) throws IOException{ + throw new IOException("Unexpected block: "+headerBlock.toString()+", Should be: "+ChunkType.SPEC); + } + public int getHighestEntryCount(){ + return getTypeBlockArray().getHighestEntryCount(); + } + public TypeString getTypeString(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock!=null){ + return packageBlock.getTypeStringPool().getById(getId()); + } + return null; + } + + @Override + public JSONObject toJson() { + return toJson(false); + } + @Override + public void fromJson(JSONObject json) { + getSpecBlock().fromJson(json.getJSONObject(SpecBlock.NAME_spec)); + getTypeBlockArray().fromJson(json.optJSONArray(NAME_types)); + } + public JSONObject toJson(boolean specOnly) { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(SpecBlock.NAME_spec, getSpecBlock().toJson()); + if(!specOnly){ + jsonObject.put(NAME_types, getTypeBlockArray().toJson()); + } + return jsonObject; + } + public void merge(SpecTypePair typePair){ + if(typePair==null||typePair==this){ + return; + } + if(getTypeId() != typePair.getTypeId()){ + throw new IllegalArgumentException("Can not merge different id types: " + +getTypeId()+"!="+typePair.getTypeId()); + } + getSpecBlock().merge(typePair.getSpecBlock()); + getTypeBlockArray().merge(typePair.getTypeBlockArray()); + } + @Override + public int compareTo(SpecTypePair specTypePair) { + return Integer.compare(getId(), specTypePair.getId()); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(HexUtil.toHex2(getTypeId())); + builder.append(" ("); + TypeString ts = getTypeString(); + if(ts!=null){ + builder.append(ts.get()); + }else { + builder.append("null"); + } + builder.append(") config count="); + builder.append(getTypeBlockArray().childesCount()); + return builder.toString(); + } + + public static final String NAME_types = "types"; + public static final String NAME_sparse_types = "sparse_types"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/decoder/ColorUtil.java b/src/ARSCLib/com/reandroid/arsc/decoder/ColorUtil.java new file mode 100644 index 00000000..df734916 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/decoder/ColorUtil.java @@ -0,0 +1,152 @@ +/* + * 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.decoder; + +import com.reandroid.arsc.value.ValueType; + +public class ColorUtil { + + public static String decode(ValueType valueType, int data){ + if(valueType == null){ + return null; + } + StringBuilder builder = new StringBuilder(); + builder.append('#'); + switch (valueType){ + case INT_COLOR_RGB4: + builder.append(byteToHex(data >> 16)); + builder.append(byteToHex(data >> 8)); + builder.append(byteToHex(data)); + break; + case INT_COLOR_ARGB4: + builder.append(byteToHex(data >> 24)); + builder.append(byteToHex(data >> 16)); + builder.append(byteToHex(data >> 8)); + builder.append(byteToHex(data)); + break; + case INT_COLOR_RGB8: + builder.append(byteToHex(data >> 20)); + builder.append(byteToHex(data >> 16)); + builder.append(byteToHex(data >> 12)); + builder.append(byteToHex(data >> 8)); + builder.append(byteToHex(data >> 4)); + builder.append(byteToHex(data)); + break; + case INT_COLOR_ARGB8: + builder.append(byteToHex(data >> 28)); + builder.append(byteToHex(data >> 24)); + builder.append(byteToHex(data >> 20)); + builder.append(byteToHex(data >> 16)); + builder.append(byteToHex(data >> 12)); + builder.append(byteToHex(data >> 8)); + builder.append(byteToHex(data >> 4)); + builder.append(byteToHex(data)); + break; + default: + return null; + } + return builder.toString(); + } + public static ValueDecoder.EncodeResult encode(String hexColor){ + int[] values = hexToIntegers(hexColor); + if(values == null){ + return null; + } + ValueType valueType; + int color = 0; + + int len = values.length; + if (len == 4) { + valueType = ValueType.INT_COLOR_RGB4; + color |= 0xFF000000; + color |= (values[1] << 20); + color |= (values[1] << 16); + color |= (values[2] << 12); + color |= (values[2] << 8); + color |= (values[3] << 4); + color |= values[3]; + } else if (len == 5) { + valueType = ValueType.INT_COLOR_ARGB4; + color |= values[1] << 28; + color |= values[1] << 24; + color |= values[2] << 20; + color |= values[2] << 16; + color |= values[3] << 12; + color |= values[3] << 8; + color |= values[4] << 4; + color |= values[4]; + } else if (len == 7) { + valueType = ValueType.INT_COLOR_RGB8; + color |= 0xFF000000; + color |= values[1] << 20; + color |= values[2] << 16; + color |= values[3] << 12; + color |= values[4] << 8; + color |= values[5] << 4; + color |= values[6]; + } else if (len == 9) { + valueType = ValueType.INT_COLOR_ARGB8; + color |= values[1] << 28; + color |= values[2] << 24; + color |= values[3] << 20; + color |= values[4] << 16; + color |= values[5] << 12; + color |= values[6] << 8; + color |= values[7] << 4; + color |= values[8]; + }else { + return null; + } + return new ValueDecoder.EncodeResult(valueType, color); + } + private static char byteToHex(int i){ + i = i & 0xf; + if(i < 0xa){ + return (char) ('0' + i); + } + i = i - 0xa; + return (char) ('a' + i); + } + private static int[] hexToIntegers(String hexColor){ + if(hexColor == null){ + return null; + } + int length = hexColor.length(); + if(length < 4 || length > 9){ + return null; + } + hexColor = hexColor.toUpperCase(); + char[] chars = hexColor.toCharArray(); + if(chars[0] != '#'){ + return null; + } + length = chars.length; + int[] result = new int[length]; + for(int i = 1; i < length; i++){ + int ch = chars[i]; + int value; + if(ch >= '0' && ch <= '9'){ + value = ch - '0'; + }else if(ch >= 'A' && ch <= 'F'){ + value = 10 + (ch - 'A'); + }else { + return null; + } + result[i] = value; + } + return result; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/decoder/ComplexUtil.java b/src/ARSCLib/com/reandroid/arsc/decoder/ComplexUtil.java new file mode 100644 index 00000000..e2648034 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/decoder/ComplexUtil.java @@ -0,0 +1,220 @@ +/* + * 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.decoder; + +public class ComplexUtil { + + public static String decodeComplex(boolean fraction, int complex_value){ + int radixFlag = (complex_value >> COMPLEX_RADIX_SHIFT) & COMPLEX_RADIX_MASK; + Radix radix = Radix.forFlag(radixFlag); + + int mantissa = (complex_value & ( COMPLEX_MANTISSA_MASK <> COMPLEX_UNIT_SHIFT) & COMPLEX_UNIT_MASK; + Unit unit = Unit.fromFlag(fraction, unit_type); + return radix.formatFloat(fraction, value) + unit.getSymbol(); + } + public static int encodeComplex(float value, String unit){ + return encodeComplex(value, Unit.fromSymbol(unit)); + } + public static int encodeComplex(float value, Unit unit){ + boolean neg = value < 0; + if (neg) { + value = -value; + } + long bits = (long)(value*(1<<23) + 0.5f); + + Radix radix = Radix.getRadix(bits); + int mantissa = (int)((bits>>radix.getShift()) & COMPLEX_MANTISSA_MASK); + if (neg) { + mantissa = (-mantissa) & COMPLEX_MANTISSA_MASK; + } + int result = (radix.getFlag()<= 0.5f){ + i = i + 1; + } + value = ((float) i)/multiplier; + if(neg){ + value = -value; + } + if(scale){ + value = value * 100.0f; + } + return Float.toString(value); + } + public static Radix forFlag(int flag){ + if(flag == 0){ + return RADIX_23p0; + } + if(flag == 1){ + return RADIX_16p7; + } + if(flag == 2){ + return RADIX_8p15; + } + if(flag == 3){ + return RADIX_0p23; + } + throw new NumberFormatException("Unknown radix flag = "+flag); + } + public static Radix getRadix(long bits){ + if ((bits&0x7fffff) == 0) { + return RADIX_23p0; + } + if ((bits&0xffffffffff800000L) == 0) { + return RADIX_0p23; + } + if ((bits&0xffffffff80000000L) == 0) { + return RADIX_8p15; + } + if ((bits&0xffffff8000000000L) == 0) { + return RADIX_16p7; + } + throw new NumberFormatException("Radix bits out of range bits = "+bits); + } + public int getFlag() { + return flag; + } + public int getShift() { + return shift; + } + public float getMultiplier() { + return multiplier; + } + } + private static final int COMPLEX_RADIX_SHIFT = 4; + private static final int COMPLEX_RADIX_MASK = 0x3; + private static final int COMPLEX_MANTISSA_SHIFT = 8; + private static final int COMPLEX_MANTISSA_MASK = 0x00ffffff; + private static final float MANTISSA_MULT = (1.0f / (1 << 8)); + private static final int COMPLEX_UNIT_SHIFT = 0; + private static final int COMPLEX_UNIT_MASK = 0xf; +} diff --git a/src/ARSCLib/com/reandroid/arsc/decoder/Decoder.java b/src/ARSCLib/com/reandroid/arsc/decoder/Decoder.java new file mode 100644 index 00000000..5478e675 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/decoder/Decoder.java @@ -0,0 +1,188 @@ +/* + * 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.decoder; + +import com.reandroid.apk.AndroidFrameworks; +import com.reandroid.apk.FrameworkApk; +import com.reandroid.arsc.ApkFile; +import com.reandroid.arsc.chunk.MainChunk; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.FrameworkTable; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.AttributeValue; +import com.reandroid.arsc.value.Value; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; + +import java.io.IOException; + +public class Decoder { + private final EntryStore entryStore; + private int currentPackageId; + private ApkFile mApkFile; + public Decoder(EntryStore entryStore, int currentPackageId){ + this.entryStore = entryStore; + this.currentPackageId = currentPackageId; + } + public String decodeResourceName(int resourceId){ + return decodeResourceName(resourceId, true); + } + public String decodeResourceName(int resourceId, boolean defaultHex){ + if(resourceId == 0){ + return null; + } + EntryGroup entryGroup = getEntryStore().getEntryGroup(resourceId); + if(entryGroup!=null){ + return entryGroup.getSpecName(); + } + if(defaultHex){ + return hexResourceName(resourceId); + } + return null; + } + private String hexResourceName(int resourceId){ + return HexUtil.toHex8("@0x", resourceId); + } + public String decodeValue(Value value){ + if(value==null){ + return null; + } + ValueType valueType = value.getValueType(); + if(valueType == ValueType.STRING){ + return value.getValueAsString(); + } + return ValueDecoder.decode(getEntryStore(), getCurrentPackageId(), value); + } + public String decodeAttributeValue(AttributeValue attributeValue){ + return decodeAttributeValue(attributeValue, true); + } + public String decodeAttributeValue(AttributeValue attributeValue, boolean escapeStartChar){ + if(attributeValue == null){ + return null; + } + String result = ValueDecoder.decode(getEntryStore(), getCurrentPackageId(), attributeValue); + if(!escapeStartChar || result == null || result.length() == 0 + || attributeValue.getValueType() != ValueType.STRING){ + return result; + } + return ValueDecoder.escapeSpecialCharacter(result); + } + private EntryStore getEntryStore() { + return entryStore; + } + public int getCurrentPackageId() { + return currentPackageId; + } + public void setCurrentPackageId(int currentPackageId) { + this.currentPackageId = currentPackageId; + } + public ApkFile getApkFile(){ + return mApkFile; + } + public void setApkFile(ApkFile apkFile) { + this.mApkFile = apkFile; + } + public boolean isNullDecoder(){ + return false; + } + + public static Decoder create(ResXmlDocument resXmlDocument){ + MainChunk mainChunk = resXmlDocument.getMainChunk(); + if(mainChunk == null){ + return getNullEntryStoreDecoder(); + } + ApkFile apkFile = mainChunk.getApkFile(); + if(apkFile == null){ + return getNullEntryStoreDecoder(); + } + TableBlock tableBlock = apkFile.getTableBlock(); + if(tableBlock == null){ + return getNullEntryStoreDecoder(); + } + AndroidManifestBlock manifestBlock = apkFile.getAndroidManifestBlock(); + if(manifestBlock!=null){ + int currentPackageId = manifestBlock.guessCurrentPackageId(); + if(currentPackageId!=0){ + return create(tableBlock, currentPackageId); + } + } + return create(tableBlock); + } + public static Decoder create(TableBlock tableBlock){ + if(!tableBlock.hasFramework() && !tableBlock.isAndroid()){ + tableBlock.addFramework(getFramework()); + } + int currentPackageId; + PackageBlock packageBlock = tableBlock.pickOne(); + if(packageBlock!=null){ + currentPackageId = packageBlock.getId(); + }else { + // 0x7f most common + currentPackageId = 0x7f; + } + return create(tableBlock, currentPackageId); + } + public static Decoder create(TableBlock tableBlock, int currentPackageId){ + if(!tableBlock.hasFramework() && !tableBlock.isAndroid()){ + TableBlock framework = getFramework(); + if(framework!=null){ + PackageBlock packageBlock = framework.pickOne(); + if(packageBlock!=null && packageBlock.getId() != currentPackageId){ + tableBlock.addFramework(framework); + } + } + } + return new Decoder(tableBlock, currentPackageId); + } + private static TableBlock getFramework(){ + try { + FrameworkApk frameworkApk = AndroidFrameworks.getCurrent(); + if(frameworkApk == null){ + frameworkApk = AndroidFrameworks.getLatest(); + AndroidFrameworks.setCurrent(frameworkApk); + } + return frameworkApk.getTableBlock(); + } catch (IOException ignored) { + } + // Should not reach here but to be safe return dummy + return new FrameworkTable(); + } + + public static Decoder getNullEntryStoreDecoder(){ + if(NULL_ENTRY_STORE_DECODER!=null){ + return NULL_ENTRY_STORE_DECODER; + } + synchronized (Decoder.class){ + NullEntryDecoder decoder = new NullEntryDecoder(getFramework(), 0x7f); + NULL_ENTRY_STORE_DECODER = decoder; + return decoder; + } + } + static class NullEntryDecoder extends Decoder{ + public NullEntryDecoder(EntryStore entryStore, int currentPackageId) { + super(entryStore, currentPackageId); + } + @Override + public boolean isNullDecoder(){ + return true; + } + } + private static NullEntryDecoder NULL_ENTRY_STORE_DECODER; +} diff --git a/src/ARSCLib/com/reandroid/arsc/decoder/ThreeByteCharsetDecoder.java b/src/ARSCLib/com/reandroid/arsc/decoder/ThreeByteCharsetDecoder.java new file mode 100644 index 00000000..0f37c363 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/decoder/ThreeByteCharsetDecoder.java @@ -0,0 +1,222 @@ + /* + * 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.decoder; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; + +public class ThreeByteCharsetDecoder extends CharsetDecoder { + public static final ThreeByteCharsetDecoder INSTANCE = new ThreeByteCharsetDecoder(); + public ThreeByteCharsetDecoder() { + super(StandardCharsets.UTF_8, 1.0F, 1.0F); + } + @Override + protected CoderResult decodeLoop(ByteBuffer src, CharBuffer dst) { + return src.hasArray() && dst.hasArray() ? this.decodeArrayLoop(src, dst) : this.decodeBufferLoop(src, dst); + } + private CoderResult decodeArrayLoop(ByteBuffer src, CharBuffer dst) { + byte[] srcBytes = src.array(); + int sourcePosition = src.arrayOffset() + src.position(); + int sourceLimit = src.arrayOffset() + src.limit(); + char[] dstChars = dst.array(); + int dstPosition = dst.arrayOffset() + dst.position(); + int dstLimit = dst.arrayOffset() + dst.limit(); + int min = sourceLimit - sourcePosition; + int start = min; + min = dstLimit - dstPosition; + if(min < start){ + start = min; + } + start = dstPosition + start; + + while ( dstPosition < start && srcBytes[sourcePosition] >= 0) { + dstChars[dstPosition++] = (char) srcBytes[sourcePosition++]; + } + + while (sourcePosition < sourceLimit) { + int b1 = srcBytes[sourcePosition]; + if (b1 < 0) { + if (b1 >> 5 == -2 && (b1 & 0x1E) != 0) { + if (sourceLimit - sourcePosition < 2 || dstPosition >= dstLimit) { + return xFlow(src, sourcePosition, sourceLimit, dst, dstPosition, 2); + } + int b2 = srcBytes[sourcePosition + 1]; + if (isNotContinuation(b2)) { + return malformedForLength(src, sourcePosition, dst, dstPosition); + } + dstChars[dstPosition++] = (char) (b1 << 6 ^ b2 ^ 0x0F80); + sourcePosition += 2; + } else { + if (b1 >> 4 != -2) { + return malformed(src, sourcePosition, dst, dstPosition, 1); + } + + int srcRemaining = sourceLimit - sourcePosition; + if (srcRemaining < 3 || dstPosition >= dstLimit) { + if (srcRemaining > 1 && isMalformed3_2(b1, srcBytes[sourcePosition + 1])) { + return malformedForLength(src, sourcePosition, dst, dstPosition); + } + + return xFlow(src, sourcePosition, sourceLimit, dst, dstPosition, 3); + } + + int b2 = srcBytes[sourcePosition + 1]; + int b3 = srcBytes[sourcePosition + 2]; + if (isMalformed3(b1, b2, b3)) { + return malformed(src, sourcePosition, dst, dstPosition, 3); + } + + dstChars[dstPosition++] = (char) (b1 << 12 ^ b2 << 6 ^ b3 ^ 0xFFFE1F80); + sourcePosition += 3; + } + } else { + if (dstPosition >= dstLimit) { + return xFlow(src, sourcePosition, sourceLimit, dst, dstPosition, 1); + } + + dstChars[dstPosition++] = (char) b1; + ++sourcePosition; + } + } + + return xFlow(src, sourcePosition, sourceLimit, dst, dstPosition, 0); + } + private CoderResult decodeBufferLoop(ByteBuffer src, CharBuffer dst) { + int mark = src.position(); + int limit = src.limit(); + + while (mark < limit) { + int b1 = src.get(); + if (b1 < 0) { + if (b1 >> 5 == -2 && (b1 & 0x1E) != 0) { + if (limit - mark < 2 || dst.remaining() < 1) { + return xFlow(src, mark, 2); + } + int b2 = src.get(); + if (isNotContinuation(b2)) { + return malformedForLength(src, mark); + } + dst.put((char) (b1 << 6 ^ b2 ^ 0x0F80)); + mark += 2; + } else { + if (b1 >> 4 != -2) { + return malformed(src, mark, 1); + } + + int srcRemaining = limit - mark; + if (srcRemaining < 3 || dst.remaining() < 1) { + if (srcRemaining > 1 && isMalformed3_2(b1, src.get())) { + return malformedForLength(src, mark); + } + + return xFlow(src, mark, 3); + } + + int b2 = src.get(); + int b3 = src.get(); + if (isMalformed3(b1, b2, b3)) { + return malformed(src, mark, 3); + } + + dst.put((char) (b1 << 12 ^ b2 << 6 ^ b3 ^ 0xFFFE1F80)); + mark += 3; + } + } else { + if (dst.remaining() < 1) { + return xFlow(src, mark, 1); + } + + dst.put((char) b1); + ++mark; + } + } + return xFlow(src, mark, 0); + } + + private static void updatePositions(Buffer src, int sourcePosition, Buffer dst, int dstPosition) { + src.position(sourcePosition - src.arrayOffset()); + dst.position(dstPosition - dst.arrayOffset()); + } + private static boolean isNotContinuation(int b) { + return (b & 0xC0) != 0x80; + } + private static boolean isMalformed3(int b1, int b2, int b3) { + return b1 == -32 && (b2 & 0xE0) == 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80; + } + private static boolean isMalformed3_2(int b1, int b2) { + return b1 == -32 && (b2 & 0xE0) == 0x80 || (b2 & 0xC0) != 0x80; + } + private static CoderResult malformedN(ByteBuffer src, int nb) { + int b1; + int b2; + switch (nb) { + case 1: + case 2: + return CoderResult.malformedForLength(1); + case 3: + b1 = src.get(); + b2 = src.get(); + return CoderResult.malformedForLength((b1 != -32 || (b2 & 0xE0) != 0x80) && !isNotContinuation(b2) ? 2 : 1); + case 4: + b1 = src.get() & 0xFF; + b2 = src.get() & 0xFF; + if (b1 <= 244 + && (b1 != 0xF0 || b2 >= 144 && b2 <= 0xBF) + && (b1 != 244 || (b2 & 0xF0) == 0x80) + && !isNotContinuation(b2)) { + if (isNotContinuation(src.get())) { + return CoderResult.malformedForLength(2); + } + return CoderResult.malformedForLength(3); + } + return CoderResult.malformedForLength(1); + default: + return null; + } + } + private static CoderResult malformed(ByteBuffer src, int sourcePosition, CharBuffer dst, int dstPosition, int numBytes) { + src.position(sourcePosition - src.arrayOffset()); + CoderResult cr = malformedN(src, numBytes); + updatePositions(src, sourcePosition, dst, dstPosition); + return cr; + } + private static CoderResult malformed(ByteBuffer src, int mark, int nb) { + src.position(mark); + CoderResult cr = malformedN(src, nb); + src.position(mark); + return cr; + } + private static CoderResult malformedForLength(ByteBuffer src, int sourcePosition, CharBuffer dst, int dstPosition) { + updatePositions(src, sourcePosition, dst, dstPosition); + return CoderResult.malformedForLength(1); + } + private static CoderResult malformedForLength(ByteBuffer src, int mark) { + src.position(mark); + return CoderResult.malformedForLength(1); + } + private static CoderResult xFlow(Buffer src, int sourcePosition, int sourceLimit, Buffer dst, int dstPosition, int numBytes) { + updatePositions(src, sourcePosition, dst, dstPosition); + return numBytes != 0 && sourceLimit - sourcePosition >= numBytes ? CoderResult.OVERFLOW : CoderResult.UNDERFLOW; + } + private static CoderResult xFlow(Buffer src, int mark, int nb) { + src.position(mark); + return nb != 0 && src.remaining() >= nb ? CoderResult.OVERFLOW : CoderResult.UNDERFLOW; + } +} \ No newline at end of file diff --git a/src/ARSCLib/com/reandroid/arsc/decoder/ValueDecoder.java b/src/ARSCLib/com/reandroid/arsc/decoder/ValueDecoder.java new file mode 100755 index 00000000..151768ff --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/decoder/ValueDecoder.java @@ -0,0 +1,922 @@ +/* + * 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.decoder; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.*; +import com.reandroid.arsc.value.attribute.AttributeBag; +import com.reandroid.common.EntryStore; + +import java.util.Collection; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ValueDecoder { + + public static String escapeSpecialCharacter(String text){ + if(text==null || text.length()==0){ + return text; + } + if(isSpecialCharacter(text.charAt(0))){ + return '\\' +text; + } + return text; + } + public static String unEscapeSpecialCharacter(String text){ + if(text==null || text.length()<2){ + return text; + } + if(text.charAt(0)!='\\' || !isSpecialCharacter(text.charAt(1))){ + return text; + } + return text.substring(1); + } + public static String quoteWhitespace(String text){ + if(!isWhiteSpace(text)){ + return text; + } + return "\"" + text + "\""; + } + public static String unQuoteWhitespace(String text){ + if(text == null || text.length() < 3){ + return text; + } + if(text.charAt(0) != '"' || text.charAt(text.length()-1) != '"'){ + return text; + } + String unQuoted = text.substring(1, text.length()-1); + if(!isWhiteSpace(unQuoted)){ + return text; + } + return unQuoted; + } + private static boolean isWhiteSpace(String text){ + if(text == null || text.length() == 0){ + return false; + } + char[] chars = text.toCharArray(); + for(int i = 0; i < chars.length; i++){ + if(!isWhiteSpace(chars[i])){ + return false; + } + } + return true; + } + private static boolean isWhiteSpace(char ch){ + switch (ch){ + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } + } + private static boolean isSpecialCharacter(char ch){ + switch (ch){ + case '@': + case '?': + case '#': + return true; + default: + return false; + } + } + public static EncodeResult encodeGuessAny(String txt){ + if(txt==null){ + return null; + } + EncodeResult result = encodeNullReference(txt); + if(result != null){ + return result; + } + result=encodeColor(txt); + if(result!=null){ + return result; + } + result=encodeDimensionOrFloat(txt); + if(result!=null){ + return result; + } + result=encodeHexOrInt(txt); + if(result!=null){ + return result; + } + return encodeBoolean(txt); + } + public static EncodeResult encodeBoolean(String txt){ + if(txt==null){ + return null; + } + txt=txt.trim().toLowerCase(); + if(txt.equals("true")){ + return new EncodeResult(ValueType.INT_BOOLEAN, 0xffffffff); + } + if(txt.equals("false")){ + return new EncodeResult(ValueType.INT_BOOLEAN, 0); + } + return null; + } + public static EncodeResult encodeNullReference(String txt){ + if(txt==null){ + return null; + } + if("@empty".equals(txt)){ + return new EncodeResult(ValueType.NULL, 1); + } + if("@null".equals(txt)){ + return new EncodeResult(ValueType.REFERENCE, 0); + } + if("?null".equals(txt)){ + return new EncodeResult(ValueType.ATTRIBUTE, 0); + } + return null; + } + public static EncodeResult encodeHexReference(String txt){ + if(txt==null){ + return null; + } + txt=txt.trim().toLowerCase(); + Matcher matcher = PATTERN_HEX_REFERENCE.matcher(txt); + if(!matcher.find()){ + return null; + } + String prefix = matcher.group(1); + int value = parseHex(matcher.group(2)); + ValueType valueType; + if("?".equals(prefix)){ + valueType = ValueType.ATTRIBUTE; + }else { + valueType = ValueType.REFERENCE; + } + return new EncodeResult(valueType, value); + } + public static boolean isInteger(String txt){ + if(txt==null){ + return false; + } + return PATTERN_INTEGER.matcher(txt).matches(); + } + public static boolean isHex(String txt){ + if(txt==null){ + return false; + } + return PATTERN_HEX.matcher(txt).matches(); + } + public static boolean isReference(String txt){ + if(txt==null){ + return false; + } + if(isNullReference(txt)){ + return true; + } + if(isHexReference(txt)){ + return true; + } + return PATTERN_REFERENCE.matcher(txt).matches(); + } + private static boolean isHexReference(String txt){ + return PATTERN_HEX_REFERENCE.matcher(txt).matches(); + } + private static boolean isNullReference(String txt){ + if("@null".equals(txt)||"?null".equals(txt)){ + return true; + } + if("@empty".equals(txt)){ + return true; + } + return false; + } + public static EncodeResult encodeColor(String value){ + return ColorUtil.encode(value); + } + public static EncodeResult encodeHexOrInt(String numString){ + if(numString==null){ + return null; + } + if(isHex(numString)){ + return new EncodeResult(ValueType.INT_HEX, parseHex(numString)); + } + if(isInteger(numString)){ + return new EncodeResult(ValueType.INT_DEC, parseInteger(numString)); + } + return null; + } + public static int parseHex(String hexString){ + boolean negative=false; + hexString=hexString.trim().toLowerCase(); + if(hexString.startsWith("-")){ + negative=true; + hexString=hexString.substring(1); + } + if(!hexString.startsWith("0x")){ + hexString="0x"+hexString; + } + long l=Long.decode(hexString); + if(negative){ + l=-l; + } + return (int) l; + } + public static int parseInteger(String intString){ + intString=intString.trim(); + boolean negative=false; + if(intString.startsWith("-")){ + negative=true; + intString=intString.substring(1); + } + long l=Long.parseLong(intString); + if(negative){ + l=-l; + } + return (int) l; + } + public static EncodeResult encodeDimensionOrFloat(String value){ + if(value==null){ + return null; + } + EncodeResult result = encodeFloat(value); + if(result == null){ + result = encodeDimensionOrFraction(value); + } + return result; + } + public static EncodeResult encodeFloat(String txt){ + Float value = parseFloat(txt); + if(value==null){ + return null; + } + return new EncodeResult(ValueType.FLOAT, + Float.floatToIntBits(value)); + } + public static Float parseFloat(String txt){ + if(txt==null || txt.indexOf('.')<0){ + return null; + } + try{ + return Float.parseFloat(txt); + }catch (NumberFormatException ignored){ + return null; + } + } + public static EncodeResult encodeDimensionOrFraction(String value){ + if(value==null){ + return null; + } + Matcher matcher = PATTERN_DIMEN.matcher(value); + if(!matcher.find()){ + return null; + } + String number = matcher.group(1); + String unit = matcher.group(4); + float fraction = Float.parseFloat(number); + return encodeDimensionOrFraction(fraction, unit); + } + private static EncodeResult encodeDimensionOrFraction(float value, String unitSymbol){ + ComplexUtil.Unit unit = ComplexUtil.Unit.fromSymbol(unitSymbol); + ValueType valueType; + if(unit == ComplexUtil.Unit.FRACTION || unit == ComplexUtil.Unit.FRACTION_PARENT){ + valueType = ValueType.FRACTION; + value = value / 100.0f; + }else { + valueType = ValueType.DIMENSION; + } + int result = ComplexUtil.encodeComplex(value, unit); + return new EncodeResult(valueType, result); + } + + public static String decodeAttributeName(EntryStore store, PackageBlock currentPackage, int resourceId){ + EntryGroup entryGroup=searchEntryGroup(store, currentPackage, resourceId); + if(entryGroup==null){ + return HexUtil.toHex8("@0x", resourceId); + } + Entry entry = entryGroup.pickOne(); + if(entry == null){ + return HexUtil.toHex8("@0x", resourceId); + } + String prefix=null; + if(currentPackage!=null){ + String name=currentPackage.getName(); + String other= entry.getPackageBlock().getName(); + if(!name.equals(other)){ + prefix=other+":"; + } + } + String name=entryGroup.getSpecName(); + if(prefix!=null){ + name=prefix+name; + } + return name; + } + public static String decodeAttribute(EntryStore store, int attrResId, int rawValue){ + AttributeBag attributeBag = getAttributeBag(store, attrResId); + if(attributeBag==null){ + return null; + } + return attributeBag.decodeAttributeValue(store, rawValue); + } + + public static String decodeEntryValue(EntryStore store, PackageBlock currentPackage, ValueType valueType, int data){ + if(store==null || currentPackage==null){ + return null; + } + if(valueType==ValueType.STRING){ + return decodeIntEntryString(currentPackage, data); + } + boolean is_reference=false; + boolean is_attribute=false; + if(valueType==ValueType.REFERENCE){ + if(data==0){ + return "@null"; + } + is_reference=true; + } + if(valueType==ValueType.ATTRIBUTE){ + if(data==0){ + return "?null"; + } + is_attribute=true; + } + if(is_reference || is_attribute){ + String ref=buildReferenceValue(store, valueType, currentPackage.getName(), data); + if(ref!=null){ + return ref; + } + char atOrQues=is_reference?'@':'?'; + ref=atOrQues+toHexResourceId(data); + return ref; + } + return decode(valueType, data); + } + public static String decodeIntEntry(EntryStore store, ResValue resValue){ + if(resValue ==null){ + return null; + } + Entry parentEntry = resValue.getEntry(); + if(parentEntry==null){ + return null; + } + ValueType valueType= resValue.getValueType(); + int data= resValue.getData(); + return decodeIntEntry(store, parentEntry, valueType, data); + } + public static String decodeIntEntry(EntryStore store, ResValueMap bagItem){ + if(bagItem==null){ + return null; + } + Entry parentEntry=bagItem.getEntry(); + if(parentEntry==null){ + return null; + } + ValueType valueType=bagItem.getValueType(); + int data=bagItem.getData(); + return decodeIntEntry(store, parentEntry, valueType, data); + } + public static String decodeIntEntry(EntryStore store, Entry parentEntry, ValueType valueType, int data){ + if(valueType == ValueType.NULL){ + return decodeNull(data); + } + if(valueType==ValueType.STRING){ + return decodeIntEntryString(parentEntry, data); + } + boolean is_reference=false; + boolean is_attribute=false; + if(valueType==ValueType.REFERENCE){ + if(data==0){ + return "@null"; + } + is_reference=true; + } + if(valueType==ValueType.ATTRIBUTE){ + if(data==0){ + return "?null"; + } + is_attribute=true; + } + if(is_reference || is_attribute){ + String ref=buildReferenceValue(store, parentEntry, valueType, data); + if(ref!=null){ + return ref; + } + char atOrQues=is_reference?'@':'?'; + ref=atOrQues+toHexResourceId(data); + return ref; + } + return decode(valueType, data); + } + public static String buildReferenceValue(EntryStore store, Entry entry){ + if(entry ==null){ + return null; + } + TableEntry tableEntry = entry.getTableEntry(); + if(tableEntry == null || (tableEntry instanceof CompoundEntry)){ + return null; + } + ResValue resValue = (ResValue) tableEntry.getValue(); + int resourceId = resValue.getData(); + ValueType valueType= resValue.getValueType(); + return buildReferenceValue(store, entry, valueType, resourceId); + } + public static String decode(EntryStore entryStore, int currentPackageId, Value value){ + + ValueType valueType = value.getValueType(); + if(valueType == ValueType.STRING){ + return value.getValueAsString(); + } + int data = value.getData(); + if(valueType==ValueType.REFERENCE || valueType==ValueType.ATTRIBUTE){ + String currentPackageName = getPackageName(entryStore, currentPackageId); + return buildReferenceValue(entryStore, + valueType, currentPackageName, + data); + } + return decode(valueType, data); + } + public static String decode(EntryStore entryStore, int currentPackageId, AttributeValue attributeValue){ + ValueType valueType = attributeValue.getValueType(); + if(valueType == ValueType.STRING){ + return attributeValue.getValueAsString(); + } + int data = attributeValue.getData(); + if(valueType==ValueType.REFERENCE || valueType==ValueType.ATTRIBUTE){ + String currentPackageName = getPackageName(entryStore, currentPackageId); + return buildReferenceValue(entryStore, + valueType, currentPackageName, + data); + } + if(valueType==ValueType.INT_DEC || valueType==ValueType.INT_HEX){ + String result = decodeAttribute(entryStore, + attributeValue.getNameResourceID(), + attributeValue.getData()); + if(result!=null){ + return result; + } + } + return decode(valueType, data); + } + @Deprecated + public static String decode(EntryStore entryStore, int currentPackageId, int nameResourceId, ValueType valueType, int rawVal){ + String currPackageName=getPackageName(entryStore, currentPackageId); + String result=buildReferenceValue(entryStore, valueType, currPackageName, rawVal); + if(result!=null){ + return result; + } + if(valueType==ValueType.STRING){ + // Should not happen the string could be in ResXmlBlock, but if you are lazy here it goes + return decodeString(entryStore, currentPackageId, rawVal); + } + if(valueType==ValueType.INT_DEC||valueType==ValueType.INT_HEX){ + result=decodeAttribute(entryStore, nameResourceId, rawVal); + if(result!=null){ + return result; + } + } + return decode(valueType, rawVal); + } + public static String decode(ValueType valueType, int data){ + if(valueType==null){ + return null; + } + String hexColor = ColorUtil.decode(valueType, data); + if(hexColor != null){ + return hexColor; + } + switch (valueType){ + case INT_BOOLEAN: + return decodeBoolean(data); + case DIMENSION: + case FLOAT: + case FRACTION: + return decodeDimensionOrFloat(valueType, data); + case INT_HEX: + return decodeHex(data); + case INT_DEC: + return decodeInt(data); + case NULL: + return decodeNull(data); + } + return null; + } + public static String buildReference(String currentPackageName, + String referredPackageName, + char atOrQues, + String typeName, + String resourceName){ + StringBuilder builder=new StringBuilder(); + if(atOrQues!=0){ + builder.append(atOrQues); + } + if(!isEqualString(currentPackageName, referredPackageName)){ + if(!isEmpty(currentPackageName) && !isEmpty(referredPackageName)){ + builder.append(referredPackageName); + if(!referredPackageName.endsWith(":")){ + builder.append(':'); + } + } + } + if(!isEmpty(typeName)){ + builder.append(typeName); + builder.append('/'); + } + builder.append(resourceName); + return builder.toString(); + } + + private static String buildReferenceValue(EntryStore store, Entry entry, ValueType valueType, int resourceId){ + if(entry ==null){ + return null; + } + EntryGroup value=searchEntryGroup(store, entry, resourceId); + if(value==null){ + return null; + } + return buildReferenceValue(valueType, entry, value); + } + private static String buildReferenceValue(ValueType valueType, Entry entry, EntryGroup value){ + char atOrQues; + if(valueType==ValueType.REFERENCE){ + atOrQues='@'; + }else if(valueType==ValueType.ATTRIBUTE){ + atOrQues='?'; + }else { + atOrQues=0; + } + String currentPackageName=getPackageName(entry); + String referredPackageName=getPackageName(value); + String typeName=value.getTypeName(); + String name=value.getSpecName(); + return buildReference(currentPackageName, referredPackageName, atOrQues, typeName, name); + } + private static String buildReferenceValue(EntryStore entryStore, ValueType valueType, String currentPackageName, int resourceId){ + char atOrQues; + if(valueType==ValueType.REFERENCE){ + if(resourceId==0){ + return "@null"; + } + atOrQues='@'; + }else if(valueType==ValueType.ATTRIBUTE){ + if(resourceId==0){ + return "?null"; + } + atOrQues='?'; + }else { + return null; + } + EntryGroup value=null; + if(entryStore!=null){ + value=entryStore.getEntryGroup(resourceId); + } + if(value==null){ + return atOrQues+toHexResourceId(resourceId); + } + String referredPackageName=getPackageName(value); + String typeName=value.getTypeName(); + String name=value.getSpecName(); + return buildReference(currentPackageName, referredPackageName, atOrQues, typeName, name); + } + private static String getPackageName(EntryStore entryStore, int packageOrResourceId){ + if(entryStore==null || packageOrResourceId==0){ + return null; + } + int pkgId=(packageOrResourceId>>24)&0xFF; + if(pkgId==0){ + pkgId=packageOrResourceId; + } + pkgId = pkgId & 0xff; + Collection allPkg = entryStore.getPackageBlocks(pkgId); + if(allPkg==null){ + return null; + } + for(PackageBlock packageBlock:allPkg){ + String name=packageBlock.getName(); + if(name!=null){ + return name; + } + } + return null; + } + private static String getPackageName(EntryGroup entryGroup){ + if(entryGroup==null){ + return null; + } + return getPackageName(entryGroup.pickOne()); + } + private static String getPackageName(Entry entry){ + if(entry ==null){ + return null; + } + PackageBlock packageBlock= entry.getPackageBlock(); + if(packageBlock==null){ + return null; + } + return packageBlock.getName(); + } + private static EntryGroup searchEntryGroup(EntryStore store, Entry entry, int resourceId){ + EntryGroup entryGroup=searchEntryGroup(entry, resourceId); + if(entryGroup!=null){ + return entryGroup; + } + if(store==null){ + return null; + } + return store.getEntryGroup(resourceId); + } + private static EntryGroup searchEntryGroup(Entry entry, int resourceId){ + if(entry ==null){ + return null; + } + PackageBlock packageBlock= entry.getPackageBlock(); + if(packageBlock==null){ + return null; + } + TableBlock tableBlock=packageBlock.getTableBlock(); + if(tableBlock==null){ + return null; + } + for(PackageBlock pkg:tableBlock.listPackages()){ + EntryGroup entryGroup=pkg.getEntryGroup(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + } + return null; + } + private static EntryGroup searchEntryGroup(EntryStore store, PackageBlock packageBlock, int resourceId){ + if(packageBlock!=null){ + TableBlock tableBlock=packageBlock.getTableBlock(); + if(tableBlock!=null){ + for(PackageBlock pkg:tableBlock.listPackages()){ + EntryGroup entryGroup=pkg.getEntryGroup(resourceId); + if(entryGroup!=null){ + return entryGroup; + } + } + } + } + if(store!=null){ + return store.getEntryGroup(resourceId); + } + return null; + } + private static String decodeIntEntryString(Entry entry, int data){ + if(entry ==null){ + return null; + } + PackageBlock packageBlock= entry.getPackageBlock(); + if(packageBlock==null){ + return null; + } + TableBlock tableBlock=packageBlock.getTableBlock(); + if(tableBlock==null){ + return null; + } + TableStringPool pool = tableBlock.getTableStringPool(); + TableString tableString=pool.get(data); + if(tableString==null){ + return null; + } + return escapeSpecialCharacter(tableString.getHtml()); + } + private static String decodeString(EntryStore entryStore, int packageOrResourceId, int stringRef){ + if(entryStore==null||packageOrResourceId==0){ + return null; + } + int pkgId=(packageOrResourceId>>24)&0xFF; + if(pkgId==0){ + pkgId=packageOrResourceId; + } + Collection allPkg = entryStore.getPackageBlocks((byte) pkgId); + if(allPkg==null){ + return null; + } + TableString tableString=null; + for(PackageBlock packageBlock:allPkg){ + TableBlock tableBlock=packageBlock.getTableBlock(); + if(tableBlock==null){ + continue; + } + TableString ts=tableBlock.getTableStringPool().get(stringRef); + if(ts==null){ + continue; + } + if(tableString==null){ + tableString=ts; + }else { + // Duplicate result, could be from split apks + return null; + } + } + if(tableString!=null){ + return escapeSpecialCharacter(tableString.getHtml()); + } + return null; + } + private static String decodeIntEntryString(PackageBlock packageBlock, int data){ + if(packageBlock==null){ + return null; + } + TableBlock tableBlock=packageBlock.getTableBlock(); + if(tableBlock==null){ + return null; + } + TableStringPool pool = tableBlock.getTableStringPool(); + TableString tableString=pool.get(data); + if(tableString==null){ + return null; + } + return escapeSpecialCharacter(tableString.getHtml()); + } + + private static String decodeHex(int rawVal){ + return HexUtil.toHex(rawVal, 1); + } + private static String decodeInt(int rawVal){ + return String.valueOf(rawVal); + } + private static String decodeNull(int data){ + if(data == 1){ + return "@empty"; + } + return "@null"; + } + + private static String decodeBoolean(int data){ + if(data == 0xFFFFFFFF){ + return "true"; + } + return "false"; + } + + private static String decodeDimensionOrFloat(ValueType valueType, int rawVal){ + if(valueType==ValueType.FLOAT){ + float f=Float.intBitsToFloat(rawVal); + return Float.toString(f); + } + return ComplexUtil.decodeComplex(valueType == ValueType.FRACTION, rawVal); + } + private static AttributeBag getAttributeBag(EntryStore store, int resourceId){ + ResTableMapEntry mapEntry=getAttributeValueBag(store, resourceId); + if(mapEntry==null){ + return null; + } + return AttributeBag.create(mapEntry.getValue()); + } + private static ResTableMapEntry getAttributeValueBag(EntryStore store, int resourceId){ + if(store==null){ + return null; + } + Collection foundGroups = store.getEntryGroups(resourceId); + ResTableMapEntry best=null; + for(EntryGroup group:foundGroups){ + ResTableMapEntry valueBag = getAttributeValueBag(group); + best=chooseBest(best, valueBag); + } + return best; + } + private static ResTableMapEntry getAttributeValueBag(EntryGroup entryGroup){ + if(entryGroup==null){ + return null; + } + ResTableMapEntry best=null; + Iterator iterator=entryGroup.iterator(true); + while (iterator.hasNext()){ + Entry entry =iterator.next(); + ResTableMapEntry valueBag = getAttributeValueBag(entry); + best=chooseBest(best, valueBag); + } + return best; + } + private static ResTableMapEntry getAttributeValueBag(Entry entry){ + if(entry ==null){ + return null; + } + TableEntry tableEntry = entry.getTableEntry(); + if(tableEntry instanceof ResTableMapEntry){ + return (ResTableMapEntry) tableEntry; + } + return null; + } + private static ResTableMapEntry chooseBest(ResTableMapEntry entry1, ResTableMapEntry entry2){ + if(entry1==null){ + return entry2; + } + if(entry2==null){ + return entry1; + } + if(entry2.getValue().childesCount()>entry1.getValue().childesCount()){ + return entry2; + } + return entry1; + } + private static String toHexResourceId(int resourceId){ + return HexUtil.toHex8(resourceId); + } + private static boolean isEqualString(String str1, String str2){ + if(isEmpty(str1)){ + return isEmpty(str2); + } + return str1.equals(str2); + } + private static boolean isEmpty(String str){ + if(str==null){ + return true; + } + str=str.trim(); + return str.length()==0; + } + + public static ReferenceString parseReference(String ref){ + if(ref == null || ref.length() < 2){ + return null; + } + char first = ref.charAt(0); + if(first != '@' && first != '?'){ + return null; + } + Matcher matcher = PATTERN_REFERENCE.matcher(ref); + if(!matcher.find()){ + return null; + } + String prefix = matcher.group(1); + String packageName = matcher.group(2); + if(packageName != null){ + if(packageName.endsWith(":")){ + packageName = packageName.substring(0, packageName.length()-1); + } + if(packageName.length() == 0){ + packageName = null; + } + } + String type = matcher.group(4); + String name = matcher.group(5); + return new ReferenceString(prefix, packageName, type, name); + } + public static class ReferenceString{ + public final String prefix; + public final String packageName; + public final String type; + public final String name; + public ReferenceString(String prefix, String packageName, String type, String name){ + this.prefix = prefix; + this.packageName = packageName; + this.type = type; + this.name = name; + } + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + if(prefix != null){ + builder.append(prefix); + } + if(packageName != null){ + builder.append(packageName); + builder.append(':'); + } + if(type != null){ + builder.append(type); + builder.append('/'); + } + builder.append(name); + return builder.toString(); + } + + } + public static class EncodeResult{ + public final ValueType valueType; + public final int value; + public EncodeResult(ValueType valueType, int value){ + this.valueType=valueType; + this.value=value; + } + @Override + public String toString(){ + return valueType+": "+HexUtil.toHex8(value); + } + } + + public static final Pattern PATTERN_DIMEN = Pattern.compile("^([+\\-]?[0-9]+(\\.[0-9]+(E\\+?-?[0-9]+)?)?)(px|di?p|sp|pt|in|mm|%p?)$"); + private static final Pattern PATTERN_INTEGER = Pattern.compile("^(-?)([0-9]+)$"); + private static final Pattern PATTERN_HEX = Pattern.compile("^0x[0-9a-fA-F]+$"); + public static final Pattern PATTERN_REFERENCE = Pattern.compile("^([?@])(([^\\s:@?/]+:)?)([^\\s:@?/]+)/([^\\s:@?/]+)$"); + public static final Pattern PATTERN_HEX_REFERENCE = Pattern.compile("^([?@])(0x[0-9a-f]{7,8})$"); +} diff --git a/src/ARSCLib/com/reandroid/arsc/group/EntryGroup.java b/src/ARSCLib/com/reandroid/arsc/group/EntryGroup.java new file mode 100755 index 00000000..97c1f152 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/group/EntryGroup.java @@ -0,0 +1,264 @@ +/* + * 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.group; + +import com.reandroid.arsc.base.BlockArrayCreator; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.item.SpecString; +import com.reandroid.arsc.item.TypeString; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; + +import java.util.Iterator; + +public class EntryGroup extends ItemGroup { + private final int resourceId; + public EntryGroup(int resId) { + 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 itr=iterator(skipNull); + while (itr.hasNext()){ + Entry entry =itr.next(); + if(entry.isDefault()){ + return entry; + } + } + return null; + } + public TypeString getTypeString(){ + Entry entry =pickOne(); + if(entry !=null){ + return entry.getTypeString(); + } + return null; + } + public SpecString getSpecString(){ + Entry entry =pickOne(); + if(entry !=null){ + return entry.getSpecString(); + } + return null; + } + public String getTypeName(){ + TypeString typeString=getTypeString(); + if(typeString==null){ + return null; + } + return typeString.get(); + } + public String getSpecName(){ + SpecString specString=getSpecString(); + if(specString==null){ + return null; + } + return specString.get(); + } + private SpecStringPool getSpecStringPool(){ + Entry entry =get(0); + if(entry ==null){ + return null; + } + TypeBlock typeBlock= entry.getTypeBlock(); + if(typeBlock==null){ + return null; + } + PackageBlock packageBlock=typeBlock.getPackageBlock(); + if(packageBlock==null){ + return null; + } + return packageBlock.getSpecStringPool(); + } + @Override + public int hashCode(){ + return resourceId; + } + @Override + public String toString(){ + Entry entry =pickOne(); + if(entry ==null){ + return super.toString(); + } + return super.toString()+"{"+ entry.toString()+"}"; + } + + private static final BlockArrayCreator ARRAY_CREATOR = new BlockArrayCreator(){ + @Override + public Entry newInstance() { + return new Entry(); + } + + @Override + public Entry[] newInstance(int len) { + return new Entry[len]; + } + }; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/group/ItemGroup.java b/src/ARSCLib/com/reandroid/arsc/group/ItemGroup.java new file mode 100755 index 00000000..83b3ac9f --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/group/ItemGroup.java @@ -0,0 +1,196 @@ + /* + * 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.group; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockArrayCreator; + +import java.util.AbstractList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + + public class ItemGroup { + private final BlockArrayCreator mBlockArrayCreator; + private final String name; + private T[] items; + public ItemGroup(BlockArrayCreator blockArrayCreator, String name){ + this.mBlockArrayCreator=blockArrayCreator; + this.name=name; + this.items=blockArrayCreator.newInstance(0); + } + public Iterator iterator(){ + return iterator(false); + } + public Iterator iterator(boolean skipNullBlock){ + return new GroupIterator(skipNullBlock); + } + public List listItems(){ + return new AbstractList() { + private final int mSize = ItemGroup.this.size(); + @Override + public T get(int i) { + return ItemGroup.this.get(i); + } + + @Override + public int size() { + return mSize; + } + }; + } + public T get(int i){ + if(i<0||i>= size()){ + return null; + } + return items[i]; + } + public int size(){ + if(items==null){ + return 0; + } + return items.length; + } + public boolean contains(T block){ + if(block==null){ + return false; + } + int len=items.length; + for(int i=0;i { + private int mCursor; + private final int mMaxSize; + private final boolean mSkipNullBlock; + GroupIterator(boolean skipNullBlock){ + mSkipNullBlock=skipNullBlock; + mCursor=0; + mMaxSize=ItemGroup.this.size(); + } + @Override + public boolean hasNext() { + checkCursor(); + return !isFinished(); + } + @Override + public T next() { + if(!isFinished()){ + T item=ItemGroup.this.get(mCursor); + mCursor++; + checkCursor(); + return item; + } + return null; + } + private boolean isFinished(){ + return mCursor>=mMaxSize; + } + private void checkCursor(){ + if(!mSkipNullBlock || isFinished()){ + return; + } + T item=ItemGroup.this.get(mCursor); + while (item==null||item.isNull()){ + mCursor++; + item=ItemGroup.this.get(mCursor); + if(mCursor>=mMaxSize){ + break; + } + } + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/group/StringGroup.java b/src/ARSCLib/com/reandroid/arsc/group/StringGroup.java new file mode 100755 index 00000000..73ccdbca --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/group/StringGroup.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.arsc.group; + +import com.reandroid.arsc.base.BlockArrayCreator; +import com.reandroid.arsc.item.StringItem; + +public class StringGroup extends ItemGroup{ + public StringGroup(BlockArrayCreator blockArrayCreator, String name){ + super(blockArrayCreator, name); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/HeaderBlock.java b/src/ARSCLib/com/reandroid/arsc/header/HeaderBlock.java new file mode 100755 index 00000000..fded9c44 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/HeaderBlock.java @@ -0,0 +1,237 @@ +/* + * 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.header; + +import com.reandroid.arsc.base.BlockContainer; +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.container.BlockList; +import com.reandroid.arsc.container.ExpandableBlockContainer; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.BlockItem; +import com.reandroid.arsc.item.ByteArray; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.ShortItem; +import com.reandroid.arsc.util.HexBytesWriter; +import com.reandroid.arsc.util.HexUtil; + +import java.io.*; +import java.util.List; + +public class HeaderBlock extends ExpandableBlockContainer implements BlockLoad { + private final ShortItem mType; + private final ShortItem mHeaderSize; + private final IntegerItem mChunkSize; + private HeaderLoaded mHeaderLoaded; + private final ByteArray extraBytes; + public HeaderBlock(short type){ + super(3); + this.mType=new ShortItem(type); + this.mHeaderSize=new ShortItem(); + this.mChunkSize=new IntegerItem(); + this.extraBytes=new ByteArray(); + addChild(mType); + addChild(mHeaderSize); + addChild(mChunkSize); + this.mType.setBlockLoad(this); + this.mHeaderSize.setBlockLoad(this); + this.mChunkSize.setBlockLoad(this); + } + public HeaderBlock(ChunkType chunkType){ + this(chunkType.ID); + } + public int getMinimumSize(){ + return countBytes(); + } + public ByteArray getExtraBytes() { + return extraBytes; + } + public void setHeaderLoaded(HeaderLoaded headerLoaded){ + this.mHeaderLoaded=headerLoaded; + } + public ChunkType getChunkType(){ + return ChunkType.get(mType.get()); + } + public short getType(){ + return mType.get(); + } + public void setType(ChunkType chunkType){ + short type; + if(chunkType==null){ + type=0; + }else { + type=chunkType.ID; + } + setType(type); + } + public void setType(short type){ + mType.set(type); + } + + public int getHeaderSize(){ + return mHeaderSize.unsignedInt(); + } + public void setHeaderSize(short headerSize){ + mHeaderSize.set(headerSize); + } + public int getChunkSize(){ + return mChunkSize.get(); + } + public void setChunkSize(int chunkSize){ + mChunkSize.set(chunkSize); + } + + public final void refreshHeader(){ + refreshHeaderSize(); + refreshChunkSize(); + } + private void refreshHeaderSize(){ + setHeaderSize((short)countBytes()); + } + private void refreshChunkSize(){ + Block parent=getParent(); + if(parent==null){ + return; + } + int count=parent.countBytes(); + setChunkSize(count); + } + /**Non buffering reader*/ + public int readBytes(InputStream inputStream) throws IOException{ + int result = onReadBytes(inputStream); + super.notifyBlockLoad(); + return result; + } + private int onReadBytes(InputStream inputStream) throws IOException { + int readCount = readBytes(inputStream, this); + int difference = getHeaderSize() - readCount; + initExtraBytes(this.extraBytes, difference); + if(this.extraBytes.size()>0){ + readCount += extraBytes.readBytes(inputStream); + } + return readCount; + } + private int readBytes(InputStream inputStream, Block block) throws IOException{ + int result=0; + if(block instanceof BlockItem){ + result = ((BlockItem)block).readBytes(inputStream); + }else if(block instanceof BlockList){ + List childes= + ((BlockList) block).getChildes(); + for(Block child:childes){ + result+=readBytes(inputStream, child); + } + }else if(block instanceof BlockContainer){ + Block[] childes = + ((BlockContainer) block).getChildes(); + for(Block child:childes){ + result+=readBytes(inputStream, child); + } + }else { + throw new IOException("Can not read block type: "+block.getClass()); + } + return result; + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + int start=reader.getPosition(); + super.onReadBytes(reader); + int readActual=reader.getPosition() - start; + int difference=getHeaderSize()-readActual; + initExtraBytes(this.extraBytes, difference); + if(this.extraBytes.size()>0){ + this.extraBytes.readBytes(reader); + } + } + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender==this.mType){ + onChunkTypeLoaded(mType.get()); + }else if(sender==this.mHeaderSize){ + onHeaderSizeLoaded(mHeaderSize.unsignedInt()); + }else if(sender==this.mChunkSize){ + onChunkSizeLoaded(mHeaderSize.unsignedInt(), + mChunkSize.get()); + } + } + + @Override + protected void onRefreshed() { + // Not required, the parent should call refreshHeader() + } + @Override + protected void refreshChildes(){ + // Not required + } + void initExtraBytes(ByteArray extraBytes, int difference){ + if(difference==0){ + return; + } + if(extraBytes.getParent()==null){ + addChild(extraBytes); + } + extraBytes.setSize(difference); + } + void onChunkTypeLoaded(short chunkType){ + HeaderLoaded headerLoaded = mHeaderLoaded; + if(headerLoaded!=null){ + headerLoaded.onChunkTypeLoaded(chunkType); + } + } + void onHeaderSizeLoaded(int size){ + HeaderLoaded headerLoaded = mHeaderLoaded; + if(headerLoaded!=null){ + headerLoaded.onHeaderSizeLoaded(size); + } + } + void onChunkSizeLoaded(int headerSize, int chunkSize){ + HeaderLoaded headerLoaded = mHeaderLoaded; + if(headerLoaded!=null){ + headerLoaded.onChunkSizeLoaded(headerSize, chunkSize); + } + } + /** + * Prints bytes in hex for debug/testing + * */ + public String toHex(){ + return HexBytesWriter.toHex(getBytes()); + } + @Override + public String toString(){ + short t = getType(); + ChunkType type = ChunkType.get(t); + StringBuilder builder = new StringBuilder(); + if(type!=null){ + builder.append(type.toString()); + }else { + builder.append("Unknown type="); + builder.append(HexUtil.toHex4(t)); + } + builder.append("{ValueHeader="); + builder.append(getHeaderSize()); + builder.append(", Chunk="); + builder.append(getChunkSize()); + builder.append("}"); + return builder.toString(); + } + + public interface HeaderLoaded{ + void onChunkTypeLoaded(short type); + void onHeaderSizeLoaded(int headerSize); + void onChunkSizeLoaded(int headerSize, int chunkSize); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/InfoHeader.java b/src/ARSCLib/com/reandroid/arsc/header/InfoHeader.java new file mode 100644 index 00000000..0a5efe80 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/InfoHeader.java @@ -0,0 +1,69 @@ +/* + * 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.header; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.ByteArray; +import com.reandroid.common.FileChannelInputStream; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +public class InfoHeader extends HeaderBlock{ + public InfoHeader(short type) { + super(type); + } + public InfoHeader() { + this((short) 0); + } + + @Override + public int getMinimumSize(){ + return INFO_MIN_SIZE; + } + + @Override + void initExtraBytes(ByteArray extraBytes, int difference){ + } + @Override + public int countBytes() { + return 8; + } + + + public static InfoHeader readHeaderBlock(File file) throws IOException { + return readHeaderBlock(FileChannelInputStream.read(file, INFO_MIN_SIZE)); + } + public static InfoHeader readHeaderBlock(InputStream inputStream) throws IOException { + InfoHeader infoHeader=new InfoHeader(); + infoHeader.readBytes(inputStream); + return infoHeader; + } + public static InfoHeader readHeaderBlock(BlockReader blockReader) throws IOException { + InfoHeader infoHeader=new InfoHeader(); + infoHeader.readBytes(blockReader); + return infoHeader; + } + public static InfoHeader readHeaderBlock(byte[] bytes) throws IOException { + BlockReader reader = new BlockReader(bytes); + InfoHeader infoHeader = new InfoHeader(); + infoHeader.readBytes(reader); + return infoHeader; + } + + public static final int INFO_MIN_SIZE = 8; +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/LibraryHeader.java b/src/ARSCLib/com/reandroid/arsc/header/LibraryHeader.java new file mode 100644 index 00000000..1bf03a16 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/LibraryHeader.java @@ -0,0 +1,41 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; + +public class LibraryHeader extends HeaderBlock{ + private final IntegerItem count; + public LibraryHeader() { + super(ChunkType.LIBRARY.ID); + this.count = new IntegerItem(); + + addChild(this.count); + } + + public IntegerItem getCount() { + return count; + } + @Override + public String toString(){ + if(getChunkType()!=ChunkType.LIBRARY){ + return super.toString(); + } + return getClass().getSimpleName() + +" {count="+getCount() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/OverlayableHeader.java b/src/ARSCLib/com/reandroid/arsc/header/OverlayableHeader.java new file mode 100644 index 00000000..6292ab5e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/OverlayableHeader.java @@ -0,0 +1,49 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.FixedLengthString; + +public class OverlayableHeader extends HeaderBlock{ + private final FixedLengthString name; + private final FixedLengthString actor; + public OverlayableHeader() { + super(ChunkType.OVERLAYABLE.ID); + this.name = new FixedLengthString(512); + this.actor = new FixedLengthString(512); + + addChild(this.name); + addChild(this.actor); + } + + public FixedLengthString getName() { + return name; + } + public FixedLengthString getActor() { + return actor; + } + + @Override + public String toString(){ + if(getChunkType()!=ChunkType.OVERLAYABLE){ + return super.toString(); + } + return getClass().getSimpleName() + +" {count="+getName() + +", actor=" + getActor() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/OverlayablePolicyHeader.java b/src/ARSCLib/com/reandroid/arsc/header/OverlayablePolicyHeader.java new file mode 100644 index 00000000..e6c9316b --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/OverlayablePolicyHeader.java @@ -0,0 +1,47 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; + +public class OverlayablePolicyHeader extends HeaderBlock{ + private final IntegerItem flags; + private final IntegerItem entryCount; + public OverlayablePolicyHeader() { + super(ChunkType.OVERLAYABLE_POLICY.ID); + this.flags = new IntegerItem(); + this.entryCount = new IntegerItem(); + + addChild(this.flags); + addChild(this.entryCount); + } + public IntegerItem getFlags() { + return flags; + } + public IntegerItem getEntryCount() { + return entryCount; + } + @Override + public String toString(){ + if(getChunkType()!=ChunkType.OVERLAYABLE_POLICY){ + return super.toString(); + } + return getClass().getSimpleName() + +" {flags="+getFlags().toHex() + +", entryCount=" + getEntryCount() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/PackageHeader.java b/src/ARSCLib/com/reandroid/arsc/header/PackageHeader.java new file mode 100644 index 00000000..8094e66e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/PackageHeader.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.arsc.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.container.SingleBlockContainer; +import com.reandroid.arsc.item.FixedLengthString; +import com.reandroid.arsc.item.IntegerItem; + +public class PackageHeader extends HeaderBlock{ + private final IntegerItem packageId; + private final FixedLengthString packageName; + + private final IntegerItem typeStringPoolOffset; + private final IntegerItem typeStringPoolCount; + private final IntegerItem specStringPoolOffset; + private final IntegerItem specStringPoolCount; + private final SingleBlockContainer typeIdOffsetContainer; + private final IntegerItem typeIdOffset; + + public PackageHeader() { + super(ChunkType.PACKAGE.ID); + this.packageId = new IntegerItem(); + this.packageName = new FixedLengthString(256); + + this.typeStringPoolOffset = new IntegerItem(); + this.typeStringPoolCount = new IntegerItem(); + this.specStringPoolOffset = new IntegerItem(); + this.specStringPoolCount = new IntegerItem(); + + this.typeIdOffsetContainer = new SingleBlockContainer<>(); + this.typeIdOffset = new IntegerItem(); + this.typeIdOffsetContainer.setItem(typeIdOffset); + + addChild(this.packageId); + addChild(this.packageName); + addChild(this.typeStringPoolOffset); + addChild(this.typeStringPoolCount); + addChild(this.specStringPoolOffset); + addChild(this.specStringPoolCount); + addChild(this.typeIdOffsetContainer); + } + + public IntegerItem getPackageId() { + return packageId; + } + public FixedLengthString getPackageName() { + return packageName; + } + public IntegerItem getTypeStringPoolOffset() { + return typeStringPoolOffset; + } + public IntegerItem getTypeStringPoolCount() { + return typeStringPoolCount; + } + public IntegerItem getSpecStringPoolOffset() { + return specStringPoolOffset; + } + public IntegerItem getSpecStringPoolCount() { + return specStringPoolCount; + } + public IntegerItem getTypeIdOffsetItem() { + return typeIdOffset; + } + public void setTypeIdOffset(int offset){ + typeIdOffset.set(offset); + typeIdOffsetContainer.setItem(typeIdOffset); + } + public int getTypeIdOffset() { + if(typeIdOffset.getParent()==null){ + typeIdOffset.set(0); + } + return typeIdOffset.get(); + } + @Override + void onHeaderSizeLoaded(int size){ + super.onHeaderSizeLoaded(size); + if(size<288){ + typeIdOffset.set(0); + typeIdOffsetContainer.setItem(null); + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/SpecHeader.java b/src/ARSCLib/com/reandroid/arsc/header/SpecHeader.java new file mode 100644 index 00000000..d1135491 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/SpecHeader.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.arsc.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.ByteItem; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.ShortItem; + +public class SpecHeader extends HeaderBlock{ + private final ByteItem id; + private final IntegerItem entryCount; + public SpecHeader() { + super(ChunkType.SPEC.ID); + this.id = new ByteItem(); + ByteItem res0 = new ByteItem(); + ShortItem res1 = new ShortItem(); + this.entryCount = new IntegerItem(); + addChild(id); + addChild(res0); + addChild(res1); + addChild(entryCount); + } + public ByteItem getId() { + return id; + } + public IntegerItem getEntryCount() { + return entryCount; + } + @Override + public String toString(){ + if(getChunkType() != ChunkType.SPEC){ + return super.toString(); + } + return getClass().getSimpleName() + +" {id="+getId().toHex() + +", entryCount=" + getEntryCount() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/StagedAliasHeader.java b/src/ARSCLib/com/reandroid/arsc/header/StagedAliasHeader.java new file mode 100644 index 00000000..38996da6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/StagedAliasHeader.java @@ -0,0 +1,41 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; + +public class StagedAliasHeader extends HeaderBlock{ + private final IntegerItem count; + public StagedAliasHeader() { + super(ChunkType.STAGED_ALIAS.ID); + this.count = new IntegerItem(); + + addChild(count); + } + + public IntegerItem getCount() { + return count; + } + @Override + public String toString(){ + if(getChunkType()!=ChunkType.STAGED_ALIAS){ + return super.toString(); + } + return getClass().getSimpleName() + +" {count="+getCount()+ '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/StringPoolHeader.java b/src/ARSCLib/com/reandroid/arsc/header/StringPoolHeader.java new file mode 100644 index 00000000..09c41f60 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/StringPoolHeader.java @@ -0,0 +1,99 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.ByteItem; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.ShortItem; + + +public class StringPoolHeader extends HeaderBlock{ + private final IntegerItem countStrings; + private final IntegerItem countStyles; + private final ByteItem flagSorted; + private final ByteItem flagUtf8; + private final ShortItem flagExtra; + private final IntegerItem startStrings; + private final IntegerItem startStyles; + public StringPoolHeader() { + super(ChunkType.STRING.ID); + this.countStrings = new IntegerItem(); + this.countStyles = new IntegerItem(); + this.flagSorted = new ByteItem(); + this.flagUtf8 = new ByteItem(); + this.flagExtra = new ShortItem(); + this.startStrings = new IntegerItem(); + this.startStyles = new IntegerItem(); + + addChild(countStrings); + addChild(countStyles); + addChild(flagSorted); + addChild(flagUtf8); + addChild(flagExtra); + addChild(startStrings); + addChild(startStyles); + } + public IntegerItem getCountStrings() { + return countStrings; + } + public IntegerItem getCountStyles() { + return countStyles; + } + public ByteItem getFlagUtf8() { + return flagUtf8; + } + public ByteItem getFlagSorted() { + return flagSorted; + } + public ShortItem getFlagExtra(){ + return flagExtra; + } + public IntegerItem getStartStrings() { + return startStrings; + } + public IntegerItem getStartStyles() { + return startStyles; + } + + public boolean isUtf8(){ + return (getFlagUtf8().get() & 0x01) !=0; + } + public void setUtf8(boolean utf8){ + getFlagUtf8().set((byte) (utf8 ? 0x01 : 0x00)); + } + public boolean isSorted(){ + return (getFlagSorted().get() & 0x01) !=0; + } + public void setSorted(boolean sorted){ + getFlagSorted().set((byte) (sorted ? 0x01 : 0x00)); + } + + @Override + public String toString(){ + if(getChunkType()!=ChunkType.STRING){ + return super.toString(); + } + return getClass().getSimpleName() + +" {strings="+getCountStrings() + +", styles="+getCountStyles() + +", utf8="+isUtf8() + +", sorted="+isSorted() + +", flagExtra="+getFlagExtra().toHex() + +", offset-strings="+getStartStrings().get() + +", offset-styles="+getStartStyles().get() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/TableHeader.java b/src/ARSCLib/com/reandroid/arsc/header/TableHeader.java new file mode 100644 index 00000000..3a8aed78 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/TableHeader.java @@ -0,0 +1,39 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; + +public class TableHeader extends HeaderBlock{ + private final IntegerItem packageCount; + public TableHeader() { + super(ChunkType.TABLE.ID); + this.packageCount = new IntegerItem(); + addChild(packageCount); + } + public IntegerItem getPackageCount() { + return packageCount; + } + @Override + public String toString(){ + if(getChunkType()!=ChunkType.TABLE){ + return super.toString(); + } + return getClass().getSimpleName() + +" {packageCount=" + getPackageCount() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/TypeHeader.java b/src/ARSCLib/com/reandroid/arsc/header/TypeHeader.java new file mode 100644 index 00000000..ed1f9d91 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/TypeHeader.java @@ -0,0 +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. + */ +package com.reandroid.arsc.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.ByteItem; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.ShortItem; +import com.reandroid.arsc.value.ResConfig; + +public class TypeHeader extends HeaderBlock{ + private final ByteItem id; + private final ByteItem flags; + private final IntegerItem count; + private final IntegerItem entriesStart; + private final ResConfig config; + public TypeHeader(boolean sparse) { + super(ChunkType.TYPE.ID); + this.id = new ByteItem(); + this.flags = new ByteItem(); + ShortItem reserved = new ShortItem(); + this.count = new IntegerItem(); + this.entriesStart = new IntegerItem(); + this.config = new ResConfig(); + + addChild(id); + addChild(flags); + addChild(reserved); + addChild(count); + addChild(entriesStart); + addChild(config); + setSparse(sparse); + } + public boolean isSparse(){ + return (getFlags().get() & FLAG_SPARSE) == FLAG_SPARSE; + } + public void setSparse(boolean sparse){ + byte flag = getFlags().get(); + if(sparse){ + flag = (byte) (flag | FLAG_SPARSE); + }else { + flag = (byte) (flag & (~FLAG_SPARSE & 0xff)); + } + getFlags().set(flag); + } + + @Override + public int getMinimumSize(){ + return TYPE_MIN_SIZE; + } + public ByteItem getId() { + return id; + } + public ByteItem getFlags() { + return flags; + } + public IntegerItem getCount() { + return count; + } + public IntegerItem getEntriesStart() { + return entriesStart; + } + public ResConfig getConfig() { + return config; + } + + @Override + public String toString(){ + if(getChunkType()!=ChunkType.TYPE){ + return super.toString(); + } + return getClass().getSimpleName() + +" {id="+getId().toHex() + +", flags=" + getFlags().toHex() + +", count=" + getCount() + +", entriesStart=" + getEntriesStart() + +", config=" + getConfig() + '}'; + } + + private static final byte FLAG_SPARSE = 0x1; + + //typeHeader.countBytes() - getConfig().countBytes() + ResConfig.SIZE_16 + private static final int TYPE_MIN_SIZE = 36; +} diff --git a/src/ARSCLib/com/reandroid/arsc/header/XmlNodeHeader.java b/src/ARSCLib/com/reandroid/arsc/header/XmlNodeHeader.java new file mode 100644 index 00000000..1feba224 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/header/XmlNodeHeader.java @@ -0,0 +1,49 @@ + /* + * 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.header; + +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.item.IntegerItem; + +public class XmlNodeHeader extends HeaderBlock{ + private final IntegerItem lineNumber; + private final IntegerItem commentReference; + private final ChunkType chunkType; + public XmlNodeHeader(ChunkType chunkType) { + super(chunkType.ID); + this.chunkType = chunkType; + this.lineNumber = new IntegerItem(); + this.commentReference = new IntegerItem(-1); + + addChild(lineNumber); + addChild(commentReference); + } + public IntegerItem getLineNumber() { + return lineNumber; + } + public IntegerItem getCommentReference() { + return commentReference; + } + @Override + public String toString(){ + if(getChunkType() != chunkType){ + return super.toString(); + } + return getClass().getSimpleName() + +" {lineNumber="+getLineNumber() + +", commentReference=" + getCommentReference() + '}'; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/io/BlockLoad.java b/src/ARSCLib/com/reandroid/arsc/io/BlockLoad.java new file mode 100755 index 00000000..7728ae91 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/io/BlockLoad.java @@ -0,0 +1,24 @@ + /* + * 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.io; + +import com.reandroid.arsc.base.Block; + +import java.io.IOException; + +public interface BlockLoad { + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException; +} diff --git a/src/ARSCLib/com/reandroid/arsc/io/BlockReader.java b/src/ARSCLib/com/reandroid/arsc/io/BlockReader.java new file mode 100755 index 00000000..d0fe59bc --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/io/BlockReader.java @@ -0,0 +1,342 @@ +/* + * 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.io; + +import com.reandroid.arsc.header.InfoHeader; +import com.reandroid.arsc.header.SpecHeader; +import com.reandroid.arsc.header.TypeHeader; + +import java.io.*; + + +public class BlockReader extends InputStream { + private final Object mLock=new Object(); + private byte[] BUFFER; + private final int mStart; + private final int mLength; + private int mPosition; + private boolean mIsClosed; + private int mMark; + public BlockReader(byte[] buffer, int start, int length) { + this.BUFFER=buffer; + this.mStart=start; + this.mLength=length; + this.mPosition =0; + } + public BlockReader(byte[] buffer) { + this(buffer, 0, buffer.length); + } + public BlockReader(InputStream in) throws IOException { + this(loadBuffer(in)); + } + public BlockReader(InputStream in, int length) throws IOException { + this(loadBuffer(in, length)); + } + public BlockReader(File file) throws IOException { + this(loadBuffer(file)); + } + public int readUnsignedShort() throws IOException { + return 0x0000ffff & readShort(); + } + public short readShort() throws IOException { + int pos = getPosition(); + byte[] bts = new byte[2]; + readFully(bts); + seek(pos); + return toShort(bts, 0); + } + public SpecHeader readSpecHeader() throws IOException{ + SpecHeader specHeader = new SpecHeader(); + if(available() < specHeader.countBytes()){ + return null; + } + int pos = getPosition(); + specHeader.readBytes(this); + seek(pos); + return specHeader; + } + public TypeHeader readTypeHeader() throws IOException{ + TypeHeader typeHeader = new TypeHeader(false); + if(available() < typeHeader.getMinimumSize()){ + return null; + } + int pos = getPosition(); + typeHeader.readBytes(this); + seek(pos); + return typeHeader; + } + public InfoHeader readHeaderBlock() throws IOException { + InfoHeader infoHeader = new InfoHeader(); + if(available() < infoHeader.getMinimumSize()){ + return null; + } + int pos = getPosition(); + infoHeader.readBytes(this); + seek(pos); + return infoHeader; + } + public int searchNextIntPosition(int bytesOffset, int value){ + if(mIsClosed || mPosition>=mLength){ + return -1; + } + synchronized (mLock){ + int actPos=mStart+mPosition+bytesOffset; + int max=available()/4; + for(int i=0;i this.mLength){ + len = this.mLength - start; + } + start = start + this.mStart; + return new BlockReader(BUFFER, start, len); + } + public boolean isAvailable(){ + if(mIsClosed){ + return false; + } + return available()>0; + } + public void offset(int off){ + int pos=getPosition()+off; + seek(pos); + } + public void seek(int relPos){ + if(relPos<0){ + relPos=0; + }else if(relPos>length()){ + relPos=length(); + } + setPosition(relPos); + } + private void setPosition(int pos){ + if(pos==mPosition){ + return; + } + synchronized (mLock){ + mPosition=pos; + } + } + public int length(){ + return mLength; + } + public byte[] readBytes(int len) throws IOException { + byte[] result=new byte[len]; + if(len==0){ + return result; + } + int len2=read(result); + if(len2<0){ + throw new EOFException("Finished reading: "+ mPosition); + } + if(len==len2){ + return result; + } + byte[] result2=new byte[len2]; + System.arraycopy(result, 0, result2, 0, len2); + return result2; + } + public int readFully(byte[] bts) throws IOException{ + return readFully(bts, 0, bts.length); + } + public int readFully(byte[] bts, int length) throws IOException{ + if(length==0){ + return 0; + } + return readFully(bts, 0, length); + } + + public int readFully(byte[] bts, int start, int length) throws IOException { + if(length==0){ + return 0; + } + if(mIsClosed){ + throw new IOException("Stream is closed"); + } + if(mPosition>=mLength){ + throw new EOFException("Finished reading: "+mPosition); + } + int len=bts.length; + if(length=mLength){ + i++; + break; + } + } + return i; + } + } + public int getPosition(){ + return mPosition; + } + public int getActualPosition(){ + return mStart + mPosition; + } + public int getStartPosition(){ + return mStart; + } + @Override + public int read() throws IOException { + if(mIsClosed){ + throw new IOException("Stream is closed"); + } + int i=mPosition; + if(i>=mLength){ + throw new EOFException("Finished reading: "+i); + } + synchronized (mLock){ + int actPos=mStart+i; + int val=BUFFER[actPos] & 0xff; + mPosition++; + return val; + } + } + @Override + public void mark(int pos){ + mMark=pos; + } + @Override + public int available(){ + return mLength-mPosition; + } + @Override + public void reset() throws IOException{ + if(mIsClosed){ + throw new IOException("Can not reset stream is closed"); + } + mPosition=mMark; + } + @Override + public void close(){ + mIsClosed=true; + BUFFER=null; + mMark=0; + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append(": "); + if(mIsClosed){ + builder.append("Closed"); + }else{ + int av=available(); + if(av==0){ + builder.append("Finished: "); + builder.append(getPosition()); + }else { + if(mStart>0){ + builder.append("START="); + builder.append(mStart); + builder.append(", ACTUAL="); + builder.append(getActualPosition()); + builder.append(", "); + } + builder.append("POS="); + builder.append(getPosition()); + builder.append(", available="); + builder.append(av); + } + } + return builder.toString(); + } + + + private static byte[] loadBuffer(File file) throws IOException { + FileInputStream in=new FileInputStream(file); + byte[] result = loadBuffer(in); + in.close(); + return result; + } + private static byte[] loadBuffer(InputStream in) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buff=new byte[40960]; + int len; + while((len=in.read(buff))>0){ + outputStream.write(buff, 0, len); + } + if(in instanceof FileInputStream){ + in.close(); + } + outputStream.close(); + return outputStream.toByteArray(); + } + private static byte[] loadBuffer(InputStream in, int length) throws IOException { + byte[] buff=new byte[length]; + if(length==0){ + return buff; + } + int readLength = in.read(buff, 0, length); + if(readLength < length){ + throw new IOException("Read length is less than expected: length=" + +length+", read="+readLength); + } + return buff; + } + public static InfoHeader readHeaderBlock(File file) throws IOException{ + return InfoHeader.readHeaderBlock(file); + } + public static InfoHeader readHeaderBlock(InputStream inputStream) throws IOException{ + return InfoHeader.readHeaderBlock(inputStream); + } + public static InfoHeader readHeaderBlock(byte[] bytes) throws IOException{ + return InfoHeader.readHeaderBlock(bytes); + } + + private static final int MAX_FILE_SIZE = 1024 * 1000 * 40; +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/BlockItem.java b/src/ARSCLib/com/reandroid/arsc/item/BlockItem.java new file mode 100755 index 00000000..c34c07c6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/BlockItem.java @@ -0,0 +1,186 @@ + /* + * 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.item; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.io.BlockReader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public abstract class BlockItem extends Block { + + private byte[] mBytes; + public BlockItem(int bytesLength){ + super(); + mBytes=new byte[bytesLength]; + } + protected void onBytesChanged(){ + } + protected byte[] getBytesInternal() { + return mBytes; + } + void setBytesInternal(byte[] bts){ + if(bts==null){ + bts=new byte[0]; + } + if(bts==mBytes){ + return; + } + mBytes=bts; + onBytesChanged(); + } + final void setBytesLength(int length){ + setBytesLength(length, true); + } + protected final void setBytesLength(int length, boolean notify){ + if(length<0){ + length=0; + } + int old=mBytes.length; + if(length==old){ + return; + } + byte[] bts=new byte[length]; + if(length0 && read>0){ + read = inputStream.read(bts, offset, length); + length-=read; + offset+=read; + } + onBytesChanged(); + super.notifyBlockLoad(); + return bts.length; + } + + protected static int getInteger(byte[] bts, int offset){ + if((offset+4)>bts.length){ + return 0; + } + return bts[offset] & 0xff | + (bts[offset+1] & 0xff) << 8 | + (bts[offset+2] & 0xff) << 16 | + (bts[offset+3] & 0xff) << 24; + } + protected static short getShort(byte[] bts, int offset){ + return (short) (bts[offset] & 0xff | (bts[offset+1] & 0xff) << 8); + } + protected static void putInteger(byte[] bts, int offset, int val){ + if((offset+4)>bts.length){ + return; + } + bts[offset+3]= (byte) (val >>> 24 & 0xff); + bts[offset+2]= (byte) (val >>> 16 & 0xff); + bts[offset+1]= (byte) (val >>> 8 & 0xff); + bts[offset]= (byte) (val & 0xff); + } + protected static void putShort(byte[] bts, int offset, short val){ + bts[offset+1]= (byte) (val >>> 8 & 0xff); + bts[offset]= (byte) (val & 0xff); + } + protected static boolean getBit(byte[] bts, int byteOffset, int bitIndex){ + return (((bts[byteOffset] & 0xff) >>bitIndex) & 0x1) == 1; + } + protected static void putBit(byte[] bytes, int byteOffset, int bitIndex, boolean bit){ + int mask = 1 << bitIndex; + int add = bit ? mask : 0; + mask = (~mask) & 0xff; + int value = (bytes[byteOffset] & mask) | add; + bytes[byteOffset] = (byte) value; + } + protected static long getLong(byte[] bytes, int offset){ + if((offset + 8)>bytes.length){ + return 0; + } + long result = 0; + int index = offset + 7; + while (index>=offset){ + result = result << 8; + result |= (bytes[index] & 0xff); + index --; + } + return result; + } + protected static void putLong(byte[] bytes, int offset, long value){ + if((offset + 8) > bytes.length){ + return; + } + int index = offset; + offset = index + 8; + while (index>> 8; + index++; + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ByteArray.java b/src/ARSCLib/com/reandroid/arsc/item/ByteArray.java new file mode 100755 index 00000000..8b777a12 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ByteArray.java @@ -0,0 +1,235 @@ + /* + * 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.item; + +import java.util.AbstractList; +import java.util.List; + +public class ByteArray extends BlockItem { + public ByteArray(int bytesLength) { + super(bytesLength); + } + public ByteArray() { + this(0); + } + public final void clear(){ + setSize(0); + } + public final void add(byte[] values){ + if(values==null || values.length==0){ + return; + } + int old=size(); + int len=values.length; + setBytesLength(old+len, false); + byte[] bts = getBytesInternal(); + System.arraycopy(values, 0, bts, old, len); + } + public final void set(byte[] values){ + super.setBytesInternal(values); + } + public final byte[] toArray(){ + return getBytes(); + } + public final void fill(byte value){ + byte[] bts=getBytesInternal(); + int max=bts.length; + for(int i=0;i=s){ + return; + } + setSize(s); + } + public final void setSize(int s){ + if(s<0){ + s=0; + } + setBytesLength(s); + } + public final int size(){ + return getBytesLength(); + } + public Byte get(int index){ + if(index<0 || index>=size()){ + return null; + } + return getBytesInternal()[index]; + } + public int getByteUnsigned(int index){ + Byte b = get(index); + if(b==null){ + return 0; + } + return 0xff & b; + } + public final void putByte(int index, int byteValue){ + put(index, (byte) byteValue); + } + public final void put(int index, byte value){ + byte[] bts = getBytesInternal(); + bts[index]=value; + } + public boolean getBit(int byteOffset, int bitIndex){ + return getBit(getBytesInternal(), byteOffset, bitIndex); + } + public void putBit(int byteOffset, int bitIndex, boolean bit){ + putBit(getBytesInternal(), byteOffset, bitIndex, bit); + } + public final void putShort(int offset, int value){ + putShort(offset, (short) value); + } + public final void putShort(int offset, short val){ + byte[] bts = getBytesInternal(); + bts[offset+1]= (byte) (val >>> 8 & 0xff); + bts[offset]= (byte) (val & 0xff); + } + public final int getShortUnsigned(int offset){ + return 0xffff & getShort(offset); + } + public final short getShort(int offset){ + byte[] bts = getBytesInternal(); + return (short) (bts[offset] & 0xff | (bts[offset+1] & 0xff) << 8); + } + public final void putInteger(int offset, int val){ + byte[] bts = getBytesInternal(); + if((offset+4)>bts.length){ + return; + } + bts[offset+3]= (byte) (val >>> 24 & 0xff); + bts[offset+2]= (byte) (val >>> 16 & 0xff); + bts[offset+1]= (byte) (val >>> 8 & 0xff); + bts[offset]= (byte) (val & 0xff); + } + public final int getInteger(int offset){ + byte[] bts = getBytesInternal(); + if((offset+4)>bts.length){ + return 0; + } + return bts[offset] & 0xff | + (bts[offset+1] & 0xff) << 8 | + (bts[offset+2] & 0xff) << 16 | + (bts[offset+3] & 0xff) << 24; + } + public final void putByteArray(int offset, byte[] val){ + byte[] bts = getBytesInternal(); + int avail = bts.length-offset; + if(avail<=0){ + return; + } + int len = val.length; + if(len>avail){ + len=avail; + } + System.arraycopy(val, 0, bts, offset, len); + } + public final byte[] getByteArray(int offset, int length){ + byte[] bts = getBytesInternal(); + byte[] result = new byte[length]; + if (result.length >= 0) { + System.arraycopy(bts, offset, result, 0, result.length); + } + return result; + } + + public final List toByteList(){ + return new AbstractList() { + @Override + public Byte get(int i) { + return ByteArray.this.get(i); + } + @Override + public int size() { + return ByteArray.this.size(); + } + }; + } + public final List toShortList(){ + return new AbstractList() { + @Override + public Short get(int i) { + return ByteArray.this.getShort(i); + } + @Override + public int size() { + return ByteArray.this.size()/2; + } + }; + } + public final List toIntegerList(){ + return new AbstractList() { + @Override + public Integer get(int i) { + return ByteArray.this.getInteger(i); + } + @Override + public int size() { + return ByteArray.this.size()/4; + } + }; + } + @Override + public String toString(){ + return "size="+size(); + } + + public static byte[] trimTrailZeros(byte[] bts){ + if(bts==null){ + return new byte[0]; + } + int len=0; + for(int i=0;i0){ + System.arraycopy(bts, 0, result, 0, result.length); + } + return result; + } + public static boolean equals(byte[] bts1, byte[] bts2){ + if(bts1==bts2){ + return true; + } + if(bts1==null || bts1.length==0){ + return bts2==null || bts2.length==0; + } + if(bts2==null || bts2.length==0){ + return false; + } + if(bts1.length!=bts2.length){ + return false; + } + for(int i=0;i>index) & 0x1) == 1; + } + public void putBit(int index, boolean bit){ + int val=get(); + int left=val>>index; + if(bit){ + left=left|0x1; + }else { + left=left & 0xFE; + } + left=left<>index) & val; + val=left|right; + set((byte) val); + } + public void set(byte b){ + getBytesInternal()[0]=b; + } + public byte get(){ + return getBytesInternal()[0]; + } + public int unsignedInt(){ + return 0xff & get(); + } + public String toHex(){ + return HexUtil.toHex2(get()); + } + @Override + public String toString(){ + return String.valueOf(get()); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/FixedLengthString.java b/src/ARSCLib/com/reandroid/arsc/item/FixedLengthString.java new file mode 100644 index 00000000..172197dd --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/FixedLengthString.java @@ -0,0 +1,85 @@ + /* + * 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.item; + + import com.reandroid.arsc.io.BlockReader; + + import java.nio.charset.StandardCharsets; + + public class FixedLengthString extends StringItem{ + private final int bytesLength; + public FixedLengthString(int bytesLength){ + super(true); + this.bytesLength=bytesLength; + setBytesLength(bytesLength); + } + @Override + byte[] encodeString(String str){ + if(str==null){ + return new byte[bytesLength]; + } + byte[] bts=getUtf16Bytes(str); + byte[] results=new byte[bytesLength]; + int len=bts.length; + if(len>bytesLength){ + len=bytesLength; + } + System.arraycopy(bts, 0, results, 0, len); + return results; + } + @Override + String decodeString(){ + return decodeUtf16Bytes(getBytesInternal()); + } + @Override + public StyleItem getStyle(){ + return null; + } + @Override + int calculateReadLength(BlockReader reader){ + return bytesLength; + } + private static String decodeUtf16Bytes(byte[] bts){ + if(isNullBytes(bts)){ + return null; + } + int len=getEndNullPosition(bts); + return new String(bts,0, len, StandardCharsets.UTF_16LE); + } + private static int getEndNullPosition(byte[] bts){ + int max=bts.length; + int result=0; + boolean found=false; + for(int i=1; i { + private final T blockItem; + private final int offset; + public IndirectItem(T blockItem, int offset){ + this.blockItem = blockItem; + this.offset = offset; + } + public T getBlockItem() { + return blockItem; + } + public int getOffset() { + return offset; + } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IndirectItem other = (IndirectItem) obj; + return this.getOffset() == other.getOffset() && this.getBlockItem() == other.getBlockItem(); + } + @Override + public int hashCode(){ + return Objects.hash(this.getOffset(), this.getBlockItem()); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/IntegerArray.java b/src/ARSCLib/com/reandroid/arsc/item/IntegerArray.java new file mode 100755 index 00000000..b9e1fd8b --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/IntegerArray.java @@ -0,0 +1,139 @@ + /* + * 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.item; + + +import java.util.AbstractList; +import java.util.List; + +public class IntegerArray extends BlockItem { + public IntegerArray() { + super(0); + } + public final boolean contains(int value){ + int s=size(); + for(int i=0;i toList(){ + return new AbstractList() { + @Override + public Integer get(int i) { + return IntegerArray.this.get(i); + } + @Override + public int size() { + return IntegerArray.this.size(); + } + }; + } + public final int[] toArray(){ + int s=size(); + int[] result=new int[s]; + for(int i=0;i=s){ + return; + } + setSize(s); + } + public final void setSize(int s){ + if(s<0){ + s=0; + } + int len=s*4; + setBytesLength(len); + } + public Integer get(int index){ + if(index<0 || index>=size()){ + return null; + } + int i=index*4; + byte[] bts = getBytesInternal(); + return bts[i] & 0xff | + (bts[i+1] & 0xff) << 8 | + (bts[i+2] & 0xff) << 16 | + (bts[i+3] & 0xff) << 24; + } + public int getAt(int index){ + int i=index*4; + byte[] bts = getBytesInternal(); + return bts[i] & 0xff | + (bts[i+1] & 0xff) << 8 | + (bts[i+2] & 0xff) << 16 | + (bts[i+3] & 0xff) << 24; + } + public final int size(){ + return getBytesLength()/4; + } + public final void put(int index, int value){ + int i=index*4; + byte[] bts = getBytesInternal(); + bts[i+3]= (byte) (value >>> 24 & 0xff); + bts[i+2]= (byte) (value >>> 16 & 0xff); + bts[i+1]= (byte) (value >>> 8 & 0xff); + bts[i]= (byte) (value & 0xff); + } + @Override + public String toString(){ + return "size="+size(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/IntegerItem.java b/src/ARSCLib/com/reandroid/arsc/item/IntegerItem.java new file mode 100755 index 00000000..cb3c45a9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/IntegerItem.java @@ -0,0 +1,82 @@ +/* + * 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.item; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.util.HexUtil; + +import java.io.IOException; +import java.io.InputStream; + +public class IntegerItem extends BlockItem implements ReferenceItem{ + private int mCache; + public IntegerItem(){ + super(4); + } + public IntegerItem(int val){ + this(); + set(val); + } + @Override + public void set(int val){ + if(val==mCache){ + return; + } + mCache=val; + byte[] bts = getBytesInternal(); + bts[3]= (byte) (val >>> 24 & 0xff); + bts[2]= (byte) (val >>> 16 & 0xff); + bts[1]= (byte) (val >>> 8 & 0xff); + bts[0]= (byte) (val & 0xff); + } + @Override + public int get(){ + return mCache; + } + public long unsignedLong(){ + return get() & 0x00000000ffffffffL; + } + public String toHex(){ + return HexUtil.toHex8(get()); + } + @Override + protected void onBytesChanged() { + // To save cpu usage, better to calculate once only when bytes changed + mCache=readIntBytes(); + } + private int readIntBytes(){ + byte[] bts = getBytesInternal(); + return bts[0] & 0xff | + (bts[1] & 0xff) << 8 | + (bts[2] & 0xff) << 16 | + (bts[3] & 0xff) << 24; + } + @Override + public String toString(){ + return String.valueOf(get()); + } + + public static int readInteger(BlockReader reader) throws IOException { + IntegerItem integerItem = new IntegerItem(); + integerItem.readBytes(reader); + return integerItem.get(); + } + public static int readInteger(InputStream inputStream) throws IOException { + IntegerItem integerItem = new IntegerItem(); + integerItem.readBytes(inputStream); + return integerItem.get(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/LongItem.java b/src/ARSCLib/com/reandroid/arsc/item/LongItem.java new file mode 100644 index 00000000..7ca372b1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/LongItem.java @@ -0,0 +1,47 @@ +/* + * 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.item; + +import com.reandroid.arsc.util.HexUtil; + +public class LongItem extends BlockItem{ + private long mCache; + public LongItem() { + super(8); + } + public void set(long value){ + if(value == mCache){ + return; + } + mCache = value; + putLong(getBytesInternal(), 0, value); + } + public long get(){ + return mCache; + } + public String toHex(){ + return HexUtil.toHex(get(), 16); + } + + @Override + protected void onBytesChanged() { + mCache = getLong(getBytesInternal(), 0); + } + @Override + public String toString(){ + return String.valueOf(get()); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ReferenceBlock.java b/src/ARSCLib/com/reandroid/arsc/item/ReferenceBlock.java new file mode 100644 index 00000000..9a1302f2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ReferenceBlock.java @@ -0,0 +1,42 @@ + /* + * 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.item; + +import com.reandroid.arsc.base.Block; + +public class ReferenceBlock implements ReferenceItem{ + private final T block; + private final int offset; + public ReferenceBlock(T block, int offset){ + this.block = block; + this.offset = offset; + } + public T getBlock(){ + return this.block; + } + @Override + public void set(int val) { + BlockItem.putInteger(this.block.getBytes(), this.offset, val); + } + @Override + public int get() { + return BlockItem.getInteger(this.block.getBytes(), this.offset); + } + @Override + public String toString(){ + return get()+":"+this.block; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ReferenceItem.java b/src/ARSCLib/com/reandroid/arsc/item/ReferenceItem.java new file mode 100755 index 00000000..f3b40510 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ReferenceItem.java @@ -0,0 +1,21 @@ + /* + * 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.item; + +public interface ReferenceItem { + void set(int val); + int get(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ResXmlID.java b/src/ARSCLib/com/reandroid/arsc/item/ResXmlID.java new file mode 100755 index 00000000..3535ec0d --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ResXmlID.java @@ -0,0 +1,98 @@ +/* + * 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.item; + +import com.reandroid.arsc.chunk.xml.ResXmlDocument; +import com.reandroid.arsc.pool.ResXmlStringPool; +import com.reandroid.arsc.util.HexUtil; + +import java.util.ArrayList; +import java.util.List; + +public class ResXmlID extends IntegerItem { + private final List mReferencedList; + public ResXmlID(int resId){ + super(resId); + this.mReferencedList=new ArrayList<>(); + } + public ResXmlID(){ + this(0); + } + public boolean removeReference(ReferenceItem ref){ + return mReferencedList.remove(ref); + } + public List getReferencedList(){ + return mReferencedList; + } + public void addReference(ReferenceItem ref){ + if(ref!=null){ + mReferencedList.add(ref); + } + } + public boolean hasReference(){ + return mReferencedList.size()>0; + } + public int getReferenceCount(){ + return mReferencedList.size(); + } + private void reUpdateReferences(int newIndex){ + for(ReferenceItem ref:mReferencedList){ + ref.set(newIndex); + } + } + @Override + public void onIndexChanged(int oldIndex, int newIndex){ + //TODO: We have to ignore this to avoid conflict with ResXmlIDMap.removeSafely + } + public String getName(){ + ResXmlString xmlString = getResXmlString(); + if(xmlString==null){ + return null; + } + return xmlString.getHtml(); + } + public ResXmlString getResXmlString(){ + ResXmlStringPool stringPool=getXmlStringPool(); + if(stringPool==null){ + return null; + } + return stringPool.get(getIndex()); + } + private ResXmlStringPool getXmlStringPool(){ + ResXmlDocument resXmlDocument = getParentInstance(ResXmlDocument.class); + if(resXmlDocument!=null){ + return resXmlDocument.getStringPool(); + } + return null; + } + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append("USED-BY="); + builder.append(getReferenceCount()); + builder.append('{'); + String name = getName(); + if(name!=null){ + builder.append(name); + }else { + builder.append(getIndex()); + } + builder.append(':'); + builder.append(HexUtil.toHex8(get())); + builder.append('}'); + return builder.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ResXmlString.java b/src/ARSCLib/com/reandroid/arsc/item/ResXmlString.java new file mode 100755 index 00000000..3cacfb84 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ResXmlString.java @@ -0,0 +1,29 @@ +/* + * 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.item; + +public class ResXmlString extends StringItem { + public ResXmlString(boolean utf8) { + super(utf8); + } + public ResXmlString(boolean utf8, String value) { + this(utf8); + set(value); + } + @Override + void ensureStringLinkUnlocked(){ + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/ShortItem.java b/src/ARSCLib/com/reandroid/arsc/item/ShortItem.java new file mode 100755 index 00000000..15addb4f --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/ShortItem.java @@ -0,0 +1,82 @@ +/* + * 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.item; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.util.HexUtil; + +import java.io.IOException; +import java.io.InputStream; + +public class ShortItem extends BlockItem { + private short mCache; + + public ShortItem(){ + super(2); + } + public ShortItem(short val){ + this(); + set(val); + } + public void set(short val){ + if(val==mCache){ + return; + } + mCache=val; + byte[] bts = getBytesInternal(); + bts[1]= (byte) (val >>> 8 & 0xff); + bts[0]= (byte) (val & 0xff); + } + public short get(){ + return mCache; + } + public int unsignedInt(){ + return 0xffff & get(); + } + public String toHex(){ + return HexUtil.toHex4(get()); + } + @Override + protected void onBytesChanged() { + // To save cpu usage, better to calculate once only when bytes changed + mCache=readShortBytes(); + } + private short readShortBytes(){ + byte[] bts = getBytesInternal(); + return (short) (bts[0] & 0xff | (bts[1] & 0xff) << 8); + } + @Override + public String toString(){ + return String.valueOf(get()); + } + + public static short readShort(BlockReader reader) throws IOException { + ShortItem shortItem = new ShortItem(); + shortItem.readBytes(reader); + return shortItem.get(); + } + public static short readShort(InputStream inputStream) throws IOException { + ShortItem shortItem = new ShortItem(); + shortItem.readBytes(inputStream); + return shortItem.get(); + } + public static int readUnsignedShort(BlockReader reader) throws IOException { + return 0x0000ffff & readShort(reader); + } + public static int readUnsignedShort(InputStream inputStream) throws IOException { + return 0x0000ffff & readShort(inputStream); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/SpecFlag.java b/src/ARSCLib/com/reandroid/arsc/item/SpecFlag.java new file mode 100644 index 00000000..7808b059 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/SpecFlag.java @@ -0,0 +1,69 @@ +/* + * 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.item; + +import com.reandroid.arsc.chunk.SpecBlock; +import com.reandroid.arsc.util.HexUtil; + +public class SpecFlag extends IndirectItem { + public SpecFlag(SpecFlagsArray specFlagsArray, int offset) { + super(specFlagsArray, offset); + } + public byte getFlagByte(){ + return getBlockItem().getBytesInternal()[getOffset() + OFFSET_FLAG]; + } + public void setFlagByte(byte flag){ + getBlockItem().getBytesInternal()[getOffset() + OFFSET_FLAG] = flag; + } + public void addFlagByte(byte flag){ + flag = (byte) ((getFlagByte() & 0xff) | (flag & 0xff)); + setFlagByte(flag); + } + public void addFlag(SpecBlock.Flag flag){ + addFlagByte(flag.getFlag()); + } + public void setPublic(){ + addFlag(SpecBlock.Flag.SPEC_PUBLIC); + } + public boolean isPublic(){ + return SpecBlock.Flag.isPublic(getFlagByte()); + } + public int getInteger(){ + return BlockItem.getInteger(this.getBlockItem().getBytesInternal(), this.getOffset()); + } + public void setInteger(int value){ + if(value == getInteger()){ + return; + } + BlockItem.putInteger(this.getBlockItem().getBytesInternal(), this.getOffset(), value); + this.getBlockItem().onBytesChanged(); + } + @Override + public String toString(){ + byte flag = getFlagByte(); + if(flag != 0){ + return SpecBlock.Flag.toString(getFlagByte()); + } + int val = getInteger(); + if(val != 0){ + return HexUtil.toHex8(val); + } + return ""; + } + + private static final int OFFSET_FLAG = 3; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/SpecFlagsArray.java b/src/ARSCLib/com/reandroid/arsc/item/SpecFlagsArray.java new file mode 100644 index 00000000..b967a236 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/SpecFlagsArray.java @@ -0,0 +1,146 @@ + /* + * 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.item; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.chunk.SpecBlock; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.value.Entry; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.AbstractList; + +public class SpecFlagsArray extends IntegerArray implements BlockLoad, JSONConvert { + private final IntegerItem entryCount; + private AbstractList specFlagList; + public SpecFlagsArray(IntegerItem entryCount) { + super(); + this.entryCount = entryCount; + this.entryCount.setBlockLoad(this); + setBlockLoad(this); + } + public AbstractList listSpecFlags(){ + if(specFlagList==null){ + specFlagList = new AbstractList() { + @Override + public SpecFlag get(int i) { + return SpecFlagsArray.this.getFlag(i); + } + @Override + public int size() { + return SpecFlagsArray.this.size(); + } + }; + } + return specFlagList; + } + public SpecFlag getFlag(int id){ + id = id & 0xffff; + if(id >= size()){ + return null; + } + int offset = id * 4; + return new SpecFlag(this, offset); + } + public void set(int entryId, int value){ + setFlag(entryId, value); + refresh(); + } + private void setFlag(int id, int flag){ + id = 0xffff & id; + ensureArraySize(id+1); + super.put(id, flag); + } + @Override + public Integer get(int entryId){ + entryId = 0xffff & entryId; + return super.get(entryId); + } + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + if(sender==this.entryCount){ + super.setSize(entryCount.get()); + } + } + public void refresh(){ + entryCount.set(size()); + } + + public void merge(SpecFlagsArray specFlagsArray){ + if(specFlagsArray == null || specFlagsArray==this){ + return; + } + this.ensureArraySize(specFlagsArray.size()); + int[] comingValues = specFlagsArray.toArray(); + int[] existValues = this.toArray(); + for(int i=0; i { + private String mCache; + private boolean mUtf8; + private final Set mReferencedList; + public StringItem(boolean utf8) { + super(0); + this.mUtf8=utf8; + this.mReferencedList = new HashSet<>(); + } + public boolean removeReference(ReferenceItem ref){ + return mReferencedList.remove(ref); + } + public boolean removeAllReference(Collection referenceItems){ + return mReferencedList.removeAll(referenceItems); + } + public void removeAllReference(){ + mReferencedList.clear(); + } + public boolean hasReference(){ + ensureStringLinkUnlocked(); + return mReferencedList.size()>0; + } + public Collection getReferencedList(){ + ensureStringLinkUnlocked(); + return mReferencedList; + } + void ensureStringLinkUnlocked(){ + StringPool stringPool = getParentInstance(StringPool.class); + if(stringPool != null){ + stringPool.ensureStringLinkUnlockedInternal(); + } + } + public void addReference(ReferenceItem ref){ + if(ref!=null){ + mReferencedList.add(ref); + } + } + public void addReferenceIfAbsent(ReferenceItem ref){ + if(ref!=null){ + mReferencedList.add(ref); + } + } + public void addReference(Collection refList){ + if(refList == null){ + return; + } + for(ReferenceItem ref:refList){ + if(ref != null){ + this.mReferencedList.add(ref); + } + } + } + private void reUpdateReferences(int newIndex){ + List referenceItems=new ArrayList<>(mReferencedList); + for(ReferenceItem ref:referenceItems){ + ref.set(newIndex); + } + } + public void onRemoved(){ + StyleItem style = getStyle(); + if(style!=null){ + style.onRemoved(); + } + setParent(null); + } + @Override + public void onIndexChanged(int oldIndex, int newIndex){ + reUpdateReferences(newIndex); + } + public String getHtml(){ + String str=get(); + if(str==null){ + return null; + } + StyleItem styleItem=getStyle(); + if(styleItem==null){ + return str; + } + return styleItem.applyHtml(str, false); + } + public String getXml(){ + String str=get(); + if(str==null){ + return null; + } + StyleItem styleItem=getStyle(); + if(styleItem==null){ + return str; + } + return styleItem.applyHtml(str, true); + } + public String get(){ + return mCache; + } + public void set(String str){ + String old=get(); + if(str==null){ + if(old==null){ + return; + } + }else if(str.equals(old)){ + return; + } + if(str==null){ + StyleItem styleItem = getStyle(); + if(styleItem!=null){ + styleItem.onRemoved(); + } + } + byte[] bts=encodeString(str); + setBytesInternal(bts); + } + + public boolean isUtf8(){ + return mUtf8; + } + public void setUtf8(boolean utf8){ + if(utf8==mUtf8){ + return; + } + mUtf8=utf8; + onBytesChanged(); + } + @Override + protected void onBytesChanged() { + // To save cpu/memory usage, better to decode once only when bytes changed + mCache=decodeString(); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + if(reader.available()<4){ + return; + } + int len=calculateReadLength(reader); + setBytesLength(len, false); + byte[] bts=getBytesInternal(); + reader.readFully(bts); + onBytesChanged(); + } + int calculateReadLength(BlockReader reader) throws IOException { + if(reader.available()<4){ + return reader.available(); + } + byte[] bts=new byte[4]; + reader.readFully(bts); + reader.offset(-4); + int[] len; + if(isUtf8()){ + len=decodeUtf8StringByteLength(bts); + }else { + len=decodeUtf16StringByteLength(bts); + } + int add=isUtf8()?1:2; + return len[0]+len[1]+add; + } + String decodeString(){ + return decodeString(getBytesInternal(), mUtf8); + } + byte[] encodeString(String str){ + if(mUtf8){ + return encodeUtf8ToBytes(str); + }else { + return encodeUtf16ToBytes(str); + } + } + private String decodeString(byte[] allStringBytes, boolean isUtf8) { + if(isNullBytes(allStringBytes)){ + if(allStringBytes==null||allStringBytes.length==0){ + return null; + } + return ""; + } + int[] offLen; + if(isUtf8){ + offLen=decodeUtf8StringByteLength(allStringBytes); + }else { + offLen=decodeUtf16StringByteLength(allStringBytes); + } + CharsetDecoder charsetDecoder; + if(isUtf8){ + charsetDecoder=UTF8_DECODER; + }else { + charsetDecoder=UTF16LE_DECODER; + } + try { + ByteBuffer buf=ByteBuffer.wrap(allStringBytes, offLen[0], offLen[1]); + CharBuffer charBuffer=charsetDecoder.decode(buf); + return charBuffer.toString(); + } catch (CharacterCodingException ex) { + if(isUtf8){ + return tryThreeByteDecoder(allStringBytes, offLen[0], offLen[1]); + } + return new String(allStringBytes, offLen[0], offLen[1], StandardCharsets.UTF_16LE); + } + } + private String tryThreeByteDecoder(byte[] bytes, int offset, int length){ + try { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, offset, length); + CharBuffer charBuffer = DECODER_3B.decode(byteBuffer); + return charBuffer.toString(); + } catch (CharacterCodingException e) { + return new String(bytes, offset, length, StandardCharsets.UTF_8); + } + } + public boolean hasStyle(){ + StyleItem styleItem=getStyle(); + if(styleItem==null){ + return false; + } + return styleItem.getSpanInfoList().size()>0; + } + public StyleItem getStyle(){ + StringPool stringPool = getParentInstance(StringPool.class); + if(stringPool==null){ + return null; + } + int index=getIndex(); + return stringPool.getStyle(index); + } + @Override + public JSONObject toJson() { + if(isNull()){ + return null; + } + StyleItem styleItem=getStyle(); + if(styleItem == null){ + return null; + } + JSONObject jsonObject=new JSONObject(); + jsonObject.put(NAME_string, get()); + JSONObject styleJson = styleItem.toJson(); + if(styleJson == null){ + return null; + } + jsonObject.put(NAME_style, styleJson); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + String str = json.getString(NAME_string); + set(str); + throw new IllegalArgumentException("Not implemented"); + } + @Override + public String toString(){ + String str = getHtml(); + if(str == null){ + return "NULL"; + } + return "USED BY=" + mReferencedList.size() + "{" + str + "}"; + } + + private static int[] decodeUtf8StringByteLength(byte[] lengthBytes) { + int offset=0; + int val = lengthBytes[offset]; + int length; + if ((val & 0x80) != 0) { + offset += 2; + } else { + offset += 1; + } + val = lengthBytes[offset]; + offset += 1; + if ((val & 0x80) != 0) { + int low = (lengthBytes[offset] & 0xFF); + length = val & 0x7F; + length = length << 8; + length = length + low; + offset += 1; + } else { + length = val; + } + return new int[] { offset, length}; + } + private static int[] decodeUtf16StringByteLength(byte[] lengthBytes) { + int val = ((lengthBytes[1] & 0xFF) << 8 | lengthBytes[0] & 0xFF); + if ((val & 0x8000) != 0) { + int high = (lengthBytes[3] & 0xFF) << 8; + int low = (lengthBytes[2] & 0xFF); + int len_value = ((val & 0x7FFF) << 16) + (high + low); + return new int[] {4, len_value * 2}; + + } + return new int[] {2, val * 2}; + } + static boolean isNullBytes(byte[] bts){ + if(bts==null){ + return true; + } + int max=bts.length; + if(max<2){ + return true; + } + for(int i=2; i>8; + lenBytes[3]=(byte) (l2); + lenBytes[2]=(byte) (l1|0x80); + strLen=str.length(); + l2=strLen&0xff; + l1=(strLen-l2)>>8; + lenBytes[1]=(byte) (l2); + lenBytes[0]=(byte) (l1|0x80); + }else{ + lenBytes=new ShortItem((short) strLen).getBytesInternal(); + lenBytes[1]=lenBytes[0]; + lenBytes[0]=(byte)str.length(); + } + }else { + bts=new byte[0]; + } + return addBytes(lenBytes, bts, new byte[1]); + } + private static byte[] encodeUtf16ToBytes(String str){ + if(str==null){ + return null; + } + byte[] lenBytes; + byte[] bts=getUtf16Bytes(str); + int strLen=bts.length; + strLen=strLen/2; + if((strLen & 0xffff8000)!=0){ + lenBytes=new byte[4]; + int low=strLen&0xff; + int high=(strLen-low)&0xff00; + int rem=strLen-low-high; + lenBytes[3]=(byte) (high>>8); + lenBytes[2]=(byte) (low); + low=rem&0xff; + high=(rem&0xff00)>>8; + lenBytes[1]=(byte) (high|0x80); + lenBytes[0]=(byte) (low); + }else{ + lenBytes=new ShortItem((short) strLen).getBytesInternal(); + } + return addBytes(lenBytes, bts, new byte[2]); + } + static byte[] getUtf16Bytes(String str){ + return str.getBytes(StandardCharsets.UTF_16LE); + } + + private static byte[] addBytes(byte[] bts1, byte[] bts2, byte[] bts3){ + if(bts1==null && bts2==null && bts3==null){ + return null; + } + int len=0; + if(bts1!=null){ + len=bts1.length; + } + if(bts2!=null){ + len+=bts2.length; + } + if(bts3!=null){ + len+=bts3.length; + } + byte[] result=new byte[len]; + int start=0; + if(bts1!=null){ + start=bts1.length; + System.arraycopy(bts1, 0, result, 0, start); + } + if(bts2!=null){ + System.arraycopy(bts2, 0, result, start, bts2.length); + start+=bts2.length; + } + if(bts3!=null){ + System.arraycopy(bts3, 0, result, start, bts3.length); + } + return result; + } + + private static final CharsetDecoder UTF16LE_DECODER = StandardCharsets.UTF_16LE.newDecoder(); + private static final CharsetDecoder UTF8_DECODER = StandardCharsets.UTF_8.newDecoder(); + private static final CharsetDecoder DECODER_3B = ThreeByteCharsetDecoder.INSTANCE; + + public static final String NAME_string="string"; + public static final String NAME_style="style"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/StyleItem.java b/src/ARSCLib/com/reandroid/arsc/item/StyleItem.java new file mode 100755 index 00000000..dd0bbb44 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/StyleItem.java @@ -0,0 +1,363 @@ +/* + * 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.item; + +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.model.StyleSpanInfo; +import com.reandroid.arsc.model.StyledStringBuilder; +import com.reandroid.arsc.pool.StringPool; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.*; + +public class StyleItem extends IntegerArray implements JSONConvert { + private List mSpanInfoList; + private final Set mReferences; + public StyleItem() { + super(); + this.mReferences = new HashSet<>(); + } + public void onRemoved(){ + setStylePieceCount(0); + mSpanInfoList = null; + setParent(null); + } + public void onDataLoaded(){ + linkAll(); + } + private void setEndValue(int negOne){ + super.put(size()-1, negOne); + } + final Integer getEndValue(){ + return super.get(size()-1); + } + final Integer getStringRef(int index){ + int i=index * INTEGERS_COUNT + INDEX_STRING_REF; + return super.get(i); + } + final void setStringRef(int index, int val){ + setStringRef(index, val, true); + } + final void setStringRef(int index, int val, boolean link){ + if(link){ + unLink(index); + } + int i=index * INTEGERS_COUNT + INDEX_STRING_REF; + super.put(i, val); + if(link){ + link(getStringItem(val), index); + } + } + private void linkAll(){ + int count = getStylePieceCount(); + for(int i=0; i stringPool = getStringPool(); + if(stringPool==null){ + throw new IllegalArgumentException("Null string pool, must be added to parent StyleArray first"); + } + StringItem stringItem=stringPool.getOrCreate(tag); + addStylePiece(stringItem.getIndex(), firstChar, lastChar); + } + public void addStylePiece(int refString, int firstChar, int lastChar){ + int index=getStylePieceCount(); + setStylePieceCount(index+1); + setStylePiece(index, refString, firstChar, lastChar); + } + final void setStylePiece(int index, int refString, int firstChar, int lastChar){ + unLink(index); + int i=index * INTEGERS_COUNT; + super.put(i+ INDEX_STRING_REF, refString); + super.put(i+ INDEX_CHAR_FIRST, firstChar); + super.put(i+ INDEX_CHAR_LAST, lastChar); + link(getStringItem(refString), index); + } + final int[] getStylePiece(int index){ + if(index<0||index>= getStylePieceCount()){ + return null; + } + int[] result=new int[INTEGERS_COUNT]; + int i=index * INTEGERS_COUNT; + result[INDEX_STRING_REF]=super.get(i); + result[INDEX_CHAR_FIRST]=super.get(i+ INDEX_CHAR_FIRST); + result[INDEX_CHAR_LAST]=super.get(i+ INDEX_CHAR_LAST); + return result; + } + final void setStylePiece(int index, int[] three){ + if(three==null || three.length< INTEGERS_COUNT){ + return; + } + int i = index * INTEGERS_COUNT; + super.put(i + INDEX_STRING_REF, three[INDEX_STRING_REF]); + super.put(i + INDEX_CHAR_FIRST, three[INDEX_CHAR_FIRST]); + super.put(i + INDEX_CHAR_LAST, three[INDEX_CHAR_LAST]); + } + final int getStylePieceCount(){ + int sz=size()-1; + if(sz<0){ + sz=0; + } + return sz/ INTEGERS_COUNT; + } + final void setStylePieceCount(int count){ + if(count<0){ + count=0; + } + int cur = getStylePieceCount(); + if(count==cur){ + return; + } + if(count == 0){ + unlinkAll(); + } + int max=count * INTEGERS_COUNT + 1; + if(size()==0 || count==0){ + super.setSize(max); + setEndValue(END_VALUE); + return; + } + List copy=new ArrayList<>(getIntSpanInfoList()); + Integer end= getEndValue(); + if(end==null){ + end=END_VALUE; + } + super.setSize(max); + max=count; + int copyMax=copy.size(); + if(copyMax>max){ + copyMax=max; + } + for(int i=0;i getIntSpanInfoList(){ + return new AbstractList() { + @Override + public int[] get(int i) { + return StyleItem.this.getStylePiece(i); + } + @Override + public int size() { + return StyleItem.this.getStylePieceCount(); + } + }; + } + public final List getSpanInfoList(){ + if(mSpanInfoList!=null){ + return mSpanInfoList; + } + mSpanInfoList = new AbstractList() { + @Override + public StyleSpanInfo get(int i) { + int ref=getStringRef(i); + if(ref<=0){ + return null; + } + StyleSpanInfo spanInfo = new StyleSpanInfo( + getStringFromPool(ref), + getFirstChar(i), + getLastChar(i)); + if(!spanInfo.isValid()){ + return null; + } + return spanInfo; + } + @Override + public int size() { + return getStylePieceCount(); + } + }; + return mSpanInfoList; + } + private String getStringFromPool(int ref){ + StringItem stringItem = getStringItem(ref); + if(stringItem!=null){ + return stringItem.get(); + } + return null; + } + private StringItem getStringItem(int ref){ + StringPool stringPool = getStringPool(); + if(stringPool!=null){ + return stringPool.get(ref); + } + return null; + } + private StringPool getStringPool(){ + return getParentInstance(StringPool.class); + } + + public String applyHtml(String str, boolean xml){ + if(str == null){ + return null; + } + return StyledStringBuilder.build(str, getSpanInfoList(), xml); + } + @Override + public void setNull(boolean is_null){ + if(!is_null){ + return; + } + setStylePieceCount(0); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + int nextPos=reader.searchNextIntPosition(4, END_VALUE); + if(nextPos<0){ + return; + } + int len=nextPos-reader.getPosition()+4; + super.setBytesLength(len, false); + byte[] bts=getBytesInternal(); + reader.readFully(bts); + onBytesChanged(); + } + public void addSpanInfo(String tag, int first, int last){ + int index=getStylePieceCount(); + setStylePieceCount(index+1); + StringPool stringPool = getStringPool(); + if(stringPool==null){ + throw new IllegalArgumentException("Null string pool, must be added to parent StyleArray first"); + } + StringItem stringItem=stringPool.getOrCreate(tag); + setStylePiece(index, stringItem.getIndex(), first, last); + } + @Override + public JSONObject toJson() { + if(isNull()){ + return null; + } + JSONObject jsonObject=new JSONObject(); + JSONArray jsonArray=new JSONArray(); + int i=0; + for(StyleSpanInfo spanInfo:getSpanInfoList()){ + if(spanInfo==null){ + continue; + } + JSONObject jsonObjectSpan=spanInfo.toJson(); + jsonArray.put(i, jsonObjectSpan); + i++; + } + if(i==0){ + return null; + } + jsonObject.put(NAME_spans, jsonArray); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setNull(true); + if(json==null){ + return; + } + JSONArray jsonArray= json.getJSONArray(NAME_spans); + int length = jsonArray.length(); + for(int i=0;i listReferencedResValueEntries(){ + List results=new ArrayList<>(); + for(ReferenceItem ref:getReferencedList()){ + if(!(ref instanceof ReferenceBlock)){ + continue; + } + Block block = ((ReferenceBlock)ref).getBlock(); + if(block ==null){ + continue; + } + if(!(block instanceof ResValue)){ + continue; + } + ResValue resValue = (ResValue) block; + results.add(resValue.getEntry()); + } + return results; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/item/TypeString.java b/src/ARSCLib/com/reandroid/arsc/item/TypeString.java new file mode 100755 index 00000000..55ebba34 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/item/TypeString.java @@ -0,0 +1,46 @@ +/* + * 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.item; + + +import com.reandroid.arsc.pool.TypeStringPool; +import com.reandroid.arsc.util.HexUtil; + +public class TypeString extends StringItem { + public TypeString(boolean utf8) { + super(utf8); + } + public int getId(){ + TypeStringPool stringPool = getParent(TypeStringPool.class); + if(stringPool!=null){ + return stringPool.idOf(this); + } + // Should not reach here , this means it not added to string pool + return getIndex()+1; + } + @Override + public StyleItem getStyle(){ + // Type don't have style unless to obfuscate/confuse other decompilers + return null; + } + @Override + void ensureStringLinkUnlocked(){ + } + @Override + public String toString(){ + return HexUtil.toHex2((byte) getId())+':'+get(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/list/OverlayableList.java b/src/ARSCLib/com/reandroid/arsc/list/OverlayableList.java new file mode 100644 index 00000000..800311d0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/list/OverlayableList.java @@ -0,0 +1,73 @@ + /* + * 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.list; + +import com.reandroid.arsc.chunk.Overlayable; +import com.reandroid.arsc.container.BlockList; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +public class OverlayableList extends BlockList implements JSONConvert { + public OverlayableList(){ + super(); + } + public Overlayable get(String name){ + for(Overlayable overlayable:getChildes()){ + if(name.equals(overlayable.getName())){ + return overlayable; + } + } + return null; + } + @Override + public JSONArray toJson() { + if(size()==0){ + return null; + } + JSONArray jsonArray = new JSONArray(); + for(Overlayable overlayable:getChildes()){ + JSONObject jsonOverlayble = overlayable.toJson(); + jsonArray.put(jsonOverlayble); + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + if(json==null){ + return; + } + int length = json.length(); + for(int i=0;i { + public StagedAliasList(){ + super(); + } + private StagedAlias pickOne(){ + for(StagedAlias stagedAlias:getChildes()){ + if(stagedAlias!=null){ + return stagedAlias; + } + } + return null; + } + public void merge(StagedAliasList stagedAliasList){ + if(stagedAliasList==null || stagedAliasList==this || stagedAliasList.size()==0){ + return; + } + StagedAlias exist = pickOne(); + if(exist==null){ + exist=new StagedAlias(); + add(exist); + } + for(StagedAlias stagedAlias:stagedAliasList.getChildes()){ + exist.merge(stagedAlias); + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/model/StyleSpanInfo.java b/src/ARSCLib/com/reandroid/arsc/model/StyleSpanInfo.java new file mode 100755 index 00000000..89b61e46 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/model/StyleSpanInfo.java @@ -0,0 +1,124 @@ +/* + * 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.model; + +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +public class StyleSpanInfo implements JSONConvert { + private String mTag; + private int mFirst; + private int mLast; + public StyleSpanInfo(String tag, int first, int last){ + this.mTag = tag; + this.mFirst = first; + this.mLast = last; + } + public boolean isValid(){ + return mFirst < mLast; + } + public int getFirst() { + return mFirst; + } + public void setFirst(int first) { + this.mFirst = first; + } + public int getLast() { + return mLast; + } + public void setLast(int last) { + this.mLast = last; + } + public String getTag() { + return mTag; + } + public void setTag(String tag) { + this.mTag = tag; + } + + public String getStartTag(boolean xml){ + int i= mTag.indexOf(';'); + StringBuilder builder=new StringBuilder(); + builder.append('<'); + if(i<0){ + builder.append(mTag); + }else { + builder.append(mTag, 0, i); + builder.append(' '); + String attrs = mTag.substring(i+1); + if(xml){ + appendXmlAttrs(builder, attrs); + }else { + builder.append(attrs); + } + } + builder.append('>'); + return builder.toString(); + } + private void appendXmlAttrs(StringBuilder builder, String rawAttr){ + String[] split=rawAttr.split("(\\s*;\\s*)"); + for(int i=0;i'); + return builder.toString(); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(NAME_tag, mTag); + jsonObject.put(NAME_first, mFirst); + jsonObject.put(NAME_last, mLast); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setTag(json.getString(NAME_tag)); + setFirst(json.getInt(NAME_first)); + setLast(json.getInt(NAME_last)); + } + @Override + public String toString(){ + return mTag +" ("+ mFirst +", "+ mLast +")"; + } + + public static final String NAME_tag="tag"; + public static final String NAME_first="first"; + public static final String NAME_last="last"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/model/StyledStringBuilder.java b/src/ARSCLib/com/reandroid/arsc/model/StyledStringBuilder.java new file mode 100644 index 00000000..f736da9f --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/model/StyledStringBuilder.java @@ -0,0 +1,196 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class StyledStringBuilder { + + public static String build(String text, Collection spanInfoList, boolean xml){ + if(isEmpty(spanInfoList)){ + return text; + } + CharPiece[] charPieceArray = toCharPieceArray(text); + boolean spansOk = initializeTags(charPieceArray, spanInfoList, xml); + if(!spansOk){ + // TODO: should throw here ? + return text; + } + if(xml){ + escapeXmlChars(charPieceArray); + } + StringBuilder builder = new StringBuilder(); + int length = charPieceArray.length; + for(int i = 0; i < length; i++){ + CharPiece charPiece = charPieceArray[i]; + charPiece.append(builder); + } + return builder.toString(); + } + + private static boolean isEmpty(Collection spanInfoList){ + if(spanInfoList == null || spanInfoList.size()==0){ + return true; + } + for(StyleSpanInfo spanInfo:spanInfoList){ + if(spanInfo != null){ + return false; + } + } + return true; + } + private static boolean initializeTags(CharPiece[] charPieceArray, Collection spanInfoList, boolean xml){ + for(StyleSpanInfo spanInfo : spanInfoList){ + if(spanInfo == null){ + continue; + } + boolean spanOk = initializeTag(charPieceArray, spanInfo, xml); + if(!spanOk){ + return false; + } + } + return true; + } + private static boolean initializeTag(CharPiece[] charPieceArray, StyleSpanInfo spanInfo, boolean xml){ + int length = charPieceArray.length; + int pos = spanInfo.getFirst(); + if(pos < 0 || pos >= length){ + return false; + } + CharPiece charPiece = charPieceArray[pos]; + charPiece.addFirstTag(spanInfo.getStartTag(xml)); + + pos = spanInfo.getLast(); + if(pos < 0 || pos >= length){ + return false; + } + charPiece = charPieceArray[pos]; + charPiece.addLastTag(spanInfo.getEndTag()); + return true; + } + + private static void escapeXmlChars(CharPiece[] charPieceArray){ + int length = charPieceArray.length; + for(int i = 0; i < length; i++){ + CharPiece charPiece = charPieceArray[i]; + if(isSpecialXmlChar(charPiece.mChar) && !isAlreadyEscaped(charPieceArray, i)){ + charPiece.escapedXml = escapeXmlChar(charPiece.mChar); + } + } + } + private static boolean isAlreadyEscaped(CharPiece[] charPieceArray, int position){ + if(charPieceArray[position].mChar != '&'){ + return false; + } + if((position + 3) >= charPieceArray.length){ + return false; + } + if(charPieceArray[position + 3].mChar == ';'){ + char ch = charPieceArray[position + 1].mChar; + return charPieceArray[position + 2].mChar == 't' + && (ch == 'l' || ch == 'g') ; + } + if((position + 4) >= charPieceArray.length){ + return false; + } + if(charPieceArray[position + 4].mChar == ';'){ + return charPieceArray[position + 1].mChar == 'a' + && charPieceArray[position + 2].mChar == 'm' + && charPieceArray[position + 3].mChar == 'p'; + } + return false; + } + private static String escapeXmlChar(char ch){ + switch (ch){ + case '&': + return "&"; + case '<': + return "<"; + case '>': + return ">"; + default: + throw new IllegalArgumentException("Not special xml char: '" + ch + "'"); + } + } + private static boolean isSpecialXmlChar(char ch){ + switch (ch){ + case '&': + case '<': + case '>': + return true; + default: + return false; + } + } + private static CharPiece[] toCharPieceArray(String text){ + char[] chars = text.toCharArray(); + int length = chars.length; + CharPiece[] results = new CharPiece[length]; + for(int i = 0; i < length; i++){ + results[i] = new CharPiece(i, chars[i]); + } + return results; + } + + static class CharPiece{ + final int position; + private List firstTagList; + final char mChar; + private List lastTagList; + String escapedXml; + CharPiece(int position, char ch){ + this.position = position; + this.mChar = ch; + } + void append(StringBuilder builder){ + if(firstTagList != null){ + for(String tag : firstTagList){ + builder.append(tag); + } + } + if(escapedXml != null){ + builder.append(escapedXml); + }else { + builder.append(mChar); + } + if(lastTagList != null){ + for(String tag : lastTagList){ + builder.append(tag); + } + } + } + void addFirstTag(String tag){ + if(tag == null){ + return; + } + if(this.firstTagList == null){ + this.firstTagList = new ArrayList<>(2); + } + this.firstTagList.add(tag); + } + void addLastTag(String tag){ + if(tag == null){ + return; + } + if(this.lastTagList == null){ + this.lastTagList = new ArrayList<>(2); + } + this.lastTagList.add(0, tag); + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/JsonStringPoolHelper.java b/src/ARSCLib/com/reandroid/arsc/pool/JsonStringPoolHelper.java new file mode 100644 index 00000000..2f69fbcb --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/JsonStringPoolHelper.java @@ -0,0 +1,138 @@ +/* + * 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.pool; + +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.array.StyleArray; +import com.reandroid.arsc.item.StringItem; +import com.reandroid.arsc.item.StyleItem; +import com.reandroid.arsc.model.StyleSpanInfo; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONObject; + +import java.util.*; + +class JsonStringPoolHelper { + + private final StringPool stringPool; + JsonStringPoolHelper(StringPool stringPool){ + this.stringPool=stringPool; + } + void loadStyledStrings(JSONArray jsonArray) { + //Styled strings should be at first rows of string pool thus we clear all before adding + stringPool.getStringsArray().clearChildes(); + stringPool.getStyleArray().clearChildes(); + + List styledStringList = StyledString.fromJson(jsonArray); + loadText(styledStringList); + Map tagIndexMap = loadStyleTags(styledStringList); + loadStyles(styledStringList, tagIndexMap); + stringPool.refreshUniqueIdMap(); + } + private void loadText(List styledStringList) { + StringArray stringsArray = stringPool.getStringsArray(); + int size=styledStringList.size(); + stringsArray.ensureSize(size); + for(int i=0;i loadStyleTags(List styledStringList) { + Map indexMap=new HashMap<>(); + List tagList=new ArrayList<>(getStyleTags(styledStringList)); + tagList.sort(stringPool); + StringArray stringsArray = stringPool.getStringsArray(); + int tagsSize = tagList.size(); + int initialSize = stringsArray.childesCount(); + stringsArray.ensureSize(initialSize + tagsSize); + for(int i=0;i styledStringList, Map tagIndexMap){ + StyleArray styleArray = stringPool.getStyleArray(); + int size=styledStringList.size(); + styleArray.ensureSize(size); + for(int i=0;i getStyleTags(List styledStringList){ + Set results=new HashSet<>(); + for(StyledString ss:styledStringList){ + for(StyleSpanInfo spanInfo:ss.spanInfoList){ + results.add(spanInfo.getTag()); + } + } + return results; + } + private static class StyledString{ + final String text; + final List spanInfoList; + StyledString(String text, List spanInfoList){ + this.text=text; + this.spanInfoList=spanInfoList; + } + @Override + public String toString(){ + return text; + } + static List fromJson(JSONArray jsonArray){ + int length = jsonArray.length(); + List results = new ArrayList<>(length); + for(int i=0; i < length; i++){ + StyledString styledString = + fromJson(jsonArray.getJSONObject(i)); + if(styledString != null){ + results.add(styledString); + } + } + return results; + } + private static StyledString fromJson(JSONObject jsonObject){ + if(!jsonObject.has(StringItem.NAME_style)){ + return null; + } + String text = jsonObject.getString(StringItem.NAME_string); + JSONObject style=jsonObject.getJSONObject(StringItem.NAME_style); + JSONArray spansArray=style.getJSONArray(StyleItem.NAME_spans); + List spanInfoList = toSpanInfoList(spansArray); + return new StyledString(text, spanInfoList); + } + private static List toSpanInfoList(JSONArray jsonArray){ + int length = jsonArray.length(); + List results=new ArrayList<>(length); + for(int i=0;i { + public ResXmlStringPool(boolean is_utf8) { + super(is_utf8, false); + } + @Override + public ResXmlString removeReference(ReferenceItem referenceItem){ + if(referenceItem==null){ + return null; + } + ResXmlString stringItem = super.removeReference(referenceItem); + removeNotUsedItem(stringItem); + return stringItem; + } + private void removeNotUsedItem(ResXmlString xmlString){ + if(xmlString == null || xmlString.hasReference()){ + return; + } + ResXmlIDMap idMap = getResXmlIDMap(); + int lastIdIndex = -1; + if(idMap!=null){ + lastIdIndex = idMap.countId() - 1; + } + if(idMap!=null && xmlString.getIndex()>lastIdIndex){ + removeString(xmlString); + }else { + xmlString.set(""); + } + } + @Override + StringArray newInstance(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + return new ResXmlStringArray(offsets, itemCount, itemStart, is_utf8); + } + public ResXmlString getOrCreate(String str){ + return getOrCreateAttribute(0, str); + } + public ResXmlString createNew(String str){ + StringArray stringsArray = getStringsArray(); + ResXmlString xmlString = stringsArray.createNext(); + xmlString.set(str); + return xmlString; + } + public ResXmlString getOrCreateAttribute(int resourceId, String str){ + ResXmlIDMap resXmlIDMap = getResXmlIDMap(); + if(resXmlIDMap == null){ + return super.getOrCreate(str); + } + ResXmlIDArray idArray = resXmlIDMap.getResXmlIDArray(); + int count = idArray.childesCount(); + if(resourceId == 0){ + return getOrCreateAfter(count, str); + } + StringArray stringsArray = getStringsArray(); + ResXmlID xmlID = idArray.getByResId(resourceId); + if(xmlID != null){ + ResXmlString xmlString = stringsArray.get(xmlID.getIndex()); + if(xmlString!=null && Objects.equals(str, xmlString.get())){ + return xmlString; + } + } + count = idArray.childesCount() + 1; + stringsArray.ensureSize(count); + idArray.setChildesCount(count); + int index = count - 1; + xmlID = idArray.get(index); + xmlID.set(resourceId); + idArray.refreshIdMap(); + + ResXmlString xmlString = stringsArray.newInstance(); + xmlString.set(str); + stringsArray.insertItem(index, xmlString); + + updateUniqueIdMap(xmlString); + return xmlString; + } + private ResXmlString getOrCreateAfter(int position, String str){ + if(position<0){ + position=0; + } + StringGroup group = get(str); + if(group!=null){ + for(ResXmlString xmlString:group.listItems()){ + int index = xmlString.getIndex(); + if(index > position || (position==0 && position == index)){ + return xmlString; + } + } + } + StringArray stringsArray = getStringsArray(); + int count = stringsArray.childesCount(); + if(count < position){ + count = position; + } + stringsArray.ensureSize(count+1); + ResXmlString xmlString = stringsArray.get(count); + xmlString.set(str); + super.updateUniqueIdMap(xmlString); + return xmlString; + } + private ResXmlIDMap getResXmlIDMap(){ + ResXmlDocument resXmlDocument = getParentInstance(ResXmlDocument.class); + if(resXmlDocument!=null){ + return resXmlDocument.getResXmlIDMap(); + } + return null; + } + public ResXmlString getOrCreateAttributeName(int idMapCount, String str){ + StringGroup group = get(str); + if(group!=null){ + for(ResXmlString xmlString:group.listItems()){ + if(xmlString.getIndex()>idMapCount){ + return xmlString; + } + } + } + StringArray stringsArray = getStringsArray(); + stringsArray.ensureSize(idMapCount); + int i=stringsArray.childesCount(); + stringsArray.ensureSize(i+1); + ResXmlString xmlString=stringsArray.get(i); + xmlString.set(str); + refreshUniqueIdMap(); + return xmlString; + } + @Override + public void onChunkLoaded() { + super.onChunkLoaded(); + StyleArray styleArray = getStyleArray(); + if(styleArray.childesCount()>0){ + notifyResXmlStringPoolHasStyles(styleArray.childesCount()); + } + } + private static void notifyResXmlStringPoolHasStyles(int styleArrayCount){ + if(HAS_STYLE_NOTIFIED){ + return; + } + String msg="Not expecting ResXmlStringPool to have styles count=" + +styleArrayCount+",\n please create issue along with this apk/file on https://github.com/REAndroid/ARSCEditor"; + System.err.println(msg); + HAS_STYLE_NOTIFIED=true; + } + private static boolean HAS_STYLE_NOTIFIED; +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/SpecStringPool.java b/src/ARSCLib/com/reandroid/arsc/pool/SpecStringPool.java new file mode 100755 index 00000000..a7ec8aef --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/SpecStringPool.java @@ -0,0 +1,45 @@ +/* + * 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.pool; + +import com.reandroid.arsc.array.OffsetArray; +import com.reandroid.arsc.array.SpecStringArray; +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.SpecString; + +public class SpecStringPool extends StringPool { + public SpecStringPool(boolean is_utf8){ + super(is_utf8); + } + + @Override + StringArray newInstance(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + return new SpecStringArray(offsets, itemCount, itemStart, is_utf8); + } + public PackageBlock getPackageBlock(){ + return getParent(PackageBlock.class); + } + + @Override + void linkStrings(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock != null){ + packageBlock.linkSpecStringsInternal(this); + } + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/StringPool.java b/src/ARSCLib/com/reandroid/arsc/pool/StringPool.java new file mode 100755 index 00000000..1f1cf466 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/StringPool.java @@ -0,0 +1,350 @@ +/* + * 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.pool; + +import com.reandroid.arsc.array.OffsetArray; +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.array.StyleArray; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.chunk.Chunk; +import com.reandroid.arsc.group.StringGroup; +import com.reandroid.arsc.header.StringPoolHeader; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.*; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; + +import java.io.IOException; +import java.util.*; + + +public abstract class StringPool extends Chunk implements BlockLoad, JSONConvert, Comparator { + private final Object mLock = new Object(); + private final StringArray mArrayStrings; + private final StyleArray mArrayStyles; + + private final Map> mUniqueMap; + private boolean stringLinkLocked; + + StringPool(boolean is_utf8, boolean stringLinkLocked){ + super(new StringPoolHeader(), 4); + + OffsetArray offsetStrings = new OffsetArray(); + OffsetArray offsetStyles = new OffsetArray(); + + StringPoolHeader header = getHeaderBlock(); + + this.mArrayStrings = newInstance( + offsetStrings, + header.getCountStrings(), + header.getStartStrings(), + is_utf8); + + this.mArrayStyles = new StyleArray( + offsetStyles, + header.getCountStyles(), + header.getStartStyles()); + + + addChild(offsetStrings); + addChild(offsetStyles); + addChild(mArrayStrings); + addChild(mArrayStyles); + + setUtf8(is_utf8, false); + + header.getFlagUtf8().setBlockLoad(this); + + mUniqueMap = new HashMap<>(); + this.stringLinkLocked = stringLinkLocked; + } + StringPool(boolean is_utf8){ + this(is_utf8, true); + } + + public boolean isStringLinkLocked(){ + return stringLinkLocked; + } + public void ensureStringLinkUnlockedInternal(){ + if(!stringLinkLocked){ + return; + } + synchronized (mLock){ + if(!stringLinkLocked){ + return; + } + stringLinkLocked = false; + linkStrings(); + } + } + void linkStrings(){ + } + + public void removeString(T item){ + getStringsArray().remove(item); + } + public void destroy(){ + getStyleArray().clearChildes(); + getStringsArray().clearChildes(); + } + public List toStringList(){ + return getStringsArray().toStringList(); + } + public void addStrings(Collection stringList){ + if(stringList==null || stringList.size()==0){ + return; + } + Set uniqueSet; + if(stringList instanceof HashSet){ + uniqueSet=(HashSet)stringList; + }else { + uniqueSet=new HashSet<>(stringList); + } + refreshUniqueIdMap(); + Set keySet=mUniqueMap.keySet(); + for(String key:keySet){ + uniqueSet.remove(key); + } + List sortedList=new ArrayList<>(stringList); + sortedList.sort(this); + insertStringList(sortedList); + } + private void insertStringList(List stringList){ + StringArray stringsArray = getStringsArray(); + int initialSize=stringsArray.childesCount(); + stringsArray.ensureSize(initialSize + stringList.size()); + int size=stringsArray.childesCount(); + int j=0; + for (int i=initialSize;i insertStrings(List stringList){ + Map results=new HashMap<>(); + StringArray stringsArray = getStringsArray(); + int initialSize=stringsArray.childesCount(); + stringsArray.ensureSize(initialSize + stringList.size()); + int size=stringsArray.childesCount(); + int j=0; + for (int i=initialSize;i group= getOrCreateGroup(str); + group.add(item); + } + } + void updateUniqueIdMap(T item){ + if(item==null){ + return; + } + StringGroup group = getOrCreateGroup(item.getHtml()); + group.add(item); + } + public List removeUnusedStrings(){ + return getStringsArray().removeUnusedStrings(); + } + public List listUnusedStrings(){ + return getStringsArray().listUnusedStrings(); + } + public Collection listStrings(){ + return getStringsArray().listItems(); + } + public StyleArray getStyleArray(){ + return mArrayStyles; + } + public StringArray getStringsArray(){ + return mArrayStrings; + } + public void removeReferences(Collection referenceList){ + if(referenceList==null){ + return; + } + for(ReferenceItem ref:referenceList){ + removeReference(ref); + } + } + public T removeReference(ReferenceItem ref){ + if(ref==null){ + return null; + } + T item=get(ref.get()); + if(item!=null){ + item.removeReference(ref); + return item; + } + return null; + } + public void addReference(ReferenceItem ref){ + if(ref==null){ + return; + } + T item=get(ref.get()); + if(item!=null){ + item.addReference(ref); + } + } + public void addReferences(Collection referenceList){ + if(referenceList==null){ + return; + } + for(ReferenceItem ref:referenceList){ + addReference(ref); + } + } + + public boolean contains(String str){ + return mUniqueMap.containsKey(str); + } + public final T get(int index){ + return mArrayStrings.get(index); + } + public final StringGroup get(String str){ + return mUniqueMap.get(str); + } + public T getOrCreate(String str){ + StringGroup group=getOrCreateGroup(str); + T[] items=group.getItems(); + if(items.length==0){ + T t=createNewString(str); + group.add(t); + items=group.getItems(); + } + return items[0]; + } + private StringGroup getOrCreateGroup(String str){ + StringGroup group=get(str); + if(group!=null){ + return group; + } + group=new StringGroup<>(mArrayStrings, str); + mUniqueMap.put(str, group); + return group; + } + private T createNewString(String str){ + T item=mArrayStrings.createNext(); + item.set(str); + getHeaderBlock().getCountStrings().set(mArrayStrings.childesCount()); + return item; + } + public final StyleItem getStyle(int index){ + return mArrayStyles.get(index); + } + public final int countStrings(){ + return mArrayStrings.childesCount(); + } + public final int countStyles(){ + return mArrayStyles.childesCount(); + } + public final T[] getStrings(){ + return mArrayStrings.getChildes(); + } + public final StyleItem[] getStyles(){ + return mArrayStyles.getChildes(); + } + public void setUtf8(boolean is_utf8){ + setUtf8(is_utf8, true); + } + private void setUtf8(boolean is_utf8, boolean updateAll){ + StringPoolHeader header = getHeaderBlock(); + if(is_utf8 == header.isUtf8()){ + return; + } + ByteItem flagUtf8 = header.getFlagUtf8(); + if(is_utf8){ + flagUtf8.set((byte) 0x01); + }else { + flagUtf8.set((byte) 0x00); + } + if(!updateAll){ + return; + } + mArrayStrings.setUtf8(is_utf8); + } + public void setFlagSorted(boolean sorted){ + getHeaderBlock().setSorted(sorted); + } + + abstract StringArray newInstance(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8); + @Override + protected void onChunkRefreshed() { + mArrayStrings.refreshCountAndStart(); + mArrayStyles.refreshCountAndStart(); + } + @Override + public void onChunkLoaded() { + refreshUniqueIdMap(); + StyleItem[] styles = getStyles(); + if(styles!=null){ + for(StyleItem styleItem:styles){ + styleItem.onDataLoaded(); + } + } + } + + @Override + public void onBlockLoaded(BlockReader reader, Block sender) throws IOException { + StringPoolHeader header = getHeaderBlock(); + if(sender == header.getFlagUtf8()){ + mArrayStrings.setUtf8(header.isUtf8()); + } + } + @Override + public JSONArray toJson() { + return getStringsArray().toJson(); + } + //Only for styled strings + @Override + public void fromJson(JSONArray json) { + if(json==null){ + return; + } + JsonStringPoolHelper helper=new JsonStringPoolHelper<>(this); + helper.loadStyledStrings(json); + refresh(); + } + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } + +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/TableStringPool.java b/src/ARSCLib/com/reandroid/arsc/pool/TableStringPool.java new file mode 100755 index 00000000..02481897 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/TableStringPool.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.arsc.pool; + +import com.reandroid.arsc.array.OffsetArray; +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.array.TableStringArray; +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.UnknownChunk; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.header.TableHeader; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.item.TableString; + +import java.io.IOException; +import java.io.InputStream; + +public class TableStringPool extends StringPool { + public TableStringPool(boolean is_utf8) { + super(is_utf8); + } + + @Override + void linkStrings(){ + TableBlock tableBlock = getParentInstance(TableBlock.class); + if(tableBlock != null){ + tableBlock.linkTableStringsInternal(this); + } + } + @Override + StringArray newInstance(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + return new TableStringArray(offsets, itemCount, itemStart, is_utf8); + } + public void merge(TableStringPool stringPool){ + if(stringPool==null||stringPool==this){ + return; + } + StringArray existArray = getStringsArray(); + if(existArray.childesCount()!=0){ + return; + } + StringArray comingArray = stringPool.getStringsArray(); + int count=comingArray.childesCount(); + existArray.ensureSize(count); + for(int i=0;i { + private final IntegerItem mTypeIdOffset; + public TypeStringPool(boolean is_utf8, IntegerItem typeIdOffset) { + super(is_utf8, false); + this.mTypeIdOffset = typeIdOffset; + } + public int getLastId(){ + int count = countStrings(); + if(count == 0){ + return 0; + } + return get(count - 1).getId(); + } + public int idOf(String typeName){ + return idOf(getByName(typeName)); + } + /** + * Resolves id of {@link TypeBlock} + * Not recommend to use unless unless you are sure of proper pool + **/ + public int idOf(TypeString typeString){ + if(typeString==null){ + return 0; + } + return (typeString.getIndex()+mTypeIdOffset.get()+1); + } + /** + * Searches string entry {@link TypeBlock} + * {@param name} is name of {@link TypeBlock} + * This might not working if duplicate type names are present + **/ + public TypeString getByName(String name){ + for(TypeString typeString:listStrings()){ + if(name.equals(typeString.get())){ + return typeString; + } + } + return null; + } + public TypeString getById(int id){ + int index=id-mTypeIdOffset.get()-1; + return super.get(index); + } + public TypeString getOrCreate(int typeId, String typeName){ + StringArray stringsArray = getStringsArray(); + int old = stringsArray.childesCount(); + int size = typeId - mTypeIdOffset.get(); + stringsArray.ensureSize(size); + TypeString typeString = getById(typeId); + typeString.set(typeName); + if(old != stringsArray.childesCount()){ + updateUniqueIdMap(typeString); + } + return typeString; + } + /** + * Use getOrCreate(typeId, typeName)} + **/ + @Deprecated + @Override + public final TypeString getOrCreate(String str){ + StringGroup group = get(str); + if(group==null||group.size()==0){ + throw new IllegalArgumentException("Can not create TypeString (" + str + +") without type id. use getOrCreate(typeId, typeName)"); + } + return group.get(0); + } + @Override + StringArray newInstance(OffsetArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { + return new TypeStringArray(offsets, itemCount, itemStart, is_utf8); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/builder/StringPoolMerger.java b/src/ARSCLib/com/reandroid/arsc/pool/builder/StringPoolMerger.java new file mode 100644 index 00000000..3e21ce9e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/builder/StringPoolMerger.java @@ -0,0 +1,160 @@ + /* + * 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.pool.builder; + +import com.reandroid.arsc.array.StringArray; +import com.reandroid.arsc.array.StyleArray; +import com.reandroid.arsc.item.StyleItem; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.model.StyleSpanInfo; +import com.reandroid.arsc.pool.TableStringPool; + +import java.util.*; + +public class StringPoolMerger implements Comparator { + private final Set mPools; + private int mMergedPools; + private int mMergedStrings; + private int mMergedStyleStrings; + public StringPoolMerger(){ + this.mPools=new HashSet<>(); + } + public void mergeTo(TableStringPool destination){ + mMergedPools=0; + mMergedStrings=0; + mMergedStyleStrings=0; + if(destination.countStrings()>0 || destination.countStyles()>0){ + throw new IllegalArgumentException("Destination string pool is not empty"); + } + mergeStyledStrings(destination); + mergeNonStyledStrings(destination); + mMergedPools = mPools.size(); + mPools.clear(); + destination.refresh(); + } + public void add(TableStringPool stringPool){ + mPools.add(stringPool); + } + public int getMergedPools() { + return mMergedPools; + } + public int getMergedStyleStrings() { + return mMergedStyleStrings; + } + public int getMergedStrings() { + return mMergedStrings; + } + + private void mergeStyledStrings(TableStringPool destination){ + List styledStrings = getStyledStrings(); + Map mapTableStrings = + destination.insertStrings(toStringList(styledStrings)); + Map mapTags = + destination.insertStrings(listStyleTags(styledStrings)); + + StyleArray styleArray = destination.getStyleArray(); + styleArray.setChildesCount(styledStrings.size()); + + for(TableString tableString:styledStrings){ + TableString createdString = mapTableStrings.get(tableString.get()); + StyleItem createdStyle = styleArray.get(createdString.getIndex()); + + StyleItem styleItem = tableString.getStyle(); + for(StyleSpanInfo spanInfo:styleItem.getSpanInfoList()){ + if(spanInfo!=null && createdStyle!=null){ + int tagReference = mapTags.get(spanInfo.getTag()) + .getIndex(); + createdStyle.addStylePiece( + tagReference, + spanInfo.getFirst(), + spanInfo.getLast()); + } + } + } + mMergedStyleStrings=styledStrings.size(); + } + private void mergeNonStyledStrings(TableStringPool destination){ + List nonStyledStrings=getNonStyledStrings(); + destination.insertStrings(nonStyledStrings); + mMergedStrings=nonStyledStrings.size(); + } + private List getStyledStrings(){ + Map mapUniqueHtml = new HashMap<>(); + for(TableStringPool pool:mPools){ + int styleCount = pool.countStyles(); + StringArray stringArray = pool.getStringsArray(); + for(int i=0;i(mapUniqueHtml.values()); + } + private List getNonStyledStrings(){ + Set uniqueSet = new HashSet<>(); + for(TableStringPool pool:mPools){ + TableString[] tableStrings = pool.getStrings(); + if(tableStrings==null){ + continue; + } + for(int i=0;i results=new ArrayList<>(uniqueSet); + results.sort(this); + return results; + } + private List toStringList(Collection tableStringList){ + List results=new ArrayList<>(tableStringList.size()); + for(TableString tableString:tableStringList){ + String str=tableString.get(); + if(str!=null){ + results.add(str); + } + } + results.sort(this); + return results; + } + private List listStyleTags(List styledStrings){ + Set resultSet=new HashSet<>(); + for(TableString tableString:styledStrings){ + StyleItem style = tableString.getStyle(); + if(style==null){ + continue; + } + for(StyleSpanInfo spanInfo:style.getSpanInfoList()){ + if(spanInfo!=null){ + resultSet.add(spanInfo.getTag()); + } + } + } + List results=new ArrayList<>(resultSet); + results.sort(this); + return results; + } + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/pool/builder/StyleBuilder.java b/src/ARSCLib/com/reandroid/arsc/pool/builder/StyleBuilder.java new file mode 100755 index 00000000..e41e1efa --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/pool/builder/StyleBuilder.java @@ -0,0 +1,43 @@ + /* + * 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.pool.builder; + +import com.reandroid.arsc.item.StringItem; + +import java.util.regex.Pattern; + +public class StyleBuilder { + public static void buildStyle(StringItem stringItem){ + } + public static boolean hasStyle(StringItem stringItem){ + if(stringItem==null){ + return false; + } + return hasStyle(stringItem.getHtml()); + } + public static boolean hasStyle(String text){ + if(text==null){ + return false; + } + int i=text.indexOf('<'); + if(i<0){ + return false; + } + i=text.indexOf('>'); + return i>1; + } + private static final Pattern PATTERN_STYLE=Pattern.compile(""); +} diff --git a/src/ARSCLib/com/reandroid/arsc/util/FrameworkTable.java b/src/ARSCLib/com/reandroid/arsc/util/FrameworkTable.java new file mode 100755 index 00000000..3d4501ad --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/util/FrameworkTable.java @@ -0,0 +1,435 @@ +/* + * 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.util; + +import com.reandroid.arsc.BuildInfo; +import com.reandroid.arsc.array.SpecTypePairArray; +import com.reandroid.arsc.array.TypeBlockArray; +import com.reandroid.arsc.chunk.ChunkType; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.header.HeaderBlock; +import com.reandroid.arsc.item.ReferenceItem; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.common.FileChannelInputStream; + +import java.io.*; +import java.util.*; + +public class FrameworkTable extends TableBlock { + + private String frameworkName; + private int versionCode; + private int mainPackageId; + private ResNameMap mNameGroupMap; + private boolean mOptimized; + private boolean mOptimizeChecked; + public FrameworkTable(){ + super(); + } + + public boolean isAndroid(){ + return "android".equals(getFrameworkName()) + && getMainPackageId() == 0x01; + } + + public int getMainPackageId() { + if(mainPackageId!=0){ + return mainPackageId; + } + PackageBlock packageBlock = pickOne(); + if(packageBlock!=null){ + mainPackageId = packageBlock.getId(); + } + return mainPackageId; + } + + @Override + public void destroy(){ + clearResourceNameMap(); + this.frameworkName = null; + this.versionCode = 0; + this.mainPackageId = 0; + super.destroy(); + } + public int resolveResourceId(String typeName, String entryName){ + Entry entry = searchEntry(typeName, entryName); + if(entry !=null){ + return entry.getResourceId(); + } + return 0; + } + /** + * Loads all resource name map to memory for faster use + * Call this if you plan to search entries frequently + */ + public void loadResourceNameMap(){ + ResNameMap resNameMap = mNameGroupMap; + if(resNameMap == null){ + resNameMap = new ResNameMap<>(); + for(PackageBlock packageBlock:listPackages()){ + for(EntryGroup group:packageBlock.listEntryGroup()){ + resNameMap.add(group.getTypeName(), + group.getSpecName(), + group); + } + } + mNameGroupMap = resNameMap; + } + } + /** + * Clears resource name map from memory + */ + public void clearResourceNameMap(){ + if(mNameGroupMap!=null){ + mNameGroupMap.clear(); + mNameGroupMap =null; + } + } + private boolean hasResourceGroupMap(){ + return mNameGroupMap!=null; + } + private Entry searchEntryFromMap(String typeName, String entryName){ + if(mNameGroupMap ==null){ + return null; + } + EntryGroup entryGroup = mNameGroupMap.get(typeName, entryName); + if(entryGroup!=null){ + return entryGroup.pickOne(); + } + return null; + } + public Entry searchEntry(String typeName, String entryName){ + if(hasResourceGroupMap()){ + return searchEntryFromMap(typeName, entryName); + } + return searchEntryFromTable(typeName, entryName); + } + /** + * Since this is framework, we are sure of proper names. + */ + public Entry searchEntryFromTable(String typeName, String entryName){ + for(PackageBlock packageBlock:listPackages()){ + SpecTypePair specTypePair = packageBlock.getSpecTypePair(typeName); + if(specTypePair!=null){ + return specTypePair.getAnyEntry(entryName); + } + } + return null; + } + public int getVersionCode(){ + if(versionCode == 0 && isOptimized()){ + String version = loadProperty(PROP_VERSION_CODE); + if(version!=null){ + try{ + versionCode = Integer.parseInt(version); + }catch (NumberFormatException ignored){ + } + } + } + return versionCode; + } + public void setVersionCode(int value){ + versionCode = value; + if(isOptimized()){ + writeVersionCode(value); + } + } + public String getFrameworkName(){ + if(frameworkName == null){ + frameworkName = loadProperty(PROP_NAME); + } + if(frameworkName == null){ + PackageBlock packageBlock = pickOne(); + if(packageBlock!=null){ + String name = packageBlock.getName(); + if(name!=null && !name.trim().isEmpty()){ + frameworkName = name; + } + } + } + return frameworkName; + } + public void setFrameworkName(String value){ + frameworkName = value; + if(isOptimized()){ + writeProperty(PROP_NAME, value); + } + } + public void optimize(String name, int version){ + mOptimizeChecked = true; + mOptimized = false; + ensureTypeBlockNonNullEntries(); + optimizeEntries(); + optimizeTableString(); + writeVersionCode(version); + mOptimizeChecked = false; + setFrameworkName(name); + refresh(); + } + private void ensureTypeBlockNonNullEntries(){ + for(PackageBlock packageBlock:listPackages()){ + ensureTypeBlockNonNullEntries(packageBlock); + } + } + private void ensureTypeBlockNonNullEntries(PackageBlock packageBlock){ + for(SpecTypePair specTypePair:packageBlock.listSpecTypePairs()){ + ensureTypeBlockNonNullEntries(specTypePair); + } + } + private void ensureTypeBlockNonNullEntries(SpecTypePair specTypePair){ + Map map = specTypePair.createEntryGroups(); + for(EntryGroup entryGroup:map.values()){ + ensureNonNullDefaultEntry(entryGroup); + } + } + private void ensureNonNullDefaultEntry(EntryGroup entryGroup){ + Entry defEntry = entryGroup.getDefault(false); + Entry entry; + if(defEntry==null){ + entry = entryGroup.pickOne(); + if(entry == null){ + return; + } + SpecTypePair specTypePair = entry.getTypeBlock().getParentSpecTypePair(); + TypeBlock type = specTypePair.getOrCreateTypeBlock(new ResConfig()); + defEntry = type.getOrCreateEntry((short) (entry.getId() & 0xffff)); + } + if(!defEntry.isNull()){ + return; + } + entry = entryGroup.pickOne(); + if(entry.isNull()){ + return; + } + defEntry.merge(entry); + defEntry.isDefault(); + } + private void optimizeEntries(){ + Map groupMap=scanAllEntryGroups(); + for(EntryGroup group:groupMap.values()){ + List entryList = getEntriesToRemove(group); + removeEntries(entryList); + } + for(PackageBlock pkg:listPackages()){ + removeEmptyBlocks(pkg); + } + for(PackageBlock pkg:listPackages()){ + pkg.removeEmpty(); + pkg.refresh(); + } + } + private void removeEmptyBlocks(PackageBlock pkg){ + SpecTypePairArray specTypePairArray = pkg.getSpecTypePairArray(); + specTypePairArray.sort(); + List specTypePairList=new ArrayList<>(specTypePairArray.listItems()); + for(SpecTypePair specTypePair:specTypePairList){ + removeEmptyBlocks(specTypePair); + } + } + private void removeEmptyBlocks(SpecTypePair specTypePair){ + TypeBlockArray typeBlockArray = specTypePair.getTypeBlockArray(); + if(typeBlockArray.childesCount()<2){ + return; + } + typeBlockArray.removeEmptyBlocks(); + } + private void optimizeTableString(){ + removeUnusedTableString(); + shrinkTableString(); + getStringPool().getStyleArray().clearChildes(); + removeUnusedTableString(); + } + private void removeUnusedTableString(){ + TableStringPool tableStringPool=getStringPool(); + tableStringPool.removeUnusedStrings(); + tableStringPool.refresh(); + } + private void shrinkTableString(){ + TableStringPool tableStringPool=getStringPool(); + tableStringPool.getStringsArray().ensureSize(1); + TableString title=tableStringPool.get(0); + title.set(BuildInfo.getRepo()); + for(TableString tableString:tableStringPool.getStringsArray().listItems()){ + if(tableString==title){ + continue; + } + shrinkTableString(title, tableString); + } + tableStringPool.refresh(); + } + private void shrinkTableString(TableString zero, TableString tableString){ + List allRef = new ArrayList<>(tableString.getReferencedList()); + tableString.removeAllReference(); + for(ReferenceItem item:allRef){ + item.set(zero.getIndex()); + } + zero.addReference(allRef); + } + private void removeEntries(List removeList){ + for(Entry entry :removeList){ + removeEntry(entry); + } + } + private void removeEntry(Entry entry){ + TypeBlock typeBlock= entry.getTypeBlock(); + if(typeBlock==null){ + return; + } + typeBlock.removeEntry(entry); + + } + private List getEntriesToRemove(EntryGroup group){ + List results=new ArrayList<>(); + Entry mainEntry=group.pickOne(); + if(mainEntry==null){ + return results; + } + Iterator itr = group.iterator(true); + while (itr.hasNext()){ + Entry entry =itr.next(); + if(entry ==mainEntry){ + continue; + } + results.add(entry); + } + return results; + } + private Map scanAllEntryGroups(){ + Map results=new HashMap<>(); + for(PackageBlock packageBlock:listPackages()){ + Map map=packageBlock.getEntriesGroupMap(); + for(Map.Entry entry:map.entrySet()){ + int id=entry.getKey(); + EntryGroup group=entry.getValue(); + EntryGroup exist=results.get(id); + if(exist!=null && exist.getDefault()!=null){ + if(exist.getDefault()!=null){ + continue; + } + results.remove(id); + } + results.put(id, group); + } + } + return results; + } + private TableString writeProperty(String name, String value){ + if(!name.endsWith(":")){ + name=name+":"; + } + if(value==null){ + value=""; + } + if(!value.startsWith(name)){ + value=name+value; + } + TableString tableString=loadPropertyString(name); + if(tableString!=null){ + tableString.set(value); + }else { + TableStringPool tableStringPool=getStringPool(); + tableString=tableStringPool.getOrCreate(value); + } + return tableString; + } + private String loadProperty(String name){ + if(name==null){ + return null; + } + if(!name.endsWith(":")){ + name=name+":"; + } + TableString tableString=loadPropertyString(name); + if(tableString==null){ + return null; + } + String str=tableString.get().trim(); + return str.substring(name.length()).trim(); + } + private TableString loadPropertyString(String name){ + if(name==null){ + return null; + } + if(!name.endsWith(":")){ + name=name+":"; + } + TableStringPool tableStringPool=getStringPool(); + int max=PROP_COUNT; + for(int i=0;i0){ + writeString(writer,x , length); + } + } + private void writeHex(Writer writer, byte b) throws IOException { + String hex = HexUtil.toHex(null, (0xff & b), 2).toUpperCase(); + writer.write(hex); + } + private void writeString(Writer writer, int width, int position) throws IOException { + if(!mAppendString){ + return; + } + int start = position - width; + if(start<0){ + start=0; + } + int rem = this.width - width; + if(rem > 0){ + fillLastRow(writer, position); + } + writer.write(' '); + writer.write(' '); + writer.write(' '); + writer.write(' '); + String text = new String(this.byteArray, start, width, getEncoding()); + for(char ch:text.toCharArray()){ + printChar(writer, ch); + } + } + private void fillLastRow(Writer writer, int position) throws IOException { + int rem = width - position % width; + for(int i=0; ibase){ + digits++; + max=max/base; + } + lineNumberFormat = "%0"+digits+(decimalLineNumber?"d":"x"); + } + private void writeNewLine(Writer writer) throws IOException { + writer.write('\n'); + } + + public static String printHex(byte[] byteArray){ + return toHex(byteArray, false, false); + } + public static String toHex(byte[] byteArray){ + if(byteArray==null){ + return "null"; + } + return toHex(byteArray, byteArray.length>64); + } + public static String toHex(byte[] byteArray, boolean showLineNumber){ + return toHex(byteArray, showLineNumber, true); + } + public static String toHex(byte[] byteArray, boolean showLineNumber, boolean appendString){ + if(byteArray==null){ + return "null"; + } + StringWriter writer=new StringWriter(); + HexBytesWriter hexBytesWriter = new HexBytesWriter(byteArray); + hexBytesWriter.setShowLineNumber(showLineNumber); + hexBytesWriter.setAppendString(appendString); + try { + hexBytesWriter.write(writer); + writer.flush(); + writer.close(); + } catch (IOException ignored) { + } + return writer.toString(); + } + + private static final int DEFAULT_WIDTH = 16; + private static final int DEFAULT_COLUMNS = 4; + private static final int DEFAULT_INDENT = 0; +} diff --git a/src/ARSCLib/com/reandroid/arsc/util/HexUtil.java b/src/ARSCLib/com/reandroid/arsc/util/HexUtil.java new file mode 100644 index 00000000..1aa3c26a --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/util/HexUtil.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.arsc.util; + +public class HexUtil { + public static String toHex2(byte num){ + return toHex((long)(num & 0x00000000000000ffL), 2); + } + public static String toHex4(short num){ + return toHex((long)(num & 0x000000000000ffffL), 4); + } + public static String toHex8(int num){ + return toHex(num, 8); + } + public static String toHex8(long num){ + return toHex(num, 8); + } + public static String toHex(int num, int minLength){ + return toHex((0x00000000ffffffffL & num), minLength); + } + public static String toHex(long num, int minLength){ + String hex = Long.toHexString(num); + StringBuilder builder = new StringBuilder(); + builder.append('0'); + builder.append('x'); + int rem = minLength - hex.length(); + for(int i=0; i < rem; i++){ + builder.append('0'); + } + builder.append(hex); + return builder.toString(); + } + public static String toHexNoPrefix8(int num){ + return toHex(null, (0x00000000ffffffffL & num), 8); + } + public static String toHexNoPrefix(int num, int minLength){ + return toHex(null, (0x00000000ffffffffL & num), minLength); + } + public static String toHex8(String prefix, int num){ + return toHex(prefix, (0x00000000ffffffffL & num), 8); + } + public static String toHex(String prefix, int num, int minLength){ + return toHex(prefix, (0x00000000ffffffffL & num), minLength); + } + public static String toHex(String prefix, long num, int minLength){ + String hex = Long.toHexString(num); + StringBuilder builder = new StringBuilder(); + if(prefix != null){ + builder.append(prefix); + } + int rem = minLength - hex.length(); + for(int i=0; i < rem; i++){ + builder.append('0'); + } + builder.append(hex); + return builder.toString(); + } + public static int parseHex(String hexString){ + hexString = trim0x(hexString); + return (int) Long.parseLong(hexString, 16); + } + private static String trim0x(String hexString){ + if(hexString == null || hexString.length() < 3){ + return hexString; + } + if(hexString.charAt(0) == '0' && hexString.charAt(1) == 'x'){ + hexString = hexString.substring(2); + } + return hexString; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/util/ResNameMap.java b/src/ARSCLib/com/reandroid/arsc/util/ResNameMap.java new file mode 100644 index 00000000..e4db8c66 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/util/ResNameMap.java @@ -0,0 +1,65 @@ + /* + * 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.util; + +import java.util.HashMap; +import java.util.Map; + +public class ResNameMap { + private final Object mLock = new Object(); + private final Map> mainMap; + public ResNameMap(){ + this.mainMap = new HashMap<>(); + } + + public VALUE get(String type, String name){ + synchronized (mLock){ + if(type==null || name==null){ + return null; + } + Map valueMap = mainMap.get(type); + if(valueMap!=null){ + return valueMap.get(name); + } + return null; + } + } + public void add(String type, String name, VALUE value){ + synchronized (mLock){ + if(type==null || name==null || value==null){ + return; + } + Map valueMap = mainMap.get(type); + if(valueMap==null){ + valueMap=new HashMap<>(); + mainMap.put(type, valueMap); + } + valueMap.putIfAbsent(name, value); + } + } + public void clear(){ + synchronized (mLock){ + for(Map valueMap:mainMap.values()){ + valueMap.clear(); + } + mainMap.clear(); + } + } + + private static final String TYPE_ATTR = "attr"; + private static final String TYPE_ID = "id"; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/AttributeDataFormat.java b/src/ARSCLib/com/reandroid/arsc/value/AttributeDataFormat.java new file mode 100644 index 00000000..81f983d1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/AttributeDataFormat.java @@ -0,0 +1,258 @@ +/* + * 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.value; + +public enum AttributeDataFormat { + + REFERENCE(1<<0, + new ValueType[]{ + ValueType.REFERENCE, + ValueType.ATTRIBUTE, + ValueType.DYNAMIC_REFERENCE, + ValueType.DYNAMIC_ATTRIBUTE, + ValueType.NULL, + }), + STRING(1<<1, new ValueType[]{ + ValueType.STRING + }), + INTEGER(1<<2, new ValueType[]{ + ValueType.INT_DEC, + ValueType.INT_HEX + }), + BOOL(1<<3, new ValueType[]{ + ValueType.INT_BOOLEAN + }), + COLOR(1<<4, new ValueType[]{ + ValueType.INT_COLOR_RGB4, + ValueType.INT_COLOR_ARGB4, + ValueType.INT_COLOR_RGB8, + ValueType.INT_COLOR_ARGB8 + }), + FLOAT(1<<5, new ValueType[]{ + ValueType.FLOAT + }), + DIMENSION(1<<6, new ValueType[]{ + ValueType.DIMENSION + }), + FRACTION(1<<7, new ValueType[]{ + ValueType.FRACTION + }), + ANY(0x0000FFFF, ValueType.values().clone()), + + ENUM(1<<16, new ValueType[]{ + ValueType.INT_DEC, + ValueType.INT_HEX + }), + FLAG(1<<17, new ValueType[]{ + ValueType.INT_HEX, + ValueType.INT_DEC + }); + + private final int mask; + private final ValueType[] valueTypes; + + AttributeDataFormat(int mask, ValueType[] valueTypes) { + this.mask = mask; + this.valueTypes = valueTypes; + } + + public int getMask() { + return mask; + } + public boolean matches(int value){ + int mask = this.mask; + return (value & mask) == mask; + } + + public ValueType[] getValueTypes() { + return valueTypes.clone(); + } + public boolean contains(ValueType valueType){ + ValueType[] valueTypes = this.valueTypes; + for(int i = 0; i < valueTypes.length; i++){ + if(valueType == valueTypes[i]){ + return true; + } + } + return false; + } + public String getName(){ + return name().toLowerCase(); + } + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(getName()); + builder.append('{'); + ValueType[] valueTypes = this.valueTypes; + for(int i = 0; i < valueTypes.length; i++){ + if(i != 0){ + builder.append(','); + } + builder.append(valueTypes[i]); + } + builder.append('}'); + return builder.toString(); + } + + public static String toStringValueTypes(int data){ + return toString(decodeValueTypes(data)); + } + public static String toString(AttributeDataFormat[] typeValues){ + if(typeValues == null || typeValues.length == 0){ + return null; + } + StringBuilder builder = new StringBuilder(); + + boolean appendOnce = false; + int appendedTypes = 0; + for(AttributeDataFormat typeValue : typeValues){ + if(typeValue == ENUM || typeValue == FLAG){ + continue; + } + if(typeValue == AttributeDataFormat.ANY){ + return AttributeDataFormat.ANY.getName(); + } + int mask = typeValue.getMask(); + if((appendedTypes & mask) == mask){ + continue; + } + if(appendOnce){ + builder.append('|'); + } + builder.append(typeValue.getName()); + appendOnce = true; + appendedTypes = appendedTypes | mask; + } + return builder.toString(); + } + public static int sum(AttributeDataFormat[] typeValues){ + if(typeValues == null){ + return 0; + } + int result = 0; + for(AttributeDataFormat typeValue:typeValues){ + if(typeValue==null){ + continue; + } + result |= typeValue.getMask(); + } + return result; + } + + public static AttributeDataFormat[] decodeValueTypes(int data){ + AttributeDataFormat[] tmp = new AttributeDataFormat[VALUE_TYPES.length]; + int length = 0; + for(AttributeDataFormat typeValue : VALUE_TYPES){ + int mask = typeValue.getMask(); + if(mask == data){ + return new AttributeDataFormat[]{typeValue}; + } + if(typeValue == ANY){ + continue; + } + if((data & mask) == mask){ + tmp[length] = typeValue; + length++; + } + } + if(length == 0){ + return null; + } + AttributeDataFormat[] results = new AttributeDataFormat[length]; + System.arraycopy(tmp, 0, results, 0, length); + return results; + } + public static AttributeDataFormat[] parseValueTypes(String valuesTypes){ + if(valuesTypes == null){ + return null; + } + valuesTypes = valuesTypes.trim(); + String[] valueNames = valuesTypes.split("\\s*\\|\\s*"); + AttributeDataFormat[] tmp = new AttributeDataFormat[VALUE_TYPES.length]; + int length = 0; + for(String name:valueNames){ + AttributeDataFormat typeValue = fromValueTypeName(name); + if(typeValue!=null){ + tmp[length] = typeValue; + length++; + } + } + if(length == 0){ + return null; + } + AttributeDataFormat[] results = new AttributeDataFormat[length]; + System.arraycopy(tmp, 0, results, 0, length); + return results; + } + public static AttributeDataFormat valueOf(int mask){ + for(AttributeDataFormat typeValue : VALUE_TYPES){ + if(typeValue.getMask() == mask){ + return typeValue; + } + } + return null; + } + public static AttributeDataFormat typeOfBag(int data){ + for(AttributeDataFormat typeValue : BAG_TYPES){ + if(typeValue.matches(data)){ + return typeValue; + } + } + return null; + } + public static AttributeDataFormat fromValueTypeName(String name){ + if(name == null){ + return null; + } + name = name.trim().toUpperCase(); + for(AttributeDataFormat typeValue : VALUE_TYPES){ + if(name.equals(typeValue.name())){ + return typeValue; + } + } + return null; + } + public static AttributeDataFormat fromBagTypeName(String bagTypeName){ + if(bagTypeName == null){ + return null; + } + bagTypeName = bagTypeName.trim().toUpperCase(); + for(AttributeDataFormat typeValue: BAG_TYPES){ + if(bagTypeName.equals(typeValue.name())){ + return typeValue; + } + } + return null; + } + + private static final AttributeDataFormat[] VALUE_TYPES = new AttributeDataFormat[]{ + REFERENCE, + STRING, + INTEGER, + BOOL, + COLOR, + FLOAT, + DIMENSION, + FRACTION, + ANY + }; + + private static final AttributeDataFormat[] BAG_TYPES = new AttributeDataFormat[]{ + ENUM, + FLAG + }; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/AttributeType.java b/src/ARSCLib/com/reandroid/arsc/value/AttributeType.java new file mode 100644 index 00000000..6682ab8e --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/AttributeType.java @@ -0,0 +1,80 @@ +/* + * 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.value; + +public enum AttributeType { + + FORMATS(0x01000000), + MIN(0x01000001), + MAX(0x01000002), + L10N(0x01000003), + + OTHER(0x01000004), + ZERO(0x01000005), + ONE(0x01000006), + TWO(0x01000007), + FEW(0x01000008), + MANY(0x01000009); + + private final int id; + AttributeType(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public boolean isPlural(){ + int i = id & 0xffff; + return i>=4 && i<=9; + } + + public String getName(){ + return name().toLowerCase(); + } + @Override + public String toString(){ + return getName(); + } + + public static AttributeType valueOf(int value){ + for(AttributeType type:VALUES){ + if(type.getId() == value){ + return type; + } + } + return null; + } + + public static AttributeType fromName(String name){ + if(name == null){ + return null; + } + name = name.toUpperCase(); + if("FORMAT".equals(name)){ + return FORMATS; + } + for(AttributeType type:VALUES){ + if(name.equals(type.name())){ + return type; + } + } + return null; + } + + private static final AttributeType[] VALUES = values(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/AttributeValue.java b/src/ARSCLib/com/reandroid/arsc/value/AttributeValue.java new file mode 100644 index 00000000..751ef9e8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/AttributeValue.java @@ -0,0 +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. + */ +package com.reandroid.arsc.value; + +public interface AttributeValue extends Value{ + int getNameResourceID(); + void setNameResourceID(int resourceId); + Entry resolveName(); +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/CompoundEntry.java b/src/ARSCLib/com/reandroid/arsc/value/CompoundEntry.java new file mode 100644 index 00000000..7ff54f1b --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/CompoundEntry.java @@ -0,0 +1,114 @@ +/* + * 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.value; + +import com.reandroid.arsc.array.CompoundItemArray; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.json.JSONObject; + +public abstract class CompoundEntry> extends TableEntry { + public CompoundEntry(ARRAY mapArray){ + super(new EntryHeaderMap(), mapArray); + } + // Valid for type attr + public AttributeDataFormat[] getAttributeTypeFormats(){ + ITEM item = getByType(AttributeType.FORMATS); + if(item != null){ + return item.getAttributeTypeFormats(); + } + return null; + } + public boolean containsType(AttributeType attributeType){ + return getValue().containsType(attributeType); + } + public ITEM getByType(AttributeType attributeType){ + return getValue().getByType(attributeType); + } + public void refresh(){ + getHeader().setValuesCount(getValue().childesCount()); + } + public ITEM[] listResValueMap(){ + return getValue().getChildes(); + } + public int getParentId(){ + return getHeader().getParentId(); + } + public void setParentId(int parentId){ + getHeader().setParentId(parentId); + } + public int getValuesCount(){ + return getHeader().getValuesCount(); + } + public void setValuesCount(int valuesCount){ + getHeader().setValuesCount(valuesCount); + getValue().setChildesCount(valuesCount); + } + @Override + void linkTableStringsInternal(TableStringPool tableStringPool){ + for(ITEM item : listResValueMap()){ + item.linkTableStrings(tableStringPool); + } + } + @Override + void onHeaderLoaded(ValueHeader valueHeader){ + getValue().setChildesCount(getValuesCount()); + } + + @Override + void onRemoved(){ + getHeader().onRemoved(); + getValue().onRemoved(); + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + getHeader().toJson(jsonObject); + jsonObject.put(NAME_values, getValue().toJson()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + getHeader().fromJson(json); + getValue().fromJson(json.optJSONArray(NAME_values)); + refresh(); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(getHeader()); + ITEM[] valueMaps = listResValueMap(); + int len = valueMaps.length; + int max = len; + if(max>4){ + max = 4; + } + for(int i=0;i0){ + if(max!=len){ + builder.append("\n ..."); + } + builder.append("\n "); + } + return builder.toString(); + } + + public static final String NAME_values = "values"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/Entry.java b/src/ARSCLib/com/reandroid/arsc/value/Entry.java new file mode 100755 index 00000000..5696e317 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/Entry.java @@ -0,0 +1,477 @@ +/* + * 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.value; + +import com.reandroid.arsc.array.EntryArray; +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.SpecBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.*; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + + +import java.io.IOException; +import java.io.OutputStream; + +public class Entry extends Block implements JSONConvert { + private TableEntry mTableEntry; + + public Entry(){ + super(); + } + + public void linkTableStringsInternal(TableStringPool tableStringPool){ + TableEntry tableEntry = getTableEntry(); + tableEntry.linkTableStringsInternal(tableStringPool); + } + public void linkSpecStringsInternal(SpecStringPool specStringPool){ + TableEntry tableEntry = getTableEntry(); + ValueHeader header = tableEntry.getHeader(); + header.linkSpecStringsInternal(specStringPool); + } + public ResValue getResValue(){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry instanceof ResTableEntry){ + return ((ResTableEntry)tableEntry).getValue(); + } + return null; + } + public ResValueMapArray getResValueMapArray(){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry instanceof ResTableMapEntry){ + return ((ResTableMapEntry)tableEntry).getValue(); + } + return null; + } + public SpecFlag getSpecFlag(){ + SpecBlock specBlock = getSpecBlock(); + if(specBlock == null){ + return null; + } + return specBlock.getSpecFlag(getId()); + } + public void ensureComplex(boolean isComplex){ + ensureTableEntry(isComplex); + } + public int getId(){ + int id = getIndex(); + EntryArray entryArray = getParentInstance(EntryArray.class); + if(entryArray != null){ + id = entryArray.getEntryId(id); + } + return id; + } + public String getName(){ + SpecString specString = getSpecString(); + if(specString!=null){ + return specString.get(); + } + return null; + } + public String getTypeName(){ + TypeBlock typeBlock = getTypeBlock(); + if(typeBlock!=null){ + return typeBlock.getTypeName(); + } + return null; + } + public int getResourceId(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock==null){ + return 0; + } + TypeBlock typeBlock = getTypeBlock(); + if(typeBlock==null){ + return 0; + } + return (packageBlock.getId()<<24) + | (typeBlock.getId() << 16) + | getId(); + } + public int getSpecReference(){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry == null){ + return 0; + } + return tableEntry.getHeader().getKey(); + } + public TypeString getTypeString(){ + TypeBlock typeBlock = getTypeBlock(); + if(typeBlock!=null){ + return typeBlock.getTypeString(); + } + return null; + } + public boolean isDefault(){ + ResConfig resConfig = getResConfig(); + if(resConfig!=null){ + return resConfig.isDefault(); + } + return false; + } + public void setSpecReference(StringItem specReference){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry == null){ + return; + } + tableEntry.getHeader().setKey(specReference); + } + public void setSpecReference(int ref){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry == null){ + return; + } + tableEntry.getHeader().setKey(ref); + } + private Entry searchEntry(int resourceId){ + if(resourceId==getResourceId()){ + return this; + } + PackageBlock packageBlock= getPackageBlock(); + if(packageBlock==null){ + return null; + } + TableBlock tableBlock = packageBlock.getTableBlock(); + if(tableBlock==null){ + return null; + } + EntryGroup entryGroup = tableBlock.search(resourceId); + if(entryGroup!=null){ + return entryGroup.pickOne(); + } + return null; + } + public ResValue setValueAsRaw(ValueType valueType, int data){ + TableEntry tableEntry = ensureTableEntry(false); + ResValue resValue = (ResValue) tableEntry.getValue(); + resValue.setTypeAndData(valueType, data); + return resValue; + } + public ResValue setValueAsBoolean(boolean val){ + int data = val?0xffffffff:0; + return setValueAsRaw(ValueType.INT_BOOLEAN, data); + } + public ResValue setValueAsReference(int resourceId){ + return setValueAsRaw(ValueType.REFERENCE, resourceId); + } + public ResValue setValueAsString(String str){ + TableEntry tableEntry = ensureTableEntry(false); + ResValue resValue = (ResValue) tableEntry.getValue(); + resValue.setValueAsString(str); + return resValue; + } + public SpecString getSpecString(){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry == null){ + return null; + } + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock == null){ + return null; + } + return packageBlock.getSpecStringPool() + .get(tableEntry.getHeader().getKey()); + } + public ResConfig getResConfig(){ + TypeBlock typeBlock = getTypeBlock(); + if(typeBlock!=null){ + return typeBlock.getResConfig(); + } + return null; + } + public SpecBlock getSpecBlock(){ + TypeBlock typeBlock = getTypeBlock(); + if(typeBlock == null){ + return null; + } + SpecTypePair specTypePair = typeBlock.getParentSpecTypePair(); + if(specTypePair==null){ + return null; + } + return specTypePair.getSpecBlock(); + } + public TypeBlock getTypeBlock(){ + return getParent(TypeBlock.class); + } + private String getPackageName(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock!=null){ + return packageBlock.getName(); + } + return null; + } + public PackageBlock getPackageBlock(){ + return getParent(PackageBlock.class); + } + private TableEntry ensureTableEntry(boolean is_complex){ + TableEntry tableEntry = getTableEntry(); + + boolean is_correct_type = (is_complex && tableEntry instanceof ResTableMapEntry) || (!is_complex && tableEntry instanceof ResTableEntry); + if (tableEntry == null || !is_correct_type) { + tableEntry = createTableEntry(is_complex); + setTableEntry(tableEntry); + } + return tableEntry; + } + + public TableEntry getTableEntry(){ + return mTableEntry; + } + public ValueHeader getHeader(){ + TableEntry tableEntry = getTableEntry(); + if(tableEntry!=null){ + return tableEntry.getHeader(); + } + return null; + } + + @Override + public boolean isNull(){ + return getTableEntry()==null; + } + @Override + public void setNull(boolean is_null){ + if(is_null){ + setTableEntry(null); + } + } + @Override + public byte[] getBytes() { + if(isNull()){ + return null; + } + return getTableEntry().getBytes(); + } + @Override + public int countBytes() { + if(isNull()){ + return 0; + } + return getTableEntry().countBytes(); + } + @Override + public void onCountUpTo(BlockCounter counter) { + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + if(isNull()){ + return; + } + counter.addCount(getTableEntry().countBytes()); + } + @Override + protected int onWriteBytes(OutputStream stream) throws IOException { + if(isNull()){ + return 0; + } + return getTableEntry().writeBytes(stream); + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + TableEntry tableEntry = createTableEntry(reader); + setTableEntry(tableEntry); + tableEntry.readBytes(reader); + } + + public boolean isComplex(){ + return getTableEntry() instanceof CompoundEntry; + } + public void setTableEntry(TableEntry tableEntry){ + if(tableEntry==this.mTableEntry){ + return; + } + onTableEntryRemoved(); + if(tableEntry==null){ + return; + } + tableEntry.setIndex(0); + tableEntry.setParent(this); + this.mTableEntry = tableEntry; + onTableEntryAdded(); + } + private void onTableEntryAdded(){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock!=null){ + packageBlock.onEntryAdded(this); + } + } + private void onTableEntryRemoved(){ + TableEntry exist = this.mTableEntry; + if(exist == null){ + return; + } + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock!=null){ + packageBlock.removeEntryGroup(this); + } + exist.onRemoved(); + exist.setIndex(-1); + exist.setParent(null); + this.mTableEntry = null; + } + private TableEntry createTableEntry(BlockReader reader) throws IOException { + int startPosition = reader.getPosition(); + reader.offset(2); + boolean is_complex = (0x0001 & reader.readShort()) == 0x0001; + reader.seek(startPosition); + return createTableEntry(is_complex); + } + private TableEntry createTableEntry(boolean is_complex) { + if(is_complex){ + return new ResTableMapEntry(); + }else { + return new ResTableEntry(); + } + } + + @Override + public JSONObject toJson() { + if(isNull()){ + return null; + } + return getTableEntry().toJson(); + } + @Override + public void fromJson(JSONObject json) { + if(json==null){ + setNull(true); + return; + } + boolean is_complex = json.optBoolean(ValueHeader.NAME_is_complex, false); + TableEntry entry = createTableEntry(is_complex); + setTableEntry(entry); + entry.fromJson(json); + } + + public void merge(Entry entry){ + if(!shouldMerge(entry)){ + return; + } + TableEntry tableEntry = entry.getTableEntry(); + TableEntry existEntry = ensureTableEntry(tableEntry instanceof ResTableMapEntry); + existEntry.merge(tableEntry); + } + private boolean shouldMerge(Entry coming){ + if(coming == null || coming == this || coming.isNull()){ + return false; + } + if(this.isNull()){ + return true; + } + return getTableEntry().shouldMerge(coming.getTableEntry()); + } + + public String buildResourceName(int resourceId, char prefix, boolean includeType){ + if(resourceId==0){ + return null; + } + Entry entry=searchEntry(resourceId); + return buildResourceName(entry, prefix, includeType); + } + public String buildResourceName(Entry entry, char prefix, boolean includeType){ + if(entry==null){ + return null; + } + String pkgName=entry.getPackageName(); + if(getResourceId()==entry.getResourceId()){ + pkgName=null; + }else if(pkgName!=null){ + if(pkgName.equals(this.getPackageName())){ + pkgName=null; + } + } + String type=null; + if(includeType){ + type=entry.getTypeName(); + } + String name=entry.getName(); + return buildResourceName(prefix, pkgName, type, name); + } + public String getResourceName(){ + return buildResourceName('@',null, getTypeName(), getName()); + } + public String getResourceName(char prefix){ + return getResourceName(prefix, false, true); + } + public String getResourceName(char prefix, boolean includePackage, boolean includeType){ + String pkg=includePackage?getPackageName():null; + String type=includeType?getTypeName():null; + return buildResourceName(prefix,pkg, type, getName()); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(HexUtil.toHex8(getResourceId())); + builder.append(' '); + ResConfig resConfig = getResConfig(); + if(resConfig!=null){ + builder.append(resConfig); + builder.append(' '); + } + SpecFlag specFlag = getSpecFlag(); + if(specFlag!=null){ + builder.append(specFlag); + builder.append(' '); + } + if(isNull()){ + builder.append("NULL"); + return builder.toString(); + } + builder.append('@'); + builder.append(getTypeName()); + builder.append('/'); + builder.append(getName()); + return builder.toString(); + } + + public static String buildResourceName(char prefix, String packageName, String type, String name){ + if(name==null){ + return null; + } + StringBuilder builder=new StringBuilder(); + if(prefix!=0){ + builder.append(prefix); + } + if(packageName!=null){ + builder.append(packageName); + builder.append(':'); + } + if(type!=null){ + builder.append(type); + builder.append('/'); + } + builder.append(name); + return builder.toString(); + } + + public static final String NAME_id = "id"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/EntryHeader.java b/src/ARSCLib/com/reandroid/arsc/value/EntryHeader.java new file mode 100644 index 00000000..375232f9 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/EntryHeader.java @@ -0,0 +1,56 @@ + /* + * 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.value; + +public class EntryHeader extends ValueHeader { + public EntryHeader(){ + super(HEADER_SIZE_SCALAR); + } + + @Override + public String toString(){ + if(isNull()){ + return "null"; + } + StringBuilder builder=new StringBuilder(); + int byte_size = getSize(); + int read_size = readSize(); + if(byte_size!=8){ + builder.append("size=").append(byte_size); + } + if(byte_size!=read_size){ + builder.append(" readSize=").append(read_size); + } + if(isComplex()){ + builder.append(" complex"); + } + if(isPublic()){ + builder.append(" public"); + } + if(isWeak()){ + builder.append(" weak"); + } + String name = getName(); + if(name!=null){ + builder.append(" name=").append(name); + }else { + builder.append(" key=").append(getKey()); + } + return builder.toString(); + } + + private static final short HEADER_SIZE_SCALAR = 8; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/EntryHeaderMap.java b/src/ARSCLib/com/reandroid/arsc/value/EntryHeaderMap.java new file mode 100644 index 00000000..0714e373 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/EntryHeaderMap.java @@ -0,0 +1,108 @@ +/* + * 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.value; + +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONObject; + +public class EntryHeaderMap extends ValueHeader { + public EntryHeaderMap(){ + super(HEADER_SIZE_COMPLEX); + setComplex(true); + } + public int getParentId(){ + return getInteger(getBytesInternal(), OFFSET_PARENT_ID); + } + public void setParentId(int parentId){ + putInteger(getBytesInternal(), OFFSET_PARENT_ID, parentId); + } + public int getValuesCount(){ + return getInteger(getBytesInternal(), OFFSET_VALUE_COUNT); + } + public void setValuesCount(int valuesCount){ + putInteger(getBytesInternal(), OFFSET_VALUE_COUNT, valuesCount); + } + + @Override + public void merge(ValueHeader valueHeader){ + if(valueHeader == this || !(valueHeader instanceof EntryHeaderMap)){ + return; + } + super.merge(valueHeader); + EntryHeaderMap entryHeaderMap = (EntryHeaderMap) valueHeader; + setParentId(entryHeaderMap.getParentId()); + setValuesCount(entryHeaderMap.getValuesCount()); + } + @Override + public void toJson(JSONObject jsonObject) { + super.toJson(jsonObject); + jsonObject.put(NAME_is_complex, true); + int parent_id = getParentId(); + if(parent_id!=0){ + jsonObject.put(NAME_parent_id, parent_id); + } + } + @Override + public void fromJson(JSONObject json) { + super.fromJson(json); + setComplex(json.optBoolean(NAME_is_complex, true)); + setParentId(json.optInt(NAME_parent_id)); + } + @Override + public String toString(){ + if(isNull()){ + return "null"; + } + StringBuilder builder=new StringBuilder(); + int byte_size = getSize(); + int read_size = readSize(); + if(byte_size!=16){ + builder.append("size=").append(byte_size); + } + if(byte_size!=read_size){ + builder.append(", readSize=").append(read_size); + } + if(isComplex()){ + builder.append(" complex"); + } + if(isPublic()){ + builder.append(", public"); + } + if(isWeak()){ + builder.append(", weak"); + } + String name = getName(); + if(name!=null){ + builder.append(", name=").append(name); + }else { + builder.append(", key=").append(getKey()); + } + int parentId = getParentId(); + if(parentId!=0){ + builder.append(", parentId="); + builder.append(HexUtil.toHex8(getParentId())); + } + builder.append(", count=").append(getValuesCount()); + return builder.toString(); + } + + private static final short HEADER_SIZE_COMPLEX = 16; + + private static final int OFFSET_PARENT_ID = 8; + private static final int OFFSET_VALUE_COUNT = 12; + + public static final String NAME_parent_id = "parent_id"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/LibraryInfo.java b/src/ARSCLib/com/reandroid/arsc/value/LibraryInfo.java new file mode 100755 index 00000000..92bacd04 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/LibraryInfo.java @@ -0,0 +1,131 @@ +/* + * 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.value; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.FixedLengthString; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; + +public class LibraryInfo extends Block implements JSONConvert { + private final IntegerItem mPackageId; + private final FixedLengthString mPackageName; + + public LibraryInfo(){ + super(); + this.mPackageId=new IntegerItem(); + this.mPackageName = new FixedLengthString(256); + mPackageId.setIndex(0); + mPackageId.setParent(this); + mPackageName.setIndex(1); + mPackageName.setParent(this); + } + + public int getPackageId(){ + return mPackageId.get(); + } + public void setPackageId(int id){ + mPackageId.set(id); + } + public String getPackageName(){ + return mPackageName.get(); + } + public void setPackageName(String packageName){ + mPackageName.set(packageName); + } + + @Override + public byte[] getBytes() { + if(isNull()){ + return null; + } + return addBytes(mPackageId.getBytes(), mPackageName.getBytes()); + } + @Override + public int countBytes() { + if(isNull()){ + return 0; + } + return mPackageId.countBytes()+mPackageName.countBytes(); + } + @Override + public void onCountUpTo(BlockCounter counter) { + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + mPackageId.onCountUpTo(counter); + mPackageName.onCountUpTo(counter); + } + @Override + protected int onWriteBytes(OutputStream stream) throws IOException { + int result=mPackageId.writeBytes(stream); + result+=mPackageName.writeBytes(stream); + return result; + } + @Override + public void onReadBytes(BlockReader reader) throws IOException{ + mPackageId.readBytes(reader); + mPackageName.readBytes(reader); + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put("id", getPackageId()); + jsonObject.put("name", getPackageName()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setPackageId(json.getInt("id")); + setPackageName(json.getString("name")); + } + public void merge(LibraryInfo info){ + if(info==null||info==this){ + return; + } + if(getPackageId()!=info.getPackageId()){ + throw new IllegalArgumentException("Can not add different id libraries: " + +getPackageId()+"!="+info.getPackageId()); + } + setPackageName(info.getPackageName()); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append("LIBRARY{"); + builder.append(HexUtil.toHex2((byte) getPackageId())); + builder.append(':'); + String name=getPackageName(); + if(name==null){ + name="NULL"; + } + builder.append(name); + builder.append('}'); + return builder.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ResConfig.java b/src/ARSCLib/com/reandroid/arsc/value/ResConfig.java new file mode 100755 index 00000000..41da0746 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ResConfig.java @@ -0,0 +1,2322 @@ +/* + * 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.value; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.container.FixedBlockContainer; +import com.reandroid.arsc.io.BlockLoad; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.ByteArray; +import com.reandroid.arsc.item.IntegerItem; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ResConfig extends FixedBlockContainer + implements BlockLoad, JSONConvert, Comparable { + + private final IntegerItem configSize; + private final ByteArray mValuesContainer; + + private String mQualifiers; + private int mQualifiersStamp; + + public ResConfig(){ + super(2); + this.configSize = new IntegerItem(SIZE_64); + this.mValuesContainer = new ByteArray(SIZE_64 - 4); + addChild(0, configSize); + addChild(1, mValuesContainer); + this.configSize.setBlockLoad(this); + this.mQualifiersStamp = 0; + } + public boolean isEqualOrMoreSpecificThan(ResConfig resConfig){ + if(resConfig == null){ + return false; + } + if(resConfig == this || resConfig.isDefault()){ + return true; + } + byte[] bytes = ByteArray.trimTrailZeros(this.mValuesContainer.getBytes()); + byte[] otherBytes = ByteArray.trimTrailZeros(resConfig.mValuesContainer.getBytes()); + int max = otherBytes.length; + if(max > bytes.length){ + return false; + } + for(int i = 0; i> 5) + ((in0 & 0x03) << 3)); + byte third = (byte) ((in0 & 0x7c) >> 2); + + out[0] = (char) (first + base); + out[1] = (char) (second + base); + out[2] = (char) (third + base); + }else if (in0 != 0) { + out = new char[2]; + out[0] = (char) in0; + out[1] = (char) in1; + }else { + out = new char[2]; + } + return out; + } + private static byte[] packLanguage(char[] language) { + return packLanguageOrRegion(language, 'a'); + } + private static byte[] packRegion(char[] region) { + return packLanguageOrRegion(region, '0'); + } + private static byte[] packLanguageOrRegion(char[] in, char base) { + byte[] out = new byte[2]; + if(in == null || in.length<2){ + return out; + } + if (in.length == 2 || in[2] == 0 || in[2] == '-') { + out[0] = (byte) in[0]; + out[1] = (byte) in[1]; + } else { + byte first = (byte) ((in[0] - base) & 0x007f); + byte second = (byte) ((in[1] - base) & 0x007f); + byte third = (byte) ((in[2] - base) & 0x007f); + + out[0] = (byte) (0x80 | (third << 2) | (second >> 3)); + out[1] = (byte) ((second << 5) | first); + } + return out; + } + + private static byte[] toByteArray(char[] chs, int len){ + byte[] bts = new byte[len]; + if(chs == null){ + return bts; + } + int sz = chs.length; + for(int i = 0; i < sz; i++){ + bts[i]= (byte) chs[i]; + } + return bts; + } + private static char[] toCharArray(byte[] bts){ + if(isNull(bts)){ + return null; + } + int sz = bts.length; + char[] chs = new char[sz]; + for(int i = 0; i < sz; i++){ + int val = 0xff & bts[i]; + chs[i]= (char) val; + } + return chs; + } + private static char[] trimEndingZero(char[] chars){ + if(chars == null){ + return null; + } + int lastNonZero = -1; + for(int i = 0; i < chars.length; i++){ + if(chars[i]!= 0){ + lastNonZero = i; + } + } + if(lastNonZero==-1){ + return null; + } + lastNonZero = lastNonZero+1; + if(lastNonZero== chars.length){ + return chars; + } + char[] result = new char[lastNonZero]; + System.arraycopy(chars, 0, result, 0, lastNonZero); + return result; + } + private static boolean isNull(char[] chs){ + if(chs == null){ + return true; + } + for(int i = 0; i < chs.length; i++){ + if(chs[i]!= 0){ + return false; + } + } + return true; + } + private static boolean isNull(byte[] bts){ + if(bts == null){ + return true; + } + for(int i = 0; i < bts.length; i++){ + if(bts[i] != 0){ + return false; + } + } + return true; + } + private static byte[] ensureArrayLength(byte[] bts, int length){ + if(bts == null || length == 0){ + return new byte[length]; + } + if(bts.length == length){ + return bts; + } + byte[] result = new byte[length]; + int max = result.length; + if(bts.length < max){ + max = bts.length; + } + System.arraycopy(bts, 0, result, 0, max); + return result; + } + private static String ensureLength(String str, int min, char postfix){ + int length = str.length(); + if(length >= min){ + return str; + } + StringBuilder builder = new StringBuilder(); + builder.append(str); + int remain = min - length; + for(int i = 0; i < remain; i++){ + builder.append(postfix); + } + return builder.toString(); + } + private static String trimPostfix(String str, char postfix){ + if(str == null){ + return null; + } + int length = str.length(); + int index = length-1; + while (length>0 && str.charAt(index) == postfix){ + str = str.substring(0, index); + length = str.length(); + index = length - 1; + } + return str; + } + public static boolean isValidSize(int size){ + switch (size){ + case SIZE_16: + case SIZE_28: + case SIZE_32: + case SIZE_36: + case SIZE_48: + case SIZE_52: + case SIZE_56: + case SIZE_64: + return true; + default: + return size > SIZE_64; + } + } + + public static final class Orientation extends Flag{ + public static final int MASK = 0x0f; + + public static final Orientation PORT = new Orientation("port", 0x01); + public static final Orientation LAND = new Orientation("land", 0x02); + public static final Orientation SQUARE = new Orientation("square", 0x03); + + public static final Orientation[] VALUES = new Orientation[]{ + PORT, + LAND, + SQUARE + }; + private Orientation(String name, int flag) { + super(name, flag); + } + public static Orientation valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static Orientation valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static Orientation fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static Orientation fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(Orientation flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class Touchscreen extends Flag{ + public static final int MASK = 0x0f; + + public static final Touchscreen NOTOUCH = new Touchscreen("notouch", 0x01); + public static final Touchscreen STYLUS = new Touchscreen("stylus", 0x02); + public static final Touchscreen FINGER = new Touchscreen("finger", 0x03); + + public static final Touchscreen[] VALUES = new Touchscreen[]{ + NOTOUCH, + STYLUS, + FINGER + }; + private Touchscreen(String name, int flag) { + super(name, flag); + } + public static Touchscreen valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static Touchscreen valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static Touchscreen fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static Touchscreen fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(Touchscreen flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class Density extends Flag{ + public static final int MASK = 0xffff; + + public static final Density LDPI = new Density("ldpi", 120); + public static final Density MDPI = new Density("mdpi", 160); + public static final Density TVDPI = new Density("tvdpi", 213); + public static final Density HDPI = new Density("hdpi", 240); + public static final Density XHDPI = new Density("xhdpi", 320); + public static final Density XXHDPI = new Density("xxhdpi", 480); + public static final Density XXXHDPI = new Density("xxxhdpi", 640); + public static final Density ANYDPI = new Density("anydpi", 0xfffe); + public static final Density NODPI = new Density("nodpi", 0xffff); + + public static final Density[] VALUES = new Density[]{LDPI, + MDPI, + TVDPI, + HDPI, + XHDPI, + XXHDPI, + XXXHDPI, + ANYDPI, + NODPI + }; + private Density(String name, int flag) { + super(name, flag); + } + public static Density valueOf(int flag){ + if(flag== 0){ + return null; + } + Density density = Flag.valueOf(VALUES, MASK, flag); + if(density == null){ + flag = flag & MASK; + density = new Density(flag+"dpi", flag); + } + return density; + } + public static Density valueOf(String name){ + if(name == null || name.length() < 4){ + return null; + } + name = name.toLowerCase(); + if(name.charAt(0) == '-'){ + name = name.substring(1); + } + Density density = Flag.valueOf(VALUES, name); + if(density == null && name.endsWith("dpi")){ + name = name.substring(0, name.length()-3); + try{ + int flag = Integer.parseInt(name); + density = new Density(flag+"dpi", flag); + }catch (NumberFormatException ignored){ + } + } + return density; + } + public static Density fromQualifiers(String qualifiers){ + return fromQualifiers(qualifiers.split("\\s*-\\s*")); + } + public static Density fromQualifiers(String[] qualifiers){ + if(qualifiers == null){ + return null; + } + for(int i = 0; i < qualifiers.length; i++){ + Density density = valueOf(qualifiers[i]); + if(density== null){ + continue; + } + qualifiers[i] = null; + return density; + } + return null; + } + public static int update(Density flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class Keyboard extends Flag{ + public static final int MASK = 0x0f; + + public static final Keyboard NOKEYS = new Keyboard("nokeys", 0x01); + public static final Keyboard QWERTY = new Keyboard("qwerty", 0x02); + public static final Keyboard KEY12 = new Keyboard("12key", 0x03); + + public static final Keyboard[] VALUES = new Keyboard[]{ + NOKEYS, + QWERTY, + KEY12 + }; + private Keyboard(String name, int flag) { + super(name, flag); + } + public static Keyboard valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static Keyboard valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static Keyboard fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static Keyboard fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(Keyboard flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class Navigation extends Flag{ + public static final int MASK = 0x0f; + + public static final Navigation NONAV = new Navigation("nonav", 0x01); + public static final Navigation DPAD = new Navigation("dpad", 0x02); + public static final Navigation TRACKBALL = new Navigation("trackball", 0x03); + public static final Navigation WHEEL = new Navigation("wheel", 0x04); + public static final Navigation[] VALUES = new Navigation[]{ + NONAV, + DPAD, + TRACKBALL, + WHEEL + }; + private Navigation(String name, int flag) { + super(name, flag); + } + public static Navigation valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static Navigation valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static Navigation fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static Navigation fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(Navigation flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class InputFlagsKeysHidden extends Flag{ + public static final int MASK = 0x03; + + public static final InputFlagsKeysHidden KEYSEXPOSED = new InputFlagsKeysHidden("keysexposed", 0x01); + public static final InputFlagsKeysHidden KEYSHIDDEN = new InputFlagsKeysHidden("keyshidden", 0x02); + public static final InputFlagsKeysHidden KEYSSOFT = new InputFlagsKeysHidden("keyssoft", 0x03); + public static final InputFlagsKeysHidden[] VALUES = new InputFlagsKeysHidden[]{ + KEYSEXPOSED, + KEYSHIDDEN, + KEYSSOFT + }; + private InputFlagsKeysHidden(String name, int flag) { + super(name, flag); + } + public static InputFlagsKeysHidden valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static InputFlagsKeysHidden valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static InputFlagsKeysHidden fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static InputFlagsKeysHidden fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(InputFlagsKeysHidden flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class InputFlagsNavHidden extends Flag{ + public static final int MASK = 0x0C; + + public static final InputFlagsNavHidden NAVEXPOSED = new InputFlagsNavHidden("navexposed", 0x04); + public static final InputFlagsNavHidden NAVHIDDEN = new InputFlagsNavHidden("navhidden", 0x08); + public static final InputFlagsNavHidden[] VALUES = new InputFlagsNavHidden[]{ + NAVEXPOSED, + NAVHIDDEN + }; + private InputFlagsNavHidden(String name, int flag) { + super(name, flag); + } + public static InputFlagsNavHidden valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static InputFlagsNavHidden valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static InputFlagsNavHidden fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static InputFlagsNavHidden fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(InputFlagsNavHidden flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class UiModeType extends Flag{ + public static final int MASK = 0x0f; + + public static final UiModeType NORMAL = new UiModeType("normal", 0x01); + public static final UiModeType DESK = new UiModeType("desk", 0x02); + public static final UiModeType CAR = new UiModeType("car", 0x03); + public static final UiModeType TELEVISION = new UiModeType("television", 0x04); + public static final UiModeType APPLIANCE = new UiModeType("appliance", 0x05); + public static final UiModeType WATCH = new UiModeType("watch", 0x06); + public static final UiModeType VRHEADSET = new UiModeType("vrheadset", 0x07); + public static final UiModeType GODZILLAUI = new UiModeType("godzillaui", 0x0b); + public static final UiModeType SMALLUI = new UiModeType("smallui", 0x0c); + public static final UiModeType MEDIUMUI = new UiModeType("mediumui", 0x0d); + public static final UiModeType LARGEUI = new UiModeType("largeui", 0x0e); + public static final UiModeType HUGEUI = new UiModeType("hugeui", 0x0f); + + private static final UiModeType[] VALUES = new UiModeType[]{ + NORMAL, + DESK, + CAR, + TELEVISION, + APPLIANCE, + WATCH, + VRHEADSET, + GODZILLAUI, + SMALLUI, + MEDIUMUI, + LARGEUI, + HUGEUI + }; + + private UiModeType(String name, int flag) { + super(name, flag); + } + public static UiModeType valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static UiModeType valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static UiModeType fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static UiModeType fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(UiModeType flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class UiModeNight extends Flag{ + public static final int MASK = 0x30; + public static final UiModeNight NOTNIGHT = new UiModeNight("notnight",0x10); + public static final UiModeNight NIGHT = new UiModeNight("night",0x20); + private static final UiModeNight[] VALUES = new UiModeNight[]{ + NOTNIGHT, + NIGHT + }; + private UiModeNight(String name, int flag) { + super(name, flag); + } + public static UiModeNight valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static UiModeNight valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static UiModeNight fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static UiModeNight fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(UiModeNight flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ScreenLayoutSize extends Flag{ + public static final int MASK = 0x0f; + + public static final ScreenLayoutSize SMALL = new ScreenLayoutSize("small", 0x01); + public static final ScreenLayoutSize NORMAL = new ScreenLayoutSize("normal", 0x02); + public static final ScreenLayoutSize LARGE = new ScreenLayoutSize("large", 0x03); + public static final ScreenLayoutSize XLARGE = new ScreenLayoutSize("xlarge", 0x04); + public static final ScreenLayoutSize[] VALUES = new ScreenLayoutSize[]{ + SMALL, + NORMAL, + LARGE, + XLARGE + }; + private ScreenLayoutSize(String name, int flag) { + super(name, flag); + } + public static ScreenLayoutSize valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ScreenLayoutSize valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ScreenLayoutSize fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ScreenLayoutSize fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ScreenLayoutSize flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ScreenLayoutLong extends Flag{ + public static final int MASK = 0x30; + public static final ScreenLayoutLong NOTLONG = new ScreenLayoutLong("notlong", 0x10); + public static final ScreenLayoutLong LONG = new ScreenLayoutLong("long", 0x20); + public static final ScreenLayoutLong[] VALUES = new ScreenLayoutLong[]{ + NOTLONG, + LONG + }; + private ScreenLayoutLong(String name, int flag) { + super(name, flag); + } + public static ScreenLayoutLong valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ScreenLayoutLong valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ScreenLayoutLong fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ScreenLayoutLong fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ScreenLayoutLong flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ScreenLayoutDir extends Flag{ + public static final int MASK = 0xC0; + public static final ScreenLayoutDir LDLTR = new ScreenLayoutDir("ldltr", 0x40); + public static final ScreenLayoutDir LDRTL = new ScreenLayoutDir("ldrtl", 0x80); + public static final ScreenLayoutDir[] VALUES = new ScreenLayoutDir[]{ + LDLTR, + LDRTL + }; + private ScreenLayoutDir(String name, int flag) { + super(name, flag); + } + public static ScreenLayoutDir valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ScreenLayoutDir valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ScreenLayoutDir fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ScreenLayoutDir fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ScreenLayoutDir flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ScreenLayoutRound extends Flag{ + public static final int MASK = 0x03; + public static final ScreenLayoutRound NOTROUND = new ScreenLayoutRound("notround", 0x01); + public static final ScreenLayoutRound ROUND = new ScreenLayoutRound("round", 0x02); + public static final ScreenLayoutRound[] VALUES = new ScreenLayoutRound[]{ + NOTROUND, + ROUND + }; + private ScreenLayoutRound(String name, int flag) { + super(name, flag); + } + public static ScreenLayoutRound valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ScreenLayoutRound valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ScreenLayoutRound fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ScreenLayoutRound fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ScreenLayoutRound flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ColorModeWide extends Flag{ + public static final int MASK = 0x03; + public static final ColorModeWide NOWIDECG = new ColorModeWide("nowidecg", 0x01); + public static final ColorModeWide WIDECG = new ColorModeWide("widecg", 0x02); + public static final ColorModeWide[] VALUES = new ColorModeWide[]{ + NOWIDECG, + WIDECG + }; + private ColorModeWide(String name, int flag) { + super(name, flag); + } + public static ColorModeWide valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ColorModeWide valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ColorModeWide fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ColorModeWide fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ColorModeWide flag, int value){ + return Flag.update(MASK, flag, value); + } + } + public static final class ColorModeHdr extends Flag{ + public static final int MASK = 0x0C; + public static final ColorModeHdr LOWDR = new ColorModeHdr("lowdr", 0x04); + public static final ColorModeHdr HIGHDR = new ColorModeHdr("highdr", 0x08); + public static final ColorModeHdr[] VALUES = new ColorModeHdr[]{ + LOWDR, + HIGHDR + }; + private ColorModeHdr(String name, int flag) { + super(name, flag); + } + public static ColorModeHdr valueOf(int flag){ + return Flag.valueOf(VALUES, MASK, flag); + } + public static ColorModeHdr valueOf(String name){ + return Flag.valueOf(VALUES, name); + } + public static ColorModeHdr fromQualifiers(String qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static ColorModeHdr fromQualifiers(String[] qualifiers){ + return Flag.fromQualifiers(VALUES, qualifiers); + } + public static int update(ColorModeHdr flag, int value){ + return Flag.update(MASK, flag, value); + } + } + + static class Flag{ + private final String name; + private final int flag; + Flag(String name, int flag){ + this.name = name; + this.flag = flag; + } + public int getFlag() { + return flag; + } + @Override + public boolean equals(Object obj) { + return obj == this; + } + @Override + public int hashCode() { + return super.hashCode(); + } + @Override + public String toString() { + return name; + } + public static String toString(Flag flag){ + if(flag!= null){ + return flag.toString(); + } + return null; + } + static T fromQualifiers(T[] values, String qualifiers){ + if(qualifiers == null){ + return null; + } + return fromQualifiers(values, qualifiers.split("\\s*-\\s*")); + } + static T fromQualifiers(T[] values, String[] qualifiers){ + if(qualifiers == null){ + return null; + } + for(int i = 0; i < qualifiers.length; i++){ + T flag = Flag.valueOf(values, qualifiers[i]); + if(flag != null){ + qualifiers[i] = null; + return flag; + } + } + return null; + } + static T valueOf(T[] values, int mask, int flagValue){ + flagValue = flagValue & mask; + for(T flag:values){ + if(flagValue == flag.getFlag()){ + return flag; + } + } + return null; + } + static T valueOf(T[] values, String name){ + if(name == null || name.length() == 0){ + return null; + } + if(name.charAt(0) == '-'){ + name = name.substring(1); + } + name = name.toLowerCase(); + for(T flag:values){ + if(name.equals(flag.toString())){ + return flag; + } + } + return null; + } + public static int update(int mask, Flag flag, int value){ + int flip = (~mask) & 0xff; + value = value & flip; + if(flag != null){ + value = value | flag.getFlag(); + } + return value; + } + } + + static class QualifierBuilder{ + private final ResConfig mConfig; + private StringBuilder mBuilder; + public QualifierBuilder(ResConfig resConfig){ + this.mConfig = resConfig; + } + public String build(){ + ResConfig resConfig = this.mConfig; + if(resConfig.isDefault()){ + return ""; + } + this.mBuilder = new StringBuilder(); + appendPrefixedNumber("mcc", resConfig.getMcc()); + appendPrefixedNumber("mnc", resConfig.getMnc()); + + appendLanguageAndRegion(); + + appendFlag(resConfig.getOrientation()); + appendFlag(resConfig.getTouchscreen()); + appendFlag(resConfig.getDensity()); + appendFlag(resConfig.getKeyboard()); + appendFlag(resConfig.getNavigation()); + appendFlag(resConfig.getInputFlagsKeysHidden()); + appendFlag(resConfig.getInputFlagsNavHidden()); + + appendScreenWidthHeight(); + + appendPrefixedNumber("v", resConfig.getSdkVersion()); + // append resConfig.getMinorVersion() + appendFlag(resConfig.getScreenLayoutSize()); + appendFlag(resConfig.getScreenLayoutLong()); + appendFlag(resConfig.getScreenLayoutDir()); + + appendFlag(resConfig.getUiModeType()); + appendFlag(resConfig.getUiModeNight()); + + appendDp("sw", resConfig.getSmallestScreenWidthDp()); + appendDp("w", resConfig.getScreenWidthDp()); + appendDp("h", resConfig.getScreenHeightDp()); + + appendFlag(resConfig.getScreenLayoutRound()); + + appendFlag(resConfig.getColorModeWide()); + appendFlag(resConfig.getColorModeHdr()); + + appendLocaleNumberingSystem(); + + return mBuilder.toString(); + } + private void appendScreenWidthHeight(){ + ResConfig resConfig = this.mConfig; + int width = resConfig.getScreenWidth(); + int height = resConfig.getScreenHeight(); + if(width == 0 && height == 0){ + return; + } + mBuilder.append('-').append(width).append('x').append(height); + } + private void appendLanguageAndRegion(){ + ResConfig resConfig = this.mConfig; + String language = resConfig.getLanguage(); + String region = resConfig.getRegion(); + String script = resConfig.getLocaleScript(); + String variant = resConfig.getLocaleVariant(); + if(language == null && region == null){ + return; + } + StringBuilder builder = this.mBuilder; + char separator; + if(script != null || variant != null){ + builder.append('-'); + builder.append('b'); + separator = '+'; + }else { + separator = '-'; + } + if(language!= null){ + builder.append(separator); + builder.append(language); + } + if(region!= null){ + builder.append(separator); + if(region.length() == 2){ + builder.append('r'); + } + builder.append(region); + } + if(script!= null){ + builder.append(separator); + builder.append(script); + } + if(variant!= null){ + builder.append(separator); + builder.append(variant); + } + } + private void appendLocaleNumberingSystem(){ + String numberingSystem = mConfig.getLocaleNumberingSystem(); + if(numberingSystem== null){ + return; + } + StringBuilder builder = mBuilder; + builder.append("-u+nu+"); + builder.append(numberingSystem); + } + private void appendFlag(ResConfig.Flag flag){ + if(flag== null){ + return; + } + mBuilder.append('-').append(flag.toString()); + } + private void appendDp(String prefix, int number){ + if(number == 0){ + return; + } + StringBuilder builder = this.mBuilder; + builder.append('-'); + if(prefix!= null){ + builder.append(prefix); + } + builder.append(number); + builder.append("dp"); + } + private void appendPrefixedNumber(String prefix, int number){ + if(number == 0){ + return; + } + StringBuilder builder = this.mBuilder; + builder.append('-'); + builder.append(prefix); + builder.append(number); + } + } + static class QualifierParser{ + private final ResConfig mConfig; + private final String[] mQualifiers; + private final int mPreferredSize; + private boolean mEmpty; + private boolean mLanguageRegionParsed; + private boolean mParseComplete; + + public QualifierParser(ResConfig resConfig, String[] qualifiers){ + this.mConfig = resConfig; + this.mQualifiers = qualifiers; + this.mPreferredSize = resConfig.getConfigSize(); + } + public QualifierParser(ResConfig resConfig, String qualifiers){ + this(resConfig, splitQualifiers(qualifiers)); + } + + public void parse(){ + if(this.mParseComplete){ + return; + } + if(isEmpty()){ + onParseComplete(); + return; + } + ResConfig resConfig = this.mConfig; + resConfig.setConfigSize(ResConfig.SIZE_64); + parsePrefixedNumber(); + parseDp(); + parseWidthHeight(); + parseLocaleNumberingSystem(); + if(isEmpty()){ + onParseComplete(); + return; + } + String[] qualifiers = this.mQualifiers; + resConfig.setOrientation(ResConfig.Orientation.fromQualifiers(qualifiers)); + resConfig.setTouchscreen(ResConfig.Touchscreen.fromQualifiers(qualifiers)); + resConfig.setDensity(ResConfig.Density.fromQualifiers(qualifiers)); + resConfig.setKeyboard(ResConfig.Keyboard.fromQualifiers(qualifiers)); + resConfig.setNavigation(ResConfig.Navigation.fromQualifiers(qualifiers)); + if(isEmpty()){ + onParseComplete(); + return; + } + resConfig.setInputFlagsKeysHidden(ResConfig.InputFlagsKeysHidden.fromQualifiers(qualifiers)); + resConfig.setInputFlagsNavHidden(ResConfig.InputFlagsNavHidden.fromQualifiers(qualifiers)); + resConfig.setScreenLayoutSize(ResConfig.ScreenLayoutSize.fromQualifiers(qualifiers)); + resConfig.setScreenLayoutLong(ResConfig.ScreenLayoutLong.fromQualifiers(qualifiers)); + resConfig.setScreenLayoutDir(ResConfig.ScreenLayoutDir.fromQualifiers(qualifiers)); + if(isEmpty()){ + onParseComplete(); + return; + } + resConfig.setUiModeType(ResConfig.UiModeType.fromQualifiers(qualifiers)); + resConfig.setUiModeNight(ResConfig.UiModeNight.fromQualifiers(qualifiers)); + + resConfig.setScreenLayoutRound(ResConfig.ScreenLayoutRound.fromQualifiers(qualifiers)); + + resConfig.setColorModeWide(ResConfig.ColorModeWide.fromQualifiers(qualifiers)); + resConfig.setColorModeHdr(ResConfig.ColorModeHdr.fromQualifiers(qualifiers)); + if(isEmpty()){ + onParseComplete(); + return; + } + parseLocaleScriptVariant(); + parseLanguage(); + parseRegion(); + onParseComplete(); + } + public String[] getErrors(){ + if(!this.mParseComplete){ + return null; + } + String[] qualifiers = this.mQualifiers; + if(qualifiers == null || qualifiers.length == 0){ + return null; + } + int length = qualifiers.length; + String[] tmp = new String[length]; + int count = 0; + for(int i = 0; i < length; i++){ + String qualifier = qualifiers[i]; + if(qualifier == null || qualifier.length() == 0){ + continue; + } + tmp[count] = qualifier; + count++; + } + if(count == 0){ + return null; + } + if(count == length){ + return tmp; + } + String[] errors = new String[count]; + System.arraycopy(tmp, 0, errors, 0, count); + return errors; + } + private void onParseComplete(){ + this.mConfig.trimToSize(this.mPreferredSize); + this.mParseComplete = true; + } + + private void parsePrefixedNumber(){ + if(isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parsePrefixedNumber(qualifiers[i])){ + qualifiers[i] = null; + } + } + } + private boolean parsePrefixedNumber(String qualifier){ + if(qualifier == null){ + return false; + } + Matcher matcher = PATTERN_PREFIX_NUMBER.matcher(qualifier); + if(!matcher.find()){ + return false; + } + String prefix = matcher.group(1); + int value = Integer.parseInt(matcher.group(2)); + ResConfig resConfig = mConfig; + if("mcc".equals(prefix)){ + resConfig.setMcc(value); + }else if("mnc".equals(prefix)) { + resConfig.setMnc(value); + }else if("v".equals(prefix)){ + resConfig.setSdkVersion(value); + }else { + return false; + } + return true; + } + private void parseDp(){ + if(isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseDp(qualifiers[i])){ + qualifiers[i] = null; + } + } + } + private boolean parseDp(String qualifier){ + if(qualifier == null){ + return false; + } + Matcher matcher = PATTERN_DP.matcher(qualifier); + if(!matcher.find()){ + return false; + } + String prefix = matcher.group(1); + int value = Integer.parseInt(matcher.group(2)); + ResConfig resConfig = this.mConfig; + if("sw".equals(prefix)){ + resConfig.setSmallestScreenWidthDp(value); + }else if("w".equals(prefix)) { + resConfig.setScreenWidthDp(value); + }else if("h".equals(prefix)){ + resConfig.setScreenHeightDp(value); + }else { + return false; + } + return true; + } + private void parseWidthHeight(){ + if(isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseWidthHeight(qualifiers[i])){ + qualifiers[i] = null; + return; + } + } + } + private boolean parseWidthHeight(String qualifier){ + if(qualifier == null){ + return false; + } + Matcher matcher = PATTERN_WIDTH_HEIGHT.matcher(qualifier); + if(!matcher.find()){ + return false; + } + int width = Integer.parseInt(matcher.group(1)); + int height = Integer.parseInt(matcher.group(2)); + ResConfig resConfig = this.mConfig; + resConfig.setScreenWidth(width); + resConfig.setScreenHeight(height); + return true; + } + private void parseLocaleNumberingSystem(){ + if(isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseLocaleNumberingSystem(qualifiers[i])){ + qualifiers[i] = null; + return; + } + } + } + private boolean parseLocaleNumberingSystem(String qualifier){ + if(qualifier == null){ + return false; + } + Matcher matcher = PATTERN_LOCALE_NUMBERING_SYSTEM.matcher(qualifier); + if(!matcher.find()){ + return false; + } + this.mConfig.setLocaleNumberingSystem(matcher.group(1)); + return true; + } + private void parseLocaleScriptVariant(){ + if(this.mLanguageRegionParsed || isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseLocaleScriptVariant(qualifiers[i])){ + qualifiers[i] = null; + this.mLanguageRegionParsed = true; + return; + } + } + } + private boolean parseLocaleScriptVariant(String qualifier){ + if(qualifier == null || qualifier.length() < 4 ){ + return false; + } + char[] chars = qualifier.toCharArray(); + if(chars[0] != 'b' || chars[1] != '+'){ + return false; + } + Matcher matcher = PATTERN_LOCALE_SCRIPT_VARIANT.matcher(qualifier); + if(!matcher.find()){ + return false; + } + String language = trimPlus(matcher.group(1)); + String region = trimPlus(matcher.group(2)); + String script = trimPlus(matcher.group(3)); + String variant = trimPlus(matcher.group(4)); + if(script == null && variant == null){ + return false; + } + ResConfig resConfig = this.mConfig; + resConfig.setLanguage(language); + resConfig.setRegion(region); + resConfig.setLocaleScript(script); + resConfig.setLocaleVariant(variant); + return true; + } + + private void parseLanguage(){ + if(mLanguageRegionParsed || isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseLanguage(qualifiers[i])){ + qualifiers[i] = null; + return; + } + } + } + private boolean parseLanguage(String qualifier){ + if(!isLanguage(qualifier)){ + return false; + } + this.mConfig.setLanguage(qualifier); + return true; + } + private void parseRegion(){ + if(mLanguageRegionParsed || isEmpty()){ + return; + } + String[] qualifiers = this.mQualifiers; + for(int i = 0; i < qualifiers.length; i++){ + if(parseRegion(qualifiers[i])){ + qualifiers[i] = null; + return; + } + } + } + private boolean parseRegion(String qualifier){ + if(!isRegion(qualifier)){ + return false; + } + this.mConfig.setRegion(qualifier); + return true; + } + + + private boolean isEmpty(){ + if(!mEmpty){ + mEmpty = isEmpty(mQualifiers); + } + return mEmpty; + } + + private static boolean isEmpty(String[] qualifiers){ + if(qualifiers == null){ + return true; + } + for(int i = 0; i < qualifiers.length; i++){ + String qualifier = qualifiers[i]; + if(qualifier == null){ + continue; + } + if(qualifier.length() == 0){ + qualifiers[i] = null; + continue; + } + return false; + } + return true; + } + private static String trimPlus(String text){ + if(text == null||text.length() == 0){ + return null; + } + if(text.charAt(0) == '+'){ + text = text.substring(1); + } + return text; + } + private static boolean isLanguage(String qualifier){ + if(qualifier == null){ + return false; + } + char[] chars = qualifier.toCharArray(); + int length = chars.length; + if(length != 2 && length !=3 ){ + return false; + } + for(int i = 0; i < length; i++){ + if(!isAtoZLower(chars[i])) { + return false; + } + } + return true; + } + private static boolean isRegion(String qualifier){ + if(qualifier == null || qualifier.length() != 3){ + return false; + } + char[] chars = qualifier.toCharArray(); + boolean checkDigit = false; + for(int i = 0; i < chars.length; i++){ + char ch = chars[i]; + if(i == 0){ + if(ch == 'r'){ + continue; + } + checkDigit = isDigit(ch); + if(checkDigit){ + continue; + } + return false; + } + if(checkDigit){ + if(!isDigit(ch)){ + return false; + } + }else if(!isAtoZUpper(ch)) { + return false; + } + } + return true; + } + private static String[] splitQualifiers(String qualifier){ + if(qualifier == null || qualifier.length() == 0){ + return null; + } + return qualifier.split("-"); + } + private static boolean isDigit(char ch){ + return ch <= '9' && ch >= '0'; + } + private static boolean isAtoZLower(char ch){ + return ch <= 'z' && ch >= 'a'; + } + private static boolean isAtoZUpper(char ch){ + return ch <= 'Z' && ch >= 'A'; + } + + private static final Pattern PATTERN_PREFIX_NUMBER = Pattern.compile("^([mcnv]+)([0-9]+)$"); + private static final Pattern PATTERN_DP = Pattern.compile("^([swh]+)([0-9]+)dp$"); + private static final Pattern PATTERN_WIDTH_HEIGHT = Pattern.compile("^([0-9]+)[xX]([0-9]+)$"); + private static final Pattern PATTERN_LOCALE_NUMBERING_SYSTEM = Pattern.compile("^u\\+nu\\+(.{1,8})$"); + private static final Pattern PATTERN_LOCALE_SCRIPT_VARIANT = Pattern.compile("^b(\\+[a-z]{2})?(\\+r[A-Z]{2})?(\\+[A-Z][a-z]{3})?(\\+[A-Z]{2,8})?$"); + } + + public static final int SIZE_16 = 16; + public static final int SIZE_28 = 28; + public static final int SIZE_32 = 32; + public static final int SIZE_36 = 36; + public static final int SIZE_48 = 48; + public static final int SIZE_52 = 52; + public static final int SIZE_56 = 56; + public static final int SIZE_64 = 64; + + private static final int OFFSET_mcc = 0; + private static final int OFFSET_mnc = 2; + private static final int OFFSET_language = 4; + private static final int OFFSET_region = 6; + private static final int OFFSET_orientation = 8; + private static final int OFFSET_touchscreen = 9; + private static final int OFFSET_density = 10; + //SIZE=16 + private static final int OFFSET_keyboard = 12; + private static final int OFFSET_navigation = 13; + private static final int OFFSET_inputFlags = 14; + private static final int OFFSET_inputPad0 = 15; + private static final int OFFSET_screenWidth = 16; + private static final int OFFSET_screenHeight = 18; + private static final int OFFSET_sdkVersion = 20; + private static final int OFFSET_minorVersion = 22; + //SIZE=28 + private static final int OFFSET_screenLayout = 24; + private static final int OFFSET_uiMode = 25; + private static final int OFFSET_smallestScreenWidthDp = 26; + //SIZE=32 + private static final int OFFSET_screenWidthDp = 28; + private static final int OFFSET_screenHeightDp = 30; + //SIZE=36 + private static final int OFFSET_localeScript = 32; + private static final int OFFSET_localeVariant = 36; + //SIZE=48 + private static final int OFFSET_screenLayout2 = 44; + private static final int OFFSET_colorMode = 45; + private static final int OFFSET_reservedPadding = 46; + //SIZE=52 + private static final int OFFSET_localeNumberingSystem = 48; + //SIZE=60 + + private static final int LEN_localeScript = 4; + private static final int LEN_localeVariant = 8; + private static final int LEN_localeNumberingSystem = 8; + + private static final String NAME_mcc = "mcc"; + private static final String NAME_mnc = "mnc"; + private static final String NAME_language = "language"; + private static final String NAME_region = "region"; + private static final String NAME_orientation = "orientation"; + private static final String NAME_touchscreen = "touchscreen"; + private static final String NAME_density = "density"; + //SIZE=16 + private static final String NAME_keyboard = "keyboard"; + private static final String NAME_navigation = "navigation"; + private static final String NAME_input_flags_keys_hidden = "input_flags_keys_hidden"; + private static final String NAME_input_flags_nav_hidden = "input_flags_nav_hidden"; + private static final String NAME_inputPad0 = "inputPad0"; + private static final String NAME_screenWidth = "screenWidth"; + private static final String NAME_screenHeight = "screenHeight"; + private static final String NAME_sdkVersion = "sdkVersion"; + private static final String NAME_minorVersion = "minorVersion"; + //SIZE=28 + private static final String NAME_screen_layout_size = "screen_layout_size"; + private static final String NAME_screen_layout_long = "screen_layout_long"; + private static final String NAME_screen_layout_dir = "screen_layout_dir"; + private static final String NAME_ui_mode_type = "ui_mode_type"; + private static final String NAME_ui_mode_night = "ui_mode_night"; + private static final String NAME_smallestScreenWidthDp = "smallestScreenWidthDp"; + //SIZE=32 = ""; + private static final String NAME_screenWidthDp = "screenWidthDp"; + private static final String NAME_screenHeightDp = "screenHeightDp"; + //SIZE=36 + private static final String NAME_localeScript = "localeScript"; + private static final String NAME_localeVariant = "localeVariant"; + private static final String NAME_screen_layout_round = "screen_layout_round"; + private static final String NAME_color_mode_wide = "color_mode_wide"; + private static final String NAME_color_mode_hdr = "color_mode_hdr"; + + private static final String NAME_localeNumberingSystem = "localeNumberingSystem"; + + private static final char POSTFIX_locale = '#'; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ResTableEntry.java b/src/ARSCLib/com/reandroid/arsc/value/ResTableEntry.java new file mode 100644 index 00000000..e3d505c2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ResTableEntry.java @@ -0,0 +1,71 @@ +/* + * 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.value; + +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.json.JSONObject; + +public class ResTableEntry extends TableEntry { + public ResTableEntry() { + super(new EntryHeader(), new ResValue()); + } + + @Override + void linkTableStringsInternal(TableStringPool tableStringPool){ + getValue().linkTableStrings(tableStringPool); + } + @Override + void onRemoved(){ + getHeader().onRemoved(); + getValue().onRemoved(); + } + @Override + boolean shouldMerge(TableEntry tableEntry){ + if(tableEntry == this || !(tableEntry instanceof ResTableEntry)){ + return false; + } + ResValue coming = ((ResTableEntry) tableEntry).getValue(); + ValueType valueType = coming.getValueType(); + if(valueType == null || valueType == ValueType.NULL){ + return false; + } + valueType = getValue().getValueType(); + return valueType == null || valueType == ValueType.NULL; + } + @Override + public void merge(TableEntry tableEntry){ + if(tableEntry == this || !(tableEntry instanceof ResTableEntry)){ + return; + } + getHeader().merge(tableEntry.getHeader()); + getValue().merge((ValueItem) tableEntry.getValue()); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + getHeader().toJson(jsonObject); + jsonObject.put(NAME_value, getValue().toJson()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + getHeader().fromJson(json); + JSONObject jsonObject = json.getJSONObject(NAME_value); + getValue().fromJson(jsonObject); + } + + public static final String NAME_value = "value"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ResTableMapEntry.java b/src/ARSCLib/com/reandroid/arsc/value/ResTableMapEntry.java new file mode 100644 index 00000000..ef3969a4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ResTableMapEntry.java @@ -0,0 +1,46 @@ +/* + * 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.value; + +import com.reandroid.arsc.array.ResValueMapArray; + +public class ResTableMapEntry extends CompoundEntry { + public ResTableMapEntry(){ + super(new ResValueMapArray()); + } + @Override + boolean shouldMerge(TableEntry tableEntry){ + if(tableEntry == this || !(tableEntry instanceof ResTableMapEntry)){ + return false; + } + ResValueMapArray coming = ((ResTableMapEntry) tableEntry).getValue(); + if(coming.childesCount() == 0){ + return false; + } + return getValue().childesCount() == 0; + } + + @Override + public void merge(TableEntry tableEntry){ + if(tableEntry==null || tableEntry==this){ + return; + } + ResTableMapEntry coming = (ResTableMapEntry) tableEntry; + getHeader().merge(coming.getHeader()); + getValue().merge(coming.getValue()); + refresh(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ResValue.java b/src/ARSCLib/com/reandroid/arsc/value/ResValue.java new file mode 100755 index 00000000..8b8247ae --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ResValue.java @@ -0,0 +1,39 @@ +/* + * 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.value; + +import com.reandroid.arsc.chunk.PackageBlock; + +public class ResValue extends ValueItem { + public ResValue() { + super(8, OFFSET_SIZE); + } + + public Entry getEntry(){ + return getParent(Entry.class); + } + + @Override + public PackageBlock getParentChunk(){ + Entry entry = getEntry(); + if(entry != null){ + return entry.getPackageBlock(); + } + return null; + } + + private static final int OFFSET_SIZE = 0; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ResValueMap.java b/src/ARSCLib/com/reandroid/arsc/value/ResValueMap.java new file mode 100755 index 00000000..e7014122 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ResValueMap.java @@ -0,0 +1,186 @@ +/* + * 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.value; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONObject; + +public class ResValueMap extends ValueItem implements AttributeValue{ + + public ResValueMap() { + super(12, OFFSET_SIZE); + } + + public String decodeData(){ + String value = decodeDataAsAttrFormats(); + if(value != null){ + return value; + } + ValueType valueType = getValueType(); + if(valueType == ValueType.STRING){ + return getValueAsString(); + } + int data = getData(); + if(AttributeDataFormat.REFERENCE.contains(valueType)){ + Entry entry = resolve(data); + if(entry == null){ + return HexUtil.toHex8("@0x", data); + } + return buildReference(entry, valueType, true); + } + return ValueDecoder.decode(valueType, data); + } + private String decodeDataAsAttrFormats(){ + AttributeType attributeType = getAttributeType(); + if(attributeType != AttributeType.FORMATS){ + return null; + } + return AttributeDataFormat.toString(AttributeDataFormat.decodeValueTypes(getData())); + } + public String decodeName(){ + AttributeType attributeType = getAttributeType(); + if(attributeType != null){ + return attributeType.getName(); + } + Entry entry = resolveName(); + return buildReference(entry, null, false); + } + @Override + public Entry resolveName(){ + return resolve(getNameResourceID()); + } + public AttributeType getAttributeType(){ + return AttributeType.valueOf(getNameResourceID()); + } + public void setAttributeType(AttributeType attributeType){ + setNameResourceID(attributeType.getId()); + } + public AttributeDataFormat[] getAttributeTypeFormats(){ + AttributeType attributeType = getAttributeType(); + if(attributeType != AttributeType.FORMATS){ + return null; + } + return AttributeDataFormat.decodeValueTypes(getData()); + } + public void addAttributeTypeFormats(AttributeDataFormat[] formats){ + if(formats == null){ + return; + } + int data = getData() | AttributeDataFormat.sum(formats); + setData(data); + } + public void addAttributeTypeFormat(AttributeDataFormat format){ + if(format == null){ + return; + } + int data = getData() | format.getMask(); + setData(data); + } + public Entry getEntry(){ + return getParent(Entry.class); + } + @Override + public PackageBlock getParentChunk(){ + Entry entry = getEntry(); + if(entry!=null){ + return entry.getPackageBlock(); + } + return null; + } + + public ResTableMapEntry getParentMapEntry(){ + return getParentInstance(ResTableMapEntry.class); + } + + public int getName(){ + return getInteger(getBytesInternal(), OFFSET_NAME); + } + public void setName(int name){ + putInteger(getBytesInternal(), OFFSET_NAME, name); + } + + @Override + public int getNameResourceID() { + return getName(); + } + @Override + public void setNameResourceID(int resourceId){ + setName(resourceId); + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject = super.toJson(); + if(jsonObject==null){ + return null; + } + jsonObject.put(NAME_name, getName()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + super.fromJson(json); + setName(json.getInt(NAME_name)); + } + + public void setNameHigh(short val){ + int name = getName() & 0xffff; + name = ((val & 0xffff) <<16 ) | name; + setName(name); + } + public void setNameLow(short val){ + int name = getName() & 0xffff0000; + name = (val & 0xffff) | name; + setName(name); + } + public void setDataHigh(short val){ + int data = getData() & 0xffff; + data = ((val & 0xffff) <<16 ) | data; + setData(data); + } + public void setDataLow(short val){ + int data = getData() & 0xffff0000; + data = (val & 0xffff) | data; + setData(data); + } + @Override + public void merge(ValueItem valueItem){ + if(valueItem==this || !(valueItem instanceof ResValueMap)){ + return; + } + ResValueMap resValueMap = (ResValueMap) valueItem; + super.merge(resValueMap); + setName(resValueMap.getName()); + } + @Override + public String toString(){ + String name = decodeName(); + String data = decodeData(); + if(name != null && data != null){ + return name + "=\"" + data + "\""; + } + return "name=" + HexUtil.toHex8(getName()) + +", "+super.toString(); + } + + private static final int OFFSET_NAME = 0; + private static final int OFFSET_SIZE = 4; + + public static final String NAME_name = "name"; + +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/StagedAliasEntry.java b/src/ARSCLib/com/reandroid/arsc/value/StagedAliasEntry.java new file mode 100644 index 00000000..714b473b --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/StagedAliasEntry.java @@ -0,0 +1,68 @@ +/* + * 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.value; + +import com.reandroid.arsc.item.ByteArray; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +public class StagedAliasEntry extends ByteArray implements JSONConvert { + public StagedAliasEntry(){ + super(8); + } + public boolean isEqual(StagedAliasEntry other){ + if(other==null){ + return false; + } + if(other==this){ + return true; + } + return getStagedResId()==other.getStagedResId() + && getFinalizedResId()==other.getFinalizedResId(); + } + public int getStagedResId(){ + return getInteger(0); + } + public void setStagedResId(int id){ + putInteger(0, id); + } + public int getFinalizedResId(){ + return getInteger(4); + } + public void setFinalizedResId(int id){ + putInteger(4, id); + } + @Override + public String toString(){ + return "stagedResId=" + HexUtil.toHex8(getStagedResId()) + +", finalizedResId=" + HexUtil.toHex8(getFinalizedResId()); + } + @Override + public JSONObject toJson() { + JSONObject jsonObject=new JSONObject(); + jsonObject.put(NAME_staged_resource_id, getStagedResId()); + jsonObject.put(NAME_finalized_resource_id, getFinalizedResId()); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setStagedResId(json.getInt(NAME_staged_resource_id)); + setFinalizedResId(json.getInt(NAME_finalized_resource_id)); + } + public static final String NAME_staged_resource_id = "staged_resource_id"; + public static final String NAME_finalized_resource_id = "finalized_resource_id"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/TableEntry.java b/src/ARSCLib/com/reandroid/arsc/value/TableEntry.java new file mode 100644 index 00000000..33e67a21 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/TableEntry.java @@ -0,0 +1,111 @@ +/* + * 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.value; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.base.BlockCounter; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class TableEntry

extends Block implements + JSONConvert { + private final HEADER header; + private final VALUE resValue; + + TableEntry(HEADER header, VALUE resValue){ + super(); + this.header = header; + this.resValue = resValue; + + this.header.setParent(this); + this.header.setIndex(0); + this.resValue.setParent(this); + this.resValue.setIndex(1); + } + public Entry getParentEntry(){ + return getParent(Entry.class); + } + public void refresh(){ + } + public final HEADER getHeader() { + return header; + } + public VALUE getValue(){ + return resValue; + } + + @Override + public byte[] getBytes() { + byte[] results = getHeader().getBytes(); + results = addBytes(results, getValue().getBytes()); + return results; + } + @Override + public int countBytes() { + int result = getHeader().countBytes(); + result += getValue().countBytes(); + return result; + } + @Override + public void onCountUpTo(BlockCounter counter) { + if(counter.FOUND){ + return; + } + if(counter.END==this){ + counter.FOUND=true; + return; + } + getHeader().onCountUpTo(counter); + getValue().onCountUpTo(counter); + } + + @Override + public void onReadBytes(BlockReader reader) throws IOException { + ValueHeader valueHeader = getHeader(); + valueHeader.readBytes(reader); + onHeaderLoaded(valueHeader); + getValue().readBytes(reader); + } + + @Override + protected int onWriteBytes(OutputStream stream) throws IOException { + int result; + result = getHeader().writeBytes(stream); + result += getValue().writeBytes(stream); + return result; + } + + void onHeaderLoaded(ValueHeader valueHeader){ + } + abstract void onRemoved(); + abstract boolean shouldMerge(TableEntry tableEntry); + abstract void linkTableStringsInternal(TableStringPool tableStringPool); + + public abstract void merge(TableEntry tableEntry); + @Override + public abstract JSONObject toJson(); + @Override + public abstract void fromJson(JSONObject json); + @Override + public String toString(){ + return getHeader()+", value={"+getValue()+"}"; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/Value.java b/src/ARSCLib/com/reandroid/arsc/value/Value.java new file mode 100644 index 00000000..33c4b06c --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/Value.java @@ -0,0 +1,35 @@ + /* + * 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.value; + + import com.reandroid.arsc.chunk.MainChunk; + import com.reandroid.arsc.chunk.ParentChunk; + + public interface Value { + void setValueType(ValueType valueType); + ValueType getValueType(); + int getData(); + void setData(int data); + String getValueAsString(); + ParentChunk getParentChunk(); + default MainChunk getMainChunk(){ + ParentChunk parentChunk = getParentChunk(); + if(parentChunk!=null){ + return parentChunk.getMainChunk(); + } + return null; + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/value/ValueHeader.java b/src/ARSCLib/com/reandroid/arsc/value/ValueHeader.java new file mode 100644 index 00000000..6f34ef27 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ValueHeader.java @@ -0,0 +1,274 @@ +/* + * 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.value; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.chunk.ParentChunk; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.*; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.StringPool; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; + +public class ValueHeader extends BlockItem implements JSONConvert { + private ReferenceItem mStringReference; + public ValueHeader(int size){ + super(size); + writeSize(); + putInteger(getBytesInternal(), OFFSET_SPEC_REFERENCE, -1); + } + + void linkSpecStringsInternal(SpecStringPool specStringPool){ + int key = getKey(); + SpecString specString = specStringPool.get(key); + if(specString == null){ + mStringReference = null; + return; + } + if(mStringReference != null){ + specString.removeReference(mStringReference); + } + ReferenceItem stringReference = new ReferenceBlock<>(this, OFFSET_SPEC_REFERENCE); + mStringReference = stringReference; + specString.addReference(stringReference); + } + public void onRemoved(){ + unLinkStringReference(); + } + public String getName(){ + StringItem stringItem = getNameString(); + if(stringItem!=null){ + return stringItem.get(); + } + return null; + } + public boolean isComplex(){ + return getBit(getBytesInternal(), OFFSET_FLAGS,0); + } + public void setComplex(boolean complex){ + putBit(getBytesInternal(), OFFSET_FLAGS, 0, complex); + } + public void setPublic(boolean b){ + putBit(getBytesInternal(), OFFSET_FLAGS,1, b); + } + public boolean isPublic(){ + return getBit(getBytesInternal(), OFFSET_FLAGS,1); + } + public void setWeak(boolean b){ + putBit(getBytesInternal(), OFFSET_FLAGS, 2, b); + } + public boolean isWeak(){ + return getBit(getBytesInternal(), OFFSET_FLAGS,2); + } + + public int getKey(){ + return getInteger(getBytesInternal(), OFFSET_SPEC_REFERENCE); + } + public void setKey(int key){ + if(key == getKey()){ + return; + } + unLinkStringReference(); + putInteger(getBytesInternal(), OFFSET_SPEC_REFERENCE, key); + linkStringReference(); + } + public void setKey(StringItem stringItem){ + if(ignoreUpdateKey(stringItem)){ + return; + } + unLinkStringReference(); + int key = -1; + if(stringItem!=null){ + key=stringItem.getIndex(); + } + putInteger(getBytesInternal(), OFFSET_SPEC_REFERENCE, key); + linkStringReference(stringItem); + } + private boolean ignoreUpdateKey(StringItem stringItem){ + int key = getKey(); + ReferenceItem referenceItem = this.mStringReference; + if(stringItem == null){ + return referenceItem == null && key == -1; + } + if(referenceItem == null || key != stringItem.getIndex()){ + return false; + } + return getSpecString(key) == stringItem; + } + public void setSize(int size){ + super.setBytesLength(size, false); + writeSize(); + } + public int getSize(){ + return getBytesInternal().length; + } + int readSize(){ + if(getSize()<2){ + return 0; + } + return 0xffff & getShort(getBytesInternal(), OFFSET_SIZE); + } + private void writeSize(){ + int size = getSize(); + if(size>1){ + putShort(getBytesInternal(), OFFSET_SIZE, (short) size); + } + } + + private void linkStringReference(){ + StringPool specStringPool = getSpecStringPool(); + if(specStringPool == null || specStringPool.isStringLinkLocked()){ + return; + } + linkStringReference(specStringPool.get(getKey())); + } + private void linkStringReference(StringItem stringItem){ + unLinkStringReference(); + if(stringItem==null){ + return; + } + ReferenceItem stringReference = new ReferenceBlock<>(this, OFFSET_SPEC_REFERENCE); + mStringReference = stringReference; + stringItem.addReference(stringReference); + } + private void unLinkStringReference(){ + ReferenceItem stringReference = mStringReference; + if(stringReference==null){ + return; + } + mStringReference = null; + StringItem stringItem = getNameString(); + if(stringItem == null){ + return; + } + stringItem.removeReference(stringReference); + } + public StringItem getNameString(){ + return getSpecString(getKey()); + } + private StringItem getSpecString(int key){ + if(key < 0){ + return null; + } + StringPool specStringPool = getSpecStringPool(); + if(specStringPool==null){ + return null; + } + return specStringPool.get(key); + } + private StringPool getSpecStringPool(){ + Block parent = getParent(); + while (parent!=null){ + if(parent instanceof ParentChunk){ + return ((ParentChunk) parent).getSpecStringPool(); + } + parent = parent.getParent(); + } + return null; + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + int size = reader.readUnsignedShort(); + setBytesLength(size, false); + reader.readFully(getBytesInternal()); + } + private void setName(String name){ + if(name==null){ + name = ""; + } + StringPool stringPool = getSpecStringPool(); + if(stringPool==null){ + return; + } + StringItem stringItem = stringPool.getOrCreate(name); + setKey(stringItem); + } + public void merge(ValueHeader valueHeader){ + if(valueHeader == null || valueHeader ==this){ + return; + } + setComplex(valueHeader.isComplex()); + setWeak(valueHeader.isWeak()); + setPublic(valueHeader.isPublic()); + setName(valueHeader.getName()); + } + public void toJson(JSONObject jsonObject) { + jsonObject.put(NAME_entry_name, getName()); + if(isWeak()){ + jsonObject.put(NAME_is_weak, true); + } + if(isPublic()){ + jsonObject.put(NAME_is_public, true); + } + } + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + toJson(jsonObject); + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + setWeak(json.optBoolean(NAME_is_weak, false)); + setPublic(json.optBoolean(NAME_is_public, false)); + setName(json.optString(NAME_entry_name)); + } + @Override + public String toString(){ + if(isNull()){ + return "null"; + } + StringBuilder builder=new StringBuilder(); + int byte_size = getSize(); + int read_size = readSize(); + if(byte_size!=8){ + builder.append("size=").append(byte_size); + } + if(byte_size!=read_size){ + builder.append(", readSize=").append(read_size); + } + if(isComplex()){ + builder.append(", complex"); + } + if(isPublic()){ + builder.append(", public"); + } + if(isWeak()){ + builder.append(", weak"); + } + String name = getName(); + if(name!=null){ + builder.append(", name=").append(name); + }else { + builder.append(", key=").append(getKey()); + } + return builder.toString(); + } + + private static final int OFFSET_SIZE = 0; + private static final int OFFSET_FLAGS = 2; + private static final int OFFSET_SPEC_REFERENCE = 4; + + + public static final String NAME_is_complex = "is_complex"; + public static final String NAME_is_public = "is_public"; + public static final String NAME_is_weak = "is_weak"; + + public static final String NAME_entry_name = "entry_name"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ValueItem.java b/src/ARSCLib/com/reandroid/arsc/value/ValueItem.java new file mode 100755 index 00000000..62a123cc --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ValueItem.java @@ -0,0 +1,389 @@ +/* + * 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.value; + +import com.reandroid.arsc.base.Block; +import com.reandroid.arsc.chunk.MainChunk; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.ParentChunk; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.io.BlockReader; +import com.reandroid.arsc.item.BlockItem; +import com.reandroid.arsc.item.ReferenceBlock; +import com.reandroid.arsc.item.ReferenceItem; +import com.reandroid.arsc.item.StringItem; +import com.reandroid.arsc.pool.StringPool; +import com.reandroid.arsc.pool.TableStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.io.IOException; +import java.util.Objects; + +public abstract class ValueItem extends BlockItem implements Value, + JSONConvert{ + private ReferenceItem mStringReference; + private final int sizeOffset; + public ValueItem(int bytesLength, int sizeOffset) { + super(bytesLength); + this.sizeOffset = sizeOffset; + + writeSize(); + } + public Entry resolve(int resourceId){ + PackageBlock packageBlock = getPackageBlock(); + if(packageBlock == null){ + return null; + } + Entry entry = packageBlock.getAnyEntry(resourceId); + if(entry != null){ + return entry; + } + TableBlock tableBlock = packageBlock.getTableBlock(); + if(tableBlock == null){ + return null; + } + return tableBlock.getAnyEntry(resourceId); + } + + public String buildReference(Entry entry, ValueType referenceType, boolean addType){ + if(entry == null){ + return null; + } + PackageBlock packageBlock = entry.getPackageBlock(); + PackageBlock myPackageBlock = getPackageBlock(); + StringBuilder builder = new StringBuilder(); + if(referenceType == ValueType.REFERENCE + || referenceType == ValueType.DYNAMIC_REFERENCE){ + builder.append('@'); + }else if(referenceType == ValueType.ATTRIBUTE + || referenceType == ValueType.DYNAMIC_ATTRIBUTE){ + builder.append('?'); + } + if(packageBlock != myPackageBlock && packageBlock != null && myPackageBlock != null){ + String packageName = packageBlock.getName(); + + if(!packageName.equals(myPackageBlock.getName()) + || packageBlock.getId() != myPackageBlock.getId()){ + builder.append(packageName); + builder.append(':'); + } + + } + if(addType){ + builder.append(entry.getTypeName()); + builder.append('/'); + } + builder.append(entry.getName()); + return builder.toString(); + } + public PackageBlock getPackageBlock(){ + ParentChunk parentChunk = getParentChunk(); + if(parentChunk != null){ + return parentChunk.getPackageBlock(); + } + return null; + } + + void linkTableStrings(TableStringPool tableStringPool){ + if(getValueType() == ValueType.STRING){ + linkStringReference(tableStringPool); + } + } + public void onRemoved(){ + unLinkStringReference(); + } + protected void onDataChanged(){ + } + public void refresh(){ + writeSize(); + } + + byte getRes0(){ + return getBytesInternal()[this.sizeOffset + OFFSET_RES0]; + } + public byte getType(){ + return getBytesInternal()[this.sizeOffset + OFFSET_TYPE]; + } + public void setType(byte type){ + if(type == getType()){ + return; + } + byte[] bts = getBytesInternal(); + int offset = this.sizeOffset + OFFSET_TYPE; + byte old = bts[offset]; + bts[offset] = type; + onTypeChanged(old, type); + onDataChanged(); + } + public int getSize(){ + return 0xffff & getShort(getBytesInternal(), this.sizeOffset + OFFSET_SIZE); + } + public void setSize(int size){ + size = this.sizeOffset + size; + setBytesLength(size, false); + writeSize(); + } + private void writeSize(){ + int offset = this.sizeOffset; + int size = countBytes() - offset; + putShort(getBytesInternal(), offset + OFFSET_SIZE, (short) size); + } + protected void onDataLoaded(){ + if(getValueType() == ValueType.STRING){ + linkStringReference(); + }else { + unLinkStringReference(); + } + } + @Override + public ValueType getValueType(){ + return ValueType.valueOf(getType()); + } + @Override + public void setValueType(ValueType valueType){ + byte type = 0; + if(valueType!=null){ + type = valueType.getByte(); + } + setType(type); + } + @Override + public int getData(){ + return getInteger(getBytesInternal(), this.sizeOffset + OFFSET_DATA); + } + @Override + public void setData(int data){ + byte[] bts = getBytesInternal(); + int old = getInteger(bts, this.sizeOffset + OFFSET_DATA); + if(old == data){ + return; + } + unLinkStringReference(); + putInteger(bts, this.sizeOffset + OFFSET_DATA, data); + if(ValueType.STRING==getValueType()){ + linkStringReference(); + } + onDataChanged(); + } + + + public StringItem getDataAsPoolString(){ + if(getValueType()!=ValueType.STRING){ + return null; + } + StringPool stringPool = getStringPool(); + if(stringPool == null){ + return null; + } + return stringPool.get(getData()); + } + private void onTypeChanged(byte old, byte type){ + byte typeString = ValueType.STRING.getByte(); + if(old == typeString){ + unLinkStringReference(); + }else if(type == typeString){ + linkStringReference(); + } + } + private void linkStringReference(){ + StringPool stringPool = getStringPool(); + if(stringPool == null || stringPool.isStringLinkLocked()){ + return; + } + linkStringReference(stringPool); + } + private void linkStringReference(StringPool stringPool){ + StringItem tableString = stringPool.get(getData()); + if(tableString == null){ + unLinkStringReference(); + return; + } + ReferenceItem stringReference = mStringReference; + if(stringReference!=null){ + unLinkStringReference(); + } + stringReference = new ReferenceBlock<>(this, this.sizeOffset + OFFSET_DATA); + mStringReference = stringReference; + tableString.addReference(stringReference); + } + private void unLinkStringReference(){ + ReferenceItem stringReference = mStringReference; + if(stringReference==null){ + return; + } + mStringReference = null; + onUnlinkDataString(stringReference); + } + protected void onUnlinkDataString(ReferenceItem referenceItem){ + StringPool stringPool = getStringPool(); + if(stringPool == null){ + return; + } + stringPool.removeReference(referenceItem); + } + public StringPool getStringPool(){ + Block parent = getParent(); + while (parent!=null){ + if(parent instanceof MainChunk){ + return ((MainChunk) parent).getStringPool(); + } + parent=parent.getParent(); + } + return null; + } + @Override + public void onReadBytes(BlockReader reader) throws IOException { + int readSize = initializeBytes(reader); + super.onReadBytes(reader); + if(readSize<8){ + setBytesLength(this.sizeOffset + 8, false); + writeSize(); + } + } + private int initializeBytes(BlockReader reader) throws IOException { + int position = reader.getPosition(); + int offset = this.sizeOffset; + reader.offset(offset); + int readSize = reader.readUnsignedShort(); + int size = readSize; + if(size<8){ + if(reader.available()>=8){ + size = 8; + } + } + reader.seek(position); + setBytesLength(offset + size, false); + return readSize; + } + @Override + public String getValueAsString(){ + StringItem stringItem = getDataAsPoolString(); + if(stringItem!=null){ + String value = stringItem.getHtml(); + if(value == null){ + value = ""; + } + return value; + } + return null; + } + public void setValueAsString(String str){ + if(getValueType() == ValueType.STRING + && Objects.equals(str, getValueAsString())){ + return; + } + if(str == null){ + str = ""; + } + StringItem stringItem = getStringPool().getOrCreate(str); + setData(stringItem.getIndex()); + setValueType(ValueType.STRING); + } + public boolean getValueAsBoolean(){ + return getData()!=0; + } + public void setValueAsBoolean(boolean val){ + setValueType(ValueType.INT_BOOLEAN); + int data=val?0xffffffff:0; + setData(data); + } + public void setTypeAndData(ValueType valueType, int data){ + setData(data); + setValueType(valueType); + } + public void merge(ValueItem valueItem){ + if(valueItem == null || valueItem==this){ + return; + } + setSize(valueItem.getSize()); + ValueType coming = valueItem.getValueType(); + if(coming == ValueType.STRING){ + setValueAsString(valueItem.getValueAsString()); + }else { + setTypeAndData(coming, valueItem.getData()); + } + } + @Override + public JSONObject toJson() { + if(isNull()){ + return null; + } + JSONObject jsonObject = new JSONObject(); + ValueType valueType = getValueType(); + jsonObject.put(NAME_value_type, valueType.name()); + if(valueType==ValueType.STRING){ + jsonObject.put(NAME_data, getValueAsString()); + }else if(valueType==ValueType.INT_BOOLEAN){ + jsonObject.put(NAME_data, getValueAsBoolean()); + }else { + jsonObject.put(NAME_data, getData()); + } + return jsonObject; + } + @Override + public void fromJson(JSONObject json) { + ValueType valueType = ValueType.fromName(json.getString(NAME_value_type)); + if(valueType==ValueType.STRING){ + setValueAsString(json.optString(NAME_data, "")); + }else if(valueType==ValueType.INT_BOOLEAN){ + setValueAsBoolean(json.getBoolean(NAME_data)); + }else { + setValueType(valueType); + setData(json.getInt(NAME_data)); + } + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + int size = getSize(); + if(size!=8){ + builder.append("size=").append(getSize()); + builder.append(", "); + } + builder.append("type="); + ValueType valueType=getValueType(); + if(valueType!=null){ + builder.append(valueType); + }else { + builder.append(HexUtil.toHex2(getType())); + } + builder.append(", data="); + int data = getData(); + if(valueType==ValueType.STRING){ + StringItem tableString = getDataAsPoolString(); + if(tableString!=null){ + builder.append(tableString.getHtml()); + }else { + builder.append(HexUtil.toHex8(data)); + } + }else { + builder.append(HexUtil.toHex8(data)); + } + return builder.toString(); + } + + private static final int OFFSET_SIZE = 0; + private static final int OFFSET_RES0 = 2; + private static final int OFFSET_TYPE = 3; + private static final int OFFSET_DATA = 4; + + + public static final String NAME_data = "data"; + public static final String NAME_value_type = "value_type"; +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/ValueType.java b/src/ARSCLib/com/reandroid/arsc/value/ValueType.java new file mode 100755 index 00000000..7d009dbd --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/ValueType.java @@ -0,0 +1,67 @@ + /* + * 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.value; + + +public enum ValueType { + + NULL((byte) 0x00), + REFERENCE((byte) 0x01), + ATTRIBUTE((byte) 0x02), + STRING((byte) 0x03), + FLOAT((byte) 0x04), + DIMENSION((byte) 0x05), + FRACTION((byte) 0x06), + DYNAMIC_REFERENCE((byte) 0x07), + DYNAMIC_ATTRIBUTE((byte) 0x08), + INT_DEC((byte) 0x10), + INT_HEX((byte) 0x11), + INT_BOOLEAN((byte) 0x12), + INT_COLOR_ARGB8((byte) 0x1c), + INT_COLOR_RGB8((byte) 0x1d), + INT_COLOR_ARGB4((byte) 0x1e), + INT_COLOR_RGB4((byte) 0x1f); + + private final byte mByte; + ValueType(byte b) { + this.mByte=b; + } + public byte getByte(){ + return mByte; + } + public static ValueType valueOf(byte b){ + ValueType[] all=values(); + for(ValueType vt:all){ + if(vt.mByte==b){ + return vt; + } + } + return null; + } + public static ValueType fromName(String name){ + if(name==null){ + return null; + } + name=name.toUpperCase(); + ValueType[] all=values(); + for(ValueType vt:all){ + if(name.equals(vt.name())){ + return vt; + } + } + return null; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBag.java b/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBag.java new file mode 100644 index 00000000..29413259 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBag.java @@ -0,0 +1,175 @@ + /* + * 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.value.array; + + import com.reandroid.arsc.array.ResValueMapArray; + import com.reandroid.arsc.value.Entry; + import com.reandroid.arsc.value.ResTableMapEntry; + import com.reandroid.arsc.value.ResValueMap; + import com.reandroid.arsc.value.bag.Bag; + + import java.util.AbstractList; + import java.util.RandomAccess; + + public class ArrayBag extends AbstractList implements Bag, RandomAccess { + private final Entry entry; + + private ArrayBag(Entry entry) { + this.entry = entry; + } + + private ResTableMapEntry getTableEntry() { + return (ResTableMapEntry) entry.getTableEntry(); + } + + private ResValueMapArray getMapArray() { + return getTableEntry().getValue(); + } + + private void updateStructure(int regenStart) { + getTableEntry().setValuesCount(size()); + modCount += 1; + if (regenStart < 1) { + return; + } + + ResValueMapArray array = getMapArray(); + for (int i = regenStart; i < array.childesCount(); i++) { + setIndex(array.get(i), i); + } + } + + @Override + public Entry getEntry() { + return entry; + } + + public ArrayBagItem[] getBagItems() { + return toArray(new ArrayBagItem[0]); + } + + @Override + public int size() { + return getMapArray().childesCount(); + } + + @Override + public ArrayBagItem get(int i) { + return ArrayBagItem.create(getMapArray().get(i)); + } + + @Override + public ArrayBagItem set(int index, ArrayBagItem value) { + ArrayBagItem target = get(index); + value.copyTo(target.getBagItem()); + return target; + } + + private void setIndex(ResValueMap valueMap, int index) { + valueMap.setName(0x01000001 + index); + } + + @Override + public void add(int index, ArrayBagItem value) { + if (index < 0 || index > size()) { + throw new IndexOutOfBoundsException(); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + + ResValueMap valueMap = new ResValueMap(); + setIndex(valueMap, index); + getMapArray().insertItem(index, valueMap); + value.copyTo(valueMap); + updateStructure(index); + } + + @Override + public ArrayBagItem remove(int index) { + ResValueMapArray array = getMapArray(); + ResValueMap target = array.getChildes()[index]; + array.remove(target); + updateStructure(index); + return ArrayBagItem.copyOf(target); + } + + @Override + public void clear() { + getMapArray().clearChildes(); + updateStructure(-1); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("<"); + String type = getTypeName(); + builder.append(type); + builder.append(" name=\""); + builder.append(getName()); + builder.append("\">"); + ArrayBagItem[] allItems = getBagItems(); + for (ArrayBagItem allItem : allItems) { + builder.append("\n "); + builder.append(allItem.toString()); + } + builder.append("\n"); + return builder.toString(); + } + + /** + * The result of this is not always 100% accurate, + * in addition to this use your methods to cross check like type-name == "array" + **/ + public static boolean isArray(Entry entry) { + ArrayBag array = create(entry); + if (array == null) { + return false; + } + ResTableMapEntry tableEntry = array.getTableEntry(); + if (tableEntry.getParentId() != 0) { + return false; + } + ResValueMap[] items = tableEntry.listResValueMap(); + if (items.length == 0) { + return false; + } + + for (int i = 0; i < items.length; i++) { + ResValueMap resValueMap = items[i]; + int name = resValueMap.getName(); + int high = (name >> 16) & 0xffff; + if(high!=0x0100){ + return false; + } + int low = name & 0xffff; + if(low != (i+1)){ + return false; + } + } + return true; + } + + public static ArrayBag create(Entry entry) { + if (entry == null || !entry.isComplex()) { + return null; + } + return new ArrayBag(entry); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBagItem.java b/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBagItem.java new file mode 100644 index 00000000..fa9cf2ff --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/array/ArrayBagItem.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.arsc.value.array; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.item.StringItem; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.arsc.value.bag.BagItem; +import com.reandroid.arsc.value.ResValueMap; + +public class ArrayBagItem extends BagItem { + private ArrayBagItem(ResValueMap valueMap) { + super(valueMap); + } + + private ArrayBagItem(StringItem str) { + super(str); + } + + private ArrayBagItem(ValueType valueType, int value) { + super(valueType, value); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(""); + if (hasStringValue()) { + builder.append(getStringValue()); + } else { + builder.append(HexUtil.toHex8(getValue())); + } + builder.append(""); + return builder.toString(); + } + + protected static ArrayBagItem create(ResValueMap valueMap) { + if (valueMap == null) { + return null; + } + return new ArrayBagItem(valueMap); + } + + public static ArrayBagItem create(ValueType valueType, int value) { + if (valueType == null || valueType == ValueType.STRING) { + return null; + } + return new ArrayBagItem(valueType, value); + } + + protected static ArrayBagItem copyOf(ResValueMap resValueMap) { + ValueType valueType = resValueMap.getValueType(); + if (valueType == ValueType.STRING) { + return new ArrayBagItem(resValueMap.getDataAsPoolString()); + } else { + return new ArrayBagItem(valueType, resValueMap.getData()); + } + } + + public static ArrayBagItem encoded(ValueDecoder.EncodeResult encodeResult) { + if (encodeResult == null) { + return null; + } + return create(encodeResult.valueType, encodeResult.value); + } + + public static ArrayBagItem integer(int n) { + return create(ValueType.INT_DEC, n); + } + + public static ArrayBagItem string(TableString str) { + if (str == null) { + return null; + } + return new ArrayBagItem(str); + } + + public static ArrayBagItem reference(int resourceId) { + return create(ValueType.REFERENCE, resourceId); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/attribute/AttributeBag.java b/src/ARSCLib/com/reandroid/arsc/value/attribute/AttributeBag.java new file mode 100755 index 00000000..287f80c0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/attribute/AttributeBag.java @@ -0,0 +1,248 @@ + /* + * 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.value.attribute; + +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.value.*; +import com.reandroid.common.EntryStore; + + +public class AttributeBag { + private final AttributeBagItem[] mBagItems; + public AttributeBag(AttributeBagItem[] bagItems){ + this.mBagItems=bagItems; + } + + public boolean contains(AttributeDataFormat valueType){ + return getFormat().contains(valueType); + } + public boolean isEqualType(AttributeDataFormat valueType){ + return getFormat().isEqualType(valueType); + } + public ValueDecoder.EncodeResult encodeEnumOrFlagValue(String valueString){ + if(valueString==null || !isEnumOrFlag()){ + return null; + } + int value=0; + boolean foundOnce=false; + String[] names=valueString.split("[\\s|]+"); + for(String name:names){ + AttributeBagItem item=searchByName(name); + if(item==null){ + continue; + } + value|=item.getBagItem().getData(); + foundOnce=true; + } + if(!foundOnce){ + return null; + } + ValueType valueType = isFlag()?ValueType.INT_HEX:ValueType.INT_DEC; + return new ValueDecoder.EncodeResult(valueType, value); + } + public String decodeAttributeValue(EntryStore entryStore, int attrValue){ + AttributeBagItem[] bagItems=searchValue(attrValue); + return AttributeBagItem.toString(entryStore, bagItems); + } + public AttributeBagItem searchByName(String entryName){ + AttributeBagItem[] bagItems= getBagItems(); + for(AttributeBagItem item:bagItems){ + if(item.isType()){ + continue; + } + if(entryName.equals(item.getNameOrHex())){ + return item; + } + } + return null; + } + public AttributeBagItem[] searchValue(int attrValue){ + if(isFlag()){ + return searchFlagValue(attrValue); + } + AttributeBagItem item = searchEnumValue(attrValue); + if(item != null){ + return new AttributeBagItem[]{item}; + } + return null; + } + private AttributeBagItem searchEnumValue(int attrValue){ + AttributeBagItem[] bagItems= getBagItems(); + for(AttributeBagItem item:bagItems){ + if(item.isType()){ + continue; + } + int data=item.getData(); + if(attrValue==data){ + return item; + } + } + return null; + } + + private AttributeBagItem[] searchFlagValue(int attrValue){ + AttributeBagItem[] bagItems= getBagItems(); + int len=bagItems.length; + AttributeBagItem[] foundBags = new AttributeBagItem[len]; + for(int i=0;i= 0) { + foundBags[index] = item; + } + } + return removeNull(foundBags); + } + + private int indexOf(AttributeBagItem[] foundFlag, int data) { + for (int i = 0; i < foundFlag.length; i++) { + AttributeBagItem item=foundFlag[i]; + if(item==null){ + return i; + } + int flag=item.getData(); + if(flag==0){ + return i; + } + if ((flag & data) == data) { + return -1; + } + if ((flag & data) == flag) { + return i; + } + } + return -1; + } + + private AttributeBagItem[] removeNull(AttributeBagItem[] bagItems){ + int count=countNonNull(bagItems); + if(count==0){ + return null; + } + AttributeBagItem[] results=new AttributeBagItem[count]; + int index=0; + int len=bagItems.length; + for(int i=0;i extends AbstractMap implements Bag { + protected final com.reandroid.arsc.value.Entry entry; + private int modCount = 0; + + protected MapBag(com.reandroid.arsc.value.Entry entry) { + this.entry = entry; + } + + protected ResTableMapEntry getTableEntry() { + return (ResTableMapEntry) entry.getTableEntry(); + } + + protected ResValueMapArray getMapArray() { + return getTableEntry().getValue(); + } + + private void updateSize() { + getTableEntry().setValuesCount(size()); + modCount += 1; + } + + @Override + public com.reandroid.arsc.value.Entry getEntry() { + return entry; + } + + protected abstract V createBagItem(ResValueMap valueMap, boolean copied); + + protected abstract ResValueMap newKey(K key); + + protected abstract K getKeyFor(ResValueMap valueMap); + + protected TableStringPool getStringPool() { + com.reandroid.arsc.value.Entry entry = getEntry(); + if (entry == null) { + return null; + } + PackageBlock pkg = entry.getPackageBlock(); + if (pkg == null) { + return null; + } + TableBlock tableBlock = pkg.getTableBlock(); + if (tableBlock == null) { + return null; + } + return tableBlock.getTableStringPool(); + } + + private class MapEntry implements Map.Entry { + private final ResValueMap item; + + private MapEntry(ResValueMap item) { + this.item = item; + } + + @Override + public K getKey() { + return getKeyFor(item); + } + + @Override + public V getValue() { + return createBagItem(item, false); + } + + @Override + public V setValue(V v) { + v.copyTo(item); + return getValue(); + } + } + + private class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new Iterator>() { + private final Iterator iterator = getMapArray().iterator(); + private final int expectedModCount = modCount; + + private void checkValidity() { + if (expectedModCount != modCount) { + throw new ConcurrentModificationException("Iterator is no longer valid because the size has changed."); + } + } + + @Override + public boolean hasNext() { + checkValidity(); + return iterator.hasNext(); + } + + @Override + public Entry next() { + checkValidity(); + return new MapEntry(iterator.next()); + } + }; + } + + @Override + public int size() { + return getMapArray().childesCount(); + } + } + + @Override + public V remove(Object key) { + ResValueMapArray array = getMapArray(); + for (ResValueMap item : array.getChildes()) { + if (getKeyFor(item).equals(key)) { + if (!array.remove(item)) { + throw new IllegalStateException("Could not remove item"); + } + updateSize(); + return createBagItem(item, true); + } + } + return null; + } + + @Override + public void clear() { + getMapArray().clearChildes(); + updateSize(); + } + + @Override + public Set> entrySet() { + return new EntrySet(); + } + + @Override + public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("key is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + ResValueMapArray array = getMapArray(); + ResValueMap valueMap = null; + for (ResValueMap item : array.getChildes()) { + if (getKeyFor(item).equals(key)) { + valueMap = item; + break; + } + } + + if (valueMap == null) { + valueMap = newKey(key); + array.add(valueMap); + updateSize(); + } + + value.copyTo(valueMap); + return createBagItem(valueMap, false); + } + + @Override + public void putAll(Map m) { + LinkedHashSet keys = new LinkedHashSet<>(m.keySet()); + ResValueMapArray array = getMapArray(); + + for (ResValueMap item : array.getChildes()) { + K currentKey = getKeyFor(item); + + if (keys.remove(currentKey)) { + V src = m.get(currentKey); + src.copyTo(item); + } + } + + for (K key : keys) { + if (key == null) { + throw new NullPointerException("Key is null"); + } + ResValueMap item = newKey(key); + array.add(item); + V src = m.get(key); + src.copyTo(item); + } + + updateSize(); + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBag.java b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBag.java new file mode 100644 index 00000000..4c03194c --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBag.java @@ -0,0 +1,126 @@ + /* + * 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.value.plurals; + + import com.reandroid.arsc.value.ResConfig; + import com.reandroid.arsc.value.ResValueMap; + import com.reandroid.arsc.value.ValueType; + import com.reandroid.arsc.value.bag.MapBag; + + import java.util.Arrays; + import java.util.HashSet; + import java.util.Set; + + public class PluralsBag extends MapBag { + private PluralsBag(com.reandroid.arsc.value.Entry entry) { + super(entry); + } + + @Override + protected PluralsBagItem createBagItem(ResValueMap valueMap, boolean copied) { + if (copied) { + return PluralsBagItem.copyOf(valueMap); + } + return PluralsBagItem.create(valueMap); + } + + @Override + protected ResValueMap newKey(PluralsQuantity key) { + ResValueMap valueMap = new ResValueMap(); + valueMap.setParent(getMapArray()); + valueMap.setNameHigh((short) 0x0100); + valueMap.setNameLow(key.getId()); + return valueMap; + } + + @Override + protected PluralsQuantity getKeyFor(ResValueMap valueMap) { + return PluralsQuantity.valueOf(valueMap); + } + + public String getQuantityString(PluralsQuantity quantity, ResConfig resConfig) { + PluralsBagItem item = get(quantity); + if (item == null) { + return null; + } + return item.getQualityString(resConfig); + } + public String getQuantityString(PluralsQuantity quantity) { + return getQuantityString(quantity, null); + } + + public void setQuantityString(PluralsQuantity quantity, String str) { + if (quantity == null || str == null) { + return; + } + put(quantity, PluralsBagItem.string(getStringPool().getOrCreate(str))); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("<"); + String type = getTypeName(); + builder.append(type); + builder.append(" name=\""); + builder.append(getName()); + builder.append("\">"); + for (PluralsBagItem pluralsBagItem : values()) { + builder.append("\n "); + builder.append(pluralsBagItem.toString()); + } + builder.append("\n"); + return builder.toString(); + } + + private final static Set validTypes = new HashSet<>(Arrays.asList(ValueType.NULL, ValueType.STRING, ValueType.REFERENCE)); + + /** + * The result of this is not always 100% accurate, + * in addition to this use your methods to cross check like type-name == "plurals" + **/ + public static boolean isPlurals(com.reandroid.arsc.value.Entry entry) { + PluralsBag plurals = create(entry); + if (plurals == null) { + return false; + } + ResValueMap[] items = plurals.getMapArray().getChildes(); + if (items.length == 0) { + return false; + } + + for (ResValueMap item : items) { + if (item == null || !validTypes.contains(item.getValueType())) { + return false; + } + int name = item.getName(); + int high = (name >> 16) & 0xffff; + if (PluralsQuantity.valueOf(item) == null || high != 0x0100) { + return false; + } + } + return true; + } + + public static PluralsBag create(com.reandroid.arsc.value.Entry entry) { + if (entry == null || !entry.isComplex()) { + return null; + } + return new PluralsBag(entry); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBagItem.java b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBagItem.java new file mode 100644 index 00000000..34b5ba79 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsBagItem.java @@ -0,0 +1,131 @@ + /* + * 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.value.plurals; + + import com.reandroid.arsc.chunk.TableBlock; + import com.reandroid.arsc.item.StringItem; + import com.reandroid.arsc.item.TableString; + import com.reandroid.arsc.util.HexUtil; + import com.reandroid.arsc.value.*; + import com.reandroid.arsc.value.bag.BagItem; + + import java.util.List; + + public class PluralsBagItem extends BagItem { + private PluralsBagItem(ResValueMap bagItem) { + super(bagItem); + } + + private PluralsBagItem(StringItem str) { + super(str); + } + + private PluralsBagItem(ValueType valueType, int data) { + super(valueType, data); + } + + public PluralsQuantity getQuantity() { + if (mBagItem == null || mBagItem.getName() == 0) { + return null; + } + return PluralsQuantity.valueOf(mBagItem); + } + + public String getQualityString(ResConfig resConfig) { + switch (getValueType()) { + case STRING: + return getStringValue(); + case REFERENCE: + Entry entry = null; + if (mBagItem != null) { + entry = mBagItem.getEntry(); + } + if (entry == null) { + return null; + } + + if (resConfig == null) { + resConfig = entry.getResConfig(); + } + + Entry stringRes = null; + if (resConfig != null) { + TableBlock tableBlock = entry.getPackageBlock().getTableBlock(); + List resolvedList = tableBlock.resolveReferenceWithConfig(getValue(), resConfig); + if (resolvedList.size() > 0) { + stringRes = resolvedList.get(0); + } + } + + if (stringRes == null) { + return null; + } + ResValue resValue = stringRes.getResValue(); + if (resValue == null || resValue.getValueType() != ValueType.STRING) { + throw new IllegalArgumentException("Not a STR reference: " + formattedRefValue()); + } + return resValue.getValueAsString(); + default: + throw new IllegalArgumentException("Not STR/REFERENCE ValueType=" + getValueType()); + } + } + + private String formattedRefValue() { + return HexUtil.toHex8("@0x", getValue()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(""); + if (hasStringValue()) { + builder.append(getStringValue()); + } else { + builder.append(formattedRefValue()); + } + builder.append(""); + return builder.toString(); + } + + protected static PluralsBagItem create(ResValueMap resValueMap) { + if (resValueMap == null) { + return null; + } + return new PluralsBagItem(resValueMap); + } + + protected static PluralsBagItem copyOf(ResValueMap resValueMap) { + ValueType valueType = resValueMap.getValueType(); + if (valueType == ValueType.STRING) { + return new PluralsBagItem(resValueMap.getDataAsPoolString()); + } else { + return new PluralsBagItem(valueType, resValueMap.getData()); + } + } + + public static PluralsBagItem string(TableString str) { + if (str == null) { + return null; + } + return new PluralsBagItem(str); + } + + public static PluralsBagItem reference(int resourceId) { + return new PluralsBagItem(ValueType.REFERENCE, resourceId); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsQuantity.java b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsQuantity.java new file mode 100755 index 00000000..d02f05e4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/plurals/PluralsQuantity.java @@ -0,0 +1,69 @@ + /* + * 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.value.plurals; + + + import com.reandroid.arsc.value.ResValueMap; + + public enum PluralsQuantity { + OTHER((short) 0x0004), + ZERO((short) 0x0005), + ONE((short) 0x0006), + TWO((short) 0x0007), + FEW((short) 0x0008), + MANY((short) 0x0009); + + private final short mId; + PluralsQuantity(short id) { + this.mId=id; + } + public short getId() { + return mId; + } + @Override + public String toString(){ + return name().toLowerCase(); + } + public static PluralsQuantity valueOf(short id){ + PluralsQuantity[] all=values(); + for(PluralsQuantity pq:all){ + if(id==pq.mId){ + return pq; + } + } + return null; + } + public static PluralsQuantity valueOf(ResValueMap valueMap){ + if (valueMap == null) { + return null; + } + int low = valueMap.getName() & 0xffff; + return valueOf((short) low); + } + public static PluralsQuantity value(String name){ + if(name==null){ + return null; + } + name=name.trim().toUpperCase(); + PluralsQuantity[] all=values(); + for(PluralsQuantity pq:all){ + if(name.equals(pq.name())){ + return pq; + } + } + return null; + } +} diff --git a/src/ARSCLib/com/reandroid/arsc/value/style/StyleBag.java b/src/ARSCLib/com/reandroid/arsc/value/style/StyleBag.java new file mode 100644 index 00000000..752749b8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/style/StyleBag.java @@ -0,0 +1,139 @@ + /* + * 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.value.style; + + import com.reandroid.apk.xmlencoder.EncodeMaterials; + import com.reandroid.arsc.chunk.TableBlock; + import com.reandroid.arsc.value.ResValueMap; + import com.reandroid.arsc.value.bag.MapBag; + + public class StyleBag extends MapBag { + private StyleBag(com.reandroid.arsc.value.Entry entry) { + super(entry); + } + + public String getParentResourceName() { + int id = getParentId(); + if (id == 0) { + return null; + } + com.reandroid.arsc.value.Entry entry = getEntry(); + if (entry == null) { + return null; + } + return entry.buildResourceName(id, '@', true); + } + + public int getParentId() { + return getTableEntry().getParentId(); + } + public void setParentId(int id) { + getTableEntry().setParentId(id); + } + + public int getResourceId() { + com.reandroid.arsc.value.Entry entry = getEntry(); + if (entry == null) { + return 0; + } + return entry.getResourceId(); + } + + @Override + protected StyleBagItem createBagItem(ResValueMap valueMap, boolean copied) { + if (copied) { + return StyleBagItem.copyOf(valueMap); + } + return StyleBagItem.create(valueMap); + } + + @Override + protected ResValueMap newKey(Integer attrId) { + ResValueMap valueMap = new ResValueMap(); + valueMap.setParent(getMapArray()); + valueMap.setName(attrId); + return valueMap; + } + + @Override + protected Integer getKeyFor(ResValueMap valueMap) { + return valueMap.getName(); + } + + public static int resolve(EncodeMaterials materials, String name) { + return materials.getAttributeBlock(name).getResourceId(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("<"); + String type = getTypeName(); + builder.append(type); + builder.append(" name=\""); + builder.append(getName()); + builder.append("\""); + String parent = getParentResourceName(); + if (parent != null) { + builder.append(" parent=\""); + builder.append(parent); + builder.append("\""); + } + builder.append("\">"); + for (StyleBagItem item : values()) { + builder.append("\n "); + builder.append(item.toString()); + } + builder.append("\n"); + return builder.toString(); + } + + /** + * The result of this is not always 100% accurate, + * in addition to this use your methods to cross check like type-name == "plurals" + **/ + public static boolean isStyle(com.reandroid.arsc.value.Entry entry) { + StyleBag style = create(entry); + if (style == null) { + return false; + } + + TableBlock tableBlock = entry.getPackageBlock().getTableBlock(); + if (tableBlock == null) { + return false; + } + ResValueMap[] items = style.getMapArray().getChildes(); + if (items.length == 0) { + return false; + } + + for (ResValueMap item : items) { + if (item == null || tableBlock.search(item.getNameResourceID()) == null) { + return false; + } + } + return true; + } + + public static StyleBag create(com.reandroid.arsc.value.Entry entry) { + if (entry == null || !entry.isComplex()) { + return null; + } + return new StyleBag(entry); + } + } diff --git a/src/ARSCLib/com/reandroid/arsc/value/style/StyleBagItem.java b/src/ARSCLib/com/reandroid/arsc/value/style/StyleBagItem.java new file mode 100644 index 00000000..b6d78389 --- /dev/null +++ b/src/ARSCLib/com/reandroid/arsc/value/style/StyleBagItem.java @@ -0,0 +1,190 @@ +/* + * 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.value.style; + +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.item.StringItem; +import com.reandroid.arsc.item.TableString; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.attribute.AttributeBag; +import com.reandroid.arsc.value.attribute.AttributeBagItem; +import com.reandroid.arsc.value.bag.BagItem; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResValueMap; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.common.EntryStore; + +public class StyleBagItem extends BagItem { + private StyleBagItem(ResValueMap bagItem) { + super(bagItem); + } + + private StyleBagItem(ValueType valueType, int data) { + super(valueType, data); + } + + private StyleBagItem(StringItem str) { + super(str); + } + + public String getName() { + if (mBagItem == null) { + return null; + } + Entry block = mBagItem.getEntry(); + if (block == null) { + return null; + } + char prefix = 0; + return block.buildResourceName(mBagItem.getName(), prefix, false); + } + public Entry getAttributeEntry(EntryStore entryStore) { + if (mBagItem == null) { + return null; + } + return entryStore.getEntryGroup(mBagItem.getName()).pickOne(); + } + + public int getNameId() { + if (mBagItem == null) { + return 0; + } + return mBagItem.getName(); + } + + public boolean hasAttributeValue() { + return getValueType() == ValueType.ATTRIBUTE; + } + public boolean hasIntValue() { + ValueType valueType = getValueType(); + return valueType == ValueType.INT_DEC || valueType == ValueType.INT_HEX; + } + + public String getValueAsReference() { + ValueType valueType = getValueType(); + if (valueType != ValueType.REFERENCE && valueType != ValueType.ATTRIBUTE) { + throw new IllegalArgumentException("Not REF ValueType=" + valueType); + } + Entry entry = getBagItem().getEntry(); + if (entry == null) { + return null; + } + char prefix = '@'; + boolean includeType = true; + if (valueType == ValueType.ATTRIBUTE) { + prefix = '?'; + includeType = false; + } + int id = getValue(); + return entry.buildResourceName(id, prefix, includeType); + } + public String decodeAttributeValue(AttributeBag attr, EntryStore entryStore) { + if (!hasIntValue()) { + return null; + } + return attr.decodeAttributeValue(entryStore, getValue()); + } + public AttributeBagItem[] getFlagsOrEnum(AttributeBag attr) { + if (!hasIntValue()) { + return null; + } + return attr.searchValue(getValue()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(""); + if (hasStringValue()) { + builder.append(getStringValue()); + } + String val = null; + if (hasReferenceValue() || hasAttributeValue()) { + val = getValueAsReference(); + } + if (val == null) { + val = HexUtil.toHex8(getValue()); + } + builder.append(val); + builder.append(""); + return builder.toString(); + } + + protected static StyleBagItem create(ResValueMap resValueMap) { + if (resValueMap == null) { + return null; + } + return new StyleBagItem(resValueMap); + } + + public static StyleBagItem create(ValueType valueType, int value) { + if (valueType == null || valueType == ValueType.STRING) { + return null; + } + return new StyleBagItem(valueType, value); + } + + protected static StyleBagItem copyOf(ResValueMap resValueMap) { + ValueType valueType = resValueMap.getValueType(); + if (valueType == ValueType.STRING) { + return new StyleBagItem(resValueMap.getDataAsPoolString()); + } else { + return new StyleBagItem(valueType, resValueMap.getData()); + } + } + + public static StyleBagItem integer(int n) { + return new StyleBagItem(ValueType.INT_DEC, n); + } + + public static StyleBagItem string(TableString str) { + if (str == null) { + return null; + } + return new StyleBagItem(str); + } + + public static StyleBagItem reference(int resourceId) { + return new StyleBagItem(ValueType.REFERENCE, resourceId); + } + public static StyleBagItem attribute(int resourceId) { + return new StyleBagItem(ValueType.ATTRIBUTE, resourceId); + } + public static StyleBagItem encoded(ValueDecoder.EncodeResult encodeResult) { + if (encodeResult == null) { + return null; + } + return create(encodeResult.valueType, encodeResult.value); + } + public static StyleBagItem color(String color) { + return encoded(ValueDecoder.encodeColor(color)); + } + public static StyleBagItem dimensionOrFraction(String str) { + return encoded(ValueDecoder.encodeDimensionOrFraction(str)); + } + public static StyleBagItem createFloat(float n) { + return new StyleBagItem(ValueType.FLOAT, Float.floatToIntBits(n)); + } + public static StyleBagItem enumOrFlag(AttributeBag attr, String valueString) { + return encoded(attr.encodeEnumOrFlagValue(valueString)); + } +} diff --git a/src/ARSCLib/com/reandroid/common/EntryStore.java b/src/ARSCLib/com/reandroid/common/EntryStore.java new file mode 100755 index 00000000..4d6e24de --- /dev/null +++ b/src/ARSCLib/com/reandroid/common/EntryStore.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.common; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.item.TableString; + +import java.util.Collection; + +public interface EntryStore { + Collection getEntryGroups(int resourceId); + EntryGroup getEntryGroup(int resourceId); + Collection getPackageBlocks(int packageId); +} diff --git a/src/ARSCLib/com/reandroid/common/FileChannelInputStream.java b/src/ARSCLib/com/reandroid/common/FileChannelInputStream.java new file mode 100644 index 00000000..fffad55e --- /dev/null +++ b/src/ARSCLib/com/reandroid/common/FileChannelInputStream.java @@ -0,0 +1,239 @@ +/* + * 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.common; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; + +public class FileChannelInputStream extends InputStream { + private final FileChannel fileChannel; + private final long totalLength; + private long startOffset; + private long position; + private final byte[] buffer; + private int bufferPosition; + private int bufferLength; + private boolean mAutoClosable; + private boolean mIsClosed; + + public FileChannelInputStream(FileChannel fileChannel, long length, int bufferSize) throws IOException { + this.fileChannel = fileChannel; + this.totalLength = length; + if(bufferSize <= 0){ + bufferSize = 8; + } + if(length < bufferSize){ + bufferSize = (int) length; + } + this.buffer = new byte[bufferSize]; + this.bufferLength = bufferSize; + this.bufferPosition = bufferSize; + this.startOffset = fileChannel.position(); + } + public FileChannelInputStream(FileChannel fileChannel, long length) throws IOException { + this(fileChannel, length, DEFAULT_BUFFER_SIZE); + } + public FileChannelInputStream(File file, long length, int bufferSize) throws IOException { + this(FileChannel.open(file.toPath(), StandardOpenOption.READ), length, bufferSize); + this.mAutoClosable = true; + } + public FileChannelInputStream(File file) throws IOException { + this(FileChannel.open(file.toPath(), StandardOpenOption.READ), file.length()); + this.mAutoClosable = true; + } + + @Override + public int read(byte[] bytes) throws IOException { + return read(bytes, 0, bytes.length); + } + @Override + public int read(byte[] bytes, int offset, int length) throws IOException { + if(isFinished()){ + return -1; + } + if(length==0){ + return 0; + } + loadBuffer(); + int result = 0; + int read = readBuffer(bytes, offset, length); + result += read; + length = length - read; + offset = offset + read; + while (length>0 && !isFinished()){ + loadBuffer(); + read = readBuffer(bytes, offset, length); + result += read; + length = length - read; + offset = offset + read; + } + return result; + } + private int readBuffer(byte[] bytes, int offset, int length){ + int avail = bufferLength - bufferPosition; + if(avail == 0){ + return 0; + } + int read = length; + if(read > avail){ + read = avail; + } + System.arraycopy(buffer, bufferPosition, bytes, offset, read); + bufferPosition += read; + position += read; + return read; + } + private void loadBuffer() throws IOException { + byte[] buffer = this.buffer; + if(this.bufferPosition < bufferLength){ + return; + } + int length = buffer.length; + long available = totalLength - position; + boolean is_last = false; + if(length > available){ + length = (int) available; + is_last = true; + } + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, length); + bufferLength = fileChannel.read(byteBuffer); + bufferPosition = 0; + if(is_last){ + closeAuto(); + } + } + private boolean isFinished() throws IOException { + boolean finished = position >= totalLength; + if(finished){ + closeAuto(); + } + return finished; + } + @Override + public int read() throws IOException { + byte[] bytes = new byte[1]; + int read = read(bytes); + if(read < 0){ + return read; + } + return bytes[0] & 0xff; + } + public long transferTo(OutputStream out) throws IOException{ + long transferred = 0; + if(isFinished()){ + return transferred; + } + while (!isFinished()){ + loadBuffer(); + int offset = bufferPosition; + int length = bufferLength - bufferPosition; + if(length <= 0){ + break; + } + out.write(buffer, offset, length); + bufferPosition += length; + position += length; + transferred += length; + } + return transferred; + } + @Override + public long skip(long amount) throws IOException { + if(amount <= 0){ + return amount; + } + long remaining = amount; + remaining = remaining - skipBuffer((int) remaining); + if(remaining == 0){ + return amount; + } + long availableChannel = totalLength - position; + if(availableChannel > remaining){ + availableChannel = remaining; + } + position += availableChannel; + remaining = remaining - availableChannel; + amount = amount - remaining; + fileChannel.position(fileChannel.position() + availableChannel); + return amount; + } + private int skipBuffer(int amount){ + int availableBuffer = bufferLength - bufferPosition; + if(availableBuffer > amount){ + availableBuffer = amount; + } + bufferPosition += availableBuffer; + position += availableBuffer; + return availableBuffer; + } + public FileChannel getFileChannel() { + return fileChannel; + } + + public void setAutoClosable(boolean autoClosable) { + this.mAutoClosable = autoClosable; + } + private void closeAuto() throws IOException { + if(mAutoClosable && !mIsClosed){ + mIsClosed = true; + fileChannel.close(); + } + } + + @Override + public void close() throws IOException { + closeAuto(); + } + @Override + public void reset() throws IOException { + position = 0; + bufferPosition = bufferLength; + fileChannel.position(startOffset); + } + @Override + public int available(){ + return (int) (totalLength - position); + } + @Override + public boolean markSupported() { + return true; + } + @Override + public synchronized void mark(int readLimit){ + if(readLimit < 0){ + readLimit = 0; + } + startOffset = readLimit; + } + @Override + public String toString(){ + return position + " / " + totalLength; + } + + public static byte[] read(File file, int length) throws IOException{ + FileChannelInputStream inputStream = new FileChannelInputStream(file,length, length); + inputStream.loadBuffer(); + inputStream.closeAuto(); + return inputStream.buffer; + } + + private static final int DEFAULT_BUFFER_SIZE = 1024 * 100; +} diff --git a/src/ARSCLib/com/reandroid/common/Frameworks.java b/src/ARSCLib/com/reandroid/common/Frameworks.java new file mode 100755 index 00000000..f3fd9c24 --- /dev/null +++ b/src/ARSCLib/com/reandroid/common/Frameworks.java @@ -0,0 +1,43 @@ + /* + * 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.common; + +import com.reandroid.apk.AndroidFrameworks; +import com.reandroid.arsc.util.FrameworkTable; + +import java.io.IOException; +import java.io.InputStream; + +/**Use {@link AndroidFrameworks} */ +@Deprecated +public class Frameworks { + private static FrameworkTable android_table; + private static boolean load_once; + @Deprecated + public static FrameworkTable getAndroid(){ + if(android_table!=null || load_once){ + return android_table; + } + load_once=true; + FrameworkTable frameworkTable=null; + try { + frameworkTable = AndroidFrameworks.getLatest().getTableBlock(); + } catch (IOException e) { + } + android_table=frameworkTable; + return android_table; + } +} diff --git a/src/ARSCLib/com/reandroid/common/ReferenceResolver.java b/src/ARSCLib/com/reandroid/common/ReferenceResolver.java new file mode 100644 index 00000000..a82a32bd --- /dev/null +++ b/src/ARSCLib/com/reandroid/common/ReferenceResolver.java @@ -0,0 +1,144 @@ + /* + * 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.common; + +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResConfig; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.arsc.value.ValueType; + +import java.util.*; +import java.util.function.Predicate; + +public class ReferenceResolver{ + private final EntryStore entryStore; + private final List results; + private final Set resolvedIds; + private int limit; + public ReferenceResolver(EntryStore entryStore){ + this.entryStore = entryStore; + this.results = new ArrayList<>(); + this.resolvedIds = new HashSet<>(); + this.limit = -1; + } + public Entry resolve(int referenceId){ + return resolve(referenceId, null); + } + public synchronized Entry resolve(int referenceId, Predicate filter){ + resolveReference(referenceId, filter); + List results = new ArrayList<>(this.results); + reset(); + if(results.size() > 0){ + return results.get(0); + } + return null; + } + + public List resolveWithConfig(int referenceId, ResConfig resConfig){ + ConfigFilter configFilter = new ConfigFilter(resConfig); + List results = resolveAll(referenceId, configFilter); + results.sort(configFilter); + return results; + } + public List resolveAll(int referenceId){ + return resolveAll(referenceId, (Predicate)null); + } + public synchronized List resolveAll(int referenceId, Predicate filter){ + resolveReference(referenceId, filter); + List results = new ArrayList<>(this.results); + reset(); + return results; + } + private void resolveReference(int referenceId, Predicate filter){ + if(referenceId == 0 || isFinished() || this.resolvedIds.contains(referenceId)){ + return; + } + this.resolvedIds.add(referenceId); + List entryList = listNonNullEntries(referenceId); + List results = this.results; + for(Entry entry:entryList){ + if(isFinished()){ + return; + } + if(results.contains(entry)){ + continue; + } + if(entry.isComplex()){ + addResult(filter, entry); + continue; + } + ResValue resValue = entry.getResValue(); + if(resValue.getValueType() != ValueType.REFERENCE){ + addResult(filter, entry); + continue; + } + resolveReference(resValue.getData(), filter); + } + } + private void reset(){ + this.results.clear(); + this.resolvedIds.clear(); + this.limit = -1; + } + private boolean isFinished(){ + return this.limit >= this.results.size(); + } + private void addResult(Predicate filter, Entry entry){ + if(filter == null || filter.test(entry)){ + this.results.add(entry); + } + } + private List listNonNullEntries(int resourceId){ + List results = new ArrayList<>(); + EntryGroup entryGroup = this.entryStore.getEntryGroup(resourceId); + if(entryGroup==null){ + return results; + } + Iterator itr = entryGroup.iterator(true); + while (itr.hasNext()){ + results.add(itr.next()); + } + return results; + } + + public static class ConfigFilter implements Predicate, Comparator{ + private final ResConfig config; + public ConfigFilter(ResConfig config){ + this.config = config; + } + @Override + public boolean test(Entry entry) { + ResConfig resConfig = entry.getResConfig(); + if(resConfig == null){ + return false; + } + return resConfig.isEqualOrMoreSpecificThan(this.config); + } + @Override + public int compare(Entry entry1, Entry entry2) { + ResConfig config1 = entry1.getResConfig(); + ResConfig config2 = entry1.getResConfig(); + if (config.equals(config1)){ + return -1; + } + if(config.equals(config2)){ + return 1; + } + return 0; + } + } +} diff --git a/src/ARSCLib/com/reandroid/common/TableEntryStore.java b/src/ARSCLib/com/reandroid/common/TableEntryStore.java new file mode 100755 index 00000000..8404410f --- /dev/null +++ b/src/ARSCLib/com/reandroid/common/TableEntryStore.java @@ -0,0 +1,224 @@ + /* + * 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.common; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.FrameworkTable; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.StagedAliasEntry; + +import java.util.*; + +public class TableEntryStore implements EntryStore{ + private final Map> mLocalPackagesMap; + private final Map> mFrameworkPackagesMap; + public TableEntryStore(){ + this.mLocalPackagesMap = new HashMap<>(); + this.mFrameworkPackagesMap = new HashMap<>(); + } + + public String getEntryName(int resourceId){ + Entry entry = getEntry(resourceId); + if(entry ==null){ + return null; + } + return entry.getName(); + } + public Entry getEntry(int resourceId){ + if(resourceId==0){ + return null; + } + EntryGroup entryGroup=getEntryGroup(resourceId); + if(entryGroup==null){ + return null; + } + return entryGroup.pickOne(); + } + public void add(TableBlock tableBlock){ + if(tableBlock==null){ + return; + } + for(PackageBlock packageBlock:tableBlock.listPackages()){ + add(packageBlock); + } + } + public void add(PackageBlock packageBlock){ + if(packageBlock==null){ + return; + } + Set packageBlockSet; + if(packageBlock.getTableBlock() instanceof FrameworkTable){ + packageBlockSet = getOrCreateFrameworks(packageBlock.getId()); + }else { + packageBlockSet = getOrCreateLocal(packageBlock.getId()); + } + if(packageBlockSet.contains(packageBlock)){ + return; + } + packageBlockSet.add(packageBlock); + } + private Set getOrCreateLocal(int packageId){ + Integer id=packageId; + Set packageBlockSet = mLocalPackagesMap.get(id); + if(packageBlockSet==null){ + packageBlockSet=new HashSet<>(); + mLocalPackagesMap.put(id, packageBlockSet); + } + return packageBlockSet; + } + private Set getOrCreateFrameworks(int packageId){ + Integer id=packageId; + Set packageBlockSet = mFrameworkPackagesMap.get(id); + if(packageBlockSet==null){ + packageBlockSet=new HashSet<>(); + mFrameworkPackagesMap.put(id, packageBlockSet); + } + return packageBlockSet; + } + @Override + public List getEntryGroups(int resourceId) { + List results = searchEntryGroupsLocal(resourceId); + if(results.size()>0){ + return results; + } + int alias = searchIdAliasLocal(resourceId); + results = searchEntryGroupsLocal(alias); + if(results.size()>0){ + return results; + } + results = searchEntryGroupsFramework(resourceId); + if(results.size()>0){ + return results; + } + alias = searchIdAliasFramework(resourceId); + return searchEntryGroupsFramework(alias); + } + @Override + public EntryGroup getEntryGroup(int resourceId) { + EntryGroup entryGroup = searchEntryLocal(resourceId); + if(entryGroup==null){ + entryGroup = searchEntryLocal(searchIdAliasLocal(resourceId)); + } + if(entryGroup==null){ + entryGroup = searchEntryFramework(resourceId); + } + if(entryGroup==null){ + entryGroup = searchEntryFramework(searchIdAliasFramework(resourceId)); + } + return entryGroup; + } + @Override + public List getPackageBlocks(int packageId) { + List results=new ArrayList<>(); + packageId = 0xff & packageId; + Set packageBlockSet = mLocalPackagesMap.get(packageId); + if(packageBlockSet==null){ + packageBlockSet = mFrameworkPackagesMap.get(packageId); + } + if(packageBlockSet!=null){ + results.addAll(packageBlockSet); + } + return results; + } + private List searchEntryGroupsLocal(int resourceId) { + if(resourceId==0){ + return new ArrayList<>(); + } + List results=new ArrayList<>(); + int pkgId = (resourceId>>24)&0xff; + Set packageBlockSet = mLocalPackagesMap.get(pkgId); + if(packageBlockSet==null){ + return results; + } + for(PackageBlock packageBlock: packageBlockSet){ + EntryGroup group=packageBlock.getEntryGroup(resourceId); + if(group!=null){ + results.add(group); + } + } + return results; + } + private List searchEntryGroupsFramework(int resourceId) { + if(resourceId==0){ + return new ArrayList<>(); + } + List results=new ArrayList<>(); + int pkgId = (resourceId>>24)&0xff; + Set packageBlockSet = mFrameworkPackagesMap.get(pkgId); + if(packageBlockSet==null){ + return results; + } + for(PackageBlock packageBlock: packageBlockSet){ + EntryGroup group=packageBlock.getEntryGroup(resourceId); + if(group!=null){ + results.add(group); + } + } + return results; + } + private EntryGroup searchEntryLocal(int resourceId) { + int packageId = (resourceId>>24)&0xff; + Set packageBlockSet = mLocalPackagesMap.get(packageId); + if(packageBlockSet==null){ + return null; + } + for(PackageBlock packageBlock: packageBlockSet){ + EntryGroup group=packageBlock.getEntryGroup(resourceId); + if(group!=null && group.pickOne()!=null){ + return group; + } + } + return null; + } + private EntryGroup searchEntryFramework(int resourceId) { + int packageId = (resourceId>>24)&0xff; + Set packageBlockSet = mFrameworkPackagesMap.get(packageId); + if(packageBlockSet==null){ + return null; + } + for(PackageBlock packageBlock: packageBlockSet){ + EntryGroup group=packageBlock.getEntryGroup(resourceId); + if(group!=null && group.pickOne()!=null){ + return group; + } + } + return null; + } + private int searchIdAliasLocal(int resourceId) { + for(Set packageBlockSet : mLocalPackagesMap.values()){ + for(PackageBlock packageBlock:packageBlockSet){ + StagedAliasEntry stagedAliasEntry = packageBlock.searchByStagedResId(resourceId); + if(stagedAliasEntry!=null){ + return stagedAliasEntry.getFinalizedResId(); + } + } + } + return 0; + } + private int searchIdAliasFramework(int resourceId) { + for(Set packageBlockSet : mLocalPackagesMap.values()){ + for(PackageBlock packageBlock:packageBlockSet){ + StagedAliasEntry stagedAliasEntry = packageBlock.searchByStagedResId(resourceId); + if(stagedAliasEntry!=null){ + return stagedAliasEntry.getFinalizedResId(); + } + } + } + return 0; + } +} diff --git a/src/ARSCLib/com/reandroid/identifiers/Identifier.java b/src/ARSCLib/com/reandroid/identifiers/Identifier.java new file mode 100644 index 00000000..76cc4859 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/Identifier.java @@ -0,0 +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. + */ +package com.reandroid.identifiers; + +import com.reandroid.arsc.util.HexUtil; + +public class Identifier implements Comparable{ + private int id; + private String name; + private Identifier mParent; + private Object mTag; + public Identifier(int id, String name){ + this.id = id; + this.name = name; + } + + public Object getTag() { + return mTag; + } + public void setTag(Object tag) { + this.mTag = tag; + } + + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + Identifier getParent() { + return mParent; + } + void setParent(Identifier parent) { + if(parent == this){ + return; + } + this.mParent = parent; + } + public String getHexId(){ + return HexUtil.toHex2((byte) getId()); + } + long getUniqueId(){ + return getId(); + } + + @Override + public int compareTo(Identifier identifier) { + return Long.compare(getUniqueId(), identifier.getUniqueId()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Identifier other = (Identifier) obj; + return this.getUniqueId() == other.getUniqueId(); + } + @Override + public int hashCode() { + return Long.hashCode(getUniqueId()); + } + @Override + public String toString(){ + return getName() + "(" + getHexId() + ")"; + } + + static final String XML_TAG_RESOURCES = "resources"; + static final String XML_TAG_PUBLIC = "public"; + + static final String XML_ATTRIBUTE_ID = "id"; + static final String XML_ATTRIBUTE_NAME = "name"; + static final String XML_ATTRIBUTE_PACKAGE = "package"; + static final String XML_ATTRIBUTE_TYPE = "type"; +} diff --git a/src/ARSCLib/com/reandroid/identifiers/IdentifierMap.java b/src/ARSCLib/com/reandroid/identifiers/IdentifierMap.java new file mode 100644 index 00000000..0dba7372 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/IdentifierMap.java @@ -0,0 +1,151 @@ +/* + * 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.identifiers; + +import java.util.*; + +class IdentifierMap extends Identifier + implements Comparator { + private final Object mLock = new Object(); + private final Map idMap; + private final Map nameMap; + + public IdentifierMap(int id, String name){ + super(id, name); + this.idMap = new HashMap<>(); + this.nameMap = new HashMap<>(); + } + public List listDuplicates(){ + List results = new ArrayList<>(getItems()); + Set uniques = new HashSet<>(); + for(CHILD item : getItems()){ + String name = item.getName(); + if(uniques.contains(name)){ + results.add(item); + }else { + uniques.add(name); + } + } + results.sort(this); + return results; + } + public boolean hasDuplicates(){ + Set uniques = new HashSet<>(); + for(CHILD item : getItems()){ + String name = item.getName(); + if(uniques.contains(name)){ + return true; + }else { + uniques.add(name); + } + } + return false; + } + public List listNames(){ + List results = new ArrayList<>(size()); + for(CHILD item : list()){ + results.add(item.getName()); + } + return results; + } + public List list(){ + List childList = new ArrayList<>(getItems()); + childList.sort(this); + return childList; + } + public Collection getItems(){ + synchronized (mLock){ + return this.idMap.values(); + } + } + public void clear(){ + synchronized (mLock){ + this.idMap.clear(); + this.nameMap.clear(); + } + } + public CHILD getByTag(Object tag){ + for(CHILD item : getItems()){ + if(Objects.equals(tag, item.getTag())){ + return item; + } + } + return null; + } + public int size(){ + synchronized (mLock){ + return this.idMap.size(); + } + } + public CHILD get(String childName){ + synchronized (mLock){ + return this.nameMap.get(childName); + } + } + public CHILD get(int childId){ + synchronized (mLock){ + return this.idMap.get(childId); + } + } + public void remove(CHILD entry){ + synchronized (mLock){ + if(entry == null){ + return; + } + this.idMap.remove(entry.getId()); + this.nameMap.remove(entry.getName()); + } + } + public CHILD add(CHILD child){ + synchronized (mLock){ + if(child == null){ + return null; + } + child.setParent(this); + Integer entryId = child.getId(); + CHILD exist = this.idMap.get(entryId); + if(exist != null){ + if(exist.getName() == null){ + exist.setName(child.getName()); + addNameMap(exist); + } + return exist; + } + this.idMap.put(entryId, child); + addNameMap(child); + return child; + } + } + private void addNameMap(CHILD child){ + String childName = child.getName(); + if(childName == null){ + return; + } + CHILD exist = this.nameMap.get(childName); + if(exist != null){ + return; + } + this.nameMap.put(childName, child); + } + @Override + public int compare(CHILD child1, CHILD child2) { + return child1.compareTo(child2); + } + @Override + public String toString(){ + return super.toString() + " entries = " + size(); + } +} diff --git a/src/ARSCLib/com/reandroid/identifiers/PackageIdentifier.java b/src/ARSCLib/com/reandroid/identifiers/PackageIdentifier.java new file mode 100644 index 00000000..8f4334c5 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/PackageIdentifier.java @@ -0,0 +1,394 @@ +/* + * 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.identifiers; + +import com.android.org.kxml2.io.KXmlParser; +import com.android.org.kxml2.io.KXmlSerializer; +import com.reandroid.arsc.array.EntryArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TypeBlock; +import com.reandroid.arsc.decoder.ValueDecoder; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.item.SpecString; +import com.reandroid.arsc.pool.SpecStringPool; +import com.reandroid.arsc.pool.TypeStringPool; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueHeader; +import com.reandroid.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; + +public class PackageIdentifier extends IdentifierMap{ + private PackageBlock mPackageBlock; + public PackageIdentifier(int id, String name){ + super(id, name); + } + public PackageIdentifier(){ + this(0, null); + } + + public void initialize(PackageBlock packageBlock){ + initialize(packageBlock, true); + } + public void initialize(PackageBlock packageBlock, boolean initialize_ids){ + packageBlock.setId(getId()); + String name = getName(); + if(name != null){ + packageBlock.setName(name); + } + initializeTypeName(packageBlock.getTypeStringPool()); + initializeSpecNames(packageBlock.getSpecStringPool()); + if(initialize_ids){ + initializeIds(packageBlock); + } + initializePackageJson(packageBlock); + setPackageBlock(packageBlock); + } + private void initializeTypeName(TypeStringPool typeStringPool){ + for(TypeIdentifier ti : list()){ + typeStringPool.getOrCreate(ti.getId(), ti.getName()); + } + } + private void initializeSpecNames(SpecStringPool specStringPool){ + List nameList = new ArrayList<>(getResourcesCount()); + for(TypeIdentifier ti : list()){ + nameList.addAll(ti.listNames()); + } + specStringPool.addStrings(nameList); + } + private void initializeIds(PackageBlock packageBlock){ + TypeIdentifier identifierID = get("id"); + if(identifierID == null){ + return; + } + TypeBlock typeBlock = packageBlock + .getOrCreateTypeBlock("", "id"); + EntryArray entryArray = typeBlock.getEntryArray(); + entryArray.ensureSize(identifierID.size()); + SpecStringPool specStringPool = packageBlock.getSpecStringPool(); + for(ResourceIdentifier ri : identifierID.list()){ + Entry entry = entryArray.getOrCreate((short) ri.getId()); + SpecString specString = specStringPool.getOrCreate(ri.getName()); + if(!entry.isNull() && !entry.isComplex()){ + entry.setSpecReference(specString); + continue; + } + entry.setValueAsBoolean(false); + entry.setSpecReference(specString); + setIdEntryVisibility(entry); + } + } + private void setIdEntryVisibility(Entry entry){ + ValueHeader valueHeader = entry.getHeader(); + valueHeader.setWeak(true); + valueHeader.setPublic(true); + } + private void initializePackageJson(PackageBlock packageBlock){ + File jsonFile = searchPackageJsonFromTag(); + if(jsonFile == null){ + return; + } + try { + JSONObject jsonObject = new JSONObject(new FileInputStream(jsonFile)); + packageBlock.fromJson(jsonObject); + if(getName() == null){ + setName(packageBlock.getName()); + } + } catch (FileNotFoundException ignored) { + } + } + // public.xml file is assumed to be stored via setTag during loadPublicXml(File) + private File searchPackageJsonFromTag(){ + Object tag = getTag(); + if(!(tag instanceof File)){ + return null; + } + File publicXml = (File) tag; + File dir = publicXml.getParentFile(); + //values + if(dir == null || !"values".equals(dir.getName())){ + return null; + } + dir = dir.getParentFile(); + //res + if(dir == null){ + return null; + } + dir = dir.getParentFile(); + if(dir == null){ + return null; + } + File json = new File(dir, "package.json"); + if(!json.isFile()){ + return null; + } + return json; + } + public List listDuplicateResources(){ + List results = new ArrayList<>(); + for(TypeIdentifier typeIdentifier : list()){ + results.addAll(typeIdentifier.listDuplicates()); + } + return results; + } + public boolean hasDuplicateResources(){ + for(TypeIdentifier typeIdentifier : getItems()){ + if(typeIdentifier.hasDuplicates()){ + return true; + } + } + return false; + } + public ResourceIdentifier getResourceIdentifier(int resourceId){ + TypeIdentifier typeIdentifier = get((resourceId >> 16) & 0xff); + if(typeIdentifier != null){ + return typeIdentifier.get(resourceId & 0xffff); + } + return null; + } + public ResourceIdentifier getResourceIdentifier(String referenceString){ + if(referenceString == null){ + return null; + } + Matcher matcher = ValueDecoder.PATTERN_REFERENCE.matcher(referenceString); + if(!matcher.find()){ + return null; + } + return getResourceIdentifier(matcher.group(4), matcher.group(5)); + } + public ResourceIdentifier getResourceIdentifier(String type, String name){ + TypeIdentifier typeIdentifier = get(type); + if(typeIdentifier != null){ + return typeIdentifier.get(name); + } + return null; + } + public int getResourcesCount(){ + int result = 0; + for(TypeIdentifier ti : getItems()){ + result += ti.size(); + } + return result; + } + + public void writePublicXml(File file) throws IOException { + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(file); + writePublicXml(outputStream); + outputStream.close(); + } + public void writePublicXml(OutputStream outputStream) throws IOException { + XmlSerializer serializer = new KXmlSerializer(); + serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); + write(serializer); + } + public void write(XmlSerializer serializer) throws IOException { + serializer.startDocument("utf-8", null); + serializer.text("\n"); + serializer.startTag(null, XML_TAG_RESOURCES); + writePackageInfo(serializer); + writeTypes(serializer); + serializer.text("\n"); + serializer.endTag(null, XML_TAG_RESOURCES); + serializer.endDocument(); + closeSerializer(serializer); + } + private void writePackageInfo(XmlSerializer serializer) throws IOException { + String name = getName(); + if(name != null){ + serializer.attribute(null, XML_ATTRIBUTE_PACKAGE, name); + } + int id = getId(); + if(id != 0){ + serializer.attribute(null, XML_ATTRIBUTE_ID, HexUtil.toHex2((byte)id)); + } + } + private void writeTypes(XmlSerializer serializer) throws IOException { + for(TypeIdentifier typeIdentifier : list()){ + typeIdentifier.write(serializer); + } + } + public void load(PackageBlock packageBlock){ + setId(packageBlock.getId()); + setName(packageBlock.getName()); + for(EntryGroup entryGroup : packageBlock.listEntryGroup()){ + add(entryGroup); + } + setTag(packageBlock); + } + public void loadPublicXml(File file) throws IOException, XmlPullParserException { + FileInputStream fileInputStream = new FileInputStream(file); + loadPublicXml(fileInputStream); + fileInputStream.close(); + } + public void loadPublicXml(InputStream inputStream) throws IOException, XmlPullParserException { + XmlPullParser parser = new KXmlParser(); + parser.setInput(inputStream, StandardCharsets.UTF_8.name()); + loadPublicXml(parser); + } + public void loadPublicXml(Reader reader) throws IOException, XmlPullParserException { + XmlPullParser parser = new KXmlParser(); + parser.setInput(reader); + loadPublicXml(parser); + } + public void loadPublicXml(XmlPullParser parser) throws IOException, XmlPullParserException { + boolean resourcesFound = false; + int event; + while ((event = parser.nextToken()) != XmlPullParser.END_DOCUMENT){ + if(event != XmlPullParser.START_TAG){ + continue; + } + if(!resourcesFound){ + resourcesFound = parser.getName().equals(XML_TAG_RESOURCES); + if(!resourcesFound){ + throw new XmlPullParserException("Invalid public.xml, expecting first tag '" + + getName() + "' " + parser.getPositionDescription()); + } + loadPackageInfo(parser); + continue; + } + parseEntry(parser); + } + closeParser(parser); + } + private void closeParser(XmlPullParser parser){ + if(!(parser instanceof Closeable)){ + return; + } + Closeable closeable = (Closeable)parser; + try { + closeable.close(); + } catch (IOException ignored) { + } + } + private void closeSerializer(XmlSerializer serializer){ + if(!(serializer instanceof Closeable)){ + return; + } + Closeable closeable = (Closeable)serializer; + try { + closeable.close(); + } catch (IOException ignored) { + } + } + private void loadPackageInfo(XmlPullParser parser){ + int count = parser.getAttributeCount(); + for(int i = 0; i < count; i++){ + if(XML_ATTRIBUTE_PACKAGE.equals(parser.getAttributeName(i))){ + setName(parser.getAttributeValue(i)); + }else if(XML_ATTRIBUTE_ID.equals(parser.getAttributeName(i))){ + int id = Integer.decode(parser.getAttributeValue(i)); + if(id != 0){ + setId(id); + } + } + } + } + private void parseEntry(XmlPullParser parser) throws XmlPullParserException { + if(!XML_TAG_PUBLIC.equals(parser.getName())){ + throw new XmlPullParserException("Invalid tag, expecting '" + + XML_TAG_PUBLIC + "' " + parser.getPositionDescription()); + } + String resourceIdStr = null; + String typeName = null; + String entryName = null; + int count = parser.getAttributeCount(); + for(int i = 0; i < count; i++){ + String attrName = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if(XML_ATTRIBUTE_ID.equals(attrName)){ + resourceIdStr = value; + }else if(XML_ATTRIBUTE_TYPE.equals(attrName)){ + typeName = value; + }else if(XML_ATTRIBUTE_NAME.equals(attrName)){ + entryName = value; + } + } + if(typeName == null){ + throw new XmlPullParserException("Missing attribute '" + + XML_ATTRIBUTE_TYPE + "' " + parser.getPositionDescription()); + } + if(resourceIdStr == null){ + throw new XmlPullParserException("Missing attribute '" + + XML_ATTRIBUTE_ID + "' " + parser.getPositionDescription()); + } + if(entryName == null){ + throw new XmlPullParserException("Missing attribute '" + + XML_ATTRIBUTE_NAME + "' " + parser.getPositionDescription()); + } + + int resourceId = (int) Long.decode(resourceIdStr).longValue(); + int packageId = (resourceId >> 24) & 0xff; + int typeId = (resourceId >> 16) & 0xff; + int entryId = resourceId & 0xffff; + + TypeIdentifier typeIdentifier = getOrCreate(typeId, typeName); + ResourceIdentifier entry = new ResourceIdentifier(entryId, entryName); + typeIdentifier.add(entry); + if(getId() == 0){ + setId(packageId); + } + } + + public PackageBlock getPackageBlock() { + return mPackageBlock; + } + public void setPackageBlock(PackageBlock packageBlock) { + this.mPackageBlock = packageBlock; + } + + public void add(EntryGroup entryGroup){ + add(entryGroup.pickOne()); + } + public void add(Entry entry){ + if(entry == null || entry.isNull()){ + return; + } + TypeBlock typeBlock = entry.getTypeBlock(); + TypeIdentifier typeIdentifier = getOrCreate(typeBlock.getId(), typeBlock.getTypeName()); + ResourceIdentifier resourceIdentifier = new ResourceIdentifier(entry.getId(), entry.getName()); + typeIdentifier.add(resourceIdentifier); + } + public TypeIdentifier getOrCreate(int typeId, String typeName){ + TypeIdentifier identifier = get(typeId); + if(identifier == null){ + return super.add(new TypeIdentifier(typeId, typeName)); + } + if(typeName !=null && identifier.getName() == null){ + identifier.setName(typeName); + identifier = super.add(identifier); + } + return identifier; + } + @Override + public void clear(){ + for(TypeIdentifier identifier : getItems()){ + identifier.clear(); + } + super.clear(); + } +} diff --git a/src/ARSCLib/com/reandroid/identifiers/ResourceIdentifier.java b/src/ARSCLib/com/reandroid/identifiers/ResourceIdentifier.java new file mode 100644 index 00000000..4bbb78a4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/ResourceIdentifier.java @@ -0,0 +1,147 @@ +/* + * 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.identifiers; + +import com.reandroid.arsc.util.HexUtil; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + +public class ResourceIdentifier extends Identifier{ + public ResourceIdentifier(int id, String name){ + super(id, name); + } + public ResourceIdentifier(){ + this(0, null); + } + + + public void write(XmlSerializer serializer) throws IOException { + serializer.text("\n "); + serializer.startTag(null, XML_TAG_PUBLIC); + serializer.attribute(null, XML_ATTRIBUTE_ID, getHexId()); + serializer.attribute(null, TypeIdentifier.XML_ATTRIBUTE_TYPE, getTypeName()); + serializer.attribute(null, XML_ATTRIBUTE_NAME, getName()); + serializer.endTag(null, XML_TAG_PUBLIC); + } + public TypeIdentifier getTypeIdentifier() { + return (TypeIdentifier) getParent(); + } + public void setTypeIdentifier(TypeIdentifier typeIdentifier) { + setParent(typeIdentifier); + } + public PackageIdentifier getPackageIdentifier(){ + TypeIdentifier typeIdentifier = getTypeIdentifier(); + if(typeIdentifier != null){ + return typeIdentifier.getPackageIdentifier(); + } + return null; + } + public String getTypeName(){ + TypeIdentifier typeIdentifier = getTypeIdentifier(); + if(typeIdentifier != null){ + return typeIdentifier.getName(); + } + return null; + } + public String getPackageName(){ + TypeIdentifier typeIdentifier = getTypeIdentifier(); + if(typeIdentifier != null){ + return typeIdentifier.getPackageName(); + } + return null; + } + public int getTypeId(){ + TypeIdentifier typeIdentifier = getTypeIdentifier(); + if(typeIdentifier != null){ + return typeIdentifier.getId(); + } + return 0; + } + public int getPackageId(){ + TypeIdentifier typeIdentifier = getTypeIdentifier(); + if(typeIdentifier != null){ + return typeIdentifier.getPackageId(); + } + return 0; + } + public int getResourceId(){ + int resourceId = getPackageId() << 24; + resourceId |= getTypeId() << 16; + resourceId |= getId(); + return resourceId; + } + @Override + public void setId(int id) { + super.setId(id & 0xffff); + } + @Override + public String getHexId(){ + return HexUtil.toHex8(getResourceId()); + } + public String getResourceName(){ + return getResourceName(null); + } + public String getResourceName(PackageIdentifier context){ + boolean appendPackage = context != getPackageIdentifier(); + return getResourceName('@', appendPackage, true); + } + public String getResourceName(char prefix, boolean appendPackage, boolean appendType){ + String packageName = appendPackage ? getPackageName() : null; + String typeName = appendType ? getTypeName() : null; + return buildResourceName(prefix, packageName, typeName, getName()); + } + @Override + long getUniqueId(){ + return 0x00000000ffffffffL & this.getResourceId(); + } + @Override + public void setTag(Object tag){ + TypeIdentifier ti = getTypeIdentifier(); + if(ti == null){ + super.setTag(tag); + return; + } + Object exist = getTag(); + if(exist != null){ + ti.removeTag(exist); + } + ti.addTag(tag, this); + super.setTag(tag); + } + @Override + public String toString(){ + return getHexId() + " " + getResourceName(); + } + + public static String buildResourceName(char prefix, String packageName, String type, String entry){ + StringBuilder builder = new StringBuilder(); + if(prefix != 0){ + builder.append(prefix); + } + if(packageName != null){ + builder.append(packageName); + builder.append(':'); + } + if(type != null){ + builder.append(type); + builder.append('/'); + } + builder.append(entry); + return builder.toString(); + } + +} diff --git a/src/ARSCLib/com/reandroid/identifiers/TableIdentifier.java b/src/ARSCLib/com/reandroid/identifiers/TableIdentifier.java new file mode 100644 index 00000000..97e3c2c0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/TableIdentifier.java @@ -0,0 +1,159 @@ +/* + * 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.identifiers; + +import com.reandroid.arsc.array.PackageArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.*; +import java.util.*; + +public class TableIdentifier{ + private final List mPackages; + private final Map mNameMap; + public TableIdentifier() { + this.mPackages = new ArrayList<>(); + this.mNameMap = new HashMap<>(); + } + + public void initialize(TableBlock tableBlock){ + initialize(tableBlock, true); + } + public void initialize(TableBlock tableBlock, boolean initialize_ids){ + PackageArray packageArray = tableBlock.getPackageArray(); + for(PackageIdentifier pi : getPackages()){ + PackageBlock packageBlock = packageArray.createNext(); + pi.initialize(packageBlock, initialize_ids); + } + } + + public void load(TableBlock tableBlock){ + for(PackageBlock packageBlock : tableBlock.listPackages()){ + load(packageBlock); + } + } + public PackageIdentifier load(PackageBlock packageBlock){ + PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.load(packageBlock); + add(packageIdentifier); + mNameMap.put(packageIdentifier.getName(), packageIdentifier); + return packageIdentifier; + } + public void loadPublicXml(Collection pubXmlFileList) throws IOException { + for(File file : pubXmlFileList){ + try { + loadPublicXml(file); + } catch (XmlPullParserException ex) { + throw new IOException(ex); + } + } + } + public PackageIdentifier loadPublicXml(File file) throws IOException, XmlPullParserException { + PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.loadPublicXml(file); + add(packageIdentifier); + packageIdentifier.setTag(file); + return packageIdentifier; + } + public PackageIdentifier loadPublicXml(InputStream inputStream) throws IOException, XmlPullParserException { + PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.loadPublicXml(inputStream); + add(packageIdentifier); + return packageIdentifier; + } + public PackageIdentifier loadPublicXml(Reader reader) throws IOException, XmlPullParserException {PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.loadPublicXml(reader); + add(packageIdentifier); + return packageIdentifier; + } + public PackageIdentifier loadPublicXml(XmlPullParser parser) throws IOException, XmlPullParserException { + PackageIdentifier packageIdentifier = new PackageIdentifier(); + packageIdentifier.loadPublicXml(parser); + add(packageIdentifier); + return packageIdentifier; + } + public ResourceIdentifier get(String packageName, String type, String name){ + PackageIdentifier packageIdentifier = mNameMap.get(packageName); + if(packageIdentifier != null){ + ResourceIdentifier ri = packageIdentifier.getResourceIdentifier(type, name); + if(ri != null){ + return ri; + } + } + for(PackageIdentifier pi : getPackages()){ + if(Objects.equals(packageName, pi.getName())){ + ResourceIdentifier ri = pi.getResourceIdentifier(type, name); + if(ri != null){ + return ri; + } + } + } + return null; + } + public ResourceIdentifier get(String type, String name){ + for(PackageIdentifier pi : getPackages()){ + ResourceIdentifier ri = pi.getResourceIdentifier(type, name); + if(ri != null){ + return ri; + } + } + return null; + } + public int countPackages(){ + return getPackages().size(); + } + public void add(PackageIdentifier packageIdentifier){ + if(packageIdentifier != null){ + mPackages.add(packageIdentifier); + } + } + public List getPackages() { + return mPackages; + } + public PackageIdentifier getByTag(Object tag){ + for(PackageIdentifier pi : getPackages()){ + if(Objects.equals(tag, pi.getTag())){ + return pi; + } + } + return null; + } + public PackageIdentifier getByPackage(PackageBlock packageBlock){ + for(PackageIdentifier pi : getPackages()){ + if(packageBlock == pi.getPackageBlock()){ + return pi; + } + } + return null; + } + public void clear(){ + for(PackageIdentifier identifier : getPackages()){ + identifier.clear(); + } + mPackages.clear(); + mNameMap.clear(); + } + + @Override + public String toString(){ + return getClass().getSimpleName() + + ": packages = " + + countPackages(); + } +} diff --git a/src/ARSCLib/com/reandroid/identifiers/TypeIdentifier.java b/src/ARSCLib/com/reandroid/identifiers/TypeIdentifier.java new file mode 100644 index 00000000..b51a1555 --- /dev/null +++ b/src/ARSCLib/com/reandroid/identifiers/TypeIdentifier.java @@ -0,0 +1,89 @@ +/* + * 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.identifiers; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class TypeIdentifier extends IdentifierMap { + private final Map tagMap; + public TypeIdentifier(int id, String name){ + super(id, name); + this.tagMap = new HashMap<>(); + } + public TypeIdentifier(){ + this(0, null); + } + + + public void write(XmlSerializer serializer) throws IOException { + for(ResourceIdentifier resourceIdentifier : list()){ + resourceIdentifier.write(serializer); + } + } + public PackageIdentifier getPackageIdentifier() { + return (PackageIdentifier) getParent(); + } + public void setPackageIdentifier(PackageIdentifier packageIdentifier) { + setParent(packageIdentifier); + } + public String getPackageName(){ + PackageIdentifier packageIdentifier = getPackageIdentifier(); + if(packageIdentifier != null){ + return packageIdentifier.getName(); + } + return null; + } + public int getPackageId(){ + PackageIdentifier packageIdentifier = getPackageIdentifier(); + if(packageIdentifier != null){ + return packageIdentifier.getId(); + } + return 0; + } + + @Override + public ResourceIdentifier getByTag(Object tag){ + ResourceIdentifier ri = this.tagMap.get(tag); + if(ri != null){ + return ri; + } + return super.getByTag(tag); + } + @Override + public void clear(){ + tagMap.clear(); + super.clear(); + } + @Override + long getUniqueId(){ + int uniqueId = getPackageId() << 8; + uniqueId |= getId(); + return uniqueId; + } + void addTag(Object tag, ResourceIdentifier ri){ + if(tag != null){ + tagMap.put(tag, ri); + } + } + void removeTag(Object tag){ + tagMap.remove(tag); + } + +} diff --git a/src/ARSCLib/com/reandroid/json/CDL.java b/src/ARSCLib/com/reandroid/json/CDL.java new file mode 100644 index 00000000..ae47086d --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/CDL.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class CDL { + + private static String getValue(JSONTokener x) throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = x.next(); + } while (c == ' ' || c == '\t'); + switch (c) { + case 0: + return null; + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = x.next(); + if (c == q) { + //Handle escaped double-quote + char nextC = x.next(); + if(nextC != '\"') { + // if our quote was the end of the file, don't step + if(nextC > 0) { + x.back(); + } + break; + } + } + if (c == 0 || c == '\n' || c == '\r') { + throw x.syntaxError("Missing close quote '" + q + "'."); + } + sb.append(c); + } + return sb.toString(); + case ',': + x.back(); + return ""; + default: + x.back(); + return x.nextTo(','); + } + } + + public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { + JSONArray ja = new JSONArray(); + for (;;) { + String value = getValue(x); + char c = x.next(); + if (value == null || + (ja.length() == 0 && value.length() == 0 && c != ',')) { + return null; + } + ja.put(value); + for (;;) { + if (c == ',') { + break; + } + if (c != ' ') { + if (c == '\n' || c == '\r' || c == 0) { + return ja; + } + throw x.syntaxError("Bad character '" + c + "' (" + + (int)c + ")."); + } + c = x.next(); + } + } + } + + public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) + throws JSONException { + JSONArray ja = rowToJSONArray(x); + return ja != null ? ja.toJSONObject(names) : null; + } + + public static String rowToString(JSONArray ja) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + if (i > 0) { + sb.append(','); + } + Object object = ja.opt(i); + if (object != null) { + String string = object.toString(); + if (string.length() > 0 && (string.indexOf(',') >= 0 || + string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || + string.indexOf(0) >= 0 || string.charAt(0) == '"')) { + sb.append('"'); + int length = string.length(); + for (int j = 0; j < length; j += 1) { + char c = string.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } else { + sb.append(string); + } + } + } + sb.append('\n'); + return sb.toString(); + } + + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new JSONTokener(string)); + } + + public static JSONArray toJSONArray(JSONTokener x) throws JSONException { + return toJSONArray(rowToJSONArray(x), x); + } + + public static JSONArray toJSONArray(JSONArray names, String string) + throws JSONException { + return toJSONArray(names, new JSONTokener(string)); + } + + public static JSONArray toJSONArray(JSONArray names, JSONTokener x) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (;;) { + JSONObject jo = rowToJSONObject(names, x); + if (jo == null) { + break; + } + ja.put(jo); + } + if (ja.length() == 0) { + return null; + } + return ja; + } + public static String toString(JSONArray ja) throws JSONException { + JSONObject jo = ja.optJSONObject(0); + if (jo != null) { + JSONArray names = jo.names(); + if (names != null) { + return rowToString(names) + toString(names, ja); + } + } + return null; + } + + public static String toString(JSONArray names, JSONArray ja) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + JSONObject jo = ja.optJSONObject(i); + if (jo != null) { + sb.append(rowToString(jo.toJSONArray(names))); + } + } + return sb.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/json/Cookie.java b/src/ARSCLib/com/reandroid/json/Cookie.java new file mode 100644 index 00000000..1443fac2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/Cookie.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.util.Locale; + +public class Cookie { + + public static String escape(String string) { + char c; + String s = string.trim(); + int length = s.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i += 1) { + c = s.charAt(i); + if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { + sb.append('%'); + sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); + sb.append(Character.forDigit((char)(c & 0x0f), 16)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + public static JSONObject toJSONObject(String string) { + final JSONObject jo = new JSONObject(); + String name; + Object value; + + + JSONTokener x = new JSONTokener(string); + + name = unescape(x.nextTo('=').trim()); + //per RFC6265, if the name is blank, the cookie should be ignored. + if("".equals(name)) { + throw new JSONException("Cookies must have a 'name'"); + } + jo.put("name", name); + // per RFC6265, if there is no '=', the cookie should be ignored. + // the 'next' call here throws an exception if the '=' is not found. + x.next('='); + jo.put("value", unescape(x.nextTo(';')).trim()); + // discard the ';' + x.next(); + // parse the remaining cookie attributes + while (x.more()) { + name = unescape(x.nextTo("=;")).trim().toLowerCase(Locale.ROOT); + // don't allow a cookies attributes to overwrite it's name or value. + if("name".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'name'"); + } + if("value".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'value'"); + } + // check to see if it's a flag property + if (x.next() != '=') { + value = Boolean.TRUE; + } else { + value = unescape(x.nextTo(';')).trim(); + x.next(); + } + // only store non-blank attributes + if(!"".equals(name) && !"".equals(value)) { + jo.put(name, value); + } + } + return jo; + } + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + + String name = null; + Object value = null; + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key)) { + name = jo.getString(key).trim(); + } + if("value".equalsIgnoreCase(key)) { + value=jo.getString(key).trim(); + } + if(name != null && value != null) { + break; + } + } + + if(name == null || "".equals(name.trim())) { + throw new JSONException("Cookie does not have a name"); + } + if(value == null) { + value = ""; + } + + sb.append(escape(name)); + sb.append("="); + sb.append(escape((String)value)); + + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key) + || "value".equalsIgnoreCase(key)) { + // already processed above + continue; + } + value = jo.opt(key); + if(value instanceof Boolean) { + if(Boolean.TRUE.equals(value)) { + sb.append(';').append(escape(key)); + } + // don't emit false values + } else { + sb.append(';') + .append(escape(key)) + .append('=') + .append(escape(value.toString())); + } + } + + return sb.toString(); + } + + public static String unescape(String string) { + int length = string.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + char c = string.charAt(i); + if (c == '+') { + c = ' '; + } else if (c == '%' && i + 2 < length) { + int d = JSONTokener.dehexchar(string.charAt(i + 1)); + int e = JSONTokener.dehexchar(string.charAt(i + 2)); + if (d >= 0 && e >= 0) { + c = (char)(d * 16 + e); + i += 2; + } + } + sb.append(c); + } + return sb.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/json/CookieList.java b/src/ARSCLib/com/reandroid/json/CookieList.java new file mode 100644 index 00000000..52a63399 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/CookieList.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class CookieList { + + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + JSONTokener x = new JSONTokener(string); + while (x.more()) { + String name = Cookie.unescape(x.nextTo('=')); + x.next('='); + jo.put(name, Cookie.unescape(x.nextTo(';'))); + x.next(); + } + return jo; + } + + public static String toString(JSONObject jo) throws JSONException { + boolean b = false; + final StringBuilder sb = new StringBuilder(); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + final Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + if (b) { + sb.append(';'); + } + sb.append(Cookie.escape(key)); + sb.append("="); + sb.append(Cookie.escape(value.toString())); + b = true; + } + } + return sb.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/json/HTTP.java b/src/ARSCLib/com/reandroid/json/HTTP.java new file mode 100644 index 00000000..10f5e42c --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/HTTP.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.util.Locale; + +public class HTTP { + + /** Carriage return/line feed. */ + public static final String CRLF = "\r\n"; + + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + HTTPTokener x = new HTTPTokener(string); + String token; + + token = x.nextToken(); + if (token.toUpperCase(Locale.ROOT).startsWith("HTTP")) { + +// Response + + jo.put("HTTP-Version", token); + jo.put("Status-Code", x.nextToken()); + jo.put("Reason-Phrase", x.nextTo('\0')); + x.next(); + + } else { + +// Request + + jo.put("Method", token); + jo.put("Request-URI", x.nextToken()); + jo.put("HTTP-Version", x.nextToken()); + } + +// Fields + + while (x.more()) { + String name = x.nextTo(':'); + x.next(':'); + jo.put(name, x.nextTo('\0')); + x.next(); + } + return jo; + } + + + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { + sb.append(jo.getString("HTTP-Version")); + sb.append(' '); + sb.append(jo.getString("Status-Code")); + sb.append(' '); + sb.append(jo.getString("Reason-Phrase")); + } else if (jo.has("Method") && jo.has("Request-URI")) { + sb.append(jo.getString("Method")); + sb.append(' '); + sb.append('"'); + sb.append(jo.getString("Request-URI")); + sb.append('"'); + sb.append(' '); + sb.append(jo.getString("HTTP-Version")); + } else { + throw new JSONException("Not enough material for an HTTP header."); + } + sb.append(CRLF); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + String value = jo.optString(key); + if (!"HTTP-Version".equals(key) && !"Status-Code".equals(key) && + !"Reason-Phrase".equals(key) && !"Method".equals(key) && + !"Request-URI".equals(key) && !JSONObject.NULL.equals(value)) { + sb.append(key); + sb.append(": "); + sb.append(jo.optString(key)); + sb.append(CRLF); + } + } + sb.append(CRLF); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/ARSCLib/com/reandroid/json/HTTPTokener.java b/src/ARSCLib/com/reandroid/json/HTTPTokener.java new file mode 100644 index 00000000..67f559d0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/HTTPTokener.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class HTTPTokener extends JSONTokener { + + public HTTPTokener(String string) { + super(string); + } + public String nextToken() throws JSONException { + char c; + char q; + StringBuilder sb = new StringBuilder(); + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == '"' || c == '\'') { + q = c; + for (;;) { + c = next(); + if (c < ' ') { + throw syntaxError("Unterminated string."); + } + if (c == q) { + return sb.toString(); + } + sb.append(c); + } + } + for (;;) { + if (c == 0 || Character.isWhitespace(c)) { + return sb.toString(); + } + sb.append(c); + c = next(); + } + } +} diff --git a/src/ARSCLib/com/reandroid/json/JSONArray.java b/src/ARSCLib/com/reandroid/json/JSONArray.java new file mode 100644 index 00000000..b51848a0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONArray.java @@ -0,0 +1,777 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import com.reandroid.common.FileChannelInputStream; + +import java.io.*; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class JSONArray extends JSONItem implements Iterable { + + private final ArrayList myArrayList; + + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + public JSONArray(Collection collection) { + if (collection == null) { + this.myArrayList = new ArrayList(); + } else { + this.myArrayList = new ArrayList(collection.size()); + this.addAll(collection, true); + } + } + + public JSONArray(Iterable iter) { + this(); + if (iter == null) { + return; + } + this.addAll(iter, true); + } + + public JSONArray(JSONArray array) { + if (array == null) { + this.myArrayList = new ArrayList(); + } else { + // shallow copy directly the internal array lists as any wrapping + // should have been done already in the original JSONArray + this.myArrayList = new ArrayList(array.myArrayList); + } + } + + JSONArray(Object array) throws JSONException { + this(); + if (!array.getClass().isArray()) { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + this.addAll(array, true); + } + + public JSONArray(int initialCapacity) throws JSONException { + if (initialCapacity < 0) { + throw new JSONException( + "JSONArray initial capacity cannot be negative."); + } + this.myArrayList = new ArrayList(initialCapacity); + } + + public JSONArray(File file) throws IOException { + this(new FileChannelInputStream(file)); + } + public JSONArray(Reader reader){ + this(new JSONTokener(reader)); + } + public JSONArray(InputStream inputStream) throws JSONException { + this(new JSONTokener(inputStream)); + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + public ArrayList getArrayList(){ + return myArrayList; + } + + @Override + public Iterator iterator() { + return this.myArrayList.iterator(); + } + + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(index, "boolean", null); + } + + public double getDouble(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "double", e); + } + } + + public float getFloat(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Float)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "float", e); + } + } + + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "number", e); + } + } + + public > E getEnum(Class clazz, int index) throws JSONException { + E val = optEnum(clazz, index); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(index, "enum of type " + + JSONObject.quote(clazz.getSimpleName()), null); + } + return val; + } + + public BigDecimal getBigDecimal (int index) throws JSONException { + Object object = this.get(index); + BigDecimal val = JSONObject.objectToBigDecimal(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigDecimal", object, null); + } + return val; + } + + public BigInteger getBigInteger (int index) throws JSONException { + Object object = this.get(index); + BigInteger val = JSONObject.objectToBigInteger(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigInteger", object, null); + } + return val; + } + + public int getInt(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "int", e); + } + } + + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(index, "JSONArray", null); + } + + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(index, "JSONObject", null); + } + + public long getLong(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "long", e); + } + } + + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(index, "String", null); + } + + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + public String join(String separator) throws JSONException { + int len = this.length(); + if (len == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder( + JSONObject.valueToString(this.myArrayList.get(0))); + + for (int i = 1; i < len; i++) { + sb.append(separator) + .append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + public int length() { + return this.myArrayList.size(); + } + + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList + .get(index); + } + + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + public double optDouble(int index, double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + public float optFloat(int index, float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + + public int optInt(int index) { + return this.optInt(index, 0); + } + + public int optInt(int index, int defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + public > E optEnum(Class clazz, int index) { + return this.optEnum(clazz, index, null); + } + + public > E optEnum(Class clazz, int index, E defaultValue) { + try { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigInteger(val, defaultValue); + } + + public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigDecimal(val, defaultValue); + } + + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + public long optLong(int index) { + return this.optLong(index, 0); + } + + public long optLong(int index, long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + public String optString(int index) { + return this.optString(index, ""); + } + + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object + .toString(); + } + + public JSONArray put(boolean value) { + return this.put(value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONArray put(Collection value) { + return this.put(new JSONArray(value)); + } + + public JSONArray put(double value) throws JSONException { + return this.put(Double.valueOf(value)); + } + + + public JSONArray put(float value) throws JSONException { + return this.put(Float.valueOf(value)); + } + + public JSONArray put(int value) { + return this.put(Integer.valueOf(value)); + } + + public JSONArray put(long value) { + return this.put(Long.valueOf(value)); + } + + public JSONArray put(Map value) { + return this.put(new JSONObject(value)); + } + + public JSONArray put(Object value) { + JSONObject.testValidity(value); + this.myArrayList.add(value); + return this; + } + + public JSONArray put(int index, boolean value) throws JSONException { + return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONArray put(int index, Collection value) throws JSONException { + return this.put(index, new JSONArray(value)); + } + + public JSONArray put(int index, double value) throws JSONException { + return this.put(index, Double.valueOf(value)); + } + + public JSONArray put(int index, float value) throws JSONException { + return this.put(index, Float.valueOf(value)); + } + + public JSONArray put(int index, int value) throws JSONException { + return this.put(index, Integer.valueOf(value)); + } + + public JSONArray put(int index, long value) throws JSONException { + return this.put(index, Long.valueOf(value)); + } + + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + public JSONArray put(int index, Object value) throws JSONException { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + JSONObject.testValidity(value); + this.myArrayList.set(index, value); + return this; + } + if(index == this.length()){ + // simple append + return this.put(value); + } + // if we are inserting past the length, we want to grow the array all at once + // instead of incrementally. + this.myArrayList.ensureCapacity(index + 1); + while (index != this.length()) { + // we don't need to test validity of NULL objects + this.myArrayList.add(JSONObject.NULL); + } + return this.put(value); + } + + public JSONArray putAll(Collection collection) { + this.addAll(collection, false); + return this; + } + + + public JSONArray putAll(Iterable iter) { + this.addAll(iter, false); + return this; + } + + public JSONArray putAll(JSONArray array) { + // directly copy the elements from the source array to this one + // as all wrapping should have been done already in the source. + this.myArrayList.addAll(array.myArrayList); + return this; + } + + public JSONArray putAll(Object array) throws JSONException { + this.addAll(array, false); + return this; + } + + + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + public Object remove(int index) { + return index >= 0 && index < this.length() + ? this.myArrayList.remove(index) + : null; + } + + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray)other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.myArrayList.get(i); + Object valueOther = ((JSONArray)other).myArrayList.get(i); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.isEmpty() || this.isEmpty()) { + return null; + } + JSONObject jo = new JSONObject(names.length()); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + public List toList() { + List results = new ArrayList(this.myArrayList.size()); + for (Object element : this.myArrayList) { + if (element == null || JSONObject.NULL.equals(element)) { + results.add(null); + } else if (element instanceof JSONArray) { + results.add(((JSONArray) element).toList()); + } else if (element instanceof JSONObject) { + results.add(((JSONObject) element).toMap()); + } else { + results.add(element); + } + } + return results; + } + + public boolean isEmpty() { + return this.myArrayList.isEmpty(); + } + + private void addAll(Collection collection, boolean wrap) { + this.myArrayList.ensureCapacity(this.myArrayList.size() + collection.size()); + if (wrap) { + for (Object o: collection){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: collection){ + this.put(o); + } + } + } + + private void addAll(Iterable iter, boolean wrap) { + if (wrap) { + for (Object o: iter){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: iter){ + this.put(o); + } + } + } + + + private void addAll(Object array, boolean wrap) throws JSONException { + if (array.getClass().isArray()) { + int length = Array.getLength(array); + this.myArrayList.ensureCapacity(this.myArrayList.size() + length); + if (wrap) { + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + for (int i = 0; i < length; i += 1) { + this.put(Array.get(array, i)); + } + } + } else if (array instanceof JSONArray) { + // use the built in array list `addAll` as all object + // wrapping should have been completed in the original + // JSONArray + this.myArrayList.addAll(((JSONArray)array).myArrayList); + } else if (array instanceof Collection) { + this.addAll((Collection)array, wrap); + } else if (array instanceof Iterable) { + this.addAll((Iterable)array, wrap); + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + "." + , cause); + } + + + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + " (" + value + ")." + , cause); + } + +} diff --git a/src/ARSCLib/com/reandroid/json/JSONConvert.java b/src/ARSCLib/com/reandroid/json/JSONConvert.java new file mode 100644 index 00000000..358caf2a --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONConvert.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public interface JSONConvert { + public T toJson(); + public void fromJson(T json); +} diff --git a/src/ARSCLib/com/reandroid/json/JSONException.java b/src/ARSCLib/com/reandroid/json/JSONException.java new file mode 100644 index 00000000..13b72cdf --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONException.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class JSONException extends IllegalArgumentException { + /** Serialization ID */ + private static final long serialVersionUID = 0; + + public JSONException(final String message) { + super(message); + } + + public JSONException(final String message, final Throwable cause) { + super(message, cause); + } + + public JSONException(final Throwable cause) { + super(cause.getMessage(), cause); + } + +} diff --git a/src/ARSCLib/com/reandroid/json/JSONItem.java b/src/ARSCLib/com/reandroid/json/JSONItem.java new file mode 100644 index 00000000..86b630f1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONItem.java @@ -0,0 +1,64 @@ + /* + * 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.json; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public abstract class JSONItem { + public abstract Writer write(Writer writer, int indentFactor, int indent) throws JSONException; + + public void write(File file) throws IOException{ + write(file, INDENT_FACTOR); + } + public void write(File file, int indentFactor) throws IOException{ + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + write(outputStream, indentFactor); + outputStream.close(); + } + public void write(OutputStream outputStream) throws IOException { + write(outputStream, INDENT_FACTOR); + } + public void write(OutputStream outputStream, int indentFactor) throws IOException { + Writer writer=new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writer= write(writer, indentFactor, 0); + writer.flush(); + writer.close(); + } + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + private static final int INDENT_FACTOR=1; +} diff --git a/src/ARSCLib/com/reandroid/json/JSONML.java b/src/ARSCLib/com/reandroid/json/JSONML.java new file mode 100644 index 00000000..9b15ea3a --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONML.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class JSONML { + + private static Object parse( + XMLTokener x, + boolean arrayForm, + JSONArray ja, + boolean keepStrings + ) throws JSONException { + String attribute; + char c; + String closeTag = null; + int i; + JSONArray newja = null; + JSONObject newjo = null; + Object token; + String tagName = null; + +// Test for and skip past these forms: +// +// +// +// + + while (true) { + if (!x.more()) { + throw x.syntaxError("Bad XML"); + } + token = x.nextContent(); + if (token == XML.LT) { + token = x.nextToken(); + if (token instanceof Character) { + if (token == XML.SLASH) { + +// Close tag "); + } else { + x.back(); + } + } else if (c == '[') { + token = x.nextToken(); + if (token.equals("CDATA") && x.next() == '[') { + if (ja != null) { + ja.put(x.nextCDATA()); + } + } else { + throw x.syntaxError("Expected 'CDATA['"); + } + } else { + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + } + } else if (token == XML.QUEST) { + +// "); + } else { + throw x.syntaxError("Misshaped tag"); + } + +// Open tag < + + } else { + if (!(token instanceof String)) { + throw x.syntaxError("Bad tagName '" + token + "'."); + } + tagName = (String)token; + newja = new JSONArray(); + newjo = new JSONObject(); + if (arrayForm) { + newja.put(tagName); + if (ja != null) { + ja.put(newja); + } + } else { + newjo.put("tagName", tagName); + if (ja != null) { + ja.put(newjo); + } + } + token = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (!(token instanceof String)) { + break; + } + +// attribute = value + + attribute = (String)token; + if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { + throw x.syntaxError("Reserved attribute."); + } + token = x.nextToken(); + if (token == XML.EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); + token = null; + } else { + newjo.accumulate(attribute, ""); + } + } + if (arrayForm && newjo.length() > 0) { + newja.put(newjo); + } + +// Empty tag <.../> + + if (token == XML.SLASH) { + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + +// Content, between <...> and + + } else { + if (token != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + closeTag = (String)parse(x, arrayForm, newja, keepStrings); + if (closeTag != null) { + if (!closeTag.equals(tagName)) { + throw x.syntaxError("Mismatched '" + tagName + + "' and '" + closeTag + "'"); + } + tagName = null; + if (!arrayForm && newja.length() > 0) { + newjo.put("childNodes", newja); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + } + } + } + } else { + if (ja != null) { + ja.put(token instanceof String + ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) + : token); + } + } + } + } + public static JSONArray toJSONArray(String string) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, false); + } + public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); + } + public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONArray)parse(x, true, null, keepStrings); + } + public static JSONArray toJSONArray(XMLTokener x) throws JSONException { + return (JSONArray)parse(x, true, null, false); + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x) throws JSONException { + return (JSONObject)parse(x, false, null, false); + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONObject)parse(x, false, null, keepStrings); + } + public static String toString(JSONArray ja) throws JSONException { + int i; + JSONObject jo; + int length; + Object object; + StringBuilder sb = new StringBuilder(); + String tagName; + +// Emit = length) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + do { + object = ja.get(i); + i += 1; + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } while (i < length); + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } + + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + int i; + JSONArray ja; + int length; + Object object; + String tagName; + Object value; + +//Emit '); + } else { + sb.append('>'); + length = ja.length(); + for (i = 0; i < length; i += 1) { + object = ja.get(i); + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } +} diff --git a/src/ARSCLib/com/reandroid/json/JSONObject.java b/src/ARSCLib/com/reandroid/json/JSONObject.java new file mode 100644 index 00000000..daedbf0a --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONObject.java @@ -0,0 +1,1398 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import com.reandroid.common.FileChannelInputStream; + +import java.io.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +public class JSONObject extends JSONItem { + + private static final class Null { + + @Override + protected final Object clone() { + return this; + } + + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return "null"; + } + } + + + static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); + + private final LinkedHashMap map; + + public static final Object NULL = new Null(); + + public JSONObject() { + this.map = new LinkedHashMap<>(); + } + + public JSONObject(JSONObject jo, String ... names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + public JSONObject(Map m) { + if (m == null) { + this.map = new LinkedHashMap(); + } else { + this.map = new LinkedHashMap(m.size()); + for (final Entry e : m.entrySet()) { + if(e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + private JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + + protected JSONObject(int initialCapacity){ + this.map = new LinkedHashMap(initialCapacity); + } + + + public JSONObject(File file) throws IOException { + this(new FileChannelInputStream(file)); + } + public JSONObject(Reader reader){ + this(new JSONTokener(reader)); + } + public JSONObject(InputStream inputStream) throws JSONException { + this(new JSONTokener(inputStream)); + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw wrongValueFormatException(key, "JSONArray", null, null); + } + return this; + } + + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(key, "enum of type " + quote(clazz.getSimpleName()), null); + } + return val; + } + + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(key, "Boolean", null); + } + + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + BigInteger ret = objectToBigInteger(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigInteger", object, null); + } + + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + BigDecimal ret = objectToBigDecimal(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigDecimal", object, null); + } + + public double getDouble(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "double", e); + } + } + + public float getFloat(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "float", e); + } + } + + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "number", e); + } + } + + public int getInt(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "int", e); + } + } + + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(key, "JSONArray", null); + } + + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(key, "JSONObject", null); + } + + public long getLong(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "long", e); + } + } + + public static String[] getNames(JSONObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(key, "string", null); + } + + public boolean has(String key) { + return this.map.containsKey(key); + } + + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger)value).add(BigInteger.ONE)); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal)value).add(BigDecimal.ONE)); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + public Iterator keys() { + return this.keySet().iterator(); + } + + public Set keySet() { + return this.map.keySet(); + } + + protected Set> entrySet() { + return this.map.entrySet(); + } + + public int length() { + return this.map.size(); + } + + public boolean isEmpty() { + return this.map.isEmpty(); + } + + public JSONArray names() { + if(this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + return objectToBigDecimal(val, defaultValue); + } + + static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + return objectToBigInteger(val, defaultValue); + } + + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(d).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + public double optDouble(String key, double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + public float optFloat(String key, float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + + public int optInt(String key) { + return this.optInt(key, 0); + } + + public int optInt(String key, int defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + public long optLong(String key) { + return this.optLong(key, 0); + } + + public long optLong(String key, long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + + + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + try { + return stringToNumber(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + + public String optString(String key) { + return this.optString(key, ""); + } + + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private static boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private static String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + public JSONObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + public JSONObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + + public JSONObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + public JSONObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + public JSONObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + public JSONObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONObject(value)); + } + + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.isEmpty()) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + public Object remove(String key) { + return this.map.remove(key); + } + + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + if (!this.keySet().equals(((JSONObject)other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + if(NUMBER_PATTERN.matcher(numberAsString).matches()) { + writer.write(numberAsString); + } else { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum)value).name())); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + try { + boolean needsComma = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newIndent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + public LinkedHashMap toMap() { + LinkedHashMap results = new LinkedHashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONObject) { + value = ((JSONObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } + + + private static JSONException wrongValueFormatException( + String key, + String valueType, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + "." + , cause); + } + + + private static JSONException wrongValueFormatException( + String key, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")." + , cause); + } +} diff --git a/src/ARSCLib/com/reandroid/json/JSONPointer.java b/src/ARSCLib/com/reandroid/json/JSONPointer.java new file mode 100644 index 00000000..1512969b --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONPointer.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import static java.lang.String.format; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + public static class Builder { + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + public JSONPointer build() { + return new JSONPointer(this.refTokens); + } + + public Builder append(String token) { + if (token == null) { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; + } + + public Builder append(int arrayIndex) { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; + } + } + + public static Builder builder() { + return new Builder(); + } + + // Segments for the JSONPointer string + private final List refTokens; + + public JSONPointer(final String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + refs = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } else if (slashIdx >= 0) { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } else { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending nulls" + //for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + //} + } + + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + private static String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + + public Object queryFrom(Object document) throws JSONPointerException { + if (this.refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : this.refTokens) { + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(unescape(token)); + } else if (current instanceof JSONArray) { + current = readByIndexToken(current, token); + } else { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", current, + token)); + } + } + return current; + } + + private static Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { + try { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) { + throw new JSONPointerException(format("index %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(currentArr.length()))); + } + try { + return currentArr.get(index); + } catch (JSONException e) { + throw new JSONPointerException("Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: this.refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + private static String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1") + .replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/ARSCLib/com/reandroid/json/JSONPointerException.java b/src/ARSCLib/com/reandroid/json/JSONPointerException.java new file mode 100644 index 00000000..218e6611 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONPointerException.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/ARSCLib/com/reandroid/json/JSONPropertyIgnore.java b/src/ARSCLib/com/reandroid/json/JSONPropertyIgnore.java new file mode 100644 index 00000000..160f14a6 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONPropertyIgnore.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) + +public @interface JSONPropertyIgnore { } diff --git a/src/ARSCLib/com/reandroid/json/JSONPropertyName.java b/src/ARSCLib/com/reandroid/json/JSONPropertyName.java new file mode 100644 index 00000000..5268dc45 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONPropertyName.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) + +public @interface JSONPropertyName { + + String value(); +} diff --git a/src/ARSCLib/com/reandroid/json/JSONString.java b/src/ARSCLib/com/reandroid/json/JSONString.java new file mode 100644 index 00000000..a78be929 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONString.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public interface JSONString { + + public String toJSONString(); +} diff --git a/src/ARSCLib/com/reandroid/json/JSONStringer.java b/src/ARSCLib/com/reandroid/json/JSONStringer.java new file mode 100644 index 00000000..a511ff7f --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONStringer.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.StringWriter; + +public class JSONStringer extends JSONWriter { + + public JSONStringer() { + super(new StringWriter()); + } + + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/src/ARSCLib/com/reandroid/json/JSONTokener.java b/src/ARSCLib/com/reandroid/json/JSONTokener.java new file mode 100644 index 00000000..6ea5f398 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONTokener.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +public class JSONTokener { + /** current read character position on the current line. */ + private long character; + /** flag to indicate if the end of the input has been found. */ + private boolean eof; + /** current read index of the input. */ + private long index; + /** current line of the input. */ + private long line; + /** previous character read from the input. */ + private char previous; + /** Reader for the input. */ + private final Reader reader; + /** flag to indicate that a previous character was requested. */ + private boolean usePrevious; + /** the number of characters read in the previous line. */ + private long characterPreviousLine; + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.characterPreviousLine = 0; + this.line = 1; + } + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + public JSONTokener(String s) { + this(new StringReader(s)); + } + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.decrementIndexes(); + this.usePrevious = true; + this.eof = false; + } + + private void decrementIndexes() { + this.index--; + if(this.previous=='\r' || this.previous == '\n') { + this.line--; + this.character=this.characterPreviousLine ; + } else if(this.character > 0){ + this.character--; + } + } + + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return this.eof && !this.usePrevious; + } + public boolean more() throws JSONException { + if(this.usePrevious) { + return true; + } + try { + this.reader.mark(1); + } catch (IOException e) { + throw new JSONException("Unable to preserve stream position", e); + } + try { + // -1 is EOF, but next() can not consume the null character '\0' + if(this.reader.read() <= 0) { + this.eof = true; + return false; + } + this.reader.reset(); + } catch (IOException e) { + throw new JSONException("Unable to read the next character from the stream", e); + } + return true; + } + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + if (c <= 0) { // End of stream + this.eof = true; + return 0; + } + this.incrementIndexes(c); + this.previous = (char) c; + return this.previous; + } + + private void incrementIndexes(int c) { + if(c > 0) { + this.index++; + if(c=='\r') { + this.line++; + this.characterPreviousLine = this.character; + this.character=0; + }else if (c=='\n') { + if(this.previous != '\r') { + this.line++; + this.characterPreviousLine = this.character; + } + this.character=0; + } else { + this.character++; + } + } + } + + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + if(n > 0) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + throw this.syntaxError("Expected '" + c + "' and instead saw ''"); + } + return n; + } + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char)Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + if (!this.eof) { + this.back(); + } + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + // in some readers, reset() may throw an exception if + // the remaining portion of the input is greater than + // the mark size (1,000,000 above). + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return 0; + } + } while (c != to); + this.reader.mark(1); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + public JSONException syntaxError(String message, Throwable causedBy) { + return new JSONException(message + this.toString(), causedBy); + } + + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/src/ARSCLib/com/reandroid/json/JSONWriter.java b/src/ARSCLib/com/reandroid/json/JSONWriter.java new file mode 100644 index 00000000..794de4dc --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JSONWriter.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +public class JSONWriter { + private static final int maxdepth = 200; + + private boolean comma; + + protected char mode; + + private final JSONObject stack[]; + + private int top; + + protected Appendable writer; + + public JSONWriter(Appendable w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + } + this.writer.append(string); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + JSONObject topObject = this.stack[this.top - 1]; + // don't use the built in putOnce method to maintain Android support + if(topObject.has(string)) { + throw new JSONException("Duplicate key \"" + string + "\""); + } + topObject.put(string, true); + if (this.comma) { + this.writer.append(','); + } + this.writer.append(JSONObject.quote(string)); + this.writer.append(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + String object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object != null) { + return object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex + final String numberAsString = JSONObject.numberToString((Number) value); + if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { + // Close enough to a JSON number that we will return it unquoted + return numberAsString; + } + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + return JSONObject.quote(numberAsString); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } + if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + if(value instanceof Enum){ + return JSONObject.quote(((Enum)value).name()); + } + return JSONObject.quote(value.toString()); + } + + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + public JSONWriter value(double d) throws JSONException { + return this.value(Double.valueOf(d)); + } + + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + public JSONWriter value(Object object) throws JSONException { + return this.append(valueToString(object)); + } +} diff --git a/src/ARSCLib/com/reandroid/json/JsonUtil.java b/src/ARSCLib/com/reandroid/json/JsonUtil.java new file mode 100644 index 00000000..91bf1563 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/JsonUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class JsonUtil { + + public static void readJSONObject(File file, JSONConvert jsonConvert) throws IOException { + FileInputStream inputStream=new FileInputStream(file); + readJSONObject(inputStream, jsonConvert); + inputStream.close(); + } + public static void readJSONObject(InputStream inputStream, JSONConvert jsonConvert){ + InputStreamReader reader=new InputStreamReader(inputStream, StandardCharsets.UTF_8); + readJSONObject(reader, jsonConvert); + } + public static void readJSONObject(Reader reader, JSONConvert jsonConvert){ + JSONObject jsonObject=new JSONObject(new JSONTokener(reader)); + jsonConvert.fromJson(jsonObject); + } + + public static void readJSONArray(File file, JSONConvert jsonConvert) throws IOException { + FileInputStream inputStream=new FileInputStream(file); + readJSONArray(inputStream, jsonConvert); + inputStream.close(); + } + public static void readJSONArray(InputStream inputStream, JSONConvert jsonConvert){ + InputStreamReader reader=new InputStreamReader(inputStream, StandardCharsets.UTF_8); + readJSONArray(reader, jsonConvert); + } + public static void readJSONArray(Reader reader, JSONConvert jsonConvert){ + JSONArray jsonObject=new JSONArray(new JSONTokener(reader)); + jsonConvert.fromJson(jsonObject); + } + +} diff --git a/src/ARSCLib/com/reandroid/json/Property.java b/src/ARSCLib/com/reandroid/json/Property.java new file mode 100644 index 00000000..c6c2d4d1 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/Property.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.util.Enumeration; +import java.util.Properties; + +public class Property { + + public static JSONObject toJSONObject(java.util.Properties properties) throws JSONException { + // can't use the new constructor for Android support + // JSONObject jo = new JSONObject(properties == null ? 0 : properties.size()); + JSONObject jo = new JSONObject(); + if (properties != null && !properties.isEmpty()) { + Enumeration enumProperties = properties.propertyNames(); + while(enumProperties.hasMoreElements()) { + String name = (String)enumProperties.nextElement(); + jo.put(name, properties.getProperty(name)); + } + } + return jo; + } + + public static Properties toProperties(JSONObject jo) throws JSONException { + Properties properties = new Properties(); + if (jo != null) { + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + properties.put(key, value.toString()); + } + } + } + return properties; + } +} diff --git a/src/ARSCLib/com/reandroid/json/XML.java b/src/ARSCLib/com/reandroid/json/XML.java new file mode 100644 index 00000000..a22114ae --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/XML.java @@ -0,0 +1,605 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; +@SuppressWarnings("boxing") +public class XML { + + /** The Character '&'. */ + public static final Character AMP = '&'; + + /** The Character '''. */ + public static final Character APOS = '\''; + + /** The Character '!'. */ + public static final Character BANG = '!'; + + /** The Character '='. */ + public static final Character EQ = '='; + + /** The Character
{@code '>'. }
*/ + public static final Character GT = '>'; + + /** The Character '<'. */ + public static final Character LT = '<'; + + /** The Character '?'. */ + public static final Character QUEST = '?'; + + /** The Character '"'. */ + public static final Character QUOT = '"'; + + /** The Character '/'. */ + public static final Character SLASH = '/'; + + public static final String NULL_ATTR = "xsi:nil"; + + public static final String TYPE_ATTR = "xsi:type"; + + private static Iterable codePointIterator(final String string) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private int nextIndex = 0; + private int length = string.length(); + + @Override + public boolean hasNext() { + return this.nextIndex < this.length; + } + + @Override + public Integer next() { + int result = string.codePointAt(this.nextIndex); + this.nextIndex += Character.charCount(result); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + public static String escape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (final int cp : codePointIterator(string)) { + switch (cp) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + if (mustEscape(cp)) { + sb.append("&#x"); + sb.append(Integer.toHexString(cp)); + sb.append(';'); + } else { + sb.appendCodePoint(cp); + } + } + } + return sb.toString(); + } + + private static boolean mustEscape(int cp) { + /* Valid range from https://www.w3.org/TR/REC-xml/#charsets + * + * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * + * any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. + */ + // isISOControl is true when (cp >= 0 && cp <= 0x1F) || (cp >= 0x7F && cp <= 0x9F) + // all ISO control characters are out of range except tabs and new lines + return (Character.isISOControl(cp) + && cp != 0x9 + && cp != 0xA + && cp != 0xD + ) || !( + // valid the range of acceptable characters that aren't control + (cp >= 0x20 && cp <= 0xD7FF) + || (cp >= 0xE000 && cp <= 0xFFFD) + || (cp >= 0x10000 && cp <= 0x10FFFF) + ) + ; + } + + public static String unescape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (int i = 0, length = string.length(); i < length; i++) { + char c = string.charAt(i); + if (c == '&') { + final int semic = string.indexOf(';', i); + if (semic > i) { + final String entity = string.substring(i + 1, semic); + sb.append(XMLTokener.unescapeEntity(entity)); + // skip past the entity we just parsed. + i += entity.length() + 1; + } else { + // this shouldn't happen in most cases since the parser + // errors on unclosed entries. + sb.append(c); + } + } else { + // not part of an entity + sb.append(c); + } + } + return sb.toString(); + } + + public static void noSpace(String string) throws JSONException { + int i, length = string.length(); + if (length == 0) { + throw new JSONException("Empty string."); + } + for (i = 0; i < length; i += 1) { + if (Character.isWhitespace(string.charAt(i))) { + throw new JSONException("'" + string + + "' contains a space character."); + } + } + } + + private static boolean parse(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config) + throws JSONException { + char c; + int i; + JSONObject jsonObject = null; + String string; + String tagName; + Object token; + XMLXsiTypeConverter xmlXsiTypeConverter; + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate(config.getcDataTagName(), string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (nilAttributeFound) { + context.accumulate(tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(tagName, jsonObject); + } else { + context.accumulate(tagName, ""); + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + if(xmlXsiTypeConverter != null) { + jsonObject.accumulate(config.getcDataTagName(), + stringToValue(string, xmlXsiTypeConverter)); + } else { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepStrings() ? string : stringToValue(string)); + } + } + + } else if (token == LT) { + // Nested element + if (parse(x, jsonObject, tagName, config)) { + if (jsonObject.length() == 0) { + context.accumulate(tagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(tagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.accumulate(tagName, jsonObject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + public static Object stringToValue(String string, XMLXsiTypeConverter typeConverter) { + if(typeConverter != null) { + return typeConverter.convert(string); + } + return stringToValue(string); + } + + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. + // This method should not make calls out of the XML object. + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + + private static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + + private static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(string, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader) throws JSONException { + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws JSONException { + if(keepStrings) { + return toJSONObject(reader, XMLParserConfiguration.KEEP_STRINGS); + } + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration config) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(reader); + while (x.more()) { + x.skipPast("<"); + if(x.more()) { + parse(x, jo, null, config); + } + } + return jo; + } + + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return toJSONObject(new StringReader(string), keepStrings); + } + + public static JSONObject toJSONObject(String string, XMLParserConfiguration config) throws JSONException { + return toJSONObject(new StringReader(string), config); + } + + public static String toString(Object object) throws JSONException { + return toString(object, null, XMLParserConfiguration.ORIGINAL); + } + + public static String toString(final Object object, final String tagName) { + return toString(object, tagName, XMLParserConfiguration.ORIGINAL); + } + + public static String toString(final Object object, final String tagName, final XMLParserConfiguration config) + throws JSONException { + StringBuilder sb = new StringBuilder(); + JSONArray ja; + JSONObject jo; + String string; + + if (object instanceof JSONObject) { + + // Emit + if (tagName != null) { + sb.append('<'); + sb.append(tagName); + sb.append('>'); + } + + // Loop thru the keys. + // don't use the new entrySet accessor to maintain Android Support + jo = (JSONObject) object; + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (value == null) { + value = ""; + } else if (value.getClass().isArray()) { + value = new JSONArray(value); + } + + // Emit content in body + if (key.equals(config.getcDataTagName())) { + if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + if (i > 0) { + sb.append('\n'); + } + Object val = ja.opt(i); + sb.append(escape(val.toString())); + } + } else { + sb.append(escape(value.toString())); + } + + // Emit an array of similar keys + + } else if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + if (val instanceof JSONArray) { + sb.append('<'); + sb.append(key); + sb.append('>'); + sb.append(toString(val, null, config)); + sb.append("'); + } else { + sb.append(toString(val, key, config)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + + // Emit a new tag + + } else { + sb.append(toString(value, key, config)); + } + } + if (tagName != null) { + + // Emit the close tag + sb.append("'); + } + return sb.toString(); + + } + + if (object != null && (object instanceof JSONArray || object.getClass().isArray())) { + if(object.getClass().isArray()) { + ja = new JSONArray(object); + } else { + ja = (JSONArray) object; + } + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + // XML does not have good support for arrays. If an array + // appears in a place where XML is lacking, synthesize an + // element. + sb.append(toString(val, tagName == null ? "array" : tagName, config)); + } + return sb.toString(); + } + + string = (object == null) ? "null" : escape(object.toString()); + return (tagName == null) ? "\"" + string + "\"" + : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName + + ">" + string + ""; + + } +} diff --git a/src/ARSCLib/com/reandroid/json/XMLParserConfiguration.java b/src/ARSCLib/com/reandroid/json/XMLParserConfiguration.java new file mode 100644 index 00000000..a1da111b --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/XMLParserConfiguration.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +@SuppressWarnings({""}) +public class XMLParserConfiguration { + /** Original Configuration of the XML Parser. */ + public static final XMLParserConfiguration ORIGINAL + = new XMLParserConfiguration(); + /** Original configuration of the XML Parser except that values are kept as strings. */ + public static final XMLParserConfiguration KEEP_STRINGS + = new XMLParserConfiguration().withKeepStrings(true); + + private boolean keepStrings; + + + private String cDataTagName; + + + private boolean convertNilAttributeToNull; + + private Map> xsiTypeMap; + + public XMLParserConfiguration() { + this.keepStrings = false; + this.cDataTagName = "content"; + this.convertNilAttributeToNull = false; + this.xsiTypeMap = Collections.emptyMap(); + } + + @Deprecated + public XMLParserConfiguration(final boolean keepStrings) { + this(keepStrings, "content", false); + } + + @Deprecated + public XMLParserConfiguration(final String cDataTagName) { + this(false, cDataTagName, false); + } + + @Deprecated + public XMLParserConfiguration(final boolean keepStrings, final String cDataTagName) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = false; + } + + @Deprecated + public XMLParserConfiguration(final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + } + + private XMLParserConfiguration(final boolean keepStrings, final String cDataTagName, + final boolean convertNilAttributeToNull, final Map> xsiTypeMap ) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); + } + + @Override + protected XMLParserConfiguration clone() { + // future modifications to this method should always ensure a "deep" + // clone in the case of collections. i.e. if a Map is added as a configuration + // item, a new map instance should be created and if possible each value in the + // map should be cloned as well. If the values of the map are known to also + // be immutable, then a shallow clone of the map is acceptable. + return new XMLParserConfiguration( + this.keepStrings, + this.cDataTagName, + this.convertNilAttributeToNull, + this.xsiTypeMap + ); + } + + + public boolean isKeepStrings() { + return this.keepStrings; + } + + public XMLParserConfiguration withKeepStrings(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepStrings = newVal; + return newConfig; + } + + public String getcDataTagName() { + return this.cDataTagName; + } + + public XMLParserConfiguration withcDataTagName(final String newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.cDataTagName = newVal; + return newConfig; + } + + public boolean isConvertNilAttributeToNull() { + return this.convertNilAttributeToNull; + } + + public XMLParserConfiguration withConvertNilAttributeToNull(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.convertNilAttributeToNull = newVal; + return newConfig; + } + + public Map> getXsiTypeMap() { + return this.xsiTypeMap; + } + + public XMLParserConfiguration withXsiTypeMap(final Map> xsiTypeMap) { + XMLParserConfiguration newConfig = this.clone(); + Map> cloneXsiTypeMap = new HashMap>(xsiTypeMap); + newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap); + return newConfig; + } +} diff --git a/src/ARSCLib/com/reandroid/json/XMLTokener.java b/src/ARSCLib/com/reandroid/json/XMLTokener.java new file mode 100644 index 00000000..f0c45749 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/XMLTokener.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +import java.io.Reader; + +public class XMLTokener extends JSONTokener { + + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + public XMLTokener(Reader r) { + super(r); + } + + public XMLTokener(String s) { + super(s); + } + + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuilder sb = new StringBuilder(); + while (more()) { + c = next(); + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && + sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + throw syntaxError("Unclosed CDATA"); + } + public Object nextContent() throws JSONException { + char c; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuilder(); + for (;;) { + if (c == 0) { + return sb.toString().trim(); + } + if (c == '<') { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + public Object nextEntity(@SuppressWarnings("unused") char ampersand) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String string = sb.toString(); + return unescapeEntity(string); + } + + + static String unescapeEntity(String e) { + // validate + if (e == null || e.isEmpty()) { + return ""; + } + // if our entity is an encoded unicode point, parse it. + if (e.charAt(0) == '#') { + int cp; + if (e.charAt(1) == 'x' || e.charAt(1) == 'X') { + // hex encoded unicode + cp = Integer.parseInt(e.substring(2), 16); + } else { + // decimal encoded unicode + cp = Integer.parseInt(e.substring(1)); + } + return new String(new int[] {cp},0,1); + } + Character knownEntity = entity.get(e); + if(knownEntity==null) { + // we don't know the entity so keep it encoded + return '&' + e + ';'; + } + return knownEntity.toString(); + } + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + throw syntaxError("Unterminated string"); + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + public Object nextToken() throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + +// Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + +// Name + + sb = new StringBuilder(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + // The Android implementation of JSONTokener has a public method of public void skipPast(String to) + // even though ours does not have that method, to have API compatibility, our method in the subclass + // should match. + public void skipPast(String to) { + boolean b; + char c; + int i; + int j; + int offset = 0; + int length = to.length(); + char[] circle = new char[length]; + + /* + * First fill the circle buffer with as many characters as are in the + * to string. If we reach an early end, bail. + */ + + for (i = 0; i < length; i += 1) { + c = next(); + if (c == 0) { + return; + } + circle[i] = c; + } + + /* We will loop, possibly for all of the remaining characters. */ + + for (;;) { + j = offset; + b = true; + + /* Compare the circle buffer with the to string. */ + + for (i = 0; i < length; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= length) { + j -= length; + } + } + + /* If we exit the loop with b intact, then victory is ours. */ + + if (b) { + return; + } + + /* Get the next character. If there isn't one, then defeat is ours. */ + + c = next(); + if (c == 0) { + return; + } + /* + * Shove the character in the circle buffer and advance the + * circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= length) { + offset -= length; + } + } + } +} diff --git a/src/ARSCLib/com/reandroid/json/XMLXsiTypeConverter.java b/src/ARSCLib/com/reandroid/json/XMLXsiTypeConverter.java new file mode 100644 index 00000000..af9ef099 --- /dev/null +++ b/src/ARSCLib/com/reandroid/json/XMLXsiTypeConverter.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2002 JSON.org (now "Public Domain") + * This is NOT property of REAndroid + * This package is renamed from org.json.* to avoid class conflict when used on anroid platforms +*/ +package com.reandroid.json; + +public interface XMLXsiTypeConverter { + T convert(String value); +} diff --git a/src/ARSCLib/com/reandroid/xml/ElementWriter.java b/src/ARSCLib/com/reandroid/xml/ElementWriter.java new file mode 100755 index 00000000..8676f9b2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/ElementWriter.java @@ -0,0 +1,79 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import java.io.IOException; +import java.io.Writer; + +class ElementWriter extends Writer { + private final Writer mWriter; + private final long mMaxLen; + private final boolean mUnlimitedLength; + private long mCurrentLength; + private boolean mLengthFinished; + ElementWriter(Writer writer, long maxLen){ + mWriter=writer; + this.mMaxLen=maxLen; + this.mUnlimitedLength=maxLen<0; + } + ElementWriter(Writer writer){ + this(writer, -1); + } + boolean isFinished(){ + return mLengthFinished; + } + private boolean mInterruptedWritten; + void writeInterrupted(){ + if(!mLengthFinished){ + return; + } + if(mInterruptedWritten){ + return; + } + mInterruptedWritten=true; + String txt="\n .\n .\n .\n more items ...\n"; + try { + mWriter.write(txt); + } catch (IOException e) { + } + } + @Override + public void write(char[] chars, int i, int i1) throws IOException { + updateCurrentLength(i1); + mWriter.write(chars, i, i1); + } + + @Override + public void flush() throws IOException { + mWriter.flush(); + } + @Override + public void close() throws IOException { + mWriter.close(); + } + private boolean updateCurrentLength(int len){ + if(mUnlimitedLength){ + return false; + } + if(mLengthFinished){ + mLengthFinished=true; + //return true; + } + mCurrentLength+=len; + mLengthFinished=mCurrentLength>=mMaxLen; + return mLengthFinished; + } +} diff --git a/src/ARSCLib/com/reandroid/xml/NameSpaceItem.java b/src/ARSCLib/com/reandroid/xml/NameSpaceItem.java new file mode 100755 index 00000000..392dd8f2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/NameSpaceItem.java @@ -0,0 +1,156 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NameSpaceItem { + private String prefix; + private String namespaceUri; + public NameSpaceItem(String prefix, String nsUri){ + this.prefix=prefix; + this.namespaceUri=nsUri; + validate(); + } + public String toAttributeName(){ + return ATTR_PREFIX+":"+prefix; + } + public SchemaAttr toSchemaAttribute(){ + return new SchemaAttr(getPrefix(), getNamespaceUri()); + } + public boolean isPrefixEqual(String p){ + if(XMLUtil.isEmpty(prefix)){ + return false; + } + return prefix.equals(p); + } + public boolean isUriEqual(String nsUri){ + if(XMLUtil.isEmpty(namespaceUri)){ + return false; + } + return namespaceUri.equals(nsUri); + } + public boolean isValid(){ + return isPrefixValid() && isUriValid(); + } + private boolean validate(){ + boolean preOk=isPrefixValid(); + boolean uriOk=isUriValid(); + if(preOk && uriOk){ + if(!NAME_ANDROID.equals(prefix) && URI_ANDROID.equals(namespaceUri)){ + namespaceUri= URI_APP; + } + return true; + } + if(!preOk && !uriOk){ + return false; + } + if(!preOk){ + if(URI_ANDROID.equals(namespaceUri)){ + prefix= NAME_ANDROID; + }else { + prefix= NAME_APP; + } + } + if(!uriOk){ + if(NAME_ANDROID.equals(prefix)){ + namespaceUri= URI_ANDROID; + }else { + namespaceUri= URI_APP; + } + } + return true; + } + private boolean isPrefixValid(){ + return !XMLUtil.isEmpty(prefix); + } + private boolean isUriValid(){ + if(XMLUtil.isEmpty(namespaceUri)){ + return false; + } + Matcher matcher=PATTERN_URI.matcher(namespaceUri); + return matcher.find(); + } + public String getNamespaceUri() { + return namespaceUri; + } + public String getPrefix() { + return prefix; + } + public void setNamespaceUri(String namespaceUri) { + this.namespaceUri = namespaceUri; + validate(); + } + public void setPrefix(String prefix) { + this.prefix = prefix; + validate(); + } + @Override + public boolean equals(Object o){ + if(o instanceof NameSpaceItem){ + return isUriEqual(((NameSpaceItem)o).namespaceUri); + } + return false; + } + @Override + public int hashCode(){ + String u=namespaceUri; + if(u==null){ + u=""; + } + return u.hashCode(); + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + boolean appendOnce=false; + if(namespaceUri!=null){ + builder.append(namespaceUri); + appendOnce=true; + } + if(prefix!=null){ + if(appendOnce){ + builder.append(':'); + } + builder.append(prefix); + } + return builder.toString(); + } + private static NameSpaceItem ns_android; + private static NameSpaceItem ns_app; + public static NameSpaceItem getAndroid(){ + if(ns_android==null){ + ns_android=new NameSpaceItem(NAME_ANDROID, URI_ANDROID); + } + return ns_android; + } + public static NameSpaceItem getApp(){ + if(ns_app==null){ + ns_app=new NameSpaceItem(NAME_APP, URI_APP); + } + return ns_app; + } + private static final Pattern PATTERN_URI=Pattern.compile("^https?://[^\\s/]+/[^\\s]+$"); + + + private static final String ATTR_PREFIX = "xmlns"; + private static final String URI_ANDROID = "http://schemas.android.com/apk/res/android"; + private static final String URI_APP = "http://schemas.android.com/apk/res-auto"; + private static final String NAME_ANDROID = "android"; + private static final String NAME_APP = "app"; + +} diff --git a/src/ARSCLib/com/reandroid/xml/SchemaAttr.java b/src/ARSCLib/com/reandroid/xml/SchemaAttr.java new file mode 100755 index 00000000..74933a79 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/SchemaAttr.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SchemaAttr extends XMLAttribute { + private static final String DEFAULT_XMLNS="xmlns"; + private String mXmlns; + private String mPrefix; + public SchemaAttr(String prefix, String uri) { + this(DEFAULT_XMLNS, prefix, uri); + } + public SchemaAttr(String xmlns, String prefix, String uri) { + super(prefix, uri); + this.set(xmlns, prefix, uri); + } + private void set(String xmlns, String prefix, String uri){ + setXmlns(xmlns); + if(XMLUtil.isEmpty(prefix)){ + prefix=null; + } + setName(prefix); + setUri(uri); + } + @Override + public void setName(String fullName){ + if(fullName==null){ + setPrefix(null); + return; + } + int i=fullName.indexOf(':'); + if(i>0 && i0){ + return mName.substring(0,i); + } + return null; + } + public String getNameWoPrefix(){ + int i=mName.indexOf(":"); + if(i>0){ + return mName.substring(i+1); + } + return mName; + } + public String getValue(){ + if(mValue==null){ + mValue=""; + } + return mValue; + } + public int getValueInt(){ + long l=Long.decode(getValue()); + return (int)l; + } + public boolean getValueBool(){ + String str=getValue().toLowerCase(); + if("true".equals(str)){ + return true; + } + return false; + } + public boolean isValueBool(){ + String str=getValue().toLowerCase(); + if("true".equals(str)){ + return true; + } + return "false".equals(str); + } + public void setName(String name){ + mName=name; + } + public void setValue(String val){ + mValue= XMLUtil.escapeXmlChars(val); + } + + @Override + public boolean write(Writer writer, boolean newLineAttributes) throws IOException { + writer.write(getName()); + writer.write("=\""); + String val= XMLUtil.trimQuote(getValue()); + val= XMLUtil.escapeXmlChars(val); + val= XMLUtil.escapeQuote(val); + writer.write(val); + writer.write('"'); + return true; + } + @Override + public String toText(int indent, boolean newLineAttributes) { + StringWriter writer=new StringWriter(); + try { + write(writer); + } catch (IOException ignored) { + } + writer.flush(); + return writer.toString(); + } + @Override + public int hashCode(){ + String name=getName(); + if(name==null){ + name=""; + } + return name.hashCode(); + } + @Override + public boolean equals(Object obj){ + if(obj instanceof XMLAttribute){ + XMLAttribute attr=(XMLAttribute)obj; + return getName().equals(attr.getName()); + } + return false; + } + @Override + public String toString(){ + return toText(); + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLComment.java b/src/ARSCLib/com/reandroid/xml/XMLComment.java new file mode 100755 index 00000000..ebddc585 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLComment.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.xml; + + +import java.io.IOException; +import java.io.Writer; + +public class XMLComment extends XMLElement { + private String mStart; + private String mEnd; + private boolean mIsHidden; + public XMLComment(String commentText){ + this(); + setCommentText(commentText); + } + public XMLComment(){ + super(); + initializeStartEnd(); + } + + public void setHidden(boolean hide){ + mIsHidden=hide; + } + public boolean isHidden(){ + return mIsHidden; + } + public void setCommentText(String text){ + setTextContent(text); + } + public String getCommentText(){ + return getTextContent(); + } + private void initializeStartEnd(){ + setTagName(""); + mStart=""; + setStart(mStart); + setEnd(mEnd); + setStartPrefix(""); + setEndPrefix(""); + } + @Override + int getChildIndent(){ + return getIndent(); + } + @Override + boolean isEmpty(){ + return XMLUtil.isEmpty(getTextContent()); + } + + + void buildTextContent(Writer writer) throws IOException{ + } + @Override + public boolean write(Writer writer, boolean newLineAttributes) throws IOException { + if(isHidden()){ + return false; + } + if(isEmpty()){ + return false; + } + boolean appendOnce=appendComments(writer); + if(appendOnce){ + writer.write(XMLUtil.NEW_LINE); + } + appendIndentText(writer); + writer.write(mStart); + writer.write(getCommentText()); + writer.write(mEnd); + return true; + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLDocument.java b/src/ARSCLib/com/reandroid/xml/XMLDocument.java new file mode 100755 index 00000000..128b1646 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLDocument.java @@ -0,0 +1,254 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import com.reandroid.xml.parser.XMLDocumentParser; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; + +public class XMLDocument extends XMLNode{ + private XMLElement mDocumentElement; + private Object mTag; + private String mName; + private String mConfigName; + private float mIndentScale; + private XmlHeaderElement mHeaderElement; + private Object mLastElementSorter; + public XMLDocument(String elementName){ + this(); + XMLElement docElem=new XMLElement(elementName); + setDocumentElement(docElem); + } + public XMLDocument(){ + mIndentScale=0.5f; + mHeaderElement = new XmlHeaderElement(); + } + public void setHeaderElement(XmlHeaderElement headerElement){ + this.mHeaderElement=headerElement; + } + public void hideComments(boolean hide){ + hideComments(true, hide); + } + public void hideComments(boolean recursive, boolean hide){ + if(mDocumentElement==null){ + return; + } + mDocumentElement.hideComments(recursive, hide); + } + public XmlHeaderElement getHeaderElement(){ + return mHeaderElement; + } + + public void sortDocumentElement(Comparator comparator){ + if(mDocumentElement==null||comparator==null){ + return; + } + if(mLastElementSorter !=null){ + if(mLastElementSorter.getClass().equals(comparator.getClass())){ + return; + } + } + mLastElementSorter=comparator; + mDocumentElement.sortChildes(comparator); + } + public void setIndentScalePercent(int val){ + int percent; + if(val>100){ + percent=100; + }else if(val<0){ + percent=0; + }else { + percent=val; + } + mIndentScale=percent/100.0f; + XMLElement docElem=getDocumentElement(); + if(docElem!=null){ + docElem.setIndentScale(mIndentScale); + } + } + public String getName(){ + return mName; + } + public String getConfigName(){ + return mConfigName; + } + public void setName(String name){ + mName=name; + } + public void setConfigName(String configName){ + mConfigName=configName; + } + public Object getTag(){ + return mTag; + } + public void setTag(Object obj){ + mTag=obj; + } + public XMLElement createElement(String tag) { + XMLElement docEl=getDocumentElement(); + if(docEl==null){ + docEl=new XMLElement(tag); + setDocumentElement(docEl); + return docEl; + } + XMLElement baseElement=docEl.createElement(tag); + return baseElement; + } + public XMLElement getDocumentElement(){ + return mDocumentElement; + } + public void setDocumentElement(XMLElement baseElement){ + mDocumentElement=baseElement; + if(baseElement!=null){ + baseElement.setIndentScale(mIndentScale); + } + } + private String getElementString(boolean newLineAttributes){ + XMLElement baseElement=getDocumentElement(); + if(baseElement==null){ + return null; + } + return baseElement.toString(); + } + private boolean appendDocumentElement(Writer writer, boolean newLineAttributes) throws IOException { + if(mDocumentElement==null){ + return false; + } + return mDocumentElement.write(writer, newLineAttributes); + } + private boolean appendDocumentAttribute(Writer writer) throws IOException { + XmlHeaderElement headerElement=getHeaderElement(); + if(headerElement==null){ + return false; + } + return headerElement.write(writer, false); + } + public boolean saveAndroidResource(File file) throws IOException{ + if(file==null){ + throw new IOException("File is null"); + } + File dir=file.getParentFile(); + if(!dir.exists()){ + dir.mkdirs(); + } + FileOutputStream out=new FileOutputStream(file,false); + return saveAndroidResource(out); + } + public boolean saveAndroidValuesResource(File file) throws IOException{ + if(file==null){ + throw new IOException("File is null"); + } + File dir=file.getParentFile(); + if(!dir.exists()){ + dir.mkdirs(); + } + FileOutputStream out=new FileOutputStream(file,false); + return saveAndroidValuesResource(out); + } + public boolean saveAndroidResource(OutputStream out) throws IOException{ + setIndent(1); + hideComments(true); + return save(out, true); + } + public boolean saveAndroidValuesResource(OutputStream out) throws IOException{ + setIndent(1); + //hideComments(true); + return save(out, false); + } + + public boolean save(OutputStream out, boolean newLineAttributes) throws IOException{ + OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); + boolean result= write(writer, newLineAttributes); + writer.flush(); + writer.close(); + return result; + } + public boolean save(File file, boolean newLineAttributes) throws IOException{ + File dir=file.getParentFile(); + if(dir!=null&&!dir.exists()){ + dir.mkdirs(); + } + setIndent(1); + FileWriter writer=new FileWriter(file,false); + boolean result= write(writer, newLineAttributes); + writer.flush(); + writer.close(); + return result; + } + @Override + public boolean write(Writer writer, boolean newLineAttributes) throws IOException{ + boolean has_header=appendDocumentAttribute(writer); + if(has_header){ + writer.write(XMLUtil.NEW_LINE); + } + return appendDocumentElement(writer, newLineAttributes); + } + @Override + public String toText(int indent, boolean newLineAttributes){ + StringWriter writer=new StringWriter(); + setIndent(indent); + try { + write(writer, newLineAttributes); + writer.flush(); + writer.close(); + } catch (IOException ignored) { + } + return writer.toString(); + } + + @Override + public String toString(){ + StringWriter strWriter=new StringWriter(); + ElementWriter writer=new ElementWriter(strWriter, XMLElement.DEBUG_TO_STRING); + try { + write(writer, false); + } catch (IOException e) { + } + strWriter.flush(); + return strWriter.toString(); + } + public static XMLDocument load(String text) throws XMLException { + XMLDocumentParser parser=new XMLDocumentParser(text); + return parser.parse(); + } + public static XMLDocument load(InputStream in) throws XMLException { + if(in==null){ + throw new XMLException("InputStream=null"); + } + XMLDocumentParser parser=new XMLDocumentParser(in); + return parser.parse(); + } + public static XMLDocument load(File file) throws XMLException { + XMLDocumentParser parser=new XMLDocumentParser(file); + XMLDocument resDocument=parser.parse(); + if(resDocument!=null){ + if(resDocument.getTag()==null){ + resDocument.setTag(file); + } + } + return resDocument; + } + + public void setIndent(int indent){ + XMLElement docEle=getDocumentElement(); + if(docEle==null){ + return; + } + docEle.setIndent(indent); + } + +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLElement.java b/src/ARSCLib/com/reandroid/xml/XMLElement.java new file mode 100755 index 00000000..0477f2b2 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLElement.java @@ -0,0 +1,789 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + + +import com.reandroid.xml.parser.XMLSpanParser; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.*; + +public class XMLElement extends XMLNode{ + static final long DEBUG_TO_STRING=500; + private String mTagName; + private final LinkedHashMap mAttributes = new LinkedHashMap<>(); + private final List mChildElements = new ArrayList<>(); + private final List mComments = new ArrayList<>(); + private final List mTexts = new ArrayList<>(); + private XMLElement mParent; + private int mIndent; + private float mAttributeIndentScale = 1.0f; + private Object mTag; + private int mResId; + private float mIndentScale; + private String mStart; + private String mStartPrefix; + private String mEnd; + private String mEndPrefix; + private Set nameSpaceItems; + public XMLElement(String tagName){ + this(); + setTagName(tagName); + } + public XMLElement(){ + setDefaultStartEnd(); + } + + public void addText(XMLText text){ + addTextInternal(text, true); + } + private void addTextInternal(XMLText text, boolean addSupper){ + if(text==null){ + return; + } + mTexts.add(text); + if(addSupper){ + super.addChildNodeInternal(text); + } + } + private void appendText(String text){ + if(text==null || text.length()==0){ + return; + } + addText(new XMLText(text)); + } + public String getTagNamePrefix(){ + int i=mTagName.indexOf(":"); + if(i>0){ + return mTagName.substring(0,i); + } + return null; + } + public String getTagNameWoPrefix(){ + int i=mTagName.indexOf(":"); + if(i>0){ + return mTagName.substring(i+1); + } + return mTagName; + } + private void setDefaultStartEnd(){ + this.mStart="<"; + this.mEnd=">"; + this.mStartPrefix="/"; + this.mEndPrefix="/"; + } + public void applyNameSpaceItems(){ + if(nameSpaceItems!=null){ + for(NameSpaceItem nsItem:nameSpaceItems){ + SchemaAttr schemaAttr=nsItem.toSchemaAttribute(); + XMLAttribute exist=getAttribute(schemaAttr.getName()); + if(exist!=null){ + exist.setValue(schemaAttr.getValue()); + }else { + addAttribute(schemaAttr); + } + } + } + if(mParent!=null){ + mParent.applyNameSpaceItems(); + } + } + public void addNameSpace(NameSpaceItem nsItem){ + if(nsItem==null){ + return; + } + if(mParent!=null){ + mParent.addNameSpace(nsItem); + return; + } + if(nameSpaceItems==null){ + nameSpaceItems=new HashSet<>(); + } + nameSpaceItems.add(nsItem); + } + public NameSpaceItem getNameSpaceItemForUri(String uri){ + if(nameSpaceItems!=null){ + for(NameSpaceItem ns:nameSpaceItems){ + if(ns.isUriEqual(uri)){ + return ns; + } + } + } + if(mParent!=null){ + return mParent.getNameSpaceItemForUri(uri); + } + return null; + } + public NameSpaceItem getNameSpaceItemForPrefix(String prefix){ + if(nameSpaceItems!=null){ + for(NameSpaceItem ns:nameSpaceItems){ + if(ns.isPrefixEqual(prefix)){ + return ns; + } + } + } + if(mParent!=null){ + return mParent.getNameSpaceItemForPrefix(prefix); + } + return null; + } + void setStart(String start) { + this.mStart = start; + } + void setEnd(String end) { + this.mEnd = end; + } + void setStartPrefix(String pfx) { + if(pfx==null){ + pfx=""; + } + this.mStartPrefix = pfx; + } + void setEndPrefix(String pfx) { + if(pfx==null){ + pfx=""; + } + this.mEndPrefix = pfx; + } + public void setIndentScale(float scale){ + mIndentScale=scale; + } + private float getIndentScale(){ + XMLElement parent=getParent(); + if(parent==null){ + return mIndentScale; + } + return parent.getIndentScale(); + } + public int getResourceId(){ + return mResId; + } + public void setResourceId(int id){ + mResId=id; + } + public XMLElement createElement(String tag) { + XMLElement baseElement=new XMLElement(tag); + addChildNoCheck(baseElement, true); + return baseElement; + } + public void addChild(Collection elements) { + if(elements==null){ + return; + } + for(XMLElement element:elements){ + addChild(element); + } + } + public void addChild(XMLElement child) { + addChildNoCheck(child, true); + } + private void clearChildElements(){ + mChildElements.clear(); + } + private void clearTexts(){ + mTexts.clear(); + } + public XMLComment getCommentAt(int index){ + if(index<0){ + return null; + } + if(index>=mComments.size()){ + return null; + } + return mComments.get(index); + } + public void hideComments(boolean recursive, boolean hide){ + hideComments(hide); + if(recursive){ + for(XMLElement child: mChildElements){ + child.hideComments(recursive, hide); + } + } + } + private void hideComments(boolean hide){ + for(XMLComment ce:mComments){ + ce.setHidden(hide); + } + } + public int getCommentsCount(){ + return mComments.size(); + } + public void addComments(Collection commentElements){ + if(commentElements==null){ + return; + } + for(XMLComment ce:commentElements){ + addComment(ce); + } + } + public void clearComments(){ + mComments.clear(); + } + public void addComment(XMLComment commentElement) { + addCommentInternal(commentElement, true); + } + void addCommentInternal(XMLComment commentElement, boolean addSuper) { + if(commentElement==null){ + return; + } + mComments.add(commentElement); + commentElement.setIndent(getIndent()); + commentElement.setParent(this); + if(addSuper){ + super.addChildNodeInternal(commentElement); + } + } + @Override + void clearChildNodesInternal(){ + super.clearChildNodesInternal(); + mChildElements.clear(); + mComments.clear(); + mTexts.clear(); + } + public Collection listAttributes(){ + return mAttributes.values(); + } + public int getChildesCount(){ + return mChildElements.size(); + } + public List listChildElements(){ + return mChildElements; + } + public XMLElement getChildAt(int index){ + if(index<0 || index>= mChildElements.size()){ + return null; + } + return mChildElements.get(index); + } + public int getAttributeCount(){ + return mAttributes.size(); + } + public String getAttributeValue(String name){ + XMLAttribute attr=getAttribute(name); + if (attr==null){ + return null; + } + return attr.getValue(); + } + public int getAttributeValueInt(String name, int def){ + XMLAttribute attr=getAttribute(name); + if (attr==null){ + return def; + } + return attr.getValueInt(); + } + public int getAttributeValueInt(String name) throws XMLException { + XMLAttribute attr=getAttribute(name); + if (attr==null){ + throw new XMLException("Expecting integer for attr <"+name+ "> at '"+toString()+"'"); + } + try{ + return attr.getValueInt(); + }catch (NumberFormatException ex){ + throw new XMLException(ex.getMessage()+": "+" '"+toString()+"'"); + } + } + public XMLAttribute getAttribute(String name){ + return mAttributes.get(name); + } + public XMLAttribute removeAttribute(String name){ + XMLAttribute attribute = mAttributes.remove(name); + if(attribute!=null){ + attribute.setParent(null); + } + return attribute; + } + public XMLAttribute setAttribute(String name, int value){ + return setAttribute(name, String.valueOf(value)); + } + public XMLAttribute setAttribute(String name, boolean value){ + String v=value?"true":"false"; + return setAttribute(name, v); + } + public XMLAttribute setAttribute(String name, String value){ + if(XMLUtil.isEmpty(name)){ + return null; + } + XMLAttribute attr=getAttribute(name); + if(attr==null){ + if(SchemaAttr.looksSchema(name, value)){ + attr=new SchemaAttr(name, value); + }else{ + attr=new XMLAttribute(name,value); + } + addAttribute(attr); + }else { + attr.setValue(value); + } + return attr; + } + public void addAttributes(Collection attrs){ + if(attrs==null){ + return; + } + for(XMLAttribute a:attrs){ + addAttribute(a); + } + } + public void addAttribute(XMLAttribute attr){ + if(attr==null){ + return; + } + String name = attr.getName(); + if(XMLUtil.isEmpty(name)){ + return; + } + XMLAttribute exist = mAttributes.get(name); + if(exist!=null){ + return; + } + mAttributes.put(name, attr); + attr.setParent(this); + } + public void sortChildes(Comparator comparator){ + if(comparator==null){ + return; + } + mChildElements.sort(comparator); + } + public XMLElement getParent(){ + return mParent; + } + void setParent(XMLElement baseElement){ + mParent=baseElement; + } + @Override + void onChildAdded(XMLNode xmlNode){ + if(xmlNode instanceof XMLComment){ + addCommentInternal((XMLComment) xmlNode, false); + }else if(xmlNode instanceof XMLElement){ + addChildNoCheck((XMLElement) xmlNode, false); + }else if(xmlNode instanceof XMLText){ + addTextInternal((XMLText) xmlNode, false); + } + } + private void addChildNoCheck(XMLElement child, boolean addSupper){ + if(child==null || child == this){ + return; + } + child.setParent(this); + child.setIndent(getChildIndent()); + mChildElements.add(child); + if(addSupper){ + super.addChildNodeInternal(child); + } + } + public int getLevel(){ + int rs=0; + XMLElement parent=getParent(); + if(parent!=null){ + rs=rs+1; + rs+=parent.getLevel(); + } + return rs; + } + int getIndent(){ + XMLElement parent = getParent(); + if(parent!=null && parent.hasTextContent()){ + return 0; + } + return mIndent; + } + int getChildIndent(){ + if(mIndent<=0 || hasTextContent()){ + return 0; + } + int rs=mIndent+1; + String tag= getTagName(); + if(tag!=null){ + int i=tag.length(); + if(i>10){ + i=10; + } + rs+=i; + } + return rs; + } + public void setIndent(int indent){ + mIndent=indent; + int chIndent=getChildIndent(); + for(XMLElement child: mChildElements){ + child.setIndent(chIndent); + } + if(mComments!=null){ + for(XMLComment ce:mComments){ + ce.setIndent(indent); + } + } + } + + /** + * @param indentScale scale of attributes indent relative to element tag start + */ + public void setAttributesIndentScale(float indentScale){ + setAttributesIndentScale(indentScale, true); + } + public void setAttributesIndentScale(float indentScale, boolean setToChildes){ + this.mAttributeIndentScale = indentScale; + if(!setToChildes){ + return; + } + for(XMLElement child:listChildElements()){ + child.setAttributesIndentScale(indentScale, true); + } + } + private float getAttributeIndentScale(){ + return mAttributeIndentScale; + } + private int calculateAttributesIndent(){ + float scale = getAttributeIndentScale(); + int indent = 0; + String tagName = getTagName(); + if(tagName!=null){ + indent += tagName.length(); + } + indent += 2; + if(indent>MAX_ATTRIBUTE_INDENT){ + indent = MAX_ATTRIBUTE_INDENT; + } + int baseIndent = getIndentWidth(); + indent = (int) (scale * indent); + indent = baseIndent + indent; + if(indent<0){ + indent = 0; + } + return indent; + } + private boolean appendAttributesIndentText(Writer writer, boolean appendOnce, int indent) throws IOException { + if(indent<=0){ + return false; + } + if(appendOnce){ + writer.write(XMLUtil.NEW_LINE); + for(int i=0;i40){ + i=40; + } + return i; + } + public String getTagName(){ + return mTagName; + } + public void setTagName(String tag){ + mTagName =tag; + } + public Object getTag(){ + return mTag; + } + public void setTag(Object tag){ + mTag =tag; + } + public String getTextContent(){ + if(!hasTextContent()){ + return null; + } + return buildTextContent(true); + } + public String buildTextContent(boolean unEscape){ + StringWriter writer=new StringWriter(); + try { + for(XMLNode node:getChildNodes()){ + node.buildTextContent(writer, unEscape); + } + writer.flush(); + writer.close(); + } catch (IOException ignored) { + } + return writer.toString(); + } + void buildTextContent(Writer writer, boolean unEscape) throws IOException { + writer.write("<"); + writer.write(getTagName()); + appendAttributes(writer, false); + if(!hasChildNodes()){ + writer.write("/>"); + return; + } + writer.write('>'); + for(XMLNode node:getChildNodes()){ + node.buildTextContent(writer, unEscape); + } + if(hasChildNodes()){ + writer.write("'); + } + } + private void appendTextContent(Writer writer) throws IOException { + for(XMLNode child:getChildNodes()){ + if(child instanceof XMLElement){ + ((XMLElement)child).setIndent(0); + } + child.write(writer, false); + } + } + public boolean hasChildElements(){ + return mChildElements.size()>0; + } + public boolean hasTextContent() { + return mTexts.size()>0; + } + public String getText(){ + if(mTexts.size()==0){ + return null; + } + return mTexts.get(0).getText(); + } + public void setSpannableText(String text){ + clearChildNodes(); + XMLElement element = parseSpanSafe(text); + if(element==null){ + addText( new XMLText(text)); + return; + } + for(XMLNode xmlNode:element.getChildNodes()){ + super.addChildNode(xmlNode); + } + } + public void setTextContent(String text){ + setTextContent(text, true); + } + public void setTextContent(String text, boolean escape){ + clearChildElements(); + clearTexts(); + super.getChildNodes().clear(); + if(escape){ + text=XMLUtil.escapeXmlChars(text); + } + appendText(text); + } + private boolean appendAttributes(Writer writer, boolean newLineAttributes) throws IOException { + boolean addedOnce=false; + int attributesIndent = calculateAttributesIndent(); + boolean indentAppend = false; + for(XMLAttribute attr:listAttributes()){ + if(newLineAttributes){ + indentAppend = appendAttributesIndentText(writer, indentAppend, attributesIndent); + } + if(addedOnce){ + if(!indentAppend){ + writer.write(' '); + } + }else { + writer.write(' '); + } + attr.write(writer); + addedOnce=true; + } + return addedOnce; + } + boolean isEmpty(){ + if(mTagName!=null){ + return false; + } + if(mAttributes.size()>0){ + return false; + } + if(mComments!=null && mComments.size()>0){ + return false; + } + if(mTexts.size()>0){ + return false; + } + return true; + } + private boolean canAppendChildes(){ + for(XMLElement child: mChildElements){ + if (!child.isEmpty()){ + return true; + } + } + return false; + } + boolean appendComments(Writer writer) throws IOException { + if(mComments==null){ + return false; + } + boolean appendPrevious=false; + boolean addedOnce=false; + for(XMLComment ce:mComments){ + if(ce.isEmpty()){ + continue; + } + if(appendPrevious){ + writer.write(XMLUtil.NEW_LINE); + } + appendPrevious=ce.write(writer, false); + if(appendPrevious){ + addedOnce=true; + } + } + return addedOnce; + } + private boolean appendChildes(Writer writer, boolean newLineAttributes) throws IOException { + boolean appendPrevious=true; + boolean addedOnce=false; + for(XMLElement child: mChildElements){ + if(stopWriting(writer)){ + break; + } + if(child.isEmpty()){ + continue; + } + if(appendPrevious){ + writer.write(XMLUtil.NEW_LINE); + } + appendPrevious=child.write(writer, newLineAttributes); + if(!addedOnce && appendPrevious){ + addedOnce=true; + } + } + return addedOnce; + } + private boolean stopWriting(Writer writer){ + if(!(writer instanceof ElementWriter)){ + return false; + } + ElementWriter elementWriter=(ElementWriter)writer; + if(elementWriter.isFinished()){ + elementWriter.writeInterrupted(); + return true; + } + return false; + } + @Override + public boolean write(Writer writer, boolean newLineAttributes) throws IOException { + if(isEmpty()){ + return false; + } + if(stopWriting(writer)){ + return false; + } + boolean appendOnce=appendComments(writer); + if(appendOnce){ + writer.write(XMLUtil.NEW_LINE); + } + appendIndentText(writer); + writer.write(mStart); + String tagName=getTagName(); + if(tagName!=null){ + writer.write(tagName); + } + appendAttributes(writer, newLineAttributes); + boolean useEndTag=false; + boolean hasTextCon=hasTextContent(); + if(hasTextCon){ + writer.write(mEnd); + appendTextContent(writer); + useEndTag=true; + }else if(canAppendChildes()){ + writer.write(mEnd); + appendChildes(writer, newLineAttributes); + useEndTag=true; + } + if(useEndTag){ + if(!hasTextCon){ + writer.write(XMLUtil.NEW_LINE); + appendIndentText(writer); + } + writer.write(mStart); + writer.write(mStartPrefix); + writer.write(getTagName()); + }else { + writer.write(mEndPrefix); + } + writer.write(mEnd); + return true; + } + @Override + public String toText(int indent, boolean newLineAttributes){ + StringWriter writer=new StringWriter(); + setIndent(indent); + try { + write(writer, newLineAttributes); + writer.flush(); + writer.close(); + } catch (IOException ignored) { + } + return writer.toString(); + } + protected List listSpannable(){ + List results = new ArrayList<>(); + for(XMLNode child:getChildNodes()){ + if((child instanceof XMLElement) || (child instanceof XMLText)){ + results.add(child); + } + } + return results; + } + protected String getSpannableText() { + StringBuilder builder = new StringBuilder(); + builder.append(getTagName()); + for(XMLAttribute attribute:listAttributes()){ + builder.append(' '); + builder.append(attribute.toText(0, false)); + } + return builder.toString(); + } + @Override + public String toString(){ + StringWriter strWriter=new StringWriter(); + ElementWriter writer=new ElementWriter(strWriter, DEBUG_TO_STRING); + try { + write(writer, false); + } catch (IOException ignored) { + } + strWriter.flush(); + return strWriter.toString(); + } + + 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; + } + } + + private static final int MAX_ATTRIBUTE_INDENT = 20; +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLException.java b/src/ARSCLib/com/reandroid/xml/XMLException.java new file mode 100755 index 00000000..51683fa0 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLException.java @@ -0,0 +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. + */ +package com.reandroid.xml; + +public class XMLException extends Exception { + public XMLException(String msg){ + super(msg); + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLNode.java b/src/ARSCLib/com/reandroid/xml/XMLNode.java new file mode 100644 index 00000000..a4ad863f --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLNode.java @@ -0,0 +1,95 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +public abstract class XMLNode { + private int mLineNumber; + private int mColumnNumber; + private final List mChildNodes = new ArrayList<>(); + + public int getColumnNumber() { + return mColumnNumber; + } + public void setColumnNumber(int columnNumber) { + this.mColumnNumber = columnNumber; + } + public int getLineNumber() { + return mLineNumber; + } + public void setLineNumber(int lineNumber) { + this.mLineNumber = lineNumber; + } + + public void addChildNode(XMLNode xmlNode){ + boolean addOk=addChildNodeInternal(xmlNode); + if(addOk){ + onChildAdded(xmlNode); + } + } + boolean addChildNodeInternal(XMLNode xmlNode){ + if(xmlNode!=null && canAdd(xmlNode)){ + return mChildNodes.add(xmlNode); + } + return false; + } + void onChildAdded(XMLNode xmlNode){ + + } + boolean canAdd(XMLNode xmlNode){ + return xmlNode!=null; + } + boolean contains(XMLNode xmlNode){ + return mChildNodes.contains(xmlNode); + } + void removeChildNode(XMLNode xmlNode){ + int i = mChildNodes.indexOf(xmlNode); + while (i>=0){ + i = mChildNodes.indexOf(xmlNode); + } + mChildNodes.remove(xmlNode); + } + public void clearChildNodes(){ + clearChildNodesInternal(); + } + void clearChildNodesInternal(){ + mChildNodes.clear(); + } + public List getChildNodes() { + return mChildNodes; + } + boolean hasChildNodes(){ + return mChildNodes.size()>0; + } + void buildTextContent(Writer writer, boolean unEscape) throws IOException{ + + } + public boolean write(Writer writer) throws IOException { + return write(writer, false); + } + public String toText(){ + return toText(1, false); + } + public String toText(boolean newLineAttributes){ + return toText(1, newLineAttributes); + } + public abstract boolean write(Writer writer, boolean newLineAttributes) throws IOException; + public abstract String toText(int indent, boolean newLineAttributes); +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLParserFactory.java b/src/ARSCLib/com/reandroid/xml/XMLParserFactory.java new file mode 100644 index 00000000..ca339e2f --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLParserFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import com.android.org.kxml2.io.KXmlParser; +import com.reandroid.common.FileChannelInputStream; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class XMLParserFactory { + + public static XmlPullParser newPullParser(File file) throws XmlPullParserException { + XmlPullParser parser = newPullParser(); + try { + parser.setInput(new FileChannelInputStream(file), StandardCharsets.UTF_8.name()); + } catch (IOException ex) { + throw new XmlPullParserException(ex.getMessage() + ", file = " + file); + } + return parser; + } + public static XmlPullParser newPullParser(InputStream inputStream) throws XmlPullParserException { + XmlPullParser parser = newPullParser(); + parser.setInput(inputStream, StandardCharsets.UTF_8.name()); + return parser; + } + public static XmlPullParser newPullParser(){ + return new KXmlParser(); + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLSpanInfo.java b/src/ARSCLib/com/reandroid/xml/XMLSpanInfo.java new file mode 100644 index 00000000..17e082bc --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLSpanInfo.java @@ -0,0 +1,27 @@ + /* + * 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; + +public class XMLSpanInfo { + public final String tag; + public final int start; + public int end; + public XMLSpanInfo(String tag, int start, int end){ + this.tag=tag; + this.start=start; + this.end=end; + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLSpannable.java b/src/ARSCLib/com/reandroid/xml/XMLSpannable.java new file mode 100644 index 00000000..859e91bc --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLSpannable.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import com.reandroid.xml.parser.XMLSpanParser; + +import java.util.*; + +public class XMLSpannable implements Comparable{ + private XMLElement mElement; + private String mText; + private List mSpanInfoList; + public XMLSpannable(XMLElement element){ + this.mElement=element; + } + public boolean isValid(){ + List spanInfoList = getSpanInfoList(); + if(spanInfoList.size()==0){ + return false; + } + for(XMLSpanInfo spanInfo:spanInfoList){ + if(spanInfo.end'); + for(XMLNode xmlNode: element.getChildNodes()){ + if(xmlNode instanceof XMLElement){ + appendXml(builder, (XMLElement) xmlNode); + }else if(xmlNode instanceof XMLText){ + appendXml(builder, (XMLText) xmlNode); + } + } + builder.append('<'); + builder.append('/'); + builder.append(element.getTagName()); + builder.append('>'); + } + private void appendXmlAttributes(StringBuilder builder, XMLElement element){ + for(XMLAttribute xmlAttribute : element.listAttributes()){ + builder.append(' '); + builder.append(xmlAttribute.getName()); + builder.append('='); + builder.append('"'); + builder.append(xmlAttribute.getValue()); + builder.append('"'); + } + } + private void appendXml(StringBuilder builder, XMLText xmlText){ + builder.append(xmlText.getText(true)); + } + public String getText(){ + if(mText==null){ + buildSpanInfo(); + } + return mText; + } + public List getSpanInfoList(){ + if(mSpanInfoList==null){ + buildSpanInfo(); + } + return mSpanInfoList; + } + private void buildSpanInfo(){ + mSpanInfoList=new ArrayList<>(); + StringBuilder builder=new StringBuilder(); + buildSpanInfo(mElement, builder); + mText=builder.toString(); + mElement=null; + } + private void buildSpanInfo(XMLElement element, StringBuilder builder){ + XMLSpanInfo info = null; + for(XMLNode node:element.listSpannable()){ + if(info != null){ + int pos = builder.length(); + if(pos > 0){ + pos = pos - 1; + } + info.end = pos; + info = null; + } + if(node instanceof XMLText){ + builder.append(((XMLText)node).getText()); + continue; + } + XMLElement child = (XMLElement) node; + info=new XMLSpanInfo( + child.getSpannableText(), + builder.length(), 0); + mSpanInfoList.add(info); + buildSpanInfo(child, builder); + } + if(info!=null){ + int pos = builder.length(); + if(pos > 0){ + pos = pos - 1; + } + info.end = pos; + } + } + @Override + public int compareTo(XMLSpannable xmlSpannable) { + return getText().compareTo(xmlSpannable.getText()); + } + + public static XMLSpannable parse(String text){ + if(!hasStyle(text)){ + return null; + } + try { + XMLSpannable spannable=new XMLSpannable(PARSER.parse(text)); + if(spannable.isValid()){ + return spannable; + } + } catch (Exception ignored) { + } + return null; + } + public static Set tagList(Collection spannableList){ + Set results=new HashSet<>(); + for(XMLSpannable xmlSpannable:spannableList){ + for(XMLSpanInfo spanInfo: xmlSpannable.getSpanInfoList()){ + results.add(spanInfo.tag); + } + } + return results; + } + public static List toTextList(Collection spannableList){ + List results=new ArrayList<>(spannableList.size()); + for(XMLSpannable xmlSpannable:spannableList){ + results.add(xmlSpannable.getText()); + } + return results; + } + public static void sort(List spannableList){ + Comparator cmp=new Comparator() { + @Override + public int compare(XMLSpannable s1, XMLSpannable s2) { + return s1.compareTo(s2); + } + }; + spannableList.sort(cmp); + } + private static boolean hasStyle(String text){ + if(text==null){ + return false; + } + int i=text.indexOf('<'); + if(i<0){ + return false; + } + i=text.indexOf('>'); + return i>1; + } + + private static final XMLSpanParser PARSER=new XMLSpanParser(); + +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLText.java b/src/ARSCLib/com/reandroid/xml/XMLText.java new file mode 100644 index 00000000..fed511bd --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLText.java @@ -0,0 +1,66 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import java.io.IOException; +import java.io.Writer; + +public class XMLText extends XMLNode{ + private String text; + public XMLText(String text){ + this.text=XMLUtil.escapeXmlChars(text); + } + public XMLText(){ + this(null); + } + + @Override + public void addChildNode(XMLNode xmlNode){ + throw new IllegalArgumentException("Can not add xml node on text: "+xmlNode); + } + public String getText(){ + return getText(true); + } + public String getText(boolean unEscape){ + if(unEscape){ + return XMLUtil.unEscapeXmlChars(text); + } + return text; + } + public void setText(String text){ + this.text=XMLUtil.escapeXmlChars(text); + } + @Override + void buildTextContent(Writer writer, boolean unEscape) throws IOException{ + writer.write(getText(unEscape)); + } + @Override + public boolean write(Writer writer, boolean newLineAttributes) throws IOException { + if(!XMLUtil.isEmpty(this.text)){ + writer.write(this.text); + return true; + } + return false; + } + @Override + public String toText(int indent, boolean newLineAttributes) { + return getText(false); + } + @Override + public String toString(){ + return getText(); + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XMLUtil.java b/src/ARSCLib/com/reandroid/xml/XMLUtil.java new file mode 100755 index 00000000..5c403f05 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XMLUtil.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.xml; + +import java.util.regex.Pattern; + +public class XMLUtil { + public static String NEW_LINE="\n"; + public static boolean isEmpty(String s){ + if(s==null){ + return true; + } + return s.length()==0; + } + 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; + } + public static String escapeQuote(String str){ + if(str==null){ + return null; + } + int i = str.indexOf('"'); + if(i<0){ + return str; + } + str=str.replaceAll("\"", """); + return str; + } + public static String unEscapeXmlChars(String str){ + if(str==null){ + return null; + } + int i=str.indexOf('&'); + if(i<0){ + return str; + } + str=str.replaceAll("&", "&"); + str=str.replaceAll("<", "<"); + str=str.replaceAll(">", ">"); + str=str.replaceAll(""", "\""); + return str; + } + public static String trimQuote(String txt){ + if(txt==null){ + return null; + } + String tmp=txt.trim(); + if(tmp.length()==0){ + return txt; + } + char c1=tmp.charAt(0); + if(c1!='"'){ + return txt; + } + int end=tmp.length()-1; + c1=tmp.charAt(end); + if(c1!='"'){ + return txt; + } + if(end<=1){ + return ""; + } + return tmp.substring(1,end); + } + + + private static final Pattern PATTERN_ESCAPE = Pattern.compile("^.*[><&].*$"); + +} diff --git a/src/ARSCLib/com/reandroid/xml/XmlHeaderElement.java b/src/ARSCLib/com/reandroid/xml/XmlHeaderElement.java new file mode 100755 index 00000000..357f224d --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XmlHeaderElement.java @@ -0,0 +1,108 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + + import java.io.IOException; + import java.io.Writer; + + public class XmlHeaderElement extends XMLElement { + private static final String ATTR_VERSION="version"; + private static final String ATTR_ENCODING="encoding"; + private static final String ATTR_STANDALONE="standalone"; + public XmlHeaderElement(XmlHeaderElement element){ + this(); + copyAll(element); + } + public XmlHeaderElement(){ + super(); + initializeStartEnd(); + setDefaultAttr(); + } + private void copyAll(XmlHeaderElement element){ + if(element==null){ + return; + } + for(XMLAttribute exist : element.listAttributes()){ + setAttribute(exist.getName(), exist.getValue()); + } + } + private void initializeStartEnd(){ + setTagName("xml"); + setStart(""); + setStartPrefix(""); + setEndPrefix(""); + } + private void setDefaultAttr(){ + setVersion("1.0"); + setEncoding("utf-8"); + setStandalone(null); + } + public Object getProperty(String name){ + XMLAttribute attr=getAttribute(name); + if(attr==null){ + return null; + } + String val=attr.getValue(); + if(ATTR_STANDALONE.equalsIgnoreCase(name)){ + boolean res=false; + if("true".equals(val)){ + res=true; + } + return res; + } + return val; + } + public void setProperty(String name, Object o){ + if(ATTR_STANDALONE.equalsIgnoreCase(name)){ + if(o instanceof Boolean){ + setStandalone((Boolean)o); + return; + } + } + String val=null; + if(o!=null){ + val=o.toString(); + } + setAttribute(name, val); + } + public void setVersion(String version){ + setAttribute(ATTR_VERSION, version); + } + public void setEncoding(String encoding){ + setAttribute(ATTR_ENCODING, encoding); + } + public void setStandalone(Boolean flag){ + if(flag==null){ + removeAttribute(ATTR_STANDALONE); + return; + } + String str=flag?"yes":"no"; + setAttribute(ATTR_STANDALONE, str); + } + @Override + int getChildIndent(){ + return 0; + } + @Override + int getIndent(){ + return 0; + } + @Override + void buildTextContent(Writer writer, boolean unEscape) throws IOException { + + } +} diff --git a/src/ARSCLib/com/reandroid/xml/XmlParserToSerializer.java b/src/ARSCLib/com/reandroid/xml/XmlParserToSerializer.java new file mode 100644 index 00000000..0e324adb --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/XmlParserToSerializer.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.xml; + +import android.content.res.XmlResourceParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.Closeable; +import java.io.IOException; + +public class XmlParserToSerializer { + private final XmlSerializer serializer; + private final XmlPullParser parser; + private boolean enableIndent; + boolean processNamespace; + boolean reportNamespaceAttrs; + + public XmlParserToSerializer(XmlPullParser parser, XmlSerializer serializer){ + this.parser = parser; + this.serializer = serializer; + this.enableIndent = true; + setFeatureSafe(parser, XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + setFeatureSafe(parser, XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true); + } + + public void setEnableIndent(boolean enableIndent) { + this.enableIndent = enableIndent; + } + + public void write() throws IOException, XmlPullParserException { + XmlPullParser parser = this.parser; + + this.processNamespace = getFeatureSafe(parser, + XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + + this.reportNamespaceAttrs = getFeatureSafe(parser, + XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, false); + + int event = parser.next(); + while (nextEvent(event)){ + event = parser.next(); + } + close(); + } + private void close() throws IOException { + XmlPullParser parser = this.parser; + if(parser instanceof Closeable){ + ((Closeable)parser).close(); + } + XmlSerializer serializer = this.serializer; + if(serializer instanceof Closeable){ + ((Closeable)serializer).close(); + } + } + private boolean nextEvent(int event) throws IOException, XmlPullParserException { + boolean hasNext = true; + switch (event){ + case XmlResourceParser.START_DOCUMENT: + onStartDocument(); + break; + case XmlResourceParser.START_TAG: + onStartTag(); + break; + case XmlResourceParser.TEXT: + onText(); + break; + case XmlResourceParser.COMMENT: + onComment(); + break; + case XmlResourceParser.END_TAG: + onEndTag(); + break; + case XmlResourceParser.END_DOCUMENT: + onEndDocument(); + hasNext = false; + break; + } + return hasNext; + } + + private void onStartDocument() throws IOException{ + serializer.startDocument("utf-8", null); + } + private void onStartTag() throws IOException, XmlPullParserException { + XmlPullParser parser = this.parser; + XmlSerializer serializer = this.serializer; + + boolean processNamespace = this.processNamespace; + boolean countNamespaceAsAttribute = processNamespace && reportNamespaceAttrs; + + if(enableIndent){ + setFeatureSafe(serializer, FEATURE_INDENT_OUTPUT, true); + } + + if(!countNamespaceAsAttribute){ + int nsCount = parser.getNamespaceCount(parser.getDepth()); + for(int i=0; i= elStackSize) { + // we add at least one extra slot ... + final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; // = lucky 7 + 1 //25 + final boolean needsCopying = elStackSize > 0; + String[] arr = null; + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elName, 0, arr, 0, elStackSize); + elName = arr; + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elPrefix, 0, arr, 0, elStackSize); + elPrefix = arr; + arr = new String[newSize]; + if(needsCopying) System.arraycopy(elUri, 0, arr, 0, elStackSize); + elUri = arr; + + int[] iarr = new int[newSize]; + if(needsCopying) { + System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); + } else { + iarr[0] = 0; + } + elNamespaceCount = iarr; + iarr = new int[newSize]; + if(needsCopying) { + System.arraycopy(elRawNameEnd, 0, iarr, 0, elStackSize); + } + elRawNameEnd = iarr; + + iarr = new int[newSize]; + if(needsCopying) { + System.arraycopy(elRawNameLine, 0, iarr, 0, elStackSize); + } + elRawNameLine = iarr; + + final char[][] carr = new char[newSize][]; + if(needsCopying) { + System.arraycopy(elRawName, 0, carr, 0, elStackSize); + } + elRawName = carr; + } + } + protected int attributeCount; + protected String[] attributeName; + protected int[] attributeNameHash; + protected String[] attributePrefix; + protected String[] attributeUri; + protected String[] attributeValue; + + /** + * Make sure that in attributes temporary array is enough space. + */ + protected void ensureAttributesCapacity(int size) { + final int attrPosSize = attributeName != null ? attributeName.length : 0; + if(size >= attrPosSize) { + final int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25 + + final boolean needsCopying = attrPosSize > 0; + String[] arr = null; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(attributeName, 0, arr, 0, attrPosSize); + attributeName = arr; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(attributePrefix, 0, arr, 0, attrPosSize); + attributePrefix = arr; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(attributeUri, 0, arr, 0, attrPosSize); + attributeUri = arr; + + arr = new String[newSize]; + if(needsCopying) System.arraycopy(attributeValue, 0, arr, 0, attrPosSize); + attributeValue = arr; + + if( ! allStringsInterned ) { + final int[] iarr = new int[newSize]; + if(needsCopying) System.arraycopy(attributeNameHash, 0, iarr, 0, attrPosSize); + attributeNameHash = iarr; + } + + arr = null; + } + } + protected int namespaceEnd; + protected String namespacePrefix[]; + protected int namespacePrefixHash[]; + protected String namespaceUri[]; + + protected void ensureNamespacesCapacity(int size) { + final int namespaceSize = namespacePrefix != null ? namespacePrefix.length : 0; + if(size >= namespaceSize) { + final int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25 + + final String[] newNamespacePrefix = new String[newSize]; + final String[] newNamespaceUri = new String[newSize]; + if(namespacePrefix != null) { + System.arraycopy( + namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd); + System.arraycopy( + namespaceUri, 0, newNamespaceUri, 0, namespaceEnd); + } + namespacePrefix = newNamespacePrefix; + namespaceUri = newNamespaceUri; + + + if( ! allStringsInterned ) { + final int[] newNamespacePrefixHash = new int[newSize]; + if(namespacePrefixHash != null) { + System.arraycopy( + namespacePrefixHash, 0, newNamespacePrefixHash, 0, namespaceEnd); + } + namespacePrefixHash = newNamespacePrefixHash; + } + } + } + protected static int fastHash(char ch[], int off, int len ) { + if(len == 0) { + return 0; + } + int hash = ch[off]; + hash = (hash << 7) + ch[ off + len - 1 ]; + if(len > 16) { + hash = (hash << 7) + ch[ off + (len / 4)]; + } + if(len > 8) { + hash = (hash << 7) + ch[ off + (len / 2)]; + } + return hash; + } + protected int entityEnd; + + protected String entityName[]; + protected char[] entityNameBuf[]; + protected String entityReplacement[]; + protected char[] entityReplacementBuf[]; + + protected int entityNameHash[]; + + protected void ensureEntityCapacity() { + final int entitySize = entityReplacementBuf != null ? entityReplacementBuf.length : 0; + if(entityEnd >= entitySize) { + final int newSize = entityEnd > 7 ? 2 * entityEnd : 8; // = lucky 7 + 1 //25 + final String[] newEntityName = new String[newSize]; + final char[][] newEntityNameBuf = new char[newSize][]; + final String[] newEntityReplacement = new String[newSize]; + final char[][] newEntityReplacementBuf = new char[newSize][]; + if(entityName != null) { + System.arraycopy(entityName, 0, newEntityName, 0, entityEnd); + System.arraycopy(entityNameBuf, 0, newEntityNameBuf, 0, entityEnd); + System.arraycopy(entityReplacement, 0, newEntityReplacement, 0, entityEnd); + System.arraycopy(entityReplacementBuf, 0, newEntityReplacementBuf, 0, entityEnd); + } + entityName = newEntityName; + entityNameBuf = newEntityNameBuf; + entityReplacement = newEntityReplacement; + entityReplacementBuf = newEntityReplacementBuf; + + if( ! allStringsInterned ) { + final int[] newEntityNameHash = new int[newSize]; + if(entityNameHash != null) { + System.arraycopy(entityNameHash, 0, newEntityNameHash, 0, entityEnd); + } + entityNameHash = newEntityNameHash; + } + } + } + protected static final int READ_CHUNK_SIZE = 8*1024; + protected Reader reader; + protected String inputEncoding; + protected InputStream inputStream; + + + protected int bufLoadFactor = 95; + + protected char buf[] = new char[READ_CHUNK_SIZE]; + protected int bufSoftLimit = ( bufLoadFactor * buf.length ) /100; + protected boolean preventBufferCompaction; + + protected int bufAbsoluteStart; // this is buf + protected int bufStart; + protected int bufEnd; + protected int pos; + protected int posStart; + protected int posEnd; + + protected char[] pc = new char[ + Runtime.getRuntime().freeMemory() > 1000000L ? READ_CHUNK_SIZE : 64 ]; + protected int pcStart; + protected int pcEnd; + + protected boolean usePC; + + + protected boolean seenStartTag; + protected boolean seenEndTag; + protected boolean pastEndTag; + protected boolean seenAmpersand; + protected boolean seenMarkup; + protected boolean seenDocdecl; + + protected boolean tokenize; + protected String text; + protected String entityRefName; + + protected String xmlDeclVersion; + protected Boolean xmlDeclStandalone; + protected String xmlDeclContent; + + protected void reset() { + location = null; + lineNumber = 1; + columnNumber = 0; + seenRoot = false; + reachedEnd = false; + eventType = START_DOCUMENT; + emptyElementTag = false; + + depth = 0; + + attributeCount = 0; + + namespaceEnd = 0; + + entityEnd = 0; + + reader = null; + inputEncoding = null; + + preventBufferCompaction = false; + bufAbsoluteStart = 0; + bufEnd = bufStart = 0; + pos = posStart = posEnd = 0; + + pcEnd = pcStart = 0; + + usePC = false; + + seenStartTag = false; + seenEndTag = false; + pastEndTag = false; + seenAmpersand = false; + seenMarkup = false; + seenDocdecl = false; + + xmlDeclVersion = null; + xmlDeclStandalone = null; + xmlDeclContent = null; + + resetStringCache(); + } + + public MXParser() { + } + public void setFeature(String name, boolean state) throws XmlPullParserException + { + if(name == null) throw new IllegalArgumentException("feature name should not be null"); + if(FEATURE_PROCESS_NAMESPACES.equals(name)) { + if(eventType != START_DOCUMENT) throw new XmlPullParserException( + "namespace processing feature can only be changed before parsing", this, null); + processNamespaces = state; + } else if(FEATURE_NAMES_INTERNED.equals(name)) { + if(state != false) { + throw new XmlPullParserException( + "interning names in this implementation is not supported"); + } + } else if(FEATURE_PROCESS_DOCDECL.equals(name)) { + if(state != false) { + throw new XmlPullParserException( + "processing DOCDECL is not supported"); + } + } else if(FEATURE_XML_ROUNDTRIP.equals(name)) { + roundtripSupported = state; + } else { + throw new XmlPullParserException("unsupported feature "+name); + } + } + + public boolean getFeature(String name) + { + if(name == null) throw new IllegalArgumentException("feature name should not be null"); + if(FEATURE_PROCESS_NAMESPACES.equals(name)) { + return processNamespaces; + } else if(FEATURE_NAMES_INTERNED.equals(name)) { + return false; + } else if(FEATURE_PROCESS_DOCDECL.equals(name)) { + return false; + } else if(FEATURE_XML_ROUNDTRIP.equals(name)) { + return roundtripSupported; + } + return false; + } + + public void setProperty(String name, + Object value) + throws XmlPullParserException + { + if(PROPERTY_LOCATION.equals(name)) { + location = (String) value; + } else { + throw new XmlPullParserException("unsupported property: '"+name+"'"); + } + } + + + public Object getProperty(String name) + { + if(name == null) throw new IllegalArgumentException("property name should not be null"); + if(PROPERTY_XMLDECL_VERSION.equals(name)) { + return xmlDeclVersion; + } else if(PROPERTY_XMLDECL_STANDALONE.equals(name)) { + return xmlDeclStandalone; + } else if(PROPERTY_XMLDECL_CONTENT.equals(name)) { + return xmlDeclContent; + } else if(PROPERTY_LOCATION.equals(name)) { + return location; + } + return null; + } + + + public void setInput(Reader in) throws XmlPullParserException + { + reset(); + reader = in; + } + + public void setInput(java.io.InputStream inputStream, String inputEncoding) + throws XmlPullParserException + { + if(inputStream == null) { + throw new IllegalArgumentException("input stream can not be null"); + } + this.inputStream = inputStream; + Reader reader; + try { + if(inputEncoding != null) { + reader = new InputStreamReader(inputStream, inputEncoding); + } else { + reader = new InputStreamReader(inputStream, "UTF-8"); + } + } catch (UnsupportedEncodingException une) { + throw new XmlPullParserException( + "could not create reader for encoding "+inputEncoding+" : "+une, this, une); + } + setInput(reader); + this.inputEncoding = inputEncoding; + } + public InputStream getInputStream(){ + return inputStream; + } + public Reader getReader(){ + reset(); + return reader; + } + + public String getInputEncoding() { + return inputEncoding; + } + + public void defineEntityReplacementText(String entityName, + String replacementText) + throws XmlPullParserException + { + ensureEntityCapacity(); + + // this is to make sure that if interning works we will take advantage of it ... + this.entityName[entityEnd] = newString(entityName.toCharArray(), 0, entityName.length()); + entityNameBuf[entityEnd] = entityName.toCharArray(); + + entityReplacement[entityEnd] = replacementText; + entityReplacementBuf[entityEnd] = replacementText.toCharArray(); + if(!allStringsInterned) { + entityNameHash[ entityEnd ] = + fastHash(entityNameBuf[entityEnd], 0, entityNameBuf[entityEnd].length); + } + ++entityEnd; + } + + public int getNamespaceCount(int depth) throws XmlPullParserException + { + if(!processNamespaces || depth == 0) { + return 0; + } + if(depth < 0 || depth > this.depth) { + throw new IllegalArgumentException( + "allowed namespace depth 0.."+this.depth+" not "+depth); + } + return elNamespaceCount[ depth ]; + } + + public String getNamespacePrefix(int pos) + throws XmlPullParserException + { + if(pos < namespaceEnd) { + return namespacePrefix[ pos ]; + } else { + throw new XmlPullParserException( + "position "+pos+" exceeded number of available namespaces "+namespaceEnd); + } + } + + public String getNamespaceUri(int pos) throws XmlPullParserException + { + if(pos < namespaceEnd) { + return namespaceUri[ pos ]; + } else { + throw new XmlPullParserException( + "position "+pos+" exceeded number of available namespaces "+namespaceEnd); + } + } + + public String getNamespace( String prefix ) + { + if(prefix != null) { + for( int i = namespaceEnd -1; i >= 0; i--) { + if( prefix.equals( namespacePrefix[ i ] ) ) { + return namespaceUri[ i ]; + } + } + if("xml".equals( prefix )) { + return XML_URI; + } else if("xmlns".equals( prefix )) { + return XMLNS_URI; + } + } else { + for( int i = namespaceEnd -1; i >= 0; i--) { + if( namespacePrefix[ i ] == null) { //"") { //null ) { //TODO check FIXME Alek + return namespaceUri[ i ]; + } + } + + } + return null; + } + + + public int getDepth() + { + return depth; + } + + + private static int findFragment(int bufMinPos, char[] b, int start, int end) { + if(start < bufMinPos) { + start = bufMinPos; + if(start > end) start = end; + return start; + } + if(end - start > 65) { + start = end - 10; // try to find good location + } + int i = start + 1; + while(--i > bufMinPos) { + if((end - i) > 65) break; + final char c = b[i]; + if(c == '<' && (start - i) > 10) break; + } + return i; + } + @Override + public String getPositionDescription() + { + return "line="+getLineNumber()+", col="+getColumnNumber(); + } + @Override + public int getLineNumber() + { + return lineNumber; + } + @Override + public int getColumnNumber() + { + return columnNumber; + } + + + public boolean isWhitespace() throws XmlPullParserException + { + if(eventType == TEXT || eventType == CDSECT) { + if(usePC) { + for (int i = pcStart; i = attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return attributeUri[ index ]; + } + + public String getAttributeName(int index) + { + if(eventType != START_TAG) throw new IndexOutOfBoundsException( + "only START_TAG can have attributes"); + if(index < 0 || index >= attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return attributeName[ index ]; + } + + public String getAttributePrefix(int index) + { + if(eventType != START_TAG) throw new IndexOutOfBoundsException( + "only START_TAG can have attributes"); + if(processNamespaces == false) return null; + if(index < 0 || index >= attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return attributePrefix[ index ]; + } + + public String getAttributeType(int index) { + if(eventType != START_TAG) throw new IndexOutOfBoundsException( + "only START_TAG can have attributes"); + if(index < 0 || index >= attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return "CDATA"; + } + + public boolean isAttributeDefault(int index) { + if(eventType != START_TAG) throw new IndexOutOfBoundsException( + "only START_TAG can have attributes"); + if(index < 0 || index >= attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return false; + } + + public String getAttributeValue(int index) + { + if(eventType != START_TAG) throw new IndexOutOfBoundsException( + "only START_TAG can have attributes"); + if(index < 0 || index >= attributeCount) throw new IndexOutOfBoundsException( + "attribute position must be 0.."+(attributeCount-1)+" and not "+index); + return attributeValue[ index ]; + } + + @Override + public String getAttributeValue(String namespace, String name) + { + if(eventType != START_TAG) { + throw new IndexOutOfBoundsException("only START_TAG can have attributes " + +getPositionDescription()); + } + if(name == null) { + throw new IllegalArgumentException("attribute name can not be null"); + } + // TODO make check if namespace is interned!!! etc. for names!!! + if(processNamespaces) { + if(namespace == null) { + namespace = ""; + } + + for(int i = 0; i < attributeCount; ++i) { + if((namespace == attributeUri[ i ] || + namespace.equals(attributeUri[ i ]) ) + //(namespace != null && namespace.equals(attributeUri[ i ])) + // taking advantage of String.intern() + && name.equals(attributeName[ i ]) ) + { + return attributeValue[i]; + } + } + } else { + if(namespace != null && namespace.length() == 0) { + namespace = null; + } + if(namespace != null) throw new IllegalArgumentException( + "when namespaces processing is disabled attribute namespace must be null"); + for(int i = 0; i < attributeCount; ++i) { + if(name.equals(attributeName[i])) + { + return attributeValue[i]; + } + } + } + return null; + } + + + public int getEventType() + throws XmlPullParserException + { + return eventType; + } + + public void require(int type, String namespace, String name) + throws XmlPullParserException, IOException + { + if(processNamespaces == false && namespace != null) { + throw new XmlPullParserException( + "processing namespaces must be enabled on parser (or factory)"+ + " to have possible namespaces declared on elements" + +(" (position:"+ getPositionDescription())+")"); + } + if (type != getEventType() + || (namespace != null && !namespace.equals (getNamespace())) + || (name != null && !name.equals (getName ())) ) + { + throw new XmlPullParserException ( + "expected event "+TYPES[ type ] + +(name != null ? " with name '"+name+"'" : "") + +(namespace != null && name != null ? " and" : "") + +(namespace != null ? " with namespace '"+namespace+"'" : "") + +" but got" + +(type != getEventType() ? " "+TYPES[ getEventType() ] : "") + +(name != null && getName() != null && !name.equals (getName ()) + ? " name '"+getName()+"'" : "") + +(namespace != null && name != null + && getName() != null && !name.equals (getName ()) + && getNamespace() != null && !namespace.equals (getNamespace()) + ? " and" : "") + +(namespace != null && getNamespace() != null && !namespace.equals (getNamespace()) + ? " namespace '"+getNamespace()+"'" : "") + +(" (position:"+ getPositionDescription())+")"); + } + } + + public void skipSubTree() + throws XmlPullParserException, IOException + { + require(START_TAG, null, null); + int level = 1; + while(level > 0) { + int eventType = next(); + if(eventType == END_TAG) { + --level; + } else if(eventType == START_TAG) { + ++level; + } + } + } + + public String nextText() throws XmlPullParserException, IOException + { + if(getEventType() != START_TAG) { + throw new XmlPullParserException( + "parser must be on START_TAG to read next text", this, null); + } + int eventType = next(); + if(eventType == TEXT) { + final String result = getText(); + eventType = next(); + if(eventType != END_TAG) { + throw new XmlPullParserException( + "TEXT must be immediately followed by END_TAG and not " + +TYPES[ getEventType() ], this, null); + } + return result; + } else if(eventType == END_TAG) { + return ""; + } else { + throw new XmlPullParserException( + "parser must be on START_TAG or TEXT to read text", this, null); + } + } + + public int nextTag() throws XmlPullParserException, IOException + { + next(); + if(eventType == TEXT && isWhitespace()) { // skip whitespace + next(); + } + if (eventType != START_TAG && eventType != END_TAG) { + throw new XmlPullParserException("expected START_TAG or END_TAG not " + +TYPES[ getEventType() ], this, null); + } + return eventType; + } + + public int next() + throws XmlPullParserException, IOException + { + tokenize = false; + return nextImpl(); + } + + public int nextToken() + throws XmlPullParserException, IOException + { + tokenize = true; + return nextImpl(); + } + + + protected int nextImpl() + throws XmlPullParserException, IOException + { + text = null; + pcEnd = pcStart = 0; + usePC = false; + bufStart = posEnd; + if(pastEndTag) { + pastEndTag = false; + --depth; + namespaceEnd = elNamespaceCount[ depth ]; // less namespaces available + } + if(emptyElementTag) { + emptyElementTag = false; + pastEndTag = true; + return eventType = END_TAG; + } + if(depth > 0) { + + if(seenStartTag) { + seenStartTag = false; + return eventType = parseStartTag(); + } + if(seenEndTag) { + seenEndTag = false; + return eventType = parseEndTag(); + } + + char ch; + if(seenMarkup) { // we have read ahead ... + seenMarkup = false; + ch = '<'; + } else if(seenAmpersand) { + seenAmpersand = false; + ch = '&'; + } else { + ch = more(); + } + posStart = pos - 1; // VERY IMPORTANT: this is correct start of event!!! + + // when true there is some potential event TEXT to return - keep gathering + boolean hadCharData = false; + + // when true TEXT data is not continual (like ) and requires PC merging + boolean needsMerging = false; + + MAIN_LOOP: + while(true) { + // work on MARKUP + if(ch == '<') { + if(hadCharData) { + //posEnd = pos - 1; + if(tokenize) { + seenMarkup = true; + return eventType = TEXT; + } + } + ch = more(); + if(ch == '/') { + if(!tokenize && hadCharData) { + seenEndTag = true; + //posEnd = pos - 2; + return eventType = TEXT; + } + return eventType = parseEndTag(); + } else if(ch == '!') { + ch = more(); + if(ch == '-') { + // note: if(tokenize == false) posStart/End is NOT changed!!!! + parseComment(); + if(tokenize) return eventType = COMMENT; + if( !usePC && hadCharData ) { + needsMerging = true; + } else { + posStart = pos; //completely ignore comment + } + } else if(ch == '[') { + parseCDSect(hadCharData); + if(tokenize) return eventType = CDSECT; + final int cdStart = posStart; + final int cdEnd = posEnd; + final int cdLen = cdEnd - cdStart; + + + if(cdLen > 0) { // was there anything inside CDATA section? + hadCharData = true; + if(!usePC) { + needsMerging = true; + } + } + } else { + throw new XmlPullParserException( + "unexpected character in markup "+printable(ch), this, null); + } + } else if(ch == '?') { + parsePI(); + if(tokenize) return eventType = PROCESSING_INSTRUCTION; + if( !usePC && hadCharData ) { + needsMerging = true; + } else { + posStart = pos; //completely ignore PI + } + + } else if( isNameStartChar(ch) ) { + if(!tokenize && hadCharData) { + seenStartTag = true; + //posEnd = pos - 2; + return eventType = TEXT; + } + return eventType = parseStartTag(); + } else { + throw new XmlPullParserException( + "unexpected character in markup "+printable(ch), this, null); + } + + } else if(ch == '&') { + // work on ENTITTY + //posEnd = pos - 1; + if(tokenize && hadCharData) { + seenAmpersand = true; + return eventType = TEXT; + } + final int oldStart = posStart + bufAbsoluteStart; + final int oldEnd = posEnd + bufAbsoluteStart; + final char[] resolvedEntity = parseEntityRef(); + if(tokenize) return eventType = ENTITY_REF; + // check if replacement text can be resolved !!! + if(resolvedEntity == null) { + if(entityRefName == null) { + entityRefName = newString(buf, posStart, posEnd - posStart); + } + throw new XmlPullParserException( + "could not resolve entity named '"+printable(entityRefName)+"'", + this, null); + } + //int entStart = posStart; + //int entEnd = posEnd; + posStart = oldStart - bufAbsoluteStart; + posEnd = oldEnd - bufAbsoluteStart; + if(!usePC) { + if(hadCharData) { + joinPC(); // posEnd is already set correctly!!! + needsMerging = false; + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + for (int i = 0; i < resolvedEntity.length; i++) + { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = resolvedEntity[ i ]; + + } + hadCharData = true; + } else { + + if(needsMerging) { + joinPC(); + needsMerging = false; + } + + hadCharData = true; + + boolean normalizedCR = false; + final boolean normalizeInput = tokenize == false || roundtripSupported == false; + + boolean seenBracket = false; + boolean seenBracketBracket = false; + do { + if(ch == ']') { + if(seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else if(seenBracketBracket && ch == '>') { + throw new XmlPullParserException( + "characters ]]> are not allowed in content", this, null); + } else { + if(seenBracket) { + seenBracketBracket = seenBracket = false; + } + // assert seenTwoBrackets == seenBracket == false; + } + if(normalizeInput) { + // deal with normalization issues ... + if(ch == '\r') { + normalizedCR = true; + posEnd = pos -1; + // posEnd is already is set + if(!usePC) { + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + // if(!usePC) { joinPC(); } else { if(pcEnd >= pc.length) ensurePC(); } + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + + ch = more(); + } while(ch != '<' && ch != '&'); + posEnd = pos - 1; + continue MAIN_LOOP; + } + ch = more(); + } // endless while(true) + } else { + if(seenRoot) { + return parseEpilog(); + } else { + return parseProlog(); + } + } + } + + + protected int parseProlog() + throws XmlPullParserException, IOException + { + char ch; + if(seenMarkup) { + ch = buf[ pos - 1 ]; + } else { + ch = more(); + } + + if(eventType == START_DOCUMENT) { + if(ch == '\uFFFE') { + throw new XmlPullParserException( + "first character in input was UNICODE noncharacter (0xFFFE)"+ + "- input requires int swapping", this, null); + } + if(ch == '\uFEFF') { + ch = more(); + } + } + seenMarkup = false; + boolean gotS = false; + posStart = pos - 1; + final boolean normalizeIgnorableWS = tokenize == true && roundtripSupported == false; + boolean normalizedCR = false; + while(true) { + if(ch == '<') { + if(gotS && tokenize) { + posEnd = pos - 1; + seenMarkup = true; + return eventType = IGNORABLE_WHITESPACE; + } + ch = more(); + if(ch == '?') { + if(parsePI()) { // make sure to skip XMLDecl + if(tokenize) { + return eventType = PROCESSING_INSTRUCTION; + } + } else { + // skip over - continue tokenizing + posStart = pos; + gotS = false; + } + + } else if(ch == '!') { + ch = more(); + if(ch == 'D') { + if(seenDocdecl) { + throw new XmlPullParserException( + "only one docdecl allowed in XML document", this, null); + } + seenDocdecl = true; + parseDocdecl(); + if(tokenize) return eventType = DOCDECL; + } else if(ch == '-') { + parseComment(); + if(tokenize) return eventType = COMMENT; + } else { + throw new XmlPullParserException( + "unexpected markup posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + } else { + throw new XmlPullParserException( + "only whitespace content allowed before start tag and not "+printable(ch), + this, null); + } + ch = more(); + } + } + + protected int parseEpilog() + throws XmlPullParserException, IOException + { + if(eventType == END_DOCUMENT) { + throw new XmlPullParserException("already reached end of XML input", this, null); + } + if(reachedEnd) { + return eventType = END_DOCUMENT; + } + boolean gotS = false; + final boolean normalizeIgnorableWS = tokenize && !roundtripSupported; + boolean normalizedCR = false; + try { + char ch; + if(seenMarkup) { + ch = buf[ pos - 1 ]; + } else { + ch = more(); + } + seenMarkup = false; + posStart = pos - 1; + if(!reachedEnd) { + while(true) { + if(ch == '<') { + if(gotS && tokenize) { + posEnd = pos - 1; + seenMarkup = true; + return eventType = IGNORABLE_WHITESPACE; + } + ch = more(); + if(reachedEnd) { + break; + } + if(ch == '?') { + parsePI(); + if(tokenize) return eventType = PROCESSING_INSTRUCTION; + + } else if(ch == '!') { + ch = more(); + if(reachedEnd) { + break; + } + if(ch == 'D') { + parseDocdecl(); + if(tokenize) { + return eventType = DOCDECL; + } + } else if(ch == '-') { + parseComment(); + if(tokenize) return eventType = COMMENT; + } else { + throw new XmlPullParserException( + "unexpected markup posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + } else { + throw new XmlPullParserException( + "in epilog non whitespace content is not allowed but got "+printable(ch), + this, null); + } + ch = more(); + if(reachedEnd) { + break; + } + + } + } + } catch(EOFException ex) { + reachedEnd = true; + } + if(reachedEnd) { + if(tokenize && gotS) { + posEnd = pos; // well - this is LAST available character pos + return eventType = IGNORABLE_WHITESPACE; + } + return eventType = END_DOCUMENT; + } else { + throw new XmlPullParserException("internal error in parseEpilog"); + } + } + + + public int parseEndTag() throws XmlPullParserException, IOException { + char ch = more(); + if(!isNameStartChar(ch)) { + throw new XmlPullParserException( + "expected name start and not "+printable(ch), this, null); + } + posStart = pos - 3; + final int nameStart = pos - 1 + bufAbsoluteStart; + do { + ch = more(); + } while(isNameChar(ch)); + + int off = nameStart - bufAbsoluteStart; + final int len = (pos - 1) - off; + final char[] cbuf = elRawName[depth]; + if(elRawNameEnd[depth] != len) { + // construct strings for exception + final String startname = new String(cbuf, 0, elRawNameEnd[depth]); + final String endname = new String(buf, off, len); + throw new XmlPullParserException( + "end tag name must match start tag name <"+startname+">" + +" from line "+elRawNameLine[depth], this, null); + } + for (int i = 0; i < len; i++) + { + if(buf[off++] != cbuf[i]) { + // construct strings for exception + final String startname = new String(cbuf, 0, len); + final String endname = new String(buf, off - i - 1, len); + throw new XmlPullParserException( + "end tag name must be the same as start tag <"+startname+">" + +" from line "+elRawNameLine[depth], this, null); + } + } + + while(isS(ch)) { + ch = more(); + } // skip additional white spaces + if(ch != '>') { + throw new XmlPullParserException( + "expected > to finish end tag not "+printable(ch) + +" from line "+elRawNameLine[depth], this, null); + } + posEnd = pos; + pastEndTag = true; + return eventType = END_TAG; + } + + public int parseStartTag() throws XmlPullParserException, IOException { + ++depth; + posStart = pos - 2; + emptyElementTag = false; + attributeCount = 0; + final int nameStart = pos - 1 + bufAbsoluteStart; + int colonPos = -1; + char ch = buf[ pos - 1]; + if(ch == ':' && processNamespaces) throw new XmlPullParserException( + "when namespaces processing enabled colon can not be at element name start", + this, null); + while(true) { + ch = more(); + if(!isNameChar(ch)) break; + if(ch == ':' && processNamespaces) { + if(colonPos != -1) throw new XmlPullParserException( + "only one colon is allowed in name of element when namespaces are enabled", + this, null); + colonPos = pos - 1 + bufAbsoluteStart; + } + } + ensureElementsCapacity(); + int elLen = (pos - 1) - (nameStart - bufAbsoluteStart); + if(elRawName[ depth ] == null || elRawName[ depth ].length < elLen) { + elRawName[ depth ] = new char[ 2 * elLen ]; + } + System.arraycopy(buf, nameStart - bufAbsoluteStart, elRawName[ depth ], 0, elLen); + elRawNameEnd[ depth ] = elLen; + elRawNameLine[ depth ] = lineNumber; + + String name = null; + + // work on prefixes and namespace URI + String prefix = null; + if(processNamespaces) { + if(colonPos != -1) { + prefix = elPrefix[ depth ] = newString(buf, nameStart - bufAbsoluteStart, + colonPos - nameStart); + name = elName[ depth ] = newString(buf, colonPos + 1 - bufAbsoluteStart, + //(pos -1) - (colonPos + 1)); + pos - 2 - (colonPos - bufAbsoluteStart)); + } else { + prefix = elPrefix[ depth ] = null; + name = elName[ depth ] = newString(buf, nameStart - bufAbsoluteStart, elLen); + } + } else { + name = elName[ depth ] = newString(buf, nameStart - bufAbsoluteStart, elLen); + } + + + while(true) { + while(isS(ch)) { + ch = more(); + } + + if(ch == '>') { + break; + } else if(ch == '/') { + if(emptyElementTag) throw new XmlPullParserException( + "repeated / in tag declaration", this, null); + emptyElementTag = true; + ch = more(); + if(ch != '>') throw new XmlPullParserException( + "expected > to end empty tag not "+printable(ch), this, null); + break; + } else if(isNameStartChar(ch)) { + ch = parseAttribute(); + ch = more(); + continue; + } else { + throw new XmlPullParserException( + "start tag unexpected character "+printable(ch), this, null); + } + } + + // now when namespaces were declared we can resolve them + if(processNamespaces) { + String uri = getNamespace(prefix); + if(uri == null) { + if(prefix == null) { // no prefix and no uri => use default namespace + uri = NO_NAMESPACE; + } else { + throw new XmlPullParserException( + "could not determine namespace bound to element prefix "+prefix, + this, null); + } + + } + elUri[ depth ] = uri; + + for (int i = 0; i < attributeCount; i++) + { + final String attrPrefix = attributePrefix[ i ]; + if(attrPrefix != null) { + final String attrUri = getNamespace(attrPrefix); + if(attrUri == null) { + throw new XmlPullParserException( + "could not determine namespace bound to attribute prefix "+attrPrefix, + this, null); + + } + attributeUri[ i ] = attrUri; + } else { + attributeUri[ i ] = NO_NAMESPACE; + } + } + + for (int i = 1; i < attributeCount; i++) + { + for (int j = 0; j < i; j++) + { + if( attributeUri[j] == attributeUri[i] + && (allStringsInterned && attributeName[j].equals(attributeName[i]) + || (!allStringsInterned + && attributeNameHash[ j ] == attributeNameHash[ i ] + && attributeName[j].equals(attributeName[i])) ) + + ) { + // prepare data for nice error message? + String attr1 = attributeName[j]; + if(attributeUri[j] != null) attr1 = attributeUri[j]+":"+attr1; + String attr2 = attributeName[i]; + if(attributeUri[i] != null) attr2 = attributeUri[i]+":"+attr2; + throw new XmlPullParserException( + "duplicated attributes "+attr1+" and "+attr2, this, null); + } + } + } + + + } else { + for (int i = 1; i < attributeCount; i++) + { + for (int j = 0; j < i; j++) + { + if((allStringsInterned && attributeName[j].equals(attributeName[i]) + || (!allStringsInterned + && attributeNameHash[ j ] == attributeNameHash[ i ] + && attributeName[j].equals(attributeName[i])) ) + + ) { + // prepare data for nice error message? + final String attr1 = attributeName[j]; + final String attr2 = attributeName[i]; + throw new XmlPullParserException( + "duplicated attributes "+attr1+" and "+attr2, this, null); + } + } + } + } + + elNamespaceCount[ depth ] = namespaceEnd; + posEnd = pos; + return eventType = START_TAG; + } + + protected char parseAttribute() throws XmlPullParserException, IOException + { + final int prevPosStart = posStart + bufAbsoluteStart; + final int nameStart = pos - 1 + bufAbsoluteStart; + int colonPos = -1; + char ch = buf[ pos - 1 ]; + if(ch == ':' && processNamespaces) throw new XmlPullParserException( + "when namespaces processing enabled colon can not be at attribute name start", + this, null); + + + boolean startsWithXmlns = processNamespaces && ch == 'x'; + int xmlnsPos = 0; + + ch = more(); + while(isNameChar(ch)) { + if(processNamespaces) { + if(startsWithXmlns && xmlnsPos < 5) { + ++xmlnsPos; + if(xmlnsPos == 1) { if(ch != 'm') startsWithXmlns = false; } + else if(xmlnsPos == 2) { if(ch != 'l') startsWithXmlns = false; } + else if(xmlnsPos == 3) { if(ch != 'n') startsWithXmlns = false; } + else if(xmlnsPos == 4) { if(ch != 's') startsWithXmlns = false; } + else if(xmlnsPos == 5) { + if(ch != ':') throw new XmlPullParserException( + "after xmlns in attribute name must be colon" + +"when namespaces are enabled", this, null); + //colonPos = pos - 1 + bufAbsoluteStart; + } + } + if(ch == ':') { + if(colonPos != -1) throw new XmlPullParserException( + "only one colon is allowed in attribute name" + +" when namespaces are enabled", this, null); + colonPos = pos - 1 + bufAbsoluteStart; + } + } + ch = more(); + } + + ensureAttributesCapacity(attributeCount); + + String name = null; + String prefix = null; + if(processNamespaces) { + if(xmlnsPos < 4) startsWithXmlns = false; + if(startsWithXmlns) { + if(colonPos != -1) { + //prefix = attributePrefix[ attributeCount ] = null; + final int nameLen = pos - 2 - (colonPos - bufAbsoluteStart); + if(nameLen == 0) { + throw new XmlPullParserException( + "namespace prefix is required after xmlns: " + +" when namespaces are enabled", this, null); + } + name = //attributeName[ attributeCount ] = + newString(buf, colonPos - bufAbsoluteStart + 1, nameLen); + } + } else { + if(colonPos != -1) { + int prefixLen = colonPos - nameStart; + prefix = attributePrefix[ attributeCount ] = + newString(buf, nameStart - bufAbsoluteStart,prefixLen); + //colonPos - (nameStart - bufAbsoluteStart)); + int nameLen = pos - 2 - (colonPos - bufAbsoluteStart); + name = attributeName[ attributeCount ] = + newString(buf, colonPos - bufAbsoluteStart + 1, nameLen); + } else { + prefix = attributePrefix[ attributeCount ] = null; + name = attributeName[ attributeCount ] = + newString(buf, nameStart - bufAbsoluteStart, + pos - 1 - (nameStart - bufAbsoluteStart)); + } + if(!allStringsInterned) { + attributeNameHash[ attributeCount ] = name.hashCode(); + } + } + + } else { + name = attributeName[ attributeCount ] = + newString(buf, nameStart - bufAbsoluteStart, + pos - 1 - (nameStart - bufAbsoluteStart)); + if(!allStringsInterned) { + attributeNameHash[ attributeCount ] = name.hashCode(); + } + } + + while(isS(ch)) { + ch = more(); + } // skip additional spaces + if(ch != '=') { + throw new XmlPullParserException( + "expected = after attribute name '"+name+processNamespaces+"'", this, null); + } + ch = more(); + while(isS(ch)) { + ch = more(); + } // skip additional spaces + + final char delimit = ch; + if(delimit != '"' && delimit != '\'') throw new XmlPullParserException( + "attribute value must start with quotation or apostrophe not " + +printable(delimit), this, null); + boolean normalizedCR = false; + usePC = false; + pcStart = pcEnd; + posStart = pos; + + while(true) { + ch = more(); + if(ch == delimit) { + break; + } if(ch == '<') { + throw new XmlPullParserException( + "markup not allowed inside attribute value - illegal < ", this, null); + } if(ch == '&') { + posEnd = pos - 1; + if(!usePC) { + final boolean hadCharData = posEnd > posStart; + if(hadCharData) { + // posEnd is already set correctly!!! + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + final char[] resolvedEntity = parseEntityRef(); + if(resolvedEntity == null) { + if(entityRefName == null) { + entityRefName = newString(buf, posStart, posEnd - posStart); + } + throw new XmlPullParserException( + "could not resolve entity named '"+printable(entityRefName)+"'", + this, null); + } + for (int i = 0; i < resolvedEntity.length; i++) + { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = resolvedEntity[ i ]; + } + } else if(ch == '\t' || ch == '\n' || ch == '\r') { + if(!usePC) { + posEnd = pos - 1; + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcEnd = pcStart = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + if(ch != '\n' || !normalizedCR) { + pc[pcEnd++] = ' '; //'\n'; + } + + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + } + normalizedCR = ch == '\r'; + } + + + if(processNamespaces && startsWithXmlns) { + String ns = null; + if(!usePC) { + ns = newStringIntern(buf, posStart, pos - 1 - posStart); + } else { + ns = newStringIntern(pc, pcStart, pcEnd - pcStart); + } + ensureNamespacesCapacity(namespaceEnd); + int prefixHash = -1; + if(colonPos != -1) { + if(ns.length() == 0) { + throw new XmlPullParserException( + "non-default namespace can not be declared to be empty string", this, null); + } + // declare new namespace + namespacePrefix[ namespaceEnd ] = name; + if(!allStringsInterned) { + prefixHash = namespacePrefixHash[ namespaceEnd ] = name.hashCode(); + } + } else { + // declare new default namespace ... + namespacePrefix[ namespaceEnd ] = null; + if(!allStringsInterned) { + prefixHash = namespacePrefixHash[ namespaceEnd ] = -1; + } + } + namespaceUri[ namespaceEnd ] = ns; + final int startNs = elNamespaceCount[ depth - 1 ]; + for (int i = namespaceEnd - 1; i >= startNs; --i) + { + if(((allStringsInterned || name == null) && namespacePrefix[ i ] == name) + || (!allStringsInterned && name != null && + namespacePrefixHash[ i ] == prefixHash + && name.equals(namespacePrefix[ i ]) + )) + { + final String s = name == null ? "default" : "'"+name+"'"; + throw new XmlPullParserException( + "duplicated namespace declaration for "+s+" prefix", this, null); + } + } + + ++namespaceEnd; + + } else { + if(!usePC) { + attributeValue[ attributeCount ] = + new String(buf, posStart, pos - 1 - posStart); + } else { + attributeValue[ attributeCount ] = + new String(pc, pcStart, pcEnd - pcStart); + } + ++attributeCount; + } + posStart = prevPosStart - bufAbsoluteStart; + return ch; + } + + protected char[] charRefOneCharBuf = new char[1]; + + protected char[] parseEntityRef() + throws XmlPullParserException, IOException + { + entityRefName = null; + posStart = pos; + char ch = more(); + if(ch == '#') { + // parse character reference + char charRef = 0; + ch = more(); + if(ch == 'x') { + //encoded in hex + while(true) { + ch = more(); + if(ch >= '0' && ch <= '9') { + charRef = (char)(charRef * 16 + (ch - '0')); + } else if(ch >= 'a' && ch <= 'f') { + charRef = (char)(charRef * 16 + (ch - ('a' - 10))); + } else if(ch >= 'A' && ch <= 'F') { + charRef = (char)(charRef * 16 + (ch - ('A' - 10))); + } else if(ch == ';') { + break; + } else { + throw new XmlPullParserException( + "character reference (with hex value) may not contain " + +printable(ch), this, null); + } + } + } else { + // encoded in decimal + while(true) { + if(ch >= '0' && ch <= '9') { + charRef = (char)(charRef * 10 + (ch - '0')); + } else if(ch == ';') { + break; + } else { + throw new XmlPullParserException( + "character reference (with decimal value) may not contain " + +printable(ch), this, null); + } + ch = more(); + } + } + posEnd = pos - 1; + charRefOneCharBuf[0] = charRef; + if(tokenize) { + text = newString(charRefOneCharBuf, 0, 1); + } + return charRefOneCharBuf; + } else { + // [68] EntityRef ::= '&' Name ';' + // scan name until ; + if(!isNameStartChar(ch)) { + throw new XmlPullParserException( + "entity reference names can not start with character '" + +printable(ch)+"'", this, null); + } + while(true) { + ch = more(); + if(ch == ';') { + break; + } + if(!isNameChar(ch)) { + throw new XmlPullParserException( + "entity reference name can not contain character " + +printable(ch)+"'", this, null); + } + } + posEnd = pos - 1; + // determine what name maps to + final int len = posEnd - posStart; + if(len == 2 && buf[posStart] == 'l' && buf[posStart+1] == 't') { + if(tokenize) { + text = "<"; + } + charRefOneCharBuf[0] = '<'; + return charRefOneCharBuf; + } else if(len == 3 && buf[posStart] == 'a' + && buf[posStart+1] == 'm' && buf[posStart+2] == 'p') { + if(tokenize) { + text = "&"; + } + charRefOneCharBuf[0] = '&'; + return charRefOneCharBuf; + } else if(len == 2 && buf[posStart] == 'g' && buf[posStart+1] == 't') { + if(tokenize) { + text = ">"; + } + charRefOneCharBuf[0] = '>'; + return charRefOneCharBuf; + } else if(len == 4 && buf[posStart] == 'a' && buf[posStart+1] == 'p' + && buf[posStart+2] == 'o' && buf[posStart+3] == 's') + { + if(tokenize) { + text = "'"; + } + charRefOneCharBuf[0] = '\''; + return charRefOneCharBuf; + } else if(len == 4 && buf[posStart] == 'q' && buf[posStart+1] == 'u' + && buf[posStart+2] == 'o' && buf[posStart+3] == 't') + { + if(tokenize) { + text = "\""; + } + charRefOneCharBuf[0] = '"'; + return charRefOneCharBuf; + } else { + final char[] result = lookuEntityReplacement(len); + if(result != null) { + return result; + } + } + if(tokenize) text = null; + return null; + } + } + + protected char[] lookuEntityReplacement(int entitNameLen) + throws XmlPullParserException, IOException + + { + if(!allStringsInterned) { + final int hash = fastHash(buf, posStart, posEnd - posStart); + LOOP: + for (int i = entityEnd - 1; i >= 0; --i) + { + if(hash == entityNameHash[ i ] && entitNameLen == entityNameBuf[ i ].length) { + final char[] entityBuf = entityNameBuf[ i ]; + for (int j = 0; j < entitNameLen; j++) + { + if(buf[posStart + j] != entityBuf[j]) continue LOOP; + } + if(tokenize) text = entityReplacement[ i ]; + return entityReplacementBuf[ i ]; + } + } + } else { + entityRefName = newString(buf, posStart, posEnd - posStart); + for (int i = entityEnd - 1; i >= 0; --i) + { + // take advantage that interning for newStirng is enforced + if(entityRefName == entityName[ i ]) { + if(tokenize) text = entityReplacement[ i ]; + return entityReplacementBuf[ i ]; + } + } + } + return null; + } + + + protected void parseComment() + throws XmlPullParserException, IOException + { + // implements XML 1.0 Section 2.5 Comments + + //ASSUMPTION: seen + ch = more(); + if(seenDashDash && ch != '>') { + throw new XmlPullParserException( + "in comment after two dashes (--) next character must be >" + +" not "+printable(ch), this, null); + } + if(ch == '-') { + if(!seenDash) { + seenDash = true; + } else { + seenDashDash = true; + seenDash = false; + } + } else if(ch == '>') { + if(seenDashDash) { + break; // found end sequence!!!! + } else { + seenDashDash = false; + } + seenDash = false; + } else { + seenDash = false; + } + if(normalizeIgnorableWS) { + if(ch == '\r') { + normalizedCR = true; + //posEnd = pos -1; + //joinPC(); + // posEnd is already set + if(!usePC) { + posEnd = pos -1; + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + } + + } catch(EOFException ex) { + throw new XmlPullParserException( + "comment started on line "+curLine+" and column "+curColumn+" was not closed", + this, ex); + } + if(tokenize) { + posEnd = pos - 3; + if(usePC) { + pcEnd -= 2; + } + } + } + + protected boolean parsePI() + throws XmlPullParserException, IOException + { + if(tokenize) posStart = pos; + final int curLine = lineNumber; + final int curColumn = columnNumber; + int piTargetStart = pos + bufAbsoluteStart; + int piTargetEnd = -1; + final boolean normalizeIgnorableWS = tokenize == true && roundtripSupported == false; + boolean normalizedCR = false; + + try { + boolean seenQ = false; + char ch = more(); + if(isS(ch)) { + throw new XmlPullParserException( + "processing instruction PITarget must be exactly after + //ch = more(); + + if(ch == '?') { + seenQ = true; + } else if(ch == '>') { + if(seenQ) { + break; // found end sequence!!!! + } + seenQ = false; + } else { + if(piTargetEnd == -1 && isS(ch)) { + piTargetEnd = pos - 1 + bufAbsoluteStart; + + // [17] PITarget ::= Name - (('X' | 'x') ('M' | 'm') ('L' | 'l')) + if((piTargetEnd - piTargetStart) == 3) { + if((buf[piTargetStart] == 'x' || buf[piTargetStart] == 'X') + && (buf[piTargetStart+1] == 'm' || buf[piTargetStart+1] == 'M') + && (buf[piTargetStart+2] == 'l' || buf[piTargetStart+2] == 'L') + ) + { + if(piTargetStart > 3) { // posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + ch = more(); + } + } catch(EOFException ex) { + throw new XmlPullParserException( + "processing instruction started on line "+curLine+" and column "+curColumn + +" was not closed", + this, ex); + } + if(piTargetEnd == -1) { + piTargetEnd = pos - 2 + bufAbsoluteStart; + } + piTargetStart -= bufAbsoluteStart; + piTargetEnd -= bufAbsoluteStart; + if(tokenize) { + posEnd = pos - 2; + if(normalizeIgnorableWS) { + --pcEnd; + } + } + return true; + } + + protected final static char[] VERSION = "version".toCharArray(); + protected final static char[] NCODING = "ncoding".toCharArray(); + protected final static char[] TANDALONE = "tandalone".toCharArray(); + protected final static char[] YES = "yes".toCharArray(); + protected final static char[] NO = "no".toCharArray(); + + + + protected void parseXmlDecl(char ch) + throws XmlPullParserException, IOException + { + preventBufferCompaction = true; + bufStart = 0; // necessary to keep pos unchanged during expansion! + + ch = skipS(ch); + ch = requireInput(ch, VERSION); + ch = skipS(ch); + if(ch != '=') { + throw new XmlPullParserException( + "expected equals sign (=) after version and not "+printable(ch), this, null); + } + ch = more(); + ch = skipS(ch); + if(ch != '\'' && ch != '"') { + throw new XmlPullParserException( + "expected apostrophe (') or quotation mark (\") after version and not " + +printable(ch), this, null); + } + final char quotChar = ch; + final int versionStart = pos; + ch = more(); + while(ch != quotChar) { + if((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9') + && ch != '_' && ch != '.' && ch != ':' && ch != '-') + { + throw new XmlPullParserException( + " 'z') && (ch < 'A' || ch > 'Z')) + { + throw new XmlPullParserException( + " 'z') && (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9') + && ch != '.' && ch != '_' && ch != '-') + { + throw new XmlPullParserException( + " as last part of ') { + throw new XmlPullParserException( + "expected ?> as last part of ' && bracketLevel == 0) break; + if(normalizeIgnorableWS) { + if(ch == '\r') { + normalizedCR = true; + //posEnd = pos -1; + //joinPC(); + // posEnd is alreadys set + if(!usePC) { + posEnd = pos -1; + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + + } + posEnd = pos - 1; + } + + protected void parseCDSect(boolean hadCharData) + throws XmlPullParserException, IOException + { + char ch = more(); + if(ch != 'C') throw new XmlPullParserException( + "expected <[CDATA[ for comment start", this, null); + ch = more(); + if(ch != 'D') throw new XmlPullParserException( + "expected <[CDATA[ for comment start", this, null); + ch = more(); + if(ch != 'A') throw new XmlPullParserException( + "expected <[CDATA[ for comment start", this, null); + ch = more(); + if(ch != 'T') throw new XmlPullParserException( + "expected <[CDATA[ for comment start", this, null); + ch = more(); + if(ch != 'A') throw new XmlPullParserException( + "expected <[CDATA[ for comment start", this, null); + ch = more(); + if(ch != '[') throw new XmlPullParserException( + "expected posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + } + } + boolean seenBracket = false; + boolean seenBracketBracket = false; + boolean normalizedCR = false; + while(true) { + // scan until it hits "]]>" + ch = more(); + if(ch == ']') { + if(!seenBracket) { + seenBracket = true; + } else { + seenBracketBracket = true; + //seenBracket = false; + } + } else if(ch == '>') { + if(seenBracket && seenBracketBracket) { + break; // found end sequence!!!! + } else { + seenBracketBracket = false; + } + seenBracket = false; + } else { + if(seenBracket) { + seenBracket = false; + } + } + if(normalizeInput) { + // deal with normalization issues ... + if(ch == '\r') { + normalizedCR = true; + posStart = cdStart - bufAbsoluteStart; + posEnd = pos - 1; // posEnd is alreadys set + if(!usePC) { + if(posEnd > posStart) { + joinPC(); + } else { + usePC = true; + pcStart = pcEnd = 0; + } + } + //assert usePC == true; + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } else if(ch == '\n') { + if(!normalizedCR && usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = '\n'; + } + normalizedCR = false; + } else { + if(usePC) { + if(pcEnd >= pc.length) ensurePC(pcEnd); + pc[pcEnd++] = ch; + } + normalizedCR = false; + } + } + } + } catch(EOFException ex) { + throw new XmlPullParserException( + "CDATA section started on line "+curLine+" and column "+curColumn+" was not closed", + this, ex); + } + if(normalizeInput) { + if(usePC) { + pcEnd = pcEnd - 2; + } + } + posStart = cdStart - bufAbsoluteStart; + posEnd = pos - 3; + } + + protected void fillBuf() throws IOException, XmlPullParserException { + if(reader == null) throw new XmlPullParserException( + "reader must be set before parsing is started"); + + if(bufEnd > bufSoftLimit) { + boolean compact = bufStart > bufSoftLimit; + boolean expand = false; + if(preventBufferCompaction) { + compact = false; + expand = true; + } else if(!compact) { + //freeSpace + if(bufStart < buf.length / 2) { + expand = true; + } else { + // at least half of buffer can be reclaimed --> worthwhile effort!!! + compact = true; + } + } + + // if buffer almost full then compact it + if(compact) { + System.arraycopy(buf, bufStart, buf, 0, bufEnd - bufStart); + } else if(expand) { + final int newSize = 2 * buf.length; + final char[] newBuf = new char[ newSize ]; + System.arraycopy(buf, bufStart, newBuf, 0, bufEnd - bufStart); + buf = newBuf; + if(bufLoadFactor > 0) { + bufSoftLimit = (int) (( ((long) bufLoadFactor) * buf.length ) /100); + } + + } else { + throw new XmlPullParserException("internal error in fillBuffer()"); + } + bufEnd -= bufStart; + pos -= bufStart; + posStart -= bufStart; + posEnd -= bufStart; + bufAbsoluteStart += bufStart; + bufStart = 0; + } + final int len = buf.length - bufEnd > READ_CHUNK_SIZE ? READ_CHUNK_SIZE : buf.length - bufEnd; + final int ret = reader.read(buf, bufEnd, len); + if(ret > 0) { + bufEnd += ret; + return; + } + if(ret == -1) { + if(bufAbsoluteStart == 0 && pos == 0) { + throw new EOFException("input contained no data"); + } else { + if(seenRoot && depth == 0) { + reachedEnd = true; + return; + } else { + StringBuffer expectedTagStack = new StringBuffer(); + if(depth > 0) { + expectedTagStack.append(" - expected end tag"); + if(depth > 1) { + expectedTagStack.append("s"); //more than one end tag + } + expectedTagStack.append(" "); + for (int i = depth; i > 0; i--) + { + String tagName = new String(elRawName[i], 0, elRawNameEnd[i]); + expectedTagStack.append("'); + } + expectedTagStack.append(" to close"); + for (int i = depth; i > 0; i--) + { + if(i != depth) { + expectedTagStack.append(" and"); //more than one end tag + } + String tagName = new String(elRawName[i], 0, elRawNameEnd[i]); + expectedTagStack.append(" start tag <"+tagName+">"); + expectedTagStack.append(" from line "+elRawNameLine[i]); + } + expectedTagStack.append(", parser stopped on"); + } + throw new EOFException("no more data available" + +expectedTagStack.toString()+getPositionDescription()); + } + } + } else { + throw new IOException("error reading input, returned "+ret); + } + } + + protected char more() throws IOException, XmlPullParserException { + if(pos >= bufEnd) { + fillBuf(); + if(reachedEnd) { + return (char)-1; + } + } + final char ch = buf[pos++]; + if(ch == '\n') { + ++lineNumber; columnNumber = 1; + } + else { + ++columnNumber; + } + return ch; + } + + protected void ensurePC(int end) { + final int newSize = end > READ_CHUNK_SIZE ? 2 * end : 2 * READ_CHUNK_SIZE; + final char[] newPC = new char[ newSize ]; + System.arraycopy(pc, 0, newPC, 0, pcEnd); + pc = newPC; + } + + protected void joinPC() { + final int len = posEnd - posStart; + final int newEnd = pcEnd + len + 1; + if(newEnd >= pc.length) { + ensurePC(newEnd); + } + System.arraycopy(buf, posStart, pc, pcEnd, len); + pcEnd += len; + usePC = true; + + } + + protected char requireInput(char ch, char[] input) + throws XmlPullParserException, IOException + { + for (int i = 0; i < input.length; i++) + { + if(ch != input[i]) { + throw new XmlPullParserException( + "expected "+printable(input[i])+" in "+new String(input) + +" and not "+printable(ch), this, null); + } + ch = more(); + } + return ch; + } + + protected char requireNextS() + throws XmlPullParserException, IOException + { + final char ch = more(); + if(!isS(ch)) { + throw new XmlPullParserException( + "white space is required and not "+printable(ch), this, null); + } + return skipS(ch); + } + + protected char skipS(char ch) + throws XmlPullParserException, IOException + { + while(isS(ch)) { ch = more(); } // skip additional spaces + return ch; + } + + protected static final int LOOKUP_MAX = 0x400; + protected static final char LOOKUP_MAX_CHAR = (char)LOOKUP_MAX; + protected static boolean[] lookupNameStartChar = new boolean[ LOOKUP_MAX ]; + protected static boolean[] lookupNameChar = new boolean[ LOOKUP_MAX ]; + + private static void setName(char ch) + { + lookupNameChar[ ch ] = true; + } + private static void setNameStart(char ch) + { + lookupNameStartChar[ ch ] = true; setName(ch); + } + + static { + setNameStart(':'); + for (char ch = 'A'; ch <= 'Z'; ++ch) setNameStart(ch); + setNameStart('_'); + for (char ch = 'a'; ch <= 'z'; ++ch) setNameStart(ch); + for (char ch = '\u00c0'; ch <= '\u02FF'; ++ch) setNameStart(ch); + for (char ch = '\u0370'; ch <= '\u037d'; ++ch) setNameStart(ch); + for (char ch = '\u037f'; ch < '\u0400'; ++ch) setNameStart(ch); + + setName('-'); + setName('.'); + for (char ch = '0'; ch <= '9'; ++ch) setName(ch); + setName('\u00b7'); + for (char ch = '\u0300'; ch <= '\u036f'; ++ch) setName(ch); + } + + //private final static boolean isNameStartChar(char ch) { + protected boolean isNameStartChar(char ch) { + return (ch < LOOKUP_MAX_CHAR && lookupNameStartChar[ ch ]) + || (ch >= LOOKUP_MAX_CHAR && ch <= '\u2027') + || (ch >= '\u202A' && ch <= '\u218F') + || (ch >= '\u2800' && ch <= '\uFFEF') ; + + } + + protected boolean isNameChar(char ch) { + return (ch < LOOKUP_MAX_CHAR && lookupNameChar[ ch ]) + || (ch >= LOOKUP_MAX_CHAR && ch <= '\u2027') + || (ch >= '\u202A' && ch <= '\u218F') + || (ch >= '\u2800' && ch <= '\uFFEF') + || ch=='@' || ch=='$'; + } + + protected boolean isS(char ch) { + return (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'); + } + + protected String printable(char ch) { + if(ch == '\n') { + return "\\n"; + } else if(ch == '\r') { + return "\\r"; + } else if(ch == '\t') { + return "\\t"; + } else if(ch == '\'') { + return "\\'"; + } if(ch > 127 || ch < 32) { + return "\\u"+Integer.toHexString((int)ch); + } + return ""+ch; + } + + protected String printable(String s) { + if(s == null) return null; + final int sLen = s.length(); + StringBuffer buf = new StringBuffer(sLen + 10); + for(int i = 0; i < sLen; ++i) { + buf.append(printable(s.charAt(i))); + } + s = buf.toString(); + return s; + } +} + + diff --git a/src/ARSCLib/com/reandroid/xml/parser/MXParserCachingStrings.java b/src/ARSCLib/com/reandroid/xml/parser/MXParserCachingStrings.java new file mode 100644 index 00000000..4be5a9d4 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/MXParserCachingStrings.java @@ -0,0 +1,171 @@ +/* + * This class is taken from org.xmlpull.* + * + * Check license: http://xmlpull.org + * + */ + +/*This package is renamed from org.xmlpull.* to avoid conflicts*/ +package com.reandroid.xml.parser; + +public class MXParserCachingStrings extends MXParser implements Cloneable +{ + protected final static boolean CACHE_STATISTICS = false; + protected final static boolean TRACE_SIZING = false; + protected final static int INITIAL_CAPACITY = 13; + protected int cacheStatCalls; + protected int cacheStatWalks; + protected int cacheStatResets; + protected int cacheStatRehash; + + protected static final int CACHE_LOAD = 77; + protected int cacheEntriesCount; + protected int cacheEntriesThreshold; + + protected char[][] keys; + protected String[] values; + + public MXParserCachingStrings() { + super(); + allStringsInterned = true; + initStringCache(); + } + + @Override + public void setFeature(String name, boolean state) throws XmlPullParserException + { + if(FEATURE_NAMES_INTERNED.equals(name)) { + if(eventType != START_DOCUMENT) throw new XmlPullParserException( + "interning names feature can only be changed before parsing", this, null); + allStringsInterned = state; + if(!state && keys != null) { + resetStringCache(); + } + } else { + super.setFeature(name, state); + } + } + + public boolean getFeature(String name) + { + if(FEATURE_NAMES_INTERNED.equals(name)) { + return allStringsInterned; + } else { + return super.getFeature(name); + } + } + + + protected String newString(char[] cbuf, int off, int len) { + if(allStringsInterned) { + return newStringIntern(cbuf, off, len); + } else { + return super.newString(cbuf, off, len); + } + } + + protected String newStringIntern(char[] cbuf, int off, int len) { + if(CACHE_STATISTICS) { + ++cacheStatCalls; + } + if (cacheEntriesCount >= cacheEntriesThreshold) { + rehash(); + } + int offset = fastHash(cbuf, off, len) % keys.length; + char[] k = null; + while( (k = keys[offset]) != null + && !keysAreEqual(k, 0, k.length, + cbuf, off, len)) + { + offset = (offset + 1) % keys.length; + if(CACHE_STATISTICS) ++cacheStatWalks; + } + if (k != null) { + return values[offset]; + } else { + k = new char[len]; + System.arraycopy(cbuf, off, k, 0, len); + final String v = new String(k).intern(); + keys[offset] = k; + values[offset] = v; + ++cacheEntriesCount; + return v; + } + + } + + protected void initStringCache() { + if(keys == null) { + if(INITIAL_CAPACITY < 0) { + throw new IllegalArgumentException("Illegal initial capacity: " + INITIAL_CAPACITY); + } + if(CACHE_LOAD < 0 || CACHE_LOAD > 99) { + throw new IllegalArgumentException("Illegal load factor: " + CACHE_LOAD); + } + cacheEntriesThreshold = (int)((INITIAL_CAPACITY * CACHE_LOAD)/100); + if(cacheEntriesThreshold >= INITIAL_CAPACITY) { + throw new RuntimeException( + "internal error: threshold must be less than capacity: "+INITIAL_CAPACITY); + } + keys = new char[INITIAL_CAPACITY][]; + values = new String[INITIAL_CAPACITY]; + cacheEntriesCount = 0; + } + } + + protected void resetStringCache() { + if(CACHE_STATISTICS) { + ++cacheStatResets; + } + initStringCache(); + } + + private void rehash() { + if(CACHE_STATISTICS) ++cacheStatRehash; + final int newSize = 2 * keys.length + 1; + cacheEntriesThreshold = (int)((newSize * CACHE_LOAD)/100); + if(cacheEntriesThreshold >= newSize) throw new RuntimeException( + "internal error: threshold must be less than capacity: "+newSize); + + final char[][] newKeys = new char[newSize][]; + final String[] newValues = new String[newSize]; + for(int i = 0; i < keys.length; i++) { + final char[] k = keys[i]; + keys[i] = null; + final String v = values[i]; + values[i] = null; + if(k != null) { + int newOffset = fastHash(k, 0, k.length) % newSize; + char[] newk = null; + while((newk = newKeys[newOffset]) != null) { + if(keysAreEqual(newk, 0, newk.length, + k, 0, k.length)) { + throw new RuntimeException("internal cache error: duplicated keys: "+ + new String(newk)+" and "+new String(k)); + } + newOffset = (newOffset + 1) % newSize; + } + + newKeys[newOffset] = k; + newValues[newOffset] = v; + } + } + keys = newKeys; + values = newValues; + } + + private static boolean keysAreEqual (char[] a, int astart, int alength, + char[] b, int bstart, int blength) { + if(alength != blength) { + return false; + } else { + for(int i = 0; i < alength; i++) { + if(a[astart + i] != b[bstart + i]) { + return false; + } + } + return true; + } + } + +} diff --git a/src/ARSCLib/com/reandroid/xml/parser/MXParserNonValidating.java b/src/ARSCLib/com/reandroid/xml/parser/MXParserNonValidating.java new file mode 100644 index 00000000..2141bd34 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/MXParserNonValidating.java @@ -0,0 +1,309 @@ +/* + * This class is taken from org.xmlpull.* + * + * Check license: http://xmlpull.org + * + */ + +/*This package is renamed from org.xmlpull.* to avoid conflicts*/ +package com.reandroid.xml.parser; + +import java.io.IOException; + +public class MXParserNonValidating extends MXParserCachingStrings +{ + private boolean processDocDecl; + + public MXParserNonValidating() { + super(); + } + + @Override + public void setFeature(String name, + boolean state) throws XmlPullParserException + { + if(FEATURE_PROCESS_DOCDECL.equals(name)) { + if(eventType != START_DOCUMENT) throw new XmlPullParserException( + "process DOCDECL feature can only be changed before parsing", this, null); + processDocDecl = state; + } else { + super.setFeature(name, state); + } + } + + @Override + public boolean getFeature(String name) + { + if(FEATURE_PROCESS_DOCDECL.equals(name)) { + return processDocDecl; + } else { + return super.getFeature(name); + } + } + + + @Override + protected char more() throws IOException, XmlPullParserException { + return super.more(); + } + + @Override + protected char[] lookuEntityReplacement(int entitNameLen) throws XmlPullParserException, IOException + + { + if(!allStringsInterned) { + final int hash = fastHash(buf, posStart, posEnd - posStart); + LOOP: + for (int i = entityEnd - 1; i >= 0; --i) + { + if(hash == entityNameHash[ i ] && entitNameLen == entityNameBuf[ i ].length) { + final char[] entityBuf = entityNameBuf[ i ]; + for (int j = 0; j < entitNameLen; j++) + { + if(buf[posStart + j] != entityBuf[j]) continue LOOP; + } + if(tokenize) text = entityReplacement[ i ]; + return entityReplacementBuf[ i ]; + } + } + } else { + entityRefName = newString(buf, posStart, posEnd - posStart); + for (int i = entityEnd - 1; i >= 0; --i) + { + // take advantage that interning for newStirng is enforced + if(entityRefName == entityName[ i ]) { + if(tokenize) { + text = entityReplacement[ i ]; + } + return entityReplacementBuf[ i ]; + } + } + } + return null; + } + + + @Override + protected void parseDocdecl() + throws XmlPullParserException, IOException + { + //make sure that tokenize flag is disabled temporarily!!!! + final boolean oldTokenize = tokenize; + try { + //ASSUMPTION: seen ' + ch = requireNextS(); + int nameStart = pos; + ch = readName(ch); + int nameEnd = pos; + ch = skipS(ch); + // [75] ExternalID ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral + if(ch == 'S' || ch == 'P') { + ch = processExternalId(ch); + ch = skipS(ch); + } + if(ch == '[') { + processInternalSubset(); + } + ch = skipS(ch); + if(ch != '>') { + throw new XmlPullParserException( + "expected > to finish <[DOCTYPE but got "+printable(ch), this, null); + } + posEnd = pos - 1; + } finally { + tokenize = oldTokenize; + } + } + protected char processExternalId(char ch) + throws XmlPullParserException, IOException + { + // [75] ExternalID ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral + // [11] SystemLiteral ::= ('"' [^"]* '"') | ("'" [^']* "'") + // [12] PubidLiteral ::= '"' PubidChar* '"' | "'" (PubidChar - "'")* "'" + // [13] PubidChar ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%] + + //TODO + + return ch; + } + + protected void processInternalSubset() + throws XmlPullParserException, IOException + { + // [28] ... (markupdecl | DeclSep)* ']' // [WFC: External Subset] + // [28a] DeclSep ::= PEReference | S // [WFC: PE Between Declarations] + + // [69] PEReference ::= '%' Name ';' //[WFC: No Recursion] [WFC: In DTD] + while(true) { + char ch = more(); // firs ttime called it will skip initial "[" + if(ch == ']') break; + if(ch == '%') { + processPEReference(); + } else if(isS(ch)) { + ch = skipS(ch); + } else { + processMarkupDecl(ch); + } + } + } + + protected void processPEReference() + throws XmlPullParserException, IOException + { + //TODO + } + protected void processMarkupDecl(char ch) + throws XmlPullParserException, IOException + { + // [29] markupdecl ::= elementdecl | AttlistDecl | EntityDecl | NotationDecl | PI | Comment + // [WFC: PEs in Internal Subset] + + + //BIG SWITCH statement + if(ch != '<') { + throw new XmlPullParserException("expected < for markupdecl in DTD not "+printable(ch), + this, null); + } + ch = more(); + if(ch == '?') { + parsePI(); + } else if(ch == '!') { + ch = more(); + if(ch == '-') { + // note: if(tokenize == false) posStart/End is NOT changed!!!! + parseComment(); + } else { + ch = more(); + if(ch == 'A') { + processAttlistDecl(ch); //A-TTLIST + } else if(ch == 'E') { + ch = more(); + if(ch == 'L') { + processElementDecl(ch); //EL-EMENT + } else if(ch == 'N') { + processEntityDecl(ch); // EN-TITY + } else { + throw new XmlPullParserException( + "expected ELEMENT or ENTITY after ' + //???? [VC: Unique Element Type Declaration] + // [46] contentspec ::= 'EMPTY' | 'ANY' | Mixed | children + // [47] children ::= (choice | seq) ('?' | '*' | '+')? + // [48] cp ::= (Name | choice | seq) ('?' | '*' | '+')? + // [49] choice ::= '(' S? cp ( S? '|' S? cp )+ S? ')' + // [50] seq ::= '(' S? cp ( S? ',' S? cp )* S? ')' + // [51] Mixed ::= '(' S? '#PCDATA' (S? '|' S? Name)* S? ')*' + // | '(' S? '#PCDATA' S? ')' + + //assert ch == 'L' + ch = requireNextS(); + readName(ch); + ch = requireNextS(); + // readContentSpec(ch); + } + + protected void processAttlistDecl(char ch) + throws XmlPullParserException, IOException + { + // [52] AttlistDecl ::= '' + // [53] AttDef ::= S Name S AttType S DefaultDecl + // [54] AttType ::= StringType | TokenizedType | EnumeratedType + // [55] StringType ::= 'CDATA' + // [56] TokenizedType ::= 'ID' | 'IDREF' | 'IDREFS' | 'ENTITY' | 'ENTITIES' | 'NMTOKEN' + // | 'NMTOKENS' + // [57] EnumeratedType ::= NotationType | Enumeration + // [58] NotationType ::= 'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')' + // [59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')' + // [60] DefaultDecl ::= '#REQUIRED' | '#IMPLIED' | (('#FIXED' S)? AttValue) + // [WFC: No < in Attribute Values] + + //assert ch == 'A' + + } + + + protected void processEntityDecl(char ch) + throws XmlPullParserException, IOException + { + + // [70] EntityDecl ::= GEDecl | PEDecl + // [71] GEDecl ::= '' + // [72] PEDecl ::= '' + // [73] EntityDef ::= EntityValue | (ExternalID NDataDecl?) + // [74] PEDef ::= EntityValue | ExternalID + // [75] ExternalID ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral + + //[9] EntityValue ::= '"' ([^%&"] | PEReference | Reference)* '"' + // | "'" ([^%&'] | PEReference | Reference)* "'" + + //assert ch == 'N' + + } + + protected void processNotationDecl(char ch) + throws XmlPullParserException, IOException + { + + // [82] NotationDecl ::= '' + // [83] PublicID ::= 'PUBLIC' S PubidLiteral + + //assert ch == 'N' + } + + + + protected char readName(char ch) + throws XmlPullParserException, IOException + { + if(isNameStartChar(ch)) { + throw new XmlPullParserException( + "XML name must start with name start character not "+printable(ch), this, null); + } + while(isNameChar(ch)) { + ch = more(); + } + return ch; + } +} \ No newline at end of file diff --git a/src/ARSCLib/com/reandroid/xml/parser/XMLDocumentParser.java b/src/ARSCLib/com/reandroid/xml/parser/XMLDocumentParser.java new file mode 100755 index 00000000..5457df93 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/XMLDocumentParser.java @@ -0,0 +1,384 @@ + /* + * 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.parser; + +import com.android.org.kxml2.io.KXmlParser; +import com.reandroid.common.FileChannelInputStream; +import com.reandroid.xml.*; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +public class XMLDocumentParser { + private final XmlPullParser mParser; + private XMLDocument mResDocument; + private XMLElement mCurrentElement; + private boolean mNameSpaceCreated; + private StringBuilder mCurrentText; + private List mComments; + private int mIndent; + public XMLDocumentParser(XmlPullParser parser){ + this.mParser=parser; + } + public XMLDocumentParser(InputStream in) throws XMLParseException { + this(createParser(in)); + } + public XMLDocumentParser(File file) throws XMLParseException { + this(createParser(file)); + } + public XMLDocumentParser(String text) throws XMLParseException { + this(createParser(text)); + } + + public XMLDocument parse() throws XMLParseException { + try { + XMLDocument document= parseDocument(); + close(); + return document; + } catch (XmlPullParserException | IOException e) { + XMLParseException ex=new XMLParseException(e.getMessage()); + ex.setStackTrace(e.getStackTrace()); + throw ex; + } + } + private void close(){ + closeParser(); + closeReader(); + closeFileInputStream(); + mResDocument=null; + mCurrentElement=null; + mCurrentText=null; + mComments=null; + } + private void closeFileInputStream(){ + if(!(mParser instanceof MXParser)){ + return; + } + MXParser parser=(MXParser) mParser; + InputStream inputStream = parser.getInputStream(); + if(!(inputStream instanceof FileInputStream)){ + return; + } + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + private void closeReader(){ + if(!(mParser instanceof MXParser)){ + return; + } + MXParser parser=(MXParser) mParser; + Reader reader = parser.getReader(); + if(reader!=null){ + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + private void closeParser(){ + if(!(mParser instanceof Closeable)){ + return; + } + Closeable closeable = (Closeable) mParser; + try { + closeable.close(); + } catch (IOException ignored) { + } + } + + private XMLDocument parseDocument() throws XmlPullParserException, IOException { + mResDocument=null; + int type; + while ((type=mParser.nextToken()) !=XmlPullParser.END_DOCUMENT){ + event(type); + } + event(XmlPullParser.END_DOCUMENT); + if(mResDocument==null){ + throw new XmlPullParserException("Failed to parse/empty document"); + } + return mResDocument; + } + private void event(int type) { + if (type == XmlPullParser.START_DOCUMENT){ + onStartDocument(); + }else if (type == XmlPullParser.END_DOCUMENT){ + onEndDocument(); + }else if (type == XmlPullParser.START_TAG){ + onStartTag(); + }else if (type == XmlPullParser.END_TAG){ + onEndTag(); + }else if (type == XmlPullParser.TEXT){ + onText(); + }else if (type == XmlPullParser.ENTITY_REF){ + onEntityRef(); + }else if (type == XmlPullParser.COMMENT){ + onComment(); + }else if (type == XmlPullParser.IGNORABLE_WHITESPACE){ + onIgnorableWhiteSpace(); + }else { + onUnknownType(type); + } + } + private void onStartDocument(){ + mResDocument=new XMLDocument(); + mIndent=-1; + } + private void onEndDocument(){ + flushComments(null); + applyIndent(mResDocument); + } + private void onStartTag(){ + String name=mParser.getName(); + flushTextContent(); + if(mCurrentElement==null){ + if(mResDocument==null){ + onStartDocument(); + } + mCurrentElement=new XMLElement(name); + mResDocument.setDocumentElement(mCurrentElement); + }else { + mCurrentElement=mCurrentElement.createElement(name); + } + mCurrentElement.setColumnNumber(mParser.getColumnNumber()); + mCurrentElement.setLineNumber(mParser.getLineNumber()); + checkIndent(); + flushComments(mCurrentElement); + String ns=mParser.getNamespace(); + if(!XMLUtil.isEmpty(ns)){ + String prefix=mParser.getPrefix(); + if(!XMLUtil.isEmpty(prefix)){ + String tagName=appendPrefix(prefix,name); + mCurrentElement.setTagName(tagName); + checkNamespace(prefix, ns); + } + } + loadAttributes(); + } + private void loadAttributes(){ + int max=mParser.getAttributeCount(); + for(int i=0; i(); + } + mComments.add(ce); + } + private void flushComments(XMLElement element){ + if(mComments==null){ + return; + } + if(element!=null){ + element.addComments(mComments); + } + mComments.clear(); + mComments=null; + } + private void onIgnorableWhiteSpace(){ + } + private void onIgnore(int type){ + + } + private void onUnknownType(int type){ + String typeName=toTypeName(type); + //System.err.println("Unknown TYPE = "+typeName+" "+type); + } + private String toTypeName(int type){ + String[] allTypes=XmlPullParser.TYPES; + if(type<0 || type>=allTypes.length){ + return "type:"+type; + } + return allTypes[type]; + } + + private void checkIndent(){ + if(mIndent>=0){ + return; + } + String txt=mParser.getText(); + if(txt==null){ + return; + } + int len=txt.length(); + int col=mParser.getColumnNumber(); + mIndent=col-len; + if(mIndent<0){ + mIndent=0; + } + } + private void applyIndent(XMLDocument resDocument){ + if(mIndent<=0 || mIndent>5 || resDocument==null){ + mIndent=-1; + return; + } + resDocument.setIndent(mIndent); + mIndent=-1; + } + + private static XmlPullParser createParser(String text) throws XMLParseException { + if(text == null){ + throw new XMLParseException("Text is null, failed to create XmlPullParser"); + } + InputStream in = new ByteArrayInputStream(text.getBytes()); + return createParser(in); + } + private static XmlPullParser createParser(File file) throws XMLParseException { + if(file == null){ + throw new XMLParseException("File is null, failed to create XmlPullParser"); + } + if(!file.isFile()){ + throw new XMLParseException("No such file : "+file.getAbsolutePath()); + } + InputStream in; + try { + in=new FileChannelInputStream(file); + return createParser(in); + } catch (IOException e) { + throw new XMLParseException(e.getMessage()); + } + } + private static XmlPullParser createParser(InputStream in) throws XMLParseException { + try { + XmlPullParser parser = new KXmlParser(); + parser.setInput(in, null); + return parser; + } catch (XmlPullParserException e) { + throw new XMLParseException(e.getMessage()); + } + } + + private static boolean isAndroid(int id){ + int pkgId=toPackageId(id); + return pkgId>0 && pkgId<=ANDROID_PACKAGE_MAX; + } + private static boolean isResourceId(int id){ + int pkgId=toPackageId(id); + return pkgId>0 && pkgId<128; + } + private static int toPackageId(int id){ + if(id<=0xff){ + return id; + } + return ((id >> 24) & 0xff); + } + private static final int ANDROID_PACKAGE_MAX=3; +} diff --git a/src/ARSCLib/com/reandroid/xml/parser/XMLParseException.java b/src/ARSCLib/com/reandroid/xml/parser/XMLParseException.java new file mode 100755 index 00000000..bb8cbbfc --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/XMLParseException.java @@ -0,0 +1,24 @@ + /* + * 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.parser; + +import com.reandroid.xml.XMLException; + +public class XMLParseException extends XMLException { + public XMLParseException(String msg) { + super(msg); + } +} diff --git a/src/ARSCLib/com/reandroid/xml/parser/XMLSpanParser.java b/src/ARSCLib/com/reandroid/xml/parser/XMLSpanParser.java new file mode 100644 index 00000000..9e8e08fc --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/XMLSpanParser.java @@ -0,0 +1,121 @@ +/* + * 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.parser; + +import com.reandroid.xml.*; + +import java.io.IOException; +import java.io.StringReader; + +public class XMLSpanParser { + private final Object mLock = new Object(); + private final XmlPullParser mParser; + private XMLElement mCurrentElement; + public XMLSpanParser(){ + this.mParser = new MXParserNonValidating(); + } + public XMLElement parse(String text) throws XMLException { + synchronized (mLock){ + try { + text=""+text+""; + parseString(text); + } catch (XmlPullParserException|IOException ex) { + throw new XMLException(ex.getMessage()); + } + XMLElement element=mCurrentElement; + mCurrentElement=null; + return element; + } + } + private void parseString(String text) throws XmlPullParserException, IOException { + mCurrentElement=null; + StringReader reader=new StringReader(text); + this.mParser.setInput(reader); + int type; + while ((type=mParser.next()) !=XmlPullParser.END_DOCUMENT){ + event(type); + } + } + private void event(int type) { + if (type == XmlPullParser.START_DOCUMENT){ + onStartDocument(); + }else if (type == XmlPullParser.START_TAG){ + onStartTag(); + }else if (type == XmlPullParser.END_TAG){ + onEndTag(); + }else if (type == XmlPullParser.TEXT){ + onText(); + }else if (type == XmlPullParser.ENTITY_REF){ + onEntityRef(); + }else if (type == XmlPullParser.IGNORABLE_WHITESPACE){ + onText(); + } + } + + + private void loadAttributes(){ + int max=mParser.getAttributeCount(); + for(int i=0; i0){ + mCurrentElement.addText(new XMLText(text)); + } + } + private void onEntityRef() { + String text = getEntity(mParser.getName()); + mCurrentElement.addText(new XMLText(text)); + } + private String getEntity(String name){ + if("amp".equals(name)){ + return "&"; + } + if("lt".equals(name)){ + return "<"; + } + if("gt".equals(name)){ + return ">"; + } + return name; + } + private void onStartDocument() { + this.mCurrentElement=null; + } +} diff --git a/src/ARSCLib/com/reandroid/xml/parser/XmlPullParser.java b/src/ARSCLib/com/reandroid/xml/parser/XmlPullParser.java new file mode 100644 index 00000000..fe2c9ea8 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/XmlPullParser.java @@ -0,0 +1,90 @@ +/* + * This class is taken from org.xmlpull.* + * + * Check license: http://xmlpull.org + * + */ + +/*This package is renamed from org.xmlpull.* to avoid conflicts*/ +package com.reandroid.xml.parser; + +import java.io.InputStream; +import java.io.IOException; +import java.io.Reader; + +@Deprecated +public interface XmlPullParser { + + String NO_NAMESPACE = ""; + int START_DOCUMENT = 0; + int END_DOCUMENT = 1; + int START_TAG = 2; + int END_TAG = 3; + int TEXT = 4; + int CDSECT = 5; + int ENTITY_REF = 6; + int IGNORABLE_WHITESPACE = 7; + int PROCESSING_INSTRUCTION = 8; + int COMMENT = 9; + int DOCDECL = 10; + String [] TYPES = { + "START_DOCUMENT", + "END_DOCUMENT", + "START_TAG", + "END_TAG", + "TEXT", + "CDSECT", + "ENTITY_REF", + "IGNORABLE_WHITESPACE", + "PROCESSING_INSTRUCTION", + "COMMENT", + "DOCDECL" + }; + + String FEATURE_PROCESS_NAMESPACES = "http://xmlpull.org/v1/doc/features.html#process-namespaces"; + + String FEATURE_REPORT_NAMESPACE_ATTRIBUTES = "http://xmlpull.org/v1/doc/features.html#report-namespace-prefixes"; + String FEATURE_PROCESS_DOCDECL = "http://xmlpull.org/v1/doc/features.html#process-docdecl"; + + String FEATURE_VALIDATION = "http://xmlpull.org/v1/doc/features.html#validation"; + + void setFeature(String name, boolean state) throws XmlPullParserException; + boolean getFeature(String name); + void setProperty(String name, Object value) throws XmlPullParserException; + Object getProperty(String name); + void setInput(Reader in) throws XmlPullParserException; + void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException; + String getInputEncoding(); + void defineEntityReplacementText( String entityName, String replacementText ) throws XmlPullParserException; + int getNamespaceCount(int depth) throws XmlPullParserException; + String getNamespacePrefix(int pos) throws XmlPullParserException; + String getNamespaceUri(int pos) throws XmlPullParserException; + String getNamespace (String prefix); + int getDepth(); + String getPositionDescription(); + int getLineNumber(); + int getColumnNumber(); + boolean isWhitespace() throws XmlPullParserException; + String getText (); + char[] getTextCharacters(int [] holderForStartAndLength); + String getNamespace (); + String getName(); + String getPrefix(); + boolean isEmptyElementTag() throws XmlPullParserException; + int getAttributeCount(); + String getAttributeNamespace (int index); + String getAttributeName (int index); + String getAttributePrefix(int index); + String getAttributeType(int index); + boolean isAttributeDefault(int index); + String getAttributeValue(int index); + String getAttributeValue(String namespace, String name); + int getEventType() throws XmlPullParserException; + int next() throws XmlPullParserException, IOException; + int nextToken() throws XmlPullParserException, IOException; + void require(int type, String namespace, String name) throws XmlPullParserException, IOException; + String nextText() throws XmlPullParserException, IOException; + int nextTag() throws XmlPullParserException, IOException; +// public void skipSubTree() throws XmlPullParserException, IOException; +} + diff --git a/src/ARSCLib/com/reandroid/xml/parser/XmlPullParserException.java b/src/ARSCLib/com/reandroid/xml/parser/XmlPullParserException.java new file mode 100644 index 00000000..50915cc7 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/parser/XmlPullParserException.java @@ -0,0 +1,46 @@ +/* + * This class is taken from org.xmlpull.* + * + * Check license: http://xmlpull.org + * + */ + +/*This package is renamed from org.xmlpull.* to avoid conflicts*/ +package com.reandroid.xml.parser; + +@Deprecated +public class XmlPullParserException extends Exception { + protected Throwable detail; + protected int row = -1; + protected int column = -1; + + public XmlPullParserException(String s) { + super(s); + } + public XmlPullParserException(String msg, XmlPullParser parser, Throwable chain) { + super(buildMessage(msg, parser)); + if (parser != null) { + this.row = parser.getLineNumber(); + this.column = parser.getColumnNumber(); + } + this.detail = chain; + } + public Throwable getDetail() { return detail; } + public int getLineNumber() { return row; } + public int getColumnNumber() { return column; } + private static String buildMessage(String msg, XmlPullParser parser){ + StringBuilder builder=new StringBuilder(); + if(parser!=null){ + builder.append("[line="); + builder.append(parser.getLineNumber()); + builder.append(", col="); + builder.append(parser.getColumnNumber()); + builder.append("] "); + } + if(msg!=null){ + builder.append(msg); + } + return builder.toString(); + } +} + diff --git a/src/ARSCLib/com/reandroid/xml/source/XMLDocumentSource.java b/src/ARSCLib/com/reandroid/xml/source/XMLDocumentSource.java new file mode 100644 index 00000000..20ede1ab --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/source/XMLDocumentSource.java @@ -0,0 +1,39 @@ + /* + * 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.XMLDocument; + +public class XMLDocumentSource implements XMLSource{ + private final String path; + private XMLDocument xmlDocument; + public XMLDocumentSource(String path, XMLDocument xmlDocument){ + this.path=path; + this.xmlDocument=xmlDocument; + } + @Override + public String getPath() { + return path; + } + @Override + public XMLDocument getXMLDocument(){ + return xmlDocument; + } + @Override + public void disposeXml() { + xmlDocument=null; + } +} diff --git a/src/ARSCLib/com/reandroid/xml/source/XMLFileSource.java b/src/ARSCLib/com/reandroid/xml/source/XMLFileSource.java new file mode 100644 index 00000000..5eac4f6b --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/source/XMLFileSource.java @@ -0,0 +1,46 @@ + /* + * 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.XMLDocument; +import com.reandroid.xml.XMLException; + +import java.io.File; + +public class XMLFileSource implements XMLSource{ + private final String path; + private final File file; + public XMLFileSource(String path, File file){ + this.path=path; + this.file=file; + } + public File getFile(){ + return this.file; + } + @Override + public String getPath() { + return path; + } + @Override + public XMLDocument getXMLDocument() throws XMLException { + return XMLDocument.load(getFile()); + } + @Override + public void disposeXml() { + } + + +} diff --git a/src/ARSCLib/com/reandroid/xml/source/XMLSource.java b/src/ARSCLib/com/reandroid/xml/source/XMLSource.java new file mode 100644 index 00000000..687dc146 --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/source/XMLSource.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 com.reandroid.xml.XMLDocument; +import com.reandroid.xml.XMLException; + +public interface XMLSource { + public void disposeXml(); + public String getPath(); + public XMLDocument getXMLDocument() throws XMLException; +} diff --git a/src/ARSCLib/com/reandroid/xml/source/XMLStringSource.java b/src/ARSCLib/com/reandroid/xml/source/XMLStringSource.java new file mode 100644 index 00000000..baee62ec --- /dev/null +++ b/src/ARSCLib/com/reandroid/xml/source/XMLStringSource.java @@ -0,0 +1,43 @@ + /* + * 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.XMLDocument; +import com.reandroid.xml.XMLException; + +public class XMLStringSource implements XMLSource{ + private final String path; + private String xmlString; + public XMLStringSource(String path, String xmlString){ + this.path=path; + this.xmlString=xmlString; + } + public String getXmlString() { + return xmlString; + } + @Override + public String getPath() { + return path; + } + @Override + public XMLDocument getXMLDocument() throws XMLException { + return XMLDocument.load(xmlString); + } + @Override + public void disposeXml() { + this.xmlString=null; + } +} diff --git a/src/ARSCLib/meson.build b/src/ARSCLib/meson.build new file mode 100644 index 00000000..17b15bec --- /dev/null +++ b/src/ARSCLib/meson.build @@ -0,0 +1,372 @@ +hax_arsc_lib_jar = jar('hax_arsc_lib', [ + 'com/android/org/kxml2/io/KXmlSerializer.java', + 'com/android/org/kxml2/io/LibCoreStringPool.java', + 'com/android/org/kxml2/io/KXmlParser.java', + 'com/reandroid/common/EntryStore.java', + 'com/reandroid/common/FileChannelInputStream.java', + 'com/reandroid/common/TableEntryStore.java', + 'com/reandroid/common/ReferenceResolver.java', + 'com/reandroid/common/Frameworks.java', + 'com/reandroid/apk/APKLogger.java', + 'com/reandroid/apk/ApkModuleXmlEncoder.java', + 'com/reandroid/apk/FrameworkApk.java', + 'com/reandroid/apk/JsonManifestInputSource.java', + 'com/reandroid/apk/ApkJsonDecoder.java', + 'com/reandroid/apk/ApkUtil.java', + 'com/reandroid/apk/BlockInputSource.java', + 'com/reandroid/apk/FrameworkOptimizer.java', + 'com/reandroid/apk/PathSanitizer.java', + 'com/reandroid/apk/UncompressedFiles.java', + 'com/reandroid/apk/XmlHelper.java', + 'com/reandroid/apk/ResFile.java', + 'com/reandroid/apk/TableBlockJsonBuilder.java', + 'com/reandroid/apk/StringPoolBuilder.java', + 'com/reandroid/apk/CrcOutputStream.java', + 'com/reandroid/apk/RenamedInputSource.java', + 'com/reandroid/apk/PathMap.java', + 'com/reandroid/apk/ApkJsonEncoder.java', + 'com/reandroid/apk/ApkModuleXmlDecoder.java', + 'com/reandroid/apk/SplitJsonTableInputSource.java', + 'com/reandroid/apk/AndroidFrameworks.java', + 'com/reandroid/apk/ApkDecoder.java', + 'com/reandroid/apk/ResourceIds.java', + 'com/reandroid/apk/JsonXmlInputSource.java', + 'com/reandroid/apk/ApkBundle.java', + 'com/reandroid/apk/TableBlockJson.java', + 'com/reandroid/apk/SingleJsonTableInputSource.java', + 'com/reandroid/apk/DexFileInputSource.java', + 'com/reandroid/apk/ApkModule.java', + 'com/reandroid/apk/FileMagic.java', + 'com/reandroid/apk/xmlencoder/XMLEncodeSource.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderBag.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoder.java', + 'com/reandroid/apk/xmlencoder/XMLFileEncoder.java', + 'com/reandroid/apk/xmlencoder/EncodeUtil.java', + 'com/reandroid/apk/xmlencoder/EncodeException.java', + 'com/reandroid/apk/xmlencoder/EncodeMaterials.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderColor.java', + 'com/reandroid/apk/xmlencoder/ValuesEncoder.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderInteger.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderStyle.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderDimen.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderAttr.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderString.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderPlurals.java', + 'com/reandroid/apk/xmlencoder/ValuesStringPoolBuilder.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderArray.java', + 'com/reandroid/apk/xmlencoder/RESEncoder.java', + 'com/reandroid/apk/xmlencoder/FilePathEncoder.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderId.java', + 'com/reandroid/apk/xmlencoder/XMLValuesEncoderCommon.java', + 'com/reandroid/apk/xmldecoder/XMLEntryDecoder.java', + 'com/reandroid/apk/xmldecoder/BagDecoder.java', + 'com/reandroid/apk/xmldecoder/BagDecoderAttr.java', + 'com/reandroid/apk/xmldecoder/EntryWriter.java', + 'com/reandroid/apk/xmldecoder/XMLEntryDecoderSerializer.java', + 'com/reandroid/apk/xmldecoder/EntryWriterElement.java', + 'com/reandroid/apk/xmldecoder/ResXmlDocumentSerializer.java', + 'com/reandroid/apk/xmldecoder/DecoderTableEntry.java', + 'com/reandroid/apk/xmldecoder/XMLDecodeHelper.java', + 'com/reandroid/apk/xmldecoder/DecoderResTableEntry.java', + 'com/reandroid/apk/xmldecoder/BagDecoderArray.java', + 'com/reandroid/apk/xmldecoder/DecoderResTableEntryMap.java', + 'com/reandroid/apk/xmldecoder/BagDecoderPlural.java', + 'com/reandroid/apk/xmldecoder/XMLEntryDecoderDocument.java', + 'com/reandroid/apk/xmldecoder/XMLNamespaceValidator.java', + 'com/reandroid/apk/xmldecoder/BagDecoderCommon.java', + 'com/reandroid/apk/xmldecoder/EntryWriterSerializer.java', + 'com/reandroid/apk/xmldecoder/XMLBagDecoder.java', + 'com/reandroid/arsc/item/SpecFlagsArray.java', + 'com/reandroid/arsc/item/StyleItem.java', + 'com/reandroid/arsc/item/BlockItem.java', + 'com/reandroid/arsc/item/ShortItem.java', + 'com/reandroid/arsc/item/ReferenceItem.java', + 'com/reandroid/arsc/item/ResXmlString.java', + 'com/reandroid/arsc/item/ReferenceBlock.java', + 'com/reandroid/arsc/item/IntegerArray.java', + 'com/reandroid/arsc/item/ByteItem.java', + 'com/reandroid/arsc/item/StyleItemReference.java', + 'com/reandroid/arsc/item/TypeString.java', + 'com/reandroid/arsc/item/LongItem.java', + 'com/reandroid/arsc/item/SpecString.java', + 'com/reandroid/arsc/item/TableString.java', + 'com/reandroid/arsc/item/FixedLengthString.java', + 'com/reandroid/arsc/item/ByteArray.java', + 'com/reandroid/arsc/item/SpecFlag.java', + 'com/reandroid/arsc/item/IntegerItem.java', + 'com/reandroid/arsc/item/ResXmlID.java', + 'com/reandroid/arsc/item/StringItem.java', + 'com/reandroid/arsc/item/IndirectItem.java', + 'com/reandroid/arsc/header/TableHeader.java', + 'com/reandroid/arsc/header/OverlayableHeader.java', + 'com/reandroid/arsc/header/OverlayablePolicyHeader.java', + 'com/reandroid/arsc/header/StringPoolHeader.java', + 'com/reandroid/arsc/header/InfoHeader.java', + 'com/reandroid/arsc/header/HeaderBlock.java', + 'com/reandroid/arsc/header/PackageHeader.java', + 'com/reandroid/arsc/header/XmlNodeHeader.java', + 'com/reandroid/arsc/header/StagedAliasHeader.java', + 'com/reandroid/arsc/header/LibraryHeader.java', + 'com/reandroid/arsc/header/TypeHeader.java', + 'com/reandroid/arsc/header/SpecHeader.java', + 'com/reandroid/arsc/container/BlockList.java', + 'com/reandroid/arsc/container/ResValueContainer.java', + 'com/reandroid/arsc/container/PackageBody.java', + 'com/reandroid/arsc/container/SpecTypePair.java', + 'com/reandroid/arsc/container/FixedBlockContainer.java', + 'com/reandroid/arsc/container/SingleBlockContainer.java', + 'com/reandroid/arsc/container/ExpandableBlockContainer.java', + 'com/reandroid/arsc/base/BlockArray.java', + 'com/reandroid/arsc/base/Block.java', + 'com/reandroid/arsc/base/BlockCounter.java', + 'com/reandroid/arsc/base/BlockCreator.java', + 'com/reandroid/arsc/base/BlockArrayCreator.java', + 'com/reandroid/arsc/base/BlockContainer.java', + 'com/reandroid/arsc/BuildInfo.java', + 'com/reandroid/arsc/list/OverlayableList.java', + 'com/reandroid/arsc/list/StagedAliasList.java', + 'com/reandroid/arsc/decoder/ComplexUtil.java', + 'com/reandroid/arsc/decoder/Decoder.java', + 'com/reandroid/arsc/decoder/ColorUtil.java', + 'com/reandroid/arsc/decoder/ValueDecoder.java', + 'com/reandroid/arsc/decoder/ThreeByteCharsetDecoder.java', + 'com/reandroid/arsc/io/BlockLoad.java', + 'com/reandroid/arsc/io/BlockReader.java', + 'com/reandroid/arsc/pool/ResXmlStringPool.java', + 'com/reandroid/arsc/pool/builder/StringPoolMerger.java', + 'com/reandroid/arsc/pool/builder/StyleBuilder.java', + 'com/reandroid/arsc/pool/StringPool.java', + 'com/reandroid/arsc/pool/SpecStringPool.java', + 'com/reandroid/arsc/pool/TypeStringPool.java', + 'com/reandroid/arsc/pool/TableStringPool.java', + 'com/reandroid/arsc/pool/JsonStringPoolHelper.java', + 'com/reandroid/arsc/chunk/LibraryBlock.java', + 'com/reandroid/arsc/chunk/TableBlock.java', + 'com/reandroid/arsc/chunk/UnknownChunk.java', + 'com/reandroid/arsc/chunk/MainChunk.java', + 'com/reandroid/arsc/chunk/TypeBlock.java', + 'com/reandroid/arsc/chunk/ChunkType.java', + 'com/reandroid/arsc/chunk/PackageBlock.java', + 'com/reandroid/arsc/chunk/xml/ResIdBuilder.java', + 'com/reandroid/arsc/chunk/xml/ResXmlNode.java', + 'com/reandroid/arsc/chunk/xml/ResXmlPullParser.java', + 'com/reandroid/arsc/chunk/xml/ResXmlText.java', + 'com/reandroid/arsc/chunk/xml/ResXmlIDMap.java', + 'com/reandroid/arsc/chunk/xml/ResXmlAttribute.java', + 'com/reandroid/arsc/chunk/xml/ResXmlEndElement.java', + 'com/reandroid/arsc/chunk/xml/ResXmlElement.java', + 'com/reandroid/arsc/chunk/xml/BaseXmlChunk.java', + 'com/reandroid/arsc/chunk/xml/ResXmlDocument.java', + 'com/reandroid/arsc/chunk/xml/ResXmlStartNamespace.java', + 'com/reandroid/arsc/chunk/xml/ResXmlStartElement.java', + 'com/reandroid/arsc/chunk/xml/ParserEventList.java', + 'com/reandroid/arsc/chunk/xml/ResXmlTextNode.java', + 'com/reandroid/arsc/chunk/xml/ParserEvent.java', + 'com/reandroid/arsc/chunk/xml/ResXmlNamespace.java', + 'com/reandroid/arsc/chunk/xml/AndroidManifestBlock.java', + 'com/reandroid/arsc/chunk/xml/ResXmlEndNamespace.java', + 'com/reandroid/arsc/chunk/SpecBlock.java', + 'com/reandroid/arsc/chunk/StagedAlias.java', + 'com/reandroid/arsc/chunk/ParentChunk.java', + 'com/reandroid/arsc/chunk/Overlayable.java', + 'com/reandroid/arsc/chunk/OverlayablePolicy.java', + 'com/reandroid/arsc/chunk/Chunk.java', + 'com/reandroid/arsc/ApkFile.java', + 'com/reandroid/arsc/array/EntryArray.java', + 'com/reandroid/arsc/array/SparseOffsetsArray.java', + 'com/reandroid/arsc/array/StyleArray.java', + 'com/reandroid/arsc/array/SpecTypePairArray.java', + 'com/reandroid/arsc/array/ResXmlIDArray.java', + 'com/reandroid/arsc/array/ResXmlAttributeArray.java', + 'com/reandroid/arsc/array/TypeBlockArray.java', + 'com/reandroid/arsc/array/TypeStringArray.java', + 'com/reandroid/arsc/array/SpecBlockArray.java', + 'com/reandroid/arsc/array/ResValueMapArray.java', + 'com/reandroid/arsc/array/StringArray.java', + 'com/reandroid/arsc/array/StagedAliasEntryArray.java', + 'com/reandroid/arsc/array/TableStringArray.java', + 'com/reandroid/arsc/array/SpecStringArray.java', + 'com/reandroid/arsc/array/OffsetArray.java', + 'com/reandroid/arsc/array/LibraryInfoArray.java', + 'com/reandroid/arsc/array/OffsetBlockArray.java', + 'com/reandroid/arsc/array/CompoundItemArray.java', + 'com/reandroid/arsc/array/ResXmlStringArray.java', + 'com/reandroid/arsc/array/PackageArray.java', + 'com/reandroid/arsc/model/StyleSpanInfo.java', + 'com/reandroid/arsc/model/StyledStringBuilder.java', + 'com/reandroid/arsc/value/AttributeType.java', + 'com/reandroid/arsc/value/Entry.java', + 'com/reandroid/arsc/value/StagedAliasEntry.java', + 'com/reandroid/arsc/value/CompoundEntry.java', + 'com/reandroid/arsc/value/ValueItem.java', + 'com/reandroid/arsc/value/EntryHeader.java', + 'com/reandroid/arsc/value/ResTableMapEntry.java', + 'com/reandroid/arsc/value/ResTableEntry.java', + 'com/reandroid/arsc/value/plurals/PluralsQuantity.java', + 'com/reandroid/arsc/value/plurals/PluralsBag.java', + 'com/reandroid/arsc/value/plurals/PluralsBagItem.java', + 'com/reandroid/arsc/value/Value.java', + 'com/reandroid/arsc/value/AttributeDataFormat.java', + 'com/reandroid/arsc/value/ResValueMap.java', + 'com/reandroid/arsc/value/ResConfig.java', + 'com/reandroid/arsc/value/attribute/AttributeBagItem.java', + 'com/reandroid/arsc/value/attribute/AttributeBag.java', + 'com/reandroid/arsc/value/AttributeValue.java', + 'com/reandroid/arsc/value/EntryHeaderMap.java', + 'com/reandroid/arsc/value/style/StyleBag.java', + 'com/reandroid/arsc/value/style/StyleBagItem.java', + 'com/reandroid/arsc/value/ValueType.java', + 'com/reandroid/arsc/value/TableEntry.java', + 'com/reandroid/arsc/value/ValueHeader.java', + 'com/reandroid/arsc/value/array/ArrayBagItem.java', + 'com/reandroid/arsc/value/array/ArrayBag.java', + 'com/reandroid/arsc/value/bag/BagItem.java', + 'com/reandroid/arsc/value/bag/Bag.java', + 'com/reandroid/arsc/value/bag/MapBag.java', + 'com/reandroid/arsc/value/LibraryInfo.java', + 'com/reandroid/arsc/value/ResValue.java', + 'com/reandroid/arsc/util/FrameworkTable.java', + 'com/reandroid/arsc/util/HexBytesWriter.java', + 'com/reandroid/arsc/util/ResNameMap.java', + 'com/reandroid/arsc/util/HexUtil.java', + 'com/reandroid/arsc/group/EntryGroup.java', + 'com/reandroid/arsc/group/StringGroup.java', + 'com/reandroid/arsc/group/ItemGroup.java', + 'com/reandroid/archive/ZipEntrySource.java', + 'com/reandroid/archive/ByteInputSource.java', + 'com/reandroid/archive/APKArchive.java', + 'com/reandroid/archive/WriteInterceptor.java', + 'com/reandroid/archive/FileInputSource.java', + 'com/reandroid/archive/ZipAlign.java', + 'com/reandroid/archive/InputSource.java', + 'com/reandroid/archive/ZipSerializer.java', + 'com/reandroid/archive/ZipArchive.java', + 'com/reandroid/archive/WriteProgress.java', + 'com/reandroid/archive/InputSourceUtil.java', + 'com/reandroid/xml/XMLDocument.java', + 'com/reandroid/xml/XMLElement.java', + 'com/reandroid/xml/NameSpaceItem.java', + 'com/reandroid/xml/XmlHeaderElement.java', + 'com/reandroid/xml/source/XMLDocumentSource.java', + 'com/reandroid/xml/source/XMLSource.java', + 'com/reandroid/xml/source/XMLStringSource.java', + 'com/reandroid/xml/source/XMLFileSource.java', + 'com/reandroid/xml/XmlParserToSerializer.java', + 'com/reandroid/xml/XMLNode.java', + 'com/reandroid/xml/parser/MXParser.java', + 'com/reandroid/xml/parser/XmlPullParser.java', + 'com/reandroid/xml/parser/XMLParseException.java', + 'com/reandroid/xml/parser/XmlPullParserException.java', + 'com/reandroid/xml/parser/MXParserNonValidating.java', + 'com/reandroid/xml/parser/XMLDocumentParser.java', + 'com/reandroid/xml/parser/MXParserCachingStrings.java', + 'com/reandroid/xml/parser/XMLSpanParser.java', + 'com/reandroid/xml/XMLParserFactory.java', + 'com/reandroid/xml/XMLSpannable.java', + 'com/reandroid/xml/XMLComment.java', + 'com/reandroid/xml/XMLUtil.java', + 'com/reandroid/xml/XMLSpanInfo.java', + 'com/reandroid/xml/ElementWriter.java', + 'com/reandroid/xml/XMLAttribute.java', + 'com/reandroid/xml/SchemaAttr.java', + 'com/reandroid/xml/XMLException.java', + 'com/reandroid/xml/XMLText.java', + 'com/reandroid/archive2/io/CountingOutputStream.java', + 'com/reandroid/archive2/io/ReadOnlyStream.java', + 'com/reandroid/archive2/io/CountingInputStream.java', + 'com/reandroid/archive2/io/SlicedInputStream.java', + 'com/reandroid/archive2/io/FileChannelOutputStream.java', + 'com/reandroid/archive2/io/ZipInput.java', + 'com/reandroid/archive2/io/ArchiveUtil.java', + 'com/reandroid/archive2/io/ZipFileInput.java', + 'com/reandroid/archive2/io/ZipFileOutput.java', + 'com/reandroid/archive2/io/ZipOutput.java', + 'com/reandroid/archive2/io/RandomStream.java', + 'com/reandroid/archive2/io/WriteOnlyStream.java', + 'com/reandroid/archive2/io/ArchiveEntrySource.java', + 'com/reandroid/archive2/ZipSignature.java', + 'com/reandroid/archive2/ArchiveEntry.java', + 'com/reandroid/archive2/block/CertificateBlock.java', + 'com/reandroid/archive2/block/v3/SchemeV31.java', + 'com/reandroid/archive2/block/v3/SchemeV3.java', + 'com/reandroid/archive2/block/LengthPrefixedBlock.java', + 'com/reandroid/archive2/block/LengthPrefixedBytes.java', + 'com/reandroid/archive2/block/CertificateBlockList.java', + 'com/reandroid/archive2/block/BottomBlock.java', + 'com/reandroid/archive2/block/UnknownScheme.java', + 'com/reandroid/archive2/block/DataDescriptor.java', + 'com/reandroid/archive2/block/LocalFileHeader.java', + 'com/reandroid/archive2/block/CentralEntryHeader.java', + 'com/reandroid/archive2/block/ApkSignatureBlock.java', + 'com/reandroid/archive2/block/ZipStringEncoding.java', + 'com/reandroid/archive2/block/SignatureScheme.java', + 'com/reandroid/archive2/block/LengthPrefixedList.java', + 'com/reandroid/archive2/block/stamp/SchemeStampV2.java', + 'com/reandroid/archive2/block/stamp/SchemeStampV1.java', + 'com/reandroid/archive2/block/ZipHeader.java', + 'com/reandroid/archive2/block/SignatureInfo.java', + 'com/reandroid/archive2/block/ZipBlock.java', + 'com/reandroid/archive2/block/EndRecord.java', + 'com/reandroid/archive2/block/v2/SchemeV2.java', + 'com/reandroid/archive2/block/v2/V2SignedDataList.java', + 'com/reandroid/archive2/block/v2/V2Signer.java', + 'com/reandroid/archive2/block/v2/V2Signature.java', + 'com/reandroid/archive2/block/v2/V2SignedData.java', + 'com/reandroid/archive2/block/SignatureId.java', + 'com/reandroid/archive2/block/SignatureFooter.java', + 'com/reandroid/archive2/block/CommonHeader.java', + 'com/reandroid/archive2/block/pad/SchemePadding.java', + 'com/reandroid/archive2/writer/RenamedArchiveSource.java', + 'com/reandroid/archive2/writer/ArchiveOutputSource.java', + 'com/reandroid/archive2/writer/BufferFileOutput.java', + 'com/reandroid/archive2/writer/ZipAligner.java', + 'com/reandroid/archive2/writer/ApkWriter.java', + 'com/reandroid/archive2/writer/OutputSource.java', + 'com/reandroid/archive2/writer/BufferFileInput.java', + 'com/reandroid/archive2/writer/EntryBuffer.java', + 'com/reandroid/archive2/model/CentralFileDirectory.java', + 'com/reandroid/archive2/model/LocalFileDirectory.java', + 'com/reandroid/archive2/Archive.java', + 'com/reandroid/identifiers/PackageIdentifier.java', + 'com/reandroid/identifiers/ResourceIdentifier.java', + 'com/reandroid/identifiers/TypeIdentifier.java', + 'com/reandroid/identifiers/IdentifierMap.java', + 'com/reandroid/identifiers/Identifier.java', + 'com/reandroid/identifiers/TableIdentifier.java', + 'com/reandroid/json/JSONStringer.java', + 'com/reandroid/json/JSONPointer.java', + 'com/reandroid/json/JSONPropertyIgnore.java', + 'com/reandroid/json/CookieList.java', + 'com/reandroid/json/JsonUtil.java', + 'com/reandroid/json/JSONWriter.java', + 'com/reandroid/json/HTTP.java', + 'com/reandroid/json/JSONML.java', + 'com/reandroid/json/CDL.java', + 'com/reandroid/json/XMLParserConfiguration.java', + 'com/reandroid/json/XMLTokener.java', + 'com/reandroid/json/HTTPTokener.java', + 'com/reandroid/json/JSONString.java', + 'com/reandroid/json/JSONTokener.java', + 'com/reandroid/json/JSONException.java', + 'com/reandroid/json/XMLXsiTypeConverter.java', + 'com/reandroid/json/XML.java', + 'com/reandroid/json/Cookie.java', + 'com/reandroid/json/JSONItem.java', + 'com/reandroid/json/Property.java', + 'com/reandroid/json/JSONConvert.java', + 'com/reandroid/json/JSONObject.java', + 'com/reandroid/json/JSONPropertyName.java', + 'com/reandroid/json/JSONArray.java', + 'com/reandroid/json/JSONPointerException.java', + 'android/content/res/XmlResourceParser.java', + 'android/util/AttributeSet.java', + 'org/xmlpull/v1/XmlPullParser.java', + 'org/xmlpull/v1/XmlPullParserException.java', + 'org/xmlpull/v1/XmlPullParserFactory.java', + 'org/xmlpull/v1/XmlSerializer.java', + ], + java_args: [ + '-bootclasspath', bootclasspath, + '-source', '1.8', '-target', '1.8' + ]) + diff --git a/src/ARSCLib/org/xmlpull/v1/XmlPullParser.java b/src/ARSCLib/org/xmlpull/v1/XmlPullParser.java new file mode 100644 index 00000000..675e3576 --- /dev/null +++ b/src/ARSCLib/org/xmlpull/v1/XmlPullParser.java @@ -0,0 +1,78 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) +package org.xmlpull.v1; + +import java.io.InputStream; +import java.io.IOException; +import java.io.Reader; + +public interface XmlPullParser{ + String NO_NAMESPACE = ""; + int START_DOCUMENT = 0; + int END_DOCUMENT = 1; + int START_TAG = 2; + int END_TAG = 3; + int TEXT = 4; + int CDSECT = 5; + int ENTITY_REF = 6; + int IGNORABLE_WHITESPACE = 7; + int PROCESSING_INSTRUCTION = 8; + int COMMENT = 9; + int DOCDECL = 10; + String [] TYPES = { + "START_DOCUMENT", + "END_DOCUMENT", + "START_TAG", + "END_TAG", + "TEXT", + "CDSECT", + "ENTITY_REF", + "IGNORABLE_WHITESPACE", + "PROCESSING_INSTRUCTION", + "COMMENT", + "DOCDECL" + }; + String FEATURE_PROCESS_NAMESPACES = "http://xmlpull.org/v1/doc/features.html#process-namespaces"; + String FEATURE_REPORT_NAMESPACE_ATTRIBUTES = "http://xmlpull.org/v1/doc/features.html#report-namespace-prefixes"; + String FEATURE_PROCESS_DOCDECL = "http://xmlpull.org/v1/doc/features.html#process-docdecl"; + String FEATURE_VALIDATION = "http://xmlpull.org/v1/doc/features.html#validation"; + + void setFeature(String name, boolean state) throws XmlPullParserException; + boolean getFeature(String name); + void setProperty(String name, Object value) throws XmlPullParserException; + Object getProperty(String name); + void setInput(Reader in) throws XmlPullParserException; + void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException; + String getInputEncoding(); + void defineEntityReplacementText( String entityName, String replacementText ) throws XmlPullParserException; + int getNamespaceCount(int depth) throws XmlPullParserException; + String getNamespacePrefix(int pos) throws XmlPullParserException; + String getNamespaceUri(int pos) throws XmlPullParserException; + String getNamespace (String prefix); + int getDepth(); + String getPositionDescription (); + int getLineNumber(); + int getColumnNumber(); + boolean isWhitespace() throws XmlPullParserException; + String getText (); + char[] getTextCharacters(int [] holderForStartAndLength); + String getNamespace (); + String getName(); + String getPrefix(); + boolean isEmptyElementTag() throws XmlPullParserException; + int getAttributeCount(); + String getAttributeNamespace (int index); + String getAttributeName (int index); + String getAttributePrefix(int index); + String getAttributeType(int index); + boolean isAttributeDefault(int index); + String getAttributeValue(int index); + String getAttributeValue(String namespace, String name); + int getEventType() throws XmlPullParserException; + int next() throws XmlPullParserException, IOException; + int nextToken() throws XmlPullParserException, IOException; + void require(int type, String namespace, String name) throws XmlPullParserException, IOException; + String nextText() throws XmlPullParserException, IOException; + int nextTag() throws XmlPullParserException, IOException; + +} diff --git a/src/ARSCLib/org/xmlpull/v1/XmlPullParserException.java b/src/ARSCLib/org/xmlpull/v1/XmlPullParserException.java new file mode 100644 index 00000000..c62a21b4 --- /dev/null +++ b/src/ARSCLib/org/xmlpull/v1/XmlPullParserException.java @@ -0,0 +1,76 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) + +package org.xmlpull.v1; + +/** + * This exception is thrown to signal XML Pull Parser related faults. + * + * @author
Aleksander Slominski + */ +public class XmlPullParserException extends Exception{ + protected Throwable detail; + protected int row = -1; + protected int column = -1; + + /* public XmlPullParserException() { + }*/ + + public XmlPullParserException(String s) { + super(s); + } + + /* + public XmlPullParserException(String s, Throwable thrwble) { + super(s); + this.detail = thrwble; + } + + public XmlPullParserException(String s, int row, int column) { + super(s); + this.row = row; + this.column = column; + } + */ + + public XmlPullParserException(String msg, XmlPullParser parser, Throwable chain) { + super ((msg == null ? "" : msg+" ") + + (parser == null ? "" : "(position:"+parser.getPositionDescription()+") ") + + (chain == null ? "" : "caused by: "+chain)); + + if (parser != null) { + this.row = parser.getLineNumber(); + this.column = parser.getColumnNumber(); + } + this.detail = chain; + } + + public Throwable getDetail() { return detail; } + // public void setDetail(Throwable cause) { this.detail = cause; } + public int getLineNumber() { return row; } + public int getColumnNumber() { return column; } + + /* + public String getMessage() { + if(detail == null) + return super.getMessage(); + else + return super.getMessage() + "; nested exception is: \n\t" + + detail.getMessage(); + } + */ + + //NOTE: code that prints this and detail is difficult in J2ME + public void printStackTrace() { + if (detail == null) { + super.printStackTrace(); + } else { + synchronized(System.err) { + System.err.println(super.getMessage() + "; nested exception is:"); + detail.printStackTrace(); + } + } + } + +} + diff --git a/src/ARSCLib/org/xmlpull/v1/XmlPullParserFactory.java b/src/ARSCLib/org/xmlpull/v1/XmlPullParserFactory.java new file mode 100644 index 00000000..46bff9bd --- /dev/null +++ b/src/ARSCLib/org/xmlpull/v1/XmlPullParserFactory.java @@ -0,0 +1,119 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) +package org.xmlpull.v1; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class XmlPullParserFactory { + public static final String PROPERTY_NAME = "org.xmlpull.v1.XmlPullParserFactory"; + protected ArrayList parserClasses; + protected ArrayList serializerClasses; + protected String classNamesLocation = null; + protected HashMap features = new HashMap(); + protected XmlPullParserFactory() { + parserClasses = new ArrayList(); + serializerClasses = new ArrayList(); + try { + parserClasses.add(Class.forName("com.android.org.kxml2.io.KXmlParser")); + serializerClasses.add(Class.forName("com.android.org.kxml2.io.KXmlSerializer")); + } catch (ClassNotFoundException e) { + throw new AssertionError(); + } + } + public void setFeature(String name, boolean state) throws XmlPullParserException { + features.put(name, state); + } + public boolean getFeature(String name) { + Boolean value = features.get(name); + return value != null ? value.booleanValue() : false; + } + public void setNamespaceAware(boolean awareness) { + features.put (XmlPullParser.FEATURE_PROCESS_NAMESPACES, awareness); + } + public boolean isNamespaceAware() { + return getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES); + } + public void setValidating(boolean validating) { + features.put(XmlPullParser.FEATURE_VALIDATION, validating); + } + + public boolean isValidating() { + return getFeature(XmlPullParser.FEATURE_VALIDATION); + } + public XmlPullParser newPullParser() throws XmlPullParserException { + final XmlPullParser pp = getParserInstance(); + for (Map.Entry entry : features.entrySet()) { + if (entry.getValue()) { + pp.setFeature(entry.getKey(), entry.getValue()); + } + } + return pp; + } + private XmlPullParser getParserInstance() throws XmlPullParserException { + ArrayList exceptions = null; + if (parserClasses != null && !parserClasses.isEmpty()) { + exceptions = new ArrayList(); + for (Object o : parserClasses) { + try { + if (o != null) { + Class parserClass = (Class) o; + return (XmlPullParser) parserClass.newInstance(); + } + } catch (InstantiationException e) { + exceptions.add(e); + } catch (IllegalAccessException e) { + exceptions.add(e); + } catch (ClassCastException e) { + exceptions.add(e); + } + } + } + throw newInstantiationException("Invalid parser class list", exceptions); + } + private XmlSerializer getSerializerInstance() throws XmlPullParserException { + ArrayList exceptions = null; + if (serializerClasses != null && !serializerClasses.isEmpty()) { + exceptions = new ArrayList(); + for (Object o : serializerClasses) { + try { + if (o != null) { + Class serializerClass = (Class) o; + return (XmlSerializer) serializerClass.newInstance(); + } + } catch (InstantiationException e) { + exceptions.add(e); + } catch (IllegalAccessException e) { + exceptions.add(e); + } catch (ClassCastException e) { + exceptions.add(e); + } + } + } + throw newInstantiationException("Invalid serializer class list", exceptions); + } + private static XmlPullParserException newInstantiationException(String message, + ArrayList exceptions) { + if (exceptions == null || exceptions.isEmpty()) { + return new XmlPullParserException(message); + } else { + XmlPullParserException exception = new XmlPullParserException(message); + for (Exception ex : exceptions) { + exception.addSuppressed(ex); + } + return exception; + } + } + + public XmlSerializer newSerializer() throws XmlPullParserException { + return getSerializerInstance(); + } + public static XmlPullParserFactory newInstance () throws XmlPullParserException { + return new XmlPullParserFactory(); + } + public static XmlPullParserFactory newInstance (String unused, Class unused2) + throws XmlPullParserException { + return newInstance(); + } +} diff --git a/src/ARSCLib/org/xmlpull/v1/XmlSerializer.java b/src/ARSCLib/org/xmlpull/v1/XmlSerializer.java new file mode 100644 index 00000000..2cfeec49 --- /dev/null +++ b/src/ARSCLib/org/xmlpull/v1/XmlSerializer.java @@ -0,0 +1,52 @@ +package org.xmlpull.v1; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +public interface XmlSerializer { + void setFeature(String name, boolean state) + throws IllegalArgumentException, IllegalStateException; + boolean getFeature(String name); + void setProperty(String name, Object value) + throws IllegalArgumentException, IllegalStateException; + Object getProperty(String name); + void setOutput(OutputStream os, String encoding) + throws IOException, IllegalArgumentException, IllegalStateException; + void setOutput(Writer writer) + throws IOException, IllegalArgumentException, IllegalStateException; + void startDocument(String encoding, Boolean standalone) + throws IOException, IllegalArgumentException, IllegalStateException; + void endDocument() + throws IOException, IllegalArgumentException, IllegalStateException; + void setPrefix(String prefix, String namespace) + throws IOException, IllegalArgumentException, IllegalStateException; + String getPrefix(String namespace, boolean generatePrefix) + throws IllegalArgumentException; + int getDepth(); + String getNamespace(); + String getName(); + XmlSerializer startTag(String namespace, String name) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer attribute(String namespace, String name, String value) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer endTag(String namespace, String name) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer text(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer text(char [] buf, int start, int len) + throws IOException, IllegalArgumentException, IllegalStateException; + void cdsect(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void entityRef(String text) throws IOException, + IllegalArgumentException, IllegalStateException; + void processingInstruction(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void comment(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void docdecl(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void ignorableWhitespace(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void flush() throws IOException; +} diff --git a/src/api-impl/android/content/res/AssetManager.java b/src/api-impl/android/content/res/AssetManager.java index 263ba629..6a21dfaf 100644 --- a/src/api-impl/android/content/res/AssetManager.java +++ b/src/api-impl/android/content/res/AssetManager.java @@ -16,7 +16,8 @@ package android.content.res; -import com.hq.arscresourcesparser.ArscResourcesParser; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.value.ResValueMap; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; @@ -27,6 +28,7 @@ import android.os.ParcelFileDescriptor; import android.os.Trace; import android.util.Log; import android.util.TypedValue; +import java.util.ArrayList; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; @@ -85,7 +87,7 @@ public final class AssetManager { private boolean mOpen = true; private HashMap mRefStacks; - private ArscResourcesParser arsc_parser; + private TableBlock tableBlock; /** * Create a new AssetManager containing only the basic system assets. @@ -95,8 +97,13 @@ public final class AssetManager { * {@hide} */ public AssetManager() { - // NOTE: this enforces a particular order when specifying the MicroG .apk in classpath - arsc_parser = new ArscResourcesParser(ClassLoader.getSystemClassLoader().getResource("resources.arsc")); + try { + // NOTE: this enforces a particular order when specifying the MicroG .apk in classpath + InputStream inFile = ClassLoader.getSystemClassLoader().getResourceAsStream("resources.arsc"); + tableBlock = TableBlock.load(inFile); + } catch (IOException e) { + Log.e(TAG, "failed to load resources.arsc" + e); + } // FIXME: evaluate if this can be axed synchronized (this) { @@ -160,7 +167,7 @@ public final class AssetManager { * identifier for the current configuration / skin. */ /*package*/ final CharSequence getResourceText(int id) { - return arsc_parser.getResource(id); + return tableBlock.search(id).pickOne().getResValue().getDataAsPoolString().get(); } /** @@ -187,7 +194,11 @@ public final class AssetManager { * @param id Resource id of the string array */ /*package*/ final String[] getResourceStringArray(final int id) { - return arsc_parser.getResourceArray(id); + ArrayList values = new ArrayList(); + for(ResValueMap map : tableBlock.search(id).pickOne().getResValueMapArray().getChildes()) { + values.add(map.getValueAsString()); + } + return values.toArray(new String[0]); } @@ -687,34 +698,7 @@ public final class AssetManager { /*package*/ /*native*/ final int getResourceIdentifier(String name, String type, String defPackage) { System.out.println("getResourceIdentifier("+name+","+type+","+defPackage+") called"); - int typeId; - - if(type.equals("color")) { - typeId = 1; - }else if(type.equals("drawable")) { - typeId = 2; - }else if(type.equals("layout")) { - typeId = 3; - }else if(type.equals("dimen")) { - typeId = 4; - }else if(type.equals("string")) { - typeId = 5; - }else if(type.equals("array")) { - typeId = 6; - }else if(type.equals("style")) { - typeId = 7; - }else if(type.equals("menu")) { - typeId = 8; - }else if(type.equals("id")) { - typeId = 9; - } else { - System.out.println("returning 0 (no such type: >"+type+"<)"); - return 0; - } - - System.out.println("returning: " + arsc_parser.getResourceId(name, typeId)); - System.out.println("debug: " + arsc_parser.getResource(0x7f020002)); - return arsc_parser.getResourceId(name, typeId); + return tableBlock.pickOne().getEntry("", type, name).getResourceId(); } /*package*/ native final String getResourceName(int resid); diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index f6896677..43b7effe 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -246,7 +246,7 @@ hax_jar = jar('hax', [ 'javax/microedition/khronos/opengles/GL.java', ], dependencies: [ - declare_dependency(link_with: hax_arsc_parser_jar) + declare_dependency(link_with: hax_arsc_lib_jar) ], java_args: [ '-bootclasspath', bootclasspath, diff --git a/src/arsc_parser/README.md b/src/arsc_parser/README.md deleted file mode 100644 index 41ff1754..00000000 --- a/src/arsc_parser/README.md +++ /dev/null @@ -1,7 +0,0 @@ -taken from https://github.com/ibilux/ArscResourcesParser -TODO: dig up what license this uses - -# ArscResourcesParser -A tool for decoding Android resources.arsc file using java, for decoding .arsc resources file and getting information from apk file. - -Thanks to @dutlxq2014 for his work on ApkParser. diff --git a/src/arsc_parser/com/hq/arscresourcesparser/ArscResourcesParser.java b/src/arsc_parser/com/hq/arscresourcesparser/ArscResourcesParser.java deleted file mode 100644 index 70e190b1..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/ArscResourcesParser.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.hq.arscresourcesparser; - -import com.hq.arscresourcesparser.arsc.ArscFile; -import com.hq.arscresourcesparser.arsc.ResTableEntry; -import com.hq.arscresourcesparser.arsc.ResTableValueEntry; -import com.hq.arscresourcesparser.arsc.ResTableMapEntry; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ByteArrayOutputStream; -import java.net.URL; - -//import java.util.zip.ZipEntry; -//import java.util.zip.ZipFile; - -public class ArscResourcesParser { - public final int TYPE_COLOR = 1; - public final int TYPE_DRAWABLE = 2; - public final int TYPE_LAYOUT = 3; - public final int TYPE_DIMEN = 4; - public final int TYPE_STRING = 5; - public final int TYPE_ARRAY = 6; - public final int TYPE_STYLE = 7; - public final int TYPE_MENU = 8; - public final int TYPE_ID = 9; - - private ArscFile arscFile; - - public ArscResourcesParser(URL file) { - try { - InputStream amis = file.openStream(); - System.out.println(amis); - - byte[] buffer = new byte[1000]; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try { - int temp; - - while ((temp = amis.read(buffer)) != -1) { - byteArrayOutputStream.write(buffer, 0, temp); - } - } catch (IOException e) { - // Display the exception/s on the console - System.out.println(e); - } - - byte[] byteArray = byteArrayOutputStream.toByteArray(); - - arscFile = new ArscFile(); - arscFile.parse(byteArray); - amis.close(); - } catch (IOException e) { - System.out.println("ArscResourcesParser: IOException raised: " + e.toString()); - } - } - - public String getResource(int resId) { - ResTableValueEntry res = (ResTableValueEntry)arscFile.getResource(resId); - - if(res == null) { - return "oops, arscFile.getResource returned null"; - } - - return res.resValue.toString(); - } - - public String getResourceByName(String name, int typeId) { - ResTableValueEntry res = (ResTableValueEntry)arscFile.getResourceByName(name, typeId); - - return res.resValue.toString(); - } - - public String[] getResourceArray(int resId) { - ResTableMapEntry resArray = (ResTableMapEntry)arscFile.getResource(resId); - - return resArray.asStringArray(); - } - - public int getResourceId(String name, int typeId) { - System.out.println("¯¯ in getResourceId"); - ResTableValueEntry res = (ResTableValueEntry)arscFile.getResourceByName(name, typeId); - - if(res != null) - return res.entryId; - else - return 0; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ArscFile.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ArscFile.java deleted file mode 100644 index cc392e07..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ArscFile.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.ByteArrayInputStream; - -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ArscFile { - - private static final String TAG = ArscFile.class.getSimpleName(); - - private static final int RES_TABLE_TYPE = 0x0002; - private static final int RES_STRING_POOL_TYPE = 0x0001; - private static final int RES_TABLE_PACKAGE_TYPE = 0x0200; - - private ByteArrayInputStream mStreamer; - public ResFileHeaderChunk arscHeader; - public ResStringPoolChunk resStringPoolChunk; - public ResTablePackageChunk resTablePackageChunk; - - public ArscFile() { - - } - - public void parse(byte[] sBuf) throws IOException { - mStreamer = new ByteArrayInputStream(sBuf); - - byte[] headBytes; - byte[] chunkBytes; - long cursor = 0; - ChunkHeader header; - - // Preload file header. The chunkSize represents the complete file length. - chunkBytes = new byte[ResFileHeaderChunk.LENGTH]; - mStreamer.read(chunkBytes, 0, chunkBytes.length); - header = ChunkHeader.parseFrom(new PositionInputStream(new ByteArrayInputStream(chunkBytes))); - if (header.type != RES_TABLE_TYPE) { - return; - } - // Post load file header. - mStreamer.reset(); - chunkBytes = new byte[header.headerSize]; - cursor += mStreamer.read(chunkBytes, 0, chunkBytes.length); - arscHeader = ResFileHeaderChunk.parseFrom(new PositionInputStream(new ByteArrayInputStream(chunkBytes))); - - do { - headBytes = new byte[ChunkHeader.LENGTH]; - cursor += mStreamer.read(headBytes, 0, headBytes.length); - header = ChunkHeader.parseFrom(new PositionInputStream(new ByteArrayInputStream(headBytes))); - - // Chunk size = ChunkInfo + BodySize - chunkBytes = new byte[(int) header.chunkSize]; - System.arraycopy(headBytes, 0, chunkBytes, 0, ChunkHeader.LENGTH); - cursor += mStreamer.read(chunkBytes, ChunkHeader.LENGTH, (int) header.chunkSize - ChunkHeader.LENGTH); - //LogUtil.i(TAG, header.toRowString().replace("\n", ""), "cursor=0x" + PrintUtil.hex4(cursor)); - - switch (header.type) { - case RES_STRING_POOL_TYPE: - resStringPoolChunk = ResStringPoolChunk.parseFrom(new PositionInputStream(new ByteArrayInputStream(chunkBytes))); - break; - case RES_TABLE_PACKAGE_TYPE: - resTablePackageChunk = ResTablePackageChunk.parseFrom(new PositionInputStream(new ByteArrayInputStream(chunkBytes)), resStringPoolChunk); - break; - default: - //LogUtil.e("Unknown type: 0x" + PrintUtil.hex2(header.type)); - } - - } while (cursor < sBuf.length); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(4096); - builder.append(arscHeader).append('\n'); - builder.append(resStringPoolChunk).append('\n'); - builder.append(resTablePackageChunk).append('\n'); - return builder.toString(); - } - - public String buildPublicXml() { - return resTablePackageChunk.buildEntry2String(); - } - - public ResTableEntry getResource(int resId) { - long pkgId = (resId & 0xff000000L) >> 24; - //short packageId = (short) (resId >> 24 & 0xff); - if (resTablePackageChunk.pkgId == pkgId) { - return resTablePackageChunk.getResource(resId); - } else { - return null; - } - } - - public ResTableEntry getResourceByName(String name, int typeId) { - return resTablePackageChunk.getResourceByName(name, typeId); // don't check the pkgId, this thing only supports one anyway - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/BaseTypeChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/BaseTypeChunk.java deleted file mode 100644 index 049d974f..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/BaseTypeChunk.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public abstract class BaseTypeChunk { - - public abstract String getChunkName(); - - public abstract long getEntryCount(); - - public abstract String getType(); - - public abstract int getTypeId(); - - public abstract void translateValues(ResStringPoolChunk globalStringPool, - ResStringPoolChunk typeStringPool, - ResStringPoolChunk keyStringPool); -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ChunkHeader.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ChunkHeader.java deleted file mode 100644 index 80adace4..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ChunkHeader.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ChunkHeader { - - public static final int LENGTH = 2 + 2 + 4; - public int type; - public int headerSize; - public long chunkSize; - - public static ChunkHeader parseFrom(PositionInputStream mStreamer) throws IOException { - ChunkHeader chunk = new ChunkHeader(); - chunk.type = Utils.readShort(mStreamer); - chunk.headerSize = Utils.readShort(mStreamer); - chunk.chunkSize = Utils.readInt(mStreamer); - return chunk; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResFileHeaderChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResFileHeaderChunk.java deleted file mode 100644 index 7ae41cf1..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResFileHeaderChunk.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResFileHeaderChunk { - - public static final int LENGTH = 12; - - public ChunkHeader header; - public long packageCount; - - public static ResFileHeaderChunk parseFrom(PositionInputStream mStreamer) throws IOException { - ResFileHeaderChunk chunk = new ResFileHeaderChunk(); - chunk.header = ChunkHeader.parseFrom(mStreamer); - chunk.packageCount = Utils.readInt(mStreamer); - return chunk; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolChunk.java deleted file mode 100644 index 65790b67..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolChunk.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -import java.util.ArrayList; -import java.util.List; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ResStringPoolChunk { - - // If set, the string index is sorted by the string values (based on strcmp16()). - private static final int SORTED_FLAG = 1; - // String pool is encoded in UTF-8 - private static final int UTF8_FLAG = 1 << 8; - - // Header Block 0x001c - public ChunkHeader header; - // Number of style span arrays in the pool (number of uint32_t indices - // follow the string indices). - public long stringCount; - // Number of style span arrays in the pool (number of uint32_t indices - // follow the string indices). - public long styleCount; - public long flags; - // Index from header of the string data (Offset from this chunk starting). - public long stringsStart; - // Index from header of the style data. - public long stylesStart; - // Data Block - public long stringOffsetArray[]; // Offset from string pool. The first one is 0x00000000 - public long styleOffsetArray[]; - public List strings; - public List styles; - - public static ResStringPoolChunk parseFrom(PositionInputStream mStreamer) throws IOException { - long baseCursor = mStreamer.getPosition(); - - ResStringPoolChunk chunk = new ResStringPoolChunk(); - chunk.header = ChunkHeader.parseFrom(mStreamer); - chunk.stringCount = Utils.readInt(mStreamer); - chunk.styleCount = Utils.readInt(mStreamer); - chunk.flags = Utils.readInt(mStreamer); // read flag - chunk.stringsStart = Utils.readInt(mStreamer); - chunk.stylesStart = Utils.readInt(mStreamer); - - // the string index is sorted by the string values if true - boolean sorted = (chunk.flags & SORTED_FLAG) != 0; - // string use utf-8 format if true, otherwise utf-16 - boolean utf8 = (chunk.flags & UTF8_FLAG) != 0; - - long[] strOffsets = chunk.stringOffsetArray = new long[(int) chunk.stringCount]; - long[] styleOffsets = chunk.styleOffsetArray = new long[(int) chunk.styleCount]; - List strings = chunk.strings = new ArrayList<>((int) chunk.stringCount); - List styles = chunk.styles = new ArrayList<>((int) chunk.styleCount); - - // read strings offset - for (int i = 0; i < chunk.stringCount; ++i) { - strOffsets[i] = Utils.readInt(mStreamer); - } - for (int i = 0; i < chunk.styleCount; ++i) { - styleOffsets[i] = Utils.readInt(mStreamer); - } - for (int i = 0; i < chunk.stringCount; ++i) { - long start = baseCursor + chunk.stringsStart + strOffsets[i]; - mStreamer.seek(start); - //int len = (Utils.readShort(mStreamer) & 0x7f00) >> 8; - //int len = Utils.readShort(mStreamer); - /* - * Each String entry contains Length header (2 bytes to 4 bytes) + Actual String + [0x00] - * Length header sometime contain duplicate values e.g. 20 20 - * Actual string sometime contains 00, which need to be ignored - * Ending zero might be 2 byte or 4 byte - * - * TODO: Consider both Length bytes and String length > 32767 characters - */ - /* - int len =0; - byte[] buf2 = new byte[2]; - mStreamer.read(buf2); - if (buf2[0] == buf2[1]) // Its repeating, happens for Non-Manifest file. e.g. 20 20 - len = buf2[0]; - else - len = Utils.getShort(buf2); - */ - if (utf8) { - // The lengths are encoded in the same way as for the 16-bit format - // but using 8-bit rather than 16-bit integers. - int strlen = Utils.readUInt8(mStreamer); - int len = Utils.readUInt8(mStreamer); - String str = Utils.readString(mStreamer, len); - strings.add(str); - } else { - // The length is encoded as either one or two 16-bit integers as per the commentRef... - //int len = (Utils.readShort(mStreamer) & 0x7f00) >> 8; - int len = Utils.readShort(mStreamer); - String str = Utils.readString16(mStreamer, len * 2); - strings.add(str); - } - //String str = Utils.readString16(mStreamer, len); // The last byte is 0x00 - //String str = s.readNullEndString(len); // The last byte is 0x00 - } - for (int i = 0; i < chunk.styleCount; ++i) { - long start = baseCursor + chunk.stylesStart + styleOffsets[i]; - mStreamer.seek(start); - int len = (Utils.readShort(mStreamer) & 0x7f00) >> 8; - //String str = Utils.readString32(mStreamer, len); // The last byte is 0x00 - String str = Utils.readString(mStreamer, len); - styles.add(str); - } - - return chunk; - } - - public String getString(int idx) { - try{ - return strings != null && idx < strings.size() ? strings.get(idx) : null; - } catch (Exception e) { - return null; - } - } - - public String getStyle(int idx) { - return styles != null && idx < styles.size() ? styles.get(idx) : null; - } - -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolRef.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolRef.java deleted file mode 100644 index d7793cf6..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResStringPoolRef.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResStringPoolRef { - - public long index; - - public static ResStringPoolRef parseFrom(PositionInputStream mStreamer) throws IOException { - ResStringPoolRef ref = new ResStringPoolRef(); - ref.index = Utils.readInt(mStreamer); - return ref; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableConfig.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableConfig.java deleted file mode 100644 index 6d0181f1..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableConfig.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ResTableConfig { - - public long size; // 4 size of config object - - // union 4bytes - public int mcc, mnc; // 2 + 2 - public long imsi; // 4 - - // union 4bytes - public int language, country; // 2 + 2 - public long locale; // 4 - - // union 4bytes - public int orientation, touchScreen, density; // 1 + 1 + 2 - public long screenType; // 4 - - // union 4bytes - public int keyboard, navigation, inputFlags, inputPad0; // 1 + 1 + 1 + 1 - public long input; // 4 - - // union 4bytes - public int screenWidth, screenHeight; // 2 + 2 - public long screenSize; // 4 - - // union 4bytes - public int sdkVersion, minorVersion; // 2 + 2 - public long version; // 4 - - // union 4bytes - public int screenLayout, uiModeByte, smallestScreenWidthDp; // 1 + 1 + 2 - public long screenConfig; // 4 - - // union 4bytes - public int screenWidthDp, screenHeightDp; // 2 + 2 - public long screenSizeDp; // 4 - - public byte[] localeScript; // 4 - public byte[] localeVariant; // 8 - - public static ResTableConfig parseFrom(PositionInputStream mStreamer) throws IOException { - ResTableConfig config = new ResTableConfig(); - long cursor = mStreamer.getPosition(); - long start = cursor; - - config.size = Utils.readInt(mStreamer); - cursor += 4; - - config.mcc = Utils.readShort(mStreamer); - config.mnc = Utils.readShort(mStreamer); - mStreamer.seek(cursor); // Reset cursor to get union value. - config.imsi = Utils.readInt(mStreamer); - cursor += 4; - - config.language = Utils.readShort(mStreamer); - config.country = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.locale = Utils.readInt(mStreamer); - cursor += 4; - - config.orientation = Utils.readUInt8(mStreamer); - config.touchScreen = Utils.readUInt8(mStreamer); - config.density = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.screenType = Utils.readInt(mStreamer); - cursor += 4; - - config.keyboard = Utils.readUInt8(mStreamer); - config.navigation = Utils.readUInt8(mStreamer); - config.inputFlags = Utils.readUInt8(mStreamer); - config.inputPad0 = Utils.readUInt8(mStreamer); - mStreamer.seek(cursor); - config.input = Utils.readInt(mStreamer); - cursor += 4; - - config.screenWidth = Utils.readShort(mStreamer); - config.screenHeight = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.screenSize = Utils.readInt(mStreamer); - cursor += 4; - - config.sdkVersion = Utils.readShort(mStreamer); - config.minorVersion = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.version = Utils.readInt(mStreamer); - cursor += 4; - - config.screenLayout = Utils.readUInt8(mStreamer); - config.uiModeByte = Utils.readUInt8(mStreamer); - config.smallestScreenWidthDp = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.screenConfig = Utils.readInt(mStreamer); - cursor += 4; - - config.screenWidthDp = Utils.readShort(mStreamer); - config.screenHeightDp = Utils.readShort(mStreamer); - mStreamer.seek(cursor); - config.screenSizeDp = Utils.readInt(mStreamer); - cursor += 4; - { - byte[] buf; - buf = new byte[4]; - mStreamer.read(buf); - config.localeScript = buf; - buf = new byte[8]; - mStreamer.read(buf); - config.localeVariant = buf; - } - - mStreamer.seek(start + config.size); - - return config; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableEntry.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableEntry.java deleted file mode 100644 index 6f0861ee..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableEntry.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResTableEntry { - - // If set, this is a complex entry, holding a set of name/value. It is followed by an array of ResTableMap structures. - public static final int FLAG_COMPLEX = 0x0001; - // If set, this resource has been declared public, so libraries are allowed to reference it. - public static final int FLAG_PUBLIC = 0x0002; - - public int size; // short - public int flags; // short - public ResStringPoolRef key; // Reference into ResTablePackage::keyStrings identifying this entry. - - public int entryId; // 16bit 0x7f01nnnn - public String keyStr; - - public static ResTableEntry parseFrom(PositionInputStream mStreamer) throws IOException { - ResTableEntry entry = new ResTableEntry(); - parseFrom(mStreamer, entry); - return entry; - } - - public static void parseFrom(PositionInputStream mStreamer, ResTableEntry entry) throws IOException { - entry.size = Utils.readShort(mStreamer); - entry.flags = Utils.readShort(mStreamer); - entry.key = ResStringPoolRef.parseFrom(mStreamer); - } - - @Override - public String toString() { - return " "; - } - - public void translateValues(ResStringPoolChunk globalStringPool, - ResStringPoolChunk typeStringPool, - ResStringPoolChunk keyStringPool) { - keyStr = keyStringPool.getString((int) key.index); - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMap.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMap.java deleted file mode 100644 index 566ac14a..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMap.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResTableMap { - - // Bag resource ID - public ResTableRef name; - - // Bag resource item value - public ResValue value; - - public static ResTableMap parseFrom(PositionInputStream mStreamer) throws IOException { - ResTableMap tableMap = new ResTableMap(); - tableMap.name = ResTableRef.parseFrom(mStreamer); - tableMap.value = ResValue.parseFrom(mStreamer); - return tableMap; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(String.format("%-10s {%s}\n", "name", name.toString())); - builder.append("value:\n" + value.toString()); - return builder.toString(); - } - - public void translateValues(ResStringPoolChunk globalStringPool, - ResStringPoolChunk typeStringPool, - ResStringPoolChunk keyStringPool) { - value.translateValues(globalStringPool, typeStringPool, keyStringPool); - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMapEntry.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMapEntry.java deleted file mode 100644 index c6083971..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableMapEntry.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -import java.util.ArrayList; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ResTableMapEntry extends ResTableEntry { - - public ResTableRef parent; // Reference parent ResTableMapEntry pkgId, if parent not exists the value should be zero. - public long count; // Num of ResTableMap following. - public ResTableMap[] resTableMaps; - - public static ResTableMapEntry parseFrom(PositionInputStream mStreamer) throws IOException { - ResTableMapEntry entry = new ResTableMapEntry(); - ResTableEntry.parseFrom(mStreamer, entry); - - entry.parent = ResTableRef.parseFrom(mStreamer); - entry.count = Utils.readInt(mStreamer); - - if(entry.count < 1) { - entry.resTableMaps = new ResTableMap[0]; - return entry; - } - - entry.resTableMaps = new ResTableMap[(int) entry.count]; - for (int i = 0; i < entry.count; ++i) { - entry.resTableMaps[i] = ResTableMap.parseFrom(mStreamer); - } - - return entry; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("|----- ResTableMapEntry printout\n"); - for(ResTableMap map : resTableMaps) { - builder.append("|- map:\n"); - builder.append(map.toString()+"\n"); - builder.append("|- end\n"); - } - builder.append("|----- end of printout\n"); - return builder.toString(); - } - - public String[] asStringArray() { - ArrayList values = new ArrayList(); - for(ResTableMap map : resTableMaps) { - values.add(map.value.toString()); - } - return values.toArray(new String[0]); - } - - @Override - public void translateValues(ResStringPoolChunk globalStringPool, - ResStringPoolChunk typeStringPool, - ResStringPoolChunk keyStringPool) { - super.translateValues(globalStringPool, typeStringPool, keyStringPool); - for (int i = 0; i < resTableMaps.length; ++i) { - resTableMaps[i].translateValues(globalStringPool, typeStringPool, keyStringPool); - } - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTablePackageChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTablePackageChunk.java deleted file mode 100644 index 8bc939cf..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTablePackageChunk.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ResTablePackageChunk { - - public static final String TAG = ResTablePackageChunk.class.getSimpleName(); - - public static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; - public static final int RES_TABLE_TYPE_TYPE = 0x0201; - - // Header Block 0x0120 - public ChunkHeader header; - public long pkgId; // 0x0000007f->UserResources 0x00000001->SystemResources - public String packageName; - public long typeStringOffset; // Offset in this chunk - public long lastPublicType; // Num of type string - public long keyStringOffset; // Offset in chunk - public long lastPublicKey; // Num of key string - - // DataBlock - public ResStringPoolChunk typeStringPool; - public ResStringPoolChunk keyStringPool; - public List typeChunks; - - // Create Index - public Map> typeInfoIndexer; - - public static ResTablePackageChunk parseFrom(PositionInputStream mStreamer, ResStringPoolChunk stringChunk) throws IOException { - ResTablePackageChunk chunk = new ResTablePackageChunk(); - chunk.header = ChunkHeader.parseFrom(mStreamer); - chunk.pkgId = Utils.readInt(mStreamer); - chunk.packageName = Utils.readString16(mStreamer, 128 * 2); - chunk.typeStringOffset = Utils.readInt(mStreamer); - chunk.lastPublicType = Utils.readInt(mStreamer); - chunk.keyStringOffset = Utils.readInt(mStreamer); - chunk.lastPublicKey = Utils.readInt(mStreamer); - - // Data Block - mStreamer.seek(chunk.typeStringOffset); - chunk.typeStringPool = ResStringPoolChunk.parseFrom(mStreamer); - mStreamer.seek(chunk.keyStringOffset); - chunk.keyStringPool = ResStringPoolChunk.parseFrom(mStreamer); - - // TableTypeSpecType TableTypeType - mStreamer.seek(chunk.keyStringOffset + chunk.keyStringPool.header.chunkSize); - chunk.typeChunks = new ArrayList<>(); - int resCount = 0; - StringBuilder logInfo = new StringBuilder(); - while (mStreamer.available() > 0) { - int x = mStreamer.available(); - logInfo.setLength(0); - resCount++; - ChunkHeader header = ChunkHeader.parseFrom(mStreamer); - - BaseTypeChunk typeChunk = null; - if (header.type == RES_TABLE_TYPE_SPEC_TYPE) { - mStreamer.seek(mStreamer.getPosition() - ChunkHeader.LENGTH); - typeChunk = ResTableTypeSpecChunk.parseFrom(mStreamer, stringChunk); - } else if (header.type == RES_TABLE_TYPE_TYPE) { - mStreamer.seek(mStreamer.getPosition() - ChunkHeader.LENGTH); - typeChunk = ResTableTypeInfoChunk.parseFrom(mStreamer, stringChunk); - } - if (typeChunk != null) { - logInfo.append(typeChunk.getChunkName()).append(" ") - .append(String.format("type=%s ", typeChunk.getType())) - .append(String.format("count=%s ", typeChunk.getEntryCount())); - } else { - logInfo.append("None TableTypeSpecType or TableTypeType!!"); - } - - if (typeChunk != null) { - chunk.typeChunks.add(typeChunk); - } - } - - chunk.createResourceIndex(); - for (int i = 0; i < chunk.typeChunks.size(); ++i) { - chunk.typeChunks.get(i).translateValues(stringChunk, chunk.typeStringPool, chunk.keyStringPool); - } - - return chunk; - } - - private void createResourceIndex() { - typeInfoIndexer = new HashMap>(); - for (BaseTypeChunk typeChunk : typeChunks) { - // The first chunk in typeList should be ResTableTypeSpecChunk - List typeList = typeInfoIndexer.get(typeChunk.getTypeId()); - if (typeList == null) { - typeList = new ArrayList(); - typeInfoIndexer.put(typeChunk.getTypeId(), typeList); - if (typeChunk.getTypeId() == 2) { - int x = 4; - } - } - typeList.add(typeChunk); - } - } - - public ResTableEntry getResource(int resId) { - int typeId = (resId & 0x00ff0000) >> 16; - //short typeIdx = (short) ((resId >> 16) & 0xff); - List typeList = typeInfoIndexer.get(typeId); // The first chunk in typeList should be ResTableTypeSpecChunk - for (int i = 1; i < typeList.size(); ++i) { - if (typeList.get(i) instanceof ResTableTypeInfoChunk) { - ResTableTypeInfoChunk x = (ResTableTypeInfoChunk) typeList.get(i); - ResTableEntry entry = ((ResTableTypeInfoChunk) typeList.get(i)).getResource(resId); - if (entry != null) { - return entry; - } - } - } - return null; - } - - public ResTableEntry getResourceByName(String name, int typeId) { - System.out.println("¯¯ in ResTablePackageChunk - getResourceByName"); - - List typeList = typeInfoIndexer.get(typeId); // The first chunk in typeList should be ResTableTypeSpecChunk - if(typeList == null) { - System.out.println("¯¯ typeList is null, this is sus..."); - return null; - } - for (int i = 1; i < typeList.size(); ++i) { - if (typeList.get(i) instanceof ResTableTypeInfoChunk) { - ResTableTypeInfoChunk x = (ResTableTypeInfoChunk) typeList.get(i); - ResTableEntry entry = ((ResTableTypeInfoChunk) typeList.get(i)).getResourceByName(name); - if (entry != null) { - return entry; - } - } - } - return null; - } - - public String buildEntry2String() { - StringBuilder builder = new StringBuilder(); - builder.append("" + System.lineSeparator()); - builder.append("" + System.lineSeparator()); - - for (int i = 0; i < typeChunks.size(); ++i) { - // All entries exist in ResTableTypeInfoChunk - if (typeChunks.get(i) instanceof ResTableTypeSpecChunk) { - // Extract following ResTableTypeInfoChunks - List typeInfos = new ArrayList(); - for (int j = i + 1; j < typeChunks.size(); ++j) { - if (typeChunks.get(j) instanceof ResTableTypeInfoChunk) { - typeInfos.add((ResTableTypeInfoChunk) typeChunks.get(j)); - } else { - break; - } - } - i += typeInfos.size(); - // Unique ResTableTypeInfoChunks - String entry = ResTableTypeInfoChunk.uniqueEntries2String((int) pkgId & 0xff, typeStringPool, keyStringPool, typeInfos); - builder.append("\t" + entry + System.lineSeparator()); - } - } - - builder.append(""); - return builder.toString(); - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableRef.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableRef.java deleted file mode 100644 index b65c141b..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableRef.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResTableRef { - - public long ident; - - public static ResTableRef parseFrom(PositionInputStream mStreamer) throws IOException { - ResTableRef ref = new ResTableRef(); - ref.ident = Utils.readInt(mStreamer); - return ref; - } - - @Override - public String toString() { - return String.format("%s: 0x%s", "ident", (ident)); - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeInfoChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeInfoChunk.java deleted file mode 100644 index 43564596..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeInfoChunk.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -import java.util.List; - -import java.lang.String; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ -public class ResTableTypeInfoChunk extends BaseTypeChunk { - - public static final long NO_ENTRY = 0xffffffffL; - - public ChunkHeader header; - public int typeId; // 1byte resource type 0x00ff0000 - public int res0; // 1byte - public int res1; // 2byte - public long entryCount; - public long entriesStart; // start of table entries. - public ResTableConfig resConfig; - - // Data Block - public long[] entryOffsets; // offset of table entries. - public ResTableEntry[] tableEntries; - - public static ResTableTypeInfoChunk parseFrom(PositionInputStream mStreamer, ResStringPoolChunk stringChunk) throws IOException { - ResTableTypeInfoChunk chunk = new ResTableTypeInfoChunk(); - long start = mStreamer.getPosition(); - chunk.header = ChunkHeader.parseFrom(mStreamer); - chunk.typeId = Utils.readUInt8(mStreamer); - chunk.res0 = Utils.readUInt8(mStreamer); - chunk.res1 = Utils.readShort(mStreamer); - chunk.entryCount = Utils.readInt(mStreamer); - chunk.entriesStart = Utils.readInt(mStreamer); - chunk.resConfig = ResTableConfig.parseFrom(mStreamer); - - System.out.println("### ResTableTypeInfoChunk with typeId: " + chunk.typeId + ", res0: " + chunk.res0 + ", res1:" + chunk.res1 + ", resConfig.density: " + chunk.resConfig.density); - - // read offsets table - chunk.entryOffsets = new long[(int) chunk.entryCount]; - for (int i = 0; i < chunk.entryCount; ++i) { - chunk.entryOffsets[i] = Utils.readInt(mStreamer); - } - // read entry - chunk.tableEntries = new ResTableEntry[(int) chunk.entryCount]; - mStreamer.seek(start + chunk.entriesStart); // Locate entry start point. - for (int i = 0; i < chunk.entryCount; ++i) { - System.out.println("### entry num: " + i); - // This is important! - if (chunk.entryOffsets[i] == NO_ENTRY || chunk.entryOffsets[i] == -1) { - System.out.println("### - empty: >"+chunk.entryOffsets[i]+"<"); - continue; - } - System.out.println("### - not empty: >"+chunk.entryOffsets[i]+"<"); - - long cursor = mStreamer.getPosition(); // Remember the start cursor - ResTableEntry entry = ResTableEntry.parseFrom(mStreamer); - - mStreamer.seek(cursor); // Rest cursor - // We need to parse entry into ResTableMapEntry instead of ResTableMapEntry - if (entry.flags == ResTableEntry.FLAG_COMPLEX) { - entry = ResTableMapEntry.parseFrom(mStreamer); // Complex ResTableMapEntry - } else { - entry = ResTableValueEntry.parseFrom(mStreamer); // ResTableEntry follows a ResValue - } - int x1 = entry.entryId; // Remember entry index in tableEntries to recover ids in public.xml - int x2 = i; - entry.entryId = i; - chunk.tableEntries[i] = entry; - } - - return chunk; - } - - @Override - public String getChunkName() { - return "ResTableTypeInfoChunk"; - } - - @Override - public long getEntryCount() { - return entryCount; - } - - @Override - public String getType() { - return String.format("0x%s", (typeId)); - } - - public int getTypeId() { - return typeId; - } - - @Override - public void translateValues(ResStringPoolChunk globalStringPool, ResStringPoolChunk typeStringPool, ResStringPoolChunk keyStringPool) { - for (ResTableEntry entry : tableEntries) { - if (entry != null) { - entry.translateValues(globalStringPool, typeStringPool, keyStringPool); - } - } - } - - public ResTableEntry getResource(int resId) { - int entryId = resId & 0x0000ffff; - for (ResTableEntry entry : tableEntries) { - if(entry == null) { - continue; - } - if (entry.entryId == entryId) { - return entry; - } - } - return null; - } - - public ResTableEntry getResourceByName(String name) { - System.out.println("¯¯ in ResTableInfoChunk - getResourceByName"); - for (ResTableEntry entry : tableEntries) { - System.out.println("¯¯ for loop start, entry: " + entry); - if(entry == null) { - System.out.println("¯¯ > entry is null, continuing"); - continue; - } - System.out.println("¯¯ entry id: " + entry.entryId); - System.out.println("¯¯ comparing ("+entry.keyStr+") vs ("+name+")"); - if (name.equals(entry.keyStr)) { - System.out.println("¯¯ got a match"); - return entry; - } - System.out.println("¯¯ will keep trying"); - } - return null; - } - - public static String uniqueEntries2String(int packageId, - ResStringPoolChunk typeStringPool, - ResStringPoolChunk keyStringPool, - List typeInfos) { - StringBuilder builder = new StringBuilder(); - - int configCount = typeInfos.size(); - int entryCount; - try { - entryCount = (int) typeInfos.get(0).entryCount; - } catch (Exception e) { - entryCount = 0; - } - - for (int i = 0; i < entryCount; ++i) { - for (int j = 0; j < configCount; ++j) { - String entryStr = typeInfos.get(j).buildEntry2String(i, packageId, typeStringPool, keyStringPool); - if (entryStr != null && entryStr.length() > 0) { - builder.append(entryStr); - break; // This entryId has done. - } - } - } - return builder.toString(); - } - - public String buildEntry2String(int entryId, int packageId, ResStringPoolChunk typeStringPool, ResStringPoolChunk keyStringPool) { - for (ResTableEntry entry : tableEntries) { - String typeStr = typeStringPool.getString(typeId - 1); // from 1 - if (entry != null) { - if (entry.entryId == entryId) { - return ("\"" + System.lineSeparator()); - } - } - } - return null; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeSpecChunk.java b/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeSpecChunk.java deleted file mode 100644 index 98838822..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/arsc/ResTableTypeSpecChunk.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.hq.arscresourcesparser.arsc; - -import com.hq.arscresourcesparser.common.Utils; -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.IOException; - -/** - * - * Created by xueqiulxq on 26/07/2017. - * - * @author bilux (i.bilux@gmail.com) - * - */ - -public class ResTableTypeSpecChunk extends BaseTypeChunk { - - public ChunkHeader header; - public int typeId; // 1byte - public int res0; // 1byte - public int res1; // 2byte - public long entryCount; - public long[] entryConfig; - - public static ResTableTypeSpecChunk parseFrom(PositionInputStream mStreamer, ResStringPoolChunk stringChunk) throws IOException { - ResTableTypeSpecChunk chunk = new ResTableTypeSpecChunk(); - chunk.header = ChunkHeader.parseFrom(mStreamer); - chunk.typeId = Utils.readUInt8(mStreamer); - chunk.res0 = Utils.readUInt8(mStreamer); - chunk.res1 = Utils.readShort(mStreamer); - chunk.entryCount = Utils.readInt(mStreamer); - chunk.entryConfig = new long[(int) chunk.entryCount]; - - for (int i=0; i", data, dataType); - break; - } - return resStr; - } - - private static String getPackage(long id) { - if (id >>> 24 == 1) { - return "android:"; - } - return ""; - } - - public static float complexToFloat(int complex) { - return (float) (complex & 0xFFFFFF00) * RADIX_MULTS[(complex>>4) & 3]; - } - - private static final float RADIX_MULTS[]={ - 0.00390625F,3.051758E-005F,1.192093E-007F,4.656613E-010F - }; - - private static final String DIMENSION_UNITS[]={ - "px","dip","sp","pt","in","mm","","" - }; - - private static final String FRACTION_UNITS[]={ - "%","%p","","","","","","" - }; - - private static String getDimenUnit(long data) { - //noinspection PointlessBitwiseExpression - switch ((int) (data >> COMPLEX_UNIT_SHIFT & COMPLEX_UNIT_MASK)) { - case COMPLEX_UNIT_PX: return "px"; - case COMPLEX_UNIT_DIP: return "dp"; - case COMPLEX_UNIT_SP: return "sp"; - case COMPLEX_UNIT_PT: return "pt"; - case COMPLEX_UNIT_IN: return "in"; - case COMPLEX_UNIT_MM: return "mm"; - default: return " (unknown unit)"; - } - } - - private static String getFractionUnit(long data) { - //noinspection PointlessBitwiseExpression - switch ((int) (data >> COMPLEX_UNIT_SHIFT & COMPLEX_UNIT_MASK)) { - case COMPLEX_UNIT_FRACTION: return "%"; - case COMPLEX_UNIT_FRACTION_PARENT: return "%p"; - default: return " (unknown unit)"; - } - } - - @Override - public String toString() { - return dataStr; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/common/Utils.java b/src/arsc_parser/com/hq/arscresourcesparser/common/Utils.java deleted file mode 100644 index 0bc90cc4..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/common/Utils.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.hq.arscresourcesparser.common; - -import com.hq.arscresourcesparser.stream.PositionInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; - -/** - * - * @author bilux (i.bilux@gmail.com) - */ - -public class Utils { - - /** - * read a char value from a byte array input stream - * - * @param mStreamer the byte array input stream - * @return the short (8-bit) value - * @throws java.io.IOException - */ - public static short readUInt8(PositionInputStream mStreamer) throws IOException { - byte[] bytes = new byte[1]; - mStreamer.read(bytes); - return getUInt8(bytes); - } - - /** - * read a short value from a byte array input stream - * - * @param mStreamer the byte array input stream - * @return the short (16-bit) value - * @throws java.io.IOException - */ - public static int readShort(PositionInputStream mStreamer) throws IOException { - byte[] bytes = new byte[2]; - mStreamer.read(bytes); - return getShort(bytes); - } - - /** - * read a int value from a byte array input stream - * - * @param mStreamer the byte array input stream - * @return the int (32-bit) value - * @throws java.io.IOException - */ - public static long readInt(PositionInputStream mStreamer) throws IOException { - byte[] bytes = new byte[4]; - mStreamer.read(bytes); - return getInt(bytes); - } - - /** - * Read and Convert Chars (16-bit) to String. Terminated by 0x00 and Padding byte 0. - * - * @param mStreamer the byte array input stream - * @param length string length - * @return - * @throws IOException - */ - public static String readString(PositionInputStream mStreamer, int length) throws IOException { - byte[] bytes = new byte[length];// The last byte is 0x00 - //mStreamer.read(bytes, 0,length); - //bytes[length] = 0; - mStreamer.read(bytes); - try { - return new String(bytes, "utf-8"); - } catch (UnsupportedEncodingException e) { - return new String(bytes); - } - } - - /** - * Read and Convert Chars (16-bit) to String. Terminated by 0x00 and Padding byte 0. - * - * @param mStreamer the byte array input stream - * @param length string length - * @return - * @throws IOException - */ - public static String readString16(PositionInputStream mStreamer, int length) throws IOException { - byte[] bytes = new byte[length]; - StringBuilder builder = new StringBuilder(); - mStreamer.read(bytes); - ByteArrayInputStream in = new ByteArrayInputStream(bytes); - byte[] buf_2 = new byte[2]; - while (in.read(buf_2) != -1) { - int code = getShort(buf_2); - if (code == 0x00) - break; // End of String - else - builder.append((char) code); - } - //builder.append((char) 0x00); // add null. - return builder.toString(); - } - - /** - * get a UInt8 value from a byte array - * - * @param bytes the byte array - * @return the short (8-bit) value - */ - public static short getUInt8(byte bytes[]) { - return (short) (bytes[0] & 0xFF); - } - - /** - * get a short value from a byte array - * - * @param bytes the byte array - * @return the short (16-bit) value - */ - public static int getShort(byte[] bytes) { - //return (short) ( ( data[1] & 0xFF << 8 ) + ( data[0] & 0xFF ) ); - return (int) (bytes[1] << 8 & 0xff00 | bytes[0] & 0xFF); - } - - /** - * get an int value from a byte array - * - * @param bytes the byte array - * @return the int (32-bit) value - */ - public static long getInt(byte[] bytes) { - return (long) bytes[3] - << 24 & 0xff000000 - | bytes[2] - << 16 & 0xff0000 - | bytes[1] - << 8 & 0xff00 - | bytes[0] & 0xFF; - } -} diff --git a/src/arsc_parser/com/hq/arscresourcesparser/stream/PositionInputStream.java b/src/arsc_parser/com/hq/arscresourcesparser/stream/PositionInputStream.java deleted file mode 100644 index b2fca3d3..00000000 --- a/src/arsc_parser/com/hq/arscresourcesparser/stream/PositionInputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.hq.arscresourcesparser.stream; - -import java.io.FilterInputStream; -import java.io.InputStream; -import java.io.IOException; - -/** - * - * @author bilux (i.bilux@gmail.com) - */ - -public class PositionInputStream extends FilterInputStream { - protected long position = 0; - private long markedPosition = 0; - - public PositionInputStream(InputStream inputStream) { - super(inputStream); - } - public PositionInputStream(InputStream inputStream, long position) { - super(inputStream); - this.position = position; - } - - public synchronized long getPosition() { - return position; - } - - @Override - public synchronized int read() throws IOException { - int p = in.read(); - if (p != -1) - position++; - return p; - } - - @Override - public synchronized int read(byte b[]) throws IOException { - int p = in.read(b); - if (p > 0) - position += p; - return p; - } - - @Override - public synchronized int read(byte b[], int off, int len) throws IOException { - int p = in.read(b, off, len); - if (p > 0) - position += p; - return p; - } - - @Override - public synchronized long skip(long n) throws IOException { - long p = in.skip(n); - if (p > 0) - position += p; - return p; - } - - public synchronized long seek(long n) throws IOException { - in.reset(); - position=0; - long p = in.skip(n); - if (p > 0) - position += p; - return p; - } - - @Override - public synchronized void reset() throws IOException { - in.reset(); - position = markedPosition; - } - - @Override - public synchronized void mark(int readlimit) { - in.mark(readlimit); - markedPosition = position; - } -} diff --git a/src/arsc_parser/meson.build b/src/arsc_parser/meson.build deleted file mode 100644 index 711f2347..00000000 --- a/src/arsc_parser/meson.build +++ /dev/null @@ -1,26 +0,0 @@ -hax_arsc_parser_jar = jar('hax_arsc_parser', [ - 'com/hq/arscresourcesparser/arsc/ResStringPoolRef.java', - 'com/hq/arscresourcesparser/arsc/ResTableMap.java', - 'com/hq/arscresourcesparser/arsc/ResTableTypeInfoChunk.java', - 'com/hq/arscresourcesparser/arsc/ResStringPoolChunk.java', - 'com/hq/arscresourcesparser/arsc/ResTableRef.java', - 'com/hq/arscresourcesparser/arsc/BaseTypeChunk.java', - 'com/hq/arscresourcesparser/arsc/ResTableValueEntry.java', - 'com/hq/arscresourcesparser/arsc/ResFileHeaderChunk.java', - 'com/hq/arscresourcesparser/arsc/ResValue.java', - 'com/hq/arscresourcesparser/arsc/ResTableMapEntry.java', - 'com/hq/arscresourcesparser/arsc/ResTableTypeSpecChunk.java', - 'com/hq/arscresourcesparser/arsc/ChunkHeader.java', - 'com/hq/arscresourcesparser/arsc/ArscFile.java', - 'com/hq/arscresourcesparser/arsc/ResTableEntry.java', - 'com/hq/arscresourcesparser/arsc/ResTablePackageChunk.java', - 'com/hq/arscresourcesparser/arsc/ResTableConfig.java', - 'com/hq/arscresourcesparser/ArscResourcesParser.java', - 'com/hq/arscresourcesparser/common/Utils.java', - 'com/hq/arscresourcesparser/stream/PositionInputStream.java' - ], - java_args: [ - '-bootclasspath', bootclasspath, - '-source', '1.7', '-target', '1.7' - ]) -