Skip to content
This repository has been archived by the owner on Oct 22, 2021. It is now read-only.

feat: Add EvalSymlinks to fix link related tests #64

Merged
merged 6 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions eval_symlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package fs

// evalSymlinks returns the path name after the evaluation of any symbolic
// links.
// The original implementation can be referenced to filepath.EvalSymlinks,
// but it will return the current path other then ENOENT error while walkSymlinks.
// If path is relative the result will be relative to the current directory,
// unless one of the components is an absolute symbolic link.
// EvalSymlinks calls Clean on the result.
func evalSymlinks(path string) (string, error) {
return evalSymlink(path)
}
61 changes: 61 additions & 0 deletions eval_symlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package fs

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEvalSymlinks(t *testing.T) {
tmpDir := t.TempDir()
t.Log(tmpDir)
cases := []struct {
name string
path string
lpath string
target string
targetExist bool
expected string
}{
{"symlink point to an existing target", filepath.Join(tmpDir, "lna"), "", filepath.Join(tmpDir, "a"), true, filepath.Join(tmpDir, "a")},
{"symlink point to an non-existent target", filepath.Join(tmpDir, "lnb"), "", filepath.Join(tmpDir, "b"), false, filepath.Join(tmpDir, "b")},
{"symlink point to another symlink", filepath.Join(tmpDir, "lnd"), filepath.Join(tmpDir, "lnc"), filepath.Join(tmpDir, "c"), false, filepath.Join(tmpDir, "c")},
{"symlink point to an relative target", filepath.Join(tmpDir, "lle"), "", tmpDir + "/./e", false, filepath.Join(tmpDir, "e")},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if tt.targetExist {
err := os.MkdirAll(tt.target, 0755)
if err != nil {
t.Log(err.Error())
}
}

if tt.lpath != "" {
err := os.Symlink(tt.target, tt.lpath)
if err != nil {
t.Log(err.Error())
}
err = os.Symlink(tt.lpath, tt.path)
if err != nil {
t.Log(err.Error())
}
} else {
err := os.Symlink(tt.target, tt.path)
if err != nil {
t.Log(err.Error())
}
}

actualTarget, err := evalSymlinks(tt.path)
if err != nil {
t.Log(err.Error())
}

assert.Equal(t, tt.expected, actualTarget)
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/beyondstorage/go-service-fs/v3
go 1.15

require (
github.com/beyondstorage/go-integration-test/v4 v4.2.1-0.20210728064741-17203b58d96c
github.com/beyondstorage/go-integration-test/v4 v4.3.0
github.com/beyondstorage/go-storage/v4 v4.4.1-0.20210730075750-6e541b87ea46
github.com/qingstor/go-mime v0.1.0
github.com/stretchr/testify v1.7.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/Xuanwo/templateutils v0.1.0 h1:WpkWOqQtIQ2vAIpJLa727DdN8WtxhUkkbDGa6UhntJY=
github.com/Xuanwo/templateutils v0.1.0/go.mod h1:OdE0DJ+CJxDBq6psX5DPV+gOZi8bhuHuVUpPCG++Wb8=
github.com/beyondstorage/go-integration-test/v4 v4.2.1-0.20210728064741-17203b58d96c h1:bYh9zp/ZFg6fLdAL2vpO+JMX8aOCWQzA4jEeG4OZr1w=
github.com/beyondstorage/go-integration-test/v4 v4.2.1-0.20210728064741-17203b58d96c/go.mod h1:HKgzemQZpxoHBL49JYEUnLTb5eteUhzcvmmPL7EDT/Y=
github.com/beyondstorage/go-integration-test/v4 v4.3.0 h1:WZ95f78RKlHpvft8zHcMaoa2aaTF/jzlzINhMD0EMHY=
github.com/beyondstorage/go-integration-test/v4 v4.3.0/go.mod h1:HKgzemQZpxoHBL49JYEUnLTb5eteUhzcvmmPL7EDT/Y=
github.com/beyondstorage/go-storage/v4 v4.4.0/go.mod h1:mc9VzBImjXDg1/1sLfta2MJH79elfM6m47ZZvZ+q/Uw=
github.com/beyondstorage/go-storage/v4 v4.4.1-0.20210730075750-6e541b87ea46 h1:sUmtn3cWgpjetIv/lSzZ6AA9GnzvxPjalTvRY1ZDzOg=
github.com/beyondstorage/go-storage/v4 v4.4.1-0.20210730075750-6e541b87ea46/go.mod h1:mc9VzBImjXDg1/1sLfta2MJH79elfM6m47ZZvZ+q/Uw=
Expand Down
4 changes: 1 addition & 3 deletions storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,7 @@ func (s *Storage) stat(ctx context.Context, path string, opt pairStorageStat) (o
if fi.Mode()&os.ModeSymlink != 0 {
o.Mode |= ModeLink

// FIXME: `filepath.EvalSymlinks(rp)` can't get the target exactly when target is not a exists file,
// so we have temporarily used os.ReadLink instead.
target, err := os.Readlink(rp)
target, err := evalSymlinks(rp)
if err != nil {
return nil, err
}
Expand Down
189 changes: 189 additions & 0 deletions symlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package fs

import (
"errors"
"os"
"path/filepath"
"runtime"
"syscall"
)

func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}

func volumeNameLen(path string) int {
if len(path) < 2 {
return 0
}
// with drive letter
c := path[0]
if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
return 2
}
// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
!isSlash(path[2]) && path[2] != '.' {
// first, leading `\\` and next shouldn't be `\`. its server name.
for n := 3; n < l-1; n++ {
// second, next '\' shouldn't be repeated.
if isSlash(path[n]) {
n++
// third, following something characters. its share name.
if !isSlash(path[n]) {
if path[n] == '.' {
break
}
for ; n < l; n++ {
if isSlash(path[n]) {
break
}
}
return n
}
break
}
}
}
return 0
}

func walkSymlinks(path string) (string, error) {
volLen := volumeNameLen(path)
pathSeparator := string(os.PathSeparator)

if volLen < len(path) && os.IsPathSeparator(path[volLen]) {
volLen++
}
vol := path[:volLen]
dest := vol
linksWalked := 0
var i = 0
for start, end := volLen, volLen; start < len(path); start = end {
for start < len(path) && os.IsPathSeparator(path[start]) {
start++
}
end = start
for end < len(path) && !os.IsPathSeparator(path[end]) {
end++
}

// On Windows, "." can be a symlink.
// We look it up, and use the value if it is absolute.
// If not, we just return ".".
isWindowsDot := runtime.GOOS == "windows" && path[volumeNameLen(path):] == "."

// The next path component is in path[start:end].
if end == start {
// No more path components.
break
} else if path[start:end] == "." && !isWindowsDot {
// Ignore path component ".".
continue
} else if path[start:end] == ".." {
// Back up to previous component if possible.
// Note that volLen includes any leading slash.

// Set r to the index of the last slash in dest,
// after the volume.
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen || dest[r+1:] == ".." {
// Either path has no slashes
// (it's empty or just "C:")
// or it ends in a ".." we had to keep.
// Either way, keep this "..".
if len(dest) > volLen {
dest += pathSeparator
}
dest += ".."
} else {
// Discard everything since the last slash.
dest = dest[:r]
}
continue
}

// Ordinary path component. Add it to result.

if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
dest += pathSeparator
}

dest += path[start:end]

// Resolve symlink.

i++

fi, err := os.Lstat(dest)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return filepath.Clean(dest + path[end:]), nil
}
return "", err
}

if fi.Mode()&os.ModeSymlink == 0 {
if !fi.Mode().IsDir() && end < len(path) {
return "", syscall.ENOTDIR
}
continue
}

// Found symlink.

linksWalked++
if linksWalked > 255 {
return "", errors.New("EvalSymlinks: too many links")
}

link, err := os.Readlink(dest)
if err != nil {
return "", err
}

if isWindowsDot && !filepath.IsAbs(link) {
// On Windows, if "." is a relative symlink,
// just return ".".
break
}

path = link + path[end:]

v := volumeNameLen(link)
if v > 0 {
// Symlink to drive name is an absolute path.
if v < len(link) && os.IsPathSeparator(link[v]) {
v++
}
vol = link[:v]
dest = vol
end = len(vol)
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
// Symlink to absolute path.
dest = link[:1]
end = 1
} else {
// Symlink to relative path; replace last
// path component in dest.
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
break
}
}
if r < volLen {
dest = vol
} else {
dest = dest[:r]
}
end = 0
}
}
return filepath.Clean(dest), nil
}
7 changes: 7 additions & 0 deletions symlink_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !windows

package fs

func evalSymlink(path string) (string, error) {
return walkSymlinks(path)
}
Loading