All formats

archive/tar

LanguageGo
LicenseBSD-3-Clause (Go standard library)
Repositorygo.googlesource.com/go
Documentationpkg.go.dev/archive/tar
Depends onnothing (standard library)
Read / writeyes / yes (ustar, pax, GNU)

archive/tar reads and writes TAR archives as streams. tar.NewReader(r) returns a reader; Next() advances to the next header and the reader then returns that file’s data. tar.NewWriter(w) writes headers and data. It handles pax and GNU extensions, so long and non-ASCII names work.

There’s no random access: to read a page, walk the headers until you reach it. On an *os.File, the reader seeks over data you skip, so listing a CBT is fast.

Installation#

Nothing to install: import "archive/tar".

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 (including macOS ._ files), and a natural, case-insensitive compare.

Go
package main

import (
	"archive/tar"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"slices"
)

func listPages(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tr := tar.NewReader(f)
	var pages []string
	for {
		h, err := tr.Next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}
		if h.Typeflag == tar.TypeReg && 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 pages into memory#

Read the pages you need in one pass:

Go
func readFiles(path string, want map[string]bool) (map[string][]byte, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tr := tar.NewReader(f)
	out := map[string][]byte{}
	for len(out) < len(want) {
		h, err := tr.Next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}
		if want[h.Name] {
			b, err := io.ReadAll(tr)
			if err != nil {
				return nil, err
			}
			out[h.Name] = b
		}
	}
	return out, nil
}

How to extract the cover#

Go
pages, err := listPages("comic.cbt")
if err != nil || len(pages) == 0 {
	log.Fatal("no pages")
}
files, err := readFiles("comic.cbt", map[string]bool{pages[0]: true})
if err != nil {
	log.Fatal(err)
}
os.WriteFile("cover"+strings.ToLower(path.Ext(pages[0])), files[pages[0]], 0o644)

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) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tr := tar.NewReader(f)
	for {
		h, err := tr.Next()
		if errors.Is(err, io.EOF) {
			return nil, nil
		}
		if err != nil {
			return nil, err
		}
		if strings.EqualFold(h.Name, "ComicInfo.xml") {
			var ci ComicInfo
			return &ci, xml.NewDecoder(tr).Decode(&ci)
		}
	}
}

How to create a CBT from a folder of images#

tar.FileInfoHeader builds a header from os.FileInfo; set Name to the name inside the archive:

Go
func makeCbt(dir, out string) error {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return err
	}
	var names []string
	for _, e := range entries {
		if e.Type().IsRegular() && isPage(e.Name()) {
			names = append(names, e.Name())
		}
	}
	slices.SortFunc(names, naturalCompare)

	f, err := os.Create(out)
	if err != nil {
		return err
	}
	defer f.Close()
	tw := tar.NewWriter(f)
	add := func(src, name string) error {
		fi, err := os.Stat(filepath.Join(dir, src))
		if err != nil {
			return err
		}
		h, err := tar.FileInfoHeader(fi, "")
		if err != nil {
			return err
		}
		h.Name = name
		h.Uname, h.Gname = "", "" // don't leak local user names
		if err := tw.WriteHeader(h); err != nil {
			return err
		}
		in, err := os.Open(filepath.Join(dir, src))
		if err != nil {
			return err
		}
		defer in.Close()
		_, err = io.Copy(tw, in)
		return err
	}
	for i, n := range names {
		if err := add(n, fmt.Sprintf("%03d%s", i+1, strings.ToLower(path.Ext(n)))); err != nil {
			return err
		}
	}
	if _, err := os.Stat(filepath.Join(dir, "ComicInfo.xml")); err == nil {
		if err := add("ComicInfo.xml", "ComicInfo.xml"); err != nil {
			return err
		}
	}
	return tw.Close() // writes the two zero blocks that end the archive
}

Go 1.22+ has tw.AddFS(os.DirFS(dir)), which adds a whole folder in one call when the names are already right.

Tips#