diff options
Diffstat (limited to 'internal/plan/chain.go')
| -rw-r--r-- | internal/plan/chain.go | 50 |
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, |
