feat: 用 HDiffPatch 重写增量更新并内置常用平台二进制

- 移除 jbsdiff,改为调用 hdiffz/hpatchz,整包逐字节还原
- 按包大小分五档调参,全部使用流式模式控制内存(单进程峰值约 104MB)
- 内置 windows64/linux64/linux_arm64/macos 二进制,其余平台可用 -Dttstd.hdiffpatch.dir 指定
- 新增 diff/patch/info/env 命令行
This commit is contained in:
2026-10-24 20:12:15 +08:00
parent f7a0bef676
commit e9fca08e1c
8 changed files with 596 additions and 518 deletions

View File

@@ -18,7 +18,7 @@
<configuration>
<transformers>
<transformer>
<mainClass>com.ttstd.BsDiffTool</mainClass>
<mainClass>com.ttstd.ApkDeltaUpdater</mainClass>
</transformer>
</transformers>
</configuration>

View File

@@ -13,13 +13,6 @@
<artifactId>lunar</artifactId>
<version>1.7.7</version>
</dependency>
<!-- Java 版 bsdiff/bspatch(malensek/jbsdiff,BSD 协议),
传递依赖 org.apache.commons:commons-compress -->
<dependency>
<groupId>io.sigpipe</groupId>
<artifactId>jbsdiff</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>25</maven.compiler.source>
@@ -43,7 +36,7 @@
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.ttstd.BsDiffTool</mainClass>
<mainClass>com.ttstd.ApkDeltaUpdater</mainClass>
</transformer>
</transformers>
</configuration>

File diff suppressed because it is too large Load Diff

View File

@@ -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 工具。
*
* <p>用法:</p>
* <pre>
* diff &lt;oldFile&gt; &lt;newFile&gt; &lt;patchFile&gt; [-c|--compressor bzip2|gz|pack200|xz]
* patch &lt;oldFile&gt; &lt;patchFile&gt; &lt;newFile&gt;
* </pre>
*
* <p>示例:</p>
* <pre>
* java -jar MavenUtils.jar diff old.bin new.bin patch.bin
* java -jar MavenUtils.jar patch old.bin patch.bin restored.bin
* </pre>
*
* <p>每次 bsdiff / bspatch 执行完成后都会输出耗时。</p>
*/
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 <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
说明:
diff 默认使用 bzip2 压缩(与 bsdiff 参考实现兼容),
可用 --compressor 指定 gz / pack200 / xz;
patch 会自动识别补丁所用的压缩算法。""");
}
}