diff --git a/filesystem/ext4/ext4.go b/filesystem/ext4/ext4.go index c00ab2b..3f3eef8 100644 --- a/filesystem/ext4/ext4.go +++ b/filesystem/ext4/ext4.go @@ -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. @@ -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 } diff --git a/filesystem/ext4/ext4_test.go b/filesystem/ext4/ext4_test.go index 1f326da..af36fc2 100644 --- a/filesystem/ext4/ext4_test.go +++ b/filesystem/ext4/ext4_test.go @@ -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) + } + }) +} diff --git a/filesystem/ext4/file.go b/filesystem/ext4/file.go index 8104569..876ade4 100644 --- a/filesystem/ext4/file.go +++ b/filesystem/ext4/file.go @@ -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 } diff --git a/filesystem/ext4/fileinfo.go b/filesystem/ext4/fileinfo.go index 4a6e5c3..31fdf5c 100644 --- a/filesystem/ext4/fileinfo.go +++ b/filesystem/ext4/fileinfo.go @@ -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() @@ -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 } diff --git a/filesystem/ext4/inode.go b/filesystem/ext4/inode.go index 24db4ed..4ec4d77 100644 --- a/filesystem/ext4/inode.go +++ b/filesystem/ext4/inode.go @@ -3,6 +3,7 @@ package ext4 import ( "encoding/binary" "fmt" + "os" "time" "github.com/diskfs/go-diskfs/filesystem/ext4/crc" @@ -67,6 +68,9 @@ const ( filePermissionsOtherExecute uint16 = 0x1 filePermissionsOtherWrite uint16 = 0x2 filePermissionsOtherRead uint16 = 0x4 + filePermissionsSticky uint16 = 0x200 + filePermissionsGroupSetgid uint16 = 0x400 + filePermissionsOwnerSetuid uint16 = 0x800 ) // mountOptions is a structure holding flags for an inode @@ -104,6 +108,7 @@ type filePermissions struct { read bool write bool execute bool + special bool } // inode is a structure holding the data about an inode @@ -392,11 +397,74 @@ func (i *inode) toBytes(sb *superblock) []byte { return b } +func (i *inode) permissionsToMode() os.FileMode { + var mode os.FileMode + + // Map filetype to filemode + switch i.fileType { + case fileTypeRegularFile: + // no extra bits for regular files + case fileTypeDirectory: + mode |= os.ModeDir + case fileTypeSymbolicLink: + mode |= os.ModeSymlink + case fileTypeCharacterDevice: + mode |= os.ModeDevice | os.ModeCharDevice + case fileTypeBlockDevice: + mode |= os.ModeDevice + case fileTypeFifo: + mode |= os.ModeNamedPipe + case fileTypeSocket: + mode |= os.ModeSocket + } + + // Map permissions + if i.permissionsOwner.read { + mode |= 0o400 + } + if i.permissionsOwner.write { + mode |= 0o200 + } + if i.permissionsOwner.execute { + mode |= 0o100 + } + if i.permissionsOwner.special { + mode |= os.ModeSetuid + } + if i.permissionsGroup.read { + mode |= 0o040 + } + if i.permissionsGroup.write { + mode |= 0o020 + } + if i.permissionsGroup.execute { + mode |= 0o010 + } + if i.permissionsGroup.special { + mode |= os.ModeSetgid + } + if i.permissionsOther.read { + mode |= 0o004 + } + if i.permissionsOther.write { + mode |= 0o002 + } + if i.permissionsOther.execute { + mode |= 0o001 + } + if i.permissionsOther.special { + mode |= os.ModeSticky + } + + return mode +} + func parseOwnerPermissions(mode uint16) filePermissions { return filePermissions{ execute: mode&filePermissionsOwnerExecute == filePermissionsOwnerExecute, write: mode&filePermissionsOwnerWrite == filePermissionsOwnerWrite, read: mode&filePermissionsOwnerRead == filePermissionsOwnerRead, + special: mode&filePermissionsOwnerSetuid == filePermissionsOwnerSetuid, } } func parseGroupPermissions(mode uint16) filePermissions { @@ -404,6 +472,7 @@ func parseGroupPermissions(mode uint16) filePermissions { execute: mode&filePermissionsGroupExecute == filePermissionsGroupExecute, write: mode&filePermissionsGroupWrite == filePermissionsGroupWrite, read: mode&filePermissionsGroupRead == filePermissionsGroupRead, + special: mode&filePermissionsGroupSetgid == filePermissionsGroupSetgid, } } func parseOtherPermissions(mode uint16) filePermissions { @@ -411,6 +480,7 @@ func parseOtherPermissions(mode uint16) filePermissions { execute: mode&filePermissionsOtherExecute == filePermissionsOtherExecute, write: mode&filePermissionsOtherWrite == filePermissionsOtherWrite, read: mode&filePermissionsOtherRead == filePermissionsOtherRead, + special: mode&filePermissionsSticky == filePermissionsSticky, } } @@ -426,6 +496,9 @@ func (fp *filePermissions) toOwnerInt() uint16 { if fp.read { mode |= filePermissionsOwnerRead } + if fp.special { + mode |= filePermissionsOwnerSetuid + } return mode } @@ -441,6 +514,9 @@ func (fp *filePermissions) toOtherInt() uint16 { if fp.read { mode |= filePermissionsOtherRead } + if fp.special { + mode |= filePermissionsSticky + } return mode } @@ -456,6 +532,9 @@ func (fp *filePermissions) toGroupInt() uint16 { if fp.read { mode |= filePermissionsGroupRead } + if fp.special { + mode |= filePermissionsGroupSetgid + } return mode } diff --git a/filesystem/ext4/testdata/buildimg.sh b/filesystem/ext4/testdata/buildimg.sh index 84c98a6..a399b3f 100755 --- a/filesystem/ext4/testdata/buildimg.sh +++ b/filesystem/ext4/testdata/buildimg.sh @@ -49,5 +49,5 @@ dd if=superblock.bin count=2 skip=376 bs=1 2>/dev/null| hexdump -e '1/2 "%u"' > # first, create a 1024-byte zero prefix dd if=/dev/zero of=ext4-offset.img bs=1024 count=1 # then write the original image starting at offset 1024 -dd if=ext4.img of=ext4-offset.img bs=1 seek=1024 conv=notrunc +dd if=ext4.img of=ext4-offset.img bs=1024 seek=1 conv=notrunc EOF