Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions filesystem/ext4/ext4.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,18 +779,99 @@ func (fs *FileSystem) Chtimes(p string, ctime, atime, mtime time.Time) error {

// 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.
//
//nolint:revive // parameters will be used eventually
func (fs *FileSystem) Chmod(name string, mode os.FileMode) error {
return filesystem.ErrNotImplemented
if err := validatePath(name); err != nil {
return err
}

_, entry, err := fs.getEntryAndParent(name)
if err != nil {
return err
}
if entry == nil {
return fmt.Errorf("target file %s does not exist", name)
}

// get the inode
inodeNumber := entry.inode
inode, err := fs.readInode(inodeNumber)
if err != nil {
return fmt.Errorf("could not read inode number %d: %v", inodeNumber, err)
}

// if a symlink, follow it
if inode.fileType == fileTypeSymbolicLink {
linkTarget := inode.linkTarget
if !path.IsAbs(linkTarget) {
dir := path.Dir(name)
linkTarget = path.Join(dir, linkTarget)
linkTarget = path.Clean(linkTarget)
}
return fs.Chmod(linkTarget, mode)
}

// update permissions
perm := uint16(mode.Perm())
inode.permissionsOwner = parseOwnerPermissions(perm)
inode.permissionsGroup = parseGroupPermissions(perm)
inode.permissionsOther = parseOtherPermissions(perm)

// handle special bits (setuid, setgid, sticky)
if mode&os.ModeSetuid != 0 {
inode.permissionsOwner.special = true
}
if mode&os.ModeSetgid != 0 {
inode.permissionsGroup.special = true
}
if mode&os.ModeSticky != 0 {
inode.permissionsOther.special = true
}

return fs.writeInode(inode)
}

// Chown 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's target. A uid or gid of -1 means to not change that value
//
//nolint:revive // parameters will be used eventually
func (fs *FileSystem) Chown(name string, uid, gid int) error {
return filesystem.ErrNotImplemented
if err := validatePath(name); err != nil {
return err
}

_, entry, err := fs.getEntryAndParent(name)
if err != nil {
return err
}
if entry == nil {
return fmt.Errorf("target file %s does not exist", name)
}

// get the inode
inodeNumber := entry.inode
inode, err := fs.readInode(inodeNumber)
if err != nil {
return fmt.Errorf("could not read inode number %d: %v", inodeNumber, err)
}

// if a symlink, follow it
if inode.fileType == fileTypeSymbolicLink {
linkTarget := inode.linkTarget
if !path.IsAbs(linkTarget) {
dir := path.Dir(name)
linkTarget = path.Join(dir, linkTarget)
linkTarget = path.Clean(linkTarget)
}
return fs.Chown(linkTarget, uid, gid)
}

// update uid and gid
if uid != -1 {
inode.owner = uint32(uid)
}
if gid != -1 {
inode.group = uint32(gid)
}

return fs.writeInode(inode)
}

// ReadDir return the contents of a given directory in a given filesystem.
Expand Down Expand Up @@ -1202,6 +1283,11 @@ func (fs *FileSystem) Stat(p string) (iofs.FileInfo, error) {
name: entry.filename,
size: int64(in.size),
isDir: entry.fileType == dirFileTypeDirectory,
mode: in.permissionsToMode(),
sys: &StatT{
UID: in.owner,
GID: in.group,
},
}, nil
}

Expand Down
165 changes: 165 additions & 0 deletions filesystem/ext4/ext4_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,168 @@ func TestChtimes(t *testing.T) {
})
}
}

func TestChmod(t *testing.T) {
outfile := testCreateImgCopyFrom(t, imgFile)
f, err := os.OpenFile(outfile, os.O_RDWR, 0)
if err != nil {
t.Fatalf("Error opening test image: %v", err)
}
defer f.Close()

b := file.New(f, false)
fs, err := Read(b, 100*MB, 0, 512)
if err != nil {
t.Fatalf("Error reading filesystem: %v", err)
}

targetFile := "shortfile.txt"
tests := []struct {
name string
mode os.FileMode
}{
{"0755", 0o755},
{"0644", 0o644},
{"0000", 0o000},
{"0777", 0o777},
{"sticky", 0o644 | os.ModeSticky},
{"setuid", 0o755 | os.ModeSetuid},
{"setgid", 0o755 | os.ModeSetgid},
{"all-special", 0o777 | os.ModeSticky | os.ModeSetuid | os.ModeSetgid},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := fs.Chmod(targetFile, tt.mode)
if err != nil {
t.Fatalf("Chmod failed: %v", err)
}

fi, err := fs.Stat(targetFile)
if err != nil {
t.Fatalf("Stat failed: %v", err)
}

if fi.Mode() != tt.mode {
t.Errorf("expected mode %v, got %v", tt.mode, fi.Mode())
}
})
}

t.Run("symlink", func(t *testing.T) {
link := "symlink.dat"
target := "random.dat"
mode := os.FileMode(0o600)

err := fs.Chmod(link, mode)
if err != nil {
t.Fatalf("Chmod on symlink failed: %v", err)
}

// Check target
fi, err := fs.Stat(target)
if err != nil {
t.Fatalf("Stat on target failed: %v", err)
}
if fi.Mode().Perm() != mode.Perm() {
t.Errorf("expected target mode %v, got %v", mode, fi.Mode())
}
})
}

func TestChown(t *testing.T) {
outfile := testCreateImgCopyFrom(t, imgFile)
f, err := os.OpenFile(outfile, os.O_RDWR, 0)
if err != nil {
t.Fatalf("Error opening test image: %v", err)
}
defer f.Close()

b := file.New(f, false)
fs, err := Read(b, 100*MB, 0, 512)
if err != nil {
t.Fatalf("Error reading filesystem: %v", err)
}

targetFile := "shortfile.txt"
tests := []struct {
name string
uid int
gid int
}{
{"change-both", 1000, 2000},
{"change-uid", 500, -1},
{"change-gid", -1, 600},
{"no-change", -1, -1},
{"root", 0, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Get initial values if we are not changing them
fiOld, err := fs.Stat(targetFile)
if err != nil {
t.Fatalf("Stat failed: %v", err)
}
statOld, ok := fiOld.Sys().(*StatT)
if !ok {
t.Fatalf("Sys() did not return *StatT")
}

err = fs.Chown(targetFile, tt.uid, tt.gid)
if err != nil {
t.Fatalf("Chown failed: %v", err)
}

fi, err := fs.Stat(targetFile)
if err != nil {
t.Fatalf("Stat failed: %v", err)
}
stat, ok := fi.Sys().(*StatT)
if !ok {
t.Fatalf("Sys() did not return *StatT")
}

expectedUID := uint32(tt.uid)
if tt.uid == -1 {
expectedUID = statOld.UID
}
expectedGID := uint32(tt.gid)
if tt.gid == -1 {
expectedGID = statOld.GID
}

if stat.UID != expectedUID {
t.Errorf("expected uid %d, got %d", expectedUID, stat.UID)
}
if stat.GID != expectedGID {
t.Errorf("expected gid %d, got %d", expectedGID, stat.GID)
}
})
}

t.Run("symlink", func(t *testing.T) {
link := "symlink.dat"
target := "random.dat"
uid, gid := 123, 456

err := fs.Chown(link, uid, gid)
if err != nil {
t.Fatalf("Chown on symlink failed: %v", err)
}

// Check target
fi, err := fs.Stat(target)
if err != nil {
t.Fatalf("Stat on target failed: %v", err)
}
stat, ok := fi.Sys().(*StatT)
if !ok {
t.Fatalf("Sys() did not return *StatT")
}

if int(stat.UID) != uid || int(stat.GID) != gid {
t.Errorf("expected target uid:gid %d:%d, got %d:%d", uid, gid, stat.UID, stat.GID)
}
})
}
5 changes: 5 additions & 0 deletions filesystem/ext4/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,10 @@ func (fl *File) Stat() (fs.FileInfo, error) {
name: fl.filename,
size: int64(fl.size),
isDir: fl.directoryEntry.fileType == dirFileTypeDirectory,
mode: fl.permissionsToMode(),
sys: &StatT{
UID: fl.owner,
GID: fl.group,
},
}, nil
}
8 changes: 7 additions & 1 deletion filesystem/ext4/fileinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ type FileInfo struct {
name string
size int64
isDir bool
sys *StatT
}

type StatT struct {
UID uint32
GID uint32
}

// IsDir abbreviation for Mode().IsDir()
Expand Down Expand Up @@ -44,5 +50,5 @@ func (fi *FileInfo) Size() int64 {

// Sys underlying data source - not supported yet and so will return nil
func (fi *FileInfo) Sys() interface{} {
return nil
return fi.sys
}
Loading