├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .tool-versions ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main └── java └── pink └── madis └── apk └── arsc ├── Chunk.java ├── ChunkWithChunks.java ├── LibraryChunk.java ├── PackageChunk.java ├── PackageUtils.java ├── ResourceConfiguration.java ├── ResourceFile.java ├── ResourceIdentifier.java ├── ResourceString.java ├── ResourceTableChunk.java ├── ResourceValue.java ├── SerializableResource.java ├── StringPoolChunk.java ├── TypeChunk.java ├── TypeSpecChunk.java ├── UnknownChunk.java ├── XmlAttribute.java ├── XmlCdataChunk.java ├── XmlChunk.java ├── XmlEndElementChunk.java ├── XmlNamespaceChunk.java ├── XmlNamespaceEndChunk.java ├── XmlNamespaceStartChunk.java ├── XmlNodeChunk.java ├── XmlResourceMapChunk.java └── XmlStartElementChunk.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | target-branch: "main" 6 | schedule: 7 | interval: "daily" 8 | time: "16:00" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | target-branch: "main" 12 | schedule: 13 | interval: "daily" 14 | time: "16:00" 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup JDK 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'zulu' 18 | java-version: '21' 19 | cache: gradle 20 | - name: Build plugin 21 | run: ./gradlew assemble 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - '*' 7 | jobs: 8 | maven: 9 | runs-on: ubuntu-latest 10 | environment: Maven publish 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup JDK 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'zulu' 18 | java-version: '21' 19 | cache: gradle 20 | - name: Publish to Maven Central 21 | env: 22 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_USER }} 23 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PASSWORD }} 24 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 25 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSPHRASE }} 27 | ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: "true" 28 | run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-parallel --no-daemon 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.gradle/ 3 | *.iml 4 | *.swp 5 | /build/ 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | java zulu-21.34.19 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android Chunk format reader/writer 2 | ================================== 3 | 4 | [![Build Status](https://travis-ci.org/madisp/android-chunk-utils.svg?branch=master)](https://travis-ci.org/madisp/android-chunk-utils) 5 | 6 | This project contains classes extracted from the 7 | [android-arscblamer](https://github.com/google/android-arscblamer) project 8 | that deal with reading and writing the resource table and compiled XML files 9 | present in APK files. 10 | 11 | The following modifications have been made: 12 | 13 | * Deleted `ArscBlamer`, `ArscDumper`, `ArscModule`, `ResourceEntryStatsCollector`, 14 | `InjectedApplication`, `BindingAnnotations`, `CommonParams` and `ApkUtils` 15 | * The package has been renamed to `pink.madis.apk.arsc` to avoid collisions 16 | * Fixed `<` and `>` characters in javadoc 17 | * Fixed reading/writing resource packages generated with older aapt versions 18 | 19 | License 20 | ------- 21 | 22 | ``` 23 | Original work Copyright 2016 Google Inc. 24 | Modified work Copyright 2017 Madis Pink 25 | 26 | Licensed under the Apache License, Version 2.0 (the "License"); 27 | you may not use this file except in compliance with the License. 28 | You may obtain a copy of the License at 29 | 30 | http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | Unless required by applicable law or agreed to in writing, software 33 | distributed under the License is distributed on an "AS IS" BASIS, 34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | See the License for the specific language governing permissions and 36 | limitations under the License. 37 | ``` 38 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | alias(libs.plugins.maven.publish) 4 | } 5 | 6 | dependencies { 7 | annotationProcessor(libs.autovalue.compiler) 8 | implementation(libs.autovalue.annotations) 9 | implementation(libs.guava) 10 | implementation(libs.jb.annotations) 11 | } 12 | 13 | java { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=pink.madis.apk.arsc 2 | VERSION_NAME=0.0.9 3 | 4 | SONATYPE_HOST=CENTRAL_PORTAL 5 | 6 | POM_NAME=emulator.wtf Gradle Plugin 7 | POM_DESCRIPTION=Android Chunk format reader/writer 8 | POM_INCEPTION_YEAR=2017 9 | POM_URL=https://github.com/madisp/android-chunk-utils 10 | 11 | POM_LICENSE_NAME=Apache 2.0 License 12 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0 13 | POM_LICENSE_DIST=repo 14 | 15 | POM_SCM_URL=https://github.com/madisp/android-chunk-utils/ 16 | POM_SCM_CONNECTION=scm:git:git://github.com/madisp/android-chunk-utils.git 17 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/madisp/android-chunk-utils.git 18 | 19 | POM_DEVELOPER_ID=madisp 20 | POM_DEVELOPER_NAME=Madis Pink 21 | POM_DEVELOPER_URL=https://github.com/madisp/ 22 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | autovalue = "1.11.0" 3 | guava = "33.4.8-android" 4 | annotations = "26.0.2" 5 | maven-publish = "0.32.0" 6 | 7 | [plugins] 8 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } 9 | 10 | [libraries] 11 | autovalue-compiler = { module = "com.google.auto.value:auto-value", version.ref = "autovalue" } 12 | autovalue-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "autovalue" } 13 | guava = { module = "com.google.guava:guava", version.ref = "guava" } 14 | jb-annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madisp/android-chunk-utils/e303a81d037a031330744016a77bc911de17f478/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | plugins { 11 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 12 | } 13 | 14 | dependencyResolutionManagement { 15 | repositories { 16 | mavenCentral() 17 | } 18 | versionCatalogs { 19 | create("libs") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/Chunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import com.google.common.collect.ImmutableMap; 21 | import com.google.common.collect.ImmutableMap.Builder; 22 | import com.google.common.io.LittleEndianDataOutputStream; 23 | import com.google.common.primitives.Shorts; 24 | import org.jetbrains.annotations.Nullable; 25 | 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.DataOutput; 28 | import java.io.IOException; 29 | import java.nio.ByteBuffer; 30 | import java.nio.ByteOrder; 31 | import java.util.Map; 32 | 33 | /** Represents a generic chunk. */ 34 | public abstract class Chunk implements SerializableResource { 35 | 36 | /** Types of chunks that can exist. */ 37 | public enum Type { 38 | NULL(0x0000), 39 | STRING_POOL(0x0001), 40 | TABLE(0x0002), 41 | XML(0x0003), 42 | XML_START_NAMESPACE(0x0100), 43 | XML_END_NAMESPACE(0x0101), 44 | XML_START_ELEMENT(0x0102), 45 | XML_END_ELEMENT(0x0103), 46 | XML_CDATA(0x0104), 47 | XML_RESOURCE_MAP(0x0180), 48 | TABLE_PACKAGE(0x0200), 49 | TABLE_TYPE(0x0201), 50 | TABLE_TYPE_SPEC(0x0202), 51 | TABLE_LIBRARY(0x0203); 52 | 53 | private final short code; 54 | 55 | private static final Map FROM_SHORT; 56 | 57 | static { 58 | Builder builder = ImmutableMap.builder(); 59 | for (Type type : values()) { 60 | builder.put(type.code(), type); 61 | } 62 | FROM_SHORT = builder.build(); 63 | } 64 | 65 | Type(int code) { 66 | this.code = Shorts.checkedCast(code); 67 | } 68 | 69 | public short code() { 70 | return code; 71 | } 72 | 73 | public static Type fromCode(short code) { 74 | return Preconditions.checkNotNull(FROM_SHORT.get(code), "Unknown chunk type: %s", code); 75 | } 76 | } 77 | 78 | /** The byte boundary to pad chunks on. */ 79 | public static final int PAD_BOUNDARY = 4; 80 | 81 | /** The number of bytes in every chunk that describes chunk type, header size, and chunk size. */ 82 | public static final int METADATA_SIZE = 8; 83 | 84 | /** The offset in bytes, from the start of the chunk, where the chunk size can be found. */ 85 | private static final int CHUNK_SIZE_OFFSET = 4; 86 | 87 | /** The parent to this chunk, if any. */ 88 | @Nullable 89 | private final Chunk parent; 90 | 91 | /** Size of the chunk header in bytes. */ 92 | protected final int headerSize; 93 | 94 | /** headerSize + dataSize. The total size of this chunk. */ 95 | protected final int chunkSize; 96 | 97 | /** Offset of this chunk from the start of the file. */ 98 | protected final int offset; 99 | 100 | protected Chunk(ByteBuffer buffer, @Nullable Chunk parent) { 101 | this.parent = parent; 102 | offset = buffer.position() - 2; 103 | headerSize = (buffer.getShort() & 0xFFFF); 104 | chunkSize = buffer.getInt(); 105 | } 106 | 107 | /** 108 | * Finishes initialization of a chunk. This should be called immediately after the constructor. 109 | * This is separate from the constructor so that the header of a chunk can be fully initialized 110 | * before the payload of that chunk is initialized for chunks that require such behavior. 111 | * 112 | * @param buffer The buffer that the payload will be initialized from. 113 | */ 114 | protected void init(ByteBuffer buffer) {} 115 | 116 | /** 117 | * Returns the parent to this chunk, if any. A parent is a chunk whose payload contains this 118 | * chunk. If there's no parent, null is returned. 119 | */ 120 | @Nullable 121 | public Chunk getParent() { 122 | return parent; 123 | } 124 | 125 | protected abstract Type getType(); 126 | 127 | /** Returns the size of this chunk's header. */ 128 | public final int getHeaderSize() { 129 | return headerSize; 130 | } 131 | 132 | /** 133 | * Returns the size of this chunk when it was first read from a buffer. A chunk's size can deviate 134 | * from this value when its data is modified (e.g. adding an entry, changing a string). 135 | * 136 | *

A chunk's current size can be determined from the length of the byte array returned from 137 | * {@link #toByteArray}. 138 | */ 139 | public final int getOriginalChunkSize() { 140 | return chunkSize; 141 | } 142 | 143 | /** 144 | * Reposition the buffer after this chunk. Use this at the end of a Chunk constructor. 145 | * @param buffer The buffer to be repositioned. 146 | */ 147 | private final void seekToEndOfChunk(ByteBuffer buffer) { 148 | buffer.position(offset + chunkSize); 149 | } 150 | 151 | /** 152 | * Writes the type and header size. We don't know how big this chunk will be (it could be 153 | * different since the last time we checked), so this needs to be passed in. 154 | * 155 | * @param output The buffer that will be written to. 156 | * @param chunkSize The total size of this chunk in bytes, including the header. 157 | */ 158 | protected final void writeHeader(ByteBuffer output, int chunkSize) { 159 | int start = output.position(); 160 | output.putShort(getType().code()); 161 | output.putShort((short) headerSize); 162 | output.putInt(chunkSize); 163 | writeHeader(output); 164 | int headerBytes = output.position() - start; 165 | Preconditions.checkState(headerBytes == getHeaderSize(), 166 | "Written header is wrong size. Got %s, want %s", headerBytes, getHeaderSize()); 167 | } 168 | 169 | /** 170 | * Writes the remaining header (after the type, {@code headerSize}, and {@code chunkSize}). 171 | * 172 | * @param output The buffer that the header will be written to. 173 | */ 174 | protected void writeHeader(ByteBuffer output) {} 175 | 176 | /** 177 | * Writes the chunk payload. The payload is data in a chunk which is not in 178 | * the first {@code headerSize} bytes of the chunk. 179 | * 180 | * @param output The stream that the payload will be written to. 181 | * @param header The already-written header. This can be modified to fix payload offsets. 182 | * @param shrink True if this payload should be optimized for size. 183 | * @throws IOException Thrown if {@code output} could not be written to (out of memory). 184 | */ 185 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 186 | throws IOException {} 187 | 188 | /** 189 | * Pads {@code output} until {@code currentLength} is on a 4-byte boundary. 190 | * 191 | * @param output The {@link DataOutput} that will be padded. 192 | * @param currentLength The current length, in bytes, of {@code output} 193 | * @return The new length of {@code output} 194 | * @throws IOException Thrown if {@code output} could not be written to. 195 | */ 196 | protected int writePad(DataOutput output, int currentLength) throws IOException { 197 | while (currentLength % PAD_BOUNDARY != 0) { 198 | output.write(0); 199 | ++currentLength; 200 | } 201 | return currentLength; 202 | } 203 | 204 | @Override 205 | public final byte[] toByteArray() throws IOException { 206 | return toByteArray(false); 207 | } 208 | 209 | /** 210 | * Converts this chunk into an array of bytes representation. Normally you will not need to 211 | * override this method unless your header changes based on the contents / size of the payload. 212 | */ 213 | @Override 214 | public final byte[] toByteArray(boolean shrink) throws IOException { 215 | ByteBuffer header = ByteBuffer.allocate(getHeaderSize()).order(ByteOrder.LITTLE_ENDIAN); 216 | writeHeader(header, 0); // The chunk size isn't known yet. This will be filled in later. 217 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 218 | 219 | try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) { 220 | writePayload(payload, header, shrink); 221 | } 222 | 223 | byte[] payloadBytes = baos.toByteArray(); 224 | int chunkSize = getHeaderSize() + payloadBytes.length; 225 | header.putInt(CHUNK_SIZE_OFFSET, chunkSize); 226 | 227 | // Combine results 228 | ByteBuffer result = ByteBuffer.allocate(chunkSize).order(ByteOrder.LITTLE_ENDIAN); 229 | result.put(header.array()); 230 | result.put(payloadBytes); 231 | return result.array(); 232 | } 233 | 234 | /** 235 | * Creates a new chunk whose contents start at {@code buffer}'s current position. 236 | * 237 | * @param buffer A buffer positioned at the start of a chunk. 238 | * @return new chunk 239 | */ 240 | public static Chunk newInstance(ByteBuffer buffer) { 241 | return newInstance(buffer, null); 242 | } 243 | 244 | /** 245 | * Creates a new chunk whose contents start at {@code buffer}'s current position. 246 | * 247 | * @param buffer A buffer positioned at the start of a chunk. 248 | * @param parent The parent to this chunk (or null if there's no parent). 249 | * @return new chunk 250 | */ 251 | public static Chunk newInstance(ByteBuffer buffer, @Nullable Chunk parent) { 252 | Chunk result; 253 | Type type = Type.fromCode(buffer.getShort()); 254 | switch (type) { 255 | case STRING_POOL: 256 | result = new StringPoolChunk(buffer, parent); 257 | break; 258 | case TABLE: 259 | result = new ResourceTableChunk(buffer, parent); 260 | break; 261 | case XML: 262 | result = new XmlChunk(buffer, parent); 263 | break; 264 | case XML_START_NAMESPACE: 265 | result = new XmlNamespaceStartChunk(buffer, parent); 266 | break; 267 | case XML_END_NAMESPACE: 268 | result = new XmlNamespaceEndChunk(buffer, parent); 269 | break; 270 | case XML_START_ELEMENT: 271 | result = new XmlStartElementChunk(buffer, parent); 272 | break; 273 | case XML_END_ELEMENT: 274 | result = new XmlEndElementChunk(buffer, parent); 275 | break; 276 | case XML_CDATA: 277 | result = new XmlCdataChunk(buffer, parent); 278 | break; 279 | case XML_RESOURCE_MAP: 280 | result = new XmlResourceMapChunk(buffer, parent); 281 | break; 282 | case TABLE_PACKAGE: 283 | result = new PackageChunk(buffer, parent); 284 | break; 285 | case TABLE_TYPE: 286 | result = new TypeChunk(buffer, parent); 287 | break; 288 | case TABLE_TYPE_SPEC: 289 | result = new TypeSpecChunk(buffer, parent); 290 | break; 291 | case TABLE_LIBRARY: 292 | result = new LibraryChunk(buffer, parent); 293 | break; 294 | default: 295 | result = new UnknownChunk(buffer, parent); 296 | } 297 | result.init(buffer); 298 | result.seekToEndOfChunk(buffer); 299 | return result; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ChunkWithChunks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.util.LinkedHashMap; 25 | import java.util.Map; 26 | 27 | /** Represents a chunk whose payload is a list of sub-chunks. */ 28 | public abstract class ChunkWithChunks extends Chunk { 29 | 30 | private final Map chunks = new LinkedHashMap<>(); 31 | 32 | protected ChunkWithChunks(ByteBuffer buffer, @Nullable Chunk parent) { 33 | super(buffer, parent); 34 | } 35 | 36 | @Override 37 | protected void init(ByteBuffer buffer) { 38 | super.init(buffer); 39 | chunks.clear(); 40 | int start = this.offset + getHeaderSize(); 41 | int offset = start; 42 | int end = this.offset + getOriginalChunkSize(); 43 | int position = buffer.position(); 44 | buffer.position(start); 45 | 46 | while (offset < end) { 47 | Chunk chunk = newInstance(buffer, this); 48 | chunks.put(offset, chunk); 49 | offset += chunk.getOriginalChunkSize(); 50 | } 51 | 52 | buffer.position(position); 53 | } 54 | 55 | /** 56 | * Retrieves the @{code chunks} contained in this chunk. 57 | * 58 | * @return map of buffer offset -> chunk contained in this chunk. 59 | */ 60 | public final Map getChunks() { 61 | return chunks; 62 | } 63 | 64 | @Override 65 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 66 | throws IOException { 67 | for (Chunk chunk : getChunks().values()) { 68 | byte[] chunkBytes = chunk.toByteArray(shrink); 69 | output.write(chunkBytes); 70 | writePad(output, chunkBytes.length); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/LibraryChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.io.DataOutput; 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | /** 30 | * Contains a list of package-id to package name mappings for any shared libraries used in this 31 | * {@link ResourceTableChunk}. The package-id's encoded in this resource table may be different 32 | * than the id's assigned at runtime 33 | */ 34 | public final class LibraryChunk extends Chunk { 35 | 36 | /** The number of resources of this type at creation time. */ 37 | private final int entryCount; 38 | 39 | /** The libraries used in this chunk (package id + name). */ 40 | private final List entries = new ArrayList<>(); 41 | 42 | protected LibraryChunk(ByteBuffer buffer, @Nullable Chunk parent) { 43 | super(buffer, parent); 44 | entryCount = buffer.getInt(); 45 | } 46 | 47 | @Override 48 | protected void init(ByteBuffer buffer) { 49 | super.init(buffer); 50 | entries.addAll(enumerateEntries(buffer)); 51 | } 52 | 53 | private List enumerateEntries(ByteBuffer buffer) { 54 | List result = new ArrayList<>(entryCount); 55 | int offset = this.offset + getHeaderSize(); 56 | int endOffset = offset + Entry.SIZE * entryCount; 57 | 58 | while (offset < endOffset) { 59 | result.add(Entry.create(buffer, offset)); 60 | offset += Entry.SIZE; 61 | } 62 | return result; 63 | } 64 | 65 | @Override 66 | protected Type getType() { 67 | return Chunk.Type.TABLE_LIBRARY; 68 | } 69 | 70 | @Override 71 | protected void writeHeader(ByteBuffer output) { 72 | super.writeHeader(output); 73 | output.putInt(entries.size()); 74 | } 75 | 76 | @Override 77 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 78 | throws IOException { 79 | for (Entry entry : entries) { 80 | output.write(entry.toByteArray(shrink)); 81 | } 82 | } 83 | 84 | /** A shared library package-id to package name entry. */ 85 | @AutoValue 86 | protected abstract static class Entry implements SerializableResource { 87 | 88 | /** Library entries only contain a package ID (4 bytes) and a package name. */ 89 | private static final int SIZE = 4 + PackageUtils.PACKAGE_NAME_SIZE; 90 | 91 | /** The id assigned to the shared library at build time. */ 92 | public abstract int packageId(); 93 | 94 | /** The package name of the shared library. */ 95 | public abstract String packageName(); 96 | 97 | static Entry create(ByteBuffer buffer, int offset) { 98 | int packageId = buffer.getInt(offset); 99 | String packageName = PackageUtils.readPackageName(buffer, offset + 4); 100 | return new AutoValue_LibraryChunk_Entry(packageId, packageName); 101 | } 102 | 103 | @Override 104 | public byte[] toByteArray() throws IOException { 105 | return toByteArray(false); 106 | } 107 | 108 | @Override 109 | public byte[] toByteArray(boolean shrink) throws IOException { 110 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 111 | buffer.putInt(packageId()); 112 | PackageUtils.writePackageName(buffer, packageName()); 113 | return buffer.array(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/PackageChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import com.google.common.collect.HashMultimap; 21 | import com.google.common.collect.Multimap; 22 | import org.jetbrains.annotations.Nullable; 23 | 24 | import java.io.DataOutput; 25 | import java.io.IOException; 26 | import java.nio.ByteBuffer; 27 | import java.util.Collection; 28 | import java.util.HashMap; 29 | import java.util.Map; 30 | 31 | /** A package chunk is a collection of resource data types within a package. */ 32 | public final class PackageChunk extends ChunkWithChunks { 33 | private static final int KNOWN_HEADER_SIZE = 284; 34 | 35 | /** Offset in bytes, from the start of the chunk, where {@code typeStringsOffset} can be found. */ 36 | private static final int TYPE_OFFSET_OFFSET = 268; 37 | 38 | /** Offset in bytes, from the start of the chunk, where {@code keyStringsOffset} can be found. */ 39 | private static final int KEY_OFFSET_OFFSET = 276; 40 | 41 | /** The package id if this is a base package, or 0 if not a base package. */ 42 | private final int id; 43 | 44 | /** The name of the package. */ 45 | private final String packageName; 46 | 47 | /** The offset (from {@code offset}) in the original buffer where type strings start. */ 48 | private final int typeStringsOffset; 49 | 50 | /** The index into the type string pool of the last public type. */ 51 | private final int lastPublicType; 52 | 53 | /** An offset to the string pool that contains the key strings for this package. */ 54 | private final int keyStringsOffset; 55 | 56 | /** The index into the key string pool of the last public key. */ 57 | private final int lastPublicKey; 58 | 59 | /** Extra blob data in the header. On API 21 and later this will contain at least 60 | * the typeIdOffset as the first 4 bytes and could potentially contain more data. */ 61 | private final byte[] headerBlob; 62 | 63 | /** Contains a mapping of a type index to its {@link TypeSpecChunk}. */ 64 | private final Map typeSpecs = new HashMap<>(); 65 | 66 | /** Contains a mapping of a type index to all of the {@link TypeChunk} with that index. */ 67 | private final Multimap types = HashMultimap.create(); 68 | 69 | protected PackageChunk(ByteBuffer buffer, @Nullable Chunk parent) { 70 | super(buffer, parent); 71 | id = buffer.getInt(); 72 | packageName = PackageUtils.readPackageName(buffer, buffer.position()); 73 | typeStringsOffset = buffer.getInt(); 74 | lastPublicType = buffer.getInt(); 75 | keyStringsOffset = buffer.getInt(); 76 | lastPublicKey = buffer.getInt(); 77 | 78 | // if we have any extra bytes then read them 79 | int blobSz = getHeaderSize() - KNOWN_HEADER_SIZE; 80 | Preconditions.checkState(blobSz >= 0, String.format( 81 | "Header smaller than expected! Expected: %d got: %d", KNOWN_HEADER_SIZE, getHeaderSize())); 82 | headerBlob = new byte[blobSz]; 83 | buffer.get(headerBlob, 0, blobSz); 84 | } 85 | 86 | @Override 87 | protected void init(ByteBuffer buffer) { 88 | super.init(buffer); 89 | for (Chunk chunk : getChunks().values()) { 90 | if (chunk instanceof TypeChunk) { 91 | TypeChunk typeChunk = (TypeChunk) chunk; 92 | types.put(typeChunk.getId(), typeChunk); 93 | } else if (chunk instanceof TypeSpecChunk) { 94 | TypeSpecChunk typeSpecChunk = (TypeSpecChunk) chunk; 95 | typeSpecs.put(typeSpecChunk.getId(), typeSpecChunk); 96 | } else if (!(chunk instanceof StringPoolChunk)) { 97 | throw new IllegalStateException( 98 | String.format("PackageChunk contains an unexpected chunk: %s", chunk.getClass())); 99 | } 100 | } 101 | } 102 | 103 | /** Returns the package id if this is a base package, or 0 if not a base package. */ 104 | public int getId() { 105 | return id; 106 | } 107 | 108 | /** 109 | * Returns the string pool that contains the names of the resources in this package. 110 | */ 111 | public StringPoolChunk getKeyStringPool() { 112 | Chunk chunk = Preconditions.checkNotNull(getChunks().get(keyStringsOffset + offset)); 113 | Preconditions.checkState(chunk instanceof StringPoolChunk, "Key string pool not found."); 114 | return (StringPoolChunk) chunk; 115 | } 116 | 117 | /** 118 | * Returns the string pool that contains the type strings for this package, such as "layout", 119 | * "string", "color". 120 | */ 121 | public StringPoolChunk getTypeStringPool() { 122 | Chunk chunk = Preconditions.checkNotNull(getChunks().get(typeStringsOffset + offset)); 123 | Preconditions.checkState(chunk instanceof StringPoolChunk, "Type string pool not found."); 124 | return (StringPoolChunk) chunk; 125 | } 126 | 127 | /** Returns all {@link TypeChunk} in this package. */ 128 | public Collection getTypeChunks() { 129 | return types.values(); 130 | } 131 | 132 | /** 133 | * For a given type id, returns the {@link TypeChunk} objects that match that id. The type id is 134 | * the 1-based index of the type in the type string pool (returned by {@link #getTypeStringPool}). 135 | * 136 | * @param id The 1-based type id to return {@link TypeChunk} objects for. 137 | * @return The matching {@link TypeChunk} objects, or an empty collection if there are none. 138 | */ 139 | public Collection getTypeChunks(int id) { 140 | return types.get(id); 141 | } 142 | 143 | /** 144 | * For a given type, returns the {@link TypeChunk} objects that match that type 145 | * (e.g. "attr", "id", "string", ...). 146 | * 147 | * @param type The type to return {@link TypeChunk} objects for. 148 | * @return The matching {@link TypeChunk} objects, or an empty collection if there are none. 149 | */ 150 | public Collection getTypeChunks(String type) { 151 | StringPoolChunk typeStringPool = Preconditions.checkNotNull(getTypeStringPool()); 152 | return getTypeChunks(typeStringPool.indexOf(type) + 1); // Convert 0-based index to 1-based 153 | } 154 | 155 | /** Returns all {@link TypeSpecChunk} in this package. */ 156 | public Collection getTypeSpecChunks() { 157 | return typeSpecs.values(); 158 | } 159 | 160 | /** For a given (1-based) type id, returns the {@link TypeSpecChunk} matching it. */ 161 | public TypeSpecChunk getTypeSpecChunk(int id) { 162 | return Preconditions.checkNotNull(typeSpecs.get(id)); 163 | } 164 | 165 | /** 166 | * For a given {@code type}, returns the {@link TypeSpecChunk} that matches it 167 | * (e.g. "attr", "id", "string", ...). 168 | */ 169 | public TypeSpecChunk getTypeSpecChunk(String type) { 170 | StringPoolChunk typeStringPool = Preconditions.checkNotNull(getTypeStringPool()); 171 | return getTypeSpecChunk(typeStringPool.indexOf(type) + 1); // Convert 0-based index to 1-based 172 | } 173 | 174 | /** Returns the name of this package. */ 175 | public String getPackageName() { 176 | return packageName; 177 | } 178 | 179 | @Override 180 | protected Type getType() { 181 | return Chunk.Type.TABLE_PACKAGE; 182 | } 183 | 184 | @Override 185 | protected void writeHeader(ByteBuffer output) { 186 | output.putInt(id); 187 | PackageUtils.writePackageName(output, packageName); 188 | output.putInt(0); // typeStringsOffset. This value can't be computed here. 189 | output.putInt(lastPublicType); 190 | output.putInt(0); // keyStringsOffset. This value can't be computed here. 191 | output.putInt(lastPublicKey); 192 | output.put(headerBlob); 193 | } 194 | 195 | @Override 196 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 197 | throws IOException { 198 | int typeOffset = typeStringsOffset; 199 | int keyOffset = keyStringsOffset; 200 | int payloadOffset = 0; 201 | for (Chunk chunk : getChunks().values()) { 202 | if (chunk == getTypeStringPool()) { 203 | typeOffset = payloadOffset + getHeaderSize(); 204 | } else if (chunk == getKeyStringPool()) { 205 | keyOffset = payloadOffset + getHeaderSize(); 206 | } 207 | byte[] chunkBytes = chunk.toByteArray(shrink); 208 | output.write(chunkBytes); 209 | payloadOffset = writePad(output, chunkBytes.length); 210 | } 211 | header.putInt(TYPE_OFFSET_OFFSET, typeOffset); 212 | header.putInt(KEY_OFFSET_OFFSET, keyOffset); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/PackageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.nio.charset.Charset; 21 | 22 | /** Provides utility methods for package names. */ 23 | public final class PackageUtils { 24 | 25 | public static final int PACKAGE_NAME_SIZE = 256; 26 | 27 | private PackageUtils() {} // Prevent instantiation 28 | 29 | /** 30 | * Reads the package name from the buffer and repositions the buffer to point directly after 31 | * the package name. 32 | * @param buffer The buffer containing the package name. 33 | * @param offset The offset in the buffer to read from. 34 | * @return The package name. 35 | */ 36 | public static String readPackageName(ByteBuffer buffer, int offset) { 37 | Charset utf16 = Charset.forName("UTF-16LE"); 38 | String str = new String(buffer.array(), offset, PACKAGE_NAME_SIZE, utf16); 39 | buffer.position(offset + PACKAGE_NAME_SIZE); 40 | return str; 41 | } 42 | 43 | /** 44 | * Writes the provided package name to the buffer in UTF-16. 45 | * @param buffer The buffer that will be written to. 46 | * @param packageName The package name that will be written to the buffer. 47 | */ 48 | public static void writePackageName(ByteBuffer buffer, String packageName) { 49 | buffer.put(packageName.getBytes(Charset.forName("UTF-16LE")), 0, PACKAGE_NAME_SIZE); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import static java.nio.charset.StandardCharsets.US_ASCII; 20 | 21 | import com.google.auto.value.AutoValue; 22 | import com.google.common.base.Joiner; 23 | import com.google.common.base.Preconditions; 24 | import com.google.common.collect.ImmutableMap; 25 | import com.google.common.primitives.UnsignedBytes; 26 | 27 | import java.nio.ByteBuffer; 28 | import java.nio.ByteOrder; 29 | import java.util.Arrays; 30 | import java.util.Collection; 31 | import java.util.Collections; 32 | import java.util.LinkedHashMap; 33 | import java.util.Map; 34 | 35 | /** Describes a particular resource configuration. */ 36 | @AutoValue 37 | public abstract class ResourceConfiguration implements SerializableResource { 38 | 39 | /** The different types of configs that can be present in a {@link ResourceConfiguration}. */ 40 | public enum Type { 41 | MCC, 42 | MNC, 43 | LANGUAGE_STRING, 44 | REGION_STRING, 45 | SCREEN_LAYOUT_DIRECTION, 46 | SMALLEST_SCREEN_WIDTH_DP, 47 | SCREEN_WIDTH_DP, 48 | SCREEN_HEIGHT_DP, 49 | SCREEN_LAYOUT_SIZE, 50 | SCREEN_LAYOUT_LONG, 51 | SCREEN_LAYOUT_ROUND, 52 | ORIENTATION, 53 | UI_MODE_TYPE, 54 | UI_MODE_NIGHT, 55 | DENSITY_DPI, 56 | TOUCHSCREEN, 57 | KEYBOARD_HIDDEN, 58 | KEYBOARD, 59 | NAVIGATION_HIDDEN, 60 | NAVIGATION, 61 | SDK_VERSION 62 | } 63 | 64 | /** The below constants are from android.content.res.Configuration. */ 65 | private static final int DENSITY_DPI_UNDEFINED = 0; 66 | private static final int DENSITY_DPI_LDPI = 120; 67 | private static final int DENSITY_DPI_MDPI = 160; 68 | private static final int DENSITY_DPI_TVDPI = 213; 69 | private static final int DENSITY_DPI_HDPI = 240; 70 | private static final int DENSITY_DPI_XHDPI = 320; 71 | private static final int DENSITY_DPI_XXHDPI = 480; 72 | private static final int DENSITY_DPI_XXXHDPI = 640; 73 | private static final int DENSITY_DPI_ANY = 0xFFFE; 74 | private static final int DENSITY_DPI_NONE = 0xFFFF; 75 | private static final Map DENSITY_DPI_VALUES = 76 | ImmutableMap.builder() 77 | .put(DENSITY_DPI_UNDEFINED, "") 78 | .put(DENSITY_DPI_LDPI, "ldpi") 79 | .put(DENSITY_DPI_MDPI, "mdpi") 80 | .put(DENSITY_DPI_TVDPI, "tvdpi") 81 | .put(DENSITY_DPI_HDPI, "hdpi") 82 | .put(DENSITY_DPI_XHDPI, "xhdpi") 83 | .put(DENSITY_DPI_XXHDPI, "xxhdpi") 84 | .put(DENSITY_DPI_XXXHDPI, "xxxhdpi") 85 | .put(DENSITY_DPI_ANY, "anydpi") 86 | .put(DENSITY_DPI_NONE, "nodpi") 87 | .build(); 88 | 89 | private static final int KEYBOARD_NOKEYS = 1; 90 | private static final int KEYBOARD_QWERTY = 2; 91 | private static final int KEYBOARD_12KEY = 3; 92 | private static final Map KEYBOARD_VALUES = ImmutableMap.of( 93 | KEYBOARD_NOKEYS, "nokeys", 94 | KEYBOARD_QWERTY, "qwerty", 95 | KEYBOARD_12KEY, "12key"); 96 | 97 | private static final int KEYBOARDHIDDEN_MASK = 0x03; 98 | private static final int KEYBOARDHIDDEN_NO = 1; 99 | private static final int KEYBOARDHIDDEN_YES = 2; 100 | private static final int KEYBOARDHIDDEN_SOFT = 3; 101 | private static final Map KEYBOARDHIDDEN_VALUES = ImmutableMap.of( 102 | KEYBOARDHIDDEN_NO, "keysexposed", 103 | KEYBOARDHIDDEN_YES, "keyshidden", 104 | KEYBOARDHIDDEN_SOFT, "keyssoft"); 105 | 106 | private static final int NAVIGATION_NONAV = 1; 107 | private static final int NAVIGATION_DPAD = 2; 108 | private static final int NAVIGATION_TRACKBALL = 3; 109 | private static final int NAVIGATION_WHEEL = 4; 110 | private static final Map NAVIGATION_VALUES = ImmutableMap.of( 111 | NAVIGATION_NONAV, "nonav", 112 | NAVIGATION_DPAD, "dpad", 113 | NAVIGATION_TRACKBALL, "trackball", 114 | NAVIGATION_WHEEL, "wheel"); 115 | 116 | private static final int NAVIGATIONHIDDEN_MASK = 0x0C; 117 | private static final int NAVIGATIONHIDDEN_NO = 0x04; 118 | private static final int NAVIGATIONHIDDEN_YES = 0x08; 119 | private static final Map NAVIGATIONHIDDEN_VALUES = ImmutableMap.of( 120 | NAVIGATIONHIDDEN_NO, "navexposed", 121 | NAVIGATIONHIDDEN_YES, "navhidden"); 122 | 123 | private static final int ORIENTATION_PORTRAIT = 0x01; 124 | private static final int ORIENTATION_LANDSCAPE = 0x02; 125 | private static final Map ORIENTATION_VALUES = ImmutableMap.of( 126 | ORIENTATION_PORTRAIT, "port", 127 | ORIENTATION_LANDSCAPE, "land"); 128 | 129 | private static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0xC0; 130 | private static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; 131 | private static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; 132 | private static final Map SCREENLAYOUT_LAYOUTDIR_VALUES = ImmutableMap.of( 133 | SCREENLAYOUT_LAYOUTDIR_LTR, "ldltr", 134 | SCREENLAYOUT_LAYOUTDIR_RTL, "ldrtl"); 135 | 136 | private static final int SCREENLAYOUT_LONG_MASK = 0x30; 137 | private static final int SCREENLAYOUT_LONG_NO = 0x10; 138 | private static final int SCREENLAYOUT_LONG_YES = 0x20; 139 | private static final Map SCREENLAYOUT_LONG_VALUES = ImmutableMap.of( 140 | SCREENLAYOUT_LONG_NO, "notlong", 141 | SCREENLAYOUT_LONG_YES, "long"); 142 | 143 | private static final int SCREENLAYOUT_ROUND_MASK = 0x0300; 144 | private static final int SCREENLAYOUT_ROUND_NO = 0x0100; 145 | private static final int SCREENLAYOUT_ROUND_YES = 0x0200; 146 | private static final Map SCREENLAYOUT_ROUND_VALUES = ImmutableMap.of( 147 | SCREENLAYOUT_ROUND_NO, "notround", 148 | SCREENLAYOUT_ROUND_YES, "round"); 149 | 150 | private static final int SCREENLAYOUT_SIZE_MASK = 0x0F; 151 | private static final int SCREENLAYOUT_SIZE_SMALL = 0x01; 152 | private static final int SCREENLAYOUT_SIZE_NORMAL = 0x02; 153 | private static final int SCREENLAYOUT_SIZE_LARGE = 0x03; 154 | private static final int SCREENLAYOUT_SIZE_XLARGE = 0x04; 155 | private static final Map SCREENLAYOUT_SIZE_VALUES = ImmutableMap.of( 156 | SCREENLAYOUT_SIZE_SMALL, "small", 157 | SCREENLAYOUT_SIZE_NORMAL, "normal", 158 | SCREENLAYOUT_SIZE_LARGE, "large", 159 | SCREENLAYOUT_SIZE_XLARGE, "xlarge"); 160 | 161 | private static final int TOUCHSCREEN_NOTOUCH = 1; 162 | private static final int TOUCHSCREEN_FINGER = 3; 163 | private static final Map TOUCHSCREEN_VALUES = ImmutableMap.of( 164 | TOUCHSCREEN_NOTOUCH, "notouch", 165 | TOUCHSCREEN_FINGER, "finger"); 166 | 167 | private static final int UI_MODE_NIGHT_MASK = 0x30; 168 | private static final int UI_MODE_NIGHT_NO = 0x10; 169 | private static final int UI_MODE_NIGHT_YES = 0x20; 170 | private static final Map UI_MODE_NIGHT_VALUES = ImmutableMap.of( 171 | UI_MODE_NIGHT_NO, "notnight", 172 | UI_MODE_NIGHT_YES, "night"); 173 | 174 | private static final int UI_MODE_TYPE_MASK = 0x0F; 175 | private static final int UI_MODE_TYPE_DESK = 0x02; 176 | private static final int UI_MODE_TYPE_CAR = 0x03; 177 | private static final int UI_MODE_TYPE_TELEVISION = 0x04; 178 | private static final int UI_MODE_TYPE_APPLIANCE = 0x05; 179 | private static final int UI_MODE_TYPE_WATCH = 0x06; 180 | private static final Map UI_MODE_TYPE_VALUES = ImmutableMap.of( 181 | UI_MODE_TYPE_DESK, "desk", 182 | UI_MODE_TYPE_CAR, "car", 183 | UI_MODE_TYPE_TELEVISION, "television", 184 | UI_MODE_TYPE_APPLIANCE, "appliance", 185 | UI_MODE_TYPE_WATCH, "watch"); 186 | 187 | /** The minimum size in bytes that this configuration must be to contain screen config info. */ 188 | private static final int SCREEN_CONFIG_MIN_SIZE = 32; 189 | 190 | /** The minimum size in bytes that this configuration must be to contain screen dp info. */ 191 | private static final int SCREEN_DP_MIN_SIZE = 36; 192 | 193 | /** The minimum size in bytes that this configuration must be to contain locale info. */ 194 | private static final int LOCALE_MIN_SIZE = 48; 195 | 196 | /** The minimum size in bytes that this config must be to contain the screenConfig extension. */ 197 | private static final int SCREEN_CONFIG_EXTENSION_MIN_SIZE = 52; 198 | 199 | /** The number of bytes that this resource configuration takes up. */ 200 | public abstract int size(); 201 | 202 | public abstract int mcc(); 203 | public abstract int mnc(); 204 | 205 | /** Returns a packed 2-byte language code. */ 206 | @SuppressWarnings("mutable") 207 | public abstract byte[] language(); 208 | 209 | /** Returns {@link #language} as an unpacked string representation. */ 210 | public final String languageString() { 211 | return unpackLanguage(); 212 | } 213 | 214 | /** Returns a packed 2-byte region code. */ 215 | @SuppressWarnings("mutable") 216 | public abstract byte[] region(); 217 | 218 | /** Returns {@link #region} as an unpacked string representation. */ 219 | public final String regionString() { 220 | return unpackRegion(); 221 | } 222 | 223 | public abstract int orientation(); 224 | public abstract int touchscreen(); 225 | public abstract int density(); 226 | public abstract int keyboard(); 227 | public abstract int navigation(); 228 | public abstract int inputFlags(); 229 | 230 | public final int keyboardHidden() { 231 | return inputFlags() & KEYBOARDHIDDEN_MASK; 232 | } 233 | 234 | public final int navigationHidden() { 235 | return inputFlags() & NAVIGATIONHIDDEN_MASK; 236 | } 237 | 238 | public abstract int screenWidth(); 239 | public abstract int screenHeight(); 240 | public abstract int sdkVersion(); 241 | 242 | /** 243 | * Returns a copy of this resource configuration with a different {@link #sdkVersion}, or this 244 | * configuration if the {@code sdkVersion} is the same. 245 | * 246 | * @param sdkVersion The SDK version of the returned configuration. 247 | * @return A copy of this configuration with the only difference being #sdkVersion. 248 | */ 249 | public final ResourceConfiguration withSdkVersion(int sdkVersion) { 250 | if (sdkVersion == sdkVersion()) { 251 | return this; 252 | } 253 | return new AutoValue_ResourceConfiguration(size(), mcc(), mnc(), language(), region(), 254 | orientation(), touchscreen(), density(), keyboard(), navigation(), inputFlags(), 255 | screenWidth(), screenHeight(), sdkVersion, minorVersion(), screenLayout(), uiMode(), 256 | smallestScreenWidthDp(), screenWidthDp(), screenHeightDp(), localeScript(), localeVariant(), 257 | screenLayout2(), unknown()); 258 | } 259 | 260 | public abstract int minorVersion(); 261 | public abstract int screenLayout(); 262 | 263 | public final int screenLayoutDirection() { 264 | return screenLayout() & SCREENLAYOUT_LAYOUTDIR_MASK; 265 | } 266 | 267 | public final int screenLayoutSize() { 268 | return screenLayout() & SCREENLAYOUT_SIZE_MASK; 269 | } 270 | 271 | public final int screenLayoutLong() { 272 | return screenLayout() & SCREENLAYOUT_LONG_MASK; 273 | } 274 | 275 | public final int screenLayoutRound() { 276 | return screenLayout() & SCREENLAYOUT_ROUND_MASK; 277 | } 278 | 279 | public abstract int uiMode(); 280 | 281 | public final int uiModeType() { 282 | return uiMode() & UI_MODE_TYPE_MASK; 283 | } 284 | 285 | public final int uiModeNight() { 286 | return uiMode() & UI_MODE_NIGHT_MASK; 287 | } 288 | 289 | public abstract int smallestScreenWidthDp(); 290 | public abstract int screenWidthDp(); 291 | public abstract int screenHeightDp(); 292 | 293 | /** The ISO-15924 short name for the script corresponding to this configuration. */ 294 | @SuppressWarnings("mutable") 295 | public abstract byte[] localeScript(); 296 | 297 | /** A single BCP-47 variant subtag. */ 298 | @SuppressWarnings("mutable") 299 | public abstract byte[] localeVariant(); 300 | 301 | /** An extension to {@link #screenLayout}. Contains round/notround qualifier. */ 302 | public abstract int screenLayout2(); 303 | 304 | /** Any remaining bytes in this resource configuration that are unaccounted for. */ 305 | @SuppressWarnings("mutable") 306 | public abstract byte[] unknown(); 307 | 308 | static ResourceConfiguration create(ByteBuffer buffer) { 309 | int startPosition = buffer.position(); // The starting buffer position to calculate bytes read. 310 | int size = buffer.getInt(); 311 | int mcc = buffer.getShort() & 0xFFFF; 312 | int mnc = buffer.getShort() & 0xFFFF; 313 | byte[] language = new byte[2]; 314 | buffer.get(language); 315 | byte[] region = new byte[2]; 316 | buffer.get(region); 317 | int orientation = UnsignedBytes.toInt(buffer.get()); 318 | int touchscreen = UnsignedBytes.toInt(buffer.get()); 319 | int density = buffer.getShort() & 0xFFFF; 320 | int keyboard = UnsignedBytes.toInt(buffer.get()); 321 | int navigation = UnsignedBytes.toInt(buffer.get()); 322 | int inputFlags = UnsignedBytes.toInt(buffer.get()); 323 | buffer.get(); // 1 byte of padding 324 | int screenWidth = buffer.getShort() & 0xFFFF; 325 | int screenHeight = buffer.getShort() & 0xFFFF; 326 | int sdkVersion = buffer.getShort() & 0xFFFF; 327 | int minorVersion = buffer.getShort() & 0xFFFF; 328 | 329 | // At this point, the configuration's size needs to be taken into account as not all 330 | // configurations have all values. 331 | int screenLayout = 0; 332 | int uiMode = 0; 333 | int smallestScreenWidthDp = 0; 334 | int screenWidthDp = 0; 335 | int screenHeightDp = 0; 336 | byte[] localeScript = new byte[4]; 337 | byte[] localeVariant = new byte[8]; 338 | int screenLayout2 = 0; 339 | 340 | if (size >= SCREEN_CONFIG_MIN_SIZE) { 341 | screenLayout = UnsignedBytes.toInt(buffer.get()); 342 | uiMode = UnsignedBytes.toInt(buffer.get()); 343 | smallestScreenWidthDp = buffer.getShort() & 0xFFFF; 344 | } 345 | 346 | if (size >= SCREEN_DP_MIN_SIZE) { 347 | screenWidthDp = buffer.getShort() & 0xFFFF; 348 | screenHeightDp = buffer.getShort() & 0xFFFF; 349 | } 350 | 351 | if (size >= LOCALE_MIN_SIZE) { 352 | buffer.get(localeScript); 353 | buffer.get(localeVariant); 354 | } 355 | 356 | if (size >= SCREEN_CONFIG_EXTENSION_MIN_SIZE) { 357 | screenLayout2 = UnsignedBytes.toInt(buffer.get()); 358 | buffer.get(); // Reserved padding 359 | buffer.getShort(); // More reserved padding 360 | } 361 | 362 | // After parsing everything that's known, account for anything that's unknown. 363 | int bytesRead = buffer.position() - startPosition; 364 | byte[] unknown = new byte[size - bytesRead]; 365 | buffer.get(unknown); 366 | 367 | return new AutoValue_ResourceConfiguration(size, mcc, mnc, language, region, orientation, 368 | touchscreen, density, keyboard, navigation, inputFlags, screenWidth, screenHeight, 369 | sdkVersion, minorVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, 370 | screenHeightDp, localeScript, localeVariant, screenLayout2, unknown); 371 | } 372 | 373 | private String unpackLanguage() { 374 | return unpackLanguageOrRegion(language(), 0x61); 375 | } 376 | 377 | private String unpackRegion() { 378 | return unpackLanguageOrRegion(region(), 0x30); 379 | } 380 | 381 | private String unpackLanguageOrRegion(byte[] value, int base) { 382 | Preconditions.checkState(value.length == 2, "Language or region value must be 2 bytes."); 383 | if (value[0] == 0 && value[1] == 0) { 384 | return ""; 385 | } 386 | if ((UnsignedBytes.toInt(value[0]) & 0x80) != 0) { 387 | byte[] result = new byte[3]; 388 | result[0] = (byte) (base + (value[1] & 0x1F)); 389 | result[1] = (byte) (base + ((value[1] & 0xE0) >>> 5) + ((value[0] & 0x03) << 3)); 390 | result[2] = (byte) (base + ((value[0] & 0x7C) >>> 2)); 391 | return new String(result, US_ASCII); 392 | } 393 | return new String(value, US_ASCII); 394 | } 395 | 396 | /** Returns true if this is the default "any" configuration. */ 397 | public final boolean isDefault() { 398 | return mcc() == 0 399 | && mnc() == 0 400 | && Arrays.equals(language(), new byte[2]) 401 | && Arrays.equals(region(), new byte[2]) 402 | && orientation() == 0 403 | && touchscreen() == 0 404 | && density() == 0 405 | && keyboard() == 0 406 | && navigation() == 0 407 | && inputFlags() == 0 408 | && screenWidth() == 0 409 | && screenHeight() == 0 410 | && sdkVersion() == 0 411 | && minorVersion() == 0 412 | && screenLayout() == 0 413 | && uiMode() == 0 414 | && smallestScreenWidthDp() == 0 415 | && screenWidthDp() == 0 416 | && screenHeightDp() == 0 417 | && Arrays.equals(localeScript(), new byte[4]) 418 | && Arrays.equals(localeVariant(), new byte[8]) 419 | && screenLayout2() == 0; 420 | } 421 | 422 | @Override 423 | public final byte[] toByteArray() { 424 | return toByteArray(false); 425 | } 426 | 427 | @Override 428 | public final byte[] toByteArray(boolean shrink) { 429 | ByteBuffer buffer = ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN); 430 | buffer.putInt(size()); 431 | buffer.putShort((short) mcc()); 432 | buffer.putShort((short) mnc()); 433 | buffer.put(language()); 434 | buffer.put(region()); 435 | buffer.put((byte) orientation()); 436 | buffer.put((byte) touchscreen()); 437 | buffer.putShort((short) density()); 438 | buffer.put((byte) keyboard()); 439 | buffer.put((byte) navigation()); 440 | buffer.put((byte) inputFlags()); 441 | buffer.put((byte) 0); // Padding 442 | buffer.putShort((short) screenWidth()); 443 | buffer.putShort((short) screenHeight()); 444 | buffer.putShort((short) sdkVersion()); 445 | buffer.putShort((short) minorVersion()); 446 | 447 | if (size() >= SCREEN_CONFIG_MIN_SIZE) { 448 | buffer.put((byte) screenLayout()); 449 | buffer.put((byte) uiMode()); 450 | buffer.putShort((short) smallestScreenWidthDp()); 451 | } 452 | 453 | if (size() >= SCREEN_DP_MIN_SIZE) { 454 | buffer.putShort((short) screenWidthDp()); 455 | buffer.putShort((short) screenHeightDp()); 456 | } 457 | 458 | if (size() >= LOCALE_MIN_SIZE) { 459 | buffer.put(localeScript()); 460 | buffer.put(localeVariant()); 461 | } 462 | 463 | if (size() >= SCREEN_CONFIG_EXTENSION_MIN_SIZE) { 464 | buffer.putInt(screenLayout2()); // Writing an unsigned byte + 3 padding 465 | } 466 | 467 | buffer.put(unknown()); 468 | 469 | return buffer.array(); 470 | } 471 | 472 | @Override 473 | public final String toString() { 474 | if (isDefault()) { // Prevent the default configuration from returning the empty string 475 | return "default"; 476 | } 477 | Collection parts = toStringParts().values(); 478 | parts.removeAll(Collections.singleton("")); 479 | return Joiner.on('-').join(parts); 480 | } 481 | 482 | /** 483 | * Returns a map of the configuration parts for {@link #toString}. 484 | * 485 | *

If a configuration part is not defined for this {@link ResourceConfiguration}, its value 486 | * will be the empty string. 487 | */ 488 | public final Map toStringParts() { 489 | Map result = new LinkedHashMap<>(); // Preserve order for #toString(). 490 | result.put(Type.MCC, mcc() != 0 ? "mcc" + mcc() : ""); 491 | result.put(Type.MNC, mnc() != 0 ? "mnc" + mnc() : ""); 492 | result.put(Type.LANGUAGE_STRING, !languageString().isEmpty() ? "" + languageString() : ""); 493 | result.put(Type.REGION_STRING, !regionString().isEmpty() ? "r" + regionString() : ""); 494 | result.put(Type.SCREEN_LAYOUT_DIRECTION, 495 | getOrDefault(SCREENLAYOUT_LAYOUTDIR_VALUES, screenLayoutDirection(), "")); 496 | result.put(Type.SMALLEST_SCREEN_WIDTH_DP, 497 | smallestScreenWidthDp() != 0 ? "sw" + smallestScreenWidthDp() + "dp" : ""); 498 | result.put(Type.SCREEN_WIDTH_DP, screenWidthDp() != 0 ? "w" + screenWidthDp() + "dp" : ""); 499 | result.put(Type.SCREEN_HEIGHT_DP, screenHeightDp() != 0 ? "h" + screenHeightDp() + "dp" : ""); 500 | result.put(Type.SCREEN_LAYOUT_SIZE, 501 | getOrDefault(SCREENLAYOUT_SIZE_VALUES, screenLayoutSize(), "")); 502 | result.put(Type.SCREEN_LAYOUT_LONG, 503 | getOrDefault(SCREENLAYOUT_LONG_VALUES, screenLayoutLong(), "")); 504 | result.put(Type.SCREEN_LAYOUT_ROUND, 505 | getOrDefault(SCREENLAYOUT_ROUND_VALUES, screenLayoutRound(), "")); 506 | result.put(Type.ORIENTATION, getOrDefault(ORIENTATION_VALUES, orientation(), "")); 507 | result.put(Type.UI_MODE_TYPE, getOrDefault(UI_MODE_TYPE_VALUES, uiModeType(), "")); 508 | result.put(Type.UI_MODE_NIGHT, getOrDefault(UI_MODE_NIGHT_VALUES, uiModeNight(), "")); 509 | result.put(Type.DENSITY_DPI, getOrDefault(DENSITY_DPI_VALUES, density(), density() + "dpi")); 510 | result.put(Type.TOUCHSCREEN, getOrDefault(TOUCHSCREEN_VALUES, touchscreen(), "")); 511 | result.put(Type.KEYBOARD_HIDDEN, getOrDefault(KEYBOARDHIDDEN_VALUES, keyboardHidden(), "")); 512 | result.put(Type.KEYBOARD, getOrDefault(KEYBOARD_VALUES, keyboard(), "")); 513 | result.put(Type.NAVIGATION_HIDDEN, 514 | getOrDefault(NAVIGATIONHIDDEN_VALUES, navigationHidden(), "")); 515 | result.put(Type.NAVIGATION, getOrDefault(NAVIGATION_VALUES, navigation(), "")); 516 | result.put(Type.SDK_VERSION, sdkVersion() != 0 ? "v" + sdkVersion() : ""); 517 | return result; 518 | } 519 | 520 | private V getOrDefault(Map map, K key, V defaultValue) { 521 | // TODO(acornwall): Remove this when Java 8's Map#getOrDefault is available. 522 | // Null is not returned, even if the map contains a key whose value is null. This is intended. 523 | V value = map.get(key); 524 | return value != null ? value : defaultValue; 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.io.ByteArrayDataOutput; 20 | import com.google.common.io.ByteStreams; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | 30 | /** Given an arsc file, maps the contents of the file. */ 31 | public final class ResourceFile implements SerializableResource { 32 | 33 | /** The chunks contained in this resource file. */ 34 | private final List chunks = new ArrayList<>(); 35 | 36 | public ResourceFile(byte[] buf) { 37 | ByteBuffer buffer = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); 38 | while (buffer.remaining() > 0) { 39 | chunks.add(Chunk.newInstance(buffer)); 40 | } 41 | } 42 | 43 | /** 44 | * Given an input stream, reads the stream until the end and returns a {@link ResourceFile} 45 | * representing the contents of the stream. 46 | * 47 | * @param is The input stream to read from. 48 | * @return ResourceFile represented by the @{link InputStream}. 49 | * @throws IOException 50 | */ 51 | public static ResourceFile fromInputStream(InputStream is) throws IOException { 52 | byte[] buf = ByteStreams.toByteArray(is); 53 | return new ResourceFile(buf); 54 | } 55 | 56 | /** Returns the chunks in this resource file. */ 57 | public List getChunks() { 58 | return Collections.unmodifiableList(chunks); 59 | } 60 | 61 | @Override 62 | public byte[] toByteArray() throws IOException { 63 | return toByteArray(false); 64 | } 65 | 66 | @Override 67 | public byte[] toByteArray(boolean shrink) throws IOException { 68 | ByteArrayDataOutput output = ByteStreams.newDataOutput(); 69 | for (Chunk chunk : chunks) { 70 | output.write(chunk.toByteArray(shrink)); 71 | } 72 | return output.toByteArray(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceIdentifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.base.Preconditions; 21 | 22 | /** 23 | * Resources in a {@link ResourceTableChunk} are identified by an integer of the form 0xpptteeee, 24 | * where pp is the {@link PackageChunk} id, tt is the {@link TypeChunk} id, and eeee is the index of 25 | * the entry in the {@link TypeChunk}. 26 | */ 27 | @AutoValue 28 | public abstract class ResourceIdentifier { 29 | 30 | /** The {@link PackageChunk} id mask for a packed resource id of the form 0xpptteeee. */ 31 | private static final int PACKAGE_ID_MASK = 0xFF000000; 32 | private static final int PACKAGE_ID_SHIFT = 24; 33 | 34 | /** The {@link TypeChunk} id mask for a packed resource id of the form 0xpptteeee. */ 35 | private static final int TYPE_ID_MASK = 0x00FF0000; 36 | private static final int TYPE_ID_SHIFT = 16; 37 | 38 | /** The {@link TypeChunk.Entry} id mask for a packed resource id of the form 0xpptteeee. */ 39 | private static final int ENTRY_ID_MASK = 0xFFFF; 40 | private static final int ENTRY_ID_SHIFT = 0; 41 | 42 | /** The (1-based) id of the {@link PackageChunk} containing this resource. */ 43 | public abstract int packageId(); 44 | 45 | /** The (1-based) id of the {@link TypeChunk} containing this resource. */ 46 | public abstract int typeId(); 47 | 48 | /** The (0-based) index of the entry in a {@link TypeChunk} containing this resource. */ 49 | public abstract int entryId(); 50 | 51 | /** Returns a {@link ResourceIdentifier} from a {@code resourceId} of the form 0xpptteeee. */ 52 | public static ResourceIdentifier create(int resourceId) { 53 | int packageId = (resourceId & PACKAGE_ID_MASK) >>> PACKAGE_ID_SHIFT; 54 | int typeId = (resourceId & TYPE_ID_MASK) >>> TYPE_ID_SHIFT; 55 | int entryId = (resourceId & ENTRY_ID_MASK) >>> ENTRY_ID_SHIFT; 56 | return create(packageId, typeId, entryId); 57 | } 58 | 59 | /** Returns a {@link ResourceIdentifier} with the given identifiers. */ 60 | public static ResourceIdentifier create(int packageId, int typeId, int entryId) { 61 | Preconditions.checkState((packageId & 0xFF) == packageId, "packageId must be <= 0xFF."); 62 | Preconditions.checkState((typeId & 0xFF) == typeId, "typeId must be <= 0xFF."); 63 | Preconditions.checkState((entryId & 0xFFFF) == entryId, "entryId must be <= 0xFFFF."); 64 | return new AutoValue_ResourceIdentifier(packageId, typeId, entryId); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import static java.nio.charset.StandardCharsets.UTF_16LE; 20 | import static java.nio.charset.StandardCharsets.UTF_8; 21 | 22 | import com.google.common.io.ByteArrayDataOutput; 23 | import com.google.common.io.ByteStreams; 24 | import com.google.common.primitives.UnsignedBytes; 25 | 26 | import java.nio.ByteBuffer; 27 | import java.nio.charset.Charset; 28 | 29 | /** Provides utilities to decode/encode a String packed in an arsc resource file. */ 30 | public final class ResourceString { 31 | 32 | /** Type of {@link ResourceString} to encode / decode. */ 33 | public enum Type { 34 | UTF8(UTF_8), 35 | UTF16(UTF_16LE); 36 | 37 | private final Charset charset; 38 | 39 | Type(Charset charset) { 40 | this.charset = charset; 41 | } 42 | 43 | public Charset charset() { 44 | return charset; 45 | } 46 | } 47 | 48 | private ResourceString() {} // Private constructor 49 | 50 | /** 51 | * Given a buffer and an offset into the buffer, returns a String. The {@code offset} is the 52 | * 0-based byte offset from the start of the buffer where the string resides. This should be the 53 | * location in memory where the string's character count, followed by its byte count, and then 54 | * followed by the actual string is located. 55 | * 56 | *

Here's an example UTF-8-encoded string of ab©: 57 | *

 58 |    * 03 04 61 62 C2 A9 00
 59 |    * ^ Offset should be here
 60 |    * 
61 | * 62 | * @param buffer The buffer containing the string to decode. 63 | * @param offset Offset into the buffer where the string resides. 64 | * @param type The encoding type that the {@link ResourceString} is encoded in. 65 | * @return The decoded string. 66 | */ 67 | public static String decodeString(ByteBuffer buffer, int offset, Type type) { 68 | int length; 69 | int characterCount = decodeLength(buffer, offset, type); 70 | offset += computeLengthOffset(characterCount, type); 71 | // UTF-8 strings have 2 lengths: the number of characters, and then the encoding length. 72 | // UTF-16 strings, however, only have 1 length: the number of characters. 73 | if (type == Type.UTF8) { 74 | length = decodeLength(buffer, offset, type); 75 | offset += computeLengthOffset(length, type); 76 | } else { 77 | length = characterCount * 2; 78 | } 79 | return new String(buffer.array(), offset, length, type.charset()); 80 | } 81 | 82 | /** 83 | * Encodes a string in either UTF-8 or UTF-16 and returns the bytes of the encoded string. 84 | * Strings are prefixed by 2 values. The first is the number of characters in the string. 85 | * The second is the encoding length (number of bytes in the string). 86 | * 87 | *

Here's an example UTF-8-encoded string of ab©: 88 | *

03 04 61 62 C2 A9 00
89 | * 90 | * @param str The string to be encoded. 91 | * @param type The encoding type that the {@link ResourceString} should be encoded in. 92 | * @return The encoded string. 93 | */ 94 | public static byte[] encodeString(String str, Type type) { 95 | byte[] bytes = str.getBytes(type.charset()); 96 | // The extra 5 bytes is for metadata (character count + byte count) and the NULL terminator. 97 | ByteArrayDataOutput output = ByteStreams.newDataOutput(bytes.length + 5); 98 | encodeLength(output, str.length(), type); 99 | if (type == Type.UTF8) { // Only UTF-8 strings have the encoding length. 100 | encodeLength(output, bytes.length, type); 101 | } 102 | output.write(bytes); 103 | // NULL-terminate the string 104 | if (type == Type.UTF8) { 105 | output.write(0); 106 | } else { 107 | output.writeShort(0); 108 | } 109 | return output.toByteArray(); 110 | } 111 | 112 | private static void encodeLength(ByteArrayDataOutput output, int length, Type type) { 113 | if (length < 0) { 114 | output.write(0); 115 | return; 116 | } 117 | if (type == Type.UTF8) { 118 | if (length > 0x7F) { 119 | output.write(((length & 0x7F00) >> 8) | 0x80); 120 | } 121 | output.write(length & 0xFF); 122 | } else { // UTF-16 123 | // TODO(acornwall): Replace output with a little-endian output. 124 | if (length > 0x7FFF) { 125 | int highBytes = ((length & 0x7FFF0000) >> 16) | 0x8000; 126 | output.write(highBytes & 0xFF); 127 | output.write((highBytes & 0xFF00) >> 8); 128 | } 129 | int lowBytes = length & 0xFFFF; 130 | output.write(lowBytes & 0xFF); 131 | output.write((lowBytes & 0xFF00) >> 8); 132 | } 133 | } 134 | 135 | private static int computeLengthOffset(int length, Type type) { 136 | return (type == Type.UTF8 ? 1 : 2) * (length >= (type == Type.UTF8 ? 0x80 : 0x8000) ? 2 : 1); 137 | } 138 | 139 | private static int decodeLength(ByteBuffer buffer, int offset, Type type) { 140 | return type == Type.UTF8 ? decodeLengthUTF8(buffer, offset) : decodeLengthUTF16(buffer, offset); 141 | } 142 | 143 | private static int decodeLengthUTF8(ByteBuffer buffer, int offset) { 144 | // UTF-8 strings use a clever variant of the 7-bit integer for packing the string length. 145 | // If the first byte is >= 0x80, then a second byte follows. For these values, the length 146 | // is WORD-length in big-endian & 0x7FFF. 147 | int length = UnsignedBytes.toInt(buffer.get(offset)); 148 | if ((length & 0x80) != 0) { 149 | length = ((length & 0x7F) << 8) | UnsignedBytes.toInt(buffer.get(offset + 1)); 150 | } 151 | return length; 152 | } 153 | 154 | private static int decodeLengthUTF16(ByteBuffer buffer, int offset) { 155 | // UTF-16 strings use a clever variant of the 7-bit integer for packing the string length. 156 | // If the first word is >= 0x8000, then a second word follows. For these values, the length 157 | // is DWORD-length in big-endian & 0x7FFFFFFF. 158 | int length = (buffer.getShort(offset) & 0xFFFF); 159 | if ((length & 0x8000) != 0) { 160 | length = ((length & 0x7FFF) << 16) | (buffer.getShort(offset + 2) & 0xFFFF); 161 | } 162 | return length; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceTableChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.nio.ByteBuffer; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | /** 29 | * Represents a resource table structure. Its sub-chunks contain: 30 | * 31 | * 36 | */ 37 | public final class ResourceTableChunk extends ChunkWithChunks { 38 | 39 | /** A string pool containing all string resource values in the entire resource table. */ 40 | private StringPoolChunk stringPool; 41 | 42 | /** The packages contained in this resource table. */ 43 | private final Map packages = new HashMap<>(); 44 | 45 | protected ResourceTableChunk(ByteBuffer buffer, @Nullable Chunk parent) { 46 | super(buffer, parent); 47 | // packageCount. We ignore this, because we already know how many chunks we have. 48 | Preconditions.checkState(buffer.getInt() >= 1, "ResourceTableChunk package count was < 1."); 49 | } 50 | 51 | @Override 52 | protected void init(ByteBuffer buffer) { 53 | super.init(buffer); 54 | packages.clear(); 55 | for (Chunk chunk : getChunks().values()) { 56 | if (chunk instanceof PackageChunk) { 57 | PackageChunk packageChunk = (PackageChunk) chunk; 58 | packages.put(packageChunk.getPackageName(), packageChunk); 59 | } else if (chunk instanceof StringPoolChunk) { 60 | stringPool = (StringPoolChunk) chunk; 61 | } 62 | } 63 | Preconditions.checkNotNull(stringPool, "ResourceTableChunk must have a string pool."); 64 | } 65 | 66 | /** Returns the string pool containing all string resource values in the resource table. */ 67 | public StringPoolChunk getStringPool() { 68 | return stringPool; 69 | } 70 | 71 | /** Returns the package with the given {@code packageName}. Else, returns null. */ 72 | @Nullable 73 | public PackageChunk getPackage(String packageName) { 74 | return packages.get(packageName); 75 | } 76 | 77 | /** Returns the packages contained in this resource table. */ 78 | public Collection getPackages() { 79 | return Collections.unmodifiableCollection(packages.values()); 80 | } 81 | 82 | @Override 83 | protected Type getType() { 84 | return Chunk.Type.TABLE; 85 | } 86 | 87 | @Override 88 | protected void writeHeader(ByteBuffer output) { 89 | super.writeHeader(output); 90 | output.putInt(packages.size()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/ResourceValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.base.Preconditions; 21 | import com.google.common.collect.ImmutableMap; 22 | import com.google.common.collect.ImmutableMap.Builder; 23 | import com.google.common.primitives.UnsignedBytes; 24 | 25 | import java.nio.ByteBuffer; 26 | import java.nio.ByteOrder; 27 | import java.util.Map; 28 | 29 | /** Represents a single typed resource value. */ 30 | @AutoValue 31 | public abstract class ResourceValue implements SerializableResource { 32 | 33 | /** Resource type codes. */ 34 | public enum Type { 35 | /** {@code data} is either 0 (undefined) or 1 (empty). */ 36 | NULL(0x00), 37 | /** {@code data} holds a {@link ResourceTableChunk} entry reference. */ 38 | REFERENCE(0x01), 39 | /** {@code data} holds an attribute resource identifier. */ 40 | ATTRIBUTE(0x02), 41 | /** {@code data} holds an index into the containing resource table's string pool. */ 42 | STRING(0x03), 43 | /** {@code data} holds a single-precision floating point number. */ 44 | FLOAT(0x04), 45 | /** {@code data} holds a complex number encoding a dimension value, such as "100in". */ 46 | DIMENSION(0x05), 47 | /** {@code data} holds a complex number encoding a fraction of a container. */ 48 | FRACTION(0x06), 49 | /** {@code data} holds a dynamic {@link ResourceTableChunk} entry reference. */ 50 | DYNAMIC_REFERENCE(0x07), 51 | /** {@code data} is a raw integer value of the form n..n. */ 52 | INT_DEC(0x10), 53 | /** {@code data} is a raw integer value of the form 0xn..n. */ 54 | INT_HEX(0x11), 55 | /** {@code data} is either 0 (false) or 1 (true). */ 56 | INT_BOOLEAN(0x12), 57 | /** {@code data} is a raw integer value of the form #aarrggbb. */ 58 | INT_COLOR_ARGB8(0x1c), 59 | /** {@code data} is a raw integer value of the form #rrggbb. */ 60 | INT_COLOR_RGB8(0x1d), 61 | /** {@code data} is a raw integer value of the form #argb. */ 62 | INT_COLOR_ARGB4(0x1e), 63 | /** {@code data} is a raw integer value of the form #rgb. */ 64 | INT_COLOR_RGB4(0x1f); 65 | 66 | private final byte code; 67 | 68 | private static final Map FROM_BYTE; 69 | 70 | static { 71 | Builder builder = ImmutableMap.builder(); 72 | for (Type type : values()) { 73 | builder.put(type.code(), type); 74 | } 75 | FROM_BYTE = builder.build(); 76 | } 77 | 78 | Type(int code) { 79 | this.code = UnsignedBytes.checkedCast(code); 80 | } 81 | 82 | public byte code() { 83 | return code; 84 | } 85 | 86 | public static Type fromCode(byte code) { 87 | return Preconditions.checkNotNull(FROM_BYTE.get(code), "Unknown resource type: %s", code); 88 | } 89 | } 90 | 91 | /** The serialized size in bytes of a {@link ResourceValue}. */ 92 | public static final int SIZE = 8; 93 | 94 | /** The length in bytes of this value. */ 95 | public abstract int size(); 96 | 97 | /** The raw data type of this value. */ 98 | public abstract Type type(); 99 | 100 | /** The actual 4-byte value; interpretation of the value depends on {@code dataType}. */ 101 | public abstract int data(); 102 | 103 | public static ResourceValue create(ByteBuffer buffer) { 104 | int size = (buffer.getShort() & 0xFFFF); 105 | buffer.get(); // Unused 106 | Type type = Type.fromCode(buffer.get()); 107 | int data = buffer.getInt(); 108 | return new AutoValue_ResourceValue(size, type, data); 109 | } 110 | 111 | @Override 112 | public byte[] toByteArray() { 113 | return toByteArray(false); 114 | } 115 | 116 | @Override 117 | public byte[] toByteArray(boolean shrink) { 118 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 119 | buffer.putShort((short) size()); 120 | buffer.put((byte) 0); // Unused 121 | buffer.put(type().code()); 122 | buffer.putInt(data()); 123 | return buffer.array(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/SerializableResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import java.io.IOException; 20 | 21 | /** 22 | * A resource, typically a @{link Chunk}, that can be converted to an array of bytes. 23 | */ 24 | public interface SerializableResource { 25 | 26 | /** 27 | * Converts this resource into an array of bytes representation. 28 | * @return An array of bytes representing this resource. 29 | * @throws IOException 30 | */ 31 | byte[] toByteArray() throws IOException; 32 | 33 | /** 34 | * Converts this resource into an array of bytes representation. 35 | * @param shrink True if, when converting to a byte array, this resource can modify the returned 36 | * bytes in an effort to reduce the size. 37 | * @return An array of bytes representing this resource. 38 | * @throws IOException 39 | */ 40 | byte[] toByteArray(boolean shrink) throws IOException; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/StringPoolChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.collect.ImmutableList; 21 | import com.google.common.collect.ImmutableList.Builder; 22 | import com.google.common.io.LittleEndianDataOutputStream; 23 | import org.jetbrains.annotations.Nullable; 24 | 25 | import java.io.ByteArrayOutputStream; 26 | import java.io.DataOutput; 27 | import java.io.IOException; 28 | import java.nio.ByteBuffer; 29 | import java.nio.ByteOrder; 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | /** Represents a string pool structure. */ 36 | public final class StringPoolChunk extends Chunk { 37 | 38 | // These are the defined flags for the "flags" field of ResourceStringPoolHeader 39 | private static final int SORTED_FLAG = 1 << 0; 40 | private static final int UTF8_FLAG = 1 << 8; 41 | 42 | /** The offset from the start of the header that the stylesStart field is at. */ 43 | private static final int STYLE_START_OFFSET = 24; 44 | 45 | /** Flags. */ 46 | private final int flags; 47 | 48 | /** Index from header of the string data. */ 49 | private final int stringsStart; 50 | 51 | /** Index from header of the style data. */ 52 | private final int stylesStart; 53 | 54 | /** 55 | * Number of strings in the original buffer. This is not necessarily the number of strings in 56 | * {@code strings}. 57 | */ 58 | private final int stringCount; 59 | 60 | /** 61 | * Number of styles in the original buffer. This is not necessarily the number of styles in 62 | * {@code styles}. 63 | */ 64 | private final int styleCount; 65 | 66 | /** 67 | * The strings ordered as they appear in the arsc file. e.g. strings.get(1234) gets the 1235th 68 | * string in the arsc file. 69 | */ 70 | private final List strings = new ArrayList<>(); 71 | 72 | /** 73 | * These styles have a 1:1 relationship with the strings. For example, styles.get(3) refers to 74 | * the string at location strings.get(3). There are never more styles than strings (though there 75 | * may be less). Inside of that are all of the styles referenced by that string. 76 | */ 77 | private final List styles = new ArrayList<>(); 78 | 79 | /** 80 | * True if the original {@link StringPoolChunk} shows signs of being deduped. Specifically, this 81 | * is set to true if there exists a string whose offset is <= the previous offset. This is used to 82 | * preserve the deduping of strings for pools that have been deduped. 83 | */ 84 | private boolean isOriginalDeduped = false; 85 | 86 | protected StringPoolChunk(ByteBuffer buffer, @Nullable Chunk parent) { 87 | super(buffer, parent); 88 | stringCount = buffer.getInt(); 89 | styleCount = buffer.getInt(); 90 | flags = buffer.getInt(); 91 | stringsStart = buffer.getInt(); 92 | stylesStart = buffer.getInt(); 93 | } 94 | 95 | @Override 96 | protected void init(ByteBuffer buffer) { 97 | super.init(buffer); 98 | strings.addAll(readStrings(buffer, offset + stringsStart, stringCount)); 99 | styles.addAll(readStyles(buffer, offset + stylesStart, styleCount)); 100 | } 101 | 102 | /** 103 | * Returns the 0-based index of the first occurrence of the given string, or -1 if the string is 104 | * not in the pool. This runs in O(n) time. 105 | * 106 | * @param string The string to check the pool for. 107 | * @return Index of the string, or -1 if not found. 108 | */ 109 | public int indexOf(String string) { 110 | return strings.indexOf(string); 111 | } 112 | 113 | /** 114 | * Returns a string at the given (0-based) index. 115 | * 116 | * @param index The (0-based) index of the string to return. 117 | * @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()). 118 | */ 119 | public String getString(int index) { 120 | return strings.get(index); 121 | } 122 | 123 | /** Returns the number of strings in this pool. */ 124 | public int getStringCount() { 125 | return strings.size(); 126 | } 127 | 128 | /** 129 | * Returns a style at the given (0-based) index. 130 | * 131 | * @param index The (0-based) index of the style to return. 132 | * @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()). 133 | */ 134 | public StringPoolStyle getStyle(int index) { 135 | return styles.get(index); 136 | } 137 | 138 | /** Returns the number of styles in this pool. */ 139 | public int getStyleCount() { 140 | return styles.size(); 141 | } 142 | 143 | /** Returns the type of strings in this pool. */ 144 | public ResourceString.Type getStringType() { 145 | return isUTF8() ? ResourceString.Type.UTF8 : ResourceString.Type.UTF16; 146 | } 147 | 148 | @Override 149 | protected Type getType() { 150 | return Chunk.Type.STRING_POOL; 151 | } 152 | 153 | /** Returns the number of bytes needed for offsets based on {@code strings} and {@code styles}. */ 154 | private int getOffsetSize() { 155 | return (strings.size() + styles.size()) * 4; 156 | } 157 | 158 | /** 159 | * True if this string pool contains strings in UTF-8 format. Otherwise, strings are in UTF-16. 160 | * 161 | * @return true if @{code strings} are in UTF-8; false if they're in UTF-16. 162 | */ 163 | public boolean isUTF8() { 164 | return (flags & UTF8_FLAG) != 0; 165 | } 166 | 167 | /** 168 | * True if this string pool contains already-sorted strings. 169 | * 170 | * @return true if @{code strings} are sorted. 171 | */ 172 | public boolean isSorted() { 173 | return (flags & SORTED_FLAG) != 0; 174 | } 175 | 176 | private List readStrings(ByteBuffer buffer, int offset, int count) { 177 | List result = new ArrayList<>(); 178 | int previousOffset = -1; 179 | // After the header, we now have an array of offsets for the strings in this pool. 180 | for (int i = 0; i < count; ++i) { 181 | int stringOffset = offset + buffer.getInt(); 182 | result.add(ResourceString.decodeString(buffer, stringOffset, getStringType())); 183 | if (stringOffset <= previousOffset) { 184 | isOriginalDeduped = true; 185 | } 186 | previousOffset = stringOffset; 187 | } 188 | return result; 189 | } 190 | 191 | private List readStyles(ByteBuffer buffer, int offset, int count) { 192 | List result = new ArrayList<>(); 193 | // After the array of offsets for the strings in the pool, we have an offset for the styles 194 | // in this pool. 195 | for (int i = 0; i < count; ++i) { 196 | int styleOffset = offset + buffer.getInt(); 197 | result.add(StringPoolStyle.create(buffer, styleOffset, this)); 198 | } 199 | return result; 200 | } 201 | 202 | private int writeStrings(DataOutput payload, ByteBuffer offsets, boolean shrink) 203 | throws IOException { 204 | int stringOffset = 0; 205 | Map used = new HashMap<>(); // Keeps track of strings already written 206 | for (String string : strings) { 207 | // Dedupe everything except stylized strings, unless shrink is true (then dedupe everything) 208 | if (used.containsKey(string) && (shrink || isOriginalDeduped)) { 209 | Integer offset = used.get(string); 210 | offsets.putInt(offset == null ? 0 : offset); 211 | } else { 212 | byte[] encodedString = ResourceString.encodeString(string, getStringType()); 213 | payload.write(encodedString); 214 | used.put(string, stringOffset); 215 | offsets.putInt(stringOffset); 216 | stringOffset += encodedString.length; 217 | } 218 | } 219 | 220 | // ARSC files pad to a 4-byte boundary. We should do so too. 221 | stringOffset = writePad(payload, stringOffset); 222 | return stringOffset; 223 | } 224 | 225 | private int writeStyles(DataOutput payload, ByteBuffer offsets, boolean shrink) 226 | throws IOException { 227 | int styleOffset = 0; 228 | if (styles.size() > 0) { 229 | Map used = new HashMap<>(); // Keeps track of bytes already written 230 | for (StringPoolStyle style : styles) { 231 | if (!used.containsKey(style) || !shrink) { 232 | byte[] encodedStyle = style.toByteArray(shrink); 233 | payload.write(encodedStyle); 234 | used.put(style, styleOffset); 235 | offsets.putInt(styleOffset); 236 | styleOffset += encodedStyle.length; 237 | } else { // contains key and shrink is true 238 | Integer offset = used.get(style); 239 | offsets.putInt(offset == null ? 0 : offset); 240 | } 241 | } 242 | // The end of the spans are terminated with another sentinel value 243 | payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END); 244 | styleOffset += 4; 245 | // TODO(acornwall): There appears to be an extra SPAN_END here... why? 246 | payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END); 247 | styleOffset += 4; 248 | 249 | styleOffset = writePad(payload, styleOffset); 250 | } 251 | return styleOffset; 252 | } 253 | 254 | @Override 255 | protected void writeHeader(ByteBuffer output) { 256 | int stringsStart = getHeaderSize() + getOffsetSize(); 257 | output.putInt(strings.size()); 258 | output.putInt(styles.size()); 259 | output.putInt(flags); 260 | output.putInt(strings.isEmpty() ? 0 : stringsStart); 261 | output.putInt(0); // Placeholder. The styles starting offset cannot be computed at this point. 262 | } 263 | 264 | @Override 265 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 266 | throws IOException { 267 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 268 | int stringOffset = 0; 269 | ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize()); 270 | offsets.order(ByteOrder.LITTLE_ENDIAN); 271 | 272 | // Write to a temporary payload so we can rearrange this and put the offsets first 273 | try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) { 274 | stringOffset = writeStrings(payload, offsets, shrink); 275 | writeStyles(payload, offsets, shrink); 276 | } 277 | 278 | output.write(offsets.array()); 279 | output.write(baos.toByteArray()); 280 | if (!styles.isEmpty()) { 281 | header.putInt(STYLE_START_OFFSET, getHeaderSize() + getOffsetSize() + stringOffset); 282 | } 283 | } 284 | 285 | /** 286 | * Represents all of the styles for a particular string. The string is determined by its index 287 | * in {@link StringPoolChunk}. 288 | */ 289 | @AutoValue 290 | protected abstract static class StringPoolStyle implements SerializableResource { 291 | 292 | // Styles are a list of integers with 0xFFFFFFFF serving as a sentinel value. 293 | static final int RES_STRING_POOL_SPAN_END = 0xFFFFFFFF; 294 | 295 | public abstract List spans(); 296 | 297 | static StringPoolStyle create(ByteBuffer buffer, int offset, StringPoolChunk parent) { 298 | Builder spans = ImmutableList.builder(); 299 | int nameIndex = buffer.getInt(offset); 300 | while (nameIndex != RES_STRING_POOL_SPAN_END) { 301 | spans.add(StringPoolSpan.create(buffer, offset, parent)); 302 | offset += StringPoolSpan.SPAN_LENGTH; 303 | nameIndex = buffer.getInt(offset); 304 | } 305 | return new AutoValue_StringPoolChunk_StringPoolStyle(spans.build()); 306 | } 307 | 308 | @Override 309 | public byte[] toByteArray() throws IOException { 310 | return toByteArray(false); 311 | } 312 | 313 | @Override 314 | public byte[] toByteArray(boolean shrink) throws IOException { 315 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 316 | 317 | try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) { 318 | for (StringPoolSpan span : spans()) { 319 | byte[] encodedSpan = span.toByteArray(shrink); 320 | if (encodedSpan.length != StringPoolSpan.SPAN_LENGTH) { 321 | throw new IllegalStateException("Encountered a span of invalid length."); 322 | } 323 | payload.write(encodedSpan); 324 | } 325 | payload.writeInt(RES_STRING_POOL_SPAN_END); 326 | } 327 | 328 | return baos.toByteArray(); 329 | } 330 | 331 | /** 332 | * Returns a brief description of the contents of this style. The representation of this 333 | * information is subject to change, but below is a typical example: 334 | * 335 | *
"StringPoolStyle{spans=[StringPoolSpan{foo, start=0, stop=5}, ...]}"
336 | */ 337 | @Override 338 | public String toString() { 339 | return String.format("StringPoolStyle{spans=%s}", spans()); 340 | } 341 | } 342 | 343 | /** Represents a styled span associated with a specific string. */ 344 | @AutoValue 345 | protected abstract static class StringPoolSpan implements SerializableResource { 346 | 347 | static final int SPAN_LENGTH = 12; 348 | 349 | public abstract int nameIndex(); 350 | public abstract int start(); 351 | public abstract int stop(); 352 | public abstract StringPoolChunk parent(); 353 | 354 | static StringPoolSpan create(ByteBuffer buffer, int offset, StringPoolChunk parent) { 355 | int nameIndex = buffer.getInt(offset); 356 | int start = buffer.getInt(offset + 4); 357 | int stop = buffer.getInt(offset + 8); 358 | return new AutoValue_StringPoolChunk_StringPoolSpan(nameIndex, start, stop, parent); 359 | } 360 | 361 | @Override 362 | public final byte[] toByteArray() { 363 | return toByteArray(false); 364 | } 365 | 366 | @Override 367 | public final byte[] toByteArray(boolean shrink) { 368 | ByteBuffer buffer = ByteBuffer.allocate(SPAN_LENGTH).order(ByteOrder.LITTLE_ENDIAN); 369 | buffer.putInt(nameIndex()); 370 | buffer.putInt(start()); 371 | buffer.putInt(stop()); 372 | return buffer.array(); 373 | } 374 | 375 | /** 376 | * Returns a brief description of this span. The representation of this information is subject 377 | * to change, but below is a typical example: 378 | * 379 | *
"StringPoolSpan{foo, start=0, stop=5}"
380 | */ 381 | @Override 382 | public String toString() { 383 | return String.format("StringPoolSpan{%s, start=%d, stop=%d}", 384 | parent().getString(nameIndex()), start(), stop()); 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/TypeChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.base.Preconditions; 21 | import com.google.common.io.LittleEndianDataOutputStream; 22 | import com.google.common.primitives.UnsignedBytes; 23 | import org.jetbrains.annotations.Nullable; 24 | 25 | import java.io.ByteArrayOutputStream; 26 | import java.io.DataOutput; 27 | import java.io.IOException; 28 | import java.nio.ByteBuffer; 29 | import java.nio.ByteOrder; 30 | import java.util.Collections; 31 | import java.util.LinkedHashMap; 32 | import java.util.Map; 33 | import java.util.TreeMap; 34 | 35 | /** 36 | * Represents a type chunk, which contains the resource values for a specific resource type and 37 | * configuration in a {@link PackageChunk}. The resource values in this chunk correspond to 38 | * the array of type strings in the enclosing {@link PackageChunk}. 39 | * 40 | *

A {@link PackageChunk} can have multiple of these chunks for different 41 | * (configuration, resource type) combinations. 42 | */ 43 | public final class TypeChunk extends Chunk { 44 | 45 | /** The type identifier of the resource type this chunk is holding. */ 46 | private final int id; 47 | 48 | /** The number of resources of this type at creation time. */ 49 | private final int entryCount; 50 | 51 | /** The offset (from {@code offset}) in the original buffer where {@code entries} start. */ 52 | private final int entriesStart; 53 | 54 | /** The resource configuration that these resource entries correspond to. */ 55 | private ResourceConfiguration configuration; 56 | 57 | /** A sparse list of resource entries defined by this chunk. */ 58 | private final Map entries = new TreeMap<>(); 59 | 60 | protected TypeChunk(ByteBuffer buffer, @Nullable Chunk parent) { 61 | super(buffer, parent); 62 | id = UnsignedBytes.toInt(buffer.get()); 63 | buffer.position(buffer.position() + 3); // Skip 3 bytes for packing 64 | entryCount = buffer.getInt(); 65 | entriesStart = buffer.getInt(); 66 | configuration = ResourceConfiguration.create(buffer); 67 | } 68 | 69 | @Override 70 | protected void init(ByteBuffer buffer) { 71 | int offset = this.offset + entriesStart; 72 | for (int i = 0; i < entryCount; ++i) { 73 | Entry entry = Entry.create(buffer, offset, this); 74 | if (entry != null) { 75 | entries.put(i, entry); 76 | } 77 | } 78 | } 79 | 80 | /** Returns the (1-based) type id of the resource types that this {@link TypeChunk} is holding. */ 81 | public int getId() { 82 | return id; 83 | } 84 | 85 | /** Returns the name of the type this chunk represents (e.g. string, attr, id). */ 86 | public String getTypeName() { 87 | PackageChunk packageChunk = getPackageChunk(); 88 | Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass()); 89 | StringPoolChunk typePool = packageChunk.getTypeStringPool(); 90 | Preconditions.checkNotNull(typePool, "%s's parent package has no type pool.", getClass()); 91 | return typePool.getString(getId() - 1); // - 1 here to convert to 0-based index 92 | } 93 | 94 | /** Returns the resource configuration that these resource entries correspond to. */ 95 | public ResourceConfiguration getConfiguration() { 96 | return configuration; 97 | } 98 | 99 | /** 100 | * Sets the resource configuration that this chunk's entries correspond to. 101 | * 102 | * @param configuration The new configuration. 103 | */ 104 | public void setConfiguration(ResourceConfiguration configuration) { 105 | this.configuration = configuration; 106 | } 107 | 108 | /** Returns the total number of entries for this type + configuration, including null entries. */ 109 | public int getTotalEntryCount() { 110 | return entryCount; 111 | } 112 | 113 | /** Returns a sparse list of 0-based indices to resource entries defined by this chunk. */ 114 | public Map getEntries() { 115 | return Collections.unmodifiableMap(entries); 116 | } 117 | 118 | /** Returns true if this chunk contains an entry for {@code resourceId}. */ 119 | public boolean containsResource(ResourceIdentifier resourceId) { 120 | PackageChunk packageChunk = Preconditions.checkNotNull(getPackageChunk()); 121 | int packageId = packageChunk.getId(); 122 | int typeId = getId(); 123 | return resourceId.packageId() == packageId 124 | && resourceId.typeId() == typeId 125 | && entries.containsKey(resourceId.entryId()); 126 | } 127 | 128 | /** 129 | * Overrides the entries in this chunk at the given index:entry pairs in {@code entries}. 130 | * For example, if the current list of entries is {0: foo, 1: bar, 2: baz}, and {@code entries} 131 | * is {1: qux, 3: quux}, then the entries will be changed to {0: foo, 1: qux, 2: baz}. If an entry 132 | * has an index that does not exist in the dense entry list, then it is considered a no-op for 133 | * that single entry. 134 | * 135 | * @param entries A sparse list containing index:entry pairs to override. 136 | */ 137 | public void overrideEntries(Map entries) { 138 | for (Map.Entry entry : entries.entrySet()) { 139 | int index = entry.getKey() != null ? entry.getKey() : -1; 140 | overrideEntry(index, entry.getValue()); 141 | } 142 | } 143 | 144 | /** 145 | * Overrides an entry at the given index. Passing null for the {@code entry} will remove that 146 | * entry from {@code entries}. Indices < 0 or >= {@link #getTotalEntryCount()} are a no-op. 147 | * 148 | * @param index The 0-based index for the entry to override. 149 | * @param entry The entry to override, or null if the entry should be removed at this location. 150 | */ 151 | public void overrideEntry(int index, @Nullable Entry entry) { 152 | if (index >= 0 && index < entryCount) { 153 | if (entry != null) { 154 | entries.put(index, entry); 155 | } else { 156 | entries.remove(index); 157 | } 158 | } 159 | } 160 | 161 | protected String getString(int index) { 162 | ResourceTableChunk resourceTable = getResourceTableChunk(); 163 | Preconditions.checkNotNull(resourceTable, "%s has no resource table.", getClass()); 164 | return resourceTable.getStringPool().getString(index); 165 | } 166 | 167 | protected String getKeyName(int index) { 168 | PackageChunk packageChunk = getPackageChunk(); 169 | Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass()); 170 | StringPoolChunk keyPool = packageChunk.getKeyStringPool(); 171 | Preconditions.checkNotNull(keyPool, "%s's parent package has no key pool.", getClass()); 172 | return keyPool.getString(index); 173 | } 174 | 175 | @Nullable 176 | private ResourceTableChunk getResourceTableChunk() { 177 | Chunk chunk = getParent(); 178 | while (chunk != null && !(chunk instanceof ResourceTableChunk)) { 179 | chunk = chunk.getParent(); 180 | } 181 | return chunk != null && chunk instanceof ResourceTableChunk ? (ResourceTableChunk) chunk : null; 182 | } 183 | 184 | /** Returns the package enclosing this chunk, if any. Else, returns null. */ 185 | @Nullable 186 | public PackageChunk getPackageChunk() { 187 | Chunk chunk = getParent(); 188 | while (chunk != null && !(chunk instanceof PackageChunk)) { 189 | chunk = chunk.getParent(); 190 | } 191 | return chunk != null && chunk instanceof PackageChunk ? (PackageChunk) chunk : null; 192 | } 193 | 194 | @Override 195 | protected Type getType() { 196 | return Chunk.Type.TABLE_TYPE; 197 | } 198 | 199 | /** Returns the number of bytes needed for offsets based on {@code entries}. */ 200 | private int getOffsetSize() { 201 | return entryCount * 4; 202 | } 203 | 204 | private int writeEntries(DataOutput payload, ByteBuffer offsets, boolean shrink) 205 | throws IOException { 206 | int entryOffset = 0; 207 | for (int i = 0; i < entryCount; ++i) { 208 | Entry entry = entries.get(i); 209 | if (entry == null) { 210 | offsets.putInt(Entry.NO_ENTRY); 211 | } else { 212 | byte[] encodedEntry = entry.toByteArray(shrink); 213 | payload.write(encodedEntry); 214 | offsets.putInt(entryOffset); 215 | entryOffset += encodedEntry.length; 216 | } 217 | } 218 | entryOffset = writePad(payload, entryOffset); 219 | return entryOffset; 220 | } 221 | 222 | @Override 223 | protected void writeHeader(ByteBuffer output) { 224 | int entriesStart = getHeaderSize() + getOffsetSize(); 225 | output.putInt(id); // Write an unsigned byte with 3 bytes padding 226 | output.putInt(entryCount); 227 | output.putInt(entriesStart); 228 | output.put(configuration.toByteArray(false)); 229 | } 230 | 231 | @Override 232 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 233 | throws IOException { 234 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 235 | ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize()).order(ByteOrder.LITTLE_ENDIAN); 236 | try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) { 237 | writeEntries(payload, offsets, shrink); 238 | } 239 | output.write(offsets.array()); 240 | output.write(baos.toByteArray()); 241 | } 242 | 243 | /** An {@link Entry} in a {@link TypeChunk}. Contains one or more {@link ResourceValue}. */ 244 | @AutoValue 245 | public abstract static class Entry implements SerializableResource { 246 | 247 | /** An entry offset that indicates that a given resource is not present. */ 248 | public static final int NO_ENTRY = 0xFFFFFFFF; 249 | 250 | /** Set if this is a complex resource. Otherwise, it's a simple resource. */ 251 | private static final int FLAG_COMPLEX = 0x0001; 252 | 253 | /** Size of a single resource id + value mapping entry. */ 254 | private static final int MAPPING_SIZE = 4 + ResourceValue.SIZE; 255 | 256 | /** Number of bytes in the header of the {@link Entry}. */ 257 | public abstract int headerSize(); 258 | 259 | /** Resource entry flags. */ 260 | public abstract int flags(); 261 | 262 | /** Index into {@link PackageChunk#getKeyStringPool} identifying this entry. */ 263 | public abstract int keyIndex(); 264 | 265 | /** The value of this resource entry, if this is not a complex entry. Else, null. */ 266 | @Nullable 267 | public abstract ResourceValue value(); 268 | 269 | /** The extra values in this resource entry if this {@link #isComplex}. */ 270 | public abstract Map values(); 271 | 272 | /** 273 | * Entry into {@link PackageChunk} that is the parent {@link Entry} to this entry. 274 | * This value only makes sense when this is complex ({@link #isComplex} returns true). 275 | */ 276 | public abstract int parentEntry(); 277 | 278 | /** The {@link TypeChunk} that this resource entry belongs to. */ 279 | public abstract TypeChunk parent(); 280 | 281 | /** Returns the name of the type this chunk represents (e.g. string, attr, id). */ 282 | public final String typeName() { 283 | return parent().getTypeName(); 284 | } 285 | 286 | /** The total number of bytes that this {@link Entry} takes up. */ 287 | public final int size() { 288 | return headerSize() + (isComplex() ? values().size() * MAPPING_SIZE : ResourceValue.SIZE); 289 | } 290 | 291 | /** Returns the key name identifying this resource entry. */ 292 | public final String key() { 293 | return parent().getKeyName(keyIndex()); 294 | } 295 | 296 | /** Returns true if this is a complex resource. */ 297 | public final boolean isComplex() { 298 | return (flags() & FLAG_COMPLEX) != 0; 299 | } 300 | 301 | /** 302 | * Creates a new {@link Entry} whose contents start at the 0-based position in 303 | * {@code buffer} given by a 4-byte value read from {@code buffer} and then added to 304 | * {@code baseOffset}. If the value read from {@code buffer} is equal to {@link #NO_ENTRY}, then 305 | * null is returned as there is no resource at that position. 306 | * 307 | *

Otherwise, this position is parsed and returned as an {@link Entry}. 308 | * 309 | * @param buffer A buffer positioned at an offset to an {@link Entry}. 310 | * @param baseOffset Offset that must be added to the value at {@code buffer}'s position. 311 | * @param parent The {@link TypeChunk} that this resource entry belongs to. 312 | * @return New {@link Entry} or null if there is no resource at this location. 313 | */ 314 | @Nullable 315 | public static Entry create(ByteBuffer buffer, int baseOffset, TypeChunk parent) { 316 | int offset = buffer.getInt(); 317 | if (offset == NO_ENTRY) { 318 | return null; 319 | } 320 | int position = buffer.position(); 321 | buffer.position(baseOffset + offset); // Set buffer position to resource entry start 322 | Entry result = newInstance(buffer, parent); 323 | buffer.position(position); // Restore buffer position 324 | return result; 325 | } 326 | 327 | @Nullable 328 | private static Entry newInstance(ByteBuffer buffer, TypeChunk parent) { 329 | int headerSize = buffer.getShort() & 0xFFFF; 330 | int flags = buffer.getShort() & 0xFFFF; 331 | int keyIndex = buffer.getInt(); 332 | ResourceValue value = null; 333 | Map values = new LinkedHashMap<>(); 334 | int parentEntry = 0; 335 | if ((flags & FLAG_COMPLEX) != 0) { 336 | parentEntry = buffer.getInt(); 337 | int valueCount = buffer.getInt(); 338 | for (int i = 0; i < valueCount; ++i) { 339 | values.put(buffer.getInt(), ResourceValue.create(buffer)); 340 | } 341 | } else { 342 | value = ResourceValue.create(buffer); 343 | } 344 | return new AutoValue_TypeChunk_Entry( 345 | headerSize, flags, keyIndex, value, values, parentEntry, parent); 346 | } 347 | 348 | @Override 349 | public final byte[] toByteArray() { 350 | return toByteArray(false); 351 | } 352 | 353 | @Override 354 | public final byte[] toByteArray(boolean shrink) { 355 | ByteBuffer buffer = ByteBuffer.allocate(size()); 356 | buffer.order(ByteOrder.LITTLE_ENDIAN); 357 | buffer.putShort((short) headerSize()); 358 | buffer.putShort((short) flags()); 359 | buffer.putInt(keyIndex()); 360 | if (isComplex()) { 361 | buffer.putInt(parentEntry()); 362 | buffer.putInt(values().size()); 363 | for (Map.Entry entry : values().entrySet()) { 364 | buffer.putInt(entry.getKey()); 365 | buffer.put(entry.getValue().toByteArray(shrink)); 366 | } 367 | } else { 368 | ResourceValue value = value(); 369 | Preconditions.checkNotNull(value, "A non-complex TypeChunk entry must have a value."); 370 | buffer.put(value.toByteArray()); 371 | } 372 | return buffer.array(); 373 | } 374 | 375 | @Override 376 | public final String toString() { 377 | return String.format("Entry{key=%s}", key()); 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/TypeSpecChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.primitives.UnsignedBytes; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.io.DataOutput; 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | 26 | /** A chunk that contains a collection of resource entries for a particular resource data type. */ 27 | public final class TypeSpecChunk extends Chunk { 28 | 29 | /** The id of the resource type that this type spec refers to. */ 30 | private final int id; 31 | 32 | /** Resource configuration masks. */ 33 | private final int[] resources; 34 | 35 | protected TypeSpecChunk(ByteBuffer buffer, @Nullable Chunk parent) { 36 | super(buffer, parent); 37 | id = UnsignedBytes.toInt(buffer.get()); 38 | buffer.position(buffer.position() + 3); // Skip 3 bytes for packing 39 | int resourceCount = buffer.getInt(); 40 | resources = new int[resourceCount]; 41 | 42 | for (int i = 0; i < resourceCount; ++i) { 43 | resources[i] = buffer.getInt(); 44 | } 45 | } 46 | 47 | /** 48 | * Returns the (1-based) type id of the resources that this {@link TypeSpecChunk} has 49 | * configuration masks for. 50 | */ 51 | public int getId() { 52 | return id; 53 | } 54 | 55 | /** Returns the number of resource entries that this chunk has configuration masks for. */ 56 | public int getResourceCount() { 57 | return resources.length; 58 | } 59 | 60 | @Override 61 | protected Type getType() { 62 | return Chunk.Type.TABLE_TYPE_SPEC; 63 | } 64 | 65 | @Override 66 | protected void writeHeader(ByteBuffer output) { 67 | super.writeHeader(output); 68 | // id is an unsigned byte in the range [0-255]. It is guaranteed to be non-negative. 69 | // Because our output is in little-endian, we are making use of the 4 byte packing here 70 | output.putInt(id); 71 | output.putInt(resources.length); 72 | } 73 | 74 | @Override 75 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 76 | throws IOException { 77 | for (int resource : resources) { 78 | output.writeInt(resource); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/UnknownChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** 26 | * A chunk whose contents are unknown. This is a placeholder until we add a proper chunk for the 27 | * unknown type. 28 | */ 29 | public final class UnknownChunk extends Chunk { 30 | 31 | private final Type type; 32 | 33 | private final byte[] header; 34 | 35 | private final byte[] payload; 36 | 37 | protected UnknownChunk(ByteBuffer buffer, @Nullable Chunk parent) { 38 | super(buffer, parent); 39 | 40 | type = Type.fromCode(buffer.getShort(offset)); 41 | header = new byte[headerSize - METADATA_SIZE]; 42 | payload = new byte[chunkSize - headerSize]; 43 | buffer.get(header); 44 | buffer.get(payload); 45 | } 46 | 47 | @Override 48 | protected void writeHeader(ByteBuffer output) { 49 | output.put(header); 50 | } 51 | 52 | @Override 53 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 54 | throws IOException { 55 | output.write(payload); 56 | } 57 | 58 | @Override 59 | protected Type getType() { 60 | return type; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlAttribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.auto.value.AutoValue; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.nio.ByteOrder; 23 | 24 | /** Represents an XML attribute and value. */ 25 | @AutoValue 26 | public abstract class XmlAttribute implements SerializableResource { 27 | 28 | /** The serialized size in bytes of an {@link XmlAttribute}. */ 29 | public static final int SIZE = 12 + ResourceValue.SIZE; 30 | 31 | /** A string reference to the namespace URI, or -1 if not present. */ 32 | public abstract int namespaceIndex(); 33 | 34 | /** A string reference to the attribute name. */ 35 | public abstract int nameIndex(); 36 | 37 | /** A string reference to a string containing the character value. */ 38 | public abstract int rawValueIndex(); 39 | 40 | /** A {@link ResourceValue} instance containing the parsed value. */ 41 | public abstract ResourceValue typedValue(); 42 | 43 | /** The parent of this XML attribute; used for dereferencing the namespace and name. */ 44 | public abstract XmlNodeChunk parent(); 45 | 46 | /** The namespace URI, or the empty string if not present. */ 47 | public final String namespace() { 48 | return getString(namespaceIndex()); 49 | } 50 | 51 | /** The attribute name, or the empty string if not present. */ 52 | public final String name() { 53 | return getString(nameIndex()); 54 | } 55 | 56 | /** The raw character value. */ 57 | public final String rawValue() { 58 | return getString(rawValueIndex()); 59 | } 60 | 61 | /** 62 | * Creates a new {@link XmlAttribute} based on the bytes at the current {@code buffer} position. 63 | * 64 | * @param buffer A buffer whose position is at the start of a {@link XmlAttribute}. 65 | * @param parent The parent chunk that contains this attribute; used for string lookups. 66 | */ 67 | public static XmlAttribute create(ByteBuffer buffer, XmlNodeChunk parent) { 68 | int namespace = buffer.getInt(); 69 | int name = buffer.getInt(); 70 | int rawValue = buffer.getInt(); 71 | ResourceValue typedValue = ResourceValue.create(buffer); 72 | return new AutoValue_XmlAttribute(namespace, name, rawValue, typedValue, parent); 73 | } 74 | 75 | private String getString(int index) { 76 | return parent().getString(index); 77 | } 78 | 79 | @Override 80 | public byte[] toByteArray() { 81 | return toByteArray(false); 82 | } 83 | 84 | @Override 85 | public byte[] toByteArray(boolean shrink) { 86 | ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN); 87 | buffer.putInt(namespaceIndex()); 88 | buffer.putInt(nameIndex()); 89 | buffer.putInt(rawValueIndex()); 90 | buffer.put(typedValue().toByteArray(shrink)); 91 | return buffer.array(); 92 | } 93 | 94 | /** 95 | * Returns a brief description of this XML attribute. The representation of this information is 96 | * subject to change, but below is a typical example: 97 | * 98 | *

"XmlAttribute{namespace=foo, name=bar, value=1234}"
99 | */ 100 | @Override 101 | public String toString() { 102 | return String.format("XmlAttribute{namespace=%s, name=%s, value=%s}", 103 | namespace(), name(), rawValue()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlCdataChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** Represents an XML cdata node. */ 26 | public final class XmlCdataChunk extends XmlNodeChunk { 27 | 28 | /** A string reference to a string containing the raw character data. */ 29 | private final int rawValue; 30 | 31 | /** A {@link ResourceValue} instance containing the parsed value. */ 32 | private final ResourceValue resourceValue; 33 | 34 | protected XmlCdataChunk(ByteBuffer buffer, @Nullable Chunk parent) { 35 | super(buffer, parent); 36 | rawValue = buffer.getInt(); 37 | resourceValue = ResourceValue.create(buffer); 38 | } 39 | 40 | /** Returns a string containing the raw character data of this chunk. */ 41 | public String getRawValue() { 42 | return getString(rawValue); 43 | } 44 | 45 | /** Returns a {@link ResourceValue} instance containing the parsed cdata value. */ 46 | public ResourceValue getResourceValue() { 47 | return resourceValue; 48 | } 49 | 50 | @Override 51 | protected Type getType() { 52 | return Chunk.Type.XML_CDATA; 53 | } 54 | 55 | @Override 56 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 57 | throws IOException { 58 | super.writePayload(output, header, shrink); 59 | output.writeInt(rawValue); 60 | output.write(resourceValue.toByteArray()); 61 | } 62 | 63 | /** 64 | * Returns a brief description of this XML node. The representation of this information is 65 | * subject to change, but below is a typical example: 66 | * 67 | *
"XmlCdataChunk{line=1234, comment=My awesome comment., value=1234}"
68 | */ 69 | @Override 70 | public String toString() { 71 | return String.format("XmlCdataChunk{line=%d, comment=%s, value=%s}", 72 | getLineNumber(), getComment(), getRawValue()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | /** 24 | * Represents an XML chunk structure. 25 | * 26 | *

An XML chunk can contain many nodes as well as a string pool which contains all of the strings 27 | * referenced by the nodes. 28 | */ 29 | public final class XmlChunk extends ChunkWithChunks { 30 | 31 | protected XmlChunk(ByteBuffer buffer, @Nullable Chunk parent) { 32 | super(buffer, parent); 33 | } 34 | 35 | @Override 36 | protected Type getType() { 37 | return Chunk.Type.XML; 38 | } 39 | 40 | /** Returns a string at the provided (0-based) index if the index exists in the string pool. */ 41 | public String getString(int index) { 42 | for (Chunk chunk : getChunks().values()) { 43 | if (chunk instanceof StringPoolChunk) { 44 | return ((StringPoolChunk) chunk).getString(index); 45 | } 46 | } 47 | throw new IllegalStateException("XmlChunk did not contain a string pool."); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlEndElementChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** Represents the end of an XML node. */ 26 | public final class XmlEndElementChunk extends XmlNodeChunk { 27 | 28 | /** A string reference to the namespace URI, or -1 if not present. */ 29 | private final int namespace; 30 | 31 | /** A string reference to the attribute name. */ 32 | private final int name; 33 | 34 | protected XmlEndElementChunk(ByteBuffer buffer, @Nullable Chunk parent) { 35 | super(buffer, parent); 36 | namespace = buffer.getInt(); 37 | name = buffer.getInt(); 38 | } 39 | 40 | /** Returns the namespace URI, or the empty string if no namespace is present. */ 41 | public String getNamespace() { 42 | return getString(namespace); 43 | } 44 | 45 | /** Returns the attribute name. */ 46 | public String getName() { 47 | return getString(name); 48 | } 49 | 50 | @Override 51 | protected Type getType() { 52 | return Chunk.Type.XML_END_ELEMENT; 53 | } 54 | 55 | @Override 56 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 57 | throws IOException { 58 | super.writePayload(output, header, shrink); 59 | output.writeInt(namespace); 60 | output.writeInt(name); 61 | } 62 | 63 | /** 64 | * Returns a brief description of this XML node. The representation of this information is 65 | * subject to change, but below is a typical example: 66 | * 67 | *

68 |    * "XmlEndElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar}"
69 |    * 
70 | */ 71 | @Override 72 | public String toString() { 73 | return String.format("XmlEndElementChunk{line=%d, comment=%s, namespace=%s, name=%s}", 74 | getLineNumber(), getComment(), getNamespace(), getName()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlNamespaceChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** Represents the start/end of a namespace in an XML document. */ 26 | public abstract class XmlNamespaceChunk extends XmlNodeChunk { 27 | 28 | /** A string reference to the namespace prefix. */ 29 | private final int prefix; 30 | 31 | /** A string reference to the namespace URI. */ 32 | private final int uri; 33 | 34 | protected XmlNamespaceChunk(ByteBuffer buffer, @Nullable Chunk parent) { 35 | super(buffer, parent); 36 | prefix = buffer.getInt(); 37 | uri = buffer.getInt(); 38 | } 39 | 40 | /** Returns the namespace prefix. */ 41 | public String getPrefix() { 42 | return getString(prefix); 43 | } 44 | 45 | /** Returns the namespace URI. */ 46 | public String getUri() { 47 | return getString(uri); 48 | } 49 | 50 | @Override 51 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 52 | throws IOException { 53 | super.writePayload(output, header, shrink); 54 | output.writeInt(prefix); 55 | output.writeInt(uri); 56 | } 57 | 58 | /** 59 | * Returns a brief description of this namespace chunk. The representation of this information is 60 | * subject to change, but below is a typical example: 61 | * 62 | *
63 |    * "XmlNamespaceChunk{line=1234, comment=My awesome comment., prefix=foo, uri=com.google.foo}"
64 |    * 
65 | */ 66 | @Override 67 | public String toString() { 68 | return String.format("XmlNamespaceChunk{line=%d, comment=%s, prefix=%s, uri=%s}", 69 | getLineNumber(), getComment(), getPrefix(), getUri()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlNamespaceEndChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | /** Represents the ending tag of a namespace in an XML document. */ 24 | public final class XmlNamespaceEndChunk extends XmlNamespaceChunk { 25 | 26 | protected XmlNamespaceEndChunk(ByteBuffer buffer, @Nullable Chunk parent) { 27 | super(buffer, parent); 28 | } 29 | 30 | @Override 31 | protected Type getType() { 32 | return Chunk.Type.XML_END_NAMESPACE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlNamespaceStartChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | /** Represents the starting tag of a namespace in an XML document. */ 24 | public final class XmlNamespaceStartChunk extends XmlNamespaceChunk { 25 | 26 | protected XmlNamespaceStartChunk(ByteBuffer buffer, @Nullable Chunk parent) { 27 | super(buffer, parent); 28 | } 29 | 30 | @Override 31 | protected Type getType() { 32 | return Chunk.Type.XML_START_NAMESPACE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlNodeChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | /** The common superclass for the various types of XML nodes. */ 24 | public abstract class XmlNodeChunk extends Chunk { 25 | 26 | /** The line number in the original source at which this node appeared. */ 27 | private final int lineNumber; 28 | 29 | /** A string reference of this node's comment. If this is -1, then there is no comment. */ 30 | private final int comment; 31 | 32 | protected XmlNodeChunk(ByteBuffer buffer, @Nullable Chunk parent) { 33 | super(buffer, parent); 34 | lineNumber = buffer.getInt(); 35 | comment = buffer.getInt(); 36 | } 37 | 38 | /** Returns true if this XML node contains a comment. Else, returns false. */ 39 | public boolean hasComment() { 40 | return comment != -1; 41 | } 42 | 43 | /** Returns the line number in the original source at which this node appeared. */ 44 | public int getLineNumber() { 45 | return lineNumber; 46 | } 47 | 48 | /** Returns the comment associated with this node, if any. Else, returns the empty string. */ 49 | public String getComment() { 50 | return getString(comment); 51 | } 52 | 53 | /** 54 | * An {@link XmlNodeChunk} does not know by itself what strings its indices reference. In order 55 | * to get the actual string, the first {@link XmlChunk} ancestor is found. The 56 | * {@link XmlChunk} ancestor should have a string pool which {@code index} references. 57 | * 58 | * @param index The index of the string. 59 | * @return String that the given {@code index} references, or empty string if {@code index} is -1. 60 | */ 61 | protected String getString(int index) { 62 | if (index == -1) { // Special case. Packed XML files use -1 for "no string entry" 63 | return ""; 64 | } 65 | Chunk parent = getParent(); 66 | while (parent != null) { 67 | if (parent instanceof XmlChunk) { 68 | return ((XmlChunk) parent).getString(index); 69 | } 70 | parent = parent.getParent(); 71 | } 72 | throw new IllegalStateException("XmlNodeChunk did not have an XmlChunk parent."); 73 | } 74 | 75 | /** 76 | * An {@link XmlNodeChunk} and anything that is itself an {@link XmlNodeChunk} has a header size 77 | * of 16. Anything else is, interestingly, considered to be a payload. For that reason, this 78 | * method is final. 79 | */ 80 | @Override 81 | protected final void writeHeader(ByteBuffer output) { 82 | super.writeHeader(output); 83 | output.putInt(lineNumber); 84 | output.putInt(comment); 85 | } 86 | 87 | /** 88 | * Returns a brief description of this XML node. The representation of this information is 89 | * subject to change, but below is a typical example: 90 | * 91 | *
"XmlNodeChunk{line=1234, comment=My awesome comment.}"
92 | */ 93 | @Override 94 | public String toString() { 95 | return String.format("XmlNodeChunk{line=%d, comment=%s}", getLineNumber(), getComment()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlResourceMapChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | /** 28 | * Represents an XML resource map chunk. 29 | * 30 | *

This chunk maps attribute ids to the resource ids of the attribute resource that defines the 31 | * attribute (e.g. type, enum values, etc.). 32 | */ 33 | public class XmlResourceMapChunk extends Chunk { 34 | 35 | /** The size of a resource reference for {@code resources} in bytes. */ 36 | private static final int RESOURCE_SIZE = 4; 37 | 38 | /** 39 | * Contains a mapping of attributeID to resourceID. For example, the attributeID 2 refers to the 40 | * resourceID returned by {@code resources.get(2)}. 41 | */ 42 | private final List resources = new ArrayList<>(); 43 | 44 | protected XmlResourceMapChunk(ByteBuffer buffer, @Nullable Chunk parent) { 45 | super(buffer, parent); 46 | } 47 | 48 | @Override 49 | protected void init(ByteBuffer buffer) { 50 | super.init(buffer); 51 | resources.addAll(enumerateResources(buffer)); 52 | } 53 | 54 | private List enumerateResources(ByteBuffer buffer) { 55 | int resourceCount = (getOriginalChunkSize() - getHeaderSize()) / RESOURCE_SIZE; 56 | List result = new ArrayList<>(resourceCount); 57 | int offset = this.offset + getHeaderSize(); 58 | buffer.mark(); 59 | buffer.position(offset); 60 | 61 | for (int i = 0; i < resourceCount; ++i) { 62 | result.add(buffer.getInt()); 63 | } 64 | 65 | buffer.reset(); 66 | return result; 67 | } 68 | 69 | /** Returns the resource ID that this {@code attributeId} maps to. */ 70 | public ResourceIdentifier getResourceId(int attributeId) { 71 | return ResourceIdentifier.create(resources.get(attributeId)); 72 | } 73 | 74 | @Override 75 | protected Type getType() { 76 | return Chunk.Type.XML_RESOURCE_MAP; 77 | } 78 | 79 | @Override 80 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 81 | throws IOException { 82 | super.writePayload(output, header, shrink); 83 | for (Integer resource : resources) { 84 | output.writeInt(resource); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/pink/madis/apk/arsc/XmlStartElementChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pink.madis.apk.arsc; 18 | 19 | import com.google.common.base.Preconditions; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.io.DataOutput; 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | /** Represents the beginning of an XML node. */ 30 | public final class XmlStartElementChunk extends XmlNodeChunk { 31 | 32 | /** A string reference to the namespace URI, or -1 if not present. */ 33 | private final int namespace; 34 | 35 | /** A string reference to the element name that this chunk represents. */ 36 | private final int name; 37 | 38 | /** The offset to the start of the attributes payload. */ 39 | private final int attributeStart; 40 | 41 | /** The number of attributes in the original buffer. */ 42 | private final int attributeCount; 43 | 44 | /** The (0-based) index of the id attribute, or -1 if not present. */ 45 | private final int idIndex; 46 | 47 | /** The (0-based) index of the class attribute, or -1 if not present. */ 48 | private final int classIndex; 49 | 50 | /** The (0-based) index of the style attribute, or -1 if not present. */ 51 | private final int styleIndex; 52 | 53 | /** The XML attributes associated with this element. */ 54 | private final List attributes = new ArrayList<>(); 55 | 56 | protected XmlStartElementChunk(ByteBuffer buffer, @Nullable Chunk parent) { 57 | super(buffer, parent); 58 | namespace = buffer.getInt(); 59 | name = buffer.getInt(); 60 | attributeStart = (buffer.getShort() & 0xFFFF); 61 | int attributeSize = (buffer.getShort() & 0xFFFF); 62 | Preconditions.checkState(attributeSize == XmlAttribute.SIZE, 63 | "attributeSize is wrong size. Got %s, want %s", attributeSize, XmlAttribute.SIZE); 64 | attributeCount = (buffer.getShort() & 0xFFFF); 65 | 66 | // The following indices are 1-based and need to be adjusted. 67 | idIndex = (buffer.getShort() & 0xFFFF) - 1; 68 | classIndex = (buffer.getShort() & 0xFFFF) - 1; 69 | styleIndex = (buffer.getShort() & 0xFFFF) - 1; 70 | } 71 | 72 | @Override 73 | protected void init(ByteBuffer buffer) { 74 | super.init(buffer); 75 | attributes.addAll(enumerateAttributes(buffer)); 76 | } 77 | 78 | private List enumerateAttributes(ByteBuffer buffer) { 79 | List result = new ArrayList<>(attributeCount); 80 | int offset = this.offset + getHeaderSize() + attributeStart; 81 | int endOffset = offset + XmlAttribute.SIZE * attributeCount; 82 | buffer.mark(); 83 | buffer.position(offset); 84 | 85 | while (offset < endOffset) { 86 | result.add(XmlAttribute.create(buffer, this)); 87 | offset += XmlAttribute.SIZE; 88 | } 89 | 90 | buffer.reset(); 91 | return result; 92 | } 93 | 94 | /** Returns the namespace URI, or the empty string if not present. */ 95 | public String getNamespace() { 96 | return getString(namespace); 97 | } 98 | 99 | /** Returns the element name that this chunk represents. */ 100 | public String getName() { 101 | return getString(name); 102 | } 103 | 104 | /** Returns an unmodifiable list of this XML element's attributes. */ 105 | public List getAttributes() { 106 | return Collections.unmodifiableList(attributes); 107 | } 108 | 109 | @Override 110 | protected Type getType() { 111 | return Chunk.Type.XML_START_ELEMENT; 112 | } 113 | 114 | @Override 115 | protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) 116 | throws IOException { 117 | super.writePayload(output, header, shrink); 118 | output.writeInt(namespace); 119 | output.writeInt(name); 120 | output.writeShort((short) XmlAttribute.SIZE); // attribute start 121 | output.writeShort((short) XmlAttribute.SIZE); 122 | output.writeShort((short) attributes.size()); 123 | output.writeShort((short) (idIndex + 1)); 124 | output.writeShort((short) (classIndex + 1)); 125 | output.writeShort((short) (styleIndex + 1)); 126 | for (XmlAttribute attribute : attributes) { 127 | output.write(attribute.toByteArray(shrink)); 128 | } 129 | } 130 | 131 | /** 132 | * Returns a brief description of this XML node. The representation of this information is 133 | * subject to change, but below is a typical example: 134 | * 135 | *

136 |    * "XmlStartElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar, ...}"
137 |    * 
138 | */ 139 | @Override 140 | public String toString() { 141 | return String.format( 142 | "XmlStartElementChunk{line=%d, comment=%s, namespace=%s, name=%s, attributes=%s}", 143 | getLineNumber(), getComment(), getNamespace(), getName(), attributes.toString()); 144 | } 145 | } 146 | --------------------------------------------------------------------------------