aboutsummaryrefslogtreecommitdiff
path: root/internal/apply/swap_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 14:02:24 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 14:02:24 +0200
commita65e8a0587d3e5c971bef3062dd0d8a3b5cb53b7 (patch)
tree82c606411fd814cb5003633cfa592393abcbe057 /internal/apply/swap_test.go
parentb44222fc2b061382dc601014cde287cc41b857ca (diff)
downloadkrino-a65e8a0587d3e5c971bef3062dd0d8a3b5cb53b7.tar.gz
krino-a65e8a0587d3e5c971bef3062dd0d8a3b5cb53b7.zip
a symlink in the sorted directory no longer redirects a step
Placeholders were already stopped from sending a file out of the directory a rule named. A symlink is a name too, and the directory krino sorts is by the threat model's own premise a place the internet writes into: a link named after a rule's destination sent moves and copies anywhere, and under (on-conflict overwrite) trashed a file OUTSIDE the sorted directory - while the plan showed the in-tree text and the run reported success. A step whose destination passes through a symlink at or below the directory being sorted now fails. A destination the configuration names outside it - ~/docs on another disk - is the user's own arrangement and is followed as before; both cases have a test. End to end, the review's scenario (Out -> ~/secret, overwrite): before: 1 applied, the user's file replaced and trashed after: 0 applied 1 failed, the file untouched, nothing trashed Two bookkeeping bugs in MkdirAllTracked went with it: a dangling symlink read as a missing directory and was then recorded as one krino had created - undo would have unlinked a link krino never made - and a directory created by someone else between the check and the mkdir was recorded the same way.
Diffstat (limited to 'internal/apply/swap_test.go')
-rw-r--r--internal/apply/swap_test.go63
1 files changed, 63 insertions, 0 deletions
diff --git a/internal/apply/swap_test.go b/internal/apply/swap_test.go
index 297db72..dd7cc07 100644
--- a/internal/apply/swap_test.go
+++ b/internal/apply/swap_test.go
@@ -303,3 +303,66 @@ func TestDisplaceThatCannotBeReportedFailsTheStep(t *testing.T) {
t.Error("the copy ran even though the displace could not be logged")
}
}
+
+// TestSymlinkInsideTheSortedDirectoryStopsTheStep: placeholders are already
+// stopped from redirecting a step out of the directory a rule named. A
+// symlink is a name too: one planted in the sorted directory - by an
+// unpacked archive, say - named after a rule's destination sends the file
+// anywhere, while the plan the user approved shows only "Out/".
+func TestSymlinkInsideTheSortedDirectoryStopsTheStep(t *testing.T) {
+ root := t.TempDir()
+ outside := t.TempDir()
+ if err := os.Symlink(outside, filepath.Join(root, "Out")); err != nil {
+ t.Skipf("symlinks unavailable: %v", err)
+ }
+ src := filepath.Join(root, "a.pdf")
+ dst := filepath.Join(root, "Out", "a.pdf")
+ c := planned(t, root, "a.pdf", "body", plan.Step{Kind: plan.Move, Src: src, Dst: dst})
+ c.Root = root
+
+ res, err := ChainLogged(context.Background(), c, nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res[0].Status != "failed" || !strings.Contains(res[0].Detail, "symlink") {
+ t.Errorf("step = %+v; want a failure naming the symlink", res[0])
+ }
+ if _, err := os.Lstat(filepath.Join(outside, "a.pdf")); err == nil {
+ t.Error("the file left the sorted directory through the symlink")
+ }
+ if _, err := os.Lstat(src); err != nil {
+ t.Errorf("the file is no longer where it started: %v", err)
+ }
+}
+
+// TestASymlinkedDestinationOutsideTheSortedDirectoryIsFine: a destination
+// the configuration itself names - "~/docs/work", where ~/docs is a symlink
+// to another disk - is the user's own arrangement, not something planted,
+// and must keep working.
+func TestASymlinkedDestinationOutsideTheSortedDirectoryIsFine(t *testing.T) {
+ root := t.TempDir()
+ elsewhere := t.TempDir()
+ real := filepath.Join(elsewhere, "real")
+ if err := os.MkdirAll(real, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ link := filepath.Join(elsewhere, "docs")
+ if err := os.Symlink(real, link); err != nil {
+ t.Skipf("symlinks unavailable: %v", err)
+ }
+ src := filepath.Join(root, "a.pdf")
+ dst := filepath.Join(link, "a.pdf")
+ c := planned(t, root, "a.pdf", "body", plan.Step{Kind: plan.Move, Src: src, Dst: dst})
+ c.Root = root
+
+ res, err := ChainLogged(context.Background(), c, nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res[0].Status != "ok" {
+ t.Fatalf("step = %+v; want it to go through", res[0])
+ }
+ if _, err := os.Lstat(filepath.Join(real, "a.pdf")); err != nil {
+ t.Errorf("the file did not reach the configured destination: %v", err)
+ }
+}