十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

读懂 Kaitai Struct Compiler 生成的代码:以 JavaScript 后端为例逐行拆解 _read() 解析流程

读懂 Kaitai Struct Compiler 生成的代码:以 JavaScript 后端为例逐行拆解 _read() 解析流程 读懂 Kaitai Struct Compiler 生成的代码以 JavaScript 后端为例逐行拆解 _read() 解析流程【免费下载链接】kaitai_struct_compilerKaitai Struct: compiler to translate .ksy .cpp / .cs / .dot / .go / .java / .js / .lua / .nim / .php / .pm / .py / .rb / .rs项目地址: https://gitcode.com/gh_mirrors/ka/kaitai_struct_compilerKaitai Struct Compiler是一款二进制格式解析代码生成器你只用一份.ksy文件声明二进制结构它就能编译出.js、.py、.java、.go、.rs等 12 种语言的现成解析器。本文以JavaScript 后端为例逐行拆解生成代码中_read()方法的解析流程帮你从会调用进阶到看得懂源码。⚙️ 先搞懂Kaitai Struct Compiler 是什么Kaitai Struct 是一种声明式的语言YAML 格式的.ksy文件用来描述二进制文件、网络包等数据结构。编译器把描述编译成各语言的流读取代码你拿到手的是一个带完整 API 的解析器类例如var z new ZipFile(new KaitaiStream(data)); z.entries; // 直接按名字访问字段无需手写 offset 计算本项目参考实现编译器用 Scala 编写通过多平台构建分发shared/—— 核心编译器逻辑语言无关部分 各语言后端js/—— 编译到 JavaScript 环境的版本提供编程式 APIjvm/—— 编译到 JVM 的版本桌面命令行编译器入口想集成到 JS 工具链时可直接npm install kaitai-struct-compiler后调用compile()方法详见 js/README.md。 生成代码长什么样假设.ksy里定义了一个最简结构一个无符号短整型 一个字符串数组meta: id: demo seq: - id: name type: str size: 2 - id: count type: u2编译器会为meta.id命名的类型生成一个同名构造函数和_read方法大致长这样function Demo(_io, _parent, _root) { this._io _io; this._parent _parent; this._root _root || this; this._read(); } Demo.prototype._read function() { this._name this._io.readBytes(2); this._count this._io.readU2be(); }下面我们就对这个_read()逐行拆解。 第一步构造函数——_read()的调用时机对照编译器源码 shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala构造函数的生成逻辑是保存三个上下文引用this._io当前流的KaitaiStream、this._parent父对象、this._root根对象。这三个参数是 Kaitai Struct 的黄金参数子结构解析时会层层传递_root让你在任意深度都能访问顶层对象比如用文件头里读出的版本字段决定分支逻辑。末尾调用this._read()即自动读取autoRead。编译器在compileConstructor中判断config.autoRead为真时插入这行调用见 shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala。小贴士编译为 debug 模式--no-auto-read时_read()不会自动调用需要手动g._read();——这样即使解析中途出错对象上已读到的字段仍可访问方便调试。 第二步基础类型——read*()家族_read()的主体就是按seq的声明顺序逐字段执行一次流读取。JavaScript 后端把每种.ksy类型映射成KaitaiStream上的一个读方法见JavaScriptCompiler.parseExpr.ksy 类型生成的 JS 读取调用说明u1/s1this._io.readU1()/readS1()无符号/有符号 1 字节u2/s4等this._io.readU2be()/readS4le()后缀be/le标明字节序bytes固定大小this._io.readBytes(n)读 n 字节bytes到流尾this._io.readBytesFull()读到 EOFbytes带终止符this._io.readBytesTerm(0, true, false, false)参数依次为终止符、是否包含、是否消耗、EOF 是否报错bitsthis._io.readBitsIntBe(4)按位读取字符串KaitaiStream.bytesToStr(this._io.readBytes(2), ASCII)先读字节再按编码转字符串注意所有整数读取方法名里都带字节序后缀如readU4be()大端、readU2le()小端这是从.ksy的endian: be/le编译来的读的时候不会搞混。 第三步用户类型与子流substream.ksy里嵌套自定义类型比如 ZIP 里的entry时生成的代码是标准的对象构造 黄金参数传递this._first_entry new Entry(this._io, this, this._root);如果该类型基于size/size-eos/size-until的字节子流编译器会先构造一个子流再传给构造函数var _io_body new KaitaiStream(this._body); // 子流只覆盖 body 那段字节 this._body_obj new Body(_io_body, this, this._root);这段先分配局部 IO 变量的逻辑由 shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala 中的allocateIO生成子流创建策略则定义在公共组件 shared/src/main/scala/io/kaitai/struct/languages/components/EveryReadIsExpression.scala。 由于构造时会自动调用子对象的_read()嵌套结构会随父对象构造而递归全部解析——这也是为什么生成的解析器几乎不用手动预热。 第四步循环字段——三种 repeat 形态.ksy的repeat属性决定_read()里生成什么循环repeat 写法生成的 JS 循环repeat: eoswhile (!_io.isEof()) { ... }读到流尾repeat: 表达式for (var i 0; i N; i) { ... }读 N 次repeat-until: 表达式do { ... } while (!(cond));先读一次再判断同时循环字段会被初始化为数组并逐项push例如repeat: eos生成this._items []; var i 0; while (!this._io.isEof()) { this._items.push(new Item(this._io, this, this._root)); i; }循环控制代码的生成位置在JavaScriptCompiler的condRepeatEosHeader、condRepeatExprHeader等方法中而读什么的通用流程条件判断 → 循环 → 读取 → 校验封装在 shared/src/main/scala/io/kaitai/struct/languages/components/CommonReads.scala 的attrParse里。 第五步大小端分身——_read()/_readLE()/_readBE()如果.ksy的endian是固定值be或le只生成一个_read。但如果用了endian: inherited或endian: calc运行期才知道字节序编译器会生成三个方法Demo.prototype._read function() { this._is_le (this._root.magic 1) 1; // endian: calc 的判断表达式 if (this._is_le true) { this._readLE(); } else if (this._is_le false) { this._readBE(); } else { throw new KaitaiStream.UndecidedEndiannessError(); } } Demo.prototype._readLE function() { /* 内部全部用 read*le() */ } Demo.prototype._readBE function() { /* 内部全部用 read*be() */ }这套调度_read 拆出 LE/BE 双份实现的策略由compileEagerRead统一控制见 shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala。️ 第六步校验与异常.ksy中给字段写的validate条件会在对应读取语句之后生成一行检查失败就抛出带字段名的异常this._version this._io.readU1(); if (this._version 3) { throw new KaitaiStream.ValidationNotValueError(this._version, this, this._version 3); }校验失败抛出的都是KaitaiStream命名空间下的具体错误类型如ValidationNotValueError方便上层精确捕获。 彩蛋instance 是懒加载的.ksy里用instances定义的字段不在_read()中出现而是生成为 getter第一次访问时才计算并缓存Object.defineProperty(Demo.prototype, total_size, { get: function() { if (this._total_size ! undefined) return this._total_size; this._total_size this._count * 4; // 计算表达式 return this._total_size; } });对应生成逻辑见JavaScriptCompiler的instanceHeader/instanceCheckCacheAndReturn。理解这一点后你就能解释一个常见现象改实例依赖的字段后再访问 instance值不会变——缓存不会自动失效。 快速上手亲手编译一次想亲眼看到本文拆解的代码被生成出来步骤很短克隆仓库git clone https://gitcode.com/gh_mirrors/ka/kaitai_struct_compiler或直接npm install kaitai-struct-compiler在 Node.js 中调用var compiler require(kaitai-struct-compiler); var ksy { /* .ksy 解析后的 JS 对象 */ }; compiler.compile(javascript, ksy, null, false).then(function(files) { console.log(files[Demo.js]); // 就是上面逐行拆解的那个文件 });查看输出的Demo.js对照本文第五步检查_readLE/_readBE是否按预期生成 ✅想改编译器行为比如换语言、加新后端核心入口是 js/src/main/scala/io/kaitai/struct/MainJs.scala 和格式解析器 js/src/main/scala/io/kaitai/struct/format/JavaScriptKSYParser.scala。 总结读懂_read()只需要记住这条主线构造函数保存_io / _parent / _root然后自动调用_read()_read()按seq顺序逐字段生成this._io.read*()调用嵌套类型 new 子类型(this._io, this, this._root)子流 先new KaitaiStream(...)再传参repeat决定while / for / do-while三种循环形态endian: inherited|calc会拆出_readLE()与_readBE()两份实现validate生成紧跟其后的抛错检查instances则是懒加载 getter掌握这套心智模型后无论生成的.js是 ZIP、GIF 还是自定义协议你都能在几分钟内看懂它的每个字节从哪里来。【免费下载链接】kaitai_struct_compilerKaitai Struct: compiler to translate .ksy .cpp / .cs / .dot / .go / .java / .js / .lua / .nim / .php / .pm / .py / .rb / .rs项目地址: https://gitcode.com/gh_mirrors/ka/kaitai_struct_compiler创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表