"""USB Serial/JTAG SPI NAND file transfer client (Python 3 + pyserial).""" import argparse import os import secrets import stat import struct import sys import time import zlib from pathlib import Path try: import serial except ImportError: serial = None MAGIC = b"NXFR" HEADER = struct.Struct("<4sBBHII") INFO, LIST, MKDIR, BEGIN, BLOCK, END, VERIFY, CANCEL, PREPARE, CONFIRM, HELLO = range(1, 12) ERRORS = ["OK", "bad frame", "bad path", "not mounted", "I/O error", "bad state", "CRC/size mismatch", "denied"] ROOT = "/extflash" def path_bytes(path, allow_root=False): if not isinstance(path, str) or not path.startswith(ROOT): raise ValueError("destination must start with /extflash") if path == ROOT: if not allow_root: raise ValueError("the volume root is not a file/directory name") elif not path.startswith(ROOT + "/"): raise ValueError("path outside /extflash") else: parts = path[len(ROOT) + 1:].split("/") if any(not p or p in (".", "..") or p[-1] in " ." or any(ord(c) < 32 or c in '\\:*?"<>|' for c in p) for p in parts): raise ValueError("invalid FAT path") value = path.encode("utf-8") if len(value) >= 512: raise ValueError("UTF-8 path too long") return value def encode(cmd, seq, payload=b""): if len(payload) > 4352: raise ValueError("frame too large") body = HEADER.pack(MAGIC, 1, cmd, 0, seq, len(payload)) + payload return body + struct.pack(" 4352: raise ValueError("invalid response header") payload = self._exact(size, deadline) checksum, = struct.unpack(" len(body): raise ValueError("truncated directory entry") is_dir, name_len, size = struct.unpack_from(" len(body): raise ValueError("truncated directory name") name = body[i:i + name_len].decode("utf-8") i += name_len yield (is_dir, name, size) if done: return if next_cursor <= cursor: raise ValueError("directory cursor did not advance") cursor = next_cursor def upload(self, source, dest, log=print): dest = path_bytes(dest).decode("utf-8") try: self.call(BEGIN, path_bytes(dest)) size = 0 crc = 0 with open(source, "rb") as stream: while True: data = stream.read(4096) if not data: break if size > 0xffffffff: raise ValueError("file exceeds 4 GiB protocol offset limit") ack = self.call(BLOCK, struct.pack(" {dest} ({size} bytes, CRC32 {crc:08x})") except Exception: try: self.call(CANCEL, timeout=1, retries=1) except Exception: pass raise def directory_entries(source): root = Path(source) for base, dirs, files in os.walk(root, followlinks=False): dirs.sort() files.sort() for name in sorted(dirs + files): path = Path(base) / name mode = path.lstat().st_mode if stat.S_ISLNK(mode) or (hasattr(path, "is_junction") and path.is_junction()): raise ValueError(f"link/junction not allowed: {path}") if not (stat.S_ISDIR(mode) or stat.S_ISREG(mode)): raise ValueError(f"not a normal file or directory: {path}") yield path, path.relative_to(root).parts, stat.S_ISDIR(mode) def open_port(name): port = serial.Serial() port.port = name port.baudrate = 115200 port.timeout = 0.2 port.write_timeout = 15 port.dsrdtr = False port.rtscts = False port.open() try: port.setRTS(False) port.setDTR(True) except (AttributeError, OSError): pass return port def format_volume(client): token = client.call(PREPARE) if len(token) != 4: raise ValueError("invalid format challenge") # Confirmation is acquired before PREPARE so the 30-second device token cannot expire at the prompt. client.call(CONFIRM, token + struct.pack("