diff --git a/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/extension/TinkerResourceExtension.groovy b/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/extension/TinkerResourceExtension.groovy index b01b2544..39571502 100644 --- a/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/extension/TinkerResourceExtension.groovy +++ b/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/extension/TinkerResourceExtension.groovy @@ -49,6 +49,20 @@ public class TinkerResourceExtension { */ int largeModSize + /** + * On case insensitive os like macOS, when unzip apk, if res filename in apk is minified, + * minified res filenames may be conflict, eg: aBc.xml and Abc.xml. but they represent the same file + * on macOS + * In this case, you cannot generate the correct patch file because some res files are overwritten + * + * default false + * false: has no effect on the final patch. print log if conflict files are detected + * true: detect conflict files and rename them to temp unique filename (except the first one). + * the temp unique filenames will remain in some files ( old/new unzip dir, tinker_result, + * res_log.txt, log.txt), but will not remain in the final patch file. + */ + boolean caseInsensitiveCompat + public TinkerResourceExtension() { pattern = [] ignoreChange = [] diff --git a/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/task/TinkerPatchSchemaTask.groovy b/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/task/TinkerPatchSchemaTask.groovy index 8a29a039..ced3c1c9 100644 --- a/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/task/TinkerPatchSchemaTask.groovy +++ b/tinker-build/tinker-patch-gradle-plugin/src/main/groovy/com/tencent/tinker/build/gradle/task/TinkerPatchSchemaTask.groovy @@ -155,6 +155,7 @@ public class TinkerPatchSchemaTask extends DefaultTask { .setConfigFields(packageConfigFields) .setSevenZipPath(configuration.sevenZip.path) .setUseSign(configuration.useSign) + .setCaseInsensitiveCompat(configuration.res.caseInsensitiveCompat) .setArkHotPath(configuration.arkHot.path) .setArkHotName(configuration.arkHot.name) diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/decoder/ResDiffDecoder.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/decoder/ResDiffDecoder.java index b7d0ec33..e19cce53 100644 --- a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/decoder/ResDiffDecoder.java +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/decoder/ResDiffDecoder.java @@ -19,6 +19,7 @@ import com.tencent.tinker.build.apkparser.AndroidParser; import com.tencent.tinker.build.info.InfoWriter; import com.tencent.tinker.build.patch.Configuration; +import com.tencent.tinker.build.util.CaseSensitive; import com.tencent.tinker.build.util.DiffFactory; import com.tencent.tinker.build.util.FileOperation; import com.tencent.tinker.build.util.CustomDiff; @@ -125,6 +126,8 @@ private boolean checkLargeModFile(File file) { @Override public void onAllPatchesStart() throws IOException, TinkerPatchException { newApkParser.parseResourceTable(); + CaseSensitive.caseInsensitiveCompat = config.mCaseInsensitiveCompat; + final Map newApkResPkgNameMap = newApkParser.getResourceTable().getPackageNameMap(); do { if (newApkResPkgNameMap == null) { @@ -502,6 +505,7 @@ private void writeMetaFile(ArrayList set, int mode) { } metaWriter.writeLineToInfoFile(title); for (String name : set) { + name = CaseSensitive.getOriginalEntryName(name); String line = name; if (mode == TypedValue.LARGE_MOD) { LargeModeInfo info = largeModifiedMap.get(name); diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/Configuration.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/Configuration.java index 9db035c2..46a06af0 100644 --- a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/Configuration.java +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/Configuration.java @@ -76,6 +76,8 @@ public class Configuration { protected static final String ATTR_IGNORE_CHANGE = "ignoreChange"; protected static final String ATTR_IGNORE_CHANGE_WARNING = "ignoreChangeWarning"; protected static final String ATTR_RES_LARGE_MOD = "largeModSize"; + protected static final String ATTR_RES_CASE_SENSITIVE = "caseInsensitiveCompat"; + protected static final String ATTR_ARKHOT_PATH = "path"; protected static final String ATTR_ARKHOT_NAME = "name"; @@ -120,6 +122,7 @@ public class Configuration { public HashSet mResIgnoreChangeWarningPattern; public HashSet mResRawPattern; public int mLargeModSize; + public boolean mCaseInsensitiveCompat; /** * only gradle have the param */ @@ -237,6 +240,7 @@ public Configuration(InputParam param) throws IOException, TinkerPatchException mLargeModSize = param.largeModSize; //only gradle have the param mUseApplyResource = param.useApplyResource; + mCaseInsensitiveCompat = param.caseInsensitiveCompat; mDexLoaderPattern.addAll(param.dexLoaderPattern); mDexIgnoreWarningLoaderPattern.addAll(param.dexIgnoreWarningLoaderPattern); @@ -267,6 +271,7 @@ public Configuration(InputParam param) throws IOException, TinkerPatchException mPackageFields = param.configFields; mUseSignAPk = param.useSign; + mCustomDiffPath = param.customDiffPath; mCustomDiffPathArgs = param.customDiffPathArgs; setSignData(param.signFile, param.keypass, param.storealias, param.storepass); @@ -642,6 +647,8 @@ private void readResPatternsFromXml(Node node) throws IOException { } } else if (tagName.equals(ATTR_RES_LARGE_MOD)) { mLargeModSize = Integer.valueOf(value); + } else if (tagName.equals(ATTR_RES_CASE_SENSITIVE)) { + mCaseInsensitiveCompat = Boolean.valueOf(value); } else { System.err.println("unknown dex tag " + tagName); } diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/InputParam.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/InputParam.java index 61074cf0..fbcab66b 100644 --- a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/InputParam.java +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/patch/InputParam.java @@ -42,6 +42,7 @@ public class InputParam { public final boolean isProtectedApp; public final boolean supportHotplugComponent; public final boolean useSign; + public final boolean caseInsensitiveCompat; /** * tinkerPatch.dex @@ -106,6 +107,7 @@ private InputParam( boolean isProtectedApp, boolean supportHotplugComponent, boolean useSign, + boolean caseInsensitiveCompat, ArrayList dexFilePattern, ArrayList dexLoaderPattern, @@ -139,6 +141,7 @@ private InputParam( this.isProtectedApp = isProtectedApp; this.supportHotplugComponent = supportHotplugComponent; this.useSign = useSign; + this.caseInsensitiveCompat = caseInsensitiveCompat; this.dexFilePattern = dexFilePattern; this.dexLoaderPattern = dexLoaderPattern; @@ -178,6 +181,7 @@ public static class Builder { private boolean isProtectedApp; private boolean isComponentHotplugSupported; private boolean useSign; + private boolean caseInsensitiveCompat; /** * tinkerPatch.dex @@ -365,6 +369,11 @@ public Builder setUseSign(boolean useSign) { return this; } + public Builder setCaseInsensitiveCompat(boolean caseInsensitiveCompat) { + this.caseInsensitiveCompat = caseInsensitiveCompat; + return this; + } + public Builder setArkHotPath(String path) { this.arkHotPatchPath = path; return this; @@ -392,6 +401,7 @@ public InputParam create() { isProtectedApp, isComponentHotplugSupported, useSign, + caseInsensitiveCompat, dexFilePattern, dexLoaderPattern, dexIgnoreWarningLoaderPattern, diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/CaseSensitive.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/CaseSensitive.java new file mode 100644 index 00000000..15ac08b3 --- /dev/null +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/CaseSensitive.java @@ -0,0 +1,110 @@ +package com.tencent.tinker.build.util; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class CaseSensitive { + + public static boolean caseInsensitiveCompat = false; + private static final String PREFIX = "CS"; + private static final String UNDERLINE = "_"; + private static long seq = System.currentTimeMillis(); + + // + private static final Map wrappedFileMap = new HashMap<>(); + + /** + * return original file if no conflict files detected, otherwise return a wrapped file with a unique filename. + * + * "conflict files" means 2 different filenames point to a same File in case insensitive os (like macOS) + * + * eg: File aBc.xml exists, input: abC.xml, return: CS12345678_abC.xml + * + * @param target original target file + */ + public static File wrap(File target) { + if (!target.exists()) { + return target; + } + File parentFile = target.getParentFile(); + if (parentFile == null || !parentFile.exists()) { + return target; + } + File[] list = parentFile.listFiles(); + if (list == null || list.length == 0) { + return target; + } + + String filename = target.getName(); + String oldName = ""; + for (File child : list) { + if (child.getName().equalsIgnoreCase(filename)) { + oldName = child.getName(); + break; + } + } + if (oldName.isEmpty() || oldName.equals(filename)) { + return target; + } + if (!caseInsensitiveCompat) { + Logger.e("find conflict files " + oldName + " and " + filename); + return target; + } + if (wrappedFileMap.containsKey(filename)) { + return new File(parentFile, wrappedFileMap.get(filename)); + } + + File wrappedFile = new File(parentFile, PREFIX + (seq++) + UNDERLINE + filename); + wrappedFileMap.put(filename, wrappedFile.getName()); + Logger.d("find conflict file exists:" + oldName + ", wrapped:" + wrappedFile); + return wrappedFile; + } + + /** + * eg: + * input: CS12345678_abc.xml + * return: abc.xml + */ + public static String getOriginalFileName(String name) { + if (!caseInsensitiveCompat + || name == null + || name.isEmpty() + || !name.startsWith(PREFIX) + || !wrappedFileMap.containsValue(name)) { + return name; + } + int index = name.indexOf(UNDERLINE); + if (index < 0) { + return name; + } + try { + long verifyLong = Long.parseLong(name.substring(2, index)); + } catch (NumberFormatException e) { + return name; + } + return name.substring(index + 1); + } + + /** + * eg: + * input: res/CS12345678_abc.xml + * return: res/abc.xml + */ + public static String getOriginalEntryName(String entryName) { + if (!caseInsensitiveCompat + || entryName == null + || entryName.isEmpty()) { + return entryName; + } + int lastSeparatorIndex = entryName.lastIndexOf(File.separator); + if (lastSeparatorIndex < 0) { + return getOriginalFileName(entryName); + } + String path = entryName.substring(0, lastSeparatorIndex); + String name = entryName.substring(lastSeparatorIndex + 1); + name = getOriginalFileName(name); + return path + File.separator + name; + } + +} diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/FileOperation.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/FileOperation.java index d0e5e025..9a3371f7 100644 --- a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/FileOperation.java +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/FileOperation.java @@ -197,6 +197,8 @@ public static void unZipAPk(String fileName, String filePath) throws IOException File file = new File(filePath + File.separator + entry.getName()); + file = CaseSensitive.wrap(file); + File parentFile = file.getParentFile(); if (parentFile != null && (!parentFile.exists())) { parentFile.mkdirs(); @@ -250,7 +252,11 @@ public static void zipFiles(Collection resFileList, File zipFile, String c } private static void zipFile(File resFile, ZipOutputStream zipout, String rootpath) throws IOException { - rootpath = rootpath + (rootpath.trim().length() == 0 ? "" : File.separator) + resFile.getName(); + String resName = resFile.getName(); + if (resFile.isFile()) { + resName = CaseSensitive.getOriginalFileName(resName); + } + rootpath = rootpath + (rootpath.trim().length() == 0 ? "" : File.separator) + resName; if (resFile.isDirectory()) { File[] fileList = resFile.listFiles(); for (File file : fileList) { diff --git a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/Utils.java b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/Utils.java index e57f6515..96673665 100644 --- a/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/Utils.java +++ b/tinker-build/tinker-patch-lib/src/main/java/com/tencent/tinker/build/util/Utils.java @@ -139,6 +139,7 @@ public static String genResOutputFile(File output, File newZipFile, Configuratio ); } String name = zipEntry.getName(); + name = CaseSensitive.getOriginalEntryName(name); if (!TinkerZipUtil.validateZipEntryName(output.getParentFile(), name)) { throw new IOException("Bad ZipEntry name: " + name); } @@ -162,6 +163,7 @@ public static String genResOutputFile(File output, File newZipFile, Configuratio TinkerZipUtil.extractTinkerEntry(oldApk, manifestZipEntry, out); for (String name : largeModifiedSet) { + name = CaseSensitive.getOriginalEntryName(name); TinkerZipEntry largeZipEntry = oldApk.getEntry(name); if (largeZipEntry == null) { throw new TinkerPatchException( @@ -173,6 +175,7 @@ public static String genResOutputFile(File output, File newZipFile, Configuratio } for (String name : addedSet) { + name = CaseSensitive.getOriginalEntryName(name); TinkerZipEntry addZipEntry = newApk.getEntry(name); if (addZipEntry == null) { throw new TinkerPatchException( @@ -183,6 +186,7 @@ public static String genResOutputFile(File output, File newZipFile, Configuratio } for (String name : modifiedSet) { + name = CaseSensitive.getOriginalEntryName(name); TinkerZipEntry modZipEntry = newApk.getEntry(name); if (modZipEntry == null) { throw new TinkerPatchException(