Files
cgofuse/fs/fs_windows.go

982 lines
27 KiB
Go

//go:build windows
package fs
import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"os"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/winfsp"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/winfsp/filetime"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/winfsp/pathlock"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/winfsp/procsd"
)
var (
_ winfsp.BehaviourBase = (*fileSystem[File])(nil)
_ winfsp.BehaviourRename = (*fileSystem[File])(nil)
_ winfsp.BehaviourCreate = (*fileSystem[File])(nil)
_ winfsp.BehaviourGetSecurityByName = (*fileSystem[File])(nil)
_ winfsp.BehaviourOverwrite = (*fileSystem[File])(nil)
_ winfsp.BehaviourReadDirectory = (*fileSystem[File])(nil)
_ winfsp.BehaviourGetFileInfo = (*fileSystem[File])(nil)
_ winfsp.BehaviourGetSecurity = (*fileSystem[File])(nil)
_ winfsp.BehaviourGetVolumeInfo = (*fileSystem[File])(nil)
_ winfsp.BehaviourSetVolumeLabel = (*fileSystem[File])(nil)
_ winfsp.BehaviourSetBasicInfo = (*fileSystem[File])(nil)
_ winfsp.BehaviourSetFileSize = (*fileSystem[File])(nil)
_ winfsp.BehaviourRead = (*fileSystem[File])(nil)
_ winfsp.BehaviourWrite = (*fileSystem[File])(nil)
_ winfsp.BehaviourFlush = (*fileSystem[File])(nil)
_ winfsp.BehaviourCanDelete = (*fileSystem[File])(nil)
_ winfsp.BehaviourCleanup = (*fileSystem[File])(nil)
)
type _FileSystemMount[T File, F FileSystem[T]] struct {
target string
winfsp *winfsp.FileSystem
opts []winfsp.Option
donner <-chan struct{}
fs F
}
func (fs _FileSystemMount[_, F]) FS() F { return fs.fs }
func (fs _FileSystemMount[_, F]) Done() {
defer func() { recover() }()
if fs.donner == nil {
return
}
_, _ = <-fs.donner
}
func (fs _FileSystemMount[T, F]) Mount(ctx context.Context) (err error) {
// Remove folder or file if exists
if _, err := os.Stat(fs.target); err == nil {
_ = os.Remove(fs.target)
}
doneChan := make(chan struct{})
if fs.winfsp, err = winfsp.Mount(ctx, &fileSystem[T]{inner: fs.fs, Donner: doneChan}, fs.target, fs.opts...); err == nil {
if v, ok := any(fs.FS()).(FileSystemInitDestroy); ok {
v.Init()
}
}
return
}
func (fs _FileSystemMount[T, F]) SetOption(args ...any) {
if len(args) >= 1 {
if v, ok := args[0].(winfsp.Option); ok {
fs.opts = append(fs.opts, v)
} else if v, ok := args[0].(string); ok {
switch v {
case "CaseSensitive":
if len(args) >= 2 {
if v, ok := args[1].(bool); ok {
fs.opts = append(fs.opts, winfsp.CaseSensitive(v))
} else {
fs.opts = append(fs.opts, winfsp.CaseSensitive(true))
}
} else {
fs.opts = append(fs.opts, winfsp.CaseSensitive(true))
}
case "PassPattern":
if len(args) >= 2 {
if v, ok := args[1].(bool); ok {
fs.opts = append(fs.opts, winfsp.PassPattern(v))
} else {
fs.opts = append(fs.opts, winfsp.PassPattern(true))
}
} else {
fs.opts = append(fs.opts, winfsp.PassPattern(true))
}
case "CreationTime":
if len(args) >= 2 {
if v, ok := args[1].(time.Time); ok {
fs.opts = append(fs.opts, winfsp.CreationTime(v))
} else {
fs.opts = append(fs.opts, winfsp.CreationTime(time.Now()))
}
} else {
fs.opts = append(fs.opts, winfsp.CreationTime(time.Now()))
}
case "FileSystemName":
if len(args) >= 2 {
if v, ok := args[1].(string); ok {
fs.opts = append(fs.opts, winfsp.FileSystemName(v))
}
}
case "VolumePrefix":
if len(args) >= 2 {
if v, ok := args[1].(string); ok {
fs.opts = append(fs.opts, winfsp.VolumePrefix(v))
}
}
}
}
}
}
// Mount winfsp
func New[T File](target string, fs FileSystem[T]) FileSystemMount[T, FileSystem[T]] {
return &_FileSystemMount[T, FileSystem[T]]{
target: target,
fs: fs,
opts: []winfsp.Option{},
}
}
type fileHandle[F File] struct {
lock *pathlock.Lock
dir winfsp.DirBuffer
file File
flags int
mtx sync.RWMutex
evaluatedIndex uint64
}
type fileSystem[F File] struct {
inner FileSystem[F]
handles sync.Map
locker pathlock.PathLocker
Donner chan<- struct{}
labelLen int
label [32]uint16
}
func (fs *fileSystem[F]) Done() {
defer func() { recover() }()
if fs.Donner != nil {
close(fs.Donner)
}
}
func (handle *fileHandle[F]) reopenFile(fs *fileSystem[F]) (File, error) {
return fs.inner.OpenFile(handle.lock.FilePath(), handle.flags, os.FileMode(0))
}
func attributesFromFileMode(mode os.FileMode) uint32 {
var attributes uint32
if mode.IsDir() {
attributes |= windows.FILE_ATTRIBUTE_DIRECTORY
}
if (uint32(mode.Perm()) & 0200) == 0 {
attributes |= windows.FILE_ATTRIBUTE_READONLY
}
if attributes == 0 {
attributes = windows.FILE_ATTRIBUTE_NORMAL
}
return attributes
}
func (fs *fileSystem[F]) GetSecurityByName(ref *winfsp.FileSystemRef, name string, flags winfsp.GetSecurityByNameFlags) (uint32, *windows.SECURITY_DESCRIPTOR, error) {
info, err := fs.inner.Stat(name)
if err != nil || flags == winfsp.GetExistenceOnly {
return 0, nil, err
}
attributes := attributesFromFileMode(info.Mode())
var sd *windows.SECURITY_DESCRIPTOR
if (flags & winfsp.GetSecurityByName) != 0 {
// XXX: this is a mock up, the file is considered to
// be owned by current process, so it is okay to
// return the security descriptor of the process.
sd, err = procsd.Load()
}
return attributes, sd, err
}
func evaluateIndexNumber(p string) uint64 {
// XXX: we evaluate the index number for a file by hashing,
// so each file is identified by its path. Since we will not
// support open by file ID in this scenario, it is okay to
// simply map a path to its hash value.
//
// And we caches the index number right at file creation,
// the index number will only be available while stating an
// open file, not on reading directories.
data := sha256.Sum256([]byte(p))
a := binary.BigEndian.Uint64(data[0:8])
b := binary.BigEndian.Uint64(data[8:16])
c := binary.BigEndian.Uint64(data[16:24])
d := binary.BigEndian.Uint64(data[24:32])
return a ^ b ^ c ^ d
}
func dirInfoFromStat(target *winfsp.FSP_FSCTL_FILE_INFO, source os.DirEntry, evaluatedIndexNumber uint64) {
if s, err := source.Info(); err == nil {
fileInfoFromStat(target, s, evaluatedIndexNumber)
}
}
func fileInfoFromStat(target *winfsp.FSP_FSCTL_FILE_INFO, source os.FileInfo, evaluatedIndexNumber uint64) {
target.FileAttributes = attributesFromFileMode(source.Mode())
target.ReparseTag = 0
target.FileSize = uint64(source.Size())
target.AllocationSize = ((target.FileSize + 4095) / 4096) * 4096
target.CreationTime = filetime.Timestamp(source.ModTime())
target.LastAccessTime = target.CreationTime
target.LastWriteTime = target.CreationTime
target.ChangeTime = target.LastWriteTime
target.IndexNumber = evaluatedIndexNumber
target.HardLinks = 0
target.EaSize = 0
// We can extract more data from it if it is find data from
// windows, which is the one from golang's standard library.
sys := source.Sys()
if sys == nil {
return
}
findData, ok := sys.(*syscall.Win32FileAttributeData)
if !ok {
return
}
target.CreationTime = filetime.Filetime(findData.CreationTime)
target.LastAccessTime = filetime.Filetime(findData.LastAccessTime)
target.LastWriteTime = filetime.Filetime(findData.LastWriteTime)
target.ChangeTime = target.LastWriteTime
}
const (
// unsupportedCreateOptions are the options that are not
// supported by the file system driver.
//
// There're many of them, but it is good to eliminate
// behaviours that might violates the intention of the
// caller processes and maintain the integrity of the
// inner file system.
unsupportedCreateOptions = windows.FILE_WRITE_THROUGH |
windows.FILE_CREATE_TREE_CONNECTION |
windows.FILE_NO_EA_KNOWLEDGE |
windows.FILE_OPEN_BY_FILE_ID |
windows.FILE_RESERVE_OPFILTER |
windows.FILE_OPEN_REQUIRING_OPLOCK |
windows.FILE_COMPLETE_IF_OPLOCKED |
windows.FILE_OPEN_NO_RECALL
// bothDirectoryFlags are the flags of directory or-ing
// the non directory flags. If both flags are set, this
// is obsolutely an invalid flag, you know.
bothDirectoryFlags = windows.FILE_DIRECTORY_FILE |
windows.FILE_NON_DIRECTORY_FILE
)
func (fs *fileSystem[F]) openFile(_ *winfsp.FileSystemRef, name string, createOptions, grantedAccess uint32, mode os.FileMode, info *winfsp.FSP_FSCTL_FILE_INFO) (uintptr, error) {
if createOptions&unsupportedCreateOptions != 0 {
return 0, windows.STATUS_INVALID_PARAMETER
}
if createOptions&bothDirectoryFlags == bothDirectoryFlags {
return 0, windows.STATUS_INVALID_PARAMETER
}
// Determine the current access flag for writer here.
flags := 0
accessFlags := 0
readAccess := grantedAccess & windows.FILE_READ_DATA
writeAccess := grantedAccess &
(windows.FILE_WRITE_DATA | windows.FILE_APPEND_DATA)
switch {
case readAccess == 0 && writeAccess == 0:
case readAccess != 0 && writeAccess == 0:
accessFlags = os.O_RDONLY
case readAccess == 0 && writeAccess != 0:
accessFlags = os.O_WRONLY
case readAccess != 0 && writeAccess != 0:
accessFlags = os.O_RDWR
}
if writeAccess == windows.FILE_APPEND_DATA {
flags |= os.O_APPEND
}
// Determine the creation mode for writer here.
//
// TODO: I've not studied the dispositions here carefully
// so the actual behaviour might be bizarre, and it would
// be helpful of you to correct them.
disposition := (createOptions >> 24) & 0x0ff
switch disposition {
case windows.FILE_SUPERSEDE:
// XXX: FILE_SUPERSEDE means to remove the file on disk
// and then replace it by our file, we don't support
// removing file while there's open file handles. But
// it can still be open when it is the only one to open
// the specified file.
flags |= os.O_CREATE | os.O_TRUNC
case windows.FILE_CREATE:
flags |= os.O_CREATE | os.O_EXCL
case windows.FILE_OPEN:
case windows.FILE_OPEN_IF:
flags |= os.O_CREATE
case windows.FILE_OVERWRITE:
flags |= os.O_TRUNC
case windows.FILE_OVERWRITE_IF:
flags |= os.O_CREATE | os.O_TRUNC
default:
return 0, windows.STATUS_INVALID_PARAMETER
}
// Lock the file with desired mode.
lockFunc := fs.locker.RLock
if (createOptions&windows.FILE_DELETE_ON_CLOSE != 0) ||
(grantedAccess&windows.DELETE != 0) ||
(disposition == windows.FILE_SUPERSEDE) {
lockFunc = fs.locker.Lock
}
lock := lockFunc(name)
if lock == nil {
return 0, windows.STATUS_SHARING_VIOLATION
}
created := false
defer func() {
if !created {
lock.Unlock()
}
}()
// Attempt to allocate the file handle.
handle := &fileHandle[F]{lock: lock}
handleAddr := uintptr(unsafe.Pointer(handle))
_, loaded := fs.handles.LoadOrStore(handleAddr, handle)
if loaded {
return 0, windows.ERROR_NOT_ENOUGH_MEMORY
}
defer func() {
if !created {
fs.handles.Delete(handleAddr)
}
}()
// Normalize the path to ensure identity of operation.
name = lock.FilePath()
// See if we are asked to create directories here.
if (createOptions&windows.FILE_DIRECTORY_FILE != 0) &&
(flags&os.O_CREATE != 0) {
if flags&os.O_TRUNC != 0 {
return 0, windows.STATUS_INVALID_PARAMETER
}
mode |= os.FileMode(0111)
if err := fs.inner.Mkdir(name, mode); err != nil {
if os.IsExist(err) ||
errors.Is(err, windows.STATUS_OBJECT_NAME_COLLISION) {
err = windows.STATUS_OBJECT_NAME_COLLISION
if flags&os.O_EXCL == 0 {
err = nil
}
}
if err != nil {
return 0, err
}
}
// Clear the flags since the create directory has
// already been handled properly above.
flags = 0
mode = os.FileMode(0)
accessFlags = os.O_RDONLY
}
// Attempt to open the file in the underlying file system.
dirCheckErr := windows.STATUS_NOT_A_DIRECTORY
file, err := fs.inner.OpenFile(name, accessFlags|flags, mode)
if err != nil {
// We will only try again if it complains about opening a
// directory file failed, but we should be able to open the
// directory with POSIX compatible flags.
//
// We will perform extra check to ensure we have really
// opened a directory rather than been entangled in some
// TOCTOU scenario.
//
// XXX: The O_RDONLY, O_WRONLY and O_APPEND flags (or their
// preimages FILE_LIST_DIRECTORY, FILE_ADD_FILE and
// FILE_ADD_SUBDIRECTORY) are not mandatory. All these
// operations are retranslated into POSIX style operations.
if (createOptions&bothDirectoryFlags !=
windows.FILE_NON_DIRECTORY_FILE) &&
(errors.Is(err, syscall.EISDIR) ||
errors.Is(err, windows.STATUS_FILE_IS_A_DIRECTORY) ||
errors.Is(err, windows.ERROR_DIRECTORY)) {
accessFlags = os.O_RDONLY
flags = 0
file, err = fs.inner.OpenFile(name, accessFlags|flags, mode)
createOptions |= windows.FILE_DIRECTORY_FILE
dirCheckErr = windows.STATUS_OBJECT_NAME_NOT_FOUND
}
if err != nil {
return 0, err
}
}
defer func() {
if !created {
_ = file.Close()
}
}()
handle.file = file
handle.flags = accessFlags | (flags & os.O_APPEND)
// Judge whether this is the stuff we would like to open.
fileInfo, err := file.Stat()
if err != nil {
return 0, err
}
switch createOptions & bothDirectoryFlags {
case windows.FILE_DIRECTORY_FILE:
if !fileInfo.IsDir() {
return 0, dirCheckErr
}
case windows.FILE_NON_DIRECTORY_FILE:
if fileInfo.IsDir() {
return 0, windows.STATUS_FILE_IS_A_DIRECTORY
}
default:
}
// Downgrade the lock to reader lock if it is the file
// to supersede, and other processes can access it with
// such flag from now on.
if disposition == windows.FILE_SUPERSEDE {
lock.Downgrade()
}
// Evaluate the file index for the file and cache it.
handle.evaluatedIndex = evaluateIndexNumber(lock.Path())
// Copy the status out to the file information block.
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
// Finish opening the file and return to the caller.
created = true
return handleAddr, nil
}
func (fs *fileSystem[F]) Create(ref *winfsp.FileSystemRef, name string, createOptions, grantedAccess, fileAttributes uint32, securityDescriptor *windows.SECURITY_DESCRIPTOR, allocationSize uint64, info *winfsp.FSP_FSCTL_FILE_INFO) (uintptr, error) {
fileMode := os.FileMode(0444)
if fileAttributes&windows.FILE_ATTRIBUTE_READONLY == 0 {
fileMode |= os.FileMode(0666)
}
if fileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
fileMode |= os.FileMode(0111)
}
return fs.openFile(ref, name, createOptions, grantedAccess, fileMode, info)
}
func (fs *fileSystem[F]) Open(ref *winfsp.FileSystemRef, name string, createOptions, grantedAccess uint32, info *winfsp.FSP_FSCTL_FILE_INFO) (uintptr, error) {
return fs.openFile(ref, name, createOptions, grantedAccess, os.FileMode(0), info)
}
func (fs *fileSystem[F]) load(file uintptr) (*fileHandle[F], error) {
obj, ok := fs.handles.Load(file)
if !ok {
return nil, windows.STATUS_INVALID_HANDLE
}
return obj.(*fileHandle[F]), nil
}
func (fs *fileSystem[F]) Close(ref *winfsp.FileSystemRef, file uintptr) {
object, ok := fs.handles.LoadAndDelete(file)
if !ok {
return
}
fileHandle := object.(*fileHandle[F])
fileHandle.mtx.Lock()
defer fileHandle.mtx.Unlock()
defer fileHandle.lock.Unlock()
defer fileHandle.dir.Delete()
if fileHandle.file != nil {
_ = fileHandle.file.Close()
fileHandle.file = nil
}
}
func (handle *fileHandle[_]) lockChecked() error {
handle.mtx.RLock()
valid := false
defer func() {
if !valid {
handle.mtx.RUnlock()
}
}()
if handle.file == nil {
return windows.STATUS_INVALID_HANDLE
}
valid = true
return nil
}
func (handle *fileHandle[_]) unlockChecked() {
handle.mtx.RUnlock()
}
func (fs *fileSystem[F]) Overwrite(ref *winfsp.FileSystemRef, file uintptr, attributes uint32, replaceAttributes bool, allocationSize uint64, info *winfsp.FSP_FSCTL_FILE_INFO) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
if err := handle.file.Truncate(0); err != nil {
return err
}
// TODO: support chmod operation in the future.
//
// It might seems like we are just ignoring the attribute
// update but we might support them in the future.
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
return nil
}
func (fs *fileSystem[F]) GetOrNewDirBuffer(ref *winfsp.FileSystemRef, file uintptr) (*winfsp.DirBuffer, error) {
fileHandle, err := fs.load(file)
if err != nil {
return nil, err
}
return &fileHandle.dir, nil
}
func (fs *fileSystem[F]) ReadDirectory(ref *winfsp.FileSystemRef, file uintptr, pattern string, fill func(string, *winfsp.FSP_FSCTL_FILE_INFO) (bool, error)) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
f, err := handle.reopenFile(fs)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
fileInfos, err := f.ReadDir(-1)
if err != nil {
return err
}
for _, fileInfo := range fileInfos {
var info winfsp.FSP_FSCTL_FILE_INFO
dirInfoFromStat(&info, fileInfo, 0)
ok, err := fill(fileInfo.Name(), &info)
if err != nil || !ok {
return err
}
}
return nil
}
func (fs *fileSystem[F]) GetFileInfo(ref *winfsp.FileSystemRef, file uintptr, info *winfsp.FSP_FSCTL_FILE_INFO) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
return nil
}
func (fs *fileSystem[F]) GetSecurity(ref *winfsp.FileSystemRef, file uintptr) (*windows.SECURITY_DESCRIPTOR, error) {
_, err := fs.load(file)
if err != nil {
return nil, err
}
return procsd.Load()
}
func (fs *fileSystem[F]) GetVolumeInfo(ref *winfsp.FileSystemRef, info *winfsp.FSP_FSCTL_VOLUME_INFO) error {
// TODO: support file system remaining size query.
info.TotalSize = 8 * 1024 * 1024 * 1024 * 1024 // 8TB
info.FreeSize = info.TotalSize
length := fs.labelLen
info.VolumeLabelLength = 2 * uint16(copy(
info.VolumeLabel[:length], fs.label[:length]))
return nil
}
func (fs *fileSystem[F]) SetVolumeLabel(ref *winfsp.FileSystemRef, label string, info *winfsp.FSP_FSCTL_VOLUME_INFO) error {
utf16, err := windows.UTF16FromString(label)
if err != nil {
return err
}
fs.labelLen = copy(fs.label[:], utf16)
return fs.GetVolumeInfo(ref, info)
}
func (fs *fileSystem[F]) SetBasicInfo(ref *winfsp.FileSystemRef, file uintptr, flags winfsp.SetBasicInfoFlags, attribute uint32, creationTime, lastAccessTime, lastWriteTime, changeTime uint64, info *winfsp.FSP_FSCTL_FILE_INFO) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
return windows.STATUS_ACCESS_DENIED
}
// FileTruncateEx is the truncate interface related to Windows
// style opertations. Without this interface, we will be
// imitating the set allocation size behaviour of file, making
// it behaves stragely under certain racing circumstances.
type FileTruncateEx interface {
File
// Shrink means it will not expand the file size if a size
// greater than the file size is passed.
Shrink(newSize int64) error
}
type fileMimicTruncate struct {
File
}
func (f *fileMimicTruncate) Shrink(newSize int64) error {
fileInfo, err := f.Stat()
if err != nil {
return err
}
if fileInfo.Size() > newSize {
return f.Truncate(newSize)
}
return nil
}
func (fs *fileSystem[F]) SetFileSize(ref *winfsp.FileSystemRef, file uintptr, newSize uint64, setAllocationSize bool, info *winfsp.FSP_FSCTL_FILE_INFO) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
size := int64(newSize)
if setAllocationSize {
var shrinker FileTruncateEx
if obj, ok := handle.file.(FileTruncateEx); ok {
shrinker = obj
} else {
shrinker = &fileMimicTruncate{
File: handle.file,
}
}
if err := shrinker.Shrink(size); err != nil {
return err
}
} else {
if err := handle.file.Truncate(size); err != nil {
return err
}
}
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
return nil
}
func (fs *fileSystem[F]) Read(ref *winfsp.FileSystemRef, file uintptr, buf []byte, offset uint64) (int, error) {
handle, err := fs.load(file)
if err != nil {
return 0, err
}
if err := handle.lockChecked(); err != nil {
return 0, err
}
defer handle.unlockChecked()
// No matter random access or append only file handle
// on windows should support random read.
return handle.file.ReadAt(buf, int64(offset))
}
// FileWriteEx is the write interface related to Windows style
// writing. Without this interface, we will be imitating the
// write behaviour of file, making it behaves strangely under
// certain racing circumstances.
type FileWriteEx interface {
File
// Append means the data will always be written to the
// tail of the file, regardless of the file's current
// open mode.
Append([]byte) (int, error)
// ConstrainedWriteAt means the data will be written at
// specified offset and the data within the file's size
// range will be copied out.
ConstrainedWriteAt([]byte, int64) (int, error)
}
type fileMimicWrite struct {
File
flags int
}
func (f *fileMimicWrite) Append(b []byte) (int, error) {
if f.flags&os.O_APPEND != 0 {
return f.Write(b)
} else {
// BUG: since we imitates the append behaviour
// by fetching the file size first and then
// appending to it, two concurrent append
// operations will overlaps with each other.
fileInfo, err := f.Stat()
if err != nil {
return 0, err
}
return f.WriteAt(b, fileInfo.Size())
}
}
func (f *fileMimicWrite) ConstrainedWriteAt(b []byte, offset int64) (int, error) {
// BUG: this is also a buggy part when two
// concurrent write operation happens. You
// might expect the reordering of constrained
// write operation and an boundary extending
// operation.
fileInfo, err := f.Stat()
if err != nil {
return 0, err
}
size := fileInfo.Size()
if offset >= size {
return 0, nil
} else if offset+int64(len(b)) >= size {
b = b[:len(b)+int(size-offset)]
}
return f.WriteAt(b, offset)
}
func (fs *fileSystem[F]) Write(ref *winfsp.FileSystemRef, file uintptr, b []byte, offset uint64, writeToEndOfFile, constrainedIo bool, info *winfsp.FSP_FSCTL_FILE_INFO) (int, error) {
handle, err := fs.load(file)
if err != nil {
return 0, err
}
if (handle.flags&os.O_APPEND != 0) && !writeToEndOfFile {
// You may not write to an append-only file.
return 0, windows.STATUS_ACCESS_DENIED
}
if err := handle.lockChecked(); err != nil {
return 0, err
}
defer handle.unlockChecked()
var writer FileWriteEx
if obj, ok := handle.file.(FileWriteEx); ok {
writer = obj
} else {
writer = &fileMimicWrite{
File: handle.file,
flags: handle.flags,
}
}
var n int
if writeToEndOfFile && constrainedIo {
// Nothing to do here.
} else if writeToEndOfFile {
n, err = writer.Append(b)
} else if constrainedIo {
n, err = writer.ConstrainedWriteAt(b, int64(offset))
} else {
n, err = handle.file.WriteAt(b, int64(offset))
}
fileInfo, statErr := handle.file.Stat()
if statErr != nil && err == nil {
err = statErr
}
if fileInfo != nil {
// XXX: since the driver code just take the information
// field for notification and display purpose, so only
// the lastly updated information is required.
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
}
return n, err
}
func (fs *fileSystem[F]) Flush(ref *winfsp.FileSystemRef, file uintptr, info *winfsp.FSP_FSCTL_FILE_INFO) error {
if file == 0 {
// Flush the whole filesystem, not a single file.
return nil
}
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
if err := handle.file.Sync(); err != nil {
return err
}
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
fileInfoFromStat(info, fileInfo, handle.evaluatedIndex)
return nil
}
func (fs *fileSystem[F]) CanDelete(ref *winfsp.FileSystemRef, file uintptr, name string) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if err := handle.lockChecked(); err != nil {
return err
}
defer handle.unlockChecked()
if !handle.lock.IsWrite() {
return windows.STATUS_ACCESS_DENIED
}
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
if !fileInfo.IsDir() {
return nil
}
f, err := handle.reopenFile(fs)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
fileInfos, err := f.ReadDir(-1)
if err != nil {
return err
}
if len(fileInfos) > 0 {
return windows.STATUS_DIRECTORY_NOT_EMPTY
}
return nil
}
func (fs *fileSystem[F]) Cleanup(ref *winfsp.FileSystemRef, file uintptr, name string, cleanupFlags uint32) {
handle, err := fs.load(file)
if err != nil {
return
}
if cleanupFlags&winfsp.FspCleanupDelete == 0 {
return
}
if !handle.lock.IsWrite() {
return
}
handle.mtx.Lock()
defer handle.mtx.Unlock()
if handle.file == nil {
return
}
_ = handle.file.Close()
handle.file = nil
_ = fs.inner.Remove(handle.lock.FilePath())
}
func (fs *fileSystem[F]) Rename(ref *winfsp.FileSystemRef, file uintptr, source, target string, replaceIfExist bool) error {
handle, err := fs.load(file)
if err != nil {
return err
}
if !handle.lock.IsWrite() {
return windows.STATUS_ACCESS_DENIED
}
handle.mtx.Lock()
defer handle.mtx.Unlock()
if handle.file == nil {
return windows.STATUS_INVALID_HANDLE
}
// Try to grab the target path's lock. And upon exit
// either the source or the target lock will be released.
newLock := fs.locker.Lock(target)
if newLock == nil {
return windows.STATUS_SHARING_VIOLATION
}
target = newLock.FilePath()
defer func() { newLock.Unlock() }()
// Check for the rename precondition so that we could
// avoid performing sophiscated operations.
if !replaceIfExist {
fileInfo, err := fs.inner.Stat(target)
if err != nil && !os.IsNotExist(err) &&
!errors.Is(err, windows.STATUS_OBJECT_NAME_NOT_FOUND) {
return err
}
if fileInfo != nil {
return windows.STATUS_OBJECT_NAME_COLLISION
}
}
// After exit, the remaining file will be reopened and
// seek to its orignal offset, so that we can continue
// our operations.
fileInfo, err := handle.file.Stat()
if err != nil {
return err
}
var pos *int64
if fileInfo.Mode().IsRegular() {
value, err := handle.file.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
pos = new(int64)
*pos = value
}
_ = handle.file.Close()
handle.file = nil
defer func() {
f, err := handle.reopenFile(fs)
if err != nil {
return
}
defer func() {
if f != nil {
_ = f.Close()
}
}()
if pos != nil {
if _, err := f.Seek(*pos, io.SeekStart); err != nil {
return
}
}
handle.file, f = f, nil
}()
// Attempt to perform the rename operation now.
source = handle.lock.FilePath()
if err := fs.inner.Rename(source, target); err != nil {
return err
}
handle.lock, newLock = newLock, handle.lock
return nil
}