diff options
Diffstat (limited to 'internal/apply/fs.go')
| -rw-r--r-- | internal/apply/fs.go | 59 |
1 files changed, 57 insertions, 2 deletions
diff --git a/internal/apply/fs.go b/internal/apply/fs.go index a8feccc..0eee780 100644 --- a/internal/apply/fs.go +++ b/internal/apply/fs.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" @@ -206,7 +207,17 @@ func splitExt(name string) (stem, ext string) { // is not included, and nothing is created or returned on error. func MkdirAllTracked(dir string) ([]string, error) { dir = filepath.Clean(dir) - if fi, err := os.Stat(dir); err == nil { + // Lstat, not Stat: a dangling symlink here reports ENOENT to Stat, so + // the "already a directory" branch is missed, os.Mkdir then fails with + // EEXIST, and the path used to be recorded as one krino created - which + // undo would later remove, unlinking a symlink krino never made. + if fi, err := os.Lstat(dir); err == nil { + if fi.Mode()&fs.ModeSymlink != 0 { + if resolved, serr := os.Stat(dir); serr != nil || !resolved.IsDir() { + return nil, fmt.Errorf("%s is a symlink that does not lead to a directory", dir) + } + return nil, nil + } if !fi.IsDir() { return nil, fmt.Errorf("%s exists and is not a directory", dir) } @@ -224,8 +235,52 @@ func MkdirAllTracked(dir string) ([]string, error) { } made = parentMade } - if err := os.Mkdir(dir, 0o755); err != nil && !os.IsExist(err) { + if err := os.Mkdir(dir, 0o755); err != nil { + if os.IsExist(err) { + // Something else created it between the Lstat and here: it is + // not ours to remove again. + return made, nil + } return made, err } return append(made, dir), nil } + +// UnderSymlink reports the first component of path, at or below root, that +// is a symlink - "" when there is none, and "" when path is not under root +// at all. +// +// Placeholders are already stopped from redirecting a step out of the +// directory a rule named (internal/plan's expandDir); a symlink is a name +// too, and one planted inside the directory krino sorts - by an unpacked +// archive, say - redirects a move or a copy exactly the same way, while the +// plan the user approved shows only the text. A destination the +// configuration itself names, outside the sorted directory, is the user's +// own arrangement and is not second-guessed. +func UnderSymlink(path, root string) string { + if root == "" { + return "" + } + root = filepath.Clean(root) + path = filepath.Clean(path) + rel, err := filepath.Rel(root, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "" + } + at := root + for _, part := range strings.Split(filepath.ToSlash(rel), "/") { + if part == "." || part == "" { + continue + } + at = filepath.Join(at, part) + fi, err := os.Lstat(at) + if err != nil { + // Not there yet: nothing below it can be a symlink either. + return "" + } + if fi.Mode()&fs.ModeSymlink != 0 { + return at + } + } + return "" +} |
