Skip to content

Commit 10cff82

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 10cff82

File tree

6 files changed

+184
-14
lines changed

6 files changed

+184
-14
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: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"os"
1010
"path/filepath"
11+
"runtime"
1112
"sort"
1213
"strings"
1314
"syscall"
@@ -17,32 +18,44 @@ import (
1718
"github.com/go-git/go-billy/v6/util"
1819
)
1920

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

2228
// Memory a very convenient filesystem based on memory files.
2329
type Memory struct {
24-
s *storage
30+
s *storage
31+
umask uint32
2532
}
2633

2734
// New returns a new Memory filesystem.
2835
func New(opts ...Option) billy.Filesystem {
2936
o := &options{}
37+
// Aligns default umask with general Windows behaviour.
38+
if runtime.GOOS != "windows" {
39+
o.umask = defaultUmask
40+
}
41+
3042
for _, opt := range opts {
3143
opt(o)
3244
}
3345

3446
fs := &Memory{
35-
s: newStorage(),
47+
s: newStorage(),
48+
umask: o.umask,
3649
}
37-
_, err := fs.s.New("/", 0o755|os.ModeDir, 0)
50+
_, err := fs.s.New("/", fs.applyUmask(defaultDirMode)|os.ModeDir, 0)
3851
if err != nil {
3952
log.Printf("failed to create root dir: %v", err)
4053
}
4154
return chroot.New(fs, string(separator))
4255
}
4356

4457
func (fs *Memory) Create(filename string) (billy.File, error) {
45-
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
58+
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultFileMode)
4659
}
4760

4861
func (fs *Memory) Open(filename string) (billy.File, error) {
@@ -57,7 +70,7 @@ func (fs *Memory) OpenFile(filename string, flag int, perm gofs.FileMode) (billy
5770
}
5871

5972
var err error
60-
f, err = fs.s.New(filename, perm, flag)
73+
f, err = fs.s.New(filename, fs.applyUmask(perm), flag)
6174
if err != nil {
6275
return nil, err
6376
}
@@ -163,7 +176,7 @@ func (fs *Memory) ReadDir(path string) ([]gofs.DirEntry, error) {
163176
}
164177

165178
func (fs *Memory) MkdirAll(path string, perm gofs.FileMode) error {
166-
_, err := fs.s.New(path, perm|os.ModeDir, 0)
179+
_, err := fs.s.New(path, fs.applyUmask(perm)|os.ModeDir, 0)
167180
return err
168181
}
169182

@@ -228,6 +241,13 @@ func (fs *Memory) Capabilities() billy.Capability {
228241
billy.TruncateCapability
229242
}
230243

244+
// applyUmask applies the filesystem's umask to a mode by clearing the bits
245+
// specified in the umask. For example, with umask 0o022, the mode 0o666
246+
// becomes 0o644 (rw-r--r--).
247+
func (fs *Memory) applyUmask(mode gofs.FileMode) gofs.FileMode {
248+
return mode &^ gofs.FileMode(fs.umask)
249+
}
250+
231251
func (c *content) Truncate() {
232252
c.bytes = make([]byte, 0)
233253
}

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: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"reflect"
11+
"runtime"
1112
"slices"
1213
"strings"
1314
"testing"
@@ -22,8 +23,8 @@ import (
2223
func eachBasicFS(t *testing.T, test func(t *testing.T, fs Basic)) {
2324
t.Helper()
2425

25-
for _, fs := range allFS(t.TempDir) {
26-
t.Run(fmt.Sprintf("%T", fs), func(t *testing.T) {
26+
for i, fs := range allFS(t.TempDir) {
27+
t.Run(fmt.Sprintf("%d-%T", i, fs), func(t *testing.T) {
2728
test(t, fs)
2829
})
2930
}
@@ -251,9 +252,14 @@ func TestOpenFileWithModes(t *testing.T) {
251252
require.NoError(t, err)
252253
require.NoError(t, f.Close())
253254

255+
want := customMode
256+
if runtime.GOOS == "windows" {
257+
want = 0o666
258+
}
259+
254260
fi, err := fs.Stat("foo")
255261
require.NoError(t, err)
256-
assert.Equal(t, customMode, fi.Mode())
262+
assert.Equal(t, want, fi.Mode())
257263
})
258264
}
259265

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

463+
want := customMode
464+
if runtime.GOOS == "windows" {
465+
want = 0o666
466+
}
467+
457468
fi, err := fs.Stat("foo/bar")
458469
require.NoError(t, err)
459470
assert.Equal(t, "bar", fi.Name())
460471
assert.Equal(t, int64(3), fi.Size())
461-
assert.Equal(t, customMode, fi.Mode())
472+
assert.Equal(t, want, fi.Mode())
462473
assert.False(t, fi.ModTime().IsZero())
463474
assert.False(t, fi.IsDir())
464475
})

test/symlink_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ func TestStatLink(t *testing.T) {
260260
t.Skip("skipping on Plan 9; symlinks are not supported")
261261
}
262262

263+
want := customMode
263264
eachSymlinkFS(t, func(t *testing.T, fs symlinkFS) {
264265
t.Helper()
265266
err := util.WriteFile(fs, "foo/bar", []byte("foo"), customMode)
@@ -268,11 +269,15 @@ func TestStatLink(t *testing.T) {
268269
err = fs.Symlink("bar", "foo/qux")
269270
require.NoError(t, err)
270271

272+
if runtime.GOOS == "windows" {
273+
want = 0o777
274+
}
275+
271276
fi, err := fs.Stat("foo/qux")
272277
require.NoError(t, err)
273278
assert.Equal(t, fi.Name(), "qux")
274279
assert.Equal(t, fi.Size(), int64(3))
275-
assert.Equal(t, fi.Mode(), customMode)
280+
assert.Equal(t, fi.Mode(), want)
276281
assert.Equal(t, fi.ModTime().IsZero(), false)
277282
assert.Equal(t, fi.IsDir(), false)
278283
})

0 commit comments

Comments
 (0)