Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/bodgit/sevenzip

go 1.25.0
go 1.25.5

require (
github.com/andybalholm/brotli v1.2.2
Expand All @@ -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.22.0
golang.org/x/text v0.40.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.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=
Expand Down
13 changes: 10 additions & 3 deletions internal/lzma/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"fmt"
"io"

"github.com/ulikunitz/xz/lzma"
"github.com/unxed/xz/lzma"
)

type readCloser struct {
Expand All @@ -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
}

Expand Down
55 changes: 52 additions & 3 deletions internal/lzma2/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@
package lzma2

import (
xz "github.com/unxed/xz"

Check failure on line 5 in internal/lzma2/reader.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

File is not properly formatted (gci)
"errors"
"fmt"

Check failure on line 7 in internal/lzma2/reader.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

File is not properly formatted (gofmt)
"io"

"github.com/ulikunitz/xz/lzma"
"github.com/unxed/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

Check failure on line 28 in internal/lzma2/reader.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

return with no blank line before (nlreturn)
}
type readCloser struct {
c io.Closer
r io.Reader
Expand All @@ -26,12 +44,19 @@
return errAlreadyClosed
}

if err := rc.c.Close(); err != nil {
return fmt.Errorf("lzma2: error closing: %w", err)
var errs []error
// Закрываем ридер из библиотеки xz, чтобы вернуть буферы в пул и остановить горутины
Comment thread
unxed marked this conversation as resolved.
Outdated
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
}

Expand All @@ -49,7 +74,7 @@
}

// NewReader returns a new LZMA2 io.ReadCloser.
func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {

Check failure on line 77 in internal/lzma2/reader.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

calculated cyclomatic complexity for function NewReader is 11, max is 10 (cyclop)
if len(readers) != 1 {
return nil, errNeedOneReader
}
Expand All @@ -70,6 +95,30 @@
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 {

Check failure on line 99 in internal/lzma2/reader.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

`if ok` has complex nested blocks (complexity: 9) (nestif)
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)
Expand Down
34 changes: 34 additions & 0 deletions internal/lzma2/reader_test.go
Comment thread
unxed marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,38 @@
if errors.Is(err, errInvalidProperties) {
t.Errorf("unexpected errInvalidProperties for valid property, got %v", err)
}
}

Check failure on line 44 in internal/lzma2/reader_test.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

File is not properly formatted (gci)
type mockSeekReaderAt struct {
io.Reader
}

func (m mockSeekReaderAt) ReadAt(p []byte, off int64) (n int, err error) { return 0, io.EOF }

Check failure on line 49 in internal/lzma2/reader_test.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

unused-parameter: parameter 'p' seems to be unused, consider removing or renaming it as _ (revive)
func (m mockSeekReaderAt) Seek(offset int64, whence int) (int64, error) {

Check failure on line 50 in internal/lzma2/reader_test.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

unused-parameter: parameter 'offset' seems to be unused, consider removing or renaming it as _ (revive)
if whence == io.SeekEnd {
return 100, nil // Имитируем наличие конца для триггера ParallelReader
}
return 0, nil

Check failure on line 54 in internal/lzma2/reader_test.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

return with no blank line before (nlreturn)
}

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()

Check failure on line 65 in internal/lzma2/reader_test.go

View workflow job for this annotation

GitHub Actions / Golang checks / Run lint checks

Error return value of `rc1.Close` is not checked (errcheck)

// 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)
}
}