Skip to content

Commit 633d15c

Browse files
sunnylqmclaude
andcommitted
feat: HBC transform (hdiffv2) support in the patch core + capability reporting
Patch core: - hbc_transform.{h,cpp}: generic interpreter applying the delta-friendly reversible transform to Hermes bytecode offset tables. Layout descriptors arrive as untrusted wire metadata; every section bound, entry size and bit range is validated before any byte is written - hbc_transform_wire.{h,cpp}: strict parser for the __diff.json hbcTransform metadata (v / hbcVersion / layout), depth- and size-capped, unknown keys skipped for forward compatibility - ApplyPatchFromFileSource: when bundle_hbc_transform_meta is present, apply T(origin) -> hpatch -> T-inverse with temp-file cleanup; unparseable metadata or an unsupported transform version fails fast so callers fall back to the full package. Metadata absent = legacy path, byte-for-byte unchanged Platforms: - capability constant hbcTransformVersion exported from all three natives (Android JNI via NativeUpdateCore, iOS constantsToExport, HarmonyOS napi + getConstants); JS reports it as hbcT in checkUpdate so the server can gate the hdiffv2 track by actual capability - metadata threaded from __diff.json to the native patch call on Android (DownloadTask + JNI signature), iOS (RCTPushy.mm) and HarmonyOS (DownloadTask.ts + napi); build manifests updated (Android.mk / podspec / harmony CMakeLists) Tests: hbc_transform_test golden-compares against the CLI's JS reference implementation (real v96 + both v98 header variants) and fuzzes malformed descriptors; patch_core_test adds end-to-end transformed-patch apply and bad-metadata rejection. All pass under ASan+UBSan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fcaab71 commit 633d15c

33 files changed

Lines changed: 1134 additions & 11 deletions

android/jni/Android.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Hdp_Files := \
2121

2222
LOCAL_SRC_FILES := \
2323
../../cpp/patch_core/archive_patch_core.cpp \
24+
../../cpp/patch_core/hbc_transform.cpp \
25+
../../cpp/patch_core/hbc_transform_wire.cpp \
2426
../../cpp/patch_core/patch_core.cpp \
2527
../../cpp/patch_core/patch_core_android.cpp \
2628
../../cpp/patch_core/state_core.cpp \

android/src/main/java/cn/reactnative/modules/update/DownloadTask.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ private static final class PatchArchiveContents {
5353
final ArrayList<String> copyFroms = new ArrayList<String>();
5454
final ArrayList<String> copyTos = new ArrayList<String>();
5555
final ArrayList<String> deletes = new ArrayList<String>();
56+
// __diff.json 的 hbcTransform 元数据(按 patch 条目名索引);
57+
// 为空对象/缺失时返回 ""(native 走现状路径)
58+
JSONObject hbcTransform;
59+
60+
String hbcTransformMetaFor(String patchEntryName) {
61+
if (hbcTransform == null) {
62+
return "";
63+
}
64+
JSONObject meta = hbcTransform.optJSONObject(patchEntryName);
65+
return meta != null ? meta.toString() : "";
66+
}
5667
// Maps a copy source path ("from") to the CRC32 of the file content,
5768
// when provided by the manifest ("copiesCrc"). Lets the resource
5869
// copier locate the file by content if the path is not present on
@@ -251,6 +262,7 @@ private PatchArchiveContents extractPatchArchive(File archiveFile, File unzipDir
251262
contents.deletes,
252263
contents.copyCrcs
253264
);
265+
contents.hbcTransform = manifest.optJSONObject("hbcTransform");
254266
continue;
255267
}
256268

@@ -311,7 +323,8 @@ private void doPatchFromApk() throws IOException, JSONException {
311323
false,
312324
new String[0],
313325
new String[0],
314-
new String[0]
326+
new String[0],
327+
contents.hbcTransformMetaFor("index.bundlejs.patch")
315328
);
316329
} finally {
317330
originBundleFile.delete();
@@ -345,7 +358,8 @@ private void doPatchFromPpk() throws IOException, JSONException {
345358
plan.enableMerge,
346359
contents.copyFroms.toArray(new String[0]),
347360
contents.copyTos.toArray(new String[0]),
348-
contents.deletes.toArray(new String[0])
361+
contents.deletes.toArray(new String[0]),
362+
contents.hbcTransformMetaFor("index.bundlejs.patch")
349363
);
350364
if (params.targetFile.exists()) {
351365
params.targetFile.delete();
@@ -444,7 +458,8 @@ private static native void applyPatchFromFileSource(
444458
boolean enableMerge,
445459
String[] copyFroms,
446460
String[] copyTos,
447-
String[] deletes
461+
String[] deletes,
462+
String hbcTransformMeta
448463
);
449464

450465
private static native void cleanupOldEntries(

android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,15 @@ static synchronized void ensureLoaded() {
2323

2424
loaded = true;
2525
}
26+
27+
/**
28+
* 原生 patch 内核支持的 HBC 变换规范版本(hdiffv2 能力特征)。
29+
* 经 getConstants 暴露给 JS,再随 checkUpdate 上报,服务端按能力门控。
30+
*/
31+
static int hbcTransformVersion() {
32+
ensureLoaded();
33+
return getHbcTransformVersion();
34+
}
35+
36+
private static native int getHbcTransformVersion();
2637
}

android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ static Map<String, Object> getConstants(UpdateContext updateContext) {
3535
}
3636

3737
constants.put("uuid", updateContext.getKv("uuid"));
38+
constants.put("hbcTransformVersion", NativeUpdateCore.hbcTransformVersion());
3839
return constants;
3940
}
4041

cpp/patch_core/hbc_transform.cpp

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#include "hbc_transform.h"
2+
3+
#include <cstring>
4+
5+
namespace pushy {
6+
namespace hbc {
7+
8+
namespace {
9+
10+
constexpr uint8_t kHbcMagic[8] =
11+
{0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f};
12+
constexpr size_t kHeaderSize = 128;
13+
constexpr size_t kCountsOffset = 32; // magic(8) + version(4) + sourceHash(20)
14+
constexpr uint32_t kMaxEntryCount = 0x0fffffff;
15+
constexpr uint32_t kMaxEntrySize = 4096;
16+
constexpr uint32_t kMaxSections = 64;
17+
constexpr uint32_t kMaxCountFields = (kHeaderSize - kCountsOffset) / 4; // 24
18+
19+
inline uint32_t ReadU32(const uint8_t* p) {
20+
return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) |
21+
(static_cast<uint32_t>(p[2]) << 16) |
22+
(static_cast<uint32_t>(p[3]) << 24);
23+
}
24+
25+
inline void WriteU32(uint8_t* p, uint32_t v) {
26+
p[0] = static_cast<uint8_t>(v);
27+
p[1] = static_cast<uint8_t>(v >> 8);
28+
p[2] = static_cast<uint8_t>(v >> 16);
29+
p[3] = static_cast<uint8_t>(v >> 24);
30+
}
31+
32+
inline uint64_t Align4(uint64_t x) {
33+
return (x + 3) & ~static_cast<uint64_t>(3);
34+
}
35+
36+
struct ResolvedSection {
37+
uint64_t start;
38+
uint64_t size;
39+
const HbcSectionDesc* desc;
40+
};
41+
42+
} // namespace
43+
44+
bool TransformHbcInPlace(
45+
uint8_t* data,
46+
size_t size,
47+
const HbcLayoutDesc& layout,
48+
bool inverse) {
49+
// ---- 校验阶段:改写任何字节之前完成全部检查 ----
50+
if (data == nullptr || size < kHeaderSize) {
51+
return false;
52+
}
53+
if (std::memcmp(data, kHbcMagic, sizeof(kHbcMagic)) != 0) {
54+
return false;
55+
}
56+
if (layout.headerCountFields < 2 ||
57+
layout.headerCountFields > kMaxCountFields) {
58+
return false;
59+
}
60+
if (layout.sections == nullptr || layout.sectionCount == 0 ||
61+
layout.sectionCount > kMaxSections) {
62+
return false;
63+
}
64+
65+
uint32_t counts[kMaxCountFields];
66+
for (uint32_t i = 0; i < layout.headerCountFields; ++i) {
67+
counts[i] = ReadU32(data + kCountsOffset + static_cast<size_t>(i) * 4);
68+
}
69+
// 位置约定:槽位 0 = fileLength,最后一个槽位 = debugInfoOffset
70+
const uint64_t fileLength = counts[0];
71+
const uint64_t debugInfoOffset = counts[layout.headerCountFields - 1];
72+
if (fileLength != size) {
73+
return false;
74+
}
75+
if (debugInfoOffset < kHeaderSize || debugInfoOffset > size) {
76+
return false;
77+
}
78+
79+
ResolvedSection resolved[kMaxSections];
80+
uint64_t off = kHeaderSize;
81+
for (uint32_t i = 0; i < layout.sectionCount; ++i) {
82+
const HbcSectionDesc& s = layout.sections[i];
83+
if (s.countIndex >= layout.headerCountFields) {
84+
return false;
85+
}
86+
if (s.entrySize == 0 || s.entrySize > kMaxEntrySize) {
87+
return false;
88+
}
89+
const uint64_t count = counts[s.countIndex];
90+
if (count > kMaxEntryCount) {
91+
return false;
92+
}
93+
if (s.deltaFieldCount > 0 && s.deltaFields == nullptr) {
94+
return false;
95+
}
96+
for (uint32_t f = 0; f < s.deltaFieldCount; ++f) {
97+
const HbcDeltaField& field = s.deltaFields[f];
98+
if (field.bits < 1 || field.bits > 32 || field.bit + field.bits > 32 ||
99+
static_cast<uint64_t>(field.byte) + 4 > s.entrySize) {
100+
return false;
101+
}
102+
}
103+
const uint64_t sectionSize = count * s.entrySize; // ≤ 2^28 × 2^12 < 2^40
104+
off = Align4(off);
105+
if (off + sectionSize > debugInfoOffset) {
106+
return false;
107+
}
108+
resolved[i] = {off, sectionSize, &s};
109+
off += sectionSize;
110+
}
111+
112+
// ---- 改写阶段:校验通过后不再有失败路径 ----
113+
for (uint32_t i = 0; i < layout.sectionCount; ++i) {
114+
const ResolvedSection& r = resolved[i];
115+
const HbcSectionDesc& s = *r.desc;
116+
for (uint32_t f = 0; f < s.deltaFieldCount; ++f) {
117+
const HbcDeltaField& field = s.deltaFields[f];
118+
const uint32_t fieldMask =
119+
field.bits == 32 ? 0xffffffffu : ((1u << field.bits) - 1u);
120+
const uint32_t mask = fieldMask << field.bit;
121+
uint32_t prev = 0;
122+
const uint64_t end = r.start + r.size;
123+
for (uint64_t p = r.start + field.byte; p < end; p += s.entrySize) {
124+
uint8_t* wordPtr = data + p;
125+
const uint32_t word = ReadU32(wordPtr);
126+
const uint32_t val = (word >> field.bit) & fieldMask;
127+
uint32_t enc;
128+
if (!inverse) {
129+
enc = (val - prev) & fieldMask;
130+
prev = val;
131+
} else {
132+
enc = (val + prev) & fieldMask;
133+
prev = enc;
134+
}
135+
WriteU32(wordPtr, (word & ~mask) | (enc << field.bit));
136+
}
137+
}
138+
}
139+
return true;
140+
}
141+
142+
} // namespace hbc
143+
} // namespace pushy

cpp/patch_core/hbc_transform.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
6+
namespace pushy {
7+
namespace hbc {
8+
9+
// Hermes 字节码(HBC)delta-friendly 可逆变换的通用解释器。
10+
//
11+
// 布局描述表来自 patch 的 __diff.json 元数据(生成端 CLI 写入),本解释器
12+
// 不含任何 HBC 版本分支——Hermes 版本演进只需要生成端更新描述表,客户端
13+
// 零跟进。描述表被视为不可信输入:所有段边界、条目大小、位域范围在改写
14+
// 任何字节之前完成全量校验;校验失败时 buffer 保持原样。
15+
//
16+
// 变换语义与生成端(react-native-update-cli src/utils/hbcTransform.ts)
17+
// 严格一致:对描述的偏移位域做前项差分(wrapping,模字段位宽)。
18+
// wrapping 保证与数据单调性无关的严格可逆。
19+
20+
struct HbcDeltaField {
21+
// 条目内字节偏移处按小端读取 u32,取 [bit, bit+bits) 位做差分
22+
uint32_t byte;
23+
uint32_t bit;
24+
uint32_t bits;
25+
};
26+
27+
struct HbcSectionDesc {
28+
// 段大小 = headerCounts[countIndex] × entrySize(字节段 entrySize 为 1)
29+
uint32_t countIndex;
30+
uint32_t entrySize;
31+
const HbcDeltaField* deltaFields;
32+
uint32_t deltaFieldCount;
33+
};
34+
35+
// 与 wire 格式的位置约定一致:counts 槽位 0 = fileLength,
36+
// 最后一个槽位 = debugInfoOffset(结构校验依赖)。
37+
struct HbcLayoutDesc {
38+
uint32_t headerCountFields;
39+
const HbcSectionDesc* sections;
40+
uint32_t sectionCount;
41+
};
42+
43+
// 对 data 原地执行变换(inverse=false)或逆变换(inverse=true)。
44+
// 返回 false 表示 data 不是该描述表下结构合法的 HBC(或描述表本身非法),
45+
// 此时 data 未被修改。
46+
bool TransformHbcInPlace(
47+
uint8_t* data,
48+
size_t size,
49+
const HbcLayoutDesc& layout,
50+
bool inverse);
51+
52+
} // namespace hbc
53+
} // namespace pushy

0 commit comments

Comments
 (0)