react-native-update 10.59.2 → 10.60.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 (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2634
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +73 -82
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -0,0 +1,689 @@
1
+ // The install pipeline shared by JS calls and the native round: download
2
+ // (RCTPushyDownloader) → unzip (archive guard) → patch (cpp/patch_core) →
3
+ // promote (RCTPushyInstallRecord.mm), plus version activation and cleanup.
4
+ #import "RCTPushyInternal.h"
5
+ #import "RCTPushyDownloader.h"
6
+ #import <SSZipArchive/SSZipArchive.h>
7
+ #include "../../cpp/patch_core/archive_limits.h"
8
+ #include "../../cpp/patch_core/archive_patch_core.h"
9
+ #include "../../cpp/patch_core/digest.h"
10
+ #include "../../cpp/patch_core/error_codes.h"
11
+ #include "../../cpp/patch_core/install_record.h"
12
+ #include "../../cpp/patch_core/patch_core.h"
13
+
14
+ #include <memory>
15
+
16
+ // file def
17
+ static NSString *const SOURCE_PATCH_NAME = @"__diff.json";
18
+ static NSString *const ERROR_FILE_OPERATION = @"file operation error";
19
+
20
+ // Protocol strings stored encoded (RCTPushyRevealText) so they do not appear
21
+ // as plain binary strings; decoded on first use.
22
+ static NSString *BUNDLE_PATCH_NAME(void) {
23
+ static NSString *value = RCTPushyRevealText("3319f0d4b6c56a502c3b10fcdca0de7d4b3307e9");
24
+ return value;
25
+ }
26
+ PUSHY_HIDDEN NSString *PushyOptionUrlKey(void) {
27
+ static NSString *value = RCTPushyRevealText("2f07f0d0ba8e5d572e");
28
+ return value;
29
+ }
30
+ static NSString *PushyIpaDeltaSuffix(void) {
31
+ static NSString *value = RCTPushyRevealText("741ee4d0e09b69512137");
32
+ return value;
33
+ }
34
+ static NSString *PushyPpkDeltaSuffix(void) {
35
+ static NSString *value = RCTPushyRevealText("7407e4dae09b69512137");
36
+ return value;
37
+ }
38
+
39
+ static NSString *PushyArchiveExtension(PushyType type) {
40
+ switch (type) {
41
+ case PushyTypePatchFromPackage:
42
+ return PushyIpaDeltaSuffix();
43
+ case PushyTypePatchFromPpk:
44
+ return PushyPpkDeltaSuffix();
45
+ case PushyTypeFullDownload:
46
+ break;
47
+ }
48
+ return @".ppk";
49
+ }
50
+
51
+ PUSHY_HIDDEN dispatch_queue_t PushyFileQueue(void) {
52
+ // One process-wide serial queue: JS calls (from any bridge instance; a
53
+ // reload can briefly keep two alive) and the native round share it, so
54
+ // destructive file work (resetToPackagedBundle's full cleanup) stays
55
+ // serialized with every unzip/patch job.
56
+ static dispatch_queue_t queue =
57
+ dispatch_queue_create("cn.reactnative.pushy.file", DISPATCH_QUEUE_SERIAL);
58
+ return queue;
59
+ }
60
+
61
+ // nil when the volume holding `path` can take `bytesToWrite` plus the safety
62
+ // margin (or the free space is unknown); a PATCH_FAILED error otherwise. The
63
+ // probe is the downloader's (RCTPushyFreeSpaceShortfall), which applies the
64
+ // same rule to the announced Content-Length before the first byte lands.
65
+ static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
66
+ NSString *shortfall = RCTPushyFreeSpaceShortfall(path, bytesToWrite);
67
+ if (shortfall == nil) {
68
+ return nil;
69
+ }
70
+ return PushyErrorWithCode(pushy::error_codes::kDeltaFailed, shortfall);
71
+ }
72
+
73
+ // SSZipArchive delegate feeding cpp/patch_core/archive_limits.h's
74
+ // EntryAccumulator while the archive is walked (entry count, sizes, ratios,
75
+ // symlinks: SSZipArchive materialises them with any target regardless of
76
+ // preserveAttributes, so `assets -> ../../Documents` followed by `assets/x`
77
+ // would write outside the version directory). The entry name is only exposed
78
+ // once the entry is on disk, so the reserved `.pushy-` names (the completion
79
+ // record is the SDK's alone) are checked in the did-unzip callback.
80
+ // A NO from zipArchiveShouldUnzipFileAtIndex: cancels the whole unzip (the
81
+ // 2.x contract the podspec pins), and the completion handler turns the
82
+ // recorded violation into a hard PATCH_FAILED; the caller then discards the
83
+ // staging directory.
84
+ @interface PushyUnzipGuard : NSObject <SSZipArchiveDelegate>
85
+ @property (nonatomic, copy) NSString *violation;
86
+ - (instancetype)initWithArchiveBytes:(long long)archiveBytes destination:(NSString *)destination;
87
+ @end
88
+
89
+ @implementation PushyUnzipGuard {
90
+ std::unique_ptr<pushy::archive_limits::EntryAccumulator> _accumulator;
91
+ NSArray<NSString *> *_destinationComponents;
92
+ }
93
+
94
+ - (instancetype)initWithArchiveBytes:(long long)archiveBytes destination:(NSString *)destination {
95
+ if ((self = [super init])) {
96
+ _accumulator.reset(new pushy::archive_limits::EntryAccumulator(archiveBytes));
97
+ // Joined the way SSZipArchive builds each unpacked path
98
+ // (stringByAppendingPathComponent:), so a trailing slash compares equal.
99
+ NSArray<NSString *> *probe =
100
+ [destination stringByAppendingPathComponent:@"x"].pathComponents;
101
+ _destinationComponents = [probe subarrayWithRange:NSMakeRange(0, probe.count - 1)];
102
+ }
103
+ return self;
104
+ }
105
+
106
+ - (void)recordStatus:(const pushy::delta::Status &)status {
107
+ if (!status.ok && self.violation == nil) {
108
+ self.violation = @(status.message.c_str());
109
+ }
110
+ }
111
+
112
+ - (void)zipArchiveWillUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo {
113
+ [self recordStatus:pushy::archive_limits::CheckEntryCount((long long)zipInfo.number_entry)];
114
+ }
115
+
116
+ - (BOOL)zipArchiveShouldUnzipFileAtIndex:(NSInteger)fileIndex
117
+ totalFiles:(NSInteger)totalFiles
118
+ archivePath:(NSString *)archivePath
119
+ fileInfo:(unz_file_info)fileInfo {
120
+ if (self.violation != nil) {
121
+ return NO;
122
+ }
123
+ // Unix mode bits travel in the high 16 bits of the external attributes
124
+ // (the same field SSZipArchive keys its own symlink branch on).
125
+ BOOL symlink = ((fileInfo.external_fa >> 16) & S_IFMT) == S_IFLNK;
126
+ [self recordStatus:_accumulator->Accept((long long)fileInfo.uncompressed_size,
127
+ (long long)fileInfo.compressed_size,
128
+ symlink, std::string())];
129
+ return self.violation == nil;
130
+ }
131
+
132
+ // A reserved name is flagged here (any component of the path below the
133
+ // destination); the next shouldUnzip refuses (cancelling the unzip) or, for
134
+ // the last entry, the completion handler picks the violation up. Either way
135
+ // the caller drops the whole staging directory, so the written file never
136
+ // survives.
137
+ - (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex
138
+ totalFiles:(NSInteger)totalFiles
139
+ archivePath:(NSString *)archivePath
140
+ unzippedFilePath:(NSString *)unzippedFilePath {
141
+ if (self.violation != nil) {
142
+ return;
143
+ }
144
+ NSArray<NSString *> *components = unzippedFilePath.pathComponents;
145
+ NSUInteger base = _destinationComponents.count;
146
+ if (components.count <= base
147
+ || ![[components subarrayWithRange:NSMakeRange(0, base)]
148
+ isEqualToArray:_destinationComponents]) {
149
+ self.violation = [NSString stringWithFormat:@"archive entry #%ld outside the destination",
150
+ (long)fileIndex];
151
+ return;
152
+ }
153
+ NSString *name = [[components subarrayWithRange:NSMakeRange(base, components.count - base)]
154
+ componentsJoinedByString:@"/"];
155
+ if (pushy::archive_limits::IsReservedEntryName(PushyToStdString(name))) {
156
+ self.violation = [NSString stringWithFormat:@"archive contains reserved entry %@", name];
157
+ }
158
+ }
159
+
160
+ @end
161
+
162
+ // JS (any bridge instance) and the native round share one download per target
163
+ // hash. Without this process-wide registry two transfers race over the same
164
+ // archive path and the later unzip can delete the first task's valid output.
165
+ static NSMutableDictionary<NSString *, NSMutableDictionary *> *PushyInFlightDownloads(void) {
166
+ static NSMutableDictionary<NSString *, NSMutableDictionary *> *downloads =
167
+ [NSMutableDictionary dictionary];
168
+ return downloads;
169
+ }
170
+
171
+ typedef NS_ENUM(NSInteger, PushyDownloadRegistration) {
172
+ PushyDownloadRegistrationOwner,
173
+ PushyDownloadRegistrationJoined,
174
+ PushyDownloadRegistrationDeferred,
175
+ };
176
+
177
+ static PushyDownloadRegistration PushyRegisterDownload(
178
+ NSString *hash,
179
+ NSInteger type,
180
+ NSTimeInterval deadlineUptime,
181
+ void (^callback)(NSError *),
182
+ void (^progress)(long long, long long),
183
+ void (^deferredStart)(void)
184
+ ) {
185
+ NSMutableDictionary *downloads = PushyInFlightDownloads();
186
+ @synchronized (downloads) {
187
+ NSMutableDictionary *entry = downloads[hash];
188
+ if (entry != nil) {
189
+ if ([entry[@"type"] integerValue] == type) {
190
+ NSTimeInterval ownerDeadline = [entry[@"deadlineUptime"] doubleValue];
191
+ // A caller with substantially more time must not inherit an
192
+ // owner's nearly-exhausted timeout: it observes the current
193
+ // transfer and restarts after the owner settles (the
194
+ // completion-marker preflight makes a successful owner free).
195
+ // The comparison is on remaining budget, not on the absolute
196
+ // deadline: a JS caller always computes now+600 a few seconds
197
+ // after the owner did, so a strict `>` would defer every
198
+ // second caller and turn the shared download back into
199
+ // serialized re-downloads. Only a genuinely starved owner
200
+ // (less than half the newcomer's budget left) defers.
201
+ const NSTimeInterval now = PushyMonotonicNow();
202
+ if (2 * (ownerDeadline - now) < (deadlineUptime - now)) {
203
+ if (progress != nil) {
204
+ [entry[@"progress"] addObject:[progress copy]];
205
+ }
206
+ [entry[@"deferred"] addObject:[deferredStart copy]];
207
+ return PushyDownloadRegistrationDeferred;
208
+ }
209
+ [entry[@"callbacks"] addObject:[callback copy]];
210
+ if (progress != nil) {
211
+ [entry[@"progress"] addObject:[progress copy]];
212
+ }
213
+ return PushyDownloadRegistrationJoined;
214
+ }
215
+ // A diff failure must not settle a joined full request. Queue the
216
+ // different artifact type behind the owner; once restarted it
217
+ // registers normally (and re-checks the completion marker).
218
+ if (progress != nil) {
219
+ // The artifact type differs, but it still installs the same
220
+ // target hash. Keep the waiting JS UI moving while its own
221
+ // transfer is queued behind the current owner.
222
+ [entry[@"progress"] addObject:[progress copy]];
223
+ }
224
+ [entry[@"deferred"] addObject:[deferredStart copy]];
225
+ return PushyDownloadRegistrationDeferred;
226
+ }
227
+ NSMutableArray *progressHandlers = [NSMutableArray array];
228
+ if (progress != nil) {
229
+ [progressHandlers addObject:[progress copy]];
230
+ }
231
+ downloads[hash] = [@{
232
+ @"type": @(type),
233
+ @"deadlineUptime": @(deadlineUptime),
234
+ @"callbacks": [NSMutableArray arrayWithObject:[callback copy]],
235
+ @"progress": progressHandlers,
236
+ @"deferred": [NSMutableArray array],
237
+ } mutableCopy];
238
+ return PushyDownloadRegistrationOwner;
239
+ }
240
+ }
241
+
242
+ static void PushyReportDownloadProgress(
243
+ NSString *hash, long long received, long long total
244
+ ) {
245
+ NSMutableDictionary *downloads = PushyInFlightDownloads();
246
+ NSArray *handlers = nil;
247
+ @synchronized (downloads) {
248
+ handlers = [downloads[hash][@"progress"] copy];
249
+ }
250
+ for (id value in handlers) {
251
+ void (^handler)(long long, long long) =
252
+ (void (^)(long long, long long))value;
253
+ handler(received, total);
254
+ }
255
+ }
256
+
257
+ static void PushyFinishDownload(NSString *hash, NSError *error) {
258
+ NSMutableDictionary *downloads = PushyInFlightDownloads();
259
+ NSArray *callbacks = nil;
260
+ NSArray *deferred = nil;
261
+ @synchronized (downloads) {
262
+ callbacks = [downloads[hash][@"callbacks"] copy];
263
+ deferred = [downloads[hash][@"deferred"] copy];
264
+ [downloads removeObjectForKey:hash];
265
+ }
266
+ // Establish the next (different-type) owner before waking the completed
267
+ // owner's callbacks; otherwise its strategy loop could race the waiter.
268
+ for (id value in deferred) {
269
+ void (^start)(void) = (void (^)(void))value;
270
+ start();
271
+ }
272
+ for (id value in callbacks) {
273
+ void (^callback)(NSError *) = (void (^)(NSError *))value;
274
+ callback(error);
275
+ }
276
+ }
277
+
278
+ // Everything under rctpushy is re-downloadable, and Application Support is
279
+ // backed up to iCloud by default — Apple requires such content to be
280
+ // excluded from backups.
281
+ static void PushyExcludeFromBackup(NSString *path) {
282
+ #if !TARGET_OS_TV // tvOS stores under Caches, which is never backed up.
283
+ NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES];
284
+ NSError *error = nil;
285
+ if (![url setResourceValue:@YES
286
+ forKey:NSURLIsExcludedFromBackupKey
287
+ error:&error]) {
288
+ RCTLogWarn(@"Pushy exclude from backup error: %@", error.localizedDescription);
289
+ }
290
+ #endif
291
+ }
292
+
293
+ static BOOL PushyEnsureDirectoryExists(NSString *path) {
294
+ // No file-queue hop here: that queue also runs multi-second unzip/patch
295
+ // work, and a dispatch_sync onto it would block the whole module method
296
+ // queue for the duration. createDirectoryAtPath is idempotent and
297
+ // thread-safe, so checking inline is fine.
298
+ NSFileManager *fileManager = [NSFileManager defaultManager];
299
+ BOOL isDirectory = NO;
300
+ if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
301
+ if (isDirectory) {
302
+ // Directories created by older versions never got the flag.
303
+ PushyExcludeFromBackup(path);
304
+ }
305
+ return isDirectory;
306
+ }
307
+
308
+ NSError *error = nil;
309
+ BOOL success = [fileManager createDirectoryAtPath:path
310
+ withIntermediateDirectories:YES
311
+ attributes:nil
312
+ error:&error];
313
+ if (!success && error != nil) {
314
+ RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
315
+ }
316
+ if (success) {
317
+ PushyExcludeFromBackup(path);
318
+ }
319
+
320
+ return success;
321
+ }
322
+
323
+ @implementation RCTPushyInstaller
324
+
325
+ + (void)fetch:(PushyType)type
326
+ options:(NSDictionary *)options
327
+ progress:(void (^)(long long, long long))progress
328
+ callback:(void (^)(NSError *error))callback
329
+ {
330
+ NSString *updateUrl = PushyOptionString(options, PushyOptionUrlKey());
331
+ NSString *hash = PushyOptionString(options, @"hash");
332
+
333
+ if (PushyStringIsBlank(updateUrl) || !PushyIsSafePathComponent(hash)) {
334
+ callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
335
+ return;
336
+ }
337
+ NSString *originHash = PushyOptionString(options, @"originHash");
338
+ if (type == PushyTypePatchFromPpk && !PushyIsSafePathComponent(originHash)) {
339
+ callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
340
+ return;
341
+ }
342
+
343
+ NSString *dir = [RCTPushy downloadDir];
344
+ if (!PushyEnsureDirectoryExists(dir)) {
345
+ callback(PushyErrorWithCode(pushy::error_codes::kFileOperationFailed, ERROR_FILE_OPERATION));
346
+ return;
347
+ }
348
+
349
+ if (PushyHasCompletedVersionAtPath(PushyVersionDir(hash), hash)) {
350
+ callback(nil);
351
+ return;
352
+ }
353
+
354
+ NSTimeInterval deadlineUptime = PushyMonotonicNow() + 600;
355
+ NSNumber *configuredDeadline = options[@"deadlineUptime"];
356
+ if ([configuredDeadline isKindOfClass:[NSNumber class]]) {
357
+ deadlineUptime = configuredDeadline.doubleValue;
358
+ }
359
+ NSError *(^deadlineExpired)(void) = ^NSError * {
360
+ return PushyErrorWithCode(pushy::error_codes::kDownloadFailed,
361
+ @"download deadline expired before start");
362
+ };
363
+ if (deadlineUptime <= PushyMonotonicNow()) {
364
+ callback(deadlineExpired());
365
+ return;
366
+ }
367
+
368
+ void (^deferredStart)(void) = ^{
369
+ [self fetch:type options:options progress:progress callback:callback];
370
+ };
371
+ PushyDownloadRegistration registration = PushyRegisterDownload(
372
+ hash, type, deadlineUptime, callback, progress, deferredStart);
373
+ if (registration != PushyDownloadRegistrationOwner) {
374
+ RCTLogInfo(
375
+ @"RCTPushy -- %@ in-flight download for %@",
376
+ registration == PushyDownloadRegistrationJoined ? @"join" : @"defer",
377
+ hash);
378
+ return;
379
+ }
380
+
381
+ NSString *zipFilePath = [dir stringByAppendingPathComponent:
382
+ [hash stringByAppendingString:PushyArchiveExtension(type)]];
383
+ // SHA-256 of the downloaded archive for the completion record. Written and
384
+ // read on the serial file queue only: taken before the unzip consumes the
385
+ // file, consumed by the completion block after it.
386
+ __block NSString *artifactSha256 = nil;
387
+
388
+ // Settles every JS/native waiter only after the staging directory was
389
+ // promoted (or dropped) on the process-wide file queue. On failure only
390
+ // the staging directory is removed, like Android/Harmony: a half-unzipped
391
+ // or half-patched dir leaks disk and could later be mistaken for a
392
+ // complete version, while the final version directory is never touched
393
+ // by a failed install. hash is validated above, so this can never resolve
394
+ // to the download root itself.
395
+ void (^completion)(NSError *) = ^(NSError *error) {
396
+ dispatch_async(PushyFileQueue(), ^{
397
+ NSError *finalError = error ?: PushyPromoteStagingDir(hash, artifactSha256);
398
+ if (finalError != nil) {
399
+ [[NSFileManager defaultManager] removeItemAtPath:PushyStagingDir(hash) error:nil];
400
+ }
401
+ PushyFinishDownload(hash, finalError);
402
+ });
403
+ };
404
+
405
+ RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
406
+ NSTimeInterval timeoutSeconds = deadlineUptime - PushyMonotonicNow();
407
+ if (timeoutSeconds <= 0) {
408
+ completion(deadlineExpired());
409
+ return;
410
+ }
411
+ [RCTPushyDownloader download:updateUrl
412
+ savePath:zipFilePath
413
+ timeoutInterval:timeoutSeconds
414
+ progressHandler:^(long long receivedBytes, long long totalBytes) {
415
+ PushyReportDownloadProgress(hash, receivedBytes, totalBytes);
416
+ } completionHandler:^(NSString *path, NSError *error) {
417
+ if (error != nil) {
418
+ completion(error);
419
+ return;
420
+ }
421
+ dispatch_async(PushyFileQueue(), ^{
422
+ artifactSha256 = PushyFromStdString(
423
+ pushy::digest::Sha256File(PushyToStdString(zipFilePath)));
424
+ });
425
+ [self unzipDownloadedPackage:zipFilePath
426
+ hash:hash
427
+ type:type
428
+ originHash:originHash
429
+ callback:completion];
430
+ }];
431
+ }
432
+
433
+ + (void)unzipDownloadedPackage:(NSString *)zipFilePath
434
+ hash:(NSString *)hash
435
+ type:(PushyType)type
436
+ originHash:(NSString *)originHash
437
+ callback:(void (^)(NSError *error))callback
438
+ {
439
+ RCTLogInfo(@"RCTPushy -- unzip file %@", zipFilePath);
440
+ // Everything lands in <hash>.staging; the completion block promotes it.
441
+ [self unzipFileAtPath:zipFilePath
442
+ toDestination:PushyStagingDir(hash)
443
+ completionHandler:^(NSError *error) {
444
+ dispatch_async(PushyFileQueue(), ^{
445
+ if (error != nil) {
446
+ callback(error);
447
+ return;
448
+ }
449
+ @try {
450
+ [self finishDownloadedPackage:hash type:type originHash:originHash callback:callback];
451
+ } @catch (NSException *exception) {
452
+ // An uncaught exception in a GCD block is fatal (and would
453
+ // trip the crash rescue); a bad package is a PATCH_FAILED.
454
+ callback(PushyErrorWithCode(pushy::error_codes::kDeltaFailed,
455
+ exception.reason ?: @"delta failed"));
456
+ }
457
+ });
458
+ }];
459
+ }
460
+
461
+ + (void)finishDownloadedPackage:(NSString *)hash
462
+ type:(PushyType)type
463
+ originHash:(NSString *)originHash
464
+ callback:(void (^)(NSError *error))callback
465
+ {
466
+ switch (type) {
467
+ case PushyTypePatchFromPackage:
468
+ callback([self applyDeltaForHash:hash
469
+ type:type
470
+ fromBundle:[[RCTPushy binaryBundleURL] path]
471
+ source:[[NSBundle mainBundle] resourcePath]]);
472
+ return;
473
+ case PushyTypePatchFromPpk: {
474
+ NSString *lastVersionDir = PushyVersionDir(originHash);
475
+ callback([self applyDeltaForHash:hash
476
+ type:type
477
+ fromBundle:[lastVersionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]
478
+ source:lastVersionDir]);
479
+ return;
480
+ }
481
+ case PushyTypeFullDownload:
482
+ callback(nil);
483
+ return;
484
+ }
485
+ }
486
+
487
+ // Applies the unpacked patch in the staging directory (two-phase install);
488
+ // nil on success, a PATCH_FAILED error otherwise. __diff.json is handed to
489
+ // the patch core as text (cpp/patch_core/patch_manifest.h parses it).
490
+ + (NSError *)applyDeltaForHash:(NSString *)hash
491
+ type:(PushyType)type
492
+ fromBundle:(NSString *)bundleOrigin
493
+ source:(NSString *)sourceOrigin
494
+ {
495
+ NSString *unzipDir = PushyStagingDir(hash);
496
+ NSString *sourcePatch = [unzipDir stringByAppendingPathComponent:SOURCE_PATCH_NAME];
497
+ NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME()];
498
+ NSFileManager *fileManager = [NSFileManager defaultManager];
499
+
500
+ pushy::archive_patch::ArchivePatchRequest request;
501
+ request.archive.type = type == PushyTypePatchFromPackage
502
+ ? pushy::archive_patch::ArchivePatchType::kPatchFromPackage
503
+ : pushy::archive_patch::ArchivePatchType::kPatchFromPpk;
504
+ if ([fileManager fileExistsAtPath:sourcePatch isDirectory:NULL]) {
505
+ long long manifestBytes = RCTPushyFileSize(sourcePatch);
506
+ if (manifestBytes > pushy::archive_limits::kMaxManifestBytes) {
507
+ return PushyErrorWithCode(pushy::error_codes::kDeltaFailed,
508
+ [NSString stringWithFormat:@"delta manifest too large: %lld bytes", manifestBytes]);
509
+ }
510
+ NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
511
+ if (data == nil) {
512
+ return PushyErrorWithCode(pushy::error_codes::kDeltaFailed, @"unreadable delta manifest");
513
+ }
514
+ request.archive.entry_names.push_back(PushyToStdString(SOURCE_PATCH_NAME));
515
+ request.archive.manifest_json.assign(static_cast<const char *>(data.bytes), data.length);
516
+ }
517
+ if ([fileManager fileExistsAtPath:bundlePatch isDirectory:NULL]) {
518
+ request.archive.entry_names.push_back(PushyToStdString(BUNDLE_PATCH_NAME()));
519
+ }
520
+ request.source_root = PushyToStdString(sourceOrigin);
521
+ request.target_root = PushyToStdString(unzipDir);
522
+ request.origin_bundle_path = PushyToStdString(bundleOrigin);
523
+ request.bundle_patch_path = PushyToStdString(bundlePatch);
524
+ request.bundle_output_path = PushyToStdString(
525
+ [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]);
526
+
527
+ pushy::delta::Status status = pushy::archive_patch::ApplyArchivePatch(request, nullptr);
528
+ if (!status.ok) {
529
+ return PushyErrorWithCode(pushy::error_codes::kDeltaFailed, @(status.message.c_str()));
530
+ }
531
+ return nil;
532
+ }
533
+
534
+ + (void)unzipFileAtPath:(NSString *)path
535
+ toDestination:(NSString *)destination
536
+ completionHandler:(void (^)(NSError *error))completionHandler
537
+ {
538
+ dispatch_async(PushyFileQueue(), ^{
539
+ NSFileManager *fileManager = [NSFileManager defaultManager];
540
+ if ([fileManager fileExistsAtPath:destination]) {
541
+ [fileManager removeItemAtPath:destination error:nil];
542
+ }
543
+
544
+ // Resource caps before the first byte is extracted
545
+ // (cpp/patch_core/archive_limits.h). SSZipArchive only exposes the
546
+ // entry count up front (zipArchiveWillUnzipArchiveAtPath:zipInfo:),
547
+ // not the uncompressed total, so the disk check uses a 2x-archive
548
+ // heuristic; the guard enforces the exact caps entry by entry.
549
+ long long archiveBytes = RCTPushyFileSize(path);
550
+ NSError *preflight = nil;
551
+ if (archiveBytes > pushy::archive_limits::kMaxArchiveBytes) {
552
+ preflight = PushyErrorWithCode(pushy::error_codes::kDeltaFailed,
553
+ [NSString stringWithFormat:@"archive too large: %lld bytes", archiveBytes]);
554
+ } else {
555
+ preflight = PushyEnsureFreeSpace(destination, MAX(0LL, archiveBytes) * 2);
556
+ }
557
+ if (preflight != nil) {
558
+ RCTPushyDiscardArchive(path);
559
+ completionHandler(preflight);
560
+ return;
561
+ }
562
+
563
+ PushyUnzipGuard *guard = [[PushyUnzipGuard alloc] initWithArchiveBytes:archiveBytes
564
+ destination:destination];
565
+ // preserveAttributes:NO — the archive's mode/mtime bits have no use
566
+ // here and are one less thing a hostile package controls.
567
+ [SSZipArchive unzipFileAtPath:path
568
+ toDestination:destination
569
+ preserveAttributes:NO
570
+ overwrite:YES
571
+ nestedZipLevel:0
572
+ password:nil
573
+ error:nil
574
+ delegate:guard
575
+ progressHandler:nil
576
+ completionHandler:^(NSString *archivePath, BOOL succeeded, NSError *error) {
577
+ // The resume sidecar dies with its archive (§11.4): whether the
578
+ // unzip consumed it or classified it as poisoned, nothing must
579
+ // survive to vouch for bytes that are gone.
580
+ RCTPushyDiscardArchive(archivePath);
581
+
582
+ NSError *unzipError = error;
583
+ if (guard.violation != nil) {
584
+ unzipError = PushyErrorWithCode(pushy::error_codes::kDeltaFailed, guard.violation);
585
+ } else if (!succeeded && unzipError == nil) {
586
+ unzipError = PushyErrorWithCode(pushy::error_codes::kDeltaFailed, @"unzip failed");
587
+ } else if (unzipError != nil && unzipError.userInfo[RCTPushyErrorCodeKey] == nil) {
588
+ // SSZipArchive's own NSError (corrupt zip, bad magic, ...) has
589
+ // no stable code; without one, fetchPackage's fallback would
590
+ // classify it as DOWNLOAD_FAILED even though the download
591
+ // succeeded — keep the classification deterministic.
592
+ unzipError = PushyErrorWithCode(pushy::error_codes::kDeltaFailed,
593
+ unzipError.localizedDescription ?: @"unzip failed");
594
+ }
595
+ completionHandler(unzipError);
596
+ }];
597
+ });
598
+ }
599
+
600
+ + (BOOL)switchVersion:(NSString *)hash error:(NSError **)error
601
+ {
602
+ if (!PushyIsSafePathComponent(hash)) {
603
+ if (error != NULL) {
604
+ *error = PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS);
605
+ }
606
+ return NO;
607
+ }
608
+
609
+ // Same gate as Android and Harmony (install_record::CheckActivation):
610
+ // only a version this SDK recorded as completely installed, whose
611
+ // record's bundle digest matches the bytes on disk, may be activated.
612
+ // Versions activated before records existed are grandfathered through
613
+ // current/last state, but only while their directory has no record file.
614
+ std::string versionDir = PushyToStdString(PushyVersionDir(hash));
615
+ std::string hashStd = PushyToStdString(hash);
616
+ std::string bundleName = PushyToStdString(BUNDLE_FILE_NAME);
617
+ BOOL (^isCurrentOrLast)(void) = ^BOOL {
618
+ pushy::state::State state = PushyStateFromDefaults(PushyDefaults());
619
+ return state.current_version == hashStd || state.last_version == hashStd;
620
+ };
621
+
622
+ // The digest check reads the whole bundle, so it runs OUTSIDE the state
623
+ // lock: +bundleURL takes that lock on the main thread at launch.
624
+ __block BOOL grandfathered = NO;
625
+ PushyWithStateLock(^{
626
+ grandfathered = isCurrentOrLast();
627
+ });
628
+ pushy::install_record::Activation activation;
629
+ pushy::install_record::Status verified = pushy::install_record::CheckActivation(
630
+ versionDir, hashStd, bundleName, grandfathered, &activation);
631
+
632
+ __block NSError *lockedError = verified.ok
633
+ ? nil
634
+ : PushyErrorWithCode(pushy::error_codes::kSwitchVersionFailed,
635
+ PushyFromStdString(verified.message));
636
+ if (lockedError == nil) {
637
+ PushyWithStateLock(^{
638
+ // Re-check cheaply that the directory is still the one that was
639
+ // verified: same grandfathering, same record, same bundle file.
640
+ if (!pushy::install_record::Unchanged(versionDir, bundleName, isCurrentOrLast(),
641
+ activation)) {
642
+ lockedError = PushyErrorWithCode(
643
+ pushy::error_codes::kSwitchVersionFailed,
644
+ [NSString stringWithFormat:@"Bundle version %@ changed while switching.", hash]);
645
+ return;
646
+ }
647
+ PushySwitchVersionLocked(hash);
648
+ });
649
+ }
650
+ if (lockedError != nil) {
651
+ if (error != NULL) {
652
+ *error = lockedError;
653
+ }
654
+ return NO;
655
+ }
656
+ return YES;
657
+ }
658
+
659
+ + (void)clearInvalidFiles
660
+ {
661
+ dispatch_async(PushyFileQueue(), ^{
662
+ // Snapshot the state under the lock, but run the (slow) filesystem
663
+ // cleanup outside of it so state operations are not blocked.
664
+ __block pushy::state::State state;
665
+ __block NSString *launchVersion = nil;
666
+ PushyWithStateLock(^{
667
+ state = PushyStateFromDefaults(PushyDefaults());
668
+ launchVersion = pushyLaunchVersion;
669
+ });
670
+ // Keep the persisted current/last versions AND the version this
671
+ // process booted from: two switches without a restart would otherwise
672
+ // evict the running bundle while its on-demand assets are still
673
+ // being served.
674
+ pushy::delta::Status status = pushy::delta::CleanupOldEntries(
675
+ PushyToStdString([RCTPushy downloadDir]),
676
+ std::vector<std::string>{
677
+ state.current_version,
678
+ state.last_version,
679
+ PushyToStdString(launchVersion),
680
+ },
681
+ 3
682
+ );
683
+ if (!status.ok) {
684
+ RCTLogWarn(@"Pushy cleanup error: %s", status.message.c_str());
685
+ }
686
+ });
687
+ }
688
+
689
+ @end