diff --git a/.gitignore b/.gitignore index 01692ee..cd7da9a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ dev-only/ # Internal working handoff (not a public artifact) HANDOFF.md .gstack/ + +# 대회 주최측이 배포한 오리엔테이션 자료 — 제3자 저작물이라 재배포하지 않는다 +docs/contest/ot-2026/ diff --git a/scripts/check_assets_separation.py b/scripts/check_assets_separation.py index 6672f76..009bcd3 100644 --- a/scripts/check_assets_separation.py +++ b/scripts/check_assets_separation.py @@ -15,6 +15,7 @@ from __future__ import annotations import re +import subprocess from pathlib import Path PICTOGRAM_EXTS = {".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"} @@ -40,12 +41,50 @@ def _iter_files(root: Path): yield path +def _gitignored(root: Path, paths: list[Path]) -> set[Path]: + """``paths`` 중 git 이 무시하는 것들의 집합. + + 이 검사가 지키려는 것은 배포되는 트리의 라이선스 분리다. gitignore 된 파일은 + 정의상 배포물에 들어가지 않으므로 유출일 수 없다 — 로컬 참고자료 스크린샷 + 따위를 유출로 신고하면 거짓 경보가 나고 게이트가 무뎌진다. + + git 이 없거나 저장소가 아니면 **빈 집합**을 돌려준다. 즉 아무것도 면제되지 + 않고 예전처럼 전부 검사한다 — 폴백이 게이트를 끄면 안 된다. + """ + if not paths: + return set() + try: + # -z 는 필수다. 이것 없이는 git 이 비ASCII 경로를 따옴표로 감싸고 + # 8진 이스케이프해서 내보내(예: "docs/\354\235\274\354\240\225.png") + # 원래 경로와 대조가 되지 않는다 — 한글 경로가 조용히 전부 어긋난다. + # -z 를 주면 입출력 모두 NUL 구분이 되고, 공백·개행이 든 경로도 안전하다. + proc = subprocess.run( + ["git", "-C", str(root), "check-ignore", "-z", "--stdin"], + input="\0".join(str(p) for p in paths), + capture_output=True, + text=True, + timeout=60, + ) + except (OSError, subprocess.SubprocessError): + return set() + # 0 = 일부가 무시됨, 1 = 무시되는 것 없음. 그 밖(128 = git 저장소 아님 등)은 + # 판단 불가로 보고 아무것도 면제하지 않는다. + if proc.returncode not in (0, 1): + return set() + return {Path(entry) for entry in proc.stdout.split("\0") if entry} + + def find_asset_leaks(repo_root: Path | str) -> list[str]: root = Path(repo_root).resolve() assets_dir = root / "assets" leaks: list[str] = [] - for path in _iter_files(root): + candidates = list(_iter_files(root)) + ignored = _gitignored(root, candidates) + + for path in candidates: + if path in ignored: + continue rel = path.relative_to(root) in_assets = assets_dir in path.parents diff --git a/tests/test_assets_separation.py b/tests/test_assets_separation.py index 7bbaf06..c10c45e 100644 --- a/tests/test_assets_separation.py +++ b/tests/test_assets_separation.py @@ -1,10 +1,25 @@ +import subprocess from pathlib import Path +import pytest + from scripts.check_assets_separation import find_asset_leaks ROOT = Path(__file__).resolve().parent.parent +def _git_repo(tmp_path: Path, gitignore: str) -> Path: + """`.gitignore` 를 가진 최소 git 저장소를 만든다. git 이 없으면 skip.""" + try: + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True, + capture_output=True, timeout=30) + except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover + pytest.skip(f"git unavailable: {exc}") + (tmp_path / ".gitignore").write_text(gitignore, encoding="utf-8") + (tmp_path / "assets").mkdir(exist_ok=True) + return tmp_path + + def test_assets_dir_exists_with_readme(): assert (ROOT / "assets").is_dir() readme = (ROOT / "assets" / "README.md").read_text(encoding="utf-8") @@ -108,3 +123,63 @@ def test_skip_dirs_use_relative_parts(tmp_path): assert not any("output.svg" in p for p in leaks) # scripts/logo.png MUST appear assert any("logo.png" in p for p in leaks) + + +# --------------------------------------------------------------------------- +# gitignore 존중 (2026-07-29) +# +# 이 스캐너가 지키려는 것은 "배포되는 트리에서 CC BY-SA 픽토그램과 Apache 코드가 +# 섞이지 않는가" 다. gitignore 된 파일은 정의상 배포물에 들어가지 않는다 — +# 참고자료 스크린샷, 내려받은 자료 같은 로컬 작업 파일까지 유출로 신고하면 +# 거짓 경보가 나고, 진짜 유출과 구분이 안 되어 게이트가 무뎌진다. +# --------------------------------------------------------------------------- + + +def test_gitignored_pictogram_is_not_a_leak(tmp_path): + """gitignore 된 이미지는 배포되지 않으므로 유출이 아니다.""" + repo = _git_repo(tmp_path, "docs/scratch/\n") + (repo / "docs" / "scratch").mkdir(parents=True) + (repo / "docs" / "scratch" / "reference-shot.png").write_bytes(b"\x89PNG\r\n") + assert find_asset_leaks(repo) == [] + + +def test_tracked_pictogram_outside_assets_is_still_a_leak(tmp_path): + """gitignore 되지 않은 이미지는 여전히 유출이다 — 게이트를 무디게 하지 않는다.""" + repo = _git_repo(tmp_path, "docs/scratch/\n") + (repo / "ttobak").mkdir() + (repo / "ttobak" / "stray_glyph.svg").write_text("", encoding="utf-8") + leaks = find_asset_leaks(repo) + assert any("stray_glyph.svg" in p for p in leaks) + + +def test_gitignored_data_uri_is_not_a_leak(tmp_path): + """규칙 2(base64 인라인)도 같은 기준을 따른다.""" + repo = _git_repo(tmp_path, "ttobak/generated/\n") + (repo / "ttobak" / "generated").mkdir(parents=True) + (repo / "ttobak" / "generated" / "out.html").write_text( + '\n', encoding="utf-8" + ) + assert find_asset_leaks(repo) == [] + + +def test_non_git_directory_still_scans_everything(tmp_path): + """git 저장소가 아니면 예전처럼 전부 검사한다(폴백이 게이트를 끄면 안 된다).""" + (tmp_path / "assets").mkdir() + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "shot.png").write_bytes(b"\x89PNG\r\n") + leaks = find_asset_leaks(tmp_path) + assert any("shot.png" in p for p in leaks) + + +def test_gitignored_non_ascii_path_is_matched(tmp_path): + """한글 등 비ASCII 경로도 정확히 대조된다. + + `git check-ignore` 는 -z 없이 실행하면 비ASCII 경로를 따옴표로 감싸고 8진 + 이스케이프해서 출력한다("docs/\\354\\235\\274...png"). 그러면 원래 경로와 + 매칭에 실패해 무시 설정이 조용히 통째로 먹히지 않는다 — 한글 경로가 많은 + 이 저장소에서는 사실상 기능이 없는 것과 같다. + """ + repo = _git_repo(tmp_path, "문서/자료/\n") + (repo / "문서" / "자료").mkdir(parents=True) + (repo / "문서" / "자료" / "배점표.png").write_bytes(b"\x89PNG\r\n") + assert find_asset_leaks(repo) == []