@bravemobile/react-native-code-push 12.4.1 → 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 +47 -2
- package/README.md +118 -3
- package/THIRD-PARTY-NOTICES +96 -0
- package/android/app/build.gradle +26 -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 +46 -11
- 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 +79 -88
- 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 +98 -27
- 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 +112 -26
- 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 +58 -9
- package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
- 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/ios/CodePush.xcodeproj/project.pbxproj +6 -6
- package/package.json +28 -4
- package/react-native.config.js +3 -1
- package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
- package/scripts/export-embedded-bundle.sh +76 -0
- package/src/CodePush.js +190 -96
- package/src/CodePush.test.js +382 -0
- package/src/native/NativeCodePush.ts +162 -0
- package/src/package-mixins.js +24 -11
- package/src/specs/NativeCodePush.ts +43 -0
- package/typings/react-native-code-push.d.ts +77 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
- package/src/AlertAdapter.js +0 -24
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The record of one attempt at installing an update from its binary patch archive: whether
|
|
7
|
+
* the update was installed from the patch, why the full archive had to be downloaded when it
|
|
8
|
+
* was not, and how long the work took.
|
|
9
|
+
*
|
|
10
|
+
* The record exists for an app that wants to judge a patch rollout with its own telemetry, so
|
|
11
|
+
* it travels to whoever asked for the download and nowhere else. It is not part of the
|
|
12
|
+
* update's metadata, it is never written to disk, and nothing here sends it anywhere.
|
|
13
|
+
*/
|
|
14
|
+
class BinaryPatchAttempt {
|
|
15
|
+
|
|
16
|
+
private static final String APPLY_DURATION_MS_KEY = "applyDurationMs";
|
|
17
|
+
private static final String FALLBACK_REASON_KEY = "fallbackReason";
|
|
18
|
+
private static final String STATUS_KEY = "status";
|
|
19
|
+
|
|
20
|
+
private static final String STATUS_APPLIED = "applied";
|
|
21
|
+
private static final String STATUS_FALLBACK = "fallback";
|
|
22
|
+
|
|
23
|
+
private final long mStartTimeMs = System.currentTimeMillis();
|
|
24
|
+
|
|
25
|
+
/** How long the applier took, once it has restored the bundle. */
|
|
26
|
+
private long mApplyDurationMs = -1;
|
|
27
|
+
|
|
28
|
+
private boolean mFellBack;
|
|
29
|
+
private String mFallbackReason;
|
|
30
|
+
private long mAttemptDurationMs;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The applier restored the bundle. Whether the update installs is decided by the checks
|
|
34
|
+
* that follow, so this is not yet the attempt succeeding.
|
|
35
|
+
*/
|
|
36
|
+
void recordBundleRestored(long applyDurationMs) {
|
|
37
|
+
mApplyDurationMs = applyDurationMs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The attempt ended in one of the reasons the appliers report.
|
|
42
|
+
*
|
|
43
|
+
* The attempt is timed here rather than when it is read, because what happens next is the
|
|
44
|
+
* full archive being downloaded and that is not time the patch spent.
|
|
45
|
+
*/
|
|
46
|
+
void recordFallback(String failureReason) {
|
|
47
|
+
mFellBack = true;
|
|
48
|
+
mFallbackReason = failureReason;
|
|
49
|
+
mAttemptDurationMs = System.currentTimeMillis() - mStartTimeMs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The attempt ended in an error rather than in a verdict of its own.
|
|
54
|
+
*
|
|
55
|
+
* An error raised after the bundle was restored is the restored update failing the checks
|
|
56
|
+
* that every update passes before it is installed. Before that point the appliers have no
|
|
57
|
+
* word for what happened - the patch archive not being downloadable, say - and inventing
|
|
58
|
+
* one here would put a value on the wire that no platform reports, so the fallback is
|
|
59
|
+
* reported without a reason.
|
|
60
|
+
*/
|
|
61
|
+
void recordFallbackAfterError() {
|
|
62
|
+
recordFallback(bundleWasRestored() ? BinaryPatchResult.REASON_PACKAGE_VERIFICATION_FAILED : null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private boolean bundleWasRestored() {
|
|
66
|
+
return mApplyDurationMs >= 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What the attempt ended in, as the app reads it.
|
|
71
|
+
*
|
|
72
|
+
* An attempt that never fell back is one the update was installed from, and it reports the
|
|
73
|
+
* time the applier took. A fallback reports how long the attempt ran before it was given
|
|
74
|
+
* up on, because there is no completed apply to time.
|
|
75
|
+
*/
|
|
76
|
+
JSONObject result() {
|
|
77
|
+
JSONObject result = new JSONObject();
|
|
78
|
+
if (!mFellBack) {
|
|
79
|
+
CodePushUtils.setJSONValueForKey(result, STATUS_KEY, STATUS_APPLIED);
|
|
80
|
+
CodePushUtils.setJSONValueForKey(result, APPLY_DURATION_MS_KEY, Math.max(mApplyDurationMs, 0));
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
CodePushUtils.setJSONValueForKey(result, STATUS_KEY, STATUS_FALLBACK);
|
|
85
|
+
if (mFallbackReason != null) {
|
|
86
|
+
CodePushUtils.setJSONValueForKey(result, FALLBACK_REASON_KEY, mFallbackReason);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
CodePushUtils.setJSONValueForKey(result, APPLY_DURATION_MS_KEY, mAttemptDurationMs);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outcome of restoring an update from its binary patch archive.
|
|
5
|
+
*
|
|
6
|
+
* A failed result is not an error the user hears about: it is the signal to download the
|
|
7
|
+
* update's full archive instead. The reason strings are the vocabulary the appliers of
|
|
8
|
+
* every platform report, and the logs a rollout is judged from are read for exactly these
|
|
9
|
+
* words, so they must not be reworded.
|
|
10
|
+
*/
|
|
11
|
+
public class BinaryPatchResult {
|
|
12
|
+
|
|
13
|
+
/** The bundle inside the app binary could not be opened or read. */
|
|
14
|
+
public static final String REASON_BASE_BUNDLE_UNAVAILABLE = "base_bundle_unavailable";
|
|
15
|
+
|
|
16
|
+
/** The bundle inside the app binary is not the one the patch was computed against. */
|
|
17
|
+
public static final String REASON_BASE_HASH_MISMATCH = "base_hash_mismatch";
|
|
18
|
+
|
|
19
|
+
/** The manifest is missing, malformed, points outside the archive, or asks for too much. */
|
|
20
|
+
public static final String REASON_INVALID_MANIFEST = "invalid_manifest";
|
|
21
|
+
|
|
22
|
+
/** The patch was produced by a format or a codec this client cannot apply. */
|
|
23
|
+
public static final String REASON_UNSUPPORTED_FORMAT = "unsupported_format";
|
|
24
|
+
|
|
25
|
+
/** The applier refused the patch, or the restored bundle could not be written. */
|
|
26
|
+
public static final String REASON_PATCH_APPLY_FAILED = "patch_apply_failed";
|
|
27
|
+
|
|
28
|
+
/** The restored bundle is not the one the manifest promised. */
|
|
29
|
+
public static final String REASON_TARGET_VERIFICATION_FAILED = "target_verification_failed";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The update restored from the patch did not pass the checks every update passes before
|
|
33
|
+
* it is installed - the folder hash above all.
|
|
34
|
+
*
|
|
35
|
+
* This one is reported by the caller rather than by the applier: by the time it applies,
|
|
36
|
+
* the applier has already handed back a bundle it was happy with.
|
|
37
|
+
*/
|
|
38
|
+
public static final String REASON_PACKAGE_VERIFICATION_FAILED = "package_verification_failed";
|
|
39
|
+
|
|
40
|
+
private final boolean mSucceeded;
|
|
41
|
+
private final String mFailureReason;
|
|
42
|
+
|
|
43
|
+
private BinaryPatchResult(boolean succeeded, String failureReason) {
|
|
44
|
+
mSucceeded = succeeded;
|
|
45
|
+
mFailureReason = failureReason;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static BinaryPatchResult success() {
|
|
49
|
+
return new BinaryPatchResult(true, null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static BinaryPatchResult failure(String failureReason) {
|
|
53
|
+
return new BinaryPatchResult(false, failureReason);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public boolean succeeded() {
|
|
57
|
+
return mSucceeded;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Why the full archive has to be downloaded instead, or null when the patch was applied. */
|
|
61
|
+
public String getFailureReason() {
|
|
62
|
+
return mFailureReason;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -4,18 +4,25 @@ import android.content.Context;
|
|
|
4
4
|
import android.content.pm.PackageInfo;
|
|
5
5
|
import android.content.pm.PackageManager;
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import androidx.annotation.NonNull;
|
|
8
|
+
import androidx.annotation.Nullable;
|
|
9
|
+
|
|
10
|
+
import com.facebook.react.TurboReactPackage;
|
|
8
11
|
import com.facebook.react.bridge.NativeModule;
|
|
9
12
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
13
|
+
import com.facebook.react.module.model.ReactModuleInfo;
|
|
14
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
10
15
|
import com.facebook.react.uimanager.ViewManager;
|
|
11
16
|
|
|
12
17
|
import org.json.JSONException;
|
|
13
18
|
import org.json.JSONObject;
|
|
14
19
|
|
|
15
20
|
import java.util.ArrayList;
|
|
21
|
+
import java.util.HashMap;
|
|
16
22
|
import java.util.List;
|
|
23
|
+
import java.util.Map;
|
|
17
24
|
|
|
18
|
-
public class CodePush
|
|
25
|
+
public class CodePush extends TurboReactPackage {
|
|
19
26
|
|
|
20
27
|
private static boolean sIsRunningBinaryVersion = false;
|
|
21
28
|
private static boolean sNeedToReportRollback = false;
|
|
@@ -54,7 +61,7 @@ public class CodePush implements ReactPackage {
|
|
|
54
61
|
private CodePush(Context context, boolean isDebugMode) {
|
|
55
62
|
mContext = context.getApplicationContext();
|
|
56
63
|
|
|
57
|
-
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath());
|
|
64
|
+
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath(), mContext);
|
|
58
65
|
mTelemetryManager = new CodePushTelemetryManager(mContext);
|
|
59
66
|
mDeploymentKey = "deprecated_deployment_key";
|
|
60
67
|
mIsDebugMode = isDebugMode;
|
|
@@ -300,17 +307,45 @@ public class CodePush implements ReactPackage {
|
|
|
300
307
|
}
|
|
301
308
|
|
|
302
309
|
@Override
|
|
303
|
-
public
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
public @Nullable NativeModule getModule(String name, ReactApplicationContext reactApplicationContext) {
|
|
311
|
+
if (!CodePushNativeModule.NAME.equals(name)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return new CodePushNativeModule(
|
|
316
|
+
reactApplicationContext,
|
|
317
|
+
this,
|
|
318
|
+
mUpdateManager,
|
|
319
|
+
mTelemetryManager,
|
|
320
|
+
mSettingsManager
|
|
321
|
+
);
|
|
322
|
+
}
|
|
306
323
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
@Override
|
|
325
|
+
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
|
326
|
+
return new ReactModuleInfoProvider() {
|
|
327
|
+
@Override
|
|
328
|
+
public Map<String, ReactModuleInfo> getReactModuleInfos() {
|
|
329
|
+
Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
|
|
330
|
+
reactModuleInfoMap.put(
|
|
331
|
+
CodePushNativeModule.NAME,
|
|
332
|
+
new ReactModuleInfo(
|
|
333
|
+
CodePushNativeModule.NAME,
|
|
334
|
+
CodePushNativeModule.class.getName(),
|
|
335
|
+
false,
|
|
336
|
+
false,
|
|
337
|
+
false,
|
|
338
|
+
false,
|
|
339
|
+
true
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
return reactModuleInfoMap;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
311
345
|
}
|
|
346
|
+
|
|
312
347
|
@Override
|
|
313
|
-
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
|
|
348
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
|
314
349
|
return new ArrayList<>();
|
|
315
350
|
}
|
|
316
351
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject;
|
|
4
|
+
|
|
5
|
+
import java.io.File;
|
|
6
|
+
import java.io.FileInputStream;
|
|
7
|
+
import java.io.FileOutputStream;
|
|
8
|
+
import java.io.IOException;
|
|
9
|
+
import java.io.InputStream;
|
|
10
|
+
import java.io.OutputStream;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rebuilds the JS bundle of an update that was downloaded as a binary patch archive.
|
|
14
|
+
*
|
|
15
|
+
* A patch archive holds everything the full archive holds except the JS bundle, which it
|
|
16
|
+
* carries as a patch against the bundle that shipped inside the app binary, plus a
|
|
17
|
+
* manifest describing how to rebuild it. Restoring means applying that patch, verifying
|
|
18
|
+
* the result, moving it to where the bundle belongs and deleting the two patch-only
|
|
19
|
+
* files. What is left is byte for byte the contents of the full archive, so the folder
|
|
20
|
+
* hash check that follows the install is unchanged and stays the last line of defence.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here trusts the patch. Neither the diff format nor the zstd streams inside it
|
|
23
|
+
* carry a checksum of the data they produce, so an apply that reports success is not
|
|
24
|
+
* proof of a correct result: a base bundle of the right size but the wrong content, or a
|
|
25
|
+
* corrupted patch body, both produce wrong bytes without any error. The base bundle is
|
|
26
|
+
* hashed before the patch is applied and the restored bundle is hashed afterwards, and
|
|
27
|
+
* the restored bytes only reach the update contents once both checks have passed.
|
|
28
|
+
*
|
|
29
|
+
* Every failure is reported as a {@link BinaryPatchResult}, never as an exception: the
|
|
30
|
+
* caller answers all of them the same way, by downloading the full archive instead.
|
|
31
|
+
*/
|
|
32
|
+
public class CodePushBinaryPatch {
|
|
33
|
+
|
|
34
|
+
/** Reads the JS bundle that shipped inside the app binary. */
|
|
35
|
+
public interface BaseBundleProvider {
|
|
36
|
+
byte[] readBaseBundle(String bundleFileName) throws IOException;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Applies a patch to a base bundle and writes the restored bundle to a file.
|
|
41
|
+
*
|
|
42
|
+
* The result codes are the ones the native applier returns, so they have to stay in
|
|
43
|
+
* step with the codes in the JNI wrapper.
|
|
44
|
+
*/
|
|
45
|
+
public interface PatchApplier {
|
|
46
|
+
int RESULT_OK = 0;
|
|
47
|
+
int RESULT_INVALID_ARGUMENT = 1;
|
|
48
|
+
int RESULT_IO_ERROR = 2;
|
|
49
|
+
int RESULT_INVALID_HEADER = 3;
|
|
50
|
+
int RESULT_UNSUPPORTED_COMPRESSION = 4;
|
|
51
|
+
int RESULT_SIZE_MISMATCH = 5;
|
|
52
|
+
int RESULT_APPLY_FAILED = 6;
|
|
53
|
+
/** Reported by the wrapper itself, when the native library could not be loaded. */
|
|
54
|
+
int RESULT_LIBRARY_UNAVAILABLE = 7;
|
|
55
|
+
|
|
56
|
+
int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static final int COPY_BUFFER_SIZE = 1024 * 8;
|
|
60
|
+
|
|
61
|
+
private final BaseBundleProvider mBaseBundleProvider;
|
|
62
|
+
private final PatchApplier mPatchApplier;
|
|
63
|
+
|
|
64
|
+
public CodePushBinaryPatch(BaseBundleProvider baseBundleProvider, PatchApplier patchApplier) {
|
|
65
|
+
mBaseBundleProvider = baseBundleProvider;
|
|
66
|
+
mPatchApplier = patchApplier;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Turns the contents of a downloaded patch archive into the contents of the full one.
|
|
71
|
+
*
|
|
72
|
+
* @param unzippedFolderPath the unzipped archive, which is modified in place
|
|
73
|
+
* @param workingFolderPath scratch directory for the restored bundle, emptied before
|
|
74
|
+
* and after the attempt so an interrupted run leaves nothing
|
|
75
|
+
* @param baseBundleFileName name of the JS bundle inside the app binary
|
|
76
|
+
*/
|
|
77
|
+
public BinaryPatchResult restoreBundle(String unzippedFolderPath, String workingFolderPath, String baseBundleFileName) {
|
|
78
|
+
File contentsFolder = resolveContentsFolder(new File(unzippedFolderPath));
|
|
79
|
+
File manifestFile = new File(contentsFolder, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME);
|
|
80
|
+
if (!manifestFile.isFile()) {
|
|
81
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
JSONObject manifest;
|
|
85
|
+
try {
|
|
86
|
+
manifest = CodePushUtils.getJsonObjectFromFile(manifestFile.getAbsolutePath());
|
|
87
|
+
} catch (IOException | CodePushMalformedDataException e) {
|
|
88
|
+
CodePushUtils.log(e);
|
|
89
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (manifest.optInt(CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY, -1) != CodePushConstants.BINARY_PATCH_FORMAT_VERSION
|
|
93
|
+
|| !CodePushConstants.BINARY_PATCH_ALGORITHM.equals(manifest.optString(CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, null))) {
|
|
94
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
File targetBundleFile = resolveInsideFolder(contentsFolder, manifest.optString(CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY, null));
|
|
98
|
+
File patchFile = resolveInsideFolder(contentsFolder, manifest.optString(CodePushConstants.BINARY_PATCH_FILE_KEY, null));
|
|
99
|
+
String baseBundleHash = manifest.optString(CodePushConstants.BINARY_PATCH_BASE_BUNDLE_HASH_KEY, null);
|
|
100
|
+
String targetBundleHash = manifest.optString(CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_HASH_KEY, null);
|
|
101
|
+
long targetBundleSize = manifest.optLong(CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY, -1);
|
|
102
|
+
if (targetBundleFile == null || patchFile == null || !patchFile.isFile()
|
|
103
|
+
|| isNullOrEmpty(baseBundleHash) || isNullOrEmpty(targetBundleHash)
|
|
104
|
+
|| targetBundleSize <= 0 || targetBundleSize > CodePushConstants.BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE) {
|
|
105
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// An earlier attempt that was killed while patching leaves its restored bundle behind.
|
|
109
|
+
FileUtils.deleteDirectoryAtPath(workingFolderPath);
|
|
110
|
+
File workingFolder = new File(workingFolderPath);
|
|
111
|
+
if (!workingFolder.mkdirs()) {
|
|
112
|
+
CodePushUtils.log("Unable to create the binary patch working directory at " + workingFolderPath);
|
|
113
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (workingFolder.getUsableSpace() < targetBundleSize) {
|
|
118
|
+
CodePushUtils.log("Not enough free space to restore a " + targetBundleSize + " byte bundle.");
|
|
119
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
byte[] baseBundle;
|
|
123
|
+
try {
|
|
124
|
+
baseBundle = mBaseBundleProvider.readBaseBundle(baseBundleFileName);
|
|
125
|
+
} catch (Exception e) {
|
|
126
|
+
CodePushUtils.log(e);
|
|
127
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE);
|
|
128
|
+
}
|
|
129
|
+
if (baseBundle == null) {
|
|
130
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE);
|
|
131
|
+
}
|
|
132
|
+
if (!baseBundleHash.equals(CodePushUpdateUtils.computeHashForBytes(baseBundle))) {
|
|
133
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
byte[] patch;
|
|
137
|
+
try {
|
|
138
|
+
patch = readFile(patchFile);
|
|
139
|
+
} catch (IOException e) {
|
|
140
|
+
CodePushUtils.log(e);
|
|
141
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
File restoredBundleFile = new File(workingFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME);
|
|
145
|
+
int resultCode = mPatchApplier.apply(baseBundle, patch, restoredBundleFile.getAbsolutePath(), targetBundleSize);
|
|
146
|
+
if (resultCode != PatchApplier.RESULT_OK) {
|
|
147
|
+
CodePushUtils.log("The binary patch applier returned " + resultCode + ".");
|
|
148
|
+
return BinaryPatchResult.failure(reasonForResultCode(resultCode));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (restoredBundleFile.length() != targetBundleSize) {
|
|
152
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
153
|
+
}
|
|
154
|
+
String restoredBundleHash;
|
|
155
|
+
try {
|
|
156
|
+
restoredBundleHash = CodePushUpdateUtils.computeHashForFile(restoredBundleFile);
|
|
157
|
+
} catch (IOException e) {
|
|
158
|
+
CodePushUtils.log(e);
|
|
159
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
160
|
+
}
|
|
161
|
+
if (!targetBundleHash.equals(restoredBundleHash)) {
|
|
162
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!moveFile(restoredBundleFile, targetBundleFile) || !patchFile.delete() || !manifestFile.delete()) {
|
|
166
|
+
// The contents are half restored, so they must not be installed. The full
|
|
167
|
+
// archive that follows unzips over them, which is what clears them.
|
|
168
|
+
CodePushUtils.log("Unable to put the restored bundle in place of the patch.");
|
|
169
|
+
return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return BinaryPatchResult.success();
|
|
173
|
+
} finally {
|
|
174
|
+
FileUtils.deleteDirectoryAtPath(workingFolderPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Finds the contents root inside an unzipped archive.
|
|
180
|
+
*
|
|
181
|
+
* An archive wraps its files in a single directory, and the manifest's paths are
|
|
182
|
+
* relative to that directory rather than to the archive. An archive whose files are at
|
|
183
|
+
* the top level is its own contents root, which is how the tooling that unpacks one
|
|
184
|
+
* reads it too.
|
|
185
|
+
*/
|
|
186
|
+
private static File resolveContentsFolder(File unzippedFolder) {
|
|
187
|
+
File[] entries = unzippedFolder.listFiles();
|
|
188
|
+
if (entries != null && entries.length == 1 && entries[0].isDirectory()) {
|
|
189
|
+
return entries[0];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return unzippedFolder;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolves a path the manifest points at, refusing anything that would reach outside
|
|
197
|
+
* the archive - an archive is untrusted input, and its manifest is no more trusted
|
|
198
|
+
* than its entries.
|
|
199
|
+
*
|
|
200
|
+
* @return the resolved file, or null when the path is unusable
|
|
201
|
+
*/
|
|
202
|
+
private static File resolveInsideFolder(File folder, String relativePath) {
|
|
203
|
+
if (isNullOrEmpty(relativePath) || new File(relativePath).isAbsolute()) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
String folderPath = folder.getCanonicalPath() + File.separator;
|
|
209
|
+
String resolvedPath = new File(folder, relativePath).getCanonicalPath();
|
|
210
|
+
if (!resolvedPath.startsWith(folderPath)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return new File(resolvedPath);
|
|
215
|
+
} catch (IOException e) {
|
|
216
|
+
CodePushUtils.log(e);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static String reasonForResultCode(int resultCode) {
|
|
222
|
+
return resultCode == PatchApplier.RESULT_UNSUPPORTED_COMPRESSION
|
|
223
|
+
? BinaryPatchResult.REASON_UNSUPPORTED_FORMAT
|
|
224
|
+
: BinaryPatchResult.REASON_PATCH_APPLY_FAILED;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Rename, falling back to a copy for the case where the two paths are on different volumes. */
|
|
228
|
+
private static boolean moveFile(File sourceFile, File destinationFile) {
|
|
229
|
+
File destinationFolder = destinationFile.getParentFile();
|
|
230
|
+
if (destinationFolder != null && !destinationFolder.exists() && !destinationFolder.mkdirs()) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (sourceFile.renameTo(destinationFile)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
copyFile(sourceFile, destinationFile);
|
|
240
|
+
} catch (IOException e) {
|
|
241
|
+
CodePushUtils.log(e);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return sourceFile.delete();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static void copyFile(File sourceFile, File destinationFile) throws IOException {
|
|
249
|
+
InputStream input = new FileInputStream(sourceFile);
|
|
250
|
+
try {
|
|
251
|
+
OutputStream output = new FileOutputStream(destinationFile);
|
|
252
|
+
try {
|
|
253
|
+
byte[] buffer = new byte[COPY_BUFFER_SIZE];
|
|
254
|
+
int bytesRead;
|
|
255
|
+
while ((bytesRead = input.read(buffer)) > 0) {
|
|
256
|
+
output.write(buffer, 0, bytesRead);
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
output.close();
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
input.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static byte[] readFile(File file) throws IOException {
|
|
267
|
+
long fileSize = file.length();
|
|
268
|
+
if (fileSize <= 0 || fileSize > Integer.MAX_VALUE) {
|
|
269
|
+
throw new IOException("Cannot read " + file.getAbsolutePath() + ", it is " + fileSize + " bytes long.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
byte[] contents = new byte[(int) fileSize];
|
|
273
|
+
InputStream input = new FileInputStream(file);
|
|
274
|
+
try {
|
|
275
|
+
int offset = 0;
|
|
276
|
+
while (offset < contents.length) {
|
|
277
|
+
int bytesRead = input.read(contents, offset, contents.length - offset);
|
|
278
|
+
if (bytesRead < 0) {
|
|
279
|
+
throw new IOException("Unexpected end of " + file.getAbsolutePath() + ".");
|
|
280
|
+
}
|
|
281
|
+
offset += bytesRead;
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
input.close();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return contents;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private static boolean isNullOrEmpty(String value) {
|
|
291
|
+
return value == null || value.isEmpty();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -2,6 +2,31 @@ package com.microsoft.codepush.react;
|
|
|
2
2
|
|
|
3
3
|
public class CodePushConstants {
|
|
4
4
|
public static final String ASSETS_BUNDLE_PREFIX = "assets://";
|
|
5
|
+
// The binary patch values below are the client half of the contract the CLI writes
|
|
6
|
+
// into a patch archive. The CLI is TypeScript, so its constants cannot be imported
|
|
7
|
+
// here: any change to the manifest it produces has to be repeated in this file.
|
|
8
|
+
public static final String BINARY_PATCH_ALGORITHM = "hdiffpatch-m-zstd";
|
|
9
|
+
public static final String BINARY_PATCH_ALGORITHM_KEY = "algorithm";
|
|
10
|
+
public static final String BINARY_PATCH_BASE_BUNDLE_HASH_KEY = "baseBundleHash";
|
|
11
|
+
public static final String BINARY_PATCH_BUNDLE_PATH_KEY = "bundlePath";
|
|
12
|
+
public static final String BINARY_PATCH_DOWNLOAD_URL_KEY = "binaryPatchDownloadUrl";
|
|
13
|
+
public static final String BINARY_PATCH_FILE_KEY = "patchFile";
|
|
14
|
+
public static final String BINARY_PATCH_FOLDER_NAME = "binary-patch";
|
|
15
|
+
public static final int BINARY_PATCH_FORMAT_VERSION = 1;
|
|
16
|
+
public static final String BINARY_PATCH_FORMAT_VERSION_KEY = "formatVersion";
|
|
17
|
+
public static final String BINARY_PATCH_MANIFEST_FILE_NAME = "codepush-binary-patch.json";
|
|
18
|
+
// A manifest asking for a bundle larger than this is treated as malformed rather than
|
|
19
|
+
// as a reason to reserve that much memory and disk. A large Hermes bundle stays under
|
|
20
|
+
// 50 MB, and a release that somehow exceeds the bound still installs from its full
|
|
21
|
+
// archive, so the headroom below is generous enough for the limit to only ever catch
|
|
22
|
+
// a manifest that is wrong. The other platform's applier holds the same value.
|
|
23
|
+
public static final long BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE = 128L * 1024 * 1024;
|
|
24
|
+
// Not part of the CLI's contract: the field a download that tried a patch reports its
|
|
25
|
+
// outcome under, on the package it resolves with and on no package it stores.
|
|
26
|
+
public static final String BINARY_PATCH_RESULT_KEY = "binaryPatchResult";
|
|
27
|
+
public static final String BINARY_PATCH_TARGET_BUNDLE_HASH_KEY = "targetBundleHash";
|
|
28
|
+
public static final String BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY = "targetBundleSize";
|
|
29
|
+
public static final String BINARY_PATCH_TARGET_FILE_NAME = "target.bundle";
|
|
5
30
|
public static final String CODE_PUSH_FOLDER_PREFIX = "CodePush";
|
|
6
31
|
public static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash";
|
|
7
32
|
public static final String CODE_PUSH_OLD_HASH_FILE_NAME = "CodePushHash.json";
|