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 | optional zlib, bzip2, liblzma, zstd, lz4, OpenSSL (each enables more formats) |
| Read / write | yes / no (reads RAR4 and RAR5; no RAR writing, but writes ZIP, 7z and TAR) |
libarchive is the library behind bsdtar (the tar command on macOS, FreeBSD and Windows 10+). It reads RAR archives with its own decoder: RAR 2.9-4.x and RAR5 (since libarchive 3.4). It doesn’t decrypt password-protected RAR files and, like every open-source library, can’t create RAR. The same code opens CBZ, CB7 and CBT files, and it detects the format from the content, so a .cbr that is really a ZIP opens too.
The API is sequential: archive_read_next_header returns entries in archive order and archive_read_data reads the current one. That suits solid RAR archives, where entries must be decompressed in order anyway. Read the names (and the data you need) in one pass, then sort. Unlike the UnRAR library, libarchive is BSD-licensed.
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 RAR, walk the entries, 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, and in a solid RAR it decompresses every entry before the one you want. 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); // e.g. "comic.cbr"
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 convert CBR to CBZ#
Read every entry from the RAR and write it to a ZIP in one pass. Pages are stored without recompression:
int cbrToCbz(const char* in, const char* out) {
archive* r = openComic(in);
if (!r) return 1;
archive* w = archive_write_new();
archive_write_set_format_zip(w);
archive_write_set_options(w, "zip:compression=store");
if (archive_write_open_filename(w, out) != ARCHIVE_OK) {
archive_read_free(r);
archive_write_free(w);
return 1;
}
archive_entry* e;
int rc = 0;
while ((rc = archive_read_next_header(r, &e)) == ARCHIVE_OK) {
if (archive_entry_filetype(e) != AE_IFREG) continue;
std::vector<unsigned char> data = readCurrent(r);
archive_entry* oe = archive_entry_new();
archive_entry_set_pathname(oe, archive_entry_pathname(e));
archive_entry_set_size(oe, (la_int64_t)data.size());
archive_entry_set_filetype(oe, AE_IFREG);
archive_entry_set_perm(oe, 0644);
archive_entry_set_mtime(oe, archive_entry_mtime(e), 0);
archive_write_header(w, oe);
archive_write_data(w, data.data(), data.size());
archive_entry_free(oe);
}
archive_read_free(r);
archive_write_close(w);
archive_write_free(w);
return rc == ARCHIVE_EOF ? 0 : 1; // anything else is a read error
}
Keep the original names: they define the page order. Converting is lossless.
Tips#
- Check what the archive really is with
archive_format(a)after the first header:ARCHIVE_FORMAT_RAR,ARCHIVE_FORMAT_RAR_V5orARCHIVE_FORMAT_ZIP. Many.cbrfiles are ZIP. - An encrypted RAR fails in
archive_read_datawith an error;archive_read_has_encrypted_entries(a)tells you up front when the format supports the check. - Multi-volume RAR (
.part1.rar,.part2.rar) opens witharchive_read_open_filenames, passing all parts in order. archive_entry_pathname_utf8(e)returns names as UTF-8 regardless of locale; on Windows usearchive_entry_pathname_w.- Limit the size you read for untrusted files: check
archive_entry_size(e)before reading a page.