react-native-update 10.59.1 → 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 +11 -12
  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 +19 -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 -2628
  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 +74 -83
  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,34 @@
1
+ // Internal to the RCTPushy pod and Foundation-only: what every translation
2
+ // unit shares, including RCTPushyConfiguration.mm and RCTPushyDownloader.mm,
3
+ // which compile without React Native (the purge-restore test host builds
4
+ // RCTPushyConfiguration.mm on its own). Not a public header.
5
+ #pragma once
6
+
7
+ #import <Foundation/Foundation.h>
8
+
9
+ // Shared across the pod's translation units, never exported from a dynamic
10
+ // framework.
11
+ #ifndef PUSHY_HIDDEN
12
+ #define PUSHY_HIDDEN __attribute__((visibility("hidden")))
13
+ #endif
14
+
15
+ // Every NSError this pod creates: the domain, and the userInfo key carrying
16
+ // the stable cross-platform code (cpp/patch_core/error_codes.h).
17
+ static NSString *const RCTPushyErrorDomain = @"cn.reactnative.pushy";
18
+ static NSString *const RCTPushyErrorCodeKey = @"PushyErrorCode";
19
+
20
+ // Text produced by scripts/encode-native-text.ts, decoded with
21
+ // pushy::text::Reveal (cpp/patch_core/obscured_text.h). Keeps service
22
+ // addresses, paths and protocol strings out of static string scans of the
23
+ // binary; not a secret.
24
+ PUSHY_HIDDEN NSString *RCTPushyRevealText(const char *hex);
25
+
26
+ // Whether following a redirect from `from` to `to` would leave https for
27
+ // plaintext http. Every https request (check and artifact) refuses that
28
+ // downgrade before the redirected request is sent.
29
+ PUSHY_HIDDEN BOOL RCTPushyIsHttpsDowngrade(NSURL *from, NSURL *to);
30
+
31
+ // Removes a downloaded archive together with its resume sidecar
32
+ // (RCTPushyResumeSidecarPath): nothing may survive to vouch for bytes that
33
+ // are gone.
34
+ PUSHY_HIDDEN void RCTPushyDiscardArchive(NSString *path);
@@ -3,11 +3,3 @@
3
3
  // Validates and snapshots host options without changing any update state.
4
4
  FOUNDATION_EXPORT NSString * _Nullable RCTPushyNormalizeConfiguration(
5
5
  NSDictionary * _Nonnull options, NSError * _Nullable * _Nullable error);
6
-
7
- // Decodes text produced by scripts/encode-native-text.ts (byte i XORed with
8
- // (0x5A + 0x1D * i) & 0xFF), keeping service addresses and paths out of static
9
- // string scans of the binary. Not a secret.
10
- FOUNDATION_EXPORT NSString * _Nonnull RCTPushyRevealText(const char * _Nonnull hex);
11
-
12
- // Request path appended to an endpoint base.
13
- FOUNDATION_EXPORT NSString * _Nonnull RCTPushyQueryPath(void);
@@ -1,130 +1,49 @@
1
1
  #import "RCTPushyConfiguration.h"
2
-
3
- // Read through a volatile so the optimizer cannot fold the decode of a constant
4
- // argument back into a plain string in the binary.
5
- static volatile unsigned char PushyTextKeyBase = 0x5A;
2
+ #import "RCTPushyCommon.h"
3
+ #include "../../cpp/patch_core/error_codes.h"
4
+ #include "../../cpp/patch_core/obscured_text.h"
5
+ #include "../../cpp/update_flow_core/native_config.h"
6
6
 
7
7
  NSString *RCTPushyRevealText(const char *hex) {
8
- size_t length = strlen(hex) / 2;
9
- NSMutableString *text = [NSMutableString stringWithCapacity:length];
10
- unsigned char base = PushyTextKeyBase;
11
- for (size_t i = 0; i < length; i++) {
12
- char pair[3] = {hex[i * 2], hex[i * 2 + 1], 0};
13
- unsigned char byte = (unsigned char)strtoul(pair, NULL, 16);
14
- [text appendFormat:@"%c", (char)(byte ^ (unsigned char)(base + 0x1D * i))];
15
- }
16
- return text;
17
- }
18
-
19
- NSString *RCTPushyQueryPath(void) {
20
- static NSString *path;
21
- static dispatch_once_t once;
22
- dispatch_once(&once, ^{
23
- path = RCTPushyRevealText("7514fcd4ad805d55263e08fc99");
24
- });
25
- return path;
26
- }
27
-
28
- static void PushyConfigInvalid(NSString *message) {
29
- @throw [NSException exceptionWithName:NSInvalidArgumentException
30
- reason:[@"Invalid native configuration: " stringByAppendingString:message]
31
- userInfo:nil];
32
- }
33
-
34
- static NSString *PushyConfigString(id value, NSString *name, BOOL allowEmpty) {
35
- if (![value isKindOfClass:NSString.class]
36
- || (!allowEmpty && [[value stringByTrimmingCharactersInSet:
37
- NSCharacterSet.whitespaceAndNewlineCharacterSet] length] == 0)) {
38
- PushyConfigInvalid([NSString stringWithFormat:@"%@ must be a string%@",
39
- name, allowEmpty ? @"" : @" and must not be blank"]);
40
- }
41
- return value;
42
- }
43
-
44
- static NSArray *PushyConfigUrls(id value, NSString *name, BOOL base) {
45
- if (![value isKindOfClass:NSArray.class] || (base && [value count] == 0)) {
46
- PushyConfigInvalid([NSString stringWithFormat:@"%@ must be %@ array",
47
- name, base ? @"a non-empty" : @"an"]);
48
- }
49
- NSMutableArray *result = [NSMutableArray array];
50
- NSRegularExpression *authority = [NSRegularExpression regularExpressionWithPattern:
51
- @"^https?://(\\[[0-9a-fA-F:]+\\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)"
52
- options:0 error:nil];
53
- for (id item in value) {
54
- NSString *url = [PushyConfigString(item, name, NO) stringByTrimmingCharactersInSet:
55
- NSCharacterSet.whitespaceAndNewlineCharacterSet];
56
- if ([authority firstMatchInString:url options:0 range:NSMakeRange(0, url.length)] == nil
57
- || [url rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].location != NSNotFound
58
- || [url containsString:@"\\"]
59
- || (base && ([url containsString:@"?"] || [url containsString:@"#"]))) {
60
- PushyConfigInvalid([NSString stringWithFormat:
61
- @"%@ requires absolute HTTP(S) URLs without credentials%@",
62
- name, base ? @", queries or fragments" : @""]);
63
- }
64
- if (base) {
65
- while ([url hasSuffix:@"/"]) {
66
- url = [url substringToIndex:url.length - 1];
67
- }
68
- }
69
- if (![result containsObject:url]) {
70
- [result addObject:url];
71
- }
72
- }
73
- return result;
8
+ // The texts are ASCII (scripts/encode-native-text.ts).
9
+ std::string text = pushy::text::Reveal(hex);
10
+ return [[NSString alloc] initWithBytes:text.data()
11
+ length:text.size()
12
+ encoding:NSUTF8StringEncoding] ?: @"";
74
13
  }
75
14
 
15
+ // Validation and defaults are the shared core's (flowcore::NormalizeConfiguration,
16
+ // also used by Android and Harmony); this only bridges the dictionary.
76
17
  NSString *RCTPushyNormalizeConfiguration(NSDictionary *options, NSError **error) {
77
- @try {
78
- if (![options isKindOfClass:NSDictionary.class]) {
79
- PushyConfigInvalid(@"expected an object");
80
- }
81
- NSArray *keys = @[@"appKey", @"endpoints", @"queryUrls", @"afterDownload",
82
- @"disabled", @"packageVersion", @"rnu", @"rn"];
83
- for (id key in options) {
84
- if (![keys containsObject:key]) {
85
- PushyConfigInvalid([NSString stringWithFormat:@"unknown option %@", key]);
86
- }
87
- }
88
- NSString *appKey = PushyConfigString(options[@"appKey"], @"appKey", NO);
89
- BOOL customEndpoints = options[@"endpoints"] != nil;
90
- NSArray *endpoints = PushyConfigUrls(customEndpoints ? options[@"endpoints"]
91
- : @[RCTPushyRevealText("3203e0c1bdd1270a372f18f8c2b6de7f4f2607f5b3d5b9817b592947e5cdefbc8a7e"),
92
- RCTPushyRevealText("3203e0c1bdd1270a372f18f8c2b6de7f4f2607f5f0daac9c644a620ae88ca1ad93")],
93
- @"endpoints", YES);
94
- NSArray *queryUrls = PushyConfigUrls(options[@"queryUrls"] ?: (customEndpoints ? @[]
95
- : @[RCTPushyRevealText("3203e0c1bdd1270a253608fcd3fd9362476817f4f0d5a1996342631be3c2a3a9d779552507fdcde8928a6f512f5ce2ccbdc869404d2f1de79daa826d562c0913eec4fa9b7d4426"),
96
- RCTPushyRevealText("3203e0c1bdd1270a213b12b7dca09468462e12f3b0d5bd813d482446f4c6a1be8e79552507fdcda68cd06e5c3710e480a4867048483e55e0c2ab8d7d43030d1ce9c3b183214e2601f2f0d5b782601e2719e8ca")]),
97
- @"queryUrls", NO);
98
- NSString *afterDownload = options[@"afterDownload"]
99
- ? PushyConfigString(options[@"afterDownload"], @"afterDownload", NO) : @"none";
100
- NSString *nextLaunch = RCTPushyRevealText("2912e0ffab8e6c70323b1dedd3");
101
- if (![@[@"none", nextLaunch] containsObject:afterDownload]) {
102
- PushyConfigInvalid([@"afterDownload must be none or " stringByAppendingString:nextLaunch]);
18
+ // Anything NSJSONSerialization cannot write is not a JSON object; the core
19
+ // reports it as such.
20
+ std::string optionsJson;
21
+ if ([options isKindOfClass:NSDictionary.class]
22
+ && [NSJSONSerialization isValidJSONObject:options]) {
23
+ NSData *data = [NSJSONSerialization dataWithJSONObject:options options:0 error:NULL];
24
+ if (data != nil) {
25
+ optionsJson.assign(static_cast<const char *>(data.bytes), data.length);
103
26
  }
104
- id disabled = options[@"disabled"] ?: @NO;
105
- if (![disabled isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)disabled) != CFBooleanGetTypeID()) {
106
- PushyConfigInvalid(@"disabled must be a boolean");
107
- }
108
- NSMutableDictionary *result = [@{
109
- @"appKey": appKey, @"endpoints": endpoints, @"queryUrls": queryUrls,
110
- @"afterDownload": afterDownload, @"disabled": disabled,
111
- @"rnu": options[@"rnu"] ? PushyConfigString(options[@"rnu"], @"rnu", YES) : @"",
112
- @"rn": options[@"rn"] ? PushyConfigString(options[@"rn"], @"rn", YES) : @""
113
- } mutableCopy];
114
- if (options[@"packageVersion"] != nil) {
115
- result[@"packageVersion"] = PushyConfigString(options[@"packageVersion"], @"packageVersion", NO);
116
- }
117
- // Stable ordering makes repeated identical configure calls idempotent.
118
- NSData *data = [NSJSONSerialization dataWithJSONObject:result
119
- options:NSJSONWritingSortedKeys error:error];
120
- return data == nil ? nil : [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
121
- } @catch (NSException *exception) {
122
- if (error != NULL) {
123
- *error = [NSError errorWithDomain:@"cn.reactnative.pushy" code:1 userInfo:@{
124
- NSLocalizedDescriptionKey: exception.reason ?: @"Invalid native configuration",
125
- @"PushyErrorCode": @"INVALID_OPTIONS"
126
- }];
127
- }
128
- return nil;
129
27
  }
28
+ std::string config;
29
+ std::string message;
30
+ if (flowcore::NormalizeConfiguration(optionsJson, &config, &message)) {
31
+ NSString *result = [[NSString alloc] initWithBytes:config.data()
32
+ length:config.size()
33
+ encoding:NSUTF8StringEncoding];
34
+ if (result != nil) {
35
+ return result;
36
+ }
37
+ message = "Invalid native configuration: not UTF-8";
38
+ }
39
+ if (error != NULL) {
40
+ *error = [NSError errorWithDomain:RCTPushyErrorDomain code:1 userInfo:@{
41
+ NSLocalizedDescriptionKey: [[NSString alloc] initWithBytes:message.data()
42
+ length:message.size()
43
+ encoding:NSUTF8StringEncoding]
44
+ ?: @"Invalid native configuration",
45
+ RCTPushyErrorCodeKey: @(pushy::error_codes::kInvalidOptions)
46
+ }];
47
+ }
48
+ return nil;
130
49
  }
@@ -1,8 +1,7 @@
1
1
  #import "RCTPushyDownloader.h"
2
+ #import "RCTPushyCommon.h"
2
3
  #include "../../cpp/patch_core/archive_limits.h"
3
4
 
4
- static NSString *const RCTPushyDownloaderErrorDomain = @"cn.reactnative.pushy";
5
-
6
5
  // Cross-launch resumable download (NATIVE_CHECKUPDATE_DESIGN §11.4): a data
7
6
  // task streams the archive straight into savePath (a download task's
8
7
  // temporary file is discarded on failure, which made every partial byte
@@ -36,6 +35,16 @@ static void RCTPushyDeleteResumeSidecar(NSString *savePath) {
36
35
  error:nil];
37
36
  }
38
37
 
38
+ void RCTPushyDiscardArchive(NSString *path) {
39
+ [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
40
+ RCTPushyDeleteResumeSidecar(path);
41
+ }
42
+
43
+ BOOL RCTPushyIsHttpsDowngrade(NSURL *from, NSURL *to) {
44
+ return [[from.scheme lowercaseString] isEqualToString:@"https"]
45
+ && [[to.scheme lowercaseString] isEqualToString:@"http"];
46
+ }
47
+
39
48
  long long RCTPushyFileSize(NSString *path) {
40
49
  NSDictionary *attributes =
41
50
  [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
@@ -174,7 +183,7 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
174
183
  {
175
184
  NSURL *url = [NSURL URLWithString:self.urlString];
176
185
  if (url == nil) {
177
- [self completeWithError:[NSError errorWithDomain:RCTPushyDownloaderErrorDomain
186
+ [self completeWithError:[NSError errorWithDomain:RCTPushyErrorDomain
178
187
  code:-1
179
188
  userInfo:@{
180
189
  NSLocalizedDescriptionKey: @"invalid download url",
@@ -263,13 +272,8 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
263
272
  }
264
273
  }
265
274
 
266
- - (void)completeWithError:(NSError *)error
275
+ - (void)closeFileHandle
267
276
  {
268
- if (self.finished) {
269
- return;
270
- }
271
- self.finished = YES;
272
-
273
277
  if (self.fileHandle != nil) {
274
278
  @try {
275
279
  [self.fileHandle closeFile];
@@ -277,9 +281,18 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
277
281
  }
278
282
  self.fileHandle = nil;
279
283
  }
284
+ }
285
+
286
+ - (void)completeWithError:(NSError *)error
287
+ {
288
+ if (self.finished) {
289
+ return;
290
+ }
291
+ self.finished = YES;
292
+
293
+ [self closeFileHandle];
280
294
  if (self.discardPartial) {
281
- [[NSFileManager defaultManager] removeItemAtPath:self.savePath error:nil];
282
- RCTPushyDeleteResumeSidecar(self.savePath);
295
+ RCTPushyDiscardArchive(self.savePath);
283
296
  }
284
297
 
285
298
  void (^completionHandler)(NSString *, NSError *) = self.completionHandler;
@@ -297,7 +310,7 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
297
310
 
298
311
  - (void)failWithDescription:(NSString *)description code:(NSInteger)code
299
312
  {
300
- self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
313
+ self.fileError = [NSError errorWithDomain:RCTPushyErrorDomain
301
314
  code:code
302
315
  userInfo:@{NSLocalizedDescriptionKey: description}];
303
316
  }
@@ -317,15 +330,8 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
317
330
  self.encodedBody = NO;
318
331
  self.lastReportedPercentage = 0;
319
332
  self.lastReportedBytes = 0;
320
- if (self.fileHandle != nil) {
321
- @try {
322
- [self.fileHandle closeFile];
323
- } @catch (NSException *exception) {
324
- }
325
- self.fileHandle = nil;
326
- }
327
- [[NSFileManager defaultManager] removeItemAtPath:self.savePath error:nil];
328
- RCTPushyDeleteResumeSidecar(self.savePath);
333
+ [self closeFileHandle];
334
+ RCTPushyDiscardArchive(self.savePath);
329
335
  [self.session finishTasksAndInvalidate];
330
336
  self.session = nil;
331
337
  [self startTransfer];
@@ -346,9 +352,7 @@ willPerformHTTPRedirection:(NSHTTPURLResponse *)response
346
352
  completionHandler(nil);
347
353
  return;
348
354
  }
349
- NSString *originalScheme = [[NSURL URLWithString:self.urlString].scheme lowercaseString];
350
- NSString *nextScheme = [request.URL.scheme lowercaseString];
351
- if ([originalScheme isEqualToString:@"https"] && [nextScheme isEqualToString:@"http"]) {
355
+ if (RCTPushyIsHttpsDowngrade([NSURL URLWithString:self.urlString], request.URL)) {
352
356
  [self failWithDescription:[NSString stringWithFormat:
353
357
  @"https download redirected to plaintext http: %@", request.URL.absoluteString]
354
358
  code:-1];
@@ -523,19 +527,18 @@ didReceiveResponse:(NSURLResponse *)response
523
527
  [dataTask cancel];
524
528
  return;
525
529
  }
526
- if (self.expectedTotal <= 0
527
- && self.receivedBytes + (long long)data.length > self.freeSpaceReservedUntil) {
528
- // Unknown/encoded length: the response-time check could only
529
- // reserve the margin, so probe before any write that would run past
530
- // the bytes reserved so far, each probe reserving at least the next
531
- // PROBE bytes (more when one callback carries a larger buffer) — no
532
- // write can ever eat into the margin. The archive cap alone (512
533
- // MiB) is far more than the margin protects. The partial stays: a
534
- // later attempt may find the space.
535
- long long reserve = MAX(pushy::archive_limits::kUnknownLengthFreeSpaceProbeBytes,
536
- (long long)data.length);
537
- self.freeSpaceReservedUntil = self.receivedBytes + reserve;
538
- NSString *shortfall = RCTPushyFreeSpaceShortfall(self.savePath, reserve);
530
+ // Unknown/encoded length: the response-time check could only reserve the
531
+ // margin, so the disk is probed before any write that would run past the
532
+ // bytes reserved so far (archive_limits::PlanUnknownLengthFreeSpaceProbe)
533
+ // — no write can ever eat into the margin. The partial stays: a later
534
+ // attempt may find the space.
535
+ pushy::archive_limits::FreeSpaceProbe plan =
536
+ pushy::archive_limits::PlanUnknownLengthFreeSpaceProbe(
537
+ self.expectedTotal, self.receivedBytes, (long long)data.length,
538
+ self.freeSpaceReservedUntil);
539
+ self.freeSpaceReservedUntil = plan.reserved_until;
540
+ if (plan.probe) {
541
+ NSString *shortfall = RCTPushyFreeSpaceShortfall(self.savePath, plan.reserve);
539
542
  if (shortfall != nil) {
540
543
  [self failWithDescription:shortfall code:-1];
541
544
  [dataTask cancel];
@@ -0,0 +1,61 @@
1
+ // Version directories and the completion record: where an install lands and
2
+ // how it is recorded as complete. The record itself (format, atomic write,
3
+ // presence and activation checks) is the shared core's
4
+ // (cpp/patch_core/install_record.h); switchVersion:error: calls its
5
+ // activation check directly.
6
+ #import "RCTPushyInternal.h"
7
+ #include "../../cpp/patch_core/error_codes.h"
8
+ #include "../../cpp/patch_core/install_record.h"
9
+
10
+ PUSHY_HIDDEN NSString *const BUNDLE_FILE_NAME = @"index.bundlejs";
11
+
12
+ PUSHY_HIDDEN NSString *PushyVersionDir(NSString *hash) {
13
+ return [[RCTPushy downloadDir] stringByAppendingPathComponent:hash];
14
+ }
15
+
16
+ // Two-phase install: everything lands in <hash>.staging first.
17
+ PUSHY_HIDDEN NSString *PushyStagingDir(NSString *hash) {
18
+ return [PushyVersionDir(hash) stringByAppendingString:@(pushy::install_record::kStagingSuffix)];
19
+ }
20
+
21
+ // Presence check (launch / dedup paths, no digest work): bundle present and
22
+ // the record is the legacy marker or names this version.
23
+ PUSHY_HIDDEN BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash) {
24
+ return pushy::install_record::IsComplete(PushyToStdString(versionDir), PushyToStdString(hash),
25
+ PushyToStdString(BUNDLE_FILE_NAME));
26
+ }
27
+
28
+ // The record's bundleSha256 (crash attribution); @"" when there is none.
29
+ PUSHY_HIDDEN NSString *PushyRecordedBundleSha256(NSString *versionDir) {
30
+ return PushyFromStdString(
31
+ pushy::install_record::RecordedBundleSha256(PushyToStdString(versionDir))) ?: @"";
32
+ }
33
+
34
+ // Second phase of an install: seals <hash>.staging with the completion
35
+ // record (the final bundle's digest is mandatory: a version whose bundle
36
+ // could not be hashed is never recorded as installed) and renames it over
37
+ // <hash> in one atomic step. nil on success. Runs on the file queue. The
38
+ // caller drops the staging directory on failure.
39
+ PUSHY_HIDDEN NSError *PushyPromoteStagingDir(NSString *hash, NSString *artifactSha256) {
40
+ std::string staging = PushyToStdString(PushyStagingDir(hash));
41
+ pushy::install_record::SealResult sealed = pushy::install_record::Seal(
42
+ staging, PushyToStdString(hash), PushyToStdString(BUNDLE_FILE_NAME),
43
+ PushyToStdString(artifactSha256), std::string());
44
+ if (!sealed.status.ok) {
45
+ return PushyErrorWithCode(sealed.file_failure ? pushy::error_codes::kFileOperationFailed
46
+ : pushy::error_codes::kDeltaFailed,
47
+ PushyFromStdString(sealed.status.message));
48
+ }
49
+ NSString *versionDir = PushyVersionDir(hash);
50
+ NSFileManager *fileManager = [NSFileManager defaultManager];
51
+ if ([fileManager fileExistsAtPath:versionDir]) {
52
+ [fileManager removeItemAtPath:versionDir error:nil];
53
+ }
54
+ pushy::install_record::Status promoted =
55
+ pushy::install_record::Promote(staging, PushyToStdString(versionDir));
56
+ if (!promoted.ok) {
57
+ return PushyErrorWithCode(pushy::error_codes::kFileOperationFailed,
58
+ PushyFromStdString(promoted.message));
59
+ }
60
+ return nil;
61
+ }