mirror of
https://gitlab.com/android_translation_layer/android_translation_layer.git
synced 2025-04-28 20:27:58 +03:00
remove ARSCLib from the source tree
Also remove references to ARSCLib from doc/Architecture.md
This commit is contained in:
parent
778d19f268
commit
b25545c3c0
370 changed files with 3 additions and 58977 deletions
|
@ -1,5 +1,4 @@
|
|||
#### directory structure
|
||||
`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)
|
||||
|
@ -36,9 +35,7 @@ against)
|
|||
2. GtkApplication glue parses the cmdline and calls `static void open(GtkApplication *app, GFile** files, gint nfiles, const gchar* hint, struct jni_callback_data *d)`
|
||||
3. `static void open(GtkApplication *app, GFile** files, gint nfiles, const gchar* hint, struct jni_callback_data *d)`:
|
||||
1. constructs the classpath from the following:
|
||||
- the path to api-impl.jar (contains the following, renamed to classes{2}.dex so that art loads them)
|
||||
- `hax_arsc_parser.dex` is dalvik bytecode implementing .arsc parsing duties (to be replaced by C code eventually)
|
||||
- `hax.dex` contains all the implementations of android framework functions
|
||||
- the path to api-impl.jar (contains all the implementations of android framework functions)
|
||||
- the path to the app's apk (passed to us on cmdline), making the bytecode within (and resources.arsc, which is currently the only other file read straight from the apk) available in classpath
|
||||
- the path to a microG apk, needed for apps with a dependency on GSF; this is specified after the app's apk so that the the app's apk is the first zip file in the classpath (needed for getting the right resources.arsc, TODO: ask for the classloader which loaded the activity that was specified on the cmdline)
|
||||
2. contructs other options (mainly library path) for and launches the dalvik virtual machine
|
||||
|
|
|
@ -157,21 +157,16 @@ executable('android-translation-layer', [
|
|||
],
|
||||
install_rpath: get_option('prefix') / get_option('libdir') / 'art:' + get_option('prefix') / get_option('libdir') / 'java/dex/android_translation_layer/natives')
|
||||
|
||||
# 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')
|
||||
hax_dex = custom_target('hax.dex', build_by_default: true, input: [hax_jar], output: ['classes.dex'],
|
||||
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_lib_dex], output: ['api-impl.jar'],
|
||||
custom_target('api-impl.jar', build_by_default: true, input: [hax_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_lib_dex])
|
||||
command: ['jar', '-cvf', join_paths(builddir_base, 'api-impl.jar'), '-C', builddir_base, hax_dex])
|
||||
|
||||
#framework-res.apk
|
||||
subdir('res')
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
|
@ -1,6 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,565 +0,0 @@
|
|||
/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE. */
|
||||
|
||||
package com.android.org.kxml2.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import org.xmlpull.v1.*;
|
||||
|
||||
public class KXmlSerializer implements XmlSerializer {
|
||||
|
||||
private static final int BUFFER_LEN = 8192;
|
||||
private final char[] mText = new char[BUFFER_LEN];
|
||||
private int mPos;
|
||||
private Writer writer;
|
||||
private boolean pending;
|
||||
private int auto;
|
||||
private int depth;
|
||||
private String[] elementStack = new String[12];
|
||||
private int[] nspCounts = new int[4];
|
||||
private String[] nspStack = new String[8];
|
||||
private boolean[] indent = new boolean[4];
|
||||
private boolean firstAttributeWritten;
|
||||
private int indentAttributeReference;
|
||||
private boolean unicode;
|
||||
private String encoding;
|
||||
|
||||
private void append(char c) throws IOException {
|
||||
if(mPos >= BUFFER_LEN){
|
||||
flushBuffer();
|
||||
}
|
||||
mText[mPos++] = c;
|
||||
}
|
||||
|
||||
private void append(String str, int i, int length) throws IOException {
|
||||
while (length > 0){
|
||||
if(mPos == BUFFER_LEN){
|
||||
flushBuffer();
|
||||
}
|
||||
int batch = BUFFER_LEN - mPos;
|
||||
if(batch > length){
|
||||
batch = length;
|
||||
}
|
||||
str.getChars(i, i + batch, mText, mPos);
|
||||
i += batch;
|
||||
length -= batch;
|
||||
mPos += batch;
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSpace(int length) throws IOException {
|
||||
while (length > 0){
|
||||
if(mPos == BUFFER_LEN){
|
||||
flushBuffer();
|
||||
}
|
||||
int batch = BUFFER_LEN - mPos;
|
||||
if(batch > length){
|
||||
batch = length;
|
||||
}
|
||||
Arrays.fill(mText, mPos, mPos + batch, ' ');
|
||||
length -= batch;
|
||||
mPos += batch;
|
||||
}
|
||||
}
|
||||
|
||||
private void append(String str) throws IOException {
|
||||
append(str, 0, str.length());
|
||||
}
|
||||
|
||||
private void flushBuffer() throws IOException {
|
||||
if(mPos > 0){
|
||||
writer.write(mText, 0, mPos);
|
||||
writer.flush();
|
||||
mPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void check(boolean close) throws IOException {
|
||||
if(!pending)
|
||||
return;
|
||||
|
||||
depth++;
|
||||
pending = false;
|
||||
|
||||
if(indent.length <= depth){
|
||||
boolean[] hlp = new boolean[depth + 4];
|
||||
System.arraycopy(indent, 0, hlp, 0, depth);
|
||||
indent = hlp;
|
||||
}
|
||||
indent[depth] = indent[depth - 1];
|
||||
|
||||
for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++){
|
||||
append(" xmlns");
|
||||
if(!nspStack[i * 2].isEmpty()){
|
||||
append(':');
|
||||
append(nspStack[i * 2]);
|
||||
}
|
||||
else if(getNamespace().isEmpty() && !nspStack[i * 2 + 1].isEmpty())
|
||||
throw new IllegalStateException("Cannot set default namespace for elements in no namespace");
|
||||
append("=\"");
|
||||
writeEscaped(nspStack[i * 2 + 1], '"');
|
||||
append('"');
|
||||
}
|
||||
|
||||
if(nspCounts.length <= depth + 1){
|
||||
int[] hlp = new int[depth + 8];
|
||||
System.arraycopy(nspCounts, 0, hlp, 0, depth + 1);
|
||||
nspCounts = hlp;
|
||||
}
|
||||
|
||||
nspCounts[depth + 1] = nspCounts[depth];
|
||||
if(close){
|
||||
append(" />");
|
||||
} else {
|
||||
append('>');
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEscaped(String s, int quot) throws IOException {
|
||||
for (int i = 0; i < s.length(); i++){
|
||||
char c = s.charAt(i);
|
||||
switch (c){
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
if(quot == -1)
|
||||
append(c);
|
||||
else
|
||||
append("&#"+((int) c)+';');
|
||||
break;
|
||||
case '&' :
|
||||
append("&");
|
||||
break;
|
||||
case '>' :
|
||||
append(">");
|
||||
break;
|
||||
case '<' :
|
||||
append("<");
|
||||
break;
|
||||
default:
|
||||
if(c == quot){
|
||||
append(c == '"' ? """ : "'");
|
||||
break;
|
||||
}
|
||||
boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
|
||||
if(allowedInXml){
|
||||
if(unicode || c < 127){
|
||||
append(c);
|
||||
} else {
|
||||
append("&#" + ((int) c) + ";");
|
||||
}
|
||||
} else if(Character.isHighSurrogate(c) && i < s.length() - 1){
|
||||
writeSurrogate(c, s.charAt(i + 1));
|
||||
++i;
|
||||
} else {
|
||||
reportInvalidCharacter(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void reportInvalidCharacter(char ch){
|
||||
throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")");
|
||||
}
|
||||
@Override
|
||||
public void docdecl(String dd) throws IOException {
|
||||
append("<!DOCTYPE");
|
||||
append(dd);
|
||||
append('>');
|
||||
}
|
||||
@Override
|
||||
public void endDocument() throws IOException {
|
||||
while (depth > 0){
|
||||
endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]);
|
||||
}
|
||||
flush();
|
||||
}
|
||||
@Override
|
||||
public void entityRef(String name) throws IOException {
|
||||
check(false);
|
||||
append('&');
|
||||
append(name);
|
||||
append(';');
|
||||
}
|
||||
@Override
|
||||
public boolean getFeature(String name){
|
||||
return "http://xmlpull.org/v1/doc/features.html#indent-output"
|
||||
.equals(name) && indent[depth];
|
||||
}
|
||||
@Override
|
||||
public String getPrefix(String namespace, boolean create){
|
||||
try {
|
||||
return getPrefix(namespace, false, create);
|
||||
}
|
||||
catch (IOException e){
|
||||
throw new RuntimeException(e.toString());
|
||||
}
|
||||
}
|
||||
private String getPrefix(String namespace, boolean includeDefault, boolean create)
|
||||
throws IOException {
|
||||
int[] nspCounts = this.nspCounts;
|
||||
int depth = this.depth;
|
||||
String[] nspStack = this.nspStack;
|
||||
|
||||
for (int i = nspCounts[depth + 1] * 2 - 2; i >= 0;i -= 2){
|
||||
if(nspStack[i + 1].equals(namespace)
|
||||
&& (includeDefault
|
||||
|| !nspStack[i].isEmpty())){
|
||||
String cand = nspStack[i];
|
||||
for (int j = i + 2; j < nspCounts[depth + 1] * 2; j++){
|
||||
if(nspStack[j].equals(cand)){
|
||||
cand = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(cand != null){
|
||||
return cand;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!create){
|
||||
return null;
|
||||
}
|
||||
|
||||
String prefix;
|
||||
|
||||
if(namespace.isEmpty()) {
|
||||
prefix = "";
|
||||
}else {
|
||||
do {
|
||||
prefix = "n" + (auto++);
|
||||
for (int i = nspCounts[depth + 1] * 2 - 2;i >= 0;i -= 2){
|
||||
if(prefix.equals(nspStack[i])){
|
||||
prefix = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (prefix == null);
|
||||
}
|
||||
|
||||
boolean p = pending;
|
||||
pending = false;
|
||||
setPrefix(prefix, namespace);
|
||||
pending = p;
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name){
|
||||
throw new RuntimeException("Unsupported property: "+name);
|
||||
}
|
||||
@Override
|
||||
public void ignorableWhitespace(String s) throws IOException {
|
||||
text(s);
|
||||
}
|
||||
@Override
|
||||
public void setFeature(String name, boolean value){
|
||||
if("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)){
|
||||
indent[depth] = value;
|
||||
firstAttributeWritten = false;
|
||||
}else {
|
||||
throw new RuntimeException("Unsupported Feature: "+name);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setProperty(String name, Object value){
|
||||
throw new RuntimeException("Unsupported Property:" + value);
|
||||
}
|
||||
@Override
|
||||
public void setPrefix(String prefix, String namespace)
|
||||
throws IOException {
|
||||
|
||||
check(false);
|
||||
if(prefix == null) {
|
||||
prefix = "";
|
||||
}
|
||||
if(namespace == null) {
|
||||
namespace = "";
|
||||
}
|
||||
String defined = getPrefix(namespace, true, false);
|
||||
if(prefix.equals(defined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int pos = (nspCounts[depth + 1]++) << 1;
|
||||
|
||||
if(nspStack.length < pos + 1){
|
||||
String[] hlp = new String[nspStack.length + 16];
|
||||
System.arraycopy(nspStack, 0, hlp, 0, pos);
|
||||
nspStack = hlp;
|
||||
}
|
||||
|
||||
nspStack[pos++] = prefix;
|
||||
nspStack[pos] = namespace;
|
||||
}
|
||||
|
||||
public void setOutput(Writer writer){
|
||||
this.writer = writer;
|
||||
nspCounts[0] = 2;
|
||||
nspCounts[1] = 2;
|
||||
nspStack[0] = "";
|
||||
nspStack[1] = "";
|
||||
nspStack[2] = "xml";
|
||||
nspStack[3] = "http://www.w3.org/XML/1998/namespace";
|
||||
pending = false;
|
||||
auto = 0;
|
||||
depth = 0;
|
||||
|
||||
unicode = false;
|
||||
}
|
||||
@Override
|
||||
public void setOutput(OutputStream os, String encoding)
|
||||
throws IOException {
|
||||
if(os == null) {
|
||||
throw new IllegalArgumentException("os == null");
|
||||
}
|
||||
setOutput(encoding == null
|
||||
? new OutputStreamWriter(os)
|
||||
: new OutputStreamWriter(os, encoding));
|
||||
this.encoding = encoding;
|
||||
if(encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")){
|
||||
unicode = true;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void startDocument(String encoding, Boolean standalone) throws IOException {
|
||||
append("<?xml version='1.0' ");
|
||||
|
||||
if(encoding != null){
|
||||
this.encoding = encoding;
|
||||
if(encoding.toLowerCase(Locale.US).startsWith("utf")){
|
||||
unicode = true;
|
||||
}
|
||||
}
|
||||
if(this.encoding != null){
|
||||
append("encoding='");
|
||||
append(this.encoding);
|
||||
append("' ");
|
||||
}
|
||||
if(standalone != null){
|
||||
append("standalone='");
|
||||
append(standalone ? "yes" : "no");
|
||||
append("' ");
|
||||
}
|
||||
append("?>");
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer startTag(String namespace, String name)
|
||||
throws IOException {
|
||||
check(false);
|
||||
firstAttributeWritten = false;
|
||||
indentAttributeReference = 0;
|
||||
if(indent[depth]){
|
||||
append('\r');
|
||||
append('\n');
|
||||
int spaceLength = 2 * depth;
|
||||
appendSpace(spaceLength);
|
||||
indentAttributeReference = spaceLength;
|
||||
}
|
||||
int esp = depth * 3;
|
||||
if(elementStack.length < esp + 3){
|
||||
String[] hlp = new String[elementStack.length + 12];
|
||||
System.arraycopy(elementStack, 0, hlp, 0, esp);
|
||||
elementStack = hlp;
|
||||
}
|
||||
String prefix = namespace == null?
|
||||
"" : getPrefix(namespace, true, true);
|
||||
|
||||
if(namespace != null && namespace.isEmpty()){
|
||||
for (int i = nspCounts[depth]; i < nspCounts[depth + 1]; i++){
|
||||
if(nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()){
|
||||
throw new IllegalStateException("Cannot set default namespace for elements in no namespace");
|
||||
}
|
||||
}
|
||||
}
|
||||
elementStack[esp++] = namespace;
|
||||
elementStack[esp++] = prefix;
|
||||
elementStack[esp] = name;
|
||||
append('<');
|
||||
indentAttributeReference += 1;
|
||||
if(!prefix.isEmpty()){
|
||||
append(prefix);
|
||||
append(':');
|
||||
indentAttributeReference += prefix.length() + 1;
|
||||
}
|
||||
append(name);
|
||||
int len = name.length();
|
||||
if(len > 20){
|
||||
len = 20;
|
||||
}
|
||||
indentAttributeReference += len;
|
||||
pending = true;
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer attribute(String namespace, String name, String value)
|
||||
throws IOException {
|
||||
if(!pending) {
|
||||
throw new IllegalStateException("illegal position for attribute");
|
||||
}
|
||||
if(namespace == null) {
|
||||
namespace = "";
|
||||
}
|
||||
String prefix = namespace.isEmpty() ?
|
||||
"" : getPrefix(namespace, false, true);
|
||||
attributeIndent();
|
||||
append(' ');
|
||||
if(!prefix.isEmpty()){
|
||||
append(prefix);
|
||||
append(':');
|
||||
}
|
||||
append(name);
|
||||
append('=');
|
||||
char q = value.indexOf('"') == -1 ? '"' : '\'';
|
||||
append(q);
|
||||
writeEscaped(value, q);
|
||||
append(q);
|
||||
firstAttributeWritten = true;
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
check(false);
|
||||
flushBuffer();
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer endTag(String namespace, String name)throws IOException {
|
||||
if(!pending) {
|
||||
depth--;
|
||||
}
|
||||
if((namespace == null
|
||||
&& elementStack[depth * 3] != null)
|
||||
|| (namespace != null
|
||||
&& !namespace.equals(elementStack[depth * 3]))
|
||||
|| !elementStack[depth * 3 + 2].equals(name)) {
|
||||
throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start");
|
||||
}
|
||||
|
||||
if(pending){
|
||||
check(true);
|
||||
depth--;
|
||||
}
|
||||
else {
|
||||
if(indent[depth + 1]){
|
||||
append('\r');
|
||||
append('\n');
|
||||
appendSpace(2 * depth);
|
||||
}
|
||||
append("</");
|
||||
String prefix = elementStack[depth * 3 + 1];
|
||||
if(!prefix.isEmpty()){
|
||||
append(prefix);
|
||||
append(':');
|
||||
}
|
||||
append(name);
|
||||
append('>');
|
||||
}
|
||||
|
||||
nspCounts[depth + 1] = nspCounts[depth];
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public String getNamespace(){
|
||||
return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3];
|
||||
}
|
||||
@Override
|
||||
public String getName(){
|
||||
return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1];
|
||||
}
|
||||
@Override
|
||||
public int getDepth(){
|
||||
return pending ? depth + 1 : depth;
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer text(String text) throws IOException {
|
||||
check(false);
|
||||
indent[depth] = false;
|
||||
writeEscaped(text, -1);
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer text(char[] text, int start, int len)
|
||||
throws IOException {
|
||||
text(new String(text, start, len));
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public void cdsect(String data) throws IOException {
|
||||
check(false);
|
||||
data = data.replace("]]>", "]]]]><![CDATA[>");
|
||||
append("<![CDATA[");
|
||||
for (int i = 0; i < data.length(); ++i){
|
||||
char ch = data.charAt(i);
|
||||
boolean allowedInCdata = (ch >= 0x20 && ch <= 0xd7ff) ||
|
||||
(ch == '\t' || ch == '\n' || ch == '\r') ||
|
||||
(ch >= 0xe000 && ch <= 0xfffd);
|
||||
if(allowedInCdata){
|
||||
append(ch);
|
||||
} else if(Character.isHighSurrogate(ch) && i < data.length() - 1){
|
||||
// Character entities aren't valid in CDATA, so break out for this.
|
||||
append("]]>");
|
||||
writeSurrogate(ch, data.charAt(++i));
|
||||
append("<![CDATA[");
|
||||
} else {
|
||||
reportInvalidCharacter(ch);
|
||||
}
|
||||
}
|
||||
append("]]>");
|
||||
}
|
||||
|
||||
private void writeSurrogate(char high, char low) throws IOException {
|
||||
if(!Character.isLowSurrogate(low)){
|
||||
throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) +
|
||||
" U+" + Integer.toHexString((int) low) + ")");
|
||||
}
|
||||
int codePoint = Character.toCodePoint(high, low);
|
||||
append("&#" + codePoint + ";");
|
||||
}
|
||||
@Override
|
||||
public void comment(String comment) throws IOException {
|
||||
check(false);
|
||||
append("<!--");
|
||||
append(comment);
|
||||
append("-->");
|
||||
}
|
||||
@Override
|
||||
public void processingInstruction(String pi)
|
||||
throws IOException {
|
||||
check(false);
|
||||
append("<?");
|
||||
append(pi);
|
||||
append("?>");
|
||||
}
|
||||
|
||||
private void attributeIndent() throws IOException {
|
||||
if(!firstAttributeWritten || !indent[depth]){
|
||||
return;
|
||||
}
|
||||
int length = this.indentAttributeReference;
|
||||
if(length <= 0){
|
||||
return;
|
||||
}
|
||||
append('\r');
|
||||
append('\n');
|
||||
appendSpace(length);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
public interface APKLogger {
|
||||
void logMessage(String msg);
|
||||
void logError(String msg, Throwable tr);
|
||||
void logVerbose(String msg);
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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";
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,838 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.*;
|
||||
import com.reandroid.archive2.Archive;
|
||||
import com.reandroid.archive2.block.ApkSignatureBlock;
|
||||
import com.reandroid.archive2.writer.ApkWriter;
|
||||
import com.reandroid.arsc.ApkFile;
|
||||
import com.reandroid.arsc.array.PackageArray;
|
||||
import com.reandroid.arsc.chunk.Chunk;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.container.SpecTypePair;
|
||||
import com.reandroid.arsc.decoder.Decoder;
|
||||
import com.reandroid.arsc.group.StringGroup;
|
||||
import com.reandroid.arsc.item.TableString;
|
||||
import com.reandroid.arsc.pool.TableStringPool;
|
||||
import com.reandroid.arsc.util.FrameworkTable;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ResConfig;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
import com.reandroid.xml.XMLException;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class ApkModule implements ApkFile {
|
||||
private final String moduleName;
|
||||
private final APKArchive apkArchive;
|
||||
private boolean loadDefaultFramework = true;
|
||||
private boolean mDisableLoadFramework = false;
|
||||
private TableBlock mTableBlock;
|
||||
private AndroidManifestBlock mManifestBlock;
|
||||
private final UncompressedFiles mUncompressedFiles;
|
||||
private APKLogger apkLogger;
|
||||
private Decoder mDecoder;
|
||||
private ApkType mApkType;
|
||||
private ApkSignatureBlock apkSignatureBlock;
|
||||
private Integer preferredFramework;
|
||||
|
||||
public ApkModule(String moduleName, APKArchive apkArchive){
|
||||
this.moduleName=moduleName;
|
||||
this.apkArchive=apkArchive;
|
||||
this.mUncompressedFiles=new UncompressedFiles();
|
||||
this.mUncompressedFiles.addPath(apkArchive);
|
||||
}
|
||||
|
||||
public ApkSignatureBlock getApkSignatureBlock() {
|
||||
return apkSignatureBlock;
|
||||
}
|
||||
public void setApkSignatureBlock(ApkSignatureBlock apkSignatureBlock) {
|
||||
this.apkSignatureBlock = apkSignatureBlock;
|
||||
}
|
||||
|
||||
public boolean hasSignatureBlock(){
|
||||
return getApkSignatureBlock() != null;
|
||||
}
|
||||
|
||||
public void dumpSignatureInfoFiles(File directory) throws IOException{
|
||||
ApkSignatureBlock apkSignatureBlock = getApkSignatureBlock();
|
||||
if(apkSignatureBlock == null){
|
||||
throw new IOException("Don't have signature block");
|
||||
}
|
||||
apkSignatureBlock.writeSplitRawToDirectory(directory);
|
||||
}
|
||||
public void dumpSignatureBlock(File file) throws IOException{
|
||||
ApkSignatureBlock apkSignatureBlock = getApkSignatureBlock();
|
||||
if(apkSignatureBlock == null){
|
||||
throw new IOException("Don't have signature block");
|
||||
}
|
||||
apkSignatureBlock.writeRaw(file);
|
||||
}
|
||||
|
||||
public void scanSignatureInfoFiles(File directory) throws IOException{
|
||||
if(!directory.isDirectory()){
|
||||
throw new IOException("No such directory: " + directory);
|
||||
}
|
||||
ApkSignatureBlock apkSignatureBlock = this.apkSignatureBlock;
|
||||
if(apkSignatureBlock == null){
|
||||
apkSignatureBlock = new ApkSignatureBlock();
|
||||
}
|
||||
apkSignatureBlock.scanSplitFiles(directory);
|
||||
setApkSignatureBlock(apkSignatureBlock);
|
||||
}
|
||||
public void loadSignatureBlock(File file) throws IOException{
|
||||
if(!file.isFile()){
|
||||
throw new IOException("No such file: " + file);
|
||||
}
|
||||
ApkSignatureBlock apkSignatureBlock = this.apkSignatureBlock;
|
||||
if(apkSignatureBlock == null){
|
||||
apkSignatureBlock = new ApkSignatureBlock();
|
||||
}
|
||||
apkSignatureBlock.read(file);
|
||||
setApkSignatureBlock(apkSignatureBlock);
|
||||
}
|
||||
|
||||
public String getSplit(){
|
||||
if(!hasAndroidManifestBlock()){
|
||||
return null;
|
||||
}
|
||||
return getAndroidManifestBlock().getSplit();
|
||||
}
|
||||
public FrameworkApk initializeAndroidFramework(TableBlock tableBlock, Integer version) throws IOException {
|
||||
if(tableBlock == null || isAndroid(tableBlock)){
|
||||
return null;
|
||||
}
|
||||
List<TableBlock> frameWorkList = tableBlock.getFrameWorks();
|
||||
for(TableBlock frameWork:frameWorkList){
|
||||
if(isAndroid(frameWork)){
|
||||
ApkFile apkFile = frameWork.getApkFile();
|
||||
if(apkFile instanceof FrameworkApk){
|
||||
return (FrameworkApk) apkFile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
logMessage("Initializing android framework ...");
|
||||
FrameworkApk frameworkApk;
|
||||
if(version==null){
|
||||
logMessage("Can not read framework version, loading latest");
|
||||
frameworkApk = AndroidFrameworks.getLatest();
|
||||
}else {
|
||||
logMessage("Loading android framework for version: " + version);
|
||||
frameworkApk = AndroidFrameworks.getBestMatch(version);
|
||||
}
|
||||
FrameworkTable frameworkTable = frameworkApk.getTableBlock();
|
||||
tableBlock.addFramework(frameworkTable);
|
||||
logMessage("Initialized framework: "+frameworkApk.getName());
|
||||
return frameworkApk;
|
||||
}
|
||||
private boolean isAndroid(TableBlock tableBlock){
|
||||
if(tableBlock instanceof FrameworkTable){
|
||||
FrameworkTable frameworkTable = (FrameworkTable) tableBlock;
|
||||
return frameworkTable.isAndroid();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public FrameworkApk initializeAndroidFramework(XmlPullParser parser) throws IOException {
|
||||
Map<String, String> manifestAttributes;
|
||||
try {
|
||||
manifestAttributes = XmlHelper.readAttributes(parser, AndroidManifestBlock.TAG_manifest);
|
||||
} catch (XmlPullParserException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
if(manifestAttributes == null){
|
||||
throw new IOException("Invalid AndroidManifest, missing element: '"
|
||||
+ AndroidManifestBlock.TAG_manifest + "'");
|
||||
}
|
||||
return initializeAndroidFramework(manifestAttributes);
|
||||
}
|
||||
public FrameworkApk initializeAndroidFramework(Map<String, String> manifestAttributes) throws IOException {
|
||||
String coreApp = manifestAttributes.get(AndroidManifestBlock.NAME_coreApp);
|
||||
String packageName = manifestAttributes.get(AndroidManifestBlock.NAME_PACKAGE);
|
||||
if("true".equals(coreApp) && "android".equals(packageName)){
|
||||
logMessage("Looks framework itself, skip loading frameworks");
|
||||
return null;
|
||||
}
|
||||
String compileSdkVersion = manifestAttributes.get(AndroidManifestBlock.NAME_compileSdkVersion);
|
||||
if(compileSdkVersion == null){
|
||||
logMessage("Missing attribute: '" + AndroidManifestBlock.NAME_compileSdkVersion + "', skip loading frameworks");
|
||||
return null;
|
||||
}
|
||||
int version;
|
||||
try{
|
||||
version = Integer.parseInt(compileSdkVersion);
|
||||
}catch (NumberFormatException exception){
|
||||
logMessage("NumberFormatException on reading: '"
|
||||
+ AndroidManifestBlock.NAME_compileSdkVersion + "=\""
|
||||
+ compileSdkVersion +"\"' : " + exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
TableBlock tableBlock = getTableBlock(false);
|
||||
return initializeAndroidFramework(tableBlock, version);
|
||||
}
|
||||
public FrameworkApk initializeAndroidFramework(XMLDocument xmlDocument) throws IOException {
|
||||
TableBlock tableBlock = getTableBlock(false);
|
||||
if(isAndroidCoreApp(xmlDocument)){
|
||||
logMessage("Looks framework itself, skip loading frameworks");
|
||||
return null;
|
||||
}
|
||||
Integer version = readCompileVersionCode(xmlDocument);
|
||||
return initializeAndroidFramework(tableBlock, version);
|
||||
}
|
||||
private boolean isAndroidCoreApp(XMLDocument manifestDocument){
|
||||
XMLElement root = manifestDocument.getDocumentElement();
|
||||
if(root == null){
|
||||
return false;
|
||||
}
|
||||
if(!"android".equals(root.getAttributeValue("package"))){
|
||||
return false;
|
||||
}
|
||||
String coreApp = root.getAttributeValue("coreApp");
|
||||
return "true".equals(coreApp);
|
||||
}
|
||||
private Integer readCompileVersionCode(XMLDocument manifestDocument) {
|
||||
XMLElement root = manifestDocument.getDocumentElement();
|
||||
String versionString = readVersionCodeString(root);
|
||||
if(versionString==null){
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
return Integer.parseInt(versionString);
|
||||
}catch (NumberFormatException exception){
|
||||
logMessage("NumberFormatException on manifest version reading: '"
|
||||
+versionString+"': "+exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private String readVersionCodeString(XMLElement manifestRoot){
|
||||
String versionString = manifestRoot.getAttributeValue("android:compileSdkVersion");
|
||||
if(versionString!=null){
|
||||
return versionString;
|
||||
}
|
||||
versionString = manifestRoot.getAttributeValue("platformBuildVersionCode");
|
||||
if(versionString!=null){
|
||||
return versionString;
|
||||
}
|
||||
for(XMLElement element:manifestRoot.listChildElements()){
|
||||
if(AndroidManifestBlock.TAG_uses_sdk.equals(element.getTagName())){
|
||||
versionString = element.getAttributeValue("android:targetSdkVersion");
|
||||
if(versionString!=null){
|
||||
return versionString;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setPreferredFramework(Integer version) throws IOException {
|
||||
if(version!=null && version.equals(preferredFramework)){
|
||||
return;
|
||||
}
|
||||
this.preferredFramework = version;
|
||||
if(version == null || mTableBlock==null){
|
||||
return;
|
||||
}
|
||||
logMessage("Initializing preferred framework: " + version);
|
||||
mTableBlock.clearFrameworks();
|
||||
FrameworkApk frameworkApk = AndroidFrameworks.getBestMatch(version);
|
||||
AndroidFrameworks.setCurrent(frameworkApk);
|
||||
mTableBlock.addFramework(frameworkApk.getTableBlock());
|
||||
logMessage("Initialized framework: " + frameworkApk.getVersionCode());
|
||||
}
|
||||
|
||||
public Integer getAndroidFrameworkVersion(){
|
||||
if(preferredFramework != null){
|
||||
return preferredFramework;
|
||||
}
|
||||
if(!hasAndroidManifestBlock()){
|
||||
return null;
|
||||
}
|
||||
AndroidManifestBlock manifestBlock = getAndroidManifestBlock();
|
||||
Integer version = manifestBlock.getCompileSdkVersion();
|
||||
if(version == null){
|
||||
version = manifestBlock.getPlatformBuildVersionCode();
|
||||
}
|
||||
if(version == null){
|
||||
version = manifestBlock.getTargetSdkVersion();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
public void removeResFilesWithEntry(int resourceId) {
|
||||
removeResFilesWithEntry(resourceId, null, true);
|
||||
}
|
||||
public void removeResFilesWithEntry(int resourceId, ResConfig resConfig, boolean trimEntryArray) {
|
||||
List<Entry> removedList = removeResFiles(resourceId, resConfig);
|
||||
SpecTypePair specTypePair = null;
|
||||
for(Entry entry:removedList){
|
||||
if(entry == null || entry.isNull()){
|
||||
continue;
|
||||
}
|
||||
if(trimEntryArray && specTypePair==null){
|
||||
specTypePair = entry.getTypeBlock().getParentSpecTypePair();
|
||||
}
|
||||
entry.setNull(true);
|
||||
}
|
||||
if(specTypePair!=null){
|
||||
specTypePair.removeNullEntries(resourceId);
|
||||
}
|
||||
}
|
||||
public List<Entry> removeResFiles(int resourceId) {
|
||||
return removeResFiles(resourceId, null);
|
||||
}
|
||||
public List<Entry> removeResFiles(int resourceId, ResConfig resConfig) {
|
||||
List<Entry> results = new ArrayList<>();
|
||||
if(resourceId == 0 && resConfig==null){
|
||||
return results;
|
||||
}
|
||||
List<ResFile> resFileList = listResFiles(resourceId, resConfig);
|
||||
APKArchive archive = getApkArchive();
|
||||
for(ResFile resFile:resFileList){
|
||||
results.addAll(resFile.getEntryList());
|
||||
String path = resFile.getFilePath();
|
||||
archive.remove(path);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public XMLDocument decodeXMLFile(String path) throws IOException, XMLException {
|
||||
ResXmlDocument resXmlDocument = loadResXmlDocument(path);
|
||||
AndroidManifestBlock manifestBlock = getAndroidManifestBlock();
|
||||
int pkgId = manifestBlock.guessCurrentPackageId();
|
||||
return resXmlDocument.decodeToXml(getTableBlock(), pkgId);
|
||||
}
|
||||
public List<DexFileInputSource> listDexFiles(){
|
||||
List<DexFileInputSource> results=new ArrayList<>();
|
||||
for(InputSource source:getApkArchive().listInputSources()){
|
||||
if(DexFileInputSource.isDexName(source.getAlias())){
|
||||
results.add(new DexFileInputSource(source.getAlias(), source));
|
||||
}
|
||||
}
|
||||
DexFileInputSource.sort(results);
|
||||
return results;
|
||||
}
|
||||
public boolean isBaseModule(){
|
||||
if(!hasAndroidManifestBlock()){
|
||||
return false;
|
||||
}
|
||||
AndroidManifestBlock manifestBlock;
|
||||
try {
|
||||
manifestBlock=getAndroidManifestBlock();
|
||||
return manifestBlock.getMainActivity()!=null;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public String getModuleName(){
|
||||
return moduleName;
|
||||
}
|
||||
public void writeApk(File file) throws IOException {
|
||||
writeApk(file, null);
|
||||
}
|
||||
public void writeApk(File file, WriteProgress progress) throws IOException {
|
||||
writeApk(file, progress, null);
|
||||
}
|
||||
public void writeApk(File file, WriteProgress progress, WriteInterceptor interceptor) throws IOException {
|
||||
APKArchive archive = getApkArchive();
|
||||
UncompressedFiles uf = getUncompressedFiles();
|
||||
uf.apply(archive);
|
||||
ApkWriter apkWriter = new ApkWriter(file, archive.listInputSources());
|
||||
apkWriter.setAPKLogger(getApkLogger());
|
||||
apkWriter.setWriteProgress(progress);
|
||||
apkWriter.setApkSignatureBlock(getApkSignatureBlock());
|
||||
apkWriter.write();
|
||||
apkWriter.close();
|
||||
}
|
||||
public void uncompressNonXmlResFiles() {
|
||||
for(ResFile resFile:listResFiles()){
|
||||
if(resFile.isBinaryXml()){
|
||||
continue;
|
||||
}
|
||||
resFile.getInputSource().setMethod(ZipEntry.STORED);
|
||||
}
|
||||
}
|
||||
public UncompressedFiles getUncompressedFiles(){
|
||||
return mUncompressedFiles;
|
||||
}
|
||||
public void removeDir(String dirName){
|
||||
getApkArchive().removeDir(dirName);
|
||||
}
|
||||
public void validateResourcesDir() {
|
||||
List<ResFile> resFileList = listResFiles();
|
||||
Set<String> existPaths=new HashSet<>();
|
||||
List<InputSource> sourceList = getApkArchive().listInputSources();
|
||||
for(InputSource inputSource:sourceList){
|
||||
existPaths.add(inputSource.getAlias());
|
||||
}
|
||||
for(ResFile resFile:resFileList){
|
||||
String path=resFile.getFilePath();
|
||||
String pathNew=resFile.validateTypeDirectoryName();
|
||||
if(pathNew==null || pathNew.equals(path)){
|
||||
continue;
|
||||
}
|
||||
if(existPaths.contains(pathNew)){
|
||||
continue;
|
||||
}
|
||||
existPaths.remove(path);
|
||||
existPaths.add(pathNew);
|
||||
resFile.setFilePath(pathNew);
|
||||
if(resFile.getInputSource().getMethod() == ZipEntry.STORED){
|
||||
getUncompressedFiles().replacePath(path, pathNew);
|
||||
}
|
||||
logVerbose("Dir validated: '"+path+"' -> '"+pathNew+"'");
|
||||
}
|
||||
TableStringPool stringPool= getTableBlock().getStringPool();
|
||||
stringPool.refreshUniqueIdMap();
|
||||
getTableBlock().refresh();
|
||||
}
|
||||
public void setResourcesRootDir(String dirName) {
|
||||
List<ResFile> resFileList = listResFiles();
|
||||
Set<String> existPaths=new HashSet<>();
|
||||
List<InputSource> sourceList = getApkArchive().listInputSources();
|
||||
for(InputSource inputSource:sourceList){
|
||||
existPaths.add(inputSource.getAlias());
|
||||
}
|
||||
for(ResFile resFile:resFileList){
|
||||
String path=resFile.getFilePath();
|
||||
String pathNew=ApkUtil.replaceRootDir(path, dirName);
|
||||
if(existPaths.contains(pathNew)){
|
||||
continue;
|
||||
}
|
||||
existPaths.remove(path);
|
||||
existPaths.add(pathNew);
|
||||
resFile.setFilePath(pathNew);
|
||||
if(resFile.getInputSource().getMethod() == ZipEntry.STORED){
|
||||
getUncompressedFiles().replacePath(path, pathNew);
|
||||
}
|
||||
logVerbose("Root changed: '"+path+"' -> '"+pathNew+"'");
|
||||
}
|
||||
TableStringPool stringPool= getTableBlock().getStringPool();
|
||||
stringPool.refreshUniqueIdMap();
|
||||
getTableBlock().refresh();
|
||||
}
|
||||
public List<ResFile> listResFiles() {
|
||||
return listResFiles(0, null);
|
||||
}
|
||||
public List<ResFile> listResFiles(int resourceId, ResConfig resConfig) {
|
||||
List<ResFile> results=new ArrayList<>();
|
||||
TableBlock tableBlock=getTableBlock();
|
||||
if (tableBlock==null){
|
||||
return results;
|
||||
}
|
||||
TableStringPool stringPool= tableBlock.getStringPool();
|
||||
for(InputSource inputSource:getApkArchive().listInputSources()){
|
||||
String name=inputSource.getAlias();
|
||||
StringGroup<TableString> groupTableString = stringPool.get(name);
|
||||
if(groupTableString==null){
|
||||
continue;
|
||||
}
|
||||
for(TableString tableString:groupTableString.listItems()){
|
||||
List<Entry> entryList = filterResFileEntries(
|
||||
tableString.listReferencedResValueEntries(), resourceId, resConfig);
|
||||
if(entryList.size()==0){
|
||||
continue;
|
||||
}
|
||||
ResFile resFile = new ResFile(inputSource, entryList);
|
||||
results.add(resFile);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private List<Entry> filterResFileEntries(List<Entry> entryList, int resourceId, ResConfig resConfig){
|
||||
if(resourceId == 0 && resConfig == null || entryList.size()==0){
|
||||
return entryList;
|
||||
}
|
||||
List<Entry> results = new ArrayList<>();
|
||||
for(Entry entry:entryList){
|
||||
if(entry==null || entry.isNull()){
|
||||
continue;
|
||||
}
|
||||
if(resourceId!=0 && resourceId!=entry.getResourceId()){
|
||||
continue;
|
||||
}
|
||||
if(resConfig!=null && !resConfig.equals(entry.getResConfig())){
|
||||
continue;
|
||||
}
|
||||
results.add(entry);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public String getPackageName(){
|
||||
if(hasAndroidManifestBlock()){
|
||||
return getAndroidManifestBlock().getPackageName();
|
||||
}
|
||||
if(!hasTableBlock()){
|
||||
return null;
|
||||
}
|
||||
TableBlock tableBlock=getTableBlock();
|
||||
PackageArray pkgArray = tableBlock.getPackageArray();
|
||||
PackageBlock pkg = pkgArray.get(0);
|
||||
if(pkg==null){
|
||||
return null;
|
||||
}
|
||||
return pkg.getName();
|
||||
}
|
||||
public void setPackageName(String name) {
|
||||
String old=getPackageName();
|
||||
if(hasAndroidManifestBlock()){
|
||||
getAndroidManifestBlock().setPackageName(name);
|
||||
}
|
||||
if(!hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
TableBlock tableBlock=getTableBlock();
|
||||
PackageArray pkgArray = tableBlock.getPackageArray();
|
||||
for(PackageBlock pkg:pkgArray.listItems()){
|
||||
if(pkgArray.childesCount()==1){
|
||||
pkg.setName(name);
|
||||
continue;
|
||||
}
|
||||
String pkgName=pkg.getName();
|
||||
if(pkgName.startsWith(old)){
|
||||
pkgName=pkgName.replace(old, name);
|
||||
pkg.setName(pkgName);
|
||||
}
|
||||
}
|
||||
}
|
||||
public boolean hasAndroidManifestBlock(){
|
||||
return mManifestBlock!=null
|
||||
|| getApkArchive().getInputSource(AndroidManifestBlock.FILE_NAME)!=null;
|
||||
}
|
||||
public boolean hasTableBlock(){
|
||||
return mTableBlock!=null
|
||||
|| getApkArchive().getInputSource(TableBlock.FILE_NAME)!=null;
|
||||
}
|
||||
public void destroy(){
|
||||
getApkArchive().clear();
|
||||
AndroidManifestBlock manifestBlock = this.mManifestBlock;
|
||||
if(manifestBlock!=null){
|
||||
manifestBlock.destroy();
|
||||
this.mManifestBlock = null;
|
||||
}
|
||||
TableBlock tableBlock = this.mTableBlock;
|
||||
if(tableBlock!=null){
|
||||
tableBlock.destroy();
|
||||
this.mTableBlock = null;
|
||||
}
|
||||
}
|
||||
public void setManifest(AndroidManifestBlock manifestBlock){
|
||||
APKArchive archive = getApkArchive();
|
||||
if(manifestBlock==null){
|
||||
mManifestBlock = null;
|
||||
archive.remove(AndroidManifestBlock.FILE_NAME);
|
||||
return;
|
||||
}
|
||||
manifestBlock.setApkFile(this);
|
||||
BlockInputSource<AndroidManifestBlock> source =
|
||||
new BlockInputSource<>(AndroidManifestBlock.FILE_NAME, manifestBlock);
|
||||
archive.add(source);
|
||||
mManifestBlock = manifestBlock;
|
||||
}
|
||||
public void setTableBlock(TableBlock tableBlock){
|
||||
APKArchive archive = getApkArchive();
|
||||
if(tableBlock == null){
|
||||
mTableBlock = null;
|
||||
archive.remove(TableBlock.FILE_NAME);
|
||||
return;
|
||||
}
|
||||
tableBlock.setApkFile(this);
|
||||
BlockInputSource<TableBlock> source =
|
||||
new BlockInputSource<>(TableBlock.FILE_NAME, tableBlock);
|
||||
archive.add(source);
|
||||
source.setMethod(ZipEntry.STORED);
|
||||
getUncompressedFiles().addPath(source);
|
||||
mTableBlock = tableBlock;
|
||||
}
|
||||
@Override
|
||||
public AndroidManifestBlock getAndroidManifestBlock() {
|
||||
if(mManifestBlock!=null){
|
||||
return mManifestBlock;
|
||||
}
|
||||
APKArchive archive=getApkArchive();
|
||||
InputSource inputSource = archive.getInputSource(AndroidManifestBlock.FILE_NAME);
|
||||
if(inputSource==null){
|
||||
return null;
|
||||
}
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = inputSource.openStream();
|
||||
AndroidManifestBlock manifestBlock=AndroidManifestBlock.load(inputStream);
|
||||
inputStream.close();
|
||||
BlockInputSource<AndroidManifestBlock> blockInputSource=new BlockInputSource<>(inputSource.getName(),manifestBlock);
|
||||
blockInputSource.setSort(inputSource.getSort());
|
||||
blockInputSource.setMethod(inputSource.getMethod());
|
||||
archive.add(blockInputSource);
|
||||
manifestBlock.setApkFile(this);
|
||||
TableBlock tableBlock = this.mTableBlock;
|
||||
if(tableBlock != null){
|
||||
int packageId = manifestBlock.guessCurrentPackageId();
|
||||
if(packageId != 0){
|
||||
manifestBlock.setPackageBlock(tableBlock.pickOne(packageId));
|
||||
}else {
|
||||
manifestBlock.setPackageBlock(tableBlock.pickOne());
|
||||
}
|
||||
}
|
||||
mManifestBlock = manifestBlock;
|
||||
onManifestBlockLoaded(manifestBlock);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException(exception);
|
||||
}
|
||||
return mManifestBlock;
|
||||
}
|
||||
private void onManifestBlockLoaded(AndroidManifestBlock manifestBlock){
|
||||
initializeApkType(manifestBlock);
|
||||
}
|
||||
public TableBlock getTableBlock(boolean initFramework) {
|
||||
if(mTableBlock==null){
|
||||
if(!hasTableBlock()){
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
mTableBlock = loadTableBlock();
|
||||
if(initFramework && loadDefaultFramework){
|
||||
Integer version = getAndroidFrameworkVersion();
|
||||
initializeAndroidFramework(mTableBlock, version);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException(exception);
|
||||
}
|
||||
}
|
||||
return mTableBlock;
|
||||
}
|
||||
@Override
|
||||
public TableBlock getTableBlock() {
|
||||
return getTableBlock(!mDisableLoadFramework);
|
||||
}
|
||||
@Override
|
||||
public ResXmlDocument loadResXmlDocument(String path) throws IOException{
|
||||
InputSource inputSource = getApkArchive().getInputSource(path);
|
||||
if(inputSource==null){
|
||||
throw new FileNotFoundException("No such file in apk: " + path);
|
||||
}
|
||||
return loadResXmlDocument(inputSource);
|
||||
}
|
||||
public ResXmlDocument loadResXmlDocument(InputSource inputSource) throws IOException{
|
||||
ResXmlDocument resXmlDocument = new ResXmlDocument();
|
||||
resXmlDocument.setApkFile(this);
|
||||
resXmlDocument.readBytes(inputSource.openStream());
|
||||
return resXmlDocument;
|
||||
}
|
||||
@Override
|
||||
public Decoder getDecoder(){
|
||||
return mDecoder;
|
||||
}
|
||||
@Override
|
||||
public void setDecoder(Decoder decoder){
|
||||
this.mDecoder = decoder;
|
||||
}
|
||||
public ApkType getApkType(){
|
||||
if(mApkType!=null){
|
||||
return mApkType;
|
||||
}
|
||||
return initializeApkType(mManifestBlock);
|
||||
}
|
||||
public void setApkType(ApkType apkType){
|
||||
this.mApkType = apkType;
|
||||
}
|
||||
private ApkType initializeApkType(AndroidManifestBlock manifestBlock){
|
||||
if(mApkType!=null){
|
||||
return mApkType;
|
||||
}
|
||||
ApkType apkType = null;
|
||||
if(manifestBlock!=null){
|
||||
apkType = manifestBlock.guessApkType();
|
||||
}
|
||||
if(apkType != null){
|
||||
mApkType = apkType;
|
||||
}else {
|
||||
apkType = ApkType.UNKNOWN;
|
||||
}
|
||||
return apkType;
|
||||
}
|
||||
|
||||
// If we need TableStringPool only, this loads pool without
|
||||
// loading packages and other chunk blocks for faster and less memory usage
|
||||
public TableStringPool getVolatileTableStringPool() throws IOException{
|
||||
if(mTableBlock!=null){
|
||||
return mTableBlock.getStringPool();
|
||||
}
|
||||
InputSource inputSource = getApkArchive()
|
||||
.getInputSource(TableBlock.FILE_NAME);
|
||||
if(inputSource==null){
|
||||
throw new IOException("Module don't have: "+TableBlock.FILE_NAME);
|
||||
}
|
||||
if((inputSource instanceof ZipEntrySource)
|
||||
||(inputSource instanceof FileInputSource)){
|
||||
InputStream inputStream = inputSource.openStream();
|
||||
TableStringPool stringPool = TableStringPool.readFromTable(inputStream);
|
||||
inputStream.close();
|
||||
return stringPool;
|
||||
}
|
||||
return getTableBlock().getStringPool();
|
||||
}
|
||||
TableBlock loadTableBlock() throws IOException {
|
||||
APKArchive archive=getApkArchive();
|
||||
InputSource inputSource = archive.getInputSource(TableBlock.FILE_NAME);
|
||||
if(inputSource==null){
|
||||
throw new IOException("Entry not found: "+TableBlock.FILE_NAME);
|
||||
}
|
||||
TableBlock tableBlock;
|
||||
if(inputSource instanceof SplitJsonTableInputSource){
|
||||
tableBlock=((SplitJsonTableInputSource)inputSource).getTableBlock();
|
||||
}else if(inputSource instanceof SingleJsonTableInputSource){
|
||||
tableBlock=((SingleJsonTableInputSource)inputSource).getTableBlock();
|
||||
}else if(inputSource instanceof BlockInputSource){
|
||||
Chunk<?> block = ((BlockInputSource<?>) inputSource).getBlock();
|
||||
tableBlock = (TableBlock) block;
|
||||
}else {
|
||||
InputStream inputStream = inputSource.openStream();
|
||||
tableBlock = TableBlock.load(inputStream);
|
||||
inputStream.close();
|
||||
}
|
||||
BlockInputSource<TableBlock> blockInputSource=new BlockInputSource<>(inputSource.getName(), tableBlock);
|
||||
blockInputSource.setMethod(inputSource.getMethod());
|
||||
blockInputSource.setSort(inputSource.getSort());
|
||||
archive.add(blockInputSource);
|
||||
tableBlock.setApkFile(this);
|
||||
return tableBlock;
|
||||
}
|
||||
public APKArchive getApkArchive() {
|
||||
return apkArchive;
|
||||
}
|
||||
public void setLoadDefaultFramework(boolean loadDefaultFramework) {
|
||||
this.loadDefaultFramework = loadDefaultFramework;
|
||||
this.mDisableLoadFramework = !loadDefaultFramework;
|
||||
}
|
||||
|
||||
public void merge(ApkModule module) throws IOException {
|
||||
if(module==null||module==this){
|
||||
return;
|
||||
}
|
||||
logMessage("Merging: "+module.getModuleName());
|
||||
mergeDexFiles(module);
|
||||
mergeTable(module);
|
||||
mergeFiles(module);
|
||||
getUncompressedFiles().merge(module.getUncompressedFiles());
|
||||
}
|
||||
private void mergeTable(ApkModule module) {
|
||||
if(!module.hasTableBlock()){
|
||||
return;
|
||||
}
|
||||
logMessage("Merging resource table: "+module.getModuleName());
|
||||
TableBlock exist;
|
||||
if(!hasTableBlock()){
|
||||
exist=new TableBlock();
|
||||
BlockInputSource<TableBlock> inputSource=new BlockInputSource<>(TableBlock.FILE_NAME, exist);
|
||||
getApkArchive().add(inputSource);
|
||||
}else{
|
||||
exist=getTableBlock();
|
||||
}
|
||||
TableBlock coming=module.getTableBlock();
|
||||
exist.merge(coming);
|
||||
}
|
||||
private void mergeFiles(ApkModule module) {
|
||||
APKArchive archiveExist = getApkArchive();
|
||||
APKArchive archiveComing = module.getApkArchive();
|
||||
Map<String, InputSource> comingAlias=ApkUtil.toAliasMap(archiveComing.listInputSources());
|
||||
Map<String, InputSource> existAlias=ApkUtil.toAliasMap(archiveExist.listInputSources());
|
||||
UncompressedFiles uncompressedFiles = module.getUncompressedFiles();
|
||||
for(InputSource inputSource:comingAlias.values()){
|
||||
if(existAlias.containsKey(inputSource.getAlias())||existAlias.containsKey(inputSource.getName())){
|
||||
continue;
|
||||
}
|
||||
if(DexFileInputSource.isDexName(inputSource.getName())){
|
||||
continue;
|
||||
}
|
||||
if (inputSource.getAlias().startsWith("lib/")){
|
||||
uncompressedFiles.removePath(inputSource.getAlias());
|
||||
}
|
||||
logVerbose("Added: "+inputSource.getAlias());
|
||||
archiveExist.add(inputSource);
|
||||
}
|
||||
}
|
||||
private void mergeDexFiles(ApkModule module){
|
||||
UncompressedFiles uncompressedFiles=module.getUncompressedFiles();
|
||||
List<DexFileInputSource> existList=listDexFiles();
|
||||
List<DexFileInputSource> comingList=module.listDexFiles();
|
||||
APKArchive archive=getApkArchive();
|
||||
int index=0;
|
||||
if(existList.size()>0){
|
||||
index=existList.get(existList.size()-1).getDexNumber();
|
||||
if(index==0){
|
||||
index=2;
|
||||
}else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
for(DexFileInputSource source:comingList){
|
||||
uncompressedFiles.removePath(source.getAlias());
|
||||
String name= DexFileInputSource.getDexName(index);
|
||||
DexFileInputSource add=new DexFileInputSource(name, source.getInputSource());
|
||||
archive.add(add);
|
||||
logMessage("Added ["+module.getModuleName()+"] "
|
||||
+source.getAlias()+" -> "+name);
|
||||
index++;
|
||||
if(index==1){
|
||||
index=2;
|
||||
}
|
||||
}
|
||||
}
|
||||
APKLogger getApkLogger(){
|
||||
return apkLogger;
|
||||
}
|
||||
public void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return getModuleName();
|
||||
}
|
||||
public static ApkModule loadApkFile(File apkFile) throws IOException {
|
||||
return loadApkFile(apkFile, ApkUtil.DEF_MODULE_NAME);
|
||||
}
|
||||
public static ApkModule loadApkFile(File apkFile, String moduleName) throws IOException {
|
||||
Archive archive = new Archive(apkFile);
|
||||
ApkModule apkModule = new ApkModule(moduleName, archive.createAPKArchive());
|
||||
apkModule.setApkSignatureBlock(archive.getApkSignatureBlock());
|
||||
return apkModule;
|
||||
}
|
||||
}
|
|
@ -1,320 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
public class CrcOutputStream extends OutputStream {
|
||||
private final CRC32 crc;
|
||||
private long length;
|
||||
private long mCheckSum;
|
||||
public CrcOutputStream() {
|
||||
super();
|
||||
this.crc = new CRC32();
|
||||
}
|
||||
public long getLength(){
|
||||
return length;
|
||||
}
|
||||
public long getCrcValue(){
|
||||
if(mCheckSum==0){
|
||||
mCheckSum=crc.getValue();
|
||||
}
|
||||
return mCheckSum;
|
||||
}
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
this.crc.update(b);
|
||||
length=length+1;
|
||||
}
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
this.write(b, 0, b.length);
|
||||
}
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
this.crc.update(b, off, len);
|
||||
length=length+len;
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DexFileInputSource extends RenamedInputSource<InputSource> implements Comparable<DexFileInputSource>{
|
||||
public DexFileInputSource(String name, InputSource inputSource){
|
||||
super(name, inputSource);
|
||||
}
|
||||
public int getDexNumber(){
|
||||
return getDexNumber(getAlias());
|
||||
}
|
||||
@Override
|
||||
public int compareTo(DexFileInputSource source) {
|
||||
return Integer.compare(getDexNumber(), source.getDexNumber());
|
||||
}
|
||||
public static void sort(List<DexFileInputSource> sourceList){
|
||||
sourceList.sort(new Comparator<DexFileInputSource>() {
|
||||
@Override
|
||||
public int compare(DexFileInputSource s1, DexFileInputSource s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
});
|
||||
}
|
||||
public static boolean isDexName(String name){
|
||||
return getDexNumber(name)>=0;
|
||||
}
|
||||
static String getDexName(int i){
|
||||
if(i==0){
|
||||
return "classes.dex";
|
||||
}
|
||||
return "classes"+i+".dex";
|
||||
}
|
||||
static int getDexNumber(String name){
|
||||
Matcher matcher=PATTERN.matcher(name);
|
||||
if(!matcher.find()){
|
||||
return -1;
|
||||
}
|
||||
String num=matcher.group(1);
|
||||
if(num.length()==0){
|
||||
return 0;
|
||||
}
|
||||
return Integer.parseInt(num);
|
||||
}
|
||||
private static final Pattern PATTERN=Pattern.compile("^classes([0-9]*)\\.dex$");
|
||||
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class FileMagic {
|
||||
|
||||
public static String getExtensionFromMagic(InputSource inputSource) throws IOException {
|
||||
byte[] magic=readFileMagic(inputSource);
|
||||
if(magic==null){
|
||||
return null;
|
||||
}
|
||||
if(isPng(magic)){
|
||||
return ".png";
|
||||
}
|
||||
if(isJpeg(magic)){
|
||||
return ".jpg";
|
||||
}
|
||||
if(isWebp(magic)){
|
||||
return ".webp";
|
||||
}
|
||||
if(isTtf(magic)){
|
||||
return ".ttf";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isJpeg(byte[] magic){
|
||||
return compareMagic(MAGIC_JPG, magic);
|
||||
}
|
||||
private static boolean isPng(byte[] magic){
|
||||
return compareMagic(MAGIC_PNG, magic);
|
||||
}
|
||||
private static boolean isWebp(byte[] magic){
|
||||
return compareMagic(MAGIC_WEBP, magic);
|
||||
}
|
||||
private static boolean isTtf(byte[] magic){
|
||||
return compareMagic(MAGIC_TTF, magic);
|
||||
}
|
||||
private static boolean compareMagic(byte[] magic, byte[] readMagic){
|
||||
if(magic==null || readMagic==null){
|
||||
return false;
|
||||
}
|
||||
int max=magic.length;
|
||||
if(max>readMagic.length){
|
||||
max=readMagic.length;
|
||||
}
|
||||
if(max==0){
|
||||
return false;
|
||||
}
|
||||
for(int i=0;i<max;i++){
|
||||
int m=magic[i];
|
||||
if(m==-1){
|
||||
continue;
|
||||
}
|
||||
if(m != readMagic[i]){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static byte[] readFileMagic(InputSource inputSource) throws IOException {
|
||||
InputStream inputStream=inputSource.openStream();
|
||||
byte[] magic=new byte[MAGIC_MAX_LENGTH];
|
||||
int count=inputStream.read(magic, 0, magic.length);
|
||||
inputStream.close();
|
||||
if(count<magic.length){
|
||||
return null;
|
||||
}
|
||||
return magic;
|
||||
}
|
||||
|
||||
private static final int MAGIC_MAX_LENGTH=16;
|
||||
private static final byte[] MAGIC_PNG=new byte[]{(byte) 137, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
|
||||
private static final byte[] MAGIC_JPG=new byte[]{-0x01, (byte) 0xd8, -0x01, (byte) 224, 0x00, 0x10, 0x4a, 0x46};
|
||||
private static final byte[] MAGIC_WEBP=new byte[]{0x52, 0x49, 0x46, 0x46, -0x01, -0x01, -0x01, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38};
|
||||
private static final byte[] MAGIC_TTF=new byte[]{0x00, 0x01, 0x00, 0x00, 0x00, -0x01, -0x01, -0x01};
|
||||
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.ByteInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive.InputSourceUtil;
|
||||
import com.reandroid.archive2.Archive;
|
||||
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.ResXmlAttribute;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlElement;
|
||||
import com.reandroid.arsc.util.FrameworkTable;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/*
|
||||
* Produces compressed framework apk by removing irrelevant files and entries,
|
||||
* basically it keeps only resources.arsc and AndroidManifest.xml
|
||||
*/
|
||||
public class FrameworkApk extends ApkModule{
|
||||
private final Object mLock = new Object();
|
||||
private int versionCode;
|
||||
private String versionName;
|
||||
private String packageName;
|
||||
private boolean mOptimizing;
|
||||
private boolean mDestroyed;
|
||||
public FrameworkApk(String moduleName, APKArchive apkArchive) {
|
||||
super(moduleName, apkArchive);
|
||||
super.setLoadDefaultFramework(false);
|
||||
}
|
||||
public FrameworkApk(APKArchive apkArchive) {
|
||||
this("framework", apkArchive);
|
||||
}
|
||||
|
||||
public void destroy(){
|
||||
synchronized (mLock){
|
||||
this.versionCode = -1;
|
||||
this.versionName = "-1";
|
||||
this.packageName = "destroyed";
|
||||
super.destroy();
|
||||
this.mDestroyed = true;
|
||||
}
|
||||
}
|
||||
public boolean isDestroyed() {
|
||||
synchronized (mLock){
|
||||
if(!mDestroyed){
|
||||
return false;
|
||||
}
|
||||
if(hasTableBlock()){
|
||||
this.versionCode = 0;
|
||||
this.versionName = null;
|
||||
this.packageName = null;
|
||||
mDestroyed = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public int getVersionCode() {
|
||||
if(this.versionCode == 0){
|
||||
initValues();
|
||||
}
|
||||
return this.versionCode;
|
||||
}
|
||||
public String getVersionName() {
|
||||
if(this.versionName == null){
|
||||
initValues();
|
||||
}
|
||||
return this.versionName;
|
||||
}
|
||||
@Override
|
||||
public String getPackageName() {
|
||||
if(this.packageName == null){
|
||||
initValues();
|
||||
}
|
||||
return this.packageName;
|
||||
}
|
||||
@Override
|
||||
public void setPackageName(String packageName) {
|
||||
super.setPackageName(packageName);
|
||||
this.packageName = null;
|
||||
}
|
||||
private void initValues() {
|
||||
if(hasAndroidManifestBlock()){
|
||||
AndroidManifestBlock manifest = getAndroidManifestBlock();
|
||||
Integer code = manifest.getVersionCode();
|
||||
if(code!=null){
|
||||
this.versionCode = code;
|
||||
}
|
||||
if(this.versionName == null){
|
||||
this.versionName = manifest.getVersionName();
|
||||
}
|
||||
if(this.packageName == null){
|
||||
this.packageName = manifest.getPackageName();
|
||||
}
|
||||
}
|
||||
if(hasTableBlock()){
|
||||
FrameworkTable table = getTableBlock();
|
||||
if(this.versionCode == 0 && table.isOptimized()){
|
||||
int version = table.getVersionCode();
|
||||
if(version!=0){
|
||||
versionCode = version;
|
||||
if(this.versionName == null){
|
||||
this.versionName = String.valueOf(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(this.packageName == null){
|
||||
PackageBlock packageBlock = table.pickOne();
|
||||
if(packageBlock!=null){
|
||||
this.packageName = packageBlock.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setManifest(AndroidManifestBlock manifestBlock){
|
||||
synchronized (mLock){
|
||||
super.setManifest(manifestBlock);
|
||||
this.versionCode = 0;
|
||||
this.versionName = null;
|
||||
this.packageName = null;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setTableBlock(TableBlock tableBlock){
|
||||
synchronized (mLock){
|
||||
super.setTableBlock(tableBlock);
|
||||
this.versionCode = 0;
|
||||
this.versionName = null;
|
||||
this.packageName = null;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public FrameworkTable getTableBlock() {
|
||||
return (FrameworkTable) super.getTableBlock();
|
||||
}
|
||||
@Override
|
||||
FrameworkTable loadTableBlock() throws IOException {
|
||||
APKArchive archive=getApkArchive();
|
||||
InputSource inputSource = archive.getInputSource(TableBlock.FILE_NAME);
|
||||
if(inputSource==null){
|
||||
throw new IOException("Entry not found: "+TableBlock.FILE_NAME);
|
||||
}
|
||||
InputStream inputStream = inputSource.openStream();
|
||||
FrameworkTable frameworkTable=FrameworkTable.load(inputStream);
|
||||
frameworkTable.setApkFile(this);
|
||||
|
||||
BlockInputSource<FrameworkTable> blockInputSource=new BlockInputSource<>(inputSource.getName(), frameworkTable);
|
||||
blockInputSource.setMethod(inputSource.getMethod());
|
||||
blockInputSource.setSort(inputSource.getSort());
|
||||
archive.add(blockInputSource);
|
||||
return frameworkTable;
|
||||
}
|
||||
public void optimize(){
|
||||
synchronized (mLock){
|
||||
if(mOptimizing){
|
||||
return;
|
||||
}
|
||||
if(!hasTableBlock()){
|
||||
mOptimizing = false;
|
||||
return;
|
||||
}
|
||||
FrameworkTable frameworkTable = getTableBlock();
|
||||
if(frameworkTable.isOptimized()){
|
||||
mOptimizing = false;
|
||||
return;
|
||||
}
|
||||
FrameworkOptimizer optimizer = new FrameworkOptimizer(this);
|
||||
optimizer.optimize();
|
||||
mOptimizing = false;
|
||||
}
|
||||
}
|
||||
public String getName(){
|
||||
if(isDestroyed()){
|
||||
return "destroyed";
|
||||
}
|
||||
String pkg = getPackageName();
|
||||
if(pkg==null){
|
||||
return "";
|
||||
}
|
||||
return pkg + "-" + getVersionCode();
|
||||
}
|
||||
@Override
|
||||
public int hashCode(){
|
||||
return Objects.hash(getClass(), getName());
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object obj){
|
||||
if(obj==this){
|
||||
return true;
|
||||
}
|
||||
if(getClass()!=obj.getClass()){
|
||||
return false;
|
||||
}
|
||||
FrameworkApk other = (FrameworkApk) obj;
|
||||
return getName().equals(other.getName());
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return getName();
|
||||
}
|
||||
public static FrameworkApk loadApkFile(File apkFile) throws IOException {
|
||||
Archive archive = new Archive(apkFile);
|
||||
APKArchive apkArchive = new APKArchive(archive.mapEntrySource());
|
||||
return new FrameworkApk(apkArchive);
|
||||
}
|
||||
public static FrameworkApk loadApkFile(File apkFile, String moduleName) throws IOException {
|
||||
Archive archive = new Archive(apkFile);
|
||||
APKArchive apkArchive = new APKArchive(archive.mapEntrySource());
|
||||
return new FrameworkApk(moduleName, apkArchive);
|
||||
}
|
||||
public static boolean isFramework(ApkModule apkModule) {
|
||||
if(!apkModule.hasAndroidManifestBlock()){
|
||||
return false;
|
||||
}
|
||||
return isFramework(apkModule.getAndroidManifestBlock());
|
||||
}
|
||||
public static boolean isFramework(AndroidManifestBlock manifestBlock){
|
||||
ResXmlElement root = manifestBlock.getManifestElement();
|
||||
ResXmlAttribute attribute = root.getStartElement()
|
||||
.searchAttributeByName(AndroidManifestBlock.NAME_coreApp);
|
||||
if(attribute==null || attribute.getValueType()!= ValueType.INT_BOOLEAN){
|
||||
return false;
|
||||
}
|
||||
return attribute.getValueAsBoolean();
|
||||
}
|
||||
public static FrameworkApk loadApkBuffer(InputStream inputStream) throws IOException{
|
||||
return loadApkBuffer("framework", inputStream);
|
||||
}
|
||||
public static FrameworkApk loadApkBuffer(String moduleName, InputStream inputStream) throws IOException {
|
||||
APKArchive archive = new APKArchive();
|
||||
FrameworkApk frameworkApk = new FrameworkApk(moduleName, archive);
|
||||
Map<String, ByteInputSource> inputSourceMap = InputSourceUtil.mapInputStreamAsBuffer(inputStream);
|
||||
ByteInputSource source = inputSourceMap.get(TableBlock.FILE_NAME);
|
||||
FrameworkTable tableBlock = new FrameworkTable();
|
||||
if(source!=null){
|
||||
tableBlock.readBytes(source.openStream());
|
||||
}
|
||||
frameworkApk.setTableBlock(tableBlock);
|
||||
|
||||
AndroidManifestBlock manifestBlock = new AndroidManifestBlock();
|
||||
source = inputSourceMap.get(AndroidManifestBlock.FILE_NAME);
|
||||
if(source!=null){
|
||||
manifestBlock.readBytes(source.openStream());
|
||||
}
|
||||
frameworkApk.setManifest(manifestBlock);
|
||||
archive.addAll(inputSourceMap.values());
|
||||
return frameworkApk;
|
||||
}
|
||||
public static void optimize(File in, File out, APKLogger apkLogger) throws IOException{
|
||||
FrameworkApk frameworkApk = FrameworkApk.loadApkFile(in);
|
||||
frameworkApk.setAPKLogger(apkLogger);
|
||||
frameworkApk.optimize();
|
||||
frameworkApk.writeApk(out);
|
||||
}
|
||||
}
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlElement;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlNode;
|
||||
import com.reandroid.arsc.group.EntryGroup;
|
||||
import com.reandroid.arsc.io.BlockReader;
|
||||
import com.reandroid.arsc.pool.ResXmlStringPool;
|
||||
import com.reandroid.arsc.util.FrameworkTable;
|
||||
import com.reandroid.arsc.value.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class FrameworkOptimizer {
|
||||
private final ApkModule frameworkApk;
|
||||
private APKLogger apkLogger;
|
||||
private boolean mOptimizing;
|
||||
public FrameworkOptimizer(ApkModule frameworkApk){
|
||||
this.frameworkApk = frameworkApk;
|
||||
this.apkLogger = frameworkApk.getApkLogger();
|
||||
}
|
||||
public void optimize(){
|
||||
if(mOptimizing){
|
||||
return;
|
||||
}
|
||||
mOptimizing = true;
|
||||
if(!frameworkApk.hasTableBlock()){
|
||||
logMessage("Don't have: "+TableBlock.FILE_NAME);
|
||||
mOptimizing = false;
|
||||
return;
|
||||
}
|
||||
FrameworkTable frameworkTable = getFrameworkTable();
|
||||
AndroidManifestBlock manifestBlock = null;
|
||||
if(frameworkApk.hasAndroidManifestBlock()){
|
||||
manifestBlock = frameworkApk.getAndroidManifestBlock();
|
||||
}
|
||||
optimizeTable(frameworkTable, manifestBlock);
|
||||
UncompressedFiles uncompressedFiles = frameworkApk.getUncompressedFiles();
|
||||
uncompressedFiles.clearExtensions();
|
||||
uncompressedFiles.clearPaths();
|
||||
clearFiles(frameworkApk.getApkArchive());
|
||||
logMessage("Optimized");
|
||||
}
|
||||
private void clearFiles(APKArchive archive){
|
||||
int count = archive.entriesCount();
|
||||
if(count==2){
|
||||
return;
|
||||
}
|
||||
logMessage("Removing files from: "+count);
|
||||
InputSource tableSource = archive.getInputSource(TableBlock.FILE_NAME);
|
||||
InputSource manifestSource = archive.getInputSource(AndroidManifestBlock.FILE_NAME);
|
||||
archive.clear();
|
||||
if(tableSource!=null){
|
||||
tableSource.setMethod(ZipEntry.DEFLATED);
|
||||
}
|
||||
if(manifestSource!=null){
|
||||
manifestSource.setMethod(ZipEntry.DEFLATED);
|
||||
}
|
||||
archive.add(tableSource);
|
||||
archive.add(manifestSource);
|
||||
count = count - archive.entriesCount();
|
||||
logMessage("Removed files: "+count);
|
||||
}
|
||||
private void optimizeTable(FrameworkTable table, AndroidManifestBlock manifestBlock){
|
||||
if(table.isOptimized()){
|
||||
return;
|
||||
}
|
||||
logMessage("Optimizing ...");
|
||||
int prev = table.countBytes();
|
||||
int version = 0;
|
||||
String name = "framework";
|
||||
if(manifestBlock !=null){
|
||||
Integer code = manifestBlock.getVersionCode();
|
||||
if(code!=null){
|
||||
version = code;
|
||||
}
|
||||
name = manifestBlock.getPackageName();
|
||||
compressManifest(manifestBlock);
|
||||
backupManifestValue(manifestBlock, table);
|
||||
}
|
||||
logMessage("Optimizing table ...");
|
||||
table.optimize(name, version);
|
||||
long diff=prev - table.countBytes();
|
||||
long percent=(diff*100L)/prev;
|
||||
logMessage("Table size reduced by: "+percent+" %");
|
||||
mOptimizing = false;
|
||||
}
|
||||
|
||||
private FrameworkTable getFrameworkTable(){
|
||||
TableBlock tableBlock = frameworkApk.getTableBlock();
|
||||
if(tableBlock instanceof FrameworkTable){
|
||||
return (FrameworkTable) tableBlock;
|
||||
}
|
||||
FrameworkTable frameworkTable = toFramework(tableBlock);
|
||||
frameworkApk.setTableBlock(frameworkTable);
|
||||
return frameworkTable;
|
||||
}
|
||||
private FrameworkTable toFramework(TableBlock tableBlock){
|
||||
logMessage("Converting to framework ...");
|
||||
BlockReader reader = new BlockReader(tableBlock.getBytes());
|
||||
FrameworkTable frameworkTable = new FrameworkTable();
|
||||
try {
|
||||
frameworkTable.readBytes(reader);
|
||||
} catch (IOException exception) {
|
||||
logError("Error re-loading framework: ", exception);
|
||||
}
|
||||
return frameworkTable;
|
||||
}
|
||||
private void compressManifest(AndroidManifestBlock manifestBlock){
|
||||
logMessage("Compressing manifest ...");
|
||||
int prev = manifestBlock.countBytes();
|
||||
ResXmlElement manifest = manifestBlock.getResXmlElement();
|
||||
List<ResXmlNode> removeList = getManifestElementToRemove(manifest);
|
||||
for(ResXmlNode node:removeList){
|
||||
manifest.removeNode(node);
|
||||
}
|
||||
ResXmlElement application = manifestBlock.getApplicationElement();
|
||||
if(application!=null){
|
||||
removeList = application.listXmlNodes();
|
||||
for(ResXmlNode node:removeList){
|
||||
application.removeNode(node);
|
||||
}
|
||||
}
|
||||
ResXmlStringPool stringPool = manifestBlock.getStringPool();
|
||||
stringPool.removeUnusedStrings();
|
||||
manifestBlock.refresh();
|
||||
long diff=prev - manifestBlock.countBytes();
|
||||
long percent=(diff*100L)/prev;
|
||||
logMessage("Manifest size reduced by: "+percent+" %");
|
||||
}
|
||||
private List<ResXmlNode> getManifestElementToRemove(ResXmlElement manifest){
|
||||
List<ResXmlNode> results = new ArrayList<>();
|
||||
for(ResXmlNode node:manifest.listXmlNodes()){
|
||||
if(!(node instanceof ResXmlElement)){
|
||||
continue;
|
||||
}
|
||||
ResXmlElement element = (ResXmlElement)node;
|
||||
if(AndroidManifestBlock.TAG_application.equals(element.getTag())){
|
||||
continue;
|
||||
}
|
||||
results.add(element);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private void backupManifestValue(AndroidManifestBlock manifestBlock, TableBlock tableBlock){
|
||||
logMessage("Backup manifest values ...");
|
||||
ResXmlElement application = manifestBlock.getApplicationElement();
|
||||
ResXmlAttribute iconAttribute = null;
|
||||
int iconReference = 0;
|
||||
if(application!=null){
|
||||
ResXmlAttribute attribute = application
|
||||
.searchAttributeByResourceId(AndroidManifestBlock.ID_icon);
|
||||
if(attribute!=null && attribute.getValueType()==ValueType.REFERENCE){
|
||||
iconAttribute = attribute;
|
||||
iconReference = attribute.getData();
|
||||
}
|
||||
}
|
||||
|
||||
ResXmlElement element = manifestBlock.getResXmlElement();
|
||||
backupAttributeValues(tableBlock, element);
|
||||
|
||||
if(iconAttribute!=null){
|
||||
iconAttribute.setTypeAndData(ValueType.REFERENCE, iconReference);
|
||||
}
|
||||
}
|
||||
private void backupAttributeValues(TableBlock tableBlock, ResXmlElement element){
|
||||
if(element==null){
|
||||
return;
|
||||
}
|
||||
for(ResXmlAttribute attribute: element.listAttributes()){
|
||||
backupAttributeValues(tableBlock, attribute);
|
||||
}
|
||||
for(ResXmlElement child: element.listElements()){
|
||||
backupAttributeValues(tableBlock, child);
|
||||
}
|
||||
}
|
||||
private void backupAttributeValues(TableBlock tableBlock, ResXmlAttribute attribute){
|
||||
if(attribute==null){
|
||||
return;
|
||||
}
|
||||
ValueType valueType = attribute.getValueType();
|
||||
if(valueType!=ValueType.REFERENCE && valueType!=ValueType.ATTRIBUTE){
|
||||
return;
|
||||
}
|
||||
int reference = attribute.getData();
|
||||
Entry entry = getEntryWithValue(tableBlock, reference);
|
||||
if(entry == null || isReferenceEntry(entry) || entry.isComplex()){
|
||||
return;
|
||||
}
|
||||
ResTableEntry resTableEntry = (ResTableEntry) entry.getTableEntry();
|
||||
ResValue resValue = resTableEntry.getValue();
|
||||
valueType = resValue.getValueType();
|
||||
if(valueType==ValueType.STRING){
|
||||
String value = resValue.getValueAsString();
|
||||
attribute.setValueAsString(value);
|
||||
}else {
|
||||
int data = resValue.getData();
|
||||
attribute.setTypeAndData(valueType, data);
|
||||
}
|
||||
}
|
||||
private Entry getEntryWithValue(TableBlock tableBlock, int resourceId){
|
||||
Set<Integer> circularReference = new HashSet<>();
|
||||
return getEntryWithValue(tableBlock, resourceId, circularReference);
|
||||
}
|
||||
private Entry getEntryWithValue(TableBlock tableBlock, int resourceId, Set<Integer> circularReference){
|
||||
if(circularReference.contains(resourceId)){
|
||||
return null;
|
||||
}
|
||||
circularReference.add(resourceId);
|
||||
EntryGroup entryGroup = tableBlock.getEntryGroup(resourceId);
|
||||
Entry entry = entryGroup.pickOne();
|
||||
if(entry==null){
|
||||
return null;
|
||||
}
|
||||
if(isReferenceEntry(entry)){
|
||||
return getEntryWithValue(
|
||||
tableBlock,
|
||||
((ResValue)entry.getTableEntry().getValue()).getData(),
|
||||
circularReference);
|
||||
}
|
||||
if(!entry.isNull()){
|
||||
return entry;
|
||||
}
|
||||
Iterator<Entry> itr = entryGroup.iterator(true);
|
||||
while (itr.hasNext()){
|
||||
entry = itr.next();
|
||||
if(!isReferenceEntry(entry)){
|
||||
if(!entry.isNull()){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private boolean isReferenceEntry(Entry entry){
|
||||
if(entry==null || entry.isNull()){
|
||||
return false;
|
||||
}
|
||||
TableEntry<?, ?> tableEntry = entry.getTableEntry();
|
||||
if(tableEntry instanceof CompoundEntry){
|
||||
return false;
|
||||
}
|
||||
if(!(tableEntry instanceof ResTableEntry)){
|
||||
return false;
|
||||
}
|
||||
ResTableEntry resTableEntry = (ResTableEntry) tableEntry;
|
||||
ResValue resValue = resTableEntry.getValue();
|
||||
|
||||
ValueType valueType = resValue.getValueType();
|
||||
|
||||
return valueType == ValueType.REFERENCE
|
||||
|| valueType == ValueType.ATTRIBUTE;
|
||||
}
|
||||
|
||||
APKLogger getApkLogger(){
|
||||
return apkLogger;
|
||||
}
|
||||
public void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class JsonManifestInputSource extends JsonXmlInputSource {
|
||||
public JsonManifestInputSource(InputSource inputSource) {
|
||||
super(inputSource);
|
||||
}
|
||||
AndroidManifestBlock newInstance(){
|
||||
return new AndroidManifestBlock();
|
||||
}
|
||||
public static JsonManifestInputSource fromFile(File rootDir, File jsonFile){
|
||||
String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile);
|
||||
FileInputSource fileInputSource=new FileInputSource(jsonFile, path);
|
||||
return new JsonManifestInputSource(fileInputSource);
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.json.JSONException;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class JsonXmlInputSource extends InputSource {
|
||||
private final InputSource inputSource;
|
||||
private APKLogger apkLogger;
|
||||
public JsonXmlInputSource(InputSource inputSource) {
|
||||
super(inputSource.getAlias());
|
||||
this.inputSource=inputSource;
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getResXmlBlock().writeBytes(outputStream);
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
ResXmlDocument resXmlDocument = getResXmlBlock();
|
||||
return new ByteArrayInputStream(resXmlDocument.getBytes());
|
||||
}
|
||||
@Override
|
||||
public long getLength() throws IOException{
|
||||
ResXmlDocument resXmlDocument = getResXmlBlock();
|
||||
return resXmlDocument.countBytes();
|
||||
}
|
||||
private ResXmlDocument getResXmlBlock() throws IOException{
|
||||
logVerbose("From json: "+getAlias());
|
||||
ResXmlDocument resXmlDocument =newInstance();
|
||||
InputStream inputStream=inputSource.openStream();
|
||||
try{
|
||||
JSONObject jsonObject=new JSONObject(inputStream);
|
||||
resXmlDocument.fromJson(jsonObject);
|
||||
}catch (JSONException ex){
|
||||
throw new IOException(inputSource.getAlias()+": "+ex.getMessage(), ex);
|
||||
}
|
||||
return resXmlDocument;
|
||||
}
|
||||
ResXmlDocument newInstance(){
|
||||
return new ResXmlDocument();
|
||||
}
|
||||
void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonXmlInputSource fromFile(File rootDir, File jsonFile){
|
||||
String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile);
|
||||
FileInputSource fileInputSource=new FileInputSource(jsonFile, path);
|
||||
return new JsonXmlInputSource(fileInputSource);
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive.ZipArchive;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.json.JSONConvert;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class PathMap implements JSONConvert<JSONArray> {
|
||||
private final Object mLock = new Object();
|
||||
private final Map<String, String> mNameAliasMap;
|
||||
private final Map<String, String> mAliasNameMap;
|
||||
|
||||
public PathMap(){
|
||||
this.mNameAliasMap = new HashMap<>();
|
||||
this.mAliasNameMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public void restore(ApkModule apkModule){
|
||||
restoreResFile(apkModule.listResFiles());
|
||||
restore(apkModule.getApkArchive().listInputSources());
|
||||
}
|
||||
public List<String> restoreResFile(Collection<ResFile> files){
|
||||
List<String> results = new ArrayList<>();
|
||||
if(files == null){
|
||||
return results;
|
||||
}
|
||||
for(ResFile resFile:files){
|
||||
String alias = restoreResFile(resFile);
|
||||
if(alias==null){
|
||||
continue;
|
||||
}
|
||||
results.add(alias);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public String restoreResFile(ResFile resFile){
|
||||
InputSource inputSource = resFile.getInputSource();
|
||||
String alias = restore(inputSource);
|
||||
if(alias==null){
|
||||
return null;
|
||||
}
|
||||
resFile.setFilePath(alias);
|
||||
return alias;
|
||||
}
|
||||
public List<String> restore(Collection<InputSource> sources){
|
||||
List<String> results = new ArrayList<>();
|
||||
if(sources == null){
|
||||
return results;
|
||||
}
|
||||
for(InputSource inputSource:sources){
|
||||
String alias = restore(inputSource);
|
||||
if(alias==null){
|
||||
continue;
|
||||
}
|
||||
results.add(alias);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public String restore(InputSource inputSource){
|
||||
if(inputSource==null){
|
||||
return null;
|
||||
}
|
||||
String name = inputSource.getName();
|
||||
String alias = getName(name);
|
||||
if(alias==null){
|
||||
name = inputSource.getAlias();
|
||||
alias = getName(name);
|
||||
}
|
||||
if(alias==null || alias.equals(inputSource.getAlias())){
|
||||
return null;
|
||||
}
|
||||
inputSource.setAlias(alias);
|
||||
return alias;
|
||||
}
|
||||
|
||||
public String getAlias(String name){
|
||||
synchronized (mLock){
|
||||
return mNameAliasMap.get(name);
|
||||
}
|
||||
}
|
||||
public String getName(String alias){
|
||||
synchronized (mLock){
|
||||
return mAliasNameMap.get(alias);
|
||||
}
|
||||
}
|
||||
public int size(){
|
||||
synchronized (mLock){
|
||||
return mNameAliasMap.size();
|
||||
}
|
||||
}
|
||||
public void clear(){
|
||||
synchronized (mLock){
|
||||
mNameAliasMap.clear();
|
||||
mAliasNameMap.clear();
|
||||
}
|
||||
}
|
||||
public void add(ZipArchive archive){
|
||||
if(archive == null){
|
||||
return;
|
||||
}
|
||||
add(archive.listInputSources());
|
||||
}
|
||||
public void add(Collection<? extends InputSource> sources){
|
||||
if(sources==null){
|
||||
return;
|
||||
}
|
||||
for(InputSource inputSource:sources){
|
||||
add(inputSource);
|
||||
}
|
||||
}
|
||||
public void add(InputSource inputSource){
|
||||
if(inputSource==null){
|
||||
return;
|
||||
}
|
||||
add(inputSource.getName(), inputSource.getAlias());
|
||||
}
|
||||
public void add(String name, String alias){
|
||||
if(name==null || alias==null){
|
||||
return;
|
||||
}
|
||||
if(name.equals(alias)){
|
||||
return;
|
||||
}
|
||||
synchronized (mLock){
|
||||
mNameAliasMap.remove(name);
|
||||
mNameAliasMap.put(name, alias);
|
||||
mAliasNameMap.remove(alias);
|
||||
mAliasNameMap.put(alias, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void add(JSONObject json){
|
||||
if(json==null){
|
||||
return;
|
||||
}
|
||||
add(json.optString(NAME_name), json.optString(NAME_alias));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONArray toJson() {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
Map<String, String> nameMap = this.mNameAliasMap;
|
||||
List<String> nameList = toSortedList(nameMap.keySet());
|
||||
for(String name:nameList){
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(NAME_name, name);
|
||||
jsonObject.put(NAME_alias, nameMap.get(name));
|
||||
jsonArray.put(jsonObject);
|
||||
}
|
||||
return jsonArray;
|
||||
}
|
||||
@Override
|
||||
public void fromJson(JSONArray json) {
|
||||
clear();
|
||||
if(json==null){
|
||||
return;
|
||||
}
|
||||
int length = json.length();
|
||||
for(int i=0;i<length;i++){
|
||||
add(json.optJSONObject(i));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return "PathMap size="+size();
|
||||
}
|
||||
|
||||
private static List<String> toSortedList(Collection<String> stringCollection){
|
||||
List<String> results;
|
||||
if(stringCollection instanceof List){
|
||||
results = (List<String>) stringCollection;
|
||||
}else {
|
||||
results = new ArrayList<>(stringCollection);
|
||||
}
|
||||
Comparator<String> cmp = new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
};
|
||||
results.sort(cmp);
|
||||
return results;
|
||||
}
|
||||
|
||||
public static final String NAME_name = "name";
|
||||
public static final String NAME_alias = "alias";
|
||||
public static final String JSON_FILE = "path-map.json";
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class PathSanitizer {
|
||||
private final Collection<? extends InputSource> sourceList;
|
||||
private final boolean sanitizeResourceFiles;
|
||||
private Collection<ResFile> resFileList;
|
||||
private APKLogger apkLogger;
|
||||
private final Set<String> mSanitizedPaths;
|
||||
public PathSanitizer(Collection<? extends InputSource> sourceList, boolean sanitizeResourceFiles){
|
||||
this.sourceList = sourceList;
|
||||
this.mSanitizedPaths = new HashSet<>();
|
||||
this.sanitizeResourceFiles = sanitizeResourceFiles;
|
||||
}
|
||||
public PathSanitizer(Collection<? extends InputSource> sourceList){
|
||||
this(sourceList, false);
|
||||
}
|
||||
public void sanitize(){
|
||||
mSanitizedPaths.clear();
|
||||
logMessage("Sanitizing paths ...");
|
||||
sanitizeResFiles();
|
||||
for(InputSource inputSource:sourceList){
|
||||
sanitize(inputSource, 1, false);
|
||||
}
|
||||
logMessage("DONE = "+mSanitizedPaths.size());
|
||||
}
|
||||
public void setResourceFileList(Collection<ResFile> resFileList){
|
||||
this.resFileList = resFileList;
|
||||
}
|
||||
private void sanitizeResFiles(){
|
||||
Collection<ResFile> resFileList = this.resFileList;
|
||||
if(resFileList == null){
|
||||
return;
|
||||
}
|
||||
boolean sanitizeRes = this.sanitizeResourceFiles;
|
||||
Set<String> sanitizedPaths = this.mSanitizedPaths;
|
||||
if(sanitizeRes){
|
||||
logMessage("Sanitizing resource files ...");
|
||||
}
|
||||
for(ResFile resFile:resFileList){
|
||||
if(sanitizeRes){
|
||||
sanitize(resFile);
|
||||
}else {
|
||||
sanitizedPaths.add(resFile.getFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
private void sanitize(ResFile resFile){
|
||||
InputSource inputSource = resFile.getInputSource();
|
||||
String replace = sanitize(inputSource, 3, true);
|
||||
if(replace==null){
|
||||
return;
|
||||
}
|
||||
resFile.setFilePath(replace);
|
||||
}
|
||||
private String sanitize(InputSource inputSource, int depth, boolean fixedDepth){
|
||||
String name = inputSource.getName();
|
||||
if(mSanitizedPaths.contains(name)){
|
||||
return null;
|
||||
}
|
||||
mSanitizedPaths.add(name);
|
||||
String alias = inputSource.getAlias();
|
||||
if(shouldIgnore(alias)){
|
||||
return null;
|
||||
}
|
||||
String replace = sanitize(alias, depth, fixedDepth);
|
||||
if(alias.equals(replace)){
|
||||
return null;
|
||||
}
|
||||
inputSource.setAlias(replace);
|
||||
logVerbose("REN: '"+alias+"' -> '"+replace+"'");
|
||||
return replace;
|
||||
}
|
||||
|
||||
private String sanitize(String name, int depth, boolean fixedDepth){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String[] nameSplit = name.split("/");
|
||||
|
||||
boolean pathIsLong = name.length() >= MAX_PATH_LENGTH;
|
||||
int length = nameSplit.length;
|
||||
for(int i=0;i<length;i++){
|
||||
String split = nameSplit[i];
|
||||
boolean good = isGoodSimpleName(split);
|
||||
if(!good || (pathIsLong && i>=depth)){
|
||||
split = createUniqueName(name);
|
||||
appendPathName(builder, split);
|
||||
break;
|
||||
}
|
||||
if(fixedDepth && i>=(depth-1)){
|
||||
if(i < length-1){
|
||||
split = createUniqueName(name);
|
||||
}
|
||||
appendPathName(builder, split);
|
||||
break;
|
||||
}
|
||||
appendPathName(builder, split);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
private boolean shouldIgnore(String path){
|
||||
return path.startsWith("lib/") && path.endsWith(".so");
|
||||
}
|
||||
|
||||
public void setApkLogger(APKLogger apkLogger) {
|
||||
this.apkLogger = apkLogger;
|
||||
}
|
||||
private String getLogTag(){
|
||||
return "[SANITIZE]: ";
|
||||
}
|
||||
void logMessage(String msg){
|
||||
APKLogger logger = this.apkLogger;
|
||||
if(logger!=null){
|
||||
logger.logMessage(getLogTag()+msg);
|
||||
}
|
||||
}
|
||||
void logVerbose(String msg){
|
||||
APKLogger logger = this.apkLogger;
|
||||
if(logger!=null){
|
||||
logger.logVerbose(getLogTag()+msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendPathName(StringBuilder builder, String name){
|
||||
if(builder.length()>0){
|
||||
builder.append('/');
|
||||
}
|
||||
builder.append(name);
|
||||
}
|
||||
private static String createUniqueName(String name){
|
||||
int hash = name.hashCode();
|
||||
return "alias_" + HexUtil.toHexNoPrefix8(hash);
|
||||
}
|
||||
private static boolean isGoodSimpleName(String name){
|
||||
if(name==null){
|
||||
return false;
|
||||
}
|
||||
String alias = sanitizeSimpleName(name);
|
||||
return name.equals(alias);
|
||||
}
|
||||
public static String sanitizeSimpleName(String name){
|
||||
if(name==null){
|
||||
return null;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
char[] chars = name.toCharArray();
|
||||
boolean skipNext = true;
|
||||
int length = 0;
|
||||
int lengthMax = MAX_NAME_LENGTH;
|
||||
for(int i=0;i<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 == '#';
|
||||
}
|
||||
private static boolean isGoodFileNameChar(char ch){
|
||||
return ch == '_'
|
||||
|| (ch >= '0' && ch <= '9')
|
||||
|| (ch >= 'A' && ch <= 'Z')
|
||||
|| (ch >= 'a' && ch <= 'z');
|
||||
}
|
||||
|
||||
public static PathSanitizer create(ApkModule apkModule){
|
||||
PathSanitizer pathSanitizer = new PathSanitizer(
|
||||
apkModule.getApkArchive().listInputSources());
|
||||
pathSanitizer.setApkLogger(apkModule.getApkLogger());
|
||||
pathSanitizer.setResourceFileList(apkModule.listResFiles());
|
||||
return pathSanitizer;
|
||||
}
|
||||
|
||||
private static final int MAX_NAME_LENGTH = 75;
|
||||
private static final int MAX_PATH_LENGTH = 100;
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class RenamedInputSource<T extends InputSource> extends InputSource {
|
||||
private final T inputSource;
|
||||
public RenamedInputSource(String name, T input){
|
||||
super(name);
|
||||
this.inputSource=input;
|
||||
super.setMethod(input.getMethod());
|
||||
super.setSort(input.getSort());
|
||||
}
|
||||
public T getInputSource() {
|
||||
return inputSource;
|
||||
}
|
||||
@Override
|
||||
public void close(InputStream inputStream) throws IOException {
|
||||
getInputSource().close(inputStream);
|
||||
}
|
||||
@Override
|
||||
public long getLength() throws IOException {
|
||||
return getInputSource().getLength();
|
||||
}
|
||||
@Override
|
||||
public long getCrc() throws IOException {
|
||||
return getInputSource().getCrc();
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getInputSource().write(outputStream);
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
return getInputSource().openStream();
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.apk.xmlencoder.XMLEncodeSource;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.header.InfoHeader;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class ResFile {
|
||||
private final List<Entry> entryList;
|
||||
private final InputSource inputSource;
|
||||
private boolean mBinXml;
|
||||
private boolean mBinXmlChecked;
|
||||
private String mFileExtension;
|
||||
private boolean mFileExtensionChecked;
|
||||
private Entry mSelectedEntry;
|
||||
public ResFile(InputSource inputSource, List<Entry> entryList){
|
||||
this.inputSource=inputSource;
|
||||
this.entryList = entryList;
|
||||
}
|
||||
public List<Entry> getEntryList(){
|
||||
return entryList;
|
||||
}
|
||||
public String validateTypeDirectoryName(){
|
||||
Entry entry =pickOne();
|
||||
if(entry ==null){
|
||||
return null;
|
||||
}
|
||||
String path=getFilePath();
|
||||
String root="";
|
||||
int i=path.indexOf('/');
|
||||
if(i>0){
|
||||
i++;
|
||||
root=path.substring(0, i);
|
||||
path=path.substring(i);
|
||||
}
|
||||
String name=path;
|
||||
i=path.lastIndexOf('/');
|
||||
if(i>0){
|
||||
i++;
|
||||
name=path.substring(i);
|
||||
}
|
||||
TypeBlock typeBlock= entry.getTypeBlock();
|
||||
String typeName=typeBlock.getTypeName()+typeBlock.getResConfig().getQualifiers();
|
||||
return root+typeName+"/"+name;
|
||||
}
|
||||
public Entry pickOne(){
|
||||
if(mSelectedEntry ==null){
|
||||
mSelectedEntry =selectOne();
|
||||
}
|
||||
return mSelectedEntry;
|
||||
}
|
||||
private Entry selectOne(){
|
||||
List<Entry> entryList = this.entryList;
|
||||
if(entryList.size()==0){
|
||||
return null;
|
||||
}
|
||||
for(Entry entry :entryList){
|
||||
if(!entry.isNull() && entry.isDefault()){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
for(Entry entry :entryList){
|
||||
if(!entry.isNull()){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
for(Entry entry :entryList){
|
||||
if(entry.isDefault()){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return entryList.get(0);
|
||||
}
|
||||
public String getFilePath(){
|
||||
return getInputSource().getAlias();
|
||||
}
|
||||
public void setFilePath(String filePath){
|
||||
getInputSource().setAlias(filePath);
|
||||
for(Entry entry : entryList){
|
||||
TableEntry<?, ?> tableEntry = entry.getTableEntry();
|
||||
if(!(tableEntry instanceof ResTableEntry)){
|
||||
continue;
|
||||
}
|
||||
ResValue resValue = ((ResTableEntry) tableEntry).getValue();
|
||||
resValue.setValueAsString(filePath);
|
||||
}
|
||||
}
|
||||
public InputSource getInputSource() {
|
||||
return inputSource;
|
||||
}
|
||||
public boolean isBinaryXml(){
|
||||
if(mBinXmlChecked){
|
||||
return mBinXml;
|
||||
}
|
||||
mBinXmlChecked = true;
|
||||
InputSource inputSource = getInputSource();
|
||||
if((inputSource instanceof XMLEncodeSource)
|
||||
|| (inputSource instanceof JsonXmlInputSource)){
|
||||
mBinXml=true;
|
||||
}else{
|
||||
try {
|
||||
mBinXml = ResXmlDocument.isResXmlBlock(inputSource.getBytes(InfoHeader.INFO_MIN_SIZE));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
return mBinXml;
|
||||
}
|
||||
public File buildOutFile(File dir){
|
||||
String path=getFilePath();
|
||||
path=path.replace('/', File.separatorChar);
|
||||
return new File(dir, path);
|
||||
}
|
||||
public String buildPath(){
|
||||
return buildPath(null);
|
||||
}
|
||||
public String buildPath(String parent){
|
||||
Entry entry = pickOne();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if(parent!=null){
|
||||
builder.append(parent);
|
||||
if(!parent.endsWith("/")){
|
||||
builder.append('/');
|
||||
}
|
||||
}
|
||||
TypeBlock typeBlock = entry.getTypeBlock();
|
||||
builder.append(typeBlock.getTypeName());
|
||||
builder.append(typeBlock.getQualifiers());
|
||||
builder.append('/');
|
||||
builder.append(entry.getName());
|
||||
String ext = getFileExtension();
|
||||
if(ext!=null){
|
||||
builder.append(ext);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
private String getFileExtension(){
|
||||
if(!mFileExtensionChecked){
|
||||
mFileExtensionChecked=true;
|
||||
mFileExtension=readFileExtension();
|
||||
}
|
||||
return mFileExtension;
|
||||
}
|
||||
private String readFileExtension(){
|
||||
if(isBinaryXml()){
|
||||
return ".xml";
|
||||
}
|
||||
String path=getFilePath();
|
||||
int i=path.lastIndexOf('.');
|
||||
if(i>0){
|
||||
return path.substring(i);
|
||||
}
|
||||
try {
|
||||
String magicExt=FileMagic.getExtensionFromMagic(getInputSource());
|
||||
if(magicExt!=null){
|
||||
return magicExt;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return getFilePath();
|
||||
}
|
||||
}
|
|
@ -1,845 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.group.EntryGroup;
|
||||
import com.reandroid.arsc.pool.SpecStringPool;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
import com.reandroid.arsc.util.ResNameMap;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.json.JSONObject;
|
||||
import com.reandroid.xml.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**Use {@link com.reandroid.identifiers.TableIdentifier} */
|
||||
@Deprecated
|
||||
public class ResourceIds {
|
||||
private final Table mTable;
|
||||
public ResourceIds(Table table){
|
||||
this.mTable=table;
|
||||
}
|
||||
public ResourceIds(){
|
||||
this(new Table());
|
||||
}
|
||||
public Table getTable(){
|
||||
return mTable;
|
||||
}
|
||||
public int applyTo(TableBlock tableBlock){
|
||||
return mTable.applyTo(tableBlock);
|
||||
}
|
||||
public void fromJson(JSONObject jsonObject){
|
||||
mTable.fromJson(jsonObject);
|
||||
}
|
||||
public JSONObject toJson(){
|
||||
return mTable.toJson();
|
||||
}
|
||||
public void loadTableBlock(TableBlock tableBlock){
|
||||
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||||
loadPackageBlock(packageBlock);
|
||||
}
|
||||
}
|
||||
public void loadPackageBlock(PackageBlock packageBlock){
|
||||
Collection<EntryGroup> entryGroupList = packageBlock.listEntryGroup();
|
||||
String name= packageBlock.getName();
|
||||
for(EntryGroup entryGroup:entryGroupList){
|
||||
Table.Package.Type.Entry entry= Table.Package.Type.Entry.fromEntryGroup(entryGroup);
|
||||
mTable.add(entry);
|
||||
if(name==null){
|
||||
continue;
|
||||
}
|
||||
Table.Package.Type type=entry.type;
|
||||
if(type!=null && type.mPackage!=null){
|
||||
type.mPackage.name=name;
|
||||
name=null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void writeXml(File file) throws IOException {
|
||||
mTable.writeXml(file);
|
||||
}
|
||||
public void writeXml(OutputStream outputStream) throws IOException {
|
||||
mTable.writeXml(outputStream);
|
||||
}
|
||||
public void fromXml(File file) throws IOException {
|
||||
mTable.fromXml(file);
|
||||
}
|
||||
public void fromXml(InputStream inputStream) throws IOException {
|
||||
mTable.fromXml(inputStream);
|
||||
}
|
||||
public void fromXml(XMLDocument xmlDocument) throws IOException {
|
||||
mTable.fromXml(xmlDocument);
|
||||
}
|
||||
|
||||
public XMLDocument toXMLDocument(){
|
||||
return mTable.toXMLDocument();
|
||||
}
|
||||
|
||||
public static class Table implements Comparator<Table.Package>{
|
||||
public final Map<Byte, Package> packageMap;
|
||||
public Table(){
|
||||
this.packageMap = new HashMap<>();
|
||||
}
|
||||
public int applyTo(TableBlock tableBlock){
|
||||
int renameCount=0;
|
||||
for(PackageBlock packageBlock : tableBlock.listPackages()){
|
||||
Package pkg=getPackage((byte) packageBlock.getId());
|
||||
if(pkg!=null){
|
||||
renameCount+=pkg.applyTo(packageBlock);
|
||||
}
|
||||
}
|
||||
if(renameCount>0){
|
||||
tableBlock.refresh();
|
||||
}
|
||||
return renameCount;
|
||||
}
|
||||
public void add(Package pkg){
|
||||
Package exist=this.packageMap.get(pkg.id);
|
||||
if(exist!=null){
|
||||
exist.merge(pkg);
|
||||
return;
|
||||
}
|
||||
this.packageMap.put(pkg.id, pkg);
|
||||
}
|
||||
public Package add(Package.Type.Entry entry){
|
||||
if(entry==null){
|
||||
return null;
|
||||
}
|
||||
byte pkgId=entry.getPackageId();
|
||||
Package pkg = packageMap.get(pkgId);
|
||||
if(pkg==null){
|
||||
pkg=new Package(pkgId);
|
||||
packageMap.put(pkgId, pkg);
|
||||
}
|
||||
pkg.add(entry);
|
||||
return pkg;
|
||||
}
|
||||
public Package.Type.Entry getEntry(int resourceId){
|
||||
byte packageId = (byte) ((resourceId>>24) & 0xff);
|
||||
byte typeId = (byte) ((resourceId>>16) & 0xff);
|
||||
short entryId = (short) (resourceId & 0xff);
|
||||
Package pkg = getPackage(packageId);
|
||||
if(pkg == null){
|
||||
return null;
|
||||
}
|
||||
return getEntry(packageId, typeId, entryId);
|
||||
}
|
||||
public Package getPackage(byte packageId){
|
||||
return packageMap.get(packageId);
|
||||
}
|
||||
public Package.Type getType(byte packageId, byte typeId){
|
||||
Package pkg=getPackage(packageId);
|
||||
if(pkg==null){
|
||||
return null;
|
||||
}
|
||||
return pkg.getType(typeId);
|
||||
}
|
||||
public Package.Type.Entry getEntry(byte packageId, byte typeId, short entryId){
|
||||
Package pkg=getPackage(packageId);
|
||||
if(pkg==null){
|
||||
return null;
|
||||
}
|
||||
return pkg.getEntry(typeId, entryId);
|
||||
}
|
||||
public List<Package> listPackages(){
|
||||
List<Package> results=new ArrayList<>(packageMap.values());
|
||||
results.sort(this);
|
||||
return results;
|
||||
}
|
||||
public List<Package.Type.Entry> listEntries(){
|
||||
List<Package.Type.Entry> results=new ArrayList<>(countEntries());
|
||||
for(Package pkg:packageMap.values()){
|
||||
results.addAll(pkg.listEntries());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
int countEntries(){
|
||||
int result=0;
|
||||
for(Package pkg:packageMap.values()){
|
||||
result+=pkg.countEntries();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public JSONObject toJson(){
|
||||
JSONObject jsonObject=new JSONObject();
|
||||
JSONArray jsonArray=new JSONArray();
|
||||
for(Package pkg: packageMap.values()){
|
||||
jsonArray.put(pkg.toJson());
|
||||
}
|
||||
jsonObject.put("packages", jsonArray);
|
||||
return jsonObject;
|
||||
}
|
||||
public void fromJson(JSONObject jsonObject){
|
||||
JSONArray jsonArray= jsonObject.optJSONArray("packages");
|
||||
if(jsonArray!=null){
|
||||
int length= jsonArray.length();
|
||||
for(int i=0;i<length;i++){
|
||||
this.add(Package.fromJson(jsonArray.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
public XMLDocument toXMLDocument(){
|
||||
XMLDocument xmlDocument = new XMLDocument();
|
||||
XMLElement documentElement = new XMLElement("resources");
|
||||
for(Package pkg:listPackages()){
|
||||
pkg.toXMLElements(documentElement);
|
||||
}
|
||||
xmlDocument.setDocumentElement(documentElement);
|
||||
return xmlDocument;
|
||||
}
|
||||
public void writeXml(File file) throws IOException {
|
||||
File dir=file.getParentFile();
|
||||
if(dir!=null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream=new FileOutputStream(file);
|
||||
writeXml(outputStream);
|
||||
}
|
||||
public void writeXml(OutputStream outputStream) throws IOException {
|
||||
String indent = " ";
|
||||
writeXml(indent, outputStream);
|
||||
}
|
||||
public void writeXml(String indent, OutputStream outputStream) throws IOException {
|
||||
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
|
||||
writeXml(indent, writer);
|
||||
writer.close();
|
||||
outputStream.close();
|
||||
}
|
||||
public void writeXml(String indent, Writer writer) throws IOException{
|
||||
writeBegin(writer);
|
||||
for(Package pkg:listPackages()){
|
||||
pkg.writeXml(indent, writer);
|
||||
}
|
||||
writeEnd(writer);
|
||||
writer.flush();
|
||||
}
|
||||
private void writeBegin(Writer writer) throws IOException{
|
||||
writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
writer.write('\n');
|
||||
writer.write("<resources>");
|
||||
}
|
||||
private void writeEnd(Writer writer) throws IOException{
|
||||
writer.write('\n');
|
||||
writer.write("</resources>");
|
||||
}
|
||||
public void fromXml(File file) throws IOException {
|
||||
FileInputStream inputStream=new FileInputStream(file);
|
||||
fromXml(inputStream);
|
||||
inputStream.close();
|
||||
}
|
||||
public void fromXml(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
fromXml(XMLDocument.load(inputStream));
|
||||
} catch (XMLException ex) {
|
||||
throw new IOException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
public void fromXml(XMLDocument xmlDocument) {
|
||||
XMLElement documentElement = xmlDocument.getDocumentElement();
|
||||
int count=documentElement.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement element=documentElement.getChildAt(i);
|
||||
Package pkg = add(Package.Type.Entry.fromXml(element));
|
||||
pkg.setPackageName(element.getCommentAt(0));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int compare(Package pkg1, Package pkg2) {
|
||||
return pkg1.compareTo(pkg2);
|
||||
}
|
||||
|
||||
public static class Package implements Comparable<Package>, Comparator<Package.Type>{
|
||||
public final byte id;
|
||||
public String name;
|
||||
public final Map<Byte, Type> typeMap;
|
||||
private final ResNameMap<Integer> mEntryNameMap;
|
||||
public Package(byte id){
|
||||
this.id = id;
|
||||
this.typeMap = new HashMap<>();
|
||||
this.mEntryNameMap = new ResNameMap<>();
|
||||
}
|
||||
public void loadEntryMap(){
|
||||
mEntryNameMap.clear();
|
||||
for(Type type:typeMap.values()){
|
||||
String typeName=type.getName();
|
||||
for(Type.Entry entry: type.entryMap.values()){
|
||||
mEntryNameMap.add(typeName, entry.getName(), entry.getResourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
public Integer getResourceId(String typeName, String name){
|
||||
return mEntryNameMap.get(typeName, name);
|
||||
}
|
||||
public Type.Entry getEntry(String typeName, String name){
|
||||
Type type=getType(typeName);
|
||||
if(type==null){
|
||||
return null;
|
||||
}
|
||||
return type.getEntry(name);
|
||||
}
|
||||
private Type getType(String typeName){
|
||||
for(Type type:typeMap.values()){
|
||||
if(type.getName().equals(typeName)){
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public int getIdInt(){
|
||||
return 0xff & id;
|
||||
}
|
||||
public int applyTo(PackageBlock packageBlock){
|
||||
int renameCount=0;
|
||||
Map<Integer, EntryGroup> map = packageBlock.getEntriesGroupMap();
|
||||
for(Map.Entry<Integer, EntryGroup> entry:map.entrySet()){
|
||||
byte typeId=Table.toTypeId(entry.getKey());
|
||||
Type type=typeMap.get(typeId);
|
||||
if(type==null){
|
||||
continue;
|
||||
}
|
||||
EntryGroup entryGroup=entry.getValue();
|
||||
if(type.applyTo(entryGroup)){
|
||||
renameCount++;
|
||||
}
|
||||
}
|
||||
if(renameCount>0){
|
||||
cleanSpecStringPool(packageBlock);
|
||||
}
|
||||
return renameCount;
|
||||
}
|
||||
private void cleanSpecStringPool(PackageBlock packageBlock){
|
||||
SpecStringPool specStringPool = packageBlock.getSpecStringPool();
|
||||
specStringPool.refreshUniqueIdMap();
|
||||
specStringPool.removeUnusedStrings();
|
||||
packageBlock.refresh();
|
||||
}
|
||||
public void merge(Package pkg){
|
||||
if(pkg==this||pkg==null){
|
||||
return;
|
||||
}
|
||||
if(pkg.id!=this.id){
|
||||
throw new DuplicateException("Different package id: "+this.id+"!="+pkg.id);
|
||||
}
|
||||
if(pkg.name!=null){
|
||||
this.name = pkg.name;
|
||||
}
|
||||
for(Type type:pkg.typeMap.values()){
|
||||
add(type);
|
||||
}
|
||||
}
|
||||
public Type getType(byte typeId){
|
||||
return typeMap.get(typeId);
|
||||
}
|
||||
public void add(Type type){
|
||||
Byte typeId= type.id;;
|
||||
Type exist=this.typeMap.get(typeId);
|
||||
if(exist!=null){
|
||||
exist.merge(type);
|
||||
return;
|
||||
}
|
||||
type.mPackage=this;
|
||||
this.typeMap.put(typeId, type);
|
||||
}
|
||||
public Package.Type.Entry getEntry(byte typeId, short entryId){
|
||||
Package.Type type=getType(typeId);
|
||||
if(type==null){
|
||||
return null;
|
||||
}
|
||||
return type.getEntry(entryId);
|
||||
}
|
||||
public void add(Type.Entry entry){
|
||||
if(entry==null){
|
||||
return;
|
||||
}
|
||||
if(entry.getPackageId()!=this.id){
|
||||
throw new DuplicateException("Different package id: "+entry);
|
||||
}
|
||||
byte typeId=entry.getTypeId();
|
||||
Type type=typeMap.get(typeId);
|
||||
if(type==null){
|
||||
type=new Type(typeId);
|
||||
type.mPackage=this;
|
||||
typeMap.put(typeId, type);
|
||||
}
|
||||
type.add(entry);
|
||||
}
|
||||
public String getHexId(){
|
||||
return HexUtil.toHex2(id);
|
||||
}
|
||||
public JSONObject toJson(){
|
||||
JSONObject jsonObject=new JSONObject();
|
||||
jsonObject.put("id", this.getIdInt());
|
||||
if(this.name!=null){
|
||||
jsonObject.put("name", this.name);
|
||||
}
|
||||
JSONArray jsonArray=new JSONArray();
|
||||
for(Type type:typeMap.values()){
|
||||
jsonArray.put(type.toJson());
|
||||
}
|
||||
jsonObject.put("types", jsonArray);
|
||||
return jsonObject;
|
||||
}
|
||||
@Override
|
||||
public int compareTo(Package pkg) {
|
||||
return Integer.compare(getIdInt(), pkg.getIdInt());
|
||||
}
|
||||
@Override
|
||||
public int compare(Type t1, Type t2) {
|
||||
return t1.compareTo(t2);
|
||||
}
|
||||
public void toXMLElements(XMLElement documentElement){
|
||||
int count = documentElement.getChildesCount();
|
||||
for(Type type:listTypes()){
|
||||
type.toXMLElements(documentElement);
|
||||
}
|
||||
XMLElement firstElement = documentElement.getChildAt(count);
|
||||
if(firstElement!=null){
|
||||
XMLComment comment = new XMLComment(
|
||||
"packageName=\""+this.name+"\"");
|
||||
firstElement.addComment(comment);
|
||||
}
|
||||
}
|
||||
void setPackageName(XMLComment xmlComment){
|
||||
if(xmlComment==null){
|
||||
return;
|
||||
}
|
||||
String pkgName = xmlComment.getCommentText();
|
||||
if(pkgName==null || !pkgName.contains("packageName")){
|
||||
return;
|
||||
}
|
||||
int i = pkgName.indexOf('"');
|
||||
if(i>0){
|
||||
i++;
|
||||
pkgName=pkgName.substring(i);
|
||||
}else {
|
||||
return;
|
||||
}
|
||||
i = pkgName.indexOf('"');
|
||||
if(i>0){
|
||||
pkgName=pkgName.substring(0, i);
|
||||
}else {
|
||||
return;
|
||||
}
|
||||
this.name=pkgName.trim();
|
||||
}
|
||||
public List<Package.Type.Entry> listEntries(){
|
||||
List<Package.Type.Entry> results=new ArrayList<>(countEntries());
|
||||
for(Package.Type type:typeMap.values()){
|
||||
results.addAll(type.listEntries());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public List<Type> listTypes(){
|
||||
List<Type> results=new ArrayList<>(typeMap.values());
|
||||
results.sort(this);
|
||||
return results;
|
||||
}
|
||||
int countEntries(){
|
||||
int results=0;
|
||||
for(Type type:typeMap.values()){
|
||||
results+=type.countEntries();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public void writeXml(String indent, Writer writer) throws IOException{
|
||||
writeComment(indent, writer);
|
||||
for(Type type:listTypes()){
|
||||
type.writeXml(indent, writer);
|
||||
}
|
||||
}
|
||||
private void writeComment(String indent, Writer writer) throws IOException{
|
||||
String name = this.name;
|
||||
if(name == null){
|
||||
return;
|
||||
}
|
||||
writer.write('\n');
|
||||
writer.write(indent);
|
||||
writer.write("<!--packageName=\"");
|
||||
writer.write(name);
|
||||
writer.write("\"-->");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Package aPackage = (Package) o;
|
||||
return id == aPackage.id;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return getHexId() + ", types=" + typeMap.size();
|
||||
}
|
||||
|
||||
public static Package fromJson(JSONObject jsonObject){
|
||||
Package pkg=new Package((byte) jsonObject.getInt("id"));
|
||||
pkg.name = jsonObject.optString("name", null);
|
||||
JSONArray jsonArray = jsonObject.optJSONArray("types");
|
||||
int length = jsonArray.length();
|
||||
for(int i=0;i<length;i++){
|
||||
Type type=Type.fromJson(jsonArray.getJSONObject(i));
|
||||
pkg.add(type);
|
||||
}
|
||||
return pkg;
|
||||
}
|
||||
|
||||
public static class Type implements Comparable<Type>, Comparator<Type.Entry>{
|
||||
public final byte id;
|
||||
public String name;
|
||||
public String nameAlias;
|
||||
public Package mPackage;
|
||||
public final Map<Short, Entry> entryMap;
|
||||
public Type(byte id){
|
||||
this.id = id;
|
||||
this.entryMap = new HashMap<>();
|
||||
}
|
||||
public Entry getEntry(String entryName){
|
||||
for(Entry entry:entryMap.values()){
|
||||
if(entry.getName().equals(entryName)){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public int getIdInt(){
|
||||
return 0xff & id;
|
||||
}
|
||||
public boolean applyTo(EntryGroup entryGroup){
|
||||
boolean renamed=false;
|
||||
Entry entry=entryMap.get(entryGroup.getEntryId());
|
||||
if(entry!=null){
|
||||
if(entry.applyTo(entryGroup)){
|
||||
renamed=true;
|
||||
}
|
||||
}
|
||||
return renamed;
|
||||
}
|
||||
public String getName() {
|
||||
if(nameAlias!=null){
|
||||
return nameAlias;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public byte getId(){
|
||||
return id;
|
||||
}
|
||||
public byte getPackageId(){
|
||||
if(mPackage!=null){
|
||||
return mPackage.id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public void merge(Type type){
|
||||
if(type==this||type==null){
|
||||
return;
|
||||
}
|
||||
if(this.id!= type.id){
|
||||
throw new DuplicateException("Different type ids: "+id+"!="+type.id);
|
||||
}
|
||||
String n=type.getName();
|
||||
if(n!=null){
|
||||
this.name=n;
|
||||
}
|
||||
for(Entry entry:type.entryMap.values()){
|
||||
Short entryId=entry.getEntryId();
|
||||
Entry existEntry=this.entryMap.get(entryId);
|
||||
if(existEntry != null && Objects.equals(existEntry.getName(), entry.getName())){
|
||||
continue;
|
||||
}
|
||||
this.entryMap.remove(entryId);
|
||||
entry.type=this;
|
||||
this.entryMap.put(entryId, entry);
|
||||
}
|
||||
}
|
||||
public Entry getEntry(short entryId){
|
||||
return entryMap.get(entryId);
|
||||
}
|
||||
public String getHexId(){
|
||||
return HexUtil.toHex2(id);
|
||||
}
|
||||
public void add(Entry entry){
|
||||
if(entry==null){
|
||||
return;
|
||||
}
|
||||
if(entry.getTypeId()!=this.id){
|
||||
throw new DuplicateException("Different type id: "+entry);
|
||||
}
|
||||
short key=entry.getEntryId();
|
||||
Entry exist=entryMap.get(key);
|
||||
if(exist!=null){
|
||||
if(Objects.equals(exist.getName(), entry.getName())){
|
||||
return;
|
||||
}
|
||||
/* Developer may have a reason adding duplicate
|
||||
resource ids , lets ignore rather than throw
|
||||
*/
|
||||
// throw new DuplicateException("Duplicate entry exist: "+exist+", entry: "+entry);
|
||||
return;
|
||||
}
|
||||
if(getName() == null){
|
||||
this.name = entry.getTypeName();
|
||||
}
|
||||
entry.type=this;
|
||||
entryMap.put(key, entry);
|
||||
}
|
||||
|
||||
public JSONObject toJson(){
|
||||
JSONObject jsonObject=new JSONObject();
|
||||
jsonObject.put("id", getIdInt());
|
||||
jsonObject.put("name", getName());
|
||||
JSONArray jsonArray=new JSONArray();
|
||||
for(Entry entry: entryMap.values()){
|
||||
jsonArray.put(entry.toJson());
|
||||
}
|
||||
jsonObject.put("entries", jsonArray);
|
||||
return jsonObject;
|
||||
}
|
||||
public void toXMLElements(XMLElement documentElement){
|
||||
for(Entry entry:listEntries()){
|
||||
documentElement.addChild(entry.toXMLElement());
|
||||
}
|
||||
}
|
||||
public List<Entry> listEntries(){
|
||||
List<Entry> results=new ArrayList<>(entryMap.values());
|
||||
results.sort(this);
|
||||
return results;
|
||||
}
|
||||
int countEntries(){
|
||||
return entryMap.size();
|
||||
}
|
||||
public void writeXml(String indent, Writer writer) throws IOException{
|
||||
for(Entry entry:listEntries()){
|
||||
entry.writeXml(indent, writer);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int compareTo(Type type) {
|
||||
return Integer.compare(getIdInt(), type.getIdInt());
|
||||
}
|
||||
@Override
|
||||
public int compare(Entry entry1, Entry entry2) {
|
||||
return entry1.compareTo(entry2);
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Type that = (Type) o;
|
||||
return id == that.id;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
StringBuilder builder=new StringBuilder();
|
||||
builder.append(getHexId());
|
||||
String n=getName();
|
||||
if(n !=null){
|
||||
builder.append(" ").append(n);
|
||||
}
|
||||
builder.append(", entries=").append(entryMap.size());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static Type fromJson(JSONObject jsonObject){
|
||||
Type type = new Type((byte) jsonObject.getInt("id"));
|
||||
type.name = jsonObject.optString("name", null);
|
||||
JSONArray jsonArray = jsonObject.optJSONArray("entries");
|
||||
if(jsonArray!=null){
|
||||
int length=jsonArray.length();
|
||||
for(int i=0;i<length;i++){
|
||||
Entry entry=Entry.fromJson(jsonArray.getJSONObject(i));
|
||||
type.add(entry);
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public static class Entry implements Comparable<Entry>{
|
||||
public int resourceId;
|
||||
public String typeName;
|
||||
public String name;
|
||||
public String nameAlias;
|
||||
public Type type;
|
||||
public Entry(int resourceId, String typeName, String name){
|
||||
this.resourceId = resourceId;
|
||||
this.typeName = typeName;
|
||||
this.name = name;
|
||||
}
|
||||
public Entry(int resourceId, String name){
|
||||
this(resourceId, null, name);
|
||||
}
|
||||
public boolean applyTo(EntryGroup entryGroup){
|
||||
return entryGroup.renameSpec(this.getName());
|
||||
}
|
||||
public String getName() {
|
||||
if(nameAlias!=null){
|
||||
return nameAlias;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
public String getTypeName(){
|
||||
if(this.type!=null){
|
||||
return this.type.getName();
|
||||
}
|
||||
return this.typeName;
|
||||
}
|
||||
public byte getPackageId(){
|
||||
if(this.type!=null){
|
||||
Package pkg=this.type.mPackage;
|
||||
if(pkg!=null){
|
||||
return pkg.id;
|
||||
}
|
||||
}
|
||||
return (byte) ((resourceId>>24) & 0xff);
|
||||
}
|
||||
public byte getTypeId(){
|
||||
if(this.type!=null){
|
||||
return this.type.id;
|
||||
}
|
||||
return (byte) ((resourceId>>16) & 0xff);
|
||||
}
|
||||
public short getEntryId(){
|
||||
return (short) (resourceId & 0xffff);
|
||||
}
|
||||
public int getEntryIdInt(){
|
||||
return resourceId & 0xffff;
|
||||
}
|
||||
public int getResourceId(){
|
||||
return ((getPackageId() & 0xff)<<24)
|
||||
| ((getTypeId() & 0xff)<<16)
|
||||
| (getEntryId() & 0xffff);
|
||||
}
|
||||
public String getHexId(){
|
||||
return HexUtil.toHex8(getResourceId());
|
||||
}
|
||||
|
||||
public void writeXml(String indent, Writer writer) throws IOException{
|
||||
writer.write('\n');
|
||||
writer.write(indent);
|
||||
writer.write("<public id=\"");
|
||||
writer.write(getHexId());
|
||||
writer.write("\" type=\"");
|
||||
String str = getTypeName();
|
||||
if(str != null){
|
||||
writer.write(str);
|
||||
}
|
||||
writer.write("\" name=\"");
|
||||
str = getName();
|
||||
if(str != null){
|
||||
writer.write(str);
|
||||
}
|
||||
writer.write("\"/>");
|
||||
}
|
||||
@Override
|
||||
public int compareTo(Entry entry) {
|
||||
return Integer.compare(getEntryIdInt(), entry.getEntryIdInt());
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Entry that = (Entry) o;
|
||||
return getResourceId() == that.getResourceId();
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getResourceId());
|
||||
}
|
||||
public JSONObject toJson(){
|
||||
JSONObject jsonObject=new JSONObject();
|
||||
jsonObject.put("id", getResourceId());
|
||||
jsonObject.put("name", getName());
|
||||
return jsonObject;
|
||||
}
|
||||
public XMLElement toXMLElement(){
|
||||
XMLElement element=new XMLElement("public");
|
||||
element.setResourceId(getResourceId());
|
||||
element.addAttribute(new XMLAttribute("id", getHexId()));
|
||||
element.addAttribute(new XMLAttribute("type", getTypeName()));
|
||||
element.addAttribute(new XMLAttribute("name", getName()));
|
||||
return element;
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return toXMLElement().toText(false);
|
||||
}
|
||||
public static Entry fromEntryGroup(EntryGroup entryGroup){
|
||||
return new Entry(entryGroup.getResourceId(),
|
||||
entryGroup.getTypeName(),
|
||||
entryGroup.getSpecName());
|
||||
}
|
||||
public static Entry fromJson(JSONObject jsonObject){
|
||||
return new Entry(jsonObject.getInt("id"),
|
||||
jsonObject.optString("type", null),
|
||||
jsonObject.getString("name"));
|
||||
}
|
||||
public static Entry fromXml(XMLElement element){
|
||||
return new Entry(
|
||||
ApkUtil.parseHex(element.getAttributeValue("id")),
|
||||
element.getAttributeValue("type"),
|
||||
element.getAttributeValue("name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private static short toEntryId(int resourceId){
|
||||
int i=resourceId&0xffff;
|
||||
return (short) i;
|
||||
}
|
||||
static byte toTypeId(int resourceId){
|
||||
int i=resourceId>>16;
|
||||
i=i&0xff;
|
||||
return (byte) i;
|
||||
}
|
||||
static byte toPackageId(int resourceId){
|
||||
int i=resourceId>>24;
|
||||
i=i&0xff;
|
||||
return (byte) i;
|
||||
}
|
||||
static int toResourceId(byte pkgId, byte typeId, short entryId){
|
||||
return (pkgId & 0xff)<<24
|
||||
| (typeId & 0xff)<<16
|
||||
| (entryId & 0xffff);
|
||||
}
|
||||
}
|
||||
public static class DuplicateException extends IllegalArgumentException{
|
||||
public DuplicateException(String message){
|
||||
super(message);
|
||||
}
|
||||
public DuplicateException(String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
public DuplicateException(Throwable cause) {
|
||||
super(cause.getMessage(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static final String JSON_FILE_NAME ="resource-ids.json";
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.json.JSONException;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class SingleJsonTableInputSource extends InputSource {
|
||||
private final InputSource inputSource;
|
||||
private TableBlock mCache;
|
||||
private APKLogger apkLogger;
|
||||
public SingleJsonTableInputSource(InputSource inputSource) {
|
||||
super(inputSource.getAlias());
|
||||
this.inputSource=inputSource;
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getTableBlock().writeBytes(outputStream);
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
TableBlock tableBlock = getTableBlock();
|
||||
return new ByteArrayInputStream(tableBlock.getBytes());
|
||||
}
|
||||
@Override
|
||||
public long getLength() throws IOException{
|
||||
TableBlock tableBlock = getTableBlock();
|
||||
return tableBlock.countBytes();
|
||||
}
|
||||
@Override
|
||||
public long getCrc() throws IOException {
|
||||
CrcOutputStream outputStream=new CrcOutputStream();
|
||||
this.write(outputStream);
|
||||
return outputStream.getCrcValue();
|
||||
}
|
||||
public TableBlock getTableBlock() throws IOException{
|
||||
if(mCache != null){
|
||||
return mCache;
|
||||
}
|
||||
logMessage("Building resources table: " + inputSource.getAlias());
|
||||
TableBlock tableBlock=newInstance();
|
||||
InputStream inputStream = inputSource.openStream();
|
||||
try{
|
||||
StringPoolBuilder poolBuilder = new StringPoolBuilder();
|
||||
JSONObject jsonObject = new JSONObject(inputStream);
|
||||
poolBuilder.build(jsonObject);
|
||||
poolBuilder.apply(tableBlock);
|
||||
tableBlock.fromJson(jsonObject);
|
||||
}catch (JSONException ex){
|
||||
throw new IOException(inputSource.getAlias(), ex);
|
||||
}
|
||||
mCache = tableBlock;
|
||||
return tableBlock;
|
||||
}
|
||||
TableBlock newInstance(){
|
||||
return new TableBlock();
|
||||
}
|
||||
public static SingleJsonTableInputSource fromFile(File rootDir, File jsonFile){
|
||||
String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile);
|
||||
FileInputSource fileInputSource=new FileInputSource(jsonFile, path);
|
||||
return new SingleJsonTableInputSource(fileInputSource);
|
||||
}
|
||||
void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
private void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class SplitJsonTableInputSource extends InputSource {
|
||||
private final File dir;
|
||||
private TableBlock mCache;
|
||||
private APKLogger apkLogger;
|
||||
public SplitJsonTableInputSource(File dir) {
|
||||
super(TableBlock.FILE_NAME);
|
||||
this.dir=dir;
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getTableBlock().writeBytes(outputStream);
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
TableBlock tableBlock = getTableBlock();
|
||||
return new ByteArrayInputStream(tableBlock.getBytes());
|
||||
}
|
||||
@Override
|
||||
public long getLength() throws IOException{
|
||||
TableBlock tableBlock = getTableBlock();
|
||||
return tableBlock.countBytes();
|
||||
}
|
||||
@Override
|
||||
public long getCrc() throws IOException {
|
||||
CrcOutputStream outputStream=new CrcOutputStream();
|
||||
this.write(outputStream);
|
||||
return outputStream.getCrcValue();
|
||||
}
|
||||
public TableBlock getTableBlock() throws IOException {
|
||||
if(mCache!=null){
|
||||
return mCache;
|
||||
}
|
||||
TableBlockJsonBuilder builder=new TableBlockJsonBuilder();
|
||||
TableBlock tableBlock=builder.scanDirectory(dir);
|
||||
mCache=tableBlock;
|
||||
return tableBlock;
|
||||
}
|
||||
void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
}
|
||||
void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.pool.SpecStringPool;
|
||||
import com.reandroid.arsc.pool.TableStringPool;
|
||||
import com.reandroid.arsc.value.ValueHeader;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.json.JSONException;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class StringPoolBuilder {
|
||||
private final Map<Integer, Set<String>> mSpecNameMap;
|
||||
private final Set<String> mTableStrings;
|
||||
private int mCurrentPackageId;
|
||||
private JSONArray mStyledStrings;
|
||||
public StringPoolBuilder(){
|
||||
this.mSpecNameMap = new HashMap<>();
|
||||
this.mTableStrings = new HashSet<>();
|
||||
}
|
||||
public void apply(TableBlock tableBlock){
|
||||
applyTableString(tableBlock.getTableStringPool());
|
||||
for(int pkgId:mSpecNameMap.keySet()){
|
||||
PackageBlock packageBlock=tableBlock.getPackageArray().getOrCreate(pkgId);
|
||||
applySpecString(packageBlock.getSpecStringPool());
|
||||
}
|
||||
}
|
||||
private void applyTableString(TableStringPool stringPool){
|
||||
stringPool.fromJson(mStyledStrings);
|
||||
stringPool.addStrings(getTableString());
|
||||
stringPool.refresh();
|
||||
}
|
||||
private void applySpecString(SpecStringPool stringPool){
|
||||
int pkgId = stringPool.getPackageBlock().getId();
|
||||
stringPool.addStrings(getSpecString(pkgId));
|
||||
stringPool.refresh();
|
||||
}
|
||||
public void scanDirectory(File resourcesDir) throws IOException {
|
||||
mCurrentPackageId=0;
|
||||
List<File> pkgDirList=ApkUtil.listDirectories(resourcesDir);
|
||||
for(File dir:pkgDirList){
|
||||
File pkgFile=new File(dir, PackageBlock.JSON_FILE_NAME);
|
||||
scanFile(pkgFile);
|
||||
List<File> jsonFileList=ApkUtil.recursiveFiles(dir, ".json");
|
||||
for(File file:jsonFileList){
|
||||
if(file.equals(pkgFile)){
|
||||
continue;
|
||||
}
|
||||
scanFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void scanFile(File jsonFile) throws IOException {
|
||||
try{
|
||||
FileInputStream inputStream=new FileInputStream(jsonFile);
|
||||
JSONObject jsonObject=new JSONObject(inputStream);
|
||||
build(jsonObject);
|
||||
}catch (JSONException ex){
|
||||
throw new IOException(jsonFile+": "+ex.getMessage());
|
||||
}
|
||||
}
|
||||
public void build(JSONObject jsonObject){
|
||||
scan(jsonObject);
|
||||
}
|
||||
public Set<String> getTableString(){
|
||||
return mTableStrings;
|
||||
}
|
||||
public Set<String> getSpecString(int pkgId){
|
||||
return mSpecNameMap.get(pkgId);
|
||||
}
|
||||
private void scan(JSONObject jsonObject){
|
||||
if(jsonObject.has(ValueHeader.NAME_entry_name)){
|
||||
addSpecName(jsonObject.optString(ValueHeader.NAME_entry_name));
|
||||
}
|
||||
if(jsonObject.has(ApkUtil.NAME_value_type)){
|
||||
if(ValueType.STRING.name().equals(jsonObject.getString(ApkUtil.NAME_value_type))){
|
||||
String data= jsonObject.optString(ApkUtil.NAME_data, "");
|
||||
addTableString(data);
|
||||
}
|
||||
return;
|
||||
}else if(jsonObject.has(PackageBlock.NAME_package_id)){
|
||||
mCurrentPackageId = jsonObject.getInt(PackageBlock.NAME_package_id);
|
||||
}
|
||||
Set<String> keyList = jsonObject.keySet();
|
||||
for(String key:keyList){
|
||||
Object obj=jsonObject.get(key);
|
||||
if(obj instanceof JSONObject){
|
||||
scan((JSONObject) obj);
|
||||
continue;
|
||||
}
|
||||
if(obj instanceof JSONArray){
|
||||
JSONArray jsonArray = (JSONArray) obj;
|
||||
if(TableBlock.NAME_styled_strings.equals(key)){
|
||||
this.mStyledStrings = jsonArray;
|
||||
}else {
|
||||
scan(jsonArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void scan(JSONArray jsonArray){
|
||||
if(jsonArray==null){
|
||||
return;
|
||||
}
|
||||
for(Object obj:jsonArray.getArrayList()){
|
||||
if(obj instanceof JSONObject){
|
||||
scan((JSONObject) obj);
|
||||
continue;
|
||||
}
|
||||
if(obj instanceof JSONArray){
|
||||
scan((JSONArray) obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void addTableString(String name){
|
||||
if(name==null){
|
||||
return;
|
||||
}
|
||||
mTableStrings.add(name);
|
||||
}
|
||||
private void addSpecName(String name){
|
||||
if(name==null){
|
||||
return;
|
||||
}
|
||||
int pkgId=mCurrentPackageId;
|
||||
if(pkgId==0){
|
||||
throw new IllegalArgumentException("Current package id is 0");
|
||||
}
|
||||
Set<String> specNames=mSpecNameMap.get(pkgId);
|
||||
if(specNames==null){
|
||||
specNames=new HashSet<>();
|
||||
mSpecNameMap.put(pkgId, specNames);
|
||||
}
|
||||
specNames.add(name);
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.arsc.BuildInfo;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.StagedAlias;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.container.SpecTypePair;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TableBlockJson {
|
||||
private final TableBlock tableBlock;
|
||||
public TableBlockJson(TableBlock tableBlock){
|
||||
this.tableBlock=tableBlock;
|
||||
}
|
||||
public void writeJsonFiles(File outDir) throws IOException {
|
||||
for(PackageBlock packageBlock: tableBlock.listPackages()){
|
||||
writePackageJsonFiles(outDir, packageBlock);
|
||||
}
|
||||
}
|
||||
private void writePackageJsonFiles(File rootDir, PackageBlock packageBlock) throws IOException {
|
||||
File pkgDir = new File(rootDir, getDirName(packageBlock));
|
||||
|
||||
writePackageJson(pkgDir, packageBlock);
|
||||
|
||||
for(SpecTypePair specTypePair: packageBlock.listSpecTypePairs()){
|
||||
for(TypeBlock typeBlock:specTypePair.getTypeBlockArray().listItems()){
|
||||
writeTypeJsonFiles(pkgDir, typeBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void writePackageJson(File packageDirectory, PackageBlock packageBlock) throws IOException {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
jsonObject.put(BuildInfo.NAME_arsc_lib_version, BuildInfo.getVersion());
|
||||
|
||||
jsonObject.put(PackageBlock.NAME_package_id, packageBlock.getId());
|
||||
jsonObject.put(PackageBlock.NAME_package_name, packageBlock.getName());
|
||||
StagedAlias stagedAlias=StagedAlias
|
||||
.mergeAll(packageBlock.getStagedAliasList().getChildes());
|
||||
if(stagedAlias!=null){
|
||||
jsonObject.put(PackageBlock.NAME_staged_aliases,
|
||||
stagedAlias.getStagedAliasEntryArray().toJson());
|
||||
}
|
||||
|
||||
File packageFile = new File(packageDirectory, PackageBlock.JSON_FILE_NAME);
|
||||
jsonObject.write(packageFile);
|
||||
}
|
||||
private void writeTypeJsonFiles(File packageDirectory, TypeBlock typeBlock) throws IOException {
|
||||
File file=new File(packageDirectory,
|
||||
getFileName(typeBlock) + ApkUtil.JSON_FILE_EXTENSION);
|
||||
JSONObject jsonObject = typeBlock.toJson();
|
||||
jsonObject.write(file);
|
||||
}
|
||||
private String getFileName(TypeBlock typeBlock){
|
||||
StringBuilder builder=new StringBuilder();
|
||||
builder.append(String.format("%03d-", typeBlock.getIndex()));
|
||||
builder.append(HexUtil.toHex2(typeBlock.getTypeId()));
|
||||
String name= typeBlock.getTypeName();
|
||||
builder.append('-').append(name);
|
||||
builder.append(typeBlock.getResConfig().getQualifiers());
|
||||
return builder.toString();
|
||||
}
|
||||
private String getDirName(PackageBlock packageBlock){
|
||||
StringBuilder builder=new StringBuilder();
|
||||
builder.append(HexUtil.toHex2((byte) packageBlock.getId()));
|
||||
builder.append("-");
|
||||
builder.append(packageBlock.getIndex());
|
||||
String name= ApkUtil.sanitizeForFileName(packageBlock.getName());
|
||||
if(name!=null){
|
||||
builder.append('-');
|
||||
builder.append(name);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.StagedAlias;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.value.ResConfig;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class TableBlockJsonBuilder {
|
||||
private final StringPoolBuilder poolBuilder;
|
||||
public TableBlockJsonBuilder(){
|
||||
poolBuilder=new StringPoolBuilder();
|
||||
}
|
||||
public TableBlock scanDirectory(File resourcesDir) throws IOException {
|
||||
if(!resourcesDir.isDirectory()){
|
||||
throw new IOException("No such directory: "+resourcesDir);
|
||||
}
|
||||
List<File> pkgDirList=ApkUtil.listDirectories(resourcesDir);
|
||||
if(pkgDirList.size()==0){
|
||||
throw new IOException("No package sub directory found in : "+resourcesDir);
|
||||
}
|
||||
TableBlock tableBlock=new TableBlock();
|
||||
poolBuilder.scanDirectory(resourcesDir);
|
||||
poolBuilder.apply(tableBlock);
|
||||
for(File pkgDir:pkgDirList){
|
||||
scanPackageDirectory(tableBlock, pkgDir);
|
||||
}
|
||||
tableBlock.sortPackages();
|
||||
tableBlock.refresh();
|
||||
return tableBlock;
|
||||
}
|
||||
private void scanPackageDirectory(TableBlock tableBlock, File pkgDir) throws IOException{
|
||||
File pkgFile=new File(pkgDir, PackageBlock.JSON_FILE_NAME);
|
||||
if(!pkgFile.isFile()){
|
||||
throw new IOException("Invalid package directory! Package file missing: "+pkgFile);
|
||||
}
|
||||
FileInputStream inputStream=new FileInputStream(pkgFile);
|
||||
JSONObject jsonObject=new JSONObject(inputStream);
|
||||
PackageBlock pkg=tableBlock.getPackageArray()
|
||||
.getOrCreate(jsonObject.getInt(PackageBlock.NAME_package_id));
|
||||
pkg.setName(jsonObject.optString(PackageBlock.NAME_package_name));
|
||||
if(jsonObject.has(PackageBlock.NAME_staged_aliases)){
|
||||
JSONArray stagedJson = jsonObject.getJSONArray(PackageBlock.NAME_staged_aliases);
|
||||
StagedAlias stagedAlias = new StagedAlias();
|
||||
stagedAlias.getStagedAliasEntryArray().fromJson(stagedJson);
|
||||
pkg.getStagedAliasList().add(stagedAlias);
|
||||
}
|
||||
List<File> typeFileList = ApkUtil.listFiles(pkgDir, ApkUtil.JSON_FILE_EXTENSION);
|
||||
typeFileList.remove(pkgFile);
|
||||
for(File typeFile:typeFileList){
|
||||
loadType(pkg, typeFile);
|
||||
}
|
||||
pkg.sortTypes();
|
||||
}
|
||||
private void loadType(PackageBlock packageBlock, File typeJsonFile) throws IOException{
|
||||
FileInputStream inputStream=new FileInputStream(typeJsonFile);
|
||||
JSONObject jsonObject=new JSONObject(inputStream);
|
||||
JSONObject configObj=jsonObject.getJSONObject(TypeBlock.NAME_config);
|
||||
ResConfig resConfig=new ResConfig();
|
||||
resConfig.fromJson(configObj);
|
||||
TypeBlock typeBlock=packageBlock.getSpecTypePairArray()
|
||||
.getOrCreate(
|
||||
((byte)(0xff & jsonObject.getInt(TypeBlock.NAME_id)))
|
||||
, resConfig);
|
||||
typeBlock.fromJson(jsonObject);
|
||||
}
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive.ZipArchive;
|
||||
import com.reandroid.json.JSONArray;
|
||||
import com.reandroid.json.JSONConvert;
|
||||
import com.reandroid.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class UncompressedFiles implements JSONConvert<JSONObject> {
|
||||
private final Set<String> mPathList;
|
||||
private final Set<String> mExtensionList;
|
||||
private String mResRawDir;
|
||||
public UncompressedFiles(){
|
||||
this.mPathList=new HashSet<>();
|
||||
this.mExtensionList=new HashSet<>();
|
||||
}
|
||||
public void setResRawDir(String resRawDir){
|
||||
this.mResRawDir=resRawDir;
|
||||
}
|
||||
public void apply(ZipArchive archive){
|
||||
for(InputSource inputSource:archive.listInputSources()){
|
||||
apply(inputSource);
|
||||
}
|
||||
}
|
||||
public void apply(InputSource inputSource){
|
||||
if(isUncompressed(inputSource.getAlias()) || isUncompressed(inputSource.getName())){
|
||||
inputSource.setMethod(ZipEntry.STORED);
|
||||
}else {
|
||||
inputSource.setMethod(ZipEntry.DEFLATED);
|
||||
}
|
||||
}
|
||||
public boolean isUncompressed(String path){
|
||||
if(path==null){
|
||||
return false;
|
||||
}
|
||||
if(containsPath(path)||containsExtension(path)||isResRawDir(path)){
|
||||
return true;
|
||||
}
|
||||
String extension=getExtension(path);
|
||||
return containsExtension(extension);
|
||||
}
|
||||
private boolean isResRawDir(String path){
|
||||
String dir=mResRawDir;
|
||||
if(dir==null||dir.length()==0){
|
||||
return false;
|
||||
}
|
||||
return path.startsWith(dir);
|
||||
}
|
||||
public boolean containsExtension(String extension){
|
||||
if(extension==null){
|
||||
return false;
|
||||
}
|
||||
if(mExtensionList.contains(extension)){
|
||||
return true;
|
||||
}
|
||||
if(!extension.startsWith(".")){
|
||||
return mExtensionList.contains("."+extension);
|
||||
}
|
||||
return mExtensionList.contains(extension.substring(1));
|
||||
}
|
||||
public boolean containsPath(String path){
|
||||
path=sanitizePath(path);
|
||||
if(path==null){
|
||||
return false;
|
||||
}
|
||||
return mPathList.contains(path);
|
||||
}
|
||||
public void addPath(ZipArchive zipArchive){
|
||||
for(InputSource inputSource: zipArchive.listInputSources()){
|
||||
addPath(inputSource);
|
||||
}
|
||||
}
|
||||
public void addPath(InputSource inputSource){
|
||||
if(inputSource.getMethod()!=ZipEntry.STORED){
|
||||
return;
|
||||
}
|
||||
addPath(inputSource.getAlias());
|
||||
}
|
||||
public void addPath(String path){
|
||||
path=sanitizePath(path);
|
||||
if(path==null){
|
||||
return;
|
||||
}
|
||||
mPathList.add(path);
|
||||
}
|
||||
public void removePath(String path){
|
||||
path=sanitizePath(path);
|
||||
if(path==null){
|
||||
return;
|
||||
}
|
||||
mPathList.remove(path);
|
||||
}
|
||||
public void replacePath(String path, String rep){
|
||||
path=sanitizePath(path);
|
||||
rep=sanitizePath(rep);
|
||||
if(path==null||rep==null){
|
||||
return;
|
||||
}
|
||||
if(!mPathList.contains(path)){
|
||||
return;
|
||||
}
|
||||
mPathList.remove(path);
|
||||
mPathList.add(rep);
|
||||
}
|
||||
public void addCommonExtensions(){
|
||||
for(String ext:COMMON_EXTENSIONS){
|
||||
addExtension(ext);
|
||||
}
|
||||
}
|
||||
public void addExtension(String extension){
|
||||
if(extension==null || extension.length()==0){
|
||||
return;
|
||||
}
|
||||
mExtensionList.add(extension);
|
||||
}
|
||||
public void clearPaths(){
|
||||
mPathList.clear();
|
||||
}
|
||||
public void clearExtensions(){
|
||||
mExtensionList.clear();
|
||||
}
|
||||
@Override
|
||||
public JSONObject toJson() {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(NAME_extensions, new JSONArray(mExtensionList));
|
||||
jsonObject.put(NAME_paths, new JSONArray(mPathList));
|
||||
return jsonObject;
|
||||
}
|
||||
@Override
|
||||
public void fromJson(JSONObject json) {
|
||||
clearExtensions();
|
||||
clearPaths();
|
||||
if(json==null){
|
||||
return;
|
||||
}
|
||||
JSONArray extensions = json.optJSONArray(NAME_extensions);
|
||||
if(extensions!=null){
|
||||
int length = extensions.length();
|
||||
for(int i=0;i<length;i++){
|
||||
this.addExtension(extensions.getString(i));
|
||||
}
|
||||
}
|
||||
JSONArray paths = json.optJSONArray(NAME_paths);
|
||||
if(paths!=null){
|
||||
int length = paths.length();
|
||||
for(int i=0;i<length;i++){
|
||||
this.addPath(paths.getString(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
public void merge(UncompressedFiles uf){
|
||||
if(uf==null||uf==this){
|
||||
return;
|
||||
}
|
||||
for(String path: uf.mPathList){
|
||||
addPath(path);
|
||||
}
|
||||
for(String ext:uf.mExtensionList){
|
||||
addExtension(ext);
|
||||
}
|
||||
}
|
||||
public void fromJson(File jsonFile) throws IOException {
|
||||
if(!jsonFile.isFile()){
|
||||
return;
|
||||
}
|
||||
JSONObject jsonObject=new JSONObject(new FileInputStream(jsonFile));
|
||||
fromJson(jsonObject);
|
||||
}
|
||||
private static String sanitizePath(String path){
|
||||
if(path==null || path.length()==0){
|
||||
return null;
|
||||
}
|
||||
path=path.replace(File.separatorChar, '/').trim();
|
||||
while (path.startsWith("/")){
|
||||
path=path.substring(1);
|
||||
}
|
||||
if(path.length()==0){
|
||||
return null;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
private static String getExtension(String path){
|
||||
path=sanitizePath(path);
|
||||
if(path==null){
|
||||
return null;
|
||||
}
|
||||
int i = path.lastIndexOf('/');
|
||||
if(i>0){
|
||||
i++;
|
||||
path=path.substring(i);
|
||||
}
|
||||
i = path.lastIndexOf('.');
|
||||
if(i>0){
|
||||
return path.substring(i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final String JSON_FILE = "uncompressed-files.json";
|
||||
public static final String NAME_paths = "paths";
|
||||
public static final String NAME_extensions = "extensions";
|
||||
public static String[] COMMON_EXTENSIONS=new String[]{
|
||||
".png",
|
||||
".jpg",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".wav",
|
||||
".webp",
|
||||
};
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk;
|
||||
|
||||
import com.reandroid.arsc.item.StringItem;
|
||||
import com.reandroid.xml.*;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class XmlHelper {
|
||||
|
||||
public static Map<String, String> readAttributes(XmlPullParser parser, String elementName) throws IOException, XmlPullParserException {
|
||||
if(!findElement(parser, elementName)){
|
||||
return null;
|
||||
}
|
||||
return mapAttributes(parser);
|
||||
}
|
||||
public static Map<String, String> mapAttributes(XmlPullParser parser){
|
||||
Map<String, String> map = new HashMap<>();
|
||||
int count = parser.getAttributeCount();
|
||||
for(int i = 0; i < count; i++){
|
||||
String name = parser.getAttributeName(i);
|
||||
int index = name.indexOf(':');
|
||||
if(index > 0 && index < name.length() && !name.startsWith("xmlns:")){
|
||||
index++;
|
||||
name = name.substring(index);
|
||||
}
|
||||
map.put(name,
|
||||
parser.getAttributeValue(i));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
private static boolean findElement(XmlPullParser parser, String elementName) throws IOException, XmlPullParserException {
|
||||
int event;
|
||||
while ((event = parser.next()) != XmlPullParser.END_DOCUMENT){
|
||||
if(event != XmlPullParser.START_TAG){
|
||||
continue;
|
||||
}
|
||||
if(elementName.equals(parser.getName())){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setTextContent(XMLElement element, StringItem stringItem){
|
||||
if(stringItem==null){
|
||||
element.clearChildNodes();
|
||||
return;
|
||||
}
|
||||
if(!stringItem.hasStyle()){
|
||||
element.setTextContent(stringItem.get());
|
||||
}else {
|
||||
element.setSpannableText(stringItem.getXml());
|
||||
}
|
||||
}
|
||||
public static String toXMLTagName(String typeName){
|
||||
// e.g ^attr-private
|
||||
if(typeName.length()>0 && typeName.charAt(0)=='^'){
|
||||
typeName = typeName.substring(1);
|
||||
}
|
||||
return typeName;
|
||||
}
|
||||
|
||||
public static void closeSilent(Object obj){
|
||||
if(!(obj instanceof Closeable)){
|
||||
return;
|
||||
}
|
||||
Closeable closeable = (Closeable) obj;
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static final String RESOURCES_TAG = "resources";
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.common.EntryStore;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
abstract class BagDecoder<OUTPUT> extends DecoderTableEntry<ResTableMapEntry, OUTPUT> {
|
||||
public BagDecoder(EntryStore entryStore){
|
||||
super(entryStore);
|
||||
}
|
||||
public abstract boolean canDecode(ResTableMapEntry mapEntry);
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.ApkUtil;
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class BagDecoderArray<OUTPUT> extends BagDecoder<OUTPUT>{
|
||||
public BagDecoderArray(EntryStore entryStore) {
|
||||
super(entryStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter<OUTPUT> writer) throws IOException {
|
||||
Entry entry = mapEntry.getParentEntry();
|
||||
String tag = getTagName(mapEntry);
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(tag);
|
||||
writer.attribute("name", entry.getName());
|
||||
|
||||
PackageBlock packageBlock = entry.getPackageBlock();
|
||||
EntryStore entryStore = getEntryStore();
|
||||
ResValueMap[] resValueMaps = mapEntry.listResValueMap();
|
||||
boolean zero_name = isZeroNameArray(resValueMaps);
|
||||
for(int i = 0; i < resValueMaps.length; i++){
|
||||
ResValueMap valueMap = resValueMaps[i];
|
||||
String childTag = "item";
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(childTag);
|
||||
if(zero_name){
|
||||
String name = ValueDecoder.decodeAttributeName(
|
||||
entryStore, packageBlock, valueMap.getName());
|
||||
writer.attribute("name", name);
|
||||
}
|
||||
writeText(writer, packageBlock, valueMap);
|
||||
writer.endTag(childTag);
|
||||
}
|
||||
return writer.endTag(tag);
|
||||
}
|
||||
private String getTagName(ResTableMapEntry mapEntry){
|
||||
ResValueMap[] resValueMaps = mapEntry.listResValueMap();
|
||||
Set<ValueType> valueTypes = new HashSet<>();
|
||||
for(int i = 0; i < resValueMaps.length; i++){
|
||||
valueTypes.add(resValueMaps[i].getValueType());
|
||||
}
|
||||
if(valueTypes.contains(ValueType.STRING)){
|
||||
return ApkUtil.TAG_STRING_ARRAY;
|
||||
}
|
||||
if(valueTypes.size() == 1 && valueTypes.contains(ValueType.INT_DEC)){
|
||||
return ApkUtil.TAG_INTEGER_ARRAY;
|
||||
}
|
||||
return XmlHelper.toXMLTagName(mapEntry.getParentEntry().getTypeName());
|
||||
}
|
||||
@Override
|
||||
public boolean canDecode(ResTableMapEntry mapEntry) {
|
||||
return isArrayValue(mapEntry);
|
||||
}
|
||||
public static boolean isArrayValue(ResTableMapEntry mapEntry){
|
||||
int parentId=mapEntry.getParentId();
|
||||
if(parentId!=0){
|
||||
return false;
|
||||
}
|
||||
ResValueMap[] valueMapList = mapEntry.listResValueMap();
|
||||
if(valueMapList == null || valueMapList.length == 0){
|
||||
return false;
|
||||
}
|
||||
if(isIndexedArray(valueMapList)){
|
||||
return true;
|
||||
}
|
||||
return isZeroNameArray(valueMapList);
|
||||
}
|
||||
private static boolean isIndexedArray(ResValueMap[] resValueMapList){
|
||||
int length = resValueMapList.length;
|
||||
for(int i = 0; i < length; i++){
|
||||
ResValueMap valueMap = resValueMapList[i];
|
||||
int name = valueMap.getName();
|
||||
int high = (name >> 16) & 0xffff;
|
||||
if(high!=0x0100 && high!=0x0200){
|
||||
return false;
|
||||
}
|
||||
int low = name & 0xffff;
|
||||
int id = low - 1;
|
||||
if(id!=i){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean isZeroNameArray(ResValueMap[] resValueMapList){
|
||||
int length = resValueMapList.length;
|
||||
for(int i = 0; i < length; i++){
|
||||
if(!isZeroName(resValueMapList[i])){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean isZeroName(ResValueMap resValueMap){
|
||||
return resValueMap.getName() == 0;
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.array.CompoundItemArray;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.arsc.value.attribute.AttributeBag;
|
||||
import com.reandroid.arsc.value.attribute.AttributeBagItem;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class BagDecoderAttr<OUTPUT> extends BagDecoder<OUTPUT>{
|
||||
public BagDecoderAttr(EntryStore entryStore){
|
||||
super(entryStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter<OUTPUT> writer) throws IOException {
|
||||
Entry entry = mapEntry.getParentEntry();
|
||||
String tag = XmlHelper.toXMLTagName(entry.getTypeName());
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(tag);
|
||||
writer.attribute("name", entry.getName());
|
||||
writeParentAttributes(writer, mapEntry.getValue());
|
||||
ResValueMap formatsMap = mapEntry.getByType(AttributeType.FORMATS);
|
||||
|
||||
AttributeDataFormat bagType = AttributeDataFormat.typeOfBag(formatsMap.getData());
|
||||
|
||||
ResValueMap[] bagItems = mapEntry.listResValueMap();
|
||||
|
||||
|
||||
for(int i = 0; i < bagItems.length; i++){
|
||||
ResValueMap item = bagItems[i];
|
||||
AttributeType attributeType = item.getAttributeType();
|
||||
if(attributeType != null){
|
||||
continue;
|
||||
}
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(bagType.getName());
|
||||
|
||||
String name = item.decodeName();
|
||||
writer.attribute("name", name);
|
||||
int rawVal = item.getData();
|
||||
String value;
|
||||
if(item.getValueType() == ValueType.INT_HEX){
|
||||
value = HexUtil.toHex8(rawVal);
|
||||
}else {
|
||||
value = Integer.toString(rawVal);
|
||||
}
|
||||
writer.text(value);
|
||||
|
||||
writer.endTag(bagType.getName());
|
||||
}
|
||||
return writer.endTag(tag);
|
||||
}
|
||||
|
||||
private void writeParentAttributes(EntryWriter<OUTPUT> writer, CompoundItemArray<? extends ResValueMap> itemArray) throws IOException {
|
||||
for(ResValueMap valueMap : itemArray.getChildes()){
|
||||
AttributeType type = valueMap.getAttributeType();
|
||||
if(type == null){
|
||||
continue;
|
||||
}
|
||||
String value;
|
||||
if(type == AttributeType.FORMATS){
|
||||
value = AttributeDataFormat.toString(
|
||||
AttributeDataFormat.decodeValueTypes(valueMap.getData()));
|
||||
}else {
|
||||
value = Integer.toString(valueMap.getData());
|
||||
}
|
||||
if(value == null){
|
||||
continue;
|
||||
}
|
||||
writer.attribute(type.getName(), value);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean canDecode(ResTableMapEntry mapEntry) {
|
||||
return AttributeBag.isAttribute(mapEntry);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.arsc.value.ResValueMap;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.common.EntryStore;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class BagDecoderCommon<OUTPUT> extends BagDecoder<OUTPUT>{
|
||||
public BagDecoderCommon(EntryStore entryStore) {
|
||||
super(entryStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter<OUTPUT> writer) throws IOException {
|
||||
Entry entry = mapEntry.getParentEntry();
|
||||
String tag = XmlHelper.toXMLTagName(entry.getTypeName());
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(tag);
|
||||
writer.attribute("name", entry.getName());
|
||||
|
||||
PackageBlock packageBlock = entry.getPackageBlock();
|
||||
|
||||
int parentId = mapEntry.getParentId();
|
||||
String parent;
|
||||
if(parentId != 0){
|
||||
parent = ValueDecoder.decodeEntryValue(getEntryStore(),
|
||||
packageBlock, ValueType.REFERENCE, parentId);
|
||||
}else {
|
||||
parent = null;
|
||||
}
|
||||
if(parent != null){
|
||||
writer.attribute("parent", parent);
|
||||
}
|
||||
|
||||
EntryStore entryStore = getEntryStore();
|
||||
ResValueMap[] resValueMaps = mapEntry.listResValueMap();
|
||||
for(int i = 0; i < resValueMaps.length; i++){
|
||||
ResValueMap valueMap = resValueMaps[i];
|
||||
String childTag = "item";
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(childTag);
|
||||
|
||||
String name = ValueDecoder.decodeAttributeName(
|
||||
entryStore, packageBlock, valueMap.getName());
|
||||
writer.attribute("name", name);
|
||||
|
||||
writeText(writer, valueMap);
|
||||
|
||||
writer.endTag(childTag);
|
||||
}
|
||||
return writer.endTag(tag);
|
||||
}
|
||||
@Override
|
||||
public boolean canDecode(ResTableMapEntry mapEntry) {
|
||||
return mapEntry !=null;
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.arsc.value.plurals.PluralsQuantity;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class BagDecoderPlural<OUTPUT> extends BagDecoder<OUTPUT>{
|
||||
public BagDecoderPlural(EntryStore entryStore) {
|
||||
super(entryStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OUTPUT decode(ResTableMapEntry mapEntry, EntryWriter<OUTPUT> writer) throws IOException {
|
||||
Entry entry = mapEntry.getParentEntry();
|
||||
String tag = XmlHelper.toXMLTagName(entry.getTypeName());
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(tag);
|
||||
writer.attribute("name", entry.getName());
|
||||
|
||||
ResValueMap[] resValueMaps = mapEntry.listResValueMap();
|
||||
PackageBlock packageBlock = entry.getPackageBlock();
|
||||
for(int i=0; i < resValueMaps.length; i++){
|
||||
ResValueMap valueMap = resValueMaps[i];
|
||||
String childTag = "item";
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(childTag);
|
||||
|
||||
AttributeType quantity = valueMap.getAttributeType();
|
||||
if(quantity == null || !quantity.isPlural()){
|
||||
throw new IOException("Unknown plural quantity: " + valueMap);
|
||||
}
|
||||
writer.attribute("quantity", quantity.getName());
|
||||
|
||||
writeText(writer, packageBlock, valueMap);
|
||||
|
||||
writer.endTag(childTag);
|
||||
}
|
||||
return writer.endTag(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(ResTableMapEntry mapEntry) {
|
||||
return isResBagPluralsValue(mapEntry);
|
||||
}
|
||||
|
||||
public static boolean isResBagPluralsValue(ResTableMapEntry valueItem){
|
||||
int parentId=valueItem.getParentId();
|
||||
if(parentId!=0){
|
||||
return false;
|
||||
}
|
||||
ResValueMap[] bagItems = valueItem.listResValueMap();
|
||||
if(bagItems==null||bagItems.length==0){
|
||||
return false;
|
||||
}
|
||||
int len=bagItems.length;
|
||||
for(int i=0;i<len;i++){
|
||||
ResValueMap item=bagItems[i];
|
||||
int name = item.getName();
|
||||
int high = (name >> 16) & 0xffff;
|
||||
if(high!=0x0100){
|
||||
return false;
|
||||
}
|
||||
int low = name & 0xffff;
|
||||
PluralsQuantity pq=PluralsQuantity.valueOf((short) low);
|
||||
if(pq==null){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ResTableEntry;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DecoderResTableEntry<OUTPUT> extends DecoderTableEntry<ResTableEntry, OUTPUT> {
|
||||
public DecoderResTableEntry(EntryStore entryStore){
|
||||
super(entryStore);
|
||||
}
|
||||
@Override
|
||||
public OUTPUT decode(ResTableEntry tableEntry, EntryWriter<OUTPUT> writer) throws IOException{
|
||||
Entry entry = tableEntry.getParentEntry();
|
||||
String tag = XmlHelper.toXMLTagName(entry.getTypeName());
|
||||
writer.enableIndent(true);
|
||||
writer.startTag(tag);
|
||||
writer.attribute("name", entry.getName());
|
||||
if(!isId(tag)){
|
||||
writeText(writer, entry.getPackageBlock(), tableEntry.getValue());
|
||||
}
|
||||
return writer.endTag(tag);
|
||||
}
|
||||
|
||||
private boolean isId(String tag){
|
||||
return "id".equals(tag);
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class DecoderResTableEntryMap<OUTPUT> extends DecoderTableEntry<ResTableMapEntry, OUTPUT> {
|
||||
private final Object[] decoderList;
|
||||
private final BagDecoderCommon<OUTPUT> bagDecoderCommon;
|
||||
|
||||
public DecoderResTableEntryMap(EntryStore entryStore) {
|
||||
super(entryStore);
|
||||
this.decoderList = new Object[] {
|
||||
new BagDecoderAttr<>(entryStore),
|
||||
new BagDecoderPlural<>(entryStore),
|
||||
new BagDecoderArray<>(entryStore)
|
||||
};
|
||||
|
||||
this.bagDecoderCommon = new BagDecoderCommon<>(entryStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OUTPUT decode(ResTableMapEntry tableEntry, EntryWriter<OUTPUT> writer) throws IOException {
|
||||
return getFor(tableEntry).decode(tableEntry, writer);
|
||||
}
|
||||
private BagDecoder<OUTPUT> getFor(ResTableMapEntry mapEntry){
|
||||
Object[] decoderList = this.decoderList;
|
||||
for(int i = 0; i < decoderList.length; i++){
|
||||
BagDecoder<OUTPUT> bagDecoder = (BagDecoder<OUTPUT>) decoderList[i];
|
||||
if(bagDecoder.canDecode(mapEntry)){
|
||||
return bagDecoder;
|
||||
}
|
||||
}
|
||||
return bagDecoderCommon;
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
abstract class DecoderTableEntry<INPUT extends TableEntry<?, ?>, OUTPUT> {
|
||||
private final EntryStore entryStore;
|
||||
public DecoderTableEntry(EntryStore entryStore){
|
||||
this.entryStore = entryStore;
|
||||
}
|
||||
public EntryStore getEntryStore() {
|
||||
return entryStore;
|
||||
}
|
||||
public abstract OUTPUT decode(INPUT tableEntry, EntryWriter<OUTPUT> writer) throws IOException;
|
||||
|
||||
void writeText(EntryWriter<?> writer, PackageBlock packageBlock, ValueItem valueItem)
|
||||
throws IOException {
|
||||
|
||||
if(valueItem.getValueType() == ValueType.STRING){
|
||||
XMLDecodeHelper.writeTextContent(writer, valueItem.getDataAsPoolString());
|
||||
}else {
|
||||
String value = ValueDecoder.decodeEntryValue(
|
||||
getEntryStore(),
|
||||
packageBlock,
|
||||
valueItem.getValueType(),
|
||||
valueItem.getData());
|
||||
writer.text(value);
|
||||
}
|
||||
}
|
||||
void writeText(EntryWriter<?> writer, ResValueMap attributeValue)
|
||||
throws IOException {
|
||||
if(attributeValue.getValueType() == ValueType.STRING){
|
||||
XMLDecodeHelper.writeTextContent(writer, attributeValue.getDataAsPoolString());
|
||||
}else {
|
||||
String value = ValueDecoder.decode(getEntryStore(),
|
||||
attributeValue.getPackageBlock().getId(),
|
||||
attributeValue);
|
||||
writer.text(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface EntryWriter<T>{
|
||||
void setFeature(String name, Object value);
|
||||
T startTag(String name) throws IOException;
|
||||
T endTag(String name) throws IOException;
|
||||
T attribute(String name, String value) throws IOException;
|
||||
T text(String text) throws IOException;
|
||||
void comment(String comment) throws IOException;
|
||||
void flush() throws IOException;
|
||||
void enableIndent(boolean enable);
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.xml.XMLComment;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EntryWriterElement implements EntryWriter<XMLElement> {
|
||||
private XMLElement mCurrentElement;
|
||||
private XMLElement mResult;
|
||||
private boolean mEnableIndent;
|
||||
|
||||
public EntryWriterElement(){
|
||||
}
|
||||
|
||||
public XMLElement getElement() {
|
||||
return mResult;
|
||||
}
|
||||
@Override
|
||||
public void setFeature(String name, Object value) {
|
||||
if(!FEATURE_INDENT.equals(name)){
|
||||
return;
|
||||
}
|
||||
boolean state = false;
|
||||
if(value instanceof Boolean){
|
||||
state = (Boolean)value;
|
||||
}
|
||||
mEnableIndent = state;
|
||||
}
|
||||
@Override
|
||||
public XMLElement startTag(String name) throws IOException {
|
||||
XMLElement xmlElement = new XMLElement(name);
|
||||
XMLElement current = mCurrentElement;
|
||||
if(current != null){
|
||||
current.addChild(xmlElement);
|
||||
}else {
|
||||
mResult = null;
|
||||
}
|
||||
mCurrentElement = xmlElement;
|
||||
if(mEnableIndent){
|
||||
xmlElement.setIndent(2);
|
||||
xmlElement.setIndentScale(1.0f);
|
||||
}else {
|
||||
xmlElement.setIndent(0);
|
||||
xmlElement.setIndentScale(0.0f);
|
||||
}
|
||||
return xmlElement;
|
||||
}
|
||||
@Override
|
||||
public XMLElement endTag(String name) throws IOException {
|
||||
XMLElement current = mCurrentElement;
|
||||
if(current == null){
|
||||
throw new IOException("endTag called before startTag");
|
||||
}
|
||||
if(!name.equals(current.getTagName())){
|
||||
throw new IOException("Mismatch endTag = "
|
||||
+ name + ", expect = " + current.getTagName());
|
||||
}
|
||||
XMLElement parent = current.getParent();
|
||||
if(parent == null){
|
||||
mResult = current;
|
||||
}else {
|
||||
current = parent;
|
||||
}
|
||||
mCurrentElement = parent;
|
||||
return current;
|
||||
}
|
||||
@Override
|
||||
public XMLElement attribute(String name, String value) {
|
||||
mCurrentElement.setAttribute(name, value);
|
||||
return mCurrentElement;
|
||||
}
|
||||
@Override
|
||||
public XMLElement text(String text) throws IOException {
|
||||
mCurrentElement.setTextContent(text, false);
|
||||
return mCurrentElement;
|
||||
}
|
||||
@Override
|
||||
public void comment(String comment) throws IOException {
|
||||
if(comment != null){
|
||||
mCurrentElement.addComment(new XMLComment(comment));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
}
|
||||
@Override
|
||||
public void enableIndent(boolean enable){
|
||||
setFeature(FEATURE_INDENT, enable);
|
||||
}
|
||||
|
||||
private static final String FEATURE_INDENT = "http://xmlpull.org/v1/doc/features.html#indent-output";
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EntryWriterSerializer implements EntryWriter<XmlSerializer> {
|
||||
private final XmlSerializer xmlSerializer;
|
||||
public EntryWriterSerializer(XmlSerializer xmlSerializer){
|
||||
this.xmlSerializer = xmlSerializer;
|
||||
}
|
||||
|
||||
public XmlSerializer getXmlSerializer() {
|
||||
return xmlSerializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeature(String name, Object value) {
|
||||
if(value == null){
|
||||
value = false;
|
||||
}else if(!(value instanceof Boolean)){
|
||||
return;
|
||||
}
|
||||
xmlSerializer.setFeature(name, (Boolean)value);
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer startTag(String name) throws IOException {
|
||||
return xmlSerializer.startTag(null, name);
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer endTag(String name) throws IOException {
|
||||
return xmlSerializer.endTag(null, name);
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer attribute(String name, String value) throws IOException {
|
||||
return xmlSerializer.attribute(null, name, value);
|
||||
}
|
||||
@Override
|
||||
public XmlSerializer text(String text) throws IOException {
|
||||
return xmlSerializer.text(text);
|
||||
}
|
||||
@Override
|
||||
public void comment(String comment) throws IOException {
|
||||
xmlSerializer.comment(comment);
|
||||
}
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
xmlSerializer.flush();
|
||||
}
|
||||
@Override
|
||||
public void enableIndent(boolean enable){
|
||||
setFeature(FEATURE_INDENT, enable);
|
||||
}
|
||||
|
||||
private static final String FEATURE_INDENT = "http://xmlpull.org/v1/doc/features.html#indent-output";
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.android.org.kxml2.io.KXmlSerializer;
|
||||
import com.reandroid.apk.ApkModule;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlPullParser;
|
||||
import com.reandroid.arsc.decoder.Decoder;
|
||||
import com.reandroid.xml.XmlParserToSerializer;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ResXmlDocumentSerializer implements ResXmlPullParser.DocumentLoadedListener{
|
||||
private final Object mLock = new Object();
|
||||
private final ResXmlPullParser parser;
|
||||
private final XmlSerializer serializer;
|
||||
private final XmlParserToSerializer parserToSerializer;
|
||||
private boolean validateXmlNamespace;
|
||||
private String mCurrentPath;
|
||||
public ResXmlDocumentSerializer(ResXmlPullParser parser){
|
||||
this.parser = parser;
|
||||
this.serializer = new KXmlSerializer();
|
||||
this.parserToSerializer = new XmlParserToSerializer(parser, serializer);
|
||||
this.parser.setDocumentLoadedListener(this);
|
||||
}
|
||||
public ResXmlDocumentSerializer(Decoder decoder){
|
||||
this(new ResXmlPullParser(decoder));
|
||||
}
|
||||
public ResXmlDocumentSerializer(ApkModule apkModule){
|
||||
this(createDecoder(apkModule));
|
||||
}
|
||||
|
||||
public void write(InputSource inputSource, File file)
|
||||
throws IOException, XmlPullParserException {
|
||||
write(inputSource.openStream(), file);
|
||||
}
|
||||
public void write(InputSource inputSource, OutputStream outputStream)
|
||||
throws IOException, XmlPullParserException {
|
||||
write(inputSource.openStream(), outputStream);
|
||||
inputSource.disposeInputSource();
|
||||
}
|
||||
public void write(InputStream inputStream, OutputStream outputStream)
|
||||
throws IOException, XmlPullParserException {
|
||||
synchronized (mLock){
|
||||
this.parser.setInput(inputStream, null);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
|
||||
this.serializer.setOutput(writer);
|
||||
try{
|
||||
this.parserToSerializer.write();
|
||||
}catch (Exception ex){
|
||||
throw getError(ex);
|
||||
}
|
||||
writer.close();
|
||||
outputStream.close();
|
||||
mCurrentPath = null;
|
||||
}
|
||||
}
|
||||
public void write(InputStream inputStream, File file)
|
||||
throws IOException, XmlPullParserException {
|
||||
File dir = file.getParentFile();
|
||||
if(dir != null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
mCurrentPath = String.valueOf(file);
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
write(inputStream, outputStream);
|
||||
}
|
||||
public void write(ResXmlDocument xmlDocument, File file)
|
||||
throws IOException, XmlPullParserException {
|
||||
mCurrentPath = String.valueOf(file);
|
||||
File dir = file.getParentFile();
|
||||
if(dir != null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
write(xmlDocument, outputStream);
|
||||
}
|
||||
public void write(ResXmlDocument xmlDocument, OutputStream outputStream)
|
||||
throws IOException, XmlPullParserException {
|
||||
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
|
||||
write(xmlDocument, writer);
|
||||
writer.close();
|
||||
outputStream.close();
|
||||
}
|
||||
public void write(ResXmlDocument xmlDocument, Writer writer)
|
||||
throws IOException, XmlPullParserException {
|
||||
synchronized (mLock){
|
||||
this.parser.setResXmlDocument(xmlDocument);
|
||||
this.serializer.setOutput(writer);
|
||||
this.parserToSerializer.write();
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
public Decoder getDecoder(){
|
||||
return parser.getDecoder();
|
||||
}
|
||||
|
||||
public void setValidateXmlNamespace(boolean validateXmlNamespace) {
|
||||
this.validateXmlNamespace = validateXmlNamespace;
|
||||
}
|
||||
@Override
|
||||
public ResXmlDocument onDocumentLoaded(ResXmlDocument resXmlDocument) {
|
||||
if(!validateXmlNamespace){
|
||||
return resXmlDocument;
|
||||
}
|
||||
XMLNamespaceValidator.validateNamespaces(resXmlDocument);
|
||||
return resXmlDocument;
|
||||
}
|
||||
private IOException getError(Exception exception){
|
||||
String path = mCurrentPath;
|
||||
if(exception instanceof IOException){
|
||||
String msg = path + ":" + exception.getMessage();
|
||||
IOException ioException = new IOException(msg);
|
||||
ioException.setStackTrace(exception.getStackTrace());
|
||||
Throwable cause = ioException.getCause();
|
||||
if(cause != null){
|
||||
ioException.initCause(cause);
|
||||
}
|
||||
return ioException;
|
||||
}
|
||||
String msg = path + ":" + exception.getClass() + ":" + exception.getMessage();
|
||||
IOException otherException = new IOException(msg);
|
||||
otherException.setStackTrace(exception.getStackTrace());
|
||||
Throwable cause = otherException.getCause();
|
||||
if(cause != null){
|
||||
otherException.initCause(cause);
|
||||
}
|
||||
return otherException;
|
||||
}
|
||||
|
||||
private static Decoder createDecoder(ApkModule apkModule){
|
||||
Decoder decoder = Decoder.create(apkModule.getTableBlock());
|
||||
decoder.setApkFile(apkModule);
|
||||
return decoder;
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.common.EntryStore;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Deprecated
|
||||
public class XMLBagDecoder {
|
||||
private final DecoderResTableEntryMap<XMLElement> mDocumentDecoder;
|
||||
private final EntryWriterElement mWriter;
|
||||
public XMLBagDecoder(EntryStore entryStore){
|
||||
mDocumentDecoder = new DecoderResTableEntryMap<>(entryStore);
|
||||
mWriter = new EntryWriterElement();
|
||||
}
|
||||
public void decode(ResTableMapEntry mapEntry, XMLElement parentElement){
|
||||
try {
|
||||
XMLElement child = mDocumentDecoder.decode(mapEntry, mWriter);
|
||||
parentElement.addChild(child);
|
||||
} catch (IOException exception) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.item.StringItem;
|
||||
import com.reandroid.xml.*;
|
||||
import com.reandroid.xml.parser.XMLSpanParser;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class XMLDecodeHelper {
|
||||
|
||||
public static void writeTextContent(EntryWriter<?> writer, StringItem stringItem) throws IOException {
|
||||
if(stringItem == null){
|
||||
return;
|
||||
}
|
||||
if(!stringItem.hasStyle()){
|
||||
String text = stringItem.get();
|
||||
text = ValueDecoder.escapeSpecialCharacter(text);
|
||||
text = ValueDecoder.quoteWhitespace(text);
|
||||
writer.text(text);
|
||||
}else {
|
||||
String xml = stringItem.getXml();
|
||||
XMLElement element = parseSpanSafe(xml);
|
||||
if(element != null){
|
||||
writeParsedSpannable(writer, element);
|
||||
}else {
|
||||
// TODO: throw or investigate the reason
|
||||
writer.text(xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void writeParsedSpannable(EntryWriter<?> writer, XMLElement spannableParent) throws IOException {
|
||||
for(XMLNode xmlNode : spannableParent.getChildNodes()){
|
||||
if(xmlNode instanceof XMLText){
|
||||
String text = ((XMLText)xmlNode).getText(true);
|
||||
writer.enableIndent(false);
|
||||
writer.text(ValueDecoder.escapeSpecialCharacter(text));
|
||||
}else if(xmlNode instanceof XMLElement){
|
||||
writeElement(writer, (XMLElement) xmlNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void writeElement(EntryWriter<?> writer, XMLElement element) throws IOException {
|
||||
writer.enableIndent(false);
|
||||
writer.startTag(element.getTagName());
|
||||
for(XMLAttribute xmlAttribute : element.listAttributes()){
|
||||
writer.attribute(xmlAttribute.getName(), xmlAttribute.getValue());
|
||||
}
|
||||
for(XMLNode xmlNode : element.getChildNodes()){
|
||||
if(xmlNode instanceof XMLText){
|
||||
String text = ((XMLText)xmlNode).getText(true);
|
||||
writer.text(text);
|
||||
}else if(xmlNode instanceof XMLElement){
|
||||
writeElement(writer, (XMLElement) xmlNode);
|
||||
}
|
||||
}
|
||||
writer.endTag(element.getTagName());
|
||||
}
|
||||
private static XMLElement parseSpanSafe(String spanText){
|
||||
if(spanText==null){
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
XMLSpanParser spanParser = new XMLSpanParser();
|
||||
return spanParser.parse(spanText);
|
||||
} catch (XMLException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.group.EntryGroup;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.common.EntryStore;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class XMLEntryDecoder<OUTPUT>{
|
||||
private final Object mLock = new Object();
|
||||
private final DecoderResTableEntry<OUTPUT> decoderEntry;
|
||||
private final DecoderResTableEntryMap<OUTPUT> decoderEntryMap;
|
||||
private Predicate<Entry> mDecodedEntries;
|
||||
|
||||
public XMLEntryDecoder(EntryStore entryStore){
|
||||
this.decoderEntry = new DecoderResTableEntry<>(entryStore);
|
||||
this.decoderEntryMap = new DecoderResTableEntryMap<>(entryStore);
|
||||
}
|
||||
|
||||
public void setDecodedEntries(Predicate<Entry> decodedEntries) {
|
||||
this.mDecodedEntries = decodedEntries;
|
||||
}
|
||||
|
||||
private boolean shouldDecode(Entry entry){
|
||||
if(entry == null || entry.isNull()){
|
||||
return false;
|
||||
}
|
||||
if(this.mDecodedEntries != null){
|
||||
return mDecodedEntries.test(entry);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public OUTPUT decode(EntryWriter<OUTPUT> writer, Entry entry) throws IOException{
|
||||
if(!shouldDecode(entry)){
|
||||
return null;
|
||||
}
|
||||
synchronized (mLock){
|
||||
TableEntry<?, ?> tableEntry = entry.getTableEntry();
|
||||
if(tableEntry instanceof ResTableMapEntry){
|
||||
return decoderEntryMap.decode((ResTableMapEntry) tableEntry, writer);
|
||||
}
|
||||
return decoderEntry.decode((ResTableEntry) tableEntry, writer);
|
||||
}
|
||||
}
|
||||
public int decode(EntryWriter<OUTPUT> writer, Collection<Entry> entryList) throws IOException {
|
||||
int count = 0;
|
||||
for(Entry entry : entryList){
|
||||
OUTPUT output = decode(writer, entry);
|
||||
if(output != null){
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
public int decode(EntryWriter<OUTPUT> writer, ResConfig resConfig, Collection<EntryGroup> entryGroupList) throws IOException {
|
||||
int count = 0;
|
||||
for(EntryGroup entryGroup : entryGroupList){
|
||||
OUTPUT output = decode(writer, entryGroup.getEntry(resConfig));
|
||||
if(output != null){
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
public int decode(EntryWriter<OUTPUT> writer, TypeBlock typeBlock) throws IOException {
|
||||
Iterator<Entry> iterator = typeBlock.getEntryArray()
|
||||
.iterator(true);
|
||||
int count = 0;
|
||||
while (iterator.hasNext()){
|
||||
Entry entry = iterator.next();
|
||||
OUTPUT output = decode(writer, entry);
|
||||
if(output != null){
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void deleteIfZero(int decodeCount, File file){
|
||||
if(decodeCount > 0){
|
||||
return;
|
||||
}
|
||||
file.delete();
|
||||
File dir = file.getParentFile();
|
||||
if(isEmptyDirectory(dir)){
|
||||
dir.delete();
|
||||
}
|
||||
}
|
||||
private boolean isEmptyDirectory(File dir){
|
||||
if(dir == null || !dir.isDirectory()){
|
||||
return false;
|
||||
}
|
||||
File[] files = dir.listFiles();
|
||||
return files == null || files.length == 0;
|
||||
}
|
||||
File toOutXmlFile(File resDirectory, TypeBlock typeBlock){
|
||||
String path = toValuesXml(typeBlock);
|
||||
return new File(resDirectory, path);
|
||||
}
|
||||
String toValuesXml(TypeBlock typeBlock){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
char sepChar = File.separatorChar;
|
||||
builder.append("values");
|
||||
builder.append(typeBlock.getQualifiers());
|
||||
builder.append(sepChar);
|
||||
String type = typeBlock.getTypeName();
|
||||
builder.append(type);
|
||||
if(!type.endsWith("s")){
|
||||
builder.append('s');
|
||||
}
|
||||
builder.append(".xml");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.common.EntryStore;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
public class XMLEntryDecoderDocument extends XMLEntryDecoder<XMLElement>{
|
||||
private final EntryWriterElement entryWriterElement;
|
||||
public XMLEntryDecoderDocument(EntryStore entryStore) {
|
||||
super(entryStore);
|
||||
this.entryWriterElement = new EntryWriterElement();
|
||||
}
|
||||
|
||||
public XMLElement decode(Entry entry) throws IOException {
|
||||
return super.decode(this.entryWriterElement, entry);
|
||||
}
|
||||
|
||||
public XMLDocument decode(XMLDocument xmlDocument, Collection<Entry> entryList)
|
||||
throws IOException {
|
||||
|
||||
if(xmlDocument == null){
|
||||
xmlDocument = new XMLDocument(XmlHelper.RESOURCES_TAG);
|
||||
}
|
||||
XMLElement docElement = xmlDocument.getDocumentElement();
|
||||
|
||||
if(docElement == null){
|
||||
docElement = new XMLElement(XmlHelper.RESOURCES_TAG);
|
||||
xmlDocument.setDocumentElement(docElement);
|
||||
}
|
||||
for(Entry entry : entryList){
|
||||
docElement.addChild(decode(entry));
|
||||
}
|
||||
return xmlDocument;
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.android.org.kxml2.io.KXmlSerializer;
|
||||
import com.reandroid.apk.XmlHelper;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.container.SpecTypePair;
|
||||
import com.reandroid.arsc.group.EntryGroup;
|
||||
import com.reandroid.arsc.value.ResConfig;
|
||||
import com.reandroid.common.EntryStore;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class XMLEntryDecoderSerializer extends XMLEntryDecoder<XmlSerializer> implements Closeable {
|
||||
private final EntryWriterSerializer entryWriterSerializer;
|
||||
private Closeable mClosable;
|
||||
private boolean mStart;
|
||||
|
||||
public XMLEntryDecoderSerializer(EntryStore entryStore, XmlSerializer serializer) {
|
||||
super(entryStore);
|
||||
this.entryWriterSerializer = new EntryWriterSerializer(serializer);
|
||||
}
|
||||
public XMLEntryDecoderSerializer(EntryStore entryStore) {
|
||||
this(entryStore, new KXmlSerializer());
|
||||
}
|
||||
|
||||
public int decode(File resDirectory, SpecTypePair specTypePair) throws IOException {
|
||||
int count;
|
||||
if(specTypePair.hasDuplicateResConfig(true)){
|
||||
count = decodeDuplicateConfigs(resDirectory, specTypePair);
|
||||
}else {
|
||||
count = decodeUniqueConfigs(resDirectory, specTypePair);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
private int decodeDuplicateConfigs(File resDirectory, SpecTypePair specTypePair) throws IOException {
|
||||
List<ResConfig> resConfigList = specTypePair.listResConfig();
|
||||
Collection<EntryGroup> entryGroupList = specTypePair
|
||||
.createEntryGroups(true).values();
|
||||
int total = 0;
|
||||
for(ResConfig resConfig : resConfigList){
|
||||
TypeBlock typeBlock = resConfig.getParentInstance(TypeBlock.class);
|
||||
File outXml = toOutXmlFile(resDirectory, typeBlock);
|
||||
total += decode(outXml, resConfig, entryGroupList);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
private int decodeUniqueConfigs(File resDirectory, SpecTypePair specTypePair) throws IOException {
|
||||
int total = 0;
|
||||
Iterator<TypeBlock> itr = specTypePair.iteratorNonEmpty();
|
||||
while (itr.hasNext()){
|
||||
TypeBlock typeBlock = itr.next();
|
||||
File outXml = toOutXmlFile(resDirectory, typeBlock);
|
||||
total += decode(outXml, typeBlock);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
public int decode(File outXmlFile, ResConfig resConfig, Collection<EntryGroup> entryGroupList) throws IOException {
|
||||
setOutput(outXmlFile);
|
||||
int count = decode(resConfig, entryGroupList);
|
||||
close();
|
||||
deleteIfZero(count, outXmlFile);
|
||||
return count;
|
||||
}
|
||||
public int decode(File outXmlFile, TypeBlock typeBlock) throws IOException {
|
||||
setOutput(outXmlFile);
|
||||
int count = super.decode(entryWriterSerializer, typeBlock);
|
||||
close();
|
||||
deleteIfZero(count, outXmlFile);
|
||||
return count;
|
||||
}
|
||||
public int decode(ResConfig resConfig, Collection<EntryGroup> entryGroupList) throws IOException {
|
||||
return super.decode(entryWriterSerializer, resConfig, entryGroupList);
|
||||
}
|
||||
public void setOutput(File file) throws IOException {
|
||||
File dir = file.getParentFile();
|
||||
if(dir != null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
setOutput(new FileOutputStream(file));
|
||||
}
|
||||
public void setOutput(OutputStream outputStream) throws IOException {
|
||||
close();
|
||||
getXmlSerializer().setOutput(outputStream, StandardCharsets.UTF_8.name());
|
||||
this.mClosable = outputStream;
|
||||
start();
|
||||
}
|
||||
public void setOutput(Writer writer) throws IOException {
|
||||
close();
|
||||
getXmlSerializer().setOutput(writer);
|
||||
this.mClosable = writer;
|
||||
start();
|
||||
}
|
||||
|
||||
private void start() throws IOException {
|
||||
if(!mStart){
|
||||
XmlSerializer xmlSerializer = getXmlSerializer();
|
||||
xmlSerializer.startDocument("utf-8", null);
|
||||
xmlSerializer.startTag(null, XmlHelper.RESOURCES_TAG);
|
||||
mStart = true;
|
||||
}
|
||||
}
|
||||
private void end() throws IOException {
|
||||
if(mStart){
|
||||
XmlSerializer xmlSerializer = getXmlSerializer();
|
||||
xmlSerializer.endTag(null, XmlHelper.RESOURCES_TAG);
|
||||
xmlSerializer.endDocument();
|
||||
xmlSerializer.flush();
|
||||
mStart = false;
|
||||
}
|
||||
}
|
||||
private XmlSerializer getXmlSerializer(){
|
||||
return entryWriterSerializer.getXmlSerializer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
Closeable closeable = this.mClosable;
|
||||
end();
|
||||
if(closeable != null){
|
||||
closeable.close();
|
||||
}
|
||||
this.mClosable = null;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmldecoder;
|
||||
|
||||
import com.reandroid.arsc.chunk.xml.*;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class XMLNamespaceValidator {
|
||||
private static final String URI_ANDROID = "http://schemas.android.com/apk/res/android";
|
||||
private static final String URI_APP = "http://schemas.android.com/apk/res-auto";
|
||||
private static final String PREFIX_ANDROID = "android";
|
||||
private static final String PREFIX_APP = "app";
|
||||
private final ResXmlDocument xmlBlock;
|
||||
public XMLNamespaceValidator(ResXmlDocument xmlBlock){
|
||||
this.xmlBlock=xmlBlock;
|
||||
}
|
||||
public void validate(){
|
||||
validateNamespaces(xmlBlock);
|
||||
}
|
||||
|
||||
public static boolean isValid(ResXmlAttribute attribute){
|
||||
int resourceId = attribute.getNameResourceID();
|
||||
if(resourceId == 0){
|
||||
return attribute.getUri() == null;
|
||||
}
|
||||
if(isAndroid(toPackageId(resourceId))){
|
||||
return isValidAndroidNamespace(attribute);
|
||||
}else {
|
||||
return isValidAppNamespace(attribute);
|
||||
}
|
||||
}
|
||||
public static void validateNamespaces(ResXmlDocument resXmlDocument){
|
||||
validateNamespaces(resXmlDocument.getResXmlElement());
|
||||
}
|
||||
public static void validateNamespaces(ResXmlElement element){
|
||||
validateNamespaces(element.listAttributes());
|
||||
for(ResXmlElement child : element.listElements()){
|
||||
validateNamespaces(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateNamespaces(Collection<ResXmlAttribute> attributeList){
|
||||
for(ResXmlAttribute attribute : attributeList){
|
||||
validateNamespace(attribute);
|
||||
}
|
||||
}
|
||||
private static void validateNamespace(ResXmlAttribute attribute){
|
||||
int resourceId = attribute.getNameResourceID();
|
||||
if(resourceId == 0){
|
||||
attribute.setNamespaceReference(-1);
|
||||
return;
|
||||
}
|
||||
if(isAndroid(toPackageId(resourceId))){
|
||||
if(!isValidAndroidNamespace(attribute)){
|
||||
attribute.setNamespace(URI_ANDROID, PREFIX_ANDROID);
|
||||
}
|
||||
}else {
|
||||
if(!isValidAppNamespace(attribute)){
|
||||
attribute.setNamespace(URI_APP, PREFIX_APP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidAppNamespace(ResXmlAttribute attribute){
|
||||
String uri = attribute.getUri();
|
||||
String prefix = attribute.getNamePrefix();
|
||||
if(URI_ANDROID.equals(uri) || PREFIX_ANDROID.equals(prefix)){
|
||||
return false;
|
||||
}
|
||||
if(isEmpty(uri) || isEmpty(prefix)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean isValidAndroidNamespace(ResXmlAttribute attribute){
|
||||
return URI_ANDROID.equals(attribute.getUri())
|
||||
&& PREFIX_ANDROID.equals(attribute.getNamePrefix());
|
||||
}
|
||||
|
||||
private static boolean isAndroid(int pkgId){
|
||||
return pkgId==1;
|
||||
}
|
||||
private static int toPackageId(int resId){
|
||||
return (resId >> 24 & 0xFF);
|
||||
}
|
||||
private static boolean isEmpty(String str){
|
||||
if(str==null){
|
||||
return true;
|
||||
}
|
||||
str=str.trim();
|
||||
return str.length()==0;
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
public class EncodeException extends IllegalArgumentException{
|
||||
public EncodeException(String message){
|
||||
super(message);
|
||||
}
|
||||
public EncodeException(String message, Throwable cause){
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -1,433 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.apk.APKLogger;
|
||||
import com.reandroid.apk.FrameworkApk;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.container.SpecTypePair;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.group.EntryGroup;
|
||||
import com.reandroid.arsc.item.SpecString;
|
||||
import com.reandroid.arsc.util.FrameworkTable;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
import com.reandroid.arsc.util.ResNameMap;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.identifiers.PackageIdentifier;
|
||||
import com.reandroid.identifiers.ResourceIdentifier;
|
||||
import com.reandroid.identifiers.TableIdentifier;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class EncodeMaterials {
|
||||
private PackageBlock currentPackage;
|
||||
private final Set<FrameworkTable> frameworkTables = new HashSet<>();
|
||||
private APKLogger apkLogger;
|
||||
private boolean mForceCreateNamespaces = true;
|
||||
private Set<String> mFrameworkPackageNames;
|
||||
private final ResNameMap<Entry> mLocalResNameMap = new ResNameMap<>();
|
||||
private final TableIdentifier tableIdentifier = new TableIdentifier();
|
||||
private PackageIdentifier currentPackageIdentifier;
|
||||
private Integer mMainPackageId;
|
||||
public EncodeMaterials(){
|
||||
}
|
||||
public void setMainPackageId(Integer mainPackageId){
|
||||
this.mMainPackageId = mainPackageId;
|
||||
}
|
||||
public PackageBlock pickMainPackageBlock(TableBlock tableBlock){
|
||||
if(mMainPackageId != null){
|
||||
return tableBlock.pickOne(mMainPackageId);
|
||||
}
|
||||
return tableBlock.pickOne();
|
||||
}
|
||||
public TableIdentifier getTableIdentifier(){
|
||||
return tableIdentifier;
|
||||
}
|
||||
public void setEntryName(Entry entry, String name){
|
||||
PackageBlock packageBlock = entry.getPackageBlock();
|
||||
SpecString specString = packageBlock
|
||||
.getSpecStringPool().getOrCreate(name);
|
||||
entry.setSpecReference(specString);
|
||||
}
|
||||
public SpecString getSpecString(String name){
|
||||
return currentPackage.getSpecStringPool()
|
||||
.get(name)
|
||||
.get(0);
|
||||
}
|
||||
public Entry getAttributeBlock(String refString){
|
||||
String type = "attr";
|
||||
Entry entry = getAttributeBlock(type, refString);
|
||||
if(entry == null){
|
||||
type = "^attr-private";
|
||||
entry = getAttributeBlock(type, refString);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
private Entry getAttributeBlock(String type, String refString){
|
||||
String packageName = null;
|
||||
String name = refString;
|
||||
int i=refString.lastIndexOf(':');
|
||||
if(i>=0){
|
||||
packageName=refString.substring(0, i);
|
||||
name=refString.substring(i+1);
|
||||
}
|
||||
if(EncodeUtil.isEmpty(packageName)
|
||||
|| packageName.equals(getCurrentPackageName())
|
||||
|| !isFrameworkPackageName(packageName)){
|
||||
|
||||
return getLocalEntry(type, name);
|
||||
}
|
||||
return getFrameworkEntry(type, name);
|
||||
}
|
||||
public int resolveReference(String refString){
|
||||
if("@null".equals(refString)){
|
||||
return 0;
|
||||
}
|
||||
Matcher matcher = ValueDecoder.PATTERN_REFERENCE.matcher(refString);
|
||||
if(!matcher.find()){
|
||||
ValueDecoder.EncodeResult ref = ValueDecoder.encodeHexReference(refString);
|
||||
if(ref!=null){
|
||||
return ref.value;
|
||||
}
|
||||
ref = ValueDecoder.encodeNullReference(refString);
|
||||
if(ref!=null){
|
||||
return ref.value;
|
||||
}
|
||||
throw new EncodeException(
|
||||
"Not proper reference string: '"+refString+"'");
|
||||
}
|
||||
String prefix=matcher.group(1);
|
||||
String packageName = matcher.group(2);
|
||||
if(packageName!=null && packageName.endsWith(":")){
|
||||
packageName=packageName.substring(0, packageName.length()-1);
|
||||
}
|
||||
String type = matcher.group(4);
|
||||
String name = matcher.group(5);
|
||||
if(isLocalPackageName(packageName)){
|
||||
return resolveLocalResourceId(packageName, type, name);
|
||||
}
|
||||
|
||||
if(EncodeUtil.isEmpty(packageName)
|
||||
|| packageName.equals(getCurrentPackageName())
|
||||
|| !isFrameworkPackageName(packageName)){
|
||||
return resolveLocalResourceId(type, name);
|
||||
}
|
||||
return resolveFrameworkResourceId(packageName, type, name);
|
||||
}
|
||||
private int resolveLocalResourceId(String packageName, String type, String name){
|
||||
ResourceIdentifier ri = tableIdentifier.get(packageName, type, name);
|
||||
if(ri != null){
|
||||
return ri.getResourceId();
|
||||
}
|
||||
EntryGroup entryGroup=getLocalEntryGroup(type, name);
|
||||
if(entryGroup!=null){
|
||||
return entryGroup.getResourceId();
|
||||
}
|
||||
throw new EncodeException("Local entry not found: " +
|
||||
"package=" + packageName +
|
||||
", type=" + type +
|
||||
", name=" + name);
|
||||
}
|
||||
public int resolveLocalResourceId(String type, String name){
|
||||
PackageIdentifier pi = this.currentPackageIdentifier;
|
||||
if(pi != null){
|
||||
ResourceIdentifier ri = pi.getResourceIdentifier(type, name);
|
||||
if(ri != null){
|
||||
return ri.getResourceId();
|
||||
}
|
||||
}
|
||||
EntryGroup entryGroup=getLocalEntryGroup(type, name);
|
||||
if(entryGroup!=null){
|
||||
return entryGroup.getResourceId();
|
||||
}
|
||||
throw new EncodeException("Local entry not found: " +
|
||||
"type="+type+
|
||||
", name="+name);
|
||||
}
|
||||
public int resolveFrameworkResourceId(String packageName, String type, String name){
|
||||
Entry entry = getFrameworkEntry(packageName, type, name);
|
||||
if(entry !=null){
|
||||
return entry.getResourceId();
|
||||
}
|
||||
throw new EncodeException("Framework entry not found: " +
|
||||
"package="+packageName+
|
||||
", type="+type+
|
||||
", name="+name);
|
||||
}
|
||||
public int resolveFrameworkResourceId(int packageId, String type, String name){
|
||||
Entry entry = getFrameworkEntry(packageId, type, name);
|
||||
if(entry !=null){
|
||||
return entry.getResourceId();
|
||||
}
|
||||
throw new EncodeException("Framework entry not found: " +
|
||||
"packageId=" + HexUtil.toHex2((byte) packageId)+
|
||||
", type="+type+
|
||||
", name="+name);
|
||||
}
|
||||
public EntryGroup getLocalEntryGroup(String type, String name){
|
||||
for(EntryGroup entryGroup : currentPackage.listEntryGroup()){
|
||||
if(type.equals(entryGroup.getTypeName()) &&
|
||||
name.equals(entryGroup.getSpecName())){
|
||||
return entryGroup;
|
||||
}
|
||||
}
|
||||
for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){
|
||||
for(EntryGroup entryGroup : packageBlock.listEntryGroup()){
|
||||
if(type.equals(entryGroup.getTypeName()) &&
|
||||
name.equals(entryGroup.getSpecName())){
|
||||
return entryGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public Entry getLocalEntry(String type, String name){
|
||||
Entry entry =mLocalResNameMap.get(type, name);
|
||||
if(entry !=null){
|
||||
return entry;
|
||||
}
|
||||
loadLocalEntryMap(type);
|
||||
entry =mLocalResNameMap.get(type, name);
|
||||
if(entry !=null){
|
||||
return entry;
|
||||
}
|
||||
entry = searchLocalEntry(type, name);
|
||||
if(entry !=null){
|
||||
mLocalResNameMap.add(type, name, entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
private Entry searchLocalEntry(String type, String name){
|
||||
for(EntryGroup entryGroup : currentPackage.listEntryGroup()){
|
||||
if(type.equals(entryGroup.getTypeName()) &&
|
||||
name.equals(entryGroup.getSpecName())){
|
||||
return entryGroup.pickOne();
|
||||
}
|
||||
}
|
||||
SpecTypePair specTypePair=currentPackage.getSpecTypePair(type);
|
||||
if(specTypePair!=null){
|
||||
for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){
|
||||
for(Entry entry :typeBlock.listEntries(true)){
|
||||
if(name.equals(entry.getName())){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){
|
||||
if(packageBlock==currentPackage){
|
||||
continue;
|
||||
}
|
||||
specTypePair=packageBlock.getSpecTypePair(type);
|
||||
if(specTypePair!=null){
|
||||
for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){
|
||||
for(Entry entry :typeBlock.listEntries(true)){
|
||||
if(name.equals(entry.getName())){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private void loadLocalEntryMap(String type){
|
||||
ResNameMap<Entry> localMap = mLocalResNameMap;
|
||||
for(PackageBlock packageBlock:currentPackage.getTableBlock().listPackages()){
|
||||
SpecTypePair specTypePair=packageBlock.getSpecTypePair(type);
|
||||
if(specTypePair!=null){
|
||||
for(TypeBlock typeBlock:specTypePair.listTypeBlocks()){
|
||||
for(Entry entry :typeBlock.listEntries(true)){
|
||||
localMap.add(entry.getTypeName(),
|
||||
entry.getName(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public Entry getFrameworkEntry(String type, String name){
|
||||
for(FrameworkTable table:frameworkTables){
|
||||
Entry entry = table.searchEntry(type, name);
|
||||
if(entry !=null){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private boolean isFrameworkPackageName(String packageName){
|
||||
return getFrameworkPackageNames().contains(packageName);
|
||||
}
|
||||
private Set<String> getFrameworkPackageNames(){
|
||||
if(mFrameworkPackageNames!=null){
|
||||
return mFrameworkPackageNames;
|
||||
}
|
||||
Set<String> results=new HashSet<>();
|
||||
for(FrameworkTable table:frameworkTables){
|
||||
for(PackageBlock packageBlock:table.listPackages()){
|
||||
results.add(packageBlock.getName());
|
||||
}
|
||||
}
|
||||
mFrameworkPackageNames=results;
|
||||
return results;
|
||||
}
|
||||
public Entry getFrameworkEntry(String packageName, String type, String name){
|
||||
for(FrameworkTable table:frameworkTables){
|
||||
for(PackageBlock packageBlock:table.listPackages()){
|
||||
if(packageName.equals(packageBlock.getName())){
|
||||
Entry entry = table.searchEntry(type, name);
|
||||
if(entry !=null){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public Entry getFrameworkEntry(int packageId, String type, String name){
|
||||
for(FrameworkTable table:frameworkTables){
|
||||
for(PackageBlock packageBlock:table.listPackages()){
|
||||
if(packageId==packageBlock.getId()){
|
||||
Entry entry = table.searchEntry(type, name);
|
||||
if(entry !=null){
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public EncodeMaterials setForceCreateNamespaces(boolean force) {
|
||||
this.mForceCreateNamespaces = force;
|
||||
return this;
|
||||
}
|
||||
public EncodeMaterials setCurrentPackage(PackageBlock currentPackage) {
|
||||
this.currentPackage = currentPackage;
|
||||
onCurrentPackageChanged(currentPackage);
|
||||
return this;
|
||||
}
|
||||
public EncodeMaterials setCurrentLocalPackage(PackageIdentifier packageIdentifier) {
|
||||
this.currentPackageIdentifier = packageIdentifier;
|
||||
return this;
|
||||
}
|
||||
private void onCurrentPackageChanged(PackageBlock currentPackage){
|
||||
if(currentPackage == null){
|
||||
return;
|
||||
}
|
||||
PackageIdentifier pi = tableIdentifier.getByPackage(currentPackage);
|
||||
if(pi != null){
|
||||
this.currentPackageIdentifier = pi;
|
||||
}
|
||||
}
|
||||
private boolean isLocalPackageName(String packageName){
|
||||
if(packageName == null){
|
||||
return false;
|
||||
}
|
||||
for(PackageIdentifier pi : tableIdentifier.getPackages()){
|
||||
if(packageName.equals(pi.getName())){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private boolean isUniquePackageNames(){
|
||||
Set<String> names = new HashSet<>();
|
||||
for(PackageIdentifier pi : tableIdentifier.getPackages()){
|
||||
names.add(pi.getName());
|
||||
}
|
||||
return names.size() == tableIdentifier.getPackages().size();
|
||||
}
|
||||
private boolean isUniquePackageIds(){
|
||||
Set<Integer> ids = new HashSet<>();
|
||||
for(PackageIdentifier pi : tableIdentifier.getPackages()){
|
||||
ids.add(pi.getId());
|
||||
}
|
||||
return ids.size() == tableIdentifier.getPackages().size();
|
||||
}
|
||||
public EncodeMaterials addFramework(FrameworkApk frameworkApk) {
|
||||
if(frameworkApk!=null){
|
||||
addFramework(frameworkApk.getTableBlock());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public EncodeMaterials addFramework(FrameworkTable frameworkTable) {
|
||||
frameworkTable.loadResourceNameMap();
|
||||
this.frameworkTables.add(frameworkTable);
|
||||
this.mFrameworkPackageNames=null;
|
||||
return this;
|
||||
}
|
||||
public EncodeMaterials setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PackageBlock getCurrentPackage() {
|
||||
return currentPackage;
|
||||
}
|
||||
public boolean isForceCreateNamespaces() {
|
||||
return mForceCreateNamespaces;
|
||||
}
|
||||
|
||||
public String getCurrentPackageName(){
|
||||
return currentPackage.getName();
|
||||
}
|
||||
public int getCurrentPackageId(){
|
||||
return currentPackage.getId();
|
||||
}
|
||||
|
||||
public void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
public void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
public void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
public static EncodeMaterials create(TableBlock tableBlock){
|
||||
PackageBlock packageBlock = tableBlock.pickOne();
|
||||
if(packageBlock==null){
|
||||
throw new EncodeException("No packages found on table block");
|
||||
}
|
||||
return create(packageBlock);
|
||||
}
|
||||
public static EncodeMaterials create(PackageBlock packageBlock){
|
||||
EncodeMaterials encodeMaterials = new EncodeMaterials();
|
||||
|
||||
TableBlock tableBlock = packageBlock.getTableBlock();
|
||||
encodeMaterials.getTableIdentifier().load(tableBlock);
|
||||
encodeMaterials.setCurrentPackage(packageBlock);
|
||||
|
||||
for(TableBlock frameworkTable:tableBlock.getFrameWorks()){
|
||||
if(frameworkTable instanceof FrameworkTable){
|
||||
encodeMaterials.addFramework((FrameworkTable) frameworkTable);
|
||||
}
|
||||
}
|
||||
return encodeMaterials;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.apk.ApkUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EncodeUtil {
|
||||
public static void sortStrings(List<String> stringList){
|
||||
Comparator<String> cmp=new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
};
|
||||
stringList.sort(cmp);
|
||||
}
|
||||
public static boolean isPublicXml(File file){
|
||||
if(!ApkUtil.FILE_NAME_PUBLIC_XML.equals(file.getName())){
|
||||
return false;
|
||||
}
|
||||
File dir = file.getParentFile();
|
||||
return dir!=null && dir.getName().equals("values");
|
||||
}
|
||||
public static void sortPublicXml(List<File> fileList){
|
||||
Comparator<File> cmp=new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File f1, File f2) {
|
||||
String n1=f1.getAbsolutePath();
|
||||
String n2=f2.getAbsolutePath();
|
||||
return n1.compareTo(n2);
|
||||
}
|
||||
};
|
||||
fileList.sort(cmp);
|
||||
}
|
||||
public static void sortValuesXml(List<File> fileList){
|
||||
Comparator<File> cmp=new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File f1, File f2) {
|
||||
String n1=getValuesXmlCompare(f1);
|
||||
String n2=getValuesXmlCompare(f2);
|
||||
return n1.compareTo(n2);
|
||||
}
|
||||
};
|
||||
fileList.sort(cmp);
|
||||
}
|
||||
private static String getValuesXmlCompare(File file){
|
||||
String name=file.getName().toLowerCase();
|
||||
if(name.equals("public.xml")){
|
||||
return "0";
|
||||
}
|
||||
if(name.equals("ids.xml")){
|
||||
return "1";
|
||||
}
|
||||
if(name.contains("attr")){
|
||||
return "2";
|
||||
}
|
||||
return "3 "+name;
|
||||
}
|
||||
public static boolean isEmpty(String text){
|
||||
if(text==null){
|
||||
return true;
|
||||
}
|
||||
text=text.trim();
|
||||
return text.length()==0;
|
||||
}
|
||||
public static String getQualifiersFromValuesXml(File valuesXml){
|
||||
String dirName=valuesXml.getParentFile().getName();
|
||||
int i=dirName.indexOf('-');
|
||||
if(i>0){
|
||||
return dirName.substring(i);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public static String getEntryPathFromResFile(File resFile){
|
||||
File typeDir=resFile.getParentFile();
|
||||
File resDir=typeDir.getParentFile();
|
||||
return resDir.getName()
|
||||
+"/"+typeDir.getName()
|
||||
+"/"+resFile.getName();
|
||||
}
|
||||
public static String getEntryNameFromResFile(File resFile){
|
||||
String name=resFile.getName();
|
||||
String ninePatch=".9.png";
|
||||
if(name.endsWith(ninePatch)){
|
||||
return name.substring(0, name.length()-ninePatch.length());
|
||||
}
|
||||
int i=name.lastIndexOf('.');
|
||||
if(i>0){
|
||||
name = name.substring(0, i);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
public static String getQualifiersFromResFile(File resFile){
|
||||
String name=resFile.getParentFile().getName();
|
||||
int i=name.indexOf('-');
|
||||
if(i>0){
|
||||
return name.substring(i);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public static String getTypeNameFromResFile(File resFile){
|
||||
String name=resFile.getParentFile().getName();
|
||||
int i=name.indexOf('-');
|
||||
if(i>0){
|
||||
name=name.substring(0, i);
|
||||
}
|
||||
if(!name.equals("plurals") && name.endsWith("s")){
|
||||
name=name.substring(0, name.length()-1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
public static String getTypeNameFromValuesXml(File valuesXml){
|
||||
String name=valuesXml.getName();
|
||||
name=name.substring(0, name.length()-4);
|
||||
if(!name.equals("plurals") && name.endsWith("s")){
|
||||
name=name.substring(0, name.length()-1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
public static String sanitizeType(String type){
|
||||
if(type.startsWith("^attr")){
|
||||
return type;
|
||||
}
|
||||
Matcher matcher=PATTERN_TYPE.matcher(type);
|
||||
if(!matcher.find()){
|
||||
return "";
|
||||
}
|
||||
return matcher.group(1);
|
||||
}
|
||||
public static final String NULL_PACKAGE_NAME = "NULL_PACKAGE_NAME";
|
||||
private static final Pattern PATTERN_TYPE=Pattern.compile("^([a-z]+)[^a-z]*.*$");
|
||||
|
||||
public static final String URI_ANDROID = "http://schemas.android.com/apk/res/android";
|
||||
public static final String URI_APP = "http://schemas.android.com/apk/res-auto";
|
||||
public static final String PREFIX_ANDROID = "android";
|
||||
public static final String PREFIX_APP = "app";
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.FileInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.apk.ApkUtil;
|
||||
import com.reandroid.apk.UncompressedFiles;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.xml.source.XMLFileSource;
|
||||
import com.reandroid.xml.source.XMLSource;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public class FilePathEncoder {
|
||||
private final EncodeMaterials materials;
|
||||
private APKArchive apkArchive;
|
||||
private UncompressedFiles uncompressedFiles;
|
||||
public FilePathEncoder(EncodeMaterials encodeMaterials){
|
||||
this.materials =encodeMaterials;
|
||||
}
|
||||
|
||||
public void setApkArchive(APKArchive apkArchive) {
|
||||
this.apkArchive = apkArchive;
|
||||
}
|
||||
public void setUncompressedFiles(UncompressedFiles uncompressedFiles){
|
||||
this.uncompressedFiles=uncompressedFiles;
|
||||
}
|
||||
public void encodeResDir(File resDir){
|
||||
materials.logVerbose("Scanning file list: "
|
||||
+resDir.getParentFile().getName()
|
||||
+File.separator+resDir.getName());
|
||||
List<File> dirList = ApkUtil.listDirectories(resDir);
|
||||
for(File dir:dirList){
|
||||
if(dir.getName().startsWith("values")){
|
||||
continue;
|
||||
}
|
||||
encodeTypeDir(dir);
|
||||
}
|
||||
}
|
||||
public void encodeTypeDir(File dir){
|
||||
List<File> fileList = ApkUtil.listFiles(dir, null);
|
||||
for(File file:fileList){
|
||||
encodeFileEntry(file);
|
||||
}
|
||||
}
|
||||
public InputSource encodeFileEntry(File resFile){
|
||||
String type = EncodeUtil.getTypeNameFromResFile(resFile);
|
||||
PackageBlock packageBlock = materials.getCurrentPackage();
|
||||
int typeId=packageBlock
|
||||
.getTypeStringPool().idOf(type);
|
||||
String qualifiers = EncodeUtil.getQualifiersFromResFile(resFile);
|
||||
TypeBlock typeBlock = packageBlock.getOrCreateTypeBlock((byte)typeId, qualifiers);
|
||||
String name = EncodeUtil.getEntryNameFromResFile(resFile);
|
||||
int resourceId=materials.resolveLocalResourceId(type, name);
|
||||
|
||||
Entry entry = typeBlock
|
||||
.getOrCreateEntry((short) (0xffff & resourceId));
|
||||
|
||||
String path = EncodeUtil.getEntryPathFromResFile(resFile);
|
||||
entry.setValueAsString(path);
|
||||
materials.setEntryName(entry, name);
|
||||
InputSource inputSource=createInputSource(path, resFile);
|
||||
if(inputSource instanceof XMLEncodeSource){
|
||||
((XMLEncodeSource)inputSource).setEntry(entry);
|
||||
}
|
||||
addInputSource(inputSource);
|
||||
return inputSource;
|
||||
}
|
||||
private InputSource createInputSource(String path, File resFile){
|
||||
if(isXmlFile(resFile)){
|
||||
return createXMLEncodeInputSource(path, resFile);
|
||||
}
|
||||
addUncompressedFiles(path);
|
||||
return createRawFileInputSource(path, resFile);
|
||||
}
|
||||
private InputSource createRawFileInputSource(String path, File resFile){
|
||||
return new FileInputSource(resFile, path);
|
||||
}
|
||||
private InputSource createXMLEncodeInputSource(String path, File resFile){
|
||||
XMLSource xmlSource = new XMLFileSource(path, resFile);
|
||||
return new XMLEncodeSource(materials, xmlSource);
|
||||
}
|
||||
private boolean isXmlFile(File resFile){
|
||||
String name=resFile.getName();
|
||||
if(!name.endsWith(".xml")){
|
||||
return false;
|
||||
}
|
||||
String type=EncodeUtil.getTypeNameFromResFile(resFile);
|
||||
return !type.equals("raw");
|
||||
}
|
||||
private void addInputSource(InputSource inputSource){
|
||||
if(inputSource!=null && this.apkArchive!=null){
|
||||
apkArchive.add(inputSource);
|
||||
}
|
||||
}
|
||||
private void addUncompressedFiles(String path){
|
||||
if(uncompressedFiles!=null){
|
||||
uncompressedFiles.addPath(path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,337 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.apk.*;
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TableBlock;
|
||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
import com.reandroid.identifiers.PackageIdentifier;
|
||||
import com.reandroid.identifiers.ResourceIdentifier;
|
||||
import com.reandroid.identifiers.TableIdentifier;
|
||||
import com.reandroid.xml.XMLException;
|
||||
import com.reandroid.xml.XMLParserFactory;
|
||||
import com.reandroid.xml.source.XMLFileSource;
|
||||
import com.reandroid.xml.source.XMLSource;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class RESEncoder {
|
||||
private APKLogger apkLogger;
|
||||
private final TableBlock tableBlock;
|
||||
private final Set<File> parsedFiles = new HashSet<>();
|
||||
private final ApkModule apkModule;
|
||||
public RESEncoder(){
|
||||
this(new ApkModule("encoded",
|
||||
new APKArchive()), new TableBlock());
|
||||
}
|
||||
public RESEncoder(ApkModule module, TableBlock block){
|
||||
this.apkModule = module;
|
||||
this.tableBlock = block;
|
||||
if(!module.hasTableBlock()){
|
||||
module.setLoadDefaultFramework(false);
|
||||
BlockInputSource<TableBlock> inputSource =
|
||||
new BlockInputSource<>(TableBlock.FILE_NAME, block);
|
||||
inputSource.setMethod(ZipEntry.STORED);
|
||||
this.apkModule.setTableBlock(tableBlock);
|
||||
}
|
||||
}
|
||||
public TableBlock getTableBlock(){
|
||||
return tableBlock;
|
||||
}
|
||||
public ApkModule getApkModule(){
|
||||
return apkModule;
|
||||
}
|
||||
public void scanDirectory(File mainDir) throws IOException, XMLException {
|
||||
scanResourceFiles(mainDir);
|
||||
}
|
||||
private void scanResourceFiles(File mainDir) throws IOException, XMLException {
|
||||
List<File> pubXmlFileList = searchPublicXmlFiles(mainDir);
|
||||
if(pubXmlFileList.size()==0){
|
||||
throw new IOException("No .*/values/"
|
||||
+ ApkUtil.FILE_NAME_PUBLIC_XML+" file found in '"+mainDir);
|
||||
}
|
||||
preloadStringPool(pubXmlFileList);
|
||||
EncodeMaterials encodeMaterials = new EncodeMaterials();
|
||||
encodeMaterials.setAPKLogger(apkLogger);
|
||||
|
||||
TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier();
|
||||
tableIdentifier.loadPublicXml(pubXmlFileList);
|
||||
tableIdentifier.initialize(this.tableBlock);
|
||||
|
||||
excludeIds(pubXmlFileList);
|
||||
File manifestFile = initializeFrameworkFromManifest(encodeMaterials, pubXmlFileList);
|
||||
|
||||
encodeAttrs(encodeMaterials, pubXmlFileList);
|
||||
|
||||
encodeValues(encodeMaterials, pubXmlFileList);
|
||||
|
||||
tableBlock.refresh();
|
||||
|
||||
PackageBlock packageBlock = encodeMaterials.pickMainPackageBlock(this.tableBlock);
|
||||
if(manifestFile != null){
|
||||
if(packageBlock != null){
|
||||
encodeMaterials.setCurrentPackage(packageBlock);
|
||||
}
|
||||
XMLSource xmlSource =
|
||||
new XMLFileSource(AndroidManifestBlock.FILE_NAME, manifestFile);
|
||||
XMLEncodeSource xmlEncodeSource =
|
||||
new XMLEncodeSource(encodeMaterials, xmlSource);
|
||||
getApkModule().getApkArchive().add(xmlEncodeSource);
|
||||
}
|
||||
}
|
||||
private File initializeFrameworkFromManifest(EncodeMaterials encodeMaterials, List<File> pubXmlFileList) throws IOException {
|
||||
for(File pubXmlFile:pubXmlFileList){
|
||||
addParsedFiles(pubXmlFile);
|
||||
File manifestFile = toAndroidManifest(pubXmlFile);
|
||||
if(!manifestFile.isFile()){
|
||||
continue;
|
||||
}
|
||||
initializeFrameworkFromManifest(encodeMaterials, manifestFile);
|
||||
return manifestFile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private void encodeValues(EncodeMaterials encodeMaterials, List<File> pubXmlFileList) throws XMLException, IOException {
|
||||
logMessage("Encoding values ...");
|
||||
TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier();
|
||||
|
||||
for(File pubXmlFile:pubXmlFileList){
|
||||
addParsedFiles(pubXmlFile);
|
||||
PackageIdentifier packageIdentifier = tableIdentifier.getByTag(pubXmlFile);
|
||||
|
||||
PackageBlock packageBlock = packageIdentifier.getPackageBlock();
|
||||
|
||||
encodeMaterials.setCurrentPackage(packageBlock);
|
||||
|
||||
File resDir=toResDirectory(pubXmlFile);
|
||||
encodeResDir(encodeMaterials, resDir);
|
||||
FilePathEncoder filePathEncoder = new FilePathEncoder(encodeMaterials);
|
||||
filePathEncoder.setApkArchive(getApkModule().getApkArchive());
|
||||
filePathEncoder.setUncompressedFiles(getApkModule().getUncompressedFiles());
|
||||
filePathEncoder.encodeResDir(resDir);
|
||||
|
||||
packageBlock.sortTypes();
|
||||
packageBlock.refresh();
|
||||
}
|
||||
}
|
||||
private void encodeAttrs(EncodeMaterials encodeMaterials, List<File> pubXmlFileList) throws XMLException {
|
||||
logMessage("Encoding attrs ...");
|
||||
TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier();
|
||||
|
||||
for(File pubXmlFile:pubXmlFileList){
|
||||
addParsedFiles(pubXmlFile);
|
||||
PackageIdentifier packageIdentifier = tableIdentifier.getByTag(pubXmlFile);
|
||||
|
||||
PackageBlock packageBlock = packageIdentifier.getPackageBlock();
|
||||
encodeMaterials.setCurrentPackage(packageBlock);
|
||||
|
||||
ValuesEncoder valuesEncoder = new ValuesEncoder(encodeMaterials);
|
||||
File fileAttrs = toAttr(pubXmlFile);
|
||||
if(fileAttrs.isFile()){
|
||||
valuesEncoder.encodeValuesXml(fileAttrs);
|
||||
packageBlock.sortTypes();
|
||||
packageBlock.refresh();
|
||||
addParsedFiles(fileAttrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void excludeIds(List<File> pubXmlFileList){
|
||||
for(File pubXmlFile:pubXmlFileList){
|
||||
addParsedFiles(pubXmlFile);
|
||||
File fileIds = toId(pubXmlFile);
|
||||
if(fileIds.isFile()){
|
||||
addParsedFiles(fileIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void initializeFrameworkFromManifest(EncodeMaterials encodeMaterials, File manifestFile) throws IOException {
|
||||
XmlPullParser parser;
|
||||
try {
|
||||
parser = XMLParserFactory.newPullParser(manifestFile);
|
||||
} catch (XmlPullParserException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
FrameworkApk frameworkApk = getApkModule().initializeAndroidFramework(parser);
|
||||
encodeMaterials.addFramework(frameworkApk);
|
||||
initializeMainPackageId(encodeMaterials, parser);
|
||||
XmlHelper.closeSilent(parser);
|
||||
}
|
||||
private void initializeMainPackageId(EncodeMaterials encodeMaterials, XmlPullParser parser) throws IOException {
|
||||
Map<String, String> applicationAttributes;
|
||||
try {
|
||||
applicationAttributes = XmlHelper.readAttributes(parser, AndroidManifestBlock.TAG_application);
|
||||
} catch (XmlPullParserException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
if(applicationAttributes == null){
|
||||
return;
|
||||
}
|
||||
String iconReference = applicationAttributes.get(AndroidManifestBlock.NAME_icon);
|
||||
if(iconReference == null){
|
||||
return;
|
||||
}
|
||||
logMessage("Set main package id from manifest: " + iconReference);
|
||||
ValueDecoder.ReferenceString ref = ValueDecoder.parseReference(iconReference);
|
||||
if(ref == null){
|
||||
logMessage("Something wrong on : " + AndroidManifestBlock.NAME_icon);
|
||||
return;
|
||||
}
|
||||
TableIdentifier tableIdentifier = encodeMaterials.getTableIdentifier();
|
||||
ResourceIdentifier resourceIdentifier;
|
||||
if(ref.packageName != null){
|
||||
resourceIdentifier = tableIdentifier.get(ref.packageName, ref.type, ref.name);
|
||||
}else {
|
||||
resourceIdentifier = tableIdentifier.get(ref.type, ref.name);
|
||||
}
|
||||
if(resourceIdentifier == null){
|
||||
logMessage("WARN: failed to resolve: " + ref);
|
||||
return;
|
||||
}
|
||||
int packageId = resourceIdentifier.getPackageId();
|
||||
encodeMaterials.setMainPackageId(packageId);
|
||||
logMessage("Main package id initialized: id = "
|
||||
+ HexUtil.toHex2((byte)packageId) + ", from: " + ref );
|
||||
}
|
||||
private void preloadStringPool(List<File> pubXmlFileList){
|
||||
logMessage("Loading string pool ...");
|
||||
ValuesStringPoolBuilder poolBuilder=new ValuesStringPoolBuilder();
|
||||
for(File pubXml:pubXmlFileList){
|
||||
File resDir=toResDirectory(pubXml);
|
||||
List<File> valuesDirList = listValuesDir(resDir);
|
||||
for(File dir:valuesDirList){
|
||||
logVerbose(poolBuilder.size()+" building pool: "+dir.getName());
|
||||
poolBuilder.scanValuesDirectory(dir);
|
||||
}
|
||||
}
|
||||
poolBuilder.addTo(tableBlock.getTableStringPool());
|
||||
}
|
||||
|
||||
private void encodeResDir(EncodeMaterials materials, File resDir) throws XMLException {
|
||||
|
||||
List<File> valuesDirList = listValuesDir(resDir);
|
||||
for(File valuesDir:valuesDirList){
|
||||
encodeValuesDir(materials, valuesDir);
|
||||
}
|
||||
}
|
||||
private void encodeValuesDir(EncodeMaterials materials, File valuesDir) throws XMLException {
|
||||
ValuesEncoder valuesEncoder = new ValuesEncoder(materials);
|
||||
List<File> xmlFiles = ApkUtil.listFiles(valuesDir, ".xml");
|
||||
EncodeUtil.sortValuesXml(xmlFiles);
|
||||
for(File file:xmlFiles){
|
||||
if(isAlreadyParsed(file)){
|
||||
continue;
|
||||
}
|
||||
addParsedFiles(file);
|
||||
valuesEncoder.encodeValuesXml(file);
|
||||
}
|
||||
}
|
||||
private File toAndroidManifest(File pubXmlFile){
|
||||
File resDirectory = toResDirectory(pubXmlFile);
|
||||
File packageDirectory = resDirectory.getParentFile();
|
||||
File root = packageDirectory.getParentFile();
|
||||
return new File(root, AndroidManifestBlock.FILE_NAME);
|
||||
}
|
||||
private File toPackageDirectory(File pubXmlFile){
|
||||
return toResDirectory(pubXmlFile)
|
||||
.getParentFile();
|
||||
}
|
||||
private File toResDirectory(File pubXmlFile){
|
||||
return pubXmlFile
|
||||
.getParentFile()
|
||||
.getParentFile();
|
||||
}
|
||||
private File toId(File pubXmlFile){
|
||||
return new File(pubXmlFile.getParentFile(), "ids.xml");
|
||||
}
|
||||
private File toAttr(File pubXmlFile){
|
||||
return new File(pubXmlFile.getParentFile(), "attrs.xml");
|
||||
}
|
||||
private List<File> listValuesDir(File resDir){
|
||||
List<File> results=new ArrayList<>();
|
||||
File def=new File(resDir, "values");
|
||||
results.add(def);
|
||||
File[] dirList=resDir.listFiles();
|
||||
if(dirList!=null){
|
||||
for(File dir:dirList){
|
||||
if(def.equals(dir) || !dir.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
if(dir.getName().startsWith("values-")){
|
||||
results.add(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
private List<File> searchPublicXmlFiles(File mainDir){
|
||||
logVerbose("Searching public.xml: "+mainDir);
|
||||
List<File> dirList=ApkUtil.listDirectories(mainDir);
|
||||
List<File> xmlFiles = new ArrayList<>();
|
||||
for(File dir:dirList){
|
||||
if(dir.getName().equals("root")){
|
||||
continue;
|
||||
}
|
||||
xmlFiles.addAll(
|
||||
ApkUtil.recursiveFiles(dir, ApkUtil.FILE_NAME_PUBLIC_XML));
|
||||
}
|
||||
List<File> results = new ArrayList<>();
|
||||
for(File file:xmlFiles){
|
||||
if(!EncodeUtil.isPublicXml(file)){
|
||||
continue;
|
||||
}
|
||||
if(toAndroidManifest(file).isFile()){
|
||||
results.add(file);
|
||||
}
|
||||
}
|
||||
EncodeUtil.sortPublicXml(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private boolean isAlreadyParsed(File file){
|
||||
return parsedFiles.contains(file);
|
||||
}
|
||||
private void addParsedFiles(File file){
|
||||
parsedFiles.add(file);
|
||||
}
|
||||
public void setAPKLogger(APKLogger logger) {
|
||||
this.apkLogger = logger;
|
||||
this.apkModule.setAPKLogger(logger);
|
||||
}
|
||||
private void logMessage(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logMessage(msg);
|
||||
}
|
||||
}
|
||||
private void logError(String msg, Throwable tr) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logError(msg, tr);
|
||||
}
|
||||
}
|
||||
private void logVerbose(String msg) {
|
||||
if(apkLogger!=null){
|
||||
apkLogger.logVerbose(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
import com.reandroid.xml.XMLException;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ValuesEncoder {
|
||||
private final EncodeMaterials materials;
|
||||
private final Map<String, XMLValuesEncoder> xmlEncodersMap;
|
||||
private final Map<String, XMLValuesEncoderBag> xmlBagEncodersMap;
|
||||
private final XMLValuesEncoderCommon commonEncoder;
|
||||
private final XMLValuesEncoderBag bagCommonEncoder;
|
||||
public ValuesEncoder(EncodeMaterials materials){
|
||||
this.materials=materials;
|
||||
Map<String, XMLValuesEncoder> map = new HashMap<>();
|
||||
map.put("id", new XMLValuesEncoderId(materials));
|
||||
map.put("string", new XMLValuesEncoderString(materials));
|
||||
XMLValuesEncoderDimen encoderDimen=new XMLValuesEncoderDimen(materials);
|
||||
map.put("dimen", encoderDimen);
|
||||
map.put("fraction", encoderDimen);
|
||||
map.put("color", new XMLValuesEncoderColor(materials));
|
||||
map.put("integer", new XMLValuesEncoderInteger(materials));
|
||||
|
||||
this.xmlEncodersMap=map;
|
||||
this.commonEncoder=new XMLValuesEncoderCommon(materials);
|
||||
|
||||
Map<String, XMLValuesEncoderBag> mapBag=new HashMap<>();
|
||||
XMLValuesEncoderAttr encoderAttr = new XMLValuesEncoderAttr(materials);
|
||||
mapBag.put("attr", encoderAttr);
|
||||
mapBag.put("^attr-private", encoderAttr);
|
||||
mapBag.put("plurals", new XMLValuesEncoderPlurals(materials));
|
||||
mapBag.put("array", new XMLValuesEncoderArray(materials));
|
||||
mapBag.put("style", new XMLValuesEncoderStyle(materials));
|
||||
this.xmlBagEncodersMap=mapBag;
|
||||
this.bagCommonEncoder=new XMLValuesEncoderBag(materials);
|
||||
|
||||
}
|
||||
public void encodeValuesXml(File valuesXmlFile) throws XMLException {
|
||||
if(valuesXmlFile.getName().equals("public.xml")){
|
||||
return;
|
||||
}
|
||||
String simpleName = valuesXmlFile.getParentFile().getName()
|
||||
+File.separator+valuesXmlFile.getName();
|
||||
materials.logVerbose("Encoding: "+simpleName);
|
||||
|
||||
String type = EncodeUtil.getTypeNameFromValuesXml(valuesXmlFile);
|
||||
String qualifiers = EncodeUtil.getQualifiersFromValuesXml(valuesXmlFile);
|
||||
XMLDocument xmlDocument = XMLDocument.load(valuesXmlFile);
|
||||
encodeValuesXml(type, qualifiers, xmlDocument);
|
||||
}
|
||||
public void encodeValue(String qualifiers, XMLElement element){
|
||||
String type = getType(element, null);
|
||||
if(type == null){
|
||||
throw new EncodeException("Can not determine type: " + element);
|
||||
}
|
||||
encodeValue(type, qualifiers, element);
|
||||
}
|
||||
public void encodeValue(String type, String qualifiers, XMLElement element){
|
||||
boolean is_bag = isBag(element);
|
||||
encodeValue(is_bag, type, qualifiers, element);
|
||||
}
|
||||
public void encodeValue(boolean is_bag, String type, String qualifiers, XMLElement element){
|
||||
PackageBlock packageBlock = getEncodeMaterials().getCurrentPackage();
|
||||
Entry entry = packageBlock
|
||||
.getOrCreate(qualifiers, type, element.getAttributeValue("name"));
|
||||
encodeValue(is_bag, entry, element);
|
||||
}
|
||||
public void encodeValue(Entry entry, XMLElement element){
|
||||
boolean is_bag = isBag(element);
|
||||
encodeValue(is_bag, entry, element);
|
||||
}
|
||||
public void encodeValue(boolean is_bag, Entry entry, XMLElement element){
|
||||
XMLValuesEncoder encoder;
|
||||
String type = entry.getTypeName();
|
||||
if(is_bag){
|
||||
encoder = getBagEncoder(type);
|
||||
}else{
|
||||
encoder = getEncoder(type);
|
||||
}
|
||||
encoder.encodeValue(entry, element);
|
||||
}
|
||||
public void encodeValues(String type, String qualifiers, XMLDocument xmlDocument){
|
||||
type = getType(xmlDocument, type);
|
||||
boolean is_bag = isBag(xmlDocument, type);
|
||||
encodeValues(is_bag, type, qualifiers, xmlDocument);
|
||||
}
|
||||
public void encodeValues(boolean is_bag, String type, String qualifiers, XMLDocument xmlDocument){
|
||||
XMLValuesEncoder encoder;
|
||||
if(is_bag){
|
||||
encoder = getBagEncoder(type);
|
||||
}else{
|
||||
encoder = getEncoder(type);
|
||||
}
|
||||
encoder.encode(type, qualifiers, xmlDocument);
|
||||
}
|
||||
public EncodeMaterials getEncodeMaterials(){
|
||||
return materials;
|
||||
}
|
||||
private void encodeValuesXml(String type, String qualifiers, XMLDocument xmlDocument) {
|
||||
type=getType(xmlDocument, type);
|
||||
XMLValuesEncoder encoder;
|
||||
if(isBag(xmlDocument, type)){
|
||||
encoder = getBagEncoder(type);
|
||||
}else{
|
||||
encoder=getEncoder(type);
|
||||
}
|
||||
encoder.encode(type, qualifiers, xmlDocument);
|
||||
}
|
||||
private boolean isBag(XMLElement element){
|
||||
if(element.hasChildElements()){
|
||||
return true;
|
||||
}
|
||||
return element.getAttributeCount() > 1;
|
||||
}
|
||||
private boolean isBag(XMLDocument xmlDocument, String type){
|
||||
if(type.startsWith("attr")){
|
||||
return true;
|
||||
}
|
||||
if(type.startsWith("^attr")){
|
||||
return true;
|
||||
}
|
||||
if(type.startsWith("style")){
|
||||
return true;
|
||||
}
|
||||
if(type.startsWith("plurals")){
|
||||
return true;
|
||||
}
|
||||
if(type.startsWith("array")){
|
||||
return true;
|
||||
}
|
||||
if(type.startsWith("string")){
|
||||
return false;
|
||||
}
|
||||
XMLElement documentElement=xmlDocument.getDocumentElement();
|
||||
int count=documentElement.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement element=documentElement.getChildAt(i);
|
||||
if(element.getChildesCount()>0){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private boolean hasNameAttributes(XMLDocument xmlDocument){
|
||||
XMLElement documentElement=xmlDocument.getDocumentElement();
|
||||
int count=documentElement.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement element=documentElement.getChildAt(i);
|
||||
if(element.getChildesCount()>0){
|
||||
XMLElement child = element.getChildAt(0);
|
||||
if(child.getAttributeValue("name") != null){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private String getType(XMLDocument xmlDocument, String def){
|
||||
XMLElement documentElement=xmlDocument.getDocumentElement();
|
||||
if(documentElement.getChildesCount()==0){
|
||||
return def;
|
||||
}
|
||||
XMLElement first=documentElement.getChildAt(0);
|
||||
String type=first.getAttributeValue("type");
|
||||
if(type==null){
|
||||
type=first.getTagName();
|
||||
}
|
||||
if(type==null){
|
||||
return def;
|
||||
}
|
||||
if(type.endsWith("-array")){
|
||||
return "array";
|
||||
}
|
||||
if(type.startsWith("attr-private")){
|
||||
return "^attr-private";
|
||||
}
|
||||
if(type.equals("item")){
|
||||
return def;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
private String getType(XMLElement first, String def){
|
||||
String type = first.getAttributeValue("type");
|
||||
if(type == null){
|
||||
type = first.getTagName();
|
||||
}
|
||||
if(type == null){
|
||||
return def;
|
||||
}
|
||||
if(type.endsWith("-array")){
|
||||
return "array";
|
||||
}
|
||||
if(type.startsWith("attr-private")){
|
||||
return "^attr-private";
|
||||
}
|
||||
if(type.equals("item")){
|
||||
return def;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
private XMLValuesEncoder getEncoder(String type){
|
||||
type=EncodeUtil.sanitizeType(type);
|
||||
XMLValuesEncoder encoder=xmlEncodersMap.get(type);
|
||||
if(encoder!=null){
|
||||
return encoder;
|
||||
}
|
||||
return commonEncoder;
|
||||
}
|
||||
private XMLValuesEncoderBag getBagEncoder(String type){
|
||||
type=EncodeUtil.sanitizeType(type);
|
||||
XMLValuesEncoderBag encoder=xmlBagEncodersMap.get(type);
|
||||
if(encoder!=null){
|
||||
return encoder;
|
||||
}
|
||||
return bagCommonEncoder;
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.array.StringArray;
|
||||
import com.reandroid.arsc.array.StyleArray;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.item.StyleItem;
|
||||
import com.reandroid.arsc.item.TableString;
|
||||
import com.reandroid.arsc.pool.TableStringPool;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
import com.reandroid.xml.XMLSpanInfo;
|
||||
import com.reandroid.xml.XMLSpannable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class ValuesStringPoolBuilder {
|
||||
private final Set<String> stringList;
|
||||
private final Set<String> styleList;
|
||||
public ValuesStringPoolBuilder(){
|
||||
this.stringList=new HashSet<>();
|
||||
this.styleList=new HashSet<>();
|
||||
}
|
||||
public void addTo(TableStringPool stringPool){
|
||||
if(stringPool.getStringsArray().childesCount()==0){
|
||||
buildWithStyles(stringPool);
|
||||
}
|
||||
stringPool.addStrings(stringList);
|
||||
stringList.clear();
|
||||
styleList.clear();
|
||||
stringPool.refresh();
|
||||
}
|
||||
private void buildWithStyles(TableStringPool stringPool){
|
||||
List<XMLSpannable> spannableList = buildSpannable();
|
||||
if(spannableList.size()==0){
|
||||
return;
|
||||
}
|
||||
StringArray<TableString> stringsArray = stringPool.getStringsArray();
|
||||
StyleArray styleArray = stringPool.getStyleArray();
|
||||
|
||||
int stylesCount = spannableList.size();
|
||||
stringsArray.setChildesCount(stylesCount);
|
||||
styleArray.setChildesCount(stylesCount);
|
||||
|
||||
List<String> tagList =
|
||||
new ArrayList<>(XMLSpannable.tagList(spannableList));
|
||||
EncodeUtil.sortStrings(tagList);
|
||||
Map<String, TableString> tagsMap =
|
||||
stringPool.insertStrings(tagList);
|
||||
|
||||
List<String> textList = XMLSpannable.toTextList(spannableList);
|
||||
|
||||
for(int i=0;i<stylesCount;i++){
|
||||
XMLSpannable spannable = spannableList.get(i);
|
||||
TableString tableString = stringsArray.get(i);
|
||||
StyleItem styleItem = styleArray.get(i);
|
||||
|
||||
tableString.set(textList.get(i));
|
||||
|
||||
for(XMLSpanInfo spanInfo:spannable.getSpanInfoList()){
|
||||
TableString tag = tagsMap.get(spanInfo.tag);
|
||||
int tagRef=tag.getIndex();
|
||||
styleItem.addStylePiece(tagRef, spanInfo.start, spanInfo.end);
|
||||
}
|
||||
}
|
||||
stringPool.refreshUniqueIdMap();
|
||||
}
|
||||
private List<XMLSpannable> buildSpannable(){
|
||||
List<XMLSpannable> results=new ArrayList<>();
|
||||
Set<String> removeList=new HashSet<>();
|
||||
for(String text:styleList){
|
||||
XMLSpannable spannable=XMLSpannable.parse(text);
|
||||
if(spannable!=null){
|
||||
results.add(spannable);
|
||||
removeList.add(text);
|
||||
}else {
|
||||
stringList.add(text);
|
||||
}
|
||||
}
|
||||
stringList.removeAll(removeList);
|
||||
XMLSpannable.sort(results);
|
||||
return results;
|
||||
}
|
||||
public void scanValuesDirectory(File dir){
|
||||
addStringsFile(new File(dir, "strings.xml"));
|
||||
addBagsFile(new File(dir, "plurals.xml"));
|
||||
}
|
||||
public int size(){
|
||||
return stringList.size();
|
||||
}
|
||||
private void addStringsFile(File file) {
|
||||
if(file==null||!file.isFile()){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
XMLDocument xmlDocument = XMLDocument.load(file);
|
||||
addStrings(xmlDocument);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
private void addBagsFile(File file) {
|
||||
if(file==null||!file.isFile()){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
XMLDocument xmlDocument = XMLDocument.load(file);
|
||||
addBagStrings(xmlDocument);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
private void addBagStrings(XMLDocument xmlDocument){
|
||||
if(xmlDocument == null){
|
||||
return;
|
||||
}
|
||||
XMLElement documentElement = xmlDocument.getDocumentElement();
|
||||
if(documentElement==null){
|
||||
return;
|
||||
}
|
||||
int count = documentElement.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement child=documentElement.getChildAt(i);
|
||||
int childCount=child.getChildesCount();
|
||||
for(int j=0;j<childCount;j++){
|
||||
addStrings(child.getChildAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
private void addStrings(XMLDocument xmlDocument){
|
||||
if(xmlDocument == null){
|
||||
return;
|
||||
}
|
||||
XMLElement documentElement = xmlDocument.getDocumentElement();
|
||||
if(documentElement==null){
|
||||
return;
|
||||
}
|
||||
int count = documentElement.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
addStrings(documentElement.getChildAt(i));
|
||||
}
|
||||
}
|
||||
private void addStrings(XMLElement element){
|
||||
if(element.hasChildElements()){
|
||||
addStyleElement(element);
|
||||
}else {
|
||||
String text = ValueDecoder
|
||||
.unQuoteWhitespace(element.getTextContent());
|
||||
text = ValueDecoder
|
||||
.unEscapeSpecialCharacter(text);
|
||||
addString(text);
|
||||
}
|
||||
}
|
||||
private void addString(String text){
|
||||
if(text!=null && text.length()>0 && text.charAt(0)!='@'){
|
||||
stringList.add(text);
|
||||
}
|
||||
}
|
||||
private void addStyleElement(XMLElement element){
|
||||
styleList.add(element.buildTextContent(false));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.archive.ByteInputSource;
|
||||
import com.reandroid.apk.CrcOutputStream;
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.xml.XMLException;
|
||||
import com.reandroid.xml.source.XMLSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class XMLEncodeSource extends ByteInputSource {
|
||||
private final EncodeMaterials encodeMaterials;
|
||||
private final XMLSource xmlSource;
|
||||
private ResXmlDocument resXmlDocument;
|
||||
private Entry mEntry;
|
||||
public XMLEncodeSource(EncodeMaterials encodeMaterials, XMLSource xmlSource, Entry entry){
|
||||
super(new byte[0], xmlSource.getPath());
|
||||
this.encodeMaterials = encodeMaterials;
|
||||
this.xmlSource = xmlSource;
|
||||
this.mEntry = entry;
|
||||
}
|
||||
public XMLEncodeSource(EncodeMaterials encodeMaterials, XMLSource xmlSource){
|
||||
this(encodeMaterials, xmlSource, null);
|
||||
}
|
||||
|
||||
public XMLSource getXmlSource() {
|
||||
return xmlSource;
|
||||
}
|
||||
public Entry getEntry(){
|
||||
return mEntry;
|
||||
}
|
||||
public void setEntry(Entry entry) {
|
||||
this.mEntry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength() throws IOException{
|
||||
return getResXmlBlock().countBytes();
|
||||
}
|
||||
@Override
|
||||
public long getCrc() throws IOException{
|
||||
ResXmlDocument resXmlDocument = getResXmlBlock();
|
||||
CrcOutputStream outputStream=new CrcOutputStream();
|
||||
resXmlDocument.writeBytes(outputStream);
|
||||
return outputStream.getCrcValue();
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return getResXmlBlock().writeBytes(outputStream);
|
||||
}
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
try {
|
||||
return getResXmlBlock().getBytes();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
//should not reach here
|
||||
return new byte[0];
|
||||
}
|
||||
public ResXmlDocument getResXmlBlock() throws IOException{
|
||||
if(resXmlDocument !=null){
|
||||
return resXmlDocument;
|
||||
}
|
||||
try {
|
||||
XMLFileEncoder xmlFileEncoder=new XMLFileEncoder(encodeMaterials);
|
||||
xmlFileEncoder.setCurrentPath(xmlSource.getPath());
|
||||
EncodeMaterials encodeMaterials = this.encodeMaterials;
|
||||
encodeMaterials.logVerbose("Encoding xml: " + xmlSource.getPath());
|
||||
PackageBlock currentPackage = encodeMaterials.getCurrentPackage();
|
||||
PackageBlock packageBlock = getEntryPackageBlock();
|
||||
if(packageBlock != null && packageBlock != currentPackage){
|
||||
encodeMaterials.setCurrentPackage(packageBlock);
|
||||
}
|
||||
resXmlDocument = xmlFileEncoder.encode(xmlSource.getXMLDocument());
|
||||
} catch (XMLException ex) {
|
||||
throw new EncodeException("XMLException on: '"+xmlSource.getPath()
|
||||
+"'\n '"+ex.getMessage()+"'");
|
||||
}
|
||||
return resXmlDocument;
|
||||
}
|
||||
private PackageBlock getEntryPackageBlock(){
|
||||
Entry entry = getEntry();
|
||||
if(entry != null){
|
||||
return entry.getPackageBlock();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public void disposeInputSource(){
|
||||
this.xmlSource.disposeXml();
|
||||
if(this.resXmlDocument !=null){
|
||||
resXmlDocument =null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.array.ResValueMapArray;
|
||||
import com.reandroid.arsc.chunk.xml.*;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.AttributeDataFormat;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.arsc.value.attribute.AttributeBag;
|
||||
import com.reandroid.xml.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class XMLFileEncoder {
|
||||
private final EncodeMaterials materials;
|
||||
private ResXmlDocument resXmlDocument;
|
||||
private String mCurrentPath;
|
||||
public XMLFileEncoder(EncodeMaterials materials){
|
||||
this.materials=materials;
|
||||
}
|
||||
|
||||
// Just for logging purpose
|
||||
public void setCurrentPath(String path) {
|
||||
this.mCurrentPath = path;
|
||||
}
|
||||
public ResXmlDocument encode(String xmlString){
|
||||
try {
|
||||
return encode(XMLDocument.load(xmlString));
|
||||
} catch (XMLException ex) {
|
||||
materials.logMessage(ex.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public ResXmlDocument encode(InputStream inputStream){
|
||||
try {
|
||||
return encode(XMLDocument.load(inputStream));
|
||||
} catch (XMLException ex) {
|
||||
materials.logMessage(ex.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public ResXmlDocument encode(File xmlFile){
|
||||
setCurrentPath(xmlFile.getAbsolutePath());
|
||||
try {
|
||||
return encode(XMLDocument.load(xmlFile));
|
||||
} catch (XMLException ex) {
|
||||
materials.logMessage(ex.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public ResXmlDocument encode(XMLDocument xmlDocument){
|
||||
resXmlDocument = new ResXmlDocument();
|
||||
resXmlDocument.setPackageBlock(materials.getCurrentPackage());
|
||||
buildIdMap(xmlDocument);
|
||||
buildElement(xmlDocument);
|
||||
resXmlDocument.refresh();
|
||||
return resXmlDocument;
|
||||
}
|
||||
public ResXmlDocument getResXmlBlock(){
|
||||
return resXmlDocument;
|
||||
}
|
||||
private void buildElement(XMLDocument xmlDocument){
|
||||
XMLElement element = xmlDocument.getDocumentElement();
|
||||
ResXmlElement resXmlElement = resXmlDocument.createRootElement(element.getTagName());
|
||||
buildElement(element, resXmlElement);
|
||||
}
|
||||
private void buildElement(XMLElement element, ResXmlElement resXmlElement){
|
||||
ensureNamespaces(element, resXmlElement);
|
||||
resXmlElement.setTag(element.getTagName());
|
||||
buildAttributes(element, resXmlElement);
|
||||
for(XMLNode node:element.getChildNodes()){
|
||||
if(node instanceof XMLText){
|
||||
resXmlElement.addResXmlText(((XMLText)node).getText(true));
|
||||
}else if(node instanceof XMLComment){
|
||||
resXmlElement.setComment(((XMLComment)node).getCommentText());
|
||||
}else if(node instanceof XMLElement){
|
||||
XMLElement child=(XMLElement) node;
|
||||
ResXmlElement childXml=resXmlElement.createChildElement();
|
||||
buildElement(child, childXml);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void buildAttributes(XMLElement element, ResXmlElement resXmlElement){
|
||||
for(XMLAttribute attribute:element.listAttributes()){
|
||||
if(attribute instanceof SchemaAttr){
|
||||
continue;
|
||||
}
|
||||
if(SchemaAttr.looksSchema(attribute.getName(), attribute.getValue())){
|
||||
continue;
|
||||
}
|
||||
String name=attribute.getNameWoPrefix();
|
||||
int resourceId=decodeUnknownAttributeHex(name);
|
||||
Entry entry =null;
|
||||
if(resourceId==0){
|
||||
entry =getAttributeBlock(attribute);
|
||||
if(entry !=null){
|
||||
resourceId= entry.getResourceId();
|
||||
}else if(attribute.getNamePrefix()!=null){
|
||||
throw new EncodeException("No resource found for attribute: "
|
||||
+ attribute.getName() + ", at file "+mCurrentPath);
|
||||
}
|
||||
}
|
||||
ResXmlAttribute xmlAttribute =
|
||||
resXmlElement.createAttribute(name, resourceId);
|
||||
String prefix=attribute.getNamePrefix();
|
||||
if(prefix!=null){
|
||||
ResXmlStartNamespace ns = resXmlElement.getStartNamespaceByPrefix(prefix);
|
||||
if(ns==null){
|
||||
ns=forceCreateNamespace(resXmlElement, resourceId, prefix);
|
||||
}
|
||||
if(ns==null){
|
||||
throw new EncodeException("Namespace not found: "
|
||||
+attribute.toString()
|
||||
+", path="+mCurrentPath);
|
||||
}
|
||||
xmlAttribute.setNamespaceReference(ns.getUriReference());
|
||||
}
|
||||
|
||||
String valueText=attribute.getValue();
|
||||
|
||||
if(ValueDecoder.isReference(valueText)){
|
||||
if(valueText.startsWith("?")){
|
||||
xmlAttribute.setValueType(ValueType.ATTRIBUTE);
|
||||
}else {
|
||||
xmlAttribute.setValueType(ValueType.REFERENCE);
|
||||
}
|
||||
xmlAttribute.setData(materials.resolveReference(valueText));
|
||||
continue;
|
||||
}
|
||||
if(entry !=null){
|
||||
AttributeBag attributeBag=AttributeBag
|
||||
.create((ResValueMapArray) entry.getTableEntry().getValue());
|
||||
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
attributeBag.encodeEnumOrFlagValue(valueText);
|
||||
if(encodeResult!=null){
|
||||
xmlAttribute.setValueType(encodeResult.valueType);
|
||||
xmlAttribute.setData(encodeResult.value);
|
||||
continue;
|
||||
}
|
||||
if(attributeBag.isEqualType(AttributeDataFormat.STRING)) {
|
||||
xmlAttribute.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(EncodeUtil.isEmpty(valueText)) {
|
||||
if(valueText == null){
|
||||
valueText = "";
|
||||
}
|
||||
xmlAttribute.setValueAsString(valueText);
|
||||
}else{
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeGuessAny(valueText);
|
||||
if(encodeResult!=null){
|
||||
xmlAttribute.setValueType(encodeResult.valueType);
|
||||
xmlAttribute.setData(encodeResult.value);
|
||||
}else {
|
||||
xmlAttribute.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
}
|
||||
}
|
||||
}
|
||||
resXmlElement.calculatePositions();
|
||||
}
|
||||
private void ensureNamespaces(XMLElement element, ResXmlElement resXmlElement){
|
||||
for(XMLAttribute attribute:element.listAttributes()){
|
||||
String prefix = SchemaAttr.getPrefix(attribute.getName());
|
||||
if(prefix==null){
|
||||
continue;
|
||||
}
|
||||
String uri=attribute.getValue();
|
||||
resXmlElement.getOrCreateNamespace(uri, prefix);
|
||||
}
|
||||
}
|
||||
private void buildIdMap(XMLDocument xmlDocument){
|
||||
ResIdBuilder idBuilder=new ResIdBuilder();
|
||||
XMLElement element= xmlDocument.getDocumentElement();
|
||||
searchResIds(idBuilder, element);
|
||||
idBuilder.buildTo(resXmlDocument.getResXmlIDMap());
|
||||
}
|
||||
private void searchResIds(ResIdBuilder idBuilder, XMLElement element){
|
||||
for(XMLAttribute attribute : element.listAttributes()){
|
||||
addResourceId(idBuilder, attribute);
|
||||
}
|
||||
int count=element.getChildesCount();
|
||||
for(int i=0;i<count;i++){
|
||||
searchResIds(idBuilder, element.getChildAt(i));
|
||||
}
|
||||
}
|
||||
private void addResourceId(ResIdBuilder idBuilder, XMLAttribute attribute){
|
||||
String name=attribute.getNameWoPrefix();
|
||||
int id=decodeUnknownAttributeHex(name);
|
||||
if(id!=0){
|
||||
idBuilder.add(id, name);
|
||||
return;
|
||||
}
|
||||
Entry entry = getAttributeBlock(attribute);
|
||||
if(entry !=null){
|
||||
idBuilder.add(entry.getResourceId(), entry.getName());
|
||||
}
|
||||
}
|
||||
private int decodeUnknownAttributeHex(String name){
|
||||
if(name.length()==0||name.charAt(0)!='@'){
|
||||
return 0;
|
||||
}
|
||||
name=name.substring(1);
|
||||
if(!ValueDecoder.isHex(name)){
|
||||
return 0;
|
||||
}
|
||||
return ValueDecoder.parseHex(name);
|
||||
}
|
||||
private Entry getAttributeBlock(XMLAttribute attribute){
|
||||
if(attribute instanceof SchemaAttr){
|
||||
return null;
|
||||
}
|
||||
String name=attribute.getName();
|
||||
if(name.indexOf(':')<0){
|
||||
return null;
|
||||
}
|
||||
return materials.getAttributeBlock(name);
|
||||
}
|
||||
private ResXmlStartNamespace forceCreateNamespace(ResXmlElement resXmlElement,
|
||||
int resourceId, String prefix){
|
||||
if(!materials.isForceCreateNamespaces()){
|
||||
return null;
|
||||
}
|
||||
int pkgId = (resourceId>>24) & 0xff;
|
||||
String uri;
|
||||
if(pkgId == 1){
|
||||
uri = EncodeUtil.URI_ANDROID;
|
||||
}else {
|
||||
uri=EncodeUtil.URI_APP;
|
||||
}
|
||||
ResXmlElement root = resXmlElement.getRootResXmlElement();
|
||||
ResXmlStartNamespace ns = root.getOrCreateNamespace(uri, prefix);
|
||||
materials.logVerbose("Force created ns: "+prefix+":"+uri);
|
||||
return ns;
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.TypeBlock;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
class XMLValuesEncoder {
|
||||
private final EncodeMaterials materials;
|
||||
XMLValuesEncoder(EncodeMaterials materials){
|
||||
this.materials=materials;
|
||||
}
|
||||
public void encode(String type, String qualifiers, XMLDocument xmlDocument){
|
||||
XMLElement documentElement = xmlDocument.getDocumentElement();
|
||||
TypeBlock typeBlock = getTypeBlock(type, qualifiers);
|
||||
|
||||
int count = documentElement.getChildesCount();
|
||||
|
||||
typeBlock.getEntryArray().ensureSize(count);
|
||||
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement element = documentElement.getChildAt(i);
|
||||
encode(typeBlock, element);
|
||||
}
|
||||
}
|
||||
private void encode(TypeBlock typeBlock, XMLElement element){
|
||||
String name = element.getAttributeValue("name");
|
||||
int resourceId = getMaterials()
|
||||
.resolveLocalResourceId(typeBlock.getTypeName(), name);
|
||||
Entry entry = typeBlock
|
||||
.getOrCreateEntry((short) (0xffff & resourceId));
|
||||
|
||||
encodeValue(entry, element);
|
||||
|
||||
getMaterials().setEntryName(entry, name);
|
||||
}
|
||||
void encodeValue(Entry entry, XMLElement element){
|
||||
String value = getValue(element);
|
||||
encodeValue(entry, value);
|
||||
}
|
||||
void encodeValue(Entry entry, String value){
|
||||
if(EncodeUtil.isEmpty(value)){
|
||||
encodeNullValue(entry);
|
||||
}else if(isLiteralEmpty(value)){
|
||||
encodeLiteralEmptyValue(entry, value);
|
||||
}else if(isBoolean(value)){
|
||||
encodeBooleanValue(entry, value);
|
||||
}else if(ValueDecoder.isReference(value)){
|
||||
encodeReferenceValue(entry, value);
|
||||
}else {
|
||||
encodeStringValue(entry, value);
|
||||
}
|
||||
}
|
||||
void encodeNullValue(Entry entry){
|
||||
entry.setValueAsString("");
|
||||
}
|
||||
void encodeLiteralEmptyValue(Entry entry, String value){
|
||||
entry.setValueAsRaw(ValueType.NULL, 0);
|
||||
}
|
||||
void encodeBooleanValue(Entry entry, String value){
|
||||
entry.setValueAsBoolean("true".equals(value.toLowerCase()));
|
||||
}
|
||||
void encodeReferenceValue(Entry entry, String value){
|
||||
int resourceId = getMaterials().resolveReference(value);
|
||||
ValueType valueType;
|
||||
if(value.charAt(0) == '?'){
|
||||
valueType = ValueType.ATTRIBUTE;
|
||||
}else{
|
||||
valueType = ValueType.REFERENCE;
|
||||
}
|
||||
entry.setValueAsRaw(valueType, resourceId);
|
||||
}
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
|
||||
}
|
||||
private TypeBlock getTypeBlock(String type, String qualifiers){
|
||||
PackageBlock packageBlock = getMaterials().getCurrentPackage();
|
||||
return packageBlock.getOrCreateTypeBlock(qualifiers, type);
|
||||
}
|
||||
EncodeMaterials getMaterials() {
|
||||
return materials;
|
||||
}
|
||||
|
||||
|
||||
static String getValue(XMLElement element){
|
||||
String value=element.getTextContent();
|
||||
if(value!=null){
|
||||
return value;
|
||||
}
|
||||
return element.getAttributeValue("value");
|
||||
}
|
||||
static boolean isLiteralEmpty(String value){
|
||||
if(value==null){
|
||||
return false;
|
||||
}
|
||||
value=value.trim().toLowerCase();
|
||||
return value.equals("@empty");
|
||||
}
|
||||
static boolean isBoolean(String value){
|
||||
if(value==null){
|
||||
return false;
|
||||
}
|
||||
value=value.trim().toLowerCase();
|
||||
return value.equals("true")||value.equals("false");
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.apk.ApkUtil;
|
||||
import com.reandroid.arsc.array.ResValueMapArray;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.arsc.value.ResValueMap;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
class XMLValuesEncoderArray extends XMLValuesEncoderBag{
|
||||
XMLValuesEncoderArray(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeChildes(XMLElement parentElement, ResTableMapEntry mapEntry){
|
||||
int count = parentElement.getChildesCount();
|
||||
String tagName = parentElement.getTagName();
|
||||
boolean force_string = false;
|
||||
boolean force_integer = false;
|
||||
if(ApkUtil.TAG_STRING_ARRAY.equals(tagName)){
|
||||
force_string = true;
|
||||
}else if(ApkUtil.TAG_INTEGER_ARRAY.equals(tagName)){
|
||||
force_integer = true;
|
||||
}
|
||||
EncodeMaterials materials = getMaterials();
|
||||
ResValueMapArray itemArray = mapEntry.getValue();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement child=parentElement.getChildAt(i);
|
||||
|
||||
ResValueMap bagItem = itemArray.get(i);
|
||||
String name = child.getAttributeValue("name");
|
||||
if(name == null){
|
||||
bagItem.setName(0x01000001 + i);
|
||||
}else {
|
||||
Integer unknown = decodeUnknownAttributeHex(name);
|
||||
int resourceId;
|
||||
if(unknown == null){
|
||||
resourceId = materials.resolveLocalResourceId("id", name);
|
||||
}else {
|
||||
resourceId = unknown;
|
||||
}
|
||||
bagItem.setName(resourceId);
|
||||
}
|
||||
|
||||
|
||||
String valueText=child.getTextContent();
|
||||
if(ValueDecoder.isReference(valueText)){
|
||||
ValueType valueType;
|
||||
if(valueText.charAt(0) == '?'){
|
||||
valueType = ValueType.ATTRIBUTE;
|
||||
}else {
|
||||
valueType = ValueType.REFERENCE;
|
||||
}
|
||||
bagItem.setTypeAndData(valueType,
|
||||
getMaterials().resolveReference(valueText));
|
||||
}else if(force_string){
|
||||
bagItem.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
}else if(force_integer){
|
||||
valueText=trimText(valueText);
|
||||
if(!ValueDecoder.isInteger(valueText)){
|
||||
throw new EncodeException("Invalid integer value for array name="
|
||||
+parentElement.getAttributeValue("name")
|
||||
+", entry no"+(i+1)+", near line: " + child.getLineNumber());
|
||||
}
|
||||
bagItem.setTypeAndData(ValueType.INT_DEC,
|
||||
ValueDecoder.parseInteger(valueText));
|
||||
}else if(EncodeUtil.isEmpty(valueText)) {
|
||||
bagItem.setTypeAndData(ValueType.NULL, 0);
|
||||
}else {
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeGuessAny(valueText);
|
||||
if(encodeResult!=null){
|
||||
bagItem.setTypeAndData(encodeResult.valueType,
|
||||
encodeResult.value);
|
||||
}else {
|
||||
bagItem.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static String trimText(String text){
|
||||
if(text==null){
|
||||
return null;
|
||||
}
|
||||
return text.trim();
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.array.ResValueMapArray;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.arsc.value.attribute.AttributeBag;
|
||||
import com.reandroid.xml.XMLAttribute;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
class XMLValuesEncoderAttr extends XMLValuesEncoderBag{
|
||||
XMLValuesEncoderAttr(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
int getChildesCount(XMLElement element){
|
||||
int count = element.getChildesCount() + element.getAttributeCount();
|
||||
if(element.getAttribute("formats")!=null){
|
||||
count = count-1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@Override
|
||||
void encodeChildes(XMLElement parentElement, ResTableMapEntry mapEntry){
|
||||
encodeAttributes(parentElement, mapEntry);
|
||||
encodeEnumOrFlag(parentElement, mapEntry);
|
||||
EntryHeaderMap header = mapEntry.getHeader();
|
||||
boolean is_public = !mapEntry.getParentEntry()
|
||||
.getTypeName().contains("private");
|
||||
header.setPublic(is_public);
|
||||
}
|
||||
private void encodeAttributes(XMLElement parentElement, ResTableMapEntry mapEntry){
|
||||
ResValueMapArray mapArray = mapEntry.getValue();
|
||||
|
||||
int bagIndex = 0;
|
||||
ResValueMap formatItem = mapArray.get(bagIndex);
|
||||
formatItem.setValueType(ValueType.INT_DEC);
|
||||
|
||||
AttributeType typeFormats = AttributeType.FORMATS;
|
||||
formatItem.setAttributeType(typeFormats);
|
||||
|
||||
formatItem.addAttributeTypeFormat(getFlagEnum(parentElement));
|
||||
|
||||
AttributeDataFormat[] formats = AttributeDataFormat.parseValueTypes(
|
||||
parentElement.getAttributeValue(typeFormats.getName()));
|
||||
|
||||
formatItem.addAttributeTypeFormats(formats);
|
||||
|
||||
bagIndex++;
|
||||
|
||||
for(XMLAttribute attribute : parentElement.listAttributes()){
|
||||
String name = attribute.getName();
|
||||
if("name".equals(name) || "formats".equals(name)){
|
||||
continue;
|
||||
}
|
||||
AttributeType attributeType = AttributeType.fromName(name);
|
||||
if(attributeType == null){
|
||||
throw new EncodeException("Unknown attribute: '"+name
|
||||
+"', on attribute: " + attribute.toString() + ", element = "
|
||||
+ parentElement.getAttributeValue("name"));
|
||||
}
|
||||
ResValueMap bagItem = mapArray.get(bagIndex);
|
||||
bagItem.setAttributeType(attributeType);
|
||||
String valueString = attribute.getValue();
|
||||
if(!ValueDecoder.isHex(valueString) && !ValueDecoder.isInteger(valueString)){
|
||||
throw new EncodeException("Expecting hex or integer value: '" + valueString
|
||||
+"', on attribute: " + name + ", element: "
|
||||
+ parentElement.getAttributeValue("name"));
|
||||
}
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeHexOrInt(attribute.getValue());
|
||||
bagItem.setTypeAndData(encodeResult.valueType, encodeResult.value);
|
||||
bagIndex++;
|
||||
}
|
||||
}
|
||||
private void encodeEnumOrFlag(XMLElement element, ResTableMapEntry mapEntry){
|
||||
int count = element.getChildesCount();
|
||||
if(count == 0){
|
||||
return;
|
||||
}
|
||||
ResValueMapArray mapArray = mapEntry.getValue();
|
||||
|
||||
int offset = element.getAttributeCount();
|
||||
if(element.getAttribute(AttributeType.FORMATS.getName()) != null){
|
||||
offset = offset - 1;
|
||||
}
|
||||
ResValueMap formatItem = mapArray.get(0);
|
||||
|
||||
AttributeDataFormat lastBagFormat = AttributeDataFormat.typeOfBag(
|
||||
formatItem.getData());
|
||||
|
||||
for(int i = 0; i < count; i++){
|
||||
XMLElement child = element.getChildAt(i);
|
||||
AttributeDataFormat bagFormat = AttributeDataFormat.fromBagTypeName(child.getTagName());
|
||||
if(bagFormat != lastBagFormat){
|
||||
formatItem.addAttributeTypeFormat(bagFormat);
|
||||
lastBagFormat = bagFormat;
|
||||
}
|
||||
String name = child.getAttributeValue("name");
|
||||
int resourceId = decodeNameResourceId(name);
|
||||
|
||||
ResValueMap valueMap = mapArray.get(i + offset);
|
||||
valueMap.setName(resourceId);
|
||||
|
||||
String valueString = child.getTextContent();
|
||||
|
||||
if(!ValueDecoder.isHex(valueString) && !ValueDecoder.isInteger(valueString)){
|
||||
throw new EncodeException("Expecting hex or integer value: '" + valueString
|
||||
+"', on element: " + child.toText());
|
||||
}
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeHexOrInt(child.getTextContent());
|
||||
|
||||
valueMap.setTypeAndData(encodeResult.valueType, encodeResult.value);
|
||||
}
|
||||
}
|
||||
private int decodeNameResourceId(String name){
|
||||
Integer unknown = decodeUnknownAttributeHex(name);
|
||||
int resourceId;
|
||||
if(unknown == null){
|
||||
resourceId = getMaterials().resolveLocalResourceId("id", name);
|
||||
}else {
|
||||
resourceId = unknown;
|
||||
}
|
||||
return resourceId;
|
||||
}
|
||||
private void encodeEnumOrFlagOld(XMLElement element, ResTableMapEntry mapEntry){
|
||||
int count = element.getChildesCount();
|
||||
if(count == 0){
|
||||
return;
|
||||
}
|
||||
int offset = element.getAttributeCount();
|
||||
if(element.getAttribute("formats")!=null){
|
||||
offset = offset-1;
|
||||
}
|
||||
|
||||
EncodeMaterials materials = getMaterials();
|
||||
ResValueMapArray mapArray = mapEntry.getValue();
|
||||
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement child=element.getChildAt(i);
|
||||
|
||||
String name = child.getAttributeValue("name");
|
||||
Integer unknown = decodeUnknownAttributeHex(name);
|
||||
int resourceId;
|
||||
if(unknown == null){
|
||||
resourceId = materials.resolveLocalResourceId("id",
|
||||
name);
|
||||
}else {
|
||||
resourceId = unknown;
|
||||
}
|
||||
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeHexOrInt(child.getTextContent());
|
||||
|
||||
if(encodeResult == null){
|
||||
throw new EncodeException("Unknown value for element '"+child.toText()+"'");
|
||||
}
|
||||
|
||||
ResValueMap bagItem = mapArray.get(i+offset);
|
||||
bagItem.setName(resourceId);
|
||||
bagItem.setValueType(encodeResult.valueType);
|
||||
bagItem.setData(encodeResult.value);
|
||||
}
|
||||
}
|
||||
private AttributeDataFormat getFlagEnum(XMLElement parent){
|
||||
if(parent.getChildesCount() == 0){
|
||||
return null;
|
||||
}
|
||||
return AttributeDataFormat.fromBagTypeName(
|
||||
parent.getChildAt(0).getTagName());
|
||||
}
|
||||
private short getChildTypes(XMLElement parent){
|
||||
if(parent.getChildesCount()==0){
|
||||
return 0;
|
||||
}
|
||||
String tagName=parent.getChildAt(0).getTagName();
|
||||
if("enum".equals(tagName)){
|
||||
return AttributeBag.TYPE_ENUM;
|
||||
}
|
||||
if("flag".equals(tagName)){
|
||||
return AttributeBag.TYPE_FLAG;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
private boolean isFlag(XMLElement parent){
|
||||
if(parent.getChildesCount()==0){
|
||||
return false;
|
||||
}
|
||||
String tagName=parent.getChildAt(0).getTagName();
|
||||
return "flag".equals(tagName);
|
||||
}
|
||||
private boolean isEnum(XMLElement parent){
|
||||
if(parent.getChildesCount()==0){
|
||||
return false;
|
||||
}
|
||||
String tagName=parent.getChildAt(0).getTagName();
|
||||
return "enum".equals(tagName);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
class XMLValuesEncoderBag extends XMLValuesEncoder{
|
||||
XMLValuesEncoderBag(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
final void encodeValue(Entry entry, XMLElement element){
|
||||
if(encodeIfReference(entry, element)){
|
||||
return;
|
||||
}
|
||||
entry.ensureComplex(true);
|
||||
ResTableMapEntry tableMapEntry = (ResTableMapEntry) entry.getTableEntry();
|
||||
String parent = element.getAttributeValue("parent");
|
||||
if(!EncodeUtil.isEmpty(parent)){
|
||||
int parentId = getMaterials().resolveReference(parent);
|
||||
tableMapEntry.setParentId(parentId);
|
||||
}
|
||||
tableMapEntry.setValuesCount(getChildesCount(element));
|
||||
encodeChildes(element, tableMapEntry);
|
||||
}
|
||||
private boolean encodeIfReference(Entry entry, XMLElement element){
|
||||
if(element.hasChildElements()
|
||||
|| !element.hasTextContent()
|
||||
|| element.getAttributeCount() > 1){
|
||||
return false;
|
||||
}
|
||||
String text = element.getTextContent();
|
||||
if(!ValueDecoder.isReference(text)){
|
||||
return false;
|
||||
}
|
||||
encodeReferenceValue(entry, text);
|
||||
return true;
|
||||
}
|
||||
void encodeChildes(XMLElement element, ResTableMapEntry mapEntry){
|
||||
throw new EncodeException("Unimplemented bag type encoder: "
|
||||
+element.getTagName());
|
||||
|
||||
}
|
||||
int getChildesCount(XMLElement element){
|
||||
return element.getChildesCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
void encodeNullValue(Entry entry){
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
Integer decodeUnknownAttributeHex(String name){
|
||||
if(name.length() == 0 || (name.charAt(0) !='@' && name.charAt(0) != '?')){
|
||||
return null;
|
||||
}
|
||||
name = name.substring(1);
|
||||
if(!ValueDecoder.isHex(name)){
|
||||
return null;
|
||||
}
|
||||
return ValueDecoder.parseHex(name);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
|
||||
class XMLValuesEncoderColor extends XMLValuesEncoder{
|
||||
XMLValuesEncoderColor(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeColor(value);
|
||||
if(encodeResult!=null){
|
||||
entry.setValueAsRaw(encodeResult.valueType, encodeResult.value);
|
||||
}else {
|
||||
// If reaches here the value might be
|
||||
// file path e.g. res/color/something.xml
|
||||
entry.setValueAsString(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
|
||||
class XMLValuesEncoderCommon extends XMLValuesEncoder{
|
||||
XMLValuesEncoderCommon(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
if(ValueDecoder.isReference(value)){
|
||||
entry.setValueAsReference(getMaterials().resolveReference(value));
|
||||
}else {
|
||||
ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeGuessAny(value);
|
||||
if(encodeResult!=null){
|
||||
entry.setValueAsRaw(encodeResult.valueType, encodeResult.value);
|
||||
}else {
|
||||
entry.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
|
||||
class XMLValuesEncoderDimen extends XMLValuesEncoder{
|
||||
XMLValuesEncoderDimen(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
ValueDecoder.EncodeResult encodeResult =
|
||||
ValueDecoder.encodeDimensionOrFloat(value);
|
||||
if(encodeResult==null){
|
||||
encodeResult=ValueDecoder.encodeHexOrInt(value);
|
||||
}
|
||||
if(encodeResult!=null){
|
||||
entry.setValueAsRaw(encodeResult.valueType, encodeResult.value);
|
||||
}else {
|
||||
getMaterials().logMessage("Encoding as string dimen value: "+value);
|
||||
entry.setValueAsString(value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.ValueHeader;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
|
||||
class XMLValuesEncoderId extends XMLValuesEncoder{
|
||||
public XMLValuesEncoderId(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
entry.setValueAsBoolean(false);
|
||||
setVisibility(entry);
|
||||
}
|
||||
@Override
|
||||
void encodeNullValue(Entry entry){
|
||||
entry.setValueAsBoolean(false);
|
||||
setVisibility(entry);
|
||||
}
|
||||
@Override
|
||||
void encodeBooleanValue(Entry entry, String value){
|
||||
super.encodeBooleanValue(entry, value);
|
||||
setVisibility(entry);
|
||||
}
|
||||
private void setVisibility(Entry entry){
|
||||
ValueHeader valueHeader = entry.getHeader();
|
||||
valueHeader.setWeak(true);
|
||||
valueHeader.setPublic(true);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
|
||||
class XMLValuesEncoderInteger extends XMLValuesEncoder{
|
||||
XMLValuesEncoderInteger(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
value=value.trim();
|
||||
if(ValueDecoder.isInteger(value)){
|
||||
entry.setValueAsRaw(ValueType.INT_DEC, ValueDecoder.parseInteger(value));
|
||||
}else if(ValueDecoder.isHex(value)){
|
||||
entry.setValueAsRaw(ValueType.INT_HEX, ValueDecoder.parseHex(value));
|
||||
}else {
|
||||
ValueDecoder.EncodeResult encodeResult=ValueDecoder.encodeDimensionOrFloat(value);
|
||||
if(encodeResult!=null){
|
||||
entry.setValueAsRaw(encodeResult.valueType, encodeResult.value);
|
||||
}else {
|
||||
throw new EncodeException("Unknown value for type <integer>: '"+value+"'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.array.ResValueMapArray;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.AttributeType;
|
||||
import com.reandroid.arsc.value.ResTableMapEntry;
|
||||
import com.reandroid.arsc.value.ResValueMap;
|
||||
import com.reandroid.arsc.value.ValueType;
|
||||
import com.reandroid.arsc.value.plurals.PluralsQuantity;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
class XMLValuesEncoderPlurals extends XMLValuesEncoderBag{
|
||||
XMLValuesEncoderPlurals(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeChildes(XMLElement parentElement, ResTableMapEntry resValueBag){
|
||||
int count = parentElement.getChildesCount();
|
||||
ResValueMapArray itemArray = resValueBag.getValue();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement child=parentElement.getChildAt(i);
|
||||
ResValueMap bagItem = itemArray.get(i);
|
||||
AttributeType quantity = AttributeType
|
||||
.fromName(child.getAttributeValue("quantity"));
|
||||
if(quantity==null){
|
||||
throw new EncodeException("Unknown plurals quantity: "
|
||||
+ child.toText());
|
||||
}
|
||||
bagItem.setName(quantity.getId());
|
||||
|
||||
String valueText=child.getTextContent();
|
||||
|
||||
if(ValueDecoder.isReference(valueText)){
|
||||
bagItem.setValueType(ValueType.REFERENCE);
|
||||
bagItem.setData(getMaterials().resolveReference(valueText));
|
||||
}else if(EncodeUtil.isEmpty(valueText)) {
|
||||
bagItem.setValueAsString("");
|
||||
}else{
|
||||
bagItem.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.Entry;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
import com.reandroid.xml.XMLSpannable;
|
||||
|
||||
class XMLValuesEncoderString extends XMLValuesEncoder{
|
||||
XMLValuesEncoderString(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
|
||||
@Override
|
||||
void encodeValue(Entry entry, XMLElement element){
|
||||
if(!element.hasChildElements()){
|
||||
super.encodeValue(entry, element);
|
||||
return;
|
||||
}
|
||||
encodeStyledString(entry, element);
|
||||
}
|
||||
@Override
|
||||
void encodeStringValue(Entry entry, String value){
|
||||
value = ValueDecoder.unQuoteWhitespace(value);
|
||||
value = ValueDecoder.unEscapeSpecialCharacter(value);
|
||||
entry.setValueAsString(value);
|
||||
}
|
||||
@Override
|
||||
void encodeNullValue(Entry entry){
|
||||
entry.setValueAsString("");
|
||||
}
|
||||
@Override
|
||||
void encodeBooleanValue(Entry entry, String value){
|
||||
entry.setValueAsString(value);
|
||||
}
|
||||
private void encodeStyledString(Entry entry, XMLElement element){
|
||||
XMLSpannable xmlSpannable = new XMLSpannable(element);
|
||||
entry.setValueAsString(xmlSpannable.getXml());
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.apk.xmlencoder;
|
||||
|
||||
import com.reandroid.arsc.array.ResValueMapArray;
|
||||
import com.reandroid.arsc.decoder.ValueDecoder;
|
||||
import com.reandroid.arsc.value.*;
|
||||
import com.reandroid.arsc.value.attribute.AttributeBag;
|
||||
import com.reandroid.xml.XMLElement;
|
||||
|
||||
|
||||
class XMLValuesEncoderStyle extends XMLValuesEncoderBag{
|
||||
XMLValuesEncoderStyle(EncodeMaterials materials) {
|
||||
super(materials);
|
||||
}
|
||||
@Override
|
||||
void encodeChildes(XMLElement parentElement, ResTableMapEntry resValueBag){
|
||||
int count = parentElement.getChildesCount();
|
||||
ResValueMapArray itemArray = resValueBag.getValue();
|
||||
for(int i=0;i<count;i++){
|
||||
XMLElement child=parentElement.getChildAt(i);
|
||||
ResValueMap item = itemArray.get(i);
|
||||
String name=child.getAttributeValue("name");
|
||||
Integer id = decodeUnknownAttributeHex(name);
|
||||
if(id != null){
|
||||
item.setName(id);
|
||||
String value = child.getTextContent();
|
||||
ValueDecoder.EncodeResult encodeResult = ValueDecoder.encodeNullReference(value);
|
||||
if(encodeResult!=null){
|
||||
item.setTypeAndData(encodeResult.valueType, encodeResult.value);
|
||||
}else if(ValueDecoder.isReference(value)){
|
||||
ValueType valueType;
|
||||
if(value.charAt(0) == '?'){
|
||||
valueType = ValueType.ATTRIBUTE;
|
||||
}else {
|
||||
valueType = ValueType.REFERENCE;
|
||||
}
|
||||
item.setTypeAndData(valueType,
|
||||
getMaterials().resolveReference(value));
|
||||
}else {
|
||||
encodeResult = ValueDecoder.encodeGuessAny(value);
|
||||
if(encodeResult!=null){
|
||||
item.setTypeAndData(encodeResult.valueType, encodeResult.value);
|
||||
}else {
|
||||
item.setValueAsString(value);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Entry attributeEntry=getMaterials()
|
||||
.getAttributeBlock(name);
|
||||
if(attributeEntry==null){
|
||||
throw new EncodeException("Unknown attribute name: '"+child.toText()
|
||||
+"', for style: "+parentElement.getAttributeValue("name"));
|
||||
}
|
||||
encodeChild(child, attributeEntry, item);
|
||||
}
|
||||
}
|
||||
private void encodeChild(XMLElement child, Entry attributeEntry, ResValueMap bagItem){
|
||||
|
||||
bagItem.setName(attributeEntry.getResourceId());
|
||||
ResTableMapEntry tableEntry = (ResTableMapEntry) attributeEntry.getTableEntry();
|
||||
AttributeBag attributeBag=AttributeBag
|
||||
.create(tableEntry.getValue());
|
||||
|
||||
String valueText=child.getTextContent();
|
||||
ValueDecoder.EncodeResult encodeEnumFlag =
|
||||
attributeBag.encodeEnumOrFlagValue(valueText);
|
||||
if(encodeEnumFlag!=null){
|
||||
bagItem.setTypeAndData(encodeEnumFlag.valueType, encodeEnumFlag.value);
|
||||
return;
|
||||
}
|
||||
ValueDecoder.EncodeResult encodeResult = ValueDecoder.encodeNullReference(valueText);
|
||||
if(encodeResult!=null){
|
||||
bagItem.setTypeAndData(encodeResult.valueType, encodeResult.value);
|
||||
return;
|
||||
}
|
||||
if(ValueDecoder.isReference(valueText)){
|
||||
if(valueText.startsWith("?")){
|
||||
bagItem.setValueType(ValueType.ATTRIBUTE);
|
||||
}else {
|
||||
bagItem.setValueType(ValueType.REFERENCE);
|
||||
}
|
||||
bagItem.setData(getMaterials().resolveReference(valueText));
|
||||
}else if(attributeBag.isEqualType(AttributeDataFormat.STRING)) {
|
||||
bagItem.setValueAsString(ValueDecoder
|
||||
.unEscapeSpecialCharacter(valueText));
|
||||
}else if(EncodeUtil.isEmpty(valueText)) {
|
||||
bagItem.setTypeAndData(ValueType.NULL, 0);
|
||||
}else{
|
||||
encodeResult = ValueDecoder.encodeGuessAny(valueText);
|
||||
if(encodeResult!=null){
|
||||
bagItem.setTypeAndData(encodeResult.valueType,
|
||||
encodeResult.value);
|
||||
}else {
|
||||
bagItem.setValueAsString(ValueDecoder.unEscapeSpecialCharacter(valueText));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class APKArchive extends ZipArchive {
|
||||
public APKArchive(Map<String, InputSource> entriesMap){
|
||||
super(entriesMap);
|
||||
}
|
||||
public APKArchive(){
|
||||
super();
|
||||
}
|
||||
|
||||
public void refresh(){
|
||||
List<InputSource> inputSourceList = listInputSources();
|
||||
applySort(inputSourceList);
|
||||
set(inputSourceList);
|
||||
}
|
||||
public void autoSortApkFiles(){
|
||||
List<InputSource> inputSourceList = listInputSources();
|
||||
autoSortApkFiles(inputSourceList);
|
||||
set(inputSourceList);
|
||||
}
|
||||
public long writeApk(File outApk) throws IOException{
|
||||
ZipSerializer serializer=new ZipSerializer(listInputSources());
|
||||
return serializer.writeZip(outApk);
|
||||
}
|
||||
public long writeApk(OutputStream outputStream) throws IOException{
|
||||
ZipSerializer serializer=new ZipSerializer(listInputSources());
|
||||
return serializer.writeZip(outputStream);
|
||||
}
|
||||
public static APKArchive loadZippedApk(File zipFile) throws IOException {
|
||||
return loadZippedApk(new ZipFile(zipFile));
|
||||
}
|
||||
public static APKArchive loadZippedApk(ZipFile zipFile) {
|
||||
Map<String, InputSource> entriesMap = InputSourceUtil.mapZipFileSources(zipFile);
|
||||
return new APKArchive(entriesMap);
|
||||
}
|
||||
public static void repackApk(File apkFile) throws IOException{
|
||||
APKArchive apkArchive =loadZippedApk(apkFile);
|
||||
apkArchive.writeApk(apkFile);
|
||||
}
|
||||
public static void applySort(List<InputSource> sourceList){
|
||||
Comparator<InputSource> cmp=new Comparator<InputSource>() {
|
||||
@Override
|
||||
public int compare(InputSource in1, InputSource in2) {
|
||||
return Integer.compare(in1.getSort(), in2.getSort());
|
||||
}
|
||||
};
|
||||
sourceList.sort(cmp);
|
||||
}
|
||||
public static void autoSortApkFiles(List<InputSource> sourceList){
|
||||
Comparator<InputSource> cmp=new Comparator<InputSource>() {
|
||||
@Override
|
||||
public int compare(InputSource in1, InputSource in2) {
|
||||
return getSortName(in1).compareTo(getSortName(in2));
|
||||
}
|
||||
};
|
||||
sourceList.sort(cmp);
|
||||
int i=0;
|
||||
for(InputSource inputSource:sourceList){
|
||||
inputSource.setSort(i);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
private static String getSortName(InputSource inputSource){
|
||||
String name=inputSource.getAlias();
|
||||
StringBuilder builder=new StringBuilder();
|
||||
if(name.equals("AndroidManifest.xml")){
|
||||
builder.append("0 ");
|
||||
}else if(name.startsWith("META-INF/")){
|
||||
builder.append("1 ");
|
||||
}else if(name.equals("resources.arsc")){
|
||||
builder.append("2 ");
|
||||
}else if(name.startsWith("classes")){
|
||||
builder.append("3 ");
|
||||
}else if(name.startsWith("res/")){
|
||||
builder.append("4 ");
|
||||
}else if(name.startsWith("lib/")){
|
||||
builder.append("5 ");
|
||||
}else if(name.startsWith("assets/")){
|
||||
builder.append("6 ");
|
||||
}else {
|
||||
builder.append("7 ");
|
||||
}
|
||||
builder.append(name.toLowerCase());
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class ByteInputSource extends InputSource {
|
||||
private byte[] inBytes;
|
||||
public ByteInputSource(byte[] inBytes, String name) {
|
||||
super(name);
|
||||
this.inBytes=inBytes;
|
||||
}
|
||||
@Override
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
byte[] bts=getBytes();
|
||||
outputStream.write(bts);
|
||||
return bts.length;
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
return new ByteArrayInputStream(getBytes());
|
||||
}
|
||||
public byte[] getBytes() {
|
||||
return inBytes;
|
||||
}
|
||||
@Override
|
||||
public void disposeInputSource(){
|
||||
inBytes=new byte[0];
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import com.reandroid.common.FileChannelInputStream;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class FileInputSource extends InputSource {
|
||||
private final File file;
|
||||
public FileInputSource(File file, String name){
|
||||
super(name);
|
||||
this.file=file;
|
||||
}
|
||||
@Override
|
||||
public byte[] getBytes(int length) throws IOException{
|
||||
return FileChannelInputStream.read(getFile(), length);
|
||||
}
|
||||
@Override
|
||||
public long getLength() {
|
||||
return getFile().length();
|
||||
}
|
||||
@Override
|
||||
public void close(InputStream inputStream) throws IOException {
|
||||
inputStream.close();
|
||||
}
|
||||
@Override
|
||||
public FileChannelInputStream openStream() throws IOException {
|
||||
return new FileChannelInputStream(this.file);
|
||||
}
|
||||
public File getFile(){
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public abstract class InputSource {
|
||||
private final String name;
|
||||
private String alias;
|
||||
private long mCrc;
|
||||
private long mLength;
|
||||
private int method = ZipEntry.DEFLATED;
|
||||
private int sort;
|
||||
public InputSource(String name){
|
||||
this.name = name;
|
||||
this.alias = InputSourceUtil.sanitize(name);
|
||||
}
|
||||
public byte[] getBytes(int length) throws IOException{
|
||||
InputStream inputStream = openStream();
|
||||
byte[] bytes = new byte[length];
|
||||
inputStream.read(bytes, 0, length);
|
||||
close(inputStream);
|
||||
return bytes;
|
||||
}
|
||||
public void disposeInputSource(){
|
||||
}
|
||||
public int getSort() {
|
||||
return sort;
|
||||
}
|
||||
public void setSort(int sort) {
|
||||
this.sort = sort;
|
||||
}
|
||||
public int getMethod() {
|
||||
return method;
|
||||
}
|
||||
public void setMethod(int method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getAlias(){
|
||||
if(alias!=null){
|
||||
return alias;
|
||||
}
|
||||
return getName();
|
||||
}
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
public void close(InputStream inputStream) throws IOException {
|
||||
inputStream.close();
|
||||
}
|
||||
public long write(OutputStream outputStream) throws IOException {
|
||||
return write(outputStream, openStream());
|
||||
}
|
||||
private long write(OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
long result=0;
|
||||
byte[] buffer=new byte[1024 * 1000];
|
||||
int len;
|
||||
while ((len=inputStream.read(buffer))>0){
|
||||
outputStream.write(buffer, 0, len);
|
||||
result+=len;
|
||||
}
|
||||
close(inputStream);
|
||||
return result;
|
||||
}
|
||||
public String getName(){
|
||||
return name;
|
||||
}
|
||||
public long getLength() throws IOException{
|
||||
if(mLength==0){
|
||||
calculateCrc();
|
||||
}
|
||||
return mLength;
|
||||
}
|
||||
public long getCrc() throws IOException{
|
||||
if(mCrc==0){
|
||||
calculateCrc();
|
||||
}
|
||||
return mCrc;
|
||||
}
|
||||
public abstract InputStream openStream() throws IOException;
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof InputSource)) {
|
||||
return false;
|
||||
}
|
||||
InputSource that = (InputSource) o;
|
||||
return getName().equals(that.getName());
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getName().hashCode();
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return getClass().getSimpleName()+": "+getName();
|
||||
}
|
||||
private void calculateCrc() throws IOException {
|
||||
InputStream inputStream=openStream();
|
||||
long length=0;
|
||||
CRC32 crc = new CRC32();
|
||||
int bytesRead;
|
||||
byte[] buffer = new byte[1024*64];
|
||||
while((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
crc.update(buffer, 0, bytesRead);
|
||||
length+=bytesRead;
|
||||
}
|
||||
close(inputStream);
|
||||
mCrc=crc.getValue();
|
||||
mLength=length;
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class InputSourceUtil {
|
||||
|
||||
public static String toRelative(File rootDir, File file){
|
||||
int len=rootDir.getAbsolutePath().length();
|
||||
String path=file.getAbsolutePath();
|
||||
path=path.substring(len);
|
||||
path=sanitize(path);
|
||||
return path;
|
||||
}
|
||||
public static String sanitize(String path){
|
||||
path=path.replace('\\', '/');
|
||||
while (path.startsWith("./")){
|
||||
path=path.substring(2);
|
||||
}
|
||||
while (path.startsWith("/")){
|
||||
path=path.substring(1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public static Map<String, InputSource> mapZipFileSources(ZipFile zipFile){
|
||||
Map<String, InputSource> results=new LinkedHashMap<>();
|
||||
Enumeration<? extends ZipEntry> entriesEnum = zipFile.entries();
|
||||
int i=0;
|
||||
while (entriesEnum.hasMoreElements()){
|
||||
ZipEntry zipEntry = entriesEnum.nextElement();
|
||||
if(zipEntry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry);
|
||||
source.setSort(i);
|
||||
source.setMethod(zipEntry.getMethod());
|
||||
results.put(source.getName(), source);
|
||||
i++;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public static Map<String, ByteInputSource> mapInputStreamAsBuffer(InputStream inputStream) throws IOException {
|
||||
Map<String, ByteInputSource> results = new LinkedHashMap<>();
|
||||
ZipInputStream zin = new ZipInputStream(inputStream);
|
||||
ZipEntry zipEntry;
|
||||
int i=0;
|
||||
while ((zipEntry=zin.getNextEntry())!=null){
|
||||
if(zipEntry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
byte[] buffer = loadBuffer(zin);
|
||||
String name = sanitize(zipEntry.getName());
|
||||
ByteInputSource source = new ByteInputSource(buffer, name);
|
||||
source.setSort(i);
|
||||
source.setMethod(zipEntry.getMethod());
|
||||
results.put(name, source);
|
||||
i++;
|
||||
}
|
||||
zin.close();
|
||||
return results;
|
||||
}
|
||||
private static byte[] loadBuffer(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buff=new byte[40960];
|
||||
int len;
|
||||
while((len=in.read(buff))>0){
|
||||
outputStream.write(buff, 0, len);
|
||||
}
|
||||
outputStream.close();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
public static List<InputSource> listZipFileSources(ZipFile zipFile){
|
||||
List<InputSource> results=new ArrayList<>();
|
||||
Enumeration<? extends ZipEntry> entriesEnum = zipFile.entries();
|
||||
int i=0;
|
||||
while (entriesEnum.hasMoreElements()){
|
||||
ZipEntry zipEntry = entriesEnum.nextElement();
|
||||
if(zipEntry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
ZipEntrySource source=new ZipEntrySource(zipFile, zipEntry);
|
||||
source.setSort(i);
|
||||
results.add(source);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
public static List<InputSource> listDirectory(File dir){
|
||||
List<InputSource> results=new ArrayList<>();
|
||||
recursiveDirectory(results, dir, dir);
|
||||
return results;
|
||||
}
|
||||
private static void recursiveDirectory(List<InputSource> results, File rootDir, File dir){
|
||||
if(dir.isFile()){
|
||||
String name;
|
||||
if(rootDir.equals(dir)){
|
||||
name=dir.getName();
|
||||
}else {
|
||||
name=toRelative(rootDir, dir);
|
||||
}
|
||||
results.add(new FileInputSource(dir, name));
|
||||
return;
|
||||
}
|
||||
File[] childFiles=dir.listFiles();
|
||||
if(childFiles==null){
|
||||
return;
|
||||
}
|
||||
for(File file:childFiles){
|
||||
recursiveDirectory(results, rootDir, file);
|
||||
}
|
||||
}
|
||||
public static List<String> sortString(List<String> stringList){
|
||||
Comparator<String> cmp=new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
};
|
||||
stringList.sort(cmp);
|
||||
return stringList;
|
||||
}
|
||||
|
||||
public static List<InputSource> sort(List<InputSource> sourceList){
|
||||
Comparator<InputSource> cmp=new Comparator<InputSource>() {
|
||||
@Override
|
||||
public int compare(InputSource in1, InputSource in2) {
|
||||
return Integer.compare(in1.getSort(), in2.getSort());
|
||||
}
|
||||
};
|
||||
sourceList.sort(cmp);
|
||||
return sourceList;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
public interface WriteInterceptor {
|
||||
InputSource onWriteArchive(InputSource inputSource);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
public interface WriteProgress {
|
||||
void onCompressFile(String path, int mode, long writtenBytes);
|
||||
}
|
|
@ -1,333 +0,0 @@
|
|||
/*
|
||||
This class is copied from "apksigner" and I couldn't find the
|
||||
original repo/author to credit here.
|
||||
*/
|
||||
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
|
||||
public class ZipAlign {
|
||||
private static final int ZIP_ENTRY_HEADER_LEN = 30;
|
||||
private static final int ZIP_ENTRY_VERSION = 20;
|
||||
private static final int ZIP_ENTRY_USES_DATA_DESCR = 0x0008;
|
||||
private static final int ZIP_ENTRY_DATA_DESCRIPTOR_LEN = 16;
|
||||
private static final int ALIGNMENT_4 = 4;
|
||||
private static final int ALIGNMENT_PAGE = 4096;
|
||||
|
||||
private static class XEntry {
|
||||
public final ZipEntry entry;
|
||||
public final long headerOffset;
|
||||
public final int flags;
|
||||
public final int padding;
|
||||
|
||||
public XEntry(ZipEntry entry, long headerOffset, int flags, int padding) {
|
||||
this.entry = entry;
|
||||
this.headerOffset = headerOffset;
|
||||
this.flags = flags;
|
||||
this.padding = padding;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class FilterOutputStreamEx extends FilterOutputStream {
|
||||
private long totalWritten = 0;
|
||||
public FilterOutputStreamEx(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
out.write(b);
|
||||
totalWritten += b.length;
|
||||
}
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
out.write(b, off, len);
|
||||
totalWritten += len;
|
||||
}
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
out.write(b);
|
||||
totalWritten += 1;
|
||||
}
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
}
|
||||
public void writeInt(long v) throws IOException {
|
||||
write((int) (v & 0xff));
|
||||
write((int) ((v >>> 8) & 0xff));
|
||||
write((int) ((v >>> 16) & 0xff));
|
||||
write((int) ((v >>> 24) & 0xff));
|
||||
}
|
||||
public void writeShort(int v) throws IOException {
|
||||
write((v) & 0xff);
|
||||
write((v >>> 8) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private File mInputFile;
|
||||
private int mAlignment;
|
||||
private File mOutputFile;
|
||||
private ZipFile mZipFile;
|
||||
private RandomAccessFile mRafInput;
|
||||
private FilterOutputStreamEx mOutputStream;
|
||||
private final List<XEntry> mXEntries = new ArrayList<>();
|
||||
private long mInputFileOffset = 0;
|
||||
private int mTotalPadding = 0;
|
||||
|
||||
public void zipAlign(File input, File output) throws IOException {
|
||||
zipAlign(input, output, ALIGNMENT_4);
|
||||
}
|
||||
public void zipAlign(File input, File output, int alignment) throws IOException {
|
||||
mInputFile = input;
|
||||
mAlignment = alignment;
|
||||
mOutputFile = output;
|
||||
openFiles();
|
||||
copyAllEntries();
|
||||
buildCentralDirectory();
|
||||
closeFiles();
|
||||
}
|
||||
private void openFiles() throws IOException {
|
||||
mZipFile = new ZipFile(mInputFile);
|
||||
mRafInput = new RandomAccessFile(mInputFile, "r");
|
||||
mOutputStream = new FilterOutputStreamEx(new BufferedOutputStream(new FileOutputStream(mOutputFile), 32 * 1024));
|
||||
}
|
||||
private void copyAllEntries() throws IOException {
|
||||
final int entryCount = mZipFile.size();
|
||||
if (entryCount == 0) {
|
||||
return;
|
||||
}
|
||||
final Enumeration<?> entries = mZipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
final ZipEntry entry = (ZipEntry) entries.nextElement();
|
||||
final String name = entry.getName();
|
||||
|
||||
int flags = entry.getMethod() == ZipEntry.STORED ? 0 : 1 << 3;
|
||||
flags |= 1 << 11;
|
||||
|
||||
final long outputEntryHeaderOffset = mOutputStream.totalWritten;
|
||||
|
||||
final int inputEntryHeaderSize = ZIP_ENTRY_HEADER_LEN + (entry.getExtra() != null ? entry.getExtra().length : 0)
|
||||
+ name.getBytes(StandardCharsets.UTF_8).length;
|
||||
final long inputEntryDataOffset = mInputFileOffset + inputEntryHeaderSize;
|
||||
|
||||
final int padding;
|
||||
|
||||
if (entry.getMethod() != ZipEntry.STORED) {
|
||||
padding = 0;
|
||||
} else {
|
||||
int alignment = mAlignment;
|
||||
if (name.startsWith("lib/") && name.endsWith(".so")) {
|
||||
alignment = ALIGNMENT_PAGE;
|
||||
}
|
||||
long newOffset = inputEntryDataOffset + mTotalPadding;
|
||||
padding = (int) ((alignment - (newOffset % alignment)) % alignment);
|
||||
mTotalPadding += padding;
|
||||
}
|
||||
|
||||
final XEntry xentry = new XEntry(entry, outputEntryHeaderOffset, flags, padding);
|
||||
mXEntries.add(xentry);
|
||||
byte[] extra = entry.getExtra();
|
||||
if (extra == null) {
|
||||
extra = new byte[padding];
|
||||
} else {
|
||||
byte[] newExtra = new byte[extra.length + padding];
|
||||
System.arraycopy(extra, 0, newExtra, 0, extra.length);
|
||||
Arrays.fill(newExtra, extra.length, newExtra.length, (byte) 0);
|
||||
extra = newExtra;
|
||||
}
|
||||
entry.setExtra(extra);
|
||||
mOutputStream.writeInt(ZipOutputStream.LOCSIG);
|
||||
mOutputStream.writeShort(ZIP_ENTRY_VERSION);
|
||||
mOutputStream.writeShort(flags);
|
||||
mOutputStream.writeShort(entry.getMethod());
|
||||
|
||||
int modDate;
|
||||
int time;
|
||||
GregorianCalendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date(entry.getTime()));
|
||||
int year = cal.get(Calendar.YEAR);
|
||||
if (year < 1980) {
|
||||
modDate = 0x21;
|
||||
time = 0;
|
||||
} else {
|
||||
modDate = cal.get(Calendar.DATE);
|
||||
modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate;
|
||||
modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate;
|
||||
time = cal.get(Calendar.SECOND) >> 1;
|
||||
time = (cal.get(Calendar.MINUTE) << 5) | time;
|
||||
time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
|
||||
}
|
||||
|
||||
mOutputStream.writeShort(time);
|
||||
mOutputStream.writeShort(modDate);
|
||||
|
||||
mOutputStream.writeInt(entry.getCrc());
|
||||
mOutputStream.writeInt(entry.getCompressedSize());
|
||||
mOutputStream.writeInt(entry.getSize());
|
||||
|
||||
mOutputStream.writeShort(entry.getName().getBytes(StandardCharsets.UTF_8).length);
|
||||
mOutputStream.writeShort(entry.getExtra().length);
|
||||
mOutputStream.write(entry.getName().getBytes(StandardCharsets.UTF_8));
|
||||
mOutputStream.write(entry.getExtra(), 0, entry.getExtra().length);
|
||||
|
||||
mInputFileOffset += inputEntryHeaderSize;
|
||||
|
||||
final long sizeToCopy;
|
||||
if ((flags & ZIP_ENTRY_USES_DATA_DESCR) != 0) {
|
||||
sizeToCopy = (entry.isDirectory() ? 0 : entry.getCompressedSize()) + ZIP_ENTRY_DATA_DESCRIPTOR_LEN;
|
||||
} else {
|
||||
sizeToCopy = entry.isDirectory() ? 0 : entry.getCompressedSize();
|
||||
}
|
||||
|
||||
if (sizeToCopy > 0) {
|
||||
mRafInput.seek(mInputFileOffset);
|
||||
|
||||
long totalSizeCopied = 0;
|
||||
final byte[] buf = new byte[32 * 1024];
|
||||
while (totalSizeCopied < sizeToCopy) {
|
||||
int read = mRafInput.read(buf, 0, (int) Math.min(32 * 1024, sizeToCopy - totalSizeCopied));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
mOutputStream.write(buf, 0, read);
|
||||
totalSizeCopied += read;
|
||||
}
|
||||
}
|
||||
|
||||
mInputFileOffset += sizeToCopy;
|
||||
}
|
||||
}
|
||||
|
||||
private void buildCentralDirectory() throws IOException {
|
||||
final long centralDirOffset = mOutputStream.totalWritten;
|
||||
final int entryCount = mXEntries.size();
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
XEntry xentry = mXEntries.get(i);
|
||||
final ZipEntry entry = xentry.entry;
|
||||
int modDate;
|
||||
int time;
|
||||
GregorianCalendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date(entry.getTime()));
|
||||
int year = cal.get(Calendar.YEAR);
|
||||
if (year < 1980) {
|
||||
modDate = 0x21;
|
||||
time = 0;
|
||||
} else {
|
||||
modDate = cal.get(Calendar.DATE);
|
||||
modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate;
|
||||
modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate;
|
||||
time = cal.get(Calendar.SECOND) >> 1;
|
||||
time = (cal.get(Calendar.MINUTE) << 5) | time;
|
||||
time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
|
||||
}
|
||||
|
||||
mOutputStream.writeInt(ZipFile.CENSIG); // CEN header signature
|
||||
mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version made by
|
||||
mOutputStream.writeShort(ZIP_ENTRY_VERSION); // version needed to extract
|
||||
mOutputStream.writeShort(xentry.flags); // general purpose bit flag
|
||||
mOutputStream.writeShort(entry.getMethod()); // compression method
|
||||
mOutputStream.writeShort(time);
|
||||
mOutputStream.writeShort(modDate);
|
||||
mOutputStream.writeInt(entry.getCrc()); // crc-32
|
||||
mOutputStream.writeInt(entry.getCompressedSize()); // compressed size
|
||||
mOutputStream.writeInt(entry.getSize()); // uncompressed size
|
||||
final byte[] nameBytes = entry.getName().getBytes(StandardCharsets.UTF_8);
|
||||
mOutputStream.writeShort(nameBytes.length);
|
||||
mOutputStream.writeShort(entry.getExtra() != null ? entry.getExtra().length - xentry.padding : 0);
|
||||
final byte[] commentBytes;
|
||||
if (entry.getComment() != null) {
|
||||
commentBytes = entry.getComment().getBytes(StandardCharsets.UTF_8);
|
||||
mOutputStream.writeShort(Math.min(commentBytes.length, 0xffff));
|
||||
} else {
|
||||
commentBytes = null;
|
||||
mOutputStream.writeShort(0);
|
||||
}
|
||||
mOutputStream.writeShort(0); // starting disk number
|
||||
mOutputStream.writeShort(0); // internal file attributes (unused)
|
||||
mOutputStream.writeInt(0); // external file attributes (unused)
|
||||
mOutputStream.writeInt(xentry.headerOffset); // relative offset of local header
|
||||
mOutputStream.write(nameBytes);
|
||||
if (entry.getExtra() != null) {
|
||||
mOutputStream.write(entry.getExtra(), 0, entry.getExtra().length - xentry.padding);
|
||||
}
|
||||
if (commentBytes != null) {
|
||||
mOutputStream.write(commentBytes, 0, Math.min(commentBytes.length, 0xffff));
|
||||
}
|
||||
}
|
||||
final long centralDirSize = mOutputStream.totalWritten - centralDirOffset;
|
||||
|
||||
mOutputStream.writeInt(ZipFile.ENDSIG); // END record signature
|
||||
mOutputStream.writeShort(0); // number of this disk
|
||||
mOutputStream.writeShort(0); // central directory start disk
|
||||
mOutputStream.writeShort(entryCount); // number of directory entries on disk
|
||||
mOutputStream.writeShort(entryCount); // total number of directory entries
|
||||
mOutputStream.writeInt(centralDirSize); // length of central directory
|
||||
mOutputStream.writeInt(centralDirOffset); // offset of central directory
|
||||
mOutputStream.writeShort(0);
|
||||
mOutputStream.flush();
|
||||
}
|
||||
|
||||
private void closeFiles() throws IOException {
|
||||
try {
|
||||
mZipFile.close();
|
||||
} finally {
|
||||
try {
|
||||
mRafInput.close();
|
||||
} finally {
|
||||
mOutputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void align4(File inFile) throws IOException{
|
||||
align(inFile, ALIGNMENT_4);
|
||||
}
|
||||
public static void align4(File inFile, File outFile) throws IOException{
|
||||
align(inFile, outFile, ALIGNMENT_4);
|
||||
}
|
||||
public static void align(File inFile, int alignment) throws IOException{
|
||||
File tmp=toTmpFile(inFile);
|
||||
tmp.delete();
|
||||
align(inFile, tmp, alignment);
|
||||
inFile.delete();
|
||||
tmp.renameTo(inFile);
|
||||
}
|
||||
public static void align(File inFile, File outFile, int alignment) throws IOException{
|
||||
ZipAlign zipAlign=new ZipAlign();
|
||||
File dir=outFile.getParentFile();
|
||||
if(dir!=null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
zipAlign.zipAlign(inFile, outFile, alignment);
|
||||
}
|
||||
private static File toTmpFile(File file){
|
||||
String name=file.getName()+".align.tmp";
|
||||
File dir=file.getParentFile();
|
||||
if(dir==null){
|
||||
return new File(name);
|
||||
}
|
||||
return new File(dir, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ZipArchive {
|
||||
private final Map<String, InputSource> mEntriesMap;
|
||||
public ZipArchive(Map<String, InputSource> entriesMap){
|
||||
this.mEntriesMap=entriesMap;
|
||||
}
|
||||
public ZipArchive(){
|
||||
this(new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return mEntriesMap.size();
|
||||
}
|
||||
public void extract(File outDir) throws IOException {
|
||||
for(InputSource inputSource:listInputSources()){
|
||||
extract(outDir, inputSource);
|
||||
}
|
||||
}
|
||||
private void extract(File outDir, InputSource inputSource) throws IOException {
|
||||
File file=toOutFile(outDir, inputSource.getAlias());
|
||||
File dir=file.getParentFile();
|
||||
if(dir!=null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream=new FileOutputStream(file);
|
||||
inputSource.write(outputStream);
|
||||
outputStream.close();
|
||||
inputSource.disposeInputSource();
|
||||
}
|
||||
private File toOutFile(File outDir, String path){
|
||||
path=path.replace('/', File.separatorChar);
|
||||
return new File(outDir, path);
|
||||
}
|
||||
public void removeDir(String dirName){
|
||||
if(!dirName.endsWith("/")){
|
||||
dirName=dirName+"/";
|
||||
}
|
||||
for(InputSource inputSource:listInputSources()){
|
||||
if(inputSource.getName().startsWith(dirName)){
|
||||
remove(inputSource.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
public void removeAll(Pattern patternAlias){
|
||||
for(InputSource inputSource:listInputSources()){
|
||||
Matcher matcher = patternAlias.matcher(inputSource.getAlias());
|
||||
if(matcher.matches()){
|
||||
mEntriesMap.remove(inputSource.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
public void clear(){
|
||||
mEntriesMap.clear();
|
||||
}
|
||||
public int entriesCount(){
|
||||
return mEntriesMap.size();
|
||||
}
|
||||
public InputSource remove(String name){
|
||||
InputSource inputSource=mEntriesMap.remove(name);
|
||||
if(inputSource==null){
|
||||
return null;
|
||||
}
|
||||
return inputSource;
|
||||
}
|
||||
public void addArchive(File archiveFile) throws IOException {
|
||||
ZipFile zipFile=new ZipFile(archiveFile);
|
||||
add(zipFile);
|
||||
}
|
||||
public void addDirectory(File dir){
|
||||
addAll(InputSourceUtil.listDirectory(dir));
|
||||
}
|
||||
public void add(ZipFile zipFile){
|
||||
List<InputSource> sourceList = InputSourceUtil.listZipFileSources(zipFile);
|
||||
this.addAll(sourceList);
|
||||
}
|
||||
public void set(Collection<? extends InputSource> inputSourceList){
|
||||
clear();
|
||||
addAll(inputSourceList);
|
||||
}
|
||||
public void addAll(Collection<? extends InputSource> inputSourceList){
|
||||
for(InputSource inputSource:inputSourceList){
|
||||
add(inputSource);
|
||||
}
|
||||
}
|
||||
public void add(InputSource inputSource){
|
||||
if(inputSource==null){
|
||||
return;
|
||||
}
|
||||
String name=inputSource.getName();
|
||||
Map<String, InputSource> map=mEntriesMap;
|
||||
map.remove(name);
|
||||
map.put(name, inputSource);
|
||||
}
|
||||
public List<InputSource> listInputSources(){
|
||||
return new ArrayList<>(mEntriesMap.values());
|
||||
}
|
||||
public InputSource getInputSource(String name){
|
||||
return mEntriesMap.get(name);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ZipEntrySource extends InputSource {
|
||||
private final ZipFile zipFile;
|
||||
private final ZipEntry zipEntry;
|
||||
public ZipEntrySource(ZipFile zipFile, ZipEntry zipEntry){
|
||||
super(zipEntry.getName());
|
||||
this.zipFile=zipFile;
|
||||
this.zipEntry=zipEntry;
|
||||
super.setMethod(zipEntry.getMethod());
|
||||
}
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
return zipFile.getInputStream(zipEntry);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class ZipSerializer {
|
||||
private final List<InputSource> mSourceList;
|
||||
private WriteProgress writeProgress;
|
||||
private WriteInterceptor writeInterceptor;
|
||||
public ZipSerializer(List<InputSource> sourceList){
|
||||
this.mSourceList=sourceList;
|
||||
}
|
||||
|
||||
public void setWriteInterceptor(WriteInterceptor writeInterceptor) {
|
||||
this.writeInterceptor = writeInterceptor;
|
||||
}
|
||||
public void setWriteProgress(WriteProgress writeProgress){
|
||||
this.writeProgress=writeProgress;
|
||||
}
|
||||
public long writeZip(File outZip) throws IOException{
|
||||
File dir=outZip.getParentFile();
|
||||
if(dir!=null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
File tmp=toTmpFile(outZip);
|
||||
FileOutputStream fileOutputStream=new FileOutputStream(tmp);
|
||||
long length= writeZip(fileOutputStream);
|
||||
fileOutputStream.close();
|
||||
outZip.delete();
|
||||
tmp.renameTo(outZip);
|
||||
return length;
|
||||
}
|
||||
private File toTmpFile(File file){
|
||||
File dir=file.getParentFile();
|
||||
String name=file.getName()+".tmp";
|
||||
return new File(dir, name);
|
||||
}
|
||||
public long writeZip(OutputStream outputStream) throws IOException{
|
||||
long length=0;
|
||||
WriteProgress progress=writeProgress;
|
||||
ZipOutputStream zipOutputStream=new ZipOutputStream(outputStream);
|
||||
for(InputSource inputSource:mSourceList){
|
||||
inputSource = interceptWrite(inputSource);
|
||||
if(inputSource==null){
|
||||
continue;
|
||||
}
|
||||
if(progress!=null){
|
||||
progress.onCompressFile(inputSource.getAlias(), inputSource.getMethod(), length);
|
||||
}
|
||||
length+=write(zipOutputStream, inputSource);
|
||||
zipOutputStream.closeEntry();
|
||||
inputSource.disposeInputSource();
|
||||
}
|
||||
zipOutputStream.close();
|
||||
return length;
|
||||
}
|
||||
private long write(ZipOutputStream zipOutputStream, InputSource inputSource) throws IOException{
|
||||
ZipEntry zipEntry=createZipEntry(inputSource);
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
return inputSource.write(zipOutputStream);
|
||||
}
|
||||
private ZipEntry createZipEntry(InputSource inputSource) throws IOException {
|
||||
String name=inputSource.getAlias();
|
||||
ZipEntry zipEntry=new ZipEntry(name);
|
||||
int method = inputSource.getMethod();
|
||||
zipEntry.setMethod(method);
|
||||
if(method==ZipEntry.STORED){
|
||||
zipEntry.setCrc(inputSource.getCrc());
|
||||
zipEntry.setSize(inputSource.getLength());
|
||||
}
|
||||
return zipEntry;
|
||||
}
|
||||
private InputSource interceptWrite(InputSource inputSource){
|
||||
WriteInterceptor interceptor=writeInterceptor;
|
||||
if(interceptor!=null){
|
||||
return interceptor.onWriteArchive(inputSource);
|
||||
}
|
||||
return inputSource;
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2;
|
||||
|
||||
import com.reandroid.archive.APKArchive;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import com.reandroid.archive2.block.*;
|
||||
import com.reandroid.archive2.io.ArchiveEntrySource;
|
||||
import com.reandroid.archive2.io.ZipFileInput;
|
||||
import com.reandroid.archive2.io.ArchiveUtil;
|
||||
import com.reandroid.archive2.io.ZipInput;
|
||||
import com.reandroid.archive2.model.LocalFileDirectory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class Archive {
|
||||
private final ZipInput zipInput;
|
||||
private final List<ArchiveEntry> entryList;
|
||||
private final EndRecord endRecord;
|
||||
private final ApkSignatureBlock apkSignatureBlock;
|
||||
public Archive(ZipInput zipInput) throws IOException {
|
||||
this.zipInput = zipInput;
|
||||
LocalFileDirectory lfd = new LocalFileDirectory();
|
||||
lfd.visit(zipInput);
|
||||
List<LocalFileHeader> localFileHeaderList = lfd.getHeaderList();
|
||||
List<CentralEntryHeader> centralEntryHeaderList = lfd.getCentralFileDirectory().getHeaderList();
|
||||
List<ArchiveEntry> entryList = new ArrayList<>(localFileHeaderList.size());
|
||||
for(int i=0;i<localFileHeaderList.size();i++){
|
||||
LocalFileHeader lfh = localFileHeaderList.get(i);
|
||||
CentralEntryHeader ceh = centralEntryHeaderList.get(i);
|
||||
ArchiveEntry archiveEntry = new ArchiveEntry(lfh, ceh);
|
||||
if(archiveEntry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
entryList.add(archiveEntry);
|
||||
}
|
||||
this.entryList = entryList;
|
||||
this.endRecord = lfd.getCentralFileDirectory().getEndRecord();
|
||||
this.apkSignatureBlock = lfd.getApkSigBlock();
|
||||
}
|
||||
public Archive(File file) throws IOException {
|
||||
this(new ZipFileInput(file));
|
||||
}
|
||||
public APKArchive createAPKArchive(){
|
||||
return new APKArchive(mapEntrySource());
|
||||
}
|
||||
public Map<String, InputSource> mapEntrySource(){
|
||||
Map<String, InputSource> map = new LinkedHashMap<>();
|
||||
ZipInput zipInput = this.zipInput;
|
||||
List<ArchiveEntry> entryList = this.entryList;
|
||||
for(int i=0; i<entryList.size(); i++){
|
||||
ArchiveEntry entry = entryList.get(i);
|
||||
if(entry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
ArchiveEntrySource entrySource = new ArchiveEntrySource(zipInput, entry);
|
||||
map.put(entrySource.getAlias(), entrySource);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
public InputStream openRawInputStream(ArchiveEntry archiveEntry) throws IOException {
|
||||
return zipInput.getInputStream(archiveEntry.getFileOffset(), archiveEntry.getDataSize());
|
||||
}
|
||||
public InputStream openInputStream(ArchiveEntry archiveEntry) throws IOException {
|
||||
InputStream rawInputStream = openRawInputStream(archiveEntry);
|
||||
if(archiveEntry.getMethod() == ZipEntry.STORED){
|
||||
return rawInputStream;
|
||||
}
|
||||
return new InflaterInputStream(rawInputStream,
|
||||
new Inflater(true), 1024*1000);
|
||||
}
|
||||
public List<ArchiveEntry> getEntryList() {
|
||||
return entryList;
|
||||
}
|
||||
|
||||
public ApkSignatureBlock getApkSignatureBlock() {
|
||||
return apkSignatureBlock;
|
||||
}
|
||||
public EndRecord getEndRecord() {
|
||||
return endRecord;
|
||||
}
|
||||
|
||||
// for test
|
||||
public void extract(File dir) throws IOException {
|
||||
for(ArchiveEntry archiveEntry:getEntryList()){
|
||||
if(archiveEntry.isDirectory()){
|
||||
continue;
|
||||
}
|
||||
extract(dir, archiveEntry);
|
||||
}
|
||||
}
|
||||
private void extract(File dir, ArchiveEntry archiveEntry) throws IOException{
|
||||
File out = toFile(dir, archiveEntry);
|
||||
File parent = out.getParentFile();
|
||||
if(!parent.exists()){
|
||||
parent.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream = new FileOutputStream(out);
|
||||
ArchiveUtil.writeAll(openInputStream(archiveEntry), outputStream);
|
||||
outputStream.close();
|
||||
}
|
||||
private File toFile(File dir, ArchiveEntry archiveEntry){
|
||||
String name = archiveEntry.getName().replace('/', File.separatorChar);
|
||||
return new File(dir, name);
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2;
|
||||
|
||||
import com.reandroid.archive2.block.CentralEntryHeader;
|
||||
import com.reandroid.archive2.block.LocalFileHeader;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public class ArchiveEntry extends ZipEntry {
|
||||
private final CentralEntryHeader centralEntryHeader;
|
||||
private final LocalFileHeader localFileHeader;
|
||||
public ArchiveEntry(LocalFileHeader lfh, CentralEntryHeader ceh){
|
||||
super(lfh.getFileName());
|
||||
this.localFileHeader = lfh;
|
||||
this.centralEntryHeader = ceh;
|
||||
}
|
||||
public ArchiveEntry(String name){
|
||||
this(new LocalFileHeader(name), new CentralEntryHeader(name));
|
||||
}
|
||||
public ArchiveEntry(){
|
||||
this(new LocalFileHeader(), new CentralEntryHeader());
|
||||
}
|
||||
|
||||
public long getDataSize(){
|
||||
if(getMethod() == ZipEntry.STORED){
|
||||
return getSize();
|
||||
}
|
||||
return getCompressedSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMethod(){
|
||||
return localFileHeader.getMethod();
|
||||
}
|
||||
@Override
|
||||
public void setMethod(int method){
|
||||
localFileHeader.setMethod(method);
|
||||
centralEntryHeader.setMethod(method);
|
||||
}
|
||||
@Override
|
||||
public long getSize() {
|
||||
return centralEntryHeader.getSize();
|
||||
}
|
||||
@Override
|
||||
public void setSize(long size) {
|
||||
centralEntryHeader.setSize(size);
|
||||
localFileHeader.setSize(size);
|
||||
}
|
||||
@Override
|
||||
public long getCrc() {
|
||||
return centralEntryHeader.getCrc();
|
||||
}
|
||||
@Override
|
||||
public void setCrc(long crc) {
|
||||
centralEntryHeader.setCrc(crc);
|
||||
localFileHeader.setCrc(crc);
|
||||
}
|
||||
@Override
|
||||
public long getCompressedSize() {
|
||||
return centralEntryHeader.getCompressedSize();
|
||||
}
|
||||
@Override
|
||||
public void setCompressedSize(long csize) {
|
||||
centralEntryHeader.setCompressedSize(csize);
|
||||
localFileHeader.setCompressedSize(csize);
|
||||
}
|
||||
public long getFileOffset() {
|
||||
return localFileHeader.getFileOffset();
|
||||
}
|
||||
@Override
|
||||
public String getName(){
|
||||
return centralEntryHeader.getFileName();
|
||||
}
|
||||
public void setName(String name){
|
||||
centralEntryHeader.setFileName(name);
|
||||
localFileHeader.setFileName(name);
|
||||
}
|
||||
@Override
|
||||
public String getComment(){
|
||||
return centralEntryHeader.getComment();
|
||||
}
|
||||
@Override
|
||||
public void setComment(String name){
|
||||
centralEntryHeader.setComment(name);
|
||||
}
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.getName().endsWith("/");
|
||||
}
|
||||
public CentralEntryHeader getCentralEntryHeader(){
|
||||
return centralEntryHeader;
|
||||
}
|
||||
public LocalFileHeader getLocalFileHeader() {
|
||||
return localFileHeader;
|
||||
}
|
||||
public boolean matches(CentralEntryHeader centralEntryHeader){
|
||||
if(centralEntryHeader==null){
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "["+ getFileOffset()+"] " + getName() + getComment()
|
||||
+ HexUtil.toHex(" 0x", getCrc(), 8);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2;
|
||||
|
||||
public enum ZipSignature {
|
||||
CENTRAL_FILE(0X02014B50),
|
||||
LOCAL_FILE(0X04034B50),
|
||||
DATA_DESCRIPTOR(0X08074B50),
|
||||
END_RECORD(0X06054B50);
|
||||
|
||||
private final int value;
|
||||
|
||||
ZipSignature(int value){
|
||||
this.value = value;
|
||||
}
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
public static ZipSignature valueOf(int value){
|
||||
for(ZipSignature signature:VALUES){
|
||||
if(value == signature.getValue()){
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static final ZipSignature[] VALUES = values();
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
|
||||
import com.reandroid.archive2.block.pad.SchemePadding;
|
||||
import com.reandroid.arsc.io.BlockReader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class ApkSignatureBlock extends LengthPrefixedList<SignatureInfo>
|
||||
implements Comparator<SignatureInfo> {
|
||||
public ApkSignatureBlock(SignatureFooter signatureFooter){
|
||||
super(true);
|
||||
setBottomBlock(signatureFooter);
|
||||
}
|
||||
public ApkSignatureBlock(){
|
||||
this(new SignatureFooter());
|
||||
}
|
||||
public List<SignatureInfo> getSignatures(){
|
||||
return super.getElements();
|
||||
}
|
||||
public int countSignatures(){
|
||||
return super.getElementsCount();
|
||||
}
|
||||
public void sortSignatures(){
|
||||
sort(this);
|
||||
}
|
||||
public void updatePadding(){
|
||||
SchemePadding schemePadding = getOrCreateSchemePadding();
|
||||
schemePadding.setPadding(0);
|
||||
sortSignatures();
|
||||
refresh();
|
||||
int size = countBytes();
|
||||
int alignment = 4096;
|
||||
int padding = (alignment - (size % alignment)) % alignment;
|
||||
schemePadding.setPadding(padding);
|
||||
refresh();
|
||||
}
|
||||
private SchemePadding getOrCreateSchemePadding(){
|
||||
SignatureInfo signatureInfo = getSignature(SignatureId.PADDING);
|
||||
if(signatureInfo == null){
|
||||
signatureInfo = new SignatureInfo();
|
||||
signatureInfo.setId(SignatureId.PADDING);
|
||||
signatureInfo.setSignatureScheme(new SchemePadding());
|
||||
add(signatureInfo);
|
||||
}
|
||||
SignatureScheme scheme = signatureInfo.getSignatureScheme();
|
||||
if(!(scheme instanceof SchemePadding)){
|
||||
scheme = new SchemePadding();
|
||||
signatureInfo.setSignatureScheme(scheme);
|
||||
}
|
||||
return (SchemePadding) scheme;
|
||||
}
|
||||
public SignatureInfo getSignature(SignatureId signatureId){
|
||||
for(SignatureInfo signatureInfo:getSignatures()){
|
||||
if(signatureInfo.getId().equals(signatureId)){
|
||||
return signatureInfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public SignatureFooter getSignatureFooter(){
|
||||
return (SignatureFooter) getBottomBlock();
|
||||
}
|
||||
@Override
|
||||
public SignatureInfo newInstance() {
|
||||
return new SignatureInfo();
|
||||
}
|
||||
@Override
|
||||
protected void onRefreshed(){
|
||||
SignatureFooter footer = getSignatureFooter();
|
||||
footer.updateMagic();
|
||||
super.onRefreshed();
|
||||
footer.setSignatureSize(getDataSize());
|
||||
}
|
||||
|
||||
public void writeRaw(File file) throws IOException{
|
||||
refresh();
|
||||
File dir = file.getParentFile();
|
||||
if(dir != null && !dir.exists()){
|
||||
dir.mkdirs();
|
||||
}
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
writeBytes(outputStream);
|
||||
outputStream.close();
|
||||
}
|
||||
public List<File> writeSplitRawToDirectory(File dir) throws IOException{
|
||||
refresh();
|
||||
List<SignatureInfo> signatureInfoList = getElements();
|
||||
List<File> writtenFiles = new ArrayList<>(signatureInfoList.size());
|
||||
for(SignatureInfo signatureInfo:signatureInfoList){
|
||||
File file = signatureInfo.writeRawToDirectory(dir);
|
||||
writtenFiles.add(file);
|
||||
}
|
||||
return writtenFiles;
|
||||
}
|
||||
public void read(File file) throws IOException {
|
||||
super.readBytes(new BlockReader(file));
|
||||
}
|
||||
public void scanSplitFiles(File dir) throws IOException {
|
||||
if(!dir.isDirectory()){
|
||||
throw new IOException("No such directory");
|
||||
}
|
||||
FileFilter filter = new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
if(!file.isFile()){
|
||||
return false;
|
||||
}
|
||||
String name = file.getName().toLowerCase();
|
||||
return name.endsWith(SignatureId.FILE_EXT_RAW);
|
||||
}
|
||||
};
|
||||
File[] files = dir.listFiles(filter);
|
||||
if(files == null){
|
||||
return;
|
||||
}
|
||||
for(File file:files){
|
||||
addSplitRaw(file);
|
||||
}
|
||||
sortSignatures();
|
||||
}
|
||||
public SignatureInfo addSplitRaw(File signatureInfoFile) throws IOException {
|
||||
SignatureInfo signatureInfo = new SignatureInfo();
|
||||
signatureInfo.read(signatureInfoFile);
|
||||
add(signatureInfo);
|
||||
return signatureInfo;
|
||||
}
|
||||
@Override
|
||||
public int compare(SignatureInfo info1, SignatureInfo info2) {
|
||||
return info1.getId().compareTo(info2.getId());
|
||||
}
|
||||
|
||||
public static final String FILE_EXT = ".sig";
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import com.reandroid.arsc.container.BlockList;
|
||||
import com.reandroid.arsc.io.BlockReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
// General purpose block to consume the remaining bytes of BlockReader
|
||||
public class BottomBlock extends BlockList<LengthPrefixedBytes> {
|
||||
public BottomBlock(){
|
||||
super();
|
||||
}
|
||||
@Override
|
||||
public void onReadBytes(BlockReader reader) throws IOException {
|
||||
while (reader.isAvailable()){
|
||||
LengthPrefixedBytes prefixedBytes = new LengthPrefixedBytes(false);
|
||||
prefixedBytes.readBytes(reader);
|
||||
this.add(prefixedBytes);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import com.reandroid.archive2.ZipSignature;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CentralEntryHeader extends CommonHeader {
|
||||
private String mComment;
|
||||
public CentralEntryHeader(){
|
||||
super(OFFSET_fileName, ZipSignature.CENTRAL_FILE, OFFSET_general_purpose);
|
||||
}
|
||||
public CentralEntryHeader(String name){
|
||||
this();
|
||||
setFileName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
int readComment(InputStream inputStream) throws IOException {
|
||||
int commentLength = getCommentLength();
|
||||
if(commentLength==0){
|
||||
mComment = "";
|
||||
return 0;
|
||||
}
|
||||
setCommentLength(commentLength);
|
||||
byte[] bytes = getBytesInternal();
|
||||
int read = inputStream.read(bytes, getOffsetComment(), commentLength);
|
||||
if(read != commentLength){
|
||||
throw new IOException("Stream ended before reading comment: read="
|
||||
+read+", name length="+commentLength);
|
||||
}
|
||||
mComment = null;
|
||||
return commentLength;
|
||||
}
|
||||
|
||||
public int getVersionExtract(){
|
||||
return getShortUnsigned(OFFSET_versionExtract);
|
||||
}
|
||||
public void setVersionExtract(int value){
|
||||
putShort(OFFSET_versionExtract, value);
|
||||
}
|
||||
public String getComment(){
|
||||
if(mComment == null){
|
||||
mComment = decodeComment();
|
||||
}
|
||||
return mComment;
|
||||
}
|
||||
public void setComment(String comment){
|
||||
if(comment==null){
|
||||
comment="";
|
||||
}
|
||||
byte[] strBytes = ZipStringEncoding.encodeString(isUtf8(), comment);
|
||||
int length = strBytes.length;
|
||||
setCommentLength(length);
|
||||
if(length==0){
|
||||
mComment = comment;
|
||||
return;
|
||||
}
|
||||
byte[] bytes = getBytesInternal();
|
||||
System.arraycopy(strBytes, 0, bytes, getOffsetComment(), length);
|
||||
mComment = comment;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getCommentLength(){
|
||||
return getShortUnsigned(OFFSET_commentLength);
|
||||
}
|
||||
public void setCommentLength(int value){
|
||||
int length = getOffsetComment() + value;
|
||||
setBytesLength(length, false);
|
||||
putShort(OFFSET_commentLength, value);
|
||||
}
|
||||
public long getLocalRelativeOffset(){
|
||||
return getIntegerUnsigned(OFFSET_localRelativeOffset);
|
||||
}
|
||||
public void setLocalRelativeOffset(long offset){
|
||||
putInteger(OFFSET_localRelativeOffset, offset);
|
||||
}
|
||||
@Override
|
||||
void onUtf8Changed(boolean oldValue){
|
||||
String str = mComment;
|
||||
if(str != null){
|
||||
setComment(str);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean matches(LocalFileHeader localFileHeader){
|
||||
if(localFileHeader==null){
|
||||
return false;
|
||||
}
|
||||
return getCrc() == localFileHeader.getCrc()
|
||||
&& Objects.equals(getFileName(), localFileHeader.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
if(countBytes()<getMinByteLength()){
|
||||
return "Invalid";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('[').append(getFileOffset()).append(']');
|
||||
String str = getFileName();
|
||||
boolean appendOnce = false;
|
||||
if(str.length()>0){
|
||||
builder.append("name=").append(str);
|
||||
appendOnce = true;
|
||||
}
|
||||
str = getComment();
|
||||
if(str.length()>0){
|
||||
if(appendOnce){
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("comment=").append(str);
|
||||
appendOnce = true;
|
||||
}
|
||||
if(appendOnce){
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("SIG=").append(getSignature());
|
||||
builder.append(", versionMadeBy=").append(HexUtil.toHex4((short) getVersionMadeBy()));
|
||||
builder.append(", versionExtract=").append(HexUtil.toHex4((short) getVersionExtract()));
|
||||
builder.append(", GP={").append(getGeneralPurposeFlag()).append("}");
|
||||
builder.append(", method=").append(getMethod());
|
||||
builder.append(", date=").append(getDate());
|
||||
builder.append(", crc=").append(HexUtil.toHex8(getCrc()));
|
||||
builder.append(", cSize=").append(getCompressedSize());
|
||||
builder.append(", size=").append(getSize());
|
||||
builder.append(", fileNameLength=").append(getFileNameLength());
|
||||
builder.append(", extraLength=").append(getExtraLength());
|
||||
builder.append(", commentLength=").append(getCommentLength());
|
||||
builder.append(", offset=").append(getLocalRelativeOffset());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
|
||||
public static CentralEntryHeader fromLocalFileHeader(LocalFileHeader lfh){
|
||||
CentralEntryHeader ceh = new CentralEntryHeader();
|
||||
ceh.setSignature(ZipSignature.CENTRAL_FILE);
|
||||
ceh.setVersionMadeBy(0x0300);
|
||||
long offset = lfh.getFileOffset() - lfh.countBytes();
|
||||
ceh.setLocalRelativeOffset(offset);
|
||||
ceh.getGeneralPurposeFlag().setValue(lfh.getGeneralPurposeFlag().getValue());
|
||||
ceh.setMethod(lfh.getMethod());
|
||||
ceh.setDosTime(lfh.getDosTime());
|
||||
ceh.setCrc(lfh.getCrc());
|
||||
ceh.setCompressedSize(lfh.getCompressedSize());
|
||||
ceh.setSize(lfh.getSize());
|
||||
ceh.setFileName(lfh.getFileName());
|
||||
ceh.setExtra(lfh.getExtra());
|
||||
return ceh;
|
||||
}
|
||||
private static final int OFFSET_signature = 0;
|
||||
private static final int OFFSET_versionMadeBy = 4;
|
||||
private static final int OFFSET_versionExtract = 6;
|
||||
private static final int OFFSET_general_purpose = 8;
|
||||
private static final int OFFSET_method = 10;
|
||||
private static final int OFFSET_dos_time = 12;
|
||||
private static final int OFFSET_dos_date = 14;
|
||||
private static final int OFFSET_crc = 16;
|
||||
private static final int OFFSET_compressed_size = 20;
|
||||
private static final int OFFSET_size = 24;
|
||||
private static final int OFFSET_fileNameLength = 28;
|
||||
private static final int OFFSET_extraLength = 30;
|
||||
private static final int OFFSET_commentLength = 32;
|
||||
private static final int OFFSET_diskStart = 34;
|
||||
private static final int OFFSET_internalFileAttributes = 36;
|
||||
private static final int OFFSET_externalFileAttributes = 38;
|
||||
private static final int OFFSET_localRelativeOffset = 42;
|
||||
private static final int OFFSET_fileName = 46;
|
||||
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class CertificateBlock extends LengthPrefixedBytes{
|
||||
public CertificateBlock() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
public X509Certificate getCertificate(){
|
||||
return generateCertificate(getByteArray().toArray());
|
||||
}
|
||||
public static X509Certificate generateCertificate(byte[] encodedForm){
|
||||
CertificateFactory factory = getCertFactory();
|
||||
if(factory == null){
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
// TODO: cert bytes could be in DER format ?
|
||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(encodedForm));
|
||||
}catch (CertificateException ignored){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private static CertificateFactory getCertFactory() {
|
||||
if (sCertFactory == null) {
|
||||
try {
|
||||
sCertFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException ignored) {
|
||||
}
|
||||
}
|
||||
return sCertFactory;
|
||||
}
|
||||
|
||||
private static CertificateFactory sCertFactory = null;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
|
||||
public class CertificateBlockList extends LengthPrefixedList<CertificateBlock>{
|
||||
public CertificateBlockList() {
|
||||
super(false);
|
||||
}
|
||||
@Override
|
||||
public CertificateBlock newInstance() {
|
||||
return new CertificateBlock();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,419 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import com.reandroid.archive2.ZipSignature;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
public abstract class CommonHeader extends ZipHeader {
|
||||
private final int offsetFileName;
|
||||
private final int offsetGeneralPurpose;
|
||||
private final GeneralPurposeFlag generalPurposeFlag;
|
||||
private String mFileName;
|
||||
private long mFileOffset;
|
||||
public CommonHeader(int offsetFileName, ZipSignature expectedSignature, int offsetGeneralPurpose){
|
||||
super(offsetFileName, expectedSignature);
|
||||
this.offsetFileName = offsetFileName;
|
||||
this.offsetGeneralPurpose = offsetGeneralPurpose;
|
||||
this.generalPurposeFlag = new GeneralPurposeFlag(this, offsetGeneralPurpose);
|
||||
}
|
||||
public long getFileOffset() {
|
||||
return mFileOffset;
|
||||
}
|
||||
public void setFileOffset(long fileOffset){
|
||||
this.mFileOffset = fileOffset;
|
||||
}
|
||||
public long getDataSize(){
|
||||
if(getMethod() == ZipEntry.STORED){
|
||||
return getSize();
|
||||
}
|
||||
return getCompressedSize();
|
||||
}
|
||||
public void setDataSize(long size){
|
||||
if(getMethod() == ZipEntry.STORED){
|
||||
setSize(size);
|
||||
}
|
||||
setCompressedSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
int readNext(InputStream inputStream) throws IOException {
|
||||
int read = 0;
|
||||
read += readFileName(inputStream);
|
||||
read += readExtra(inputStream);
|
||||
read += readComment(inputStream);
|
||||
mFileName = null;
|
||||
return read;
|
||||
}
|
||||
private int readFileName(InputStream inputStream) throws IOException {
|
||||
int fileNameLength = getFileNameLength();
|
||||
if(fileNameLength==0){
|
||||
mFileName = "";
|
||||
return 0;
|
||||
}
|
||||
setFileNameLength(fileNameLength);
|
||||
byte[] bytes = getBytesInternal();
|
||||
int read = inputStream.read(bytes, offsetFileName, fileNameLength);
|
||||
if(read != fileNameLength){
|
||||
throw new IOException("Stream ended before reading file name: read="
|
||||
+read+", name length="+fileNameLength);
|
||||
}
|
||||
mFileName = null;
|
||||
return fileNameLength;
|
||||
}
|
||||
private int readExtra(InputStream inputStream) throws IOException {
|
||||
int extraLength = getExtraLength();
|
||||
if(extraLength==0){
|
||||
return 0;
|
||||
}
|
||||
setExtraLength(extraLength);
|
||||
byte[] bytes = getBytesInternal();
|
||||
int offset = getOffsetExtra();
|
||||
int read = inputStream.read(bytes, offset, extraLength);
|
||||
if(read != extraLength){
|
||||
throw new IOException("Stream ended before reading extra bytes: read="
|
||||
+ read +", extra length="+extraLength);
|
||||
}
|
||||
return extraLength;
|
||||
}
|
||||
int readComment(InputStream inputStream) throws IOException {
|
||||
return 0;
|
||||
}
|
||||
public int getVersionMadeBy(){
|
||||
return getShortUnsigned(OFFSET_versionMadeBy);
|
||||
}
|
||||
public void setVersionMadeBy(int value){
|
||||
putShort(OFFSET_versionMadeBy, value);
|
||||
}
|
||||
public int getPlatform(){
|
||||
return getByteUnsigned(OFFSET_platform);
|
||||
}
|
||||
public void setPlatform(int value){
|
||||
getBytesInternal()[OFFSET_platform] = (byte) value;
|
||||
}
|
||||
public GeneralPurposeFlag getGeneralPurposeFlag() {
|
||||
return generalPurposeFlag;
|
||||
}
|
||||
public int getMethod(){
|
||||
return getShortUnsigned(offsetGeneralPurpose + 2);
|
||||
}
|
||||
public void setMethod(int value){
|
||||
putShort(offsetGeneralPurpose + 2, value);
|
||||
GeneralPurposeFlag gpf = getGeneralPurposeFlag();
|
||||
//gpf.setHasDataDescriptor(value != ZipEntry.STORED);
|
||||
}
|
||||
public long getDosTime(){
|
||||
return getIntegerUnsigned(offsetGeneralPurpose + 4);
|
||||
}
|
||||
public void setDosTime(long value){
|
||||
putInteger(offsetGeneralPurpose + 4, value);
|
||||
}
|
||||
public Date getDate(){
|
||||
return dosToJavaDate(getDosTime());
|
||||
}
|
||||
public void setDate(Date date){
|
||||
setDate(date==null ? 0L : date.getTime());
|
||||
}
|
||||
public void setDate(long date){
|
||||
setDosTime(javaToDosTime(date));
|
||||
}
|
||||
public long getCrc(){
|
||||
return getIntegerUnsigned(offsetGeneralPurpose + 8);
|
||||
}
|
||||
public void setCrc(long value){
|
||||
putInteger(offsetGeneralPurpose + 8, value);
|
||||
}
|
||||
public long getCompressedSize(){
|
||||
return getIntegerUnsigned(offsetGeneralPurpose + 12);
|
||||
}
|
||||
public void setCompressedSize(long value){
|
||||
putInteger(offsetGeneralPurpose + 12, value);
|
||||
}
|
||||
public long getSize(){
|
||||
return getIntegerUnsigned(offsetGeneralPurpose + 16);
|
||||
}
|
||||
public void setSize(long value){
|
||||
putInteger(offsetGeneralPurpose + 16, value);
|
||||
}
|
||||
public int getFileNameLength(){
|
||||
return getShortUnsigned(offsetGeneralPurpose + 20);
|
||||
}
|
||||
private void setFileNameLength(int value){
|
||||
int length = offsetFileName + value + getExtraLength() + getCommentLength();
|
||||
super.setBytesLength(length, false);
|
||||
putShort(offsetGeneralPurpose + 20, value);
|
||||
}
|
||||
public int getExtraLength(){
|
||||
return getShortUnsigned(offsetGeneralPurpose + 22);
|
||||
}
|
||||
public void setExtraLength(int value){
|
||||
int length = offsetFileName + getFileNameLength() + value + getCommentLength();
|
||||
super.setBytesLength(length, false);
|
||||
putShort(offsetGeneralPurpose + 22, value);
|
||||
}
|
||||
public byte[] getExtra(){
|
||||
int length = getExtraLength();
|
||||
byte[] result = new byte[length];
|
||||
if(length==0){
|
||||
return result;
|
||||
}
|
||||
byte[] bytes = getBytesInternal();
|
||||
int offset = getOffsetExtra();
|
||||
System.arraycopy(bytes, offset, result, 0, length);
|
||||
return result;
|
||||
}
|
||||
public void setExtra(byte[] extra){
|
||||
if(extra == null){
|
||||
extra = new byte[0];
|
||||
}
|
||||
int length = extra.length;
|
||||
setExtraLength(length);
|
||||
if(length == 0){
|
||||
return;
|
||||
}
|
||||
putBytes(extra, 0, getOffsetExtra(), length);
|
||||
}
|
||||
public int getCommentLength(){
|
||||
return 0;
|
||||
}
|
||||
int getOffsetComment(){
|
||||
return offsetFileName + getFileNameLength() + getExtraLength();
|
||||
}
|
||||
private int getOffsetExtra(){
|
||||
return offsetFileName + getFileNameLength();
|
||||
}
|
||||
|
||||
public String getFileName(){
|
||||
if(mFileName == null){
|
||||
mFileName = decodeFileName();
|
||||
}
|
||||
return mFileName;
|
||||
}
|
||||
public void setFileName(String fileName){
|
||||
if(fileName==null){
|
||||
fileName="";
|
||||
}
|
||||
byte[] nameBytes;
|
||||
if(getGeneralPurposeFlag().getUtf8()){
|
||||
nameBytes = fileName.getBytes(StandardCharsets.UTF_8);
|
||||
}else {
|
||||
nameBytes = fileName.getBytes();
|
||||
}
|
||||
int length = nameBytes.length;
|
||||
setFileNameLength(length);
|
||||
if(length==0){
|
||||
mFileName = fileName;
|
||||
return;
|
||||
}
|
||||
byte[] bytes = getBytesInternal();
|
||||
System.arraycopy(nameBytes, 0, bytes, offsetFileName, length);
|
||||
mFileName = fileName;
|
||||
}
|
||||
public boolean isUtf8(){
|
||||
return getGeneralPurposeFlag().getUtf8();
|
||||
}
|
||||
public boolean hasDataDescriptor(){
|
||||
return getGeneralPurposeFlag().hasDataDescriptor();
|
||||
}
|
||||
private String decodeFileName(){
|
||||
int length = getFileNameLength();
|
||||
byte[] bytes = getBytesInternal();
|
||||
int offset = offsetFileName;
|
||||
int max = bytes.length - offset;
|
||||
if(max<=0){
|
||||
return "";
|
||||
}
|
||||
if(length>max){
|
||||
length = max;
|
||||
}
|
||||
return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(),
|
||||
getBytesInternal(), offset, length);
|
||||
}
|
||||
public String decodeComment(){
|
||||
int length = getExtraLength();
|
||||
byte[] bytes = getBytesInternal();
|
||||
int offset = getOffsetExtra();
|
||||
int max = bytes.length - offset;
|
||||
if(max<=0){
|
||||
return "";
|
||||
}
|
||||
if(length>max){
|
||||
length = max;
|
||||
}
|
||||
return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(),
|
||||
getBytesInternal(), offset, length);
|
||||
}
|
||||
void onUtf8Changed(boolean oldValue){
|
||||
String str = mFileName;
|
||||
if(str != null){
|
||||
setFileName(str);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
if(countBytes()<getMinByteLength()){
|
||||
return "Invalid";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('[').append(getFileOffset()).append("] ");
|
||||
String str = getFileName();
|
||||
boolean appendOnce = false;
|
||||
if(str.length()>0){
|
||||
builder.append("name=").append(str);
|
||||
appendOnce = true;
|
||||
}
|
||||
if(appendOnce){
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("SIG=").append(getSignature());
|
||||
builder.append(", versionMadeBy=").append(HexUtil.toHex4((short) getVersionMadeBy()));
|
||||
builder.append(", platform=").append(HexUtil.toHex2((byte) getPlatform()));
|
||||
builder.append(", GP={").append(getGeneralPurposeFlag()).append("}");
|
||||
builder.append(", method=").append(getMethod());
|
||||
builder.append(", date=").append(getDate());
|
||||
builder.append(", crc=").append(HexUtil.toHex8(getCrc()));
|
||||
builder.append(", cSize=").append(getCompressedSize());
|
||||
builder.append(", size=").append(getSize());
|
||||
builder.append(", fileNameLength=").append(getFileNameLength());
|
||||
builder.append(", extraLength=").append(getExtraLength());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static Date dosToJavaDate(final long dosTime) {
|
||||
final Calendar cal = Calendar.getInstance();
|
||||
cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
|
||||
cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
|
||||
cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
|
||||
cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
|
||||
cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
|
||||
cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
return cal.getTime();
|
||||
}
|
||||
private static long javaToDosTime(long javaTime) {
|
||||
int date;
|
||||
int time;
|
||||
GregorianCalendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date(javaTime));
|
||||
int year = cal.get(Calendar.YEAR);
|
||||
if (year < 1980) {
|
||||
date = 0x21;
|
||||
time = 0;
|
||||
} else {
|
||||
date = cal.get(Calendar.DATE);
|
||||
date = (cal.get(Calendar.MONTH) + 1 << 5) | date;
|
||||
date = ((cal.get(Calendar.YEAR) - 1980) << 9) | date;
|
||||
time = cal.get(Calendar.SECOND) >> 1;
|
||||
time = (cal.get(Calendar.MINUTE) << 5) | time;
|
||||
time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
|
||||
}
|
||||
return ((long) date << 16) | time;
|
||||
}
|
||||
|
||||
public static class GeneralPurposeFlag {
|
||||
private final CommonHeader localFileHeader;
|
||||
private final int offset;
|
||||
public GeneralPurposeFlag(CommonHeader commonHeader, int offset){
|
||||
this.localFileHeader = commonHeader;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public boolean getEncryption(){
|
||||
return this.localFileHeader.getBit(offset, 0);
|
||||
}
|
||||
public void setEncryption(boolean flag){
|
||||
this.localFileHeader.putBit(offset, 0, flag);
|
||||
}
|
||||
public boolean hasDataDescriptor(){
|
||||
return this.localFileHeader.getBit(offset, 3);
|
||||
}
|
||||
public void setHasDataDescriptor(boolean flag){
|
||||
this.localFileHeader.putBit(offset, 3, flag);
|
||||
}
|
||||
public boolean getStrongEncryption(){
|
||||
return this.localFileHeader.getBit(offset, 6);
|
||||
}
|
||||
public void setStrongEncryption(boolean flag){
|
||||
this.localFileHeader.putBit(offset, 6, flag);
|
||||
}
|
||||
public boolean getUtf8(){
|
||||
return this.localFileHeader.getBit(offset + 1, 3);
|
||||
}
|
||||
public void setUtf8(boolean flag){
|
||||
setUtf8(flag, true);
|
||||
}
|
||||
private void setUtf8(boolean flag, boolean notify){
|
||||
boolean oldUtf8 = getUtf8();
|
||||
if(oldUtf8 == flag){
|
||||
return;
|
||||
}
|
||||
this.localFileHeader.putBit(offset +1, 3, flag);
|
||||
if(notify){
|
||||
this.localFileHeader.onUtf8Changed(oldUtf8);
|
||||
}
|
||||
}
|
||||
|
||||
public int getValue(){
|
||||
return this.localFileHeader.getShortUnsigned(offset);
|
||||
}
|
||||
public void setValue(int value){
|
||||
if(value == getValue()){
|
||||
return;
|
||||
}
|
||||
boolean oldUtf8 = getUtf8();
|
||||
this.localFileHeader.putShort(offset, value);
|
||||
if(oldUtf8 != getUtf8()){
|
||||
this.localFileHeader.onUtf8Changed(oldUtf8);
|
||||
}
|
||||
}
|
||||
public void initDefault(){
|
||||
setUtf8(false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Enc="+ getEncryption()
|
||||
+", Descriptor="+ hasDataDescriptor()
|
||||
+", StrongEnc="+ getStrongEncryption()
|
||||
+", UTF8="+ getUtf8();
|
||||
}
|
||||
}
|
||||
|
||||
private static final int OFFSET_versionMadeBy = 4;
|
||||
private static final int OFFSET_platform = 5;
|
||||
|
||||
private static final int OFFSET_general_purpose = 6;
|
||||
|
||||
private static final int OFFSET_method = 8;
|
||||
private static final int OFFSET_dos_time = 10;
|
||||
private static final int OFFSET_crc = 14;
|
||||
private static final int OFFSET_compressed_size = 18;
|
||||
private static final int OFFSET_size = 22;
|
||||
private static final int OFFSET_fileNameLength = 26;
|
||||
private static final int OFFSET_extraLength = 28;
|
||||
|
||||
private static final int OFFSET_fileName = 30;
|
||||
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import com.reandroid.archive2.ZipSignature;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
public class DataDescriptor extends ZipHeader{
|
||||
public DataDescriptor() {
|
||||
super(MIN_LENGTH, ZipSignature.DATA_DESCRIPTOR);
|
||||
}
|
||||
public long getCrc(){
|
||||
return getIntegerUnsigned(OFFSET_crc);
|
||||
}
|
||||
public void setCrc(long value){
|
||||
putInteger(OFFSET_crc, value);
|
||||
}
|
||||
public long getCompressedSize(){
|
||||
return getIntegerUnsigned(OFFSET_compressed_size);
|
||||
}
|
||||
public void setCompressedSize(long value){
|
||||
putInteger(OFFSET_compressed_size, value);
|
||||
}
|
||||
public long getSize(){
|
||||
return getIntegerUnsigned(OFFSET_size);
|
||||
}
|
||||
public void setSize(long value){
|
||||
putInteger(OFFSET_size, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(getSignature());
|
||||
builder.append(", crc=").append(HexUtil.toHex8(getCrc()));
|
||||
builder.append(", compressed=").append(getCompressedSize());
|
||||
builder.append(", size=").append(getSize());
|
||||
return builder.toString();
|
||||
}
|
||||
public static DataDescriptor fromLocalFile(LocalFileHeader lfh){
|
||||
DataDescriptor dataDescriptor = new DataDescriptor();
|
||||
dataDescriptor.setSignature(ZipSignature.DATA_DESCRIPTOR);
|
||||
dataDescriptor.setSize(lfh.getSize());
|
||||
dataDescriptor.setCompressedSize(lfh.getCompressedSize());
|
||||
dataDescriptor.setCrc(lfh.getCrc());
|
||||
return dataDescriptor;
|
||||
}
|
||||
|
||||
private static final int OFFSET_crc = 4;
|
||||
private static final int OFFSET_compressed_size = 8;
|
||||
private static final int OFFSET_size = 12;
|
||||
|
||||
public static final int MIN_LENGTH = 16;
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 github.com/REAndroid
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.reandroid.archive2.block;
|
||||
|
||||
import com.reandroid.archive2.ZipSignature;
|
||||
import com.reandroid.arsc.util.HexUtil;
|
||||
|
||||
public class EndRecord extends ZipHeader{
|
||||
public EndRecord() {
|
||||
super(MIN_LENGTH, ZipSignature.END_RECORD);
|
||||
}
|
||||
public int getNumberOfDisk(){
|
||||
return getShortUnsigned(OFFSET_numberOfDisk);
|
||||
}
|
||||
public void setNumberOfDisk(int value){
|
||||
putShort(OFFSET_numberOfDisk, value);
|
||||
}
|
||||
public int getCentralDirectoryStartDisk(){
|
||||
return getShortUnsigned(OFFSET_centralDirectoryStartDisk);
|
||||
}
|
||||
public void setCentralDirectoryStartDisk(int value){
|
||||
putShort(OFFSET_centralDirectoryStartDisk, value);
|
||||
}
|
||||
public int getNumberOfDirectories(){
|
||||
return getShortUnsigned(OFFSET_numberOfDirectories);
|
||||
}
|
||||
public void setNumberOfDirectories(int value){
|
||||
putShort(OFFSET_numberOfDirectories, value);
|
||||
}
|
||||
public int getTotalNumberOfDirectories(){
|
||||
return getShortUnsigned(OFFSET_totalNumberOfDirectories);
|
||||
}
|
||||
public void setTotalNumberOfDirectories(int value){
|
||||
putShort(OFFSET_totalNumberOfDirectories, value);
|
||||
}
|
||||
public long getLengthOfCentralDirectory(){
|
||||
return getIntegerUnsigned(OFFSET_lengthOfCentralDirectory);
|
||||
}
|
||||
public void setLengthOfCentralDirectory(long value){
|
||||
putInteger(OFFSET_lengthOfCentralDirectory, value);
|
||||
}
|
||||
public long getOffsetOfCentralDirectory(){
|
||||
return getIntegerUnsigned(OFFSET_offsetOfCentralDirectory);
|
||||
}
|
||||
public void setOffsetOfCentralDirectory(int value){
|
||||
putInteger(OFFSET_offsetOfCentralDirectory, value);
|
||||
}
|
||||
public int getLastShort(){
|
||||
return getShortUnsigned(OFFSET_lastShort);
|
||||
}
|
||||
public void getLastShort(int value){
|
||||
putShort(OFFSET_lastShort, value);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
if(countBytes()<getMinByteLength()){
|
||||
return "Invalid";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(getSignature());
|
||||
builder.append(", disks=").append(getNumberOfDisk());
|
||||
builder.append(", start disk=").append(getCentralDirectoryStartDisk());
|
||||
builder.append(", dirs=").append(getNumberOfDirectories());
|
||||
builder.append(", total dirs=").append(getTotalNumberOfDirectories());
|
||||
builder.append(", length=").append(getLengthOfCentralDirectory());
|
||||
builder.append(", offset=").append(getOffsetOfCentralDirectory());
|
||||
builder.append(", last=").append(HexUtil.toHex8(getLastShort()));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final int OFFSET_numberOfDisk = 4;
|
||||
private static final int OFFSET_centralDirectoryStartDisk = 6;
|
||||
private static final int OFFSET_numberOfDirectories = 8;
|
||||
private static final int OFFSET_totalNumberOfDirectories = 10;
|
||||
private static final int OFFSET_lengthOfCentralDirectory = 12;
|
||||
private static final int OFFSET_offsetOfCentralDirectory = 16;
|
||||
private static final int OFFSET_lastShort = 20;
|
||||
|
||||
public static final int MIN_LENGTH = 22;
|
||||
public static final int MAX_LENGTH = 0xffff + 22;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue