rardecode
| Language | Go |
|---|---|
| License | BSD-2-Clause |
| Repository | github.com/nwaples/rardecode |
| Documentation | pkg.go.dev/github.com/nwaples/rardecode/v2 |
| Depends on | nothing (pure Go) |
| Read / write | yes / no (RAR 1.5-4 and RAR5, solid, multi-volume, encrypted) |
rardecode is a pure Go RAR decompressor. It reads RAR4 and RAR5 archives, solid archives, multi-volume sets and password-protected files, with no cgo and no external unrar. rardecode.OpenReader returns a reader that walks the entries in order like archive/tar: call Next() to get a header, then read the file’s data from the reader.
rardecode.OpenFS returns an fs.FS for random access, but files in a solid archive can’t be opened through it (except the first one): reading fails with “solid files don’t support Open”. Use the sequential reader for comic code, or mholt/archives, which handles solid archives.
Installation#
go get github.com/nwaples/rardecode/v2
Basic usage: list pages in natural order#
isPage and naturalCompare are the helpers from the archive/zip CBZ page: an image-extension filter that skips __MACOSX and dotfiles, and a natural, case-insensitive compare.
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
"slices"
"github.com/nwaples/rardecode/v2"
)
func listPages(path string) ([]string, error) {
r, err := rardecode.OpenReader(path)
if err != nil {
return nil, err
}
defer r.Close()
var pages []string
for {
h, err := r.Next() // the data of the previous file is skipped
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if !h.IsDir && isPage(h.Name) {
pages = append(pages, h.Name)
}
}
slices.SortFunc(pages, naturalCompare)
return pages, nil
}
func main() {
pages, err := listPages(os.Args[1])
if err != nil {
log.Fatal(err)
}
for i, p := range pages {
fmt.Printf("%3d %s\n", i+1, p)
}
}
How to read all pages into memory#
In a solid archive every file depends on the ones before it, so read the whole archive in one pass and sort afterwards:
func readPages(path string) (names []string, data map[string][]byte, err error) {
r, err := rardecode.OpenReader(path)
if err != nil {
return nil, nil, err
}
defer r.Close()
data = map[string][]byte{}
for {
h, err := r.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, nil, err
}
if h.IsDir || !isPage(h.Name) {
continue
}
b, err := io.ReadAll(r)
if err != nil {
return nil, nil, err
}
names = append(names, h.Name)
data[h.Name] = b
}
slices.SortFunc(names, naturalCompare)
return names, data, nil
}
h.UnPackedSize is the uncompressed size; check it before reading untrusted files, or set a limit with the rardecode.MaxDictionarySize option.
How to extract the cover#
The cover is the first page in natural order. Find its name first, then stop reading as soon as you reach it:
func extractCover(path string) error {
pages, err := listPages(path)
if err != nil || len(pages) == 0 {
return fmt.Errorf("no pages: %v", err)
}
r, err := rardecode.OpenReader(path)
if err != nil {
return err
}
defer r.Close()
for {
h, err := r.Next()
if err != nil {
return err // io.EOF here means the page wasn't found
}
if h.Name == pages[0] {
out, err := os.Create("cover" + strings.ToLower(filepath.Ext(h.Name)))
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
}
}
How to read ComicInfo.xml#
type ComicInfo struct {
Title, Series, Number, Writer, Manga string
PageCount int
Pages []struct {
Image int `xml:"Image,attr"` // index in sorted page order
Type string `xml:"Type,attr"`
} `xml:"Pages>Page"`
}
func readComicInfo(path string) (*ComicInfo, error) {
r, err := rardecode.OpenReader(path)
if err != nil {
return nil, err
}
defer r.Close()
for {
h, err := r.Next()
if errors.Is(err, io.EOF) {
return nil, nil // no metadata
}
if err != nil {
return nil, err
}
if strings.EqualFold(h.Name, "ComicInfo.xml") {
var ci ComicInfo
return &ci, xml.NewDecoder(r).Decode(&ci)
}
}
}
How to convert CBR to CBZ#
Stream every file from the RAR into a ZIP with stored entries:
func cbrToCbz(in, out string) error {
r, err := rardecode.OpenReader(in)
if err != nil {
return err
}
defer r.Close()
f, err := os.Create(out)
if err != nil {
return err
}
defer f.Close()
zw := zip.NewWriter(f)
for {
h, err := r.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
if h.IsDir {
continue
}
w, err := zw.CreateHeader(&zip.FileHeader{
Name: strings.ReplaceAll(h.Name, "\\", "/"), Method: zip.Store, Modified: h.ModificationTime,
})
if err != nil {
return err
}
if _, err := io.Copy(w, r); err != nil {
return err
}
}
return zw.Close()
}
Tips#
- Password-protected archives:
rardecode.OpenReader(path, rardecode.Password("secret")). OpenReaderfollows multi-volume sets (.part1.rar,.part2.rar) automatically;NewReader(io.Reader)reads single-volume archives from any stream.h.Solidis true for files that continue a solid stream.