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  }