|
| 1 | +package internal |
| 2 | + |
| 3 | +import ( |
| 4 | + "os" |
| 5 | + "path/filepath" |
| 6 | + "strings" |
| 7 | + "unicode" |
| 8 | + |
| 9 | + "github.com/pkg/errors" |
| 10 | +) |
| 11 | + |
| 12 | +func IsDir(name string) (bool, error) { |
| 13 | + // TODO: lstat? |
| 14 | + fi, err := os.Stat(name) |
| 15 | + if os.IsNotExist(err) { |
| 16 | + return false, nil |
| 17 | + } |
| 18 | + if err != nil { |
| 19 | + return false, err |
| 20 | + } |
| 21 | + if !fi.IsDir() { |
| 22 | + return false, errors.Errorf("%q is not a directory", name) |
| 23 | + } |
| 24 | + return true, nil |
| 25 | +} |
| 26 | + |
| 27 | +// HasFilepathPrefix will determine if "path" starts with "prefix" from |
| 28 | +// the point of view of a filesystem. |
| 29 | +// |
| 30 | +// Unlike filepath.HasPrefix, this function is path-aware, meaning that |
| 31 | +// it knows that two directories /foo and /foobar are not the same |
| 32 | +// thing, and therefore HasFilepathPrefix("/foobar", "/foo") will return |
| 33 | +// false. |
| 34 | +// |
| 35 | +// This function also handles the case where the involved filesystems |
| 36 | +// are case-insensitive, meaning /foo/bar and /Foo/Bar correspond to the |
| 37 | +// same file. In that situation HasFilepathPrefix("/Foo/Bar", "/foo") |
| 38 | +// will return true. The implementation is *not* OS-specific, so a FAT32 |
| 39 | +// filesystem mounted on Linux will be handled correctly. |
| 40 | +func HasFilepathPrefix(path, prefix string) bool { |
| 41 | + if filepath.VolumeName(path) != filepath.VolumeName(prefix) { |
| 42 | + return false |
| 43 | + } |
| 44 | + |
| 45 | + var dn string |
| 46 | + |
| 47 | + if isDir, err := IsDir(path); err != nil { |
| 48 | + return false |
| 49 | + } else if isDir { |
| 50 | + dn = path |
| 51 | + } else { |
| 52 | + dn = filepath.Dir(path) |
| 53 | + } |
| 54 | + |
| 55 | + dn = strings.TrimSuffix(dn, string(os.PathSeparator)) |
| 56 | + prefix = strings.TrimSuffix(prefix, string(os.PathSeparator)) |
| 57 | + |
| 58 | + dirs := strings.Split(dn, string(os.PathSeparator))[1:] |
| 59 | + prefixes := strings.Split(prefix, string(os.PathSeparator))[1:] |
| 60 | + |
| 61 | + if len(prefixes) > len(dirs) { |
| 62 | + return false |
| 63 | + } |
| 64 | + |
| 65 | + var d, p string |
| 66 | + |
| 67 | + for i := range prefixes { |
| 68 | + // need to test each component of the path for |
| 69 | + // case-sensitiveness because on Unix we could have |
| 70 | + // something like ext4 filesystem mounted on FAT |
| 71 | + // mountpoint, mounted on ext4 filesystem, i.e. the |
| 72 | + // problematic filesystem is not the last one. |
| 73 | + if isCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) { |
| 74 | + d = filepath.Join(d, dirs[i]) |
| 75 | + p = filepath.Join(p, prefixes[i]) |
| 76 | + } else { |
| 77 | + d = filepath.Join(d, strings.ToLower(dirs[i])) |
| 78 | + p = filepath.Join(p, strings.ToLower(prefixes[i])) |
| 79 | + } |
| 80 | + |
| 81 | + if p != d { |
| 82 | + return false |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + return true |
| 87 | +} |
| 88 | + |
| 89 | +// genTestFilename returns a string with at most one rune case-flipped. |
| 90 | +// |
| 91 | +// The transformation is applied only to the first rune that can be |
| 92 | +// reversibly case-flipped, meaning: |
| 93 | +// |
| 94 | +// * A lowercase rune for which it's true that lower(upper(r)) == r |
| 95 | +// * An uppercase rune for which it's true that upper(lower(r)) == r |
| 96 | +// |
| 97 | +// All the other runes are left intact. |
| 98 | +func genTestFilename(str string) string { |
| 99 | + flip := true |
| 100 | + return strings.Map(func(r rune) rune { |
| 101 | + if flip { |
| 102 | + if unicode.IsLower(r) { |
| 103 | + u := unicode.ToUpper(r) |
| 104 | + if unicode.ToLower(u) == r { |
| 105 | + r = u |
| 106 | + flip = false |
| 107 | + } |
| 108 | + } else if unicode.IsUpper(r) { |
| 109 | + l := unicode.ToLower(r) |
| 110 | + if unicode.ToUpper(l) == r { |
| 111 | + r = l |
| 112 | + flip = false |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + return r |
| 117 | + }, str) |
| 118 | +} |
| 119 | + |
| 120 | +// isCaseSensitiveFilesystem determines if the filesystem where dir |
| 121 | +// exists is case sensitive or not. |
| 122 | +// |
| 123 | +// CAVEAT: this function works by taking the last component of the given |
| 124 | +// path and flipping the case of the first letter for which case |
| 125 | +// flipping is a reversible operation (/foo/Bar → /foo/bar), then |
| 126 | +// testing for the existence of the new filename. There are two |
| 127 | +// possibilities: |
| 128 | +// |
| 129 | +// 1. The alternate filename does not exist. We can conclude that the |
| 130 | +// filesystem is case sensitive. |
| 131 | +// |
| 132 | +// 2. The filename happens to exist. We have to test if the two files |
| 133 | +// are the same file (case insensitive file system) or different ones |
| 134 | +// (case sensitive filesystem). |
| 135 | +// |
| 136 | +// If the input directory is such that the last component is composed |
| 137 | +// exclusively of case-less codepoints (e.g. numbers), this function will |
| 138 | +// return false. |
| 139 | +func isCaseSensitiveFilesystem(dir string) bool { |
| 140 | + alt := filepath.Join(filepath.Dir(dir), |
| 141 | + genTestFilename(filepath.Base(dir))) |
| 142 | + |
| 143 | + dInfo, err := os.Stat(dir) |
| 144 | + if err != nil { |
| 145 | + return true |
| 146 | + } |
| 147 | + |
| 148 | + aInfo, err := os.Stat(alt) |
| 149 | + if err != nil { |
| 150 | + return true |
| 151 | + } |
| 152 | + |
| 153 | + return !os.SameFile(dInfo, aInfo) |
| 154 | +} |
0 commit comments