diff --git a/internal/commands/flag.go b/internal/commands/flag.go index 796685c8..66f04254 100644 --- a/internal/commands/flag.go +++ b/internal/commands/flag.go @@ -14,6 +14,7 @@ import ( "unicode/utf8" "unsafe" + "github.com/djdv/go-filesystem-utils/internal/filesystem" "github.com/djdv/go-filesystem-utils/internal/generic" "github.com/djdv/p9/p9" "github.com/multiformats/go-multiaddr" @@ -579,3 +580,13 @@ func parseMultiaddrList(parameter string) ([]multiaddr.Multiaddr, error) { } return maddrs, nil } + +func prefixIDFlag[ + T interface { + ~string + filesystem.Host | filesystem.ID + }, +](ID T, +) string { + return strings.ToLower(string(ID)) + "-" +} diff --git a/internal/commands/mount.go b/internal/commands/mount.go index 4629d06f..2555c7f3 100644 --- a/internal/commands/mount.go +++ b/internal/commands/mount.go @@ -272,6 +272,7 @@ func makeHostCommands() []command.Command { var ( commandMakers = []makeCommand{ makeFUSECommand, + makePlan9HostCommand, } commands = make([]command.Command, 0, len(commandMakers)) ) @@ -292,6 +293,7 @@ func makeGuestCommands[ ](host filesystem.Host, ) []command.Command { guests := makeIPFSCommands[HC, HM](host) + guests = append(guests, makePlan9GuestCommand[HC, HM](host)) sortCommands(guests) return guests } diff --git a/internal/commands/mountpoint.go b/internal/commands/mountpoint.go index ba3ec88a..08f7e0c7 100644 --- a/internal/commands/mountpoint.go +++ b/internal/commands/mountpoint.go @@ -90,6 +90,7 @@ func makeMountPointHosts(path ninePath, autoUnlink bool) mountPointHosts { var ( hostMakers = []makeHostsFunc{ makeFUSEHost, + makePlan9Host, } hosts = make(mountPointHosts, len(hostMakers)) ) @@ -142,6 +143,7 @@ func makeMountPointGuests[ ) mountPointGuests { guests := make(mountPointGuests) makeIPFSGuests[HC](guests, path) + guests[p9fs.GuestID] = newMountPointFunc[HC, p9fs.Guest](path) return guests } diff --git a/internal/commands/mountpoint_9p.go b/internal/commands/mountpoint_9p.go new file mode 100644 index 00000000..60a9ac29 --- /dev/null +++ b/internal/commands/mountpoint_9p.go @@ -0,0 +1,121 @@ +package commands + +import ( + "encoding/json" + "flag" + "fmt" + + "github.com/djdv/go-filesystem-utils/internal/command" + "github.com/djdv/go-filesystem-utils/internal/filesystem" + p9fs "github.com/djdv/go-filesystem-utils/internal/filesystem/9p" + "github.com/djdv/go-filesystem-utils/internal/generic" + "github.com/multiformats/go-multiaddr" +) + +type ( + plan9GuestSettings p9fs.Guest + plan9GuestOption func(*plan9GuestSettings) error + plan9GuestOptions []plan9GuestOption + + plan9HostSettings p9fs.Host + plan9HostOption func(*plan9HostSettings) error + plan9HostOptions []plan9HostOption +) + +const p9ServerFlagName = "server" + +func makePlan9HostCommand() command.Command { + return makeMountSubcommand( + p9fs.HostID, + makeGuestCommands[plan9HostOptions, plan9HostSettings](p9fs.HostID), + ) +} + +func makePlan9Host(path ninePath, autoUnlink bool) (filesystem.Host, p9fs.MakeGuestFunc) { + guests := makeMountPointGuests[p9fs.Host](path) + return p9fs.HostID, newMakeGuestFunc(guests, path, autoUnlink) +} + +func unmarshalPlan9() (filesystem.Host, decodeFunc) { + return p9fs.HostID, func(b []byte) (string, error) { + var host p9fs.Host + err := json.Unmarshal(b, &host) + return host.Maddr.String(), err + } +} + +func (*plan9HostOptions) usage(guest filesystem.ID) string { + return string(p9fs.HostID) + " hosts " + + string(guest) + " as a 9P file server" +} + +func (*plan9HostOptions) BindFlags(*flag.FlagSet) { /* NOOP */ } + +func (o9 plan9HostOptions) make() (plan9HostSettings, error) { + return makeWithOptions(o9...) +} + +func (set plan9HostSettings) marshal(arg string) ([]byte, error) { + if arg == "" { + err := command.UsageError{ + Err: generic.ConstError( + "expected server multiaddr", + ), + } + return nil, err + } + maddr, err := multiaddr.NewMultiaddr(arg) + if err != nil { + return nil, err + } + set.Maddr = maddr + return json.Marshal(set) +} + +func makePlan9GuestCommand[ + HC mountCmdHost[HT, HM], + HM marshaller, + HT any, +](host filesystem.Host, +) command.Command { + return makeMountCommand[HC, HM, plan9GuestOptions, plan9GuestSettings](host, p9fs.GuestID) +} + +func (*plan9GuestOptions) usage(filesystem.Host) string { + return string(p9fs.GuestID) + " attaches to a 9P file server" +} + +func (o9 *plan9GuestOptions) BindFlags(flagSet *flag.FlagSet) { + var ( + flagPrefix = prefixIDFlag(p9fs.GuestID) + srvUsage = "9P2000.L file system server `maddr`" + srvName = flagPrefix + p9ServerFlagName + ) + flagSetFunc(flagSet, srvName, srvUsage, o9, + func(value multiaddr.Multiaddr, settings *plan9GuestSettings) error { + settings.Maddr = value + return nil + }) +} + +func (o9 plan9GuestOptions) make() (plan9GuestSettings, error) { + settings, err := makeWithOptions(o9...) + if err != nil { + return plan9GuestSettings{}, err + } + if settings.Maddr == nil { + var ( + flagPrefix = prefixIDFlag(p9fs.GuestID) + srvName = flagPrefix + p9ServerFlagName + ) + return plan9GuestSettings{}, fmt.Errorf( + "flag `-%s` must be provided for 9P guests", + srvName, + ) + } + return settings, nil +} + +func (s9 plan9GuestSettings) marshal(string) ([]byte, error) { + return json.Marshal(s9) +} diff --git a/internal/commands/mountpoint_ipfs.go b/internal/commands/mountpoint_ipfs.go index 9561207a..87e7a899 100644 --- a/internal/commands/mountpoint_ipfs.go +++ b/internal/commands/mountpoint_ipfs.go @@ -71,10 +71,6 @@ func guestOverlayText(overlay, overlaid filesystem.ID) string { return string(overlay) + " is an " + string(overlaid) + " overlay" } -func prefixIDFlag(system filesystem.ID) string { - return strings.ToLower(string(system)) + "-" -} - func (*ipfsOptions) usage(filesystem.Host) string { return string(ipfs.IPFSID) + " provides an empty root directory." + "\nChild paths are forwarded to the IPFS API." diff --git a/internal/commands/unmount.go b/internal/commands/unmount.go index 834eb651..8fa06f82 100644 --- a/internal/commands/unmount.go +++ b/internal/commands/unmount.go @@ -146,6 +146,7 @@ func newDecodeTargetFunc() p9fs.DecodeTargetFunc { var ( decoderMakers = []makeDecoderFunc{ unmarshalFUSE, + unmarshalPlan9, } decoders = make(decoders, len(decoderMakers)) ) diff --git a/internal/filesystem/9p/client.go b/internal/filesystem/9p/client.go new file mode 100644 index 00000000..4bff1b3f --- /dev/null +++ b/internal/filesystem/9p/client.go @@ -0,0 +1,355 @@ +package p9 + +import ( + "encoding/json" + "errors" + "io" + "io/fs" + "path" + "strings" + "time" + "unsafe" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" + fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors" + "github.com/djdv/go-filesystem-utils/internal/generic" + perrors "github.com/djdv/p9/errors" + "github.com/djdv/p9/fsimpl/templatefs" + "github.com/djdv/p9/p9" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +type ( + Guest struct { + Maddr multiaddr.Multiaddr `json:"maddr,omitempty"` + } + plan9FS struct { + client *p9.Client + root p9.File + } + plan9File struct { + walkFID, ioFID p9.File + name string + cursor int64 + } + lazyIO struct { + templatefs.NoopFile + container *plan9File + p9.OpenFlags + } + plan9Info struct { + attr *p9.Attr + name string + } + plan9Entry struct { + *p9.Dirent + parent p9.File + parentName string + } +) + +var ( + _ fs.FS = (*plan9FS)(nil) + _ fs.StatFS = (*plan9FS)(nil) + _ filesystem.IDFS = (*plan9FS)(nil) + _ fs.File = (*plan9File)(nil) + _ fs.FileInfo = (*plan9Info)(nil) +) + +const ( + GuestID filesystem.ID = "9P" + pathSeparatorGo = "/" +) + +func (*Guest) GuestID() filesystem.ID { return GuestID } +func (g9 *Guest) MakeFS() (fs.FS, error) { + conn, err := manet.Dial(g9.Maddr) + if err != nil { + return nil, err + } + return NewPlan9Guest(conn) +} + +// TODO: Options: +// - Client log +func NewPlan9Guest(channel io.ReadWriteCloser) (*plan9FS, error) { + client, err := p9.NewClient(channel) + if err != nil { + return nil, err + } + root, err := client.Attach("") + if err != nil { + return nil, err + } + fsys := plan9FS{ + client: client, + root: root, + } + return &fsys, nil +} + +func (*plan9FS) ID() filesystem.ID { return GuestID } + +func (fsys *plan9FS) Stat(name string) (fs.FileInfo, error) { + const op = "stat" + file, err := fsys.walkTo(op, name) + if err != nil { + return nil, err + } + info, err := getInfoGo(name, file) + if err := errors.Join(err, file.Close()); err != nil { + return nil, fserrors.New(op, name, err, fserrors.IO) + } + return info, err +} + +func (fsys *plan9FS) walkTo(op, name string) (p9.File, error) { + if !fs.ValidPath(name) { + return nil, fserrors.New(op, name, filesystem.ErrPath, fserrors.InvalidItem) + } + var names []string + if name != filesystem.Root { + names = strings.Split(name, pathSeparatorGo) + } + _, file, err := fsys.root.Walk(names) + if err != nil { + var kind fserrors.Kind + if errors.Is(err, perrors.ENOENT) { + kind = fserrors.NotExist + } else { + kind = fserrors.IO + } + return nil, fserrors.New(op, name, err, kind) + } + return file, nil +} + +func (fsys *plan9FS) Open(name string) (fs.File, error) { + const op = "open" + walkFID, err := fsys.walkTo(op, name) + if err != nil { + return nil, err + } + var wrapper plan9File //🥚 Self referential. + wrapper = plan9File{ + name: name, + walkFID: walkFID, + ioFID: &lazyIO{ + container: &wrapper, // 🐣 Self referential. + OpenFlags: p9.ReadOnly, + }, + } + return &wrapper, nil +} + +func (fsys *plan9FS) Close() error { + var errs []error + for _, closer := range []io.Closer{ + fsys.root, + fsys.client, + } { + if err := closer.Close(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func (f9 *plan9File) Stat() (fs.FileInfo, error) { + return getInfoGo(f9.name, f9.walkFID) +} + +func (f9 *plan9File) Read(p []byte) (int, error) { + n, err := f9.ioFID.ReadAt(p, f9.cursor) + if err == nil { + f9.cursor += int64(n) + } + return n, err +} + +func (f9 *plan9File) ReadDir(count int) ([]fs.DirEntry, error) { + const entrySize = unsafe.Sizeof(p9.Dirent{}) + count9 := count * int(entrySize) // Index -> bytes. + entries9, err := f9.ioFID.Readdir(uint64(f9.cursor), uint32(count9)) + if err != nil { + return nil, err + } + limit := len(entries9) + if limit == 0 && count > 0 { + return nil, io.EOF + } + if count > 0 && limit > count { + limit = count + } + var ( + entries = make([]fs.DirEntry, limit) + parent = f9.walkFID + parentName = f9.name + ) + for i := range entries { + entries[i] = plan9Entry{ + parent: parent, + parentName: parentName, + Dirent: &entries9[i], + } + } + f9.cursor += int64(len(entries)) + return entries, nil +} + +func (f9 *plan9File) Seek(offset int64, whence int) (int64, error) { + const op = "seek" + switch whence { + case io.SeekStart: + if offset < 0 { + err := generic.ConstError( + "tried to seek to a position before the beginning of the file", + ) + return -1, fserrors.New( + op, f9.name, + err, fserrors.InvalidItem, + ) + } + f9.cursor = offset + case io.SeekCurrent: + f9.cursor += offset + case io.SeekEnd: + var ( + want = p9.AttrMask{Size: true} + info, err = getInfo(f9.name, f9.walkFID, want) + ) + if err != nil { + return -1, err + } + end := info.attr.Size + f9.cursor = int64(end) + offset + } + return f9.cursor, nil +} + +func (f9 *plan9File) Close() error { + return errors.Join( + f9.ioFID.Close(), + f9.walkFID.Close(), + ) +} + +func (i9 *plan9Info) Name() string { return i9.name } +func (i9 *plan9Info) Size() int64 { return int64(i9.attr.Size) } +func (i9 *plan9Info) Mode() fs.FileMode { return i9.attr.Mode.OSMode() } +func (i9 *plan9Info) ModTime() time.Time { + return time.Unix(0, int64(i9.attr.MTimeNanoSeconds)) +} +func (i9 *plan9Info) IsDir() bool { return i9.Mode().IsDir() } + +func (i9 *plan9Info) Sys() any { return i9 } + +func (g9 *Guest) UnmarshalJSON(b []byte) error { + // multiformats/go-multiaddr issue #100 + var maddrWorkaround struct { + Maddr multiaddrContainer `json:"maddr,omitempty"` + } + if err := json.Unmarshal(b, &maddrWorkaround); err != nil { + return err + } + g9.Maddr = maddrWorkaround.Maddr.Multiaddr + return nil +} + +func (e9 plan9Entry) Name() string { return e9.Dirent.Name } +func (e9 plan9Entry) IsDir() bool { return e9.Dirent.Type == p9.TypeDir } +func (e9 plan9Entry) Type() fs.FileMode { + switch e9.Dirent.Type { + case p9.TypeRegular: + return fs.FileMode(0) + case p9.TypeDir: + return fs.ModeDir + case p9.TypeAppendOnly: + return fs.ModeAppend + case p9.TypeExclusive: + return fs.ModeExclusive + case p9.TypeTemporary: + return fs.ModeTemporary + case p9.TypeSymlink: + return fs.ModeSymlink + default: + return fs.ModeIrregular + } +} + +func (e9 plan9Entry) Info() (fs.FileInfo, error) { + wnames := []string{e9.Dirent.Name} + _, file, err := e9.parent.Walk(wnames) + if err != nil { + return nil, err + } + var ( + name = path.Join(e9.parentName, e9.Dirent.Name) + errs []error + ) + info, err := getInfoGo(name, file) + if err != nil { + errs = append(errs, err) + } + if err := file.Close(); err != nil { + errs = append(errs, err) + } + return info, errors.Join(errs...) +} + +func getInfoGo(name string, file p9.File) (*plan9Info, error) { + return getInfo(name, file, p9.AttrMask{ + Mode: true, + Size: true, + MTime: true, + }) +} + +func getInfo(name string, file p9.File, want p9.AttrMask) (*plan9Info, error) { + _, valid, attr, err := file.GetAttr(want) + const op = "stat" + if err == nil && + !valid.Contains(want) { + err = attrErr(valid, want) + } + if err != nil { + return nil, fserrors.New(op, name, err, fserrors.IO) + } + return &plan9Info{ + name: name, + attr: &attr, + }, nil +} + +func (lio *lazyIO) initAndSwapIO() (p9.File, error) { + container := lio.container + _, clone, err := container.walkFID.Walk(nil) + if err != nil { + return nil, err + } + if _, _, err := clone.Open(lio.OpenFlags); err != nil { + if cErr := clone.Close(); cErr != nil { + return nil, errors.Join(err, cErr) + } + return nil, err + } + container.ioFID = clone + return clone, nil +} + +func (lio *lazyIO) ReadAt(p []byte, offset int64) (int, error) { + file, err := lio.initAndSwapIO() + if err != nil { + return -1, err + } + return file.ReadAt(p, offset) +} + +func (lio *lazyIO) Readdir(offset uint64, count uint32) (p9.Dirents, error) { + file, err := lio.initAndSwapIO() + if err != nil { + return nil, err + } + return file.Readdir(offset, count) +} diff --git a/internal/filesystem/9p/multiaddr.go b/internal/filesystem/9p/multiaddr.go new file mode 100644 index 00000000..38b3eb30 --- /dev/null +++ b/internal/filesystem/9p/multiaddr.go @@ -0,0 +1,53 @@ +package p9 + +import ( + "encoding/json" + + "github.com/multiformats/go-multiaddr" +) + +type multiaddrContainer struct{ multiaddr.Multiaddr } + +func (mc *multiaddrContainer) MarshalBinary() ([]byte, error) { + if maddr := mc.Multiaddr; maddr != nil { + return maddr.MarshalBinary() + } + return []byte{}, nil +} + +func (mc *multiaddrContainer) UnmarshalBinary(b []byte) (err error) { + mc.Multiaddr, err = multiaddr.NewMultiaddrBytes(b) + return +} + +func (mc *multiaddrContainer) MarshalText() ([]byte, error) { + if maddr := mc.Multiaddr; maddr != nil { + return maddr.MarshalText() + } + return []byte{}, nil +} + +func (mc *multiaddrContainer) UnmarshalText(b []byte) (err error) { + mc.Multiaddr, err = multiaddr.NewMultiaddr(string(b)) + return +} + +func (mc *multiaddrContainer) MarshalJSON() ([]byte, error) { + if maddr := mc.Multiaddr; maddr != nil { + return maddr.MarshalJSON() + } + return []byte("null"), nil +} + +func (mc *multiaddrContainer) UnmarshalJSON(b []byte) error { + var mcStr string + if err := json.Unmarshal(b, &mcStr); err != nil { + return err + } + maddr, err := multiaddr.NewMultiaddr(mcStr) + if err != nil { + return err + } + mc.Multiaddr = maddr + return nil +} diff --git a/internal/filesystem/9p/server.go b/internal/filesystem/9p/server.go new file mode 100644 index 00000000..97c978a7 --- /dev/null +++ b/internal/filesystem/9p/server.go @@ -0,0 +1,363 @@ +package p9 + +import ( + "context" + "encoding/json" + "errors" + "hash/maphash" + "io" + "io/fs" + "log" + "os" + "path" + "time" + "unsafe" + + "github.com/djdv/go-filesystem-utils/internal/filesystem" + "github.com/djdv/go-filesystem-utils/internal/generic" + p9net "github.com/djdv/go-filesystem-utils/internal/net/9p" + perrors "github.com/djdv/p9/errors" + "github.com/djdv/p9/fsimpl/templatefs" + "github.com/djdv/p9/p9" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +type ( + Host struct { + Maddr multiaddr.Multiaddr `json:"maddr,omitempty"` + ShutdownTimeout time.Duration `json:"shutdownTimeout,omitempty"` + } + goAttacher struct { + fsys fs.FS + maphash.Hash + } + goFile struct { + openFlags + templatefs.NoopFile + fsys fs.FS + file fs.File + names []string + p9.QID // TODO: the path value for this isn't spec compliant + // "The path is an integer unique among all files in the hierarchy. If a file is deleted and recreated with the same name in the same directory, the old and new path components of the qids should be different." intro (5) + // We can keep track of changes /we/ make + // and modify some path salt + // (global map[paths-hash]atomicInt |> hasher.append) + // but since `fs.FS` has no unique number like path, ino, etc. + // or even creation date, we won't know if someone else + // created a new file with the same path-names. + // tracking ops+birthtime will be best effort. + cursor uint64 + hashSeed maphash.Seed + } +) + +const HostID filesystem.Host = "9P" + +func (*Host) HostID() filesystem.Host { return HostID } + +func (h9 *Host) UnmarshalJSON(b []byte) error { + // multiformats/go-multiaddr issue #100 + var maddrWorkaround struct { + Maddr multiaddrContainer `json:"maddr,omitempty"` + } + if err := json.Unmarshal(b, &maddrWorkaround); err != nil { + return err + } + h9.Maddr = maddrWorkaround.Maddr.Multiaddr + return nil +} + +func (h9 *Host) Mount(fsys fs.FS) (io.Closer, error) { + listener, err := manet.Listen(h9.Maddr) + if err != nil { + return nil, err + } + attacher := &goAttacher{ + fsys: fsys, + } + var ( + l = log.New(os.Stdout, "srv9 ", log.Lshortfile) + // TODO: opts passthrough. + options = []p9net.ServerOpt{ + p9net.WithServerLogger(l), + } + server = p9net.NewServer(attacher, options...) + srvErr = make(chan error, 1) + ) + go func() { + defer close(srvErr) + err := server.Serve(listener) + if err == nil || + errors.Is(err, p9net.ErrServerClosed) { + return + } + srvErr <- err + }() + if h9.ShutdownTimeout == 0 { + return generic.Closer(server.Close), nil + } + var closer generic.Closer = func() error { + ctx, cancel := context.WithTimeout( + context.Background(), + h9.ShutdownTimeout, + ) + defer cancel() + return errors.Join( + server.Shutdown(ctx), + <-srvErr, + ) + } + return closer, nil +} + +func (a9 *goAttacher) Attach() (p9.File, error) { + return &goFile{ + fsys: a9.fsys, + QID: p9.QID{ + Type: p9.TypeDir, + Path: a9.Hash.Sum64(), + }, + hashSeed: a9.Hash.Seed(), + }, nil +} + +func (f9 *goFile) goName(names ...string) string { + if len(f9.names) == 0 { + return filesystem.Root + } + return path.Join(append(f9.names, names...)...) +} + +func (f9 *goFile) makeHasher() (hasher maphash.Hash, err error) { + hasher.SetSeed(f9.hashSeed) + err = f9.hashNames(&hasher) + return +} + +func (f9 *goFile) hashNames(hasher *maphash.Hash) error { + for _, name := range f9.names { + if _, err := hasher.WriteString(name); err != nil { + return err + } + } + return nil +} + +func (f9 *goFile) Walk(names []string) ([]p9.QID, p9.File, error) { + if len(names) == 0 { + if f9.opened() { + return nil, nil, fidOpenedErr + } + file := &goFile{ + fsys: f9.fsys, + hashSeed: f9.hashSeed, + QID: f9.QID, + names: f9.names, + } + return nil, file, nil + } + hasher, err := f9.makeHasher() + if err != nil { + return nil, nil, err + } + qids := make([]p9.QID, len(names)) + for i, name := range names { + info, err := fs.Stat(f9.fsys, f9.goName(names[:i+1]...)) + if err != nil { + return qids[:i], nil, err + } + if _, err := hasher.WriteString(name); err != nil { + return qids[:i], nil, err + } + qids[i] = p9.QID{ + Type: goToQIDType(info.Mode().Type()), + Path: hasher.Sum64(), + } + } + file := &goFile{ + fsys: f9.fsys, + hashSeed: f9.hashSeed, + QID: qids[len(qids)-1], + names: append(f9.names, names...), + } + return qids, file, nil +} + +func goToQIDType(typ fs.FileMode) p9.QIDType { + switch typ { + default: + return p9.TypeRegular + case fs.ModeDir: + return p9.TypeDir + case fs.ModeAppend: + return p9.TypeAppendOnly + case fs.ModeExclusive: + return p9.TypeExclusive + case fs.ModeTemporary: + return p9.TypeTemporary + case fs.ModeSymlink: + return p9.TypeSymlink + } +} + +func (f9 *goFile) GetAttr(req p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) { + var ( + attr p9.Attr + valid p9.AttrMask + info, err = fs.Stat(f9.fsys, f9.goName()) + ) + if err != nil { + return f9.QID, valid, attr, err + } + attr.Mode, valid.Mode = p9.ModeFromOS(info.Mode()), true + attr.Size, valid.Size = uint64(info.Size()), true + var ( + modTime = info.ModTime() + mSec = uint64(modTime.Unix()) + mNSec = uint64(modTime.UnixNano()) + ) + attr.MTimeSeconds, attr.MTimeNanoSeconds, + valid.MTime = mSec, mNSec, true + if atimer, ok := info.(filesystem.AccessTimeInfo); ok { + var ( + accessTime = atimer.AccessTime() + aSec = uint64(accessTime.Unix()) + aNSec = uint64(accessTime.UnixNano()) + ) + attr.ATimeSeconds, attr.ATimeNanoSeconds, + valid.ATime = aSec, aNSec, true + } + if ctimer, ok := info.(filesystem.ChangeTimeInfo); ok { + var ( + changeTime = ctimer.ChangeTime() + cSec = uint64(changeTime.Unix()) + cNSec = uint64(changeTime.UnixNano()) + ) + attr.CTimeSeconds, attr.CTimeNanoSeconds, + valid.CTime = cSec, cNSec, true + } + if crtimer, ok := info.(filesystem.CreationTimeInfo); ok { + var ( + birthTime = crtimer.CreationTime() + bSec = uint64(birthTime.Unix()) + bNSec = uint64(birthTime.UnixNano()) + ) + attr.BTimeSeconds, attr.BTimeNanoSeconds, + valid.BTime = bSec, bNSec, true + } + return f9.QID, valid, attr, nil +} + +func (f9 *goFile) Open(mode p9.OpenFlags) (p9.QID, ioUnit, error) { + if f9.opened() { + return f9.QID, 0, perrors.EINVAL + } + var ( + file fs.File + err error + name = f9.goName() + ) + if mode.Mode() == p9.ReadOnly { + file, err = f9.fsys.Open(name) + } else { + opener, ok := f9.fsys.(filesystem.OpenFileFS) + if !ok { + return f9.QID, 0, perrors.EROFS + } + // TODO: mode conversion - 9P.L to OS independent representation + file, err = opener.OpenFile(name, int(mode), 0) + } + if err != nil { + return p9.QID{}, 0, err + } + f9.file = file + f9.openFlags = f9.withOpenedFlag(mode) + return f9.QID, noIOUnit, nil +} + +func (f9 *goFile) Readdir(offset uint64, count uint32) (p9.Dirents, error) { + if !f9.canRead() { + return nil, perrors.EBADF + } + directory, ok := f9.file.(fs.ReadDirFile) + if !ok { + return nil, perrors.ENOTDIR + } + if offset != f9.cursor { + return nil, perrors.ENOENT + } + const entrySize = unsafe.Sizeof(p9.Dirent{}) + countGo := int(count / uint32(entrySize)) // Bytes -> index. + ents, err := directory.ReadDir(countGo) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + var ( + entryOffset = f9.cursor + 1 + entryCount = len(ents) + ) + f9.cursor += uint64(entryCount) + entries := make(p9.Dirents, entryCount) + hasher, err := f9.makeHasher() + if err != nil { + return nil, err + } + end := entryCount - 1 + for i, ent := range ents { + var ( + name = ent.Name() + typ = goToQIDType(ent.Type()) + ) + if _, err := hasher.WriteString(name); err != nil { + return nil, err + } + entries[i] = p9.Dirent{ + Name: name, + QID: p9.QID{ + Type: typ, + Path: hasher.Sum64(), + }, + Offset: entryOffset, + Type: typ, + } + if i == end { + break + } + entryOffset++ + hasher.Reset() + if err := f9.hashNames(&hasher); err != nil { + return nil, err + } + } + return entries, nil +} + +func (f9 *goFile) ReadAt(p []byte, offset int64) (int, error) { + if !f9.canRead() { + return -1, perrors.EBADF + } + var ( + file = f9.file + seeker, ok = file.(io.Seeker) + ) + if !ok { + return -1, perrors.ESPIPE + } + if _, err := seeker.Seek(offset, io.SeekStart); err != nil { + return -1, err + } + return file.Read(p) +} + +func (f9 *goFile) Close() error { + f9.openFlags = 0 + if file := f9.file; file != nil { + f9.file = nil + return file.Close() + } + return nil +}