From c9308df3fc10d71c9d1f25a7147493bdb4e9a0ef Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch <297513015+Pitchfork-and-Torch@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:45:51 -0400 Subject: [PATCH] fix(windows-health): accept UTF-8 BOM in cleanup selection JSON Windows PowerShell Set-Content -Encoding UTF8 writes a BOM that made apply-cleanup-selection.py fail before dry-run/apply. Load selection files with utf-8-sig (and the scan data path in render-cleanup-plan) and cover BOM + BOM-less inputs in the validator regression suite. Fixes #1 --- .../assets/apply-cleanup-selection.py | 12 +++- .../assets/render-cleanup-plan.py | 2 +- .../assets/test_validate_command.py | 57 ++++++++++++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/skills/maintaining-windows-health/assets/apply-cleanup-selection.py b/skills/maintaining-windows-health/assets/apply-cleanup-selection.py index a2c5e03..0d64dea 100755 --- a/skills/maintaining-windows-health/assets/apply-cleanup-selection.py +++ b/skills/maintaining-windows-health/assets/apply-cleanup-selection.py @@ -383,6 +383,16 @@ def _run(command: str) -> subprocess.CompletedProcess: ) +def load_selection_json(path: Path) -> dict: + """Load a cleanup selection JSON, tolerating a UTF-8 BOM. + + Windows PowerShell `Set-Content -Encoding UTF8` (and several editors) + write a BOM. `json.loads` rejects a leading U+FEFF under encoding=utf-8; + utf-8-sig strips it when present and is a no-op for BOM-less files. + """ + return json.loads(path.read_text(encoding="utf-8-sig")) + + def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description="Apply a cleanup selection JSON (Windows)") ap.add_argument("selection", help="path to cleanup-selection-.json") @@ -398,7 +408,7 @@ def main(argv: list[str]) -> int: print(f"selection file not found: {sel_path}", file=sys.stderr) return 1 try: - sel = json.loads(sel_path.read_text(encoding="utf-8")) + sel = load_selection_json(sel_path) except Exception as exc: print(f"invalid selection JSON: {exc}", file=sys.stderr) return 1 diff --git a/skills/maintaining-windows-health/assets/render-cleanup-plan.py b/skills/maintaining-windows-health/assets/render-cleanup-plan.py index f9c6126..c92c508 100755 --- a/skills/maintaining-windows-health/assets/render-cleanup-plan.py +++ b/skills/maintaining-windows-health/assets/render-cleanup-plan.py @@ -1238,7 +1238,7 @@ def main(argv: list[str]) -> int: if not data_path.is_file(): print(f"data file not found: {data_path}", file=sys.stderr) return 2 - data = json.loads(data_path.read_text(encoding="utf-8")) + data = json.loads(data_path.read_text(encoding="utf-8-sig")) html_content = render_html(data) handler = make_handler(html_content, data.get("categories", [])) diff --git a/skills/maintaining-windows-health/assets/test_validate_command.py b/skills/maintaining-windows-health/assets/test_validate_command.py index 70387dc..c987362 100644 --- a/skills/maintaining-windows-health/assets/test_validate_command.py +++ b/skills/maintaining-windows-health/assets/test_validate_command.py @@ -143,6 +143,56 @@ def _load_validator(): ] +def test_load_selection_bom() -> int: + """Regression for issue #1: PowerShell UTF8 selection JSON carries a BOM.""" + import json + import tempfile + from pathlib import Path + + mod = _load_validator() + payload = { + "selected_items": [ + { + "id": "t1", + "label": "temp sample", + "path": r"C:\Users\dev\AppData\Local\Temp\sample", + "command": ( + r"Remove-Item -LiteralPath " + r"'C:\Users\dev\AppData\Local\Temp\sample' " + r"-Recurse -Force" + ), + "size_bytes": 1, + } + ], + "protected_overrides": [], + } + body = json.dumps(payload, indent=2).encode("utf-8") + bom_body = b"\xef\xbb\xbf" + body + failed = 0 + with tempfile.TemporaryDirectory() as td: + plain = Path(td) / "plain.json" + bom = Path(td) / "bom.json" + plain.write_bytes(body) + bom.write_bytes(bom_body) + try: + a = mod.load_selection_json(plain) + b = mod.load_selection_json(bom) + except Exception as exc: + print(f"FAIL [BOM load raised]: {exc}") + return 1 + if a != payload or b != payload: + print("FAIL [BOM load] decoded payload mismatch") + failed += 1 + else: + print("OK [BOM load] utf-8 and utf-8-sig selection JSON both load") + code = mod.main([str(HERE / "apply-cleanup-selection.py"), str(bom), "--dry-run"]) + if code != 0: + print(f"FAIL [BOM dry-run] exit={code}") + failed += 1 + else: + print("OK [BOM dry-run] apply --dry-run accepts BOM selection") + return failed + def main() -> int: os.environ.update(FAKE_ENV) mod = _load_validator() @@ -158,8 +208,13 @@ def main() -> int: print(f" cmd = {command!r}") print(f" expected = ok={exp_ok} prot={exp_prot}") print(f" got = ok={ok} prot={prot} reason={reason!r}") + bom_failed = test_load_selection_bom() + failed += bom_failed + if bom_failed == 0: + passed += 2 + print() - print(f"=== {passed} passed | {failed} failed | {len(CASES)} total ===") + print(f"=== {passed} passed | {failed} failed | {len(CASES)} total (+ BOM checks) ===") return 0 if failed == 0 else 1