libarchive
| Language | C (usable from C++) |
|---|---|
| License | BSD-2-Clause (mostly; see COPYING) |
| Repository | github.com/libarchive/libarchive |
| Documentation | libarchive wiki, man pages archive_read(3), archive_write(3) |
| Depends on | nothing for plain TAR (optional zlib, bzip2, liblzma, zstd for compressed tar) |
| Read / write | yes / 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#
# 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:
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):
#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:
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:
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):
#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.
#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#
- Skip AppleDouble entries (
._001.jpg) from macOS tar: theisPagefilter does that by rejecting names that start with a dot. archive_format(a)returnsARCHIVE_FORMAT_TAR_*values for tar; check it to catch.cbtfiles that are really ZIP.- For a reader, record each page’s name during the first pass and keep the archive file open; reading a page later is another sequential pass, so cache decoded pages.
- To write CBZ or CB7 instead, call
archive_write_set_format_ziporarchive_write_set_format_7zip.