react-native-update 10.44.0 → 10.46.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.
- package/android/jni/Android.mk +0 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +10 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +19 -4
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +15 -15
- package/cpp/patch_core/error_codes.h +43 -0
- package/cpp/patch_core/patch_core.cpp +16 -1
- package/cpp/patch_core/patch_core.h +6 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +77 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +1 -93
- package/harmony/pushy/src/main/ets/DownloadTask.ts +13 -10
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +223 -132
- package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
- package/package.json +7 -3
- package/src/client.ts +151 -53
- package/src/context.ts +21 -7
- package/src/core.ts +8 -2
- package/src/endpoint.ts +4 -2
- package/src/error.ts +68 -0
- package/src/index.ts +10 -1
- package/src/locales/en.ts +5 -1
- package/src/locales/zh.ts +3 -1
- package/src/provider.tsx +101 -54
- package/src/type.ts +6 -0
- package/src/utils.ts +56 -16
- package/android/jni/DownloadTask.c +0 -56
- package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
package/android/jni/Android.mk
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -33,7 +33,16 @@ class DownloadTask implements Runnable {
|
|
|
33
33
|
// events on percentage change, so throttle by bytes to avoid flooding the
|
|
34
34
|
// bridge (e.g. a 20MB chunked download would otherwise emit ~5000 events).
|
|
35
35
|
private static final long PROGRESS_BYTES_THRESHOLD = 256 * 1024;
|
|
36
|
-
|
|
36
|
+
// Explicit timeouts: the default client has no call timeout, so a
|
|
37
|
+
// slow-dripping connection could occupy the single-threaded download
|
|
38
|
+
// executor indefinitely and starve queued tasks. The call timeout is a
|
|
39
|
+
// generous upper bound sized for large full-package downloads.
|
|
40
|
+
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
|
|
41
|
+
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
|
|
42
|
+
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
|
43
|
+
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
|
44
|
+
.callTimeout(10, java.util.concurrent.TimeUnit.MINUTES)
|
|
45
|
+
.build();
|
|
37
46
|
|
|
38
47
|
static {
|
|
39
48
|
NativeUpdateCore.ensureLoaded();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable, machine-readable error codes used as the promise rejection code so
|
|
5
|
+
* the JS layer and user loggers can aggregate errors across platforms.
|
|
6
|
+
*
|
|
7
|
+
* MUST stay in sync with cpp/patch_core/error_codes.h (the single source of
|
|
8
|
+
* truth) and src/error.ts (UpdateErrorCode). Messages are free-form; only the
|
|
9
|
+
* codes are part of the contract.
|
|
10
|
+
*/
|
|
11
|
+
final class ErrorCodes {
|
|
12
|
+
static final String INVALID_OPTIONS = "INVALID_OPTIONS";
|
|
13
|
+
static final String DOWNLOAD_FAILED = "DOWNLOAD_FAILED";
|
|
14
|
+
static final String PATCH_FAILED = "PATCH_FAILED";
|
|
15
|
+
static final String FILE_OPERATION_FAILED = "FILE_OPERATION_FAILED";
|
|
16
|
+
static final String SWITCH_VERSION_FAILED = "SWITCH_VERSION_FAILED";
|
|
17
|
+
static final String MARK_SUCCESS_FAILED = "MARK_SUCCESS_FAILED";
|
|
18
|
+
static final String RESTART_FAILED = "RESTART_FAILED";
|
|
19
|
+
static final String INVALID_HASH_INFO = "INVALID_HASH_INFO";
|
|
20
|
+
static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM";
|
|
21
|
+
|
|
22
|
+
private ErrorCodes() {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -43,6 +43,7 @@ final class StateSerialRunner {
|
|
|
43
43
|
|
|
44
44
|
static void run(
|
|
45
45
|
@Nullable final Promise promise,
|
|
46
|
+
final String errorCode,
|
|
46
47
|
final String operationName,
|
|
47
48
|
final Operation operation
|
|
48
49
|
) {
|
|
@@ -53,7 +54,7 @@ final class StateSerialRunner {
|
|
|
53
54
|
operation.run();
|
|
54
55
|
} catch (Throwable error) {
|
|
55
56
|
if (promise != null) {
|
|
56
|
-
promise.reject(operationName + " failed", error);
|
|
57
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
57
58
|
} else {
|
|
58
59
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
59
60
|
}
|
|
@@ -15,6 +15,7 @@ final class UiThreadRunner {
|
|
|
15
15
|
|
|
16
16
|
static void run(
|
|
17
17
|
@Nullable final Promise promise,
|
|
18
|
+
final String errorCode,
|
|
18
19
|
final String operationName,
|
|
19
20
|
final Operation operation
|
|
20
21
|
) {
|
|
@@ -25,7 +26,7 @@ final class UiThreadRunner {
|
|
|
25
26
|
operation.run();
|
|
26
27
|
} catch (Throwable error) {
|
|
27
28
|
if (promise != null) {
|
|
28
|
-
promise.reject(operationName + " failed", error);
|
|
29
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
29
30
|
} else {
|
|
30
31
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
31
32
|
}
|
|
@@ -207,14 +207,16 @@ public class UpdateContext {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
private void persistEditor(SharedPreferences.Editor editor, String reason) {
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// A lost state write can mean a missed rollback or a version switch
|
|
211
|
+
// that silently never happens, so this must be visible in release too.
|
|
212
|
+
if (!editor.commit()) {
|
|
213
|
+
Log.e(TAG, "Failed to persist update state for " + reason);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
public void switchVersion(String hash) {
|
|
216
218
|
if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
|
|
217
|
-
throw new
|
|
219
|
+
throw new IllegalStateException("Bundle version " + hash + " not found.");
|
|
218
220
|
}
|
|
219
221
|
StateCoreResult currentState = getStateSnapshot();
|
|
220
222
|
StateCoreResult nextState = runStateCore(
|
|
@@ -367,6 +369,13 @@ public class UpdateContext {
|
|
|
367
369
|
ignoreRollback,
|
|
368
370
|
true
|
|
369
371
|
);
|
|
372
|
+
if (launchState.didRollback) {
|
|
373
|
+
// The crash-protection rollback: the new version never called
|
|
374
|
+
// markSuccess. Keep this visible in release logs.
|
|
375
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
376
|
+
+ " was not marked as successful, rolling back to "
|
|
377
|
+
+ launchState.currentVersion);
|
|
378
|
+
}
|
|
370
379
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
371
380
|
SharedPreferences.Editor editor = sp.edit();
|
|
372
381
|
applyState(editor, launchState);
|
|
@@ -385,7 +394,11 @@ public class UpdateContext {
|
|
|
385
394
|
return defaultAssetsUrl;
|
|
386
395
|
}
|
|
387
396
|
|
|
388
|
-
|
|
397
|
+
// Guard the rollback chain against cycles: a corrupted state returning
|
|
398
|
+
// an already-visited version would otherwise spin this loop forever on
|
|
399
|
+
// the main thread.
|
|
400
|
+
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
|
|
401
|
+
while (currentVersion != null && visitedVersions.add(currentVersion)) {
|
|
389
402
|
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
|
|
390
403
|
if (!bundleFile.exists()) {
|
|
391
404
|
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
|
|
@@ -407,6 +420,8 @@ public class UpdateContext {
|
|
|
407
420
|
false,
|
|
408
421
|
false
|
|
409
422
|
);
|
|
423
|
+
Log.e(TAG, "Rolling back version " + currentState.currentVersion
|
|
424
|
+
+ " to " + nextState.currentVersion);
|
|
410
425
|
SharedPreferences.Editor editor = sp.edit();
|
|
411
426
|
applyState(editor, nextState);
|
|
412
427
|
persistEditor(editor, "rollback");
|
|
@@ -24,7 +24,7 @@ final class UpdateEventEmitter {
|
|
|
24
24
|
|
|
25
25
|
static void sendEvent(String eventName, WritableMap params) {
|
|
26
26
|
ReactApplicationContext reactContext = getReactContext();
|
|
27
|
-
if (reactContext == null || !reactContext
|
|
27
|
+
if (reactContext == null || !hasActiveInstance(reactContext)) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,4 +32,16 @@ final class UpdateEventEmitter {
|
|
|
32
32
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
33
33
|
.emit(eventName, params);
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
@SuppressWarnings("deprecation")
|
|
37
|
+
private static boolean hasActiveInstance(ReactApplicationContext reactContext) {
|
|
38
|
+
try {
|
|
39
|
+
// hasActiveCatalystInstance() is always false in bridgeless mode, which
|
|
40
|
+
// silently drops every progress event on the new architecture.
|
|
41
|
+
return reactContext.hasActiveReactInstance();
|
|
42
|
+
} catch (NoSuchMethodError e) {
|
|
43
|
+
// RN < 0.68 has no hasActiveReactInstance(); fall back for old peers.
|
|
44
|
+
return reactContext.hasActiveCatalystInstance();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
}
|
|
@@ -41,7 +41,7 @@ public class UpdateModuleImpl {
|
|
|
41
41
|
|
|
42
42
|
@Override
|
|
43
43
|
public void onDownloadFailed(Throwable error) {
|
|
44
|
-
promise.reject(error);
|
|
44
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -64,7 +64,7 @@ public class UpdateModuleImpl {
|
|
|
64
64
|
|
|
65
65
|
@Override
|
|
66
66
|
public void onDownloadFailed(Throwable error) {
|
|
67
|
-
promise.reject(error);
|
|
67
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -84,7 +84,7 @@ public class UpdateModuleImpl {
|
|
|
84
84
|
|
|
85
85
|
@Override
|
|
86
86
|
public void onDownloadFailed(Throwable error) {
|
|
87
|
-
promise.reject(error);
|
|
87
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
@@ -107,11 +107,11 @@ public class UpdateModuleImpl {
|
|
|
107
107
|
|
|
108
108
|
@Override
|
|
109
109
|
public void onDownloadFailed(Throwable error) {
|
|
110
|
-
promise.reject(error);
|
|
110
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
} catch (Exception e) {
|
|
114
|
-
promise.reject("downloadPatchFromPpk failed: " + e.getMessage());
|
|
114
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "downloadPatchFromPpk failed: " + e.getMessage(), e);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -130,7 +130,7 @@ public class UpdateModuleImpl {
|
|
|
130
130
|
@Nullable final String hash,
|
|
131
131
|
final Promise promise
|
|
132
132
|
) {
|
|
133
|
-
UiThreadRunner.run(promise, "restartApp", new UiThreadRunner.Operation() {
|
|
133
|
+
UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
|
|
134
134
|
@Override
|
|
135
135
|
public void run() throws Throwable {
|
|
136
136
|
ReactReloadManager.restartApp(updateContext, reactContext, hash);
|
|
@@ -149,7 +149,7 @@ public class UpdateModuleImpl {
|
|
|
149
149
|
final Promise promise
|
|
150
150
|
) {
|
|
151
151
|
final String hash = options.getString("hash");
|
|
152
|
-
StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
152
|
+
StateSerialRunner.run(promise, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
153
153
|
@Override
|
|
154
154
|
public void run() {
|
|
155
155
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -160,7 +160,7 @@ public class UpdateModuleImpl {
|
|
|
160
160
|
|
|
161
161
|
public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
|
|
162
162
|
final String hash = options.getString("hash");
|
|
163
|
-
StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
163
|
+
StateSerialRunner.run(null, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
164
164
|
@Override
|
|
165
165
|
public void run() {
|
|
166
166
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -173,7 +173,7 @@ public class UpdateModuleImpl {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
|
|
176
|
-
StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
|
|
176
|
+
StateSerialRunner.run(promise, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
177
177
|
@Override
|
|
178
178
|
public void run() {
|
|
179
179
|
markSuccessInternal(updateContext);
|
|
@@ -183,7 +183,7 @@ public class UpdateModuleImpl {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
public static void markSuccess(final UpdateContext updateContext) {
|
|
186
|
-
StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
|
|
186
|
+
StateSerialRunner.run(null, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
187
187
|
@Override
|
|
188
188
|
public void run() {
|
|
189
189
|
markSuccessInternal(updateContext);
|
|
@@ -200,7 +200,7 @@ public class UpdateModuleImpl {
|
|
|
200
200
|
final String uuid,
|
|
201
201
|
final Promise promise
|
|
202
202
|
) {
|
|
203
|
-
StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
|
|
203
|
+
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
204
204
|
@Override
|
|
205
205
|
public void run() {
|
|
206
206
|
setUuidInternal(updateContext, uuid);
|
|
@@ -210,7 +210,7 @@ public class UpdateModuleImpl {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
public static void setUuid(final UpdateContext updateContext, final String uuid) {
|
|
213
|
-
StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
|
|
213
|
+
StateSerialRunner.run(null, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
214
214
|
@Override
|
|
215
215
|
public void run() {
|
|
216
216
|
setUuidInternal(updateContext, uuid);
|
|
@@ -235,7 +235,7 @@ public class UpdateModuleImpl {
|
|
|
235
235
|
final String info,
|
|
236
236
|
final Promise promise
|
|
237
237
|
) {
|
|
238
|
-
StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
238
|
+
StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
239
239
|
@Override
|
|
240
240
|
public void run() {
|
|
241
241
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -249,7 +249,7 @@ public class UpdateModuleImpl {
|
|
|
249
249
|
final String hash,
|
|
250
250
|
final String info
|
|
251
251
|
) {
|
|
252
|
-
StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
252
|
+
StateSerialRunner.run(null, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
253
253
|
@Override
|
|
254
254
|
public void run() {
|
|
255
255
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -264,7 +264,7 @@ public class UpdateModuleImpl {
|
|
|
264
264
|
) {
|
|
265
265
|
String value = updateContext.getKv("hash_" + hash);
|
|
266
266
|
if (!isValidHashInfo(value)) {
|
|
267
|
-
promise.reject("getLocalHashInfo failed: invalid json string");
|
|
267
|
+
promise.reject(ErrorCodes.INVALID_HASH_INFO, "getLocalHashInfo failed: invalid json string");
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#ifndef PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
2
|
+
#define PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
3
|
+
|
|
4
|
+
// Single source of truth for the stable, machine-readable error codes shared
|
|
5
|
+
// by every platform. Native modules reject promises with one of these codes
|
|
6
|
+
// so the JS layer (src/error.ts UpdateErrorCode) and user loggers can
|
|
7
|
+
// aggregate errors across platforms and locales.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors that cannot include this header MUST stay in sync by hand:
|
|
10
|
+
// - src/error.ts (UpdateErrorCode union, JS layer)
|
|
11
|
+
// - android/.../ErrorCodes.java (Java constants)
|
|
12
|
+
// iOS (RCTPushy.mm) includes this header directly.
|
|
13
|
+
//
|
|
14
|
+
// Human-readable messages are NOT part of this contract: they may differ per
|
|
15
|
+
// platform and locale. Only the codes are stable.
|
|
16
|
+
|
|
17
|
+
namespace pushy {
|
|
18
|
+
namespace error_codes {
|
|
19
|
+
|
|
20
|
+
// Method options missing or malformed (blank hash/url, wrong types).
|
|
21
|
+
constexpr const char* kInvalidOptions = "INVALID_OPTIONS";
|
|
22
|
+
// Native download failed (network error, bad HTTP status, truncated body).
|
|
23
|
+
constexpr const char* kDownloadFailed = "DOWNLOAD_FAILED";
|
|
24
|
+
// Unzip or hdiff patch application failed.
|
|
25
|
+
constexpr const char* kPatchFailed = "PATCH_FAILED";
|
|
26
|
+
// Local file or state persistence operation failed.
|
|
27
|
+
constexpr const char* kFileOperationFailed = "FILE_OPERATION_FAILED";
|
|
28
|
+
// switchVersion / setNeedUpdate state transition failed.
|
|
29
|
+
constexpr const char* kSwitchVersionFailed = "SWITCH_VERSION_FAILED";
|
|
30
|
+
// markSuccess state transition failed.
|
|
31
|
+
constexpr const char* kMarkSuccessFailed = "MARK_SUCCESS_FAILED";
|
|
32
|
+
// reloadUpdate / restartApp failed.
|
|
33
|
+
constexpr const char* kRestartFailed = "RESTART_FAILED";
|
|
34
|
+
// Stored or provided hash info is not a valid JSON object.
|
|
35
|
+
constexpr const char* kInvalidHashInfo = "INVALID_HASH_INFO";
|
|
36
|
+
// The method is not supported on this platform (e.g. downloadAndInstallApk
|
|
37
|
+
// outside Android).
|
|
38
|
+
constexpr const char* kUnsupportedPlatform = "UNSUPPORTED_PLATFORM";
|
|
39
|
+
|
|
40
|
+
} // namespace error_codes
|
|
41
|
+
} // namespace pushy
|
|
42
|
+
|
|
43
|
+
#endif // PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
#include <sys/types.h>
|
|
9
9
|
#include <unistd.h>
|
|
10
10
|
|
|
11
|
-
#include <set>
|
|
12
11
|
#include <vector>
|
|
13
12
|
|
|
14
13
|
extern "C" {
|
|
@@ -17,6 +16,11 @@ extern "C" {
|
|
|
17
16
|
|
|
18
17
|
namespace pushy {
|
|
19
18
|
namespace patch {
|
|
19
|
+
|
|
20
|
+
namespace internal {
|
|
21
|
+
bool g_disable_hard_links = false;
|
|
22
|
+
} // namespace internal
|
|
23
|
+
|
|
20
24
|
namespace {
|
|
21
25
|
|
|
22
26
|
constexpr size_t kCopyBufferSize = 16 * 1024;
|
|
@@ -202,6 +206,17 @@ Status CopyFile(const std::string& from, const std::string& to, bool overwrite)
|
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
// Prefer a hard link over copying bytes: unchanged files between versions
|
|
210
|
+
// are identical, so linking is O(1) per file, writes nothing to flash, and
|
|
211
|
+
// shares disk blocks. Version directories are immutable once created (patch
|
|
212
|
+
// outputs are always written as new files), so sharing the inode with the
|
|
213
|
+
// source is safe. Fall back to a byte copy whenever linking is not possible
|
|
214
|
+
// (cross-device source such as the installed app bundle, EPERM, EMLINK, or
|
|
215
|
+
// filesystems without hard-link support).
|
|
216
|
+
if (!internal::g_disable_hard_links && link(from.c_str(), to.c_str()) == 0) {
|
|
217
|
+
return Status::Ok();
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
FILE* source = std::fopen(from.c_str(), "rb");
|
|
206
221
|
if (!source) {
|
|
207
222
|
return MakeErrnoStatus("Failed to open source file " + from);
|
|
@@ -64,5 +64,11 @@ Status CleanupOldEntries(
|
|
|
64
64
|
|
|
65
65
|
bool IsSafeRelativePath(const std::string& path);
|
|
66
66
|
|
|
67
|
+
namespace internal {
|
|
68
|
+
// Test-only escape hatch: forces file copies to take the byte-copy fallback
|
|
69
|
+
// instead of hard-linking, so tests can cover both paths on one filesystem.
|
|
70
|
+
extern bool g_disable_hard_links;
|
|
71
|
+
} // namespace internal
|
|
72
|
+
|
|
67
73
|
} // namespace patch
|
|
68
74
|
} // namespace pushy
|
|
@@ -175,6 +175,81 @@ void TestApplyPatchFromFileSourceMergesAndCopies() {
|
|
|
175
175
|
Expect(!Exists(JoinPath(target, "assets/delete.txt")), "deleted asset should not be copied");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
void TestApplyPatchMergeHardLinksUnchangedFiles() {
|
|
179
|
+
TempDir temp;
|
|
180
|
+
const std::string source = JoinPath(temp.path, "origin");
|
|
181
|
+
const std::string target = JoinPath(temp.path, "target");
|
|
182
|
+
const std::string patch = JoinPath(temp.path, "bundle.patch");
|
|
183
|
+
|
|
184
|
+
WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
|
|
185
|
+
WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
|
|
186
|
+
WriteFile(patch, "unused patch");
|
|
187
|
+
|
|
188
|
+
FakeBundlePatcher patcher("patched bundle");
|
|
189
|
+
FileSourcePatchOptions options;
|
|
190
|
+
options.source_root = source;
|
|
191
|
+
options.target_root = target;
|
|
192
|
+
options.origin_bundle_path = JoinPath(source, "index.bundlejs");
|
|
193
|
+
options.bundle_patch_path = patch;
|
|
194
|
+
options.bundle_output_path = JoinPath(target, "index.bundlejs");
|
|
195
|
+
options.manifest.copies.push_back(CopyOperation{"assets/keep.txt", "assets/copied.txt"});
|
|
196
|
+
|
|
197
|
+
Status status = ApplyPatchFromFileSource(options, patcher);
|
|
198
|
+
Expect(status.ok, status.message);
|
|
199
|
+
|
|
200
|
+
// Unchanged files must share the source inode (hard link) instead of being
|
|
201
|
+
// byte-copied: same filesystem, so the fast path has to kick in.
|
|
202
|
+
struct stat source_stat;
|
|
203
|
+
struct stat merged_stat;
|
|
204
|
+
struct stat copied_stat;
|
|
205
|
+
Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
|
|
206
|
+
Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
|
|
207
|
+
Expect(stat(JoinPath(target, "assets/copied.txt").c_str(), &copied_stat) == 0, "stat copied");
|
|
208
|
+
Expect(merged_stat.st_ino == source_stat.st_ino, "merged file should hard-link the source");
|
|
209
|
+
Expect(copied_stat.st_ino == source_stat.st_ino, "manifest copy should hard-link the source");
|
|
210
|
+
Expect(source_stat.st_nlink >= 3, "source should have three names");
|
|
211
|
+
ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "merged content mismatch");
|
|
212
|
+
|
|
213
|
+
// The patched bundle must be a fresh file, never a link into the source dir.
|
|
214
|
+
struct stat bundle_stat;
|
|
215
|
+
struct stat origin_stat;
|
|
216
|
+
Expect(stat(JoinPath(target, "index.bundlejs").c_str(), &bundle_stat) == 0, "stat bundle");
|
|
217
|
+
Expect(stat(JoinPath(source, "index.bundlejs").c_str(), &origin_stat) == 0, "stat origin bundle");
|
|
218
|
+
Expect(bundle_stat.st_ino != origin_stat.st_ino, "patched bundle must not link the origin");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
void TestApplyPatchMergeFallsBackToByteCopy() {
|
|
222
|
+
TempDir temp;
|
|
223
|
+
const std::string source = JoinPath(temp.path, "origin");
|
|
224
|
+
const std::string target = JoinPath(temp.path, "target");
|
|
225
|
+
const std::string patch = JoinPath(temp.path, "bundle.patch");
|
|
226
|
+
|
|
227
|
+
WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
|
|
228
|
+
WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
|
|
229
|
+
WriteFile(patch, "unused patch");
|
|
230
|
+
|
|
231
|
+
FakeBundlePatcher patcher("patched bundle");
|
|
232
|
+
FileSourcePatchOptions options;
|
|
233
|
+
options.source_root = source;
|
|
234
|
+
options.target_root = target;
|
|
235
|
+
options.origin_bundle_path = JoinPath(source, "index.bundlejs");
|
|
236
|
+
options.bundle_patch_path = patch;
|
|
237
|
+
options.bundle_output_path = JoinPath(target, "index.bundlejs");
|
|
238
|
+
|
|
239
|
+
pushy::patch::internal::g_disable_hard_links = true;
|
|
240
|
+
Status status = ApplyPatchFromFileSource(options, patcher);
|
|
241
|
+
pushy::patch::internal::g_disable_hard_links = false;
|
|
242
|
+
Expect(status.ok, status.message);
|
|
243
|
+
|
|
244
|
+
struct stat source_stat;
|
|
245
|
+
struct stat merged_stat;
|
|
246
|
+
Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
|
|
247
|
+
Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
|
|
248
|
+
Expect(merged_stat.st_ino != source_stat.st_ino, "fallback should produce an independent copy");
|
|
249
|
+
Expect(merged_stat.st_nlink == 1, "fallback copy should have a single name");
|
|
250
|
+
ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "fallback content mismatch");
|
|
251
|
+
}
|
|
252
|
+
|
|
178
253
|
void TestApplyPatchFromFileSourceCanLimitMergeSubdir() {
|
|
179
254
|
TempDir temp;
|
|
180
255
|
const std::string source = JoinPath(temp.path, "origin");
|
|
@@ -537,6 +612,8 @@ void TestStateCoreSwitchToSameVersion() {
|
|
|
537
612
|
int main() {
|
|
538
613
|
const std::vector<std::pair<std::string, void (*)()>> tests = {
|
|
539
614
|
{"ApplyPatchFromFileSourceMergesAndCopies", TestApplyPatchFromFileSourceMergesAndCopies},
|
|
615
|
+
{"ApplyPatchMergeHardLinksUnchangedFiles", TestApplyPatchMergeHardLinksUnchangedFiles},
|
|
616
|
+
{"ApplyPatchMergeFallsBackToByteCopy", TestApplyPatchMergeFallsBackToByteCopy},
|
|
540
617
|
{"ApplyPatchFromFileSourceCanLimitMergeSubdir", TestApplyPatchFromFileSourceCanLimitMergeSubdir},
|
|
541
618
|
{"ApplyPatchFromFileSourceRejectsUnsafePaths", TestApplyPatchFromFileSourceRejectsUnsafePaths},
|
|
542
619
|
{"CleanupOldEntriesRemovesOnlyExpiredPaths", TestCleanupOldEntriesRemovesOnlyExpiredPaths},
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
#include "state_ops.h"
|
|
12
12
|
|
|
13
13
|
extern "C" {
|
|
14
|
-
#include "hpatch.h"
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
namespace {
|
|
@@ -407,96 +406,6 @@ pushy::patch::PatchManifest BuildManifest(
|
|
|
407
406
|
return manifest;
|
|
408
407
|
}
|
|
409
408
|
|
|
410
|
-
napi_value HdiffPatch(napi_env env, napi_callback_info info) {
|
|
411
|
-
napi_value args[2] = {nullptr, nullptr};
|
|
412
|
-
size_t argc = 2;
|
|
413
|
-
if (!GetArgCount(env, info, &argc, args) || argc < 2) {
|
|
414
|
-
ThrowError(env, "Wrong number of arguments");
|
|
415
|
-
return nullptr;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
bool is_typed_array = false;
|
|
419
|
-
if (napi_is_typedarray(env, args[0], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
420
|
-
ThrowError(env, "First argument must be a TypedArray");
|
|
421
|
-
return nullptr;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
uint8_t* origin_ptr = nullptr;
|
|
425
|
-
size_t origin_length = 0;
|
|
426
|
-
if (napi_get_typedarray_info(
|
|
427
|
-
env,
|
|
428
|
-
args[0],
|
|
429
|
-
nullptr,
|
|
430
|
-
&origin_length,
|
|
431
|
-
reinterpret_cast<void**>(&origin_ptr),
|
|
432
|
-
nullptr,
|
|
433
|
-
nullptr) != napi_ok) {
|
|
434
|
-
ThrowError(env, "Failed to get origin buffer");
|
|
435
|
-
return nullptr;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (napi_is_typedarray(env, args[1], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
439
|
-
ThrowError(env, "Second argument must be a TypedArray");
|
|
440
|
-
return nullptr;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
uint8_t* patch_ptr = nullptr;
|
|
444
|
-
size_t patch_length = 0;
|
|
445
|
-
if (napi_get_typedarray_info(
|
|
446
|
-
env,
|
|
447
|
-
args[1],
|
|
448
|
-
nullptr,
|
|
449
|
-
&patch_length,
|
|
450
|
-
reinterpret_cast<void**>(&patch_ptr),
|
|
451
|
-
nullptr,
|
|
452
|
-
nullptr) != napi_ok) {
|
|
453
|
-
ThrowError(env, "Failed to get patch buffer");
|
|
454
|
-
return nullptr;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
hpatch_singleCompressedDiffInfo patch_info;
|
|
458
|
-
if (!((origin_length == 0) || origin_ptr) || !patch_ptr || patch_length == 0) {
|
|
459
|
-
ThrowError(env, "Corrupt patch");
|
|
460
|
-
return nullptr;
|
|
461
|
-
}
|
|
462
|
-
if (kHPatch_ok != hpatch_getInfo_by_mem(&patch_info, patch_ptr, patch_length)) {
|
|
463
|
-
ThrowError(env, "Error info in hpatch");
|
|
464
|
-
return nullptr;
|
|
465
|
-
}
|
|
466
|
-
if (origin_length != patch_info.oldDataSize) {
|
|
467
|
-
ThrowError(env, "Error oldDataSize in hpatch");
|
|
468
|
-
return nullptr;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
size_t new_size = static_cast<size_t>(patch_info.newDataSize);
|
|
472
|
-
if (sizeof(size_t) != sizeof(hpatch_StreamPos_t) &&
|
|
473
|
-
new_size != patch_info.newDataSize) {
|
|
474
|
-
ThrowError(env, "Error newDataSize in hpatch");
|
|
475
|
-
return nullptr;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
void* output_data = nullptr;
|
|
479
|
-
napi_value result = nullptr;
|
|
480
|
-
if (napi_create_arraybuffer(env, new_size, &output_data, &result) != napi_ok) {
|
|
481
|
-
ThrowError(env, "Failed to create result buffer");
|
|
482
|
-
return nullptr;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (kHPatch_ok != hpatch_by_mem(
|
|
486
|
-
origin_ptr,
|
|
487
|
-
origin_length,
|
|
488
|
-
static_cast<uint8_t*>(output_data),
|
|
489
|
-
new_size,
|
|
490
|
-
patch_ptr,
|
|
491
|
-
patch_length,
|
|
492
|
-
&patch_info)) {
|
|
493
|
-
ThrowError(env, "hpatch");
|
|
494
|
-
return nullptr;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return result;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
409
|
napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
|
|
501
410
|
napi_value args[3] = {nullptr, nullptr, nullptr};
|
|
502
411
|
size_t argc = 3;
|
|
@@ -968,8 +877,7 @@ bool ExportFunction(
|
|
|
968
877
|
} // namespace
|
|
969
878
|
|
|
970
879
|
napi_value Init(napi_env env, napi_value exports) {
|
|
971
|
-
if (!ExportFunction(env, exports, "
|
|
972
|
-
!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
880
|
+
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
973
881
|
!ExportFunction(env, exports, "runStateCore", RunStateCore) ||
|
|
974
882
|
!ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
|
|
975
883
|
!ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
|