aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/xz/reader_test.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-07-25 14:03:03 +0900
committerOphestra <cat@gensokyo.uk>2026-07-25 14:03:34 +0900
commite5abf51296fa1405c4a6314eafc1b97d247c5b60 (patch)
tree8e54f3a684dd0b875756b3a4eca13cd79ab695f1 /internal/xz/reader_test.go
parente26914cd6f86341daff4c3d7282d9a8cf8e4611a (diff)
internal/pkg: decompress xz streams
This uses a port of the public domain xz implementation. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/xz/reader_test.go')
-rw-r--r--internal/xz/reader_test.go710
1 files changed, 710 insertions, 0 deletions
diff --git a/internal/xz/reader_test.go b/internal/xz/reader_test.go
new file mode 100644
index 00000000..3e7ffa48
--- /dev/null
+++ b/internal/xz/reader_test.go
@@ -0,0 +1,710 @@
+/*
+ * Package xz tests
+ *
+ * Author: Michael Cross <https://github.com/xi2>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+package xz_test
+
+import (
+ "bytes"
+ "crypto/md5"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+ "testing/iotest"
+
+ "hakurei.app/internal/xz"
+)
+
+type testFile struct {
+ file string
+ md5sum string
+ err error
+}
+
+// Note that the md5sums below were generated with XZ Utils and
+// XZ Embedded, not with this package.
+
+var badFiles = []testFile{
+ {
+ file: "bad-0-backward_size.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-0cat-alone.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrFormat,
+ },
+ {
+ file: "bad-0cat-header_magic.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrFormat,
+ },
+ {
+ file: "bad-0catpad-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-0-empty-truncated.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrBuf,
+ },
+ {
+ file: "bad-0-footer_magic.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-0-header_magic.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrFormat,
+ },
+ {
+ file: "bad-0-nonempty_index.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-0pad-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-1.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-2.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-3.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-4.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-5.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-block_header-6.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-check-crc32.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-check-crc64.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-check-sha256.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-1.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-2.xz",
+ md5sum: "211dbb3d39f3c244585397f6d3c09be3",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-3.xz",
+ md5sum: "211dbb3d39f3c244585397f6d3c09be3",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-4.xz",
+ md5sum: "6492b8d167aee3ca222d07a49d24015a",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-5.xz",
+ md5sum: "6492b8d167aee3ca222d07a49d24015a",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-6.xz",
+ md5sum: "09f7e02f1290be211da707a266f153b3",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-7.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-lzma2-8.xz",
+ md5sum: "211dbb3d39f3c244585397f6d3c09be3",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-stream_flags-1.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-stream_flags-2.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-stream_flags-3.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-vli-1.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-1-vli-2.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-compressed_data_padding.xz",
+ md5sum: "09f7e02f1290be211da707a266f153b3",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-index-1.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-index-2.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-index-3.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-index-4.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+ {
+ file: "bad-2-index-5.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: xz.ErrData,
+ },
+}
+
+var goodFiles = []testFile{
+ {
+ file: "good-0cat-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: nil,
+ },
+ {
+ file: "good-0catpad-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: nil,
+ },
+ {
+ file: "good-0-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: nil,
+ },
+ {
+ file: "good-0pad-empty.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: nil,
+ },
+ {
+ file: "good-1-3delta-lzma2.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: nil,
+ },
+ {
+ file: "good-1-block_header-1.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-block_header-2.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-block_header-3.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-check-crc32.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-check-crc64.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-check-none.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-check-sha256.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+ {
+ file: "good-1-delta-lzma2.tiff.xz",
+ md5sum: "c692be6d1987989af5eeafc329085ad2",
+ err: nil,
+ },
+ {
+ file: "good-1-lzma2-1.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: nil,
+ },
+ {
+ file: "good-1-lzma2-2.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: nil,
+ },
+ {
+ file: "good-1-lzma2-3.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: nil,
+ },
+ {
+ file: "good-1-lzma2-4.xz",
+ md5sum: "c214a5e586cb3f0673cc6138f7de25ab",
+ err: nil,
+ },
+ {
+ file: "good-1-lzma2-5.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: nil,
+ },
+ {
+ file: "good-1-sparc-lzma2.xz",
+ md5sum: "835f2865f1d7c7ad2c7de0d5fd07faef",
+ err: nil,
+ },
+ {
+ file: "good-1-x86-lzma2.xz",
+ md5sum: "ce212d6a1cfe73d8395a2b42f94c2419",
+ err: nil,
+ },
+ {
+ file: "good-2-lzma2.xz",
+ md5sum: "fbf68a8e34b2ded53bba54e68794b4fe",
+ err: nil,
+ },
+}
+
+var unsupportedFiles = []testFile{
+ {
+ file: "unsupported-block_header.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrOptions,
+ },
+ {
+ file: "unsupported-check.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrUnsupportedCheck,
+ },
+ {
+ file: "unsupported-filter_flags-1.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrOptions,
+ },
+ {
+ file: "unsupported-filter_flags-2.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrOptions,
+ },
+ {
+ file: "unsupported-filter_flags-3.xz",
+ md5sum: "d41d8cd98f00b204e9800998ecf8427e",
+ err: xz.ErrOptions,
+ },
+}
+
+var otherFiles = []testFile{
+ {
+ file: "good-1-x86-lzma2-offset-2048.xz",
+ md5sum: "ce212d6a1cfe73d8395a2b42f94c2419",
+ err: nil,
+ },
+ {
+ file: "good-2-lzma2-corrupt.xz",
+ md5sum: "d9c5223e7e6e305e6c1c6ed73789df88",
+ err: xz.ErrData,
+ },
+ {
+ file: "words.xz",
+ md5sum: "00e28a90cb4a975fdaa3b375d3124a66",
+ err: nil,
+ },
+ {
+ file: "random-1mb.xz",
+ md5sum: "3f04b090e5d26a1cbeea53c21ebcad03",
+ err: nil,
+ },
+ {
+ file: "zeros-100mb.xz",
+ md5sum: "0f86d7c5a6180cf9584c1d21144d85b0",
+ err: nil,
+ },
+}
+
+func openTestFile(file string) (*os.File, error) {
+ f, err := os.Open(filepath.Join("testdata", "xz-utils", file))
+ if err == nil {
+ return f, nil
+ }
+ if err != nil && !os.IsNotExist(err) {
+ return nil, err
+ }
+ f, err = os.Open(filepath.Join("testdata", "other", file))
+ if err == nil {
+ return f, nil
+ }
+ return nil, err
+}
+
+func readTestFile(file string) ([]byte, error) {
+ b, err := ioutil.ReadFile(filepath.Join("testdata", "xz-utils", file))
+ if err == nil {
+ return b, nil
+ }
+ if err != nil && !os.IsNotExist(err) {
+ return nil, err
+ }
+ b, err = ioutil.ReadFile(filepath.Join("testdata", "other", file))
+ if err == nil {
+ return b, nil
+ }
+ return nil, err
+}
+
+// testFileData returns the data (md5sum and err) associated with the
+// named testFile
+func testFileData(file string) (md5sum string, err error) {
+ fileList := badFiles
+ fileList = append(fileList, goodFiles...)
+ fileList = append(fileList, unsupportedFiles...)
+ fileList = append(fileList, otherFiles...)
+ for _, f := range fileList {
+ if f.file == file {
+ return f.md5sum, f.err
+ }
+ }
+ return "", nil
+}
+
+// testFileList tests the decoding of a list of files against their
+// expected error and md5sum.
+func testFileList(t *testing.T, files []testFile, reuseReader bool) {
+ var r *xz.Reader
+ var err error
+ if reuseReader {
+ r, err = xz.NewReader(nil, 0)
+ }
+ for _, f := range files {
+ func() {
+ var fr *os.File
+ fr, err = openTestFile(f.file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fr.Close()
+ hash := md5.New()
+ switch reuseReader {
+ case true:
+ err = r.Reset(fr)
+ case false:
+ r, err = xz.NewReader(fr, 0)
+ }
+ if err == nil {
+ _, err = io.Copy(hash, r)
+ }
+ if err != f.err {
+ t.Fatalf("%s: wanted error: %v, got: %v\n", f.file, f.err, err)
+ }
+ md5sum := fmt.Sprintf("%x", hash.Sum(nil))
+ if f.md5sum != md5sum {
+ t.Fatalf(
+ "%s: wanted md5: %v, got: %v\n", f.file, f.md5sum, md5sum)
+ }
+ }()
+ }
+}
+
+// testFileListByteReads tests the decoding of a list of files against
+// their expected error and md5sum. It uses a one byte input buffer
+// and one byte output buffer for each run of the decoder.
+func testFileListByteReads(t *testing.T, files []testFile) {
+ for _, f := range files {
+ func() {
+ fr, err := openTestFile(f.file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fr.Close()
+ hash := md5.New()
+ obr := iotest.OneByteReader(fr)
+ r, err := xz.NewReader(obr, 0)
+ if err == nil {
+ b := make([]byte, 1)
+ var n int
+ for err == nil {
+ n, err = r.Read(b)
+ if n == 1 {
+ _, _ = hash.Write(b)
+ }
+ }
+ if err == io.EOF {
+ err = nil
+ }
+ }
+ if err != f.err {
+ t.Fatalf("%s: wanted error: %v, got: %v\n", f.file, f.err, err)
+ }
+ md5sum := fmt.Sprintf("%x", hash.Sum(nil))
+ if f.md5sum != md5sum {
+ t.Fatalf(
+ "%s: wanted md5: %v, got: %v\n", f.file, f.md5sum, md5sum)
+ }
+ }()
+ }
+}
+
+func TestBadFiles(t *testing.T) {
+ testFileList(t, badFiles, false)
+}
+
+func TestGoodFiles(t *testing.T) {
+ testFileList(t, goodFiles, false)
+}
+
+func TestUnsupportedFiles(t *testing.T) {
+ testFileList(t, unsupportedFiles, false)
+}
+
+func TestOtherFiles(t *testing.T) {
+ testFileList(t, otherFiles, false)
+}
+
+func TestMemlimit(t *testing.T) {
+ data, err := readTestFile("words.xz")
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err := xz.NewReader(bytes.NewReader(data), 1<<25)
+ if err == nil {
+ b := new(bytes.Buffer)
+ _, err = io.Copy(b, r)
+ }
+ if err != xz.ErrMemlimit {
+ t.Fatalf("wanted error: %v, got: %v\n", xz.ErrMemlimit, err)
+ }
+}
+
+// test to ensure that decoder errors are not returned prematurely
+// the test file returns 6 decoded bytes before corruption occurs
+func TestPrematureError(t *testing.T) {
+ data, err := readTestFile("good-2-lzma2-corrupt.xz")
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err := xz.NewReader(bytes.NewReader(data), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b := make([]byte, 2)
+ n, err := r.Read(b)
+ if n != 2 || err != nil {
+ t.Fatalf("Read returned: (%d,%v), expected: (2,%v)\n", n, err, nil)
+ }
+ n, err = r.Read(b)
+ if n != 2 || err != nil {
+ t.Fatalf("Read returned: (%d,%v), expected: (2,%v)\n", n, err, nil)
+ }
+ n, err = r.Read(b)
+ if n != 2 || err != xz.ErrData {
+ t.Fatalf("Read returned: (%d,%v), expected: (2,%v)\n",
+ n, err, xz.ErrData)
+ }
+}
+
+func TestMultipleBadReads(t *testing.T) {
+ data, err := readTestFile("good-2-lzma2-corrupt.xz")
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err := xz.NewReader(bytes.NewReader(data), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b := make([]byte, 100)
+ n, err := r.Read(b)
+ if n != 6 || err != xz.ErrData {
+ t.Fatalf("Read returned: (%d,%v), expected: (6,%v)\n",
+ n, err, xz.ErrData)
+ }
+ n, err = r.Read(b)
+ if n != 0 || err != xz.ErrData {
+ t.Fatalf("Read returned: (%d,%v), expected: (0,%v)\n",
+ n, err, xz.ErrData)
+ }
+ n, err = r.Read(b)
+ if n != 0 || err != xz.ErrData {
+ t.Fatalf("Read returned: (%d,%v), expected: (0,%v)\n",
+ n, err, xz.ErrData)
+ }
+}
+
+// TestByteReads decodes the test files with a one byte input buffer
+// and one byte output buffer. This should exercise the stream decoder
+// and filter code nicely by testing most of the xzOK exits paths.
+func TestByteReads(t *testing.T) {
+ fileList := badFiles
+ fileList = append(fileList, goodFiles...)
+ fileList = append(fileList, unsupportedFiles...)
+ fileList = append(fileList, otherFiles...)
+ fileListSmall := []testFile{}
+ for _, f := range fileList {
+ if f.file != "zeros-100mb.xz" {
+ fileListSmall = append(fileListSmall, f)
+ }
+ }
+ testFileListByteReads(t, fileListSmall)
+}
+
+func TestMultistream(t *testing.T) {
+ files := []string{
+ "good-1-x86-lzma2-offset-2048.xz",
+ "random-1mb.xz",
+ "words.xz",
+ "good-1-x86-lzma2-offset-2048.xz",
+ "random-1mb.xz",
+ "words.xz",
+ }
+ var readers []io.Reader
+ for _, f := range files {
+ data, err := readTestFile(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ readers = append(readers, bytes.NewReader(data))
+ }
+ mr := io.MultiReader(readers...)
+ r, err := xz.NewReader(mr, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for i, f := range files {
+ r.Multistream(false)
+ hash := md5.New()
+ _, err = io.Copy(hash, r)
+ if err != nil {
+ t.Fatalf("%s: wanted copy error: %v, got: %v\n", f, nil, err)
+ }
+ md5sum := fmt.Sprintf("%x", hash.Sum(nil))
+ wantedMD5, _ := testFileData(f)
+ if wantedMD5 != md5sum {
+ t.Fatalf(
+ "%s: wanted md5: %v, got: %v\n", f, wantedMD5, md5sum)
+ }
+ err = r.Reset(nil)
+ var wantedErr error
+ switch {
+ case i < len(files)-1:
+ wantedErr = nil
+ case i == len(files)-1:
+ wantedErr = io.EOF
+ }
+ if wantedErr != err {
+ t.Fatalf("%s: wanted reset error: %v, got: %v\n",
+ f, wantedErr, err)
+ }
+ }
+}
+
+// TestReuseReader decodes the test files reusing the same Reader for
+// all files instead of allocating a new Reader for each file.
+func TestReuseReader(t *testing.T) {
+ fileList := badFiles
+ fileList = append(fileList, goodFiles...)
+ fileList = append(fileList, unsupportedFiles...)
+ fileList = append(fileList, otherFiles...)
+ testFileList(t, fileList, true)
+}
+
+// TestReuseReaderPartialReads repeatedly tests decoding a file with a
+// reused Reader that has been used immediately before to partially
+// decode a file. The amount of partial decoding before the full
+// decode is varied on each loop iteration.
+func TestReuseReaderPartialReads(t *testing.T) {
+ data, err := readTestFile("words.xz")
+ if err != nil {
+ t.Fatal(err)
+ }
+ z, err := xz.NewReader(nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for i := 0; i <= 80000; i += 10000 {
+ err = z.Reset(bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ b := make([]byte, i)
+ _, err = io.ReadFull(z, b)
+ if err != nil {
+ t.Fatalf("io.ReadFull: wanted error: %v, got: %v\n", nil, err)
+ }
+ err = z.Reset(bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ hash := md5.New()
+ _, err = io.Copy(hash, z)
+ if err != nil {
+ t.Fatalf("io.Copy: wanted error: %v, got: %v\n", nil, err)
+ }
+ md5sum := fmt.Sprintf("%x", hash.Sum(nil))
+ wantedMD5, _ := testFileData("words.xz")
+ if wantedMD5 != md5sum {
+ t.Fatalf(
+ "hash.Sum: wanted md5: %v, got: %v\n", wantedMD5, md5sum)
+ }
+ }
+}