Adds the reverse conversion (sab -> sax) and makes the tools usable without remembering paths on the command line. - sax.py: decoder ported from OfflineApp.decodeAppBinary + encodeAppXml. Rebuilds the tree from the firstChild/nextSibling chain and writes XML the way XWriter does. Reproduces java.lang.Float.toString, and rounds manifest defaults to f4 before comparing them - otherwise props equal to their default (0.1) get written where sedonac omits them. - sab2sax.py: CLI dispatching on the input extension, like sedonac. sab.py still runs and hands off to it. - config.py: sedona.home lives in system.properties. If it is missing or has no manifests/, a folder chooser opens and the answer is written back. File choosers for the input, save dialogs for the output; the default output name carries a timestamp so a run never overwrites the previous one. - verify.py: checks both directions plus a sab -> sax -> sab round trip, and points at testprogram/ where the test files actually live. All three checks pass byte-identical against the checked-in sedonac output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
7.2 KiB
Markdown
158 lines
7.2 KiB
Markdown
# PySedonac
|
|
|
|
Pure-Python Sedona app converter, both directions — `.sax` -> `.sab` and `.sab` -> `.sax`. 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` in both directions on a 189-component / 217-link app across
|
|
18 kits.
|
|
|
|
## Usage
|
|
|
|
python sab2sax.py [in.sax|in.sab] [out]
|
|
|
|
Like `sedonac`, the direction follows the input's extension: feed it a `.sax` and you get a `.sab`,
|
|
feed it a `.sab` and you get a `.sax`. Both arguments are optional, so you can run it by
|
|
double-click:
|
|
|
|
* no input → a file chooser opens, listing `.sax` and `.sab`;
|
|
* no output → a save dialog opens next to the input, pre-filled with a timestamped name:
|
|
`app.sax` → `app-sab2sax-20260728-140609.sab`, so a run never overwrites the previous one.
|
|
|
|
`python sab.py ...` still works and does the same thing — it hands off to `sab2sax.py`.
|
|
|
|
Sedona home is *not* an argument — it comes from `system.properties` (below). As a library:
|
|
|
|
import sab2sax
|
|
sab2sax.convert("app.sax") # -> ("app-sab2sax-<stamp>.sab", nbytes)
|
|
sab2sax.convert("app.sab", "out.sax") # explicit output, no dialogs
|
|
|
|
import sab, sax
|
|
sab.encode("app.sax", home, "out.sab") # one direction each, if you prefer
|
|
sax.decode("app.sab", home, "out.sax") # `home=None` reads system.properties
|
|
|
|
### What a `.sab` cannot give back
|
|
|
|
Only **config** props are stored in the binary, so a `.sab` -> `.sax` conversion cannot recover
|
|
runtime prop values — they come back as their manifest defaults. `sedonac` has exactly the same hole;
|
|
this is a property of the format, not of the port. (A prop equal to its default is not written to the
|
|
`.sax` at all, by either tool.)
|
|
|
|
## Configuration
|
|
|
|
Sedona home lives in `system.properties`, next to the scripts, under the same key sedonac uses:
|
|
|
|
sedona.home=C:\path\to\sedona
|
|
|
|
It must be the directory holding `manifests/` — kit manifests are required, since slot ids and types
|
|
are resolved from them.
|
|
|
|
Plain Windows paths, no escaping. Java's `Properties` — what sedonac itself reads this file with —
|
|
treats `\` as an escape and so doubles them up; that form parses here too. The rule is `\\` collapses
|
|
to `\` and every other backslash is kept literally, so `C:\niagara` never turns into a newline.
|
|
|
|
If the key is missing, or the folder it names has no `manifests/`, a folder chooser opens on first
|
|
run and the choice is written back to `system.properties` (other lines and comments are preserved; a
|
|
duplicate `sedona.home` further down gets commented out). Dialogs need `tkinter` — on a headless box
|
|
set the key by hand instead.
|
|
|
|
`python config.py` prints the resolved home, prompting if it isn't set yet.
|
|
|
|
## Verify
|
|
|
|
python verify.py [sedona_home]
|
|
|
|
Without an argument the home comes from `system.properties`. Three checks, both against the
|
|
checked-in pair in `testprogram/`:
|
|
|
|
test_normal.sax -> sab vs test_normal_sedonac.sab
|
|
test_normal_sedonac.sab -> sax vs test_normal.sax
|
|
test_normal_sedonac.sab -> sax -> sab back to the same bytes
|
|
|
|
`check_encode(sax)` / `check_decode(sab)` with no reference argument run `bin/sedonac.exe` on a copy
|
|
of the input and diff against that instead — a live comparison, if you have a JRE it will start
|
|
under.
|
|
|
|
## Files
|
|
|
|
sab2sax.py the CLI: dispatches on the input extension
|
|
sab.py sax -> sab encoder
|
|
sax.py sab -> sax decoder
|
|
config.py system.properties read/write, folder + file choosers
|
|
verify.py regression check against sedonac's output, both directions
|
|
system.properties sedona.home
|
|
testprogram/ test_normal.sax and the sedonac .sab to match it against
|
|
|
|
## 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
|
|
`<schema>` 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 `<prop>` 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".
|
|
|
|
## Going back: sab -> sax
|
|
|
|
The tree is rebuilt from the `firstChild`/`nextSibling` id chain, then written the way
|
|
`OfflineApp.encodeAppXml` does: two-space indent per level, a `<!-- /path -->` comment above every
|
|
`<comp>`, and a prop line only where the value differs from the slot default.
|
|
|
|
Two details worth knowing:
|
|
|
|
**Java float formatting.** `Value.encodeString` for a float is `java.lang.Float.toString` — shortest
|
|
digits that round-trip, always a decimal point, `E` notation outside `[1e-3, 1e7)`. `sax.java_float`
|
|
reproduces it (`2500.0`, `1.0E7`, `1.0E-4`).
|
|
|
|
**The default is a 32-bit float too.** Comparing the decoded value against a manifest default parsed
|
|
as a Python double makes `0.1` look different from `0.1` and writes out props that sedonac omits —
|
|
round the default to `f4` first.
|
|
|
|
## Not covered
|
|
|
|
Only what `testprogram/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.
|
|
|
|
Also, `java_float` matches modern Java, not the old JDK's extra-digit quirk on subnormals — Java
|
|
prints `Float.MIN_VALUE` as `1.4E-45` where we print the equally round-trippable `1.0E-45`. Only
|
|
reachable with denormal floats in an app.
|
|
|
|
## Alternative
|
|
|
|
If a JRE is present, `sedona/bin/sedonac.exe <file.sax|.sab>` converts either direction off the file
|
|
extension, and additionally validates the app (schema resolution, RAM/FLASH sizing) in a way this
|
|
encoder does not.
|