libEntries = entries.stream()
137 | .filter(entry -> entry.getPath().startsWith(BundleModule.LIB_DIRECTORY))
138 | .toList();
139 | Files.NativeLibraries nativeLibraries = bundleModule.getNativeConfig().orElse(null);
140 | if (libEntries.isEmpty())
141 | return nativeLibraries;
142 | if (nativeLibraries == null)
143 | throw new UnexpectedException(String.format("can not find nativeLibraries file `native.pb` in %s module", bundleModule.getName().getName()));
144 | Files.NativeLibraries filteredNativeLibraries = nativeLibraries;
145 | for (Files.TargetedNativeDirectory directory : nativeLibraries.getDirectoryList()) {
146 | int directoryNativeSize = libEntries.stream()
147 | .filter(entry -> entry.getPath().startsWith(directory.getPath()))
148 | .toList().size();
149 | if (directoryNativeSize > 0) {
150 | int moduleNativeSize = bundleModule.getEntries().stream()
151 | .filter(entry -> entry.getPath().startsWith(directory.getPath()))
152 | .toList().size();
153 | if (directoryNativeSize == moduleNativeSize)
154 | filteredNativeLibraries = NativeLibrariesOperation.removeDirectory(filteredNativeLibraries, directory.getPath());
155 | }
156 | }
157 | return filteredNativeLibraries;
158 | }
159 |
160 | /**
161 | * Filter metadata directory and return filtered list.
162 | *
163 | * @return The filtered metadata.
164 | */
165 | private BundleMetadata filterMetaData() {
166 | BundleMetadata.Builder builder = BundleMetadata.builder();
167 | Stream.of(rawAppBundle.getBundleMetadata())
168 | .map(BundleMetadata::getFileContentMap)
169 | .map(ImmutableMap::entrySet)
170 | .flatMap(Collection::stream)
171 | .filter(entry -> {
172 | ZipPath entryZipPath = ZipPath.create(AppBundle.METADATA_DIRECTORY + "/" + entry.getKey());
173 | if (getMatchedFilterRule(entryZipPath) != null) {
174 | System.out.printf(" - %s%n", entryZipPath);
175 | filterTotalCount += 1;
176 | filterTotalSize += (int) AppBundleUtils.getZipEntrySize(bundleZipFile, entryZipPath);
177 | return false;
178 | }
179 | return true;
180 | }).forEach(entry -> builder.addFile(entry.getKey(), entry.getValue()));
181 | return builder.build();
182 | }
183 |
184 | /**
185 | * Checks if the filtered entry is valid and can be filtered.
186 | *
187 | * @param entry The module entry to be checked.
188 | * @param filterRule The filter rule applied to the entry.
189 | */
190 | private void checkFilteredEntry(@org.jetbrains.annotations.NotNull ModuleEntry entry, String filterRule) {
191 | if (!entry.getPath().startsWith(BundleModule.LIB_DIRECTORY) && !entry.getPath().startsWith(METADATA_DIRECTORY.toString()))
192 | throw new UnsupportedOperationException(String.format("%s entry can not be filtered, please check the filter rule [%s].", entry.getPath(), filterRule));
193 | }
194 |
195 | /**
196 | * Get the filter rule that matches the given ZipPath.
197 | *
198 | * @param zipPath The ZipPath to match against filter rules.
199 | * @return The matched filter rule, or null if no rule matches.
200 | */
201 | private @Nullable String getMatchedFilterRule(ZipPath zipPath) {
202 | for (String rule : filterRules) {
203 | Pattern filterPattern = Pattern.compile(Utils.convertToPatternString(rule));
204 | if (filterPattern.matcher(zipPath.toString()).matches())
205 | return rule;
206 | }
207 | return null;
208 | }
209 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/extensions/BundleStringFilter.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.extensions;
2 |
3 | import com.android.aapt.Resources;
4 | import com.android.tools.build.bundletool.model.AppBundle;
5 | import com.android.tools.build.bundletool.model.BundleModule;
6 | import com.android.tools.build.bundletool.model.BundleModuleName;
7 | import com.google.common.collect.ImmutableMap;
8 | import io.github.goldfish07.reschiper.plugin.bundle.ResourceTableBuilder;
9 | import io.github.goldfish07.reschiper.plugin.utils.TimeClock;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | import java.io.File;
14 | import java.io.IOException;
15 | import java.nio.file.Files;
16 | import java.nio.file.Path;
17 | import java.nio.file.Paths;
18 | import java.util.*;
19 | import java.util.stream.Collectors;
20 |
21 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
22 |
23 | /**
24 | * The `BundleStringFilter` class is responsible for filtering strings in an Android App Bundle (AAB) based on specific criteria.
25 | * It can remove unused strings and, optionally, specific languages from the resource table in each module of the bundle.
26 | *
27 | * This class provides methods for filtering strings in the App Bundle and obfuscates the resource tables in each module
28 | * while respecting white-listed languages and a list of unused string names.
29 | */
30 | public class BundleStringFilter {
31 | private static final String replaceValue = "[value removed]";
32 | private final AppBundle rawAppBundle;
33 | private final String unusedStrPath;
34 | private final Set languageWhiteList;
35 | private final Set unUsedNameSet = new HashSet<>(5000);
36 |
37 | /**
38 | * Constructs a `BundleStringFilter` instance with the provided parameters.
39 | *
40 | * @param bundlePath The path to the input AAB file.
41 | * @param rawAppBundle The original unfiltered App Bundle.
42 | * @param unusedStrPath The path to a file containing a list of unused string names (one per line).
43 | * @param languageWhiteList A set of language codes to be preserved (optional).
44 | */
45 | public BundleStringFilter(Path bundlePath, AppBundle rawAppBundle, String unusedStrPath, Set languageWhiteList) {
46 | checkFileExistsAndReadable(bundlePath);
47 | this.rawAppBundle = rawAppBundle;
48 | this.unusedStrPath = unusedStrPath;
49 | this.languageWhiteList = languageWhiteList;
50 | }
51 |
52 | /**
53 | * Filters the strings in the App Bundle based on the provided criteria.
54 | *
55 | * @return An AppBundle with filtered strings, or the original AppBundle if no filtering is applied.
56 | * @throws IOException If there is an issue with reading files or bundle contents.
57 | */
58 | public AppBundle filter() throws IOException {
59 | TimeClock timeClock = new TimeClock();
60 | File unusedStrFile = new File(unusedStrPath);
61 | Map obfuscatedModules = new HashMap<>();
62 | if (unusedStrFile.exists()) {
63 | //shrink-results
64 | unUsedNameSet.addAll(Files.readAllLines(Paths.get(unusedStrPath)));
65 | System.out.println("unused string : " + unUsedNameSet.size());
66 | }
67 | if (!unUsedNameSet.isEmpty() || !languageWhiteList.isEmpty())
68 | for (Map.Entry entry : rawAppBundle.getModules().entrySet()) {
69 | BundleModule bundleModule = entry.getValue();
70 | BundleModuleName bundleModuleName = entry.getKey();
71 | // obfuscate bundle module
72 | BundleModule obfuscatedModule = obfuscateBundleModule(bundleModule);
73 | obfuscatedModules.put(bundleModuleName, obfuscatedModule);
74 | }
75 | else
76 | return rawAppBundle;
77 | AppBundle appBundle = rawAppBundle.toBuilder()
78 | .setModules(ImmutableMap.copyOf(obfuscatedModules))
79 | .build();
80 | System.out.printf("filtering strings completed in %s\n%n", timeClock.getElapsedTime());
81 | return appBundle;
82 | }
83 |
84 | /**
85 | * Obfuscates the resource table of a bundle module based on the specified criteria.
86 | *
87 | * @param bundleModule The bundle module to obfuscate.
88 | * @return The obfuscated bundle module.
89 | */
90 | private BundleModule obfuscateBundleModule(@NotNull BundleModule bundleModule) {
91 | BundleModule.Builder builder = bundleModule.toBuilder();
92 | // obfuscate resourceTable
93 | Resources.ResourceTable obfuscatedResTable = obfuscateResourceTable(bundleModule);
94 | if (obfuscatedResTable != null)
95 | builder.setResourceTable(obfuscatedResTable);
96 | return builder.build();
97 | }
98 |
99 | /**
100 | * Obfuscates the resource table of a bundle module by removing unused strings and languages.
101 | *
102 | * @param bundleModule The bundle module containing the resource table to obfuscate.
103 | * @return The obfuscated resource table, or null if it's empty.
104 | */
105 | private Resources.@Nullable ResourceTable obfuscateResourceTable(@NotNull BundleModule bundleModule) {
106 | if (bundleModule.getResourceTable().isEmpty()) {
107 | return null;
108 | }
109 | Resources.ResourceTable rawTable = bundleModule.getResourceTable().get();
110 | ResourceTableBuilder tableBuilder = new ResourceTableBuilder();
111 | List packageList = rawTable.getPackageList();
112 | if (packageList.isEmpty())
113 | return tableBuilder.build();
114 | for (Resources.Package resPackage : packageList) {
115 | if (resPackage == null)
116 | continue;
117 | ResourceTableBuilder.PackageBuilder packageBuilder = tableBuilder.addPackage(resPackage);
118 | List typeList = resPackage.getTypeList();
119 | Set languageFilterSet = new HashSet<>(100);
120 | List nameFilterList = new ArrayList<>(3000);
121 | for (Resources.Type resType : typeList) {
122 | if (resType == null)
123 | continue;
124 | List entryList = resType.getEntryList();
125 | for (Resources.Entry resEntry : entryList) {
126 | if (resEntry == null)
127 | continue;
128 | if (resPackage.getPackageId().getId() == 127 && resType.getName().equals("string") &&
129 | languageWhiteList != null && !languageWhiteList.isEmpty()) {
130 | //delete language
131 | List languageValue = resEntry.getConfigValueList().stream()
132 | .filter(Objects::nonNull)
133 | .filter(configValue -> {
134 | String locale = configValue.getConfig().getLocale();
135 | if (keepLanguage(locale))
136 | return true;
137 | languageFilterSet.add(locale);
138 | return false;
139 | }).collect(Collectors.toList());
140 | resEntry = resEntry.toBuilder().clearConfigValue().addAllConfigValue(languageValue).build();
141 | }
142 | // delete unused strings identified by the shrink process
143 | if (resPackage.getPackageId().getId() == 127 && resType.getName().equals("string")
144 | && !unUsedNameSet.isEmpty() && unUsedNameSet.contains(resEntry.getName())) {
145 | List proguardConfigValue = resEntry.getConfigValueList().stream()
146 | .filter(Objects::nonNull)
147 | .map(configValue -> {
148 | Resources.ConfigValue.Builder rcb = configValue.toBuilder();
149 | Resources.Value.Builder rvb = rcb.getValueBuilder();
150 | Resources.Item.Builder rib = rvb.getItemBuilder();
151 | Resources.String.Builder rfb = rib.getStrBuilder();
152 | return rcb.setValue(
153 | rvb.setItem(
154 | rib.setStr(
155 | rfb.setValue(replaceValue).build()
156 | ).build()
157 | ).build()
158 | ).build();
159 | }).collect(Collectors.toList());
160 | nameFilterList.add(resEntry.getName());
161 | resEntry = resEntry.toBuilder().clearConfigValue().addAllConfigValue(proguardConfigValue).build();
162 | }
163 | packageBuilder.addResource(resType, resEntry);
164 | }
165 | }
166 | System.out.println("filtering " + resPackage.getPackageName() + " id:" + resPackage.getPackageId().getId());
167 | StringBuilder l = new StringBuilder();
168 | for (String lan : languageFilterSet)
169 | l.append("[remove language] : ").append(lan).append("\n");
170 | System.out.println(l);
171 | l = new StringBuilder();
172 | for (String name : nameFilterList)
173 | l.append("[delete name] ").append(name).append("\n");
174 | System.out.println(l);
175 | System.out.println("-----------");
176 | packageBuilder.build();
177 | }
178 | return tableBuilder.build();
179 | }
180 |
181 | /**
182 | * Determines whether a language should be preserved based on the language white list.
183 | *
184 | * @param lan The language code to check.
185 | * @return True if the language should be preserved, false otherwise.
186 | */
187 | private boolean keepLanguage(String lan) {
188 | if (lan == null || lan.equals(" ") || lan.isEmpty())
189 | return true;
190 | if (lan.contains("-")) {
191 | int index = lan.indexOf("-");
192 | if (index != -1) {
193 | String language = lan.substring(0, index);
194 | return languageWhiteList.contains(language);
195 | }
196 | } else
197 | return languageWhiteList.contains(lan);
198 | return false;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/extensions/DuplicateResourceMerger.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.extensions;
2 |
3 | import com.android.aapt.Resources;
4 | import com.android.tools.build.bundletool.model.*;
5 | import com.android.tools.build.bundletool.model.utils.ResourcesUtils;
6 | import io.github.goldfish07.reschiper.plugin.bundle.AppBundleUtils;
7 | import io.github.goldfish07.reschiper.plugin.bundle.ResourceTableBuilder;
8 | import io.github.goldfish07.reschiper.plugin.operations.ResourceTableOperation;
9 | import io.github.goldfish07.reschiper.plugin.operations.FileOperation;
10 | import io.github.goldfish07.reschiper.plugin.utils.TimeClock;
11 | import org.jetbrains.annotations.NotNull;
12 |
13 | import java.io.*;
14 | import java.nio.file.Files;
15 | import java.nio.file.Path;
16 | import java.util.*;
17 | import java.util.logging.Logger;
18 | import java.util.stream.Collectors;
19 | import java.util.stream.Stream;
20 | import java.util.zip.ZipFile;
21 |
22 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist;
23 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
24 | import static com.google.common.collect.ImmutableList.toImmutableList;
25 |
26 | /**
27 | * The `DuplicateResourceMerger` class is responsible for merging and removing duplicated resources
28 | * in an Android App Bundle (AAB). It identifies duplicated resources by their MD5 hash values and
29 | * merges them to reduce the size of the bundle.
30 | *
31 | * This class processes each module in the App Bundle, identifies duplicated resources, and generates
32 | * a log file with information about the merged resources and their original paths.
33 | */
34 | public class DuplicateResourceMerger {
35 | private static final Logger logger = Logger.getLogger(DuplicateResourceMerger.class.getName());
36 | public static final String DUPLICATE_LOGGER_FILE_SUFFIX = "-duplicate.txt";
37 | private final Path outputLogLocationDir;
38 | private final ZipFile bundleZipFile;
39 | private final AppBundle rawAppBundle;
40 | private final Map md5FileList = new HashMap<>();
41 | private final Map duplicatedFileList = new HashMap<>();
42 | private int mergeDuplicatedTotalSize = 0;
43 | private int mergeDuplicatedTotalCount = 0;
44 |
45 | /**
46 | * Constructs a `DuplicateResourceMerger` instance with the provided parameters.
47 | *
48 | * @param bundlePath The path to the input AAB file.
49 | * @param appBundle The original unfiltered App Bundle.
50 | * @param outputLogLocationDir The directory where log files containing information about duplicated resources will be stored.
51 | * @throws IOException If there is an issue with reading files or bundle contents.
52 | */
53 | public DuplicateResourceMerger(Path bundlePath, AppBundle appBundle, Path outputLogLocationDir) throws IOException {
54 | checkFileExistsAndReadable(bundlePath);
55 | this.outputLogLocationDir = outputLogLocationDir;
56 | bundleZipFile = new ZipFile(bundlePath.toFile());
57 | rawAppBundle = appBundle;
58 | }
59 |
60 | /**
61 | * Merges duplicated resources in all modules of the App Bundle, removing duplicates based on their MD5 hash values.
62 | * Generates log files containing information about the merged resources and their original paths.
63 | *
64 | * @return An AppBundle with duplicated resources removed.
65 | * @throws IOException If there is an issue with reading files or bundle contents.
66 | */
67 | public AppBundle merge() throws IOException {
68 | TimeClock timeClock = new TimeClock();
69 | List mergedBundleModuleList = new ArrayList<>();
70 | for (Map.Entry moduleEntry : rawAppBundle.getModules().entrySet())
71 | mergedBundleModuleList.add(mergeBundleModule(moduleEntry.getValue()));
72 | AppBundle mergedAppBundle = AppBundle.buildFromModules(
73 | mergedBundleModuleList.stream().collect(toImmutableList()),
74 | rawAppBundle.getBundleConfig(),
75 | rawAppBundle.getBundleMetadata()
76 | );
77 | System.out.printf(
78 | """
79 | removed duplicate resources done, took %s
80 | -----------------------------------------
81 | Reduce file count: %s
82 | Reduce file size: %s
83 | -----------------------------------------%n""",
84 | timeClock.getElapsedTime(), mergeDuplicatedTotalCount,
85 | FileOperation.getNetFileSizeDescription(mergeDuplicatedTotalSize)
86 | );
87 | return mergedAppBundle;
88 | }
89 |
90 | /**
91 | * Merges duplicated resources within a single module of the App Bundle, removing duplicates based on their MD5 hash values.
92 | * Generates a log file containing information about the merged resources and their original paths for the module.
93 | *
94 | * @param bundleModule The bundle module to process.
95 | * @return A modified bundle module with duplicated resources removed.
96 | * @throws IOException If there is an issue with reading files or bundle contents.
97 | */
98 | private BundleModule mergeBundleModule(@NotNull BundleModule bundleModule) throws IOException {
99 | File logFile = new File(outputLogLocationDir.toFile(), bundleModule.getName().getName() + DUPLICATE_LOGGER_FILE_SUFFIX);
100 | if (Files.exists(logFile.toPath())) {
101 | System.out.println("Log File Cleanup:");
102 | logger.warning("- Deleted existing log file: " + logFile.toPath());
103 | Files.delete(logFile.toPath());
104 | }
105 | Resources.ResourceTable table = bundleModule.getResourceTable().orElse(Resources.ResourceTable.getDefaultInstance());
106 | if (table.getPackageList().isEmpty() || bundleModule.getEntries().isEmpty())
107 | return bundleModule;
108 | md5FileList.clear();
109 | duplicatedFileList.clear();
110 | List mergedModuleEntry = new ArrayList<>();
111 | for (ModuleEntry entry : bundleModule.getEntries()) {
112 | if (!entry.getPath().startsWith(BundleModule.RESOURCES_DIRECTORY)) {
113 | mergedModuleEntry.add(entry);
114 | continue;
115 | }
116 | String md5 = AppBundleUtils.getEntryMd5(bundleZipFile, entry, bundleModule);
117 | if (md5FileList.containsKey(md5))
118 | duplicatedFileList.put(entry.getPath(), md5);
119 | else {
120 | md5FileList.put(md5, entry.getPath());
121 | mergedModuleEntry.add(entry);
122 | }
123 | }
124 | generateDuplicatedLog(logFile, bundleModule);
125 | Resources.ResourceTable mergedTable = mergeResourceTable(table);
126 | return bundleModule.toBuilder()
127 | .setResourceTable(mergedTable)
128 | .setRawEntries(mergedModuleEntry)
129 | .build();
130 | }
131 |
132 | /**
133 | * Merges the resource table of a module, removing duplicated resources based on their MD5 hash values.
134 | *
135 | * @param resourceTable The original resource table to be modified.
136 | * @return A modified resource table with duplicated resources removed.
137 | */
138 | private Resources.ResourceTable mergeResourceTable(Resources.ResourceTable resourceTable) {
139 | ResourceTableBuilder resourceTableBuilder = new ResourceTableBuilder();
140 | ResourcesUtils.entries(resourceTable).forEach(entry -> {
141 | ResourceTableBuilder.PackageBuilder packageBuilder = resourceTableBuilder.addPackage(entry.getPackage());
142 | // replace the duplicated path
143 | List configValues = getDuplicatedMergedConfigValues(entry.getEntry());
144 | Resources.Entry mergedEntry = ResourceTableOperation.updateEntryConfigValueList(entry.getEntry(), configValues);
145 | packageBuilder.addResource(entry.getType(), mergedEntry);
146 | });
147 | return resourceTableBuilder.build();
148 | }
149 |
150 | /**
151 | * Modifies the configuration values of duplicated resources within an entry, updating file paths if necessary.
152 | *
153 | * @param entry The entry containing configuration values to be modified.
154 | * @return A list of modified configuration values with updated file paths for duplicated resources.
155 | */
156 | private List getDuplicatedMergedConfigValues(Resources.@NotNull Entry entry) {
157 | return Stream.of(entry.getConfigValueList())
158 | .flatMap(Collection::stream)
159 | .map(configValue -> {
160 | if (!configValue.getValue().getItem().hasFile())
161 | return configValue;
162 | ZipPath zipPath = ZipPath.create(configValue.getValue().getItem().getFile().getPath());
163 | if (duplicatedFileList.containsKey(zipPath))
164 | zipPath = md5FileList.get(duplicatedFileList.get(zipPath));
165 | return ResourceTableOperation.replaceEntryPath(configValue, zipPath.toString());
166 | }).collect(Collectors.toList());
167 | }
168 |
169 | /**
170 | * Generates a log file containing information about duplicated resources and their original paths.
171 | *
172 | * @param logFile The file where the log information will be written.
173 | * @param bundleModule The bundle module containing the duplicated resources.
174 | * @throws IOException If there is an issue with writing the log file.
175 | */
176 | private void generateDuplicatedLog(@NotNull File logFile, BundleModule bundleModule) throws IOException {
177 | int duplicatedSize = 0;
178 | checkFileDoesNotExist(logFile.toPath());
179 | Writer writer = new BufferedWriter(new FileWriter(logFile, false));
180 | writer.write("res filter path mapping:\n");
181 | writer.flush();
182 | System.out.println("----------------------------------------");
183 | System.out.println(" Resource Duplication Detected:");
184 | System.out.println("----------------------------------------");
185 |
186 | for (Map.Entry entry : duplicatedFileList.entrySet()) {
187 | ModuleEntry moduleEntry = bundleModule.getEntry(entry.getKey()).get();
188 | long fileSize = AppBundleUtils.getZipEntrySize(bundleZipFile, moduleEntry, bundleModule);
189 | duplicatedSize += (int) fileSize;
190 | }
191 |
192 | System.out.printf("Found duplicated resources (Count: %d, Total Size: %s):\n%n", duplicatedFileList.size(), FileOperation.getNetFileSizeDescription(duplicatedSize));
193 | duplicatedSize = 0;
194 | for (Map.Entry entry : duplicatedFileList.entrySet()) {
195 | ZipPath keepPath = md5FileList.get(entry.getValue());
196 | ModuleEntry moduleEntry = bundleModule.getEntry(entry.getKey()).get();
197 | long fileSize = AppBundleUtils.getZipEntrySize(bundleZipFile, moduleEntry, bundleModule);
198 | duplicatedSize += (int) fileSize;
199 | System.out.printf("- %s (size %s)%n", entry.getKey().toString(), FileOperation.getNetFileSizeDescription(duplicatedSize));
200 | writer.write("\t" + entry.getKey().toString()
201 | + " -> "
202 | + keepPath.toString()
203 | + " (size " + FileOperation.getNetFileSizeDescription(fileSize) + ")"
204 | + "\n"
205 | );
206 | }
207 | writer.write("removed: count(" + duplicatedFileList.size() + "), totalSize(" + FileOperation.getNetFileSizeDescription(duplicatedSize) + ")");
208 | writer.close();
209 | mergeDuplicatedTotalSize += duplicatedSize;
210 | mergeDuplicatedTotalCount += duplicatedFileList.size();
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/model/DuplicateResMergerCommand.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.model;
2 |
3 | import com.google.auto.value.AutoValue;
4 | import org.jetbrains.annotations.Contract;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.util.Optional;
8 |
9 | /**
10 | * A command to configure merging of duplicated resources in the bundle.
11 | */
12 | @AutoValue
13 | public abstract class DuplicateResMergerCommand {
14 |
15 | /**
16 | * Create a new builder for configuring the DuplicateResMergerCommand.
17 | *
18 | * @return A new instance of the DuplicateResMergerCommand.Builder.
19 | */
20 | @Contract(" -> new")
21 | public static @NotNull Builder builder() {
22 | return new AutoValue_DuplicateResMergerCommand.Builder();
23 | }
24 |
25 | /**
26 | * Get the flag indicating whether to disable signing during resource merging.
27 | *
28 | * @return An optional boolean flag, which, if present, specifies whether signing should be disabled during resource merging.
29 | */
30 | public abstract Optional getDisableSign();
31 |
32 | /**
33 | * A builder for configuring the DuplicateResMergerCommand.
34 | */
35 | @AutoValue.Builder
36 | public abstract static class Builder {
37 |
38 | /**
39 | * Set the flag to disable signing during resource merging.
40 | *
41 | * @param disableSign If true, signing during resource merging will be disabled.
42 | * @return The builder instance for method chaining.
43 | */
44 | public abstract Builder setDisableSign(Boolean disableSign);
45 |
46 | /**
47 | * Build the DuplicateResMergerCommand instance with the configured options.
48 | *
49 | * @return The configured DuplicateResMergerCommand instance.
50 | */
51 | public abstract DuplicateResMergerCommand build();
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/model/FileFilterCommand.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.model;
2 |
3 | import com.google.auto.value.AutoValue;
4 | import org.jetbrains.annotations.Contract;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.util.Optional;
8 | import java.util.Set;
9 |
10 | /**
11 | * Represents a configuration command for file filtering in the ResChiper tool.
12 | * This class is immutable and uses the AutoValue library for code generation.
13 | */
14 | @AutoValue
15 | public abstract class FileFilterCommand {
16 |
17 | /**
18 | * Creates a new {@link Builder} instance to construct a {@link FileFilterCommand}.
19 | *
20 | * @return A new {@link Builder} instance.
21 | */
22 | @Contract(" -> new")
23 | public static @NotNull Builder builder() {
24 | return new AutoValue_FileFilterCommand.Builder();
25 | }
26 |
27 | /**
28 | * Get the set of file filtering rules.
29 | *
30 | * @return The set of file filtering rules.
31 | */
32 | public abstract Set getFileFilterRules();
33 |
34 | /**
35 | * Get an optional flag indicating whether file signing is disabled.
36 | *
37 | * @return An optional flag indicating whether file signing is disabled.
38 | */
39 | public abstract Optional getDisableSign();
40 |
41 | /**
42 | * Builder pattern for constructing {@link FileFilterCommand} instances.
43 | */
44 | @AutoValue.Builder
45 | public abstract static class Builder {
46 |
47 | /**
48 | * Set the file filtering rules for the command.
49 | *
50 | * @param fileFilterRules The set of file filtering rules.
51 | * @return This builder for method chaining.
52 | */
53 | public abstract Builder setFileFilterRules(Set fileFilterRules);
54 |
55 | /**
56 | * Set the flag indicating whether file signing should be disabled for the command.
57 | *
58 | * @param disableSign An optional flag indicating whether file signing is disabled.
59 | * @return This builder for method chaining.
60 | */
61 | public abstract Builder setDisableSign(Boolean disableSign);
62 |
63 | /**
64 | * Build a new {@link FileFilterCommand} instance with the configured properties.
65 | *
66 | * @return A new {@link FileFilterCommand} instance.
67 | */
68 | public abstract FileFilterCommand build();
69 | }
70 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/model/ObfuscateBundleCommand.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.model;
2 |
3 | import com.google.auto.value.AutoValue;
4 | import org.jetbrains.annotations.Contract;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.nio.file.Path;
8 | import java.util.Optional;
9 | import java.util.Set;
10 |
11 | /**
12 | * Represents a command responsible for obfuscating resources in an App Bundle.
13 | * This class is immutable and uses the AutoValue library for code generation.
14 | */
15 | @AutoValue
16 | public abstract class ObfuscateBundleCommand {
17 |
18 | /**
19 | * Creates a new {@link Builder} instance to construct an {@link ObfuscateBundleCommand}.
20 | *
21 | * @return A new {@link Builder} instance.
22 | */
23 | @Contract(" -> new")
24 | public static @NotNull Builder builder() {
25 | return new AutoValue_ObfuscateBundleCommand.Builder();
26 | }
27 |
28 | /**
29 | * Get the flag indicating whether obfuscation is enabled.
30 | *
31 | * @return A boolean flag indicating whether obfuscation is enabled.
32 | */
33 | public abstract Boolean getEnableObfuscate();
34 |
35 | /**
36 | * Get the resource obfuscation mode.
37 | *
38 | * @return A resource obfuscation mode, Mode are [dir, file, default].
39 | */
40 | public abstract String getObfuscationMode();
41 |
42 | /**
43 | * Get an optional path to the obfuscation mapping file.
44 | *
45 | * @return An optional path to the obfuscation mapping file.
46 | */
47 | public abstract Optional getMappingPath();
48 |
49 | /**
50 | * Get an optional flag indicating whether duplicated resources should be merged.
51 | *
52 | * @return An optional flag indicating whether duplicated resources should be merged.
53 | */
54 | public abstract Optional getMergeDuplicatedResources();
55 |
56 | /**
57 | * Get an optional flag indicating whether resource signing is disabled.
58 | *
59 | * @return An optional flag indicating whether resource signing is disabled.
60 | */
61 | public abstract Optional getDisableSign();
62 |
63 | /**
64 | * Get the set of white-listed resources that should not be obfuscated.
65 | *
66 | * @return The set of white-listed resources.
67 | */
68 | public abstract Set getWhiteList();
69 |
70 | /**
71 | * Get an optional set of file filtering rules.
72 | *
73 | * @return An optional set of file filtering rules.
74 | */
75 | public abstract Optional> getFileFilterRules();
76 |
77 | /**
78 | * Get an optional flag indicating whether file filtering is enabled.
79 | *
80 | * @return An optional flag indicating whether file filtering is enabled.
81 | */
82 | public abstract Optional getFilterFile();
83 |
84 | /**
85 | * Get an optional flag indicating whether string removal is enabled.
86 | *
87 | * @return An optional flag indicating whether string removal is enabled.
88 | */
89 | public abstract Optional getRemoveStr();
90 |
91 | /**
92 | * Get an optional path to the unused string resources file.
93 | *
94 | * @return An optional path to the unused string resources file.
95 | */
96 | public abstract Optional getUnusedStrPath();
97 |
98 | /**
99 | * Get an optional set of language white-lists for string filtering.
100 | *
101 | * @return An optional set of language white-lists.
102 | */
103 | public abstract Optional> getLanguageWhiteList();
104 |
105 | /**
106 | * Builder pattern for constructing {@link ObfuscateBundleCommand} instances.
107 | */
108 | @AutoValue.Builder
109 | public abstract static class Builder {
110 |
111 | /**
112 | * Set the flag indicating whether obfuscation is enabled.
113 | *
114 | * @param enable A boolean flag indicating whether obfuscation is enabled.
115 | * @return This builder instance for method chaining.
116 | */
117 | public abstract Builder setEnableObfuscate(Boolean enable);
118 |
119 | /**
120 | * Set the resource obfuscation mode.
121 | *
122 | * @param mode flag indicating to toggle resource obfuscation mode [dir, file, default].
123 | * @return This builder instance for method chaining.
124 | */
125 | public abstract Builder setObfuscationMode(String mode);
126 |
127 | /**
128 | * Set the set of white-listed resources that should not be obfuscated.
129 | *
130 | * @param whiteList The set of white-listed resources.
131 | * @return This builder instance for method chaining.
132 | */
133 | public abstract Builder setWhiteList(Set whiteList);
134 |
135 | /**
136 | * Set the flag indicating whether string removal is enabled.
137 | *
138 | * @param removeStr A boolean flag indicating whether string removal is enabled.
139 | * @return This builder instance for method chaining.
140 | */
141 | public abstract Builder setRemoveStr(Boolean removeStr);
142 |
143 | /**
144 | * Set the path to the unused string resources file.
145 | *
146 | * @param unusedStrPath The path to the unused string resources file.
147 | * @return This builder instance for method chaining.
148 | */
149 | public abstract Builder setUnusedStrPath(String unusedStrPath);
150 |
151 | /**
152 | * Set the set of language white-lists for string filtering.
153 | *
154 | * @param languageWhiteList The set of language white-lists.
155 | * @return This builder instance for method chaining.
156 | */
157 | public abstract Builder setLanguageWhiteList(Set languageWhiteList);
158 |
159 | /**
160 | * Set the flag indicating whether file filtering is enabled.
161 | *
162 | * @param filterFile A boolean flag indicating whether file filtering is enabled.
163 | * @return This builder instance for method chaining.
164 | */
165 | public abstract Builder setFilterFile(Boolean filterFile);
166 |
167 | /**
168 | * Set the set of file filtering rules.
169 | *
170 | * @param fileFilterRules The set of file filtering rules.
171 | * @return This builder instance for method chaining.
172 | */
173 | public abstract Builder setFileFilterRules(Set fileFilterRules);
174 |
175 | /**
176 | * Set the path to the obfuscation mapping file.
177 | *
178 | * @param mappingPath The path to the obfuscation mapping file.
179 | * @return This builder instance for method chaining.
180 | */
181 | public abstract Builder setMappingPath(Path mappingPath);
182 |
183 | /**
184 | * Set the flag indicating whether duplicated resources should be merged.
185 | *
186 | * @param mergeDuplicatedResources A boolean flag indicating whether duplicated resources should be merged.
187 | * @return This builder instance for method chaining.
188 | */
189 | public abstract Builder setMergeDuplicatedResources(Boolean mergeDuplicatedResources);
190 |
191 | /**
192 | * Set the flag indicating whether resource signing is disabled.
193 | *
194 | * @param disableSign A boolean flag indicating whether resource signing is disabled.
195 | * @return This builder instance for method chaining.
196 | */
197 | public abstract Builder setDisableSign(Boolean disableSign);
198 |
199 | /**
200 | * Build a new {@link ObfuscateBundleCommand} instance with the configured properties.
201 | *
202 | * @return A new {@link ObfuscateBundleCommand} instance.
203 | */
204 | public abstract ObfuscateBundleCommand build();
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/command/model/StringFilterCommand.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.command.model;
2 |
3 | import com.google.auto.value.AutoValue;
4 | import org.jetbrains.annotations.Contract;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.nio.file.Path;
8 | import java.util.Optional;
9 |
10 | /**
11 | * Represents a command responsible for filtering and processing string resources in an App Bundle.
12 | * This class is immutable and uses the AutoValue library for code generation.
13 | */
14 | @AutoValue
15 | public abstract class StringFilterCommand {
16 |
17 | /**
18 | * Creates a new {@link Builder} instance to construct a {@link StringFilterCommand}.
19 | *
20 | * @return A new {@link Builder} instance.
21 | */
22 | @Contract(" -> new")
23 | public static @NotNull Builder builder() {
24 | return new AutoValue_StringFilterCommand.Builder();
25 | }
26 |
27 | /**
28 | * Get an optional path to the configuration file for string filtering.
29 | *
30 | * @return An optional path to the configuration file for string filtering.
31 | */
32 | public abstract Optional getConfigPath();
33 |
34 | /**
35 | * Builder pattern for constructing {@link StringFilterCommand} instances.
36 | */
37 | @AutoValue.Builder
38 | public abstract static class Builder {
39 |
40 | /**
41 | * Set the path to the configuration file for string filtering.
42 | *
43 | * @param configPath The path to the configuration file.
44 | * @return This builder instance for method chaining.
45 | */
46 | public abstract Builder setConfigPath(Path configPath);
47 |
48 | /**
49 | * Build a new {@link StringFilterCommand} instance with the configured properties.
50 | *
51 | * @return A new {@link StringFilterCommand} instance.
52 | */
53 | public abstract StringFilterCommand build();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/internal/AGP.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.internal;
2 |
3 | import org.gradle.api.GradleException;
4 | import org.gradle.api.Project;
5 | import org.gradle.api.initialization.dsl.ScriptHandler;
6 | import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | public class AGP {
10 | public static @NotNull String getAGPVersion(@NotNull Project project) {
11 | String agpVersion = null;
12 | for (org.gradle.api.artifacts.ResolvedArtifact artifact : project.getRootProject().getBuildscript().getConfigurations().getByName(ScriptHandler.CLASSPATH_CONFIGURATION)
13 | .getResolvedConfiguration().getResolvedArtifacts()) {
14 | DefaultModuleComponentIdentifier identifier = (DefaultModuleComponentIdentifier) artifact.getId().getComponentIdentifier();
15 | if ("com.android.tools.build".equals(identifier.getGroup()) || 432891823 == identifier.getGroup().hashCode())
16 | if ("gradle".equals(identifier.getModule()))
17 | agpVersion = identifier.getVersion();
18 | }
19 | if (agpVersion == null)
20 | throw new GradleException("Failed to get AGP version");
21 | return agpVersion;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/internal/Bundle.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.internal;
2 |
3 | import com.android.build.gradle.api.ApplicationVariant;
4 | import org.gradle.api.Project;
5 | import org.gradle.api.Task;
6 | import org.jetbrains.annotations.NotNull;
7 | import org.jetbrains.annotations.Nullable;
8 |
9 | import java.io.File;
10 | import java.lang.reflect.InvocationTargetException;
11 | import java.nio.file.Path;
12 |
13 | public class Bundle {
14 | public static @NotNull Path getBundleFilePath(Project project, @NotNull ApplicationVariant variant) {
15 | String flavor = variant.getName();
16 | return getBundleFileForAGP(project, flavor).toPath();
17 | }
18 |
19 | public static @Nullable File getBundleFileForAGP(@NotNull Project project, String flavor) {
20 | Task finalizeBundleTask = project.getTasks().getByName("sign" + capitalize(flavor) + "Bundle");
21 | Object bundleFile = finalizeBundleTask.property("finalBundleFile");
22 | Object regularFile;
23 | try {
24 | if (bundleFile != null) {
25 | regularFile = bundleFile.getClass().getMethod("get").invoke(bundleFile);
26 | return (File) regularFile.getClass().getMethod("getAsFile").invoke(regularFile);
27 | } else
28 | return null;
29 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
30 | throw new RuntimeException(e);
31 | }
32 | }
33 |
34 | private static @NotNull String capitalize(@NotNull String str) {
35 | return Character.toUpperCase(str.charAt(0)) + str.substring(1);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/internal/SigningConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.internal;
2 |
3 | import com.android.build.gradle.api.ApplicationVariant;
4 | import io.github.goldfish07.reschiper.plugin.model.KeyStore;
5 | import org.jetbrains.annotations.Contract;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | public class SigningConfig {
9 | @Contract("_ -> new")
10 | public static @NotNull KeyStore getSigningConfig(@NotNull ApplicationVariant variant) {
11 | return new KeyStore(
12 | variant.getSigningConfig().getStoreFile(),
13 | variant.getSigningConfig().getStorePassword(),
14 | variant.getSigningConfig().getKeyAlias(),
15 | variant.getSigningConfig().getKeyPassword()
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/model/KeyStore.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.model;
2 |
3 | import java.io.File;
4 |
5 | /**
6 | * Represents a keystore containing a cryptographic key pair for signing purposes.
7 | */
8 | public record KeyStore(File storeFile, String storePassword, String keyAlias, String keyPassword) {
9 | /**
10 | * Constructs a new KeyStore with the provided parameters.
11 | *
12 | * @param storeFile The keystore file.
13 | * @param storePassword The password for the keystore.
14 | * @param keyAlias The alias for the key within the keystore.
15 | * @param keyPassword The password for the key.
16 | */
17 | public KeyStore(File storeFile, String storePassword, String keyAlias, String keyPassword) {
18 | this.storeFile = storeFile;
19 | this.storePassword = storePassword;
20 | this.keyAlias = keyAlias;
21 | this.keyPassword = keyPassword;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/obfuscation/StringObfuscator.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.obfuscation;
2 |
3 | import io.github.goldfish07.reschiper.plugin.utils.Utils;
4 |
5 | import java.util.*;
6 | import java.util.regex.Pattern;
7 |
8 | /**
9 | * A utility class for generating obfuscated replacement strings.
10 | */
11 | public class StringObfuscator {
12 |
13 | private final List replaceStringBuffer;
14 | private final Set isReplaced;
15 | private final Set isWhiteList;
16 |
17 | private static final String[] A_TO_Z = {
18 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
19 | "w", "x", "y", "z"
20 | };
21 | private static final String[] A_TO_ALL = {
22 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
23 | "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
24 | };
25 | private static final Set FILE_NAME_BLACKLIST = new HashSet<>(Arrays.asList("con", "prn", "aux", "nul"));
26 | private static final int MAX_OBFUSCATION_LIMIT = 35594;
27 |
28 | /**
29 | * Initializes a new instance of the StringObfuscator class.
30 | */
31 | public StringObfuscator() {
32 | replaceStringBuffer = new ArrayList<>();
33 | isReplaced = new HashSet<>();
34 | isWhiteList = new HashSet<>();
35 | }
36 |
37 | /**
38 | * Resets the state of the StringObfuscator with the provided blacklist patterns.
39 | *
40 | * @param blacklistPatterns A set of regular expression patterns for blacklisted strings.
41 | */
42 | public void reset(HashSet blacklistPatterns) {
43 | replaceStringBuffer.clear();
44 | isReplaced.clear();
45 | isWhiteList.clear();
46 |
47 | for (String str : A_TO_Z)
48 | if (Utils.match(str, blacklistPatterns))
49 | replaceStringBuffer.add(str);
50 |
51 | for (String first : A_TO_Z)
52 | for (String aMAToAll : A_TO_ALL) {
53 | String str = first + aMAToAll;
54 | if (Utils.match(str, blacklistPatterns))
55 | replaceStringBuffer.add(str);
56 | }
57 |
58 | for (String first : A_TO_Z)
59 | for (String second : A_TO_ALL)
60 | for (String third : A_TO_ALL) {
61 | String str = first + second + third;
62 | if (!FILE_NAME_BLACKLIST.contains(str) && Utils.match(str, blacklistPatterns))
63 | replaceStringBuffer.add(str);
64 | }
65 | }
66 |
67 | /**
68 | * Removes a collection of strings from the replacement buffer.
69 | *
70 | * @param collection The collection of strings to remove.
71 | */
72 | public void removeStrings(Collection collection) {
73 | if (collection == null)
74 | return;
75 | replaceStringBuffer.removeAll(collection);
76 | }
77 |
78 | /**
79 | * Checks if a specific identifier has been replaced.
80 | *
81 | * @param id The identifier to check.
82 | * @return True if the identifier has been replaced; otherwise, false.
83 | */
84 | public boolean isReplaced(int id) {
85 | return isReplaced.contains(id);
86 | }
87 |
88 | /**
89 | * Checks if a specific identifier is in the white list.
90 | *
91 | * @param id The identifier to check.
92 | * @return True if the identifier is in the white list; otherwise, false.
93 | */
94 | public boolean isInWhiteList(int id) {
95 | return isWhiteList.contains(id);
96 | }
97 |
98 | /**
99 | * Adds an identifier to the white list.
100 | *
101 | * @param id The identifier to add to the white list.
102 | */
103 | public void setInWhiteList(int id) {
104 | isWhiteList.add(id);
105 | }
106 |
107 | /**
108 | * Adds an identifier to the replacement list.
109 | *
110 | * @param id The identifier to add to the replacement list.
111 | */
112 | public void setInReplaceList(int id) {
113 | isReplaced.add(id);
114 | }
115 |
116 | /**
117 | * Gets a replacement string from the buffer based on the provided names.
118 | *
119 | * @param names A collection of names to exclude from the replacements.
120 | * @return The replacement string.
121 | * @throws IllegalArgumentException If the replacement buffer is empty.
122 | */
123 | public String getReplaceString(Collection names) throws IllegalArgumentException {
124 | if (replaceStringBuffer.isEmpty())
125 | throw new IllegalArgumentException("Now can only obfuscate up to " + MAX_OBFUSCATION_LIMIT + " in a single type");
126 | if (names != null)
127 | for (int i = 0; i < replaceStringBuffer.size(); i++) {
128 | String name = replaceStringBuffer.get(i);
129 | if (names.contains(name))
130 | continue;
131 | return replaceStringBuffer.remove(i);
132 | }
133 | return replaceStringBuffer.remove(0);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/operations/FileOperation.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.operations;
2 |
3 | import com.android.tools.build.bundletool.model.ZipPath;
4 | import com.android.tools.build.bundletool.model.utils.ZipUtils;
5 | import com.android.tools.build.bundletool.model.utils.files.FileUtils;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | import java.io.*;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.text.DecimalFormat;
12 | import java.util.Enumeration;
13 | import java.util.logging.Level;
14 | import java.util.logging.Logger;
15 | import java.util.zip.ZipEntry;
16 | import java.util.zip.ZipFile;
17 |
18 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
19 |
20 | /**
21 | * Utility class for various file operations.
22 | */
23 | public class FileOperation {
24 | private static final Logger logger = Logger.getLogger(FileOperation.class.getName());
25 | private static final int BUFFER = 8192;
26 |
27 | /**
28 | * Recursively deletes a directory and its contents.
29 | *
30 | * @param file The directory to delete.
31 | * @return true if the directory was successfully deleted, false otherwise.
32 | */
33 | public static boolean deleteDir(File file) {
34 | if (file == null || (!file.exists())) {
35 | return false;
36 | }
37 | if (file.isFile()) {
38 | file.delete();
39 | } else if (file.isDirectory()) {
40 | File[] files = file.listFiles();
41 | if (files != null) {
42 | for (File value : files) {
43 | deleteDir(value);
44 | }
45 | }
46 | }
47 | file.delete();
48 | return true;
49 | }
50 |
51 | /**
52 | * Uncompressed a ZIP file to a target directory.
53 | *
54 | * @param uncompressedFile The ZIP file to uncompress.
55 | * @param targetDir The target directory to extract the contents.
56 | * @throws IOException If an I/O error occurs during the uncompressed.
57 | */
58 | public static void uncompress(Path uncompressedFile, Path targetDir) throws IOException {
59 | checkFileExistsAndReadable(uncompressedFile);
60 | if (Files.exists(targetDir)) {
61 | targetDir.toFile().delete();
62 | } else {
63 | FileUtils.createDirectories(targetDir);
64 | }
65 | ZipFile zipFile = new ZipFile(uncompressedFile.toFile());
66 | try (zipFile) {
67 | Enumeration extends ZipEntry> emu = zipFile.entries();
68 | while (emu.hasMoreElements()) {
69 | ZipEntry entry = emu.nextElement();
70 | if (entry.isDirectory()) {
71 | FileUtils.createDirectories(new File(targetDir.toFile(), entry.getName()).toPath());
72 | continue;
73 | }
74 | BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
75 | File file = new File(targetDir.toFile() + File.separator + entry.getName());
76 | File parent = file.getParentFile();
77 | if (parent != null && (!parent.exists())) {
78 | FileUtils.createDirectories(parent.toPath());
79 | }
80 | FileOutputStream fos = new FileOutputStream(file);
81 | BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER);
82 | byte[] buf = new byte[BUFFER];
83 | int len;
84 | while ((len = bis.read(buf, 0, BUFFER)) != -1) {
85 | fos.write(buf, 0, len);
86 | }
87 | bos.flush();
88 | bos.close();
89 | bis.close();
90 | }
91 | }
92 | }
93 |
94 | /**
95 | * Gets a human-readable file size description from a file size in bytes.
96 | *
97 | * @param size The file size in bytes.
98 | * @return A string representing the file size with appropriate units (B, KB, MB, GB).
99 | */
100 | public static @NotNull String getNetFileSizeDescription(long size) {
101 | StringBuilder bytes = new StringBuilder();
102 | DecimalFormat format = new DecimalFormat("###.0");
103 | if (size >= 1024 * 1024 * 1024) {
104 | double i = (size / (1024.0 * 1024.0 * 1024.0));
105 | bytes.append(format.format(i)).append("GB");
106 | } else if (size >= 1024 * 1024) {
107 | double i = (size / (1024.0 * 1024.0));
108 | bytes.append(format.format(i)).append("MB");
109 | } else if (size >= 1024) {
110 | double i = (size / (1024.0));
111 | bytes.append(format.format(i)).append("KB");
112 | } else {
113 | if (size <= 0) {
114 | bytes.append("0B");
115 | } else {
116 | bytes.append((int) size).append("B");
117 | }
118 | }
119 | return bytes.toString();
120 | }
121 |
122 | /**
123 | * Gets the size of a file in bytes.
124 | *
125 | * @param f The file to get the size of.
126 | * @return The file size in bytes.
127 | */
128 | public static long getFileSizes(@NotNull File f) {
129 | long size = 0;
130 | if (f.exists() && f.isFile()) {
131 | FileInputStream fis = null;
132 | try {
133 | fis = new FileInputStream(f);
134 | size = fis.available();
135 | } catch (IOException e) {
136 | logger.log(Level.WARNING, "Unable to get FileSize", e);
137 | } finally {
138 | try {
139 | if (fis != null) {
140 | fis.close();
141 | }
142 | } catch (IOException e) {
143 | logger.log(Level.WARNING, "Unable to get file size", e);
144 | }
145 | }
146 | }
147 | return size;
148 | }
149 |
150 | /**
151 | * Gets the size of a file within a ZIP archive.
152 | *
153 | * @param zipFile The ZIP file.
154 | * @param zipEntry The ZIP entry representing the file.
155 | * @return The size of the file in bytes.
156 | */
157 | public static long getZipPathFileSize(ZipFile zipFile, ZipEntry zipEntry) {
158 | long size = 0;
159 | //todo changed
160 | try {
161 | InputStream is = ZipUtils.asByteSource(zipFile, zipEntry).openStream();
162 | size = is.available();
163 | is.close();
164 | } catch (IOException e) {
165 | logger.log(Level.WARNING, "Unable to get ZipPath file size", e);
166 | }
167 | return size;
168 | }
169 |
170 | /**
171 | * Copies a file from a source location to a destination location using streams.
172 | *
173 | * @param source The source file to copy.
174 | * @param dest The destination file.
175 | * @throws IOException If an I/O error occurs during the copying process.
176 | */
177 | public static void copyFileUsingStream(File source, @NotNull File dest) throws IOException {
178 | FileInputStream is = null;
179 | FileOutputStream os = null;
180 | File parent = dest.getParentFile();
181 | if (parent != null && (!parent.exists())) {
182 | parent.mkdirs();
183 | }
184 | try {
185 | is = new FileInputStream(source);
186 | os = new FileOutputStream(dest, false);
187 |
188 | byte[] buffer = new byte[BUFFER];
189 | int length;
190 | while ((length = is.read(buffer)) > 0) {
191 | os.write(buffer, 0, length);
192 | }
193 | } finally {
194 | if (is != null) {
195 | is.close();
196 | }
197 | if (os != null) {
198 | os.close();
199 | }
200 | }
201 | }
202 |
203 | /**
204 | * Gets the simple name of a file from a ZipPath.
205 | *
206 | * @param zipPath The ZipPath representing the file.
207 | * @return The simple name of the file.
208 | */
209 | public static @NotNull String getFileSimpleName(@NotNull ZipPath zipPath) {
210 | return zipPath.getFileName().toString();
211 | }
212 |
213 | /**
214 | * Gets the file suffix (extension) from a ZipPath.
215 | *
216 | * @param zipPath The ZipPath representing the file.
217 | * @return The file suffix (extension).
218 | */
219 | public static @NotNull String getFileSuffix(@NotNull ZipPath zipPath) {
220 | String fileName = zipPath.getName(zipPath.getNameCount() - 1).toString();
221 | if (!fileName.contains(".")) {
222 | return fileName;
223 | }
224 | String[] values = fileName.replace(".", "/").split("/");
225 | return fileName.substring(values[0].length());
226 | }
227 |
228 | /**
229 | * Gets the parent directory path from a Zip file path.
230 | *
231 | * @param zipPath The Zip file path.
232 | * @return The parent directory path.
233 | */
234 | public static @NotNull String getParentFromZipFilePath(@NotNull String zipPath) {
235 | if (!zipPath.contains("/")) {
236 | throw new IllegalArgumentException("invalid zipPath: " + zipPath);
237 | }
238 | String[] values = zipPath.split("/");
239 | return zipPath.substring(0, zipPath.indexOf(values[values.length - 1]) - 1);
240 | }
241 |
242 | /**
243 | * Gets the name of a file from a Zip file path.
244 | *
245 | * @param zipPath The Zip file path.
246 | * @return The file name.
247 | */
248 | public static String getNameFromZipFilePath(@NotNull String zipPath) {
249 | if (!zipPath.contains("/")) {
250 | throw new IllegalArgumentException("invalid zipPath: " + zipPath);
251 | }
252 | String[] values = zipPath.split("/");
253 | return values[values.length - 1];
254 | }
255 |
256 | /**
257 | * Gets the file prefix (name without extension) from a file name.
258 | *
259 | * @param fileName The file name.
260 | * @return The file prefix.
261 | */
262 | public static String getFilePrefixByFileName(@NotNull String fileName) {
263 | if (!fileName.contains(".")) {
264 | throw new IllegalArgumentException("invalid file name: " + fileName);
265 | }
266 | String[] values = fileName.replace(".", "/").split("/");
267 | return values[0];
268 | }
269 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/operations/NativeLibrariesOperation.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.operations;
2 |
3 | import com.android.bundle.Files;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | /**
7 | * Utility class for working with Native Libraries in Android App Bundles.
8 | */
9 | public class NativeLibrariesOperation {
10 |
11 | /**
12 | * Removes a directory from Native Libraries if it exists.
13 | *
14 | * @param nativeLibraries The NativeLibraries object to modify.
15 | * @param zipPath The path of the directory to remove.
16 | * @return The modified NativeLibraries object.
17 | */
18 | public static Files.NativeLibraries removeDirectory(Files.@NotNull NativeLibraries nativeLibraries, String zipPath) {
19 | int index = -1;
20 | for (int i = 0; i < nativeLibraries.getDirectoryList().size(); i++) {
21 | Files.TargetedNativeDirectory directory = nativeLibraries.getDirectoryList().get(i);
22 | if (directory.getPath().equals(zipPath)) {
23 | index = i;
24 | break;
25 | }
26 | }
27 | if (index == -1)
28 | return nativeLibraries;
29 | return nativeLibraries.toBuilder().removeDirectory(index).build();
30 | }
31 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/operations/ResourceTableOperation.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.operations;
2 |
3 | import com.android.aapt.Resources;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.util.HashSet;
7 | import java.util.List;
8 | import java.util.Set;
9 |
10 | /**
11 | * Utility class for operations on Android resource tables.
12 | */
13 | public class ResourceTableOperation {
14 |
15 | /**
16 | * Replaces the entry path in a ConfigValue.
17 | *
18 | * @param configValue The ConfigValue to update.
19 | * @param path The new path to set.
20 | * @return The updated ConfigValue.
21 | */
22 | public static Resources.@NotNull ConfigValue replaceEntryPath(Resources.@NotNull ConfigValue configValue, String path) {
23 | Resources.ConfigValue.Builder entryBuilder = configValue.toBuilder();
24 | entryBuilder.setValue(
25 | configValue.getValue().toBuilder().setItem(
26 | configValue.getValue().getItem().toBuilder().setFile(
27 | configValue.getValue().getItem().getFile().toBuilder().setPath(path).build()
28 | ).build()
29 | ).build()
30 | );
31 | return entryBuilder.build();
32 | }
33 |
34 | /**
35 | * Updates the configuration values list for an Entry.
36 | *
37 | * @param entry The Entry to update.
38 | * @param configValueList The new list of ConfigValues.
39 | * @return The updated Entry.
40 | */
41 | public static Resources.@NotNull Entry updateEntryConfigValueList(Resources.@NotNull Entry entry, List configValueList) {
42 | Resources.Entry.Builder entryBuilder = entry.toBuilder();
43 | entryBuilder.clearConfigValue();
44 | entryBuilder.addAllConfigValue(configValueList);
45 | return entryBuilder.build();
46 | }
47 |
48 | /**
49 | * Updates the name of an Entry.
50 | *
51 | * @param entry The Entry to update.
52 | * @param name The new name to set.
53 | * @return The updated Entry.
54 | */
55 | public static Resources.@NotNull Entry updateEntryName(Resources.@NotNull Entry entry, String name) {
56 | Resources.Entry.Builder builder = entry.toBuilder();
57 | builder.setName(name);
58 | return builder.build();
59 | }
60 |
61 | /**
62 | * Checks for duplicate configurations in an Entry.
63 | *
64 | * @param entry The Entry to check.
65 | * @throws IllegalArgumentException if duplicate configurations are found.
66 | */
67 | public static void checkConfiguration(Resources.@NotNull Entry entry) {
68 | if (entry.getConfigValueCount() == 0)
69 | return;
70 | Set configValues = new HashSet<>();
71 | for (Resources.ConfigValue configValue : entry.getConfigValueList()) {
72 | if (configValues.contains(configValue))
73 | throw new IllegalArgumentException("Duplicate configuration for entry: " + entry.getName());
74 | configValues.add(configValue);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/parser/Parser.java:
--------------------------------------------------------------------------------
1 |
2 | package io.github.goldfish07.reschiper.plugin.parser;
3 |
4 | import io.github.goldfish07.reschiper.plugin.parser.xml.FileFilterConfig;
5 | import io.github.goldfish07.reschiper.plugin.parser.xml.ResChiperConfig;
6 | import io.github.goldfish07.reschiper.plugin.parser.xml.StringFilterConfig;
7 | import org.dom4j.Document;
8 | import org.dom4j.DocumentException;
9 | import org.dom4j.Element;
10 | import org.dom4j.io.SAXReader;
11 |
12 | import java.nio.file.Path;
13 | import java.util.Iterator;
14 |
15 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
16 |
17 | /**
18 | * The Parser class provides methods for parsing configuration files and extracting specific settings.
19 | */
20 | public class Parser {
21 |
22 | /**
23 | * The XML class provides methods to parse XML configuration files and extract specific settings.
24 | */
25 | public static class XML {
26 |
27 | private final Path configPath;
28 |
29 | /**
30 | * Constructs an XML parser with the specified configuration file path.
31 | *
32 | * @param configPath The path to the XML configuration files to be parsed.
33 | */
34 | public XML(Path configPath) {
35 | checkFileExistsAndReadable(configPath);
36 | this.configPath = configPath;
37 | }
38 |
39 | /**
40 | * Parses a file filter configuration from the XML document.
41 | *
42 | * @return A FileFilterConfig object containing file filter settings.
43 | * @throws DocumentException If there is an issue parsing the XML document.
44 | */
45 | public FileFilterConfig fileFilterParse() throws DocumentException {
46 | FileFilterConfig fileFilter = new FileFilterConfig();
47 | SAXReader reader = new SAXReader();
48 | Document doc = reader.read(configPath.toFile());
49 | Element root = doc.getRootElement();
50 | for (Iterator i = root.elementIterator("filter"); i.hasNext(); ) {
51 | Element element = i.next();
52 | String isActiveValue = element.attributeValue("isactive");
53 | if (isActiveValue != null && isActiveValue.equals("true"))
54 | fileFilter.setActive(true);
55 | for (Iterator rules = element.elementIterator("rule"); rules.hasNext(); ) {
56 | Element ruleElement = rules.next();
57 | String rule = ruleElement.attributeValue("value");
58 | if (rule != null)
59 | fileFilter.addRule(rule);
60 | }
61 | }
62 | return fileFilter;
63 | }
64 |
65 | /**
66 | * Parses a ResChiper configuration from the XML document.
67 | *
68 | * @return A ResChiperConfig object containing ResChiper settings.
69 | * @throws DocumentException If there is an issue parsing the XML document.
70 | */
71 | public ResChiperConfig resChiperParse() throws DocumentException {
72 | ResChiperConfig resChiperConfig = new ResChiperConfig();
73 | SAXReader reader = new SAXReader();
74 | Document doc = reader.read(configPath.toFile());
75 | Element root = doc.getRootElement();
76 | for (Iterator i = root.elementIterator("issue"); i.hasNext(); ) {
77 | Element element = i.next();
78 | String id = element.attributeValue("id");
79 | if (id == null || !id.equals("whitelist"))
80 | continue;
81 | String isActive = element.attributeValue("isactive");
82 | if (isActive != null && isActive.equals("true"))
83 | resChiperConfig.setUseWhiteList(true);
84 | for (Iterator rules = element.elementIterator("path"); rules.hasNext(); ) {
85 | Element ruleElement = rules.next();
86 | String rule = ruleElement.attributeValue("value");
87 | if (rule != null)
88 | resChiperConfig.addWhiteList(rule);
89 | }
90 | }
91 | // File filter
92 | resChiperConfig.setFileFilter(fileFilterParse());
93 | // String filter
94 | resChiperConfig.setStringFilterConfig(stringFilterParse());
95 | return resChiperConfig;
96 | }
97 |
98 | /**
99 | * Parses a StringFilterConfig from the XML document.
100 | *
101 | * @return A StringFilterConfig object containing string filter settings.
102 | * @throws DocumentException If there is an issue parsing the XML document.
103 | */
104 | public StringFilterConfig stringFilterParse() throws DocumentException {
105 | StringFilterConfig config = new StringFilterConfig();
106 | SAXReader reader = new SAXReader();
107 | Document doc = reader.read(configPath.toFile());
108 | Element root = doc.getRootElement();
109 | for (Iterator i = root.elementIterator("filter-str"); i.hasNext(); ) {
110 | Element element = i.next();
111 | String isActive = element.attributeValue("isactive");
112 | if (isActive != null && isActive.equalsIgnoreCase("true"))
113 | config.setActive(true);
114 | for (Iterator rules = element.elementIterator("path"); rules.hasNext(); ) {
115 | Element ruleElement = rules.next();
116 | String path = ruleElement.attributeValue("value");
117 | config.setPath(path);
118 | }
119 | for (Iterator rules = element.elementIterator("language"); rules.hasNext(); ) {
120 | Element ruleElement = rules.next();
121 | String path = ruleElement.attributeValue("value");
122 | config.getLanguageWhiteList().add(path);
123 | }
124 | }
125 | return config;
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/parser/ResourcesMappingParser.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.parser;
2 |
3 | import io.github.goldfish07.reschiper.plugin.bundle.ResourceMapping;
4 |
5 | import java.io.BufferedReader;
6 | import java.io.FileReader;
7 | import java.io.IOException;
8 | import java.nio.file.Path;
9 | import java.util.regex.Matcher;
10 | import java.util.regex.Pattern;
11 |
12 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
13 |
14 | /**
15 | * This class is responsible for parsing a resource mapping file used for resource obfuscation
16 | * in Android development and populating a {@link ResourceMapping} object with the mappings.
17 | */
18 | public class ResourcesMappingParser {
19 | private static final Pattern MAP_DIR_PATTERN = Pattern.compile("^\\s+(.*)->(.*)");
20 | private static final Pattern MAP_RES_PATTERN = Pattern.compile("^\\s+(.*):(.*)->(.*)");
21 | private final Path mappingPath;
22 |
23 | /**
24 | * Constructs a new ResourcesMappingParser with the specified mapping file path.
25 | *
26 | * @param mappingPath The path to the resource mapping file.
27 | * @throws IllegalArgumentException If the mapping file does not exist or is not readable.
28 | */
29 | public ResourcesMappingParser(Path mappingPath) {
30 | checkFileExistsAndReadable(mappingPath);
31 | this.mappingPath = mappingPath;
32 | }
33 |
34 | /**
35 | * Parses the resource mapping file and returns a populated {@link ResourceMapping} object.
36 | *
37 | * @return A {@link ResourceMapping} object containing the parsed resource mappings.
38 | * @throws IOException If an I/O error occurs while reading the mapping file.
39 | */
40 | public ResourceMapping parse() throws IOException {
41 | ResourceMapping mapping = new ResourceMapping();
42 | FileReader fr = new FileReader(mappingPath.toFile());
43 | BufferedReader br = new BufferedReader(fr);
44 | String line = br.readLine();
45 | while (line != null) {
46 | if (line.isEmpty()) {
47 | line = br.readLine();
48 | continue;
49 | }
50 | System.out.println("Res: " + line);
51 | if (!line.contains(":")) {
52 | Matcher mat = MAP_DIR_PATTERN.matcher(line);
53 | if (mat.find()) {
54 | String rawName = mat.group(1).trim();
55 | String obfuscateName = mat.group(2).trim();
56 | if (!line.contains("/") || line.contains("."))
57 | throw new IllegalArgumentException("Unexpected resource dir: " + line);
58 | mapping.putDirMapping(rawName, obfuscateName);
59 | }
60 | } else {
61 | Matcher mat = MAP_RES_PATTERN.matcher(line);
62 | if (mat.find()) {
63 | String rawName = mat.group(2).trim();
64 | String obfuscateName = mat.group(3).trim();
65 | if (line.contains("/"))
66 | mapping.putEntryFileMapping(rawName, obfuscateName);
67 | else {
68 | int packagePos = rawName.indexOf(".R.");
69 | if (packagePos == -1)
70 | throw new IllegalArgumentException(String.format("the mapping file packageName is malformed, "
71 | + "it should be like com.github.goldfish07.ugc.R.attr.test, yours %s\n", rawName));
72 | mapping.putResourceMapping(rawName, obfuscateName);
73 | }
74 | }
75 | }
76 | line = br.readLine();
77 | }
78 | br.close();
79 | fr.close();
80 | return mapping;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/parser/xml/FileFilterConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.parser.xml;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * The `FileFilterConfig` class represents a configuration for filtering files based on a set of rules.
8 | * It allows specifying whether the filter is active and maintains a set of rules for file filtering.
9 | */
10 | public class FileFilterConfig {
11 | private final Set rules = new HashSet<>();
12 | private boolean isActive;
13 |
14 | /**
15 | * Checks if the file filter is active.
16 | *
17 | * @return `true` if the filter is active, `false` otherwise.
18 | */
19 | public boolean isActive() {
20 | return isActive;
21 | }
22 |
23 | /**
24 | * Sets the activity status of the file filter.
25 | *
26 | * @param active `true` to activate the filter, `false` to deactivate it.
27 | */
28 | public void setActive(boolean active) {
29 | isActive = active;
30 | }
31 |
32 | /**
33 | * Gets the set of rules used for file filtering.
34 | *
35 | * @return The set of rules for file filtering.
36 | */
37 | public Set getRules() {
38 | return rules;
39 | }
40 |
41 | /**
42 | * Adds a rule to the set of rules for file filtering.
43 | *
44 | * @param rule The rule to add.
45 | */
46 | public void addRule(String rule) {
47 | rules.add(rule);
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/parser/xml/ResChiperConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.parser.xml;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * The `ResChiperConfig` class represents the configuration for the Resource Chiper tool.
8 | * It includes settings related to file filtering, string filtering, and a whitelist of rules.
9 | */
10 | public class ResChiperConfig {
11 | private final Set whiteList = new HashSet<>();
12 | private FileFilterConfig fileFilter;
13 | private StringFilterConfig stringFilterConfig;
14 | private boolean useWhiteList;
15 |
16 | /**
17 | * Gets the file filter configuration for the Resource Chiper tool.
18 | *
19 | * @return The file filter configuration.
20 | */
21 | public FileFilterConfig getFileFilter() {
22 | return fileFilter;
23 | }
24 |
25 | /**
26 | * Sets the file filter configuration for the Resource Chiper tool.
27 | *
28 | * @param fileFilter The file filter configuration to set.
29 | */
30 | public void setFileFilter(FileFilterConfig fileFilter) {
31 | this.fileFilter = fileFilter;
32 | }
33 |
34 | /**
35 | * Checks if the whitelist is active for the Resource Chiper tool.
36 | *
37 | * @return `true` if the whitelist is active, `false` otherwise.
38 | */
39 | public boolean isUseWhiteList() {
40 | return useWhiteList;
41 | }
42 |
43 | /**
44 | * Sets the whitelist activity status for the Resource Chiper tool.
45 | *
46 | * @param useWhiteList `true` to activate the whitelist, `false` to deactivate it.
47 | */
48 | public void setUseWhiteList(boolean useWhiteList) {
49 | this.useWhiteList = useWhiteList;
50 | }
51 |
52 | /**
53 | * Gets the whitelist of rules used for resource filtering.
54 | *
55 | * @return The set of whitelist rules.
56 | */
57 | public Set getWhiteList() {
58 | return whiteList;
59 | }
60 |
61 | /**
62 | * Adds a rule to the whitelist for resource filtering.
63 | *
64 | * @param whiteRule The rule to add to the whitelist.
65 | */
66 | public void addWhiteList(String whiteRule) {
67 | this.whiteList.add(whiteRule);
68 | }
69 |
70 | /**
71 | * Gets the string filter configuration for the Resource Chiper tool.
72 | *
73 | * @return The string filter configuration.
74 | */
75 | public StringFilterConfig getStringFilterConfig() {
76 | return stringFilterConfig;
77 | }
78 |
79 | /**
80 | * Sets the string filter configuration for the Resource Chiper tool.
81 | *
82 | * @param stringFilterConfig The string filter configuration to set.
83 | */
84 | public void setStringFilterConfig(StringFilterConfig stringFilterConfig) {
85 | this.stringFilterConfig = stringFilterConfig;
86 | }
87 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/parser/xml/StringFilterConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.parser.xml;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * The `StringFilterConfig` class represents the configuration for filtering strings in resources.
8 | * It allows specifying whether the string filter is active, a custom path, and a whitelist of languages.
9 | */
10 | public class StringFilterConfig {
11 | private final Set languageWhiteList = new HashSet<>();
12 | private boolean isActive;
13 | private String path = "";
14 |
15 | /**
16 | * Checks if the string filter is active.
17 | *
18 | * @return `true` if the filter is active, `false` otherwise.
19 | */
20 | public boolean isActive() {
21 | return isActive;
22 | }
23 |
24 | /**
25 | * Sets the activity status of the string filter.
26 | *
27 | * @param active `true` to activate the filter, `false` to deactivate it.
28 | */
29 | public void setActive(boolean active) {
30 | isActive = active;
31 | }
32 |
33 | /**
34 | * Gets the custom path used for filtering strings.
35 | *
36 | * @return The custom path for string filtering.
37 | */
38 | public String getPath() {
39 | return path;
40 | }
41 |
42 | /**
43 | * Sets the custom path for filtering strings.
44 | *
45 | * @param path The custom path to set.
46 | */
47 | public void setPath(String path) {
48 | this.path = path;
49 | }
50 |
51 | /**
52 | * Gets the whitelist of languages used for string filtering.
53 | *
54 | * @return The set of whitelisted languages for string filtering.
55 | */
56 | public Set getLanguageWhiteList() {
57 | return languageWhiteList;
58 | }
59 |
60 | /**
61 | * Returns a string representation of the `StringFilterConfig` object.
62 | *
63 | * @return A string representation of the object's properties.
64 | */
65 | @Override
66 | public String toString() {
67 | return "StringFilterConfig{" +
68 | "isActive=" + isActive +
69 | ", path='" + path + '\'' +
70 | ", languageWhiteList=" + languageWhiteList +
71 | '}';
72 | }
73 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/tasks/ResChiperTask.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.tasks;
2 |
3 | import com.android.build.gradle.api.ApplicationVariant;
4 | import io.github.goldfish07.reschiper.plugin.command.Command;
5 | import io.github.goldfish07.reschiper.plugin.command.model.DuplicateResMergerCommand;
6 | import io.github.goldfish07.reschiper.plugin.command.model.FileFilterCommand;
7 | import io.github.goldfish07.reschiper.plugin.command.model.ObfuscateBundleCommand;
8 | import io.github.goldfish07.reschiper.plugin.command.model.StringFilterCommand;
9 | import io.github.goldfish07.reschiper.plugin.Extension;
10 | import io.github.goldfish07.reschiper.plugin.model.KeyStore;
11 | import io.github.goldfish07.reschiper.plugin.internal.Bundle;
12 | import io.github.goldfish07.reschiper.plugin.internal.SigningConfig;
13 | import org.gradle.api.DefaultTask;
14 | import org.gradle.api.tasks.TaskAction;
15 | import org.jetbrains.annotations.NotNull;
16 |
17 | import java.io.File;
18 | import java.nio.file.Path;
19 | import java.util.logging.Level;
20 | import java.util.logging.Logger;
21 |
22 | /**
23 | * Custom Gradle task for running ResChiper.
24 | */
25 | public class ResChiperTask extends DefaultTask {
26 |
27 | private static final Logger logger = Logger.getLogger(ResChiperTask.class.getName());
28 | private final Extension resChiperExtension = (Extension) getProject().getExtensions().getByName("resChiper");
29 | private ApplicationVariant variant;
30 | private KeyStore keyStore;
31 | private Path bundlePath;
32 | private Path obfuscatedBundlePath;
33 |
34 | /**
35 | * Constructor for the ResChiperTask.
36 | */
37 | public ResChiperTask() {
38 | setDescription("Assemble resource proguard for bundle file");
39 | setGroup("bundle");
40 | getOutputs().upToDateWhen(task -> false);
41 | }
42 |
43 | /**
44 | * Sets the variant scope for the task.
45 | *
46 | * @param variant The ApplicationVariant for the Android application.
47 | */
48 | public void setVariantScope(ApplicationVariant variant) {
49 | this.variant = variant;
50 | bundlePath = Bundle.getBundleFilePath(getProject(), variant);
51 | obfuscatedBundlePath = new File(bundlePath.toFile().getParentFile(), resChiperExtension.getObfuscatedBundleName()).toPath();
52 | }
53 |
54 | /**
55 | * Executes the ResChiperTask.
56 | *
57 | * @throws Exception If an error occurs during execution.
58 | */
59 | @TaskAction
60 | public void execute() throws Exception {
61 | logger.log(Level.INFO, resChiperExtension.toString());
62 | keyStore = SigningConfig.getSigningConfig(variant);
63 | printSignConfiguration();
64 | printOutputFileLocation();
65 | prepareUnusedFile();
66 | Command.Builder builder = Command.builder();
67 | builder.setBundlePath(bundlePath);
68 | builder.setOutputPath(obfuscatedBundlePath);
69 |
70 | ObfuscateBundleCommand.Builder obfuscateBuilder = ObfuscateBundleCommand.builder()
71 | .setEnableObfuscate(resChiperExtension.getEnableObfuscation())
72 | .setObfuscationMode(resChiperExtension.getObfuscationMode())
73 | .setMergeDuplicatedResources(resChiperExtension.getMergeDuplicateResources())
74 | .setWhiteList(resChiperExtension.getWhiteList())
75 | .setFilterFile(resChiperExtension.getEnableFileFiltering())
76 | .setFileFilterRules(resChiperExtension.getFileFilterList())
77 | .setRemoveStr(resChiperExtension.getEnableFilterStrings())
78 | .setUnusedStrPath(resChiperExtension.getUnusedStringFile())
79 | .setLanguageWhiteList(resChiperExtension.getLocaleWhiteList());
80 | if (resChiperExtension.getMappingFile() != null)
81 | obfuscateBuilder.setMappingPath(resChiperExtension.getMappingFile());
82 |
83 | if (keyStore.storeFile() != null && keyStore.storeFile().exists())
84 | builder.setStoreFile(keyStore.storeFile().toPath())
85 | .setKeyAlias(keyStore.keyAlias())
86 | .setKeyPassword(keyStore.keyPassword())
87 | .setStorePassword(keyStore.storePassword());
88 |
89 | builder.setObfuscateBundleBuilder(obfuscateBuilder.build());
90 |
91 | FileFilterCommand.Builder fileFilterBuilder = FileFilterCommand.builder();
92 | fileFilterBuilder.setFileFilterRules(resChiperExtension.getFileFilterList());
93 | builder.setFileFilterBuilder(fileFilterBuilder.build());
94 |
95 | StringFilterCommand.Builder stringFilterBuilder = StringFilterCommand.builder();
96 | builder.setStringFilterBuilder(stringFilterBuilder.build());
97 |
98 | DuplicateResMergerCommand.Builder duplicateResMergeBuilder = DuplicateResMergerCommand.builder();
99 | builder.setDuplicateResMergeBuilder(duplicateResMergeBuilder.build());
100 |
101 | Command command = builder.build(builder.build(), Command.TYPE.OBFUSCATE_BUNDLE);
102 | command.execute(Command.TYPE.OBFUSCATE_BUNDLE);
103 | }
104 |
105 | /**
106 | * Prepares the unused file for filtering.
107 | */
108 | private void prepareUnusedFile() {
109 | String simpleName = variant.getName().replace("Release", "");
110 | String name = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
111 | String resourcePath = getProject().getBuildDir() + "/outputs/mapping/" + name + "/release/unused_strings.txt";
112 | File usedFile = new File(resourcePath);
113 |
114 | if (usedFile.exists()) {
115 | System.out.println("find unused_strings.txt: " + usedFile.getAbsolutePath());
116 | if (resChiperExtension.getEnableFilterStrings())
117 | if (resChiperExtension.getUnusedStringFile() == null || resChiperExtension.getUnusedStringFile().isBlank()) {
118 | resChiperExtension.setUnusedStringFile(usedFile.getAbsolutePath());
119 | logger.log(Level.SEVERE, "replace unused_strings.txt!");
120 | }
121 | } else
122 | logger.log(Level.SEVERE, "not exists unused_strings.txt: " + usedFile.getAbsolutePath()
123 | + "\nuse default path: " + resChiperExtension.getUnusedStringFile());
124 | }
125 |
126 | /**
127 | * Prints the signing configuration.
128 | */
129 | private void printSignConfiguration() {
130 | System.out.println("----------------------------------------");
131 | System.out.println(" Signing Configuration");
132 | System.out.println("----------------------------------------");
133 | System.out.println("\tKeyStoreFile:\t\t" + keyStore.storeFile());
134 | System.out.println("\tKeyPassword:\t" + encrypt(keyStore.keyPassword()));
135 | System.out.println("\tAlias:\t\t\t" + encrypt(keyStore.keyAlias()));
136 | System.out.println("\tStorePassword:\t" + encrypt(keyStore.storePassword()));
137 | }
138 |
139 | /**
140 | * Prints the output file location.
141 | */
142 | private void printOutputFileLocation() {
143 | System.out.println("----------------------------------------");
144 | System.out.println(" Output configuration");
145 | System.out.println("----------------------------------------");
146 | System.out.println("\tFolder:\t\t" + obfuscatedBundlePath.getParent());
147 | System.out.println("\tFile:\t\t" + obfuscatedBundlePath.getFileName());
148 | System.out.println("----------------------------------------");
149 | }
150 |
151 | /**
152 | * Encrypts a value for printing (partially).
153 | *
154 | * @param value The value to encrypt.
155 | * @return The encrypted value.
156 | */
157 | private @NotNull String encrypt(String value) {
158 | if (value == null)
159 | return "/";
160 | if (value.length() > 2)
161 | return value.substring(0, value.length() / 2) + "****";
162 | return "****";
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/utils/FileUtils.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.utils;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.io.*;
6 | import java.nio.charset.StandardCharsets;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | import static com.google.common.base.Preconditions.checkArgument;
11 | import static com.google.common.base.Preconditions.checkNotNull;
12 |
13 | public class FileUtils {
14 | private static final String UNIX_LINE_SEPARATOR = "\n";
15 |
16 | /**
17 | * Loads a text file, forcing the line separator to be Unix-style '\n'.
18 | *
19 | * @param file the file to read from
20 | * @return the content of the file with Unix-style line separators
21 | * @throws IOException if an I/O error occurs or the file does not exist
22 | */
23 | public static @NotNull String loadFileWithUnixLineSeparators(@NotNull File file) throws IOException {
24 | checkNotNull(file, "File must not be null");
25 | checkArgument(file.exists(), "File does not exist");
26 | checkArgument(file.isFile(), "File is not a regular file");
27 | try (BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
28 | List lines = new ArrayList<>();
29 | String line;
30 | while ((line = reader.readLine()) != null)
31 | lines.add(line);
32 | return String.join(UNIX_LINE_SEPARATOR, lines);
33 | }
34 | }
35 |
36 | /**
37 | * Creates a new text file or replaces the content of an existing file.
38 | *
39 | * @param file the file to write to
40 | * @param content the new content of the file
41 | * @throws IOException if an I/O error occurs
42 | */
43 | public static void writeToFile(@NotNull File file, @NotNull String content) throws IOException {
44 | checkNotNull(file, "File must not be null");
45 | checkNotNull(content, "Content must not be null");
46 | try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
47 | writer.write(content);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/utils/TimeClock.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.utils;
2 |
3 | /**
4 | * A utility class for measuring elapsed time.
5 | */
6 | public class TimeClock {
7 |
8 | private final long startTime;
9 |
10 | /**
11 | * Constructs a TimeClock and starts the timer.
12 | */
13 | public TimeClock() {
14 | startTime = System.currentTimeMillis();
15 | }
16 |
17 | /**
18 | * Gets the elapsed time since the TimeClock was constructed.
19 | *
20 | * @return A string representing the elapsed time in a human-readable format (e.g., "1min 30s", "45s", "500ms").
21 | */
22 | public String getElapsedTime() {
23 | long elapsedTimeMillis = System.currentTimeMillis() - startTime;
24 | if (elapsedTimeMillis >= 60000) {
25 | long elapsedMinutes = elapsedTimeMillis / 60000;
26 | long remainingSeconds = (elapsedTimeMillis % 60000) / 1000;
27 | if (remainingSeconds > 0)
28 | return elapsedMinutes + "min " + remainingSeconds + "s";
29 | else
30 | return elapsedMinutes + "min";
31 | } else if (elapsedTimeMillis >= 1000)
32 | return elapsedTimeMillis / 1000 + "s";
33 | else
34 | return elapsedTimeMillis + "ms";
35 | }
36 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/goldfish07/reschiper/plugin/utils/Utils.java:
--------------------------------------------------------------------------------
1 | package io.github.goldfish07.reschiper.plugin.utils;
2 |
3 | import io.github.goldfish07.reschiper.plugin.operations.FileOperation;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.io.*;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.*;
9 | import java.util.regex.Pattern;
10 |
11 | public class Utils {
12 | public static boolean isPresent(String str) {
13 | return str != null && !str.isEmpty();
14 | }
15 |
16 | public static boolean isBlank(String str) {
17 | return !isPresent(str);
18 | }
19 |
20 | public static boolean isPresent(Iterator iterator) {
21 | return iterator != null && iterator.hasNext();
22 | }
23 |
24 | public static boolean isBlank(Iterator iterator) {
25 | return !isPresent(iterator);
26 | }
27 |
28 | public static String convertToPatternString(String input) {
29 | // ? Zero or one character
30 | // * Zero or more of character
31 | // + One or more of characters
32 | final String[] searchList = new String[]{".", "?", "*", "+"};
33 | final String[] replacementList = new String[]{"\\.", ".?", ".*", ".+"};
34 | return replaceEach(input, searchList, replacementList);
35 | }
36 |
37 | public static boolean match(String str, HashSet patterns) {
38 | if (patterns == null)
39 | return true;
40 | for (Pattern p : patterns) {
41 | boolean isMatch = p.matcher(str).matches();
42 | if (isMatch)
43 | return false;
44 | }
45 | return true;
46 | }
47 |
48 | public static void cleanDir(@NotNull File dir) {
49 | if (dir.exists()) {
50 | FileOperation.deleteDir(dir);
51 | dir.mkdirs();
52 | }
53 | }
54 |
55 | public static String readInputStream(@NotNull InputStream inputStream) throws IOException {
56 | ByteArrayOutputStream result = new ByteArrayOutputStream();
57 | byte[] buffer = new byte[4096];
58 | int length;
59 | while ((length = inputStream.read(buffer)) != -1)
60 | result.write(buffer, 0, length);
61 | return result.toString(StandardCharsets.UTF_8);
62 | }
63 |
64 | public static String runCmd(String... cmd) throws IOException, InterruptedException {
65 | String output;
66 | Process process = null;
67 | try {
68 | process = new ProcessBuilder(cmd).start();
69 | output = readInputStream(process.getInputStream());
70 | process.waitFor();
71 | if (process.exitValue() != 0) {
72 | System.err.printf("%s Failed! Please check your signature file.\n%n", cmd[0]);
73 | throw new RuntimeException(readInputStream(process.getErrorStream()));
74 | }
75 | } finally {
76 | if (process != null)
77 | process.destroy();
78 | }
79 | return output;
80 | }
81 |
82 | public static String runExec(String[] argv) throws IOException, InterruptedException {
83 | Process process = null;
84 | String output;
85 | try {
86 | process = Runtime.getRuntime().exec(argv);
87 | output = readInputStream(process.getInputStream());
88 | process.waitFor();
89 | if (process.exitValue() != 0) {
90 | System.err.printf("%s Failed! Please check your signature file.\n%n", argv[0]);
91 | throw new RuntimeException(readInputStream(process.getErrorStream()));
92 | }
93 | } finally {
94 | if (process != null)
95 | process.destroy();
96 | }
97 | return output;
98 | }
99 |
100 | private static void processOutputStreamInThread(@NotNull Process process) throws IOException {
101 | InputStreamReader ir = new InputStreamReader(process.getInputStream());
102 | LineNumberReader input = new LineNumberReader(ir);
103 | // If not read, there may be issues; it is blocked.
104 | while (input.readLine() != null) {
105 | }
106 | }
107 |
108 | private static String replaceEach(String text, String[] searchList, String[] replacementList) {
109 | // TODO: throw new IllegalArgumentException() if any param doesn't make sense
110 | //validateParams(text, searchList, replacementList);
111 |
112 | SearchTracker tracker = new SearchTracker(text, searchList, replacementList);
113 | if (!tracker.hasNextMatch(0))
114 | return text;
115 | StringBuilder buf = new StringBuilder(text.length() * 2);
116 | int start = 0;
117 | do {
118 | SearchTracker.MatchInfo matchInfo = tracker.matchInfo;
119 | int textIndex = matchInfo.textIndex;
120 | String pattern = matchInfo.pattern;
121 | String replacement = matchInfo.replacement;
122 | buf.append(text, start, textIndex);
123 | buf.append(replacement);
124 | start = textIndex + pattern.length();
125 | } while (tracker.hasNextMatch(start));
126 | return buf.append(text.substring(start)).toString();
127 | }
128 |
129 | static class SearchTracker {
130 | final String text;
131 | final Map patternToReplacement = new HashMap<>();
132 | final Set pendingPatterns = new HashSet<>();
133 | MatchInfo matchInfo = null;
134 |
135 | SearchTracker(String text, String @NotNull [] searchList, String[] replacementList) {
136 | this.text = text;
137 | for (int i = 0; i < searchList.length; ++i) {
138 | String pattern = searchList[i];
139 | patternToReplacement.put(pattern, replacementList[i]);
140 | pendingPatterns.add(pattern);
141 | }
142 | }
143 |
144 | boolean hasNextMatch(int start) {
145 | int textIndex = -1;
146 | String nextPattern = null;
147 | for (String pattern : new ArrayList<>(pendingPatterns)) {
148 | int matchIndex = text.indexOf(pattern, start);
149 | if (matchIndex == -1)
150 | pendingPatterns.remove(pattern);
151 | else {
152 | if (textIndex == -1 || matchIndex < textIndex) {
153 | textIndex = matchIndex;
154 | nextPattern = pattern;
155 | }
156 | }
157 | }
158 | if (nextPattern != null) {
159 | matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex);
160 | return true;
161 | }
162 | return false;
163 | }
164 |
165 | private record MatchInfo(String pattern, String replacement, int textIndex) {
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/gradle-plugins/io.github.goldfish07.reschiper.properties:
--------------------------------------------------------------------------------
1 | implementation-class=io.github.goldfish07.reschiper.plugin.ResChiperPlugin
--------------------------------------------------------------------------------