Skip to content

Commit

Permalink
plan9: add io/fs.FS implementation
Browse files Browse the repository at this point in the history
Tests run only on linux and need u9fs (https://github.com/unofficial-mirror/u9fs).
Runs with go test -tags u9fs
  • Loading branch information
anastasop authored and Spyros Anastasopoulos committed Aug 25, 2022
1 parent aab160e commit 065af96
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 0 deletions.
252 changes: 252 additions & 0 deletions plan9/fs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Package fs implements io/fs.FS for a 9p filesystem.
// An example is to run a local http.FileServer with a remote 9p system.
package fs

import (
"io"
"io/fs"
"strings"
"time"

"9fans.net/go/plan9"
"9fans.net/go/plan9/client"
)

// NewFS returns an io/fs.FS filesystem that wraps a 9p filesystem.
func NewFS(fsys *client.Fsys) fs.FS {
return fs9p{fsys: fsys}
}

// MountWithNames attaches to the 9p filesystem at network!addr with the
// provided user and attach names and wraps an io/fs.FS filesystem around it.
// Assumes the 9p server does not require Auth.
func MountWithNames(network, addr, uname, aname string) (fs.FS, error) {
c, err := client.Dial(network, addr)
if err != nil {
return nil, err
}
fsys, err := c.Attach(nil, uname, aname)
if err != nil {
c.Close()
}
return fs9p{fsys: fsys}, nil
}

// fs9p implements io/fs.FS.
type fs9p struct {
fsys *client.Fsys
}

func (f fs9p) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}

fid, err := f.fsys.Open(name, plan9.OREAD)
if err != nil {
// error detection is based on freebsd/u9fs
werr := err
if strings.Contains(werr.Error(), "Permission denied") {
werr = fs.ErrPermission
} else if strings.Contains(werr.Error(), "No such file or directory") {
werr = fs.ErrNotExist
}
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: werr,
}
}

if fid.Qid().Type&plan9.QTDIR > 0 {
return &dir9p{file9p: file9p{fid: fid}}, nil
}
return file9p{fid: fid}, nil
}

// file9p implements io/fs.File.
type file9p struct {
fid *client.Fid
}

func (f file9p) Stat() (fs.FileInfo, error) {
dir, err := f.fid.Stat()
if err != nil {
return nil, err
}
return fileInfo9p{sys: dir}, nil

}

func (f file9p) Read(b []byte) (int, error) {
return f.fid.Read(b)
}

func (f file9p) Close() error {
return f.fid.Close()
}

func (f file9p) ReadAt(p []byte, off int64) (n int, err error) {
return f.fid.ReadAt(p, off)
}

func (f file9p) Seek(offset int64, whence int) (int64, error) {
return f.fid.Seek(offset, whence)
}

// dir9p implements io/fs.ReadDirFile.
type dir9p struct {
file9p

dirsRead []*plan9.Dir // directories read ahead by dirread
}

func (d *dir9p) ReadDir(n int) ([]fs.DirEntry, error) {
var dirs []*plan9.Dir
var err error

if n <= 0 {
dirs, err = d.fid.Dirreadall()
dirs = append(d.dirsRead, dirs...) // preserve directory order
d.dirsRead = d.dirsRead[:0]
} else {
for err == nil && len(d.dirsRead) < n {
dirs, err = d.fid.Dirread()
d.dirsRead = append(d.dirsRead, dirs...) // preserve directory order
}
if len(d.dirsRead) >= n {
dirs = d.dirsRead[0:n]
d.dirsRead = d.dirsRead[n:]
} else {
dirs = d.dirsRead
d.dirsRead = d.dirsRead[:0]
}
}

if len(d.dirsRead) > 0 && err == io.EOF {
err = nil
}

entries := make([]fs.DirEntry, len(dirs))
for i, dir := range dirs {
entries[i] = dirEntry9p{sys: dir}
}
return entries, err
}

// fileInfo9p implements io/fs.FileInfo.
type fileInfo9p struct {
sys *plan9.Dir
}

func (f fileInfo9p) Name() string {
return f.sys.Name
}

func (f fileInfo9p) Size() int64 {
// for directories size is implementation defined. Use 0 for portability.
if f.sys.Mode&plan9.DMDIR > 0 {
return 0
}

// 9p uses uint64 but FileInfo uses int64. For most practical cases,
// the conversion it OK.
return int64(f.sys.Length)
}

func (f fileInfo9p) Mode() fs.FileMode {
// init mode to the permission bits and then set the others
mode := fs.FileMode(f.sys.Mode & 0777)
if f.sys.Mode&plan9.DMDIR > 0 {
mode |= fs.ModeDir
}
if f.sys.Mode&plan9.DMAPPEND > 0 {
mode |= fs.ModeAppend
}
if f.sys.Mode&plan9.DMEXCL > 0 {
mode |= fs.ModeExclusive
}
if f.sys.Mode&plan9.DMTMP > 0 {
mode |= fs.ModeTemporary
}

// The following are not defined by 9p (http://9p.io/sys/man/5/INDEX.html)
// but are defined by the plan9 client
if f.sys.Mode&plan9.DMSYMLINK > 0 {
mode |= fs.ModeSymlink
}
if f.sys.Mode&plan9.DMDEVICE > 0 {
mode |= fs.ModeDevice
}
if f.sys.Mode&plan9.DMNAMEDPIPE > 0 {
mode |= fs.ModeNamedPipe
}
if f.sys.Mode&plan9.DMSOCKET > 0 {
mode |= fs.ModeSocket
}
if f.sys.Mode&plan9.DMSETUID > 0 {
mode |= fs.ModeSetuid
}
if f.sys.Mode&plan9.DMSETGID > 0 {
mode |= fs.ModeSetgid
}

return mode
}

func (f fileInfo9p) ModTime() time.Time {
return time.Unix(int64(f.sys.Mtime), 0)
}

func (f fileInfo9p) IsDir() bool {
return f.sys.Mode&plan9.DMDIR > 0
}

func (f fileInfo9p) Sys() interface{} {
return f.sys
}

// dirEntry9p implements io/fs.DirEntry.
type dirEntry9p struct {
sys *plan9.Dir
}

func (d dirEntry9p) Name() string {
return d.sys.Name
}

func (d dirEntry9p) IsDir() bool {
return d.sys.Mode&plan9.DMDIR > 0
}

func (d dirEntry9p) Info() (fs.FileInfo, error) {
return fileInfo9p{sys: d.sys}, nil
}

func (d dirEntry9p) Type() fs.FileMode {
var mode fs.FileMode
if d.sys.Mode&plan9.DMDIR > 0 {
mode |= fs.ModeDir
}

// The following are not defined by 9p (http://9p.io/sys/man/5/INDEX.html)
// but are defined by the plan9 client
if d.sys.Mode&plan9.DMSYMLINK > 0 {
mode |= fs.ModeSymlink
}
if d.sys.Mode&plan9.DMDEVICE > 0 {
mode |= fs.ModeDevice
}
if d.sys.Mode&plan9.DMNAMEDPIPE > 0 {
mode |= fs.ModeNamedPipe
}
if d.sys.Mode&plan9.DMSOCKET > 0 {
mode |= fs.ModeSocket
}

return mode
}
66 changes: 66 additions & 0 deletions plan9/fs/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build u9fs && linux

package fs

import (
"flag"
"os"
"os/exec"
"path"
"strings"
"syscall"
"testing"
"testing/fstest"

"9fans.net/go/plan9/client"
"golang.org/x/sys/unix"
)

// tests an io.FS backedup by a 9p server. Needs u9fs https://github.com/unofficial-mirror/u9fs
// Use the root flag to test any folder, for example
// go test -tags u9fs -root /home/user/images -exp 'gopher.png,glenda.png' -timeout 0

var root = flag.String("root", "./testdata", "the root tree to check")
var exp = flag.String("exp", "fortunes.txt", "a comma separated list of files expected to find in root tree")

func TestFS(t *testing.T) {
execPath, err := exec.LookPath("u9fs")
check(t, err, "u9fs is not in PATH")

// create a socket pair to connect the 9p client with a u9fs serving root
fds, err := unix.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
check(t, err, "failed to create socket pair")
defer unix.Close(fds[0])
defer unix.Close(fds[1])

user := os.Getenv("USER")

// first init the server
srv := os.NewFile(uintptr(fds[1]), "srv")
cmd := exec.Cmd{
Path: execPath,
Args: []string{"u9fs", "-u", user, "-n", "-a", "none",
"-l", path.Join(t.TempDir(), "u9fs.log"), *root},
Stdin: srv,
Stdout: srv,
}
check(t, cmd.Start(), "failed to start u9fs")
defer cmd.Process.Kill()

// init the client last because server must be up to read Tversion
cli := os.NewFile(uintptr(fds[0]), "cli")
conn, err := client.NewConn(cli)
check(t, err, "failed to create client")
fsys, err := conn.Attach(nil, user, "")
check(t, err, "failed to attach client")

// create and check the filesystem
err = fstest.TestFS(NewFS(fsys), strings.Split(*exp, ",")...)
check(t, err, "FS test failed")
}

func check(t *testing.T, err error, msg string) {
if err != nil {
t.Fatalf("%s: %s", msg, err)
}
}
1 change: 1 addition & 0 deletions plan9/fs/testdata/a/fortunesA.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The whole UNIX operating system whose documentation fits in a chinese computer terminal!
1 change: 1 addition & 0 deletions plan9/fs/testdata/b/fortunesB.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A watched terminal never prints.
1 change: 1 addition & 0 deletions plan9/fs/testdata/fortunes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You should be automatically redirected to the new page in 0 seconds.

0 comments on commit 065af96

Please sign in to comment.