From 6ac787a2cc9004902756b909747754706901eaa3 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 15:05:31 +0800 Subject: [PATCH 1/6] refactor(hermes): fold duplicated branches and layout entries - classifyHermesCommand: the build / commitly / stable version forms share one result instead of three identical return blocks - resolveHermesBase never read its hermesCommand parameter; drop it - hermes-base: one isBundleHash() for the four cache-name checks - hbcTransform: tryTransformPair and tryTransformPairFiles share the same-version check and the meta builder; the late-v98 and v99 layouts were identical apart from the version and become one 98..99 entry, and the early-v98 header is the late one minus numStringSwitchImms. The candidates and wire layout for every version 80..100 are byte-identical to before. Co-Authored-By: Claude Opus 5.5 --- src/bundle-runner.ts | 1 - src/utils/hbcTransform.ts | 91 ++++++++++++++------------------------- src/utils/hermes-base.ts | 58 ++++++++++--------------- tests/hermes-base.test.ts | 7 --- 4 files changed, 54 insertions(+), 103 deletions(-) diff --git a/src/bundle-runner.ts b/src/bundle-runner.ts index 323a647..b97629a 100644 --- a/src/bundle-runner.ts +++ b/src/bundle-runner.ts @@ -1358,7 +1358,6 @@ async function selectHermesBase( try { const base = await resolveHermesBase({ option: request.option, - hermesCommand, bytecodeVersion, appId: request.appId, cacheMaxMb: request.cacheMaxMb, diff --git a/src/utils/hbcTransform.ts b/src/utils/hbcTransform.ts index 43f205f..3c6d158 100644 --- a/src/utils/hbcTransform.ts +++ b/src/utils/hbcTransform.ts @@ -219,16 +219,10 @@ export const HBC_LAYOUTS: HbcLayout[] = [ // 多一个 numStringSwitchImms 槽(hermes static_h 在不 bump 版本号的 // 情况下插入,同一版本号 98 因此存在两种布局,依结构校验区分)。 // 段布局与早期 v98 相同。 - minVersion: 98, - maxVersion: 98, - headerFields: V98_LATE_HEADER_FIELDS, - sections: V98_SECTIONS, - }, - { - // Hermes V1 v99(hermes-compiler 260318099,RN 0.88+):文件头与段布局 + // Hermes V1 v99(hermes-compiler 260318099,RN 0.88+)文件头与段布局 // 同晚期 v98,只改了函数头内部位域(去 NumCacheNewObject)与操作码表, // 二者都不在本变换触及的范围内。 - minVersion: 99, + minVersion: 98, maxVersion: 99, headerFields: V98_LATE_HEADER_FIELDS, sections: V98_SECTIONS, @@ -240,27 +234,9 @@ export const HBC_LAYOUTS: HbcLayout[] = [ // arrayBufferSize 更名 literalValueBufferSize(槽位不变)。 minVersion: 98, maxVersion: 98, - headerFields: [ - 'fileLength', - 'globalCodeIndex', - 'functionCount', - 'stringKindCount', - 'identifierCount', - 'stringCount', - 'overflowStringCount', - 'stringStorageSize', - 'bigIntCount', - 'bigIntStorageSize', - 'regExpCount', - 'regExpStorageSize', - 'literalValueBufferSize', - 'objKeyBufferSize', - 'objShapeTableCount', - 'segmentID', - 'cjsModuleCount', - 'functionSourceCount', - 'debugInfoOffset', - ], + headerFields: V98_LATE_HEADER_FIELDS.filter( + (field) => field !== 'numStringSwitchImms', + ), sections: V98_SECTIONS, }, ]; @@ -610,6 +586,25 @@ export type HbcTransformMeta = { layout: HbcLayoutWire; }; +/** 两侧共同的 HBC 版本;任一侧不是 HBC 或版本不同时为 null */ +function sharedHbcVersion(oldHeader: Buffer, newHeader: Buffer): number | null { + const version = getHbcVersion(newHeader); + return version !== null && getHbcVersion(oldHeader) === version + ? version + : null; +} + +function transformMeta( + hbcVersion: number, + layout: HbcLayout, +): HbcTransformMeta { + return { + v: HBC_TRANSFORM_VERSION, + hbcVersion, + layout: compileLayoutToWire(layout), + }; +} + /** * 若 old/new 均为同版本且存在两侧都通过结构校验的候选布局,返回变换结果、 * 选中布局与写入 __diff.json 的元数据;否则返回 null。 @@ -623,25 +618,13 @@ export function tryTransformPair( layout: HbcLayout; meta: HbcTransformMeta; } | null { - const oldVersion = getHbcVersion(oldBuf); - const newVersion = getHbcVersion(newBuf); - if (oldVersion === null || newVersion === null || oldVersion !== newVersion) { - return null; - } - for (const layout of findLayouts(newVersion)) { + const version = sharedHbcVersion(oldBuf, newBuf); + if (version === null) return null; + for (const layout of findLayouts(version)) { const tOld = transformHbcWithLayout(oldBuf, layout, false); const tNew = transformHbcWithLayout(newBuf, layout, false); if (!tOld || !tNew) continue; - return { - tOld, - tNew, - layout, - meta: { - v: HBC_TRANSFORM_VERSION, - hbcVersion: newVersion, - layout: compileLayoutToWire(layout), - }, - }; + return { tOld, tNew, layout, meta: transformMeta(version, layout) }; } return null; } @@ -662,12 +645,9 @@ export async function tryTransformPairFiles( readHbcFileHeader(newPath), ]); if (!oldInfo || !newInfo) return null; - const oldVersion = getHbcVersion(oldInfo.header); - const newVersion = getHbcVersion(newInfo.header); - if (oldVersion === null || newVersion === null || oldVersion !== newVersion) { - return null; - } - for (const layout of findLayouts(newVersion)) { + const version = sharedHbcVersion(oldInfo.header, newInfo.header); + if (version === null) return null; + for (const layout of findLayouts(version)) { if ( !resolveSectionsFromHeader(oldInfo.header, oldInfo.size, layout) || !resolveSectionsFromHeader(newInfo.header, newInfo.size, layout) @@ -679,14 +659,7 @@ export async function tryTransformPairFiles( transformHbcFile(newPath, tNewPath, layout, false), ]); if (!tOld || !tNew) continue; - return { - layout, - meta: { - v: HBC_TRANSFORM_VERSION, - hbcVersion: newVersion, - layout: compileLayoutToWire(layout), - }, - }; + return { layout, meta: transformMeta(version, layout) }; } return null; } diff --git a/src/utils/hermes-base.ts b/src/utils/hermes-base.ts index 60c5812..ac6e5dd 100644 --- a/src/utils/hermes-base.ts +++ b/src/utils/hermes-base.ts @@ -194,48 +194,32 @@ export function classifyHermesCommand(hermesCommand: string): { }; } const build = /^(\d{9})\./.exec(version); - if (build) { - const ok = Number(build[1]) >= MIN_HERMES_COMPILER_BUILD; - return { - allowed: ok, - kind: 'hermes-compiler', - version, - reason: ok - ? undefined - : `hermes-compiler ${version} predates the delta-mode fix`, - }; - } const commitly = /^0\.(\d+)\.\d+-commitly-(\d{8})/.exec(version); - if (commitly) { - const ok = + const stable = /^0\.(\d+)\.\d+$/.exec(version); + let ok: boolean; + if (build) { + ok = Number(build[1]) >= MIN_HERMES_COMPILER_BUILD; + } else if (commitly) { + ok = Number(commitly[1]) >= 14 && Number(commitly[2]) >= MIN_HERMES_COMPILER_COMMITLY_DATE; + } else if (stable) { + ok = Number(stable[1]) >= 14; + } else { return { - allowed: ok, - kind: 'hermes-compiler', - version, - reason: ok - ? undefined - : `hermes-compiler ${version} predates the delta-mode fix`, - }; - } - const stable = /^0\.(\d+)\.\d+$/.exec(version); - if (stable) { - const ok = Number(stable[1]) >= 14; - return { - allowed: ok, + allowed: false, kind: 'hermes-compiler', version, - reason: ok - ? undefined - : `hermes-compiler ${version} predates the delta-mode fix`, + reason: `unrecognized hermes-compiler version ${version}`, }; } return { - allowed: false, + allowed: ok, kind: 'hermes-compiler', version, - reason: `unrecognized hermes-compiler version ${version}`, + reason: ok + ? undefined + : `hermes-compiler ${version} predates the delta-mode fix`, }; } return { @@ -415,6 +399,9 @@ export async function extractBundleFromArchive( export const DEFAULT_CACHE_MAX_BYTES = 500 * 1024 * 1024; export const DEFAULT_CACHE_MAX_FILES = 20; +/** cache entries are named by the sha256 of their bundle */ +const isBundleHash = (name: string) => /^[0-9a-f]{64}$/.test(name); + export function cacheDir(): string { return process.env.PUSHY_CACHE_DIR || path.join(tempDir, 'cache'); } @@ -436,7 +423,7 @@ export function cacheLimits(overrideMaxMb?: number) { /** Returns the cached bundle path when present and its sha256 still matches. */ export async function cacheLookup(bundleHash: string): Promise { - if (!/^[0-9a-f]{64}$/.test(bundleHash)) return null; + if (!isBundleHash(bundleHash)) return null; const file = path.join(cacheDir(), bundleHash); if (!(await pathExists(file))) return null; if ((await sha256File(file).catch(() => '')) !== bundleHash) { @@ -482,7 +469,7 @@ export async function enforceCacheLimits( const staleTmpCutoff = Date.now() - STALE_CACHE_TMP_MS; for (const name of await fs.promises.readdir(dir)) { const isTmp = isCacheTmpName(name); - if (!isTmp && !/^[0-9a-f]{64}$/.test(name)) continue; + if (!isTmp && !isBundleHash(name)) continue; const stat = await fs.promises.stat(path.join(dir, name)).catch(() => null); if (!stat?.isFile()) continue; if (isTmp) { @@ -516,7 +503,7 @@ export async function cleanCache(): Promise { if (!(await pathExists(dir))) return 0; let bundles = 0; for (const name of await fs.promises.readdir(dir)) { - const isBundle = /^[0-9a-f]{64}$/.test(name); + const isBundle = isBundleHash(name); if (!isBundle && !isCacheTmpName(name)) continue; await remove(path.join(dir, name)); if (isBundle) bundles += 1; @@ -534,7 +521,7 @@ export async function cacheStats(): Promise<{ let bytes = 0; if (await pathExists(dir)) { for (const name of await fs.promises.readdir(dir)) { - if (!/^[0-9a-f]{64}$/.test(name)) continue; + if (!isBundleHash(name)) continue; const stat = await fs.promises .stat(path.join(dir, name)) .catch(() => null); @@ -804,7 +791,6 @@ export async function fetchBaseBundle( export interface ResolveHermesBaseParams { option: HermesBaseOption; - hermesCommand: string; bytecodeVersion: number; appId?: string; cacheMaxMb?: number; diff --git a/tests/hermes-base.test.ts b/tests/hermes-base.test.ts index 2f5522f..2e90181 100644 --- a/tests/hermes-base.test.ts +++ b/tests/hermes-base.test.ts @@ -240,7 +240,6 @@ describe('resolveHermesBase', () => { removeSync(dir); }); const common = { - hermesCommand: 'hermesc', bytecodeVersion: 98, log: (m: string) => logs.push(m), }; @@ -431,8 +430,6 @@ describe('resolveHermesBase over HTTP Range', () => { ))!; const selection = await resolveHermesBase({ option: 'auto', - hermesCommand: - '/x/node_modules/react-native/sdks/hermesc/osx-bin/hermesc', bytecodeVersion: 98, appId: '1', log: (m) => messages.push(m), @@ -491,8 +488,6 @@ describe('resolveHermesBase over HTTP Range', () => { try { const selection = await resolveHermesBase({ option: 'auto', - hermesCommand: - '/x/node_modules/react-native/sdks/hermesc/osx-bin/hermesc', bytecodeVersion: 98, appId: '1', log: (m) => messages.push(m), @@ -533,8 +528,6 @@ describe('resolveHermesBase over HTTP Range', () => { }; const params = { option: 'auto', - hermesCommand: - '/x/node_modules/react-native/sdks/hermesc/osx-bin/hermesc', appId: '1', log: (m: string) => messages.push(m), fetchBase: async () => record, From 17bfb00b05d143e4fe2f20442df7ec36bd553168 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 15:06:17 +0800 Subject: [PATCH 2/6] refactor(hermes-raw): reuse the layout resolveHbcSections picked readHermesSemanticData ran the same first-fitting-layout search twice (resolveHbcSections, then pickLayout on the same bytes); resolveHbcSections now returns the layout it chose and pickLayout is internal again. The raw dump splitter calls normalizeRawHermesFunction through one local helper instead of three copies of the same call. Co-Authored-By: Claude Opus 5.5 --- src/utils/hbcTransform.ts | 9 +++++++-- src/utils/hermes-raw.ts | 30 +++++++----------------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/utils/hbcTransform.ts b/src/utils/hbcTransform.ts index 3c6d158..d89d0e9 100644 --- a/src/utils/hbcTransform.ts +++ b/src/utils/hbcTransform.ts @@ -374,7 +374,11 @@ export type HbcSectionRange = { start: number; size: number }; export function resolveHbcSections( header: Buffer, fileLength: number, -): { version: number; sections: Map } | null { +): { + version: number; + layout: HbcLayout; + sections: Map; +} | null { const version = getHbcVersion(header); if (version === null) return null; for (const layout of findLayouts(version)) { @@ -382,6 +386,7 @@ export function resolveHbcSections( if (resolved) { return { version, + layout, sections: new Map( resolved.map((s) => [s.desc.name, { start: s.start, size: s.size }]), ), @@ -431,7 +436,7 @@ export function transformHbc(buf: Buffer, inverse = false): Buffer | null { } /** 返回该 buffer 结构上匹配的布局(候选中第一个校验通过者)。 */ -export function pickLayout(buf: Buffer): HbcLayout | null { +function pickLayout(buf: Buffer): HbcLayout | null { const version = getHbcVersion(buf); if (version === null) return null; for (const layout of findLayouts(version)) { diff --git a/src/utils/hermes-raw.ts b/src/utils/hermes-raw.ts index 7832adc..a805c9c 100644 --- a/src/utils/hermes-raw.ts +++ b/src/utils/hermes-raw.ts @@ -11,7 +11,7 @@ import { readFile } from 'node:fs/promises'; import { StringDecoder } from 'node:string_decoder'; import { spawn } from 'child_process'; -import { pickLayout, resolveHbcSections } from './hbcTransform'; +import { resolveHbcSections } from './hbcTransform'; import { type LiteralBuffers, LiteralResolver } from './hermes-literals'; export class UnverifiableHermesBytecode extends Error {} @@ -74,7 +74,7 @@ export async function readHermesSemanticData( ) { throw new UnverifiableHermesBytecode('unsupported semantic HBC version'); } - const layout = requireValue(pickLayout(bytes), 'unknown HBC layout'); + const { layout } = resolved; const section = (name: string) => { const r = requireValue(resolved.sections.get(name), `missing ${name}`); return checkedSlice(bytes, r.start, r.size); @@ -561,38 +561,22 @@ export async function auditRawHermesBytecode( const functions = async function* (side: number): AsyncGenerator { let lines: string[] | null = null; let index = 0; + const normalize = (body: string[]) => + normalizeRawHermesFunction(body, data[side], index++, buffers[side]); for await (const line of linesOf(children[side].stdout!)) { if (/^(?:Function|NCFunction|Constructor) Date: Mon, 12 Oct 2026 15:07:13 +0800 Subject: [PATCH 3/6] refactor(hermes-base): fold fetchBaseBundle's full-archive fallbacks - the three "server ignores Range" catches share rangeIgnored(), and the two full-archive endings share fromArchive() - resolveHermesBase computes `source` after the null-record check, picks the artifact type once instead of per attempt, and checks a cache hit in one branch Co-Authored-By: Claude Opus 5.5 --- src/utils/hermes-base.ts | 84 ++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/src/utils/hermes-base.ts b/src/utils/hermes-base.ts index ac6e5dd..b42194f 100644 --- a/src/utils/hermes-base.ts +++ b/src/utils/hermes-base.ts @@ -693,17 +693,18 @@ export async function fetchBaseBundle( } return { bundle, bundleHash }; }; - const fromFullResponse = async ( - response: Response, - ): Promise => { - await saveResponse(response, archive); - return { - ...verified(await extractBundleFromArchive(archive)), - transport: 'full', - }; - }; + const fromArchive = async (): Promise => ({ + ...verified(await extractBundleFromArchive(archive)), + transport: 'full', + }); const skip = (reason: string) => log(t('hermesBaseRangeFallback', { reason })); + // a 200 to a Range request already carries the whole archive + const rangeIgnored = async (response: Response) => { + skip('server ignores Range'); + await saveResponse(response, archive); + return fromArchive(); + }; // harmony .app nests the bundle in a second zip, so a location inside the // outer archive is never reported for it @@ -732,8 +733,7 @@ export async function fetchBaseBundle( }; } catch (error: any) { if (error instanceof RangeUnsupportedError) { - skip('server ignores Range'); - return fromFullResponse(error.response); + return rangeIgnored(error.response); } // not a transport failure: the directory read below settles it if (!(error instanceof StaleHeadError)) { @@ -746,8 +746,7 @@ export async function fetchBaseBundle( try { const remote = await openRemoteZip(record.url); if (remote.kind === 'full') { - skip('server ignores Range'); - return fromFullResponse(remote.response); + return rangeIgnored(remote.response); } const bundle = await readRemoteZipEntry( remote.zipFile, @@ -773,8 +772,7 @@ export async function fetchBaseBundle( }; } catch (error: any) { if (error instanceof RangeUnsupportedError) { - skip('server ignores Range'); - return fromFullResponse(error.response); + return rangeIgnored(error.response); } // the real entry was read and is not the recorded one: final if (error instanceof PermanentBaseError) throw error; @@ -783,10 +781,7 @@ export async function fetchBaseBundle( } await downloadToFile(record.url, archive); - return { - ...verified(await extractBundleFromArchive(archive)), - transport: 'full', - }; + return fromArchive(); } export interface ResolveHermesBaseParams { @@ -865,12 +860,6 @@ export async function resolveHermesBase( ); return null; } - const source: HermesBaseSelection['source'] = - record?.artifactType && record.artifactType !== 'ppk' - ? 'native-package' - : record?.bytecodeVersion == null - ? 'latest-version' - : 'download'; if (!record) { log(t('hermesBaseNone', { reason: 'no published version yet' })); return null; @@ -889,20 +878,20 @@ export async function resolveHermesBase( // 1. cache by bundleHash if (record.bundleHash) { const hit = await cacheLookup(record.bundleHash); - if (hit && record.bytecodeVersion == null) { - // the server does not know the HBC version (legacy version, native - // package): the download path checks it, the cache path must too - const version = await readFileHbcVersion(hit); - if (version !== bytecodeVersion) { - log( - t('hermesBaseNone', { - reason: `cached base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`, - }), - ); - return null; - } - } if (hit) { + if (record.bytecodeVersion == null) { + // the server does not know the HBC version (legacy version, native + // package): the download path checks it, the cache path must too + const version = await readFileHbcVersion(hit); + if (version !== bytecodeVersion) { + log( + t('hermesBaseNone', { + reason: `cached base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`, + }), + ); + return null; + } + } log( t('hermesBaseUsing', { source: `cache ${record.bundleHash.slice(0, 12)}${record.versionId == null ? '' : ` (version ${record.versionId})`}`, @@ -920,17 +909,20 @@ export async function resolveHermesBase( } } // 2. download, extract, verify, cache — transport errors get a second try + const artifactType: BaseArtifactType = ['ppk', 'apk', 'ipa', 'app'].includes( + record.artifactType ?? '', + ) + ? (record.artifactType as BaseArtifactType) + : 'ppk'; + const source: HermesBaseSelection['source'] = + record.artifactType && record.artifactType !== 'ppk' + ? 'native-package' + : record.bytecodeVersion == null + ? 'latest-version' + : 'download'; for (let attempt = 1; attempt <= 2; attempt++) { const dir = tmpDir(); await ensureDir(dir); - const artifactType: BaseArtifactType = [ - 'ppk', - 'apk', - 'ipa', - 'app', - ].includes(record.artifactType ?? '') - ? (record.artifactType as BaseArtifactType) - : 'ppk'; const archive = path.join( dir, `base-${process.pid}-${Date.now()}-${attempt}.${artifactType}`, From c2df7ecda6b5c4886b9f90e48e7f400c9d896a1e Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 15:08:09 +0800 Subject: [PATCH 4/6] docs(hermes): bring the verification notes up to HBC v99 - hermes-base-verification.md: v99 (hermes-compiler 260318099.0.4) in the verified range, the shaped-layout tables and the CI matrix (four jobs); a note on the v99 function header and the undecoded NewTypedObjectWithBuffer; the `literals` result field is not logged, so step 5.3 now keys off the detail text - fold the one lasting rule of hermes-review-follow-up.md (keep the pretty pass: raw does not cover exception-handler tables) into it and drop that PR #85 review log - hermes-base pointed at a design doc that is not in the repo; v98-only comments now say v98 and v99 Co-Authored-By: Claude Opus 5.5 --- docs/hermes-base-verification.md | 17 ++++++----- docs/hermes-review-follow-up.md | 52 -------------------------------- src/utils/hermes-base.ts | 4 +-- src/utils/hermes-literals.ts | 2 +- 4 files changed, 13 insertions(+), 62 deletions(-) delete mode 100644 docs/hermes-review-follow-up.md diff --git a/docs/hermes-base-verification.md b/docs/hermes-base-verification.md index a475307..bd9396e 100644 --- a/docs/hermes-base-verification.md +++ b/docs/hermes-base-verification.md @@ -18,7 +18,7 @@ |---|---| | `Bytecode File Information` 等头部 | 文本头不比较;raw 核对从二进制比较运行时选项、全局函数索引、segment ID、模块与函数源码表 | | `Global String Table` | 不比较死字符串;有效 HBC 的字符串按二进制表恢复完整内容,而非截断或转义后的显示文本 | -| `Array Buffer` / `Object Key Buffer` / `Object Value Buffer`(v98:`Literal Value Buffer` / `Object Key Buffer`) | 文本段本身**不比较**(见下);无法读二进制缓冲区时整段文本仅用于诊断,不能据此判定等价 | +| `Array Buffer` / `Object Key Buffer` / `Object Value Buffer`(v98/v99:`Literal Value Buffer` / `Object Key Buffer`) | 文本段本身**不比较**(见下);无法读二进制缓冲区时整段文本仅用于诊断,不能据此判定等价 | | `Function<…>` / `NCFunction<…>` / `Constructor<…>` 各函数 | 头文本必须相同;正文逐行经 `normalizeDisassemblyLine` 归一化后比较;**按函数分段**,错位不级联 | | `Debug *` / `Textified callees table` | 排掉(只服务调试器/性能工具) | @@ -26,14 +26,14 @@ 两种布局: -| | v87–96(`split`) | v98(`shaped`) | +| | v87–96(`split`) | v98/v99(`shaped`) | |---|---|---| | 缓冲区 | `arrayBuffer` / `objKeyBuffer` / `objValueBuffer` | `literalValueBuffer`(数组元素与对象值共用)/ `objKeyBuffer` / `objShapeTable`(8 字节项:keyOffset u32、numProps u32) | | 数组 | `NewArrayWithBuffer rX, sizeHint, count, offset` | 同左,offset 指向值缓冲区 | | 对象 | `NewObjectWithBuffer rX, sizeHint, count, keyOffset, valueOffset` | `NewObjectWithBuffer rX, shapeIndex, valueOffset`;`NewObjectWithBufferAndParent rX, rParent, shapeIndex, valueOffset` | | tag 类型 6 | 1 字节字符串 id | `undefined`(无值字节);字符串 id 只用 2/4 字节 | -v98 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape 表),归一化后只留 shape 指向的键与值;`AndParent` 的父对象寄存器原样保留比较。 +v98/v99 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape 表),归一化后只留 shape 指向的键与值;`AndParent` 的父对象寄存器原样保留比较。 为什么不能按 dump 的整段文本比:Hermes 的缓冲区构建器会**重叠/去重**序列化后的字面量——一个字面量的最后一个值字节可以同时是下一个字面量的 tag 字节(模糊测试实测:`61 52 | cd 09 b3 05 11`,前一段以 `[String 82]` 结尾,后一条指令的 offset 正指向 `52`)。顺序解析整段缓冲区(hermesc 的 dump 就是这么打印的)从这里开始失步,之后的条目全是噪声;delta 构建的 id 宽度不同,重叠位置也不同,于是两段"噪声"在某处不一致就被判为差异。2026-09-10 的 20 轮冒烟模糊测试里 3 次误杀全部源于此,改按指令比较后全部等价。无法读二进制缓冲区(文件结构不识别)时两侧一起比较整段文本,仅辅助诊断;即使文本相等也返回 `dump-failed` 并回退 plain,不能以丢失 offset/count 的文本确认等价。结果里 `literals: 'buffer'` 标明这一点。 @@ -43,7 +43,7 @@ v98 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape **原始操作数核对**:`hermes-raw.ts` 从 raw dump 读取指令起点和操作数类型,并检查操作数与 HBC 字节一致、指令覆盖完整函数体。字符串从 small/overflow string table 与 string storage 按完整 ASCII/UTF-16 code unit 解码;BigInt、正则和 double 读取真实字节(保留 `-0` 和尾部精度);函数引用保留索引,与顺序对齐的函数表共同检查,同名函数不能互换。地址映射为目标指令序号;整数和字符串 switch 从二进制恢复 case 值与目的地。函数运行时 flags 和参数/寄存器等字段也参与比较,剔除的仅是物理地址、debug presence 与 compact/overflow 表示。 -这不是一个可以忽略所有新指令的通用语义证明器。已验证的函数头范围为 HBC v87–96,以及 `hermes-compiler@250829098.0.16/.17` 的 v98 快照;升级布局或引入新的字符串/shape 引用指令时,需要复核 `hermes-raw.ts` 的解码规则及测试,不可仅扩展 diff-transform 的布局表。 +这不是一个可以忽略所有新指令的通用语义证明器。已验证的函数头范围为 HBC v87–96、`hermes-compiler@250829098.0.16/.17` 的 v98 快照,以及 `hermes-compiler@260318099.0.4` 的 v99 快照;升级布局或引入新的字符串/shape 引用指令时,需要复核 `hermes-raw.ts` 的解码规则及测试,不可仅扩展 diff-transform 的布局表。 结果三态:`equivalent` / `different`(带第一处差异:函数、行号、两侧内容,或缓冲区条目)/ `dump-failed`(无法解析完整语义数据、dump 进程失败/超时/提前结束;带原因或 stderr 末行)。后两种都放弃 base,但日志分开。 @@ -51,6 +51,8 @@ v98 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape **v98 函数头快照边界**:当前大头按 37 字节、flags 位于 `[36]` 读取,小头 cache 位域为 6/1/1,绑定 `250829098` 稳定快照。上游 [7193d4485b](https://github.com/facebook/hermes/commit/7193d4485beeb87cd7a3b6ca8b6b5d97a1a433c4) 删除 `NumCacheNewObject` 后,仍报 v98 的构建曾使用 36 字节 / flags `[35]`、cache 位域 7/1。`hbcTransform` 的两套 v98 文件头布局不能识别这次**函数头**变化;本轮不宣称支持该后续快照,也不能仅凭文件头或版本号选择它。更换编译器时必须补对应的大头、小头和真实 Metro 测试;未审核的 v98 构建应使用 `--hermesBase none`。 +**v99 函数头**:`hermes-compiler@260318099.0.4`(RN 0.88)是上述删除之后的首次版本号升级,文件头与段布局同晚期 v98(`hbcTransform` 共用一条布局),函数头一律是删除后的形态:大头 36 字节、flags 位于 `[35]`,小头 cache 位域 7/1。v99 新增的 typed 模式 `NewTypedObjectWithBuffer` 的字面量操作数尚未解码,遇到即 `dump-failed` 回退 plain。 + pretty 输出本身会截断长字符串与 BigInt、用函数名替代函数索引,并可能把 `-0` 显示为 `0`,因此不再以 pretty 相等作为最终结论。新增归一化规则时必须同时添加真实 HBC 负例,确保没有把语义差异折叠掉。 ## 3. 已完成与待办 @@ -70,7 +72,7 @@ SELECT hermesBaseOutcome, COUNT(*) FROM versions 旧版服务端忽略未知字段,CLI 先于服务端发布也安全。 -### 3.2 指令级缓冲区偏移 —— 已完成(v87–96、v98) +### 3.2 指令级缓冲区偏移 —— 已完成(v87–96、v98、v99) 见 §2"字面量按指令比较"。v98 最初按 v96 的 tag 语义解码,模糊测试(300 轮,RN 0.87 的 hermes-compiler)报出 259 次对象值不一致——全部源于 tag 类型 6 在 v98 表示 `undefined` 而非 1 字节字符串 id;修正后 v98 两个种子共 800 轮:0 次差异、75 处植入差异全部抓到。 @@ -90,9 +92,10 @@ raw 核对保留函数引用索引,与出现顺序对齐的函数体及二进 - **资源开销**:base/plain 编译并发完成后,执行 pretty 和 raw 两遍 dump;每遍两个进程,raw 不增加编译。二进制元数据读取目前持有两份 HBC、完整字符串映射及字面量缓冲区,反汇编仅保留当前函数;这是完整数据核对的额外内存和时间开销。不要以删除验证数据来优化内存,可后续改为按段读取或降低并行度。 - **进程期限**:版本探测默认 30 秒(`PUSHY_HERMES_PROBE_TIMEOUT_MS`),完整校验两遍合计默认 120 秒(`PUSHY_HERMES_VERIFY_TIMEOUT_MS`),单个编译/源码映射子进程默认 300 秒(`PUSHY_HERMES_COMPILE_TIMEOUT_MS`)。环境变量单位均为毫秒,必须是 1–2147483647 的整数,否则用默认值。超时终止子进程,优化失败回退 plain;真正的 plain 编译或最终 sourcemap 失败仍使构建失败。校验函数还接受 `AbortSignal`;base 下载任务的取消传播尚未统一。 - **源码映射竞态**:推测执行的 base sourcemap 合成任务启动时立即观察拒绝,之后再根据最终采用哪份字节码决定抛出错误还是为 plain 重做合成。 -- **CI**:`hermes-hbc-96` / `hermes-hbc-98` / `hermes-hbc-98-patch17` 分别安装固定的 `react-native@0.77.3` / `hermes-compiler@250829098.0.16` / `.17`。除小程序回归与 50 轮固定种子 fuzz 外,三组都执行真实 Metro bundle 的自比和 base/plain 比较;v98 测试断言产物确实包含零长度函数。缺编译器或指定的 fixture 缺失/哈希不符会失败,不静默跳过。 +- **CI**:`hermes-hbc-96` / `hermes-hbc-98` / `hermes-hbc-98-patch17` / `hermes-hbc-99` 分别安装固定的 `react-native@0.77.3` / `hermes-compiler@250829098.0.16` / `.17` / `hermes-compiler@260318099.0.4`。除小程序回归与 50 轮固定种子 fuzz 外,四组都执行真实 Metro bundle 的自比和 base/plain 比较;v98 测试断言产物确实包含零长度函数。缺编译器或指定的 fixture 缺失/哈希不符会失败,不静默跳过。 - **真实 Metro 数据**:[hbc-diff-benchmark](https://github.com/sunnylqm/hbc-diff-benchmark/tree/e6a870a1c26c4b64c7860d7e1aa575707d22ad88) 的 `base.jsbundle` 和 `s3-medium-feature.jsbundle`(MIT,保留其 LICENSE;来源与生成步骤见该仓库 `fixtures/GENERATION.md`)。CI 固定提交和 SHA256,不运行下载的 JS,仅交给固定编译器。离线运行:`HERMESC= HERMES_METRO_FIXTURES= bun test tests/hermes-metro.test.ts`;普通单元测试不联网下载。 - **调试输出异常回归**:`tests/hermes-blockers.test.ts` 在独立进程覆盖带 `dumpTo` 的超时、运行中取消、预取消、ENOENT,同时检查返回标记、退出状态和晚到的异步错误。错误处理立即 unpipe 全部目的地并结束 debug 文件,避免 `finish()` 等待一个只会在后续 `kill()` 中结束的流;Node 18 CI 也执行相同场景。 +- **不要删掉 pretty 这一遍**:raw 指令归一化不覆盖异常处理表等信息,pretty 对比仍是必要的一层(PR #85 评审结论)。 - **`hbcdump`/`hbc-diff`**:Hermes 仓库自带的工具,RN 的 hermesc 不随附;如果将来 hermes-compiler 包里带上,可替代文本 dump 解析。 ## 4. 明确接受的剩余风险 @@ -104,6 +107,6 @@ raw 核对保留函数引用索引,与出现顺序对齐的函数体及二进 1. 看日志里的 detail:函数 + 行号 + 两侧内容,或缓冲区条目,或 dump 失败原因。 2. 两行只差宽度后缀/数字 id/偏移:先核对解析后的完整值与控制流目标。只有证明是表示差异后才能补归一化规则,不能直接删除操作数;同时添加负例与 fuzz。 3. 指令数量/顺序/opcode 不同,或某条 `New*WithBuffer` 解析后的字面量不同(detail 形如 `NewArrayWithBuffer r2 size=44 n=44 entry 7: [String "a"] vs [String "b"]`)→ 真差异,回退正确,向 Hermes 上游报告。detail 带 `undecodable` 说明无法解码,归为 `dump-failed`;可能是损坏的 HBC,也可能是验证器尚不支持的布局,不能直接断言上游错误。 - 若日志/元数据里 `literals` 为 `buffer`(文件结构不识别),缓冲区是按整段文本比的,`Array Buffer entry N` 形式的差异可能是 §2 的重叠误杀,需人工用二进制解码核对。 + detail 形如 `Array Buffer entry N: … vs …` 时,说明文件结构未被识别、缓冲区是按整段 dump 文本比的(比较结果的 `literals` 为 `buffer`,该字段不进日志),差异可能是 §2 的重叠误杀,需人工用二进制解码核对。 4. `dump-failed` → 校验无法完成;检查解析原因、编译器版本、超时与 stderr。它不是等价证据,也不必然是与字节码无关的进程错误。 5. 需要完整现场:`PUSHY_HERMES_BASE_DEBUG=1 pushy bundle …`,两份反汇编在中间目录旁 `hermes-base-dump-{base,plain}.txt`。 diff --git a/docs/hermes-review-follow-up.md b/docs/hermes-review-follow-up.md deleted file mode 100644 index 604207b..0000000 --- a/docs/hermes-review-follow-up.md +++ /dev/null @@ -1,52 +0,0 @@ -# PR #85 review follow-up - -## Changes - -- `CacheNewObject` now resolves its shape index into the ordered, fully decoded - property keys in the pretty pass. Both registers and the per-function cache - index remain part of the comparison. Unknown shapes, undecodable keys and - unsupported operands fail closed. With no binary resolver, the shape is only - folded for diagnostics; the existing binary-data and raw-audit gates still - forbid text-only equivalence. -- Fuzz success requires a positive integer round count, all requested positive - comparisons, zero compilation/comparison failures, at least one effective - planted difference, and no missed or unbuildable negatives. Optimized-away - differences do not count. The last round also attempts a negative, so runs - shorter than ten rounds are not structurally unable to exercise rejection. -- New regressions cover shape-index relocation, key changes, key order, - register/cache preservation, malformed/missing references, text-only fallback, - and fuzz runs with zero or incomplete useful coverage. -- Enforcing that gate exposed a pre-existing generator defect in the HBC 96 - CI run (seed 96, round 41): the quoted-string regex matched a gap after an - escaped quote and inserted `~` before a quoted property name. String mutation - and negative planting now use real string-token boundaries from the already - installed Babel parser and JSON-encode replacements. The minimized regression - also checks comments, regexps, templates, escapes and UTF-16 source offsets. - The failure gate remains strict; the generator is fixed rather than skipping - the failing round or changing the seed. - -## Scope and evidence - -The CacheNewObject regression uses synthetic instruction bytes and binary literal -sections to exercise the production pretty and raw normalizers. It does not claim -that a source-level fixture was compiled into CacheNewObject by a real hermesc. -The existing pinned HBC 96/98 compiler CI remains required before merging. - -Unknown-opcode semantic coverage remains a separate follow-up, not a verified -current-compiler false-acceptance bug. Before accepting additional compiler -snapshots, audit their complete opcode/operand schemas and referenced sections. -Do not remove the whole pretty pass: information such as exception-handler tables -is not currently covered by the raw instruction normalizer alone. - -Local validation in the authoring environment is limited to Node.js execution of -transpiled helpers and synthetic fixtures; Bun and hermesc are not installed. -The PR description records the subsequent CI state separately. - -## CodeRabbit argument-validation follow-up - -Only an omitted `--rounds` flag selects the default of 200. A flag without a -value, an empty value, or a following option is rejected with exit code 2 before -creating the output directory or invoking the compiler. Four CLI regressions -use an executable compiler fixture with a call marker to verify those side -effects do not occur; the valid-count control verifies the marker does work. -Argument-reading, compilation and run-entry functions now document their contracts. diff --git a/src/utils/hermes-base.ts b/src/utils/hermes-base.ts index b42194f..5ad580d 100644 --- a/src/utils/hermes-base.ts +++ b/src/utils/hermes-base.ts @@ -2,7 +2,7 @@ // // Hermes re-sorts its string table by identifier frequency on every compile, so // any source change renumbers most string IDs and the bytecode diff explodes -// (see hermes-base-bytecode-方案.md). Compiling with the previous HBC of the +// (see docs/hermes-base-verification.md). Compiling with the previous HBC of the // same app as `-base-bytecode` keeps old IDs stable and shrinks patches 5-30×. // This module resolves that base: gating by hermesc build, HBC version probe, // server lookup, a sha256-named local cache, download + verification, and the @@ -1297,7 +1297,7 @@ export interface HermesEquivalenceResult { functions: number; /** * `instruction`: literals decoded from the binary buffers at each - * New*WithBuffer instruction (HBC v87–96 and v98); `buffer`: the dumped + * New*WithBuffer instruction (HBC v87–96, v98 and v99); `buffer`: the dumped * buffers compared as a whole (unknown layout) */ literals?: 'instruction' | 'buffer'; diff --git a/src/utils/hermes-literals.ts b/src/utils/hermes-literals.ts index 6bbb8cf..3927726 100644 --- a/src/utils/hermes-literals.ts +++ b/src/utils/hermes-literals.ts @@ -24,7 +24,7 @@ * - HBC v87–96 (`split`): arrayBuffer, objKeyBuffer, objValueBuffer. * `NewArrayWithBuffer dst, sizeHint, count, arrayOffset`; * `NewObjectWithBuffer dst, sizeHint, count, keyOffset, valueOffset`. - * - HBC v98 (`shaped`): literalValueBuffer (array elements and object values + * - HBC v98 and v99 (`shaped`): literalValueBuffer (array elements and object values * alike), objKeyBuffer, objShapeTable (8-byte entries: keyOffset u32, * numProps u32). `NewArrayWithBuffer dst, sizeHint, count, valueOffset`; * `NewObjectWithBuffer dst, shapeIndex, valueOffset` and From b66878107be1337bca6b6e163f1cf47df5185a24 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 15:15:39 +0800 Subject: [PATCH 5/6] refactor(hermes): read each compared HBC once compareHermesBytecode read both files whole for the semantic data, then reopened them to read the header and three literal sections again. The literal buffers are now views into the bytes already in memory (literalBuffersFromBytes / hermesSemanticData); the file-based readLiteralBuffers and readHermesSemanticData stay as thin wrappers. Co-Authored-By: Claude Opus 5.5 --- src/utils/hermes-base.ts | 34 ++++++++++------ src/utils/hermes-literals.ts | 79 ++++++++++++++---------------------- src/utils/hermes-raw.ts | 6 ++- 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/src/utils/hermes-base.ts b/src/utils/hermes-base.ts index 5ad580d..e34eb1e 100644 --- a/src/utils/hermes-base.ts +++ b/src/utils/hermes-base.ts @@ -35,9 +35,9 @@ import { normalizeCachedObjectInstruction } from './hermes-cached-object'; import { type LiteralBuffers, LiteralResolver, - readLiteralBuffers, + literalBuffersFromBytes, } from './hermes-literals'; -import { auditRawHermesBytecode, readHermesSemanticData } from './hermes-raw'; +import { auditRawHermesBytecode, hermesSemanticData } from './hermes-raw'; import { hermesTimeout } from './hermes-timeout'; import { t } from './i18n'; import { webFetch } from './runtime'; @@ -1626,16 +1626,26 @@ export async function compareHermesBytecode( killSignal: 'SIGKILL', }, ); - // The literal buffers are read from both files up front (header + three - // byte ranges). Only when both sides can be read do the instructions - // compare decoded literals; otherwise both fall back to the dumped buffers - // as a whole, so the two sides always normalize the same way. - const [buffersA, buffersB, dataA, dataB] = await Promise.all([ - readLiteralBuffers(withBase).catch(() => null), - readLiteralBuffers(plain).catch(() => null), - readHermesSemanticData(withBase).catch(() => null), - readHermesSemanticData(plain).catch(() => null), - ]); + // Each file is read once; its literal buffers and semantic data both come + // from those bytes. Only when both sides' buffers can be read do the + // instructions compare decoded literals; otherwise both fall back to the + // dumped buffers as a whole, so the two sides always normalize the same way. + const [bytesA, bytesB] = await Promise.all( + [withBase, plain].map((file) => + fs.promises.readFile(file).catch(() => null), + ), + ); + const orNull = (read: () => T): T | null => { + try { + return read(); + } catch { + return null; + } + }; + const buffersA = bytesA && orNull(() => literalBuffersFromBytes(bytesA)); + const buffersB = bytesB && orNull(() => literalBuffersFromBytes(bytesB)); + const dataA = bytesA && orNull(() => hermesSemanticData(bytesA)); + const dataB = bytesB && orNull(() => hermesSemanticData(bytesB)); const binary = buffersA && buffersB && buffersA.version === buffersB.version; const literals = binary ? 'instruction' : 'buffer'; const a = new DumpReader( diff --git a/src/utils/hermes-literals.ts b/src/utils/hermes-literals.ts index 3927726..bfec1bd 100644 --- a/src/utils/hermes-literals.ts +++ b/src/utils/hermes-literals.ts @@ -30,8 +30,8 @@ * `NewObjectWithBuffer dst, shapeIndex, valueOffset` and * `NewObjectWithBufferAndParent dst, parent, shapeIndex, valueOffset`. */ -import { open as openFile } from 'node:fs/promises'; -import { HBC_HEADER_SIZE, resolveHbcSections } from './hbcTransform'; +import { readFile } from 'node:fs/promises'; +import { resolveHbcSections } from './hbcTransform'; const TAG_TYPE_MASK = 0x70; const TAG_LENGTH_CONTINUES = 0x80; @@ -75,57 +75,38 @@ export type LiteralBuffers = const SHAPE_ENTRY_SIZE = 8; /** - * Read only the literal buffers of an HBC file (header + three byte ranges, - * never the whole file). Null for a file whose layout is unknown: the caller - * then falls back to comparing the dumped buffers as a whole. + * The literal buffers of an HBC file's bytes, as views into them. Null for a + * file whose layout is unknown: the caller then falls back to comparing the + * dumped buffers as a whole. */ +export function literalBuffersFromBytes(bytes: Buffer): LiteralBuffers | null { + const resolved = resolveHbcSections(bytes, bytes.length); + if (!resolved) return null; + // resolveHbcSections keeps every section inside the file + const views = (names: string[]) => { + const ranges = names.map((name) => resolved.sections.get(name)); + return ranges.every(Boolean) + ? ranges.map((r) => bytes.subarray(r!.start, r!.start + r!.size)) + : null; + }; + const { version } = resolved; + if (resolved.sections.has('objShapeTable')) { + const bufs = views(['literalValueBuffer', 'objKeyBuffer', 'objShapeTable']); + if (!bufs) return null; + const [values, objectKeys, shapes] = bufs; + return { layout: 'shaped', version, values, objectKeys, shapes }; + } + const bufs = views(['arrayBuffer', 'objKeyBuffer', 'objValueBuffer']); + if (!bufs) return null; + const [array, objectKeys, objectValues] = bufs; + return { layout: 'split', version, array, objectKeys, objectValues }; +} + +/** {@link literalBuffersFromBytes} for a file. */ export async function readLiteralBuffers( file: string, ): Promise { - const handle = await openFile(file, 'r'); - try { - const { size } = await handle.stat(); - if (size < HBC_HEADER_SIZE) return null; - const header = Buffer.alloc(HBC_HEADER_SIZE); - const read = await handle.read(header, 0, HBC_HEADER_SIZE, 0); - if (read.bytesRead !== HBC_HEADER_SIZE) return null; - const resolved = resolveHbcSections(header, size); - if (!resolved) return null; - const readRanges = async (names: string[]) => { - const ranges = names.map((name) => resolved.sections.get(name)); - if (ranges.some((r) => !r)) return null; - const bufs = await Promise.all( - ranges.map(async (r) => { - const buf = Buffer.alloc(r!.size); - if (r!.size === 0) return buf; - const { bytesRead } = await handle.read(buf, 0, r!.size, r!.start); - return bytesRead === r!.size ? buf : null; - }), - ); - return bufs.some((b) => !b) ? null : (bufs as Buffer[]); - }; - const { version } = resolved; - if (resolved.sections.has('objShapeTable')) { - const bufs = await readRanges([ - 'literalValueBuffer', - 'objKeyBuffer', - 'objShapeTable', - ]); - if (!bufs) return null; - const [values, objectKeys, shapes] = bufs; - return { layout: 'shaped', version, values, objectKeys, shapes }; - } - const bufs = await readRanges([ - 'arrayBuffer', - 'objKeyBuffer', - 'objValueBuffer', - ]); - if (!bufs) return null; - const [array, objectKeys, objectValues] = bufs; - return { layout: 'split', version, array, objectKeys, objectValues }; - } finally { - await handle.close(); - } + return literalBuffersFromBytes(await readFile(file)); } /** diff --git a/src/utils/hermes-raw.ts b/src/utils/hermes-raw.ts index a805c9c..a4da456 100644 --- a/src/utils/hermes-raw.ts +++ b/src/utils/hermes-raw.ts @@ -58,7 +58,11 @@ export interface HermesSemanticData { export async function readHermesSemanticData( file: string, ): Promise { - const bytes = await readFile(file); + return hermesSemanticData(await readFile(file)); +} + +/** {@link readHermesSemanticData} for HBC bytes already in memory. */ +export function hermesSemanticData(bytes: Buffer): HermesSemanticData { const resolved = requireValue( resolveHbcSections(bytes, bytes.length), 'unsupported or unreadable HBC layout', From d76677958b0b81129b8063f892f0b4affbd25ab3 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 15:16:11 +0800 Subject: [PATCH 6/6] refactor(hermes): one width-suffix fold and one function-header test The pretty (hermes-base) and raw (hermes-raw) dump readers each had their own operand-width suffix fold and function-header regex; hermes-raw now exports both. The two line splitters stay separate: only the raw one strips CR, and unifying them would change the pretty pass on CRLF output, which no test or CI job exercises. Co-Authored-By: Claude Opus 5.5 --- src/utils/hermes-base.ts | 20 ++++++-------------- src/utils/hermes-raw.ts | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/utils/hermes-base.ts b/src/utils/hermes-base.ts index e34eb1e..0a45d16 100644 --- a/src/utils/hermes-base.ts +++ b/src/utils/hermes-base.ts @@ -37,7 +37,12 @@ import { LiteralResolver, literalBuffersFromBytes, } from './hermes-literals'; -import { auditRawHermesBytecode, hermesSemanticData } from './hermes-raw'; +import { + auditRawHermesBytecode, + foldWidthSuffix, + hermesSemanticData, + isFunctionHeader, +} from './hermes-raw'; import { hermesTimeout } from './hermes-timeout'; import { t } from './i18n'; import { webFetch } from './runtime'; @@ -1252,15 +1257,6 @@ function isLetter(code: number): boolean { } /** strip one operand-width suffix (LongIndex, Long, Short) off an opcode */ -function foldWidthSuffix(opcode: string): string { - for (const suffix of ['LongIndex', 'Long', 'Short']) { - if (opcode.length > suffix.length && opcode.endsWith(suffix)) { - return opcode.slice(0, -suffix.length); - } - } - return opcode; -} - /** Split a readable stream into lines without readline (Bun's readline async iterator misbehaves when pulled manually). */ async function* streamLines( stream: NodeJS.ReadableStream, @@ -1334,10 +1330,6 @@ const DUMP_STDERR_KEEP = 4 * 1024; /** a diagnostic line is cut to this many characters */ const DETAIL_LINE_MAX = 120; -function isFunctionHeader(line: string): boolean { - return /^(?:Function|NCFunction|Constructor) suffix.length && opcode.endsWith(suffix)) { + return opcode.slice(0, -suffix.length); + } + } + return opcode; +} + +/** The first line of a function in either dump. */ +export function isFunctionHeader(line: string): boolean { + return /^(?:Function|NCFunction|Constructor) o.value); const refs = STRING_OPERANDS[op] ?? []; const operands: unknown[] = inst.operands.map((operand, i) => { @@ -568,7 +579,7 @@ export async function auditRawHermesBytecode( const normalize = (body: string[]) => normalizeRawHermesFunction(body, data[side], index++, buffers[side]); for await (const line of linesOf(children[side].stdout!)) { - if (/^(?:Function|NCFunction|Constructor)