@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.
Files changed (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -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 +46 -11
  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 +79 -88
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -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/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +98 -27
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +112 -26
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -0,0 +1,473 @@
1
+ #import "CodePushBinaryPatch.h"
2
+ #import "CodePush.h"
3
+
4
+ #include <stdio.h>
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+
8
+ /*
9
+ * The applier itself is the shared C code the other platform and the host build compile
10
+ * as well, referenced where it lives rather than copied here, which is what keeps the
11
+ * appliers of the platforms from drifting apart. It is included by this file alone, so
12
+ * no C type reaches a header of this pod.
13
+ */
14
+ #include "binarypatch_zstd_decompressor.h"
15
+ #include "libHDiffPatch/HPatch/patch.h"
16
+
17
+ NSString *const CodePushBinaryPatchReasonBaseBundleUnavailable = @"base_bundle_unavailable";
18
+ NSString *const CodePushBinaryPatchReasonBaseHashMismatch = @"base_hash_mismatch";
19
+ NSString *const CodePushBinaryPatchReasonInvalidManifest = @"invalid_manifest";
20
+ NSString *const CodePushBinaryPatchReasonUnsupportedFormat = @"unsupported_format";
21
+ NSString *const CodePushBinaryPatchReasonPatchApplyFailed = @"patch_apply_failed";
22
+ NSString *const CodePushBinaryPatchReasonTargetVerificationFailed = @"target_verification_failed";
23
+ NSString *const CodePushBinaryPatchReasonPackageVerificationFailed = @"package_verification_failed";
24
+
25
+ #pragma mark - Private constants
26
+
27
+ /*
28
+ * The manifest a patch archive carries. These values are the format contract itself: the
29
+ * CLI that writes a manifest and the applier of the other platform spell them exactly
30
+ * this way, and an archive is unreadable to a client that spells them differently.
31
+ */
32
+ static NSString *const BinaryPatchManifestFileName = @"codepush-binary-patch.json";
33
+ static NSString *const BinaryPatchAlgorithm = @"hdiffpatch-m-zstd";
34
+ static NSString *const BinaryPatchAlgorithmKey = @"algorithm";
35
+ static NSString *const BinaryPatchBaseBundleHashKey = @"baseBundleHash";
36
+ static NSString *const BinaryPatchBundlePathKey = @"bundlePath";
37
+ static NSString *const BinaryPatchFileKey = @"patchFile";
38
+ static NSString *const BinaryPatchFormatVersionKey = @"formatVersion";
39
+ static NSString *const BinaryPatchTargetBundleHashKey = @"targetBundleHash";
40
+ static NSString *const BinaryPatchTargetBundleSizeKey = @"targetBundleSize";
41
+ static const NSInteger BinaryPatchFormatVersion = 1;
42
+
43
+ /*
44
+ * The largest bundle a patch is allowed to promise. A manifest is untrusted input, and
45
+ * the size in it decides how much disk the restore asks for before a single byte of the
46
+ * patch has been read. A large Hermes bundle stays under 50 MB, and a release that somehow
47
+ * exceeds the bound still installs from its full archive, so the headroom below is generous
48
+ * enough for the limit to only ever catch a manifest that is wrong. The other platform's
49
+ * applier holds the same value.
50
+ */
51
+ static const long long BinaryPatchMaxTargetBundleSize = 128LL * 1024 * 1024;
52
+
53
+ /** Name the restored bundle is written under, inside the working directory. */
54
+ static NSString *const BinaryPatchTargetFileName = @"target.bundle";
55
+
56
+ #pragma mark - Native applier
57
+
58
+ /* Scratch buffer handed to patch_decompress_with_cache() to reduce stream reads. */
59
+ #define APPLY_CACHE_SIZE (1024 * 1024)
60
+
61
+ /*
62
+ * What the applier below reports.
63
+ *
64
+ * These are the result codes the other platform's wrapper hands back across its own
65
+ * boundary, kept in step so both wrappers answer the same failure with the same reason.
66
+ * The code that platform reserves for a native library that could not be loaded has no
67
+ * counterpart here: the applier is linked into the app binary.
68
+ */
69
+ typedef NS_ENUM(NSInteger, CodePushBinaryPatchApplyResult) {
70
+ CodePushBinaryPatchApplyResultOK = 0,
71
+ CodePushBinaryPatchApplyResultInvalidArgument = 1,
72
+ CodePushBinaryPatchApplyResultIOError = 2,
73
+ CodePushBinaryPatchApplyResultInvalidHeader = 3,
74
+ CodePushBinaryPatchApplyResultUnsupportedCompression = 4,
75
+ CodePushBinaryPatchApplyResultSizeMismatch = 5,
76
+ CodePushBinaryPatchApplyResultApplyFailed = 6
77
+ };
78
+
79
+ typedef struct {
80
+ FILE* file;
81
+ hpatch_StreamPos_t writtenSize;
82
+ } TSequentialFileWriter;
83
+
84
+ static hpatch_BOOL _write_sequential(const hpatch_TStreamOutput* stream,
85
+ hpatch_StreamPos_t writeToPos,
86
+ const unsigned char* data,
87
+ const unsigned char* data_end) {
88
+ TSequentialFileWriter* self = (TSequentialFileWriter*)stream->streamImport;
89
+ const size_t length = (size_t)(data_end - data);
90
+ /* patch_decompress_with_cache() only ever appends; anything else is a bug. */
91
+ if (writeToPos != self->writtenSize) {
92
+ return hpatch_FALSE;
93
+ }
94
+ if (fwrite(data, 1, length, self->file) != length) {
95
+ return hpatch_FALSE;
96
+ }
97
+ self->writtenSize += length;
98
+ return hpatch_TRUE;
99
+ }
100
+
101
+ /*
102
+ * Applies a patch to a base bundle and writes the restored bundle to a file.
103
+ *
104
+ * Memory contract, the same one the host build documents:
105
+ * - the base bundle is held whole, because the patches are produced with `hdiffz -m`,
106
+ * which patches with random access to the base data
107
+ * - the patch is held whole, being far smaller than either bundle
108
+ * - the restored bundle is written sequentially to a file, so a patch session never
109
+ * holds two bundles at once
110
+ *
111
+ * A patch carries no checksum of the base data and its zstd streams carry no content
112
+ * checksum, so a successful apply here is not proof of a correct result. The caller
113
+ * verifies the base and target hashes; this function cannot.
114
+ */
115
+ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned char* baseData,
116
+ size_t baseSize,
117
+ const unsigned char* patchData,
118
+ size_t patchSize,
119
+ const char* outputPath,
120
+ long long expectedTargetSize) {
121
+ unsigned char* cache = NULL;
122
+ hpatch_compressedDiffInfo diffInfo;
123
+ hpatch_TDecompress decompressor;
124
+ hpatch_TStreamInput baseStream;
125
+ hpatch_TStreamInput patchStream;
126
+ hpatch_TStreamOutput targetStream;
127
+ TSequentialFileWriter writer;
128
+ hpatch_BOOL applied;
129
+ CodePushBinaryPatchApplyResult result = CodePushBinaryPatchApplyResultOK;
130
+
131
+ if ((baseData == NULL) || (patchData == NULL) || (outputPath == NULL) || (expectedTargetSize <= 0)) {
132
+ return CodePushBinaryPatchApplyResultInvalidArgument;
133
+ }
134
+
135
+ binarypatch_zstd_decompressor_init(&decompressor);
136
+
137
+ if (!getCompressedDiffInfo_mem(&diffInfo, patchData, patchData + patchSize)) {
138
+ CPLog(@"The binary patch header could not be read.");
139
+ return CodePushBinaryPatchApplyResultInvalidHeader;
140
+ }
141
+ if ((strlen(diffInfo.compressType) > 0) && !decompressor.is_can_open(diffInfo.compressType)) {
142
+ CPLog(@"The binary patch uses an unsupported codec: %s", diffInfo.compressType);
143
+ return CodePushBinaryPatchApplyResultUnsupportedCompression;
144
+ }
145
+ if (diffInfo.oldDataSize != (hpatch_StreamPos_t)baseSize) {
146
+ CPLog(@"The binary patch expects a %llu byte base bundle, this one is %llu bytes.",
147
+ (unsigned long long)diffInfo.oldDataSize, (unsigned long long)baseSize);
148
+ return CodePushBinaryPatchApplyResultSizeMismatch;
149
+ }
150
+ if (diffInfo.newDataSize != (hpatch_StreamPos_t)expectedTargetSize) {
151
+ CPLog(@"The binary patch produces %llu bytes, the manifest promises %llu.",
152
+ (unsigned long long)diffInfo.newDataSize, (unsigned long long)expectedTargetSize);
153
+ return CodePushBinaryPatchApplyResultSizeMismatch;
154
+ }
155
+
156
+ cache = (unsigned char*)malloc(APPLY_CACHE_SIZE);
157
+ if (cache == NULL) {
158
+ CPLog(@"Out of memory while allocating the binary patch cache.");
159
+ return CodePushBinaryPatchApplyResultIOError;
160
+ }
161
+
162
+ writer.file = fopen(outputPath, "wb");
163
+ writer.writtenSize = 0;
164
+ if (writer.file == NULL) {
165
+ CPLog(@"The restored bundle could not be opened for writing.");
166
+ free(cache);
167
+ return CodePushBinaryPatchApplyResultIOError;
168
+ }
169
+
170
+ mem_as_hStreamInput(&baseStream, baseData, baseData + baseSize);
171
+ mem_as_hStreamInput(&patchStream, patchData, patchData + patchSize);
172
+ memset(&targetStream, 0, sizeof(targetStream));
173
+ targetStream.streamImport = &writer;
174
+ targetStream.streamSize = diffInfo.newDataSize;
175
+ targetStream.write = _write_sequential;
176
+
177
+ applied = patch_decompress_with_cache(&targetStream, &baseStream, &patchStream, &decompressor,
178
+ cache, cache + APPLY_CACHE_SIZE);
179
+ if (fclose(writer.file) != 0) {
180
+ CPLog(@"The restored bundle could not be flushed to disk.");
181
+ result = CodePushBinaryPatchApplyResultIOError;
182
+ } else if (!applied) {
183
+ CPLog(@"Applying the binary patch failed (decError=%d).", (int)decompressor.decError);
184
+ result = CodePushBinaryPatchApplyResultApplyFailed;
185
+ }
186
+
187
+ free(cache);
188
+ return result;
189
+ }
190
+
191
+ @implementation CodePushBinaryPatch
192
+
193
+ #pragma mark - Public methods
194
+
195
+ + (BOOL)restoreBundleInUnzippedFolder:(NSString *)unzippedFolderPath
196
+ workingFolder:(NSString *)workingFolderPath
197
+ baseBundleURL:(NSURL *)baseBundleURL
198
+ failureReason:(NSString **)failureReason
199
+ {
200
+ NSFileManager *fileManager = [NSFileManager defaultManager];
201
+ NSString *contentsFolderPath = [self contentsFolderInUnzippedFolder:unzippedFolderPath];
202
+ NSString *manifestFilePath = [contentsFolderPath stringByAppendingPathComponent:BinaryPatchManifestFileName];
203
+ if (![self isFileAtPath:manifestFilePath]) {
204
+ return [self failWithReason:CodePushBinaryPatchReasonInvalidManifest
205
+ outFailureReason:failureReason];
206
+ }
207
+
208
+ NSError *error = nil;
209
+ NSData *manifestData = [NSData dataWithContentsOfFile:manifestFilePath
210
+ options:0
211
+ error:&error];
212
+ id manifest = manifestData ? [NSJSONSerialization JSONObjectWithData:manifestData
213
+ options:kNilOptions
214
+ error:&error]
215
+ : nil;
216
+ if (![manifest isKindOfClass:[NSDictionary class]]) {
217
+ CPLog(@"The binary patch manifest could not be read: %@", error);
218
+ return [self failWithReason:CodePushBinaryPatchReasonInvalidManifest
219
+ outFailureReason:failureReason];
220
+ }
221
+
222
+ if ([[self numberInManifest:manifest forKey:BinaryPatchFormatVersionKey] integerValue] != BinaryPatchFormatVersion
223
+ || ![BinaryPatchAlgorithm isEqualToString:[self stringInManifest:manifest forKey:BinaryPatchAlgorithmKey]]) {
224
+ return [self failWithReason:CodePushBinaryPatchReasonUnsupportedFormat
225
+ outFailureReason:failureReason];
226
+ }
227
+
228
+ NSString *targetBundleFilePath = [self pathInsideFolder:contentsFolderPath
229
+ relativePath:[self stringInManifest:manifest forKey:BinaryPatchBundlePathKey]];
230
+ NSString *patchFilePath = [self pathInsideFolder:contentsFolderPath
231
+ relativePath:[self stringInManifest:manifest forKey:BinaryPatchFileKey]];
232
+ NSString *baseBundleHash = [self stringInManifest:manifest forKey:BinaryPatchBaseBundleHashKey];
233
+ NSString *targetBundleHash = [self stringInManifest:manifest forKey:BinaryPatchTargetBundleHashKey];
234
+ long long targetBundleSize = [[self numberInManifest:manifest forKey:BinaryPatchTargetBundleSizeKey] longLongValue];
235
+ if (targetBundleFilePath == nil || patchFilePath == nil || ![self isFileAtPath:patchFilePath]
236
+ || baseBundleHash.length == 0 || targetBundleHash.length == 0
237
+ || targetBundleSize <= 0 || targetBundleSize > BinaryPatchMaxTargetBundleSize) {
238
+ return [self failWithReason:CodePushBinaryPatchReasonInvalidManifest
239
+ outFailureReason:failureReason];
240
+ }
241
+
242
+ // An earlier attempt that was killed while patching leaves its restored bundle behind.
243
+ [fileManager removeItemAtPath:workingFolderPath error:nil];
244
+ if (![fileManager createDirectoryAtPath:workingFolderPath
245
+ withIntermediateDirectories:YES
246
+ attributes:nil
247
+ error:&error]) {
248
+ CPLog(@"Unable to create the binary patch working directory at %@: %@", workingFolderPath, error);
249
+ return [self failWithReason:CodePushBinaryPatchReasonPatchApplyFailed
250
+ outFailureReason:failureReason];
251
+ }
252
+
253
+ NSString *reason = [self restoreBundleFromPatchAtPath:patchFilePath
254
+ baseBundleURL:baseBundleURL
255
+ baseBundleHash:baseBundleHash
256
+ targetBundleFilePath:targetBundleFilePath
257
+ targetBundleHash:targetBundleHash
258
+ targetBundleSize:targetBundleSize
259
+ manifestFilePath:manifestFilePath
260
+ workingFolder:workingFolderPath];
261
+ // Whatever the attempt did, it leaves nothing of its own behind.
262
+ [fileManager removeItemAtPath:workingFolderPath error:nil];
263
+ if (reason != nil) {
264
+ return [self failWithReason:reason outFailureReason:failureReason];
265
+ }
266
+
267
+ return YES;
268
+ }
269
+
270
+ #pragma mark - Private methods
271
+
272
+ /**
273
+ * Everything the restore does once the manifest has been read and the working directory
274
+ * exists, so that the one caller can empty that directory whichever way this ends.
275
+ *
276
+ * @return the reason the full archive has to be downloaded instead, or nil when the
277
+ * update contents are now the ones the full archive would have delivered
278
+ */
279
+ + (NSString *)restoreBundleFromPatchAtPath:(NSString *)patchFilePath
280
+ baseBundleURL:(NSURL *)baseBundleURL
281
+ baseBundleHash:(NSString *)baseBundleHash
282
+ targetBundleFilePath:(NSString *)targetBundleFilePath
283
+ targetBundleHash:(NSString *)targetBundleHash
284
+ targetBundleSize:(long long)targetBundleSize
285
+ manifestFilePath:(NSString *)manifestFilePath
286
+ workingFolder:(NSString *)workingFolderPath
287
+ {
288
+ NSFileManager *fileManager = [NSFileManager defaultManager];
289
+ // A volume that will not say how much room is left is not taken to have none: a
290
+ // restore that runs out of space still fails, just later and by writing a short file.
291
+ NSNumber *freeSize = [fileManager attributesOfFileSystemForPath:workingFolderPath
292
+ error:nil][NSFileSystemFreeSize];
293
+ if (freeSize != nil && freeSize.longLongValue < targetBundleSize) {
294
+ CPLog(@"Not enough free space to restore a %lld byte bundle.", targetBundleSize);
295
+ return CodePushBinaryPatchReasonPatchApplyFailed;
296
+ }
297
+
298
+ NSString *baseBundlePath = [baseBundleURL path];
299
+ if (baseBundlePath == nil) {
300
+ CPLog(@"The app binary carries no JS bundle to patch against.");
301
+ return CodePushBinaryPatchReasonBaseBundleUnavailable;
302
+ }
303
+
304
+ NSError *error = nil;
305
+ // Applying a patch is the one step that needs a whole bundle at once, and the base
306
+ // bundle is a read-only file inside the app binary: mapping it hands the applier the
307
+ // random access it needs without the update ever holding a copy of it.
308
+ // The apply below reads `.bytes` for its whole duration, which is not a use of the
309
+ // NSData itself, so ARC is free to release these two right after their last mention -
310
+ // unmapping the base bundle out from under the applier. Both must stay alive until
311
+ // the scope ends.
312
+ __attribute__((objc_precise_lifetime)) NSData *baseBundle = [NSData dataWithContentsOfFile:baseBundlePath
313
+ options:NSDataReadingMappedIfSafe
314
+ error:&error];
315
+ if (baseBundle == nil) {
316
+ CPLog(@"Unable to read the JS bundle inside the app binary: %@", error);
317
+ return CodePushBinaryPatchReasonBaseBundleUnavailable;
318
+ }
319
+ if (![baseBundleHash isEqualToString:[CodePushUpdateUtils computeHashForData:baseBundle]]) {
320
+ return CodePushBinaryPatchReasonBaseHashMismatch;
321
+ }
322
+
323
+ __attribute__((objc_precise_lifetime)) NSData *patch = [NSData dataWithContentsOfFile:patchFilePath
324
+ options:0
325
+ error:&error];
326
+ if (patch == nil) {
327
+ CPLog(@"Unable to read the binary patch: %@", error);
328
+ return CodePushBinaryPatchReasonPatchApplyFailed;
329
+ }
330
+
331
+ NSString *restoredBundleFilePath = [workingFolderPath stringByAppendingPathComponent:BinaryPatchTargetFileName];
332
+ CodePushBinaryPatchApplyResult applyResult = CodePushApplyBinaryPatch(baseBundle.bytes,
333
+ baseBundle.length,
334
+ patch.bytes,
335
+ patch.length,
336
+ [restoredBundleFilePath fileSystemRepresentation],
337
+ targetBundleSize);
338
+ if (applyResult != CodePushBinaryPatchApplyResultOK) {
339
+ CPLog(@"The binary patch applier returned %ld.", (long)applyResult);
340
+ return applyResult == CodePushBinaryPatchApplyResultUnsupportedCompression
341
+ ? CodePushBinaryPatchReasonUnsupportedFormat
342
+ : CodePushBinaryPatchReasonPatchApplyFailed;
343
+ }
344
+
345
+ NSDictionary *restoredBundleAttributes = [fileManager attributesOfItemAtPath:restoredBundleFilePath
346
+ error:&error];
347
+ if (restoredBundleAttributes == nil || (long long)[restoredBundleAttributes fileSize] != targetBundleSize) {
348
+ CPLog(@"The restored bundle is not %lld bytes long: %@", targetBundleSize, error);
349
+ return CodePushBinaryPatchReasonTargetVerificationFailed;
350
+ }
351
+
352
+ NSString *restoredBundleHash = [CodePushUpdateUtils computeHashForFileAtPath:restoredBundleFilePath];
353
+ if (restoredBundleHash == nil || ![targetBundleHash isEqualToString:restoredBundleHash]) {
354
+ return CodePushBinaryPatchReasonTargetVerificationFailed;
355
+ }
356
+
357
+ if (![self moveItemAtPath:restoredBundleFilePath toPath:targetBundleFilePath]
358
+ || ![fileManager removeItemAtPath:patchFilePath error:&error]
359
+ || ![fileManager removeItemAtPath:manifestFilePath error:&error]) {
360
+ // The contents are half restored, so they must not be installed. The download
361
+ // that follows empties the unzipped archive before it replaces it, which is what
362
+ // clears them.
363
+ CPLog(@"Unable to put the restored bundle in place of the patch: %@", error);
364
+ return CodePushBinaryPatchReasonPatchApplyFailed;
365
+ }
366
+
367
+ return nil;
368
+ }
369
+
370
+ /**
371
+ * Finds the contents root inside an unzipped archive.
372
+ *
373
+ * An archive wraps its files in a single directory, and the manifest's paths are
374
+ * relative to that directory rather than to the archive. An archive whose files are at
375
+ * the top level is its own contents root, which is how the tooling that unpacks one
376
+ * reads it too.
377
+ */
378
+ + (NSString *)contentsFolderInUnzippedFolder:(NSString *)unzippedFolderPath
379
+ {
380
+ NSArray *entries = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:unzippedFolderPath
381
+ error:nil];
382
+ if (entries.count == 1) {
383
+ NSString *entryPath = [unzippedFolderPath stringByAppendingPathComponent:entries.firstObject];
384
+ BOOL isDirectory = NO;
385
+ if ([[NSFileManager defaultManager] fileExistsAtPath:entryPath isDirectory:&isDirectory] && isDirectory) {
386
+ return entryPath;
387
+ }
388
+ }
389
+
390
+ return unzippedFolderPath;
391
+ }
392
+
393
+ /**
394
+ * Resolves a path the manifest points at, refusing anything that would reach outside
395
+ * the archive - an archive is untrusted input, and its manifest is no more trusted
396
+ * than its entries.
397
+ *
398
+ * @return the resolved path, or nil when the path is unusable
399
+ */
400
+ + (NSString *)pathInsideFolder:(NSString *)folderPath
401
+ relativePath:(NSString *)relativePath
402
+ {
403
+ if (relativePath.length == 0 || [relativePath isAbsolutePath]) {
404
+ return nil;
405
+ }
406
+
407
+ NSString *resolvedFolderPath = [[folderPath stringByStandardizingPath] stringByAppendingString:@"/"];
408
+ NSString *resolvedPath = [[folderPath stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
409
+ if (![resolvedPath hasPrefix:resolvedFolderPath]) {
410
+ return nil;
411
+ }
412
+
413
+ return resolvedPath;
414
+ }
415
+
416
+ /** Move, replacing whatever the archive may have carried at the destination. */
417
+ + (BOOL)moveItemAtPath:(NSString *)sourcePath
418
+ toPath:(NSString *)destinationPath
419
+ {
420
+ NSFileManager *fileManager = [NSFileManager defaultManager];
421
+ NSString *destinationFolderPath = [destinationPath stringByDeletingLastPathComponent];
422
+ NSError *error = nil;
423
+ if (![fileManager fileExistsAtPath:destinationFolderPath]
424
+ && ![fileManager createDirectoryAtPath:destinationFolderPath
425
+ withIntermediateDirectories:YES
426
+ attributes:nil
427
+ error:&error]) {
428
+ CPLog(@"Unable to create %@: %@", destinationFolderPath, error);
429
+ return NO;
430
+ }
431
+
432
+ [fileManager removeItemAtPath:destinationPath error:nil];
433
+ if (![fileManager moveItemAtPath:sourcePath toPath:destinationPath error:&error]) {
434
+ CPLog(@"Unable to move %@ to %@: %@", sourcePath, destinationPath, error);
435
+ return NO;
436
+ }
437
+
438
+ return YES;
439
+ }
440
+
441
+ + (BOOL)isFileAtPath:(NSString *)path
442
+ {
443
+ BOOL isDirectory = NO;
444
+ return [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory] && !isDirectory;
445
+ }
446
+
447
+ /** The value of a manifest key, or nil when it is absent or not a string. */
448
+ + (NSString *)stringInManifest:(NSDictionary *)manifest
449
+ forKey:(NSString *)key
450
+ {
451
+ id value = manifest[key];
452
+ return [value isKindOfClass:[NSString class]] ? value : nil;
453
+ }
454
+
455
+ /** The value of a manifest key, or nil when it is absent or not a number. */
456
+ + (NSNumber *)numberInManifest:(NSDictionary *)manifest
457
+ forKey:(NSString *)key
458
+ {
459
+ id value = manifest[key];
460
+ return [value isKindOfClass:[NSNumber class]] ? value : nil;
461
+ }
462
+
463
+ + (BOOL)failWithReason:(NSString *)reason
464
+ outFailureReason:(NSString **)failureReason
465
+ {
466
+ if (failureReason != NULL) {
467
+ *failureReason = reason;
468
+ }
469
+
470
+ return NO;
471
+ }
472
+
473
+ @end