1package contenthash
2
3import (
4	"os"
5	"path/filepath"
6
7	"github.com/pkg/errors"
8)
9
10var (
11	errTooManyLinks = errors.New("too many links")
12)
13
14type onSymlinkFunc func(string, string) error
15
16// rootPath joins a path with a root, evaluating and bounding any
17// symlink to the root directory.
18// This is containerd/continuity/fs RootPath implementation with a callback on
19// resolving the symlink.
20func rootPath(root, path string, cb onSymlinkFunc) (string, error) {
21	if path == "" {
22		return root, nil
23	}
24	var linksWalked int // to protect against cycles
25	for {
26		i := linksWalked
27		newpath, err := walkLinks(root, path, &linksWalked, cb)
28		if err != nil {
29			return "", err
30		}
31		path = newpath
32		if i == linksWalked {
33			newpath = filepath.Join("/", newpath)
34			if path == newpath {
35				return filepath.Join(root, newpath), nil
36			}
37			path = newpath
38		}
39	}
40}
41
42func walkLink(root, path string, linksWalked *int, cb onSymlinkFunc) (newpath string, islink bool, err error) {
43	if *linksWalked > 255 {
44		return "", false, errTooManyLinks
45	}
46
47	path = filepath.Join("/", path)
48	if path == "/" {
49		return path, false, nil
50	}
51	realPath := filepath.Join(root, path)
52
53	fi, err := os.Lstat(realPath)
54	if err != nil {
55		// If path does not yet exist, treat as non-symlink
56		if errors.Is(err, os.ErrNotExist) {
57			return path, false, nil
58		}
59		return "", false, err
60	}
61	if fi.Mode()&os.ModeSymlink == 0 {
62		return path, false, nil
63	}
64	newpath, err = os.Readlink(realPath)
65	if err != nil {
66		return "", false, err
67	}
68	if cb != nil {
69		if err := cb(path, newpath); err != nil {
70			return "", false, err
71		}
72	}
73	*linksWalked++
74	return newpath, true, nil
75}
76
77func walkLinks(root, path string, linksWalked *int, cb onSymlinkFunc) (string, error) {
78	switch dir, file := filepath.Split(path); {
79	case dir == "":
80		newpath, _, err := walkLink(root, file, linksWalked, cb)
81		return newpath, err
82	case file == "":
83		if os.IsPathSeparator(dir[len(dir)-1]) {
84			if dir == "/" {
85				return dir, nil
86			}
87			return walkLinks(root, dir[:len(dir)-1], linksWalked, cb)
88		}
89		newpath, _, err := walkLink(root, dir, linksWalked, cb)
90		return newpath, err
91	default:
92		newdir, err := walkLinks(root, dir, linksWalked, cb)
93		if err != nil {
94			return "", err
95		}
96		newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, cb)
97		if err != nil {
98			return "", err
99		}
100		if !islink {
101			return newpath, nil
102		}
103		if filepath.IsAbs(newpath) {
104			return newpath, nil
105		}
106		return filepath.Join(newdir, newpath), nil
107	}
108}
109