zig-skills/references/std-tar.md

14 KiB

std.tar - Tar Archive API Reference (Zig 0.16.0)

Tar archive reading and writing. Zig 0.16 file and stream APIs use std.Io.Dir, std.Io.File, std.Io.Reader, and std.Io.Writer.

Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html

The examples below use Zig 0.16's explicit std.Io APIs. tar.extract also sanitizes archive paths to prevent traversal outside the destination.

Table of Contents

Module Structure

std.tar.Iterator    // Iterate over entries in tar archive
std.tar.Writer      // Create tar archives
std.tar.Diagnostics // Collect errors during extraction
std.tar.FileKind    // .file, .directory, .sym_link
std.tar.ExtractOptions // Options for extract
std.tar.extract()      // Extract archive to directory

Reading Tar Archives

Iterator API

Iterate over files, directories, and symlinks in a tar archive:

const data = @embedFile("archive.tar");
var reader: std.Io.Reader = .fixed(data);

// Buffers must be provided by caller
var file_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
var link_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;

var it: std.tar.Iterator = .init(&reader, .{
    .file_name_buffer = &file_name_buffer,
    .link_name_buffer = &link_name_buffer,
});

while (try it.next()) |file| {
    switch (file.kind) {
        .directory => std.debug.print("Dir: {s}\n", .{file.name}),
        .file => {
            std.debug.print("File: {s} ({d} bytes)\n", .{ file.name, file.size });
            // Read file content - see below
        },
        .sym_link => std.debug.print("Link: {s} -> {s}\n", .{ file.name, file.link_name }),
    }
}

Iterator.File Structure

pub const File = struct {
    name: []const u8,      // file/dir/symlink path
    link_name: []const u8, // symlink target (empty for files/dirs)
    size: u64,             // file size in bytes
    mode: u32,             // POSIX permission mode
    kind: FileKind,        // .file, .directory, .sym_link
};

Reading File Contents

Read or copy file content before calling next() again if the content is needed. If content remains unread, next() discards it while advancing rather than failing:

while (try it.next()) |file| {
    if (file.kind == .file) {
        // Option 1: Stream to writer
        var buf: [1024]u8 = undefined;
        var output_file = try dir.createFile(io, file.name, .{});
        defer output_file.close(io);
        var file_writer = output_file.writer(io, &buf);
        try it.streamRemaining(file, &file_writer.interface);
        try file_writer.interface.flush();

        // Option 2: Stream to allocated buffer
        var content: std.Io.Writer.Allocating = .init(allocator);
        defer content.deinit();
        try it.streamRemaining(file, &content.writer);
        const bytes = content.written();  // []const u8
    }
}

Iterator Options

pub const Options = struct {
    file_name_buffer: []u8,     // buffer for file paths (use max_path_bytes)
    link_name_buffer: []u8,     // buffer for symlink targets
    diagnostics: ?*Diagnostics, // optional error collection
};

Extracting to Filesystem

extract

Extract entire archive to a directory:

const data = @embedFile("archive.tar");
var reader: std.Io.Reader = .fixed(data);

try std.tar.extract(io, std.Io.Dir.cwd(), &reader, .{
    .strip_components = 1,        // remove leading path component
    .mode_mode = .executable_bit_only,
    .exclude_empty_directories = false,
});

From File

const file = try std.Io.Dir.cwd().openFile(io, "archive.tar", .{});
defer file.close(io);

var buf: [4096]u8 = undefined;
var file_reader = file.reader(io, &buf);

try std.tar.extract(io, output_dir, &file_reader.interface, .{});

ExtractOptions

pub const ExtractOptions = struct {
    strip_components: u32 = 0,  // directories to strip from paths
    mode_mode: ModeMode = .executable_bit_only,
    exclude_empty_directories: bool = false,
    diagnostics: ?*Diagnostics = null,

    pub const ModeMode = enum {
        ignore,              // ignore tar mode, use system defaults
        executable_bit_only, // copy only executable bit to group/other
    };
};

strip_components: Removes leading path components. strip_components = 1 converts archive/src/main.zig to src/main.zig.

mode_mode:

  • .ignore: All files created with default permissions
  • .executable_bit_only: If owner has execute bit, set it for group and other too

Writing Tar Archives

Writer API

var output: std.Io.Writer.Allocating = .init(allocator);
defer output.deinit();

var w: std.tar.Writer = .{ .underlying_writer = &output.writer };

// Optional: set root directory prefix
try w.setRoot("myproject");

// Write files
try w.writeFileBytes("README.md", "# My Project\n", .{});
try w.writeFileBytes("src/main.zig", source_code, .{ .mode = 0o644 });

// Write directory
try w.writeDir("data", .{});

// Write symlink
try w.writeLink("latest", "v1.0", .{});

// Get tar data
const tar_bytes = output.written();

setRoot first emits a directory entry for a non-empty root and then applies that prefix to subsequent entries.

Writing from File

var output_file = try std.Io.Dir.cwd().createFile(io, "archive.tar", .{});
defer output_file.close(io);
var buf: [4096]u8 = undefined;
var file_writer = output_file.writer(io, &buf);

var w: std.tar.Writer = .{ .underlying_writer = &file_writer.interface };

// Write file from disk
var src_file = try std.Io.Dir.cwd().openFile(io, "data.txt", .{});
defer src_file.close(io);
var src_buf: [4096]u8 = undefined;
var src_reader = src_file.reader(io, &src_buf);
const stat = try src_file.stat(io);

try w.writeFileTimestamp("data.txt", &src_reader, stat.mtime);

try file_writer.interface.flush();

Writing from Stream

// When you know the size upfront
var content_reader: std.Io.Reader = .fixed(content_bytes);
try w.writeFileStream("file.txt", content_bytes.len, &content_reader, .{});

Writer Methods

// Set prefix for all subsequent paths
pub fn setRoot(w: *Writer, root: []const u8) Error!void

// Write directory entry
pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void

// Write file from bytes
pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void

// Write file from reader with known size
pub fn writeFileStream(w: *Writer, sub_path: []const u8, size: u64, reader: *std.Io.Reader, options: Options) WriteFileStreamError!void

// Write file from file reader
pub fn writeFile(w: *Writer, sub_path: []const u8, file_reader: *std.Io.File.Reader, mtime_seconds: u64) WriteFileError!void

// Convenience overload for std.Io.Timestamp
pub fn writeFileTimestamp(w: *Writer, sub_path: []const u8, file_reader: *std.Io.File.Reader, mtime: std.Io.Timestamp) WriteFileError!void

// Write symbolic link
pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void

// Write two zero blocks. The stdlib recommends not calling this: readers must
// accept archives without them, and omitting them avoids unnecessary output.
pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void

Writer Options

pub const Options = struct {
    mode: u32 = 0,   // POSIX mode (0 = default: 0o664 for files)
    mtime: u64 = 0,  // seconds since POSIX epoch; zero is written as zero
};

Diagnostics and Error Handling

Using Diagnostics

Collect errors instead of failing immediately:

var diagnostics: std.tar.Diagnostics = .{ .allocator = allocator };
defer diagnostics.deinit();

std.tar.extract(io, dir, &reader, .{
    .diagnostics = &diagnostics,
}) catch |err| {
    // Some errors are still fatal
    return err;
};

// Check collected errors
for (diagnostics.errors.items) |item| {
    switch (item) {
        .unable_to_create_file => |info| {
            std.debug.print("Failed to create {s}: {}\n", .{ info.file_name, info.code });
        },
        .unable_to_create_sym_link => |info| {
            std.debug.print("Failed to link {s} -> {s}\n", .{ info.file_name, info.link_name });
        },
        .unsupported_file_type => |info| {
            std.debug.print("Unsupported: {s} (type {})\n", .{ info.file_name, info.file_type });
        },
        .components_outside_stripped_prefix => |info| {
            std.debug.print("Stripped: {s}\n", .{info.file_name});
        },
    }
}

// Diagnostics also tracks root directory discovery
std.debug.print("Root dir: {s}, entries: {d}\n", .{ diagnostics.root_dir, diagnostics.entries });

Diagnostics.Error Variants

// Descriptive shape only: the unsupported_file_type payload contains a
// private header-kind enum, so std.tar.Header.Kind is not a public type name.
union(enum) {
    unable_to_create_sym_link: struct {
        code: anyerror,
        file_name: []const u8,
        link_name: []const u8,
    },
    unable_to_create_file: struct {
        code: anyerror,
        file_name: []const u8,
    },
    unsupported_file_type: struct {
        file_name: []const u8,
        file_type: /* private archive header kind */,
    },
    components_outside_stripped_prefix: struct {
        file_name: []const u8,
    },
};

Common Patterns

Extract and Process Archive

fn extractTar(io: std.Io, allocator: Allocator, tar_data: []const u8, dest: std.Io.Dir) !void {
    var reader: std.Io.Reader = .fixed(tar_data);

    var diagnostics: std.tar.Diagnostics = .{ .allocator = allocator };
    defer diagnostics.deinit();

    try std.tar.extract(io, dest, &reader, .{
        .strip_components = 1,
        .diagnostics = &diagnostics,
    });

    if (diagnostics.errors.items.len > 0) {
        for (diagnostics.errors.items) |err| {
            std.log.warn("tar extraction issue: {}", .{err});
        }
    }
}

List Archive Contents

fn listTar(allocator: Allocator, tar_data: []const u8) !void {
    _ = allocator;
    var reader: std.Io.Reader = .fixed(tar_data);

    var file_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
    var link_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;

    var it: std.tar.Iterator = .init(&reader, .{
        .file_name_buffer = &file_name_buffer,
        .link_name_buffer = &link_name_buffer,
    });

    while (try it.next()) |file| {
        const kind_char: u8 = switch (file.kind) {
            .directory => 'd',
            .file => '-',
            .sym_link => 'l',
        };
        std.debug.print("{c} {o:0>4} {d:>10} {s}", .{
            kind_char, file.mode, file.size, file.name,
        });
        if (file.kind == .sym_link) {
            std.debug.print(" -> {s}", .{file.link_name});
        }
        std.debug.print("\n", .{});
    }
}

Create Archive from Directory

fn createTarFromDir(io: std.Io, allocator: Allocator, source_dir: std.Io.Dir, root_name: []const u8) ![]u8 {
    var output: std.Io.Writer.Allocating = .init(allocator);
    errdefer output.deinit();

    var w: std.tar.Writer = .{ .underlying_writer = &output.writer };
    try w.setRoot(root_name);

    var walker = try source_dir.walk(allocator);
    defer walker.deinit();

    while (try walker.next(io)) |entry| {
        switch (entry.kind) {
            .directory => try w.writeDir(entry.path, .{}),
            .file => {
                var file = try entry.dir.openFile(io, entry.basename, .{});
                defer file.close(io);
                var buf: [4096]u8 = undefined;
                var file_reader = file.reader(io, &buf);
                const stat = try file.stat(io);
                try w.writeFileTimestamp(entry.path, &file_reader, stat.mtime);
            },
            .sym_link => {
                var link_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;
                const target_len = try entry.dir.readLink(io, entry.basename, &link_buf);
                try w.writeLink(entry.path, link_buf[0..target_len], .{});
            },
            else => {},  // skip special files
        }
    }

    return output.toOwnedSlice();
}

Extract Single File

fn extractFile(tar_data: []const u8, target_name: []const u8, allocator: Allocator) !?[]u8 {
    var reader: std.Io.Reader = .fixed(tar_data);

    var file_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
    var link_name_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;

    var it: std.tar.Iterator = .init(&reader, .{
        .file_name_buffer = &file_name_buffer,
        .link_name_buffer = &link_name_buffer,
    });

    while (try it.next()) |file| {
        if (file.kind == .file and std.mem.eql(u8, file.name, target_name)) {
            var content: std.Io.Writer.Allocating = .init(allocator);
            errdefer content.deinit();
            try it.streamRemaining(file, &content.writer);
            return content.toOwnedSlice();
        }
    }
    return null;
}

Supported Features

Formats: A deliberately non-comprehensive reader/writer for POSIX ustar, GNU long name/link extensions, and selected PAX attributes (path, linkpath, and size). Global PAX headers are ignored.

Entry types: Regular files, directories, symbolic links

Not supported: Hard links, device nodes, FIFOs, sparse files, and other special entries. With diagnostics supplied, unsupported entries can be recorded; without diagnostics, extraction returns error.TarUnsupportedHeader.

Path handling: Automatic prefix/name splitting, GNU extended headers for paths > 256 bytes