Skip to content

Commit 8f5e41f

Browse files
committed
9p: add 9P2000.L fs.FS host support
1 parent c692c91 commit 8f5e41f

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed

internal/filesystem/9p/server.go

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
package p9
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"hash/maphash"
8+
"io"
9+
"io/fs"
10+
"log"
11+
"os"
12+
"path"
13+
"time"
14+
"unsafe"
15+
16+
"github.com/djdv/go-filesystem-utils/internal/filesystem"
17+
"github.com/djdv/go-filesystem-utils/internal/generic"
18+
p9net "github.com/djdv/go-filesystem-utils/internal/net/9p"
19+
perrors "github.com/djdv/p9/errors"
20+
"github.com/djdv/p9/fsimpl/templatefs"
21+
"github.com/djdv/p9/p9"
22+
"github.com/multiformats/go-multiaddr"
23+
manet "github.com/multiformats/go-multiaddr/net"
24+
)
25+
26+
type (
27+
Host struct {
28+
Maddr multiaddr.Multiaddr `json:"maddr,omitempty"`
29+
ShutdownTimeout time.Duration `json:"shutdownTimeout,omitempty"`
30+
}
31+
goAttacher struct {
32+
fsys fs.FS
33+
maphash.Hash
34+
}
35+
goFile struct {
36+
openFlags
37+
templatefs.NoopFile
38+
fsys fs.FS
39+
file fs.File
40+
names []string
41+
p9.QID // TODO: the path value for this isn't spec compliant
42+
// "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)
43+
// We can keep track of changes /we/ make
44+
// and modify some path salt
45+
// (global map[paths-hash]atomicInt |> hasher.append)
46+
// but since `fs.FS` has no unique number like path, ino, etc.
47+
// or even creation date, we won't know if someone else
48+
// created a new file with the same path-names.
49+
// tracking ops+birthtime will be best effort.
50+
cursor uint64
51+
hashSeed maphash.Seed
52+
}
53+
)
54+
55+
const HostID filesystem.Host = "9P"
56+
57+
func (*Host) HostID() filesystem.Host { return HostID }
58+
59+
func (h9 *Host) UnmarshalJSON(b []byte) error {
60+
// multiformats/go-multiaddr issue #100
61+
var maddrWorkaround struct {
62+
Maddr multiaddrContainer `json:"maddr,omitempty"`
63+
}
64+
if err := json.Unmarshal(b, &maddrWorkaround); err != nil {
65+
return err
66+
}
67+
h9.Maddr = maddrWorkaround.Maddr.Multiaddr
68+
return nil
69+
}
70+
71+
func (h9 *Host) Mount(fsys fs.FS) (io.Closer, error) {
72+
listener, err := manet.Listen(h9.Maddr)
73+
if err != nil {
74+
return nil, err
75+
}
76+
attacher := &goAttacher{
77+
fsys: fsys,
78+
}
79+
var (
80+
l = log.New(os.Stdout, "srv9 ", log.Lshortfile)
81+
// TODO: opts passthrough.
82+
options = []p9net.ServerOpt{
83+
p9net.WithServerLogger(l),
84+
}
85+
server = p9net.NewServer(attacher, options...)
86+
srvErr = make(chan error, 1)
87+
)
88+
go func() {
89+
defer close(srvErr)
90+
err := server.Serve(listener)
91+
if err == nil ||
92+
errors.Is(err, p9net.ErrServerClosed) {
93+
return
94+
}
95+
srvErr <- err
96+
}()
97+
if h9.ShutdownTimeout == 0 {
98+
return generic.Closer(server.Close), nil
99+
}
100+
var closer generic.Closer = func() error {
101+
ctx, cancel := context.WithTimeout(
102+
context.Background(),
103+
h9.ShutdownTimeout,
104+
)
105+
defer cancel()
106+
return errors.Join(
107+
server.Shutdown(ctx),
108+
<-srvErr,
109+
)
110+
}
111+
return closer, nil
112+
}
113+
114+
func (a9 *goAttacher) Attach() (p9.File, error) {
115+
return &goFile{
116+
fsys: a9.fsys,
117+
QID: p9.QID{
118+
Type: p9.TypeDir,
119+
Path: a9.Hash.Sum64(),
120+
},
121+
hashSeed: a9.Hash.Seed(),
122+
}, nil
123+
}
124+
125+
func (f9 *goFile) goName(names ...string) string {
126+
if len(f9.names) == 0 {
127+
return filesystem.Root
128+
}
129+
return path.Join(append(f9.names, names...)...)
130+
}
131+
132+
func (f9 *goFile) makeHasher() (hasher maphash.Hash, err error) {
133+
hasher.SetSeed(f9.hashSeed)
134+
err = f9.hashNames(&hasher)
135+
return
136+
}
137+
138+
func (f9 *goFile) hashNames(hasher *maphash.Hash) error {
139+
for _, name := range f9.names {
140+
if _, err := hasher.WriteString(name); err != nil {
141+
return err
142+
}
143+
}
144+
return nil
145+
}
146+
147+
func (f9 *goFile) Walk(names []string) ([]p9.QID, p9.File, error) {
148+
if len(names) == 0 {
149+
if f9.opened() {
150+
return nil, nil, fidOpenedErr
151+
}
152+
file := &goFile{
153+
fsys: f9.fsys,
154+
hashSeed: f9.hashSeed,
155+
QID: f9.QID,
156+
names: f9.names,
157+
}
158+
return nil, file, nil
159+
}
160+
hasher, err := f9.makeHasher()
161+
if err != nil {
162+
return nil, nil, err
163+
}
164+
qids := make([]p9.QID, len(names))
165+
for i, name := range names {
166+
info, err := fs.Stat(f9.fsys, f9.goName(names[:i+1]...))
167+
if err != nil {
168+
return qids[:i], nil, err
169+
}
170+
if _, err := hasher.WriteString(name); err != nil {
171+
return qids[:i], nil, err
172+
}
173+
qids[i] = p9.QID{
174+
Type: goToQIDType(info.Mode().Type()),
175+
Path: hasher.Sum64(),
176+
}
177+
}
178+
file := &goFile{
179+
fsys: f9.fsys,
180+
hashSeed: f9.hashSeed,
181+
QID: qids[len(qids)-1],
182+
names: append(f9.names, names...),
183+
}
184+
return qids, file, nil
185+
}
186+
187+
func goToQIDType(typ fs.FileMode) p9.QIDType {
188+
switch typ {
189+
default:
190+
return p9.TypeRegular
191+
case fs.ModeDir:
192+
return p9.TypeDir
193+
case fs.ModeAppend:
194+
return p9.TypeAppendOnly
195+
case fs.ModeExclusive:
196+
return p9.TypeExclusive
197+
case fs.ModeTemporary:
198+
return p9.TypeTemporary
199+
case fs.ModeSymlink:
200+
return p9.TypeSymlink
201+
}
202+
}
203+
204+
func (f9 *goFile) GetAttr(req p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) {
205+
var (
206+
attr p9.Attr
207+
valid p9.AttrMask
208+
info, err = fs.Stat(f9.fsys, f9.goName())
209+
)
210+
if err != nil {
211+
return f9.QID, valid, attr, err
212+
}
213+
attr.Mode, valid.Mode = p9.ModeFromOS(info.Mode()), true
214+
attr.Size, valid.Size = uint64(info.Size()), true
215+
var (
216+
modTime = info.ModTime()
217+
mSec = uint64(modTime.Unix())
218+
mNSec = uint64(modTime.UnixNano())
219+
)
220+
attr.MTimeSeconds, attr.MTimeNanoSeconds,
221+
valid.MTime = mSec, mNSec, true
222+
if atimer, ok := info.(filesystem.AccessTimeInfo); ok {
223+
var (
224+
accessTime = atimer.AccessTime()
225+
aSec = uint64(accessTime.Unix())
226+
aNSec = uint64(accessTime.UnixNano())
227+
)
228+
attr.ATimeSeconds, attr.ATimeNanoSeconds,
229+
valid.ATime = aSec, aNSec, true
230+
}
231+
if ctimer, ok := info.(filesystem.ChangeTimeInfo); ok {
232+
var (
233+
changeTime = ctimer.ChangeTime()
234+
cSec = uint64(changeTime.Unix())
235+
cNSec = uint64(changeTime.UnixNano())
236+
)
237+
attr.CTimeSeconds, attr.CTimeNanoSeconds,
238+
valid.CTime = cSec, cNSec, true
239+
}
240+
if crtimer, ok := info.(filesystem.CreationTimeInfo); ok {
241+
var (
242+
birthTime = crtimer.CreationTime()
243+
bSec = uint64(birthTime.Unix())
244+
bNSec = uint64(birthTime.UnixNano())
245+
)
246+
attr.BTimeSeconds, attr.BTimeNanoSeconds,
247+
valid.BTime = bSec, bNSec, true
248+
}
249+
return f9.QID, valid, attr, nil
250+
}
251+
252+
func (f9 *goFile) Open(mode p9.OpenFlags) (p9.QID, ioUnit, error) {
253+
if f9.opened() {
254+
return f9.QID, 0, perrors.EINVAL
255+
}
256+
var (
257+
file fs.File
258+
err error
259+
name = f9.goName()
260+
)
261+
if mode.Mode() == p9.ReadOnly {
262+
file, err = f9.fsys.Open(name)
263+
} else {
264+
opener, ok := f9.fsys.(filesystem.OpenFileFS)
265+
if !ok {
266+
return f9.QID, 0, perrors.EROFS
267+
}
268+
// TODO: mode conversion - 9P.L to OS independent representation
269+
file, err = opener.OpenFile(name, int(mode), 0)
270+
}
271+
if err != nil {
272+
return p9.QID{}, 0, err
273+
}
274+
f9.file = file
275+
f9.openFlags = f9.withOpenedFlag(mode)
276+
return f9.QID, noIOUnit, nil
277+
}
278+
279+
func (f9 *goFile) Readdir(offset uint64, count uint32) (p9.Dirents, error) {
280+
if !f9.canRead() {
281+
return nil, perrors.EBADF
282+
}
283+
directory, ok := f9.file.(fs.ReadDirFile)
284+
if !ok {
285+
return nil, perrors.ENOTDIR
286+
}
287+
if offset != f9.cursor {
288+
return nil, perrors.ENOENT
289+
}
290+
const entrySize = unsafe.Sizeof(p9.Dirent{})
291+
countGo := int(count / uint32(entrySize)) // Bytes -> index.
292+
ents, err := directory.ReadDir(countGo)
293+
if err != nil {
294+
if errors.Is(err, io.EOF) {
295+
err = nil
296+
}
297+
return nil, err
298+
}
299+
var (
300+
entryOffset = f9.cursor + 1
301+
entryCount = len(ents)
302+
)
303+
f9.cursor += uint64(entryCount)
304+
entries := make(p9.Dirents, entryCount)
305+
hasher, err := f9.makeHasher()
306+
if err != nil {
307+
return nil, err
308+
}
309+
end := entryCount - 1
310+
for i, ent := range ents {
311+
var (
312+
name = ent.Name()
313+
typ = goToQIDType(ent.Type())
314+
)
315+
if _, err := hasher.WriteString(name); err != nil {
316+
return nil, err
317+
}
318+
entries[i] = p9.Dirent{
319+
Name: name,
320+
QID: p9.QID{
321+
Type: typ,
322+
Path: hasher.Sum64(),
323+
},
324+
Offset: entryOffset,
325+
Type: typ,
326+
}
327+
if i == end {
328+
break
329+
}
330+
entryOffset++
331+
hasher.Reset()
332+
if err := f9.hashNames(&hasher); err != nil {
333+
return nil, err
334+
}
335+
}
336+
return entries, nil
337+
}
338+
339+
func (f9 *goFile) ReadAt(p []byte, offset int64) (int, error) {
340+
if !f9.canRead() {
341+
return -1, perrors.EBADF
342+
}
343+
var (
344+
file = f9.file
345+
seeker, ok = file.(io.Seeker)
346+
)
347+
if !ok {
348+
return -1, perrors.ESPIPE
349+
}
350+
if _, err := seeker.Seek(offset, io.SeekStart); err != nil {
351+
return -1, err
352+
}
353+
return file.Read(p)
354+
}
355+
356+
func (f9 *goFile) Close() error {
357+
f9.openFlags = 0
358+
if file := f9.file; file != nil {
359+
f9.file = nil
360+
return file.Close()
361+
}
362+
return nil
363+
}

0 commit comments

Comments
 (0)