jscd 0.3.1

Reverse bytenode-compiled .jsc (V8 code cache) back to JavaScript
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# jscd

把 [bytenode](https://github.com/bytenode/bytenode) 编译的 `.jsc` 还原成 JavaScript。纯静态解析:
纯 Rust 单二进制,不需要打过补丁的 V8,也不需要 Node 运行时、不联网。

支持 **Node 8.0.0 → 26.13.0**(519 个发布 / 36 个 V8 minor)与 **Electron 11、19 → 45.2**
(V8 8.7 – 15.6,含指针压缩与 V8 sandbox 构建;Electron 11 与 20–45 每个主版本都跑同一套 9 个
行为 fixture,逐用例 9/9),**≈ 25k 份 `.jsc` 通过测试、0 失败**
(32 条版本线 × 41 份用例,按 bytenode 1.7.0 的编译参数);两层优化(寄存器折叠 → cure 简化)
提升反编译源码的可读性。

[English](README.md) · **中文**

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust 1.96+](https://img.shields.io/badge/rust-1.96%2B-orange.svg)](Cargo.toml)
[![Node 8.0 – 26.13](https://img.shields.io/badge/Node-8.0%20%E2%80%93%2026.13-green.svg)](#支持的版本)
[![V8 5.8 – 14.6](https://img.shields.io/badge/V8-5.8%20%E2%80%93%2014.6-green.svg)](#支持的版本)
[![release](https://github.com/ejfkdev/jscd/actions/workflows/release.yml/badge.svg)](https://github.com/ejfkdev/jscd/actions/workflows/release.yml)
[![crates.io](https://img.shields.io/crates/v/jscd.svg)](https://crates.io/crates/jscd)

**目录:**[特性](#特性) · [快速上手](#快速上手) · [安装](#安装) · [用法](#用法) ·
[CLI 参考](#cli-参考) · [支持的版本](#支持的版本) · [实现](#实现) · [验证](#验证) ·
[仓库结构](#仓库结构) · [已知限制](#已知限制) · [参与贡献](CONTRIBUTING.md)

## 特性

- **覆盖全** —— Node 8.0.0 → 26.13.0(519 个发布 / 36 个 V8 minor)都能识别;表范围外的 V8 明确报错,不猜。
- **产物能跑** —— `--runtime` 补上 `__runtime` 桩,反编译结果直接 `node` 可跑;`--verify` 再过一遍
  `node --check`。
- **产物能读** —— 两层优化把字节码的寄存器搬运折回表达式(寄存器折叠 → cure 简化);
  `JSCD_NO_OPT=1` 可对照原样。
- **名字尽量还原** —— 作用域槽名反推、内建名查只读堆名表;绝不输出会抛 `ReferenceError` 的标识符。
- **单文件零依赖** —— 纯 Rust:不需要 Node、不联网、不打补丁;Linux 静态二进制压完约 2.8 MB。
- **验证可复现** —— 32 条版本线 × 41 份用例逐用例对拍 + 语料扫描,每个数字都能自己复跑(见[验证]#验证)。

## 快速上手

```console
$ cat hello.js
function greet(name) {
  return "hello " + name;
}
console.log(greet("world"));

$ npm i -g bytenode && bytenode -c hello.js      # 本文用 Node 20.20.2
$ xxd -l 48 hello.jsc
00000000: cc05 dec0 0bc2 e400 9200 0000 e521 2eaa  .............!..
00000010: f002 0000 0000 0000 011c 5401 2006 a860  ..........T. ..`
00000020: 0000 0000 0600 0000 010c 4c60 0000 0000  ..........L`....

$ jscd hello.jsc
console.log(greet("world"));
function greet(a0) {
    return "hello " + a0;
}

$ jscd hello.jsc --runtime > hello.out.js && node hello.out.js
hello world
```

`jscd info hello.jsc` 能解出上面的头部(样例见[用法](#用法));`JSCD_NO_OPT=1` 则可看优化前的原样。

## 安装

**macOS / Linux —— Homebrew**

```sh
brew install ejfkdev/tap/jscd
```

**Windows —— Scoop**

```powershell
scoop bucket add ejfkdev https://github.com/ejfkdev/scoop-bucket
scoop install jscd
```

不先加 bucket、直接按清单 URL 装也可以:

```powershell
scoop install https://raw.githubusercontent.com/ejfkdev/scoop-bucket/main/bucket/jscd.json
```

**预编译产物** —— 见 [Releases](https://github.com/ejfkdev/jscd/releases/latest)。

**cargo**(任何有 Rust 1.96+ 的平台)

```sh
cargo install jscd        # 从 crates.io 源码构建
cargo binstall jscd       # 或者:直接拉 release 里的可执行文件,不编译(cargo-binstall)
```

**源码构建**

```sh
cargo install --git https://github.com/ejfkdev/jscd     # 直接装 main
git clone https://github.com/ejfkdev/jscd && cd jscd
cargo build --release        # -> target/release/jscd
cargo install --path .       # ……或把这份构建装进 ~/.cargo/bin
```

需要 Rust 1.96+(工作区工具链版本;cure 本身要求 1.74),无系统依赖。

<details>
<summary>发版(维护者)</summary>

`scripts/release.sh vX.Y.Z` 先过发版门禁(`cargo test --release`、
`clippy --all-targets -- -D warnings`,以及端到端冒烟 `scripts/ci_smoke.sh` —— fixture 真编译成
`.jsc` → 反编译 → 语法门禁 + 整脚本行为对拍),再推一个**带说明的 annotated tag**;tag 说明就是
GitHub Release 的描述。tag 一推即触发 `.github/workflows/release.yml`,在上面那六个平台重建裸
二进制。之后 `cargo publish` 发到 crates.io(`release.sh` 保证 tag 与 `Cargo.toml` 版本一致),
再跑一次 `python3 scripts/update_readme_help.py` 刷新下面的 CLI 帮助;
[Homebrew tap](https://github.com/ejfkdev/homebrew-tap) 与
[Scoop bucket](https://github.com/ejfkdev/scoop-bucket) 每天自动更新时会带上新版本。

</details>

## 用法

```sh
jscd app.jsc                     # 单文件反编译到 stdout
jscd app.jsc app.js              # ……写到文件
jscd dist/                       # dist/ 下所有 .jsc → dist-out/(保持层级)
jscd dist/ out/                  # ……改写到 out/
jscd info app.jsc                # 头部字段与识别出的 Node/V8 版本
jscd disasm --filter main app.jsc
jscd ro-map probe.jsc > m.json   # 生成只读堆名表
jscd --help                      # 双语帮助(-h、`help`、`help <子命令>`)
```

- **输入**是一个 `.jsc` 文件,或目录(递归找 `*.jsc`);**输出**默认:文件 → stdout,目录 → 输入同级的
  `<输入名>-out/`(保持层级)—— `-o` / 位置参数 OUTPUT 可指定文件或目录,`-` 表示 stdout。
- 表范围外的 V8 **直接报错**,报错里点名识别到的版本与支持范围(`jscd info` 的 `supported: yes|no`
  同理),不会静默吐一份只有运行时前导的文件。
- 只读堆里的内建名(Node 22+)在**内嵌名表适用的平台(macOS)、非压缩构建**自动还原(只读堆编号跟
  构建与架构走);别的平台/构建用 `jscd ro-map` 自己建一张再 `--ro-map` 传入(`JSCD_NO_RO_MAP=1`
  可关掉内嵌表)。**指针压缩构建(Electron/Chrome)永远不套用**内嵌表:它们与 Node 共用同一段 V8
  版本字符串,只读堆布局却不同。同一张表还带**字符串根**修正(`roots`):根下标在构建之间会位移,
  用自己构建生成的名表才能把 `"0"` 还原成 `"0"`,而不是别的构建在那个下标上存的名字。平台/tagged
  size 对不上的表会被忽略;**同一个 V8 版本字符串**也可能来自不同的 Node 补丁构建(`-node.NN`),
  只读堆地址照样会位移 —— 看到 `<ro…>` 占位就用自己构建的探针重建一张。
- 界面语言跟随 `JSCD_LANG`(再依次看 `LC_ALL` / `LC_MESSAGES` / `LANGUAGE` / `LANG` / `LC_CTYPE`):
  `zh*` 中文、其余英文,`JSCD_LANG=zh|en` 可强制。

| 旗标 | 适用 | 作用 |
| --- | --- | --- |
| `-o, --output <路径>` | 全部形态 | 输出文件 / 目录 / `-`(与位置参数 OUTPUT 等价) |
| `--quiet` | 目录输入 | 不打印逐文件进度 |
| `--json` | 全部子命令 | 机器可读输出 |
| `--filter <子串>` | `disasm` | 只输出名字含该子串的函数 |
| `--ro-map <路径>` | `decompile` | 只读堆名表,由 `jscd ro-map` 生成 |
| `--verify` | `decompile` | 语法门禁:产物过一遍 `node --check`(需要 PATH 里有 `node`) |
| `--runtime` | `decompile` | 保留可运行前导(`__runtime` 占位实现 + 上下文变量提升 + 别名块)。**默认不带**;想让产物能 `node` 跑就加上 |
| `-h, --help` | 全部形态 | 打印帮助(`jscd help <子命令>` 看单个命令) |
| `-v, -V, --version` | 全部形态 | 打印名字、版本与仓库地址 |

## CLI 参考

`--help` 与 `help <子命令>` 的输出原样收在这里(刷新用 `python3 scripts/update_readme_help.py`)。

<!-- BEGIN help:cli -->
<details>
<summary>完整 CLI 帮助 —— <code>jscd --help</code> 与每个子命令,原样输出</summary>

**`jscd --help`**

```console
$ jscd --help
jscd 0.3.1 — V8 代码缓存 → JavaScript
https://github.com/ejfkdev/jscd  (MIT license)

把 bytenode 编译的 .jsc(V8 代码缓存)还原成 JavaScript。纯静态解析 ——
不需要打过补丁的 V8,也不需要 Node 运行时;目录输入会递归处理。

用法:jscd [选项] <输入> [输出]              # 默认动作就是反编译
      jscd <子命令> [参数...]              # 分阶段分析
      jscd help [子命令] | version | -h | -v | -V

输入    .jsc 文件,或目录(递归找 *.jsc)。
输出    文件、目录,或 `-` 表示 stdout:
          <文件.js>   单个输入文件时写这个文件
          <目录>      目录输入时写入该目录,保持层级
          默认:文件输入 → stdout;目录输入 → 输入同级的 <输入名>-out/

选项:
  -o, --output <路径>   输出文件 / 目录 / -(与位置参数 OUTPUT 等价)
      --ro-map <路径>   只读堆名表(decompile 用,还原内建属性名)
      --verify          用 `node --check` 实编译校验产物(decompile)
      --runtime         保留可运行前导(`__runtime` 占位实现),产物能直接 `node` 跑;
                        默认**不带**,只输出还原出来的代码本身
      --filter <子串>   只输出名字含该子串的函数(disasm)
      --json            机器可读输出(info/strings/functions/disasm/ro-map)
      --quiet           不打印逐文件进度(目录输入时)
  -h, --help            打印本帮助
  -v, -V, --version     打印名字、版本与仓库地址
      JSCD_LANG=zh|en   强制帮助/报错的语言(默认自动识别)

子命令:
  decompile   重建 JavaScript(默认动作,等价于直接 `jscd <输入>`)
  info        头部字段与识别出的 Node/V8 版本
  strings     常量池里的字符串与符号
  functions   SharedFunctionInfo 树(名字、形参、字节码长度)
  disasm      字节码反汇编(View8 兼容文本)
  ro-map      从探针缓存生成只读堆名表
  help        打印帮助(可跟子命令名)
  version     打印名字、版本与仓库地址

示例:
  jscd app.jsc                        反编译并打到 stdout
  jscd app.jsc app.js                 反编译到文件
  jscd dist/                          dist/**.jsc → dist-out/**.js
  jscd dist/ out/                     ……改写到 out/
  jscd decompile --ro-map m.json a.jsc   带只读堆名表
  jscd info app.jsc                   头部字段与 V8 版本
  jscd disasm --filter main app.jsc   只看某个函数的字节码
  jscd help disasm                    单个子命令的详细帮助(带示例)
  jscd ro-map probe.jsc -o names.json
                                      生成只读堆名表(见 `jscd help ro-map`)
```

**`jscd help decompile`**

```console
$ jscd help decompile
jscd decompile — 重建 JavaScript(默认动作,等价于直接 `jscd <输入>`)
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd decompile [选项] <输入> [输出]
      jscd [选项] <输入> [输出]           # 等价写法

输入是 .jsc 文件,或目录(递归找 *.jsc)。输出是文件、目录或 `-`(stdout)。
默认:文件输入 → stdout;目录输入 → 输入同级的 <输入名>-out/。

选项:
  -o, --output <路径>   输出文件 / 目录 / -(默认 stdout)
      --ro-map <路径>   只读堆名表(`jscd ro-map` 生成);
                        用来还原内建属性名(`a.length`、`o.push`)
      --verify          用 `node --check` 实编译校验产物
      --runtime         保留可运行前导(`__runtime` 占位实现),产物能直接跑;
                        默认不带,只输出还原出来的代码
      --quiet           不打印逐文件进度(目录输入时)

示例:
  jscd decompile app.jsc                 反编译并打到 stdout
  jscd decompile app.jsc app.js          写到 app.js
  jscd decompile dist/                   dist/**.jsc -> dist-out/**.js
  jscd --ro-map names.json a.jsc > a.js  带只读堆名表
  jscd decompile --runtime --verify a.jsc -o a.run.js
                                         可运行、已语法校验的产物

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help info`**

```console
$ jscd help info
jscd info — 头部字段与识别出的 Node/V8 版本
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd info [选项] <文件>

打印代码缓存头部:magic、版本哈希、V8/Node 版本、旗标、payload 大小,
以及当前版本是否支持该 V8 版本。

选项:
  -o, --output <路径>   写到文件('-' = stdout;默认就是 stdout)
      --json            单个 JSON 对象:file、size_raw、brotli、
                        version_hash、v8、node、supported 等

示例:
  jscd info app.jsc                      头部字段与 V8 版本
  jscd info --json app.jsc | jq .supported   这个 V8 版本受支持吗?

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help strings`**

```console
$ jscd help strings
jscd strings — 常量池里的字符串与符号
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd strings [选项] <文件>

把该缓存能引用到的字符串/符号常量逐行列出。JSON 里的 `from_roots`
是从根函数可达的那一部分。

选项:
  -o, --output <路径>   写到文件('-' = stdout;默认就是 stdout)
      --json            {"count":N,"strings":[...],"from_roots":[...]}

示例:
  jscd strings app.jsc                   所有常量,一行一个
  jscd strings --json app.jsc | jq .count    数一数有多少条

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help functions`**

```console
$ jscd help functions
jscd functions — SharedFunctionInfo 树(名字、形参、字节码长度)
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd functions [选项] <文件>

列出 SharedFunctionInfo 树:id、名字、形参个数、字节码长度、栈帧大小、
是否已编译、嵌套深度。

选项:
  -o, --output <路径>   写到文件('-' = stdout;默认就是 stdout)
      --json            {"count":N,"functions":[{"id","name","params"、
                        "bytecode_length"、"frame_size"、"compiled"、"depth"}]}

示例:
  jscd functions app.jsc                 整棵 SFI 树
  jscd functions --json app.jsc | jq '.functions[].name'

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help disasm`**

```console
$ jscd help disasm
jscd disasm — 字节码反汇编(View8 兼容文本)
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd disasm [选项] <文件>

打印 View8 兼容的字节码反汇编:默认每个函数都打,`--filter` 只看
名字含该子串的那些。

选项:
      --filter <子串>   只输出名字含该子串的函数
  -o, --output <路径>   写到文件('-' = stdout;默认就是 stdout)
      --json            {"disassembly":"<完整反汇编文本>"}

示例:
  jscd disasm app.jsc                    完整字节码清单
  jscd disasm --filter main app.jsc      只看名字含 main 的函数
  jscd disasm --json app.jsc > d.json    机器可读的清单

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help ro-map`**

```console
$ jscd help ro-map
jscd ro-map — 从探针缓存生成只读堆名表
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd ro-map [选项] <探针.jsc>

走一遍探针缓存的只读堆,写出名表:产物里 `a.length` 不再退化成 `<ro0_1120>`,
`"0"` 也不会被别的构建的名字顶替。分两段:`entries`(只读堆偏移 -> 名字)
与 `roots`(根下标 -> 名字,修 `String:` 段在构建间的位移)。每个构建建一张
(探针构造方法见 docs/VERSIONS.md)。

选项:
  -o, --output <路径>   把名表写到文件(默认 stdout)
      --json            {"schema","v8","tags","entries":{"0/1120":"length"},"roots":{"571":"0"}}

示例:
  jscd ro-map probe.jsc -o names.json    生成一张名表
  jscd ro-map probe.jsc | head           瞄一眼名表
  jscd ro-map --json probe.jsc | jq '.roots'   只看字符串根修正

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help help`**

```console
$ jscd help help
jscd help — 打印帮助(可跟子命令名)
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd help [子命令]

打印主帮助;带子命令名时打印那个子命令的帮助(用法、选项、示例)。

示例:
  jscd help                              主帮助
  jscd help disasm                       disasm 的详细帮助(带示例)

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

**`jscd help version`**

```console
$ jscd help version
jscd version — 打印名字、版本与仓库地址
https://github.com/ejfkdev/jscd  (MIT license)

用法:jscd version

打印名字、版本、仓库地址与许可(`-v` / `-V` / `--version` 同效)。

示例:
  jscd version                           与 `jscd -v` 相同

全局:每个子命令都支持 --json;子命令与全局选项见 `jscd --help`。
```

</details>
<!-- END help:cli -->


默认输出**只有还原出来的代码本身**:没有运行时前导,也没有注释(连结构性分隔注释也会被 AST 层
清掉;`JSCD_NO_OPT=1` 才保留)。加 `--runtime` 才会补上 `__runtime` 代理(字节码调到的 V8 内建给
最小实现)、被摊平的上下文变量声明和名字别名块,这样产物可以直接用 `node` 运行。

<details>
<summary><code>jscd info</code> 输出样例</summary>

```console
$ jscd info hello.jsc
file:          hello.jsc
size:          776 bytes (raw) / 776 bytes (decompressed)
brotli:        no
magic:         0xc0de05cc (external refs: 0x5cc)
version_hash:  0x00e4c20b
source_hash:   0x00000092 (source length: 146 bytes, module: no)
flag_hash:     0xaa2e21e5
ro_checksum:   (absent)
payload:       752 bytes at offset 24
checksum:      0x00000000
node:          v20.20.2
v8:            11.3.244.8 (exact)
supported:     yes
```

</details>

### 两层优化

`JSCD_NO_OPT=1` 关掉两层,便于和原样对拍。

1. **文本层**(`src/opt.rs`):把字节码的寄存器搬运折回表达式 —— 只读一次的值并进使用处、之后先被重写才被读的
   存储删掉、没人再用的寄存器声明与 `DeclareGlobals` 这类簿记清掉、`let r0; … r0 = E;` 并成声明器初始化,
   并把**接收者/实参的装载在调用点按 JS 求值顺序重建**(接收者 → 属性 → 实参),不换序。判不准的一律不并:
   跨块、循环头、无括号控制体、以及之后还要用的值(含 `r2 = new r2(a0)` 这种自引用写)。
2. **AST 层**(`src/opt_js.rs`):文本再过一遍 [cure]https://github.com/ejfkdev/cure 的
   TypeScript/JavaScript 后端 —— 面向反编译产物的容错、语义保持简化器。它做常量/纯表达式折叠、
   死存储与空语句清理、控制流简化、对象/数组/类字面量规范化,并**一条语句一行**地打印(不合并成
   逗号序列、不改名):

   ```js
   // 两层跑完
   let r0 = console.log('hello world');
   function greet(a0) {
       return 'hello ' + a0;
   }
   ```

   解析失败、含不可见原文区域或结果为空 → 原样保留文本级结果,不认识这个缓存的写法也不会比原来差。

## 支持的版本

版本表由 `scripts/codegen.py` 从 Node 源码生成、编译期嵌入;识别按 V8 版本哈希走。8.0.0 起的每个
Node 发布都在 `tables/manifest.json` 里(519 条 Node + 30 条 Electron),每个 V8 minor 一张表 ——
内容相同的表只存一份。

Electron 走同一套机制:**11、19 → 45.2**(V8 8.7 – 15.6),11 与 20–45 每个主版本都有专属表
(22 复用 Node 10.8 表),每个构建都用它自己的字节码/根表 trace 校验过,**9 个 fixture 全过**。
在 Electron 主进程/渲染进程里编译的字节码(`compileElectronMainCode` /
`compileElectronRendererCode`,bytenode 2.x)与 `ELECTRON_RUN_AS_NODE` 产物布局、version_hash
相同,只有只读快照校验和不同 —— 该校验和不参与结构匹配。

**8.0.0 到 26.13.0 的每个 Node 发布都能识别**(519 个发布、90 个 V8 版本),其中 **36 个 V8 minor
里的 29 个跑完整行为矩阵**(32 条 Node 线全绿:1,144 pass / 0 fail / 8 skip / 0 known-fail)。
剩下 7 个 minor(5.8、6.0、6.1、6.6、6.7、7.0、7.4)`jscd info` 能识别,但 payload 反序列化
**显式报错**,不会输出垃圾。

| V8 minor | Node 线 | 结果 |
| --- | --- | --- |
| 6.2 → 14.6,**36 个里的 29 个** | 32 条线,8.17 → 26.13 | **全绿** —— 1,144 pass / 0 fail / 8 skip |
| 5.8、6.0、6.1、6.6、6.7、7.0、7.4 | 8.0–8.9、10.0–10.8、11.x、12.0 的部分发布 | 能识别;payload **显式拒绝** |

<details>
<summary>Node 映射:36 张版本表 → 519 个发布</summary>

| Node 发布 | V8(该线最新) | 表 |
| --- | --- | --- |
| 8.0.0–8.2.1 | 5.8.283.41 | `tables/v5_8.json` |
| 8.3.0–8.6.0 | 6.0.287.53 | `tables/v6_0_8.6.0.json` |
| 8.7.0–8.9.4 | 6.1.534.50 | `tables/v6_1_8.9.4.json` |
| 8.10.0–9.11.2 | 6.2.414.46 | `tables/v6_2.json` |
| 10.0.0–10.3.0 | 6.6.346.32 | `tables/v6_6_10.3.0.json` |
| 10.4.0–10.8.0 | 6.7.288.49 | `tables/v6_7_10.8.0.json` |
| 10.9.0–10.24.1 | 6.8.275.32 | `tables/v6_8.json` |
| 11.0.0–11.15.0 | 7.0.276.38 | `tables/v7_0_11.15.0.json` |
| 12.0.0–12.4.0 | 7.4.288.27 | `tables/v7_4_12.4.0.json` |
| 12.5.0–12.8.1 | 7.5.288.22 | `tables/v7_5.json` |
| 12.9.0–12.10.0 | 7.6.303.29 | `tables/v7_6.json` |
| 12.11.0–12.15.0 | 7.7.299.13 | `tables/v7_7_12.15.0.json` |
| 12.16.0–13.1.0 | 7.8.279.17 | `tables/v7_8.json` |
| 13.2.0–13.14.0 | 7.9.317.25 | `tables/v7_9_13.14.0.json` |
| 14.0.0–14.4.0 | 8.1.307.31 | `tables/v8_1_14.4.0.json` |
| 14.5.0 | 8.3.110.9 | `tables/v8_3.json` |
| 14.6.0–14.21.3 | 8.4.371.23 | `tables/v8_4.json` |
| 15.0.0–15.14.0 | 8.6.395.17 | `tables/v8_6_15.14.0.json` |
| 16.0.0–16.3.0 | 9.0.257.25 | `tables/v9_0_16.3.0.json` |
| 16.4.0–16.5.0 | 9.1.269.38 | `tables/v9_1_16.5.0.json` |
| 16.6.0–16.8.0 | 9.2.230.21 | `tables/v9_2.json` |
| 16.9.0–16.10.0 | 9.3.345.19 | `tables/v9_3_16.10.0.json` |
| 16.11.0–16.20.2 | 9.4.146.26 | `tables/v9_4.json` |
| 17.0.0–17.1.0 | 9.5.172.25 | `tables/v9_5_17.1.0.json` |
| 17.2.0–17.9.1 | 9.6.180.15 | `tables/v9_6_17.9.1.json` |
| 18.0.0–18.2.0 | 10.1.124.8 | `tables/v10_1.json` |
| 18.3.0–18.20.8 | 10.2.154.26 | `tables/v10_2.json` |
| 19.0.0–19.1.0 | 10.7.193.20 | `tables/v10_7_19.1.0.json` |
| 19.2.0–19.9.0 | 10.8.168.25 | `tables/v10_8_19.9.0.json` |
| 20.0.0–20.20.2 | 11.3.244.8 | `tables/v11_3.json` |
| 21.0.0–21.7.3 | 11.8.172.17 | `tables/v11_8_21.7.3.json` |
| 22.0.0–22.23.4 | 12.4.254.21 | `tables/v12_4.json` |
| 23.0.0–23.11.1 | 12.9.202.28 | `tables/v12_9_23.11.1.json` |
| 24.0.0–24.22.2 | 13.6.233.17 | `tables/v13_6.json` |
| 25.0.0–25.9.0 | 14.1.146.11 | `tables/v14_1.json` |
| 26.0.0–26.13.0 | 14.6.202.34 | `tables/v14_6_26.10.0.json` |

</details>

<details>
<summary>跑完整矩阵的 32 条 Node 线</summary>

`8.17` `10.24` `12.5` `12.9` `12.15` `12.22` `13.14` `14.0` `14.4` `14.5` `14.6` `14.21` `15.14`
`16.3` `16.5` `16.8` `16.9` `16.20` `17.1` `17.9` `18.2` `18.20` `19.1` `19.9` `20.20` `21.7`
`22.12` `23.11` `24.12` `25.9` `26.10` `26.13`

</details>

生成时**只取它需要的那 ~30 个 V8 源文件**:经 blob 过滤的部分克隆 + 本地文件缓存
(`workspace/node-src/`),不需要整份 node 检出:

```sh
python3 scripts/codegen.py --all-from 8.0.0 --donors tables/ --per-minor --keep-existing
```

逐版本的工程笔记(6.x/7.x 各代字节码差异、指针压缩构建的布局差异都记在里面):[docs/VERSIONS.md](docs/VERSIONS.md)。

## 实现

### .jsc 里有什么

里面没有源码文本;头部的 `source_hash` 存的是源码**长度**,不是哈希。能还原出来的:头部字段
(`version_hash`、`flag_hash`、payload 大小、校验和、Brotli / 原始)、`SharedFunctionInfo` 树与作用域信息
(形参 / 上下文 / 栈局部槽)、常量池、字节码数组、源码位置表与 handler 表。

`decompile` 把只以槽位存在的名字**按槽名输出**(`rN`、`__ctx.ctxN`、`_anon_N`)——不会写成会抛
`ReferenceError` 的标识符。

### 处理流程

1. **header** —— 解析 `SerializedCodeData`(V8 ≤ 11 为 24 字节,V8 ≥ 12 为 32 字节),识别
   Brotli,定位 payload。
2. **identify** —— 用内嵌索引把 `version_hash` 映射到 V8 版本;认不出就重算哈希。
3. **deserialize** —— 走 V8 序列化流(标签、回引、hot 对象环、6.x 的 `kBackrefWithSkip` 与变长
   raw)建成对象图。
4. **disassemble** —— 按版本表解码字节码,操作数尺寸、缩放与隐式寄存器表取自 `bytecodes.h`。
5. **decompile** —— 寄存器 IR → 结构化控制流(if、循环、switch、try)→ AST → 打印;生成器/async
   状态机、完成码分派、类装配按版本折叠。

## 验证

四层门禁,每一层都能单独复现:

1. **行为矩阵**(`scripts/verify_behavior.sh`)—— 每个 fixture 在**该版本的 Node** 里编译成 `.jsc` → 反编译
   → 语法门禁 → 跑产物与原始函数**逐用例比对返回值**。另有两道结构门禁:默认产物不许丢顶层函数声明
   (防优化层 DCE)、known-fail 单独统计,不让它掩盖回归。
2. **整脚本对拍**(`scripts/script_diff.js`)—— 完整脚本跑 `node`,比 stdout 与退出码(覆盖"顶层只有语句 /
   只有 const / 只有 class / async"这些 `target` 型用例照不到的形态)。
3. **语料扫描**(`scripts/verify_samples.py`)—— 大样本 × 多版本:编译 → 反编译 → `node --check` → 在 vm 里
   加载(我们自己生成的名字若 `ReferenceError` 即判为 bug)。
4. **单元测试与静态检查** —— `cargo test`(65 条)+ `cargo clippy --all-targets`(0 告警)+ `cargo build`
   (0 告警)+ 零依赖端到端冒烟 `scripts/ci_smoke.sh`。

```sh
cargo test                                              # 65 条单测
bash scripts/verify_behavior.sh 8.17.0 10.24.1 12.22.12 14.21.3 16.20.2 18.20.8 20.20.2 22.12.0 24.12.0
python3 scripts/verify_samples.py 20.20.2 --limit 200    # 语料;另有 --offset、--samples
```

fixture 的编译方式与 bytenode 完全同参(同样的旗标 + `createCachedData()`,见
`scripts/mkcorpus.js`);快速上手那段用的是真 bytenode 1.7.0。

**数字**(都可用上面的脚本复现;矩阵那几行是当前二进制实测):

| 指标 | 数量 |
| --- | --- |
| 支持的 Node 发布 | **519 个**(`v8.0.0` → `v26.13.0`),覆盖 **90 个 V8 版本**(`5.8.283.41` → `14.6.202.34`) |
| 版本表 / 只读堆名表 | **62 张**版本表(**36** Node + **26** Electron)+ **29 张** `ro-map` 表(`tables/manifest.json` 逐发布索引) |
| 行为 fixture | **36 个**(合计 **121 条断言用例**)+ **5 个**整脚本 fixture —— 41 份源码 / 629 行 |
| 一次全矩阵 | 32 条 Node 线 × 36 = **1,152 格** + 160 格整脚本 ⇒ **3,872 次逐用例比对** + **1,312 次结构门禁** |
| 标准语料扫描 | **408 份**真实样本 × 6 条线(8.17 / 12.22 / 16.20 / 20.20 / 24.12 / 26.10) |
| 全语料扫描(累计) | **2,446 份 × 10 条线 ≈ 2.5 万次**"编译 → 反编译 → 语法 / 加载"检查 |
| 处理过的输入代码 | **2.40 MB / 76,246 行 / 240 万字符** |
| 产出的反编译代码 | **25,029 份 / 301 MB / 627 万行** |
| 真实文件 | Node 自带 `duplexpair.js`、`event_target.js`、`test-abortcontroller.js` × 9 条线 |
| 结果 | **1,144 pass / 0 fail / 8 skip / 0 known-fail** + **160 script pass / 0 fail** |

<details>
<summary>矩阵为什么可信</summary>

- `skip`(8 个格子)表示 fixture 源码语法需要更新的解析器(Node 8/10/12.15 上的 `?.`/`??`,以及
  Node 8.17 上的 BigInt 字面量),在 `tests/fixtures/behav/cases.json` 里以 `min_node` 声明,与失败
  分开统计。
- 语料里 `compile-fail` 的格子是**该版本解析器**编译不了的样本源码(现代语法,如 8.17 的 64 份),
  不是产物问题;`0 语法 / 0 反编译`指的是产物侧。
- **已无 known-fail。** `iter_no_extra_close` 覆盖一个**原生语义随版本变**的用例
  (`for (const v of [1,1,2]) { if (v === 2) continue; }`:6.2/6.8 耗尽时会调一次
  `iterator.return()`,7.8+ 不调)。这类 fixture 在 `tests/fixtures/behav/cases.json` 里标 `same_node_original`:原函数
  改用**与被编译产物相同的 Node 版本**去跑 —— 产物要忠实复现**它自己那个版本**的行为,32 条线现在
  都做到了。
- 曾**吞掉**循环体异常的 IteratorClose 收尾、以及 try/finally 的两处完成码缺口均已修 ——
  `iter_close` 与 `finally_return` 通过。

</details>

<details>
<summary>41 份 fixture 源码</summary>

行为 fixture(每个都在 32 条线上跑):`arith` `branch` `loop_for` `loop_while` `strings`
`array_ops` `object_ops` `switch_case` `try_catch` `recursion` `closure` `class_basic`
`destructure` `template` `spread_rest` `for_of_in` `arrow_opt` `generator` `operators`
`string_regex` `yield_expr` `async_await` `async_try` `optional_chain` `twoclasses` `call_args3`
`call_once` `finally_return` `finally_solo` `delete_result` `num_literals` `try_catch_after`
`iter_close` `param_defaults` `typeof_flags` `iter_no_extra_close`

整脚本 fixture(比 stdout 与退出码):`top_stmt` `top_const` `top_var` `top_class` `top_async`

单条版本线一轮语料:≈ 2,800 份产物 / 33.5 MB / ≈ 70 万行反编译代码。

`mise.toml` 固定 harness 默认用的 Node 版本(10.24.1 → 24.12.0);矩阵还要用更老/更新的线 ——
其余 23 个这样装:

```sh
mise install node@8.17.0 node@12.5.0 node@12.9.0 node@12.15.0 node@13.14.0 node@14.0.0 \
  node@14.4.0 node@14.5.0 node@14.6.0 node@15.14.0 node@16.3.0 node@16.5.0 node@16.8.0 \
  node@16.9.0 node@17.1.0 node@17.9.1 node@18.2.0 node@19.1.0 node@19.9.0 node@21.7.3 \
  node@23.11.1 node@25.9.0 node@26.13.0
```

</details>

## 仓库结构

```
src/              header、serializer、bytecode、disasm、decompile、CLI(帮助文本在 `src/help.rs`)
tables/           各 V8 版本的表:操作数、roots、scope-info 布局、运行时名、
                  只读堆名表(ro_map_*.json)+ manifest.json
scripts/          表生成、语料构造、验证 harness、release.sh、ci_smoke.sh
tests/            单元/集成测试 + fixture(现场编译成 .jsc)
docs/             VERSIONS.md(逐版本工程笔记)、ARCHITECTURE.md、DECOMPILE.md、stream-format.md
.github/          tag 触发的发版工作流:六个平台、裸二进制、带描述的 Release
CONTRIBUTING.md   构建、改动要过的门禁、怎么加 fixture
```

## 已知限制

- 拿不到源码文本与原始标识符,名字都是重建出来的。
- V8 内建显示为 `__runtime.*` / `__intrinsic.*` 调用(只有最小桩实现)。
- 解不出的作用域槽写成 `__ctx.ctxN`。Node 22+ 的内建名要靠只读堆名表:内嵌的那批只适用
  macOS(见[用法]#用法),Linux/Windows 上自己用 `jscd ro-map` 建一张,或接受 `<ro…>` 占位。
- `decompile` 追求可读可跑,不做字节级往返一致。

## 许可

MIT,见 [LICENSE](LICENSE)。

## 致谢

[bytenode](https://github.com/bytenode/bytenode)(本工具针对的打包方式);
[View8](https://github.com/suleram/View8)(反汇编格式沿用其约定);
[cure](https://github.com/ejfkdev/cure)(AST 级简化与格式化)。