All formats

rardecode

LanguageGo
LicenseBSD-2-Clause
Repositorygithub.com/nwaples/rardecode
Documentationpkg.go.dev/github.com/nwaples/rardecode/v2
Depends onnothing (pure Go)
Read / writeyes / 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#

Shell
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.

Go
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:

Go
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:

Go
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#

Go
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:

Go
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#