From 601c5311453241fd2ce17abb7b8dcf14600bdee7 Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Tue, 21 Oct 2025 22:17:52 +0200 Subject: [PATCH] Add support for Chmod on billy.Filesystem The billy package contains a `Change` interface type, which seems to have gone unused for several years now, presumably due to the difficulty of implementing most of the required methods for all supported platforms. This commit splits out a `Chmod` interface from it, which is supported on all platforms. The interface is implemented in all applicable abstractions. Supporting `chmod` in billy would help with issues such as https://github.com/go-git/go-git/issues/588 Signed-off-by: Conrad Hoffmann --- fs.go | 12 +++++++++--- helper/chroot/chroot.go | 13 +++++++++++++ memfs/memory.go | 4 ++++ memfs/storage.go | 12 ++++++++++++ osfs/os_bound.go | 8 ++++++++ osfs/os_chroot.go | 4 ++++ 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/fs.go b/fs.go index b822ff3..653b4d3 100644 --- a/fs.go +++ b/fs.go @@ -132,12 +132,18 @@ type Symlink interface { Readlink(link string) (string, error) } -// Change abstract the FileInfo change related operations in a storage-agnostic -// interface as an extension to the Basic interface -type Change interface { +// Chmod abstracts the logic around changing file modes. +type Chmod interface { // Chmod changes the mode of the named file to mode. If the file is a // symbolic link, it changes the mode of the link's target. Chmod(name string, mode fs.FileMode) error +} + +// Change abstract the FileInfo change related operations in a storage-agnostic +// interface as an extension to the Basic interface +type Change interface { + Chmod + // Lchown changes the numeric uid and gid of the named file. If the file is // a symbolic link, it changes the uid and gid of the link itself. Lchown(name string, uid, gid int) error diff --git a/helper/chroot/chroot.go b/helper/chroot/chroot.go index b4b5b25..8966e17 100644 --- a/helper/chroot/chroot.go +++ b/helper/chroot/chroot.go @@ -228,6 +228,19 @@ func (fs *ChrootHelper) Readlink(link string) (string, error) { return string(os.PathSeparator) + target, nil } +func (fs *ChrootHelper) Chmod(path string, mode fs.FileMode) error { + fullpath, err := fs.underlyingPath(path) + if err != nil { + return err + } + + c, ok := fs.underlying.(billy.Chmod) + if !ok { + return errors.New("underlying fs does not implement billy.Chmod") + } + return c.Chmod(fullpath, mode) +} + func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) { fullpath, err := fs.underlyingPath(path) if err != nil { diff --git a/memfs/memory.go b/memfs/memory.go index ed45071..9ac8891 100644 --- a/memfs/memory.go +++ b/memfs/memory.go @@ -179,6 +179,10 @@ func (fs *Memory) Remove(filename string) error { return fs.s.Remove(filename) } +func (fs *Memory) Chmod(path string, mode gofs.FileMode) error { + return fs.s.Chmod(path, mode) +} + // Falls back to Go's filepath.Join, which works differently depending on the // OS where the code is being executed. func (fs *Memory) Join(elem ...string) string { diff --git a/memfs/storage.go b/memfs/storage.go index 4412980..051129f 100644 --- a/memfs/storage.go +++ b/memfs/storage.go @@ -222,6 +222,18 @@ func (s *storage) Remove(path string) error { return nil } +func (s *storage) Chmod(path string, mode fs.FileMode) error { + path = clean(path) + + f, has := s.Get(path) + if !has { + return os.ErrNotExist + } + + f.mode = mode + return nil +} + func clean(path string) string { return filepath.Clean(filepath.FromSlash(path)) } diff --git a/osfs/os_bound.go b/osfs/os_bound.go index 6f9b539..ccbb67d 100644 --- a/osfs/os_bound.go +++ b/osfs/os_bound.go @@ -223,6 +223,14 @@ func (fs *BoundOS) Readlink(link string) (string, error) { return os.Readlink(link) } +func (fs *BoundOS) Chmod(path string, mode fs.FileMode) error { + abspath, err := fs.abs(path) + if err != nil { + return err + } + return os.Chmod(abspath, mode) +} + // Chroot returns a new BoundOS filesystem, with the base dir set to the // result of joining the provided path with the underlying base dir. func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { diff --git a/osfs/os_chroot.go b/osfs/os_chroot.go index 2cde299..f51e3ec 100644 --- a/osfs/os_chroot.go +++ b/osfs/os_chroot.go @@ -79,6 +79,10 @@ func (fs *ChrootOS) Remove(filename string) error { return os.Remove(filename) } +func (fs *ChrootOS) Chmod(path string, mode fs.FileMode) error { + return os.Chmod(path, mode) +} + func (fs *ChrootOS) TempFile(dir, prefix string) (billy.File, error) { if err := fs.createDir(dir + string(os.PathSeparator)); err != nil { return nil, err