Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.1
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.20.0
golang.org/x/text v0.37.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"
"errors"
"fmt"
"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
}
type readCloser struct {
c io.Closer
r io.Reader
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions internal/lzma2/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}