Repository: diskfs/go-diskfs
Pull request #300 (2025-06-24 16:34:50)

fix: handle caching sparse reading block
Fix: #299

Cache the previous block size too, so when deciding to use the cached block can compare size to handle sparse blocks 

deitch: @cusspvz is this good to go?
deitch: I am fine with it. 

Patch:
diff --git a/filesystem/squashfs/file.go b/filesystem/squashfs/file.go
index d632175..d6a3e80 100644
--- a/filesystem/squashfs/file.go
+++ b/filesystem/squashfs/file.go
@@ -20,6 +20,7 @@ type File struct {
 	offset        int64
 	filesystem    *FileSystem
 	blockLocation int64  // the position of the last block decompressed
+	blockSize     uint32 // the size of the last block decompressed, needed for sparse files
 	block         []byte // the actual last block decompressed
 }
 
@@ -104,7 +105,8 @@ func (fl *File) Read(b []byte) (int, error) {
 				return read, fmt.Errorf("unexpected block.size=%d > fs.blocksize=%d", block.size, fs.blocksize)
 			}
 			var input []byte
-			if fl.blockLocation == location && fl.block != nil {
+			// check location and size, location can be the same for sparse files but with a size of 0
+			if fl.blockLocation == location && fl.blockSize == block.size && fl.block != nil {
 				// Read last block from cache
 				input = fl.block
 			} else {
@@ -116,6 +118,7 @@ func (fl *File) Read(b []byte) (int, error) {
 				// Cache the last block
 				fl.blockLocation = location
 				fl.block = input
+				fl.blockSize = block.size
 			}
 			outputBlock(input)
 		}


Test patch:
diff --git a/filesystem/squashfs/const_internal_test.go b/filesystem/squashfs/const_internal_test.go
index 51d666e..73d26a9 100644
--- a/filesystem/squashfs/const_internal_test.go
+++ b/filesystem/squashfs/const_internal_test.go
@@ -288,3 +288,33 @@ func GetTestFileBig(f fs.File, c Compressor) (*File, error) {
 		filesystem:   testFs,
 	}, nil
 }
+
+// GetTestSparseFile get a *squashfs.File to a usable and known test file
+func GetTestSparseFile(f fs.File, c Compressor) (*File, error) {
+	testFs, _, err := testGetFilesystem(f)
+	if err != nil {
+		return nil, err
+	}
+	testFs.compressor = c
+	ef := &extendedFile{
+		blocksStart:        superblockSize,
+		fileSize:           uint64(7 + testFs.blocksize),
+		sparse:             uint64(testFs.blocksize),
+		links:              0,
+		fragmentBlockIndex: 0,
+		fragmentOffset:     0,
+		xAttrIndex:         0,
+		blockSizes: []*blockData{
+			{size: 0, compressed: false}, // sparse block
+			{size: 7, compressed: false},
+		},
+	}
+	// inode 0, offset 0, name "README.md", type basic file
+	return &File{
+		extendedFile: ef,
+		isReadWrite:  false,
+		isAppend:     false,
+		offset:       0,
+		filesystem:   testFs,
+	}, nil
+}
diff --git a/filesystem/squashfs/file_test.go b/filesystem/squashfs/file_test.go
index 8c04248..b7c08dc 100644
--- a/filesystem/squashfs/file_test.go
+++ b/filesystem/squashfs/file_test.go
@@ -69,7 +69,7 @@ func TestFileRead(t *testing.T) {
 		// stub the file reader
 		f, err := squashfs.GetTestFileBig(fileImpl, nil)
 		if err != nil {
-			t.Fatalf("unable to get small test file: %v", err)
+			t.Fatalf("unable to get big test file: %v", err)
 		}
 
 		filesize := blocksize + 5
@@ -86,6 +86,27 @@ func TestFileRead(t *testing.T) {
 			t.Errorf("Mismatched content:\nActual: '%s...'\nExpected: '%s...'", bString[:20], contentLong[:20])
 		}
 	})
+	t.Run("sparse blocks", func(t *testing.T) {
+		// stub the file reader
+		f, err := squashfs.GetTestSparseFile(fileImpl, nil)
+		if err != nil {
+			t.Fatalf("unable to get sparse test file: %v", err)
+		}
+		contentSparse := append(make([]byte, blocksize), contentLong[:7]...)
+
+		filesize := blocksize + 7
+		b := make([]byte, filesize)
+		read, err := f.Read(b)
+		if err != nil && err != io.EOF {
+			t.Errorf("received unexpected error when reading: %v", err)
+		}
+		if read != len(contentSparse) {
+			t.Errorf("read %d bytes instead of expected %d", read, len(contentSparse))
+		}
+		if !bytes.Equal(b[:read], contentSparse) {
+			t.Errorf("Mismatched content:\nActual: '%s'\nExpected: '%s'", b[:read], contentSparse[:read])
+		}
+	})
 }
 
 func TestFileWrite(t *testing.T) {