mirror of https://go.googlesource.com/go
369 lines
12 KiB
Go
369 lines
12 KiB
Go
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package os
|
|
|
|
import (
|
|
"internal/filepathlite"
|
|
"internal/godebug"
|
|
"internal/syscall/windows"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
// A fileStat is the implementation of FileInfo returned by Stat and Lstat.
|
|
type fileStat struct {
|
|
name string
|
|
|
|
// from ByHandleFileInformation, Win32FileAttributeData, Win32finddata, and GetFileInformationByHandleEx
|
|
FileAttributes uint32
|
|
CreationTime syscall.Filetime
|
|
LastAccessTime syscall.Filetime
|
|
LastWriteTime syscall.Filetime
|
|
FileSizeHigh uint32
|
|
FileSizeLow uint32
|
|
|
|
// from Win32finddata and GetFileInformationByHandleEx
|
|
ReparseTag uint32
|
|
|
|
// what syscall.GetFileType returns
|
|
filetype uint32
|
|
|
|
// used to implement SameFile
|
|
sync.Mutex
|
|
path string
|
|
vol uint32
|
|
idxhi uint32
|
|
idxlo uint32
|
|
appendNameToPath bool
|
|
}
|
|
|
|
// newFileStatFromGetFileInformationByHandle calls GetFileInformationByHandle
|
|
// to gather all required information about the file handle h.
|
|
func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (fs *fileStat, err error) {
|
|
var d syscall.ByHandleFileInformation
|
|
err = syscall.GetFileInformationByHandle(h, &d)
|
|
if err != nil {
|
|
return nil, &PathError{Op: "GetFileInformationByHandle", Path: path, Err: err}
|
|
}
|
|
|
|
var reparseTag uint32
|
|
if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
|
var ti windows.FILE_ATTRIBUTE_TAG_INFO
|
|
err = windows.GetFileInformationByHandleEx(h, windows.FileAttributeTagInfo, (*byte)(unsafe.Pointer(&ti)), uint32(unsafe.Sizeof(ti)))
|
|
if err != nil {
|
|
return nil, &PathError{Op: "GetFileInformationByHandleEx", Path: path, Err: err}
|
|
}
|
|
reparseTag = ti.ReparseTag
|
|
}
|
|
|
|
return &fileStat{
|
|
name: filepathlite.Base(path),
|
|
FileAttributes: d.FileAttributes,
|
|
CreationTime: d.CreationTime,
|
|
LastAccessTime: d.LastAccessTime,
|
|
LastWriteTime: d.LastWriteTime,
|
|
FileSizeHigh: d.FileSizeHigh,
|
|
FileSizeLow: d.FileSizeLow,
|
|
vol: d.VolumeSerialNumber,
|
|
idxhi: d.FileIndexHigh,
|
|
idxlo: d.FileIndexLow,
|
|
ReparseTag: reparseTag,
|
|
// fileStat.path is used by os.SameFile to decide if it needs
|
|
// to fetch vol, idxhi and idxlo. But these are already set,
|
|
// so set fileStat.path to "" to prevent os.SameFile doing it again.
|
|
}, nil
|
|
}
|
|
|
|
// newFileStatFromWin32FileAttributeData copies all required information
|
|
// from syscall.Win32FileAttributeData d into the newly created fileStat.
|
|
func newFileStatFromWin32FileAttributeData(d *syscall.Win32FileAttributeData) *fileStat {
|
|
return &fileStat{
|
|
FileAttributes: d.FileAttributes,
|
|
CreationTime: d.CreationTime,
|
|
LastAccessTime: d.LastAccessTime,
|
|
LastWriteTime: d.LastWriteTime,
|
|
FileSizeHigh: d.FileSizeHigh,
|
|
FileSizeLow: d.FileSizeLow,
|
|
}
|
|
}
|
|
|
|
// newFileStatFromFileIDBothDirInfo copies all required information
|
|
// from windows.FILE_ID_BOTH_DIR_INFO d into the newly created fileStat.
|
|
func newFileStatFromFileIDBothDirInfo(d *windows.FILE_ID_BOTH_DIR_INFO) *fileStat {
|
|
// The FILE_ID_BOTH_DIR_INFO MSDN documentations isn't completely correct.
|
|
// FileAttributes can contain any file attributes that is currently set on the file,
|
|
// not just the ones documented.
|
|
// EaSize contains the reparse tag if the file is a reparse point.
|
|
return &fileStat{
|
|
FileAttributes: d.FileAttributes,
|
|
CreationTime: d.CreationTime,
|
|
LastAccessTime: d.LastAccessTime,
|
|
LastWriteTime: d.LastWriteTime,
|
|
FileSizeHigh: uint32(d.EndOfFile >> 32),
|
|
FileSizeLow: uint32(d.EndOfFile),
|
|
ReparseTag: d.EaSize,
|
|
idxhi: uint32(d.FileID >> 32),
|
|
idxlo: uint32(d.FileID),
|
|
}
|
|
}
|
|
|
|
// newFileStatFromFileFullDirInfo copies all required information
|
|
// from windows.FILE_FULL_DIR_INFO d into the newly created fileStat.
|
|
func newFileStatFromFileFullDirInfo(d *windows.FILE_FULL_DIR_INFO) *fileStat {
|
|
return &fileStat{
|
|
FileAttributes: d.FileAttributes,
|
|
CreationTime: d.CreationTime,
|
|
LastAccessTime: d.LastAccessTime,
|
|
LastWriteTime: d.LastWriteTime,
|
|
FileSizeHigh: uint32(d.EndOfFile >> 32),
|
|
FileSizeLow: uint32(d.EndOfFile),
|
|
ReparseTag: d.EaSize,
|
|
}
|
|
}
|
|
|
|
// newFileStatFromWin32finddata copies all required information
|
|
// from syscall.Win32finddata d into the newly created fileStat.
|
|
func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
|
|
fs := &fileStat{
|
|
FileAttributes: d.FileAttributes,
|
|
CreationTime: d.CreationTime,
|
|
LastAccessTime: d.LastAccessTime,
|
|
LastWriteTime: d.LastWriteTime,
|
|
FileSizeHigh: d.FileSizeHigh,
|
|
FileSizeLow: d.FileSizeLow,
|
|
}
|
|
if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
|
// Per https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw:
|
|
// “If the dwFileAttributes member includes the FILE_ATTRIBUTE_REPARSE_POINT
|
|
// attribute, this member specifies the reparse point tag. Otherwise, this
|
|
// value is undefined and should not be used.”
|
|
fs.ReparseTag = d.Reserved0
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// isReparseTagNameSurrogate determines whether a tag's associated
|
|
// reparse point is a surrogate for another named entity (for example, a mounted folder).
|
|
//
|
|
// See https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-isreparsetagnamesurrogate
|
|
// and https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags.
|
|
func (fs *fileStat) isReparseTagNameSurrogate() bool {
|
|
// True for IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT.
|
|
return fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && fs.ReparseTag&0x20000000 != 0
|
|
}
|
|
|
|
func (fs *fileStat) Size() int64 {
|
|
return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow)
|
|
}
|
|
|
|
var winsymlink = godebug.New("winsymlink")
|
|
|
|
func (fs *fileStat) Mode() FileMode {
|
|
m := fs.mode()
|
|
if winsymlink.Value() == "0" {
|
|
old := fs.modePreGo1_23()
|
|
if old != m {
|
|
winsymlink.IncNonDefault()
|
|
m = old
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (fs *fileStat) mode() (m FileMode) {
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
|
|
m |= 0444
|
|
} else {
|
|
m |= 0666
|
|
}
|
|
|
|
// Windows reports the FILE_ATTRIBUTE_DIRECTORY bit for reparse points
|
|
// that refer to directories, such as symlinks and mount points.
|
|
// However, we follow symlink POSIX semantics and do not set the mode bits.
|
|
// This allows users to walk directories without following links
|
|
// by just calling "fi, err := os.Lstat(name); err == nil && fi.IsDir()".
|
|
// Note that POSIX only defines the semantics for symlinks, not for
|
|
// mount points or other surrogate reparse points, but we treat them
|
|
// the same way for consistency. Also, mount points can contain infinite
|
|
// loops, so it is not safe to walk them without special handling.
|
|
if !fs.isReparseTagNameSurrogate() {
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
|
m |= ModeDir | 0111
|
|
}
|
|
|
|
switch fs.filetype {
|
|
case syscall.FILE_TYPE_PIPE:
|
|
m |= ModeNamedPipe
|
|
case syscall.FILE_TYPE_CHAR:
|
|
m |= ModeDevice | ModeCharDevice
|
|
}
|
|
}
|
|
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
|
switch fs.ReparseTag {
|
|
case syscall.IO_REPARSE_TAG_SYMLINK:
|
|
m |= ModeSymlink
|
|
case windows.IO_REPARSE_TAG_AF_UNIX:
|
|
m |= ModeSocket
|
|
case windows.IO_REPARSE_TAG_DEDUP:
|
|
// If the Data Deduplication service is enabled on Windows Server, its
|
|
// Optimization job may convert regular files to IO_REPARSE_TAG_DEDUP
|
|
// whenever that job runs.
|
|
//
|
|
// However, DEDUP reparse points remain similar in most respects to
|
|
// regular files: they continue to support random-access reads and writes
|
|
// of persistent data, and they shouldn't add unexpected latency or
|
|
// unavailability in the way that a network filesystem might.
|
|
//
|
|
// Go programs may use ModeIrregular to filter out unusual files (such as
|
|
// raw device files on Linux, POSIX FIFO special files, and so on), so
|
|
// to avoid files changing unpredictably from regular to irregular we will
|
|
// consider DEDUP files to be close enough to regular to treat as such.
|
|
default:
|
|
m |= ModeIrregular
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// modePreGo1_23 returns the FileMode for the fileStat, using the pre-Go 1.23
|
|
// logic for determining the file mode.
|
|
// The logic is subtle and not well-documented, so it is better to keep it
|
|
// separate from the new logic.
|
|
func (fs *fileStat) modePreGo1_23() (m FileMode) {
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
|
|
m |= 0444
|
|
} else {
|
|
m |= 0666
|
|
}
|
|
if fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK ||
|
|
fs.ReparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT {
|
|
return m | ModeSymlink
|
|
}
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
|
m |= ModeDir | 0111
|
|
}
|
|
switch fs.filetype {
|
|
case syscall.FILE_TYPE_PIPE:
|
|
m |= ModeNamedPipe
|
|
case syscall.FILE_TYPE_CHAR:
|
|
m |= ModeDevice | ModeCharDevice
|
|
}
|
|
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
|
if fs.ReparseTag == windows.IO_REPARSE_TAG_AF_UNIX {
|
|
m |= ModeSocket
|
|
}
|
|
if m&ModeType == 0 {
|
|
if fs.ReparseTag == windows.IO_REPARSE_TAG_DEDUP {
|
|
// See comment in fs.Mode.
|
|
} else {
|
|
m |= ModeIrregular
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (fs *fileStat) ModTime() time.Time {
|
|
return time.Unix(0, fs.LastWriteTime.Nanoseconds())
|
|
}
|
|
|
|
// Sys returns syscall.Win32FileAttributeData for file fs.
|
|
func (fs *fileStat) Sys() any {
|
|
return &syscall.Win32FileAttributeData{
|
|
FileAttributes: fs.FileAttributes,
|
|
CreationTime: fs.CreationTime,
|
|
LastAccessTime: fs.LastAccessTime,
|
|
LastWriteTime: fs.LastWriteTime,
|
|
FileSizeHigh: fs.FileSizeHigh,
|
|
FileSizeLow: fs.FileSizeLow,
|
|
}
|
|
}
|
|
|
|
func (fs *fileStat) loadFileId() error {
|
|
fs.Lock()
|
|
defer fs.Unlock()
|
|
if fs.path == "" {
|
|
// already done
|
|
return nil
|
|
}
|
|
var path string
|
|
if fs.appendNameToPath {
|
|
path = fixLongPath(fs.path + `\` + fs.name)
|
|
} else {
|
|
path = fs.path
|
|
}
|
|
pathp, err := syscall.UTF16PtrFromString(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Per https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points-and-file-operations,
|
|
// “Applications that use the CreateFile function should specify the
|
|
// FILE_FLAG_OPEN_REPARSE_POINT flag when opening the file if it is a reparse
|
|
// point.”
|
|
//
|
|
// And per https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew,
|
|
// “If the file is not a reparse point, then this flag is ignored.”
|
|
//
|
|
// So we set FILE_FLAG_OPEN_REPARSE_POINT unconditionally, since we want
|
|
// information about the reparse point itself.
|
|
//
|
|
// If the file is a symlink, the symlink target should have already been
|
|
// resolved when the fileStat was created, so we don't need to worry about
|
|
// resolving symlink reparse points again here.
|
|
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT)
|
|
|
|
h, err := syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer syscall.CloseHandle(h)
|
|
var i syscall.ByHandleFileInformation
|
|
err = syscall.GetFileInformationByHandle(h, &i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.path = ""
|
|
fs.vol = i.VolumeSerialNumber
|
|
fs.idxhi = i.FileIndexHigh
|
|
fs.idxlo = i.FileIndexLow
|
|
return nil
|
|
}
|
|
|
|
// saveInfoFromPath saves full path of the file to be used by os.SameFile later,
|
|
// and set name from path.
|
|
func (fs *fileStat) saveInfoFromPath(path string) error {
|
|
fs.path = path
|
|
if !filepathlite.IsAbs(fs.path) {
|
|
var err error
|
|
fs.path, err = syscall.FullPath(fs.path)
|
|
if err != nil {
|
|
return &PathError{Op: "FullPath", Path: path, Err: err}
|
|
}
|
|
}
|
|
fs.name = filepathlite.Base(path)
|
|
return nil
|
|
}
|
|
|
|
func sameFile(fs1, fs2 *fileStat) bool {
|
|
e := fs1.loadFileId()
|
|
if e != nil {
|
|
return false
|
|
}
|
|
e = fs2.loadFileId()
|
|
if e != nil {
|
|
return false
|
|
}
|
|
return fs1.vol == fs2.vol && fs1.idxhi == fs2.idxhi && fs1.idxlo == fs2.idxlo
|
|
}
|
|
|
|
// For testing.
|
|
func atime(fi FileInfo) time.Time {
|
|
return time.Unix(0, fi.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds())
|
|
}
|