@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.
- package/CodePush.podspec +35 -3
- package/README.md +117 -3
- package/THIRD-PARTY-NOTICES +96 -0
- package/android/app/build.gradle +19 -0
- package/android/app/proguard-rules.pro +5 -0
- package/android/app/src/main/cpp/CMakeLists.txt +38 -0
- package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +20 -6
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +168 -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/DownloadProgressCallback.java +7 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressEventDispatcher.java +25 -4
- 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 +211 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +489 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/DownloadProgressEventDispatcherTest.java +80 -0
- package/android/codepush-export.gradle +115 -0
- package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
- package/cli/commands/bundleCommand/index.ts +9 -1
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +118 -0
- package/cli/commands/releaseCommand/index.ts +20 -2
- package/cli/commands/releaseCommand/release.test.ts +582 -0
- package/cli/commands/releaseCommand/release.ts +232 -15
- package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
- package/cli/dist/commands/bundleCommand/index.js +5 -1
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +8 -1
- package/cli/dist/commands/releaseCommand/index.test.js +96 -0
- package/cli/dist/commands/releaseCommand/release.js +165 -17
- package/cli/dist/commands/releaseCommand/release.test.js +367 -0
- package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
- package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
- package/cli/dist/functions/makeCodePushBundle.js +4 -2
- package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
- package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
- package/cli/dist/jest.globalSetup.js +38 -0
- package/cli/dist/utils/binaryPatch.js +86 -0
- package/cli/dist/utils/binaryPatch.test.js +190 -0
- package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
- package/cli/functions/makeBinaryPatchBundle.ts +367 -0
- package/cli/functions/makeCodePushBundle.ts +4 -2
- package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
- package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
- package/cli/jest.globalSetup.ts +43 -0
- package/cli/utils/binaryPatch.test.ts +234 -0
- package/cli/utils/binaryPatch.ts +103 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
- package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
- package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
- package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
- package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
- package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
- package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
- package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
- package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
- package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
- package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
- package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
- package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
- package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
- package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
- package/ios/CodePush/CodePush.h +18 -1
- package/ios/CodePush/CodePush.mm +24 -1
- package/ios/CodePush/CodePushBinaryPatch.h +80 -0
- package/ios/CodePush/CodePushBinaryPatch.m +473 -0
- package/ios/CodePush/CodePushDownloadHandler.m +6 -0
- package/ios/CodePush/CodePushPackage.m +183 -2
- package/ios/CodePush/CodePushUpdateUtils.m +49 -1
- package/package.json +10 -3
- package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
- package/scripts/export-embedded-bundle.sh +76 -0
- package/src/CodePush.js +46 -3
- package/src/CodePush.test.js +382 -0
- package/src/package-mixins.js +20 -2
- package/typings/react-native-code-push.d.ts +80 -1
|
@@ -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
|
|
@@ -61,6 +61,12 @@ failCallback:(void (^)(NSError *err))failCallback {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
self.expectedContentLength = response.expectedContentLength;
|
|
64
|
+
if (self.expectedContentLength > 0) {
|
|
65
|
+
// Each download opens its own progress stream. The zero-received event hands the
|
|
66
|
+
// listener the new total up front, so the full download after a binary patch
|
|
67
|
+
// fallback shows as restarting rather than running backwards.
|
|
68
|
+
self.progressCallback(self.expectedContentLength, 0);
|
|
69
|
+
}
|
|
64
70
|
[self.outputFileStream open];
|
|
65
71
|
}
|
|
66
72
|
|