You've already forked android_translation_layer
mirror of
https://gitlab.com/android_translation_layer/android_translation_layer.git
synced 2025-10-27 11:48:10 -07:00
replace arsc_parser with ARSClib
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#### directory structure
|
||||
`src/arsc_parser/` - Java .arsc parser I found somewhere, with fixes (should eventually get replaced by C code)
|
||||
`src/ARSCLib/` - Java .arsc library used to parse binary xml resources in apks
|
||||
`doc/` - documentation
|
||||
`src/api-impl/` - Java code implementing the android APIs
|
||||
`src/api-impl-jni/` - C code implementing things which it doesn't make sense to do in Java (ideally this would be most things)
|
||||
|
||||
12
meson.build
12
meson.build
@@ -104,10 +104,10 @@ executable('android-translation-layer', [
|
||||
'-rdynamic'
|
||||
])
|
||||
|
||||
# hax_arsc_parser.dex (named as classes2.dex so it works inside a jar)
|
||||
subdir('src/arsc_parser')
|
||||
hax_arsc_parser_dex = custom_target('hax_arsc_parser.dex', build_by_default: true, input: [hax_arsc_parser_jar], output: ['classes2.dex'],
|
||||
command: ['dx', '--verbose', '--dex', '--output='+join_paths(builddir_base, 'classes2.dex'), hax_arsc_parser_jar.full_path()])
|
||||
# hax_arsc_lib.dex (named as classes2.dex so it works inside a jar)
|
||||
subdir('src/ARSCLib')
|
||||
hax_arsc_lib_dex = custom_target('hax_arsc_lib.dex', build_by_default: true, input: [hax_arsc_lib_jar], output: ['classes2.dex'],
|
||||
command: ['dx', '--verbose', '--dex', '--min-sdk-version', '26', '--output='+join_paths(builddir_base, 'classes2.dex'), hax_arsc_lib_jar.full_path()])
|
||||
|
||||
# hax.dex (named as classes.dex so it works inside a jar)
|
||||
subdir('src/api-impl')
|
||||
@@ -115,9 +115,9 @@ hax_dex = custom_target('hax.dex', build_by_default: true, input: [hax_jar], out
|
||||
command: ['dx', '--verbose', '--dex', '--output='+join_paths(builddir_base, 'classes.dex'), hax_jar.full_path()])
|
||||
|
||||
# api-impl.jar
|
||||
custom_target('api-impl.jar', build_by_default: true, input: [hax_dex, hax_arsc_parser_dex], output: ['api-impl.jar'],
|
||||
custom_target('api-impl.jar', build_by_default: true, input: [hax_dex, hax_arsc_lib_dex], output: ['api-impl.jar'],
|
||||
install: true,
|
||||
install_dir : get_option('libdir') / 'java/dex/android_translation_layer',
|
||||
command: ['jar', '-cvf', join_paths(builddir_base, 'api-impl.jar'), '-C', builddir_base, hax_dex, '-C', builddir_base, hax_arsc_parser_dex])
|
||||
command: ['jar', '-cvf', join_paths(builddir_base, 'api-impl.jar'), '-C', builddir_base, hax_dex, '-C', builddir_base, hax_arsc_lib_dex])
|
||||
|
||||
|
||||
|
||||
201
src/ARSCLib/LICENSE
Normal file
201
src/ARSCLib/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2022 github.com/REAndroid
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
6
src/ARSCLib/README.md
Normal file
6
src/ARSCLib/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
taken from https://github.com/REAndroid/ARSCLib
|
||||
licensed under Apache 2.0
|
||||
|
||||
# ARSCLib
|
||||
A tool for decoding Android resources.arsc file using java, for decoding binary XML resources from apk files.
|
||||
|
||||
26
src/ARSCLib/android/content/res/XmlResourceParser.java
Normal file
26
src/ARSCLib/android/content/res/XmlResourceParser.java
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.content.res;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
|
||||
String getAttributeNamespace (int index);
|
||||
public void close();
|
||||
}
|
||||
|
||||
45
src/ARSCLib/android/util/AttributeSet.java
Normal file
45
src/ARSCLib/android/util/AttributeSet.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.util;
|
||||
|
||||
public interface AttributeSet {
|
||||
public int getAttributeCount();
|
||||
default String getAttributeNamespace (int index) {
|
||||
return null;
|
||||
}
|
||||
public String getAttributeName(int index);
|
||||
public String getAttributeValue(int index);
|
||||
public String getAttributeValue(String namespace, String name);
|
||||
public String getPositionDescription();
|
||||
public int getAttributeNameResource(int index);
|
||||
public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue);
|
||||
public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue);
|
||||
public int getAttributeResourceValue(String namespace, String attribute, int defaultValue);
|
||||
public int getAttributeIntValue(String namespace, String attribute, int defaultValue);
|
||||
public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue);
|
||||
public float getAttributeFloatValue(String namespace, String attribute, float defaultValue);
|
||||
public int getAttributeListValue(int index, String[] options, int defaultValue);
|
||||
public boolean getAttributeBooleanValue(int index, boolean defaultValue);
|
||||
public int getAttributeResourceValue(int index, int defaultValue);
|
||||
public int getAttributeIntValue(int index, int defaultValue);
|
||||
public int getAttributeUnsignedIntValue(int index, int defaultValue);
|
||||
public float getAttributeFloatValue(int index, float defaultValue);
|
||||
public String getIdAttribute();
|
||||
public String getClassAttribute();
|
||||
public int getIdAttributeResourceValue(int defaultValue);
|
||||
public int getStyleAttribute();
|
||||
}
|
||||
2137
src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java
Normal file
2137
src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java
Normal file
File diff suppressed because it is too large
Load Diff
565
src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java
Normal file
565
src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java
Normal file
File diff suppressed because it is too large
Load Diff
65
src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java
Normal file
65
src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.org.kxml2.io;
|
||||
|
||||
// Taken from libcore.internal.StringPool
|
||||
|
||||
class LibCoreStringPool {
|
||||
|
||||
private final String[] pool = new String[512];
|
||||
|
||||
public LibCoreStringPool() {
|
||||
}
|
||||
|
||||
private static boolean contentEquals(String s, char[] chars, int start, int length) {
|
||||
if (s.length() != length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (chars[start + i] != s.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string equal to {@code new String(array, start, length)}.
|
||||
*/
|
||||
public String get(char[] array, int start, int length) {
|
||||
// Compute an arbitrary hash of the content
|
||||
int hashCode = 0;
|
||||
for (int i = start; i < start + length; i++) {
|
||||
hashCode = (hashCode * 31) + array[i];
|
||||
}
|
||||
|
||||
// Pick a bucket using Doug Lea's supplemental secondaryHash function (from HashMap)
|
||||
hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
|
||||
hashCode ^= (hashCode >>> 7) ^ (hashCode >>> 4);
|
||||
int index = hashCode & (pool.length - 1);
|
||||
|
||||
String pooled = pool[index];
|
||||
if (pooled != null && contentEquals(pooled, array, start, length)) {
|
||||
return pooled;
|
||||
}
|
||||
|
||||
String result = new String(array, start, length);
|
||||
pool[index] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
22
src/ARSCLib/com/reandroid/apk/APKLogger.java
Normal file
22
src/ARSCLib/com/reandroid/apk/APKLogger.java
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
public interface APKLogger {
|
||||
void logMessage(String msg);
|
||||
void logError(String msg, Throwable tr);
|
||||
void logVerbose(String msg);
|
||||
}
|
||||
219
src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java
Normal file
219
src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AndroidFrameworks {
|
||||
private static Map<Integer, String> resource_paths;
|
||||
private static FrameworkApk mCurrent;
|
||||
|
||||
public static void setCurrent(FrameworkApk current){
|
||||
synchronized (AndroidFrameworks.class){
|
||||
mCurrent = current;
|
||||
}
|
||||
}
|
||||
public static FrameworkApk getCurrent(){
|
||||
FrameworkApk current = mCurrent;
|
||||
if(current==null){
|
||||
return null;
|
||||
}
|
||||
if(current.isDestroyed()){
|
||||
mCurrent = null;
|
||||
return null;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
public static FrameworkApk getLatest() throws IOException {
|
||||
Map<Integer, String> pathMap = getResourcePaths();
|
||||
synchronized (AndroidFrameworks.class){
|
||||
int latest = getHighestVersion();
|
||||
FrameworkApk current = getCurrent();
|
||||
if(current!=null && latest==current.getVersionCode()){
|
||||
return current;
|
||||
}
|
||||
String path = pathMap.get(latest);
|
||||
if(path == null){
|
||||
throw new IOException("Could not get latest framework");
|
||||
}
|
||||
return loadResource(latest);
|
||||
}
|
||||
}
|
||||
public static FrameworkApk getBestMatch(int version) throws IOException {
|
||||
Map<Integer, String> pathMap = getResourcePaths();
|
||||
synchronized (AndroidFrameworks.class){
|
||||
int best = getBestMatchVersion(version);
|
||||
FrameworkApk current = getCurrent();
|
||||
if(current!=null && best==current.getVersionCode()){
|
||||
return current;
|
||||
}
|
||||
String path = pathMap.get(best);
|
||||
if(path == null){
|
||||
throw new IOException("Could not get framework for version = "+version);
|
||||
}
|
||||
return loadResource(best);
|
||||
}
|
||||
}
|
||||
public static void destroyCurrent(){
|
||||
synchronized (AndroidFrameworks.class){
|
||||
FrameworkApk current = mCurrent;
|
||||
if(current==null){
|
||||
return;
|
||||
}
|
||||
current.destroy();
|
||||
}
|
||||
}
|
||||
private static int getHighestVersion() {
|
||||
Map<Integer, String> pathMap = getResourcePaths();
|
||||
int highest = 0;
|
||||
for(int id:pathMap.keySet()){
|
||||
if(highest==0){
|
||||
highest = id;
|
||||
continue;
|
||||
}
|
||||
if(id>highest){
|
||||
highest = id;
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
private static int getBestMatchVersion(int version) {
|
||||
Map<Integer, String> pathMap = getResourcePaths();
|
||||
if(pathMap.containsKey(version)){
|
||||
return version;
|
||||
}
|
||||
int highest = 0;
|
||||
int best = 0;
|
||||
int prevDifference = 0;
|
||||
for(int id:pathMap.keySet()){
|
||||
if(highest==0){
|
||||
highest = id;
|
||||
best = id;
|
||||
prevDifference = version*2 + 1000;
|
||||
continue;
|
||||
}
|
||||
if(id>highest){
|
||||
highest = id;
|
||||
}
|
||||
int diff = id-version;
|
||||
if(diff<0){
|
||||
diff=-diff;
|
||||
}
|
||||
if(diff<prevDifference || (diff==prevDifference && id>best)){
|
||||
best = id;
|
||||
prevDifference = diff;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
public static FrameworkApk loadResource(int version) throws IOException {
|
||||
String path = getResourcePath(version);
|
||||
if(path == null){
|
||||
throw new IOException("No resource found for version: "+version);
|
||||
}
|
||||
String simpleName = toSimpleName(path);
|
||||
return FrameworkApk.loadApkBuffer(simpleName, AndroidFrameworks.class.getResourceAsStream(path));
|
||||
}
|
||||
private static String getResourcePath(int version){
|
||||
return getResourcePaths().get(version);
|
||||
}
|
||||
private static Map<Integer, String> getResourcePaths(){
|
||||
if(resource_paths!=null){
|
||||
return resource_paths;
|
||||
}
|
||||
synchronized (AndroidFrameworks.class){
|
||||
resource_paths = scanAvailableResourcePaths();
|
||||
return resource_paths;
|
||||
}
|
||||
}
|
||||
private static Map<Integer, String> scanAvailableResourcePaths(){
|
||||
Map<Integer, String> results = new HashMap<>();
|
||||
int maxSearch = 50;
|
||||
for(int version=20; version<maxSearch; version++){
|
||||
String path = toResourcePath(version);
|
||||
if(!isAvailable(path)){
|
||||
continue;
|
||||
}
|
||||
results.put(version, path);
|
||||
maxSearch = version + 20;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private static String toSimpleName(String path){
|
||||
int i = path.lastIndexOf('/');
|
||||
if(i<0){
|
||||
i = path.lastIndexOf(File.separatorChar);
|
||||
}
|
||||
if(i>0){
|
||||
i++;
|
||||
path = path.substring(i);
|
||||
}
|
||||
i = path.lastIndexOf('.');
|
||||
if(i>=0){
|
||||
path = path.substring(0, i);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
private static int parseVersion(String name){
|
||||
int i = name.lastIndexOf('/');
|
||||
if(i<0){
|
||||
i = name.lastIndexOf(File.separatorChar);
|
||||
}
|
||||
if(i>0){
|
||||
i++;
|
||||
name = name.substring(i);
|
||||
}
|
||||
i = name.lastIndexOf('-');
|
||||
if(i>=0){
|
||||
i++;
|
||||
name = name.substring(i);
|
||||
}
|
||||
i = name.indexOf('.');
|
||||
if(i>=0){
|
||||
name = name.substring(0, i);
|
||||
}
|
||||
return Integer.parseInt(name);
|
||||
}
|
||||
private static boolean isAvailable(String path){
|
||||
InputStream inputStream = AndroidFrameworks.class.getResourceAsStream(path);
|
||||
if(inputStream==null){
|
||||
return false;
|
||||
}
|
||||
closeQuietly(inputStream);
|
||||
return true;
|
||||
}
|
||||
private static void closeQuietly(InputStream stream){
|
||||
if(stream == null){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
private static String toResourcePath(int version){
|
||||
return ANDROID_RESOURCE_DIRECTORY + ANDROID_PACKAGE
|
||||
+ '-' + version
|
||||
+FRAMEWORK_EXTENSION;
|
||||
}
|
||||
private static final String ANDROID_RESOURCE_DIRECTORY = "/frameworks/android/";
|
||||
private static final String ANDROID_PACKAGE = "android";
|
||||
private static final String FRAMEWORK_EXTENSION = ".apk";
|
||||
}
|
||||
216
src/ARSCLib/com/reandroid/apk/ApkBundle.java
Normal file
216
src/ARSCLib/com/reandroid/apk/ApkBundle.java
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.pool.TableStringPool;
|
||||
import com.reandroid.arsc.pool.builder.StringPoolMerger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class ApkBundle {
|
||||
private final Map<String, ApkModule> mModulesMap;
|
||||
private APKLogger apkLogger;
|
||||
public ApkBundle(){
|
||||
this.mModulesMap=new HashMap<>();
|
||||
}
|
||||
|
||||
public ApkModule mergeModules() throws IOException {
|
||||
List<ApkModule> moduleList=getApkModuleList();
|
||||
if(moduleList.size()==0){
|
||||
throw new FileNotFoundException("Nothing to merge, empty modules");
|
||||
}
|
||||
ApkModule result = new ApkModule(generateMergedModuleName(), new APKArchive());
|
||||
result.setAPKLogger(apkLogger);
|
||||
result.setLoadDefaultFramework(false);
|
||||
|
||||
mergeStringPools(result);
|
||||
|
||||
ApkModule base=getBaseModule();
|
||||
if(base==null){
|
||||
base=getLargestTableModule();
|
||||
}
|
||||
result.merge(base);
|
||||
ApkSignatureBlock signatureBlock = null;
|
||||
for(ApkModule module:moduleList){
|
||||
ApkSignatureBlock asb = module.getApkSignatureBlock();
|
||||
if(module==base){
|
||||
if(asb != null){
|
||||
signatureBlock = asb;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(signatureBlock == null){
|
||||
signatureBlock = asb;
|
||||
}
|
||||
result.merge(module);
|
||||
}
|
||||
|
||||
result.setApkSignatureBlock(signatureBlock);
|
||||
|
||||
if(result.hasTableBlock()){
|
||||
TableBlock tableBlock=result.getTableBlock();
|
||||
tableBlock.sortPackages();
|
||||
tableBlock.refresh();
|
||||
}
|
||||
result.getApkArchive().autoSortApkFiles();
|
||||
return result;
|
||||
}
|
||||
private void mergeStringPools(ApkModule mergedModule) throws IOException {
|
||||
if(!hasOneTableBlock() || mergedModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
logMessage("Merging string pools ... ");
|
||||
TableBlock createdTable = new TableBlock();
|
||||
BlockInputSource<TableBlock> inputSource=
|
||||
new BlockInputSource<>(TableBlock.FILE_NAME, createdTable);
|
||||
mergedModule.getApkArchive().add(inputSource);
|
||||
|
||||
StringPoolMerger poolMerger = new StringPoolMerger();
|
||||
|
||||
for(ApkModule apkModule:getModules()){
|
||||
if(!apkModule.hasTableBlock()){
|
||||
continue;
|
||||
}
|
||||
TableStringPool stringPool = apkModule.getVolatileTableStringPool();
|
||||
poolMerger.add(stringPool);
|
||||
}
|
||||
|
||||
poolMerger.mergeTo(createdTable.getTableStringPool());
|
||||
|
||||
logMessage("Merged string pools="+poolMerger.getMergedPools()
|
||||
+", style="+poolMerger.getMergedStyleStrings()
|
||||
+", strings="+poolMerger.getMergedStrings());
|
||||
}
|
||||
private String generateMergedModuleName(){
|
||||
Set<String> moduleNames=mModulesMap.keySet();
|
||||
String merged="merged";
|
||||
int i=1;
|
||||
String name=merged;
|
||||
while (moduleNames.contains(name)){
|
||||
name=merged+"_"+i;
|
||||
i++;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
private ApkModule getLargestTableModule(){
|
||||
ApkModule apkModule=null;
|
||||
int chunkSize=0;
|
||||
for(ApkModule module:getApkModuleList()){
|
||||
if(!module.hasTableBlock()){
|
||||
continue;
|
||||
}
|
||||
TableBlock tableBlock=module.getTableBlock();
|
||||
int size=tableBlock.getHeaderBlock().getChunkSize();
|
||||
if(apkModule==null || size>chunkSize){
|
||||
chunkSize=size;
|
||||
apkModule=module;
|
||||
}
|
||||
}
|
||||
return apkModule;
|
||||
}
|
||||
public ApkModule getBaseModule(){
|
||||
for(ApkModule module:getApkModuleList()){
|
||||
if(module.isBaseModule()){
|
||||
return module;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public List<ApkModule> getApkModuleList(){
|
||||
return new ArrayList<>(mModulesMap.values());
|
||||
}
|
||||
public void loadApkDirectory(File dir) throws IOException{
|
||||
loadApkDirectory(dir, false);
|
||||
}
|
||||
public void loadApkDirectory(File dir, boolean recursive) throws IOException {
|
||||
if(!dir.isDirectory()){
|
||||
throw new FileNotFoundException("No such directory: "+dir);
|
||||
}
|
||||
List<File> apkList;
|
||||
if(recursive){
|
||||
apkList = ApkUtil.recursiveFiles(dir, ".apk");
|
||||
}else {
|
||||
apkList = ApkUtil.listFiles(dir, ".apk");
|
||||
}
|
||||
if(apkList.size()==0){
|
||||
throw new FileNotFoundException("No '*.apk' files in directory: "+dir);
|
||||
}
|
||||
logMessage("Found apk files: "+apkList.size());
|
||||
for(File file:apkList){
|
||||
logVerbose("Loading: "+file.getName());
|
||||
String name = ApkUtil.toModuleName(file);
|
||||
ApkModule module = ApkModule.loadApkFile(file, name);
|
||||
module.setAPKLogger(apkLogger);
|
||||
addModule(module);
|
||||
}
|
||||
}
|
||||
public void addModule(ApkModule apkModule){
|
||||
apkModule.setLoadDefaultFramework(false);
|
||||
String name = apkModule.getModuleName();
|
||||
mModulesMap.remove(name);
|
||||
mModulesMap.put(name, apkModule);
|
||||
}
|
||||
public boolean containsApkModule(String moduleName){
|
||||
return mModulesMap.containsKey(moduleName);
|
||||
}
|
||||
public ApkModule removeApkModule(String moduleName){
|
||||
return mModulesMap.remove(moduleName);
|
||||
}
|
||||
public ApkModule getApkModule(String moduleName){
|
||||
return mModulesMap.get(moduleName);
|
||||
}
|
||||
public List<String> listModuleNames(){
|
||||
return new ArrayList<>(mModulesMap.keySet());
|
||||
}
|
||||
public int countModules(){
|
||||
return mModulesMap.size();
|
||||
}
|
||||
public Collection<ApkModule> getModules(){
|
||||
return mModulesMap.values();
|
||||
}
|
||||
private boolean hasOneTableBlock(){
|
||||
for(ApkModule apkModule:getModules()){
|
||||
if(apkModule.hasTableBlock()){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
private void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/ARSCLib/com/reandroid/apk/ApkDecoder.java
Normal file
110
src/ARSCLib/com/reandroid/apk/ApkDecoder.java
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class ApkDecoder {
|
||||
private final Set<String> mDecodedPaths;
|
||||
private APKLogger apkLogger;
|
||||
private boolean mLogErrors;
|
||||
|
||||
public ApkDecoder(){
|
||||
mDecodedPaths = new HashSet<>();
|
||||
}
|
||||
public final void decodeTo(File outDir) throws IOException{
|
||||
reset();
|
||||
onDecodeTo(outDir);
|
||||
}
|
||||
abstract void onDecodeTo(File outDir) throws IOException;
|
||||
|
||||
boolean containsDecodedPath(String path){
|
||||
return mDecodedPaths.contains(path);
|
||||
}
|
||||
void addDecodedPath(String path){
|
||||
mDecodedPaths.add(path);
|
||||
}
|
||||
void writePathMap(File dir, Collection<? extends InputSource> sourceList) throws IOException {
|
||||
PathMap pathMap = new PathMap();
|
||||
pathMap.add(sourceList);
|
||||
File file = new File(dir, PathMap.JSON_FILE);
|
||||
pathMap.toJson().write(file);
|
||||
}
|
||||
void dumpSignatures(File outDir, ApkSignatureBlock signatureBlock) throws IOException {
|
||||
if(signatureBlock == null){
|
||||
return;
|
||||
}
|
||||
logMessage("Dumping signatures ...");
|
||||
File dir = new File(outDir, ApkUtil.SIGNATURE_DIR_NAME);
|
||||
signatureBlock.writeSplitRawToDirectory(dir);
|
||||
}
|
||||
void logOrThrow(String message, IOException exception) throws IOException{
|
||||
if(isLogErrors()){
|
||||
logError(message, exception);
|
||||
return;
|
||||
}
|
||||
if(message == null && exception == null){
|
||||
return;
|
||||
}
|
||||
if(exception == null){
|
||||
exception = new IOException(message);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
private void reset(){
|
||||
mDecodedPaths.clear();
|
||||
}
|
||||
|
||||
public boolean isLogErrors() {
|
||||
return mLogErrors;
|
||||
}
|
||||
public void setLogErrors(boolean logErrors) {
|
||||
this.mLogErrors = logErrors;
|
||||
}
|
||||
|
||||
public void setApkLogger(APKLogger apkLogger) {
|
||||
this.apkLogger = apkLogger;
|
||||
}
|
||||
APKLogger getApkLogger() {
|
||||
return apkLogger;
|
||||
}
|
||||
void logMessage(String msg) {
|
||||
APKLogger apkLogger = this.apkLogger;
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
void logError(String msg, Throwable tr) {
|
||||
APKLogger apkLogger = this.apkLogger;
|
||||
if(apkLogger == null || (msg == null && tr == null)){
|
||||
return;
|
||||
}
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
void logVerbose(String msg) {
|
||||
APKLogger apkLogger = this.apkLogger;
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java
Normal file
233
src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class ApkJsonDecoder {
|
||||
private final ApkModule apkModule;
|
||||
private final Set<String> decodedPaths;
|
||||
private final boolean splitTypes;
|
||||
public ApkJsonDecoder(ApkModule apkModule, boolean splitTypes){
|
||||
this.apkModule = apkModule;
|
||||
this.splitTypes = splitTypes;
|
||||
this.decodedPaths = new HashSet<>();
|
||||
}
|
||||
public ApkJsonDecoder(ApkModule apkModule){
|
||||
this(apkModule, false);
|
||||
}
|
||||
public void sanitizeFilePaths(){
|
||||
PathSanitizer sanitizer = PathSanitizer.create(apkModule);
|
||||
sanitizer.sanitize();
|
||||
}
|
||||
public File writeToDirectory(File dir) throws IOException {
|
||||
this.decodedPaths.clear();
|
||||
writeUncompressed(dir);
|
||||
writeManifest(dir);
|
||||
writeTable(dir);
|
||||
//writeResourceIds(dir);
|
||||
//writePublicXml(dir);
|
||||
writeResources(dir);
|
||||
writeRootFiles(dir);
|
||||
writePathMap(dir);
|
||||
dumpSignatures(dir);
|
||||
return new File(dir, apkModule.getModuleName());
|
||||
}
|
||||
private void dumpSignatures(File outDir) throws IOException {
|
||||
ApkSignatureBlock signatureBlock = apkModule.getApkSignatureBlock();
|
||||
if(signatureBlock == null){
|
||||
return;
|
||||
}
|
||||
apkModule.logMessage("Dumping signatures ...");
|
||||
File dir = toSignatureDir(outDir);
|
||||
signatureBlock.writeSplitRawToDirectory(dir);
|
||||
}
|
||||
private void writePathMap(File dir) throws IOException {
|
||||
PathMap pathMap = new PathMap();
|
||||
pathMap.add(apkModule.getApkArchive());
|
||||
File file = toPathMapJsonFile(dir);
|
||||
pathMap.toJson().write(file);
|
||||
}
|
||||
private void writeUncompressed(File dir) throws IOException {
|
||||
File file=toUncompressedJsonFile(dir);
|
||||
UncompressedFiles uncompressedFiles=new UncompressedFiles();
|
||||
uncompressedFiles.addCommonExtensions();
|
||||
uncompressedFiles.addPath(apkModule.getApkArchive());
|
||||
uncompressedFiles.toJson().write(file);
|
||||
}
|
||||
private void writeResources(File dir) throws IOException {
|
||||
for(ResFile resFile:apkModule.listResFiles()){
|
||||
writeResource(dir, resFile);
|
||||
}
|
||||
}
|
||||
private void writeResource(File dir, ResFile resFile) throws IOException {
|
||||
if(resFile.isBinaryXml()){
|
||||
writeResourceJson(dir, resFile);
|
||||
}
|
||||
}
|
||||
private void writeResourceJson(File dir, ResFile resFile) throws IOException {
|
||||
InputSource inputSource= resFile.getInputSource();
|
||||
String path=inputSource.getAlias();
|
||||
File file=toResJson(dir, path);
|
||||
ResXmlDocument resXmlDocument =new ResXmlDocument();
|
||||
resXmlDocument.readBytes(inputSource.openStream());
|
||||
JSONObject jsonObject= resXmlDocument.toJson();
|
||||
jsonObject.write(file);
|
||||
addDecoded(path);
|
||||
}
|
||||
private void writeRootFiles(File dir) throws IOException {
|
||||
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
|
||||
writeRootFile(dir, inputSource);
|
||||
}
|
||||
}
|
||||
private void writeRootFile(File dir, InputSource inputSource) throws IOException {
|
||||
String path=inputSource.getAlias();
|
||||
if(hasDecoded(path)){
|
||||
return;
|
||||
}
|
||||
File file=toRootFile(dir, path);
|
||||
File parent=file.getParentFile();
|
||||
if(parent!=null && !parent.exists()){
|
||||
parent.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream=new FileOutputStream(file);
|
||||
inputSource.write(outputStream);
|
||||
outputStream.close();
|
||||
addDecoded(path);
|
||||
}
|
||||
private void writeTable(File dir) throws IOException {
|
||||
if(!splitTypes){
|
||||
writeTableSingle(dir);
|
||||
return;
|
||||
}
|
||||
writeTableSplit(dir);
|
||||
}
|
||||
private void writeTableSplit(File dir) throws IOException {
|
||||
if(!apkModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
TableBlock tableBlock = apkModule.getTableBlock();
|
||||
File splitDir= toJsonTableSplitDir(dir);
|
||||
TableBlockJson tableBlockJson=new TableBlockJson(tableBlock);
|
||||
tableBlockJson.writeJsonFiles(splitDir);
|
||||
addDecoded(TableBlock.FILE_NAME);
|
||||
}
|
||||
private void writeTableSingle(File dir) throws IOException {
|
||||
if(!apkModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
TableBlock tableBlock = apkModule.getTableBlock();
|
||||
File file= toJsonTableFile(dir);
|
||||
tableBlock.toJson().write(file);
|
||||
addDecoded(TableBlock.FILE_NAME);
|
||||
}
|
||||
private void writeResourceIds(File dir) throws IOException {
|
||||
if(!apkModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
TableBlock tableBlock = apkModule.getTableBlock();
|
||||
ResourceIds resourceIds=new ResourceIds();
|
||||
resourceIds.loadTableBlock(tableBlock);
|
||||
JSONObject jsonObject= resourceIds.toJson();
|
||||
File file=toResourceIds(dir);
|
||||
jsonObject.write(file);
|
||||
}
|
||||
private void writePublicXml(File dir) throws IOException {
|
||||
if(!apkModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
TableBlock tableBlock = apkModule.getTableBlock();
|
||||
ResourceIds resourceIds=new ResourceIds();
|
||||
resourceIds.loadTableBlock(tableBlock);
|
||||
File file=toResourceIdsXml(dir);
|
||||
resourceIds.writeXml(file);
|
||||
}
|
||||
private void writeManifest(File dir) throws IOException {
|
||||
if(!apkModule.hasAndroidManifestBlock()){
|
||||
return;
|
||||
}
|
||||
AndroidManifestBlock manifestBlock = apkModule.getAndroidManifestBlock();
|
||||
File file = toJsonManifestFile(dir);
|
||||
manifestBlock.toJson().write(file);
|
||||
addDecoded(AndroidManifestBlock.FILE_NAME);
|
||||
}
|
||||
private boolean hasDecoded(String path){
|
||||
return decodedPaths.contains(path);
|
||||
}
|
||||
private void addDecoded(String path){
|
||||
this.decodedPaths.add(path);
|
||||
}
|
||||
private File toJsonTableFile(File dir){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
|
||||
return new File(file, name);
|
||||
}
|
||||
private File toJsonTableSplitDir(File dir){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
return new File(file, ApkUtil.SPLIT_JSON_DIRECTORY);
|
||||
}
|
||||
private File toResourceIds(File dir){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
String name = "resource-ids.json";
|
||||
return new File(file, name);
|
||||
}
|
||||
private File toResourceIdsXml(File dir){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
String name = "public.xml";
|
||||
return new File(file, name);
|
||||
}
|
||||
private File toSignatureDir(File dir){
|
||||
dir = new File(dir, apkModule.getModuleName());
|
||||
return new File(dir, ApkUtil.SIGNATURE_DIR_NAME);
|
||||
}
|
||||
private File toPathMapJsonFile(File dir){
|
||||
File file = new File(dir, apkModule.getModuleName());
|
||||
return new File(file, PathMap.JSON_FILE);
|
||||
}
|
||||
private File toUncompressedJsonFile(File dir){
|
||||
File file = new File(dir, apkModule.getModuleName());
|
||||
return new File(file, UncompressedFiles.JSON_FILE);
|
||||
}
|
||||
private File toJsonManifestFile(File dir){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
|
||||
return new File(file, name);
|
||||
}
|
||||
private File toResJson(File dir, String path){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
file=new File(file, ApkUtil.RES_JSON_NAME);
|
||||
path=path + ApkUtil.JSON_FILE_EXTENSION;
|
||||
path=path.replace('/', File.separatorChar);
|
||||
return new File(file, path);
|
||||
}
|
||||
private File toRootFile(File dir, String path){
|
||||
File file=new File(dir, apkModule.getModuleName());
|
||||
file=new File(file, ApkUtil.ROOT_NAME);
|
||||
path=path.replace('/', File.separatorChar);
|
||||
return new File(file, path);
|
||||
}
|
||||
}
|
||||
209
src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java
Normal file
209
src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.json.JSONArray;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class ApkJsonEncoder {
|
||||
private APKArchive apkArchive;
|
||||
private APKLogger apkLogger;
|
||||
public ApkJsonEncoder(){
|
||||
}
|
||||
public ApkModule scanDirectory(File moduleDir){
|
||||
this.apkArchive=new APKArchive();
|
||||
String moduleName=moduleDir.getName();
|
||||
scanManifest(moduleDir);
|
||||
scanTable(moduleDir);
|
||||
scanResJsonDirs(moduleDir);
|
||||
scanRootDirs(moduleDir);
|
||||
ApkModule module=new ApkModule(moduleName, apkArchive);
|
||||
module.setLoadDefaultFramework(false);
|
||||
module.setAPKLogger(apkLogger);
|
||||
loadUncompressed(module, moduleDir);
|
||||
//applyResourceId(module, moduleDir);
|
||||
restorePathMap(moduleDir, module);
|
||||
restoreSignatures(moduleDir, module);
|
||||
return module;
|
||||
}
|
||||
private void restoreSignatures(File dir, ApkModule apkModule){
|
||||
File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME);
|
||||
if(!sigDir.isDirectory()){
|
||||
return;
|
||||
}
|
||||
logMessage("Loading signatures ...");
|
||||
ApkSignatureBlock signatureBlock = new ApkSignatureBlock();
|
||||
try {
|
||||
signatureBlock.scanSplitFiles(sigDir);
|
||||
apkModule.setApkSignatureBlock(signatureBlock);
|
||||
} catch (IOException exception){
|
||||
logError("Failed to load signatures: ", exception);
|
||||
}
|
||||
}
|
||||
private void restorePathMap(File dir, ApkModule apkModule){
|
||||
File file = new File(dir, PathMap.JSON_FILE);
|
||||
if(!file.isFile()){
|
||||
return;
|
||||
}
|
||||
logMessage("Restoring file path ...");
|
||||
PathMap pathMap = new PathMap();
|
||||
FileInputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(file);
|
||||
} catch (FileNotFoundException exception) {
|
||||
logError("Failed to load path-map", exception);
|
||||
return;
|
||||
}
|
||||
JSONArray jsonArray = new JSONArray(inputStream);
|
||||
pathMap.fromJson(jsonArray);
|
||||
pathMap.restore(apkModule);
|
||||
}
|
||||
private void applyResourceId(ApkModule apkModule, File moduleDir) {
|
||||
if(!apkModule.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
File pubXml=toResourceIdsXml(moduleDir);
|
||||
if(!pubXml.isFile()){
|
||||
return;
|
||||
}
|
||||
ResourceIds resourceIds=new ResourceIds();
|
||||
try {
|
||||
resourceIds.fromXml(pubXml);
|
||||
resourceIds.applyTo(apkModule.getTableBlock());
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
private void loadUncompressed(ApkModule module, File moduleDir){
|
||||
File jsonFile=toUncompressedJsonFile(moduleDir);
|
||||
UncompressedFiles uf= module.getUncompressedFiles();
|
||||
try {
|
||||
uf.fromJson(jsonFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
private void scanRootDirs(File moduleDir){
|
||||
File rootDir=toRootDir(moduleDir);
|
||||
List<File> jsonFileList=ApkUtil.recursiveFiles(rootDir);
|
||||
for(File file:jsonFileList){
|
||||
scanRootFile(rootDir, file);
|
||||
}
|
||||
}
|
||||
private void scanRootFile(File rootDir, File file){
|
||||
String path=ApkUtil.toArchivePath(rootDir, file);
|
||||
FileInputSource inputSource=new FileInputSource(file, path);
|
||||
apkArchive.add(inputSource);
|
||||
}
|
||||
private void scanResJsonDirs(File moduleDir){
|
||||
File resJsonDir=toResJsonDir(moduleDir);
|
||||
List<File> jsonFileList=ApkUtil.recursiveFiles(resJsonDir);
|
||||
for(File file:jsonFileList){
|
||||
scanResJsonFile(resJsonDir, file);
|
||||
}
|
||||
}
|
||||
private void scanResJsonFile(File resJsonDir, File file){
|
||||
JsonXmlInputSource inputSource=JsonXmlInputSource.fromFile(resJsonDir, file);
|
||||
apkArchive.add(inputSource);
|
||||
}
|
||||
private void scanManifest(File moduleDir){
|
||||
File file=toJsonManifestFile(moduleDir);
|
||||
if(!file.isFile()){
|
||||
return;
|
||||
}
|
||||
JsonManifestInputSource inputSource=JsonManifestInputSource.fromFile(moduleDir, file);
|
||||
inputSource.setAPKLogger(apkLogger);
|
||||
apkArchive.add(inputSource);
|
||||
}
|
||||
private void scanTable(File moduleDir) {
|
||||
boolean splitFound=scanTableSplitJson(moduleDir);
|
||||
if(splitFound){
|
||||
return;
|
||||
}
|
||||
scanTableSingleJson(moduleDir);
|
||||
}
|
||||
private boolean scanTableSplitJson(File moduleDir) {
|
||||
File dir=toJsonTableSplitDir(moduleDir);
|
||||
if(!dir.isDirectory()){
|
||||
return false;
|
||||
}
|
||||
SplitJsonTableInputSource inputSource=new SplitJsonTableInputSource(dir);
|
||||
inputSource.setAPKLogger(apkLogger);
|
||||
apkArchive.add(inputSource);
|
||||
return true;
|
||||
}
|
||||
private void scanTableSingleJson(File moduleDir) {
|
||||
File file=toJsonTableFile(moduleDir);
|
||||
if(!file.isFile()){
|
||||
return;
|
||||
}
|
||||
SingleJsonTableInputSource inputSource= SingleJsonTableInputSource.fromFile(moduleDir, file);
|
||||
inputSource.setAPKLogger(apkLogger);
|
||||
apkArchive.add(inputSource);
|
||||
}
|
||||
private File toJsonTableFile(File dir){
|
||||
String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
|
||||
return new File(dir, name);
|
||||
}
|
||||
private File toJsonManifestFile(File dir){
|
||||
String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
|
||||
return new File(dir, name);
|
||||
}
|
||||
private File toResourceIdsXml(File dir){
|
||||
String name = "public.xml";
|
||||
return new File(dir, name);
|
||||
}
|
||||
private File toUncompressedJsonFile(File dir){
|
||||
return new File(dir, UncompressedFiles.JSON_FILE);
|
||||
}
|
||||
private File toJsonTableSplitDir(File dir){
|
||||
return new File(dir, ApkUtil.SPLIT_JSON_DIRECTORY);
|
||||
}
|
||||
private File toResJsonDir(File dir){
|
||||
return new File(dir, ApkUtil.RES_JSON_NAME);
|
||||
}
|
||||
private File toRootDir(File dir){
|
||||
return new File(dir, ApkUtil.ROOT_NAME);
|
||||
}
|
||||
|
||||
public void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
private void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
838
src/ARSCLib/com/reandroid/apk/ApkModule.java
Normal file
838
src/ARSCLib/com/reandroid/apk/ApkModule.java
Normal file
File diff suppressed because it is too large
Load Diff
320
src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java
Normal file
320
src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.apk.xmldecoder.*;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.container.SpecTypePair;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.identifiers.PackageIdentifier;
|
||||
import com.reandroid.json.JSONObject;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate<Entry> {
|
||||
private final ApkModule apkModule;
|
||||
private final Map<Integer, Set<ResConfig>> decodedEntries;
|
||||
|
||||
private ResXmlDocumentSerializer documentSerializer;
|
||||
private XMLEntryDecoderSerializer entrySerializer;
|
||||
|
||||
|
||||
public ApkModuleXmlDecoder(ApkModule apkModule){
|
||||
super();
|
||||
this.apkModule = apkModule;
|
||||
this.decodedEntries = new HashMap<>();
|
||||
super.setApkLogger(apkModule.getApkLogger());
|
||||
}
|
||||
public void sanitizeFilePaths(){
|
||||
PathSanitizer sanitizer = PathSanitizer.create(apkModule);
|
||||
sanitizer.sanitize();
|
||||
}
|
||||
@Override
|
||||
void onDecodeTo(File outDir) throws IOException{
|
||||
this.decodedEntries.clear();
|
||||
logMessage("Decoding ...");
|
||||
|
||||
if(!apkModule.hasTableBlock()){
|
||||
logOrThrow(null, new IOException("Don't have resource table"));
|
||||
return;
|
||||
}
|
||||
|
||||
decodeUncompressedFiles(outDir);
|
||||
|
||||
TableBlock tableBlock = apkModule.getTableBlock();
|
||||
|
||||
this.entrySerializer = new XMLEntryDecoderSerializer(tableBlock);
|
||||
this.entrySerializer.setDecodedEntries(this);
|
||||
|
||||
decodeAndroidManifest(outDir, apkModule.getAndroidManifestBlock());
|
||||
decodeTableBlock(outDir, tableBlock);
|
||||
|
||||
logMessage("Decoding resource files ...");
|
||||
List<ResFile> resFileList = apkModule.listResFiles();
|
||||
for(ResFile resFile:resFileList){
|
||||
decodeResFile(outDir, resFile);
|
||||
}
|
||||
decodeValues(outDir, tableBlock);
|
||||
|
||||
extractRootFiles(outDir);
|
||||
|
||||
writePathMap(outDir, apkModule.getApkArchive().listInputSources());
|
||||
|
||||
dumpSignatures(outDir, apkModule.getApkSignatureBlock());
|
||||
}
|
||||
private void decodeTableBlock(File outDir, TableBlock tableBlock) throws IOException {
|
||||
try{
|
||||
decodePackageInfo(outDir, tableBlock);
|
||||
decodePublicXml(tableBlock, outDir);
|
||||
addDecodedPath(TableBlock.FILE_NAME);
|
||||
}catch (IOException exception){
|
||||
logOrThrow("Error decoding resource table", exception);
|
||||
}
|
||||
}
|
||||
private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException {
|
||||
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||||
decodePackageInfo(outDir, packageBlock);
|
||||
}
|
||||
}
|
||||
private void decodePackageInfo(File outDir, PackageBlock packageBlock) throws IOException {
|
||||
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||||
File packageJsonFile = new File(pkgDir, PackageBlock.JSON_FILE_NAME);
|
||||
JSONObject jsonObject = packageBlock.toJson(false);
|
||||
jsonObject.write(packageJsonFile);
|
||||
}
|
||||
private void decodeUncompressedFiles(File outDir)
|
||||
throws IOException {
|
||||
File file=new File(outDir, UncompressedFiles.JSON_FILE);
|
||||
UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles();
|
||||
uncompressedFiles.toJson().write(file);
|
||||
}
|
||||
private void decodeResFile(File outDir, ResFile resFile)
|
||||
throws IOException{
|
||||
if(resFile.isBinaryXml()){
|
||||
decodeResXml(outDir, resFile);
|
||||
}else {
|
||||
decodeResRaw(outDir, resFile);
|
||||
}
|
||||
addDecodedPath(resFile.getFilePath());
|
||||
}
|
||||
private void decodeResRaw(File outDir, ResFile resFile)
|
||||
throws IOException {
|
||||
Entry entry = resFile.pickOne();
|
||||
PackageBlock packageBlock= entry.getPackageBlock();
|
||||
|
||||
File pkgDir=new File(outDir, getPackageDirName(packageBlock));
|
||||
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
|
||||
String path = alias.replace('/', File.separatorChar);
|
||||
File file=new File(pkgDir, path);
|
||||
File dir=file.getParentFile();
|
||||
if(!dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream=new FileOutputStream(file);
|
||||
resFile.getInputSource().write(outputStream);
|
||||
outputStream.close();
|
||||
resFile.setFilePath(alias);
|
||||
|
||||
addDecodedEntry(entry);
|
||||
}
|
||||
private void decodeResXml(File outDir, ResFile resFile)
|
||||
throws IOException{
|
||||
Entry entry = resFile.pickOne();
|
||||
PackageBlock packageBlock = entry.getPackageBlock();
|
||||
|
||||
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||||
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
|
||||
String path = alias.replace('/', File.separatorChar);
|
||||
path = path.replace('/', File.separatorChar);
|
||||
File file = new File(pkgDir, path);
|
||||
|
||||
logVerbose("Decoding: " + path);
|
||||
serializeXml(packageBlock.getId(), resFile.getInputSource(), file);
|
||||
|
||||
resFile.setFilePath(alias);
|
||||
addDecodedEntry(entry);
|
||||
}
|
||||
private ResXmlDocumentSerializer getDocumentSerializer(){
|
||||
if(documentSerializer == null){
|
||||
documentSerializer = new ResXmlDocumentSerializer(apkModule);
|
||||
documentSerializer.setValidateXmlNamespace(true);
|
||||
}
|
||||
return documentSerializer;
|
||||
}
|
||||
private void decodePublicXml(TableBlock tableBlock, File outDir)
|
||||
throws IOException{
|
||||
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||||
decodePublicXml(packageBlock, outDir);
|
||||
}
|
||||
if(tableBlock.getPackageArray().childesCount()==0){
|
||||
decodeEmptyTable(outDir);
|
||||
}
|
||||
}
|
||||
private void decodeEmptyTable(File outDir) throws IOException {
|
||||
logMessage("Decoding empty table ...");
|
||||
String pkgName = apkModule.getPackageName();
|
||||
if(pkgName==null){
|
||||
return;
|
||||
}
|
||||
File pkgDir = new File(outDir, "0-"+pkgName);
|
||||
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
|
||||
File values = new File(resDir, "values");
|
||||
File pubXml = new File(values, ApkUtil.FILE_NAME_PUBLIC_XML);
|
||||
XMLDocument xmlDocument = new XMLDocument("resources");
|
||||
xmlDocument.save(pubXml, false);
|
||||
}
|
||||
private void decodePublicXml(PackageBlock packageBlock, File outDir)
|
||||
throws IOException {
|
||||
String packageDirName=getPackageDirName(packageBlock);
|
||||
logMessage("Decoding public.xml: "+packageDirName);
|
||||
File file=new File(outDir, packageDirName);
|
||||
file=new File(file, ApkUtil.RES_DIR_NAME);
|
||||
file=new File(file, "values");
|
||||
file=new File(file, ApkUtil.FILE_NAME_PUBLIC_XML);
|
||||
PackageIdentifier packageIdentifier = new PackageIdentifier();
|
||||
packageIdentifier.load(packageBlock);
|
||||
packageIdentifier.writePublicXml(file);
|
||||
}
|
||||
private void decodeAndroidManifest(File outDir, AndroidManifestBlock manifestBlock)
|
||||
throws IOException {
|
||||
if(!apkModule.hasAndroidManifestBlock()){
|
||||
logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME);
|
||||
return;
|
||||
}
|
||||
File file=new File(outDir, AndroidManifestBlock.FILE_NAME);
|
||||
logMessage("Decoding: "+file.getName());
|
||||
int currentPackageId = manifestBlock.guessCurrentPackageId();
|
||||
serializeXml(currentPackageId, manifestBlock, file);
|
||||
addDecodedPath(AndroidManifestBlock.FILE_NAME);
|
||||
}
|
||||
private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile)
|
||||
throws IOException {
|
||||
XMLNamespaceValidator.validateNamespaces(document);
|
||||
ResXmlDocumentSerializer serializer = getDocumentSerializer();
|
||||
if(currentPackageId != 0){
|
||||
serializer.getDecoder().setCurrentPackageId(currentPackageId);
|
||||
}
|
||||
try {
|
||||
serializer.write(document, outFile);
|
||||
} catch (XmlPullParserException ex) {
|
||||
throw new IOException("Error: "+outFile.getName(), ex);
|
||||
}
|
||||
}
|
||||
private void serializeXml(int currentPackageId, InputSource inputSource, File outFile)
|
||||
throws IOException {
|
||||
ResXmlDocumentSerializer serializer = getDocumentSerializer();
|
||||
if(currentPackageId != 0){
|
||||
serializer.getDecoder().setCurrentPackageId(currentPackageId);
|
||||
}
|
||||
try {
|
||||
serializer.write(inputSource, outFile);
|
||||
} catch (XmlPullParserException ex) {
|
||||
throw new IOException("Error: "+outFile.getName(), ex);
|
||||
}
|
||||
}
|
||||
private void addDecodedEntry(Entry entry){
|
||||
if(entry.isNull()){
|
||||
return;
|
||||
}
|
||||
int resourceId= entry.getResourceId();
|
||||
Set<ResConfig> resConfigSet=decodedEntries.get(resourceId);
|
||||
if(resConfigSet==null){
|
||||
resConfigSet=new HashSet<>();
|
||||
decodedEntries.put(resourceId, resConfigSet);
|
||||
}
|
||||
resConfigSet.add(entry.getResConfig());
|
||||
}
|
||||
private boolean containsDecodedEntry(Entry entry){
|
||||
Set<ResConfig> resConfigSet=decodedEntries.get(entry.getResourceId());
|
||||
if(resConfigSet==null){
|
||||
return false;
|
||||
}
|
||||
return resConfigSet.contains(entry.getResConfig());
|
||||
}
|
||||
private void decodeValues(File outDir, TableBlock tableBlock) throws IOException {
|
||||
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||||
decodeValues(outDir, packageBlock);
|
||||
}
|
||||
}
|
||||
private void decodeValues(File outDir, PackageBlock packageBlock) throws IOException {
|
||||
logMessage("Decoding values: "
|
||||
+ getPackageDirName(packageBlock));
|
||||
|
||||
packageBlock.sortTypes();
|
||||
|
||||
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||||
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
|
||||
|
||||
for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
|
||||
decodeValues(resDir, specTypePair);
|
||||
}
|
||||
}
|
||||
private void decodeValues(File outDir, SpecTypePair specTypePair) throws IOException {
|
||||
entrySerializer.decode(outDir, specTypePair);
|
||||
}
|
||||
private String getPackageDirName(PackageBlock packageBlock){
|
||||
String name = ApkUtil.sanitizeForFileName(packageBlock.getName());
|
||||
if(name==null){
|
||||
name="package";
|
||||
}
|
||||
TableBlock tableBlock = packageBlock.getTableBlock();
|
||||
int index = packageBlock.getIndex();
|
||||
String prefix;
|
||||
if(index < 10 && tableBlock.countPackages() > 10){
|
||||
prefix = "0" + index;
|
||||
}else {
|
||||
prefix = Integer.toString(index);
|
||||
}
|
||||
return prefix + "-" + name;
|
||||
}
|
||||
private void extractRootFiles(File outDir) throws IOException {
|
||||
logMessage("Extracting root files");
|
||||
File rootDir = new File(outDir, "root");
|
||||
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
|
||||
if(containsDecodedPath(inputSource.getAlias())){
|
||||
continue;
|
||||
}
|
||||
extractRootFiles(rootDir, inputSource);
|
||||
addDecodedPath(inputSource.getAlias());
|
||||
}
|
||||
}
|
||||
private void extractRootFiles(File rootDir, InputSource inputSource) throws IOException {
|
||||
String path=inputSource.getAlias();
|
||||
path=path.replace(File.separatorChar, '/');
|
||||
File file=new File(rootDir, path);
|
||||
File dir=file.getParentFile();
|
||||
if(!dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream=new FileOutputStream(file);
|
||||
inputSource.write(outputStream);
|
||||
outputStream.close();
|
||||
}
|
||||
@Override
|
||||
public boolean test(Entry entry) {
|
||||
return !containsDecodedEntry(entry);
|
||||
}
|
||||
}
|
||||
97
src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java
Normal file
97
src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.apk.xmlencoder.RESEncoder;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.pool.TableStringPool;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.xml.XMLException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class ApkModuleXmlEncoder {
|
||||
private final RESEncoder resEncoder;
|
||||
public ApkModuleXmlEncoder(){
|
||||
this.resEncoder = new RESEncoder();
|
||||
}
|
||||
public ApkModuleXmlEncoder(ApkModule module, TableBlock tableBlock){
|
||||
this.resEncoder = new RESEncoder(module, tableBlock);
|
||||
}
|
||||
public void scanDirectory(File mainDirectory) throws IOException, XMLException {
|
||||
loadUncompressedFiles(mainDirectory);
|
||||
resEncoder.scanDirectory(mainDirectory);
|
||||
File rootDir=new File(mainDirectory, "root");
|
||||
scanRootDir(rootDir);
|
||||
restorePathMap(mainDirectory);
|
||||
restoreSignatures(mainDirectory);
|
||||
sortFiles();
|
||||
TableStringPool tableStringPool = getApkModule().getTableBlock().getTableStringPool();
|
||||
tableStringPool.removeUnusedStrings();
|
||||
}
|
||||
private void restoreSignatures(File dir) throws IOException {
|
||||
File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME);
|
||||
if(!sigDir.isDirectory()){
|
||||
return;
|
||||
}
|
||||
ApkModule apkModule = getApkModule();
|
||||
apkModule.logMessage("Loading signatures ...");
|
||||
ApkSignatureBlock signatureBlock = new ApkSignatureBlock();
|
||||
signatureBlock.scanSplitFiles(sigDir);
|
||||
apkModule.setApkSignatureBlock(signatureBlock);
|
||||
}
|
||||
private void restorePathMap(File dir) throws IOException{
|
||||
File file = new File(dir, PathMap.JSON_FILE);
|
||||
if(!file.isFile()){
|
||||
return;
|
||||
}
|
||||
PathMap pathMap = new PathMap();
|
||||
JSONArray jsonArray = new JSONArray(file);
|
||||
pathMap.fromJson(jsonArray);
|
||||
pathMap.restore(getApkModule());
|
||||
}
|
||||
public ApkModule getApkModule(){
|
||||
return resEncoder.getApkModule();
|
||||
}
|
||||
|
||||
private void scanRootDir(File rootDir){
|
||||
APKArchive archive=getApkModule().getApkArchive();
|
||||
List<File> rootFileList=ApkUtil.recursiveFiles(rootDir);
|
||||
for(File file:rootFileList){
|
||||
String path=ApkUtil.toArchivePath(rootDir, file);
|
||||
FileInputSource inputSource=new FileInputSource(file, path);
|
||||
archive.add(inputSource);
|
||||
}
|
||||
}
|
||||
private void sortFiles(){
|
||||
APKArchive archive = getApkModule().getApkArchive();
|
||||
archive.autoSortApkFiles();
|
||||
}
|
||||
private void loadUncompressedFiles(File mainDirectory) throws IOException {
|
||||
File file=new File(mainDirectory, UncompressedFiles.JSON_FILE);
|
||||
UncompressedFiles uncompressedFiles = getApkModule().getUncompressedFiles();
|
||||
uncompressedFiles.fromJson(file);
|
||||
}
|
||||
public void setApkLogger(APKLogger apkLogger) {
|
||||
this.resEncoder.setAPKLogger(apkLogger);
|
||||
}
|
||||
}
|
||||
200
src/ARSCLib/com/reandroid/apk/ApkUtil.java
Normal file
200
src/ARSCLib/com/reandroid/apk/ApkUtil.java
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class ApkUtil {
|
||||
public static String sanitizeForFileName(String name){
|
||||
if(name==null){
|
||||
return null;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
char[] chars = name.toCharArray();
|
||||
boolean skipNext = true;
|
||||
int length = 0;
|
||||
int lengthMax = MAX_FILE_NAME_LENGTH;
|
||||
for(int i=0;i<chars.length;i++){
|
||||
if(length>=lengthMax){
|
||||
break;
|
||||
}
|
||||
char ch = chars[i];
|
||||
if(isGoodFileNameSymbol(ch)){
|
||||
if(!skipNext){
|
||||
builder.append(ch);
|
||||
length++;
|
||||
}
|
||||
skipNext=true;
|
||||
continue;
|
||||
}
|
||||
if(!isGoodFileNameChar(ch)){
|
||||
skipNext = true;
|
||||
continue;
|
||||
}
|
||||
builder.append(ch);
|
||||
length++;
|
||||
skipNext=false;
|
||||
}
|
||||
if(length==0){
|
||||
return null;
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
private static boolean isGoodFileNameSymbol(char ch){
|
||||
return ch == '.'
|
||||
|| ch == '+'
|
||||
|| ch == '-'
|
||||
|| ch == '_'
|
||||
|| ch == '#';
|
||||
}
|
||||
private static boolean isGoodFileNameChar(char ch){
|
||||
return (ch >= '0' && ch <= '9')
|
||||
|| (ch >= 'A' && ch <= 'Z')
|
||||
|| (ch >= 'a' && ch <= 'z');
|
||||
}
|
||||
public static int parseHex(String hex){
|
||||
long l=Long.decode(hex);
|
||||
return (int) l;
|
||||
}
|
||||
public static String replaceRootDir(String path, String dirName){
|
||||
int i=path.indexOf('/')+1;
|
||||
path=path.substring(i);
|
||||
if(dirName != null && dirName.length()>0){
|
||||
if(!dirName.endsWith("/")){
|
||||
dirName=dirName+"/";
|
||||
}
|
||||
path=dirName+path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
public static String toArchiveResourcePath(File dir, File file){
|
||||
String path = toArchivePath(dir, file);
|
||||
if(path.endsWith(ApkUtil.JSON_FILE_EXTENSION)){
|
||||
int i2=path.length()- ApkUtil.JSON_FILE_EXTENSION.length();
|
||||
path=path.substring(0, i2);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
public static String toArchivePath(File dir, File file){
|
||||
String dirPath = dir.getAbsolutePath()+File.separator;
|
||||
String path = file.getAbsolutePath().substring(dirPath.length());
|
||||
path=path.replace(File.separatorChar, '/');
|
||||
return path;
|
||||
}
|
||||
public static List<File> recursiveFiles(File dir, String ext){
|
||||
List<File> results=new ArrayList<>();
|
||||
if(dir.isFile()){
|
||||
if(hasExtension(dir, ext)){
|
||||
results.add(dir);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
if(!dir.isDirectory()){
|
||||
return results;
|
||||
}
|
||||
File[] files=dir.listFiles();
|
||||
if(files==null){
|
||||
return results;
|
||||
}
|
||||
for(File file:files){
|
||||
if(file.isFile()){
|
||||
if(!hasExtension(file, ext)){
|
||||
continue;
|
||||
}
|
||||
results.add(file);
|
||||
continue;
|
||||
}
|
||||
results.addAll(recursiveFiles(file, ext));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public static List<File> recursiveFiles(File dir){
|
||||
return recursiveFiles(dir, null);
|
||||
}
|
||||
public static List<File> listDirectories(File dir){
|
||||
List<File> results=new ArrayList<>();
|
||||
File[] files=dir.listFiles();
|
||||
if(files==null){
|
||||
return results;
|
||||
}
|
||||
for(File file:files){
|
||||
if(file.isDirectory()){
|
||||
results.add(file);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public static List<File> listFiles(File dir, String ext){
|
||||
List<File> results=new ArrayList<>();
|
||||
File[] files=dir.listFiles();
|
||||
if(files==null){
|
||||
return results;
|
||||
}
|
||||
for(File file:files){
|
||||
if(file.isFile()){
|
||||
if(!hasExtension(file, ext)){
|
||||
continue;
|
||||
}
|
||||
results.add(file);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private static boolean hasExtension(File file, String ext){
|
||||
if(ext==null){
|
||||
return true;
|
||||
}
|
||||
String name=file.getName().toLowerCase();
|
||||
ext=ext.toLowerCase();
|
||||
return name.endsWith(ext);
|
||||
}
|
||||
public static String toModuleName(File file){
|
||||
String name=file.getName();
|
||||
int i=name.lastIndexOf('.');
|
||||
if(i>0){
|
||||
name=name.substring(0,i);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
public static Map<String, InputSource> toAliasMap(Collection<InputSource> sourceList){
|
||||
Map<String, InputSource> results=new HashMap<>();
|
||||
for(InputSource inputSource:sourceList){
|
||||
results.put(inputSource.getAlias(), inputSource);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public static final String JSON_FILE_EXTENSION=".json";
|
||||
public static final String RES_JSON_NAME="res-json";
|
||||
public static final String ROOT_NAME="root";
|
||||
public static final String SPLIT_JSON_DIRECTORY="resources";
|
||||
public static final String DEF_MODULE_NAME="base";
|
||||
public static final String NAME_value_type="value_type";
|
||||
public static final String NAME_data="data";
|
||||
public static final String RES_DIR_NAME="res";
|
||||
public static final String FILE_NAME_PUBLIC_XML ="public.xml";
|
||||
|
||||
public static final String TAG_STRING_ARRAY = "string-array";
|
||||
public static final String TAG_INTEGER_ARRAY = "integer-array";
|
||||
|
||||
public static final String SIGNATURE_FILE_NAME = "signatures" + ApkSignatureBlock.FILE_EXT;
|
||||
public static final String SIGNATURE_DIR_NAME = "signatures";
|
||||
|
||||
private static final int MAX_FILE_NAME_LENGTH = 50;
|
||||
}
|
||||
55
src/ARSCLib/com/reandroid/apk/BlockInputSource.java
Normal file
55
src/ARSCLib/com/reandroid/apk/BlockInputSource.java
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.ByteInputSource;
|
||||
import com.reandroid.arsc.base.Block;
|
||||
import com.reandroid.arsc.chunk.Chunk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class BlockInputSource<T extends Chunk<?>> extends ByteInputSource{
|
||||
private final T mBlock;
|
||||
public BlockInputSource(String name, T block) {
|
||||
super(new byte[0], name);
|
||||
this.mBlock=block;
|
||||
}
|
||||
public T getBlock() {
|
||||
mBlock.refresh();
|
||||
return mBlock;
|
||||
}
|
||||
@Override
|
||||
public long getLength() throws IOException{
|
||||
Block block = getBlock();
|
||||
return block.countBytes();
|
||||
}
|
||||
@Override
|
||||
public long getCrc() throws IOException{
|
||||
Block block = getBlock();
|
||||
CrcOutputStream outputStream=new CrcOutputStream();
|
||||
block.writeBytes(outputStream);
|
||||
return outputStream.getCrcValue();
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getBlock().writeBytes(outputStream);
|
||||
}
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
return getBlock().getBytes();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user