aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/fuzz_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plan/fuzz_test.go')
-rw-r--r--internal/plan/fuzz_test.go69
1 files changed, 61 insertions, 8 deletions
diff --git a/internal/plan/fuzz_test.go b/internal/plan/fuzz_test.go
index 6deb225..b17d3c4 100644
--- a/internal/plan/fuzz_test.go
+++ b/internal/plan/fuzz_test.go
@@ -3,6 +3,7 @@
package plan
import (
+ "path"
"path/filepath"
"strings"
"testing"
@@ -10,6 +11,7 @@ import (
"krino/internal/config"
"krino/internal/scan"
+ "krino/internal/xdg"
)
// FuzzExpand: expanding any template against any file name never panics,
@@ -17,7 +19,8 @@ import (
// capture groups there are - MaxIndex and Expand read placeholders alike.
// Used as a destination - relative, in the home directory, or bare - a
// template never plans a move outside the directory its text names before
-// the first placeholder (review M1).
+// the first placeholder (review M1), checked against textDir, not the
+// planner's own staticDir.
func FuzzExpand(f *testing.F) {
for _, s := range []string{"{name}", "{stem}{ext}", "{mtime:%Y/%m}", "{1}_{2}", "{{literal}}", "{", "}", "{now:%", "{0}", "{9}", "{99999999999999999999}"} {
f.Add(s, "a.b.pdf")
@@ -37,15 +40,65 @@ func FuzzExpand(f *testing.F) {
if n, err := MaxIndex(tmpl); err == nil && n > 2 && errA == nil {
t.Fatalf("Expand(%q) = %q, though it uses {%d} and only 2 groups exist", tmpl, a, n)
}
+ if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\x00") {
+ return // not a name a directory entry can have
+ }
for _, dest := range []string{tmpl, "Out/" + tmpl, "~/docs/" + tmpl} {
- in := []Input{{
- File: scan.File{Path: "/r/x.pdf", Rel: "x.pdf", Name: "x.pdf", ModTime: facts.ModTime},
- Rules: []RuleMatch{{Name: "a", Captures: facts.Captures, Actions: []config.Action{{Kind: config.Move, Arg: dest}}}},
- }}
- s := Build("/r", in, facts.Now, NoDisk{}, NewClaims())[0].Steps[0]
- if strings.ContainsRune(dest, '{') && s.Skip == "" && !within(filepath.Dir(s.Dst), staticDir(dest, "/r")) {
- t.Fatalf("destination %q planned %q, outside %q", dest, s.Dst, staticDir(dest, "/r"))
+ if !strings.ContainsRune(dest, '{') {
+ continue
+ }
+ got := plannedDir(dest, name, facts.Captures, facts)
+ if got == "" {
+ continue
+ }
+ if base := textDir(dest, "/r", xdg.Home()); !underDir(got, base) {
+ t.Fatalf("destination %q planned %q for %q, outside %q, the directory its text names", dest, got, name, base)
}
}
})
}
+
+// plannedDir builds a move of a file named name to dest with captures and
+// returns the directory the step would put it in, or "" when the step is
+// skipped.
+func plannedDir(dest, name string, captures []string, facts Facts) string {
+ in := []Input{{
+ File: scan.File{Path: "/r/" + name, Rel: name, Name: name, ModTime: facts.ModTime},
+ Rules: []RuleMatch{{Name: "a", Captures: captures, Actions: []config.Action{{Kind: config.Move, Arg: dest}}}},
+ }}
+ s := Build("/r", in, facts.Now, NoDisk{}, NewClaims())[0].Steps[0]
+ if s.Skip != "" {
+ return ""
+ }
+ return filepath.Dir(s.Dst)
+}
+
+// textDir is the directory a destination's text names before its first
+// '{' (spec 15.1), worked out here by hand rather than by staticDir: the
+// text up to the last '/' before the brace, "~" as the home directory, a
+// relative path under root.
+func textDir(dest, root, home string) string {
+ prefix := dest[:strings.IndexByte(dest, '{')]
+ switch cut := strings.LastIndexByte(prefix, '/'); {
+ case cut < 0:
+ prefix = ""
+ case cut == 0:
+ return "/"
+ default:
+ prefix = prefix[:cut]
+ }
+ switch {
+ case prefix == "~":
+ return home
+ case strings.HasPrefix(prefix, "~/"):
+ return path.Join(home, prefix[2:])
+ case strings.HasPrefix(prefix, "/"):
+ return path.Clean(prefix)
+ }
+ return path.Join(root, prefix)
+}
+
+// underDir reports whether path is dir or lies under it, by whole segments.
+func underDir(path, dir string) bool {
+ return dir == "/" || path == dir || strings.HasPrefix(path, dir+"/")
+}