@hot-updater/react-native 0.30.12 → 0.31.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.
Files changed (119) hide show
  1. package/HotUpdater.podspec +1 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
  4. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
  6. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
  7. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
  8. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
  9. package/android/src/newarch/HotUpdater.kt +14 -2
  10. package/android/src/newarch/HotUpdaterModule.kt +112 -1
  11. package/android/src/oldarch/HotUpdater.kt +14 -2
  12. package/android/src/oldarch/HotUpdaterModule.kt +107 -3
  13. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
  14. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
  15. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
  16. package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
  17. package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
  18. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
  19. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
  20. package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
  21. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
  22. package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
  23. package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
  24. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
  25. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
  26. package/lib/commonjs/DefaultResolver.js +6 -2
  27. package/lib/commonjs/DefaultResolver.js.map +1 -1
  28. package/lib/commonjs/DefaultResolver.spec.js +79 -3
  29. package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
  30. package/lib/commonjs/checkForUpdate.js +6 -0
  31. package/lib/commonjs/checkForUpdate.js.map +1 -1
  32. package/lib/commonjs/index.js +53 -18
  33. package/lib/commonjs/index.js.map +1 -1
  34. package/lib/commonjs/index.spec.js +117 -1
  35. package/lib/commonjs/index.spec.js.map +1 -1
  36. package/lib/commonjs/native.js +40 -16
  37. package/lib/commonjs/native.js.map +1 -1
  38. package/lib/commonjs/native.spec.js +119 -3
  39. package/lib/commonjs/native.spec.js.map +1 -1
  40. package/lib/commonjs/sdkVersion.js +8 -0
  41. package/lib/commonjs/sdkVersion.js.map +1 -0
  42. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  43. package/lib/commonjs/store.js +72 -5
  44. package/lib/commonjs/store.js.map +1 -1
  45. package/lib/commonjs/store.spec.js +265 -8
  46. package/lib/commonjs/store.spec.js.map +1 -1
  47. package/lib/commonjs/wrap.js +19 -5
  48. package/lib/commonjs/wrap.js.map +1 -1
  49. package/lib/commonjs/wrap.spec.js +74 -0
  50. package/lib/commonjs/wrap.spec.js.map +1 -1
  51. package/lib/module/DefaultResolver.js +6 -2
  52. package/lib/module/DefaultResolver.js.map +1 -1
  53. package/lib/module/DefaultResolver.spec.js +78 -3
  54. package/lib/module/DefaultResolver.spec.js.map +1 -1
  55. package/lib/module/checkForUpdate.js +6 -0
  56. package/lib/module/checkForUpdate.js.map +1 -1
  57. package/lib/module/index.js +53 -18
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/index.spec.js +117 -1
  60. package/lib/module/index.spec.js.map +1 -1
  61. package/lib/module/native.js +40 -16
  62. package/lib/module/native.js.map +1 -1
  63. package/lib/module/native.spec.js +119 -3
  64. package/lib/module/native.spec.js.map +1 -1
  65. package/lib/module/sdkVersion.js +4 -0
  66. package/lib/module/sdkVersion.js.map +1 -0
  67. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  68. package/lib/module/store.js +72 -5
  69. package/lib/module/store.js.map +1 -1
  70. package/lib/module/store.spec.js +265 -8
  71. package/lib/module/store.spec.js.map +1 -1
  72. package/lib/module/wrap.js +19 -5
  73. package/lib/module/wrap.js.map +1 -1
  74. package/lib/module/wrap.spec.js +74 -0
  75. package/lib/module/wrap.spec.js.map +1 -1
  76. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
  78. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/index.d.ts +36 -8
  80. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/native.d.ts +56 -3
  82. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
  84. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
  86. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/store.d.ts +5 -0
  88. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/wrap.d.ts +43 -29
  90. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  91. package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
  92. package/lib/typescript/module/checkForUpdate.d.ts +2 -2
  93. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  94. package/lib/typescript/module/index.d.ts +36 -8
  95. package/lib/typescript/module/index.d.ts.map +1 -1
  96. package/lib/typescript/module/native.d.ts +56 -3
  97. package/lib/typescript/module/native.d.ts.map +1 -1
  98. package/lib/typescript/module/sdkVersion.d.ts +2 -0
  99. package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
  100. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
  101. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  102. package/lib/typescript/module/store.d.ts +5 -0
  103. package/lib/typescript/module/store.d.ts.map +1 -1
  104. package/lib/typescript/module/wrap.d.ts +43 -29
  105. package/lib/typescript/module/wrap.d.ts.map +1 -1
  106. package/package.json +20 -10
  107. package/src/DefaultResolver.spec.ts +103 -3
  108. package/src/DefaultResolver.ts +9 -2
  109. package/src/checkForUpdate.ts +9 -2
  110. package/src/index.spec.ts +149 -1
  111. package/src/index.ts +99 -40
  112. package/src/native.spec.ts +139 -1
  113. package/src/native.ts +145 -31
  114. package/src/sdkVersion.ts +1 -0
  115. package/src/specs/NativeHotUpdater.ts +28 -0
  116. package/src/store.spec.ts +324 -9
  117. package/src/store.ts +133 -7
  118. package/src/wrap.spec.ts +91 -0
  119. package/src/wrap.tsx +79 -42
@@ -0,0 +1,46 @@
1
+ package com.hotupdater
2
+
3
+ import org.junit.Assert.assertArrayEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertThrows
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Rule
8
+ import org.junit.Test
9
+ import org.junit.rules.TemporaryFolder
10
+ import java.util.Base64
11
+
12
+ class BsdiffPatchTest {
13
+ @get:Rule
14
+ val temporaryFolder = TemporaryFolder()
15
+
16
+ @Test
17
+ fun `applies bsdiff patch and verifies patched output hash`() {
18
+ val base = "console.log(\"base bundle\");\n".toByteArray()
19
+ val expected = "console.log(\"patched bundle\");\n".toByteArray()
20
+ val patch = Base64.getDecoder().decode(BSDIFF_PATCH_FIXTURE_BASE64)
21
+ val baseFile = temporaryFolder.newFile("base.bundle").apply { writeBytes(base) }
22
+ val patchFile = temporaryFolder.newFile("patch.bsdiff").apply { writeBytes(patch) }
23
+ val outputFile = temporaryFolder.newFile("output.bundle")
24
+
25
+ BsdiffPatch.apply(baseFile, patchFile, outputFile)
26
+
27
+ assertArrayEquals(expected, outputFile.readBytes())
28
+ assertTrue(HashUtils.verifyHash(outputFile, HashUtils.calculateSHA256(outputFile)))
29
+ assertFalse(HashUtils.verifyHash(outputFile, HashUtils.calculateSHA256(baseFile)))
30
+ }
31
+
32
+ @Test
33
+ fun `rejects invalid bsdiff patch`() {
34
+ val base = "console.log(\"base bundle\");\n".toByteArray()
35
+ val invalidPatch = "not-a-bsdiff-patch-with-header".toByteArray()
36
+
37
+ assertThrows(IllegalArgumentException::class.java) {
38
+ BsdiffPatch.apply(base, invalidPatch)
39
+ }
40
+ }
41
+
42
+ companion object {
43
+ private const val BSDIFF_PATCH_FIXTURE_BASE64 =
44
+ "RU5EU0xFWS9CU0RJRkY0Mx8AAAAAAAAAQlpoOTFBWSZTWb12MIEAAAB5gEQYAADQYQAIPsXOACAAIo0A0NAaNCgAGgZMgHAtYscVxxRtTt4nmaj70g4gQSF5+T4u5IpwoSF67GEC"
45
+ }
46
+ }
@@ -231,6 +231,28 @@ class BundleFileStorageServiceTest {
231
231
  assertEquals(stagingDir.name, service.getBundleId())
232
232
  }
233
233
 
234
+ @Test
235
+ fun `manifest driven install is disabled before first OTA`() {
236
+ val rootDir = temporaryFolder.newFolder("first-ota-manifest-disabled")
237
+ val service = createService(rootDir)
238
+
239
+ assertFalse(invokeCanUseManifestDrivenInstall(service))
240
+ }
241
+
242
+ @Test
243
+ fun `manifest driven install is enabled for active OTA bundle with manifest`() {
244
+ val rootDir = temporaryFolder.newFolder("active-ota-manifest-enabled")
245
+ val preferences = InMemoryPreferencesService()
246
+ val service = createService(rootDir, preferences)
247
+ val activeDir = createBundleDir(rootDir, "active-bundle")
248
+ val activeBundleFile = writeFile(activeDir, "index.android.bundle")
249
+ writeManifest(activeDir, listOf("index.android.bundle"))
250
+
251
+ preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
252
+
253
+ assertTrue(invokeCanUseManifestDrivenInstall(service))
254
+ }
255
+
234
256
  private fun createService(
235
257
  rootDir: File,
236
258
  preferences: InMemoryPreferencesService = InMemoryPreferencesService(),
@@ -306,6 +328,12 @@ class BundleFileStorageServiceTest {
306
328
  return method.invoke(service, bundleDir) as File?
307
329
  }
308
330
 
331
+ private fun invokeCanUseManifestDrivenInstall(service: BundleFileStorageService): Boolean {
332
+ val method = BundleFileStorageService::class.java.getDeclaredMethod("canUseManifestDrivenInstall")
333
+ method.isAccessible = true
334
+ return method.invoke(service) as Boolean
335
+ }
336
+
309
337
  private fun assertResolvedBundlePath(
310
338
  service: BundleFileStorageService,
311
339
  bundleDir: File,
@@ -368,7 +396,7 @@ class BundleFileStorageServiceTest {
368
396
  fileUrl: URL,
369
397
  destination: File,
370
398
  fileSizeCallback: ((Long) -> Unit)?,
371
- progressCallback: (Double) -> Unit,
399
+ progressCallback: (DownloadProgress) -> Unit,
372
400
  ): DownloadResult = error("downloadFile should not be called in these tests")
373
401
  }
374
402
 
@@ -179,7 +179,10 @@ class HotUpdaterImplTest {
179
179
  bundleId: String,
180
180
  fileUrl: String?,
181
181
  fileHash: String?,
182
- progressCallback: (Double) -> Unit,
182
+ manifestUrl: String?,
183
+ manifestFileHash: String?,
184
+ changedAssets: Map<String, ChangedAssetDescriptor>?,
185
+ progressCallback: (UpdateProgressPayload) -> Unit,
183
186
  ) = Unit
184
187
 
185
188
  override fun markLaunchCompleted(currentBundleId: String?) = Unit
@@ -0,0 +1,16 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ FOUNDATION_EXPORT BOOL HotUpdaterApplyBsdiffPatch(NSString *patchPath,
6
+ NSString *basePath,
7
+ NSString *outputPath);
8
+
9
+ @interface BsdiffPatchBridge : NSObject
10
+ + (BOOL)applyPatchAtPath:(NSString *)patchPath
11
+ toBaseAtPath:(NSString *)basePath
12
+ outputAtPath:(NSString *)outputPath
13
+ error:(NSError * _Nullable * _Nullable)error;
14
+ @end
15
+
16
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,343 @@
1
+ #import "BsdiffPatchBridge.h"
2
+
3
+ #include <algorithm>
4
+ #include <bzlib.h>
5
+ #include <cerrno>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+ #include <fcntl.h>
9
+ #include <sys/stat.h>
10
+ #include <unistd.h>
11
+ #include <vector>
12
+
13
+ namespace {
14
+ constexpr NSUInteger kBzipChunkSize = 64 * 1024;
15
+
16
+ NSError *MakePatchError(NSString *message) {
17
+ return [NSError errorWithDomain:@"HotUpdater.Bsdiff"
18
+ code:1
19
+ userInfo:@{NSLocalizedDescriptionKey: message}];
20
+ }
21
+
22
+ NSError *MakeSystemError(NSString *message) {
23
+ return MakePatchError(
24
+ [NSString stringWithFormat:@"%@: %s", message, strerror(errno)]);
25
+ }
26
+
27
+ int64_t ReadOfft(const uint8_t *bytes) {
28
+ uint64_t value = 0;
29
+ for (NSUInteger index = 0; index < 8; index += 1) {
30
+ value |= (static_cast<uint64_t>(bytes[index]) << (index * 8));
31
+ }
32
+
33
+ if ((value & (1ULL << 63)) == 0) {
34
+ return static_cast<int64_t>(value);
35
+ }
36
+
37
+ return -static_cast<int64_t>(value & ~(1ULL << 63));
38
+ }
39
+
40
+ bool CheckedAddInt64(int64_t left, int64_t right, int64_t *result) {
41
+ if ((right > 0 && left > INT64_MAX - right) ||
42
+ (right < 0 && left < INT64_MIN - right)) {
43
+ return false;
44
+ }
45
+ *result = left + right;
46
+ return true;
47
+ }
48
+
49
+ bool ReadBzipExactly(BZFILE *bz2,
50
+ void *buffer,
51
+ size_t length,
52
+ NSError **error) {
53
+ auto *bytes = reinterpret_cast<uint8_t *>(buffer);
54
+ size_t offset = 0;
55
+
56
+ while (offset < length) {
57
+ int bz2err = BZ_OK;
58
+ int request = static_cast<int>(
59
+ std::min<size_t>(length - offset, static_cast<size_t>(INT_MAX)));
60
+ int read = BZ2_bzRead(&bz2err, bz2, bytes + offset, request);
61
+ if (bz2err != BZ_OK && bz2err != BZ_STREAM_END) {
62
+ if (error) {
63
+ *error = MakePatchError(@"Failed to read bzip stream");
64
+ }
65
+ return false;
66
+ }
67
+
68
+ if (read <= 0) {
69
+ if (error) {
70
+ *error = MakePatchError(@"Unexpected end of ENDSLEY/BSDIFF43 stream");
71
+ }
72
+ return false;
73
+ }
74
+
75
+ offset += static_cast<size_t>(read);
76
+ if (bz2err == BZ_STREAM_END && offset < length) {
77
+ if (error) {
78
+ *error = MakePatchError(@"Unexpected end of ENDSLEY/BSDIFF43 stream");
79
+ }
80
+ return false;
81
+ }
82
+ }
83
+
84
+ return true;
85
+ }
86
+
87
+ bool WriteAll(FILE *file, const uint8_t *buffer, size_t length, NSError **error) {
88
+ size_t written = 0;
89
+ while (written < length) {
90
+ size_t result = fwrite(buffer + written, 1, length - written, file);
91
+ if (result == 0) {
92
+ if (error) {
93
+ *error = MakeSystemError(@"Failed to write patch output");
94
+ }
95
+ return false;
96
+ }
97
+ written += result;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ bool ReadBaseAt(int baseFd,
103
+ off_t baseSize,
104
+ int64_t offset,
105
+ uint8_t *target,
106
+ size_t count,
107
+ NSError **error) {
108
+ memset(target, 0, count);
109
+ if (count == 0 || offset >= baseSize ||
110
+ offset > INT64_MAX - static_cast<int64_t>(count) ||
111
+ offset + static_cast<int64_t>(count) <= 0) {
112
+ return true;
113
+ }
114
+
115
+ int64_t validStart = std::max<int64_t>(offset, 0);
116
+ int64_t validEnd = std::min<int64_t>(
117
+ offset + static_cast<int64_t>(count),
118
+ static_cast<int64_t>(baseSize));
119
+ if (validStart >= validEnd) {
120
+ return true;
121
+ }
122
+
123
+ size_t targetOffset = static_cast<size_t>(validStart - offset);
124
+ size_t validLength = static_cast<size_t>(validEnd - validStart);
125
+ size_t totalRead = 0;
126
+ while (totalRead < validLength) {
127
+ ssize_t read = pread(
128
+ baseFd,
129
+ target + targetOffset + totalRead,
130
+ validLength - totalRead,
131
+ static_cast<off_t>(validStart + static_cast<int64_t>(totalRead)));
132
+ if (read <= 0) {
133
+ if (error) {
134
+ *error = MakeSystemError(@"Failed to read base file");
135
+ }
136
+ return false;
137
+ }
138
+ totalRead += static_cast<size_t>(read);
139
+ }
140
+ return true;
141
+ }
142
+ } // namespace
143
+
144
+ extern "C" BOOL HotUpdaterApplyBsdiffPatch(NSString *patchPath,
145
+ NSString *basePath,
146
+ NSString *outputPath) {
147
+ NSError *patchError = nil;
148
+ BOOL applied = [BsdiffPatchBridge applyPatchAtPath:patchPath
149
+ toBaseAtPath:basePath
150
+ outputAtPath:outputPath
151
+ error:&patchError];
152
+ if (!applied && patchError != nil) {
153
+ NSLog(@"[BundleStorage] Failed to apply bsdiff patch: %@",
154
+ patchError.localizedDescription);
155
+ }
156
+ return applied;
157
+ }
158
+
159
+ @implementation BsdiffPatchBridge
160
+
161
+ + (BOOL)applyPatchAtPath:(NSString *)patchPath
162
+ toBaseAtPath:(NSString *)basePath
163
+ outputAtPath:(NSString *)outputPath
164
+ error:(NSError * _Nullable __autoreleasing *)error {
165
+ FILE *patchFile = nullptr;
166
+ FILE *outputFile = nullptr;
167
+ BZFILE *bz2 = nullptr;
168
+ int baseFd = -1;
169
+ BOOL success = NO;
170
+ NSString *tempOutputPath = [outputPath stringByAppendingString:@".tmp"];
171
+
172
+ auto cleanup = [&]() {
173
+ if (bz2 != nullptr) {
174
+ int bz2err = BZ_OK;
175
+ BZ2_bzReadClose(&bz2err, bz2);
176
+ bz2 = nullptr;
177
+ }
178
+ if (patchFile != nullptr) {
179
+ fclose(patchFile);
180
+ patchFile = nullptr;
181
+ }
182
+ if (baseFd >= 0) {
183
+ close(baseFd);
184
+ baseFd = -1;
185
+ }
186
+ if (outputFile != nullptr) {
187
+ fclose(outputFile);
188
+ outputFile = nullptr;
189
+ }
190
+ if (!success) {
191
+ unlink(tempOutputPath.fileSystemRepresentation);
192
+ }
193
+ };
194
+
195
+ auto fail = [&](NSError *patchError) {
196
+ if (error) {
197
+ *error = patchError;
198
+ }
199
+ cleanup();
200
+ return NO;
201
+ };
202
+
203
+ auto failMessage = [&](NSString *message) {
204
+ return fail(MakePatchError(message));
205
+ };
206
+
207
+ patchFile = fopen(patchPath.fileSystemRepresentation, "rb");
208
+ if (patchFile == nullptr) {
209
+ return fail(MakeSystemError(@"Failed to open patch file"));
210
+ }
211
+
212
+ uint8_t header[24];
213
+ if (fread(header, 1, sizeof(header), patchFile) != sizeof(header)) {
214
+ return failMessage(@"Invalid ENDSLEY/BSDIFF43 header");
215
+ }
216
+
217
+ if (memcmp(header, "ENDSLEY/BSDIFF43", 16) != 0) {
218
+ return failMessage(@"Invalid ENDSLEY/BSDIFF43 header");
219
+ }
220
+
221
+ int64_t newSize = ReadOfft(header + 16);
222
+
223
+ if (newSize < 0) {
224
+ return failMessage(@"Negative ENDSLEY/BSDIFF43 target size");
225
+ }
226
+
227
+ if (static_cast<uint64_t>(newSize) > NSUIntegerMax) {
228
+ return failMessage(@"Target file is too large");
229
+ }
230
+
231
+ baseFd = open(basePath.fileSystemRepresentation, O_RDONLY);
232
+ if (baseFd < 0) {
233
+ return fail(MakeSystemError(@"Failed to open base file"));
234
+ }
235
+
236
+ struct stat baseStat {};
237
+ if (fstat(baseFd, &baseStat) != 0) {
238
+ return fail(MakeSystemError(@"Failed to stat base file"));
239
+ }
240
+
241
+ unlink(tempOutputPath.fileSystemRepresentation);
242
+ outputFile = fopen(tempOutputPath.fileSystemRepresentation, "wb");
243
+ if (outputFile == nullptr) {
244
+ return fail(MakeSystemError(@"Failed to open patch output file"));
245
+ }
246
+
247
+ int bz2err = BZ_OK;
248
+ bz2 = BZ2_bzReadOpen(&bz2err, patchFile, 0, 0, nullptr, 0);
249
+ if (bz2 == nullptr || bz2err != BZ_OK) {
250
+ return failMessage(@"Failed to initialize bzip stream");
251
+ }
252
+
253
+ std::vector<uint8_t> diffBuffer(kBzipChunkSize);
254
+ std::vector<uint8_t> baseBuffer(kBzipChunkSize);
255
+ std::vector<uint8_t> outputBuffer(kBzipChunkSize);
256
+ int64_t outputPos = 0;
257
+ int64_t oldPos = 0;
258
+
259
+ while (outputPos < newSize) {
260
+ uint8_t controlBytes[24];
261
+ if (!ReadBzipExactly(bz2, controlBytes, sizeof(controlBytes), error)) {
262
+ cleanup();
263
+ return NO;
264
+ }
265
+
266
+ int64_t addLen = ReadOfft(controlBytes);
267
+ int64_t copyLen = ReadOfft(controlBytes + 8);
268
+ int64_t seekLen = ReadOfft(controlBytes + 16);
269
+
270
+ if (addLen < 0 || copyLen < 0) {
271
+ return failMessage(@"Negative add/copy length in control block");
272
+ }
273
+
274
+ int64_t remainingOutput = newSize - outputPos;
275
+
276
+ if (addLen > remainingOutput || copyLen > remainingOutput - addLen) {
277
+ return failMessage(@"ENDSLEY/BSDIFF43 stream is truncated");
278
+ }
279
+
280
+ size_t remainingAdd = static_cast<size_t>(addLen);
281
+ while (remainingAdd > 0) {
282
+ size_t chunkSize = std::min<size_t>(remainingAdd, kBzipChunkSize);
283
+ if (!ReadBzipExactly(bz2, diffBuffer.data(), chunkSize, error) ||
284
+ !ReadBaseAt(baseFd, baseStat.st_size, oldPos, baseBuffer.data(), chunkSize, error)) {
285
+ cleanup();
286
+ return NO;
287
+ }
288
+
289
+ for (size_t index = 0; index < chunkSize; index += 1) {
290
+ outputBuffer[index] = static_cast<uint8_t>(
291
+ (diffBuffer[index] + baseBuffer[index]) & 0xFF);
292
+ }
293
+
294
+ if (!WriteAll(outputFile, outputBuffer.data(), chunkSize, error)) {
295
+ cleanup();
296
+ return NO;
297
+ }
298
+
299
+ int64_t nextOldPos = 0;
300
+ if (!CheckedAddInt64(oldPos, static_cast<int64_t>(chunkSize), &nextOldPos)) {
301
+ return failMessage(@"Old file seek overflow");
302
+ }
303
+ oldPos = nextOldPos;
304
+ outputPos += static_cast<int64_t>(chunkSize);
305
+ remainingAdd -= chunkSize;
306
+ }
307
+
308
+ size_t remainingCopy = static_cast<size_t>(copyLen);
309
+ while (remainingCopy > 0) {
310
+ size_t chunkSize = std::min<size_t>(remainingCopy, kBzipChunkSize);
311
+ if (!ReadBzipExactly(bz2, diffBuffer.data(), chunkSize, error) ||
312
+ !WriteAll(outputFile, diffBuffer.data(), chunkSize, error)) {
313
+ cleanup();
314
+ return NO;
315
+ }
316
+ outputPos += static_cast<int64_t>(chunkSize);
317
+ remainingCopy -= chunkSize;
318
+ }
319
+
320
+ int64_t nextOldPos = 0;
321
+ if (!CheckedAddInt64(oldPos, seekLen, &nextOldPos)) {
322
+ return failMessage(@"Old file seek overflow");
323
+ }
324
+ oldPos = nextOldPos;
325
+ }
326
+
327
+ if (fclose(outputFile) != 0) {
328
+ outputFile = nullptr;
329
+ return fail(MakeSystemError(@"Failed to close patch output file"));
330
+ }
331
+ outputFile = nullptr;
332
+
333
+ if (rename(tempOutputPath.fileSystemRepresentation,
334
+ outputPath.fileSystemRepresentation) != 0) {
335
+ return fail(MakeSystemError(@"Failed to move patch output file"));
336
+ }
337
+
338
+ success = YES;
339
+ cleanup();
340
+ return YES;
341
+ }
342
+
343
+ @end