First commit tekrar
Bu dosyalar Claude uzerinden olusturuldu
This commit is contained in:
parent
1ca674ad96
commit
3e313f8223
73
README.md
Normal file
73
README.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 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 <app.sax> <sedona_home> <out.sab>
|
||||
|
||||
`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
|
||||
`<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".
|
||||
|
||||
## 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 <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.
|
||||
166
sab.py
Normal file
166
sab.py
Normal file
@ -0,0 +1,166 @@
|
||||
"""Minimal pure-Python SAX -> SAB encoder, ported from sedona.offline.OfflineApp."""
|
||||
import os, struct, sys, base64
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class Buf:
|
||||
def __init__(self): self.b = bytearray()
|
||||
def u1(self, v): self.b.append(v & 0xFF)
|
||||
def u2(self, v): self.b += struct.pack(">H", v & 0xFFFF)
|
||||
def i4(self, v): self.b += struct.pack(">i", v if -2**31 <= v < 2**31 else v - 2**32)
|
||||
def i8(self, v): self.b += struct.pack(">q", v)
|
||||
def f4(self, v): self.b += struct.pack(">f", v)
|
||||
def f8(self, v): self.b += struct.pack(">d", v)
|
||||
def str_(self, s): self.b += s.encode("ascii"); self.u1(0)
|
||||
|
||||
# ---------- schema ----------
|
||||
def load_schema(sax_root, home):
|
||||
parts = []
|
||||
for k in sax_root.find("schema").findall("kit"):
|
||||
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 = {}
|
||||
for kit_id, (name, cks) in enumerate(parts):
|
||||
d = os.path.join(home, "manifests", name)
|
||||
f = os.path.join(d, "%s-%s.xml" % (name, cks)) if cks else None
|
||||
if not f or not os.path.exists(f):
|
||||
cands = sorted(x for x in os.listdir(d) if x.startswith(name + "-") and x.endswith(".xml"))
|
||||
f = os.path.join(d, cands[-1])
|
||||
mroot = ET.parse(f).getroot()
|
||||
tlist = sorted(mroot.findall("type"), key=lambda t: int(t.get("id")))
|
||||
for t in tlist:
|
||||
types_by_q["%s::%s" % (name, t.get("name"))] = t
|
||||
kits.append((name, int(mroot.get("checksum"), 16), tlist))
|
||||
return kits, types_by_q
|
||||
|
||||
def resolve_slots(qname, types_by_q, cache):
|
||||
"""Flatten base chain -> [(name, type, flags, default)] indexed by slot id."""
|
||||
if qname in cache: return cache[qname]
|
||||
t = types_by_q[qname]
|
||||
base = t.get("base")
|
||||
out = list(resolve_slots(base, types_by_q, cache)) if base else []
|
||||
byname = {s[0]: i for i, s in enumerate(out)}
|
||||
for s in sorted(t.findall("slot"), key=lambda s: int(s.get("id"))):
|
||||
rec = (s.get("name"), s.get("type"), s.get("flags", ""), s.get("default"))
|
||||
if rec[0] in byname: # action override keeps the inherited id
|
||||
out[byname[rec[0]]] = rec
|
||||
else:
|
||||
byname[rec[0]] = len(out); out.append(rec)
|
||||
cache[qname] = out
|
||||
return out
|
||||
|
||||
# ---------- values ----------
|
||||
def _int(raw): # Integer.decode: supports 0x / # / leading 0 octal
|
||||
if raw in (None, "", "null"): return 0
|
||||
return int(raw, 0) if raw.lower().startswith(("0x", "-0x")) else int(raw)
|
||||
|
||||
def _flt(raw):
|
||||
if raw == "null": return float("nan") # explicit null only
|
||||
if raw in (None, ""): return 0.0 # Value.defaultForType -> ZERO
|
||||
return float(raw)
|
||||
|
||||
def encode_value(out, vtype, flags, raw):
|
||||
if "s" in flags: # asStr Buf
|
||||
s = raw or ""
|
||||
out.u2(len(s) + 1)
|
||||
out.b += s.encode("ascii"); out.u1(0)
|
||||
elif vtype == "bool":
|
||||
out.u1(2 if raw == "null" else (1 if raw in ("true", "1") else 0))
|
||||
elif vtype == "byte": out.u1(_int(raw))
|
||||
elif vtype == "short": out.u2(_int(raw))
|
||||
elif vtype == "int": out.i4(_int(raw))
|
||||
elif vtype == "long": out.i8(_int(raw))
|
||||
elif vtype == "float": out.f4(_flt(raw))
|
||||
elif vtype == "double": out.f8(_flt(raw))
|
||||
elif vtype == "sys::Buf" or vtype == "buf":
|
||||
data = base64.b64decode(raw) if raw else b""
|
||||
out.u2(len(data)); out.b += data
|
||||
else:
|
||||
raise Exception("unhandled slot type " + str(vtype))
|
||||
|
||||
# ---------- main ----------
|
||||
def encode(sax_path, home, sab_path):
|
||||
root = ET.parse(sax_path).getroot()
|
||||
kits, types_by_q = load_schema(root, home)
|
||||
kit_id = {k[0]: i for i, k in enumerate(kits)}
|
||||
type_id = {}
|
||||
for ki, (kname, _, tlist) in enumerate(kits):
|
||||
for t in tlist:
|
||||
type_id["%s::%s" % (kname, t.get("name"))] = (ki, int(t.get("id")))
|
||||
cache = {}
|
||||
|
||||
# walk tree -> comps with (elem, type, parent, kids)
|
||||
comps = []
|
||||
def walk(elem, qtype, parent):
|
||||
rec = {"elem": elem, "type": qtype, "parent": parent, "kids": [],
|
||||
"id": int(elem.get("id")) if elem.get("id") else None}
|
||||
comps.append(rec)
|
||||
if parent is not None: parent["kids"].append(rec)
|
||||
for c in elem.findall("comp"):
|
||||
walk(c, c.get("type"), rec)
|
||||
return rec
|
||||
|
||||
app_elem = root.find("app")
|
||||
app = walk(app_elem, "sys::App", None)
|
||||
app["id"] = 0
|
||||
|
||||
# assign missing ids
|
||||
used = {c["id"] for c in comps if c["id"] is not None}
|
||||
nxt = 1
|
||||
for c in comps:
|
||||
if c["id"] is None:
|
||||
while nxt in used: nxt += 1
|
||||
c["id"] = nxt; used.add(nxt)
|
||||
by_id = {c["id"]: c for c in comps}
|
||||
|
||||
out = Buf()
|
||||
out.i4(0x73617070) # "sapp"
|
||||
out.i4(0x0003) # version 0.3
|
||||
out.u1(len(kits)) # schema
|
||||
for name, cks, _ in kits:
|
||||
out.str_(name); out.i4(cks)
|
||||
out.u2(max(by_id)) # maxId
|
||||
|
||||
for cid in sorted(by_id): # components, ascending id
|
||||
c = by_id[cid]
|
||||
slots = resolve_slots(c["type"], types_by_q, cache)
|
||||
ki, ti = type_id[c["type"]]
|
||||
out.u2(cid); out.u1(ki); out.u1(ti)
|
||||
out.str_(c["elem"].get("name", "app") if cid else "app")
|
||||
out.u2(c["parent"]["id"] if c["parent"] else 0xFFFF)
|
||||
out.u2(c["kids"][0]["id"] if c["kids"] else 0xFFFF)
|
||||
sib = 0xFFFF
|
||||
if c["parent"]:
|
||||
k = c["parent"]["kids"]; i = k.index(c)
|
||||
if i + 1 < len(k): sib = k[i + 1]["id"]
|
||||
out.u2(sib)
|
||||
props = {p.get("name"): p.get("val") for p in c["elem"].findall("prop")}
|
||||
for name, vtype, flags, dflt in slots:
|
||||
if "a" in flags or "c" not in flags: continue # config props only
|
||||
encode_value(out, vtype, flags, props.get(name, dflt))
|
||||
out.u1(ord(';'))
|
||||
out.u2(0xFFFF)
|
||||
|
||||
links = root.find("links")
|
||||
if links is not None:
|
||||
for ln in links.findall("link"):
|
||||
def ref(s):
|
||||
path, slot = s.rsplit(".", 1)
|
||||
cur = app
|
||||
for part in [p for p in path.split("/") if p]:
|
||||
cur = next(k for k in cur["kids"] if k["elem"].get("name") == part)
|
||||
sl = resolve_slots(cur["type"], types_by_q, cache)
|
||||
return cur["id"], [x[0] for x in sl].index(slot)
|
||||
fc, fs = ref(ln.get("from")); tc, ts = ref(ln.get("to"))
|
||||
out.u2(fc); out.u1(fs); out.u2(tc); out.u1(ts)
|
||||
out.u2(0xFFFF)
|
||||
out.u1(ord('.'))
|
||||
|
||||
open(sab_path, "wb").write(bytes(out.b))
|
||||
return len(out.b)
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = encode(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
print("wrote", n, "bytes")
|
||||
53
verify.py
Normal file
53
verify.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Regression check: our SAB output must be byte-identical to sedonac.exe's.
|
||||
|
||||
Usage: python verify.py [sedona_home]
|
||||
"""
|
||||
import os, sys, subprocess, tempfile, shutil
|
||||
import sab
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def check(sax, reference=None):
|
||||
"""Encode `sax` with sab.py; compare against `reference` or a fresh sedonac run."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
try:
|
||||
if reference is None:
|
||||
work = os.path.join(tmp, os.path.basename(sax))
|
||||
shutil.copy(sax, work)
|
||||
exe = os.path.join(HOME, "bin", "sedonac.exe")
|
||||
r = subprocess.run([exe, work], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
print(" sedonac failed:\n" + r.stdout + r.stderr)
|
||||
return False
|
||||
reference = os.path.splitext(work)[0] + ".sab"
|
||||
|
||||
mine = os.path.join(tmp, "mine.sab")
|
||||
sab.encode(sax, HOME, mine)
|
||||
|
||||
a = open(reference, "rb").read()
|
||||
b = open(mine, "rb").read()
|
||||
name = os.path.basename(sax)
|
||||
if a == b:
|
||||
print(" PASS %-28s %d bytes identical" % (name, len(a)))
|
||||
return True
|
||||
|
||||
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)))
|
||||
if diffs:
|
||||
i = diffs[0]
|
||||
lo, hi = max(0, i - 12), i + 12
|
||||
print(" first diff at offset %d" % i)
|
||||
print(" sedonac %s" % a[lo:hi].hex())
|
||||
print(" ours %s" % b[lo:hi].hex())
|
||||
return False
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("sedona home: %s" % HOME)
|
||||
ok = check(os.path.join(HERE, "test_normal.sax"),
|
||||
os.path.join(HERE, "test_normal_sedonac.sab"))
|
||||
sys.exit(0 if ok else 1)
|
||||
Loading…
Reference in New Issue
Block a user