diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..6a7a66b --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "build" + + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "build" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bdf33e..7665d26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,15 +5,15 @@ jobs: test: strategy: matrix: - go-version: [1.20.x,1.21.x] + go-version: [1.20.x,1.21.x,1.22.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: + - name: Checkout code + uses: actions/checkout@v3 - name: Install Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v3 - name: Test run: make test diff --git a/.github/workflows/test_js.yml b/.github/workflows/test_js.yml index ae9fef3..539a5e9 100644 --- a/.github/workflows/test_js.yml +++ b/.github/workflows/test_js.yml @@ -5,9 +5,12 @@ jobs: test: strategy: matrix: - go-version: [1.20.x,1.21.x] + go-version: [1.21.x,1.22.x] runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Go uses: actions/setup-go@v3 with: @@ -18,9 +21,6 @@ jobs: go install github.com/agnivade/wasmbrowsertest@latest mv $HOME/go/bin/wasmbrowsertest $HOME/go/bin/go_js_wasm_exec - - name: Checkout code - uses: actions/checkout@v3 - - name: Test run: go test -exec="$HOME/go/bin/go_js_wasm_exec" ./... env: diff --git a/.github/workflows/test_wasip1.yml b/.github/workflows/test_wasip1.yml index e97334e..312a637 100644 --- a/.github/workflows/test_wasip1.yml +++ b/.github/workflows/test_wasip1.yml @@ -5,9 +5,12 @@ jobs: test: strategy: matrix: - go-version: [1.21.x] + go-version: [1.21.x,1.22.x] runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Go uses: actions/setup-go@v3 with: @@ -17,8 +20,5 @@ jobs: run: | go install github.com/stealthrocket/wasi-go/cmd/wasirun@latest - - name: Checkout code - uses: actions/checkout@v3 - - name: Test - run: make wasitest \ No newline at end of file + run: make wasitest diff --git a/go.mod b/go.mod index a6ebae7..8f7b461 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,22 @@ module github.com/go-git/go-billy/v5 // go-git supports the last 3 stable Go versions. -go 1.19 +go 1.20 require ( github.com/cyphar/filepath-securejoin v0.2.4 github.com/onsi/gomega v1.27.10 + github.com/stretchr/testify v1.9.0 golang.org/x/sys v0.18.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 076c6c2..6c638fc 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -17,9 +19,13 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= diff --git a/memfs/memory.go b/memfs/memory.go index c008702..6cbd7d0 100644 --- a/memfs/memory.go +++ b/memfs/memory.go @@ -19,7 +19,9 @@ import ( const separator = filepath.Separator -// Memory a very convenient filesystem based on memory files +var errNotLink = errors.New("not a link") + +// Memory a very convenient filesystem based on memory files. type Memory struct { s *storage @@ -59,10 +61,9 @@ func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.F } if target, isLink := fs.resolveLink(filename, f); isLink { - if target == filename { - return nil, os.ErrNotExist + if target != filename { + return fs.OpenFile(target, flag, perm) } - return fs.OpenFile(target, flag, perm) } } @@ -73,8 +74,6 @@ func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.F return f.Duplicate(filename, perm, flag), nil } -var errNotLink = errors.New("not a link") - func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) { if !isSymlink(f.mode) { return fullpath, false @@ -136,7 +135,9 @@ func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) { if f, has := fs.s.Get(path); has { if target, isLink := fs.resolveLink(path, f); isLink { - return fs.ReadDir(target) + if target != path { + return fs.ReadDir(target) + } } } else { return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT} @@ -176,17 +177,19 @@ func (fs *Memory) Remove(filename string) error { return fs.s.Remove(filename) } +// Falls back to Go's filepath.Join, which works differently depending on the +// OS where the code is being executed. func (fs *Memory) Join(elem ...string) string { return filepath.Join(elem...) } func (fs *Memory) Symlink(target, link string) error { - _, err := fs.Stat(link) + _, err := fs.Lstat(link) if err == nil { return os.ErrExist } - if !os.IsNotExist(err) { + if !errors.Is(err, os.ErrNotExist) { return err } @@ -237,7 +240,7 @@ func (f *file) Read(b []byte) (int, error) { n, err := f.ReadAt(b, f.position) f.position += int64(n) - if err == io.EOF && n != 0 { + if errors.Is(err, io.EOF) && n != 0 { err = nil } diff --git a/memfs/memory_test.go b/memfs/memory_test.go index 043a9fb..cd7fa68 100644 --- a/memfs/memory_test.go +++ b/memfs/memory_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/test" "github.com/go-git/go-billy/v5/util" + "github.com/stretchr/testify/assert" . "gopkg.in/check.v1" ) @@ -122,8 +123,212 @@ func (s *MemorySuite) TestTruncateAppend(c *C) { c.Assert(string(data), Equals, "replace") } +func TestReadlink(t *testing.T) { + tests := []struct { + name string + link string + want string + wantErr *error + }{ + { + name: "symlink not found", + link: "/404", + wantErr: &os.ErrNotExist, + }, + { + name: "self-targeting symlink", + link: "/self", + want: "/self", + }, + { + name: "symlink", + link: "/bar", + want: "/foo", + }, + { + name: "symlink to windows path", + link: "/win", + want: "c:\\test\\123", + }, + { + name: "symlink to network path", + link: "/net", + want: "\\test\\123", + }, + } + + // Cater for memfs not being os-agnostic. + if runtime.GOOS == "windows" { + tests[1].want = "\\self" + tests[2].want = "\\foo" + tests[3].want = "\\c:\\test\\123" + } + + fs := New() + + // arrange fs for tests. + assert.NoError(t, fs.Symlink("/self", "/self")) + assert.NoError(t, fs.Symlink("/foo", "/bar")) + assert.NoError(t, fs.Symlink("c:\\test\\123", "/win")) + assert.NoError(t, fs.Symlink("\\test\\123", "/net")) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := fs.Readlink(tc.link) + + if tc.wantErr == nil { + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + } else { + assert.ErrorIs(t, err, *tc.wantErr) + } + }) + } +} + +func TestSymlink(t *testing.T) { + tests := []struct { + name string + target string + link string + want string + wantErr string + }{ + { + name: "new symlink unexistent target", + target: "/bar", + link: "/foo", + want: "/bar", + }, + { + name: "self-targeting symlink", + target: "/self", + link: "/self", + want: "/self", + }, + { + name: "new symlink to file", + target: "/file", + link: "/file-link", + want: "/file", + }, + { + name: "new symlink to dir", + target: "/dir", + link: "/dir-link", + want: "/dir", + }, + { + name: "new symlink to win", + target: "c:\\foor\\bar", + link: "/win", + want: "c:\\foor\\bar", + }, + { + name: "new symlink to net", + target: "\\net\\bar", + link: "/net", + want: "\\net\\bar", + }, + { + name: "new symlink to net", + target: "\\net\\bar", + link: "/net", + want: "\\net\\bar", + }, + { + name: "duplicate symlink", + target: "/bar", + link: "/foo", + wantErr: os.ErrExist.Error(), + }, + { + name: "symlink over existing file", + target: "/foo/bar", + link: "/file", + want: "/file", + wantErr: os.ErrExist.Error(), + }, + } + + // Cater for memfs not being os-agnostic. + if runtime.GOOS == "windows" { + tests[0].want = "\\bar" + tests[1].want = "\\self" + tests[2].want = "\\file" + tests[3].want = "\\dir" + tests[4].want = "\\c:\\foor\\bar" + } + + fs := New() + + // arrange fs for tests. + err := fs.MkdirAll("/dir", 0o600) + assert.NoError(t, err) + _, err = fs.Create("/file") + assert.NoError(t, err) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := fs.Symlink(tc.target, tc.link) + + if tc.wantErr == "" { + got, err := fs.Readlink(tc.link) + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + } else { + assert.ErrorContains(t, err, tc.wantErr) + } + }) + } +} + +func TestJoin(t *testing.T) { + tests := []struct { + name string + elem []string + want string + }{ + {name: "empty", elem: []string{""}, want: ""}, + {name: "c:", elem: []string{"C:"}, want: "C:"}, + {name: "simple rel", elem: []string{"a", "b", "c"}, want: "a/b/c"}, + {name: "simple rel backslash", elem: []string{"\\", "a", "b", "c"}, want: "\\/a/b/c"}, + {name: "simple abs slash", elem: []string{"/", "a", "b", "c"}, want: "/a/b/c"}, + {name: "c: rel", elem: []string{"C:\\", "a", "b", "c"}, want: "C:\\/a/b/c"}, + {name: "c: abs", elem: []string{"/C:\\", "a", "b", "c"}, want: "/C:\\/a/b/c"}, + {name: "\\ rel", elem: []string{"\\\\", "a", "b", "c"}, want: "\\\\/a/b/c"}, + {name: "\\ abs", elem: []string{"/\\\\", "a", "b", "c"}, want: "/\\\\/a/b/c"}, + } + + // Cater for memfs not being os-agnostic. + if runtime.GOOS == "windows" { + tests[1].want = "C:." + tests[2].want = "a\\b\\c" + tests[3].want = "\\a\\b\\c" + tests[4].want = "\\a\\b\\c" + tests[5].want = "C:\\a\\b\\c" + tests[6].want = "\\C:\\a\\b\\c" + tests[7].want = "\\\\a\\b\\c" + tests[8].want = "\\\\\\a\\b\\c" + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := New().Join(tc.elem...) + assert.Equal(t, tc.want, got) + }) + } +} + func (s *MemorySuite) TestSymlink(c *C) { - s.FS.Symlink("test", "test") - _, err := s.FS.Open("test") - c.Assert(err, NotNil) + err := s.FS.Symlink("test", "test") + c.Assert(err, IsNil) + + f, err := s.FS.Open("test") + c.Assert(err, IsNil) + c.Assert(f, NotNil) + + fi, err := s.FS.ReadDir("test") + c.Assert(err, IsNil) + c.Assert(fi, IsNil) } diff --git a/memfs/storage.go b/memfs/storage.go index e3c4e38..16b48ce 100644 --- a/memfs/storage.go +++ b/memfs/storage.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" "sync" ) @@ -112,7 +113,7 @@ func (s *storage) Rename(from, to string) error { move := [][2]string{{from, to}} for pathFrom := range s.files { - if pathFrom == from || !filepath.HasPrefix(pathFrom, from) { + if pathFrom == from || !strings.HasPrefix(pathFrom, from) { continue } diff --git a/util/util.go b/util/util.go index 9fae2ae..2cdd832 100644 --- a/util/util.go +++ b/util/util.go @@ -1,6 +1,7 @@ package util import ( + "errors" "io" "os" "path/filepath" @@ -33,14 +34,14 @@ func removeAll(fs billy.Basic, path string) error { // Simple case: if Remove works, we're done. err := fs.Remove(path) - if err == nil || os.IsNotExist(err) { + if err == nil || errors.Is(err, os.ErrNotExist) { return nil } // Otherwise, is this a directory we need to recurse into? dir, serr := fs.Stat(path) if serr != nil { - if os.IsNotExist(serr) { + if errors.Is(serr, os.ErrNotExist) { return nil } @@ -60,7 +61,7 @@ func removeAll(fs billy.Basic, path string) error { // Directory. fis, err := dirfs.ReadDir(path) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { // Race. It was deleted between the Lstat and Open. // Return nil per RemoveAll's docs. return nil @@ -81,7 +82,7 @@ func removeAll(fs billy.Basic, path string) error { // Remove directory. err1 := fs.Remove(path) - if err1 == nil || os.IsNotExist(err1) { + if err1 == nil || errors.Is(err1, os.ErrNotExist) { return nil } @@ -158,7 +159,7 @@ func TempFile(fs billy.Basic, dir, prefix string) (f billy.File, err error) { for i := 0; i < 10000; i++ { name := filepath.Join(dir, prefix+nextSuffix()) f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) - if os.IsExist(err) { + if errors.Is(err, os.ErrExist) { if nconflict++; nconflict > 10 { randmu.Lock() rand = reseed() @@ -189,7 +190,7 @@ func TempDir(fs billy.Dir, dir, prefix string) (name string, err error) { for i := 0; i < 10000; i++ { try := filepath.Join(dir, prefix+nextSuffix()) err = fs.MkdirAll(try, 0700) - if os.IsExist(err) { + if errors.Is(err, os.ErrExist) { if nconflict++; nconflict > 10 { randmu.Lock() rand = reseed() @@ -197,8 +198,8 @@ func TempDir(fs billy.Dir, dir, prefix string) (name string, err error) { } continue } - if os.IsNotExist(err) { - if _, err := os.Stat(dir); os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { return "", err } } @@ -276,7 +277,7 @@ func ReadFile(fs billy.Basic, name string) ([]byte, error) { data = data[:len(data)+n] if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { err = nil }