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/filepathlite"
9	"internal/godebug"
10	"internal/syscall/windows"
11	"sync"
12	"syscall"
13	"time"
14	"unsafe"
15)
16
17// A fileStat is the implementation of FileInfo returned by Stat and Lstat.
18type fileStat struct {
19	name string
20
21	// from ByHandleFileInformation, Win32FileAttributeData, Win32finddata, and GetFileInformationByHandleEx
22	FileAttributes uint32
23	CreationTime   syscall.Filetime
24	LastAccessTime syscall.Filetime
25	LastWriteTime  syscall.Filetime
26	FileSizeHigh   uint32
27	FileSizeLow    uint32
28
29	// from Win32finddata and GetFileInformationByHandleEx
30	ReparseTag uint32
31
32	// what syscall.GetFileType returns
33	filetype uint32
34
35	// used to implement SameFile
36	sync.Mutex
37	path             string
38	vol              uint32
39	idxhi            uint32
40	idxlo            uint32
41	appendNameToPath bool
42}
43
44// newFileStatFromGetFileInformationByHandle calls GetFileInformationByHandle
45// to gather all required information about the file handle h.
46func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (fs *fileStat, err error) {
47	var d syscall.ByHandleFileInformation
48	err = syscall.GetFileInformationByHandle(h, &d)
49	if err != nil {
50		return nil, &PathError{Op: "GetFileInformationByHandle", Path: path, Err: err}
51	}
52
53	var reparseTag uint32
54	if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
55		var ti windows.FILE_ATTRIBUTE_TAG_INFO
56		err = windows.GetFileInformationByHandleEx(h, windows.FileAttributeTagInfo, (*byte)(unsafe.Pointer(&ti)), uint32(unsafe.Sizeof(ti)))
57		if err != nil {
58			return nil, &PathError{Op: "GetFileInformationByHandleEx", Path: path, Err: err}
59		}
60		reparseTag = ti.ReparseTag
61	}
62
63	return &fileStat{
64		name:           filepathlite.Base(path),
65		FileAttributes: d.FileAttributes,
66		CreationTime:   d.CreationTime,
67		LastAccessTime: d.LastAccessTime,
68		LastWriteTime:  d.LastWriteTime,
69		FileSizeHigh:   d.FileSizeHigh,
70		FileSizeLow:    d.FileSizeLow,
71		vol:            d.VolumeSerialNumber,
72		idxhi:          d.FileIndexHigh,
73		idxlo:          d.FileIndexLow,
74		ReparseTag:     reparseTag,
75		// fileStat.path is used by os.SameFile to decide if it needs
76		// to fetch vol, idxhi and idxlo. But these are already set,
77		// so set fileStat.path to "" to prevent os.SameFile doing it again.
78	}, nil
79}
80
81// newFileStatFromWin32FileAttributeData copies all required information
82// from syscall.Win32FileAttributeData d into the newly created fileStat.
83func newFileStatFromWin32FileAttributeData(d *syscall.Win32FileAttributeData) *fileStat {
84	return &fileStat{
85		FileAttributes: d.FileAttributes,
86		CreationTime:   d.CreationTime,
87		LastAccessTime: d.LastAccessTime,
88		LastWriteTime:  d.LastWriteTime,
89		FileSizeHigh:   d.FileSizeHigh,
90		FileSizeLow:    d.FileSizeLow,
91	}
92}
93
94// newFileStatFromFileIDBothDirInfo copies all required information
95// from windows.FILE_ID_BOTH_DIR_INFO d into the newly created fileStat.
96func newFileStatFromFileIDBothDirInfo(d *windows.FILE_ID_BOTH_DIR_INFO) *fileStat {
97	// The FILE_ID_BOTH_DIR_INFO MSDN documentations isn't completely correct.
98	// FileAttributes can contain any file attributes that is currently set on the file,
99	// not just the ones documented.
100	// EaSize contains the reparse tag if the file is a reparse point.
101	return &fileStat{
102		FileAttributes: d.FileAttributes,
103		CreationTime:   d.CreationTime,
104		LastAccessTime: d.LastAccessTime,
105		LastWriteTime:  d.LastWriteTime,
106		FileSizeHigh:   uint32(d.EndOfFile >> 32),
107		FileSizeLow:    uint32(d.EndOfFile),
108		ReparseTag:     d.EaSize,
109		idxhi:          uint32(d.FileID >> 32),
110		idxlo:          uint32(d.FileID),
111	}
112}
113
114// newFileStatFromFileFullDirInfo copies all required information
115// from windows.FILE_FULL_DIR_INFO d into the newly created fileStat.
116func newFileStatFromFileFullDirInfo(d *windows.FILE_FULL_DIR_INFO) *fileStat {
117	return &fileStat{
118		FileAttributes: d.FileAttributes,
119		CreationTime:   d.CreationTime,
120		LastAccessTime: d.LastAccessTime,
121		LastWriteTime:  d.LastWriteTime,
122		FileSizeHigh:   uint32(d.EndOfFile >> 32),
123		FileSizeLow:    uint32(d.EndOfFile),
124		ReparseTag:     d.EaSize,
125	}
126}
127
128// newFileStatFromWin32finddata copies all required information
129// from syscall.Win32finddata d into the newly created fileStat.
130func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
131	fs := &fileStat{
132		FileAttributes: d.FileAttributes,
133		CreationTime:   d.CreationTime,
134		LastAccessTime: d.LastAccessTime,
135		LastWriteTime:  d.LastWriteTime,
136		FileSizeHigh:   d.FileSizeHigh,
137		FileSizeLow:    d.FileSizeLow,
138	}
139	if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
140		// Per https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw:
141		// “If the dwFileAttributes member includes the FILE_ATTRIBUTE_REPARSE_POINT
142		// attribute, this member specifies the reparse point tag. Otherwise, this
143		// value is undefined and should not be used.”
144		fs.ReparseTag = d.Reserved0
145	}
146	return fs
147}
148
149// isReparseTagNameSurrogate determines whether a tag's associated
150// reparse point is a surrogate for another named entity (for example, a mounted folder).
151//
152// See https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-isreparsetagnamesurrogate
153// and https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags.
154func (fs *fileStat) isReparseTagNameSurrogate() bool {
155	// True for IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT.
156	return fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && fs.ReparseTag&0x20000000 != 0
157}
158
159func (fs *fileStat) Size() int64 {
160	return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow)
161}
162
163var winsymlink = godebug.New("winsymlink")
164
165func (fs *fileStat) Mode() FileMode {
166	m := fs.mode()
167	if winsymlink.Value() == "0" {
168		old := fs.modePreGo1_23()
169		if old != m {
170			winsymlink.IncNonDefault()
171			m = old
172		}
173	}
174	return m
175}
176
177func (fs *fileStat) mode() (m FileMode) {
178	if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
179		m |= 0444
180	} else {
181		m |= 0666
182	}
183
184	// Windows reports the FILE_ATTRIBUTE_DIRECTORY bit for reparse points
185	// that refer to directories, such as symlinks and mount points.
186	// However, we follow symlink POSIX semantics and do not set the mode bits.
187	// This allows users to walk directories without following links
188	// by just calling "fi, err := os.Lstat(name); err == nil && fi.IsDir()".
189	// Note that POSIX only defines the semantics for symlinks, not for
190	// mount points or other surrogate reparse points, but we treat them
191	// the same way for consistency. Also, mount points can contain infinite
192	// loops, so it is not safe to walk them without special handling.
193	if !fs.isReparseTagNameSurrogate() {
194		if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
195			m |= ModeDir | 0111
196		}
197
198		switch fs.filetype {
199		case syscall.FILE_TYPE_PIPE:
200			m |= ModeNamedPipe
201		case syscall.FILE_TYPE_CHAR:
202			m |= ModeDevice | ModeCharDevice
203		}
204	}
205
206	if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
207		switch fs.ReparseTag {
208		case syscall.IO_REPARSE_TAG_SYMLINK:
209			m |= ModeSymlink
210		case windows.IO_REPARSE_TAG_AF_UNIX:
211			m |= ModeSocket
212		case windows.IO_REPARSE_TAG_DEDUP:
213			// If the Data Deduplication service is enabled on Windows Server, its
214			// Optimization job may convert regular files to IO_REPARSE_TAG_DEDUP
215			// whenever that job runs.
216			//
217			// However, DEDUP reparse points remain similar in most respects to
218			// regular files: they continue to support random-access reads and writes
219			// of persistent data, and they shouldn't add unexpected latency or
220			// unavailability in the way that a network filesystem might.
221			//
222			// Go programs may use ModeIrregular to filter out unusual files (such as
223			// raw device files on Linux, POSIX FIFO special files, and so on), so
224			// to avoid files changing unpredictably from regular to irregular we will
225			// consider DEDUP files to be close enough to regular to treat as such.
226		default:
227			m |= ModeIrregular
228		}
229	}
230	return
231}
232
233// modePreGo1_23 returns the FileMode for the fileStat, using the pre-Go 1.23
234// logic for determining the file mode.
235// The logic is subtle and not well-documented, so it is better to keep it
236// separate from the new logic.
237func (fs *fileStat) modePreGo1_23() (m FileMode) {
238	if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
239		m |= 0444
240	} else {
241		m |= 0666
242	}
243	if fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK ||
244		fs.ReparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT {
245		return m | ModeSymlink
246	}
247	if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
248		m |= ModeDir | 0111
249	}
250	switch fs.filetype {
251	case syscall.FILE_TYPE_PIPE:
252		m |= ModeNamedPipe
253	case syscall.FILE_TYPE_CHAR:
254		m |= ModeDevice | ModeCharDevice
255	}
256	if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
257		if fs.ReparseTag == windows.IO_REPARSE_TAG_AF_UNIX {
258			m |= ModeSocket
259		}
260		if m&ModeType == 0 {
261			if fs.ReparseTag == windows.IO_REPARSE_TAG_DEDUP {
262				// See comment in fs.Mode.
263			} else {
264				m |= ModeIrregular
265			}
266		}
267	}
268	return m
269}
270
271func (fs *fileStat) ModTime() time.Time {
272	return time.Unix(0, fs.LastWriteTime.Nanoseconds())
273}
274
275// Sys returns syscall.Win32FileAttributeData for file fs.
276func (fs *fileStat) Sys() any {
277	return &syscall.Win32FileAttributeData{
278		FileAttributes: fs.FileAttributes,
279		CreationTime:   fs.CreationTime,
280		LastAccessTime: fs.LastAccessTime,
281		LastWriteTime:  fs.LastWriteTime,
282		FileSizeHigh:   fs.FileSizeHigh,
283		FileSizeLow:    fs.FileSizeLow,
284	}
285}
286
287func (fs *fileStat) loadFileId() error {
288	fs.Lock()
289	defer fs.Unlock()
290	if fs.path == "" {
291		// already done
292		return nil
293	}
294	var path string
295	if fs.appendNameToPath {
296		path = fixLongPath(fs.path + `\` + fs.name)
297	} else {
298		path = fs.path
299	}
300	pathp, err := syscall.UTF16PtrFromString(path)
301	if err != nil {
302		return err
303	}
304
305	// Per https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points-and-file-operations,
306	// “Applications that use the CreateFile function should specify the
307	// FILE_FLAG_OPEN_REPARSE_POINT flag when opening the file if it is a reparse
308	// point.”
309	//
310	// And per https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew,
311	// “If the file is not a reparse point, then this flag is ignored.”
312	//
313	// So we set FILE_FLAG_OPEN_REPARSE_POINT unconditionally, since we want
314	// information about the reparse point itself.
315	//
316	// If the file is a symlink, the symlink target should have already been
317	// resolved when the fileStat was created, so we don't need to worry about
318	// resolving symlink reparse points again here.
319	attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT)
320
321	h, err := syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
322	if err != nil {
323		return err
324	}
325	defer syscall.CloseHandle(h)
326	var i syscall.ByHandleFileInformation
327	err = syscall.GetFileInformationByHandle(h, &i)
328	if err != nil {
329		return err
330	}
331	fs.path = ""
332	fs.vol = i.VolumeSerialNumber
333	fs.idxhi = i.FileIndexHigh
334	fs.idxlo = i.FileIndexLow
335	return nil
336}
337
338// saveInfoFromPath saves full path of the file to be used by os.SameFile later,
339// and set name from path.
340func (fs *fileStat) saveInfoFromPath(path string) error {
341	fs.path = path
342	if !filepathlite.IsAbs(fs.path) {
343		var err error
344		fs.path, err = syscall.FullPath(fs.path)
345		if err != nil {
346			return &PathError{Op: "FullPath", Path: path, Err: err}
347		}
348	}
349	fs.name = filepathlite.Base(path)
350	return nil
351}
352
353func sameFile(fs1, fs2 *fileStat) bool {
354	e := fs1.loadFileId()
355	if e != nil {
356		return false
357	}
358	e = fs2.loadFileId()
359	if e != nil {
360		return false
361	}
362	return fs1.vol == fs2.vol && fs1.idxhi == fs2.idxhi && fs1.idxlo == fs2.idxlo
363}
364
365// For testing.
366func atime(fi FileInfo) time.Time {
367	return time.Unix(0, fi.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds())
368}
369