// 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())
}
