# PySedonac Pure-Python `.sax` -> `.sab` encoder. No Java, no JRE. Ported from the Sedona 1.2 runtime library — `sedona/src/sedona/src/sedona/offline/OfflineApp.java` and friends, **not** from `sedonac/` (that's the language compiler; app file conversion lives in the runtime lib). Verified byte-identical to `sedonac.exe` on a 189-component / 217-link app across 18 kits. ## Usage python sab.py `sedona_home` is the directory holding `manifests/` — kit manifests are required, since slot ids and types are resolved from them. ## Verify python verify.py [sedona_home] Compares our output against the checked-in `test_normal_sedonac.sab`. Pass a sedona home containing `bin/sedonac.exe` and call `check(sax)` with no reference to diff against a live sedonac run instead. ## Format Big-endian. **No padding, no alignment** — `Buf.bigEndian = true`, `checkAlignment = false`. The `align()`/`pad()` helpers exist on `Buf` but the app encoder never calls them. "sapp" (i4 0x73617070) | version (i4 0x0003) schema: u1 kitCount, then per kit: cstr name, i4 checksum u2 maxId components (ascending id, NOT tree order), each: u2 id | u1 kitId | u1 typeId cstr name | u2 parentId | u2 firstChildId | u2 nextSiblingId (0xffff = none) config prop values in slot-id order, bare - no names, no ids u1 ';' u2 0xffff links: u2 fromComp | u1 fromSlot | u2 toComp | u1 toSlot u2 0xffff u1 '.' Values: `bool` -> u1 (0/1, **2 = null**) | `byte` -> u1 | `short` -> u2 | `int` -> i4 | `long` -> i8 | `float` -> f4 | `double` -> f8 | `Buf` -> u2 len + raw bytes | `asStr` -> u2 (len+1) + ASCII + NUL. Slot flags (`SlotManifest.flagsToString`): `a` = action, `c` = config, `s` = asStr, `o` = operator. ## Three things that will bite you **Kit order is normative.** `sys` at index 0, everything else alphabetical (`Schema.sortKits`). Kit id is the position in that sorted list, and every component record references it. Document order of the `` block in the SAX is irrelevant. **Slot ids come from flattening.** `Type.resolveSlots` inherits base slots then appends declared ones; the resulting index is the id written into links. An **action override** reuses the inherited id rather than appending (`Type.addSlot`) — get that wrong and every subsequent slot id shifts. **Missing prop is not null.** When a `` is absent the value is the manifest's `default` attribute, or failing that `Value.defaultForType` -> **zero**. `null` (NaN, `0x7fc00000` for float) only when the SAX literally says `val="null"`. This was the single bug between "same length" and "byte identical". ## Not covered Only what `test_normal.sax` exercises is proven. Untested: action overrides, `Buf`-typed props (base64 path is written but unexercised), non-ASCII strings (Sedona `Str` is ASCII-only and will raise), and apps whose component ids exceed the 256-entry lookup table. ## Alternative If a JRE is present, `sedona/bin/sedonac.exe ` converts either direction off the file extension, and additionally validates the app (schema resolution, RAM/FLASH sizing) in a way this encoder does not.