// SPDX-License-Identifier: GPL-3.0-or-later package plan import ( "fmt" "testing" "time" "git.labunix.xyz/krino/internal/config" ) // TestBuildRefusesDotNames: a rename whose placeholders produce "", "." or // ".." names a directory, not a file; the step is skipped with a reason // instead of renaming a file onto its own folder or the one above. func TestBuildRefusesDotNames(t *testing.T) { for _, capture := range []string{"", ".", ".."} { in := []Input{{ File: file("/r", "x.pdf"), Rules: []RuleMatch{{Name: "a", Captures: []string{"x.pdf", capture}, Actions: []config.Action{act(config.Rename, "{1}")}}}, }} c := Build("/r", in, time.Now(), NoDisk{}, NewClaims())[0] if want := fmt.Sprintf("rename produced the name %q", capture); c.Steps[0].Skip != want { t.Errorf("capture %q: Skip = %q, want %q", capture, c.Steps[0].Skip, want) } } } // TestBuildRefusesDotDotFromPlaceholder: a file name can capture "..", // though never "/", so a destination whose placeholders add a ".." segment // would leave the directory the rule names: that step is skipped. A ".." // the rule wrote itself is the rule's own business, and "..x" is a name, // not a segment. func TestBuildRefusesDotDotFromPlaceholder(t *testing.T) { step := func(dest string) Step { in := []Input{{ File: file("/r", "x.pdf"), Rules: []RuleMatch{{Name: "a", Captures: []string{"x.pdf", ".."}, Actions: []config.Action{act(config.Move, dest)}}}, }} return Build("/r", in, time.Now(), NoDisk{}, NewClaims())[0].Steps[0] } if s := step("/w/{1}/in"); s.Skip != `destination "/w/../in" leaves /w through a placeholder` { t.Errorf("placeholder ..: Skip = %q, Dst = %q", s.Skip, s.Dst) } if s := step("/w/../in"); s.Skip != "" || s.Dst != "/in/x.pdf" { t.Errorf("written ..: Skip = %q, Dst = %q", s.Skip, s.Dst) } if s := step("/w/{1}x"); s.Skip != "" || s.Dst != "/w/..x/x.pdf" { t.Errorf("..x: Skip = %q, Dst = %q", s.Skip, s.Dst) } // "{{" is a literal brace, not a placeholder: the text names /w/{x}, and // a capture may not climb out of it (triage 28b). if s := step("/w/{{x}}/{1}/in"); s.Skip != `destination "/w/{x}/../in" leaves /w/{x} through a placeholder` { t.Errorf("literal braces: Skip = %q, Dst = %q", s.Skip, s.Dst) } } // TestBuildKeepsPlaceholdersInsideTheirDirectory: a capture of "~", an // empty capture, or an extension-less {ext} at the start of a destination // used to make it $HOME or absolute (review M1). The resolved destination // must stay under the directory the rule's text names before its first // placeholder; a destination with no placeholder is the rule's own // business. func TestBuildKeepsPlaceholdersInsideTheirDirectory(t *testing.T) { t.Setenv("HOME", "/home/x") step := func(name, dest string, caps ...string) Step { in := []Input{{File: file("/r", name), Rules: []RuleMatch{{Name: "a", Captures: caps, Actions: []config.Action{act(config.Move, dest)}}}}} return Build("/r", in, time.Now(), NoDisk{}, NewClaims())[0].Steps[0] } for _, tc := range []struct { name, dest string caps []string skip bool dst string }{ {"~_i.pdf", "{1}/Filed", []string{"~_", "~"}, true, ""}, {"_i.pdf", "{1}/Filed", []string{"_", ""}, true, ""}, {"README", "{ext}/tmp", nil, true, ""}, {"a.pdf", "Out/{1}", []string{"a", "~"}, false, "/r/Out/~/a.pdf"}, {"a.pdf", "~/docs/{1}", []string{"a", "x"}, false, "/home/x/docs/x/a.pdf"}, {"a.pdf", "~/docs/{1}", []string{"a", ".."}, true, ""}, {"a.pdf", "Work/Ac{1}", []string{"a", "me"}, false, "/r/Work/Acme/a.pdf"}, {"a.pdf", "/w/{1}/in", []string{"a", ".."}, true, ""}, {"a.pdf", "/w/../in", nil, false, "/in/a.pdf"}, {"a.pdf", "{mtime:%Y}", nil, false, "/r/2026/a.pdf"}, } { s := step(tc.name, tc.dest, tc.caps...) if tc.skip != (s.Skip != "") || (!tc.skip && s.Dst != tc.dst) { t.Errorf("%s -> %s %q: Skip %q Dst %q; want skip=%v dst=%q", tc.name, tc.dest, tc.caps, s.Skip, s.Dst, tc.skip, tc.dst) } } }