aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/chain.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plan/chain.go')
-rw-r--r--internal/plan/chain.go50
1 files changed, 36 insertions, 14 deletions
diff --git a/internal/plan/chain.go b/internal/plan/chain.go
index fda8730..d4d07da 100644
--- a/internal/plan/chain.go
+++ b/internal/plan/chain.go
@@ -212,29 +212,51 @@ func stepKind(k config.ActionKind) Kind {
// the same way the engine resolves an extra directory: ~ expands, a
// relative path joins root, and the result is cleaned.
//
-// A placeholder may not add a ".." segment (spec §15.1): a capture is part
-// of a file name, which can be "..", and must not move the destination out
-// of the directory the rule names. A ".." written in the rule itself stands.
+// A placeholder may not take the destination out of the directory the
+// rule's own text names before it (spec §15.1): a capture or {ext} is part
+// of a file name, which can be "..", "~" or empty, and ResolveDir decides
+// from the expanded text whether a destination is relative, in the home
+// directory or absolute. So the resolved destination must lie at or under
+// staticDir of the raw text. A destination with no placeholder is written
+// entirely by the rule, and stands as written.
func expandDir(raw string, facts Facts, root string) (string, error) {
expanded, err := Expand(raw, facts)
if err != nil {
return "", err
}
- if dotDots(expanded) > dotDots(raw) {
- return "", fmt.Errorf("destination %q leaves its directory through a placeholder", expanded)
+ resolved := ResolveDir(expanded, root)
+ if strings.ContainsRune(raw, '{') {
+ if base := staticDir(raw, root); !within(resolved, base) {
+ return "", fmt.Errorf("destination %q leaves %s through a placeholder", expanded, xdg.Abbrev(base))
+ }
}
- return ResolveDir(expanded, root), nil
+ return resolved, nil
}
-// dotDots counts the ".." segments of a slash-separated path.
-func dotDots(p string) int {
- n := 0
- for _, seg := range strings.Split(p, "/") {
- if seg == ".." {
- n++
- }
+// 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]
+ }
+ switch i := strings.LastIndexByte(prefix, '/'); {
+ case i < 0:
+ prefix = ""
+ case i == 0:
+ prefix = "/"
+ default:
+ prefix = prefix[:i]
}
- return n
+ return ResolveDir(prefix, root)
+}
+
+// within reports whether path is dir itself or lies under it.
+func within(path, dir string) bool {
+ return path == dir || dir == string(filepath.Separator) || strings.HasPrefix(path, dir+string(filepath.Separator))
}
// ResolveDir expands a leading ~ and joins a relative directory to root,