Files
cgofuse/fs/fs_windows.go
Matheus Sampaio Queiroga 9a774478a2
All checks were successful
Fuse test / go-test (push) Successful in 32s
Merge winfsp to fuse and update fs
Signed-off-by: Matheus Sampaio Queiroga <srherobrine20@gmail.com>
2025-08-18 19:07:05 -03:00

971 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/fuse"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/filetime"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/pathlock"
"sirherobrine23.com.br/Sirherobrine23/cgofuse/procsd"
)
var (
_ fuse.BehaviourBase = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourRename = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourCreate = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourGetSecurityByName = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourOverwrite = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourReadDirectory = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourGetFileInfo = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourGetSecurity = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourGetVolumeInfo = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourSetVolumeLabel = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourSetBasicInfo = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourSetFileSize = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourRead = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourWrite = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourFlush = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourCanDelete = (*WinfspFs[File, FileSystem[File]])(nil)
_ fuse.BehaviourCleanup = (*WinfspFs[File, FileSystem[File]])(nil)
)
// Mount winfsp
func New[T File](target string, fs FileSystem[T]) FileSystemMount[T, FileSystem[T]] {
return &WinfspFs[T, FileSystem[T]]{
Target: target,
Inner: fs,
opts: []fuse.Option{},
}
}
type fileHandle[F File, T FileSystem[F]] struct {
lock *pathlock.Lock
dir fuse.DirBuffer
file File
flags int
mtx sync.RWMutex
evaluatedIndex uint64
}
type WinfspFs[F File, T FileSystem[F]] struct {
Donner chan struct{}
Target string
Winfsp *fuse.FileSystem
opts []fuse.Option
Inner T
handles sync.Map
locker pathlock.PathLocker
labelLen int
label [32]uint16
}
func (fs *WinfspFs[F, T]) Done() {
defer func() { recover() }()
if fs.Donner == nil {
return
}
<-fs.Donner
}
func (fs *WinfspFs[F, T]) Mount(ctx context.Context) (err error) {
// Remove folder or file if exists
if _, err := os.Stat(fs.Target); err == nil {
_ = os.Remove(fs.Target)
}
fs.Donner = make(chan struct{})
if fs.Winfsp, err = fuse.Mount(ctx, fs, fs.Target, fs.opts...); err == nil {
if v, ok := any(fs.Inner).(FileSystemInitDestroy); ok {
v.Init()
}
}
return
}
func (fs *WinfspFs[F, T]) SetOption(args ...any) {
if len(args) >= 1 {
if v, ok := args[0].(fuse.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, fuse.CaseSensitive(v))
} else {
fs.opts = append(fs.opts, fuse.CaseSensitive(true))
}
} else {
fs.opts = append(fs.opts, fuse.CaseSensitive(true))
}
case "PassPattern":
if len(args) >= 2 {
if v, ok := args[1].(bool); ok {
fs.opts = append(fs.opts, fuse.PassPattern(v))
} else {
fs.opts = append(fs.opts, fuse.PassPattern(true))
}
} else {
fs.opts = append(fs.opts, fuse.PassPattern(true))
}
case "CreationTime":
if len(args) >= 2 {
if v, ok := args[1].(time.Time); ok {
fs.opts = append(fs.opts, fuse.CreationTime(v))
} else {
fs.opts = append(fs.opts, fuse.CreationTime(time.Now()))
}
} else {
fs.opts = append(fs.opts, fuse.CreationTime(time.Now()))
}
case "FileSystemName":
if len(args) >= 2 {
if v, ok := args[1].(string); ok {
fs.opts = append(fs.opts, fuse.FileSystemName(v))
}
}
case "VolumePrefix":
if len(args) >= 2 {
if v, ok := args[1].(string); ok {
fs.opts = append(fs.opts, fuse.VolumePrefix(v))
}
}
}
}
}
}
func (handle *fileHandle[F, T]) reopenFile(fs *WinfspFs[F, T]) (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 *WinfspFs[F, T]) GetSecurityByName(ref *fuse.FileSystemRef, name string, flags fuse.GetSecurityByNameFlags) (uint32, *windows.SECURITY_DESCRIPTOR, error) {
info, err := fs.Inner.Stat(name)
if err != nil || flags == fuse.GetExistenceOnly {
return 0, nil, err
}
attributes := attributesFromFileMode(info.Mode())
var sd *windows.SECURITY_DESCRIPTOR
if (flags & fuse.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 *fuse.FSP_FSCTL_FILE_INFO, source os.DirEntry, evaluatedIndexNumber uint64) {
if s, err := source.Info(); err == nil {
fileInfoFromStat(target, s, evaluatedIndexNumber)
}
}
func fileInfoFromStat(target *fuse.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 *WinfspFs[F, T]) openFile(_ *fuse.FileSystemRef, name string, createOptions, grantedAccess uint32, mode os.FileMode, info *fuse.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, T]{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 *WinfspFs[F, T]) Create(ref *fuse.FileSystemRef, name string, createOptions, grantedAccess, fileAttributes uint32, securityDescriptor *windows.SECURITY_DESCRIPTOR, allocationSize uint64, info *fuse.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 *WinfspFs[F, T]) Open(ref *fuse.FileSystemRef, name string, createOptions, grantedAccess uint32, info *fuse.FSP_FSCTL_FILE_INFO) (uintptr, error) {
return fs.openFile(ref, name, createOptions, grantedAccess, os.FileMode(0), info)
}
func (fs *WinfspFs[F, T]) load(file uintptr) (*fileHandle[F, T], error) {
obj, ok := fs.handles.Load(file)
if !ok {
return nil, windows.STATUS_INVALID_HANDLE
}
return obj.(*fileHandle[F, T]), nil
}
func (fs *WinfspFs[F, T]) Close(ref *fuse.FileSystemRef, file uintptr) {
object, ok := fs.handles.LoadAndDelete(file)
if !ok {
return
}
fileHandle := object.(*fileHandle[F, T])
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 *WinfspFs[F, T]) Overwrite(ref *fuse.FileSystemRef, file uintptr, attributes uint32, replaceAttributes bool, allocationSize uint64, info *fuse.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 *WinfspFs[F, T]) GetOrNewDirBuffer(ref *fuse.FileSystemRef, file uintptr) (*fuse.DirBuffer, error) {
fileHandle, err := fs.load(file)
if err != nil {
return nil, err
}
return &fileHandle.dir, nil
}
func (fs *WinfspFs[F, T]) ReadDirectory(ref *fuse.FileSystemRef, file uintptr, pattern string, fill func(string, *fuse.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 fuse.FSP_FSCTL_FILE_INFO
dirInfoFromStat(&info, fileInfo, 0)
ok, err := fill(fileInfo.Name(), &info)
if err != nil || !ok {
return err
}
}
return nil
}
func (fs *WinfspFs[F, T]) GetFileInfo(ref *fuse.FileSystemRef, file uintptr, info *fuse.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 *WinfspFs[F, T]) GetSecurity(ref *fuse.FileSystemRef, file uintptr) (*windows.SECURITY_DESCRIPTOR, error) {
_, err := fs.load(file)
if err != nil {
return nil, err
}
return procsd.Load()
}
func (fs *WinfspFs[F, T]) GetVolumeInfo(ref *fuse.FileSystemRef, info *fuse.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 *WinfspFs[F, T]) SetVolumeLabel(ref *fuse.FileSystemRef, label string, info *fuse.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 *WinfspFs[F, T]) SetBasicInfo(ref *fuse.FileSystemRef, file uintptr, flags fuse.SetBasicInfoFlags, attribute uint32, creationTime, lastAccessTime, lastWriteTime, changeTime uint64, info *fuse.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 *WinfspFs[F, T]) SetFileSize(ref *fuse.FileSystemRef, file uintptr, newSize uint64, setAllocationSize bool, info *fuse.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 *WinfspFs[F, T]) Read(ref *fuse.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 *WinfspFs[F, T]) Write(ref *fuse.FileSystemRef, file uintptr, b []byte, offset uint64, writeToEndOfFile, constrainedIo bool, info *fuse.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 *WinfspFs[F, T]) Flush(ref *fuse.FileSystemRef, file uintptr, info *fuse.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 *WinfspFs[F, T]) CanDelete(ref *fuse.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 *WinfspFs[F, T]) Cleanup(ref *fuse.FileSystemRef, file uintptr, name string, cleanupFlags uint32) {
handle, err := fs.load(file)
if err != nil {
return
}
if cleanupFlags&fuse.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 *WinfspFs[F, T]) Rename(ref *fuse.FileSystemRef, file uintptr, _, 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
}