Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ dev-only/
# Internal working handoff (not a public artifact)
HANDOFF.md
.gstack/

# 대회 주최측이 배포한 오리엔테이션 자료 — 제3자 저작물이라 재배포하지 않는다
docs/contest/ot-2026/
41 changes: 40 additions & 1 deletion scripts/check_assets_separation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import re
import subprocess
from pathlib import Path

PICTOGRAM_EXTS = {".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"}
Expand All @@ -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

Expand Down
75 changes: 75 additions & 0 deletions tests/test_assets_separation.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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("<svg></svg>", 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(
'<img src="data:image/png;base64,AAAA" />\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) == []
Loading