1package vfs
2
3import (
4	"errors"
5	"io/fs"
6	"os"
7	"path/filepath"
8	"syscall"
9)
10
11// A Stater implements Stat. It is assumed that the fs.FileInfos returned by
12// Stat are compatible with os.SameFile.
13type Stater interface {
14	Stat(string) (fs.FileInfo, error)
15}
16
17// Contains returns true if p is reachable by traversing through prefix. prefix
18// must exist, but p may not. It is an expensive but accurate alternative to the
19// deprecated filepath.HasPrefix.
20func Contains(fileSystem Stater, p, prefix string) (bool, error) {
21	prefixFI, err := fileSystem.Stat(prefix)
22	if err != nil {
23		return false, err
24	}
25	for {
26		fi, err := fileSystem.Stat(p)
27		switch {
28		case err == nil:
29			if os.SameFile(fi, prefixFI) {
30				return true, nil
31			}
32			goto TryParent
33		case errors.Is(err, fs.ErrNotExist):
34			goto TryParent
35		case errors.Is(err, fs.ErrPermission):
36			goto TryParent
37		default:
38			// Remove any fs.PathError or os.SyscallError wrapping, if present.
39		Unwrap:
40			for {
41				var pathError *fs.PathError
42				var syscallError *os.SyscallError
43				switch {
44				case errors.As(err, &pathError):
45					err = pathError.Err
46				case errors.As(err, &syscallError):
47					err = syscallError.Err
48				default:
49					break Unwrap
50				}
51			}
52			// Ignore some syscall.Errnos.
53			var syscallErrno syscall.Errno
54			if errors.As(err, &syscallErrno) {
55				if _, ignore := ignoreErrnoInContains[syscallErrno]; ignore {
56					goto TryParent
57				}
58			}
59			return false, err
60		}
61	TryParent:
62		parentDir := filepath.Dir(p)
63		if parentDir == p {
64			// Return when we stop making progress.
65			return false, nil
66		}
67		p = parentDir
68	}
69}
70