react-native-update 10.48.3 → 10.49.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.
@@ -191,6 +191,17 @@ public class UpdateModuleImpl {
191
191
  });
192
192
  }
193
193
 
194
+ public static void getBundleHash(final UpdateContext updateContext, final Promise promise) {
195
+ // Threading lives in UpdateContext (download executor); resolve is
196
+ // thread-safe. Never rejects — empty string means "unknown".
197
+ updateContext.getBundleHash(new UpdateContext.BundleHashListener() {
198
+ @Override
199
+ public void onBundleHash(String hash) {
200
+ promise.resolve(hash);
201
+ }
202
+ });
203
+ }
204
+
194
205
  public static void resetToPackagedBundle(
195
206
  final UpdateContext updateContext,
196
207
  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,177 @@
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 digest
177
+ } // namespace pushy
@@ -0,0 +1,43 @@
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
+ } // namespace digest
43
+ } // namespace pushy
@@ -1,4 +1,5 @@
1
1
  #include "../archive_patch_core.h"
2
+ #include "../digest.h"
2
3
  #include "../patch_core.h"
3
4
  #include "../state_core.h"
4
5
 
@@ -747,6 +748,72 @@ void TestStateCoreSwitchToSameVersion() {
747
748
  Expect(!switched.first_time_ok, "first_time_ok should be false");
748
749
  }
749
750
 
751
+ // NIST FIPS 180-4 vectors. These anchor the C++ implementation (iOS/Harmony)
752
+ // and, by transitivity, Android's java.security.MessageDigest and the CLI's
753
+ // node:crypto to the same standard — the bundleHash produced on any of them
754
+ // must be byte-identical for the same input.
755
+ void TestSha256KnownVectors() {
756
+ {
757
+ pushy::digest::Sha256 hasher;
758
+ ExpectEq(
759
+ hasher.HexDigest(),
760
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
761
+ "sha256 of empty input");
762
+ }
763
+ {
764
+ pushy::digest::Sha256 hasher;
765
+ const char* input = "abc";
766
+ hasher.Update(reinterpret_cast<const uint8_t*>(input), 3);
767
+ ExpectEq(
768
+ hasher.HexDigest(),
769
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
770
+ "sha256 of \"abc\"");
771
+ }
772
+ {
773
+ pushy::digest::Sha256 hasher;
774
+ const std::string input =
775
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
776
+ hasher.Update(
777
+ reinterpret_cast<const uint8_t*>(input.data()), input.size());
778
+ ExpectEq(
779
+ hasher.HexDigest(),
780
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
781
+ "sha256 of NIST two-block message");
782
+ }
783
+ }
784
+
785
+ void TestSha256StreamingMatchesOneShot() {
786
+ // One million 'a' (NIST long vector), fed in deliberately awkward chunk
787
+ // sizes to cross every buffer boundary case in Update.
788
+ const std::string chunk(4096 + 37, 'a');
789
+ size_t remaining = 1000000;
790
+ pushy::digest::Sha256 hasher;
791
+ while (remaining > 0) {
792
+ const size_t take = remaining < chunk.size() ? remaining : chunk.size();
793
+ hasher.Update(reinterpret_cast<const uint8_t*>(chunk.data()), take);
794
+ remaining -= take;
795
+ }
796
+ ExpectEq(
797
+ hasher.HexDigest(),
798
+ "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
799
+ "sha256 of one million 'a' fed in odd-sized chunks");
800
+ }
801
+
802
+ void TestSha256File() {
803
+ TempDir temp;
804
+ const std::string dir = temp.path;
805
+ const std::string path = dir + "/input.bin";
806
+ WriteFile(path, "abc");
807
+ ExpectEq(
808
+ pushy::digest::Sha256File(path),
809
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
810
+ "Sha256File over a small file");
811
+ ExpectEq(
812
+ pushy::digest::Sha256File(dir + "/missing.bin"),
813
+ "",
814
+ "Sha256File over a missing file returns empty");
815
+ }
816
+
750
817
  } // namespace
751
818
 
752
819
  int main(int argc, char** argv) {
@@ -778,6 +845,9 @@ int main(int argc, char** argv) {
778
845
  {"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
779
846
  {"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
780
847
  {"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
848
+ {"Sha256KnownVectors", TestSha256KnownVectors},
849
+ {"Sha256StreamingMatchesOneShot", TestSha256StreamingMatchesOneShot},
850
+ {"Sha256File", TestSha256File},
781
851
  };
782
852
 
783
853
  int failures = 0;
@@ -27,6 +27,7 @@ set(LZMA_DIR ${ANDROID_JNI_DIR}/lzma)
27
27
  set(HDP_SOURCES
28
28
  ${CMAKE_CURRENT_SOURCE_DIR}/pushy.cpp
29
29
  ${PATCH_CORE_DIR}/archive_patch_core.cpp
30
+ ${PATCH_CORE_DIR}/digest.cpp
30
31
  ${PATCH_CORE_DIR}/hbc_transform.cpp
31
32
  ${PATCH_CORE_DIR}/hbc_transform_wire.cpp
32
33
  ${PATCH_CORE_DIR}/patch_core.cpp
@@ -6,6 +6,7 @@
6
6
  #include <vector>
7
7
 
8
8
  #include "archive_patch_core.h"
9
+ #include "digest.h"
9
10
  #include "hbc_transform_wire.h"
10
11
  #include "patch_core.h"
11
12
  #include "state_core.h"
@@ -890,6 +891,51 @@ bool ExportFunction(
890
891
  } // namespace
891
892
 
892
893
 
894
+ // sha256(Uint8Array | ArrayBuffer) -> 小写 hex。同步:bundleHash 的输入是
895
+ // rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
896
+ static napi_value Sha256Hex(napi_env env, napi_callback_info info) {
897
+ size_t argc = 1;
898
+ napi_value args[1] = {nullptr};
899
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
900
+ ThrowError(env, "sha256Hex: missing data argument");
901
+ return nullptr;
902
+ }
903
+
904
+ void* data = nullptr;
905
+ size_t length = 0;
906
+ bool is_typedarray = false;
907
+ napi_is_typedarray(env, args[0], &is_typedarray);
908
+ if (is_typedarray) {
909
+ napi_typedarray_type type;
910
+ napi_value arraybuffer = nullptr;
911
+ size_t offset = 0;
912
+ if (napi_get_typedarray_info(
913
+ env, args[0], &type, &length, &data, &arraybuffer, &offset) !=
914
+ napi_ok) {
915
+ ThrowError(env, "sha256Hex: cannot read typed array");
916
+ return nullptr;
917
+ }
918
+ } else {
919
+ bool is_arraybuffer = false;
920
+ napi_is_arraybuffer(env, args[0], &is_arraybuffer);
921
+ if (!is_arraybuffer ||
922
+ napi_get_arraybuffer_info(env, args[0], &data, &length) != napi_ok) {
923
+ ThrowError(env, "sha256Hex: expected Uint8Array or ArrayBuffer");
924
+ return nullptr;
925
+ }
926
+ }
927
+
928
+ pushy::digest::Sha256 hasher;
929
+ if (length > 0) {
930
+ hasher.Update(static_cast<const uint8_t*>(data), length);
931
+ }
932
+ const std::string hex = hasher.HexDigest();
933
+
934
+ napi_value result = nullptr;
935
+ napi_create_string_utf8(env, hex.c_str(), hex.size(), &result);
936
+ return result;
937
+ }
938
+
893
939
  // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道)
894
940
  static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
895
941
  napi_value result = nullptr;
@@ -907,6 +953,7 @@ napi_value Init(napi_env env, napi_value exports) {
907
953
  !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
908
954
  !ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
909
955
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
956
+ !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
910
957
  !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
911
958
  return nullptr;
912
959
  }
@@ -80,6 +80,8 @@ interface NativePatchCoreBindings {
80
80
  keepPrevious: string,
81
81
  maxAgeDays: number,
82
82
  ): Promise<void>;
83
+ /** sha256(小写 hex)。同步:输入是已在内存的 rawfile bundle,哈希毫秒级 */
84
+ sha256Hex(data: Uint8Array): string;
83
85
  /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
84
86
  getSupportedDiffVersion(): number;
85
87
  }
@@ -233,6 +233,17 @@ export class PushyTurboModule extends UITurboModule {
233
233
  }
234
234
  }
235
235
 
236
+ async getBundleHash(): Promise<string> {
237
+ logger.debug(TAG, ',call getBundleHash');
238
+ try {
239
+ return this.context.getBundleHash();
240
+ } catch (error) {
241
+ // 空串 = "未知",服务端回退 buildTime 启发式;该方法永不 reject。
242
+ logger.error(TAG, `getBundleHash failed: ${getErrorMessage(error)}`);
243
+ return '';
244
+ }
245
+ }
246
+
236
247
  async resetToPackagedBundle(): Promise<void> {
237
248
  logger.debug(TAG, ',call resetToPackagedBundle');
238
249
  try {
@@ -632,4 +632,58 @@ export class UpdateContext {
632
632
  public getIsUsingBundleUrl(): boolean {
633
633
  return UpdateContext.isUsingBundleUrl;
634
634
  }
635
+
636
+ // bundleHash 缓存:"<packageVersion>|<updateTime>|<sha256hex>"。键标识当前
637
+ // 安装的二进制,每次(覆盖)安装 updateTime 都会变,每个安装只算一次。
638
+ private static readonly KEY_BUNDLE_HASH_CACHE = 'bundleHashCache';
639
+
640
+ private getBundleUpdateTime(): number {
641
+ try {
642
+ const bundleInfo = bundleManager.getBundleInfoForSelfSync(
643
+ this.getBundleFlags(),
644
+ );
645
+ return bundleInfo?.updateTime ?? 0;
646
+ } catch (error) {
647
+ console.error('Failed to get bundle update time:', error);
648
+ return 0;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * bundleHash = 二进制内嵌 JS bundle 的 sha256 —— 标识二进制本身,与当前运行
654
+ * 的热更版本无关。哈希对象与 pdiff 的源读取完全一致:rawfile 的
655
+ * bundle.harmony.js(DownloadTask.doPatchFromApp 同一读法)。懒计算 + 缓存;
656
+ * 永不失败——空串表示"未知",服务端回退 buildTime 启发式。
657
+ */
658
+ public async getBundleHash(): Promise<string> {
659
+ if (UpdateContext.DEBUG) {
660
+ // debug 下 bundle 由 metro 提供,与 dev 删 buildTime 的行为对齐。
661
+ return '';
662
+ }
663
+ const cachePrefix = `${this.getPackageVersion()}|${this.getBundleUpdateTime()}|`;
664
+ const cached = this.readString(UpdateContext.KEY_BUNDLE_HASH_CACHE);
665
+ if (cached.startsWith(cachePrefix)) {
666
+ return cached.slice(cachePrefix.length);
667
+ }
668
+ let hash = '';
669
+ try {
670
+ // 异步读:TurboModule 跑在 UI 线程,同步读几 MB 会卡主线程一次。C++
671
+ // 哈希本身毫秒级,同步无妨。
672
+ const content =
673
+ await this.context.resourceManager.getRawFileContent('bundle.harmony.js');
674
+ hash = NativePatchCore.sha256Hex(content);
675
+ } catch (error) {
676
+ // rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。
677
+ console.info('Cannot hash embedded bundle:', error);
678
+ return '';
679
+ }
680
+ if (hash) {
681
+ this.preferences.putSync(
682
+ UpdateContext.KEY_BUNDLE_HASH_CACHE,
683
+ cachePrefix + hash,
684
+ );
685
+ this.flushPreferences('cache bundle hash');
686
+ }
687
+ return hash;
688
+ }
635
689
  }
package/harmony/pushy.har CHANGED
Binary file
@@ -2,6 +2,7 @@
2
2
  #import "RCTPushyDownloader.h"
3
3
  #import "ZipArchive.h"
4
4
  #include "../../cpp/patch_core/archive_patch_core.h"
5
+ #include "../../cpp/patch_core/digest.h"
5
6
  #include "../../cpp/patch_core/hbc_transform_wire.h"
6
7
  #include "../../cpp/patch_core/error_codes.h"
7
8
  #include "../../cpp/patch_core/patch_core.h"
@@ -32,6 +33,10 @@ static NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_";
32
33
  static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY";
33
34
  static NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY";
34
35
  static NSString *const KeyPackageUpdatedMarked = @"REACTNATIVECN_PUSHY_ISPACKAGEUPDATEDMARKED_KEY";
36
+ // bundleHash cache: "<cacheKey>|<sha256hex>" where cacheKey identifies the
37
+ // installed binary (packageVersion + embedded bundle size + mtime). Recomputed
38
+ // only when the key changes, i.e. once per install.
39
+ static NSString *const keyBundleHashCache = @"REACTNATIVECN_PUSHY_BUNDLEHASH_KEY";
35
40
  static NSString *const PushyErrorDomain = @"cn.reactnative.pushy";
36
41
 
37
42
  // file def
@@ -512,6 +517,56 @@ RCT_EXPORT_METHOD(restartApp:(RCTPromiseResolveBlock)resolve
512
517
  resolve(@true);
513
518
  }
514
519
 
520
+ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
521
+ rejecter:(RCTPromiseRejectBlock)reject)
522
+ {
523
+ // bundleHash = sha256 of the bundle embedded in the binary — the identity
524
+ // of the binary itself, not of whatever hot update is currently running.
525
+ // It must hash exactly the bytes pdiff patches from (binaryBundleURL, the
526
+ // pdiff fromBundle). Lazily computed once per install, cached in defaults.
527
+ // Never rejects: an empty string means "unknown" and the server falls back
528
+ // to the buildTime heuristic.
529
+ #if DEBUG
530
+ // Metro serves the bundle in debug; the embedded file (if any) is not what
531
+ // is running. Mirrors the dev behaviour of buildTime.
532
+ resolve(@"");
533
+ #else
534
+ dispatch_async(_fileQueue, ^{
535
+ NSString *path = [[RCTPushy binaryBundleURL] path];
536
+ if (path == nil) {
537
+ resolve(@"");
538
+ return;
539
+ }
540
+ NSDictionary *attributes =
541
+ [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
542
+ if (attributes == nil) {
543
+ resolve(@"");
544
+ return;
545
+ }
546
+ NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
547
+ [RCTPushy packageVersion],
548
+ attributes.fileSize,
549
+ [attributes.fileModificationDate timeIntervalSince1970]];
550
+
551
+ NSUserDefaults *defaults = PushyDefaults();
552
+ NSString *cached = [defaults stringForKey:keyBundleHashCache];
553
+ NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
554
+ if ([cached hasPrefix:cachedPrefix]) {
555
+ resolve([cached substringFromIndex:cachedPrefix.length]);
556
+ return;
557
+ }
558
+
559
+ NSString *hash = PushyFromStdString(
560
+ pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
561
+ if (hash.length > 0) {
562
+ [defaults setObject:[cachedPrefix stringByAppendingString:hash]
563
+ forKey:keyBundleHashCache];
564
+ }
565
+ resolve(hash);
566
+ });
567
+ #endif
568
+ }
569
+
515
570
  RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
516
571
  rejecter:(RCTPromiseRejectBlock)reject)
517
572
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.48.3",
3
+ "version": "10.49.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
@@ -111,6 +111,7 @@ Pod::Spec.new do |s|
111
111
  s.subspec 'RCTPushy' do |ss|
112
112
  ss.source_files = ['ios/RCTPushy/*.{h,m,mm}',
113
113
  'cpp/patch_core/archive_patch_core.{h,cpp}',
114
+ 'cpp/patch_core/digest.{h,cpp}',
114
115
  'cpp/patch_core/hbc_transform.{h,cpp}',
115
116
  'cpp/patch_core/hbc_transform_wire.{h,cpp}',
116
117
  'cpp/patch_core/patch_core.{h,cpp}',
@@ -20,6 +20,12 @@ export interface Spec extends TurboModule {
20
20
  restartApp(): Promise<void>;
21
21
  setNeedUpdate(options: { hash: string }): Promise<void>;
22
22
  markSuccess(): Promise<void>;
23
+ /**
24
+ * sha256 of the JS bundle embedded in the binary (the pdiff source), lazily
25
+ * computed and cached natively. Resolves to an empty string when unknown
26
+ * (debug build, no embedded bundle, hash failure) — never rejects.
27
+ */
28
+ getBundleHash(): Promise<string>;
23
29
  resetToPackagedBundle(): Promise<void>;
24
30
  downloadPatchFromPpk(options: {
25
31
  updateUrl: string;