github.com/brass-software/os@v0.0.0-20240129060254-960f457a5dea/dir_windows.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package os 6 7 import ( 8 "internal/syscall/windows" 9 "io" 10 "io/fs" 11 "runtime" 12 "sync" 13 "syscall" 14 "unsafe" 15 ) 16 17 // Auxiliary information if the File describes a directory 18 type dirInfo struct { 19 // buf is a slice pointer so the slice header 20 // does not escape to the heap when returning 21 // buf to dirBufPool. 22 buf *[]byte // buffer for directory I/O 23 bufp int // location of next record in buf 24 vol uint32 25 class uint32 // type of entries in buf 26 path string // absolute directory path, empty if the file system supports FILE_ID_BOTH_DIR_INFO 27 } 28 29 const ( 30 // dirBufSize is the size of the dirInfo buffer. 31 // The buffer must be big enough to hold at least a single entry. 32 // The filename alone can be 512 bytes (MAX_PATH*2), and the fixed part of 33 // the FILE_ID_BOTH_DIR_INFO structure is 105 bytes, so dirBufSize 34 // should not be set below 1024 bytes (512+105+safety buffer). 35 // Windows 8.1 and earlier only works with buffer sizes up to 64 kB. 36 dirBufSize = 64 * 1024 // 64kB 37 ) 38 39 var dirBufPool = sync.Pool{ 40 New: func() any { 41 // The buffer must be at least a block long. 42 buf := make([]byte, dirBufSize) 43 return &buf 44 }, 45 } 46 47 func (d *dirInfo) close() { 48 if d.buf != nil { 49 dirBufPool.Put(d.buf) 50 d.buf = nil 51 } 52 } 53 54 // allowReadDirFileID indicates whether File.readdir should try to use FILE_ID_BOTH_DIR_INFO 55 // if the underlying file system supports it. 56 // Useful for testing purposes. 57 var allowReadDirFileID = true 58 59 func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { 60 // If this file has no dirinfo, create one. 61 if file.dirinfo == nil { 62 // vol is used by os.SameFile. 63 // It is safe to query it once and reuse the value. 64 // Hard links are not allowed to reference files in other volumes. 65 // Junctions and symbolic links can reference files and directories in other volumes, 66 // but the reparse point should still live in the parent volume. 67 var vol, flags uint32 68 err = windows.GetVolumeInformationByHandle(file.pfd.Sysfd, nil, 0, &vol, nil, &flags, nil, 0) 69 runtime.KeepAlive(file) 70 if err != nil { 71 err = &PathError{Op: "readdir", Path: file.name, Err: err} 72 return 73 } 74 file.dirinfo = new(dirInfo) 75 file.dirinfo.buf = dirBufPool.Get().(*[]byte) 76 file.dirinfo.vol = vol 77 if allowReadDirFileID && flags&windows.FILE_SUPPORTS_OPEN_BY_FILE_ID != 0 { 78 file.dirinfo.class = windows.FileIdBothDirectoryRestartInfo 79 } else { 80 file.dirinfo.class = windows.FileFullDirectoryRestartInfo 81 // Set the directory path for use by os.SameFile, as it is possible that 82 // the file system supports retrieving the file ID using GetFileInformationByHandle. 83 file.dirinfo.path = file.name 84 if !isAbs(file.dirinfo.path) { 85 // If the path is relative, we need to convert it to an absolute path 86 // in case the current directory changes between this call and a 87 // call to os.SameFile. 88 file.dirinfo.path, err = syscall.FullPath(file.dirinfo.path) 89 if err != nil { 90 err = &PathError{Op: "readdir", Path: file.name, Err: err} 91 return 92 } 93 } 94 } 95 } 96 d := file.dirinfo 97 wantAll := n <= 0 98 if wantAll { 99 n = -1 100 } 101 for n != 0 { 102 // Refill the buffer if necessary 103 if d.bufp == 0 { 104 err = windows.GetFileInformationByHandleEx(file.pfd.Sysfd, d.class, (*byte)(unsafe.Pointer(&(*d.buf)[0])), uint32(len(*d.buf))) 105 runtime.KeepAlive(file) 106 if err != nil { 107 if err == syscall.ERROR_NO_MORE_FILES { 108 break 109 } 110 if err == syscall.ERROR_FILE_NOT_FOUND && 111 (d.class == windows.FileIdBothDirectoryRestartInfo || d.class == windows.FileFullDirectoryRestartInfo) { 112 // GetFileInformationByHandleEx doesn't document the return error codes when the info class is FileIdBothDirectoryRestartInfo, 113 // but MS-FSA 2.1.5.6.3 [1] specifies that the underlying file system driver should return STATUS_NO_SUCH_FILE when 114 // reading an empty root directory, which is mapped to ERROR_FILE_NOT_FOUND by Windows. 115 // Note that some file system drivers may never return this error code, as the spec allows to return the "." and ".." 116 // entries in such cases, making the directory appear non-empty. 117 // The chances of false positive are very low, as we know that the directory exists, else GetVolumeInformationByHandle 118 // would have failed, and that the handle is still valid, as we haven't closed it. 119 // See go.dev/issue/61159. 120 // [1] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fsa/fa8194e0-53ec-413b-8315-e8fa85396fd8 121 break 122 } 123 if s, _ := file.Stat(); s != nil && !s.IsDir() { 124 err = &PathError{Op: "readdir", Path: file.name, Err: syscall.ENOTDIR} 125 } else { 126 err = &PathError{Op: "GetFileInformationByHandleEx", Path: file.name, Err: err} 127 } 128 return 129 } 130 if d.class == windows.FileIdBothDirectoryRestartInfo { 131 d.class = windows.FileIdBothDirectoryInfo 132 } else if d.class == windows.FileFullDirectoryRestartInfo { 133 d.class = windows.FileFullDirectoryInfo 134 } 135 } 136 // Drain the buffer 137 var islast bool 138 for n != 0 && !islast { 139 var nextEntryOffset uint32 140 var nameslice []uint16 141 entry := unsafe.Pointer(&(*d.buf)[d.bufp]) 142 if d.class == windows.FileIdBothDirectoryInfo { 143 info := (*windows.FILE_ID_BOTH_DIR_INFO)(entry) 144 nextEntryOffset = info.NextEntryOffset 145 nameslice = unsafe.Slice(&info.FileName[0], info.FileNameLength/2) 146 } else { 147 info := (*windows.FILE_FULL_DIR_INFO)(entry) 148 nextEntryOffset = info.NextEntryOffset 149 nameslice = unsafe.Slice(&info.FileName[0], info.FileNameLength/2) 150 } 151 d.bufp += int(nextEntryOffset) 152 islast = nextEntryOffset == 0 153 if islast { 154 d.bufp = 0 155 } 156 if (len(nameslice) == 1 && nameslice[0] == '.') || 157 (len(nameslice) == 2 && nameslice[0] == '.' && nameslice[1] == '.') { 158 // Ignore "." and ".." and avoid allocating a string for them. 159 continue 160 } 161 name := syscall.UTF16ToString(nameslice) 162 if mode == readdirName { 163 names = append(names, name) 164 } else { 165 var f *fileStat 166 if d.class == windows.FileIdBothDirectoryInfo { 167 f = newFileStatFromFileIDBothDirInfo((*windows.FILE_ID_BOTH_DIR_INFO)(entry)) 168 } else { 169 f = newFileStatFromFileFullDirInfo((*windows.FILE_FULL_DIR_INFO)(entry)) 170 // Defer appending the entry name to the parent directory path until 171 // it is really needed, to avoid allocating a string that may not be used. 172 // It is currently only used in os.SameFile. 173 f.appendNameToPath = true 174 f.path = d.path 175 } 176 f.name = name 177 f.vol = d.vol 178 if mode == readdirDirEntry { 179 dirents = append(dirents, dirEntry{f}) 180 } else { 181 infos = append(infos, f) 182 } 183 } 184 n-- 185 } 186 } 187 if !wantAll && len(names)+len(dirents)+len(infos) == 0 { 188 return nil, nil, nil, io.EOF 189 } 190 return names, dirents, infos, nil 191 } 192 193 type dirEntry struct { 194 fs *fileStat 195 } 196 197 func (de dirEntry) Name() string { return de.fs.Name() } 198 func (de dirEntry) IsDir() bool { return de.fs.IsDir() } 199 func (de dirEntry) Type() FileMode { return de.fs.Mode().Type() } 200 func (de dirEntry) Info() (FileInfo, error) { return de.fs, nil } 201 202 func (de dirEntry) String() string { 203 return fs.FormatDirEntry(de) 204 }