982 lines
27 KiB
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
|
|
}
|