react-native-update 10.48.3 → 10.50.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 (41) hide show
  1. package/BUNDLEHASH_MIGRATION.md +355 -0
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
  3. package/android/jni/Android.mk +1 -0
  4. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  5. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  6. package/android/lib/x86/librnupdate.so +0 -0
  7. package/android/lib/x86_64/librnupdate.so +0 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  9. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +22 -1
  10. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +125 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +23 -3
  13. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  14. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  15. package/cpp/patch_core/digest.cpp +230 -0
  16. package/cpp/patch_core/digest.h +61 -0
  17. package/cpp/patch_core/patch_core.cpp +17 -1
  18. package/cpp/patch_core/patch_core.h +8 -0
  19. package/cpp/patch_core/tests/patch_core_test.cpp +169 -0
  20. package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
  21. package/harmony/pushy/src/main/cpp/pushy.cpp +94 -0
  22. package/harmony/pushy/src/main/ets/DownloadTask.ts +85 -0
  23. package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
  24. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
  25. package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -4
  26. package/harmony/pushy.har +0 -0
  27. package/ios/RCTPushy/RCTPushy.mm +116 -7
  28. package/package.json +1 -1
  29. package/react-native-update.podspec +1 -0
  30. package/src/NativePushy.ts +6 -0
  31. package/src/client.ts +119 -7
  32. package/src/core.ts +27 -0
  33. package/src/endpoint.ts +98 -51
  34. package/src/error.ts +4 -0
  35. package/src/isInRollout.ts +2 -1
  36. package/src/locales/en.ts +6 -0
  37. package/src/locales/zh.ts +6 -0
  38. package/src/provider.tsx +36 -7
  39. package/src/telemetry.ts +8 -0
  40. package/src/type.ts +34 -3
  41. package/src/utils.ts +13 -0
@@ -77,6 +77,9 @@ class DownloadTask implements Runnable {
77
77
  private final byte[] buffer = new byte[DOWNLOAD_CHUNK_SIZE];
78
78
  private final BundledResourceCopier bundledResourceCopier;
79
79
  private String hash;
80
+ // Set once downloadFile() returns: failures after this point in a patch
81
+ // task are patch-application failures, not download failures.
82
+ private boolean downloadPhaseCompleted = false;
80
83
 
81
84
  DownloadTask(Context context, DownloadTaskParams params) {
82
85
  this.context = context.getApplicationContext();
@@ -160,6 +163,7 @@ class DownloadTask implements Runnable {
160
163
  }
161
164
  }
162
165
 
166
+ downloadPhaseCompleted = true;
163
167
  }
164
168
 
165
169
  private byte[] readBytes(InputStream input) throws IOException {
@@ -408,6 +412,12 @@ class DownloadTask implements Runnable {
408
412
  }
409
413
  }
410
414
 
415
+ private boolean isPatchTask(int taskType) {
416
+ return taskType == DownloadTaskParams.TASK_TYPE_PATCH_FULL
417
+ || taskType == DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK
418
+ || taskType == DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
419
+ }
420
+
411
421
  @Override
412
422
  public void run() {
413
423
  int taskType = params.type;
@@ -436,7 +446,18 @@ class DownloadTask implements Runnable {
436
446
  cleanUpAfterFailure(taskType);
437
447
 
438
448
  if (params.listener != null) {
439
- params.listener.onDownloadFailed(error);
449
+ // A patch task that failed after its artifact was fully
450
+ // downloaded (unzip / hdiff / resource copy, incl. copiesCrc
451
+ // verification) is a patch failure, not a download failure —
452
+ // the module maps the exception type to PATCH_FAILED.
453
+ Throwable classified = error;
454
+ if (downloadPhaseCompleted
455
+ && isPatchTask(taskType)
456
+ && !(error instanceof PatchFailedException)) {
457
+ classified = new PatchFailedException(
458
+ String.valueOf(error.getMessage()), error);
459
+ }
460
+ params.listener.onDownloadFailed(classified);
440
461
  }
441
462
  return;
442
463
  }
@@ -0,0 +1,16 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import java.io.IOException;
4
+
5
+ /**
6
+ * A download task failure that happened after the artifact was fully
7
+ * downloaded — unzip, hdiff apply, or bundled-resource copy (including the
8
+ * copiesCrc content verification). UpdateModuleImpl rejects these with
9
+ * PATCH_FAILED instead of DOWNLOAD_FAILED so the JS layer and server-side
10
+ * telemetry can separate patch health from network health.
11
+ */
12
+ class PatchFailedException extends IOException {
13
+ PatchFailedException(String message, Throwable cause) {
14
+ super(message, cause);
15
+ }
16
+ }
@@ -9,6 +9,10 @@ import android.os.Environment;
9
9
  import android.util.Log;
10
10
  import com.facebook.react.ReactInstanceManager;
11
11
  import java.io.File;
12
+ import java.io.IOException;
13
+ import java.io.InputStream;
14
+ import java.security.MessageDigest;
15
+ import java.security.NoSuchAlgorithmException;
12
16
  import java.util.concurrent.Executor;
13
17
  import java.util.concurrent.Executors;
14
18
 
@@ -118,16 +122,125 @@ public class UpdateContext {
118
122
  return isUsingBundleUrl;
119
123
  }
120
124
 
125
+ // bundleHash cache: "<packageVersion>|<lastUpdateTime>|<sha256hex>". The
126
+ // key identifies the installed binary; every (re)install changes
127
+ // lastUpdateTime, so the hash is recomputed once per install.
128
+ private static final String KEY_BUNDLE_HASH_CACHE = "bundleHashCache";
129
+
130
+ public interface BundleHashListener {
131
+ void onBundleHash(String hash);
132
+ }
133
+
134
+ /**
135
+ * bundleHash = sha256 of the JS bundle embedded in the binary — the
136
+ * identity of the binary itself, not of whatever hot update is currently
137
+ * running. Hashes exactly the bytes pdiff patches from: the hardcoded
138
+ * "index.android.bundle" asset read via AssetManager, same as
139
+ * DownloadTask.copyBundledAssetToFile. Runs on the download executor;
140
+ * never fails — an empty string means "unknown" and the server falls back
141
+ * to the buildTime heuristic.
142
+ *
143
+ * Deliberately java.security.MessageDigest instead of the C++
144
+ * pushy::digest: librnupdate.so is a prebuilt artifact and this must not
145
+ * force a rebuild. The NIST vectors in the patch_core tests anchor both
146
+ * implementations to the same standard.
147
+ */
148
+ public void getBundleHash(final BundleHashListener listener) {
149
+ if (DEBUG) {
150
+ // Metro serves the bundle in debug; mirror the dev buildTime behaviour.
151
+ listener.onBundleHash("");
152
+ return;
153
+ }
154
+ executor.execute(new Runnable() {
155
+ @Override
156
+ public void run() {
157
+ listener.onBundleHash(computeBundleHash());
158
+ }
159
+ });
160
+ }
161
+
162
+ private String computeBundleHash() {
163
+ String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
164
+ String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
165
+ if (cached != null && cached.startsWith(cachePrefix)) {
166
+ return cached.substring(cachePrefix.length());
167
+ }
168
+ String hash = sha256OfBundledAsset("index.android.bundle");
169
+ if (!hash.isEmpty()) {
170
+ SharedPreferences.Editor editor = sp.edit();
171
+ editor.putString(KEY_BUNDLE_HASH_CACHE, cachePrefix + hash);
172
+ persistEditor(editor, "cache bundle hash");
173
+ }
174
+ return hash;
175
+ }
176
+
177
+ private long getPackageLastUpdateTime() {
178
+ try {
179
+ PackageInfo pi = context.getPackageManager()
180
+ .getPackageInfo(context.getPackageName(), 0);
181
+ return pi.lastUpdateTime;
182
+ } catch (PackageManager.NameNotFoundException e) {
183
+ return 0;
184
+ }
185
+ }
186
+
187
+ private String sha256OfBundledAsset(String assetName) {
188
+ try (InputStream in = context.getAssets().open(assetName)) {
189
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
190
+ byte[] buffer = new byte[64 * 1024];
191
+ int read;
192
+ while ((read = in.read(buffer)) != -1) {
193
+ md.update(buffer, 0, read);
194
+ }
195
+ byte[] digest = md.digest();
196
+ StringBuilder hex = new StringBuilder(digest.length * 2);
197
+ for (byte b : digest) {
198
+ hex.append(Character.forDigit((b >> 4) & 0xf, 16));
199
+ hex.append(Character.forDigit(b & 0xf, 16));
200
+ }
201
+ return hex.toString();
202
+ } catch (IOException | NoSuchAlgorithmException e) {
203
+ // Expected when there is no embedded bundle (custom name, no
204
+ // release bundle); "unknown" is a valid answer, not an error.
205
+ Log.i(TAG, "Cannot hash bundled asset " + assetName + ": " + e);
206
+ return "";
207
+ }
208
+ }
209
+
121
210
  private void enqueue(DownloadTaskParams params) {
122
211
  executor.execute(new DownloadTask(context, params));
123
212
  }
124
213
 
214
+ // Server-provided identifiers (hash/originHash/fileName) become child
215
+ // names under rootDir; anything that could resolve outside of it (path
216
+ // separators, "..", ".") must be rejected before touching the filesystem.
217
+ static boolean isSafePathComponent(String name) {
218
+ return name != null
219
+ && !name.isEmpty()
220
+ && !name.equals(".")
221
+ && !name.equals("..")
222
+ && !name.contains("/")
223
+ && !name.contains("\\")
224
+ && name.indexOf('\0') < 0;
225
+ }
226
+
227
+ private static boolean rejectUnsafeComponent(String name, DownloadFileListener listener) {
228
+ if (isSafePathComponent(name)) {
229
+ return false;
230
+ }
231
+ listener.onDownloadFailed(new IllegalArgumentException("Invalid path component: " + name));
232
+ return true;
233
+ }
234
+
125
235
  public interface DownloadFileListener {
126
236
  void onDownloadCompleted(DownloadTaskParams params);
127
237
  void onDownloadFailed(Throwable error);
128
238
  }
129
239
 
130
240
  public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
241
+ if (rejectUnsafeComponent(hash, listener)) {
242
+ return;
243
+ }
131
244
  DownloadTaskParams params = new DownloadTaskParams();
132
245
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
133
246
  params.url = url;
@@ -139,6 +252,9 @@ public class UpdateContext {
139
252
  }
140
253
 
141
254
  public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
255
+ if (rejectUnsafeComponent(fileName, listener)) {
256
+ return;
257
+ }
142
258
  DownloadTaskParams params = new DownloadTaskParams();
143
259
  params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
144
260
  params.url = url;
@@ -157,6 +273,9 @@ public class UpdateContext {
157
273
  }
158
274
 
159
275
  public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
276
+ if (rejectUnsafeComponent(hash, listener)) {
277
+ return;
278
+ }
160
279
  DownloadTaskParams params = new DownloadTaskParams();
161
280
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
162
281
  params.url = url;
@@ -168,6 +287,9 @@ public class UpdateContext {
168
287
  }
169
288
 
170
289
  public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
290
+ if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
291
+ return;
292
+ }
171
293
  DownloadTaskParams params = new DownloadTaskParams();
172
294
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
173
295
  params.url = url;
@@ -223,6 +345,9 @@ public class UpdateContext {
223
345
  }
224
346
 
225
347
  public void switchVersion(String hash) {
348
+ if (!isSafePathComponent(hash)) {
349
+ throw new IllegalArgumentException("Invalid hash: " + hash);
350
+ }
226
351
  if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
227
352
  throw new IllegalStateException("Bundle version " + hash + " not found.");
228
353
  }
@@ -41,11 +41,20 @@ public class UpdateModuleImpl {
41
41
 
42
42
  @Override
43
43
  public void onDownloadFailed(Throwable error) {
44
- promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
44
+ promise.reject(downloadErrorCode(error), error);
45
45
  }
46
46
  });
47
47
  }
48
48
 
49
+ // Post-download failures (unzip / hdiff / resource copy, incl. copiesCrc
50
+ // verification) reject as PATCH_FAILED so JS-side telemetry can separate
51
+ // patch health from network health. DownloadTask wraps them.
52
+ private static String downloadErrorCode(Throwable error) {
53
+ return error instanceof PatchFailedException
54
+ ? ErrorCodes.PATCH_FAILED
55
+ : ErrorCodes.DOWNLOAD_FAILED;
56
+ }
57
+
49
58
  public static void downloadAndInstallApk(
50
59
  final ReactApplicationContext reactContext,
51
60
  UpdateContext updateContext,
@@ -84,7 +93,7 @@ public class UpdateModuleImpl {
84
93
 
85
94
  @Override
86
95
  public void onDownloadFailed(Throwable error) {
87
- promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
96
+ promise.reject(downloadErrorCode(error), error);
88
97
  }
89
98
  });
90
99
  }
@@ -107,7 +116,7 @@ public class UpdateModuleImpl {
107
116
 
108
117
  @Override
109
118
  public void onDownloadFailed(Throwable error) {
110
- promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
119
+ promise.reject(downloadErrorCode(error), error);
111
120
  }
112
121
  });
113
122
  } catch (Exception e) {
@@ -191,6 +200,17 @@ public class UpdateModuleImpl {
191
200
  });
192
201
  }
193
202
 
203
+ public static void getBundleHash(final UpdateContext updateContext, final Promise promise) {
204
+ // Threading lives in UpdateContext (download executor); resolve is
205
+ // thread-safe. Never rejects — empty string means "unknown".
206
+ updateContext.getBundleHash(new UpdateContext.BundleHashListener() {
207
+ @Override
208
+ public void onBundleHash(String hash) {
209
+ promise.resolve(hash);
210
+ }
211
+ });
212
+ }
213
+
194
214
  public static void resetToPackagedBundle(
195
215
  final UpdateContext updateContext,
196
216
  final Promise promise
@@ -73,6 +73,11 @@ public class UpdateModule extends NativePushySpec {
73
73
  UpdateModuleImpl.markSuccess(updateContext, promise);
74
74
  }
75
75
 
76
+ @Override
77
+ public void getBundleHash(Promise promise) {
78
+ UpdateModuleImpl.getBundleHash(updateContext, promise);
79
+ }
80
+
76
81
  @Override
77
82
  public void resetToPackagedBundle(Promise promise) {
78
83
  UpdateModuleImpl.resetToPackagedBundle(updateContext, promise);
@@ -75,6 +75,11 @@ public class UpdateModule extends ReactContextBaseJavaModule {
75
75
  UpdateModuleImpl.markSuccess(updateContext);
76
76
  }
77
77
 
78
+ @ReactMethod
79
+ public void getBundleHash(Promise promise) {
80
+ UpdateModuleImpl.getBundleHash(updateContext, promise);
81
+ }
82
+
78
83
  @ReactMethod
79
84
  public void resetToPackagedBundle(Promise promise) {
80
85
  UpdateModuleImpl.resetToPackagedBundle(updateContext, promise);
@@ -0,0 +1,230 @@
1
+ #include "digest.h"
2
+
3
+ #include <cstdio>
4
+ #include <cstring>
5
+
6
+ namespace pushy {
7
+ namespace digest {
8
+
9
+ namespace {
10
+
11
+ // FIPS 180-4 SHA-256 round constants.
12
+ constexpr uint32_t kRoundConstants[64] = {
13
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
14
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
15
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
16
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
17
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
18
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
19
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
20
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
21
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
22
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
23
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
24
+ };
25
+
26
+ inline uint32_t RotateRight(uint32_t value, uint32_t bits) {
27
+ return (value >> bits) | (value << (32 - bits));
28
+ }
29
+
30
+ } // namespace
31
+
32
+ Sha256::Sha256() {
33
+ state_[0] = 0x6a09e667;
34
+ state_[1] = 0xbb67ae85;
35
+ state_[2] = 0x3c6ef372;
36
+ state_[3] = 0xa54ff53a;
37
+ state_[4] = 0x510e527f;
38
+ state_[5] = 0x9b05688c;
39
+ state_[6] = 0x1f83d9ab;
40
+ state_[7] = 0x5be0cd19;
41
+ }
42
+
43
+ void Sha256::ProcessBlock(const uint8_t* block) {
44
+ uint32_t w[64];
45
+ for (int i = 0; i < 16; i++) {
46
+ w[i] = (static_cast<uint32_t>(block[i * 4]) << 24) |
47
+ (static_cast<uint32_t>(block[i * 4 + 1]) << 16) |
48
+ (static_cast<uint32_t>(block[i * 4 + 2]) << 8) |
49
+ static_cast<uint32_t>(block[i * 4 + 3]);
50
+ }
51
+ for (int i = 16; i < 64; i++) {
52
+ const uint32_t s0 = RotateRight(w[i - 15], 7) ^ RotateRight(w[i - 15], 18) ^
53
+ (w[i - 15] >> 3);
54
+ const uint32_t s1 = RotateRight(w[i - 2], 17) ^ RotateRight(w[i - 2], 19) ^
55
+ (w[i - 2] >> 10);
56
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
57
+ }
58
+
59
+ uint32_t a = state_[0];
60
+ uint32_t b = state_[1];
61
+ uint32_t c = state_[2];
62
+ uint32_t d = state_[3];
63
+ uint32_t e = state_[4];
64
+ uint32_t f = state_[5];
65
+ uint32_t g = state_[6];
66
+ uint32_t h = state_[7];
67
+
68
+ for (int i = 0; i < 64; i++) {
69
+ const uint32_t s1 =
70
+ RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25);
71
+ const uint32_t ch = (e & f) ^ (~e & g);
72
+ const uint32_t temp1 = h + s1 + ch + kRoundConstants[i] + w[i];
73
+ const uint32_t s0 =
74
+ RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22);
75
+ const uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
76
+ const uint32_t temp2 = s0 + maj;
77
+
78
+ h = g;
79
+ g = f;
80
+ f = e;
81
+ e = d + temp1;
82
+ d = c;
83
+ c = b;
84
+ b = a;
85
+ a = temp1 + temp2;
86
+ }
87
+
88
+ state_[0] += a;
89
+ state_[1] += b;
90
+ state_[2] += c;
91
+ state_[3] += d;
92
+ state_[4] += e;
93
+ state_[5] += f;
94
+ state_[6] += g;
95
+ state_[7] += h;
96
+ }
97
+
98
+ void Sha256::Update(const uint8_t* data, size_t length) {
99
+ total_length_ += length;
100
+
101
+ if (buffer_length_ > 0) {
102
+ const size_t fill = 64 - buffer_length_;
103
+ const size_t take = length < fill ? length : fill;
104
+ std::memcpy(buffer_ + buffer_length_, data, take);
105
+ buffer_length_ += take;
106
+ data += take;
107
+ length -= take;
108
+ if (buffer_length_ < 64) {
109
+ return;
110
+ }
111
+ ProcessBlock(buffer_);
112
+ buffer_length_ = 0;
113
+ }
114
+
115
+ while (length >= 64) {
116
+ ProcessBlock(data);
117
+ data += 64;
118
+ length -= 64;
119
+ }
120
+
121
+ if (length > 0) {
122
+ std::memcpy(buffer_, data, length);
123
+ buffer_length_ = length;
124
+ }
125
+ }
126
+
127
+ std::string Sha256::HexDigest() {
128
+ // Padding: 0x80, zeros, then the 64-bit big-endian bit length.
129
+ const uint64_t bit_length = total_length_ * 8;
130
+ const uint8_t pad_byte = 0x80;
131
+ Update(&pad_byte, 1);
132
+ const uint8_t zero = 0x00;
133
+ // total_length_ is mutated by the padding updates; the final length field is
134
+ // captured above, so this loop only needs to reach the 56-byte boundary.
135
+ while (buffer_length_ != 56) {
136
+ Update(&zero, 1);
137
+ }
138
+ uint8_t length_bytes[8];
139
+ for (int i = 0; i < 8; i++) {
140
+ length_bytes[i] = static_cast<uint8_t>(bit_length >> (56 - i * 8));
141
+ }
142
+ Update(length_bytes, 8);
143
+
144
+ static const char kHex[] = "0123456789abcdef";
145
+ std::string out;
146
+ out.reserve(64);
147
+ for (int i = 0; i < 8; i++) {
148
+ for (int shift = 24; shift >= 0; shift -= 8) {
149
+ const uint8_t byte = static_cast<uint8_t>(state_[i] >> shift);
150
+ out.push_back(kHex[byte >> 4]);
151
+ out.push_back(kHex[byte & 0x0f]);
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+
157
+ std::string Sha256File(const std::string& path) {
158
+ std::FILE* file = std::fopen(path.c_str(), "rb");
159
+ if (file == nullptr) {
160
+ return std::string();
161
+ }
162
+ Sha256 hasher;
163
+ uint8_t buffer[64 * 1024];
164
+ size_t read = 0;
165
+ while ((read = std::fread(buffer, 1, sizeof(buffer), file)) > 0) {
166
+ hasher.Update(buffer, read);
167
+ }
168
+ const bool failed = std::ferror(file) != 0;
169
+ std::fclose(file);
170
+ if (failed) {
171
+ return std::string();
172
+ }
173
+ return hasher.HexDigest();
174
+ }
175
+
176
+ namespace {
177
+
178
+ struct Crc32TableHolder {
179
+ uint32_t table[256];
180
+
181
+ Crc32TableHolder() {
182
+ for (uint32_t i = 0; i < 256; i++) {
183
+ uint32_t value = i;
184
+ for (int bit = 0; bit < 8; bit++) {
185
+ value = (value & 1u) ? (value >> 1) ^ 0xEDB88320u : value >> 1;
186
+ }
187
+ table[i] = value;
188
+ }
189
+ }
190
+ };
191
+
192
+ const uint32_t* Crc32Table() {
193
+ // Function-local static: thread-safe one-time construction (C++11).
194
+ static const Crc32TableHolder holder;
195
+ return holder.table;
196
+ }
197
+
198
+ } // namespace
199
+
200
+ void Crc32::Update(const uint8_t* data, size_t length) {
201
+ const uint32_t* table = Crc32Table();
202
+ uint32_t state = state_;
203
+ for (size_t i = 0; i < length; i++) {
204
+ state = table[(state ^ data[i]) & 0xFFu] ^ (state >> 8);
205
+ }
206
+ state_ = state;
207
+ }
208
+
209
+ bool Crc32File(const std::string& path, uint32_t* out) {
210
+ std::FILE* file = std::fopen(path.c_str(), "rb");
211
+ if (file == nullptr) {
212
+ return false;
213
+ }
214
+ Crc32 crc;
215
+ uint8_t buffer[64 * 1024];
216
+ size_t read = 0;
217
+ while ((read = std::fread(buffer, 1, sizeof(buffer), file)) > 0) {
218
+ crc.Update(buffer, read);
219
+ }
220
+ const bool failed = std::ferror(file) != 0;
221
+ std::fclose(file);
222
+ if (failed) {
223
+ return false;
224
+ }
225
+ *out = crc.Value();
226
+ return true;
227
+ }
228
+
229
+ } // namespace digest
230
+ } // namespace pushy
@@ -0,0 +1,61 @@
1
+ #pragma once
2
+
3
+ #include <cstddef>
4
+ #include <cstdint>
5
+ #include <string>
6
+
7
+ namespace pushy {
8
+ namespace digest {
9
+
10
+ // Streaming SHA-256 (FIPS 180-4). Used for bundleHash (the identity of the
11
+ // bundle embedded in the binary) and, later, download artifact verification
12
+ // (IO-6). Streaming because Android/Harmony read the bundle from the package
13
+ // (AssetManager / rawfile) rather than from a plain file.
14
+ //
15
+ // Android intentionally does NOT use this implementation: librnupdate.so is a
16
+ // prebuilt artifact, so the Java layer hashes with java.security.MessageDigest
17
+ // instead. The NIST vectors in the tests anchor both implementations to the
18
+ // same standard.
19
+ class Sha256 {
20
+ public:
21
+ Sha256();
22
+
23
+ void Update(const uint8_t* data, size_t length);
24
+
25
+ // Finalizes and returns the lowercase hex digest. The instance must not be
26
+ // reused afterwards.
27
+ std::string HexDigest();
28
+
29
+ private:
30
+ void ProcessBlock(const uint8_t* block);
31
+
32
+ uint32_t state_[8];
33
+ uint64_t total_length_ = 0;
34
+ uint8_t buffer_[64];
35
+ size_t buffer_length_ = 0;
36
+ };
37
+
38
+ // Hashes a file on disk (streaming, bounded memory). Returns the lowercase hex
39
+ // digest, or an empty string if the file cannot be read.
40
+ std::string Sha256File(const std::string& path);
41
+
42
+ // Streaming CRC32 (IEEE 802.3, reflected polynomial 0xEDB88320 — the zip /
43
+ // zlib checksum). Used to verify pdiff copy sources against the CRCs the CLI
44
+ // records from the origin package's zip entries (copiesCrc in __diff.json):
45
+ // the values must be byte-compatible with what any zip tool would report.
46
+ class Crc32 {
47
+ public:
48
+ void Update(const uint8_t* data, size_t length);
49
+
50
+ uint32_t Value() const { return state_ ^ 0xFFFFFFFFu; }
51
+
52
+ private:
53
+ uint32_t state_ = 0xFFFFFFFFu;
54
+ };
55
+
56
+ // CRC32 of a file on disk (streaming, bounded memory). Returns false if the
57
+ // file cannot be read.
58
+ bool Crc32File(const std::string& path, uint32_t* out);
59
+
60
+ } // namespace digest
61
+ } // namespace pushy
@@ -1,5 +1,7 @@
1
1
  #include "patch_core.h"
2
2
 
3
+ #include "digest.h"
4
+
3
5
  #include <cerrno>
4
6
  #include <cstdio>
5
7
  #include <cstring>
@@ -532,8 +534,22 @@ Status ApplyPatchFromFileSource(
532
534
  }
533
535
 
534
536
  for (const CopyOperation& copy : options.manifest.copies) {
537
+ const std::string source_path = JoinPath(options.source_root, copy.from);
538
+ if (copy.has_expected_crc) {
539
+ uint32_t actual_crc = 0;
540
+ if (!digest::Crc32File(source_path, &actual_crc)) {
541
+ return Status::Error("Failed to read copy source " + source_path);
542
+ }
543
+ if (actual_crc != copy.expected_crc) {
544
+ // Rebuilt binary with drifted content at an unchanged path. Fail the
545
+ // patch — the caller falls back to the full package; never install a
546
+ // resource whose bytes differ from what the diff was generated against.
547
+ return Status::Error(
548
+ "Copy source content mismatch (crc32): " + copy.from);
549
+ }
550
+ }
535
551
  Status copy_status = CopyFile(
536
- JoinPath(options.source_root, copy.from),
552
+ source_path,
537
553
  JoinPath(options.target_root, copy.to),
538
554
  true);
539
555
  if (!copy_status) {
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <cstdint>
3
4
  #include <ctime>
4
5
  #include <string>
5
6
  #include <vector>
@@ -20,6 +21,13 @@ struct Status {
20
21
  struct CopyOperation {
21
22
  std::string from;
22
23
  std::string to;
24
+ // Manifest-declared CRC32 of the source content (__diff.json copiesCrc,
25
+ // present in pdiff manifests from CLI >= 2.21.2). When set, the copy source
26
+ // is verified before copying; a mismatch fails the whole patch so the JS
27
+ // strategy chain falls back to the full package instead of installing a
28
+ // wrong resource from a rebuilt binary.
29
+ bool has_expected_crc = false;
30
+ uint32_t expected_crc = 0;
23
31
  };
24
32
 
25
33
  struct PatchManifest {