github.com/charlievieth/fastwalk@v1.0.3/entry_filter_windows.go (about) 1 //go:build windows && !appengine 2 // +build windows,!appengine 3 4 package fastwalk 5 6 import ( 7 "io/fs" 8 "os" 9 "path/filepath" 10 "sync" 11 "syscall" 12 ) 13 14 type fileKey struct { 15 VolumeSerialNumber uint32 16 FileIndexHigh uint32 17 FileIndexLow uint32 18 } 19 20 type EntryFilter struct { 21 mu sync.Mutex 22 seen map[fileKey]struct{} 23 } 24 25 func NewEntryFilter() *EntryFilter { 26 return &EntryFilter{seen: make(map[fileKey]struct{}, 128)} 27 } 28 29 func (e *EntryFilter) Entry(path string, _ fs.DirEntry) bool { 30 namep, err := syscall.UTF16PtrFromString(fixLongPath(path)) 31 if err != nil { 32 return false 33 } 34 35 h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, 36 syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) 37 if err != nil { 38 return false 39 } 40 41 var d syscall.ByHandleFileInformation 42 err = syscall.GetFileInformationByHandle(h, &d) 43 syscall.CloseHandle(h) 44 if err != nil { 45 return false 46 } 47 48 key := fileKey{ 49 VolumeSerialNumber: d.VolumeSerialNumber, 50 FileIndexHigh: d.FileIndexHigh, 51 FileIndexLow: d.FileIndexLow, 52 } 53 54 e.mu.Lock() 55 if e.seen == nil { 56 e.seen = make(map[fileKey]struct{}) 57 } 58 _, ok := e.seen[key] 59 if !ok { 60 e.seen[key] = struct{}{} 61 } 62 e.mu.Unlock() 63 64 return ok 65 } 66 67 func isAbs(path string) (b bool) { 68 v := filepath.VolumeName(path) 69 if v == "" { 70 return false 71 } 72 path = path[len(v):] 73 if path == "" { 74 return false 75 } 76 return os.IsPathSeparator(path[0]) 77 } 78 79 // fixLongPath returns the extended-length (\\?\-prefixed) form of 80 // path when needed, in order to avoid the default 260 character file 81 // path limit imposed by Windows. If path is not easily converted to 82 // the extended-length form (for example, if path is a relative path 83 // or contains .. elements), or is short enough, fixLongPath returns 84 // path unmodified. 85 // 86 // See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath 87 func fixLongPath(path string) string { 88 // Do nothing (and don't allocate) if the path is "short". 89 // Empirically (at least on the Windows Server 2013 builder), 90 // the kernel is arbitrarily okay with < 248 bytes. That 91 // matches what the docs above say: 92 // "When using an API to create a directory, the specified 93 // path cannot be so long that you cannot append an 8.3 file 94 // name (that is, the directory name cannot exceed MAX_PATH 95 // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. 96 // 97 // The MSDN docs appear to say that a normal path that is 248 bytes long 98 // will work; empirically the path must be less then 248 bytes long. 99 if len(path) < 248 { 100 // Don't fix. (This is how Go 1.7 and earlier worked, 101 // not automatically generating the \\?\ form) 102 return path 103 } 104 105 // The extended form begins with \\?\, as in 106 // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. 107 // The extended form disables evaluation of . and .. path 108 // elements and disables the interpretation of / as equivalent 109 // to \. The conversion here rewrites / to \ and elides 110 // . elements as well as trailing or duplicate separators. For 111 // simplicity it avoids the conversion entirely for relative 112 // paths or paths containing .. elements. For now, 113 // \\server\share paths are not converted to 114 // \\?\UNC\server\share paths because the rules for doing so 115 // are less well-specified. 116 if len(path) >= 2 && path[:2] == `\\` { 117 // Don't canonicalize UNC paths. 118 return path 119 } 120 if !isAbs(path) { 121 // Relative path 122 return path 123 } 124 125 const prefix = `\\?` 126 127 pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) 128 copy(pathbuf, prefix) 129 n := len(path) 130 r, w := 0, len(prefix) 131 for r < n { 132 switch { 133 case os.IsPathSeparator(path[r]): 134 // empty block 135 r++ 136 case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): 137 // /./ 138 r++ 139 case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): 140 // /../ is currently unhandled 141 return path 142 default: 143 pathbuf[w] = '\\' 144 w++ 145 for ; r < n && !os.IsPathSeparator(path[r]); r++ { 146 pathbuf[w] = path[r] 147 w++ 148 } 149 } 150 } 151 // A drive's root directory needs a trailing \ 152 if w == len(`\\?\c:`) { 153 pathbuf[w] = '\\' 154 w++ 155 } 156 return string(pathbuf[:w]) 157 }