@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CodePush.podspec +35 -3
- package/README.md +115 -3
- package/THIRD-PARTY-NOTICES +96 -0
- package/android/app/build.gradle +19 -0
- package/android/app/proguard-rules.pro +5 -0
- package/android/app/src/main/cpp/CMakeLists.txt +38 -0
- package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -2
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
- package/android/codepush-export.gradle +115 -0
- package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
- package/cli/commands/bundleCommand/index.ts +9 -1
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +118 -0
- package/cli/commands/releaseCommand/index.ts +20 -2
- package/cli/commands/releaseCommand/release.test.ts +582 -0
- package/cli/commands/releaseCommand/release.ts +232 -15
- package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
- package/cli/dist/commands/bundleCommand/index.js +5 -1
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +8 -1
- package/cli/dist/commands/releaseCommand/index.test.js +96 -0
- package/cli/dist/commands/releaseCommand/release.js +165 -17
- package/cli/dist/commands/releaseCommand/release.test.js +367 -0
- package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
- package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
- package/cli/dist/functions/makeCodePushBundle.js +4 -2
- package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
- package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
- package/cli/dist/jest.globalSetup.js +38 -0
- package/cli/dist/utils/binaryPatch.js +86 -0
- package/cli/dist/utils/binaryPatch.test.js +190 -0
- package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
- package/cli/functions/makeBinaryPatchBundle.ts +367 -0
- package/cli/functions/makeCodePushBundle.ts +4 -2
- package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
- package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
- package/cli/jest.globalSetup.ts +43 -0
- package/cli/utils/binaryPatch.test.ts +234 -0
- package/cli/utils/binaryPatch.ts +103 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
- package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
- package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
- package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
- package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
- package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
- package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
- package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
- package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
- package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
- package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
- package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
- package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
- package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
- package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
- package/ios/CodePush/CodePush.h +18 -1
- package/ios/CodePush/CodePush.mm +16 -1
- package/ios/CodePush/CodePushBinaryPatch.h +80 -0
- package/ios/CodePush/CodePushBinaryPatch.m +473 -0
- package/ios/CodePush/CodePushPackage.m +183 -2
- package/ios/CodePush/CodePushUpdateUtils.m +49 -1
- package/package.json +10 -3
- package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
- package/scripts/export-embedded-bundle.sh +76 -0
- package/src/CodePush.js +46 -3
- package/src/CodePush.test.js +382 -0
- package/src/package-mixins.js +20 -2
- package/typings/react-native-code-push.d.ts +77 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
|
7
|
+
import { applyPatch, generatePatch, resolveBinaryPatchTool } from "./binaryPatch.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Locks the binary patch codec contract: which bytes a patch restores, and which
|
|
11
|
+
* kinds of broken input the appliers do and do not catch.
|
|
12
|
+
*
|
|
13
|
+
* Everything here runs against real binaries and real bytes - hdiffz/hpatchz and a
|
|
14
|
+
* host build of the native applier - because the whole point is to prove that the
|
|
15
|
+
* committed patch format really is what every applier understands.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Building the applier from source takes a while on a cold machine. */
|
|
19
|
+
const BUILD_APPLIER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
function findRepoRoot(): string {
|
|
22
|
+
let dir = process.cwd();
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir);
|
|
28
|
+
if (parent === dir) {
|
|
29
|
+
throw new Error(`cannot locate the repository root from ${process.cwd()}`);
|
|
30
|
+
}
|
|
31
|
+
dir = parent;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const repoRoot = findRepoRoot();
|
|
36
|
+
const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
|
|
37
|
+
const baseFixture = path.join(fixtureDir, "base.bundle");
|
|
38
|
+
const targetFixture = path.join(fixtureDir, "target.bundle");
|
|
39
|
+
const patchFixture = path.join(fixtureDir, "update.patch");
|
|
40
|
+
/**
|
|
41
|
+
* One shared tree builds the applier for both platforms: iOS pulls it in through the
|
|
42
|
+
* podspec at the repository root, Android through externalNativeBuild.
|
|
43
|
+
*/
|
|
44
|
+
const applierDir = path.join(repoRoot, "cpp", "binarypatch");
|
|
45
|
+
|
|
46
|
+
let workDir: string;
|
|
47
|
+
|
|
48
|
+
function sha256(filePath: string): string {
|
|
49
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function workPath(name: string): string {
|
|
53
|
+
return path.join(workDir, name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Writes a copy of `sourcePath` with `mutate` applied to its bytes. */
|
|
57
|
+
function writeMutatedCopy(sourcePath: string, name: string, mutate: (bytes: Buffer) => Buffer): string {
|
|
58
|
+
const destination = workPath(name);
|
|
59
|
+
fs.writeFileSync(destination, mutate(fs.readFileSync(sourcePath)));
|
|
60
|
+
return destination;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cCompiler(): string {
|
|
64
|
+
return process.env.CC || "cc";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasCCompiler(): boolean {
|
|
68
|
+
return spawnSync(cCompiler(), ["--version"], { stdio: "ignore" }).status === 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
beforeAll(() => {
|
|
72
|
+
// hdiffz/hpatchz are provisioned for the whole run by the jest global setup.
|
|
73
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-binary-patch-"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterAll(() => {
|
|
77
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("generatePatch/applyPatch", () => {
|
|
81
|
+
it("restores the target bytes from a freshly generated patch", () => {
|
|
82
|
+
const generatedPatch = workPath("generated.patch");
|
|
83
|
+
const restored = workPath("restored-from-generated.bundle");
|
|
84
|
+
|
|
85
|
+
generatePatch(baseFixture, targetFixture, generatedPatch);
|
|
86
|
+
applyPatch(baseFixture, generatedPatch, restored);
|
|
87
|
+
|
|
88
|
+
expect(fs.statSync(generatedPatch).size).toBeGreaterThan(0);
|
|
89
|
+
expect(sha256(restored)).toBe(sha256(targetFixture));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("restores the target bytes from the committed patch fixture", () => {
|
|
93
|
+
const restored = workPath("restored-from-fixture.bundle");
|
|
94
|
+
|
|
95
|
+
applyPatch(baseFixture, patchFixture, restored);
|
|
96
|
+
|
|
97
|
+
expect(sha256(restored)).toBe(sha256(targetFixture));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reports a missing patch tool with a message that says how to get it", () => {
|
|
101
|
+
const previous = process.env.HDIFFPATCH_TOOLS_DIR;
|
|
102
|
+
process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
|
|
103
|
+
try {
|
|
104
|
+
expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-hdiffpatch\.sh/);
|
|
105
|
+
} finally {
|
|
106
|
+
if (previous === undefined) {
|
|
107
|
+
delete process.env.HDIFFPATCH_TOOLS_DIR;
|
|
108
|
+
} else {
|
|
109
|
+
process.env.HDIFFPATCH_TOOLS_DIR = previous;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("applyPatch failure modes", () => {
|
|
116
|
+
it("fails on a patch with a corrupted header", () => {
|
|
117
|
+
const corrupted = writeMutatedCopy(patchFixture, "corrupt-header.patch", (bytes) => {
|
|
118
|
+
// Byte 0 is inside the patch type string, which the header parser reads first.
|
|
119
|
+
bytes[0] ^= 0xff;
|
|
120
|
+
return bytes;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(() => applyPatch(baseFixture, corrupted, workPath("out-corrupt-header.bundle"))).toThrow(/hpatchz/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("fails on a truncated patch", () => {
|
|
127
|
+
const truncated = writeMutatedCopy(patchFixture, "truncated.patch", (bytes) =>
|
|
128
|
+
bytes.subarray(0, Math.floor(bytes.length / 2)),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(() => applyPatch(baseFixture, truncated, workPath("out-truncated.bundle"))).toThrow(/hpatchz/);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A compressed diff carries no checksum of the base data, and the zstd streams
|
|
137
|
+
* inside it are written without content checksums, so an applier cannot tell every
|
|
138
|
+
* kind of wrong input apart from a legitimate one. These tests pin that down: the
|
|
139
|
+
* apply step may report success, but it never produces the target bytes. Callers
|
|
140
|
+
* must therefore verify the base and target hashes themselves rather than treating
|
|
141
|
+
* a successful apply as proof of a correct result.
|
|
142
|
+
*/
|
|
143
|
+
describe("errors the codec cannot detect", () => {
|
|
144
|
+
it("never produces the target bytes when a byte inside the compressed diff data is corrupted", () => {
|
|
145
|
+
const corrupted = writeMutatedCopy(patchFixture, "corrupt-body.patch", (bytes) => {
|
|
146
|
+
bytes[Math.floor(bytes.length / 2)] ^= 0xff;
|
|
147
|
+
return bytes;
|
|
148
|
+
});
|
|
149
|
+
const restored = workPath("out-corrupt-body.bundle");
|
|
150
|
+
|
|
151
|
+
// Whether the corruption is reported depends on where it lands: a flipped bit
|
|
152
|
+
// inside a zstd literals block decodes to different bytes without any error,
|
|
153
|
+
// while one that invalidates a block header does fail. Either way the result
|
|
154
|
+
// is not the target, which is all the caller can rely on.
|
|
155
|
+
let restoredHash: string | null = null;
|
|
156
|
+
try {
|
|
157
|
+
applyPatch(baseFixture, corrupted, restored);
|
|
158
|
+
restoredHash = sha256(restored);
|
|
159
|
+
} catch {
|
|
160
|
+
restoredHash = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expect(restoredHash).not.toBe(sha256(targetFixture));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("applies but produces wrong bytes when the base has the expected size and different content", () => {
|
|
167
|
+
const wrongBase = writeMutatedCopy(baseFixture, "wrong-base.bundle", (bytes) => {
|
|
168
|
+
bytes[1000] ^= 0xff;
|
|
169
|
+
return bytes;
|
|
170
|
+
});
|
|
171
|
+
const restored = workPath("out-wrong-base.bundle");
|
|
172
|
+
|
|
173
|
+
applyPatch(wrongBase, patchFixture, restored);
|
|
174
|
+
|
|
175
|
+
expect(fs.statSync(wrongBase).size).toBe(fs.statSync(baseFixture).size);
|
|
176
|
+
expect(sha256(restored)).not.toBe(sha256(targetFixture));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The native appliers on Android and iOS are built from the vendored sources, so a
|
|
182
|
+
* host build of the same sources is what proves they agree with hdiffz/hpatchz.
|
|
183
|
+
*/
|
|
184
|
+
const describeNativeApplier = hasCCompiler() ? describe : describe.skip;
|
|
185
|
+
|
|
186
|
+
describeNativeApplier("host build of the native applier", () => {
|
|
187
|
+
let hostApplier: string;
|
|
188
|
+
|
|
189
|
+
const runHostApplier = (oldPath: string, patch: string, outPath: string): number | null => {
|
|
190
|
+
const result = spawnSync(hostApplier, [oldPath, patch, outPath], { encoding: "utf8" });
|
|
191
|
+
return result.status;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
beforeAll(() => {
|
|
195
|
+
hostApplier = workPath("apply_patch_host");
|
|
196
|
+
const script = path.join(applierDir, "host", "build.sh");
|
|
197
|
+
const result = spawnSync(script, [hostApplier], { encoding: "utf8", timeout: BUILD_APPLIER_TIMEOUT_MS });
|
|
198
|
+
if (result.status !== 0) {
|
|
199
|
+
throw new Error(`${script} failed:\n${result.stdout ?? ""}${result.stderr ?? ""}`);
|
|
200
|
+
}
|
|
201
|
+
}, BUILD_APPLIER_TIMEOUT_MS);
|
|
202
|
+
|
|
203
|
+
it("restores the same target bytes as the CLI applier", () => {
|
|
204
|
+
const restored = workPath("out-host.bundle");
|
|
205
|
+
|
|
206
|
+
expect(runHostApplier(baseFixture, patchFixture, restored)).toBe(0);
|
|
207
|
+
expect(sha256(restored)).toBe(sha256(targetFixture));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("reports a corrupted patch header as exit code 3", () => {
|
|
211
|
+
const corrupted = writeMutatedCopy(patchFixture, "host-corrupt-header.patch", (bytes) => {
|
|
212
|
+
bytes[0] ^= 0xff;
|
|
213
|
+
return bytes;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(runHostApplier(baseFixture, corrupted, workPath("out-host-corrupt-header.bundle"))).toBe(3);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("reports a base of unexpected size as exit code 5", () => {
|
|
220
|
+
const shortBase = writeMutatedCopy(baseFixture, "host-short-base.bundle", (bytes) =>
|
|
221
|
+
bytes.subarray(0, bytes.length - 16),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(runHostApplier(shortBase, patchFixture, workPath("out-host-short-base.bundle"))).toBe(5);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("reports a truncated patch as exit code 6", () => {
|
|
228
|
+
const truncated = writeMutatedCopy(patchFixture, "host-truncated.patch", (bytes) =>
|
|
229
|
+
bytes.subarray(0, Math.floor(bytes.length / 2)),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(runHostApplier(baseFixture, truncated, workPath("out-host-truncated.bundle"))).toBe(6);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary patch codec for CodePush updates.
|
|
3
|
+
*
|
|
4
|
+
* A binary patch update carries only the difference between the bundle that is
|
|
5
|
+
* already inside the app binary and the bundle a release wants to run, instead of
|
|
6
|
+
* the whole bundle. Patches are produced by HDiffPatch's `hdiffz` and applied on
|
|
7
|
+
* device by the native appliers; `hpatchz` is the reference applier used here so a
|
|
8
|
+
* patch can be verified right after it is produced.
|
|
9
|
+
*
|
|
10
|
+
* The tool options below are part of the format contract, not tuning knobs:
|
|
11
|
+
* -m-6 the base bundle is held in memory while patching, which is what
|
|
12
|
+
* the native appliers do, and match score 6 suits bytecode
|
|
13
|
+
* -c-zstd-21-24 zstd is the only decompressor the native appliers link
|
|
14
|
+
* -f overwrite the output file if it already exists
|
|
15
|
+
* Changing them produces patches the native appliers cannot read.
|
|
16
|
+
*
|
|
17
|
+
* A patch contains no checksum of the base data, and its zstd streams carry no
|
|
18
|
+
* content checksums, so a successful apply is not proof of a correct result: a base
|
|
19
|
+
* of the right size but the wrong content, or a corrupted patch body, can both apply
|
|
20
|
+
* "successfully" and yield wrong bytes. Callers must verify the base and target
|
|
21
|
+
* hashes themselves.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "child_process";
|
|
25
|
+
import fs from "fs";
|
|
26
|
+
import path from "path";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Identifies the patch format so a client can refuse a patch it cannot apply.
|
|
30
|
+
* Bump the format version whenever the tool options above change.
|
|
31
|
+
*/
|
|
32
|
+
export const BINARY_PATCH_ALGORITHM = 'hdiffpatch-m-zstd';
|
|
33
|
+
export const BINARY_PATCH_FORMAT_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
export type BinaryPatchTool = 'hdiffz' | 'hpatchz';
|
|
36
|
+
|
|
37
|
+
const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
|
|
38
|
+
const HPATCHZ_OPTIONS = ['-f', '-m'];
|
|
39
|
+
|
|
40
|
+
const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
|
|
41
|
+
const TOOLS_DIR_NAME = '.hdiffpatch-tools';
|
|
42
|
+
const BUILD_SCRIPT_PATH = 'scripts/binary-patch/build-hdiffpatch.sh';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
|
|
46
|
+
* then at a `.hdiffpatch-tools` directory in the working directory or any directory
|
|
47
|
+
* above it. The tools are built from source rather than installed as a package
|
|
48
|
+
* dependency, so the error explains how to get them.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveBinaryPatchTool(tool: BinaryPatchTool): string {
|
|
51
|
+
const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
|
|
52
|
+
if (configuredDir) {
|
|
53
|
+
const configured = path.join(configuredDir, tool);
|
|
54
|
+
if (fs.existsSync(configured)) {
|
|
55
|
+
return configured;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
|
|
59
|
+
`Build the tools with '${BUILD_SCRIPT_PATH}'.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let directory = path.resolve(process.cwd());
|
|
64
|
+
for (;;) {
|
|
65
|
+
const candidate = path.join(directory, TOOLS_DIR_NAME, tool);
|
|
66
|
+
if (fs.existsSync(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
const parent = path.dirname(directory);
|
|
70
|
+
if (parent === directory) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
directory = parent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(
|
|
77
|
+
`'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
|
|
78
|
+
`Build it with '${BUILD_SCRIPT_PATH}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Writes the patch that turns the bundle at `basePath` into the one at `targetPath`. */
|
|
83
|
+
export function generatePatch(basePath: string, targetPath: string, patchPath: string): void {
|
|
84
|
+
runTool('hdiffz', [...HDIFFZ_OPTIONS, basePath, targetPath, patchPath]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Writes the bundle that `patchPath` produces from the bundle at `basePath`. */
|
|
88
|
+
export function applyPatch(basePath: string, patchPath: string, outputPath: string): void {
|
|
89
|
+
runTool('hpatchz', [...HPATCHZ_OPTIONS, basePath, patchPath, outputPath]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runTool(tool: BinaryPatchTool, args: string[]): void {
|
|
93
|
+
const executable = resolveBinaryPatchTool(tool);
|
|
94
|
+
const result = spawnSync(executable, args, { encoding: 'utf8' });
|
|
95
|
+
|
|
96
|
+
if (result.error) {
|
|
97
|
+
throw new Error(`failed to run ${tool}: ${result.error.message}`);
|
|
98
|
+
}
|
|
99
|
+
if (result.status !== 0) {
|
|
100
|
+
const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
|
|
101
|
+
throw new Error(`${tool} failed with ${reason}\n${result.stdout ?? ''}${result.stderr ?? ''}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#include "binarypatch_zstd_decompressor.h"
|
|
2
|
+
|
|
3
|
+
#include <stdlib.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
#include "zstd.h"
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Largest window a decompression session will accept, pinned to the window that the
|
|
10
|
+
* `-c-zstd-21-24` generation options use: no patch this project produces declares a
|
|
11
|
+
* window above 2^24. Keeping the bound at the generation limit means a corrupted
|
|
12
|
+
* frame header that asks for a wider window is rejected before anything is
|
|
13
|
+
* allocated, instead of being answered with an allocation that large - and applying
|
|
14
|
+
* a patch keeps several decompression sessions open at once, so the bound applies
|
|
15
|
+
* several times over. Changing the generation options is a patch format version
|
|
16
|
+
* change, and this bound moves with them.
|
|
17
|
+
*/
|
|
18
|
+
#define BINARYPATCH_ZSTD_WINDOW_LOG_MAX 24
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* One decompression session. The input and output buffers are allocated in the
|
|
22
|
+
* same block as the struct (`buffers` is their start) to keep the number of
|
|
23
|
+
* allocations per patch session down.
|
|
24
|
+
*/
|
|
25
|
+
typedef struct {
|
|
26
|
+
const hpatch_TStreamInput* codeStream;
|
|
27
|
+
hpatch_StreamPos_t codeReadPos;
|
|
28
|
+
hpatch_StreamPos_t codeEnd;
|
|
29
|
+
|
|
30
|
+
ZSTD_DStream* dstream;
|
|
31
|
+
ZSTD_inBuffer in;
|
|
32
|
+
size_t inCapacity;
|
|
33
|
+
ZSTD_outBuffer out;
|
|
34
|
+
size_t outReadPos; /* bytes of `out` already handed to the caller */
|
|
35
|
+
|
|
36
|
+
hpatch_dec_error_t decError;
|
|
37
|
+
unsigned char buffers[1]; /* [inCapacity bytes][out.size bytes] */
|
|
38
|
+
} binarypatch_zstd_session;
|
|
39
|
+
|
|
40
|
+
static hpatch_BOOL _zstd_fail(binarypatch_zstd_session* self) {
|
|
41
|
+
if (self->decError == hpatch_dec_ok) {
|
|
42
|
+
self->decError = hpatch_dec_error;
|
|
43
|
+
}
|
|
44
|
+
return hpatch_FALSE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static hpatch_BOOL _zstd_is_can_open(const char* compressType) {
|
|
48
|
+
return (0 == strcmp(compressType, BINARYPATCH_ZSTD_COMPRESS_TYPE)) ? hpatch_TRUE : hpatch_FALSE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static hpatch_decompressHandle _zstd_open(struct hpatch_TDecompress* decompressPlugin,
|
|
52
|
+
hpatch_StreamPos_t dataSize,
|
|
53
|
+
const struct hpatch_TStreamInput* codeStream,
|
|
54
|
+
hpatch_StreamPos_t code_begin,
|
|
55
|
+
hpatch_StreamPos_t code_end) {
|
|
56
|
+
const size_t inCapacity = ZSTD_DStreamInSize();
|
|
57
|
+
const size_t outCapacity = ZSTD_DStreamOutSize();
|
|
58
|
+
binarypatch_zstd_session* self;
|
|
59
|
+
|
|
60
|
+
/* The uncompressed size is tracked by the caller's patch stream, not here. */
|
|
61
|
+
(void)dataSize;
|
|
62
|
+
|
|
63
|
+
self = (binarypatch_zstd_session*)malloc(sizeof(binarypatch_zstd_session) + inCapacity + outCapacity);
|
|
64
|
+
if (!self) {
|
|
65
|
+
_hpatch_update_decError(decompressPlugin, hpatch_dec_mem_error);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
memset(self, 0, sizeof(binarypatch_zstd_session));
|
|
69
|
+
self->codeStream = codeStream;
|
|
70
|
+
self->codeReadPos = code_begin;
|
|
71
|
+
self->codeEnd = code_end;
|
|
72
|
+
self->in.src = self->buffers;
|
|
73
|
+
self->in.size = 0;
|
|
74
|
+
self->in.pos = 0; /* pos == size: the input buffer starts empty and is refilled on demand */
|
|
75
|
+
self->inCapacity = inCapacity;
|
|
76
|
+
self->out.dst = self->buffers + inCapacity;
|
|
77
|
+
self->out.size = outCapacity;
|
|
78
|
+
self->out.pos = 0;
|
|
79
|
+
self->outReadPos = 0;
|
|
80
|
+
|
|
81
|
+
self->dstream = ZSTD_createDStream();
|
|
82
|
+
if (!self->dstream) {
|
|
83
|
+
free(self);
|
|
84
|
+
_hpatch_update_decError(decompressPlugin, hpatch_dec_open_error);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
/*
|
|
88
|
+
* The window bound is applied here rather than left at zstd's default, and a
|
|
89
|
+
* session that cannot take it is refused: without the bound a corrupted frame
|
|
90
|
+
* header could ask for a much larger window than any patch legitimately needs.
|
|
91
|
+
*/
|
|
92
|
+
if (ZSTD_isError(ZSTD_initDStream(self->dstream)) ||
|
|
93
|
+
ZSTD_isError(ZSTD_DCtx_setParameter(self->dstream, ZSTD_d_windowLogMax,
|
|
94
|
+
BINARYPATCH_ZSTD_WINDOW_LOG_MAX))) {
|
|
95
|
+
ZSTD_freeDStream(self->dstream);
|
|
96
|
+
free(self);
|
|
97
|
+
_hpatch_update_decError(decompressPlugin, hpatch_dec_open_error);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
return self;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static hpatch_BOOL _zstd_close(struct hpatch_TDecompress* decompressPlugin,
|
|
104
|
+
hpatch_decompressHandle decompressHandle) {
|
|
105
|
+
binarypatch_zstd_session* self = (binarypatch_zstd_session*)decompressHandle;
|
|
106
|
+
hpatch_BOOL result = hpatch_TRUE;
|
|
107
|
+
if (!self) {
|
|
108
|
+
return hpatch_TRUE;
|
|
109
|
+
}
|
|
110
|
+
/* Report the session's failure through the plugin so the caller can read it. */
|
|
111
|
+
if (self->decError != hpatch_dec_ok) {
|
|
112
|
+
_hpatch_update_decError(decompressPlugin, self->decError);
|
|
113
|
+
}
|
|
114
|
+
if (0 != ZSTD_freeDStream(self->dstream)) {
|
|
115
|
+
result = hpatch_FALSE;
|
|
116
|
+
_hpatch_update_decError(decompressPlugin, hpatch_dec_close_error);
|
|
117
|
+
}
|
|
118
|
+
free(self);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* Must fill the whole `[out_part_data, out_part_data_end)` range; anything less is
|
|
124
|
+
* an error by the `hpatch_TDecompress` contract.
|
|
125
|
+
*/
|
|
126
|
+
static hpatch_BOOL _zstd_decompress_part(hpatch_decompressHandle decompressHandle,
|
|
127
|
+
unsigned char* out_part_data,
|
|
128
|
+
unsigned char* out_part_data_end) {
|
|
129
|
+
binarypatch_zstd_session* self = (binarypatch_zstd_session*)decompressHandle;
|
|
130
|
+
|
|
131
|
+
while (out_part_data < out_part_data_end) {
|
|
132
|
+
size_t ready = self->out.pos - self->outReadPos;
|
|
133
|
+
size_t ret;
|
|
134
|
+
|
|
135
|
+
if (ready > 0) {
|
|
136
|
+
const size_t wanted = (size_t)(out_part_data_end - out_part_data);
|
|
137
|
+
if (ready > wanted) {
|
|
138
|
+
ready = wanted;
|
|
139
|
+
}
|
|
140
|
+
memcpy(out_part_data, (const unsigned char*)self->out.dst + self->outReadPos, ready);
|
|
141
|
+
out_part_data += ready;
|
|
142
|
+
self->outReadPos += ready;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (self->in.pos == self->in.size) {
|
|
147
|
+
const hpatch_StreamPos_t remaining = self->codeEnd - self->codeReadPos;
|
|
148
|
+
unsigned char* inBuf = (unsigned char*)self->in.src;
|
|
149
|
+
size_t toRead = self->inCapacity;
|
|
150
|
+
if (remaining < (hpatch_StreamPos_t)toRead) {
|
|
151
|
+
toRead = (size_t)remaining;
|
|
152
|
+
}
|
|
153
|
+
self->in.pos = 0;
|
|
154
|
+
self->in.size = toRead;
|
|
155
|
+
if (toRead > 0) {
|
|
156
|
+
if (!self->codeStream->read(self->codeStream, self->codeReadPos, inBuf, inBuf + toRead)) {
|
|
157
|
+
return _zstd_fail(self);
|
|
158
|
+
}
|
|
159
|
+
self->codeReadPos += toRead;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
self->out.pos = 0;
|
|
164
|
+
self->outReadPos = 0;
|
|
165
|
+
ret = ZSTD_decompressStream(self->dstream, &self->out, &self->in);
|
|
166
|
+
if (ZSTD_isError(ret)) {
|
|
167
|
+
return _zstd_fail(self);
|
|
168
|
+
}
|
|
169
|
+
/*
|
|
170
|
+
* No progress with no input left means the compressed stream ended before
|
|
171
|
+
* the patch asked for its last byte - a truncated or corrupt patch.
|
|
172
|
+
*/
|
|
173
|
+
if (self->out.pos == 0) {
|
|
174
|
+
return _zstd_fail(self);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return hpatch_TRUE;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void binarypatch_zstd_decompressor_init(hpatch_TDecompress* out_decompressor) {
|
|
181
|
+
memset(out_decompressor, 0, sizeof(hpatch_TDecompress));
|
|
182
|
+
out_decompressor->is_can_open = _zstd_is_can_open;
|
|
183
|
+
out_decompressor->open = _zstd_open;
|
|
184
|
+
out_decompressor->close = _zstd_close;
|
|
185
|
+
out_decompressor->decompress_part = _zstd_decompress_part;
|
|
186
|
+
/* reset_code stays NULL: it is only needed for vcdiff style patches. */
|
|
187
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* zstd decompressor for HDiffPatch.
|
|
3
|
+
*
|
|
4
|
+
* HDiffPatch's patch functions take the decompressor as a plugin, so every codec
|
|
5
|
+
* the appliers must understand has to be supplied through `hpatch_TDecompress`.
|
|
6
|
+
* CodePush patches are always produced with `hdiffz -f -m-6 -c-zstd-21-24`, so
|
|
7
|
+
* zstd is the only codec implemented here; upstream ships a demo header covering
|
|
8
|
+
* a dozen codecs, but pulling that in would drag along headers for codecs the
|
|
9
|
+
* appliers never see.
|
|
10
|
+
*
|
|
11
|
+
* `hpatch_TDecompress` carries a mutable `decError` field, so each patch session
|
|
12
|
+
* must own its instance. Callers therefore declare the struct themselves and
|
|
13
|
+
* initialize it through `binarypatch_zstd_decompressor_init()`.
|
|
14
|
+
*
|
|
15
|
+
* This file and the vendored sources next to it sit outside the platform
|
|
16
|
+
* directories because both platforms compile them: iOS through the podspec at the
|
|
17
|
+
* repository root, Android through externalNativeBuild. One shared copy is what
|
|
18
|
+
* keeps the two appliers from drifting apart.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
#ifndef BINARYPATCH_ZSTD_DECOMPRESSOR_H
|
|
22
|
+
#define BINARYPATCH_ZSTD_DECOMPRESSOR_H
|
|
23
|
+
|
|
24
|
+
#include "libHDiffPatch/HPatch/patch_types.h"
|
|
25
|
+
|
|
26
|
+
#ifdef __cplusplus
|
|
27
|
+
extern "C" {
|
|
28
|
+
#endif
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The `compressType` string that `hdiffz -c-zstd-...` writes into the patch
|
|
32
|
+
* header, and the only value this decompressor accepts.
|
|
33
|
+
*/
|
|
34
|
+
#define BINARYPATCH_ZSTD_COMPRESS_TYPE "zstd"
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Fills `out_decompressor` with the zstd plugin implementation. The struct is
|
|
38
|
+
* fully overwritten, so it needs no prior initialization, and it holds no state
|
|
39
|
+
* that must be released.
|
|
40
|
+
*/
|
|
41
|
+
void binarypatch_zstd_decompressor_init(hpatch_TDecompress* out_decompressor);
|
|
42
|
+
|
|
43
|
+
#ifdef __cplusplus
|
|
44
|
+
}
|
|
45
|
+
#endif
|
|
46
|
+
|
|
47
|
+
#endif /* BINARYPATCH_ZSTD_DECOMPRESSOR_H */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//checksum_plugin.h
|
|
2
|
+
// checksum plugin type
|
|
3
|
+
/*
|
|
4
|
+
The MIT License (MIT)
|
|
5
|
+
Copyright (c) 2018-2019 HouSisong
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person
|
|
8
|
+
obtaining a copy of this software and associated documentation
|
|
9
|
+
files (the "Software"), to deal in the Software without
|
|
10
|
+
restriction, including without limitation the rights to use,
|
|
11
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the
|
|
13
|
+
Software is furnished to do so, subject to the following
|
|
14
|
+
conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be
|
|
17
|
+
included in all copies of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
20
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
21
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
22
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
23
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
24
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
25
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
26
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
27
|
+
*/
|
|
28
|
+
#ifndef HPatch_checksum_plugin_h
|
|
29
|
+
#define HPatch_checksum_plugin_h
|
|
30
|
+
#include "patch_types.h"
|
|
31
|
+
#ifdef __cplusplus
|
|
32
|
+
extern "C" {
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
typedef void* hpatch_checksumHandle;
|
|
36
|
+
typedef struct hpatch_TChecksum{
|
|
37
|
+
//return type tag; strlen(result)<=hpatch_kMaxPluginTypeLength; (Note:result lifetime)
|
|
38
|
+
const char* (*checksumType)(void); //ascii cstring,cannot contain '&'
|
|
39
|
+
hpatch_size_t (*checksumByteSize)(void); //0<result<=hpatch_kStreamCacheSize
|
|
40
|
+
hpatch_checksumHandle (*open)(struct hpatch_TChecksum* plugin);
|
|
41
|
+
void (*close)(struct hpatch_TChecksum* plugin,hpatch_checksumHandle handle);
|
|
42
|
+
void (*begin)(hpatch_checksumHandle handle); //must support reset
|
|
43
|
+
void (*append)(hpatch_checksumHandle handle,
|
|
44
|
+
const unsigned char* part_data,const unsigned char* part_data_end);
|
|
45
|
+
void (*end)(hpatch_checksumHandle handle,
|
|
46
|
+
unsigned char* checksum,unsigned char* checksum_end);
|
|
47
|
+
} hpatch_TChecksum;
|
|
48
|
+
|
|
49
|
+
#ifdef __cplusplus
|
|
50
|
+
}
|
|
51
|
+
#endif
|
|
52
|
+
#endif
|