All formats

libarchive

LanguageC (usable from C++)
LicenseBSD 2-clause (a few files under other permissive licenses)
Repositorygithub.com/libarchive/libarchive
Documentationlibarchive wiki, examples, man pages archive_read(3), archive_write(3)
Depends onoptional zlib, bzip2, liblzma, libzstd, lz4 for compression; OpenSSL or similar for some formats
Read / writeyes / yes; ustar, pax, GNU, v7 tar; gzip, bzip2, xz, lzma, zstd, lz4, lzip, compress

libarchive is a C library for streaming archive access. It reads tar in every variant (it detects the format and compression from the data, not the file name) and writes ustar, pax, GNU and v7 tar with any of its compression filters. It also handles ZIP, 7z, cpio, ISO 9660, RAR (read) and more through the same API. bsdtar, the tar command on macOS, FreeBSD and Windows 10+, is a thin program on top of it.

The API is stream-oriented: you call archive_read_next_header to get each entry, then read its data or skip it. There is no random access, as TAR has no index. Extraction to disk goes through an archive_write_disk object that sets permissions, times and links and can refuse unsafe paths.

Installation#

Shell
# Debian / Ubuntu
sudo apt install libarchive-dev
# Fedora
sudo dnf install libarchive-devel
# macOS (the system library has no headers)
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 a tar archive#

Works for .tar, .tar.gz, .tar.xz, .tar.zst and others: the filter and format are detected automatically.

C
#include <archive.h>
#include <archive_entry.h>
#include <stdio.h>

int main(int argc, char **argv) {
    const char *path = argc > 1 ? argv[1] : "archive.tar.gz";
    struct archive *a = archive_read_new();
    archive_read_support_filter_all(a);   // gzip, bzip2, xz, zstd, ...
    archive_read_support_format_all(a);   // tar, zip, 7z, ...

    if (archive_read_open_filename(a, path, 10240) != ARCHIVE_OK) {
        fprintf(stderr, "open: %s\n", archive_error_string(a));
        archive_read_free(a);
        return 1;
    }
    struct archive_entry *entry;
    int r;
    while ((r = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
        printf("%10lld  %s\n", (long long)archive_entry_size(entry),
               archive_entry_pathname(entry));
        archive_read_data_skip(a);  // don't decompress the data
    }
    if (r != ARCHIVE_EOF)
        fprintf(stderr, "read: %s\n", archive_error_string(a));
    printf("format: %s, filter: %s\n", archive_format_name(a), archive_filter_name(a, 0));
    archive_read_free(a);
    return 0;
}

To accept only tar, call archive_read_support_format_tar(a) instead of archive_read_support_format_all(a).

How to read one file from a tar into memory#

C
#include <archive.h>
#include <archive_entry.h>
#include <stdlib.h>
#include <string.h>

// returns malloc'ed contents of `name` or NULL; *size receives the length
char *read_member(const char *tar_path, const char *name, size_t *size) {
    struct archive *a = archive_read_new();
    archive_read_support_filter_all(a);
    archive_read_support_format_tar(a);
    char *buf = NULL;
    if (archive_read_open_filename(a, tar_path, 10240) == ARCHIVE_OK) {
        struct archive_entry *e;
        while (archive_read_next_header(a, &e) == ARCHIVE_OK) {
            if (strcmp(archive_entry_pathname(e), name) != 0) continue;
            la_int64_t n = archive_entry_size(e);
            buf = malloc((size_t)n + 1);
            if (buf && archive_read_data(a, buf, (size_t)n) == n) {
                buf[n] = 0;
                *size = (size_t)n;
            } else {
                free(buf);
                buf = NULL;
            }
            break;
        }
    }
    archive_read_free(a);
    return buf;
}

How to extract a tar.gz safely#

Copy entries to an archive_write_disk object. The SECURE flags refuse .. paths, absolute paths and writing through symbolic links:

C
#include <archive.h>
#include <archive_entry.h>
#include <stdio.h>

static int copy_data(struct archive *ar, struct archive *aw) {
    const void *buf;
    size_t size;
    la_int64_t offset;
    for (;;) {
        int r = archive_read_data_block(ar, &buf, &size, &offset);
        if (r == ARCHIVE_EOF) return ARCHIVE_OK;
        if (r < ARCHIVE_OK) return r;
        if (archive_write_data_block(aw, buf, size, offset) < ARCHIVE_OK) return ARCHIVE_FATAL;
    }
}

int extract(const char *path) {
    int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM |
                ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS |
                ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS;
    struct archive *a = archive_read_new();
    archive_read_support_filter_all(a);
    archive_read_support_format_tar(a);
    struct archive *ext = archive_write_disk_new();
    archive_write_disk_set_options(ext, flags);
    archive_write_disk_set_standard_lookup(ext);  // map user/group names to IDs

    int status = 0;
    if (archive_read_open_filename(a, path, 10240) != ARCHIVE_OK) {
        fprintf(stderr, "%s\n", archive_error_string(a));
        status = 1;
        goto done;
    }
    struct archive_entry *entry;
    int r;
    while ((r = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
        // to extract into another folder, prefix archive_entry_pathname(entry)
        // and set it with archive_entry_set_pathname(entry, newpath)
        if (archive_write_header(ext, entry) < ARCHIVE_OK) {
            fprintf(stderr, "%s\n", archive_error_string(ext));
        } else if (archive_entry_size(entry) > 0 && copy_data(a, ext) < ARCHIVE_OK) {
            fprintf(stderr, "%s\n", archive_error_string(ext));
            status = 1;
            break;
        }
        archive_write_finish_entry(ext);
    }
    if (r != ARCHIVE_EOF && r != ARCHIVE_OK) status = 1;
done:
    archive_read_free(a);
    archive_write_free(ext);
    return status;
}

How to create a .tar.gz (pax format)#

C
#include <archive.h>
#include <archive_entry.h>
#include <string.h>
#include <time.h>

int write_tar_gz(const char *out_path) {
    struct archive *a = archive_write_new();
    archive_write_add_filter_gzip(a);          // or _xz, _zstd, _bzip2, or none
    archive_write_set_format_pax_restricted(a); // ustar, plus pax headers only when needed
    if (archive_write_open_filename(a, out_path) != ARCHIVE_OK) {
        archive_write_free(a);
        return 1;
    }
    const char *text = "hello from libarchive\n";
    struct archive_entry *e = archive_entry_new();
    archive_entry_set_pathname(e, "docs/hello.txt");
    archive_entry_set_size(e, (la_int64_t)strlen(text));
    archive_entry_set_filetype(e, AE_IFREG);
    archive_entry_set_perm(e, 0644);
    archive_entry_set_mtime(e, time(NULL), 0);
    archive_write_header(a, e);
    archive_write_data(a, text, strlen(text));
    archive_entry_free(e);

    archive_write_close(a);  // writes the end-of-archive blocks
    archive_write_free(a);
    return 0;
}

To add files from disk with their real metadata, use archive_read_disk_new and archive_read_disk_entry_from_file, or see tar/write.c in the bsdtar sources.

How to choose the tar variant when writing#

Function Writes
archive_write_set_format_pax_restricted ustar, adds pax headers only for entries that need them (bsdtar default)
archive_write_set_format_pax pax headers for every entry
archive_write_set_format_ustar strict ustar: fails on names over 255 bytes or files over 8 GiB
archive_write_set_format_gnutar GNU format with ././@LongLink entries
archive_write_set_format_v7tar old v7 format

Tips#