25
@@ -43,7 +36,7 @@
- com.ttstd.BsDiffTool
+ com.ttstd.ApkDeltaUpdater
diff --git a/src/main/java/com/ttstd/ApkDeltaUpdater.java b/src/main/java/com/ttstd/ApkDeltaUpdater.java
index fe47072..b9a7941 100644
--- a/src/main/java/com/ttstd/ApkDeltaUpdater.java
+++ b/src/main/java/com/ttstd/ApkDeltaUpdater.java
@@ -1,149 +1,292 @@
package com.ttstd;
-import io.sigpipe.jbsdiff.InvalidHeaderException;
-import io.sigpipe.jbsdiff.ui.FileUI;
-import org.apache.commons.compress.compressors.CompressorException;
-import org.apache.commons.compress.compressors.CompressorStreamFactory;
-
import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
+import java.nio.charset.Charset;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
-import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
+import java.util.zip.ZipInputStream;
/**
- * APK 省流增量更新:按文件大小分档,自动选择差量引擎与执行模式。
+ * 基于 HDiffPatch(hdiffz / hpatchz)的 APK 增量更新工具。
+ *
+ * 一、执行方式
+ * 只使用 HDiffPatch:diff 调用 {@code hdiffz},patch 调用 {@code hpatchz}。
+ * 官方 Releases v5.1.3 的常用平台二进制压缩包内置在 jar 的
+ * {@code /hdiffpatch/} 目录下,运行时按 {@code os.name} / {@code os.arch}
+ * 选择对应平台,解压到 {@code $TMP/ttstd-hdiffpatch/5.1.3-/},
+ * Unix 平台自动补执行位后以子进程方式调用。
*
- * 一、工具 Android 实现评估(先评估、再用依赖)
*
- * | 工具 | Android 端形态 | 官方 Maven/AAR | 结论 |
- * | HDiffPatch | C++,需 NDK 交叉编译 + 自行 JNI 封装 | 无 |
- * {@link Engine#HDIFFPATCH}:仅运行时探测,本工程未内置 |
- * | xdelta3 | C,需 NDK 交叉编译 + JNI,生态仅零散移植 | 无 |
- * {@link Engine#XDELTA3}:同上 |
- * | bsdiff | C,有 Android 移植(Tinker / AOSP),非 Maven 包 | 无 |
- * 由纯 Java 实现 jbsdiff 替代 |
- * | jbsdiff | 纯 Java,无 native 依赖,Android 可直接运行 | 有 |
- * {@link Engine#BSDIFF}:默认执行引擎 |
- * | UpdateEngine / imgdiff | {@code @SystemApi},系统签名才能调用 | 无 |
- * 普通 App 不可用,排除 |
+ * | os.name | os.arch | 内置目录 |
+ * | Windows | amd64 / x86_64 | windows64 |
+ * | Linux | amd64 / x86_64 | linux64 |
+ * | Linux | aarch64 / arm64 | linux_arm64 |
+ * | macOS | 任意(通用二进制) | macos |
*
- * 结论:HDiffPatch / xdelta3 无现成 Android 依赖,接入需自建 NDK/JNI;本工程为纯 Java
- * Maven 工具,执行层使用 {@code io.sigpipe:jbsdiff}。分档仍按各档理论最优引擎规划,
- * native 引擎可用时可平滑替换。
+ * 仅内置以上平台;windows32、linux32、linux_arm32、linux_loong64、
+ * linux_riscv32、linux_riscv64 等平台运行时将提示缺少内置资源,需用
+ * {@code -Dttstd.hdiffpatch.dir=<目录>} 指定外部二进制(目录内需有当前平台的
+ * {@code hdiffz}/{@code hpatchz},Windows 为 .exe)。
*
- * 二、分档策略(以新包大小为准)
+ * 二、分档参数(以 old/new 中较大者为准)
+ * hdiffz 帮助给出的内存模型:{@code -m} 需
+ * {@code newSize + oldSize*5};{@code -s-B} 只需
+ * {@code oldSize*16/B}(B 为 matchBlockSize,单位字节);{@code -SD}
+ * 把补丁压成单流,打补丁只需一个解压缓冲,并支持分步下载与多线程还原。
*
- * | 档位 | 大小 | 首选引擎 | 模式 | 压缩 |
- * 单条目上限 |
- * | SMALL | < 64MB | HDiffPatch | 整包 | bzip2 | - |
- * | MEDIUM | 64~128MB | xdelta3 | 整包 | gzip | - |
- * | LARGE | 128~256MB | bsdiff | 条目级 | bzip2 | 64MB |
- * | XLARGE | 256~512MB | bsdiff | 条目级 | bzip2 | 32MB |
- * | HUGE | > 512MB | bsdiff | 条目级 | bzip2 | 16MB |
+ * | 档位 | 包大小 | hdiffz 参数 | hpatchz 参数 | 取舍 |
+ * | SMALL | < 64MB | {@code -s-32 -c-lzma2-9 -p-2} |
+ * {@code -s-8m} | 流式而非 {@code -m}:最坏内存 359MB → 104MB,
+ * 补丁仅大 0.0025% |
+ * | MEDIUM | 64~128MB | {@code -s-64 -c-lzma2-9 -p-2} |
+ * {@code -s-16m} | {@code -m} 峰值可达 768MB,改为流式 |
+ * | LARGE | 128~256MB | {@code -s-64 -SD-256k -c-lzma2-9 -p-2} |
+ * {@code -s-32m} | 加 {@code -SD},降低客户端还原内存 |
+ * | XLARGE | 256~512MB | {@code -s-128 -SD-512k -c-lzma2-9 -p-2} |
+ * {@code -s-64m -p-2} | 放大匹配块压内存,线程降到 2 |
+ * | HUGE | ≥ 512MB | {@code -s-256 -SD-1m -c-lzma2-9 -p-2} |
+ * {@code -s-128m -p-2} | 匹配块再放大,内存进一步收敛 |
*
- * 128MB 起改条目级:整包 diff 峰值约 3~5 倍文件大小,会超过服务器 1G 可用内存;
- * 条目级把单次 diff 对象降为单条目。补丁率超过 40% 时建议回退全量下发。
+ * 实测峰值(Windows x64 / hdiffz 5.1.3,仅 hdiffz 子进程,相似度约 97%):
+ * 60MB 45MB、100MB 71MB、200MB 98MB、320MB 101MB、560MB 104MB;
+ * 新旧包几乎不相似时各档上界为 104MB、104MB、104MB、104MB、104MB。
+ * SMALL 不再使用 {@code -m}(最坏约 new+old×5,60MB 达 359MB),改为
+ * {@code -s-32}:APK 风格语料上补丁仅大 0.0025%,最坏峰值降到 104MB。
+ * 大档不使用 zstd-19 + p4:同一 293MB 测试下该组合峰值 552MB,1GB 服务器余量不足。
+ * 分档取 {@code max(oldSize, newSize)}:流式内存由旧包匹配表决定,
+ * 新包很小但旧包很大时也需要按旧包选档。
+ * 补丁率超过 {@link #FULL_PACKAGE_THRESHOLD} 时提示回退全量下发。
*
- * 三、条目级 patch 容器格式
- * patch 本身是 zip,含 {@code delta-data/NNNNNN.patch|raw} 与清单
- * {@code META-INF/delta-manifest},清单每行一条操作(tab 分隔):
+ * 三、APK 处理方式
+ * APK 按整包二进制做 diff:补丁由 old 与 new 逐字节还原,因此还原产物与
+ * 新包完全一致,签名和 zipalign 对齐都不会被破坏。生成方与使用方需使用兼容
+ * 的 hpatchz 版本({@code -SD} 需要 v5.0+,本工程内置 v5.1.3)。
+ *
+ * 四、命令行
*
- * COPY <name> old 同名同 CRC,原样拷贝
- * ADD <name> <dataEntry> old 无同名或超上限,直接携带
- * DIFF <name> <dataEntry> old 条目 + 容器内 bsdiff 补丁还原
+ * java -jar MavenUtils.jar diff <oldFile> <newFile> <patchFile> [选项]
+ * java -jar MavenUtils.jar patch <oldFile> <patchFile> <newFile> [选项]
+ * java -jar MavenUtils.jar info <patchFile>
+ * java -jar MavenUtils.jar env
*
*/
public final class ApkDeltaUpdater {
- private static final long MB = 1024L * 1024L;
-
- /** 条目级 patch 容器中清单条目的名称。 */
- private static final String MANIFEST_ENTRY = "META-INF/delta-manifest";
+ /** 内置二进制对应的 HDiffPatch 版本。 */
+ public static final String HDIFFPATCH_VERSION = "5.1.3";
/** 补丁率超过该值即失去省流意义,应回退全量下发。 */
public static final double FULL_PACKAGE_THRESHOLD = 0.40;
- private static final int BUFFER_SIZE = 64 * 1024;
+ /** 覆盖内置二进制的系统属性:值为包含 hdiffz/hpatchz 的目录。 */
+ public static final String BIN_DIR_PROPERTY = "ttstd.hdiffpatch.dir";
+
+ private static final long MB = 1024L * 1024L;
+ private static final String RESOURCE_DIR = "/hdiffpatch/";
+ private static final String CACHE_DIR = "ttstd-hdiffpatch";
+
+ private static Platform detectedPlatform;
private ApkDeltaUpdater() {
}
// ==========================================================
- // 一、差量引擎与可用性探测
+ // 一、平台探测与二进制准备
// ==========================================================
- /** 差量引擎。native 引擎需自行 NDK 编译与 JNI 绑定,本工程仅内置 jbsdiff。 */
- public enum Engine {
- /** HDiffPatch:补丁最小,native。 */
- HDIFFPATCH("HDiffPatch", "hdiffpatch"),
- /** xdelta3:流式、内存可控,native。 */
- XDELTA3("xdelta3", "xdelta3"),
- /** bsdiff:纯 Java(jbsdiff),默认执行引擎。 */
- BSDIFF("bsdiff(jbsdiff)", null);
+ /** 受支持的平台,folder 对应官方 Releases 压缩包内的目录名。 */
+ public enum Platform {
+ WINDOWS_X64("windows64", true),
+ WINDOWS_X86("windows32", true),
+ WINDOWS_ARM64("windows_arm64", true),
+ LINUX_X64("linux64", false),
+ LINUX_X86("linux32", false),
+ LINUX_ARM64("linux_arm64", false),
+ LINUX_ARM32("linux_arm32", false),
+ LINUX_LOONG64("linux_loong64", false),
+ LINUX_RISCV64("linux_riscv64", false),
+ LINUX_RISCV32("linux_riscv32", false),
+ MACOS("macos", false);
- private final String displayName;
- private final String nativeLibrary;
+ private final String folder;
+ private final boolean windows;
- Engine(String displayName, String nativeLibrary) {
- this.displayName = displayName;
- this.nativeLibrary = nativeLibrary;
+ Platform(String folder, boolean windows) {
+ this.folder = folder;
+ this.windows = windows;
}
- public String displayName() {
- return displayName;
+ public String folder() {
+ return folder;
}
- /** 是否为 native 引擎(需 NDK 编译与 JNI 绑定)。 */
- public boolean isNative() {
- return nativeLibrary != null;
+ public boolean isWindows() {
+ return windows;
}
- /** 本工程是否内置该引擎的执行实现(目前仅 jbsdiff)。 */
- public boolean hasExecutor() {
- return this == BSDIFF;
+ public String hdiffzName() {
+ return windows ? "hdiffz.exe" : "hdiffz";
}
- /** 运行时探测:native 引擎尝试加载动态库,失败即视为不可用。 */
- public boolean isAvailable() {
- if (!isNative()) {
- return true;
- }
- try {
- System.loadLibrary(nativeLibrary);
- return true;
- } catch (UnsatisfiedLinkError | SecurityException e) {
- return false;
- }
+ public String hpatchzName() {
+ return windows ? "hpatchz.exe" : "hpatchz";
+ }
+
+ String resourceName() {
+ return RESOURCE_DIR + "hdiffpatch_v" + HDIFFPATCH_VERSION + "_bin_" + folder + ".zip";
}
}
- /** 打印各引擎的 Android 实现形态与当前可用性。 */
- public static void printEngineEvaluation() {
- System.out.println("[差量引擎评估]");
- for (Engine engine : Engine.values()) {
- String form = engine.isNative()
- ? "需 NDK 交叉编译 + JNI(无官方 Maven/AAR)"
- : "纯 Java(io.sigpipe:jbsdiff)";
- System.out.printf(" %-18s %-44s 可用=%s 内置执行=%s%n",
- engine.displayName(), form, engine.isAvailable(), engine.hasExecutor());
+ /** 按 os.name / os.arch 探测当前平台(结果缓存)。 */
+ public static synchronized Platform currentPlatform() {
+ if (detectedPlatform == null) {
+ detectedPlatform = detectPlatform();
+ }
+ return detectedPlatform;
+ }
+
+ public static Platform detectPlatform() {
+ String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
+ String arch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT);
+
+ if (os.contains("win")) {
+ if (arch.contains("aarch64") || arch.contains("arm64")) {
+ return Platform.WINDOWS_ARM64;
+ }
+ return is64Bit(arch) ? Platform.WINDOWS_X64 : Platform.WINDOWS_X86;
+ }
+ if (os.contains("mac") || os.contains("darwin")) {
+ return Platform.MACOS;
+ }
+ if (os.contains("linux")) {
+ if (arch.contains("aarch64") || arch.contains("arm64")) {
+ return Platform.LINUX_ARM64;
+ }
+ if (arch.startsWith("arm")) {
+ return Platform.LINUX_ARM32;
+ }
+ if (arch.contains("loong")) {
+ return Platform.LINUX_LOONG64;
+ }
+ if (arch.contains("riscv64")) {
+ return Platform.LINUX_RISCV64;
+ }
+ if (arch.contains("riscv")) {
+ return Platform.LINUX_RISCV32;
+ }
+ return is64Bit(arch) ? Platform.LINUX_X64 : Platform.LINUX_X86;
+ }
+ throw new IllegalStateException("不支持的平台: os.name=" + os + ", os.arch=" + arch
+ + ";可用 -D" + BIN_DIR_PROPERTY + "=<含 hdiffz/hpatchz 的目录> 指定");
+ }
+
+ private static boolean is64Bit(String arch) {
+ return arch.contains("64") || arch.contains("aarch64");
+ }
+
+ /** 当前平台的 hdiffz / hpatchz 所在目录(优先系统属性覆盖,否则解压内置资源)。 */
+ public static Path binaryDir() throws IOException {
+ Platform platform = currentPlatform();
+ String override = System.getProperty(BIN_DIR_PROPERTY);
+ Path dir;
+ if (override != null && !override.isBlank()) {
+ dir = Path.of(override).toAbsolutePath().normalize();
+ if (!Files.isDirectory(dir)) {
+ throw new IOException(BIN_DIR_PROPERTY + " 不是目录: " + dir);
+ }
+ } else {
+ dir = extractBinaries(platform);
+ }
+ Path hdiffz = dir.resolve(platform.hdiffzName());
+ Path hpatchz = dir.resolve(platform.hpatchzName());
+ if (!Files.isRegularFile(hdiffz) || !Files.isRegularFile(hpatchz)) {
+ throw new IOException("目录中缺少 " + platform.hdiffzName() + " / "
+ + platform.hpatchzName() + ": " + dir);
+ }
+ return dir;
+ }
+
+ private static Path extractBinaries(Platform platform) throws IOException {
+ Path dir = Path.of(System.getProperty("java.io.tmpdir"), CACHE_DIR,
+ HDIFFPATCH_VERSION + "-" + platform.folder());
+ Path hdiffz = dir.resolve(platform.hdiffzName());
+ Path hpatchz = dir.resolve(platform.hpatchzName());
+ if (usable(hdiffz, platform) && usable(hpatchz, platform)) {
+ return dir;
+ }
+
+ try (InputStream raw = ApkDeltaUpdater.class.getResourceAsStream(platform.resourceName())) {
+ if (raw == null) {
+ throw new IOException("jar 内缺少平台二进制: " + platform.resourceName()
+ + ";可用 -D" + BIN_DIR_PROPERTY + "=<目录> 指定");
+ }
+ Files.createDirectories(dir);
+ boolean gotDiff = false;
+ boolean gotPatch = false;
+ try (ZipInputStream zip = new ZipInputStream(new BufferedInputStream(raw))) {
+ for (ZipEntry entry; (entry = zip.getNextEntry()) != null; ) {
+ if (entry.isDirectory()) {
+ continue;
+ }
+ String name = Path.of(entry.getName()).getFileName().toString();
+ if (!name.equals(platform.hdiffzName()) && !name.equals(platform.hpatchzName())) {
+ continue;
+ }
+ Path tmp = Files.createTempFile(dir, name, ".tmp");
+ try (OutputStream out = Files.newOutputStream(tmp)) {
+ zip.transferTo(out);
+ }
+ makeExecutable(tmp);
+ Path target = dir.resolve(name);
+ try {
+ Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ // 并发解压时另一进程可能已放好可执行文件
+ if (!usable(target, platform)) {
+ throw e;
+ }
+ } finally {
+ Files.deleteIfExists(tmp);
+ }
+ gotDiff |= name.equals(platform.hdiffzName());
+ gotPatch |= name.equals(platform.hpatchzName());
+ }
+ }
+ if (!gotDiff || !gotPatch) {
+ throw new IOException("压缩包缺少 hdiffz/hpatchz: " + platform.resourceName());
+ }
+ }
+ return dir;
+ }
+
+ private static boolean usable(Path exe, Platform platform) {
+ try {
+ return Files.isRegularFile(exe) && Files.size(exe) > 0
+ && (platform.isWindows() || Files.isExecutable(exe));
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private static void makeExecutable(Path file) {
+ try {
+ Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rwxr-xr-x"));
+ } catch (UnsupportedOperationException | IOException e) {
+ file.toFile().setExecutable(true, false);
}
- System.out.println(" 结论: HDiffPatch/xdelta3 无现成 Android 依赖,执行层回退 bsdiff(jbsdiff)");
}
// ==========================================================
@@ -153,22 +296,47 @@ public final class ApkDeltaUpdater {
/** 档位(上界不含):SMALL < 64MB < MEDIUM < 128MB < LARGE < 256MB
* < XLARGE < 512MB < HUGE。 */
public enum SizeTier {
- SMALL(64 * MB),
- MEDIUM(128 * MB),
- LARGE(256 * MB),
- XLARGE(512 * MB),
- HUGE(Long.MAX_VALUE);
+ SMALL(64 * MB, List.of("-s-32", "-c-lzma2-9", "-p-2"), List.of("-s-8m")),
+ MEDIUM(128 * MB, List.of("-s-64", "-c-lzma2-9", "-p-2"), List.of("-s-16m")),
+ LARGE(256 * MB, List.of("-s-64", "-SD-256k", "-c-lzma2-9", "-p-2"), List.of("-s-32m")),
+ XLARGE(512 * MB, List.of("-s-128", "-SD-512k", "-c-lzma2-9", "-p-2"), List.of("-s-64m", "-p-2")),
+ HUGE(Long.MAX_VALUE, List.of("-s-256", "-SD-1m", "-c-lzma2-9", "-p-2"), List.of("-s-128m", "-p-2"));
private final long upperBoundExclusive;
+ private final List diffArgs;
+ private final List patchArgs;
- SizeTier(long upperBoundExclusive) {
+ SizeTier(long upperBoundExclusive, List diffArgs, List patchArgs) {
this.upperBoundExclusive = upperBoundExclusive;
+ this.diffArgs = diffArgs;
+ this.patchArgs = patchArgs;
}
public long upperBoundExclusive() {
return upperBoundExclusive;
}
+ /** 档位对应的 hdiffz 参数(不含输入输出文件)。 */
+ public List diffArgs() {
+ return diffArgs;
+ }
+
+ /** 档位对应的 hpatchz 参数(不含输入输出文件)。 */
+ public List patchArgs() {
+ return patchArgs;
+ }
+
+ /** 人类可读的大小区间。 */
+ public String range() {
+ return switch (this) {
+ case SMALL -> "< 64MB";
+ case MEDIUM -> "64~128MB";
+ case LARGE -> "128~256MB";
+ case XLARGE -> "256~512MB";
+ case HUGE -> ">= 512MB";
+ };
+ }
+
/** 根据文件大小(字节)归档。 */
public static SizeTier of(long fileSize) {
for (SizeTier tier : values()) {
@@ -180,51 +348,40 @@ public final class ApkDeltaUpdater {
}
}
- /** 执行模式。 */
- public enum Mode {
- /** 整包 bsdiff。 */
- WHOLE_FILE,
- /** zip 条目级 bsdiff(压低单次 diff 内存峰值)。 */
- ENTRY_LEVEL,
- /** 不差量,直接携带全量。 */
- FULL_PACKAGE
- }
-
/**
- * 差量执行计划,由 {@link DeltaPlan#plan(long)} 按新包大小生成。
+ * 一次 diff / patch 的参数计划。
*
- * @param tier 命中的档位
- * @param preferredEngine 该档理论最优引擎(不可用时回退)
- * @param mode 执行模式
- * @param compressor bsdiff 压缩算法(commons-compress 名称)
- * @param maxDiffEntrySize 条目级下单条目 diff 上限(字节),整包模式为 0
+ * @param tier 命中的档位
+ * @param diffArgs hdiffz 参数
+ * @param patchArgs hpatchz 参数
*/
- public record DeltaPlan(SizeTier tier, Engine preferredEngine, Mode mode,
- String compressor, long maxDiffEntrySize) {
+ public record DeltaPlan(SizeTier tier, List diffArgs, List patchArgs) {
- /** 根据新包大小生成差量计划(分档策略的唯一入口)。 */
- public static DeltaPlan plan(long newFileSize) {
- SizeTier tier = SizeTier.of(newFileSize);
- return switch (tier) {
- case SMALL -> new DeltaPlan(tier, Engine.HDIFFPATCH, Mode.WHOLE_FILE,
- CompressorStreamFactory.BZIP2, 0);
- case MEDIUM -> new DeltaPlan(tier, Engine.XDELTA3, Mode.WHOLE_FILE,
- CompressorStreamFactory.GZIP, 0);
- case LARGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL,
- CompressorStreamFactory.BZIP2, 64 * MB);
- case XLARGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL,
- CompressorStreamFactory.BZIP2, 32 * MB);
- case HUGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL,
- CompressorStreamFactory.BZIP2, 16 * MB);
- };
+ public DeltaPlan {
+ diffArgs = List.copyOf(diffArgs);
+ patchArgs = List.copyOf(patchArgs);
}
- /** 实际可执行引擎:首选引擎无内置实现或不可用时回退 jbsdiff。 */
- public Engine resolveEngine() {
- if (preferredEngine.hasExecutor() && preferredEngine.isAvailable()) {
- return preferredEngine;
- }
- return Engine.BSDIFF;
+ /** 按 old/new 中较大者生成计划:流式内存由旧包匹配表决定。 */
+ public static DeltaPlan forFiles(File oldFile, File newFile) {
+ return of(SizeTier.of(Math.max(oldFile.length(), newFile.length())));
+ }
+
+ /** 按旧包大小生成计划(应用补丁用,旧包与新包大小通常接近)。 */
+ public static DeltaPlan forOldFile(File oldFile) {
+ return of(SizeTier.of(oldFile.length()));
+ }
+
+ public static DeltaPlan of(SizeTier tier) {
+ return new DeltaPlan(tier, tier.diffArgs(), tier.patchArgs());
+ }
+
+ public DeltaPlan withDiffArgs(List args) {
+ return new DeltaPlan(tier, args, patchArgs);
+ }
+
+ public DeltaPlan withPatchArgs(List args) {
+ return new DeltaPlan(tier, diffArgs, args);
}
}
@@ -232,10 +389,10 @@ public final class ApkDeltaUpdater {
// 三、生成 / 应用
// ==========================================================
- /** 按新包大小自动分档,生成 patch。 */
+ /** 按 old/new 中较大者自动分档,生成 patch。 */
public static void createPatch(File oldFile, File newFile, File patchFile)
- throws IOException, CompressorException, InvalidHeaderException {
- createPatch(oldFile, newFile, patchFile, DeltaPlan.plan(newFile.length()));
+ throws IOException, InterruptedException {
+ createPatch(oldFile, newFile, patchFile, DeltaPlan.forFiles(oldFile, newFile));
}
/**
@@ -243,23 +400,25 @@ public final class ApkDeltaUpdater {
* {@link #FULL_PACKAGE_THRESHOLD} 时输出回退全量的警告。
*/
public static void createPatch(File oldFile, File newFile, File patchFile, DeltaPlan plan)
- throws IOException, CompressorException, InvalidHeaderException {
+ throws IOException, InterruptedException {
+ requireFile(oldFile, "oldFile");
+ requireFile(newFile, "newFile");
long startNanos = System.nanoTime();
- Engine engine = plan.resolveEngine();
- if (engine != plan.preferredEngine()) {
- System.out.printf("[createPatch] 首选引擎 %s 不可用(native 未内置),回退 %s%n",
- plan.preferredEngine().displayName(), engine.displayName());
- }
- switch (plan.mode()) {
- case WHOLE_FILE -> FileUI.diff(oldFile, newFile, patchFile, plan.compressor());
- case ENTRY_LEVEL -> createEntryLevelPatch(oldFile, newFile, patchFile, plan);
- case FULL_PACKAGE -> Files.copy(newFile.toPath(), patchFile.toPath(),
- StandardCopyOption.REPLACE_EXISTING);
- }
+
+ List args = new ArrayList<>(plan.diffArgs());
+ args.add("-f");
+ args.add(oldFile.getPath());
+ args.add(newFile.getPath());
+ args.add(patchFile.getPath());
+
+ ProcessResult result = exec(hdiffzPath(), args);
+ result.requireSuccess("hdiffz");
+ System.out.print(result.output());
+
double ratio = patchRatio(newFile, patchFile);
System.out.printf(
- "[createPatch] tier=%s engine=%s mode=%s old=%d new=%d patch=%d ratio=%.1f%% 耗时 %s%n",
- plan.tier(), engine.displayName(), plan.mode(), oldFile.length(), newFile.length(),
+ "[createPatch] tier=%s diff=%s old=%d new=%d patch=%d ratio=%.1f%% 耗时 %s%n",
+ plan.tier(), String.join(" ", plan.diffArgs()), oldFile.length(), newFile.length(),
patchFile.length(), ratio * 100, formatDuration(System.nanoTime() - startNanos));
if (ratio > FULL_PACKAGE_THRESHOLD) {
System.out.printf("[createPatch][warn] 补丁率超过 %.0f%%,建议回退全量下发%n",
@@ -267,239 +426,309 @@ public final class ApkDeltaUpdater {
}
}
- /** 应用 patch 还原新包,自动识别整包 bsdiff 与条目级容器两种格式。 */
+ /** 应用 patch 还原新包(hpatchz 自动识别补丁格式与压缩算法)。 */
public static void applyPatch(File oldFile, File patchFile, File newFile)
- throws IOException, CompressorException, InvalidHeaderException {
+ throws IOException, InterruptedException {
+ applyPatch(oldFile, patchFile, newFile, DeltaPlan.forOldFile(oldFile));
+ }
+
+ public static void applyPatch(File oldFile, File patchFile, File newFile, DeltaPlan plan)
+ throws IOException, InterruptedException {
+ requireFile(oldFile, "oldFile");
+ requireFile(patchFile, "patchFile");
long startNanos = System.nanoTime();
- boolean entryLevel = isEntryLevelPatch(patchFile);
- if (entryLevel) {
- applyEntryLevelPatch(oldFile, patchFile, newFile);
- } else {
- FileUI.patch(oldFile, newFile, patchFile);
- }
- System.out.printf("[applyPatch] %s old=%d patch=%d new=%d 耗时 %s%n",
- entryLevel ? "entry-level" : "whole-file",
- oldFile.length(), patchFile.length(), newFile.length(),
+
+ List args = new ArrayList<>(plan.patchArgs());
+ args.add("-f");
+ args.add(oldFile.getPath());
+ args.add(patchFile.getPath());
+ args.add(newFile.getPath());
+
+ ProcessResult result = exec(hpatchzPath(), args);
+ result.requireSuccess("hpatchz");
+ System.out.print(result.output());
+
+ System.out.printf("[applyPatch] tier=%s patch=%s old=%d patch=%d new=%d 耗时 %s%n",
+ plan.tier(), String.join(" ", plan.patchArgs()), oldFile.length(),
+ patchFile.length(), newFile.length(),
formatDuration(System.nanoTime() - startNanos));
}
+ /** 打印补丁信息(hdiffz -info)。 */
+ public static String patchInfo(File patchFile) throws IOException, InterruptedException {
+ requireFile(patchFile, "patchFile");
+ ProcessResult result = exec(hdiffzPath(), List.of("-info", patchFile.getPath()));
+ result.requireSuccess("hdiffz -info");
+ return result.output();
+ }
+
/** 补丁率:patch 大小 / 新包大小。 */
public static double patchRatio(File newFile, File patchFile) {
long newSize = Math.max(1L, newFile.length());
return (double) patchFile.length() / newSize;
}
+ private static Path hdiffzPath() throws IOException {
+ return binaryDir().resolve(currentPlatform().hdiffzName());
+ }
+
+ private static Path hpatchzPath() throws IOException {
+ return binaryDir().resolve(currentPlatform().hpatchzName());
+ }
+
// ==========================================================
- // 四、条目级实现
+ // 四、进程调用
// ==========================================================
- /** 条目级 diff:未变化条目记 COPY,超过上限的改动条目直接携带(ADD)。 */
- private static void createEntryLevelPatch(File oldFile, File newFile, File patchFile,
- DeltaPlan plan)
- throws IOException, CompressorException, InvalidHeaderException {
- Map oldDigests = scanDigests(oldFile);
- try (ZipFile oldZip = new ZipFile(oldFile);
- ZipFile newZip = new ZipFile(newFile);
- ZipOutputStream patchOut = new ZipOutputStream(
- new BufferedOutputStream(new FileOutputStream(patchFile)))) {
+ private record ProcessResult(int exitCode, String output) {
- List manifest = new ArrayList<>();
- int dataIndex = 0;
- var entries = newZip.entries();
- while (entries.hasMoreElements()) {
- ZipEntry entry = entries.nextElement();
- if (entry.isDirectory()) {
- continue;
- }
- String name = entry.getName();
- long[] newDigest = new long[2];
- File newEntryFile = extractAndDigest(newZip, name, newDigest);
- try {
- long[] oldDigest = oldDigests.get(name);
- if (oldDigest != null
- && oldDigest[0] == newDigest[0]
- && oldDigest[1] == newDigest[1]) {
- manifest.add("COPY\t" + name);
- continue;
- }
- boolean canDiff = oldDigest != null
- && newDigest[0] <= plan.maxDiffEntrySize()
- && oldDigest[0] <= plan.maxDiffEntrySize();
- if (canDiff) {
- File oldEntryFile = extractEntry(oldZip, name);
- File entryPatch = Files.createTempFile("apkdelta-entry-", ".patch").toFile();
- try {
- FileUI.diff(oldEntryFile, newEntryFile, entryPatch, plan.compressor());
- String dataEntry = "delta-data/%06d.patch".formatted(dataIndex++);
- writeFileAsEntry(entryPatch, dataEntry, patchOut);
- manifest.add("DIFF\t" + name + "\t" + dataEntry);
- } finally {
- deleteQuietly(oldEntryFile);
- deleteQuietly(entryPatch);
- }
- } else {
- String dataEntry = "delta-data/%06d.raw".formatted(dataIndex++);
- writeFileAsEntry(newEntryFile, dataEntry, patchOut);
- manifest.add("ADD\t" + name + "\t" + dataEntry);
- }
- } finally {
- deleteQuietly(newEntryFile);
- }
+ void requireSuccess(String tool) throws IOException {
+ if (exitCode == 0) {
+ return;
}
- patchOut.putNextEntry(new ZipEntry(MANIFEST_ENTRY));
- patchOut.write(String.join("\n", manifest).getBytes(StandardCharsets.UTF_8));
- patchOut.closeEntry();
+ String text = output.stripTrailing();
+ if (text.length() > 4000) {
+ text = "..." + text.substring(text.length() - 4000);
+ }
+ throw new IOException(tool + " 执行失败 (exit=" + exitCode + ")\n" + text);
}
}
- /** 条目级 patch 应用:按清单逐条 COPY / ADD / DIFF 重建新 zip。 */
- private static void applyEntryLevelPatch(File oldFile, File patchFile, File newFile)
- throws IOException, CompressorException, InvalidHeaderException {
- try (ZipFile oldZip = new ZipFile(oldFile);
- ZipFile patchZip = new ZipFile(patchFile);
- ZipOutputStream newOut = new ZipOutputStream(
- new BufferedOutputStream(new FileOutputStream(newFile)))) {
- String manifest;
- try (InputStream in = patchZip.getInputStream(patchZip.getEntry(MANIFEST_ENTRY))) {
- manifest = new String(in.readAllBytes(), StandardCharsets.UTF_8);
- }
- for (String line : manifest.split("\n")) {
- if (line.isBlank()) {
- continue;
- }
- String[] parts = line.split("\t");
- String op = parts[0];
- String name = parts[1];
- switch (op) {
- case "COPY" -> copyZipEntry(oldZip, name, newOut, name);
- case "ADD" -> copyZipEntry(patchZip, parts[2], newOut, name);
- case "DIFF" -> {
- File oldEntryFile = extractEntry(oldZip, name);
- File entryPatch = extractEntry(patchZip, parts[2]);
- File restored = Files.createTempFile("apkdelta-restored-", ".entry").toFile();
- try {
- FileUI.patch(oldEntryFile, restored, entryPatch);
- writeFileAsEntry(restored, name, newOut);
- } finally {
- deleteQuietly(oldEntryFile);
- deleteQuietly(entryPatch);
- deleteQuietly(restored);
- }
- }
- default -> throw new IOException("未知清单操作: " + op);
- }
+ private static ProcessResult exec(Path exe, List args)
+ throws IOException, InterruptedException {
+ List command = new ArrayList<>(args.size() + 1);
+ command.add(exe.toString());
+ command.addAll(args);
+ Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
+
+ StringBuilder output = new StringBuilder();
+ Charset charset = Charset.forName(System.getProperty("native.encoding", "UTF-8"));
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream(), charset))) {
+ for (String line; (line = reader.readLine()) != null; ) {
+ output.append(line).append(System.lineSeparator());
}
}
+ return new ProcessResult(process.waitFor(), output.toString());
+ }
+
+ private static void requireFile(File file, String name) throws IOException {
+ if (file == null || !file.isFile()) {
+ throw new IOException(name + " 不存在或不是文件: " + file);
+ }
}
// ==========================================================
- // 五、工具方法
+ // 五、命令行
// ==========================================================
- /** patch 是否为条目级容器(zip 且含清单条目)。 */
- private static boolean isEntryLevelPatch(File patchFile) {
- try (ZipFile zip = new ZipFile(patchFile)) {
- return zip.getEntry(MANIFEST_ENTRY) != null;
- } catch (IOException e) {
- return false;
+ public static void main(String[] args) {
+ if (args.length == 0) {
+ printUsage();
+ return;
+ }
+ try {
+ switch (args[0].toLowerCase(Locale.ROOT)) {
+ case "diff", "hdiff" -> cliDiff(args);
+ case "patch", "hpatch" -> cliPatch(args);
+ case "info" -> cliInfo(args);
+ case "env", "platform" -> cliEnv();
+ default -> printUsage();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ System.err.println("Error: 执行被中断");
+ System.exit(1);
+ } catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(1);
}
}
- /** 扫描 zip:name -> [size, crc32]。 */
- private static Map scanDigests(File zip) throws IOException {
- Map map = new LinkedHashMap<>();
- try (ZipFile zf = new ZipFile(zip)) {
- var it = zf.entries();
- while (it.hasMoreElements()) {
- ZipEntry e = it.nextElement();
- if (e.isDirectory()) {
- continue;
+ private static void cliDiff(String[] args) throws IOException, InterruptedException {
+ if (args.length < 4) {
+ printUsage();
+ return;
+ }
+ File oldFile = new File(args[1]);
+ File newFile = new File(args[2]);
+ File patchFile = new File(args[3]);
+
+ int passthroughIndex = indexOfPassthrough(args, 4);
+ int optionEnd = passthroughIndex < 0 ? args.length : passthroughIndex;
+
+ SizeTier tier = null;
+ String compress = null;
+ Integer threads = null;
+ for (int i = 4; i < optionEnd; i++) {
+ switch (args[i]) {
+ case "-t", "--tier" -> {
+ requireValue(args, i, optionEnd);
+ tier = parseTier(args[++i]);
}
- map.put(e.getName(), new long[]{e.getSize(), crcOf(zf, e)});
+ case "-c", "--compress" -> {
+ requireValue(args, i, optionEnd);
+ compress = args[++i];
+ }
+ case "-p", "--threads" -> {
+ requireValue(args, i, optionEnd);
+ threads = parseThreads(args[++i]);
+ }
+ default -> throw new IllegalArgumentException("未知参数: " + args[i]);
}
}
- return map;
+
+ SizeTier effectiveTier = tier != null
+ ? tier
+ : SizeTier.of(Math.max(oldFile.length(), newFile.length()));
+ List extraArgs = passthroughIndex < 0
+ ? List.of()
+ : List.of(Arrays.copyOfRange(args, passthroughIndex + 1, args.length));
+ List diffArgs = overrideDiffArgs(
+ DeltaPlan.of(effectiveTier).diffArgs(), compress, threads, extraArgs);
+
+ createPatch(oldFile, newFile, patchFile,
+ DeltaPlan.of(effectiveTier).withDiffArgs(diffArgs));
}
- private static long crcOf(ZipFile zip, ZipEntry entry) throws IOException {
- CRC32 crc = new CRC32();
- byte[] buf = new byte[BUFFER_SIZE];
- try (InputStream in = zip.getInputStream(entry)) {
- int n;
- while ((n = in.read(buf)) != -1) {
- crc.update(buf, 0, n);
+ private static void cliPatch(String[] args) throws IOException, InterruptedException {
+ if (args.length < 4) {
+ printUsage();
+ return;
+ }
+ File oldFile = new File(args[1]);
+ File patchFile = new File(args[2]);
+ File newFile = new File(args[3]);
+
+ int passthroughIndex = indexOfPassthrough(args, 4);
+ int optionEnd = passthroughIndex < 0 ? args.length : passthroughIndex;
+
+ String cache = null;
+ for (int i = 4; i < optionEnd; i++) {
+ switch (args[i]) {
+ case "-c", "--cache" -> {
+ requireValue(args, i, optionEnd);
+ cache = args[++i];
+ }
+ default -> throw new IllegalArgumentException("未知参数: " + args[i]);
}
}
- return crc.getValue();
+
+ List extraArgs = passthroughIndex < 0
+ ? List.of()
+ : List.of(Arrays.copyOfRange(args, passthroughIndex + 1, args.length));
+ DeltaPlan plan = DeltaPlan.forOldFile(oldFile);
+ List patchArgs = overridePatchArgs(plan.patchArgs(), cache, extraArgs);
+
+ applyPatch(oldFile, patchFile, newFile, plan.withPatchArgs(patchArgs));
}
- /** 导出条目到临时文件,同时回填 [size, crc32]。 */
- private static File extractAndDigest(ZipFile zip, String name, long[] outSizeCrc)
- throws IOException {
- ZipEntry e = zip.getEntry(name);
- if (e == null) {
- throw new IOException("zip 条目不存在: " + name + " in " + zip.getName());
+ private static void cliInfo(String[] args) throws IOException, InterruptedException {
+ if (args.length < 2) {
+ printUsage();
+ return;
}
- File tmp = Files.createTempFile("apkdelta-", ".entry").toFile();
- CRC32 crc = new CRC32();
- long total = 0;
- byte[] buf = new byte[BUFFER_SIZE];
- try (InputStream in = zip.getInputStream(e);
- OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) {
- int n;
- while ((n = in.read(buf)) != -1) {
- out.write(buf, 0, n);
- crc.update(buf, 0, n);
- total += n;
+ System.out.print(patchInfo(new File(args[1])));
+ }
+
+ private static void cliEnv() throws IOException {
+ Platform platform = currentPlatform();
+ Path dir = binaryDir();
+ System.out.println("[环境]");
+ System.out.printf(" os.name = %s%n", System.getProperty("os.name"));
+ System.out.printf(" os.arch = %s%n", System.getProperty("os.arch"));
+ System.out.printf(" 平台 = %s (%s)%n", platform, platform.folder());
+ System.out.printf(" 版本 = HDiffPatch v%s%n", HDIFFPATCH_VERSION);
+ System.out.printf(" hdiffz = %s%n", dir.resolve(platform.hdiffzName()));
+ System.out.printf(" hpatchz = %s%n", dir.resolve(platform.hpatchzName()));
+ System.out.println();
+ System.out.println("[档位参数]");
+ for (SizeTier tier : SizeTier.values()) {
+ System.out.printf(" %-6s %-11s diff: %-48s patch: %s%n",
+ tier, tier.range(), String.join(" ", tier.diffArgs()),
+ String.join(" ", tier.patchArgs()));
+ }
+ }
+
+ private static int indexOfPassthrough(String[] args, int from) {
+ for (int i = from; i < args.length; i++) {
+ if ("--".equals(args[i])) {
+ return i;
}
}
- outSizeCrc[0] = total;
- outSizeCrc[1] = crc.getValue();
- return tmp;
+ return -1;
}
- private static File extractEntry(ZipFile zip, String name) throws IOException {
- ZipEntry e = zip.getEntry(name);
- if (e == null) {
- throw new IOException("zip 条目不存在: " + name + " in " + zip.getName());
- }
- File tmp = Files.createTempFile("apkdelta-", ".entry").toFile();
- try (InputStream in = zip.getInputStream(e);
- OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) {
- in.transferTo(out);
- }
- return tmp;
- }
-
- /** 将源 zip 条目(可改名)写入输出 zip。 */
- private static void copyZipEntry(ZipFile src, String srcName, ZipOutputStream out,
- String destName) throws IOException {
- ZipEntry e = src.getEntry(srcName);
- if (e == null) {
- throw new IOException("zip 条目不存在: " + srcName + " in " + src.getName());
- }
- try (InputStream in = src.getInputStream(e)) {
- out.putNextEntry(new ZipEntry(destName));
- in.transferTo(out);
- out.closeEntry();
+ private static void requireValue(String[] args, int index, int end) {
+ if (index + 1 >= end) {
+ throw new IllegalArgumentException(args[index] + " 缺少参数值");
}
}
- private static void writeFileAsEntry(File file, String entryName, ZipOutputStream out)
- throws IOException {
- try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
- out.putNextEntry(new ZipEntry(entryName));
- in.transferTo(out);
- out.closeEntry();
+ private static SizeTier parseTier(String value) {
+ try {
+ return SizeTier.valueOf(value.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "未知档位: " + value + "(small|medium|large|xlarge|huge)");
}
}
- private static void deleteQuietly(File file) {
- if (file != null) {
- file.delete();
+ private static int parseThreads(String value) {
+ try {
+ int threads = Integer.parseInt(value);
+ if (threads < 1) {
+ throw new NumberFormatException();
+ }
+ return threads;
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("线程数必须 >= 1: " + value);
}
}
+ private static List overrideDiffArgs(List base, String compress,
+ Integer threads, List extra) {
+ List result = new ArrayList<>();
+ for (String arg : base) {
+ if (compress != null && arg.startsWith("-c-")) {
+ continue;
+ }
+ if (threads != null && arg.matches("-p-\\d+")) {
+ continue;
+ }
+ result.add(arg);
+ }
+ if (compress != null) {
+ result.add(normalizePrefixed(compress, "-c-"));
+ }
+ if (threads != null) {
+ result.add("-p-" + threads);
+ }
+ result.addAll(extra);
+ return result;
+ }
+
+ private static List overridePatchArgs(List base, String cache,
+ List extra) {
+ List result = new ArrayList<>();
+ for (String arg : base) {
+ if (cache != null && arg.startsWith("-s-")) {
+ continue;
+ }
+ result.add(arg);
+ }
+ if (cache != null) {
+ result.add(normalizePrefixed(cache, "-s-"));
+ }
+ result.addAll(extra);
+ return result;
+ }
+
+ private static String normalizePrefixed(String value, String prefix) {
+ String trimmed = value.trim();
+ return trimmed.startsWith(prefix) ? trimmed : prefix + trimmed;
+ }
+
/** 时长格式化:ms / s / min / h。 */
private static String formatDuration(long nanos) {
double ms = nanos / 1_000_000.0;
@@ -518,4 +747,32 @@ public final class ApkDeltaUpdater {
return String.format(Locale.ROOT, "%d h %d min %.3f s",
hours, minutes % 60, totalSeconds - minutes * 60.0);
}
+
+ private static void printUsage() {
+ System.out.println("""
+ 用法:
+ diff [-t|--tier small|medium|large|xlarge|huge]
+ [-c|--compress ]
+ [-p|--threads ]
+ [-- <额外 hdiffz 参数>]
+ patch [-c|--cache ] [-- <额外 hpatchz 参数>]
+ info
+ env
+
+ 示例:
+ java -jar MavenUtils.jar diff old.apk new.apk app.patch
+ java -jar MavenUtils.jar diff old.apk new.apk app.patch -t large -c zstd-19 -p 8
+ java -jar MavenUtils.jar patch old.apk app.patch restored.apk
+ java -jar MavenUtils.jar info app.patch
+ java -jar MavenUtils.jar env
+
+ 说明:
+ diff 按 old/new 较大者自动选档(64/128/256/512MB),-t 可强制档位;
+ -c 覆盖档位默认压缩(lzma2 / zstd / bzip2 / no 等);
+ -p 覆盖 hdiffz 线程数;-- 之后的参数原样传给 hdiffz(如 -d 跳过回验);
+ patch 按旧包大小选缓存档位,-c 覆盖缓存(如 64m);
+ -- 之后的参数原样传给 hpatchz;
+ 二进制按当前平台从内置 Releases(HDiffPatch 5.1.3)解压到临时目录,
+ 可用 -Dttstd.hdiffpatch.dir=<目录> 指定现成的 hdiffz/hpatchz。""");
+ }
}
diff --git a/src/main/java/com/ttstd/BsDiffTool.java b/src/main/java/com/ttstd/BsDiffTool.java
deleted file mode 100644
index 1dc9529..0000000
--- a/src/main/java/com/ttstd/BsDiffTool.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package com.ttstd;
-
-import io.sigpipe.jbsdiff.InvalidHeaderException;
-import io.sigpipe.jbsdiff.ui.FileUI;
-import org.apache.commons.compress.compressors.CompressorException;
-import org.apache.commons.compress.compressors.CompressorStreamFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Locale;
-
-/**
- * 基于 jbsdiff(io.sigpipe:jbsdiff)的命令行 bsdiff / bspatch 工具。
- *
- * 用法:
- *
- * diff <oldFile> <newFile> <patchFile> [-c|--compressor bzip2|gz|pack200|xz]
- * patch <oldFile> <patchFile> <newFile>
- *
- *
- * 示例:
- *
- * java -jar MavenUtils.jar diff old.bin new.bin patch.bin
- * java -jar MavenUtils.jar patch old.bin patch.bin restored.bin
- *
- *
- * 每次 bsdiff / bspatch 执行完成后都会输出耗时。
- */
-public class BsDiffTool {
-
- private static final String DEFAULT_COMPRESSOR = CompressorStreamFactory.BZIP2;
-
- private BsDiffTool() {
- }
-
- public static void main(String[] args) {
- if (args.length < 1) {
- printUsage();
- return;
- }
-
- String command = args[0].toLowerCase(Locale.ROOT);
- try {
- switch (command) {
- case "bsdiff" -> diff(args);
- case "bspatch" -> patch(args);
- default -> printUsage();
- }
- } catch (CompressorException | InvalidHeaderException | IOException e) {
- System.err.println("Error: " + e.getMessage());
- e.printStackTrace();
- System.exit(1);
- }
- }
-
- /**
- * bsdiff:由 oldFile 与 newFile 生成 patchFile。
- */
- private static void diff(String[] args)
- throws CompressorException, InvalidHeaderException, IOException {
- if (args.length < 4) {
- printUsage();
- return;
- }
-
- File oldFile = new File(args[1]);
- File newFile = new File(args[2]);
- File patchFile = new File(args[3]);
- String compression = parseCompressor(args);
-
- requireExists(oldFile, "oldFile");
- requireExists(newFile, "newFile");
-
- long startNanos = System.nanoTime();
- FileUI.diff(oldFile, newFile, patchFile, compression);
-
- System.out.printf(
- "[bsdiff] %s (%d bytes) -> %s (%d bytes), compressor=%s%n",
- oldFile, oldFile.length(), newFile, newFile.length(), compression);
- System.out.printf("[bsdiff] patch 文件已生成: %s (%d bytes)%n",
- patchFile, patchFile.length());
- System.out.printf("[bsdiff] bsdiff 完成,耗时 %s%n",
- formatDuration(System.nanoTime() - startNanos));
- }
-
- /**
- * bspatch:由 oldFile 与 patchFile 还原出 newFile(压缩算法自动识别)。
- */
- private static void patch(String[] args)
- throws CompressorException, InvalidHeaderException, IOException {
- if (args.length < 4) {
- printUsage();
- return;
- }
-
- File oldFile = new File(args[1]);
- File patchFile = new File(args[2]);
- File newFile = new File(args[3]);
-
- requireExists(oldFile, "oldFile");
- requireExists(patchFile, "patchFile");
-
- long startNanos = System.nanoTime();
- FileUI.patch(oldFile, newFile, patchFile);
-
- System.out.printf(
- "[bspatch] %s (%d bytes) + %s (%d bytes) -> %s (%d bytes)%n",
- oldFile, oldFile.length(), patchFile, patchFile.length(),
- newFile, newFile.length());
- System.out.printf("[bspatch] bspatch 完成,耗时 %s%n",
- formatDuration(System.nanoTime() - startNanos));
- }
-
- /**
- * 从参数中解析压缩算法,支持 "-c xxx" / "--compressor xxx",默认 bzip2。
- */
- private static String parseCompressor(String[] args) {
- for (int i = 4; i < args.length - 1; i++) {
- if ("-c".equals(args[i]) || "--compressor".equals(args[i])) {
- String name = args[i + 1].toLowerCase(Locale.ROOT);
- // CompressorStreamFactory 中 gzip 的别名为 "gz"
- return "gzip".equals(name) ? CompressorStreamFactory.GZIP : name;
- }
- }
- return DEFAULT_COMPRESSOR;
- }
-
- private static void requireExists(File file, String name) {
- if (!file.isFile()) {
- System.err.printf("Error: %s 不存在或不是文件: %s%n", name, file);
- System.exit(1);
- }
- }
-
- /**
- * 将时长(纳秒)转换为可读字符串:
- * 不足 1 秒用 ms,不足 1 分钟用 s,不足 1 小时用 min,否则用 h。
- */
- private static String formatDuration(long nanos) {
- double ms = nanos / 1_000_000.0;
- if (ms < 1_000.0) {
- return String.format(Locale.ROOT, "%.3f ms", ms);
- }
- double totalSeconds = ms / 1_000.0;
- if (totalSeconds < 60.0) {
- return String.format(Locale.ROOT, "%.3f s", totalSeconds);
- }
- long minutes = (long) (totalSeconds / 60.0);
- if (minutes < 60) {
- return String.format(Locale.ROOT, "%d min %.3f s", minutes, totalSeconds - minutes * 60.0);
- }
- long hours = minutes / 60;
- return String.format(Locale.ROOT, "%d h %d min %.3f s",
- hours, minutes % 60, totalSeconds - minutes * 60.0);
- }
-
- private static void printUsage() {
- System.out.println("""
- 用法:
- diff [-c|--compressor bzip2|gz|pack200|xz]
- patch
-
- 示例:
- java -jar MavenUtils.jar diff old.bin new.bin patch.bin
- java -jar MavenUtils.jar patch old.bin patch.bin restored.bin
-
- 说明:
- diff 默认使用 bzip2 压缩(与 bsdiff 参考实现兼容),
- 可用 --compressor 指定 gz / pack200 / xz;
- patch 会自动识别补丁所用的压缩算法。""");
- }
-}
diff --git a/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux64.zip b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux64.zip
new file mode 100644
index 0000000..fc8f324
Binary files /dev/null and b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux64.zip differ
diff --git a/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux_arm64.zip b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux_arm64.zip
new file mode 100644
index 0000000..a3e334a
Binary files /dev/null and b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_linux_arm64.zip differ
diff --git a/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_macos.zip b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_macos.zip
new file mode 100644
index 0000000..2233f22
Binary files /dev/null and b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_macos.zip differ
diff --git a/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_windows64.zip b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_windows64.zip
new file mode 100644
index 0000000..0bd5429
Binary files /dev/null and b/src/main/resources/hdiffpatch/hdiffpatch_v5.1.3_bin_windows64.zip differ