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