aboutsummaryrefslogtreecommitdiff
path: root/internal/plan
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plan')
-rw-r--r--internal/plan/chain.go18
-rw-r--r--internal/plan/fuzz_test.go37
-rw-r--r--internal/plan/hostile_test.go5
-rw-r--r--internal/plan/placeholder.go24
-rw-r--r--internal/plan/testdata/fuzz/FuzzExpand/8d1df2cb4b20cf0b3
5 files changed, 65 insertions, 22 deletions
diff --git a/internal/plan/chain.go b/internal/plan/chain.go
index 793e0e1..b32cc7d 100644
--- a/internal/plan/chain.go
+++ b/internal/plan/chain.go
@@ -233,8 +233,8 @@ func expandDir(raw string, facts Facts, root string) (string, error) {
return "", err
}
resolved := ResolveDir(expanded, root)
- if strings.ContainsRune(raw, '{') {
- if base := staticDir(raw, root); !within(resolved, base) {
+ if prefix, templated := StaticPrefix(raw); templated {
+ if base := staticDir(prefix, root); !within(resolved, base) {
return "", fmt.Errorf("destination %q leaves %s through a placeholder", expanded, xdg.Abbrev(base))
}
}
@@ -242,15 +242,11 @@ func expandDir(raw string, facts Facts, root string) (string, error) {
}
// staticDir is the directory a destination names before its first
-// placeholder: its last complete path segment, resolved like any
-// destination. "Work/Acme/{mtime:%Y}" is root/Work/Acme, "Work/Ac{1}" is
-// root/Work, "{1}/x" is root itself, "~/{1}" is the home directory and
-// "/{1}" is "/".
-func staticDir(raw, root string) string {
- prefix := raw
- if i := strings.IndexByte(raw, '{'); i >= 0 {
- prefix = raw[:i]
- }
+// placeholder, given that text (StaticPrefix): its last complete path
+// segment, resolved like any destination. "Work/Acme/{mtime:%Y}" is
+// root/Work/Acme, "Work/Ac{1}" is root/Work, "{1}/x" is root itself, "~/{1}"
+// is the home directory and "/{1}" is "/".
+func staticDir(prefix, root string) string {
switch i := strings.LastIndexByte(prefix, '/'); {
case i < 0:
prefix = ""
diff --git a/internal/plan/fuzz_test.go b/internal/plan/fuzz_test.go
index c7261ec..91a8421 100644
--- a/internal/plan/fuzz_test.go
+++ b/internal/plan/fuzz_test.go
@@ -56,7 +56,7 @@ func FuzzExpand(f *testing.F) {
if got == "" {
continue
}
- if base := textDir(dest, "/r", xdg.Home()); !underDir(got, base) {
+ if base, templated := textDir(dest, "/r", xdg.Home()); templated && !underDir(got, base) {
t.Fatalf("destination %q planned %q for %q, outside %q, the directory its text names", dest, got, name, base)
}
}
@@ -79,28 +79,43 @@ func plannedDir(dest, name string, captures []string, facts Facts) string {
}
// 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, '{')]
+// placeholder (spec 15.1), worked out here by hand rather than by staticDir:
+// the text up to the last '/' before the first '{' that is not "{{", with
+// "{{" and "}}" read as braces, "~" as the home directory, a relative path
+// under root. templated is false when dest has no placeholder, only literal
+// braces: then a ".." in it is the rule's own.
+func textDir(dest, root, home string) (dir string, templated bool) {
+ open := 0
+ for {
+ j := strings.IndexByte(dest[open:], '{')
+ if j < 0 {
+ return "", false
+ }
+ if open+j+1 < len(dest) && dest[open+j+1] == '{' {
+ open += j + 2
+ continue
+ }
+ open += j
+ break
+ }
+ prefix := strings.NewReplacer("{{", "{", "}}", "}").Replace(dest[:open])
switch cut := strings.LastIndexByte(prefix, '/'); {
case cut < 0:
prefix = ""
case cut == 0:
- return "/"
+ return "/", true
default:
prefix = prefix[:cut]
}
switch {
case prefix == "~":
- return home
+ return home, true
case strings.HasPrefix(prefix, "~/"):
- return path.Join(home, prefix[2:])
+ return path.Join(home, prefix[2:]), true
case strings.HasPrefix(prefix, "/"):
- return path.Clean(prefix)
+ return path.Clean(prefix), true
}
- return path.Join(root, prefix)
+ return path.Join(root, prefix), true
}
// underDir reports whether path is dir or lies under it, by whole segments.
diff --git a/internal/plan/hostile_test.go b/internal/plan/hostile_test.go
index a2f2bfe..13c2afd 100644
--- a/internal/plan/hostile_test.go
+++ b/internal/plan/hostile_test.go
@@ -48,6 +48,11 @@ func TestBuildRefusesDotDotFromPlaceholder(t *testing.T) {
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
diff --git a/internal/plan/placeholder.go b/internal/plan/placeholder.go
index b947236..5922b07 100644
--- a/internal/plan/placeholder.go
+++ b/internal/plan/placeholder.go
@@ -30,6 +30,30 @@ func splitExt(name string) (stem, ext string) {
return name[:i], name[i:]
}
+// StaticPrefix returns the literal text of s before its first placeholder,
+// with "{{" and "}}" read as the braces they stand for, and whether s has a
+// placeholder at all. It scans like Expand, so a literal brace never counts
+// as a placeholder (triage 28b).
+func StaticPrefix(s string) (prefix string, templated bool) {
+ var b strings.Builder
+ for i := 0; i < len(s); {
+ switch {
+ case s[i] == '{' && i+1 < len(s) && s[i+1] == '{':
+ b.WriteByte('{')
+ i += 2
+ case s[i] == '}' && i+1 < len(s) && s[i+1] == '}':
+ b.WriteByte('}')
+ i += 2
+ case s[i] == '{':
+ return b.String(), true
+ default:
+ b.WriteByte(s[i])
+ i++
+ }
+ }
+ return b.String(), false
+}
+
// Expand replaces every placeholder in s. It returns an error naming the
// first placeholder it could not expand. It scans s once: "{{" and "}}"
// each emit one literal brace, and any other "{" opens a placeholder that
diff --git a/internal/plan/testdata/fuzz/FuzzExpand/8d1df2cb4b20cf0b b/internal/plan/testdata/fuzz/FuzzExpand/8d1df2cb4b20cf0b
new file mode 100644
index 0000000..1d02849
--- /dev/null
+++ b/internal/plan/testdata/fuzz/FuzzExpand/8d1df2cb4b20cf0b
@@ -0,0 +1,3 @@
+go test fuzz v1
+string("{{/..")
+string("0")