Repository: diskfs/go-diskfs
Pull request #203 (2023-12-21 18:50:57)

squashfs: implement a way to read the target of symlinks
This implements a way to read the target of symlinks as discussed in #200 

It is divided into two parts, the first a bit of refactoring, then the second the implementation of the new method.

- squashfs: move UID(),GID(),Xattr() methods to directory entry #200
- squashfs: implement Readlink() method on directory entry



Patch:
diff --git a/filesystem/squashfs/directoryentry.go b/filesystem/squashfs/directoryentry.go
index 44e1aa9..760bcf0 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/file.go b/filesystem/squashfs/file.go
index e7c5790..1b77d96 100644
--- a/filesystem/squashfs/file.go
+++ b/filesystem/squashfs/file.go
@@ -13,10 +13,12 @@ import (
 //	include all of the data
 type File struct {
 	*extendedFile
-	isReadWrite bool
-	isAppend    bool
-	offset      int64
-	filesystem  *FileSystem
+	isReadWrite   bool
+	isAppend      bool
+	offset        int64
+	filesystem    *FileSystem
+	blockLocation int64  // the position of the last block decompressed
+	block         []byte // the actual last block decompressed
 }
 
 // Read reads up to len(b) bytes from the File.
@@ -99,9 +101,19 @@ func (fl *File) Read(b []byte) (int, error) {
 			if int64(block.size) > fs.blocksize {
 				return read, fmt.Errorf("unexpected block.size=%d > fs.blocksize=%d", block.size, fs.blocksize)
 			}
-			input, err := fs.readBlock(location, block.compressed, block.size)
-			if err != nil {
-				return read, fmt.Errorf("error reading data block %d from squashfs: %v", i, err)
+			var input []byte
+			if fl.blockLocation == location && fl.block != nil {
+				// Read last block from cache
+				input = fl.block
+			} else {
+				var err error
+				input, err = fs.readBlock(location, block.compressed, block.size)
+				if err != nil {
+					return read, fmt.Errorf("error reading data block %d from squashfs: %v", i, err)
+				}
+				// Cache the last block
+				fl.blockLocation = location
+				fl.block = input
 			}
 			outputBlock(input)
 		}
diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go
index 0049c26..294cd6b 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


Test patch:
diff --git a/filesystem/squashfs/const_internal_test.go b/filesystem/squashfs/const_internal_test.go
index 56c47cf..0f48363 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_internal_test.go b/filesystem/squashfs/directoryentry_internal_test.go
index 20481e2..c44e087 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_test.go b/filesystem/squashfs/squashfs_test.go
index 4d53c75..75c8209 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)
+				}
+			}
 		}
 	}