Skip to content

Commit 933f011

Browse files
committed
fix: support nested shared scripts during preflight
1 parent 1b045b7 commit 933f011

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

src/specify_cli/shared_infra.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,40 @@ def _ensure_safe_shared_directory(project_path: Path, directory: Path, *, create
109109
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
110110

111111

112-
def _ensure_safe_shared_destination(project_path: Path, dest: Path) -> None:
112+
def _validate_safe_shared_directory(project_path: Path, directory: Path) -> None:
113+
"""Validate existing directory parents while allowing missing directories."""
114+
root = project_path.resolve()
115+
rel = _shared_relative_path(project_path, directory)
116+
current = project_path
117+
118+
for part in rel.parts:
119+
current = current / part
120+
label = _shared_destination_label(project_path, current)
121+
if current.is_symlink():
122+
raise ValueError(f"Refusing to use symlinked shared infrastructure directory: {label}")
123+
if not current.exists():
124+
continue
125+
if not current.is_dir():
126+
raise ValueError(f"Shared infrastructure directory path is not a directory: {label}")
127+
try:
128+
current.resolve().relative_to(root)
129+
except (OSError, ValueError):
130+
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
131+
132+
133+
def _ensure_safe_shared_destination(
134+
project_path: Path,
135+
dest: Path,
136+
*,
137+
parent_must_exist: bool = True,
138+
) -> None:
113139
"""Refuse shared infra writes that would escape or follow symlinks."""
114140
root = project_path.resolve()
115141
_shared_relative_path(project_path, dest)
116-
_ensure_safe_shared_directory(project_path, dest.parent, create=False)
142+
if parent_must_exist:
143+
_ensure_safe_shared_directory(project_path, dest.parent, create=False)
144+
else:
145+
_validate_safe_shared_directory(project_path, dest.parent)
117146
label = _shared_destination_label(project_path, dest)
118147
if dest.is_symlink():
119148
raise ValueError(f"Refusing to overwrite symlinked shared infrastructure path: {label}")
@@ -235,7 +264,7 @@ def install_shared_infra(
235264

236265
rel_path = src_path.relative_to(variant_src)
237266
dst_path = dest_variant / rel_path
238-
_ensure_safe_shared_destination(project_path, dst_path)
267+
_ensure_safe_shared_destination(project_path, dst_path, parent_must_exist=False)
239268
if dst_path.exists() and not force:
240269
skipped_files.append(str(dst_path.relative_to(project_path)))
241270
continue
@@ -264,6 +293,7 @@ def install_shared_infra(
264293
planned_templates.append((dst, rel, content))
265294

266295
for dst_path, rel, content, mode in planned_copies:
296+
_ensure_safe_shared_directory(project_path, dst_path.parent)
267297
_write_shared_bytes(project_path, dst_path, content, mode=mode)
268298
manifest.record_existing(rel)
269299

tests/integrations/test_cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,31 @@ def test_shared_infra_install_preflights_before_writing(self, tmp_path):
443443
assert existing.read_text(encoding="utf-8") == "# old a\n"
444444
assert outside.read_text(encoding="utf-8") == "# outside\n"
445445

446+
def test_shared_infra_install_supports_nested_script_sources(self, tmp_path):
447+
"""Nested script source files create safe destination parents at write time."""
448+
from specify_cli.shared_infra import install_shared_infra
449+
450+
project = tmp_path / "nested-script-install-test"
451+
project.mkdir()
452+
453+
core_pack = tmp_path / "core-pack"
454+
nested_src = core_pack / "scripts" / "bash" / "nested"
455+
nested_src.mkdir(parents=True)
456+
(nested_src / "deep.sh").write_text("# nested\n", encoding="utf-8")
457+
458+
install_shared_infra(
459+
project,
460+
"sh",
461+
version="test",
462+
core_pack=core_pack,
463+
repo_root=tmp_path / "unused",
464+
console=_NoopConsole(),
465+
force=True,
466+
)
467+
468+
nested_dest = project / ".specify" / "scripts" / "bash" / "nested" / "deep.sh"
469+
assert nested_dest.read_text(encoding="utf-8") == "# nested\n"
470+
446471
def test_shared_infra_no_warning_when_forced(self, tmp_path, capsys):
447472
"""No skip warning when force=True (all files overwritten)."""
448473
from specify_cli import _install_shared_infra

0 commit comments

Comments
 (0)