From 3f59e779ae550fa30e5f9d8ef9a004fa1cc94346 Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Mon, 22 Jun 2026 23:06:57 +0200 Subject: [PATCH 1/6] Integrate the optimized unxed/xz fork with parallel decompression support and improved resource management. This change replaces the standard LZMA library with a performance-optimized fork to enable multi-threaded block decompression when the input stream supports random access via Seek and ReadAt interfaces. The Close methods in the lzma and lzma2 packages have been updated to explicitly close the internal decoder using errors.Join, ensuring that buffers are properly returned to the pool and background goroutines are terminated to prevent memory leaks and test timeouts. Furthermore, the LZMA2 constructor now includes logic to detect seekable streams and safely wrap them in section readers for parallel processing, accompanied by new unit tests to verify the interface detection and cleanup logic. Fix #470 --- go.mod | 4 ++- go.sum | 4 +-- internal/lzma/reader.go | 11 ++++++-- internal/lzma2/reader.go | 53 +++++++++++++++++++++++++++++++++-- internal/lzma2/reader_test.go | 34 ++++++++++++++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index ed326bc..75e2f31 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bodgit/sevenzip -go 1.25.0 +go 1.25.5 require ( github.com/andybalholm/brotli v1.2.1 @@ -24,3 +24,5 @@ require ( github.com/stretchr/objx v0.5.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/ulikunitz/xz => github.com/unxed/xz v0.1.5 diff --git a/go.sum b/go.sum index d930104..210142d 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/unxed/xz v0.1.5 h1:Q5fF867WESvZTZyrrvEx22eKc0k4bZ/RoQQOkRzPBDM= +github.com/unxed/xz v0.1.5/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= diff --git a/internal/lzma/reader.go b/internal/lzma/reader.go index b8d277c..8a46c96 100644 --- a/internal/lzma/reader.go +++ b/internal/lzma/reader.go @@ -26,12 +26,19 @@ func (rc *readCloser) Close() error { return errAlreadyClosed } - if err := rc.c.Close(); err != nil { - return fmt.Errorf("lzma: error closing: %w", err) + var errs []error + // Important: close the lzma.Reader to release buffers to the pool + if closer, ok := rc.r.(io.Closer); ok { + errs = append(errs, closer.Close()) } + errs = append(errs, rc.c.Close()) rc.c, rc.r = nil, nil + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("lzma: error closing: %w", err) + } + return nil } diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 59cca7b..50b1010 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -2,6 +2,7 @@ package lzma2 import ( + xz "github.com/ulikunitz/xz" "errors" "fmt" "io" @@ -9,6 +10,23 @@ import ( "github.com/ulikunitz/xz/lzma" ) +type seekReaderAt interface { + io.ReaderAt + io.Seeker +} + +func streamSizeBySeeking(s io.Seeker) (int64, error) { + curr, err := s.Seek(0, io.SeekCurrent) + if err != nil { + return 0, err + } + size, err := s.Seek(0, io.SeekEnd) + if err != nil { + return 0, err + } + _, err = s.Seek(curr, io.SeekStart) + return size, err +} type readCloser struct { c io.Closer r io.Reader @@ -26,12 +44,19 @@ func (rc *readCloser) Close() error { return errAlreadyClosed } - if err := rc.c.Close(); err != nil { - return fmt.Errorf("lzma2: error closing: %w", err) + var errs []error + // Закрываем ридер из библиотеки xz, чтобы вернуть буферы в пул и остановить горутины + if closer, ok := rc.r.(io.Closer); ok { + errs = append(errs, closer.Close()) } + errs = append(errs, rc.c.Close()) rc.c, rc.r = nil, nil + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("lzma2: error closing: %w", err) + } + return nil } @@ -70,6 +95,30 @@ func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, erro return nil, fmt.Errorf("lzma2: error verifying config: %w", err) } + // Try parallel decompression if the input is seekable + if sra, ok := readers[0].(seekReaderAt); ok { + currentOffset, err := sra.Seek(0, io.SeekCurrent) + if err == nil { + size, err := streamSizeBySeeking(sra) + if err == nil { + var rAt io.ReaderAt = sra + streamSize := size + if currentOffset > 0 { + rAt = io.NewSectionReader(sra, currentOffset, size-currentOffset) + streamSize = size - currentOffset + } + // Use the parallel reader from github.com/unxed/xz + pconfig := xz.ReaderConfig{DictCap: config.DictCap} + if pr, err := pconfig.NewParallelReader(rAt, streamSize); err == nil { + return &readCloser{ + c: readers[0], + r: pr, + }, nil + } + } + } + } + lr, err := config.NewReader2(readers[0]) if err != nil { return nil, fmt.Errorf("lzma2: error creating reader: %w", err) diff --git a/internal/lzma2/reader_test.go b/internal/lzma2/reader_test.go index b6a743e..0edf35d 100644 --- a/internal/lzma2/reader_test.go +++ b/internal/lzma2/reader_test.go @@ -42,3 +42,37 @@ func TestNewReader(t *testing.T) { t.Errorf("unexpected errInvalidProperties for valid property, got %v", err) } } +type mockSeekReaderAt struct { + io.Reader +} + +func (m mockSeekReaderAt) ReadAt(p []byte, off int64) (n int, err error) { return 0, io.EOF } +func (m mockSeekReaderAt) Seek(offset int64, whence int) (int64, error) { + if whence == io.SeekEnd { + return 100, nil // Имитируем наличие конца для триггера ParallelReader + } + return 0, nil +} + +func TestNewReader_InterfaceLogic(t *testing.T) { + // 1. Тест с обычным Reader (должен выбрать NewReader2) + p := []byte{0} // Свойства + r1 := dummyReadCloser{bytes.NewReader([]byte{0, 0, 0, 0, 0})} + rc1, err := NewReader(p, 0, []io.ReadCloser{r1}) + if err != nil { + t.Fatalf("Failed to create basic reader: %v", err) + } + rc1.Close() + + // 2. Тест с Seeker (должен попытаться запустить ParallelReader и откатиться) + r2 := dummyReadCloser{mockSeekReaderAt{bytes.NewReader([]byte{0, 0, 0, 0, 0})}} + rc2, err := NewReader(p, 0, []io.ReadCloser{r2}) + if err != nil { + t.Fatalf("Failed to create seekable reader: %v", err) + } + + // Проверяем, что после всех попыток и откатов Close() не паникует + if err := rc2.Close(); err != nil { + t.Errorf("Close failed: %v", err) + } +} From 8b967f0d6c8c61227faf7466fc2144a561676b8e Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Mon, 22 Jun 2026 23:50:04 +0200 Subject: [PATCH 2/6] Use own module name for unxed/xz --- go.mod | 4 +--- go.sum | 4 ++-- internal/lzma/reader.go | 2 +- internal/lzma2/reader.go | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 75e2f31..bbbc13a 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/stangelandcl/ppmd v0.1.1 github.com/stretchr/testify v1.11.1 - github.com/ulikunitz/xz v0.5.15 + github.com/unxed/xz v0.1.8 go4.org v0.0.0-20260112195520-a5071408f32f golang.org/x/sync v0.20.0 golang.org/x/text v0.37.0 @@ -24,5 +24,3 @@ require ( github.com/stretchr/objx v0.5.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/ulikunitz/xz => github.com/unxed/xz v0.1.5 diff --git a/go.sum b/go.sum index 210142d..d87bbc3 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/unxed/xz v0.1.5 h1:Q5fF867WESvZTZyrrvEx22eKc0k4bZ/RoQQOkRzPBDM= -github.com/unxed/xz v0.1.5/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= +github.com/unxed/xz v0.1.8 h1:0ETuOJkpMtk1dX9/C+1a+HGDgdzAHa/PiJqhyyecKcw= +github.com/unxed/xz v0.1.8/go.mod h1:+i9oua9YYxBJGNLh9s4itPLtCpCXs8X7M8/SNU4FeZc= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= diff --git a/internal/lzma/reader.go b/internal/lzma/reader.go index 8a46c96..02d6138 100644 --- a/internal/lzma/reader.go +++ b/internal/lzma/reader.go @@ -8,7 +8,7 @@ import ( "fmt" "io" - "github.com/ulikunitz/xz/lzma" + "github.com/unxed/xz/lzma" ) type readCloser struct { diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 50b1010..0775d4a 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -2,12 +2,12 @@ package lzma2 import ( - xz "github.com/ulikunitz/xz" + xz "github.com/unxed/xz" "errors" "fmt" "io" - "github.com/ulikunitz/xz/lzma" + "github.com/unxed/xz/lzma" ) type seekReaderAt interface { From 6c0a09fc5e696bdfaa600513930defea35d1e9c7 Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Tue, 28 Jul 2026 10:12:00 +0200 Subject: [PATCH 3/6] Comments ru -> en --- internal/lzma2/reader.go | 2 +- internal/lzma2/reader_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 0775d4a..1885142 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -45,7 +45,7 @@ func (rc *readCloser) Close() error { } var errs []error - // Закрываем ридер из библиотеки xz, чтобы вернуть буферы в пул и остановить горутины + // We close the reader from the xz library to return the buffers to the pool and stop the goroutines if closer, ok := rc.r.(io.Closer); ok { errs = append(errs, closer.Close()) } diff --git a/internal/lzma2/reader_test.go b/internal/lzma2/reader_test.go index 0edf35d..baa15b2 100644 --- a/internal/lzma2/reader_test.go +++ b/internal/lzma2/reader_test.go @@ -49,14 +49,14 @@ type mockSeekReaderAt struct { func (m mockSeekReaderAt) ReadAt(p []byte, off int64) (n int, err error) { return 0, io.EOF } func (m mockSeekReaderAt) Seek(offset int64, whence int) (int64, error) { if whence == io.SeekEnd { - return 100, nil // Имитируем наличие конца для триггера ParallelReader + return 100, nil // Simulate the presence of an end for the ParallelReader trigger } return 0, nil } func TestNewReader_InterfaceLogic(t *testing.T) { - // 1. Тест с обычным Reader (должен выбрать NewReader2) - p := []byte{0} // Свойства + // 1. Test with the standard Reader (should select NewReader2) + p := []byte{0} // Properties r1 := dummyReadCloser{bytes.NewReader([]byte{0, 0, 0, 0, 0})} rc1, err := NewReader(p, 0, []io.ReadCloser{r1}) if err != nil { @@ -64,14 +64,14 @@ func TestNewReader_InterfaceLogic(t *testing.T) { } rc1.Close() - // 2. Тест с Seeker (должен попытаться запустить ParallelReader и откатиться) + // 2. Test with Seeker (should attempt to launch ParallelReader and roll back) r2 := dummyReadCloser{mockSeekReaderAt{bytes.NewReader([]byte{0, 0, 0, 0, 0})}} rc2, err := NewReader(p, 0, []io.ReadCloser{r2}) if err != nil { t.Fatalf("Failed to create seekable reader: %v", err) } - // Проверяем, что после всех попыток и откатов Close() не паникует + // We verify that Close() does not panic after all attempts and rollbacks. if err := rc2.Close(); err != nil { t.Errorf("Close failed: %v", err) } From e6f8e70e34d74c240ababeb65c808b362d6d74a4 Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Tue, 28 Jul 2026 12:01:13 +0200 Subject: [PATCH 4/6] Formatting fixes --- internal/lzma/reader.go | 1 + internal/lzma2/reader.go | 3 ++- internal/lzma2/reader_test.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/lzma/reader.go b/internal/lzma/reader.go index 02d6138..548d9a6 100644 --- a/internal/lzma/reader.go +++ b/internal/lzma/reader.go @@ -31,6 +31,7 @@ func (rc *readCloser) Close() error { if closer, ok := rc.r.(io.Closer); ok { errs = append(errs, closer.Close()) } + errs = append(errs, rc.c.Close()) rc.c, rc.r = nil, nil diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 1885142..5c18a4c 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -2,9 +2,9 @@ package lzma2 import ( - xz "github.com/unxed/xz" "errors" "fmt" + xz "github.com/unxed/xz" "io" "github.com/unxed/xz/lzma" @@ -27,6 +27,7 @@ func streamSizeBySeeking(s io.Seeker) (int64, error) { _, err = s.Seek(curr, io.SeekStart) return size, err } + type readCloser struct { c io.Closer r io.Reader diff --git a/internal/lzma2/reader_test.go b/internal/lzma2/reader_test.go index baa15b2..fdcbf30 100644 --- a/internal/lzma2/reader_test.go +++ b/internal/lzma2/reader_test.go @@ -42,6 +42,7 @@ func TestNewReader(t *testing.T) { t.Errorf("unexpected errInvalidProperties for valid property, got %v", err) } } + type mockSeekReaderAt struct { io.Reader } From 3a5d768647717324024b70a1e97dcd3316a3ca31 Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Tue, 28 Jul 2026 12:03:14 +0200 Subject: [PATCH 5/6] Fixing linter errors --- internal/lzma2/reader.go | 64 ++++++++++++++++-------- internal/lzma2/reader_test.go | 94 +++++++++++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 25 deletions(-) diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 5c18a4c..7e8f372 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -46,10 +46,12 @@ func (rc *readCloser) Close() error { } var errs []error + // We close the reader from the xz library to return the buffers to the pool and stop the goroutines if closer, ok := rc.r.(io.Closer); ok { errs = append(errs, closer.Close()) } + errs = append(errs, rc.c.Close()) rc.c, rc.r = nil, nil @@ -74,6 +76,45 @@ func (rc *readCloser) Read(p []byte) (int, error) { return n, err } +func tryParallelReader(config lzma.Reader2Config, readers []io.ReadCloser) (io.ReadCloser, bool) { + sra, ok := readers[0].(seekReaderAt) + if !ok { + return nil, false + } + + currentOffset, err := sra.Seek(0, io.SeekCurrent) + if err != nil { + return nil, false + } + + size, err := streamSizeBySeeking(sra) + if err != nil { + return nil, false + } + + var rAt io.ReaderAt = sra + + streamSize := size + + if currentOffset > 0 { + rAt = io.NewSectionReader(sra, currentOffset, size-currentOffset) + streamSize = size - currentOffset + } + + // Use the parallel reader from github.com/unxed/xz + pconfig := xz.ReaderConfig{DictCap: config.DictCap} + + pr, err := pconfig.NewParallelReader(rAt, streamSize) + if err != nil { + return nil, false + } + + return &readCloser{ + c: readers[0], + r: pr, + }, true +} + // NewReader returns a new LZMA2 io.ReadCloser. func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) { if len(readers) != 1 { @@ -97,27 +138,8 @@ func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, erro } // Try parallel decompression if the input is seekable - if sra, ok := readers[0].(seekReaderAt); ok { - currentOffset, err := sra.Seek(0, io.SeekCurrent) - if err == nil { - size, err := streamSizeBySeeking(sra) - if err == nil { - var rAt io.ReaderAt = sra - streamSize := size - if currentOffset > 0 { - rAt = io.NewSectionReader(sra, currentOffset, size-currentOffset) - streamSize = size - currentOffset - } - // Use the parallel reader from github.com/unxed/xz - pconfig := xz.ReaderConfig{DictCap: config.DictCap} - if pr, err := pconfig.NewParallelReader(rAt, streamSize); err == nil { - return &readCloser{ - c: readers[0], - r: pr, - }, nil - } - } - } + if pr, ok := tryParallelReader(config, readers); ok { + return pr, nil } lr, err := config.NewReader2(readers[0]) diff --git a/internal/lzma2/reader_test.go b/internal/lzma2/reader_test.go index fdcbf30..13f3ecb 100644 --- a/internal/lzma2/reader_test.go +++ b/internal/lzma2/reader_test.go @@ -45,13 +45,37 @@ func TestNewReader(t *testing.T) { type mockSeekReaderAt struct { io.Reader + seekStartFail bool + seekCurrentFail bool + seekEndFail bool + currentOffset int64 } -func (m mockSeekReaderAt) ReadAt(p []byte, off int64) (n int, err error) { return 0, io.EOF } -func (m mockSeekReaderAt) Seek(offset int64, whence int) (int64, error) { +func (m mockSeekReaderAt) ReadAt(_ []byte, _ int64) (int, error) { + return 0, io.EOF +} + +func (m mockSeekReaderAt) Seek(_ int64, whence int) (int64, error) { + if whence == io.SeekStart && m.seekStartFail { + return 0, errors.New("seek start failed") + } + + if whence == io.SeekCurrent { + if m.seekCurrentFail { + return 0, errors.New("seek current failed") + } + + return m.currentOffset, nil + } + if whence == io.SeekEnd { + if m.seekEndFail { + return 0, errors.New("seek end failed") + } + return 100, nil // Simulate the presence of an end for the ParallelReader trigger } + return 0, nil } @@ -59,14 +83,17 @@ func TestNewReader_InterfaceLogic(t *testing.T) { // 1. Test with the standard Reader (should select NewReader2) p := []byte{0} // Properties r1 := dummyReadCloser{bytes.NewReader([]byte{0, 0, 0, 0, 0})} + rc1, err := NewReader(p, 0, []io.ReadCloser{r1}) if err != nil { t.Fatalf("Failed to create basic reader: %v", err) } - rc1.Close() + + _ = rc1.Close() // 2. Test with Seeker (should attempt to launch ParallelReader and roll back) - r2 := dummyReadCloser{mockSeekReaderAt{bytes.NewReader([]byte{0, 0, 0, 0, 0})}} + r2 := dummyReadCloser{mockSeekReaderAt{Reader: bytes.NewReader([]byte{0, 0, 0, 0, 0})}} + rc2, err := NewReader(p, 0, []io.ReadCloser{r2}) if err != nil { t.Fatalf("Failed to create seekable reader: %v", err) @@ -77,3 +104,62 @@ func TestNewReader_InterfaceLogic(t *testing.T) { t.Errorf("Close failed: %v", err) } } + +func TestStreamSizeBySeeking_Errors(t *testing.T) { + s := mockSeekReaderAt{Reader: bytes.NewReader(nil), seekCurrentFail: true} + + _, err := streamSizeBySeeking(s) + if err == nil { + t.Error("expected error for seek current failure") + } + + s = mockSeekReaderAt{Reader: bytes.NewReader(nil), seekEndFail: true} + + _, err = streamSizeBySeeking(s) + if err == nil { + t.Error("expected error for seek end failure") + } + + s = mockSeekReaderAt{Reader: bytes.NewReader(nil), seekStartFail: true} + + _, err = streamSizeBySeeking(s) + if err == nil { + t.Error("expected error for seek start failure") + } +} + +func TestTryParallelReader_Offsets(t *testing.T) { + p := []byte{0} + r2 := dummyReadCloser{mockSeekReaderAt{Reader: bytes.NewReader([]byte{0, 0, 0, 0, 0}), currentOffset: 10}} + + rc2, err := NewReader(p, 0, []io.ReadCloser{r2}) + if err != nil { + t.Fatalf("Failed to create seekable reader: %v", err) + } + + _ = rc2.Close() +} + +func TestTryParallelReader_Errors(t *testing.T) { + p := []byte{0} + + // Test failure at SeekCurrent (should fallback to NewReader2 and succeed) + r1 := dummyReadCloser{mockSeekReaderAt{Reader: bytes.NewReader([]byte{0, 0, 0, 0, 0}), seekCurrentFail: true}} + + rc1, err := NewReader(p, 0, []io.ReadCloser{r1}) + if err != nil { + t.Fatalf("Failed to create reader despite SeekCurrent error: %v", err) + } + + _ = rc1.Close() + + // Test failure at streamSizeBySeeking (should fallback to NewReader2 and succeed) + r2 := dummyReadCloser{mockSeekReaderAt{Reader: bytes.NewReader([]byte{0, 0, 0, 0, 0}), seekEndFail: true}} + + rc2, err := NewReader(p, 0, []io.ReadCloser{r2}) + if err != nil { + t.Fatalf("Failed to create reader despite SeekEnd error: %v", err) + } + + _ = rc2.Close() +} From b5fb26109cd3bdb53ddd33fce057846a48df6e4a Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Tue, 28 Jul 2026 12:05:45 +0200 Subject: [PATCH 6/6] Fixing linter errors --- internal/lzma2/reader.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/lzma2/reader.go b/internal/lzma2/reader.go index 7e8f372..746f5b9 100644 --- a/internal/lzma2/reader.go +++ b/internal/lzma2/reader.go @@ -4,9 +4,9 @@ package lzma2 import ( "errors" "fmt" - xz "github.com/unxed/xz" "io" + "github.com/unxed/xz" "github.com/unxed/xz/lzma" ) @@ -18,14 +18,20 @@ type seekReaderAt interface { func streamSizeBySeeking(s io.Seeker) (int64, error) { curr, err := s.Seek(0, io.SeekCurrent) if err != nil { - return 0, err + return 0, fmt.Errorf("seek current: %w", err) } + size, err := s.Seek(0, io.SeekEnd) if err != nil { - return 0, err + return 0, fmt.Errorf("seek end: %w", err) } + _, err = s.Seek(curr, io.SeekStart) - return size, err + if err != nil { + return size, fmt.Errorf("seek start: %w", err) + } + + return size, nil } type readCloser struct {