feat: 添加 jbsdiff 依赖并配置 shade 打包为可执行工具

This commit is contained in:
2026-10-24 18:04:42 +08:00
parent 76ae961ebd
commit f7a0bef676
4 changed files with 761 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ttstd</groupId>
<artifactId>MavenUtils</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.ttstd.BsDiffTool</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
</properties>
</project>

33
pom.xml
View File

@@ -13,6 +13,13 @@
<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>
@@ -20,4 +27,30 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- 打包为可直接 java -jar 运行的命令行工具 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.ttstd.BsDiffTool</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,521 @@
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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap;
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;
/**
* APK 省流增量更新:按文件大小分档,自动选择差量引擎与执行模式。
*
* <h3>一、工具 Android 实现评估(先评估、再用依赖)</h3>
* <table border="1">
* <tr><th>工具</th><th>Android 端形态</th><th>官方 Maven/AAR</th><th>结论</th></tr>
* <tr><td>HDiffPatch</td><td>C++,需 NDK 交叉编译 + 自行 JNI 封装</td><td>无</td>
* <td>{@link Engine#HDIFFPATCH}:仅运行时探测,本工程未内置</td></tr>
* <tr><td>xdelta3</td><td>C,需 NDK 交叉编译 + JNI,生态仅零散移植</td><td>无</td>
* <td>{@link Engine#XDELTA3}:同上</td></tr>
* <tr><td>bsdiff</td><td>C,有 Android 移植(Tinker / AOSP),非 Maven 包</td><td>无</td>
* <td>由纯 Java 实现 jbsdiff 替代</td></tr>
* <tr><td>jbsdiff</td><td>纯 Java,无 native 依赖,Android 可直接运行</td><td>有</td>
* <td>{@link Engine#BSDIFF}:默认执行引擎</td></tr>
* <tr><td>UpdateEngine / imgdiff</td><td>{@code @SystemApi},系统签名才能调用</td><td>无</td>
* <td>普通 App 不可用,排除</td></tr>
* </table>
* <p>结论:HDiffPatch / xdelta3 无现成 Android 依赖,接入需自建 NDK/JNI;本工程为纯 Java
* Maven 工具,执行层使用 {@code io.sigpipe:jbsdiff}。分档仍按各档理论最优引擎规划,
* native 引擎可用时可平滑替换。</p>
*
* <h3>二、分档策略(以新包大小为准)</h3>
* <table border="1">
* <tr><th>档位</th><th>大小</th><th>首选引擎</th><th>模式</th><th>压缩</th>
* <th>单条目上限</th></tr>
* <tr><td>SMALL</td><td>&lt; 64MB</td><td>HDiffPatch</td><td>整包</td><td>bzip2</td><td>-</td></tr>
* <tr><td>MEDIUM</td><td>64~128MB</td><td>xdelta3</td><td>整包</td><td>gzip</td><td>-</td></tr>
* <tr><td>LARGE</td><td>128~256MB</td><td>bsdiff</td><td>条目级</td><td>bzip2</td><td>64MB</td></tr>
* <tr><td>XLARGE</td><td>256~512MB</td><td>bsdiff</td><td>条目级</td><td>bzip2</td><td>32MB</td></tr>
* <tr><td>HUGE</td><td>&gt; 512MB</td><td>bsdiff</td><td>条目级</td><td>bzip2</td><td>16MB</td></tr>
* </table>
* <p>128MB 起改条目级:整包 diff 峰值约 3~5 倍文件大小,会超过服务器 1G 可用内存;
* 条目级把单次 diff 对象降为单条目。补丁率超过 40% 时建议回退全量下发。</p>
*
* <h3>三、条目级 patch 容器格式</h3>
* <p>patch 本身是 zip,含 {@code delta-data/NNNNNN.patch|raw} 与清单
* {@code META-INF/delta-manifest},清单每行一条操作(tab 分隔):</p>
* <pre>
* COPY &lt;name&gt; old 同名同 CRC,原样拷贝
* ADD &lt;name&gt; &lt;dataEntry&gt; old 无同名或超上限,直接携带
* DIFF &lt;name&gt; &lt;dataEntry&gt; old 条目 + 容器内 bsdiff 补丁还原
* </pre>
*/
public final class ApkDeltaUpdater {
private static final long MB = 1024L * 1024L;
/** 条目级 patch 容器中清单条目的名称。 */
private static final String MANIFEST_ENTRY = "META-INF/delta-manifest";
/** 补丁率超过该值即失去省流意义,应回退全量下发。 */
public static final double FULL_PACKAGE_THRESHOLD = 0.40;
private static final int BUFFER_SIZE = 64 * 1024;
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);
private final String displayName;
private final String nativeLibrary;
Engine(String displayName, String nativeLibrary) {
this.displayName = displayName;
this.nativeLibrary = nativeLibrary;
}
public String displayName() {
return displayName;
}
/** 是否为 native 引擎(需 NDK 编译与 JNI 绑定)。 */
public boolean isNative() {
return nativeLibrary != null;
}
/** 本工程是否内置该引擎的执行实现(目前仅 jbsdiff)。 */
public boolean hasExecutor() {
return this == BSDIFF;
}
/** 运行时探测:native 引擎尝试加载动态库,失败即视为不可用。 */
public boolean isAvailable() {
if (!isNative()) {
return true;
}
try {
System.loadLibrary(nativeLibrary);
return true;
} catch (UnsatisfiedLinkError | SecurityException e) {
return false;
}
}
}
/** 打印各引擎的 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());
}
System.out.println(" 结论: HDiffPatch/xdelta3 无现成 Android 依赖,执行层回退 bsdiff(jbsdiff)");
}
// ==========================================================
// 二、按文件大小分档
// ==========================================================
/** 档位(上界不含):SMALL &lt; 64MB &lt; MEDIUM &lt; 128MB &lt; LARGE &lt; 256MB
* &lt; XLARGE &lt; 512MB &lt; HUGE。 */
public enum SizeTier {
SMALL(64 * MB),
MEDIUM(128 * MB),
LARGE(256 * MB),
XLARGE(512 * MB),
HUGE(Long.MAX_VALUE);
private final long upperBoundExclusive;
SizeTier(long upperBoundExclusive) {
this.upperBoundExclusive = upperBoundExclusive;
}
public long upperBoundExclusive() {
return upperBoundExclusive;
}
/** 根据文件大小(字节)归档。 */
public static SizeTier of(long fileSize) {
for (SizeTier tier : values()) {
if (fileSize < tier.upperBoundExclusive) {
return tier;
}
}
return HUGE;
}
}
/** 执行模式。 */
public enum Mode {
/** 整包 bsdiff。 */
WHOLE_FILE,
/** zip 条目级 bsdiff(压低单次 diff 内存峰值)。 */
ENTRY_LEVEL,
/** 不差量,直接携带全量。 */
FULL_PACKAGE
}
/**
* 差量执行计划,由 {@link DeltaPlan#plan(long)} 按新包大小生成。
*
* @param tier 命中的档位
* @param preferredEngine 该档理论最优引擎(不可用时回退)
* @param mode 执行模式
* @param compressor bsdiff 压缩算法(commons-compress 名称)
* @param maxDiffEntrySize 条目级下单条目 diff 上限(字节),整包模式为 0
*/
public record DeltaPlan(SizeTier tier, Engine preferredEngine, Mode mode,
String compressor, long maxDiffEntrySize) {
/** 根据新包大小生成差量计划(分档策略的唯一入口)。 */
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);
};
}
/** 实际可执行引擎:首选引擎无内置实现或不可用时回退 jbsdiff。 */
public Engine resolveEngine() {
if (preferredEngine.hasExecutor() && preferredEngine.isAvailable()) {
return preferredEngine;
}
return Engine.BSDIFF;
}
}
// ==========================================================
// 三、生成 / 应用
// ==========================================================
/** 按新包大小自动分档,生成 patch。 */
public static void createPatch(File oldFile, File newFile, File patchFile)
throws IOException, CompressorException, InvalidHeaderException {
createPatch(oldFile, newFile, patchFile, DeltaPlan.plan(newFile.length()));
}
/**
* 按指定计划生成 patch。生成后计算补丁率,超过
* {@link #FULL_PACKAGE_THRESHOLD} 时输出回退全量的警告。
*/
public static void createPatch(File oldFile, File newFile, File patchFile, DeltaPlan plan)
throws IOException, CompressorException, InvalidHeaderException {
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);
}
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(),
patchFile.length(), ratio * 100, formatDuration(System.nanoTime() - startNanos));
if (ratio > FULL_PACKAGE_THRESHOLD) {
System.out.printf("[createPatch][warn] 补丁率超过 %.0f%%,建议回退全量下发%n",
FULL_PACKAGE_THRESHOLD * 100);
}
}
/** 应用 patch 还原新包,自动识别整包 bsdiff 与条目级容器两种格式。 */
public static void applyPatch(File oldFile, File patchFile, File newFile)
throws IOException, CompressorException, InvalidHeaderException {
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(),
formatDuration(System.nanoTime() - startNanos));
}
/** 补丁率:patch 大小 / 新包大小。 */
public static double patchRatio(File newFile, File patchFile) {
long newSize = Math.max(1L, newFile.length());
return (double) patchFile.length() / newSize;
}
// ==========================================================
// 四、条目级实现
// ==========================================================
/** 条目级 diff:未变化条目记 COPY,超过上限的改动条目直接携带(ADD)。 */
private static void createEntryLevelPatch(File oldFile, File newFile, File patchFile,
DeltaPlan plan)
throws IOException, CompressorException, InvalidHeaderException {
Map<String, long[]> oldDigests = scanDigests(oldFile);
try (ZipFile oldZip = new ZipFile(oldFile);
ZipFile newZip = new ZipFile(newFile);
ZipOutputStream patchOut = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(patchFile)))) {
List<String> 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);
}
}
patchOut.putNextEntry(new ZipEntry(MANIFEST_ENTRY));
patchOut.write(String.join("\n", manifest).getBytes(StandardCharsets.UTF_8));
patchOut.closeEntry();
}
}
/** 条目级 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);
}
}
}
}
// ==========================================================
// 五、工具方法
// ==========================================================
/** 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;
}
}
/** 扫描 zip:name -> [size, crc32]。 */
private static Map<String, long[]> scanDigests(File zip) throws IOException {
Map<String, long[]> map = new LinkedHashMap<>();
try (ZipFile zf = new ZipFile(zip)) {
var it = zf.entries();
while (it.hasMoreElements()) {
ZipEntry e = it.nextElement();
if (e.isDirectory()) {
continue;
}
map.put(e.getName(), new long[]{e.getSize(), crcOf(zf, e)});
}
}
return map;
}
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);
}
}
return crc.getValue();
}
/** 导出条目到临时文件,同时回填 [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());
}
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;
}
}
outSizeCrc[0] = total;
outSizeCrc[1] = crc.getValue();
return tmp;
}
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 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 void deleteQuietly(File file) {
if (file != null) {
file.delete();
}
}
/** 时长格式化:ms / s / 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);
}
}

View File

@@ -0,0 +1,172 @@
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 会自动识别补丁所用的压缩算法。""");
}
}