#!/usr/bin/env python3 """ NetFS — Binary File Server for Sharp MZ BASIC Network Drive Serves MZF files from a local directory over TCP using a simple binary protocol designed for easy Z80 assembly parsing. Binary Protocol: All requests start with 1-byte command. All responses start with 1-byte status. Status: 0x00=OK, 0xFF=error, 0xFE=end of data. DIR (0x01): Request: [0x01] [unit:1] Response: [0x00] [count:1] [entry0:32] ... [entryN:32] Each entry is 32 bytes: atrb(1) + name(17) + size(2LE) + dtadr(2LE) + exadr(2LE) + pad(8) Returns up to 63 entries from the directory mapped to unit (1-7). Max 63 (2KB RX buffer). INFO (0x02): Request: [0x02] [filename:17] Response: [0x00] [entry:32] (or [0xFF] if not found) READ (0x03): Request: [0x03] [unit:1] [filename:17] [offset:2LE] [length:2LE] Response: [0x00] [actual_len:2LE] [data:actual_len] (or [0xFF] if not found) WRITE (0x04): Request: [0x04] [unit:1] [filename:17] [atrb:1] [size:2LE] [dtadr:2LE] [exadr:2LE] [data:size] Response: [0x00] (or [0xFF] on error) CLOSE (0x05): Request: [0x05] Response: [0x00] (server closes connection) DELETE (0x06): Request: [0x06] [filename:17] Response: [0x00] (or [0xFF] if not found) All commands include a unit byte (1-7) selecting the server directory. Usage: python3 netfs.py [--port 6800] [--dir ./mzf_files] [--dir2 path] ... [--dir7 path] --dir maps to NET1: (or NET: with no digit), --dir2 maps to NET2:, etc. Units without a --dirN argument fall back to --dir. (c) 2026 Philip Smart """ import argparse import os import socket import struct import threading import sys DEFAULT_PORT = 6800 DEFAULT_DIR = "." MZF_HEADER_SIZE = 128 # Protocol constants CMD_DIR = 0x01 CMD_INFO = 0x02 CMD_READ = 0x03 CMD_WRITE = 0x04 CMD_CLOSE = 0x05 CMD_DELETE = 0x06 STATUS_OK = 0x00 STATUS_ERR = 0xFF STATUS_END = 0xFE def parse_mzf_header(data): """Parse a 128-byte MZF header.""" if len(data) < MZF_HEADER_SIZE: return None atrb = data[0] # Remap RB/BSD/BRD types to BTX (type 2) so BASIC can LOAD them if atrb in (0x03, 0x04, 0x05): atrb = 0x02 name_raw = data[1:18] name = name_raw.rstrip(b'\x00\x0d').decode('ascii', errors='replace') size = struct.unpack(' 0: chunk = conn.recv(min(remaining, 4096)) if not chunk: break remaining -= len(chunk) conn.sendall(bytes([STATUS_ERR])) name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip() print(f"[{addr}] WRITE unit={unit} '{name_str}' → unit not configured") continue # Read file data data = b'' remaining = size while remaining > 0: chunk = conn.recv(min(remaining, 4096)) if not chunk: break data += chunk remaining -= len(chunk) # Build MZF file name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip() fname = name_str.replace(' ', '_') + '.mzf' fpath = os.path.join(dp, fname) hdr = bytearray(MZF_HEADER_SIZE) hdr[0] = atrb hdr[1:18] = name_bytes[:17] struct.pack_into('