1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
// This code mostly comes from <https://github.com/cyphar/filepath-securejoin>.
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package contenthash
import (
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
var errTooManyLinks = errors.New("too many links")
const maxSymlinkLimit = 255
type onSymlinkFunc func(string, string) error
// rootPath joins a path with a root, evaluating and bounding any symlink to
// the root directory. This is a slightly modified version of SecureJoin from
// github.com/cyphar/filepath-securejoin, with a callback which we call after
// each symlink resolution.
func rootPath(root, unsafePath string, followTrailing bool, cb onSymlinkFunc) (string, error) {
if unsafePath == "" {
return root, nil
}
unsafePath = filepath.FromSlash(unsafePath)
var (
currentPath string
linksWalked int
)
for unsafePath != "" {
// Windows-specific: remove any drive letters from the path.
if v := filepath.VolumeName(unsafePath); v != "" {
unsafePath = unsafePath[len(v):]
}
// Remove any unnecessary trailing slashes.
unsafePath = strings.TrimSuffix(unsafePath, string(filepath.Separator))
// Get the next path component.
var part string
if i := strings.IndexRune(unsafePath, filepath.Separator); i == -1 {
part, unsafePath = unsafePath, ""
} else {
part, unsafePath = unsafePath[:i], unsafePath[i+1:]
}
// Apply the component lexically to the path we are building. path does
// not contain any symlinks, and we are lexically dealing with a single
// component, so it's okay to do filepath.Clean here.
nextPath := filepath.Join(string(filepath.Separator), currentPath, part)
if nextPath == string(filepath.Separator) {
// If we end up back at the root, we don't need to re-evaluate /.
currentPath = ""
continue
}
fullPath := root + string(filepath.Separator) + nextPath
// Figure out whether the path is a symlink.
fi, err := os.Lstat(fullPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", err
}
// Treat non-existent path components the same as non-symlinks (we
// can't do any better here).
if errors.Is(err, os.ErrNotExist) || fi.Mode()&os.ModeSymlink == 0 {
currentPath = nextPath
continue
}
// Don't resolve the final component with !followTrailing.
if !followTrailing && unsafePath == "" {
currentPath = nextPath
break
}
// It's a symlink, so get its contents and expand it by prepending it
// to the yet-unparsed path.
linksWalked++
if linksWalked > maxSymlinkLimit {
return "", errTooManyLinks
}
dest, err := os.Readlink(fullPath)
if err != nil {
return "", err
}
if cb != nil {
if err := cb(nextPath, dest); err != nil {
return "", err
}
}
unsafePath = dest + string(filepath.Separator) + unsafePath
// Absolute symlinks reset any work we've already done.
if filepath.IsAbs(dest) {
currentPath = ""
}
}
// There should be no lexical components left in path here, but just for
// safety do a filepath.Clean before the join.
finalPath := filepath.Join(string(filepath.Separator), currentPath)
return filepath.Join(root, finalPath), nil
}
|