Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
run: uv sync --frozen
- name: Run pytest
run:
uv run poe test --cov=./ --cov-report=xml -ra
uv run poe test --cov=./ --cov-report=xml -ra -s -k test_symlink_not_followed
${PYTEST_XDIST_MAXPROCESSES:+--maxprocesses=$PYTEST_XDIST_MAXPROCESSES} .
env:
PYTEST_XDIST_MAXPROCESSES: ${{ matrix.pytest-xdist-maxprocesses }}
Expand Down
1 change: 1 addition & 0 deletions copier/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ def _render_template(self) -> None:
Path(src_abspath).relative_to(self.template_copy_root)
)
for dst_relpath, ctx in dst_relpaths_ctxs:
print(">>>", src_relpath, "->", dst_relpath, file=sys.stderr)
dst_abspath = dst_root / dst_relpath
if dst_abspath.is_symlink() and self.template.preserve_symlinks:
# If destination path is a symlink, it can safely point outside the
Expand Down
40 changes: 39 additions & 1 deletion tests/test_symlinks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
import re
from pathlib import Path

import pytest
from plumbum import local

from copier import run_copy, run_update
from copier.errors import DirtyLocalWarning
from copier.errors import DirtyLocalWarning, ForbiddenPathError

from .helpers import build_file_tree, git

Expand Down Expand Up @@ -569,3 +570,40 @@ def test_symlinked_to_outside_destination_relative(
assert (dst / "symlink_dir" / "outside.txt").read_text() == "outside"
assert (dst / "a_symlink.txt").is_symlink()
assert (dst / "a_symlink.txt").read_text() == "outside"


def test_symlink_not_followed(tmp_path_factory: pytest.TempPathFactory) -> None:
src, dst, other = map(tmp_path_factory.mktemp, ("src", "dst", "other"))
build_file_tree(
{
src / "copier.yaml": """\
_preserve_symlinks: true
""",
# HACK: The `{% yield %}` tag is used to ensure the order in which the
# symlink and the generated file path are created, so the symlink is
# guaranteed to exist when the file is created, such that generating the
# `outside.txt` file might follow the symlink to its target location
# outside the destination root which is forbidden.
src
/ "{% yield i from [1, 2] %}.{% endyield %}"
/ "{% if i == 1 %}symlink{% endif %}": other,
src
/ "{% yield i from [1, 2] %}.{% endyield %}"
/ "{% if i == 2 %}{{ pathjoin('symlink', 'outside.txt') }}{% endif %}": "overwritten",
other / "outside.txt": "outside",
}
)

with local.cwd(src):
git("init")
git("add", "-A")
git("commit", "-m", "init")

with pytest.raises(
ForbiddenPathError,
match=re.escape('"symlink/outside.txt" is forbidden'),
):
run_copy(str(src), dst, defaults=True, overwrite=True)

assert (dst / "symlink").is_symlink()
assert (dst / "symlink" / "outside.txt").read_text() == "outside"
Loading