Repository: diskfs/go-diskfs
Pull request #298 (2025-05-29 17:19:52)

Add LZO decompressor
This adds support for LZO decompression (not compression) capabilities for squashfs filesystems. The underlying library is MIT licensed and supports LZO1X only (which is fairly common).

deitch: Hey @wagoodman happy to have this, thank you. And stuff from Anchore folks always is welcome. I contributed some stuff to Syft a year or two back on behalf of a client who needed it, had a great time working with you smart folks.
deitch: Just wait for CI to be green and it can merge in.

Patch:
diff --git a/.gitignore b/.gitignore
index 281c785..d5cc403 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,9 @@
 *.so
 *.dylib
 
+# IDE specific files
+.idea/
+
 # Test binary, built with `go test -c`
 *.test
 
diff --git a/filesystem/squashfs/compressor.go b/filesystem/squashfs/compressor.go
index 1c95ff1..2b0778a 100644
--- a/filesystem/squashfs/compressor.go
+++ b/filesystem/squashfs/compressor.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 
+	"github.com/anchore/go-lzo"
 	"github.com/klauspost/compress/zstd"
 	lz4 "github.com/pierrec/lz4/v4"
 	"github.com/ulikunitz/xz"
@@ -359,6 +360,34 @@ func (c *CompressorZstd) decompress(in []byte) ([]byte, error) {
 	return p, nil
 }
 
+type CompressorLzo struct {
+}
+
+func (c *CompressorLzo) compress(_ []byte) ([]byte, error) {
+	return nil, fmt.Errorf("LZO compression not yet supported")
+}
+func (c *CompressorLzo) decompress(in []byte) ([]byte, error) {
+	b := bytes.NewReader(in)
+	lz := lzo.NewReader(b)
+	p, err := io.ReadAll(lz)
+	if err != nil {
+		return nil, fmt.Errorf("error decompressing: %v", err)
+	}
+	return p, nil
+}
+
+//nolint:unused,revive // it is important to implement the interface
+func (c *CompressorLzo) loadOptions(b []byte) error {
+	// lzo has no supported options
+	return nil
+}
+func (c *CompressorLzo) optionsBytes() []byte {
+	return []byte{}
+}
+func (c *CompressorLzo) flavour() compression {
+	return compressionLzo
+}
+
 func newCompressor(flavour compression) (Compressor, error) {
 	var c Compressor
 	switch flavour {
@@ -369,7 +398,7 @@ func newCompressor(flavour compression) (Compressor, error) {
 	case compressionLzma:
 		c = &CompressorLzma{}
 	case compressionLzo:
-		return nil, fmt.Errorf("LZO compression not yet supported")
+		c = &CompressorLzo{}
 	case compressionXz:
 		c = &CompressorXz{}
 	case compressionLz4:
diff --git a/go.mod b/go.mod
index 1d17436..6163d51 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,9 @@
 module github.com/diskfs/go-diskfs
 
-go 1.22
+go 1.24.0
 
 require (
+	github.com/anchore/go-lzo v0.1.0
 	github.com/djherbis/times v1.6.0
 	github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab
 	github.com/go-test/deep v1.0.8
diff --git a/go.sum b/go.sum
index bc7a3fc..8fc31c2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
+github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=


Test patch:
diff --git a/filesystem/squashfs/compressor_internal_test.go b/filesystem/squashfs/compressor_internal_test.go
index 136e4e3..d0e567e 100644
--- a/filesystem/squashfs/compressor_internal_test.go
+++ b/filesystem/squashfs/compressor_internal_test.go
@@ -16,7 +16,7 @@ func TestNewCompressor(t *testing.T) {
 	}{
 		{compressionGzip, &CompressorGzip{}, nil},
 		{compressionLzma, &CompressorLzma{}, nil},
-		{compressionLzo, nil, fmt.Errorf("LZO compression not yet supported")},
+		{compressionLzo, &CompressorLzo{}, nil},
 		{compressionXz, &CompressorXz{}, nil},
 		{compressionLz4, &CompressorLz4{}, nil},
 		{compressionZstd, &CompressorZstd{}, nil},
@@ -55,21 +55,52 @@ var testCompressUncompressed = []byte{
 
 //nolint:thelper // this is not a helper function
 func testCompressAndDecompress(t *testing.T, c Compressor, compressed []byte) {
+	testDecompress(t, c, compressed, "")
+	testCompress(t, c, "")
+}
+
+//nolint:thelper // this is not a helper function
+func testDecompress(t *testing.T, c Compressor, compressed []byte, errorStr string) {
 	t.Run("decompress", func(t *testing.T) {
 		out, err := c.decompress(compressed)
 		switch {
+		case errorStr != "" && err == nil:
+			t.Errorf("expected error %q, got nil", errorStr)
 		case err != nil:
-			t.Errorf("unexpected error: %v", err)
+			if errorStr == "" {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if !strings.Contains(err.Error(), errorStr) {
+				t.Errorf("expected error %q, got %v", errorStr, err)
+			}
+			if out != nil {
+				t.Errorf("compression should not have succeeded, got % x", out)
+			}
 		case !bytes.Equal(out, testCompressUncompressed):
 			t.Errorf("Mismatched decompressed, actual then expected")
 			t.Logf("% x", out)
 			t.Logf("% x", testCompressUncompressed)
 		}
 	})
+}
+
+//nolint:thelper // this is not a helper function
+func testCompress(t *testing.T, c Compressor, errorStr string) {
 	t.Run("compress", func(t *testing.T) {
 		out, err := c.compress(testCompressUncompressed)
-		if err != nil {
-			t.Errorf("unexpected error: %v", err)
+		switch {
+		case errorStr != "" && err == nil:
+			t.Errorf("expected error %q, got nil", errorStr)
+		case err != nil:
+			if errorStr == "" {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if !strings.Contains(err.Error(), errorStr) {
+				t.Errorf("expected error %q, got %v", errorStr, err)
+			}
+			if out != nil {
+				t.Errorf("compression should not have succeeded, got % x", out)
+			}
 			return
 		}
 		decompressed, err := c.decompress(out)
@@ -169,3 +200,20 @@ func TestCompressionZstd(t *testing.T) {
 	c := CompressorZstd{}
 	testCompressAndDecompress(t, &c, compressed)
 }
+
+func TestCompressionLzo(t *testing.T) {
+	compressed := []byte{
+		0x75, 0xde, 0xc9, 0x4e, 0xd0, 0xef, 0x19, 0xdb, 0x0a, 0x6a, 0x35, 0x26,
+		0x61, 0x86, 0x2d, 0xa0, 0x42, 0x18, 0xa8, 0x89, 0xe9, 0xc4, 0x7b, 0x1a,
+		0xc7, 0x85, 0x8e, 0xd6, 0x36, 0xd6, 0x83, 0x84, 0x21, 0xf4, 0x06, 0x38,
+		0x07, 0x7b, 0x33, 0x3f, 0x72, 0x4c, 0xae, 0xcd, 0xfd, 0xa0, 0xb0, 0x71,
+		0x2f, 0x64, 0x62, 0x62, 0x2f, 0xc3, 0x5f, 0xa1, 0x21, 0xc6, 0xbf, 0x2c,
+		0x39, 0xef, 0x56, 0x23, 0x61, 0xb0, 0x98, 0x84, 0xcd, 0x24, 0xc4, 0xbf,
+		0x30, 0xae, 0xd9, 0x9e, 0xb0, 0x7b, 0xc5, 0xa3, 0x8d, 0xf7, 0x4f, 0xb8,
+		0xdd, 0x7b, 0x77, 0xb6, 0x8c, 0x5a, 0x10, 0xa4, 0xce, 0xc2, 0x0a, 0x3d,
+		0x51, 0x23, 0x8c, 0x10, 0x1a, 0x11, 0x00, 0x00,
+	}
+	c := CompressorLzo{}
+	testDecompress(t, &c, compressed, "")
+	testCompress(t, &c, "LZO compression not yet supported")
+}