Skip to content

Commit 873594d

Browse files
committed
9p: add 9P2000.L fs.FS guest support
1 parent 604993b commit 873594d

File tree

2 files changed

+408
-0
lines changed

2 files changed

+408
-0
lines changed

internal/filesystem/9p/client.go

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
package p9
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"io"
7+
"io/fs"
8+
"path"
9+
"strings"
10+
"time"
11+
"unsafe"
12+
13+
"github.com/djdv/go-filesystem-utils/internal/filesystem"
14+
fserrors "github.com/djdv/go-filesystem-utils/internal/filesystem/errors"
15+
"github.com/djdv/go-filesystem-utils/internal/generic"
16+
perrors "github.com/djdv/p9/errors"
17+
"github.com/djdv/p9/fsimpl/templatefs"
18+
"github.com/djdv/p9/p9"
19+
"github.com/multiformats/go-multiaddr"
20+
manet "github.com/multiformats/go-multiaddr/net"
21+
)
22+
23+
type (
24+
Guest struct {
25+
Maddr multiaddr.Multiaddr `json:"maddr,omitempty"`
26+
}
27+
plan9FS struct {
28+
client *p9.Client
29+
root p9.File
30+
}
31+
plan9File struct {
32+
walkFID, ioFID p9.File
33+
name string
34+
cursor int64
35+
}
36+
lazyIO struct {
37+
templatefs.NoopFile
38+
container *plan9File
39+
p9.OpenFlags
40+
}
41+
plan9Info struct {
42+
attr *p9.Attr
43+
name string
44+
}
45+
plan9Entry struct {
46+
*p9.Dirent
47+
parent p9.File
48+
parentName string
49+
}
50+
)
51+
52+
var (
53+
_ fs.FS = (*plan9FS)(nil)
54+
_ fs.StatFS = (*plan9FS)(nil)
55+
_ filesystem.IDFS = (*plan9FS)(nil)
56+
_ fs.File = (*plan9File)(nil)
57+
_ fs.FileInfo = (*plan9Info)(nil)
58+
)
59+
60+
const (
61+
GuestID filesystem.ID = "9P"
62+
pathSeparatorGo = "/"
63+
)
64+
65+
func (*Guest) GuestID() filesystem.ID { return GuestID }
66+
func (g9 *Guest) MakeFS() (fs.FS, error) {
67+
conn, err := manet.Dial(g9.Maddr)
68+
if err != nil {
69+
return nil, err
70+
}
71+
return NewPlan9Guest(conn)
72+
}
73+
74+
// TODO: Options:
75+
// - Client log
76+
func NewPlan9Guest(channel io.ReadWriteCloser) (*plan9FS, error) {
77+
client, err := p9.NewClient(channel)
78+
if err != nil {
79+
return nil, err
80+
}
81+
root, err := client.Attach("")
82+
if err != nil {
83+
return nil, err
84+
}
85+
fsys := plan9FS{
86+
client: client,
87+
root: root,
88+
}
89+
return &fsys, nil
90+
}
91+
92+
func (*plan9FS) ID() filesystem.ID { return GuestID }
93+
94+
func (fsys *plan9FS) Stat(name string) (fs.FileInfo, error) {
95+
const op = "stat"
96+
file, err := fsys.walkTo(op, name)
97+
if err != nil {
98+
return nil, err
99+
}
100+
info, err := getInfoGo(name, file)
101+
if err := errors.Join(err, file.Close()); err != nil {
102+
return nil, fserrors.New(op, name, err, fserrors.IO)
103+
}
104+
return info, err
105+
}
106+
107+
func (fsys *plan9FS) walkTo(op, name string) (p9.File, error) {
108+
if !fs.ValidPath(name) {
109+
return nil, fserrors.New(op, name, filesystem.ErrPath, fserrors.InvalidItem)
110+
}
111+
var names []string
112+
if name != filesystem.Root {
113+
names = strings.Split(name, pathSeparatorGo)
114+
}
115+
_, file, err := fsys.root.Walk(names)
116+
if err != nil {
117+
var kind fserrors.Kind
118+
if errors.Is(err, perrors.ENOENT) {
119+
kind = fserrors.NotExist
120+
} else {
121+
kind = fserrors.IO
122+
}
123+
return nil, fserrors.New(op, name, err, kind)
124+
}
125+
return file, nil
126+
}
127+
128+
func (fsys *plan9FS) Open(name string) (fs.File, error) {
129+
const op = "open"
130+
walkFID, err := fsys.walkTo(op, name)
131+
if err != nil {
132+
return nil, err
133+
}
134+
var wrapper plan9File //🥚 Self referential.
135+
wrapper = plan9File{
136+
name: name,
137+
walkFID: walkFID,
138+
ioFID: &lazyIO{
139+
container: &wrapper, // 🐣 Self referential.
140+
OpenFlags: p9.ReadOnly,
141+
},
142+
}
143+
return &wrapper, nil
144+
}
145+
146+
func (fsys *plan9FS) Close() error {
147+
var errs []error
148+
for _, closer := range []io.Closer{
149+
fsys.root,
150+
fsys.client,
151+
} {
152+
if err := closer.Close(); err != nil {
153+
errs = append(errs, err)
154+
}
155+
}
156+
return errors.Join(errs...)
157+
}
158+
159+
func (f9 *plan9File) Stat() (fs.FileInfo, error) {
160+
return getInfoGo(f9.name, f9.walkFID)
161+
}
162+
163+
func (f9 *plan9File) Read(p []byte) (int, error) {
164+
n, err := f9.ioFID.ReadAt(p, f9.cursor)
165+
if err == nil {
166+
f9.cursor += int64(n)
167+
}
168+
return n, err
169+
}
170+
171+
func (f9 *plan9File) ReadDir(count int) ([]fs.DirEntry, error) {
172+
const entrySize = unsafe.Sizeof(p9.Dirent{})
173+
count9 := count * int(entrySize) // Index -> bytes.
174+
entries9, err := f9.ioFID.Readdir(uint64(f9.cursor), uint32(count9))
175+
if err != nil {
176+
return nil, err
177+
}
178+
limit := len(entries9)
179+
if limit == 0 && count > 0 {
180+
return nil, io.EOF
181+
}
182+
if count > 0 && limit > count {
183+
limit = count
184+
}
185+
var (
186+
entries = make([]fs.DirEntry, limit)
187+
parent = f9.walkFID
188+
parentName = f9.name
189+
)
190+
for i := range entries {
191+
entries[i] = plan9Entry{
192+
parent: parent,
193+
parentName: parentName,
194+
Dirent: &entries9[i],
195+
}
196+
}
197+
f9.cursor += int64(len(entries))
198+
return entries, nil
199+
}
200+
201+
func (f9 *plan9File) Seek(offset int64, whence int) (int64, error) {
202+
const op = "seek"
203+
switch whence {
204+
case io.SeekStart:
205+
if offset < 0 {
206+
err := generic.ConstError(
207+
"tried to seek to a position before the beginning of the file",
208+
)
209+
return -1, fserrors.New(
210+
op, f9.name,
211+
err, fserrors.InvalidItem,
212+
)
213+
}
214+
f9.cursor = offset
215+
case io.SeekCurrent:
216+
f9.cursor += offset
217+
case io.SeekEnd:
218+
var (
219+
want = p9.AttrMask{Size: true}
220+
info, err = getInfo(f9.name, f9.walkFID, want)
221+
)
222+
if err != nil {
223+
return -1, err
224+
}
225+
end := info.attr.Size
226+
f9.cursor = int64(end) + offset
227+
}
228+
return f9.cursor, nil
229+
}
230+
231+
func (f9 *plan9File) Close() error {
232+
return errors.Join(
233+
f9.ioFID.Close(),
234+
f9.walkFID.Close(),
235+
)
236+
}
237+
238+
func (i9 *plan9Info) Name() string { return i9.name }
239+
func (i9 *plan9Info) Size() int64 { return int64(i9.attr.Size) }
240+
func (i9 *plan9Info) Mode() fs.FileMode { return i9.attr.Mode.OSMode() }
241+
func (i9 *plan9Info) ModTime() time.Time {
242+
return time.Unix(0, int64(i9.attr.MTimeNanoSeconds))
243+
}
244+
func (i9 *plan9Info) IsDir() bool { return i9.Mode().IsDir() }
245+
246+
func (i9 *plan9Info) Sys() any { return i9 }
247+
248+
func (g9 *Guest) UnmarshalJSON(b []byte) error {
249+
// multiformats/go-multiaddr issue #100
250+
var maddrWorkaround struct {
251+
Maddr multiaddrContainer `json:"maddr,omitempty"`
252+
}
253+
if err := json.Unmarshal(b, &maddrWorkaround); err != nil {
254+
return err
255+
}
256+
g9.Maddr = maddrWorkaround.Maddr.Multiaddr
257+
return nil
258+
}
259+
260+
func (e9 plan9Entry) Name() string { return e9.Dirent.Name }
261+
func (e9 plan9Entry) IsDir() bool { return e9.Dirent.Type == p9.TypeDir }
262+
func (e9 plan9Entry) Type() fs.FileMode {
263+
switch e9.Dirent.Type {
264+
case p9.TypeRegular:
265+
return fs.FileMode(0)
266+
case p9.TypeDir:
267+
return fs.ModeDir
268+
case p9.TypeAppendOnly:
269+
return fs.ModeAppend
270+
case p9.TypeExclusive:
271+
return fs.ModeExclusive
272+
case p9.TypeTemporary:
273+
return fs.ModeTemporary
274+
case p9.TypeSymlink:
275+
return fs.ModeSymlink
276+
default:
277+
return fs.ModeIrregular
278+
}
279+
}
280+
281+
func (e9 plan9Entry) Info() (fs.FileInfo, error) {
282+
wnames := []string{e9.Dirent.Name}
283+
_, file, err := e9.parent.Walk(wnames)
284+
if err != nil {
285+
return nil, err
286+
}
287+
var (
288+
name = path.Join(e9.parentName, e9.Dirent.Name)
289+
errs []error
290+
)
291+
info, err := getInfoGo(name, file)
292+
if err != nil {
293+
errs = append(errs, err)
294+
}
295+
if err := file.Close(); err != nil {
296+
errs = append(errs, err)
297+
}
298+
return info, errors.Join(errs...)
299+
}
300+
301+
func getInfoGo(name string, file p9.File) (*plan9Info, error) {
302+
return getInfo(name, file, p9.AttrMask{
303+
Mode: true,
304+
Size: true,
305+
MTime: true,
306+
})
307+
}
308+
309+
func getInfo(name string, file p9.File, want p9.AttrMask) (*plan9Info, error) {
310+
_, valid, attr, err := file.GetAttr(want)
311+
const op = "stat"
312+
if err == nil &&
313+
!valid.Contains(want) {
314+
err = attrErr(valid, want)
315+
}
316+
if err != nil {
317+
return nil, fserrors.New(op, name, err, fserrors.IO)
318+
}
319+
return &plan9Info{
320+
name: name,
321+
attr: &attr,
322+
}, nil
323+
}
324+
325+
func (lio *lazyIO) initAndSwapIO() (p9.File, error) {
326+
container := lio.container
327+
_, clone, err := container.walkFID.Walk(nil)
328+
if err != nil {
329+
return nil, err
330+
}
331+
if _, _, err := clone.Open(lio.OpenFlags); err != nil {
332+
if cErr := clone.Close(); cErr != nil {
333+
return nil, errors.Join(err, cErr)
334+
}
335+
return nil, err
336+
}
337+
container.ioFID = clone
338+
return clone, nil
339+
}
340+
341+
func (lio *lazyIO) ReadAt(p []byte, offset int64) (int, error) {
342+
file, err := lio.initAndSwapIO()
343+
if err != nil {
344+
return -1, err
345+
}
346+
return file.ReadAt(p, offset)
347+
}
348+
349+
func (lio *lazyIO) Readdir(offset uint64, count uint32) (p9.Dirents, error) {
350+
file, err := lio.initAndSwapIO()
351+
if err != nil {
352+
return nil, err
353+
}
354+
return file.Readdir(offset, count)
355+
}

0 commit comments

Comments
 (0)