parsing kaitai ksy into struct-fu for jo

by arian_

HTML

<script type="importmap">
  {
    "imports": {
      "js-yaml": "https://cdn.jsdelivr.net/npm/[email protected]/+esm",
      "./lib.js": "https://cdn.jsdelivr.net/gh/ariankordi/struct-fu@681ffc3d8592161b1e286392b4e2e63686319474/lib.js"
    }
  }
</script>
<!-- ./lib.js = struct-fu -->

  <!-- Global error display block. -->
  <div id="global-error-box" style="display: none; background: #ffeeee; color: #000; border: 2px solid red; padding: 1em; margin: 1em; font-family: monospace;">
   <div style="font-weight: bold; font-size: 1.2em;"><span id="global-error-text-error">⚠️ JS Error:</span><span id="global-error-text-promise-rejection">⚠️ JS Promise Rejection:</span></div>
    <div id="global-error-message" style="white-space: pre-wrap; margin-top: 0.5em;"></div>
    <button id="global-error-hide-button" style="margin-top: 1em;">Close</button>
  </div>

<form id="ksy-form">

  <button id="ksy-button">parse</button>
  <br>
  <br>
  <textarea name="ksy-text"
  style="margin: 0px; height: 400px; width: 400px;">

meta:
  id: c_f_li_mii_data_packet
  application: |
    Mii data format used on 3DS, Wii U, Miitomo
    versions < 2.3.0, and on Switch inside amiibo data.
    Fields marked as "padding" and "reserved" are always zero.
  file-extension:
    - cfsd
    - ffsd
    # Unofficial:
    - 3dsmii
    #- cfcd
    #- ffcd
  xref:
    struct_names: |
      CFLiMiiDataPacket/nnmiiStoreData,
      FFLStoreData/AFLStoreData,
      nn::mii::Ver3StoreData
  endian: le
  bit-endian: le

# potential todos!!!!!:
# * what is my strategy for kaitai in general?
#   - encoding is a no-go(?)
#     * unless i want to leverage debug mode or custom kaitai parser?
#   - you MUST keep everything top-level, no nesting/unions
#     * create id as the exception? but has .data property
#   - will it be an alternative to structs in js or dwarf or something?
# * add `valid` range fields (need to somehow ignore later)?

seq:
  - id:...

JavaScript

// @ts-check
/* eslint @stylistic/indent: ['error', 2] -- Define indent rules. */

import * as _Import from './lib.js';
/** @typedef {import('./lib')} _ */
globalThis._ = /** @type {_} */ (/** @type {*} */ (globalThis)._);
let _ = globalThis._;
_ = !_ ? _Import : _;

import * as YAML from 'js-yaml';


/**
 * U8 -> Base64
 * @param {Array<number>|Uint8Array} bytes - Input data to encode.
 * @returns {string} Base64 representation of `buffer`.
 */
const bytesToBase64 = bytes =>
    // fromCharCode should be compatible with Uint8Array, but its param type is number[].
    btoa(String.fromCharCode.apply(null, /** @type {Array<number>} */ (bytes)));

/* ksy-to-structfu.js
 *
 * Convert a parsed KSY object (via js-yaml) into a struct-fu definition.
 * No `eval`, no file emission – we directly build Field objects.
 *
 * ./struct-fu/lib.js must already be loaded (CommonJS or ESM).
 */

//#region — public API ---------------------------------------------------------

/**
 * Build a struct-fu definition from a KSY YAML string.
 * @param {string} ksyYaml Raw YAML text.
 * @returns {_.StructInstance<any>} The top-level struct-fu struct.
 */
export function buildStructFromKsy(ksyYaml) {
  const ksyRoot = YAML.load(ksyYaml, { json: true });
  const typeCache = new Map();                   // name → struct instance
  return makeType(ksyRoot,
    lowerCamelCase(ksyRoot.meta?.id ?? 'root'), typeCache, {
    byteEndian : ksyRoot.meta?.endian     ?? 'be',
    bitEndian  : ksyRoot.meta?.['bit-endian'] ?? 'be'
  });
}

//#endregion ------------------------------------------------------------------

//#region — helpers -----------------------------------------------------------

/**
 * Converts snake_case to lowerCamelCase.
 * Mimics Kaitai's doWord().
 * @param {string} s
 * @returns {string}
 */
function lowerCamelCase(s) {
  if (s.startsWith("_")) return "_" + lowerCamelCase(s.substring(1));
  const [first, ...rest] =...