Skip to content

Commit 31cc4fe

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. Signed-off-by: Paulo Gomes <pjbgf@linux.com>
1 parent 8afc3eb commit 31cc4fe

File tree

3 files changed

+149
-9
lines changed

3 files changed

+149
-9
lines changed

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+
}

0 commit comments

Comments
 (0)