From 94b84fc5c88ca2d2c1e07bd4e4260dee80575ec7 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 This is a backport of commit 601c531 on main (#157). Signed-off-by: Conrad Hoffmann --- fs.go | 12 +++++++++--- helper/chroot/chroot.go | 14 ++++++++++++++ memfs/memory.go | 4 ++++ memfs/storage.go | 12 ++++++++++++ osfs/os_bound.go | 8 ++++++++ osfs/os_chroot.go | 4 ++++ 6 files changed, 51 insertions(+), 3 deletions(-) diff --git a/fs.go b/fs.go index d86f9d8..14a020a 100644 --- a/fs.go +++ b/fs.go @@ -128,12 +128,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 os.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 8b44e78..dbdf111 100644 --- a/helper/chroot/chroot.go +++ b/helper/chroot/chroot.go @@ -1,6 +1,7 @@ package chroot import ( + "errors" "os" "path/filepath" "strings" @@ -200,6 +201,19 @@ func (fs *ChrootHelper) Readlink(link string) (string, error) { return string(os.PathSeparator) + target, nil } +func (fs *ChrootHelper) Chmod(path string, mode os.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 6cbd7d0..152cb9e 100644 --- a/memfs/memory.go +++ b/memfs/memory.go @@ -177,6 +177,10 @@ func (fs *Memory) Remove(filename string) error { return fs.s.Remove(filename) } +func (fs *Memory) Chmod(path string, mode os.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 16b48ce..9960996 100644 --- a/memfs/storage.go +++ b/memfs/storage.go @@ -169,6 +169,18 @@ func (s *storage) Remove(path string) error { return nil } +func (s *storage) Chmod(path string, mode os.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 c0a6109..6f54480 100644 --- a/osfs/os_bound.go +++ b/osfs/os_bound.go @@ -176,6 +176,14 @@ func (fs *BoundOS) Readlink(link string) (string, error) { return os.Readlink(link) } +func (fs *BoundOS) Chmod(path string, mode os.FileMode) error { + abspath, err := fs.abs(path) + if err != nil { + return err + } + return os.Chmod(abspath, mode) +} + // Chroot returns a new OS 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 fd65e77..413b3b8 100644 --- a/osfs/os_chroot.go +++ b/osfs/os_chroot.go @@ -74,6 +74,10 @@ func (fs *ChrootOS) Remove(filename string) error { return os.Remove(filename) } +func (fs *ChrootOS) Chmod(path string, mode os.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