Skip to content

Commit

Permalink
squashfs: implement a way to read the target of symlinks (#203)
Browse files Browse the repository at this point in the history
* squashfs: move UID(),GID(),Xattr() methods to directory entry #200

This moves the auxillary methods UID(),GID() and Xattr() onto the
directoryEntry directly and makes the FileStat a type alias for a
pointer to a directory entry.

This enables more methods to be added to the directory entry easily.

* squashfs: implement Readlink() method on directory entry

Fixes #200
  • Loading branch information
ncw authored Dec 22, 2023
1 parent ef160a9 commit 07630e2
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 67 deletions.
16 changes: 8 additions & 8 deletions filesystem/squashfs/const_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
}
}

Expand Down
93 changes: 49 additions & 44 deletions filesystem/squashfs/directoryentry.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
19 changes: 10 additions & 9 deletions filesystem/squashfs/directoryentry_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package squashfs

import (
"reflect"
"testing"
"time"
)
Expand All @@ -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:
Expand All @@ -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)
}
}
8 changes: 3 additions & 5 deletions filesystem/squashfs/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion filesystem/squashfs/squashfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"io"
stdfs "io/fs"
"os"
"path"
"strconv"
Expand Down Expand Up @@ -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)
}
}
}
}

Expand Down

0 comments on commit 07630e2

Please sign in to comment.