Files
seaweedfs/weed/mount/winfsp/fs_windows.go
T
Chris Lu 804111745a mount: discard a path-cache insert that raced a purge (#10842)
* mount: discard a path-cache insert that raced a purge

The Windows adapter's walk resolves a component with a Lookup RPC and inserts
the result holding no lock, so a purge can land in between - and what the walk
just resolved is then the very name the purge removed. Anything opening the
old path concurrently with a rename repopulates the cache with the vacated
name, which the next stat is served from for up to a second. The release path
already guards its equivalent insert; the walk had nothing.

The cache counts purges now. A resolve snapshots the generation before its
lookups and insert discards the entry when any purge ran in between, parking
the reference in the graveyard so the in-flight caller keeps a valid inode
either way.

Seen once in CI as TestRenameOverExisting failing with 'source survived the
rename': every SeaweedFS layer is synchronous with the rename, but a
background open of the source - an antivirus scan of the just-written file
fits - can requalify the stale name through this window. The assertion also
reports what stat returned now, and whether it persisted, so a recurrence
indicts a specific layer instead of reading as a mystery.

* mount: cover the path-cache discard by key, and let a discard rest

Review follow-ups. The generation was global, so any purge between a walk's
snapshot and its insert discarded the entry whatever its name - and an open
retries resolve-then-steal only four times before failing with EIO, so
sustained unrelated churn could fail opens of untouched paths. Purges are
remembered by key now and only one that covers the inserted name discards it;
past the remembered window the insert is discarded without a check, which only
costs a retry.

A discard that itself tripped the sweep also handed its own reference straight
to forget while the walker was still using the inode. The graveyard holds two
generations now, so an appended reference always survives the sweep of the
call that appended it - which the displaced-entry and purge paths needed too.

Also restores the original path-cache test suite this branch had overwritten
instead of extended, and rewords the semantics-test failure so it no longer
claims the source survived when stat returned a transient error.

* mount: take an open's reference directly instead of stealing it back

resolveAndSteal cached the final component only to steal it back, so an open
depended on that insert surviving whatever purges raced it - four attempts and
then EIO. The keyed purge window narrowed how often an insert is discarded,
but past the window the discard is blind again, so the cliff had only moved.

A cached entry is still stolen; anything else is now looked up directly, with
the caller owning the reference from the start. No retry loop, and no way for
churn - covered, unrelated or overflowing the window - to fail an open.

Also covers the whole-cache purge: purge of the root with prefix set clears
every entry, but the covers check tested for a '/'-prefixed key that a
normalised key never has, so it covered no in-flight insert at all.
2026-08-19 17:05:49 -07:00

856 lines
25 KiB
Go

package winfsp
import (
"strings"
"sync"
"syscall"
"time"
cgofuse "github.com/winfsp/cgofuse/fuse"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/mount"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const (
rootInode = 1
// WinFsp passes this when an operation carries no open handle.
noHandle = ^uint64(0)
// utimeOmit is the nanosecond marker asking for a timestamp to be left as
// it is; cgofuse hands it through rather than resolving it.
utimeOmit = (1 << 30) - 2
// windowsEpochCutoff is 1601-01-02 in unix seconds. Anything at or below
// it is Windows' own epoch leaking through rather than a real time.
windowsEpochCutoff = -11644387200
// How many entries to pull from one readdir round before handing them to
// WinFsp. Bounded so a directory with millions of children does not
// materialise in one slice.
readdirBatch = 4096
)
// never is a nil channel: receiving blocks forever, which is what the raw
// operations expect from a caller that cannot cancel.
var never chan struct{}
// WinFS presents a mount.WFS through the path-based interface WinFsp speaks.
type WinFS struct {
cgofuse.FileSystemBase
wfs *mount.WFS
mountRoot util.FullPath
uid uint32
gid uint32
readOnly bool
// paths holds the lookup references for resolved paths, standing in for
// the kernel caches of the unix mounts.
paths *pathCache
// An open handle holds a lookup reference for its inode until Release,
// the way the kernel keeps one for an open file. WinFsp hands back only
// the handle, and the raw filesystem reuses one handle for repeated opens
// of the same inode, so the references are counted.
mu sync.Mutex
fileInodes map[uint64]*handleRef
dirInodes map[uint64]*handleRef
}
func NewWinFS(wfs *mount.WFS, uid, gid uint32, readOnly bool, cacheTimeout time.Duration) *WinFS {
w := &WinFS{
wfs: wfs,
mountRoot: wfs.MountRoot(),
uid: uid,
gid: gid,
readOnly: readOnly,
fileInodes: make(map[uint64]*handleRef),
dirInodes: make(map[uint64]*handleRef),
}
w.paths = newPathCache(cacheTimeout, w.forget)
return w
}
// stillNames reports whether the mount still tracks inode at the mount-relative
// key, which a delete-on-close or a rename while the handle was open has
// changed.
func (w *WinFS) stillNames(key string, inode uint64) bool {
current, ok := w.wfs.PathForInode(inode)
if !ok {
return false
}
rel, ok := relativeToMount(w.mountRoot, current)
return ok && cacheKey(rel) == key
}
// invalidatePath is what the notifier calls when a metadata event arrives:
// whatever is cached for that path is out of date. Directories take their
// subtree with them, since the children's cached paths point through them.
func (w *WinFS) invalidatePath(path string, isDirectory bool) {
w.paths.purge(cacheKey(path), isDirectory)
}
// handleRef is the lookup references an open handle is holding, one per open
// that has not yet been released.
type handleRef struct {
inode uint64
count int
}
// retain parks one reference under a handle.
func (w *WinFS) retain(table map[uint64]*handleRef, handle, inode uint64) {
if handle == noHandle {
return
}
var stranded uint64
w.mu.Lock()
switch existing, found := table[handle]; {
case found && existing.inode == inode:
existing.count++
case found:
// The handle was reissued for a different inode; its old reference
// would otherwise never be returned.
stranded = existing.inode
table[handle] = &handleRef{inode: inode, count: 1}
default:
table[handle] = &handleRef{inode: inode, count: 1}
}
w.mu.Unlock()
w.forget(stranded)
}
// releaseRetained hands back one reference, reporting the inode only once the
// last open of that handle is gone, so a repeated release cannot forget twice.
func (w *WinFS) releaseRetained(table map[uint64]*handleRef, handle uint64) uint64 {
w.mu.Lock()
defer w.mu.Unlock()
existing, found := table[handle]
if !found {
return 0
}
existing.count--
if existing.count > 0 {
return 0
}
delete(table, handle)
return existing.inode
}
// caller identifies who the raw filesystem should record as the owner of
// anything it creates. Windows has no uid to pass through, so entries carry
// the identity the mount was started with rather than root.
// denied reports whether a modification should be refused outright, which is
// how a read-only mount is enforced: WinFsp discards its own "ro" option.
func (w *WinFS) denied() bool { return w.readOnly }
// inodeForHandle reports the inode an open handle is holding, or 0. WinFsp
// keeps the path it opened with and never updates it across a rename, so a
// handle is the more reliable of the two.
func (w *WinFS) inodeForHandle(table map[uint64]*handleRef, handle uint64) uint64 {
if handle == noHandle {
return 0
}
w.mu.Lock()
defer w.mu.Unlock()
if existing, found := table[handle]; found {
return existing.inode
}
return 0
}
// ptr is for the operations that take the header by pointer.
func ptr(h fuse.InHeader) *fuse.InHeader { return &h }
func (w *WinFS) caller(inode uint64) fuse.InHeader {
return fuse.InHeader{
NodeId: inode,
Caller: fuse.Caller{Owner: fuse.Owner{Uid: w.uid, Gid: w.gid}},
}
}
// forget returns one reference. The root is never looked up, so it never holds
// one to give back.
func (w *WinFS) forget(inode uint64) {
if inode == rootInode || inode == 0 {
return
}
w.wfs.Forget(inode, 1)
}
// walk resolves a path one component at a time, filling the path cache as it
// goes. Every reference a lookup grants is owned by the cache, which keeps the
// returned inode alive for at least one cache sweep; an operation that needs
// to hold the inode longer steals the reference from the cache.
func (w *WinFS) walk(parts []string) (uint64, fuse.Status) {
inode := uint64(rootInode)
// Taken before the lookups: a purge racing this walk - a rename or unlink
// landing between a Lookup and its insert - makes what was just resolved
// the very thing the purge removed.
gen := w.paths.snapshot()
for i, name := range parts {
key := strings.Join(parts[:i+1], "/")
if cached, _, ok := w.paths.lookup(key); ok {
inode = cached
continue
}
var out fuse.EntryOut
if status := w.wfs.Lookup(never, ptr(w.caller(inode)), name, &out); status != fuse.OK {
return 0, status
}
w.paths.insert(key, out.NodeId, out.Attr, gen)
inode = out.NodeId
}
return inode, fuse.OK
}
// resolve walks a WinFsp path down to an inode, valid at least until the next
// cache sweep.
func (w *WinFS) resolve(path string) (uint64, fuse.Status) {
return w.walk(splitPath(path))
}
// resolveParent resolves everything but the last component, which the create
// and delete operations need separately.
func (w *WinFS) resolveParent(path string) (uint64, string, fuse.Status) {
parentParts, name, ok := splitParent(path)
if !ok {
return 0, "", fuse.EINVAL
}
parent, status := w.walk(parentParts)
if status != fuse.OK {
return 0, "", status
}
return parent, name, fuse.OK
}
// resolveAndSteal resolves path to an inode reference the caller owns, for
// the open paths that hold it for the life of a handle: a cached entry is
// stolen, anything else is looked up directly so the reference never passes
// through the cache - an open must not depend on an insert surviving the
// purges racing it. The root is handed out without a reference; it does not
// need one.
func (w *WinFS) resolveAndSteal(path string) (uint64, fuse.Status) {
key := cacheKey(path)
if key == "" {
return rootInode, fuse.OK
}
if stolen, ok := w.paths.steal(key); ok {
return stolen, fuse.OK
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return 0, status
}
var out fuse.EntryOut
if status := w.wfs.Lookup(never, ptr(w.caller(parent)), name, &out); status != fuse.OK {
return 0, status
}
return out.NodeId, fuse.OK
}
func (w *WinFS) attrToStat(attr *fuse.Attr, stat *cgofuse.Stat_t) {
stat.Ino = attr.Ino
stat.Mode = attr.Mode
stat.Nlink = attr.Nlink
stat.Size = int64(attr.Size)
stat.Blocks = int64(attr.Blocks)
stat.Blksize = int64(attr.Blksize)
stat.Uid = w.uid
stat.Gid = w.gid
stat.Atim = cgofuse.Timespec{Sec: int64(attr.Atime), Nsec: int64(attr.Atimensec)}
stat.Mtim = cgofuse.Timespec{Sec: int64(attr.Mtime), Nsec: int64(attr.Mtimensec)}
stat.Ctim = cgofuse.Timespec{Sec: int64(attr.Ctime), Nsec: int64(attr.Ctimensec)}
// The filer records a creation time, but the raw protocol's Attr carries no
// field for it outside darwin, so ctime stands in until there is a way to
// read it through.
stat.Birthtim = stat.Ctim
// Windows expects a regular file to be marked archived; with no flags at
// all it synthesises NORMAL, which is a different thing and what
// create_fileattr_test checks.
if attr.Mode&cgofuse.S_IFMT == cgofuse.S_IFREG {
stat.Flags |= cgofuse.UF_ARCHIVE
}
}
// translateOpenFlags converts cgofuse's open flags, which follow MSVC's
// numbering, into the values the raw filesystem tests against. Only the access
// mode and O_TRUNC happen to agree; O_EXCL would otherwise read as O_APPEND.
func translateOpenFlags(flags int) uint32 {
out := uint32(flags & cgofuse.O_ACCMODE)
for _, pair := range []struct {
from int
to int
}{
{cgofuse.O_APPEND, syscall.O_APPEND},
{cgofuse.O_CREAT, syscall.O_CREAT},
{cgofuse.O_TRUNC, syscall.O_TRUNC},
{cgofuse.O_EXCL, syscall.O_EXCL},
} {
if flags&pair.from != 0 {
out |= uint32(pair.to)
}
}
return out
}
// logResolveFailure keeps a missing file quiet. Windows probes for entries
// that do not exist as a matter of course, so ENOENT is an answer rather than
// a fault; anything else is worth a line.
func logResolveFailure(op, path string, status fuse.Status) {
if status == fuse.ENOENT {
glog.V(4).Infof("%s %s: no such entry", op, path)
return
}
glog.Errorf("%s %s: resolving: %v", op, path, status)
}
func (w *WinFS) Statfs(path string, stat *cgofuse.Statfs_t) int {
var out fuse.StatfsOut
if status := w.wfs.StatFs(never, ptr(w.caller(rootInode)), &out); status != fuse.OK {
return toErrno(status)
}
stat.Bsize = uint64(out.Bsize)
stat.Frsize = uint64(out.Frsize)
stat.Blocks = out.Blocks
stat.Bfree = out.Bfree
stat.Bavail = out.Bavail
stat.Files = out.Files
stat.Ffree = out.Ffree
stat.Favail = out.Ffree
stat.Namemax = uint64(out.NameLen)
return 0
}
// getattrFromCache serves attributes the way the kernel would from its
// attribute cache. A file with an open handle is excluded: its live size is
// on the handle, not in the cache.
func (w *WinFS) getattrFromCache(key string, stat *cgofuse.Stat_t) bool {
if key == "" {
return false
}
inode, attr, ok := w.paths.lookup(key)
if !ok || w.wfs.IsFileOpen(inode) {
return false
}
w.attrToStat(&attr, stat)
return true
}
func (w *WinFS) Getattr(path string, stat *cgofuse.Stat_t, fh uint64) int {
inode := w.inodeForHandle(w.fileInodes, fh)
if inode == 0 {
key := cacheKey(path)
if fh == noHandle && w.getattrFromCache(key, stat) {
return 0
}
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
logResolveFailure("getattr", path, status)
return toErrno(status)
}
// The walk just refreshed the cache, so this hits unless the file is
// open or is the root, and saves the second load of the same entry.
if fh == noHandle && w.getattrFromCache(key, stat) {
return 0
}
}
in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
if fh != noHandle {
in.Fh_ = fh
in.Flags_ = fuse.FUSE_GETATTR_FH
}
var out fuse.AttrOut
if status := w.wfs.GetAttr(never, in, &out); status != fuse.OK {
return toErrno(status)
}
w.attrToStat(&out.Attr, stat)
return 0
}
// purgeWithParent drops a mutated path from the cache along with its parent,
// whose cached mtime the mutation just outdated.
func (w *WinFS) purgeWithParent(path string, prefix bool) {
key := cacheKey(path)
w.paths.purge(key, prefix)
if i := strings.LastIndexByte(key, '/'); i > 0 {
w.paths.purge(key[:i], false)
}
}
func (w *WinFS) Mkdir(path string, mode uint32) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.MkdirIn{InHeader: w.caller(parent), Mode: mode}
var out fuse.EntryOut
status = w.wfs.Mkdir(never, in, name, &out)
if status == fuse.OK {
w.purgeWithParent(path, false)
// The new directory is about to be filled; cache it, reference and all.
w.paths.insert(cacheKey(path), out.NodeId, out.Attr, w.paths.snapshot())
}
return toErrno(status)
}
func (w *WinFS) Rmdir(path string) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
status = w.wfs.Rmdir(never, ptr(w.caller(parent)), name)
if status == fuse.OK {
w.purgeWithParent(path, true)
}
return toErrno(status)
}
func (w *WinFS) Unlink(path string) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
status = w.wfs.Unlink(never, ptr(w.caller(parent)), name)
if status == fuse.OK {
w.purgeWithParent(path, false)
}
return toErrno(status)
}
func (w *WinFS) Rename(oldpath string, newpath string) int {
if w.denied() {
return -eROFS
}
oldParent, oldName, status := w.resolveParent(oldpath)
if status != fuse.OK {
return toErrno(status)
}
newParent, newName, status := w.resolveParent(newpath)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.RenameIn{InHeader: w.caller(oldParent), Newdir: newParent}
status = w.wfs.Rename(never, in, oldName, newName)
if status == fuse.OK {
// Directories carry their subtree's cached paths with them, and the
// rename may have replaced an entry at the destination.
w.purgeWithParent(oldpath, true)
w.purgeWithParent(newpath, true)
}
return toErrno(status)
}
func (w *WinFS) Create(path string, flags int, mode uint32) (int, uint64) {
if w.denied() {
return -eROFS, noHandle
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
glog.Errorf("create %s: resolving the parent directory: %v", path, status)
return toErrno(status), noHandle
}
in := &fuse.CreateIn{InHeader: w.caller(parent), Flags: translateOpenFlags(flags), Mode: mode}
var out fuse.CreateOut
if status := w.wfs.Create(never, in, name, &out); status != fuse.OK {
glog.Errorf("create %s in inode %d: %v", name, parent, status)
return toErrno(status), noHandle
}
// Whatever the cache held for this path was replaced by the create.
w.purgeWithParent(path, false)
// Create grants a reference on the new inode; hold it for as long as the
// handle lives and give it back in Release.
w.retain(w.fileInodes, out.Fh, out.NodeId)
return 0, out.Fh
}
func (w *WinFS) Open(path string, flags int) (int, uint64) {
inode, status := w.resolveAndSteal(path)
if status != fuse.OK {
logResolveFailure("open", path, status)
return toErrno(status), noHandle
}
in := &fuse.OpenIn{InHeader: w.caller(inode), Flags: translateOpenFlags(flags)}
var out fuse.OpenOut
if status := w.wfs.Open(never, in, &out); status != fuse.OK {
glog.Errorf("open %s inode %d: %v", path, inode, status)
w.forget(inode)
return toErrno(status), noHandle
}
w.retain(w.fileInodes, out.Fh, inode)
return 0, out.Fh
}
func (w *WinFS) Read(path string, buff []byte, ofst int64, fh uint64) int {
if fh == noHandle {
return -eBADF
}
in := &fuse.ReadIn{
Fh: fh,
Offset: uint64(ofst),
Size: uint32(len(buff)),
}
result, status := w.wfs.Read(never, in, buff)
if status != fuse.OK {
return toErrno(status)
}
data, status := result.Bytes(buff)
if status != fuse.OK {
return toErrno(status)
}
if len(data) > 0 && &data[0] != &buff[0] {
copy(buff, data)
}
return len(data)
}
func (w *WinFS) Write(path string, buff []byte, ofst int64, fh uint64) int {
if w.denied() {
return -eROFS
}
if fh == noHandle {
return -eBADF
}
in := &fuse.WriteIn{
Fh: fh,
Offset: uint64(ofst),
Size: uint32(len(buff)),
}
written, status := w.wfs.Write(never, in, buff)
if status != fuse.OK {
if status == fuse.ENOENT {
glog.Errorf("write %s: handle %d is not open", path, fh)
}
return toErrno(status)
}
return int(written)
}
func (w *WinFS) Truncate(path string, size int64, fh uint64) int {
if w.denied() {
return -eROFS
}
inode := w.inodeForHandle(w.fileInodes, fh)
if inode == 0 {
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
in.Valid = fuse.FATTR_SIZE
in.Size = uint64(size)
if fh != noHandle {
in.Valid |= fuse.FATTR_FH
in.Fh = fh
}
var out fuse.AttrOut
status := w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
func (w *WinFS) Chmod(path string, mode uint32) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
in.Valid = fuse.FATTR_MODE
in.Mode = mode
var out fuse.AttrOut
status = w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
func (w *WinFS) Utimens(path string, tmsp []cgofuse.Timespec) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
if len(tmsp) < 2 {
in.Valid = fuse.FATTR_ATIME_NOW | fuse.FATTR_MTIME_NOW
} else {
// Windows sends times around its own 1601 epoch, which arrive here as
// a large negative second count and would be stored as a year-1601
// timestamp that every other client then reads. Leave those alone.
if applyTimespec(tmsp[0]) {
in.Valid |= fuse.FATTR_ATIME
in.Atime, in.Atimensec = uint64(tmsp[0].Sec), uint32(tmsp[0].Nsec)
}
if applyTimespec(tmsp[1]) {
in.Valid |= fuse.FATTR_MTIME
in.Mtime, in.Mtimensec = uint64(tmsp[1].Sec), uint32(tmsp[1].Nsec)
}
if in.Valid == 0 {
return 0
}
}
var out fuse.AttrOut
status = w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
// applyTimespec reports whether a timestamp should be written. UTIME_OMIT asks
// for the existing value to be kept, and a time at or below Windows' own 1601
// epoch arrives as a large negative second count. Exactly zero is what Windows
// sends for a field it is not setting; storing it put 1970 in the atime
// overlay, which then read back as the file's access time. A date genuinely
// before 1970 is still allowed through — only the epoch itself is the
// sentinel.
func applyTimespec(ts cgofuse.Timespec) bool {
if ts.Nsec == utimeOmit {
return false
}
if ts.Sec == 0 && ts.Nsec == 0 {
return false
}
return ts.Sec > windowsEpochCutoff
}
func (w *WinFS) Flush(path string, fh uint64) int {
if fh == noHandle {
return 0
}
return toErrno(w.wfs.Flush(never, &fuse.FlushIn{InHeader: w.caller(0), Fh: fh}))
}
func (w *WinFS) Fsync(path string, datasync bool, fh uint64) int {
if fh == noHandle {
return 0
}
return toErrno(w.wfs.Fsync(never, &fuse.FsyncIn{Fh: fh}))
}
func (w *WinFS) Release(path string, fh uint64) int {
if fh == noHandle {
return 0
}
// The handle's view of the file is the freshest there is, and the close
// has flushed by now, so read it before the handle goes away: the next
// operation on the path — a stat right after a copy, most often — is then
// served from the cache instead of walking to the filer again.
var attr *fuse.Attr
if inode := w.inodeForHandle(w.fileInodes, fh); inode != 0 {
in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
in.Fh_ = fh
in.Flags_ = fuse.FUSE_GETATTR_FH
var out fuse.AttrOut
if w.wfs.GetAttr(never, in, &out) == fuse.OK {
attr = &out.Attr
}
}
w.wfs.Release(never, &fuse.ReleaseIn{Fh: fh})
if inode := w.releaseRetained(w.fileInodes, fh); inode != 0 {
// The reference moves from the handle to the cache — but only if the
// path still names this inode: WinFsp reports the path the handle
// opened with, and after a delete-on-close or a rename caching it
// would resurrect an entry that is gone.
if key, gen := cacheKey(path), w.paths.snapshot(); attr != nil && w.stillNames(key, inode) {
w.paths.insert(key, inode, *attr, gen)
} else {
w.forget(inode)
}
}
return 0
}
func (w *WinFS) Opendir(path string) (int, uint64) {
inode, status := w.resolveAndSteal(path)
if status != fuse.OK {
return toErrno(status), noHandle
}
var out fuse.OpenOut
if status := w.wfs.OpenDir(never, &fuse.OpenIn{InHeader: w.caller(inode)}, &out); status != fuse.OK {
w.forget(inode)
return toErrno(status), noHandle
}
w.retain(w.dirInodes, out.Fh, inode)
return 0, out.Fh
}
func (w *WinFS) Releasedir(path string, fh uint64) int {
if fh == noHandle {
return 0
}
w.wfs.ReleaseDir(&fuse.ReleaseIn{Fh: fh})
w.forget(w.releaseRetained(w.dirInodes, fh))
return 0
}
// readdirSink collects one readdir round. The raw operation fills the returned
// EntryOut after AddEntryPlus returns, so nothing can be converted until the
// round is over.
type readdirSink struct {
names []string
offsets []uint64
inodes []uint64
modes []uint32
attrs []*fuse.EntryOut
limit int
// lastOffset advances over dropped entries too, so a batch that is all
// dot entries still moves the enumeration along.
lastOffset uint64
seen int
}
// WinFsp strips "." and ".." for the root directory itself and expects every
// other directory to report them, the way a real NTFS enumeration does.
func (s *readdirSink) AddEntry(entry fuse.DirEntry) bool {
if len(s.names) >= s.limit {
return false
}
s.seen++
s.lastOffset = entry.Off
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, nil)
return true
}
func (s *readdirSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
if len(s.names) >= s.limit {
return nil
}
s.seen++
s.lastOffset = entry.Off
out := &fuse.EntryOut{}
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, out)
return out
}
// WinFsp has no FORGET, and every EntryOut is converted before the round ends.
func (s *readdirSink) TakesLookupRef() bool { return false }
func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t, ofst int64) bool, ofst int64, fh uint64) int {
inode := w.inodeForHandle(w.dirInodes, fh)
if inode == 0 {
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
}
offset := uint64(ofst)
for {
sink := &readdirSink{limit: readdirBatch}
in := &fuse.ReadIn{
InHeader: w.caller(inode),
Fh: fh,
Offset: offset,
Size: 1 << 20,
}
if status := w.wfs.ReadDirectoryInto(in, sink, true); status != fuse.OK {
return toErrno(status)
}
if sink.seen == 0 {
return 0
}
filled := true
for i, name := range sink.names {
var stat cgofuse.Stat_t
var statp *cgofuse.Stat_t
if attr := sink.attrs[i]; attr != nil && attr.Attr.Mode != 0 {
w.attrToStat(&attr.Attr, &stat)
statp = &stat
} else if sink.modes[i] != 0 {
// "." and ".." are reported without attributes: the readdir
// only fills a block for real children. Windows still needs
// their type, and enumerating a directory whose first entry is
// not marked as one fails outright.
stat.Mode = sink.modes[i]
stat.Ino = sink.inodes[i]
stat.Nlink = 1
stat.Uid, stat.Gid = w.uid, w.gid
statp = &stat
}
if filled && !fill(name, statp, int64(sink.offsets[i])) {
filled = false
}
}
if !filled {
return 0
}
if sink.lastOffset <= offset {
return 0
}
offset = sink.lastOffset
}
}
func (w *WinFS) Readlink(path string) (int, string) {
// WinFsp probes the root to decide whether the volume has symlinks, and
// turns them on unless this fails. Leaving them on costs a getattr per
// path component on every open, for a feature Symlink already refuses.
if len(splitPath(path)) == 0 {
return -eNOSYS, ""
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status), ""
}
target, status := w.wfs.Readlink(never, ptr(w.caller(inode)))
if status != fuse.OK {
return toErrno(status), ""
}
return 0, string(target)
}
// Symlink is refused for now. The entry is easy to create, but WinFsp only
// follows it once the reparse point is wired up, so it would otherwise read
// back as an empty file.
func (w *WinFS) Symlink(target string, newpath string) int {
return -eNOSYS
}
// Chown accepts and discards. Windows has no uid to record, but WinFsp passes
// a chown failure straight out of SetSecurity, so refusing it breaks Explorer's
// Security tab and icacls for changes that are not about ownership at all.
func (w *WinFS) Chown(path string, uid uint32, gid uint32) int {
return 0
}
// Link is not implemented: WinFsp has no hard links.
func (w *WinFS) Link(oldpath string, newpath string) int {
return -eNOSYS
}