Compare commits

5 Commits
Author SHA1 Message Date
arda.aydin@ontrol.com.trandClaude Opus 5 2dcc1704a6 Add a window, and run.py as the entry point
gui.py: one button per direction, "SAB file to SAX file" and "SAX file to
SAB file". The button decides which extension the chooser offers and which
way the conversion goes. The log names the chosen file and its folder before
the work starts, then the output file and folder when it finishes, each value
on its own line. Output lands next to the input under the timestamped name,
so there is no save dialog and nothing is overwritten. Sedona home sits at
the top with a Change... button that validates manifests/ and rewrites
system.properties.

run.py: no arguments opens the window, any argument goes to the command line
converter. pythonw run.py for a shortcut with no console.

config: NAME/VERSION are now "SAB - SAX Sedona Files Converter 0.0.0.002",
carried by the window title, every dialog title and the CLI banner. Adds
choose_sab_input() - the existing choose_sab_file() is the save dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:05:15 +03:00
arda.aydin@ontrol.com.trandClaude Opus 5 1fdfed90dd verify.py: round-trip any app you pick, with a file chooser
Takes a file now -- python verify.py [app.sax|app.sab] [sedona_home] -- and
opens the same chooser as the converter when none is given. Direction follows
the extension:

  .sab  sab -> sax -> sab            must come back byte-identical
  .sax  sax -> sab -> sax -> sab     the two sabs must be identical

The sax case compares the binaries, not the two texts: a sab carries no
runtime prop values and no formatting, so a regenerated sax may legitimately
differ from a hand-written original. That is printed as a note, not a failure.

Cancelling the chooser falls back to the built-in testprogram/ regression,
now four checks (both sedonac comparisons plus both round trips).

Malformed input fails with a one-line reason and exit 1 instead of a
traceback, and sax.py reports a broken parent/child/sibling id as
"comp 0 app: missing child 254" rather than a bare KeyError. Sedona home is
resolved lazily so importing verify opens no dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:30:52 +03:00
arda.aydin@ontrol.com.tr af906b31dc Merge sab2sax 0.0.0.001: both-direction conversion, sedona home in system.properties 2026-08-12 22:14:30 +03:00
arda.aydin@ontrol.com.trandClaude Opus 5 dd7b586ea0 Drop testprogram/ from the repo, show version in dialog titles
testprogram/ is local test data, not part of the tool: untracked and
gitignored so it never comes back. verify.py now prints SKIP and exits 0
when the test pair is absent instead of blowing up.

Adds config.NAME / config.VERSION (sab2sax 0.0.0.001). Every dialog title
bar goes through config.title(), and the CLI prints the same line at
startup, so it is clear which build produced a file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:08:20 +03:00
arda.aydin@ontrol.com.trandClaude Opus 5 ec4019ea96 Convert both directions, and read sedona home from system.properties
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>
2026-08-12 21:51:04 +03:00
10 changed files with 1169 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
testprogram/
*-sab2sax-*.sab
*-sab2sax-*.sax
+152 -10
View File
@@ -1,26 +1,148 @@
# PySedonac # PySedonac
Pure-Python `.sax` -> `.sab` encoder. No Java, no JRE. 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` 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 and friends, **not** from `sedonac/` (that's the language compiler; app file conversion lives in the
runtime lib). runtime lib).
Verified byte-identical to `sedonac.exe` on a 189-component / 217-link app across 18 kits. Verified byte-identical to `sedonac.exe` in both directions on a 189-component / 217-link app across
18 kits.
## Usage ## Run it
python sab.py <app.sax> <sedona_home> <out.sab> python run.py the window
pythonw run.py the window with no console behind it
python run.py app.sab [out] straight to the command line converter
`sedona_home` is the directory holding `manifests/` — kit manifests are required, since slot ids and `run.py` is the single entry point: no arguments opens the GUI, any argument is handed to the
types are resolved from them. command line converter. Double-click it (or a shortcut to `pythonw run.py`) and you get the window.
## The window
python run.py (or: python gui.py)
Title bar carries the name and version (`SAB - SAX Sedona Files Converter 0.0.0.002`). One button
per direction — **SAB file to SAX file** and **SAX file to SAB file** — and the button you press
decides which extension the file chooser offers. The log shows the chosen file and its folder
*before* the conversion runs, then the output file and its folder when it finishes:
sab -> sax
INPUT FILE:
app.sab
INPUT PATH:
C:\apps
OUTPUT FILE:
app-sab2sax-20260728-140609.sax
OUTPUT PATH:
C:\apps
wrote 61294 bytes
The result is written next to the input under that timestamped name, so nothing is overwritten and
no save dialog gets in the way. Sedona home is shown at the top with a `Change...` button that
rewrites `system.properties`.
## The command line
python run.py [in.sax|in.sab] [out] (or: python sab2sax.py ...)
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 — `sab2sax.py` on its own asks for
what it needs:
* 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.
## Version
`config.NAME` / `config.VERSION` — currently `SAB - SAX Sedona Files Converter 0.0.0.002`. The window
title and every dialog title bar carry it; a chooser reads `SAB - SAX Sedona Files Converter
0.0.0.002 - Select the .sax or .sab application file to convert`, and the CLI prints the same line
when it starts, so it is obvious which build produced a file. Bump it in [config.py](config.py) and
all of them follow.
## Verify ## Verify
python verify.py [sedona_home] python verify.py [app.sax|app.sab] [sedona_home]
Compares our output against the checked-in `test_normal_sedonac.sab`. Pass a sedona home containing Point it at any app of yours and it round-trips it, direction by extension:
`bin/sedonac.exe` and call `check(sax)` with no reference to diff against a live sedonac run instead.
app.sab -> sab -> sax -> sab must come back byte-identical
app.sax -> sax -> sab -> sax -> sab the two sabs must be identical
The `.sax` case compares the two **binaries**, not the two texts, because going through a `.sab` is
lossy on purpose — runtime prop values and formatting are not in the binary. If the regenerated
`.sax` differs from the original, that is printed as a note rather than a failure. (For a `.sax`
sedonac itself wrote, they do match.)
With no file argument a chooser opens, listing `.sax` and `.sab`. Cancel it and the built-in
regression runs instead, against a `test_normal.sax` / `test_normal_sedonac.sab` pair in
`testprogram/`. That folder is **not** in the repo (gitignored) — drop the pair in yourself,
otherwise verify prints `SKIP` and exits 0:
test_normal.sax -> sab vs test_normal_sedonac.sab
test_normal_sedonac.sab -> sax vs test_normal.sax
both round trips as above
`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.
Exit code is 0 on pass, 1 on failure. A malformed file fails with a one-line reason
(`comp 0 app: missing child 254`) rather than a traceback.
## Files
run.py entry point: window with no arguments, CLI with them
gui.py the window: a button per direction
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/ local only, gitignored: test apps to run verify.py against
## Format ## Format
@@ -60,12 +182,32 @@ attribute, or failing that `Value.defaultForType` -> **zero**. `null` (NaN, `0x7
only when the SAX literally says `val="null"`. This was the single bug between "same length" and only when the SAX literally says `val="null"`. This was the single bug between "same length" and
"byte identical". "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 ## Not covered
Only what `test_normal.sax` exercises is proven. Untested: action overrides, `Buf`-typed props 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 (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. 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 ## Alternative
If a JRE is present, `sedona/bin/sedonac.exe <file.sax|.sab>` converts either direction off the file If a JRE is present, `sedona/bin/sedonac.exe <file.sax|.sab>` converts either direction off the file
+196
View File
@@ -0,0 +1,196 @@
"""Configuration: sedona home lives in `system.properties` next to this file.
Same key sedonac itself uses, but written as a plain Windows path:
sedona.home=C:\\path\\to\\sedona
We only *read* this file, so there is no reason to escape anything. Java's
Properties (what sedonac uses) treats `\\` as an escape, so its own files double
them up -- `\\\\path\\\\to`. Both parse here: `\\\\` collapses to `\\`, and every
other backslash is kept literally, so `C:\\niagara` does not turn into a newline.
If the key is missing, or the directory no longer holds `manifests/`, a folder
chooser is shown and the answer is written back to `system.properties`.
"""
import os, sys
NAME = "SAB - SAX Sedona Files Converter"
VERSION = "0.0.0.002"
HERE = os.path.dirname(os.path.abspath(__file__))
PROPS = os.path.join(HERE, "system.properties")
KEY = "sedona.home"
def title(text):
"""Every dialog carries the program name and version in its title bar."""
return "%s %s - %s" % (NAME, VERSION, text)
# ---------- .properties ----------
def _unescape(v):
"""Collapse `\\\\` to `\\`; leave any other backslash alone.
Accepts both our plain paths and java-escaped ones, and never mistakes a
path segment for an escape (`C:\\niagara` stays `C:\\niagara`).
"""
out, i = [], 0
while i < len(v):
if v[i] == "\\" and i + 1 < len(v) and v[i + 1] == "\\":
i += 1
out.append(v[i])
i += 1
return "".join(out)
def read_props(path=PROPS):
props = {}
if not os.path.exists(path):
return props
with open(path, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if not line or line[0] in "#!" or "=" not in line:
continue
k, v = line.split("=", 1)
props[k.strip()] = _unescape(v.strip()) # last wins
return props
def write_prop(key, value, path=PROPS):
"""Set `key`, keeping every other line of the file intact."""
lines = []
if os.path.exists(path):
with open(path, "r", encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
new = "%s=%s" % (key, value)
done = False
for i, line in enumerate(lines):
s = line.strip()
if s and s[0] not in "#!" and "=" in s and s.split("=", 1)[0].strip() == key:
lines[i] = new if not done else "#" + line # comment out later dupes
done = True
if not done:
lines.append(new)
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
# ---------- dialogs ----------
def _tk():
"""A hidden root window, or None when no GUI is available."""
try:
import tkinter as tk
root = tk.Tk()
root.withdraw()
return root
except Exception:
return None
def ask_directory(text, initial=None):
root = _tk()
if root is None:
return None
try:
from tkinter import filedialog
return filedialog.askdirectory(title=title(text), initialdir=initial or HERE) or None
finally:
root.destroy()
def ask_open_file(text, filetypes, initial=None):
root = _tk()
if root is None:
return None
try:
from tkinter import filedialog
return filedialog.askopenfilename(title=title(text), filetypes=filetypes,
initialdir=initial or HERE) or None
finally:
root.destroy()
def ask_save_file(text, filetypes, defaultextension, initialfile=None, initial=None):
root = _tk()
if root is None:
return None
try:
from tkinter import filedialog
return filedialog.asksaveasfilename(title=title(text), filetypes=filetypes,
defaultextension=defaultextension,
initialfile=initialfile,
initialdir=initial or HERE) or None
finally:
root.destroy()
# ---------- api ----------
def is_sedona_home(path):
return bool(path) and os.path.isdir(os.path.join(path, "manifests"))
def get_sedona_home(interactive=True):
"""Path from system.properties, prompting (and saving) when unusable."""
home = read_props().get(KEY)
if is_sedona_home(home):
return home
if not interactive:
raise SystemExit("%s not set (or has no manifests/) in %s" % (KEY, PROPS))
why = "not set in" if not home else "invalid (%s), update" % home
print("sedona.home %s %s -- please choose the sedona installation folder" % (why, PROPS))
while True:
picked = ask_directory("Select Sedona home (the folder containing manifests/)",
home if home and os.path.isdir(home) else None)
if not picked:
raise SystemExit("no sedona home selected")
picked = os.path.normpath(picked)
if is_sedona_home(picked):
write_prop(KEY, picked)
print("saved %s=%s to %s" % (KEY, picked, PROPS))
return picked
print(" %s has no manifests/ subfolder -- try again" % picked)
SAX = ("Sedona app XML", "*.sax")
SAB = ("Sedona app binary", "*.sab")
ANY = ("All files", "*.*")
def choose_input_file(initial=None):
"""Pick the file to convert -- direction follows its extension."""
return ask_open_file("Select the .sax or .sab application file to convert",
[("Sedona app (*.sax, *.sab)", "*.sax *.sab"), SAX, SAB, ANY],
initial)
def choose_sax_file(initial=None):
"""Open a .sax -- the input of a sax -> sab conversion."""
return ask_open_file("Select the .sax application file to convert to .sab",
[SAX, ANY], initial)
def choose_sab_input(initial=None):
"""Open a .sab -- the input of a sab -> sax conversion."""
return ask_open_file("Select the .sab application file to convert to .sax",
[SAB, ANY], initial)
def choose_sab_file(initialfile=None, initial=None):
"""Save-as for the .sab produced from a .sax."""
return ask_save_file("Save the .sab application file", [SAB, ANY],
".sab", initialfile, initial)
def choose_sax_output(initialfile=None, initial=None):
"""Save-as for the .sax produced from a .sab."""
return ask_save_file("Save the .sax application file", [SAX, ANY],
".sax", initialfile, initial)
if __name__ == "__main__":
print(get_sedona_home())
+166
View File
@@ -0,0 +1,166 @@
"""A window with two buttons, one per direction.
python gui.py
Pick a direction, pick a file; the log shows what was chosen before the
conversion runs and where the result landed afterwards. Output goes next to the
input under a timestamped name, so nothing is ever overwritten.
"""
import os, sys, traceback
import tkinter as tk
from tkinter import ttk, messagebox
import config, sab, sax
class App:
def __init__(self, root):
self.root = root
root.title("%s %s" % (config.NAME, config.VERSION))
root.minsize(620, 340)
frame = ttk.Frame(root, padding=12)
frame.pack(fill="both", expand=True)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(3, weight=1)
# sedona home
home_box = ttk.LabelFrame(frame, text="Sedona home", padding=(8, 4))
home_box.grid(row=0, column=0, sticky="ew")
home_box.columnconfigure(0, weight=1)
self.home_var = tk.StringVar(value="(not set)")
ttk.Label(home_box, textvariable=self.home_var).grid(row=0, column=0, sticky="w")
ttk.Button(home_box, text="Change...", command=self.change_home).grid(row=0, column=1, padx=(8, 0))
# the two directions
buttons = ttk.Frame(frame, padding=(0, 12))
buttons.grid(row=1, column=0, sticky="ew")
buttons.columnconfigure(0, weight=1)
buttons.columnconfigure(1, weight=1)
self.b_sab2sax = ttk.Button(buttons, text="SAB file to SAX file", padding=10,
command=lambda: self.run("sab"))
self.b_sab2sax.grid(row=0, column=0, sticky="ew", padx=(0, 6))
self.b_sax2sab = ttk.Button(buttons, text="SAX file to SAB file", padding=10,
command=lambda: self.run("sax"))
self.b_sax2sab.grid(row=0, column=1, sticky="ew", padx=(6, 0))
self.status = tk.StringVar(value="Choose a direction.")
ttk.Label(frame, textvariable=self.status).grid(row=2, column=0, sticky="w")
# log
log_box = ttk.Frame(frame)
log_box.grid(row=3, column=0, sticky="nsew", pady=(8, 0))
log_box.columnconfigure(0, weight=1)
log_box.rowconfigure(0, weight=1)
self.log_text = tk.Text(log_box, height=10, wrap="word", state="disabled")
self.log_text.grid(row=0, column=0, sticky="nsew")
bar = ttk.Scrollbar(log_box, orient="vertical", command=self.log_text.yview)
bar.grid(row=0, column=1, sticky="ns")
self.log_text.configure(yscrollcommand=bar.set)
self.home = None
root.after(50, self.first_home)
# ---------- helpers ----------
def log(self, line=""):
self.log_text.configure(state="normal")
self.log_text.insert("end", line + "\n")
self.log_text.see("end")
self.log_text.configure(state="disabled")
self.root.update_idletasks() # so it is on screen before the work starts
def set_home(self, path):
self.home = path
self.home_var.set(path or "(not set)")
def first_home(self):
"""Resolve sedona home at startup; the folder chooser opens if it isn't set."""
try:
self.set_home(config.get_sedona_home())
self.log("sedona home: %s" % self.home)
except SystemExit as e:
self.log("no sedona home: %s" % e)
messagebox.showwarning(config.title("Sedona home"),
"No sedona home selected.\n\n"
"Set it with Change... before converting.")
def change_home(self):
picked = config.ask_directory("Select Sedona home (the folder containing manifests/)",
self.home)
if not picked:
return
picked = os.path.normpath(picked)
if not config.is_sedona_home(picked):
messagebox.showerror(config.title("Sedona home"),
"%s has no manifests/ subfolder." % picked)
return
config.write_prop(config.KEY, picked)
self.set_home(picked)
self.log("sedona home: %s (saved to %s)" % (picked, config.PROPS))
# ---------- conversion ----------
def run(self, direction):
"""direction: 'sab' means sab -> sax, 'sax' means sax -> sab."""
if not config.is_sedona_home(self.home):
messagebox.showerror(config.title("Sedona home"),
"Set a valid sedona home first (Change...).")
return
src = config.choose_sab_input() if direction == "sab" else config.choose_sax_file()
if not src:
self.status.set("Cancelled.")
return
src = os.path.abspath(src)
# show what was chosen, before doing anything with it
self.log()
self.log("%s -> %s" % (direction, "sax" if direction == "sab" else "sab"))
self.log(" INPUT FILE:")
self.log(" %s" % os.path.basename(src))
self.log(" INPUT PATH:")
self.log(" %s" % os.path.dirname(src))
self.status.set("Converting %s ..." % os.path.basename(src))
if direction == "sab":
out = os.path.join(os.path.dirname(src), sax.default_sax_name(src))
work = lambda: sax.decode(src, self.home, out)
else:
out = os.path.join(os.path.dirname(src), sab.default_sab_name(src))
work = lambda: sab.encode(src, self.home, out)
self.busy(True)
try:
n = work()
except Exception as e:
self.busy(False)
self.log(" FAILED: %s" % e)
self.status.set("Failed.")
traceback.print_exc()
messagebox.showerror(config.title("Conversion failed"), str(e))
return
self.busy(False)
self.log(" OUTPUT FILE:")
self.log(" %s" % os.path.basename(out))
self.log(" OUTPUT PATH:")
self.log(" %s" % os.path.dirname(out))
self.log(" wrote %d bytes" % n)
self.status.set("Done: %s" % os.path.basename(out))
def busy(self, on):
state = "disabled" if on else "normal"
self.b_sab2sax.configure(state=state)
self.b_sax2sab.configure(state=state)
self.root.configure(cursor="watch" if on else "")
self.root.update_idletasks()
def main():
root = tk.Tk()
App(root)
root.mainloop()
return 0
if __name__ == "__main__":
sys.exit(main())
+22
View File
@@ -0,0 +1,22 @@
"""Start here.
python run.py the window
pythonw run.py the window, without a console behind it
python run.py app.sab [out] straight to the command line converter
No arguments means the GUI; any argument is passed through to sab2sax.py, so
the same entry point serves a double-click and a script.
"""
import sys
def main(argv):
if len(argv) > 1:
import sab2sax
return sab2sax.main(argv) or 0
import gui
return gui.main()
if __name__ == "__main__":
sys.exit(main(sys.argv))
+27 -12
View File
@@ -1,6 +1,7 @@
"""Minimal pure-Python SAX -> SAB encoder, ported from sedona.offline.OfflineApp.""" """Minimal pure-Python SAX -> SAB encoder, ported from sedona.offline.OfflineApp."""
import os, struct, sys, base64 import os, struct, sys, base64, datetime
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import config
class Buf: class Buf:
def __init__(self): self.b = bytearray() def __init__(self): self.b = bytearray()
@@ -13,14 +14,9 @@ class Buf:
def str_(self, s): self.b += s.encode("ascii"); self.u1(0) def str_(self, s): self.b += s.encode("ascii"); self.u1(0)
# ---------- schema ---------- # ---------- schema ----------
def load_schema(sax_root, home): def load_kits(parts, home):
parts = [] """[(kitName, checksumHexOrNone)] in kit-id order -> (kits, types_by_qname)."""
for k in sax_root.find("schema").findall("kit"): kits = [] # [(name, checksum, [typeElem, ...])]
parts.append((k.get("name"), k.get("checksum")))
# sortKits: sys first, rest alphabetical
parts.sort(key=lambda p: ("" if p[0] == "sys" else "\x01" + p[0]))
kits = [] # [(name, checksum, {typeName: typeElem})]
types_by_q = {} types_by_q = {}
for kit_id, (name, cks) in enumerate(parts): for kit_id, (name, cks) in enumerate(parts):
d = os.path.join(home, "manifests", name) d = os.path.join(home, "manifests", name)
@@ -35,6 +31,13 @@ def load_schema(sax_root, home):
kits.append((name, int(mroot.get("checksum"), 16), tlist)) kits.append((name, int(mroot.get("checksum"), 16), tlist))
return kits, types_by_q return kits, types_by_q
def load_schema(sax_root, home):
parts = [(k.get("name"), k.get("checksum")) for k in sax_root.find("schema").findall("kit")]
# sortKits: sys first, rest alphabetical
parts.sort(key=lambda p: ("" if p[0] == "sys" else "\x01" + p[0]))
return load_kits(parts, home)
def resolve_slots(qname, types_by_q, cache): def resolve_slots(qname, types_by_q, cache):
"""Flatten base chain -> [(name, type, flags, default)] indexed by slot id.""" """Flatten base chain -> [(name, type, flags, default)] indexed by slot id."""
if qname in cache: return cache[qname] if qname in cache: return cache[qname]
@@ -81,7 +84,19 @@ def encode_value(out, vtype, flags, raw):
raise Exception("unhandled slot type " + str(vtype)) raise Exception("unhandled slot type " + str(vtype))
# ---------- main ---------- # ---------- main ----------
def encode(sax_path, home, sab_path): def default_sab_name(sax_path):
"""`app.sax` -> `app-sab2sax-20260728-140609.sab` -- never clobbers an existing .sab."""
stem = os.path.splitext(os.path.basename(sax_path))[0]
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
return "%s-sab2sax-%s.sab" % (stem, stamp)
def encode(sax_path, home=None, sab_path=None):
if home is None:
home = config.get_sedona_home()
if sab_path is None:
sab_path = os.path.join(os.path.dirname(os.path.abspath(sax_path)),
default_sab_name(sax_path))
root = ET.parse(sax_path).getroot() root = ET.parse(sax_path).getroot()
kits, types_by_q = load_schema(root, home) kits, types_by_q = load_schema(root, home)
kit_id = {k[0]: i for i, k in enumerate(kits)} kit_id = {k[0]: i for i, k in enumerate(kits)}
@@ -162,5 +177,5 @@ def encode(sax_path, home, sab_path):
return len(out.b) return len(out.b)
if __name__ == "__main__": if __name__ == "__main__":
n = encode(sys.argv[1], sys.argv[2], sys.argv[3]) import sab2sax # the CLI converts either direction; this is just one half
print("wrote", n, "bytes") sab2sax.main(sys.argv)
+66
View File
@@ -0,0 +1,66 @@
"""Entry point: converts either direction, the way `sedonac.exe <file>` does --
the input's extension decides.
python sab2sax.py [in.sax|in.sab] [out]
.sax in -> .sab out (sab.encode)
.sab in -> .sax out (sax.decode)
Omit the input and a file chooser opens; omit the output and a save dialog opens
next to the input, pre-filled with a timestamped name. Sedona home comes from
system.properties (see config.py).
"""
import os, sys
import config, sab, sax
def convert(in_path, out_path=None, home=None):
"""Convert `in_path` by extension, no dialogs. Returns (out_path, bytes written).
With no `out_path`, writes a timestamped name next to the input."""
ext = os.path.splitext(in_path)[1].lower()
if ext not in (".sax", ".sab"):
raise Exception("don't know what to do with %s -- expected .sax or .sab" % in_path)
if out_path is None:
name = sab.default_sab_name(in_path) if ext == ".sax" else sax.default_sax_name(in_path)
out_path = os.path.join(os.path.dirname(os.path.abspath(in_path)), name)
if ext == ".sax":
return out_path, sab.encode(in_path, home, out_path)
return out_path, sax.decode(in_path, home, out_path)
def main(argv):
print("%s %s" % (config.NAME, config.VERSION))
home = config.get_sedona_home()
in_path = argv[1] if len(argv) > 1 else config.choose_input_file()
if not in_path:
raise SystemExit("no input file selected")
in_path = os.path.abspath(in_path)
if not os.path.isfile(in_path):
raise SystemExit("no such file: " + in_path)
ext = os.path.splitext(in_path)[1].lower()
if ext not in (".sax", ".sab"):
raise SystemExit("don't know what to do with %s -- expected .sax or .sab" % in_path)
if len(argv) > 2:
out_path = argv[2]
elif ext == ".sax":
out_path = config.choose_sab_file(initialfile=sab.default_sab_name(in_path),
initial=os.path.dirname(in_path))
else:
out_path = config.choose_sax_output(initialfile=sax.default_sax_name(in_path),
initial=os.path.dirname(in_path))
if not out_path:
raise SystemExit("no output file selected")
if ext == ".sax":
n = sab.encode(in_path, home, out_path)
else:
n = sax.decode(in_path, home, out_path)
print("wrote %d bytes to %s" % (n, out_path))
if __name__ == "__main__":
main(sys.argv)
+352
View File
@@ -0,0 +1,352 @@
"""The other direction: .sab -> .sax, ported from sedona.offline.OfflineApp
(decodeAppBinary + encodeAppXml) and OfflineComponent/OfflineLink.
Output is meant to match `sedonac.exe <file.sab>` byte for byte, down to the
`<!-- /path -->` comments and Java's float formatting.
Note what a .sab simply does not contain: only *config* props are stored, so
runtime prop values cannot come back. sedonac has the same hole -- both write
the manifest default for them, and a value equal to its default is not written
at all.
"""
import os, struct, base64, math, datetime
import sab, config
class Reader:
def __init__(self, data):
self.b, self.p = data, 0
def take(self, n):
if self.p + n > len(self.b):
raise Exception("truncated sab at offset %d" % self.p)
v = self.b[self.p:self.p + n]
self.p += n
return v
def u1(self): return self.take(1)[0]
def u2(self): return struct.unpack(">H", self.take(2))[0]
def i4(self): return struct.unpack(">i", self.take(4))[0]
def i8(self): return struct.unpack(">q", self.take(8))[0]
def f4(self): return struct.unpack(">f", self.take(4))[0]
def f8(self): return struct.unpack(">d", self.take(8))[0]
def str_(self):
end = self.b.index(0, self.p)
s = self.b[self.p:end].decode("ascii")
self.p = end + 1
return s
# ---------- values ----------
def decode_value(r, vtype, flags):
if "s" in flags: # asStr Buf: u2 len (incl NUL)
n = r.u2()
return r.take(n).split(b"\0")[0].decode("ascii")
if vtype == "bool":
v = r.u1()
return None if v == 2 else (v == 1)
if vtype == "byte": return r.u1()
if vtype == "short": return r.u2()
if vtype == "int": return r.i4()
if vtype == "long": return r.i8()
if vtype == "float": return r.f4()
if vtype == "double": return r.f8()
if vtype in ("sys::Buf", "buf"):
return bytes(r.take(r.u2()))
raise Exception("unhandled slot type " + str(vtype))
def default_value(vtype, flags, raw):
"""The manifest `default`, or Value.defaultForType -> zero. Same rules as sab.py."""
if "s" in flags:
return raw or ""
if vtype == "bool":
return None if raw == "null" else raw in ("true", "1")
if vtype in ("byte", "short", "int", "long"):
return sab._int(raw)
if vtype == "float":
# the default is a 32-bit Float too, so round before comparing:
# 0.1 as a double is not the 0.1 that came out of the file
return to_f32(sab._flt(raw))
if vtype == "double":
return sab._flt(raw)
if vtype in ("sys::Buf", "buf"):
return base64.b64decode(raw) if raw else b""
raise Exception("unhandled slot type " + str(vtype))
def same(a, b):
if isinstance(a, float) and isinstance(b, float):
return (math.isnan(a) and math.isnan(b)) or a == b
return type(a) is type(b) and a == b
# ---------- java number formatting ----------
F32_MAX = 3.4028234663852886e38 # largest finite float
F32_INF_EDGE = 3.4028235677973366e38 # midpoint to 2**128: below rounds to F32_MAX
def to_f32(v):
"""Round a double to the nearest float. struct raises past F32_MAX instead
of rounding, so handle that edge ourselves."""
try:
return struct.unpack(">f", struct.pack(">f", v))[0]
except OverflowError:
if abs(v) < F32_INF_EDGE:
return math.copysign(F32_MAX, v)
return math.copysign(math.inf, v)
def _shortest(a, single):
"""Fewest digits that still round-trip -- what Java's Float/Double.toString picks."""
for prec in range(0, 17):
s = "%.*e" % (prec, a)
v = float(s)
if single:
v = to_f32(v)
if v == a:
break
mant, exp = s.split("e")
digits = mant.replace(".", "").rstrip("0") or "0"
return digits, int(exp)
def java_float(v, single=True):
"""Java Float.toString / Double.toString: always a decimal point, E-form
outside [1e-3, 1e7)."""
if math.isnan(v):
return "NaN"
if math.isinf(v):
return "-Infinity" if v < 0 else "Infinity"
neg = math.copysign(1.0, v) < 0
a = abs(v)
if a == 0.0:
return "-0.0" if neg else "0.0"
digits, exp = _shortest(a, single)
if -3 <= exp < 7:
if exp >= 0:
head = digits[:exp + 1].ljust(exp + 1, "0")
tail = digits[exp + 1:] or "0"
else:
head, tail = "0", "0" * (-exp - 1) + digits
out = head + "." + tail
else:
out = digits[0] + "." + (digits[1:] or "0") + "E" + str(exp)
return "-" + out if neg else out
def encode_string(val, vtype, flags):
"""Value.encodeString()."""
if "s" in flags:
return _literal(val)
if vtype == "bool":
return "null" if val is None else ("true" if val else "false")
if vtype == "float":
return "null" if math.isnan(val) else java_float(val, True)
if vtype == "double":
return "null" if math.isnan(val) else java_float(val, False)
if vtype in ("sys::Buf", "buf"):
return base64.b64encode(val).decode("ascii")
return str(val)
def _literal(s):
"""TextUtil.toLiteral."""
out = []
for c in s:
out.append({"\0": "\\0", "\n": "\\n", "\r": "\\r", "\t": "\\t",
"\\": "\\\\", '"': '\\"', "$": "\\$"}.get(c, c))
return "".join(out)
def _safe(s):
"""XWriter.safe: escape markup, quotes, and anything outside printable ASCII."""
out = []
for c in s:
o = ord(c)
if o < 0x20 or o > 0x7e or c in "'\"":
out.append("&#x%x;" % o)
elif c == "<":
out.append("&lt;")
elif c == ">":
out.append("&gt;")
elif c == "&":
out.append("&amp;")
else:
out.append(c)
return "".join(out)
# ---------- binary decode ----------
def read_app(sab_path, home):
"""-> (kits, comps by id, links). Each comp: id/type/name/parent/kids/props."""
r = Reader(open(sab_path, "rb").read())
if r.i4() != 0x73617070:
raise Exception("%s is not a .sab (bad magic)" % sab_path)
ver = r.i4()
if ver != 0x0003:
raise Exception("unsupported sab version 0x%x" % ver)
parts = []
for _ in range(r.u1()):
name = r.str_()
parts.append((name, "%08x" % (r.i4() & 0xFFFFFFFF)))
kits, types_by_q = sab.load_kits(parts, home)
r.u2() # maxId, recomputed on the way out
qname_of = {}
for ki, (kname, _, tlist) in enumerate(kits):
for t in tlist:
qname_of[(ki, int(t.get("id")))] = "%s::%s" % (kname, t.get("name"))
cache = {}
by_id, order = {}, []
while True:
cid = r.u2()
if cid == 0xFFFF:
break
ki, ti = r.u1(), r.u1()
if (ki, ti) not in qname_of:
raise Exception("comp %d: no type %d in kit %d" % (cid, ti, ki))
qname = qname_of[(ki, ti)]
c = {"id": cid, "type": qname, "name": r.str_(), "kids": [], "props": []}
c["parentId"], c["childId"], c["siblingId"] = r.u2(), r.u2(), r.u2()
for name, vtype, flags, dflt in sab.resolve_slots(qname, types_by_q, cache):
if "a" in flags or "c" not in flags:
continue # config props only, in slot-id order
c["props"].append((name, vtype, flags, dflt, decode_value(r, vtype, flags)))
if r.u1() != ord(";"):
raise Exception("corrupted component %d %s" % (cid, c["name"]))
if cid in by_id:
raise Exception("duplicate id: %d" % cid)
by_id[cid] = c
order.append(cid)
# rebuild the tree from the child/sibling id chain
def must(cid, what, owner):
if cid not in by_id:
raise Exception("comp %d %s: missing %s %d" % (owner["id"], owner["name"], what, cid))
return by_id[cid]
for c in by_id.values():
c["parent"] = must(c["parentId"], "parent", c) if c["parentId"] != 0xFFFF else None
if c["childId"] != 0xFFFF:
kid = must(c["childId"], "child", c)
while True:
c["kids"].append(kid)
if kid["siblingId"] == 0xFFFF:
break
kid = must(kid["siblingId"], "nextSibling", kid)
links = []
while True:
fc = r.u2()
if fc == 0xFFFF:
break
fs, tc, ts = r.u1(), r.u2(), r.u1()
for cid in (fc, tc):
if cid not in by_id:
raise Exception("link references unknown comp id %d" % cid)
links.append((by_id[fc], fs, by_id[tc], ts))
if r.u1() != ord("."):
raise Exception("invalid app end marker")
return kits, by_id, order, links, types_by_q, cache
# ---------- xml encode ----------
def path_of(c):
parts = []
while c["parent"] is not None:
parts.append(c["name"])
c = c["parent"]
return "/" + "/".join(reversed(parts)) if parts else "/"
def write_xml(out, kits, app, links, types_by_q, cache):
w = out.append
w("<?xml version='1.0'?>\n")
w("<sedonaApp>\n")
w("<schema>\n")
for name, cks, _ in kits:
w(' <kit name="%s" checksum="%08x" />\n' % (_safe(name), cks & 0xFFFFFFFF))
w("</schema>\n")
w("<app>\n")
write_props(w, app, 2)
for kid in app["kids"]:
write_comp(w, kid, 2)
w("</app>\n")
w("<links>\n")
for fc, fs, tc, ts in links:
w(' <link from="%s.%s" to="%s.%s"/>\n' % (
_safe(path_of(fc)), _safe(slot_name(fc, fs, types_by_q, cache)),
_safe(path_of(tc)), _safe(slot_name(tc, ts, types_by_q, cache))))
w("</links>\n")
w("</sedonaApp>\n")
def slot_name(comp, slot_id, types_by_q, cache):
slots = sab.resolve_slots(comp["type"], types_by_q, cache)
if slot_id >= len(slots):
raise Exception("%s has no slot id %d" % (comp["type"], slot_id))
return slots[slot_id][0]
def write_props(w, c, indent):
"""Only props whose value differs from the slot default get written."""
n = 0
for name, vtype, flags, dflt, val in c["props"]:
if same(val, default_value(vtype, flags, dflt)):
continue
w('%s<prop name="%s" val="%s"/>\n' % (
" " * indent, _safe(name), _safe(encode_string(val, vtype, flags))))
n += 1
return n
def write_comp(w, c, indent):
pad = " " * indent
w("%s<!-- %s -->\n" % (pad, path_of(c)))
w('%s<comp name="%s" id="%d" type="%s"' % (pad, _safe(c["name"]), c["id"], _safe(c["type"])))
body = []
n = write_props(body.append, c, indent + 2)
for kid in c["kids"]:
write_comp(body.append, kid, indent + 2)
if not n and not c["kids"]:
w("/>\n")
else:
w(">\n")
w("".join(body))
w("%s</comp>\n" % pad)
# ---------- main ----------
def default_sax_name(sab_path):
"""`app.sab` -> `app-sab2sax-20260728-140609.sax`."""
stem = os.path.splitext(os.path.basename(sab_path))[0]
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
return "%s-sab2sax-%s.sax" % (stem, stamp)
def decode(sab_path, home=None, sax_path=None):
if home is None:
home = config.get_sedona_home()
if sax_path is None:
sax_path = os.path.join(os.path.dirname(os.path.abspath(sab_path)),
default_sax_name(sab_path))
kits, by_id, order, links, types_by_q, cache = read_app(sab_path, home)
if 0 not in by_id:
raise Exception("no app component (id 0) in " + sab_path)
out = []
write_xml(out, kits, by_id[0], links, types_by_q, cache)
text = "".join(out)
with open(sax_path, "w", encoding="ascii", newline="\n") as f:
f.write(text)
return len(text)
+4
View File
@@ -0,0 +1,4 @@
# Directory of the Sedona installation (the folder holding manifests/).
# Plain path, no escaping needed. If this is missing or wrong, sab.py asks for
# it with a folder chooser and writes the answer back here.
sedona.home=C:\Users\aa\OneDrive - Ontrol\Documents\_is\NiagaraAXSedona\sedona
+168 -25
View File
@@ -1,53 +1,196 @@
"""Regression check: our SAB output must be byte-identical to sedonac.exe's. """Regression checks: our output must be byte-identical to sedonac.exe's, and a
conversion must survive a round trip.
Usage: python verify.py [sedona_home] sax -> sab check_encode()
sab -> sax check_decode()
sab -> sax -> sab round_trip_sab() bytes must come back identical
sax -> sab -> sax -> sab round_trip_sax() the two sabs must be identical
Usage: python verify.py [app.sax|app.sab] [sedona_home]
With no file argument a chooser opens; cancel it and the built-in
`testprogram/` regression runs instead (if those files are present). Sedona home
comes from system.properties (see config.py) unless given.
""" """
import os, sys, subprocess, tempfile, shutil import os, sys, subprocess, tempfile, shutil
import sab import sab, sax, config
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
HOME = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(HERE), "sedona") TESTS = os.path.join(HERE, "testprogram")
HOME = None
def check(sax, reference=None): def home():
"""Encode `sax` with sab.py; compare against `reference` or a fresh sedonac run.""" """Resolved once, on first use -- so importing this module opens no dialog."""
tmp = tempfile.mkdtemp() global HOME
try: if HOME is None:
if reference is None: HOME = config.get_sedona_home()
work = os.path.join(tmp, os.path.basename(sax)) return HOME
shutil.copy(sax, work)
exe = os.path.join(HOME, "bin", "sedonac.exe")
def sedonac(src, tmp):
"""Run sedonac on a copy of `src`; return the file it produced (needs a JRE)."""
work = os.path.join(tmp, os.path.basename(src))
shutil.copy(src, work)
exe = os.path.join(home(), "bin", "sedonac.exe")
r = subprocess.run([exe, work], capture_output=True, text=True) r = subprocess.run([exe, work], capture_output=True, text=True)
if r.returncode != 0: if r.returncode != 0:
print(" sedonac failed:\n" + r.stdout + r.stderr) print(" sedonac failed:\n" + r.stdout + r.stderr)
return False return None
reference = os.path.splitext(work)[0] + ".sab" other = ".sax" if work.endswith(".sab") else ".sab"
return os.path.splitext(work)[0] + other
mine = os.path.join(tmp, "mine.sab")
sab.encode(sax, HOME, mine)
def compare(label, reference, mine, binary, names=("sedonac", "ours")):
a = open(reference, "rb").read() a = open(reference, "rb").read()
b = open(mine, "rb").read() b = open(mine, "rb").read()
name = os.path.basename(sax)
if a == b: if a == b:
print(" PASS %-28s %d bytes identical" % (name, len(a))) print(" PASS %-34s %d bytes identical" % (label, len(a)))
return True return True
diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]] diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]]
print(" FAIL %-28s sedonac=%d ours=%d, %d byte diffs" % (name, len(a), len(b), len(diffs))) print(" FAIL %-34s %s=%d %s=%d, %d byte diffs" % (
label, names[0], len(a), names[1], len(b), len(diffs)))
if diffs: if diffs:
i = diffs[0] i = diffs[0]
if binary:
lo, hi = max(0, i - 12), i + 12 lo, hi = max(0, i - 12), i + 12
print(" first diff at offset %d" % i) print(" first diff at offset %d" % i)
print(" sedonac %s" % a[lo:hi].hex()) print(" %-8s %s" % (names[0], a[lo:hi].hex()))
print(" ours %s" % b[lo:hi].hex()) print(" %-8s %s" % (names[1], b[lo:hi].hex()))
else:
line = a[:i].count(b"\n") + 1
print(" first diff at line %d" % line)
print(" %-8s %s" % (names[0], a.splitlines()[line - 1]))
print(" %-8s %s" % (names[1], b.splitlines()[line - 1]))
return False return False
# ---------- against sedonac ----------
def check_encode(sax_file, reference=None):
"""sax -> sab, against `reference` or a fresh sedonac run."""
tmp = tempfile.mkdtemp()
try:
reference = reference or sedonac(sax_file, tmp)
if reference is None:
return False
mine = os.path.join(tmp, "mine.sab")
sab.encode(sax_file, home(), mine)
return compare(os.path.basename(sax_file) + " -> sab", reference, mine, True)
finally: finally:
shutil.rmtree(tmp, ignore_errors=True) shutil.rmtree(tmp, ignore_errors=True)
def check_decode(sab_file, reference=None):
"""sab -> sax, against `reference` or a fresh sedonac run."""
tmp = tempfile.mkdtemp()
try:
reference = reference or sedonac(sab_file, tmp)
if reference is None:
return False
mine = os.path.join(tmp, "mine.sax")
sax.decode(sab_file, home(), mine)
return compare(os.path.basename(sab_file) + " -> sax", reference, mine, False)
finally:
shutil.rmtree(tmp, ignore_errors=True)
# ---------- round trips ----------
def round_trip_sab(sab_file):
"""sab -> sax -> sab must come back to the same bytes."""
tmp = tempfile.mkdtemp()
try:
mid = os.path.join(tmp, "mid.sax")
back = os.path.join(tmp, "back.sab")
sax.decode(sab_file, home(), mid)
sab.encode(mid, home(), back)
return compare(os.path.basename(sab_file) + " sab->sax->sab",
sab_file, back, True, ("original", "ours"))
finally:
shutil.rmtree(tmp, ignore_errors=True)
def round_trip_sax(sax_file):
"""sax -> sab -> sax -> sab: the two sabs must match.
The sax leg is not compared against the original file, because a .sab holds
no runtime prop values and no formatting -- coming back through it is lossy
on purpose. The binary is the thing that has to be stable, so we compare
that. Any difference from the original sax is printed as a note, not a
failure."""
tmp = tempfile.mkdtemp()
try:
first = os.path.join(tmp, "first.sab")
mid = os.path.join(tmp, "mid.sax")
second = os.path.join(tmp, "second.sab")
sab.encode(sax_file, home(), first)
sax.decode(first, home(), mid)
sab.encode(mid, home(), second)
ok = compare(os.path.basename(sax_file) + " sax->sab->sax->sab",
first, second, True, ("1st sab", "2nd sab"))
a = open(sax_file, "rb").read()
b = open(mid, "rb").read()
if a != b:
al, bl = a.splitlines(), b.splitlines()
print(" note: the regenerated sax differs from the original "
"(%d vs %d lines) -- expected where the original carries runtime "
"props or different formatting" % (len(al), len(bl)))
return ok
finally:
shutil.rmtree(tmp, ignore_errors=True)
def check_file(path):
"""Round-trip whatever was chosen, by extension."""
ext = os.path.splitext(path)[1].lower()
if ext not in (".sax", ".sab"):
print(" don't know what to do with %s -- expected .sax or .sab" % path)
return False
try:
return round_trip_sax(path) if ext == ".sax" else round_trip_sab(path)
except Exception as e: # a malformed file is a failure, not a traceback
print(" FAIL %-34s %s" % (os.path.basename(path), e))
return False
def builtin():
"""The checked-in pair, when testprogram/ is populated locally."""
saxf = os.path.join(TESTS, "test_normal.sax")
sabf = os.path.join(TESTS, "test_normal_sedonac.sab")
missing = [f for f in (saxf, sabf) if not os.path.isfile(f)]
if missing:
# testprogram/ is deliberately not in the repo -- put the pair back to run this
print(" SKIP no test files: %s" % ", ".join(os.path.basename(f) for f in missing))
return None
ok = check_encode(saxf, sabf)
ok &= check_decode(sabf, saxf)
ok &= round_trip_sab(sabf)
ok &= round_trip_sax(saxf)
return ok
def main(argv):
global HOME
print("%s %s" % (config.NAME, config.VERSION))
path = argv[1] if len(argv) > 1 else config.choose_input_file()
if len(argv) > 2:
HOME = argv[2]
print("sedona home: %s" % home())
if not path:
print("no file chosen -- running the built-in checks")
ok = builtin()
return 0 if ok is not False else 1
path = os.path.abspath(path)
if not os.path.isfile(path):
print("no such file: " + path)
return 1
return 0 if check_file(path) else 1
if __name__ == "__main__": if __name__ == "__main__":
print("sedona home: %s" % HOME) sys.exit(main(sys.argv))
ok = check(os.path.join(HERE, "test_normal.sax"),
os.path.join(HERE, "test_normal_sedonac.sab"))
sys.exit(0 if ok else 1)