From 0e4651ed193fa029f0d596da6e2cac5bff2cb77c Mon Sep 17 00:00:00 2001 From: Goutam Tadi Date: Mon, 23 Jul 2018 08:27:46 -0700 Subject: [PATCH 1/2] Add support for symlinks; Basic support only in MemFS In this commit, MemFS only supports absolute symlinks and can only open them when the link is at the end of the path (no interior links). Co-authored-by: Goutam Tadi Co-authored-by: David Sharp --- dummy.go | 5 ++++ filesystem.go | 2 +- memfs/memfs.go | 47 +++++++++++++++++++++++++++++------- memfs/memfs_test.go | 51 +++++++++++++++++++++++++++++++++++++++ mountfs/mountfs.go | 10 ++++++++ os.go | 5 ++++ prefixfs/prefixfs.go | 7 +++++- prefixfs/prefixfs_test.go | 21 ++++++++++++++++ readonly.go | 4 +++ readonly_test.go | 7 ++++++ 10 files changed, 148 insertions(+), 11 deletions(-) diff --git a/dummy.go b/dummy.go index a0359cd..c5fbf72 100644 --- a/dummy.go +++ b/dummy.go @@ -41,6 +41,11 @@ func (fs DummyFS) Mkdir(name string, perm os.FileMode) error { return fs.err } +// Symlink returns dummy error +func (fs DummyFS) Symlink(oldname, newname string) error { + return fs.err +} + // Stat returns dummy error func (fs DummyFS) Stat(name string) (os.FileInfo, error) { return nil, fs.err diff --git a/filesystem.go b/filesystem.go index 7271206..2ea54d0 100644 --- a/filesystem.go +++ b/filesystem.go @@ -22,7 +22,7 @@ type Filesystem interface { // RemoveAll(path string) error Rename(oldpath, newpath string) error Mkdir(name string, perm os.FileMode) error - // Symlink(oldname, newname string) error + Symlink(oldname, newname string) error // TempDir() string // Chmod(name string, mode FileMode) error // Chown(name string, uid, gid int) error diff --git a/memfs/memfs.go b/memfs/memfs.go index 6be44bd..2557e74 100644 --- a/memfs/memfs.go +++ b/memfs/memfs.go @@ -31,6 +31,8 @@ type MemFS struct { lock *sync.RWMutex } +var _ vfs.Filesystem = &MemFS{} + // Create a new MemFS filesystem which entirely resides in memory func Create() *MemFS { root := &fileInfo{ @@ -130,6 +132,18 @@ func (fs *MemFS) Mkdir(name string, perm os.FileMode) error { return nil } +func (fs *MemFS) Symlink(oldname, newname string) error { + file, err := fs.OpenFile( + newname, + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + 0777|os.ModeSymlink) + if err != nil { + return err + } + file.Write([]byte(oldname)) + return nil +} + // byName implements sort.Interface type byName []os.FileInfo @@ -185,17 +199,24 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er segments = segments[1:] // Further directories - if len(segments) > 1 { - for _, seg := range segments[:len(segments)-1] { + for _, seg := range segments[:len(segments)-1] { - if parent.childs == nil { - return nil, nil, os.ErrNotExist - } - if entry, ok := parent.childs[seg]; ok && entry.dir { - parent = entry - } else { - return nil, nil, os.ErrNotExist + if parent.childs == nil { + return nil, nil, os.ErrNotExist + } + entry, ok := parent.childs[seg] + if !ok { + return nil, nil, os.ErrNotExist + } + if entry.dir { + parent = entry + } else if entry.mode&os.ModeSymlink != 0 { + _, parent, err = fs.fileInfo(string(*entry.buf)) + if err != nil { + return nil, nil, err } + } else { + return nil, nil, os.ErrNotExist } } @@ -222,6 +243,11 @@ func (fs *MemFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, er fs.lock.Lock() defer fs.lock.Unlock() + return fs.openFile(name, flag, perm) +} + +func (fs *MemFS) openFile(name string, flag int, perm os.FileMode) (vfs.File, error) { + name = filepath.Clean(name) base := filepath.Base(name) fiParent, fiNode, err := fs.fileInfo(name) @@ -249,6 +275,9 @@ func (fs *MemFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, er if fiNode.dir { return nil, &os.PathError{"open", name, ErrIsDirectory} } + if fiNode.mode&os.ModeSymlink != 0 { + return fs.openFile(string(*fiNode.buf), flag, perm) + } } if !hasFlag(os.O_RDONLY, flag) { diff --git a/memfs/memfs_test.go b/memfs/memfs_test.go index d2f06d4..b7c60f9 100644 --- a/memfs/memfs_test.go +++ b/memfs/memfs_test.go @@ -126,6 +126,57 @@ func TestMkdirTree(t *testing.T) { //TODO: Subdir of file } +func TestSymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/tmp", 0755); err != nil { + t.Fatal("Unable to create /tmp:", err) + } + if err := vfs.WriteFile(fs, "/tmp/teacup", []byte("i am a teacup"), 0644); err != nil { + t.Fatal("Unable to fill teacup:", err) + } + err := fs.Symlink("/tmp/teacup", "/tmp/cup") + if err != nil { + t.Fatal("Symlink failed:", err) + } + _, node, err := fs.fileInfo("/tmp/cup") + if string(*node.buf) != "/tmp/teacup" { + t.Fatal("Wrong symlink contents in buf:", string(*node.buf)) + } + fluid, err := vfs.ReadFile(fs, "/tmp/cup") + if err != nil { + t.Fatal("Failed to read from /tmp/cup:", err) + } + if string(fluid) != "i am a teacup" { + t.Fatal("Wrong contents in cup. got:", string(fluid)) + } +} + +func TestDirectorySymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/foo/a/b", 0755); err != nil { + t.Fatal("Unable mkdir /foo/a/b:", err) + } + if err := vfs.WriteFile(fs, "/foo/a/b/c", []byte("I can \"c\" clearly now"), 0644); err != nil { + t.Fatal("Unable to write /foo/a/b/c:", err) + } + if err := fs.Symlink("/foo/a/b", "/foo/also_b"); err != nil { + t.Fatal("Unable to symlink /foo/also_b -> /foo/a/b:", err) + } + + contents, err := vfs.ReadFile(fs, "/foo/also_b/c") + if err != nil { + t.Fatal("Unable to read /foo/also_b/c:", err) + } + if string(contents) != "I can \"c\" clearly now" { + t.Fatal("Unexpected contents read from c:", err) + } +} + +// TODO: relative symlinks +// TODO: overwrite symlinks + func TestReadDir(t *testing.T) { fs := Create() dirs := []string{"/home", "/home/linus", "/home/rob", "/home/pike", "/home/blang"} diff --git a/mountfs/mountfs.go b/mountfs/mountfs.go index aa0396c..9e7f829 100644 --- a/mountfs/mountfs.go +++ b/mountfs/mountfs.go @@ -127,6 +127,16 @@ func (fs MountFS) Mkdir(name string, perm os.FileMode) error { return mount.Mkdir(innerPath, perm) } +// Symlink creates a symlink +func (fs MountFS) Symlink(oldname, newname string) error { + oldMount, oldInnerName := findMount(oldname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + newMount, newInnerName := findMount(newname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + if oldMount != newMount { + return ErrBoundary + } + return oldMount.Symlink(oldInnerName, newInnerName) +} + type innerFileInfo struct { os.FileInfo name string diff --git a/os.go b/os.go index feb33bb..24ba320 100644 --- a/os.go +++ b/os.go @@ -33,6 +33,11 @@ func (fs OsFS) Mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } +// Symlink wraps os.Symlink +func (fs OsFS) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + // Rename wraps os.Rename func (fs OsFS) Rename(oldpath, newpath string) error { return os.Rename(oldpath, newpath) diff --git a/prefixfs/prefixfs.go b/prefixfs/prefixfs.go index 6b84dfb..359121f 100644 --- a/prefixfs/prefixfs.go +++ b/prefixfs/prefixfs.go @@ -16,7 +16,7 @@ type FS struct { // Create returns a file system that prefixes all paths and forwards to root. func Create(root vfs.Filesystem, prefix string) *FS { - return &FS{root, prefix} + return &FS{Filesystem: root, Prefix: prefix} } // PrefixPath returns path with the prefix prefixed. @@ -47,6 +47,11 @@ func (fs *FS) Mkdir(name string, perm os.FileMode) error { return fs.Filesystem.Mkdir(fs.PrefixPath(name), perm) } +// Symlink implements vfs.Filesystem. +func (fs *FS) Symlink(oldname, newname string) error { + return fs.Filesystem.Symlink(fs.PrefixPath(oldname), fs.PrefixPath(newname)) +} + // Stat implements vfs.Filesystem. func (fs *FS) Stat(name string) (os.FileInfo, error) { return fs.Filesystem.Stat(fs.PrefixPath(name)) diff --git a/prefixfs/prefixfs_test.go b/prefixfs/prefixfs_test.go index c04e746..8cbd041 100644 --- a/prefixfs/prefixfs_test.go +++ b/prefixfs/prefixfs_test.go @@ -103,6 +103,27 @@ func TestMkdir(t *testing.T) { } } +func TestSymlink(t *testing.T) { + rfs := rootfs() + fs := Create(rfs, prefixPath) + + f, err := fs.OpenFile("file", os.O_CREATE, 0666) + defer f.Close() + if err != nil { + t.Errorf("OpenFile: %v", err) + } + + err = fs.Symlink("/file", "file2") + if err != nil { + t.Errorf("Symlink: %v", err) + } + + _, err = rfs.Stat(prefix("file2")) + if os.IsNotExist(err) { + t.Errorf("root:%v not found (%v)", prefix("file2"), err) + } +} + func TestStat(t *testing.T) { rfs := rootfs() fs := Create(rfs, prefixPath) diff --git a/readonly.go b/readonly.go index 406cd84..6c49679 100644 --- a/readonly.go +++ b/readonly.go @@ -44,6 +44,10 @@ func (fs RoFS) Mkdir(name string, perm os.FileMode) error { return ErrReadOnly } +func (fs RoFS) Symlink(oldname, newname string) error { + return ErrReadOnly +} + // OpenFile returns ErrorReadOnly if flag contains os.O_CREATE, os.O_APPEND, os.O_WRONLY. // Otherwise it returns a read-only File with disabled Write(..) operation. func (fs RoFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { diff --git a/readonly_test.go b/readonly_test.go index f1ca36e..dffc1c6 100644 --- a/readonly_test.go +++ b/readonly_test.go @@ -66,6 +66,13 @@ func TestMkDir(t *testing.T) { } } +func TestROSymlink(t *testing.T) { + err := ro.Symlink("old", "new") + if err != ErrReadOnly { + t.Errorf("Symlink error expected") + } +} + type writeDummyFS struct { Filesystem } From faa582f444eb63f891427cb75d7589c2a15ad6c2 Mon Sep 17 00:00:00 2001 From: David Sharp Date: Thu, 26 Jul 2018 11:53:01 -0700 Subject: [PATCH 2/2] Add support for relative symlinks; better testing for interior links --- memfs/memfs.go | 55 +++++++++++++++++++++------------------ memfs/memfs_test.go | 63 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/memfs/memfs.go b/memfs/memfs.go index 2557e74..c63aca9 100644 --- a/memfs/memfs.go +++ b/memfs/memfs.go @@ -179,26 +179,17 @@ func (fs *MemFS) ReadDir(path string) ([]os.FileInfo, error) { } func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err error) { - path = filepath.Clean(path) - segments := vfs.SplitPath(path, PathSeparator) + return fs.relativeFileInfo(fs.wd, path) +} - // Shortcut for working directory and root - if len(segments) == 1 { - if segments[0] == "" { - return nil, fs.root, nil - } else if segments[0] == "." { - return fs.wd.parent, fs.wd, nil - } - } +func (fs *MemFS) relativeFileInfo(wd *fileInfo, path string) (parent *fileInfo, node *fileInfo, err error) { + parent, segments := fs.dirSegments(wd, path) - // Determine root to traverse - parent = fs.root - if segments[0] == "." { - parent = fs.wd + // Shortcut for working directory and root + if len(segments) == 0 { + return parent.parent, parent, nil } - segments = segments[1:] - // Further directories for _, seg := range segments[:len(segments)-1] { if parent.childs == nil { @@ -211,10 +202,16 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er if entry.dir { parent = entry } else if entry.mode&os.ModeSymlink != 0 { - _, parent, err = fs.fileInfo(string(*entry.buf)) + // Look up interior symlink + _, parent, err = fs.relativeFileInfo(parent, string(*entry.buf)) if err != nil { return nil, nil, err } + // Symlink was not to a directory + if parent == nil { + return nil, nil, vfs.ErrNotDirectory + } + } else { return nil, nil, os.ErrNotExist } @@ -223,6 +220,9 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er lastSeg := segments[len(segments)-1] if parent.childs != nil { if node, ok := parent.childs[lastSeg]; ok { + if node.mode&os.ModeSymlink != 0 { + return fs.relativeFileInfo(parent, string(*node.buf)) + } return parent, node, nil } } else { @@ -232,6 +232,19 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er return parent, nil, nil } +func (fs *MemFS) dirSegments(wd *fileInfo, path string) (parent *fileInfo, segments []string) { + path = filepath.Clean(path) + segments = vfs.SplitPath(path, PathSeparator) + + // Determine root to traverse + parent = fs.root + if segments[0] == "." { + parent = wd + } + segments = segments[1:] + return parent, segments +} + func hasFlag(flag int, flags int) bool { return flags&flag == flag } @@ -243,11 +256,6 @@ func (fs *MemFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, er fs.lock.Lock() defer fs.lock.Unlock() - return fs.openFile(name, flag, perm) -} - -func (fs *MemFS) openFile(name string, flag int, perm os.FileMode) (vfs.File, error) { - name = filepath.Clean(name) base := filepath.Base(name) fiParent, fiNode, err := fs.fileInfo(name) @@ -275,9 +283,6 @@ func (fs *MemFS) openFile(name string, flag int, perm os.FileMode) (vfs.File, er if fiNode.dir { return nil, &os.PathError{"open", name, ErrIsDirectory} } - if fiNode.mode&os.ModeSymlink != 0 { - return fs.openFile(string(*fiNode.buf), flag, perm) - } } if !hasFlag(os.O_RDONLY, flag) { diff --git a/memfs/memfs_test.go b/memfs/memfs_test.go index b7c60f9..fa1b4bc 100644 --- a/memfs/memfs_test.go +++ b/memfs/memfs_test.go @@ -3,6 +3,7 @@ package memfs import ( "io/ioutil" "os" + "strings" "testing" "time" @@ -139,10 +140,6 @@ func TestSymlink(t *testing.T) { if err != nil { t.Fatal("Symlink failed:", err) } - _, node, err := fs.fileInfo("/tmp/cup") - if string(*node.buf) != "/tmp/teacup" { - t.Fatal("Wrong symlink contents in buf:", string(*node.buf)) - } fluid, err := vfs.ReadFile(fs, "/tmp/cup") if err != nil { t.Fatal("Failed to read from /tmp/cup:", err) @@ -158,9 +155,11 @@ func TestDirectorySymlink(t *testing.T) { if err := vfs.MkdirAll(fs, "/foo/a/b", 0755); err != nil { t.Fatal("Unable mkdir /foo/a/b:", err) } + if err := vfs.WriteFile(fs, "/foo/a/b/c", []byte("I can \"c\" clearly now"), 0644); err != nil { t.Fatal("Unable to write /foo/a/b/c:", err) } + if err := fs.Symlink("/foo/a/b", "/foo/also_b"); err != nil { t.Fatal("Unable to symlink /foo/also_b -> /foo/a/b:", err) } @@ -174,8 +173,60 @@ func TestDirectorySymlink(t *testing.T) { } } -// TODO: relative symlinks -// TODO: overwrite symlinks +func TestMultipleAndRelativeSymlinks(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + + for _, fsEntry := range []struct { + name, link, content string + }{ + {name: "a/b", link: "real_b"}, + {name: "a/b/c", link: "real_c"}, + {name: "a/b/c/real_d", content: "Lah dee dah"}, + {name: "a/b/c/d", link: "real_d"}, + {name: "a/d", link: "b/c/d"}, + } { + if fsEntry.link != "" { + if err := fs.Symlink(fsEntry.link, fsEntry.name); err != nil { + t.Fatalf("Unable to symlink %s -> %s: %v", fsEntry.name, fsEntry.link, err) + } + } else if fsEntry.content != "" { + if err := vfs.WriteFile(fs, fsEntry.name, []byte(fsEntry.content), 0644); err != nil { + t.Fatalf("Unable to write %s: %v", fsEntry.name, err) + } + } + } + + for _, fn := range []string{ + "a/b/c/d", + "a/d", + } { + contents, err := vfs.ReadFile(fs, fn) + if err != nil { + t.Fatalf("Unable to read %s: %v", fn, err) + } + if string(contents) != "Lah dee dah" { + t.Fatalf("Unexpected contents read from %s: %v", fn, err) + } + } +} + +func TestSymlinkIsNotADirectory(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + if err := fs.Symlink("broken", "a/b"); err != nil { + t.Fatal("Unable to symlink a/b -> broken:", err) + } + if err := vfs.WriteFile(fs, "a/b/c", []byte("Whatever"), 0644); !strings.Contains(err.Error(), vfs.ErrNotDirectory.Error()) { + t.Fatal("Expected an error when writing a/b/c:", err) + } +} + +// TODO: overwrite/remove symlinks func TestReadDir(t *testing.T) { fs := Create()