diff --git a/filesystem/squashfs/const_internal_test.go b/filesystem/squashfs/const_internal_test.go index 56c47cf2..0f483630 100644 --- a/filesystem/squashfs/const_internal_test.go +++ b/filesystem/squashfs/const_internal_test.go @@ -220,14 +220,14 @@ func testGetFilesystemRoot() []*directoryEntry { // data taken from reading the bytes of the file SquashfsUncompressedfile modTime := time.Unix(0x5c20d8d7, 0) return []*directoryEntry{ - {true, "foo", 9949, modTime, 0o755, nil, FileStat{0, 0, map[string]string{}}}, - {true, "zero", 32, modTime, 0o755, nil, FileStat{0, 0, map[string]string{}}}, - {true, "random", 32, modTime, 0o755, nil, FileStat{0, 0, map[string]string{}}}, - {false, "emptylink", 0, modTime, 0o777, nil, FileStat{0, 0, map[string]string{}}}, - {false, "goodlink", 0, modTime, 0o777, nil, FileStat{0, 0, map[string]string{}}}, - {false, "hardlink", 7, modTime, 0o644, nil, FileStat{1, 2, map[string]string{}}}, - {false, "README.md", 7, modTime, 0o644, nil, FileStat{1, 2, map[string]string{}}}, - {false, "attrfile", 5, modTime, 0o644, nil, FileStat{0, 0, map[string]string{"abc": "def", "myattr": "hello"}}}, + {true, "foo", 9949, modTime, 0o755, nil, 0, 0, map[string]string{}}, + {true, "zero", 32, modTime, 0o755, nil, 0, 0, map[string]string{}}, + {true, "random", 32, modTime, 0o755, nil, 0, 0, map[string]string{}}, + {false, "emptylink", 0, modTime, 0o777, nil, 0, 0, map[string]string{}}, + {false, "goodlink", 0, modTime, 0o777, nil, 0, 0, map[string]string{}}, + {false, "hardlink", 7, modTime, 0o644, nil, 1, 2, map[string]string{}}, + {false, "README.md", 7, modTime, 0o644, nil, 1, 2, map[string]string{}}, + {false, "attrfile", 5, modTime, 0o644, nil, 0, 0, map[string]string{"abc": "def", "myattr": "hello"}}, } } diff --git a/filesystem/squashfs/directoryentry.go b/filesystem/squashfs/directoryentry.go index 44e1aa96..760bcf0a 100644 --- a/filesystem/squashfs/directoryentry.go +++ b/filesystem/squashfs/directoryentry.go @@ -1,50 +1,14 @@ package squashfs import ( + "fmt" + "io/fs" "os" "time" ) // FileStat is the extended data underlying a single file, similar to https://golang.org/pkg/syscall/#Stat_t -type FileStat struct { - uid uint32 - gid uint32 - xattrs map[string]string -} - -func (f *FileStat) equal(o *FileStat) bool { - if f.uid != o.uid || f.gid != o.gid { - return false - } - if len(f.xattrs) != len(o.xattrs) { - return false - } - for k, v := range f.xattrs { - ov, ok := o.xattrs[k] - if !ok { - return false - } - if ov != v { - return false - } - } - return true -} - -// UID get uid of file -func (f *FileStat) UID() uint32 { - return f.uid -} - -// GID get gid of file -func (f *FileStat) GID() uint32 { - return f.gid -} - -// Xattrs get extended attributes of file -func (f *FileStat) Xattrs() map[string]string { - return f.xattrs -} +type FileStat = *directoryEntry // directoryEntry is a single directory entry // it combines information from inode and the actual entry @@ -63,16 +27,15 @@ type directoryEntry struct { modTime time.Time mode os.FileMode inode inode - sys FileStat + uid uint32 + gid uint32 + xattrs map[string]string } func (d *directoryEntry) equal(o *directoryEntry) bool { if o == nil { return false } - if !d.sys.equal(&o.sys) { - return false - } if d.inode == nil && o.inode == nil { return true } @@ -151,5 +114,47 @@ func (d *directoryEntry) Mode() os.FileMode { // Sys interface{} // underlying data source (can return nil) func (d *directoryEntry) Sys() interface{} { - return d.sys + return d +} + +// UID get uid of file +func (d *directoryEntry) UID() uint32 { + return d.uid +} + +// GID get gid of file +func (d *directoryEntry) GID() uint32 { + return d.gid +} + +// Xattrs get extended attributes of file +func (d *directoryEntry) Xattrs() map[string]string { + return d.xattrs +} + +// Readlink returns the destination of the symbolic link if this entry +// is a symbolic link. +// +// If this entry is not a symbolic link then it will return fs.ErrNotExist +func (d *directoryEntry) Readlink() (string, error) { + var target string + body := d.inode.getBody() + //nolint:exhaustive // all other cases fall under default + switch d.inode.inodeType() { + case inodeBasicSymlink: + link, ok := body.(*basicSymlink) + if !ok { + return "", fmt.Errorf("internal error: inode wasn't basic symlink: %T", body) + } + target = link.target + case inodeExtendedSymlink: + link, ok := body.(*extendedSymlink) + if !ok { + return "", fmt.Errorf("internal error: inode wasn't extended symlink: %T", body) + } + target = link.target + default: + return "", fs.ErrNotExist + } + return target, nil } diff --git a/filesystem/squashfs/directoryentry_internal_test.go b/filesystem/squashfs/directoryentry_internal_test.go index 20481e2c..c44e087f 100644 --- a/filesystem/squashfs/directoryentry_internal_test.go +++ b/filesystem/squashfs/directoryentry_internal_test.go @@ -1,6 +1,7 @@ package squashfs import ( + "reflect" "testing" "time" ) @@ -12,6 +13,9 @@ func TestDirectoryEntry(t *testing.T) { size: 8675309, modTime: time.Now(), mode: 0o766, + uid: 32, + gid: 33, + xattrs: map[string]string{"test": "value"}, } switch { case de.Name() != de.name: @@ -28,23 +32,20 @@ func TestDirectoryEntry(t *testing.T) { t.Errorf("Mismatched Sys(), unexpected nil") } // check that Sys() is convertible - if _, ok := de.Sys().(FileStat); !ok { + fs, ok := de.Sys().(FileStat) + if !ok { t.Errorf("Mismatched Sys(), could not convert to FileStat") } -} - -func TestFileStatUID(t *testing.T) { - fs := FileStat{uid: 32} uid := fs.UID() if uid != fs.uid { t.Errorf("Mismatched UID, actual %d expected %d", uid, fs.uid) } -} - -func TestFileStatGID(t *testing.T) { - fs := FileStat{gid: 32} gid := fs.GID() if gid != fs.gid { t.Errorf("Mismatched GID, actual %d expected %d", gid, fs.gid) } + xattrs := fs.Xattrs() + if !reflect.DeepEqual(xattrs, fs.xattrs) { + t.Errorf("Mismatched Xattrs, actual %+v expected %+v", xattrs, fs.xattrs) + } } diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go index 0049c263..294cd6bb 100644 --- a/filesystem/squashfs/squashfs.go +++ b/filesystem/squashfs/squashfs.go @@ -446,11 +446,9 @@ func (fs *FileSystem) hydrateDirectoryEntries(entries []*directoryEntryRaw) ([]* modTime: header.modTime, mode: header.mode, inode: in, - sys: FileStat{ - uid: fs.uidsGids[header.uidIdx], - gid: fs.uidsGids[header.gidIdx], - xattrs: xattrs, - }, + uid: fs.uidsGids[header.uidIdx], + gid: fs.uidsGids[header.gidIdx], + xattrs: xattrs, }) } return fullEntries, nil diff --git a/filesystem/squashfs/squashfs_test.go b/filesystem/squashfs/squashfs_test.go index 4d53c754..75c82099 100644 --- a/filesystem/squashfs/squashfs_test.go +++ b/filesystem/squashfs/squashfs_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "io" + stdfs "io/fs" "os" "path" "strconv" @@ -357,14 +358,36 @@ func TestSquashfsCheckListing(t *testing.T) { if fi.IsDir() { wantMode |= os.ModeDir } + var wantTarget string switch p { - case "/symlink", "/goodlink", "/emptylink": + case "/goodlink": + wantTarget = "README.md" + wantMode |= os.ModeSymlink + case "/emptylink": + wantTarget = "/a/b/c/d/ef/g/h" wantMode |= os.ModeSymlink } gotMode := fi.Mode() if (gotMode & os.ModeType) != wantMode { t.Errorf("%s: want mode 0o%o got mode 0o%o", p, wantMode, gotMode&os.ModeType) } + fix, ok := fi.Sys().(squashfs.FileStat) + if !ok { + t.Fatal("Wrong type") + } + gotTarget, err := fix.Readlink() + if wantTarget == "" { + if err != stdfs.ErrNotExist { + t.Errorf("%s: ReadLink want error %q got error %q", p, stdfs.ErrNotExist, err) + } + } else { + if err != nil { + t.Errorf("%s: ReadLink returned error: %v", p, err) + } + if wantTarget != gotTarget { + t.Errorf("%s: ReadLink want target %q got target %q", p, wantTarget, gotTarget) + } + } } }