fix(B05): 54-22 H1 · H2 경로 넘기 막음 — 구조물 id 안전 이름 · 지면 필터 · 표현 허용 목록

- H1: 구조물 저장 structure_id 를 안전 이름 규칙(FOLDER_PATTERN 과 한 벌)으로 검사 · 어기면 422
- H1: 복사본 맞춤에서 한 번 더 — 규칙 밖 · base 밖 폴더는 지우기 · 쓰기 건너뜀(못만듦)
- H2: filter_key · method 를 허용 목록(Literal)으로 · 파일 이름 만드는 곳(표고 sampler · 비용면 · 스켈레톤)에서 한 번 더
- 시험: test_54_22_security 26 통과 · 관 간격 시험 가짜 필터 이름을 허용 값으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-06 12:22:08 +09:00
co-authored by Claude Opus 5.5
parent 976c9b2816
commit c8ca79063c
8 changed files with 158 additions and 11 deletions
+17 -4
View File
@@ -26,6 +26,7 @@ from pathlib import Path
from typing import Any
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import STRUCTURE_ID_RULE
from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file
from common_util.common_util_json import atomic_write_json
from common_util.common_util_summary_item import GROUP_JOIN, snapshot_folder_name
@@ -51,7 +52,7 @@ LOCAL_FILES = (VALUES_FILE, PICKS_FILE)
#: 복사본 폴더 이름 — 구조물 id(영문 · 숫자 · `_` `-`) 또는 `pipe@측점` · 옵션 그룹(PLAN 46)은
#: 뒤에 `__{그룹 id}`
FOLDER_PATTERN = re.compile(
r"^(?:pipe@\d+(?:\.\d+)?(?:__g[0-9a-z]{1,16})?|[0-9A-Za-z][0-9A-Za-z_-]*)$"
rf"^(?:pipe@\d+(?:\.\d+)?(?:__g[0-9a-z]{{1,16}})?|{STRUCTURE_ID_RULE})$"
)
@@ -69,6 +70,14 @@ def folder_name(owner_id: str) -> str:
return owner_id.replace("#", "n")
def safe_folder(base: Path, name: str) -> Path | None:
"""`base` 바로 아래 복사본 폴더 — 이름이 규칙 밖 · `base` 밖이면 None(지우기 · 쓰기 막음)."""
target = base / name
if not FOLDER_PATTERN.match(name) or target.resolve().parent != base.resolve():
return None
return target
def now_text() -> str:
return datetime.now().isoformat(timespec="seconds")
@@ -181,8 +190,8 @@ def migrate_items(project_root: str | Path) -> list[str]:
continue
users = [o for o, item in wanted.items() if snapshot_folder_name(item) == child.name]
for owner in users:
target = base / folder_name(owner)
if (target / MANIFEST_NAME).is_file():
target = safe_folder(base, folder_name(owner))
if target is None or (target / MANIFEST_NAME).is_file():
continue # 이미 새 복사본이 있음
shutil.copytree(child, target, dirs_exist_ok=True)
origin = manifest.get("원본") or {}
@@ -212,7 +221,11 @@ def sync_snapshots(project_root: str | Path) -> dict[str, list[str]]:
failed: list[str] = []
removed: list[str] = []
for folder, (owner, item) in sorted(wanted.items()):
target = base / folder
target = safe_folder(base, folder)
if target is None:
logger.warning("복사본 폴더 이름이 규칙 밖 — 건너뜀: %r", folder)
failed.append(folder)
continue
manifest = read_json(target / MANIFEST_NAME)
if isinstance(manifest, dict) and same_item(manifest, item):
continue