Files
pixaroma-ComfyUI-Pixaroma/scripts/save_prefix_check.py
T
pixaroma baf9a00659 fix(preview): filename_prefix keeps Unicode (Korean) + spaces like native SaveImage
_safe_prefix flips from ASCII allowlist to Windows-illegal denylist:
only < > : " | ? * + control chars become _, everything else passes
verbatim. Adds reserved-device-name guard (CON/NUL/COM1-9/LPT1-9) and
trailing-dot/space strip per segment. All-Korean prefixes used to
dissolve to nothing -> silent 'Preview' fallback in output root; mixed
ones got mangled (te SEG st -> te_st). Traversal guards unchanged;
folder_paths' commonpath check still backs every save path.

New scripts/save_prefix_check.py locks behavior (31 cases, PREFIX PASS).
Covers save_mode=save, Save Output, Save Disk suggested name, XY Plot save.
2026-06-10 10:08:27 +03:00

102 lines
4.4 KiB
Python

"""Check _safe_prefix (filename_prefix sanitizer) behavior.
Run: python scripts/save_prefix_check.py
Exits 0 on PREFIX PASS, 1 on any mismatch.
Locks in the June-2026 fix for the Korean filename_prefix report: the
sanitizer must only neutralize genuinely dangerous characters (Windows-
illegal chars, control chars, path traversal) and let every other
character through verbatim - non-Latin scripts, accented letters, and
spaces included - matching native ComfyUI SaveImage, which does no
character filtering at all (folder_paths.get_save_image_path only
enforces that the final path stays inside output/).
"""
import os
import sys
import time
# Import the helper directly off the nodes/ dir. Do NOT use `from nodes._save...`
# - `nodes` collides with ComfyUI's top-level nodes.py when it's on sys.path.
# _save_helpers.py has no relative imports, so loading it standalone is safe.
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "nodes"))
from _save_helpers import _safe_prefix # noqa: E402
_today = time.strftime("%Y-%m-%d")
# (input, expected) - expected None means "unrecoverable, caller falls back"
CASES = [
# ── The reported Korean cases (parity with native SaveImage) ──────────
("프로젝트/테스트/이미지", "프로젝트/테스트/이미지"),
("project/te 스 st/image", "project/te 스 st/image"),
# ── Unicode + spaces survive verbatim ─────────────────────────────────
("café/naïve", "café/naïve"),
("日本語/画像", "日本語/画像"),
("Bunny WithCubes - Copy.png", "Bunny WithCubes - Copy.png"),
("a b", "a b"), # interior spaces untouched
(" padded /img", "padded/img"), # segment-edge whitespace trimmed
# ── Windows-illegal characters are neutralized to '_' ─────────────────
('a<b>c:d"e|f?g*h', "a_b_c_d_e_f_g_h"),
("a\tb", "a_b"), # control chars too
("C:\\Users\\x", "C/Users/x"), # drive colon neutralized, \ -> /
# ── Windows reserved device names get a '_' suffix ────────────────────
("CON/img", "CON_/img"),
("nul.txt/img", "nul.txt_/img"),
("com1", "com1_"),
# ── Windows trailing-dot/space rule (OS strips them silently) ─────────
("test.../img", "test/img"),
("teasda......////", "teasda"), # docstring example, must keep working
# ── Unchanged ASCII behavior (regression guards) ──────────────────────
("img", "img"),
("SDXL/portrait", "SDXL/portrait"),
("a__b", "a_b"), # underscore collapse retained
("_edge_", "edge"), # edge-underscore strip retained
("???", None), # nothing usable -> fallback
# ── Tokens ────────────────────────────────────────────────────────────
("%date:yyyy-MM-dd%/img", _today + "/img"),
("%year%-%month%/img", "%year%-%month%/img"), # native tokens preserved
# ── Traversal / absolute / garbage rejection (must stay rejected) ─────
("../evil", None),
("a/../b", None),
("/absolute", None),
("", None),
(" ", None),
(None, None),
(123, None),
("a" * 300, None), # input length cap
]
def main():
failures = []
for raw, expected in CASES:
got = _safe_prefix(raw)
if got != expected:
failures.append((raw, expected, got))
# Output-length cap: 121-char input truncates to 100
long_in = "x" * 60 + "/" + "y" * 60
long_got = _safe_prefix(long_in)
if long_got is None or len(long_got) > 100 or not long_got.startswith("x" * 60 + "/"):
failures.append((long_in, "<=100 chars keeping x.../y...", long_got))
if failures:
print(f"PREFIX FAIL - {len(failures)} case(s):")
for raw, expected, got in failures:
# ascii() so a cp1252 Windows console can't choke on Korean output
print(f" input={ascii(raw)}\n expected={ascii(expected)}\n got ={ascii(got)}")
return 1
print(f"PREFIX PASS ({len(CASES) + 1} cases)")
return 0
if __name__ == "__main__":
sys.exit(main())