Skip to content

Commit b89ce64

Browse files
committed
Update embedfs implementation
- Add chroot functionality to embedfs using billy's chroot helper, enabling compatibility with filesystem operations that require path isolation - Fix path normalization to support billy's absolute path convention - Implement Lstat method - Additional tests to cover embedfs implementation
1 parent c1f149a commit b89ce64

File tree

11 files changed

+628
-75
lines changed

11 files changed

+628
-75
lines changed

embedfs/chroot_test.go

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package embedfs
2+
3+
import (
4+
"io"
5+
"testing"
6+
7+
"github.com/go-git/go-billy/v6"
8+
"github.com/go-git/go-billy/v6/embedfs/internal/testdata"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestChroot_Basic(t *testing.T) {
14+
t.Parallel()
15+
16+
fs := New(testdata.GetTestData())
17+
18+
// Test chroot to existing directory
19+
chrootFS, err := fs.Chroot("testdata")
20+
require.NoError(t, err)
21+
require.NotNil(t, chrootFS)
22+
23+
// Test that we can access files in the chrooted filesystem
24+
f, err := chrootFS.Open("file1.txt")
25+
require.NoError(t, err)
26+
defer f.Close()
27+
28+
content, err := io.ReadAll(f)
29+
require.NoError(t, err)
30+
assert.Equal(t, "Hello from embedfs!", string(content))
31+
}
32+
33+
func TestChroot_NestedDirectory(t *testing.T) {
34+
t.Parallel()
35+
36+
fs := New(testdata.GetTestData())
37+
38+
// Test chroot to nested directory
39+
chrootFS, err := fs.Chroot("testdata/subdir")
40+
require.NoError(t, err)
41+
require.NotNil(t, chrootFS)
42+
43+
// Test that we can access nested files from the chrooted root
44+
f, err := chrootFS.Open("nested.txt")
45+
require.NoError(t, err)
46+
defer f.Close()
47+
48+
content, err := io.ReadAll(f)
49+
require.NoError(t, err)
50+
assert.Equal(t, "Nested file content", string(content))
51+
}
52+
53+
func TestChroot_StatInChroot(t *testing.T) {
54+
t.Parallel()
55+
56+
fs := New(testdata.GetTestData())
57+
58+
chrootFS, err := fs.Chroot("testdata")
59+
require.NoError(t, err)
60+
61+
// Test stat on files that exist in chrooted directory
62+
fi, err := chrootFS.Stat("file1.txt")
63+
require.NoError(t, err)
64+
assert.Equal(t, "file1.txt", fi.Name())
65+
assert.False(t, fi.IsDir())
66+
67+
// Test stat on directories that exist in chrooted directory
68+
fi, err = chrootFS.Stat("subdir")
69+
require.NoError(t, err)
70+
assert.Equal(t, "subdir", fi.Name())
71+
assert.True(t, fi.IsDir())
72+
73+
// Test stat with absolute path in chrooted filesystem
74+
fi, err = chrootFS.Stat("/file2.txt")
75+
require.NoError(t, err)
76+
assert.Equal(t, "file2.txt", fi.Name())
77+
assert.False(t, fi.IsDir())
78+
}
79+
80+
func TestChroot_ReadDirInChroot(t *testing.T) {
81+
t.Parallel()
82+
83+
fs := New(testdata.GetTestData())
84+
85+
chrootFS, err := fs.Chroot("testdata")
86+
require.NoError(t, err)
87+
88+
// Test reading directory contents from chrooted root
89+
entries, err := chrootFS.ReadDir("/")
90+
require.NoError(t, err)
91+
92+
expectedFiles := []string{"empty.txt", "file1.txt", "file2.txt", "subdir"}
93+
assert.Len(t, entries, len(expectedFiles))
94+
95+
foundFiles := make(map[string]bool)
96+
for _, entry := range entries {
97+
foundFiles[entry.Name()] = true
98+
}
99+
100+
for _, expected := range expectedFiles {
101+
assert.True(t, foundFiles[expected], "Expected file %s not found", expected)
102+
}
103+
104+
// Test reading subdirectory from chrooted filesystem
105+
entries, err = chrootFS.ReadDir("subdir")
106+
require.NoError(t, err)
107+
assert.Len(t, entries, 1)
108+
assert.Equal(t, "nested.txt", entries[0].Name())
109+
}
110+
111+
func TestChroot_PathNormalization(t *testing.T) {
112+
t.Parallel()
113+
114+
fs := New(testdata.GetTestData())
115+
116+
// Test chroot with different path formats
117+
tests := []struct {
118+
name string
119+
chrootPath string
120+
openPath string
121+
expectFile string
122+
}{
123+
{
124+
name: "absolute chroot path",
125+
chrootPath: "/testdata",
126+
openPath: "file1.txt",
127+
expectFile: "file1.txt",
128+
},
129+
{
130+
name: "relative chroot path",
131+
chrootPath: "testdata",
132+
openPath: "file1.txt",
133+
expectFile: "file1.txt",
134+
},
135+
{
136+
name: "absolute open path in chroot",
137+
chrootPath: "testdata",
138+
openPath: "/file1.txt",
139+
expectFile: "file1.txt",
140+
},
141+
{
142+
name: "nested chroot",
143+
chrootPath: "testdata/subdir",
144+
openPath: "nested.txt",
145+
expectFile: "nested.txt",
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
chrootFS, err := fs.Chroot(tt.chrootPath)
152+
require.NoError(t, err)
153+
154+
f, err := chrootFS.Open(tt.openPath)
155+
require.NoError(t, err)
156+
defer f.Close()
157+
158+
assert.Equal(t, tt.expectFile, f.Name())
159+
})
160+
}
161+
}
162+
163+
func TestChroot_NonExistentPath(t *testing.T) {
164+
t.Parallel()
165+
166+
fs := New(testdata.GetTestData())
167+
168+
// Test chroot to non-existent directory - billy's chroot helper allows this
169+
chrootFS, err := fs.Chroot("nonexistent")
170+
require.NoError(t, err)
171+
require.NotNil(t, chrootFS)
172+
173+
// But accessing files within the non-existent chroot should fail
174+
_, err = chrootFS.Open("anyfile.txt")
175+
assert.Error(t, err)
176+
}
177+
178+
func TestChroot_Join(t *testing.T) {
179+
t.Parallel()
180+
181+
fs := New(testdata.GetTestData())
182+
chrootFS, err := fs.Chroot("testdata")
183+
require.NoError(t, err)
184+
185+
// Test Join operation in chrooted filesystem
186+
joined := chrootFS.Join("subdir", "nested.txt")
187+
assert.Equal(t, "subdir/nested.txt", joined)
188+
189+
// Test that joined path can be used to open file
190+
f, err := chrootFS.Open(joined)
191+
require.NoError(t, err)
192+
defer f.Close()
193+
194+
content, err := io.ReadAll(f)
195+
require.NoError(t, err)
196+
assert.Equal(t, "Nested file content", string(content))
197+
}
198+
199+
func TestChroot_UnsupportedOperations(t *testing.T) {
200+
t.Parallel()
201+
202+
fs := New(testdata.GetTestData())
203+
chrootFS, err := fs.Chroot("testdata")
204+
require.NoError(t, err)
205+
206+
// Test that write operations still fail in chrooted embedfs
207+
_, err = chrootFS.Create("newfile.txt")
208+
require.ErrorIs(t, err, billy.ErrReadOnly)
209+
210+
err = chrootFS.Remove("file1.txt")
211+
require.ErrorIs(t, err, billy.ErrReadOnly)
212+
213+
err = chrootFS.Rename("file1.txt", "renamed.txt")
214+
require.ErrorIs(t, err, billy.ErrReadOnly)
215+
216+
err = chrootFS.MkdirAll("newdir", 0755)
217+
require.ErrorIs(t, err, billy.ErrReadOnly)
218+
}
219+
220+
func TestChroot_NestedChroot(t *testing.T) {
221+
t.Parallel()
222+
223+
fs := New(testdata.GetTestData())
224+
225+
// Test creating nested chrootfs
226+
firstChroot, err := fs.Chroot("testdata")
227+
require.NoError(t, err)
228+
229+
secondChroot, err := firstChroot.Chroot("subdir")
230+
require.NoError(t, err)
231+
232+
// Test that nested chroot works correctly
233+
f, err := secondChroot.Open("nested.txt")
234+
require.NoError(t, err)
235+
defer f.Close()
236+
237+
content, err := io.ReadAll(f)
238+
require.NoError(t, err)
239+
assert.Equal(t, "Nested file content", string(content))
240+
241+
// Test that we can't access parent directory from nested chroot
242+
entries, err := secondChroot.ReadDir("/")
243+
require.NoError(t, err)
244+
assert.Len(t, entries, 1)
245+
assert.Equal(t, "nested.txt", entries[0].Name())
246+
}
247+
248+
func TestChroot_FileOperations(t *testing.T) {
249+
t.Parallel()
250+
251+
fs := New(testdata.GetTestData())
252+
chrootFS, err := fs.Chroot("testdata")
253+
require.NoError(t, err)
254+
255+
// Test file operations in chrooted filesystem
256+
f, err := chrootFS.Open("file2.txt")
257+
require.NoError(t, err)
258+
defer f.Close()
259+
260+
// Test Read
261+
buf := make([]byte, 10)
262+
n, err := f.Read(buf)
263+
require.NoError(t, err)
264+
assert.Equal(t, "Another te", string(buf[:n]))
265+
266+
// Test Seek
267+
_, err = f.Seek(0, io.SeekStart)
268+
require.NoError(t, err)
269+
270+
// Test ReadAt
271+
buf2 := make([]byte, 7)
272+
n, err = f.ReadAt(buf2, 8)
273+
require.NoError(t, err)
274+
assert.Equal(t, "test fi", string(buf2[:n]))
275+
276+
// Test that file position wasn't affected by ReadAt
277+
n, err = f.Read(buf)
278+
require.NoError(t, err)
279+
assert.Equal(t, "Another te", string(buf[:n]))
280+
}
281+
282+
func TestChroot_Lstat(t *testing.T) {
283+
t.Parallel()
284+
285+
fs := New(testdata.GetTestData())
286+
chrootFS, err := fs.Chroot("testdata")
287+
require.NoError(t, err)
288+
289+
// Test Lstat in chrooted filesystem (should behave same as Stat for embedfs)
290+
fi, err := chrootFS.Lstat("file1.txt")
291+
require.NoError(t, err)
292+
assert.Equal(t, "file1.txt", fi.Name())
293+
assert.False(t, fi.IsDir())
294+
}

embedfs/embed.go

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"sync"
1414

1515
"github.com/go-git/go-billy/v6"
16-
"github.com/go-git/go-billy/v6/memfs"
16+
"github.com/go-git/go-billy/v6/helper/chroot"
1717
)
1818

1919
type Embed struct {
@@ -29,14 +29,29 @@ func New(efs *embed.FS) billy.Filesystem {
2929
fs.underlying = &embed.FS{}
3030
}
3131

32-
return fs
32+
return chroot.New(fs, "/")
33+
}
34+
35+
// normalizePath converts billy's absolute paths to embed.FS relative paths
36+
func (fs *Embed) normalizePath(path string) string {
37+
// embed.FS uses "." for root directory, but billy uses "/"
38+
if path == "/" {
39+
return "."
40+
}
41+
// Remove leading slash for embed.FS
42+
if strings.HasPrefix(path, "/") {
43+
return path[1:]
44+
}
45+
return path
3346
}
3447

3548
func (fs *Embed) Root() string {
3649
return ""
3750
}
3851

3952
func (fs *Embed) Stat(filename string) (os.FileInfo, error) {
53+
filename = fs.normalizePath(filename)
54+
4055
f, err := fs.underlying.Open(filename)
4156
if err != nil {
4257
return nil, err
@@ -53,6 +68,7 @@ func (fs *Embed) OpenFile(filename string, flag int, _ os.FileMode) (billy.File,
5368
return nil, billy.ErrReadOnly
5469
}
5570

71+
filename = fs.normalizePath(filename)
5672
f, err := fs.underlying.Open(filename)
5773
if err != nil {
5874
return nil, err
@@ -90,7 +106,15 @@ func (fs *Embed) Join(elem ...string) string {
90106
return ""
91107
}
92108

109+
type ByName []os.FileInfo
110+
111+
func (a ByName) Len() int { return len(a) }
112+
func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
113+
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
114+
93115
func (fs *Embed) ReadDir(path string) ([]os.FileInfo, error) {
116+
path = fs.normalizePath(path)
117+
94118
e, err := fs.underlying.ReadDir(path)
95119
if err != nil {
96120
return nil, err
@@ -102,23 +126,14 @@ func (fs *Embed) ReadDir(path string) ([]os.FileInfo, error) {
102126
entries = append(entries, fi)
103127
}
104128

105-
sort.Sort(memfs.ByName(entries))
129+
sort.Sort(ByName(entries))
106130

107131
return entries, nil
108132
}
109133

110-
// Chroot is not supported.
111-
//
112-
// Calls will always return billy.ErrNotSupported.
113-
func (fs *Embed) Chroot(_ string) (billy.Filesystem, error) {
114-
return nil, billy.ErrNotSupported
115-
}
116-
117-
// Lstat is not supported.
118-
//
119-
// Calls will always return billy.ErrNotSupported.
120-
func (fs *Embed) Lstat(_ string) (os.FileInfo, error) {
121-
return nil, billy.ErrNotSupported
134+
// Lstat behaves the same as Stat for embedded filesystems since embed.FS does not support symlinks.
135+
func (fs *Embed) Lstat(filename string) (os.FileInfo, error) {
136+
return fs.Stat(filename)
122137
}
123138

124139
// Readlink is not supported.

0 commit comments

Comments
 (0)