diff --git a/scripts/fsp-server.py b/scripts/fsp-server.py index 287063d..b83e844 100755 --- a/scripts/fsp-server.py +++ b/scripts/fsp-server.py @@ -40,6 +40,15 @@ import time CC_VERSION, CC_INFO = 0x10, 0x11 CC_ERR, CC_GET_DIR, CC_GET_FILE = 0x40, 0x41, 0x42 CC_BYE, CC_STAT = 0x4A, 0x4D +CC_GET_PRO = 0x47 +# Directory protection bits (fsplib.h). Swiss uses these to decide whether +# the device supports patches: +# if ((pro & (MKDIR|ADD|DEL|GET)) == (MKDIR|ADD|DEL) || (pro & OWNER)) +# features |= FEAT_PATCHES; emulable |= EMU_ETHERNET; +# so a plain read-only answer makes Swiss DISABLE patch support here. +PRO_OWNER, PRO_DEL, PRO_ADD, PRO_MKDIR = 0x01, 0x02, 0x04, 0x08 +PRO_GET, PRO_README, PRO_LIST, PRO_RENAME = 0x10, 0x20, 0x40, 0x80 +PRO_BYTES = 1 RDTYPE_END, RDTYPE_FILE, RDTYPE_DIR, RDTYPE_SKIP = 0x00, 0x01, 0x02, 0x2A HSIZE, FSP_SPACE = 12, 1024 SERVER_KEY = 0x1234 @@ -128,6 +137,13 @@ def main(): ap.add_argument("--port", type=int, default=21) ap.add_argument("--quiet-reads", action="store_true", help="only log the rate, not every read") + ap.add_argument("--claim-owner", action="store_true", + help="report FSP_DIR_OWNER on GET_PRO, which makes Swiss " + "enable FEAT_PATCHES and EMU_ETHERNET on this device. " + "⚠️ This server is READ-ONLY -- it has no UP_LOAD or " + "MAKE_DIR -- so Swiss believing it can write here " + "will make any patch write fail. Try without it " + "first; use it only if a boot demands patches.") a = ap.parse_args() root = os.path.realpath(a.root) @@ -192,6 +208,25 @@ def main(): s.sendto(pack(CC_STAT, SERVER_KEY, seq, pos, body), peer) continue + if cmd == CC_GET_PRO: + # Reply shape (fsplib fsp_getpro): pos MUST equal FSP_PRO_BYTES, + # and the flags byte sits at buf[len] -- i.e. the first byte of + # the EXTRA region, after the echoed name, not inside data. + # A non-existent directory must be CC_ERR so fsplib reports + # ENOENT: Swiss probes "swiss/patches", then "swiss", then "". + if not os.path.isdir(real): + s.sendto(pack(CC_ERR, SERVER_KEY, seq, pos, b"no such dir\0"), peer) + print(f"[{peer[0]}] GET_PRO {path!r} -> ENOENT (expected; " + f"Swiss falls back)") + continue + pro = PRO_GET | PRO_LIST | (PRO_OWNER if a.claim_owner else 0) + s.sendto(pack(CC_GET_PRO, SERVER_KEY, seq, PRO_BYTES, + path.encode("latin-1", "replace"), bytes([pro])), peer) + print(f"[{peer[0]}] GET_PRO {path!r} -> {pro:#04x}" + + (" (OWNER: Swiss will enable patches)" if a.claim_owner + else " (read-only: Swiss disables patches here)")) + continue + if cmd == CC_GET_DIR: if not os.path.isdir(real): s.sendto(pack(CC_ERR, SERVER_KEY, seq, pos, b"not a dir\0"), peer) diff --git a/scripts/test-fsp-server.py b/scripts/test-fsp-server.py index 0b9cc68..615351a 100755 --- a/scripts/test-fsp-server.py +++ b/scripts/test-fsp-server.py @@ -124,6 +124,36 @@ try: fails.append(f"T8 traversal escaped the root: type {ty} size {sz}") else: print("T8 ../../etc/passwd refused") + # T9 GET_PRO: flags go in the EXTRA region at buf[len], and pos must + # equal FSP_PRO_BYTES (1) or fsplib returns ENOMSG. + CC_GET_PRO = 0x47 + r = rt(req(CC_GET_PRO, 20, 0, "")) + if r["cmd"] != CC_GET_PRO: + fails.append(f"T9 GET_PRO cmd {r['cmd']:#04x}") + elif r["pos"] != 1: + fails.append(f"T9 GET_PRO pos {r['pos']}, want 1 (FSP_PRO_BYTES) " + f"- fsplib returns ENOMSG otherwise") + else: + s.sendto(req(CC_GET_PRO, 21, 0, ""), A) + raw = s.recv(2048) + ln = struct.unpack("!H", raw[8:10])[0] + pro = raw[HSIZE + ln] # buf[len] -- the first extra byte + if not (pro & 0x10) or not (pro & 0x40): + fails.append(f"T9 GET_PRO flags {pro:#04x} lack GET|LIST") + elif pro & 0x01: + fails.append(f"T9 GET_PRO claims OWNER without --claim-owner") + else: + print(f"T9 GET_PRO -> pos=1, flags {pro:#04x} (GET|LIST) at buf[len]") + + # T10 a missing directory must be CC_ERR, so fsplib reports ENOENT and + # Swiss falls back from "swiss/patches" to "swiss" to "". + r = rt(req(CC_GET_PRO, 22, 0, "swiss/patches")) + if r["cmd"] != CC_ERR: + fails.append(f"T10 GET_PRO on a missing dir returned {r['cmd']:#04x}, " + f"want CC_ERR -- Swiss's fallback chain needs ENOENT") + else: + print("T10 GET_PRO 'swiss/patches' -> CC_ERR (Swiss falls back to root)") + finally: srv.terminate()