Skip to content

Commit 3c8cef9

Browse files
committed
memfs: Add support for umask
The umask construct is now supported via new WithUMask(value) option. When not set it defaults to 0o022, as per generally used within posix systems. The polyfill had to be changed to add support for Chmod, without that the new TestUmaskChmod would simply fail to alter filemode of existing files. Signed-off-by: Paulo Gomes <pjbgf@linux.com>
1 parent 8afc3eb commit 3c8cef9

File tree

6 files changed

+184
-15
lines changed

6 files changed

+184
-15
lines changed

helper/polyfill/polyfill.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Polyfill struct {
1515
c capabilities
1616
}
1717

18-
type capabilities struct{ tempfile, dir, symlink, chroot bool }
18+
type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool }
1919

2020
// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
2121
// made and errors if fs doesn't implement any of the billy interfaces.
@@ -30,6 +30,8 @@ func New(fs billy.Basic) billy.Filesystem {
3030
_, h.c.dir = h.Basic.(billy.Dir)
3131
_, h.c.symlink = h.Basic.(billy.Symlink)
3232
_, h.c.chroot = h.Basic.(billy.Chroot)
33+
_, h.c.chmod = h.Basic.(billy.Chmod)
34+
3335
return h
3436
}
3537

@@ -89,6 +91,14 @@ func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) {
8991
return h.Basic.(billy.Chroot).Chroot(path)
9092
}
9193

94+
func (h *Polyfill) Chmod(path string, mode fs.FileMode) error {
95+
if !h.c.chmod {
96+
return billy.ErrNotSupported
97+
}
98+
99+
return h.Basic.(billy.Chmod).Chmod(path, mode)
100+
}
101+
92102
func (h *Polyfill) Root() string {
93103
if !h.c.chroot {
94104
return string(filepath.Separator)

memfs/memory.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,41 @@ import (
1717
"github.com/go-git/go-billy/v6/util"
1818
)
1919

20-
const separator = filepath.Separator
20+
const (
21+
separator = filepath.Separator
22+
defaultUmask = 0o022
23+
defaultDirMode = 0o777
24+
defaultFileMode = 0o666
25+
)
2126

2227
// Memory a very convenient filesystem based on memory files.
2328
type Memory struct {
24-
s *storage
29+
s *storage
30+
umask uint32
2531
}
2632

2733
// New returns a new Memory filesystem.
2834
func New(opts ...Option) billy.Filesystem {
29-
o := &options{}
35+
o := &options{
36+
umask: defaultUmask,
37+
}
3038
for _, opt := range opts {
3139
opt(o)
3240
}
3341

3442
fs := &Memory{
35-
s: newStorage(),
43+
s: newStorage(),
44+
umask: o.umask,
3645
}
37-
_, err := fs.s.New("/", 0o755|os.ModeDir, 0)
46+
_, err := fs.s.New("/", fs.applyUmask(defaultDirMode)|os.ModeDir, 0)
3847
if err != nil {
3948
log.Printf("failed to create root dir: %v", err)
4049
}
4150
return chroot.New(fs, string(separator))
4251
}
4352

4453
func (fs *Memory) Create(filename string) (billy.File, error) {
45-
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
54+
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultFileMode)
4655
}
4756

4857
func (fs *Memory) Open(filename string) (billy.File, error) {
@@ -57,7 +66,7 @@ func (fs *Memory) OpenFile(filename string, flag int, perm gofs.FileMode) (billy
5766
}
5867

5968
var err error
60-
f, err = fs.s.New(filename, perm, flag)
69+
f, err = fs.s.New(filename, fs.applyUmask(perm), flag)
6170
if err != nil {
6271
return nil, err
6372
}
@@ -163,7 +172,7 @@ func (fs *Memory) ReadDir(path string) ([]gofs.DirEntry, error) {
163172
}
164173

165174
func (fs *Memory) MkdirAll(path string, perm gofs.FileMode) error {
166-
_, err := fs.s.New(path, perm|os.ModeDir, 0)
175+
_, err := fs.s.New(path, fs.applyUmask(perm)|os.ModeDir, 0)
167176
return err
168177
}
169178

@@ -228,6 +237,13 @@ func (fs *Memory) Capabilities() billy.Capability {
228237
billy.TruncateCapability
229238
}
230239

240+
// applyUmask applies the filesystem's umask to a mode by clearing the bits
241+
// specified in the umask. For example, with umask 0o022, the mode 0o666
242+
// becomes 0o644 (rw-r--r--).
243+
func (fs *Memory) applyUmask(mode gofs.FileMode) gofs.FileMode {
244+
return mode &^ gofs.FileMode(fs.umask)
245+
}
246+
231247
func (c *content) Truncate() {
232248
c.bytes = make([]byte, 0)
233249
}

memfs/memory_option.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,15 @@ package memfs
22

33
type Option func(*options)
44

5-
type options struct{}
5+
type options struct {
6+
umask uint32
7+
}
8+
9+
// WithUmask sets the umask for the memfs filesystem. The umask controls the
10+
// default permissions for newly created files and directories by clearing
11+
// specified permission bits. If not set, defaults to 0o022.
12+
func WithUmask(mask uint32) Option {
13+
return func(o *options) {
14+
o.umask = mask
15+
}
16+
}

memfs/memory_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,116 @@ func TestThreadSafety(t *testing.T) {
403403
require.NoError(t, err)
404404
assert.Len(t, fi, files*2)
405405
}
406+
407+
func TestUmask(t *testing.T) {
408+
tests := []struct {
409+
name string
410+
umask *uint32
411+
expectedFileMode os.FileMode
412+
expectedDirMode os.FileMode
413+
}{
414+
{
415+
name: "default umask (0o022)",
416+
umask: nil,
417+
expectedFileMode: 0o644,
418+
expectedDirMode: 0o755,
419+
},
420+
{
421+
name: "custom umask (0o077)",
422+
umask: func() *uint32 { u := uint32(0o077); return &u }(),
423+
expectedFileMode: 0o600,
424+
expectedDirMode: 0o700,
425+
},
426+
{
427+
name: "zero umask (0o000)",
428+
umask: func() *uint32 { u := uint32(0o000); return &u }(),
429+
expectedFileMode: 0o666,
430+
expectedDirMode: 0o777,
431+
},
432+
}
433+
434+
for _, tt := range tests {
435+
t.Run(tt.name, func(t *testing.T) {
436+
var fs billy.Filesystem
437+
if tt.umask != nil {
438+
fs = New(WithUmask(*tt.umask))
439+
} else {
440+
fs = New()
441+
}
442+
443+
f, err := fs.Create("file.txt")
444+
require.NoError(t, err)
445+
f.Close()
446+
447+
fi, err := fs.Stat("file.txt")
448+
require.NoError(t, err)
449+
assert.Equal(t, tt.expectedFileMode, fi.Mode().Perm())
450+
451+
err = fs.MkdirAll("testdir", 0o777)
452+
require.NoError(t, err)
453+
454+
fi, err = fs.Stat("testdir")
455+
require.NoError(t, err)
456+
assert.Equal(t, tt.expectedDirMode, fi.Mode().Perm())
457+
})
458+
}
459+
}
460+
461+
func TestUmaskOpenFile(t *testing.T) {
462+
fs := New(WithUmask(0o077))
463+
464+
f, err := fs.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0o666)
465+
require.NoError(t, err)
466+
assert.NoError(t, f.Close())
467+
468+
fi, err := fs.Stat("test.txt")
469+
require.NoError(t, err)
470+
assert.Equal(t, os.FileMode(0o600), fi.Mode().Perm())
471+
472+
// Re-do test without the use of os.O_CREATE.
473+
err = util.WriteFile(fs, "test2.txt", []byte("content"), 0o666)
474+
require.NoError(t, err)
475+
476+
fi2, err := fs.Stat("test2.txt")
477+
require.NoError(t, err)
478+
assert.Equal(t, os.FileMode(0o600), fi2.Mode().Perm())
479+
480+
f2, err := fs.OpenFile("test2.txt", os.O_RDWR, 0o777)
481+
require.NoError(t, err)
482+
assert.NoError(t, f2.Close())
483+
484+
fi2, err = fs.Stat("test2.txt")
485+
require.NoError(t, err)
486+
// Mode must not be changed by OpenFile without os.O_CREATE.
487+
assert.Equal(t, os.FileMode(0o600), fi2.Mode().Perm())
488+
}
489+
490+
func TestUmaskChmod(t *testing.T) {
491+
fs := New()
492+
493+
f, err := fs.Create("/test.txt")
494+
require.NoError(t, err)
495+
assert.NoError(t, f.Close())
496+
497+
fi, err := fs.Stat("/test.txt")
498+
require.NoError(t, err)
499+
assert.Equal(t, os.FileMode(0o644), fi.Mode().Perm())
500+
501+
ch, ok := fs.(billy.Chmod)
502+
require.True(t, ok, "fs does not implement billy.Chmod")
503+
504+
err = ch.Chmod("/test.txt", 0o421)
505+
require.NoError(t, err)
506+
507+
fi, err = fs.Stat("/test.txt")
508+
require.NoError(t, err)
509+
assert.Equal(t, os.FileMode(0o421), fi.Mode().Perm())
510+
}
511+
512+
func TestUmaskRootDirectory(t *testing.T) {
513+
fs := New(WithUmask(0o077))
514+
515+
fi, err := fs.Stat("/")
516+
require.NoError(t, err)
517+
assert.Equal(t, os.FileMode(0o700), fi.Mode().Perm())
518+
}

test/basic_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
"os"
99
"path/filepath"
1010
"reflect"
11+
"runtime"
1112
"slices"
1213
"strings"
1314
"testing"
1415

1516
. "github.com/go-git/go-billy/v6" //nolint
17+
"github.com/go-git/go-billy/v6/memfs"
1618
"github.com/go-git/go-billy/v6/osfs"
1719
"github.com/go-git/go-billy/v6/util"
1820
"github.com/stretchr/testify/assert"
@@ -22,8 +24,8 @@ import (
2224
func eachBasicFS(t *testing.T, test func(t *testing.T, fs Basic)) {
2325
t.Helper()
2426

25-
for _, fs := range allFS(t.TempDir) {
26-
t.Run(fmt.Sprintf("%T", fs), func(t *testing.T) {
27+
for i, fs := range allFS(t.TempDir) {
28+
t.Run(fmt.Sprintf("%d-%T", i, fs), func(t *testing.T) {
2729
test(t, fs)
2830
})
2931
}
@@ -251,9 +253,14 @@ func TestOpenFileWithModes(t *testing.T) {
251253
require.NoError(t, err)
252254
require.NoError(t, f.Close())
253255

256+
want := customMode
257+
if runtime.GOOS == "windows" && reflect.TypeOf(fs) == reflect.TypeOf(memfs.New()) {
258+
want = 0o644
259+
}
260+
254261
fi, err := fs.Stat("foo")
255262
require.NoError(t, err)
256-
assert.Equal(t, customMode, fi.Mode())
263+
assert.Equal(t, want, fi.Mode())
257264
})
258265
}
259266

@@ -454,11 +461,16 @@ func TestStat(t *testing.T) {
454461
err := util.WriteFile(fs, "foo/bar", []byte("foo"), customMode)
455462
require.NoError(t, err)
456463

464+
want := customMode
465+
if runtime.GOOS == "windows" && reflect.TypeOf(fs) == reflect.TypeOf(memfs.New()) {
466+
want = 0o644
467+
}
468+
457469
fi, err := fs.Stat("foo/bar")
458470
require.NoError(t, err)
459471
assert.Equal(t, "bar", fi.Name())
460472
assert.Equal(t, int64(3), fi.Size())
461-
assert.Equal(t, customMode, fi.Mode())
473+
assert.Equal(t, want, fi.Mode())
462474
assert.False(t, fi.ModTime().IsZero())
463475
assert.False(t, fi.IsDir())
464476
})

test/symlink_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"fmt"
55
"io"
66
"os"
7+
"reflect"
78
"runtime"
89
"testing"
910

1011
. "github.com/go-git/go-billy/v6" //nolint
12+
"github.com/go-git/go-billy/v6/memfs"
1113
"github.com/go-git/go-billy/v6/util"
1214
"github.com/stretchr/testify/assert"
1315
"github.com/stretchr/testify/require"
@@ -260,6 +262,7 @@ func TestStatLink(t *testing.T) {
260262
t.Skip("skipping on Plan 9; symlinks are not supported")
261263
}
262264

265+
want := customMode
263266
eachSymlinkFS(t, func(t *testing.T, fs symlinkFS) {
264267
t.Helper()
265268
err := util.WriteFile(fs, "foo/bar", []byte("foo"), customMode)
@@ -268,11 +271,15 @@ func TestStatLink(t *testing.T) {
268271
err = fs.Symlink("bar", "foo/qux")
269272
require.NoError(t, err)
270273

274+
if runtime.GOOS == "windows" && reflect.TypeOf(fs) == reflect.TypeOf(memfs.New()) {
275+
want = 0o777
276+
}
277+
271278
fi, err := fs.Stat("foo/qux")
272279
require.NoError(t, err)
273280
assert.Equal(t, fi.Name(), "qux")
274281
assert.Equal(t, fi.Size(), int64(3))
275-
assert.Equal(t, fi.Mode(), customMode)
282+
assert.Equal(t, fi.Mode(), want)
276283
assert.Equal(t, fi.ModTime().IsZero(), false)
277284
assert.Equal(t, fi.IsDir(), false)
278285
})

0 commit comments

Comments
 (0)