@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.1

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.
Files changed (109) hide show
  1. package/CodePush.podspec +35 -3
  2. package/README.md +117 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +19 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +20 -6
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +168 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressCallback.java +7 -0
  17. package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressEventDispatcher.java +25 -4
  18. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  20. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +211 -0
  21. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +489 -0
  22. package/android/app/src/test/java/com/microsoft/codepush/react/DownloadProgressEventDispatcherTest.java +80 -0
  23. package/android/codepush-export.gradle +115 -0
  24. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  25. package/cli/commands/bundleCommand/index.ts +9 -1
  26. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  27. package/cli/commands/releaseCommand/index.test.ts +118 -0
  28. package/cli/commands/releaseCommand/index.ts +20 -2
  29. package/cli/commands/releaseCommand/release.test.ts +582 -0
  30. package/cli/commands/releaseCommand/release.ts +232 -15
  31. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  32. package/cli/dist/commands/bundleCommand/index.js +5 -1
  33. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  34. package/cli/dist/commands/releaseCommand/index.js +8 -1
  35. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  36. package/cli/dist/commands/releaseCommand/release.js +165 -17
  37. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  38. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  39. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  40. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  41. package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
  42. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  43. package/cli/dist/jest.globalSetup.js +38 -0
  44. package/cli/dist/utils/binaryPatch.js +86 -0
  45. package/cli/dist/utils/binaryPatch.test.js +190 -0
  46. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  47. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  48. package/cli/functions/makeCodePushBundle.ts +4 -2
  49. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  50. package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
  51. package/cli/jest.globalSetup.ts +43 -0
  52. package/cli/utils/binaryPatch.test.ts +234 -0
  53. package/cli/utils/binaryPatch.ts +103 -0
  54. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  55. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  59. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  60. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  61. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  62. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  63. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  64. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  65. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  66. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  67. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  68. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  69. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  70. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  71. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  72. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  73. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  74. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  75. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  76. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  77. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  78. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  79. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  80. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  81. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  82. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  84. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  85. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  86. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  91. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  92. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  93. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  94. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  95. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  96. package/ios/CodePush/CodePush.h +18 -1
  97. package/ios/CodePush/CodePush.mm +24 -1
  98. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  99. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  100. package/ios/CodePush/CodePushDownloadHandler.m +6 -0
  101. package/ios/CodePush/CodePushPackage.m +183 -2
  102. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  103. package/package.json +10 -3
  104. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  105. package/scripts/export-embedded-bundle.sh +76 -0
  106. package/src/CodePush.js +46 -3
  107. package/src/CodePush.test.js +382 -0
  108. package/src/package-mixins.js +20 -2
  109. package/typings/react-native-code-push.d.ts +80 -1
@@ -0,0 +1,100 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
5
+ import { helpOutputSupportsBaseBytecode, resolveBaseBytecodeHermesFlags, } from "./runHermesEmitBinaryCommand.js";
6
+ /**
7
+ * The base bytecode flag only exists in recent Hermes compilers, and the compiler that
8
+ * matters is the one inside the app being released - so the flag is decided by asking
9
+ * that binary, not by a version check.
10
+ */
11
+ /** Excerpt of `hermesc --help` from a compiler that has the flag. */
12
+ const HELP_WITH_BASE_BYTECODE = `OVERVIEW: Hermes driver
13
+
14
+ USAGE: hermesc [options] <input>
15
+
16
+ OPTIONS:
17
+ -Wno-direct-eval - Disable Warning when attempting a direct (local) eval
18
+ -base-bytecode=<string> - input base bytecode for delta optimizing mode
19
+ -bytecode-output-manifest=<string> - Name of the manifest file generated when compiling multiple segments to bytecode
20
+ -commonjs - Use CommonJS modules
21
+ `;
22
+ /** The same excerpt from a compiler that predates the flag. */
23
+ const HELP_WITHOUT_BASE_BYTECODE = `OVERVIEW: Hermes driver
24
+
25
+ USAGE: hermesc [options] <input>
26
+
27
+ OPTIONS:
28
+ -Wno-direct-eval - Disable Warning when attempting a direct (local) eval
29
+ -bytecode-output-manifest=<string> - Name of the manifest file generated when compiling multiple segments to bytecode
30
+ -commonjs - Use CommonJS modules
31
+ `;
32
+ function findRepoRoot() {
33
+ let dir = process.cwd();
34
+ for (;;) {
35
+ if (fs.existsSync(path.join(dir, "node_modules", "react-native"))) {
36
+ return dir;
37
+ }
38
+ const parent = path.dirname(dir);
39
+ if (parent === dir) {
40
+ throw new Error(`cannot locate the repository root from ${process.cwd()}`);
41
+ }
42
+ dir = parent;
43
+ }
44
+ }
45
+ function hermesOSBinDirName() {
46
+ switch (process.platform) {
47
+ case 'win32':
48
+ return 'win64-bin';
49
+ case 'darwin':
50
+ return 'osx-bin';
51
+ default:
52
+ return 'linux64-bin';
53
+ }
54
+ }
55
+ const repoRoot = findRepoRoot();
56
+ const baseBundlePath = path.join(repoRoot, "cli", "fixtures", "binary-patch", "base.bundle");
57
+ let workDir;
58
+ /** A project whose Hermes compiler reports a help text without the flag. */
59
+ function writeProjectWithFakeHermesc(helpOutput) {
60
+ const projectRoot = fs.mkdtempSync(path.join(workDir, "project-"));
61
+ const hermescDir = path.join(projectRoot, "node_modules", "react-native", "sdks", "hermesc", hermesOSBinDirName());
62
+ fs.mkdirSync(hermescDir, { recursive: true });
63
+ const hermescPath = path.join(hermescDir, "hermesc");
64
+ fs.writeFileSync(hermescPath, `#!/bin/sh\ncat <<'HELP'\n${helpOutput}HELP\n`);
65
+ fs.chmodSync(hermescPath, 0o755);
66
+ return projectRoot;
67
+ }
68
+ beforeAll(() => {
69
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-hermes-flags-"));
70
+ });
71
+ afterAll(() => {
72
+ fs.rmSync(workDir, { recursive: true, force: true });
73
+ });
74
+ afterEach(() => {
75
+ jest.restoreAllMocks();
76
+ });
77
+ describe("helpOutputSupportsBaseBytecode", () => {
78
+ it("detects the flag in the help output of a compiler that has it", () => {
79
+ expect(helpOutputSupportsBaseBytecode(HELP_WITH_BASE_BYTECODE)).toBe(true);
80
+ });
81
+ it("reports no support when the help output does not list the flag", () => {
82
+ expect(helpOutputSupportsBaseBytecode(HELP_WITHOUT_BASE_BYTECODE)).toBe(false);
83
+ });
84
+ });
85
+ describe("resolveBaseBytecodeHermesFlags", () => {
86
+ it("aligns the compilation with the base bundle when the compiler supports it", () => {
87
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, repoRoot)).toEqual(['-base-bytecode', baseBundlePath]);
88
+ });
89
+ const itUnlessWindows = process.platform === 'win32' ? it.skip : it;
90
+ itUnlessWindows("warns and compiles without the flag when the compiler does not support it", () => {
91
+ const projectRoot = writeProjectWithFakeHermesc(HELP_WITHOUT_BASE_BYTECODE);
92
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
93
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, projectRoot)).toEqual([]);
94
+ expect(warn.mock.calls.join('\n')).toMatch(/-base-bytecode/);
95
+ });
96
+ itUnlessWindows("uses the flag when the app's own compiler advertises it", () => {
97
+ const projectRoot = writeProjectWithFakeHermesc(HELP_WITH_BASE_BYTECODE);
98
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, projectRoot)).toEqual(['-base-bytecode', baseBundlePath]);
99
+ });
100
+ });
@@ -0,0 +1,38 @@
1
+ import { spawnSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ /**
5
+ * Builds hdiffz/hpatchz once, before any test worker starts.
6
+ *
7
+ * Several suites generate and apply real patches, which is the only way to prove the
8
+ * committed patch format is what every applier understands. Provisioning the tools here
9
+ * rather than in each suite keeps a clean checkout - and CI - free of a manual setup
10
+ * step, without letting two workers run the same build at the same time.
11
+ *
12
+ * The build script installs into `HDIFFPATCH_TOOLS_DIR` when it is set and into
13
+ * `<repo>/.hdiffpatch-tools` otherwise, which is where the CLI looks for the tools. It
14
+ * is a no-op when they are already there, so the check below only decides whether to
15
+ * warn that this run has a multi-minute build ahead of it.
16
+ *
17
+ * Note that this module is loaded outside the jest module resolver, so it cannot import
18
+ * from the CLI sources.
19
+ */
20
+ const REPO_ROOT = path.join(__dirname, '..');
21
+ const BUILD_SCRIPT_PATH = path.join(REPO_ROOT, 'scripts', 'binary-patch', 'build-hdiffpatch.sh');
22
+ const DEFAULT_TOOLS_DIR = path.join(REPO_ROOT, '.hdiffpatch-tools');
23
+ export default function ensureBinaryPatchTools() {
24
+ const toolsDir = process.env.HDIFFPATCH_TOOLS_DIR || DEFAULT_TOOLS_DIR;
25
+ const alreadyInstalled = ['hdiffz', 'hpatchz'].every((tool) => fs.existsSync(path.join(toolsDir, tool)));
26
+ if (!alreadyInstalled) {
27
+ // Announced because a first build clones and compiles HDiffPatch, which takes
28
+ // minutes; a silent wait would look like a hang.
29
+ console.log(`Building hdiffz/hpatchz into ${toolsDir} (first run only)`);
30
+ }
31
+ const result = spawnSync(BUILD_SCRIPT_PATH, { encoding: 'utf8' });
32
+ if (result.error) {
33
+ throw new Error(`failed to run ${BUILD_SCRIPT_PATH}: ${result.error.message}`);
34
+ }
35
+ if (result.status !== 0) {
36
+ throw new Error(`${BUILD_SCRIPT_PATH} failed:\n${result.stdout ?? ''}${result.stderr ?? ''}`);
37
+ }
38
+ }
@@ -0,0 +1,86 @@
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
+ import { spawnSync } from "child_process";
24
+ import fs from "fs";
25
+ import path from "path";
26
+ /**
27
+ * Identifies the patch format so a client can refuse a patch it cannot apply.
28
+ * Bump the format version whenever the tool options above change.
29
+ */
30
+ export const BINARY_PATCH_ALGORITHM = 'hdiffpatch-m-zstd';
31
+ export const BINARY_PATCH_FORMAT_VERSION = 1;
32
+ const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
33
+ const HPATCHZ_OPTIONS = ['-f', '-m'];
34
+ const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
35
+ const TOOLS_DIR_NAME = '.hdiffpatch-tools';
36
+ const BUILD_SCRIPT_PATH = 'scripts/binary-patch/build-hdiffpatch.sh';
37
+ /**
38
+ * Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
39
+ * then at a `.hdiffpatch-tools` directory in the working directory or any directory
40
+ * above it. The tools are built from source rather than installed as a package
41
+ * dependency, so the error explains how to get them.
42
+ */
43
+ export function resolveBinaryPatchTool(tool) {
44
+ const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
45
+ if (configuredDir) {
46
+ const configured = path.join(configuredDir, tool);
47
+ if (fs.existsSync(configured)) {
48
+ return configured;
49
+ }
50
+ throw new Error(`${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
51
+ `Build the tools with '${BUILD_SCRIPT_PATH}'.`);
52
+ }
53
+ let directory = path.resolve(process.cwd());
54
+ for (;;) {
55
+ const candidate = path.join(directory, TOOLS_DIR_NAME, tool);
56
+ if (fs.existsSync(candidate)) {
57
+ return candidate;
58
+ }
59
+ const parent = path.dirname(directory);
60
+ if (parent === directory) {
61
+ break;
62
+ }
63
+ directory = parent;
64
+ }
65
+ throw new Error(`'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
66
+ `Build it with '${BUILD_SCRIPT_PATH}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`);
67
+ }
68
+ /** Writes the patch that turns the bundle at `basePath` into the one at `targetPath`. */
69
+ export function generatePatch(basePath, targetPath, patchPath) {
70
+ runTool('hdiffz', [...HDIFFZ_OPTIONS, basePath, targetPath, patchPath]);
71
+ }
72
+ /** Writes the bundle that `patchPath` produces from the bundle at `basePath`. */
73
+ export function applyPatch(basePath, patchPath, outputPath) {
74
+ runTool('hpatchz', [...HPATCHZ_OPTIONS, basePath, patchPath, outputPath]);
75
+ }
76
+ function runTool(tool, args) {
77
+ const executable = resolveBinaryPatchTool(tool);
78
+ const result = spawnSync(executable, args, { encoding: 'utf8' });
79
+ if (result.error) {
80
+ throw new Error(`failed to run ${tool}: ${result.error.message}`);
81
+ }
82
+ if (result.status !== 0) {
83
+ const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
84
+ throw new Error(`${tool} failed with ${reason}\n${result.stdout ?? ''}${result.stderr ?? ''}`);
85
+ }
86
+ }
@@ -0,0 +1,190 @@
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
+ * Locks the binary patch codec contract: which bytes a patch restores, and which
10
+ * kinds of broken input the appliers do and do not catch.
11
+ *
12
+ * Everything here runs against real binaries and real bytes - hdiffz/hpatchz and a
13
+ * host build of the native applier - because the whole point is to prove that the
14
+ * committed patch format really is what every applier understands.
15
+ */
16
+ /** Building the applier from source takes a while on a cold machine. */
17
+ const BUILD_APPLIER_TIMEOUT_MS = 5 * 60 * 1000;
18
+ function findRepoRoot() {
19
+ let dir = process.cwd();
20
+ for (;;) {
21
+ if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
22
+ return dir;
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) {
26
+ throw new Error(`cannot locate the repository root from ${process.cwd()}`);
27
+ }
28
+ dir = parent;
29
+ }
30
+ }
31
+ const repoRoot = findRepoRoot();
32
+ const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
33
+ const baseFixture = path.join(fixtureDir, "base.bundle");
34
+ const targetFixture = path.join(fixtureDir, "target.bundle");
35
+ const patchFixture = path.join(fixtureDir, "update.patch");
36
+ /**
37
+ * One shared tree builds the applier for both platforms: iOS pulls it in through the
38
+ * podspec at the repository root, Android through externalNativeBuild.
39
+ */
40
+ const applierDir = path.join(repoRoot, "cpp", "binarypatch");
41
+ let workDir;
42
+ function sha256(filePath) {
43
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
44
+ }
45
+ function workPath(name) {
46
+ return path.join(workDir, name);
47
+ }
48
+ /** Writes a copy of `sourcePath` with `mutate` applied to its bytes. */
49
+ function writeMutatedCopy(sourcePath, name, mutate) {
50
+ const destination = workPath(name);
51
+ fs.writeFileSync(destination, mutate(fs.readFileSync(sourcePath)));
52
+ return destination;
53
+ }
54
+ function cCompiler() {
55
+ return process.env.CC || "cc";
56
+ }
57
+ function hasCCompiler() {
58
+ return spawnSync(cCompiler(), ["--version"], { stdio: "ignore" }).status === 0;
59
+ }
60
+ beforeAll(() => {
61
+ // hdiffz/hpatchz are provisioned for the whole run by the jest global setup.
62
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-binary-patch-"));
63
+ });
64
+ afterAll(() => {
65
+ fs.rmSync(workDir, { recursive: true, force: true });
66
+ });
67
+ describe("generatePatch/applyPatch", () => {
68
+ it("restores the target bytes from a freshly generated patch", () => {
69
+ const generatedPatch = workPath("generated.patch");
70
+ const restored = workPath("restored-from-generated.bundle");
71
+ generatePatch(baseFixture, targetFixture, generatedPatch);
72
+ applyPatch(baseFixture, generatedPatch, restored);
73
+ expect(fs.statSync(generatedPatch).size).toBeGreaterThan(0);
74
+ expect(sha256(restored)).toBe(sha256(targetFixture));
75
+ });
76
+ it("restores the target bytes from the committed patch fixture", () => {
77
+ const restored = workPath("restored-from-fixture.bundle");
78
+ applyPatch(baseFixture, patchFixture, restored);
79
+ expect(sha256(restored)).toBe(sha256(targetFixture));
80
+ });
81
+ it("reports a missing patch tool with a message that says how to get it", () => {
82
+ const previous = process.env.HDIFFPATCH_TOOLS_DIR;
83
+ process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
84
+ try {
85
+ expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-hdiffpatch\.sh/);
86
+ }
87
+ finally {
88
+ if (previous === undefined) {
89
+ delete process.env.HDIFFPATCH_TOOLS_DIR;
90
+ }
91
+ else {
92
+ process.env.HDIFFPATCH_TOOLS_DIR = previous;
93
+ }
94
+ }
95
+ });
96
+ });
97
+ describe("applyPatch failure modes", () => {
98
+ it("fails on a patch with a corrupted header", () => {
99
+ const corrupted = writeMutatedCopy(patchFixture, "corrupt-header.patch", (bytes) => {
100
+ // Byte 0 is inside the patch type string, which the header parser reads first.
101
+ bytes[0] ^= 0xff;
102
+ return bytes;
103
+ });
104
+ expect(() => applyPatch(baseFixture, corrupted, workPath("out-corrupt-header.bundle"))).toThrow(/hpatchz/);
105
+ });
106
+ it("fails on a truncated patch", () => {
107
+ const truncated = writeMutatedCopy(patchFixture, "truncated.patch", (bytes) => bytes.subarray(0, Math.floor(bytes.length / 2)));
108
+ expect(() => applyPatch(baseFixture, truncated, workPath("out-truncated.bundle"))).toThrow(/hpatchz/);
109
+ });
110
+ });
111
+ /**
112
+ * A compressed diff carries no checksum of the base data, and the zstd streams
113
+ * inside it are written without content checksums, so an applier cannot tell every
114
+ * kind of wrong input apart from a legitimate one. These tests pin that down: the
115
+ * apply step may report success, but it never produces the target bytes. Callers
116
+ * must therefore verify the base and target hashes themselves rather than treating
117
+ * a successful apply as proof of a correct result.
118
+ */
119
+ describe("errors the codec cannot detect", () => {
120
+ it("never produces the target bytes when a byte inside the compressed diff data is corrupted", () => {
121
+ const corrupted = writeMutatedCopy(patchFixture, "corrupt-body.patch", (bytes) => {
122
+ bytes[Math.floor(bytes.length / 2)] ^= 0xff;
123
+ return bytes;
124
+ });
125
+ const restored = workPath("out-corrupt-body.bundle");
126
+ // Whether the corruption is reported depends on where it lands: a flipped bit
127
+ // inside a zstd literals block decodes to different bytes without any error,
128
+ // while one that invalidates a block header does fail. Either way the result
129
+ // is not the target, which is all the caller can rely on.
130
+ let restoredHash = null;
131
+ try {
132
+ applyPatch(baseFixture, corrupted, restored);
133
+ restoredHash = sha256(restored);
134
+ }
135
+ catch {
136
+ restoredHash = null;
137
+ }
138
+ expect(restoredHash).not.toBe(sha256(targetFixture));
139
+ });
140
+ it("applies but produces wrong bytes when the base has the expected size and different content", () => {
141
+ const wrongBase = writeMutatedCopy(baseFixture, "wrong-base.bundle", (bytes) => {
142
+ bytes[1000] ^= 0xff;
143
+ return bytes;
144
+ });
145
+ const restored = workPath("out-wrong-base.bundle");
146
+ applyPatch(wrongBase, patchFixture, restored);
147
+ expect(fs.statSync(wrongBase).size).toBe(fs.statSync(baseFixture).size);
148
+ expect(sha256(restored)).not.toBe(sha256(targetFixture));
149
+ });
150
+ });
151
+ /**
152
+ * The native appliers on Android and iOS are built from the vendored sources, so a
153
+ * host build of the same sources is what proves they agree with hdiffz/hpatchz.
154
+ */
155
+ const describeNativeApplier = hasCCompiler() ? describe : describe.skip;
156
+ describeNativeApplier("host build of the native applier", () => {
157
+ let hostApplier;
158
+ const runHostApplier = (oldPath, patch, outPath) => {
159
+ const result = spawnSync(hostApplier, [oldPath, patch, outPath], { encoding: "utf8" });
160
+ return result.status;
161
+ };
162
+ beforeAll(() => {
163
+ hostApplier = workPath("apply_patch_host");
164
+ const script = path.join(applierDir, "host", "build.sh");
165
+ const result = spawnSync(script, [hostApplier], { encoding: "utf8", timeout: BUILD_APPLIER_TIMEOUT_MS });
166
+ if (result.status !== 0) {
167
+ throw new Error(`${script} failed:\n${result.stdout ?? ""}${result.stderr ?? ""}`);
168
+ }
169
+ }, BUILD_APPLIER_TIMEOUT_MS);
170
+ it("restores the same target bytes as the CLI applier", () => {
171
+ const restored = workPath("out-host.bundle");
172
+ expect(runHostApplier(baseFixture, patchFixture, restored)).toBe(0);
173
+ expect(sha256(restored)).toBe(sha256(targetFixture));
174
+ });
175
+ it("reports a corrupted patch header as exit code 3", () => {
176
+ const corrupted = writeMutatedCopy(patchFixture, "host-corrupt-header.patch", (bytes) => {
177
+ bytes[0] ^= 0xff;
178
+ return bytes;
179
+ });
180
+ expect(runHostApplier(baseFixture, corrupted, workPath("out-host-corrupt-header.bundle"))).toBe(3);
181
+ });
182
+ it("reports a base of unexpected size as exit code 5", () => {
183
+ const shortBase = writeMutatedCopy(baseFixture, "host-short-base.bundle", (bytes) => bytes.subarray(0, bytes.length - 16));
184
+ expect(runHostApplier(shortBase, patchFixture, workPath("out-host-short-base.bundle"))).toBe(5);
185
+ });
186
+ it("reports a truncated patch as exit code 6", () => {
187
+ const truncated = writeMutatedCopy(patchFixture, "host-truncated.patch", (bytes) => bytes.subarray(0, Math.floor(bytes.length / 2)));
188
+ expect(runHostApplier(baseFixture, truncated, workPath("out-host-truncated.bundle"))).toBe(6);
189
+ });
190
+ });