Roflin fd95643593 scripts: add a read-only FSP server so Swiss can stream a disc image
sudo ./scripts/fsp-server.py --root /workspace --password alltheroms

Swiss streams games over FSP/UDP rather than TCP, so hosting an image needs an
FSP server.  Serves a directory read-only: VERSION, STAT, GET_DIR, GET_FILE,
BYE, with CC_ERR for anything else.  Reports the achieved read rate every two
seconds, which is the measurement this was really built for -- the 850 KB/s
figure came from the menu's lwIP/TCP path and says nothing about streaming.

The protocol was taken from Swiss's own two clients rather than from memory,
because they disagree about nothing but exercise different commands:
  cube/swiss/source/devices/fsp/fsplib.c  (menu: browse, stat, read)
  cube/patches/bba/tcpip.c                (in-game streaming reads)

Details that are easy to get wrong, all verified against that source:
  * checksum: sum = total_size + every byte with the checksum byte zeroed,
    then sum += sum >> 8, low 8 bits.
  * password: the data field is "path\npassword\0" -- the '\n' REPLACES the
    path's terminating NUL (fsplib buildfilename), it is not appended after it.
  * GET_DIR: entries are time(4) size(4) type(1) name(NUL) padded to 4 bytes;
    the client reads blocks until one is shorter than the first, so the whole
    listing then an EMPTY reply is what terminates it.  dirpos % 4 != 0 is
    rejected outright by fsplib, hence the alignment padding.
  * STAT: the same 9-byte time/size/type prefix; type 0 means ENOENT.
  * GET_FILE: sequence AND position must be echoed -- the in-game client
    discards any reply where either differs.
  * ⚠️ a GET_FILE reply must be a multiple of 32 bytes unless it is the tail,
    because the in-game client does
        data_size = MIN(requested, OSRoundDown32B(reply_len))
    so a non-32-aligned reply is silently rounded DOWN and the stream stalls.

test-fsp-server.py runs a real server on 127.0.0.1 and drives it with a client
that packs requests exactly as Swiss does: checksums verify both ways, STAT
returns the true size, GET_DIR parses with fsplib's own walk and the second
page is empty, GET_FILE returns byte-exact data at three offsets with seq and
pos echoed, a 1024-byte request comes back 32-aligned, a wrong password gives
CC_ERR, and ../../etc/passwd is refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 16:06:08 +02:00
2026-06-13 19:02:58 +02:00
2026-08-28 10:29:41 +02:00
2026-06-16 14:59:18 +02:00
2026-08-28 10:29:41 +02:00
S
Description
No description provided
60 MiB
Languages
Python 84.9%
C 12.4%
Shell 1.1%
Dockerfile 0.7%
Makefile 0.4%
Other 0.5%