Skip to content

feat(rar): add support for multi-volume rar archives #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 6, 2025
Merged
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
44 changes: 43 additions & 1 deletion rar.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func init() {
RegisterFormat(Rar{})
}

type rarReader interface {
Next() (*rardecode.FileHeader, error)
io.Reader
io.WriterTo
}

type Rar struct {
// If true, errors encountered during reading or writing
// a file within an archive will be logged and the
Expand All @@ -28,6 +34,23 @@ type Rar struct {

// Password to open archives.
Password string

// Name for a multi-volume archive. When Name is specified,
// the named file is extracted (rather than any io.Reader that
// may be passed to Extract). If the archive is a multi-volume
// archive, this name will also be used by the decoder to derive
// the filename of the next volume in the volume set.
Name string

// FS is an fs.FS exposing the files of the archive. Unless Name is
// also specified, this does nothing. When Name is also specified,
// FS defines the fs.FS that from which the archive will be opened,
// and in the case of a multi-volume archive, from where each subsequent
// volume of the volume set will be loaded.
//
// Typically this should be a DirFS pointing at the directory containing
// the volumes of the archive.
FS fs.FS
}

func (Rar) Extension() string { return ".rar" }
Expand Down Expand Up @@ -65,7 +88,26 @@ func (r Rar) Extract(ctx context.Context, sourceArchive io.Reader, handleFile Fi
options = append(options, rardecode.Password(r.Password))
}

rr, err := rardecode.NewReader(sourceArchive, options...)
if r.FS != nil {
options = append(options, rardecode.FileSystem(r.FS))
}

var (
rr rarReader
err error
)

// If a name has been provided, then the sourceArchive stream is ignored
// and the archive is opened directly via the filesystem (or provided FS).
if r.Name != "" {
var or *rardecode.ReadCloser
if or, err = rardecode.OpenReader(r.Name, options...); err == nil {
rr = or
defer or.Close()
}
} else {
rr, err = rardecode.NewReader(sourceArchive, options...)
}
if err != nil {
return err
}
Expand Down
40 changes: 40 additions & 0 deletions rar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package archives

import (
"context"
"crypto/sha1"
"encoding/hex"
"io"
"testing"
)

func TestRarExtractMultiVolume(t *testing.T) {
// Test files testdata/test.part*.rar were created by:
// seq 0 2000 > test.txt
// rar a -v1k test.rar test.txt
rar := Rar{
Name: "test.part01.rar",
FS: DirFS("testdata"),
}

const expectedSHA1Sum = "4da7f88f69b44a3fdb705667019a65f4c6e058a3"
if err := rar.Extract(context.Background(), nil, func(_ context.Context, info FileInfo) error {
f, err := info.Open()
if err != nil {
return err
}
defer f.Close()

h := sha1.New()
if _, err = io.Copy(h, f); err != nil {
return err
}

if got := hex.EncodeToString(h.Sum(nil)); got != expectedSHA1Sum {
t.Errorf("expected %s, got %s", expectedSHA1Sum, got)
}
return nil
}); err != nil {
t.Error(err)
}
}
Binary file added testdata/test.part01.rar
Binary file not shown.
Binary file added testdata/test.part02.rar
Binary file not shown.
Loading