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> |
||
|---|---|---|
| testprogram | ||
| .gitignore | ||
| config.py | ||
| README.md | ||
| sab2sax.py | ||
| sab.py | ||
| sax.py | ||
| system.properties | ||
| verify.py | ||
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
.saxand.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.