Skip to content
Open
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
28 changes: 26 additions & 2 deletions ci/nur/combine.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,27 @@ def commit_files(files: List[str], message: str) -> None:
subprocess.check_call(["git", "commit", "-m", message])


def resolve_repo_path(path: Path, name: str) -> Path:
name_path = Path(name)
if (
name_path.is_absolute()
or name in ("", ".", "..")
or name_path.name != name
):
raise ValueError(f"invalid repository name: {name}")

base_path = path.resolve()
repo_path = base_path.joinpath(name).resolve()
try:
repo_path.relative_to(base_path)
except ValueError as e:
raise ValueError(f"invalid repository name: {name}") from e

return repo_path


def commit_repo(repo: Repo, message: str, path: Path) -> Repo:
repo_path = path.joinpath(repo.name).resolve()
repo_path = resolve_repo_path(path, repo.name)

tmp: Optional[TemporaryDirectory] = TemporaryDirectory(prefix=str(repo_path.parent))

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TemporaryDirectory(prefix=str(repo_path.parent)) uses an absolute path as the name prefix, which is a non-obvious way to influence where the temp dir is created (and can create it as a sibling of repos/, not inside it). Consider switching to dir=repo_path.parent (with a simple prefix like nur-) to make the intent explicit and avoid path-separator/portability edge cases.

Suggested change
tmp: Optional[TemporaryDirectory] = TemporaryDirectory(prefix=str(repo_path.parent))
tmp: Optional[TemporaryDirectory] = TemporaryDirectory(
dir=str(repo_path.parent), prefix="nur-"
)

Copilot uses AI. Check for mistakes.
assert tmp is not None
Expand Down Expand Up @@ -102,7 +121,12 @@ def update_combined_repo(


def remove_repo(repo: Repo, path: Path) -> None:
repo_path = path.joinpath("repos", repo.name).resolve()
try:
repo_path = resolve_repo_path(path.joinpath("repos"), repo.name)
except ValueError:
logger.warning(f"Skipping removal for invalid repository name: {repo.name}")
return

if repo_path.exists():
shutil.rmtree(repo_path)
with chdir(path):
Expand Down
9 changes: 8 additions & 1 deletion ci/nur/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@


async def eval_repo(repo: Repo, repo_path: Path) -> None:
canonicalized_repo_path = repo_path.resolve()
source_path = canonicalized_repo_path.joinpath(repo.file).resolve()
Comment on lines +17 to +18

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canonicalized_repo_path is computed for safety checks, but later logic in this function still uses the original repo_path (e.g. for include paths). Consider consistently using the canonicalized path throughout after resolving, to avoid mismatches when repo_path contains symlinks.

Copilot uses AI. Check for mistakes.
try:
source_path.relative_to(canonicalized_repo_path)
except ValueError as e:
raise EvalError(f"{repo.name} has invalid file path: {repo.file}") from e

with tempfile.TemporaryDirectory() as d:
eval_path = Path(d).joinpath("default.nix")
with open(eval_path, "w") as f:
Expand All @@ -23,7 +30,7 @@ async def eval_repo(repo: Repo, repo_path: Path) -> None:
import {EVALREPO_PATH} {{
name = "{repo.name}";
url = "{repo.url}";
src = {repo_path.joinpath(repo.file)};
src = {source_path};
inherit pkgs lib;
}}
"""
Expand Down
11 changes: 11 additions & 0 deletions ci/nur/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,23 @@ def __init__(
branch: Optional[str],
locked_version: Optional[LockedVersion],
) -> None:
name_path = Path(name)
if (
name_path.is_absolute()
or name in ("", ".", "..")
or name_path.name != name
):
raise ValueError(f"invalid repository name: {name}")
Comment on lines +63 to +69

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository-name validation logic here is effectively duplicated in ci/nur/combine.py:46-62 (resolve_repo_path). To reduce the risk of the two implementations diverging over time (and reintroducing traversal issues), consider extracting this validation into a single shared helper and reusing it in both places.

Copilot uses AI. Check for mistakes.

self.name = name
self.url = url
self.submodules = submodules
if file_ is None:
self.file = "default.nix"
else:
file_path = Path(file_)
if file_path.is_absolute() or ".." in file_path.parts:
raise ValueError(f"invalid repository file path: {file_}")
self.file = file_
self.branch = branch
self.locked_version = None
Expand Down
Loading