From ddbe5645e9d1388050fdcab3e21aae4c3698fbf2 Mon Sep 17 00:00:00 2001 From: VG31OP Date: Tue, 13 Oct 2026 17:57:38 +0530 Subject: [PATCH] fix(firmware): sanitize Windows reserved device names in state paths On Windows, files whose basenames match DOS reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) cannot be created, causing provision.py to fail with FileNotFoundError whenever Windows COM ports (e.g. COM7) were used. This also caused 16 test failures in the esp32-csi-node test suite on Windows. - Prefix reserved device names with '_' in _state_path_for. - Add legacy unprefixed path fallback in load_state and load_board_state for backward compatibility. - Add regression tests in test_provision_state.py for reserved device prefixing. - Add harness/homecore/** text=auto eol=lf in .gitattributes to maintain byte-for-byte provenance and prevent manifest.sha256 mismatch on Windows checkouts. Validation: All 73 esp32-csi-node tests pass on Windows, and all 120 harness/ruview security tests, package manifest, and brain verification pass. --- .gitattributes | 1 + firmware/esp32-csi-node/provision.py | 20 ++++++++++++++++++- .../tests/test_provision_state.py | 11 +++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 290642ce1a..8a73904ea3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ # The contributor harness hashes provenance inputs byte-for-byte. Keep text # files in this boundary on LF even when Windows enables core.autocrlf. harness/ruview/** text=auto eol=lf +harness/homecore/** text=auto eol=lf diff --git a/firmware/esp32-csi-node/provision.py b/firmware/esp32-csi-node/provision.py index 46a7aab310..a57fa3b14c 100644 --- a/firmware/esp32-csi-node/provision.py +++ b/firmware/esp32-csi-node/provision.py @@ -136,9 +136,16 @@ def _default_state_dir() -> str: return os.path.join(base, "wifi-densepose", "esp32-provision-state") +_RESERVED_DEVICE_RE = re.compile( + r"^(con|prn|aux|nul|com[1-9]|lpt[1-9])$", re.IGNORECASE +) + + def _state_path_for(port: str, state_dir: str) -> str: """File path for a given serial port. Sanitize the port for filesystem use.""" safe = port.replace("/", "_").replace(":", "_").replace("\\", "_") + if _RESERVED_DEVICE_RE.match(safe): + safe = f"_{safe}" return os.path.join(state_dir, f"{safe}.json") @@ -306,7 +313,13 @@ def load_state(port: str, state_dir: str) -> dict: """Return the merged-state dict for `port`, or `{}` if absent / unreadable.""" path = _state_path_for(port, state_dir) if not os.path.isfile(path): - return {} + legacy = os.path.join( + state_dir, f"{port.replace('/', '_').replace(':', '_').replace('\\', '_')}.json" + ) + if legacy != path and os.path.isfile(legacy): + path = legacy + else: + return {} try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) @@ -443,6 +456,11 @@ def load_board_state(port: str, mac, state_dir: str): legacy = _state_path_for(port, state_dir) if os.path.isfile(legacy): return load_state(port, state_dir), legacy + unprefixed = os.path.join( + state_dir, f"{port.replace('/', '_').replace(':', '_').replace('\\', '_')}.json" + ) + if unprefixed != legacy and os.path.isfile(unprefixed): + return load_state(port, state_dir), unprefixed return {}, None diff --git a/firmware/esp32-csi-node/tests/test_provision_state.py b/firmware/esp32-csi-node/tests/test_provision_state.py index e55270e991..33af5ecc1c 100644 --- a/firmware/esp32-csi-node/tests/test_provision_state.py +++ b/firmware/esp32-csi-node/tests/test_provision_state.py @@ -122,7 +122,16 @@ def test_slashes_in_port_are_safe(self): def test_windows_com_port_is_safe(self): path = provision._state_path_for("COM7", "/tmp/x") - self.assertTrue(path.endswith("COM7.json")) + self.assertTrue(path.endswith("_COM7.json")) + + def test_reserved_dos_device_names_are_prefixed(self): + for name in ["CON", "PRN", "AUX", "NUL", "COM1", "COM9", "LPT1"]: + path = provision._state_path_for(name, "/tmp/x") + self.assertEqual(os.path.basename(path), f"_{name}.json") + + def test_non_reserved_ports_remain_unprefixed(self): + path = provision._state_path_for("/dev/ttyUSB0", "/tmp/x") + self.assertEqual(os.path.basename(path), "_dev_ttyUSB0.json") if __name__ == "__main__":