Skip to content

Commit e9738f5

Browse files
authored
Merge pull request #179 from go-git/memfm
memfs: Add support for umask
2 parents 8afc3eb + b4794ab commit e9738f5

File tree

6 files changed

+198
-18
lines changed

6 files changed

+198
-18
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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,126 @@ 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+
if runtime.GOOS == "windows" && tt.umask == nil {
448+
tt.expectedFileMode |= 0o022
449+
tt.expectedDirMode |= 0o022
450+
}
451+
452+
fi, err := fs.Stat("file.txt")
453+
require.NoError(t, err)
454+
assert.Equal(t, tt.expectedFileMode, fi.Mode().Perm())
455+
456+
err = fs.MkdirAll("testdir", 0o777)
457+
require.NoError(t, err)
458+
459+
fi, err = fs.Stat("testdir")
460+
require.NoError(t, err)
461+
assert.Equal(t, tt.expectedDirMode, fi.Mode().Perm())
462+
})
463+
}
464+
}
465+
466+
func TestUmaskOpenFile(t *testing.T) {
467+
fs := New(WithUmask(0o077))
468+
469+
f, err := fs.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0o666)
470+
require.NoError(t, err)
471+
assert.NoError(t, f.Close())
472+
473+
fi, err := fs.Stat("test.txt")
474+
require.NoError(t, err)
475+
assert.Equal(t, os.FileMode(0o600), fi.Mode().Perm())
476+
477+
// Re-do test without the use of os.O_CREATE.
478+
err = util.WriteFile(fs, "test2.txt", []byte("content"), 0o666)
479+
require.NoError(t, err)
480+
481+
fi2, err := fs.Stat("test2.txt")
482+
require.NoError(t, err)
483+
assert.Equal(t, os.FileMode(0o600), fi2.Mode().Perm())
484+
485+
f2, err := fs.OpenFile("test2.txt", os.O_RDWR, 0o777)
486+
require.NoError(t, err)
487+
assert.NoError(t, f2.Close())
488+
489+
fi2, err = fs.Stat("test2.txt")
490+
require.NoError(t, err)
491+
// Mode must not be changed by OpenFile without os.O_CREATE.
492+
assert.Equal(t, os.FileMode(0o600), fi2.Mode().Perm())
493+
}
494+
495+
func TestUmaskChmod(t *testing.T) {
496+
fs := New()
497+
498+
f, err := fs.Create("/test.txt")
499+
require.NoError(t, err)
500+
assert.NoError(t, f.Close())
501+
502+
want := 0o644
503+
if runtime.GOOS == "windows" {
504+
want |= 0o022
505+
}
506+
507+
fi, err := fs.Stat("/test.txt")
508+
require.NoError(t, err)
509+
assert.Equal(t, os.FileMode(want), fi.Mode().Perm())
510+
511+
ch, ok := fs.(billy.Chmod)
512+
require.True(t, ok, "fs does not implement billy.Chmod")
513+
514+
err = ch.Chmod("/test.txt", 0o421)
515+
require.NoError(t, err)
516+
517+
fi, err = fs.Stat("/test.txt")
518+
require.NoError(t, err)
519+
assert.Equal(t, os.FileMode(0o421), fi.Mode().Perm())
520+
}
521+
522+
func TestUmaskRootDirectory(t *testing.T) {
523+
fs := New(WithUmask(0o077))
524+
525+
fi, err := fs.Stat("/")
526+
require.NoError(t, err)
527+
assert.Equal(t, os.FileMode(0o700), fi.Mode().Perm())
528+
}

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: 10 additions & 5 deletions
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,13 +269,17 @@ 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 |= 0o022
274+
}
275+
271276
fi, err := fs.Stat("foo/qux")
272277
require.NoError(t, err)
273-
assert.Equal(t, fi.Name(), "qux")
274-
assert.Equal(t, fi.Size(), int64(3))
275-
assert.Equal(t, fi.Mode(), customMode)
276-
assert.Equal(t, fi.ModTime().IsZero(), false)
277-
assert.Equal(t, fi.IsDir(), false)
278+
assert.Equal(t, "qux", fi.Name())
279+
assert.Equal(t, int64(3), fi.Size())
280+
assert.Equal(t, want, fi.Mode())
281+
assert.Equal(t, false, fi.ModTime().IsZero())
282+
assert.Equal(t, false, fi.IsDir())
278283
})
279284
}
280285

0 commit comments

Comments
 (0)