All formats

libarchive

LanguageC (usable from C++)
LicenseBSD-2-Clause (mostly; see COPYING)
Repositorygithub.com/libarchive/libarchive
Documentationlibarchive wiki, man pages archive_read(3), archive_write(3)
Depends onnothing for plain TAR (optional zlib, bzip2, liblzma, zstd for compressed tar)
Read / writeyes / yes (ustar, pax, GNU tar, and compressed tar)

libarchive is the library behind bsdtar (the tar command on macOS, FreeBSD and Windows 10+), so its TAR support is as complete as it gets: ustar, pax extended headers, GNU long names and sparse files, with or without gzip/bzip2/xz/zstd compression. The same code opens CBZ, CBR and CB7, and the format is detected from the content.

TAR has no index, and libarchive’s API is sequential, which matches: archive_read_next_header walks the headers and skips data you don’t read. For a plain (uncompressed) CBT, skipping is a seek, so listing pages is fast even for large files.

Installation#

Shell
# Debian / Ubuntu
sudo apt install libarchive-dev
# Fedora
sudo dnf install libarchive-devel
# macOS (keg-only: add its pkg-config path)
brew install libarchive
# Windows (vcpkg)
vcpkg install libarchive

With CMake:

cmake
find_package(LibArchive REQUIRED)
target_link_libraries(myapp PRIVATE LibArchive::LibArchive)

Basic usage: list pages in natural order#

Open the archive, walk the entries, keep the image files and sort them with a natural sort (2.jpg before 10.jpg):

C++
#include <archive.h>
#include <archive_entry.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <string>
#include <vector>

// natural, case-insensitive "a < b": digit runs compare as numbers
bool naturalLess(const std::string& a, const std::string& b) {
    size_t i = 0, j = 0;
    while (i < a.size() && j < b.size()) {
        if (isdigit((unsigned char)a[i]) && isdigit((unsigned char)b[j])) {
            size_t ie = i, je = j;
            while (ie < a.size() && isdigit((unsigned char)a[ie])) ie++;
            while (je < b.size() && isdigit((unsigned char)b[je])) je++;
            while (i < ie - 1 && a[i] == '0') i++;
            while (j < je - 1 && b[j] == '0') j++;
            if (ie - i != je - j) return ie - i < je - j;
            int c = a.compare(i, ie - i, b, j, je - j);
            if (c != 0) return c < 0;
            i = ie;
            j = je;
        } else {
            int ca = tolower((unsigned char)a[i]), cb = tolower((unsigned char)b[j]);
            if (ca != cb) return ca < cb;
            i++;
            j++;
        }
    }
    return a.size() - i < b.size() - j;
}

bool isPage(const std::string& name) {
    if (name.find("__MACOSX/") != std::string::npos) return false;
    size_t slash = name.find_last_of('/');
    std::string base = slash == std::string::npos ? name : name.substr(slash + 1);
    if (base.empty() || base[0] == '.') return false;
    size_t dot = base.find_last_of('.');
    if (dot == std::string::npos) return false;
    std::string ext = base.substr(dot + 1);
    for (auto& c : ext) c = (char)tolower((unsigned char)c);
    for (const char* e : {"jpg", "jpeg", "png", "gif", "webp", "bmp", "avif", "jxl"})
        if (ext == e) return true;
    return false;
}

archive* openComic(const char* path) {
    archive* a = archive_read_new();
    archive_read_support_format_all(a);  // zip, rar, rar5, 7zip, tar, ...
    archive_read_support_filter_all(a);
    if (archive_read_open_filename(a, path, 64 * 1024) != ARCHIVE_OK) {
        fprintf(stderr, "%s: %s\n", path, archive_error_string(a));
        archive_read_free(a);
        return nullptr;
    }
    return a;
}

std::vector<std::string> listPages(const char* path) {
    std::vector<std::string> pages;
    archive* a = openComic(path);
    if (!a) return pages;
    archive_entry* e;
    while (archive_read_next_header(a, &e) == ARCHIVE_OK) {
        if (archive_entry_filetype(e) == AE_IFREG && isPage(archive_entry_pathname(e)))
            pages.push_back(archive_entry_pathname(e));
        // the data is skipped automatically by the next archive_read_next_header
    }
    archive_read_free(a);
    std::sort(pages.begin(), pages.end(), naturalLess);
    return pages;
}

int main(int argc, char** argv) {
    std::vector<std::string> pages = listPages(argv[1]);
    for (size_t i = 0; i < pages.size(); i++)
        printf("%3zu %s\n", i + 1, pages[i].c_str());
    return 0;
}

How to read one page into memory#

Walk the entries until the name matches, then read its data:

C++
std::vector<unsigned char> readCurrent(archive* a) {
    std::vector<unsigned char> out;
    unsigned char buf[64 * 1024];
    la_ssize_t n;
    while ((n = archive_read_data(a, buf, sizeof(buf))) > 0) out.insert(out.end(), buf, buf + n);
    if (n < 0) out.clear();  // corrupt data or unsupported compression
    return out;
}

std::vector<unsigned char> readEntry(const char* path, const std::string& name) {
    std::vector<unsigned char> data;
    archive* a = openComic(path);
    if (!a) return data;
    archive_entry* e;
    while (archive_read_next_header(a, &e) == ARCHIVE_OK) {
        if (name == archive_entry_pathname(e)) {
            data = readCurrent(a);
            break;
        }
    }
    archive_read_free(a);
    return data;
}

Each call rescans the archive from the start. In an uncompressed tar, skipping an entry is a seek, so this is cheap; still, a reader that shows many pages reads them all in one pass into a map.

How to extract the cover#

The cover is the first page in natural order:

C++
std::vector<std::string> pages = listPages(path);
if (!pages.empty()) {
    std::vector<unsigned char> cover = readEntry(path, pages[0]);
    std::string ext = pages[0].substr(pages[0].find_last_of('.'));
    if (FILE* f = fopen(("cover" + ext).c_str(), "wb")) {
        fwrite(cover.data(), 1, cover.size(), f);
        fclose(f);
    }
}

How to read ComicInfo.xml#

Compare the name case-insensitively, read the bytes and parse them with an XML library (here pugixml):

C++
#include <pugixml.hpp>
#include <cstring>
#include <strings.h>  // strcasecmp; use _stricmp on Windows

void printComicInfo(const char* path) {
    archive* a = openComic(path);
    if (!a) return;
    std::vector<unsigned char> xml;
    archive_entry* e;
    while (archive_read_next_header(a, &e) == ARCHIVE_OK) {
        if (strcasecmp(archive_entry_pathname(e), "ComicInfo.xml") == 0) {
            xml = readCurrent(a);
            break;
        }
    }
    archive_read_free(a);

    pugi::xml_document doc;
    if (xml.empty() || !doc.load_buffer(xml.data(), xml.size())) return;
    pugi::xml_node ci = doc.child("ComicInfo");
    printf("%s #%s: %s\n", ci.child_value("Series"), ci.child_value("Number"), ci.child_value("Title"));
    printf("Pages: %s, Manga: %s\n", ci.child_value("PageCount"), ci.child_value("Manga"));
    for (pugi::xml_node p : ci.child("Pages").children("Page"))
        if (strstr(p.attribute("Type").value(), "FrontCover"))
            printf("cover is page index %d\n", p.attribute("Image").as_int());
}

How to create a CBT from a folder of images#

Use the restricted pax format: it writes plain ustar headers and adds pax headers only for long or non-ASCII names. Don’t add a compression filter: readers expect a plain tar.

C++
#include <archive.h>
#include <archive_entry.h>
#include <filesystem>
#include <fstream>
#include <iterator>
namespace fs = std::filesystem;

int makeCbt(const fs::path& dir, const char* out) {
    std::vector<fs::path> files;
    for (auto& f : fs::directory_iterator(dir))
        if (f.is_regular_file() && (isPage(f.path().filename().string()) ||
                                    f.path().filename() == "ComicInfo.xml"))
            files.push_back(f.path());
    std::sort(files.begin(), files.end(), [](const fs::path& a, const fs::path& b) {
        return naturalLess(a.filename().string(), b.filename().string());
    });

    archive* a = archive_write_new();
    archive_write_set_format_pax_restricted(a);  // no archive_write_add_filter_*: plain tar
    if (archive_write_open_filename(a, out) != ARCHIVE_OK) {
        archive_write_free(a);
        return 1;
    }
    for (const fs::path& p : files) {
        std::ifstream in(p, std::ios::binary);
        std::vector<char> data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
        archive_entry* e = archive_entry_new();
        archive_entry_set_pathname(e, p.filename().string().c_str());
        archive_entry_set_size(e, (la_int64_t)data.size());
        archive_entry_set_filetype(e, AE_IFREG);
        archive_entry_set_perm(e, 0644);
        archive_write_header(a, e);
        archive_write_data(a, data.data(), data.size());
        archive_entry_free(e);
    }
    archive_write_close(a);  // writes the two zero blocks that end the archive
    archive_write_free(a);
    return 0;
}

Tips#