    pub const Options = struct {
        /// Number of directory levels to skip when extracting files.
        strip_components: u32 = 0,
        /// How to handle the "mode" property of files from within the tar file.
        mode_mode: ModeMode = .executable_bit_only,

        const ModeMode = enum {
            /// The mode from the tar file is completely ignored. Files are created
            /// with the default mode when creating files.
            ignore,
            /// The mode from the tar file is inspected for the owner executable bit
            /// only. This bit is copied to the group and other executable bits.
            /// Other bits of the mode are left as the default when creating files.
            executable_bit_only,
        };
    };

    pub const Header = struct {
        bytes: *const [512]u8,

        pub const FileType = enum(u8) {
            normal = '0',
            hard_link = '1',
            symbolic_link = '2',
            character_special = '3',
            block_special = '4',
            directory = '5',
            fifo = '6',
            contiguous = '7',
            global_extended_header = 'g',
            extended_header = 'x',
            _,
        };

        pub fn fileSize(header: Header) !u64 {
            const raw = header.bytes[124..][0..12];
            const ltrimmed = std.mem.trimLeft(u8, raw, "0");
            const rtrimmed = std.mem.trimRight(u8, ltrimmed, " \x00");
            if (rtrimmed.len == 0) return 0;
            return std.fmt.parseInt(u64, rtrimmed, 8);
        }

        pub fn is_ustar(header: Header) bool {
            return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
        }

        /// Includes prefix concatenated, if any.
        /// Return value may point into Header buffer, or might point into the
        /// argument buffer.
        /// TODO: check against "../" and other nefarious things
        pub fn fullFileName(header: Header, buffer: *[255]u8) ![]const u8 {
            const n = name(header);
            if (!is_ustar(header))
                return n;
            const p = prefix(header);
            if (p.len == 0)
                return n;
            @memcpy(buffer[0..p.len], p);
            buffer[p.len] = '/';
            @memcpy(buffer[p.len + 1 ..][0..n.len], n);
            return buffer[0 .. p.len + 1 + n.len];
        }

        pub fn name(header: Header) []const u8 {
            return str(header, 0, 0 + 100);
        }

        pub fn prefix(header: Header) []const u8 {
            return str(header, 345, 345 + 155);
        }

        pub fn fileType(header: Header) FileType {
            const result = @as(FileType, @enumFromInt(header.bytes[156]));
            return if (result == @as(FileType, @enumFromInt(0))) .normal else result;
        }

        fn str(header: Header, start: usize, end: usize) []const u8 {
            var i: usize = start;
            while (i < end) : (i += 1) {
                if (header.bytes[i] == 0) break;
            }
            return header.bytes[start..i];
        }
    };

    pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
        switch (options.mode_mode) {
            .ignore => {},
            .executable_bit_only => {
                // This code does not look at the mode bits yet. To implement this feature,

                // the implementation must be adjusted to look at the mode, and check the

                // user executable bit, then call fchmod on newly created files when

                // the executable bit is supposed to be set.

                // It also needs to properly deal with ACLs on Windows.

                @panic("TODO: unimplemented: tar ModeMode.executable_bit_only");
            },
        }
        var file_name_buffer: [255]u8 = undefined;
        var buffer: [512 * 8]u8 = undefined;
        var start: usize = 0;
        var end: usize = 0;
        header: while (true) {
            if (buffer.len - start < 1024) {
                const dest_end = end - start;
                @memcpy(buffer[0..dest_end], buffer[start..end]);
                end = dest_end;
                start = 0;
            }
            const ask_header = @min(buffer.len - end, 1024 -| (end - start));
            end += try reader.readAtLeast(buffer[end..], ask_header);
            switch (end - start) {
                0 => return,
                1...511 => return error.UnexpectedEndOfStream,
                else => {},
            }
            const header: Header = .{ .bytes = buffer[start..][0..512] };
            start += 512;
            const file_size = try header.fileSize();
            const rounded_file_size = std.mem.alignForward(u64, file_size, 512);
            const pad_len = @as(usize, @intCast(rounded_file_size - file_size));
            const unstripped_file_name = try header.fullFileName(&file_name_buffer);
            switch (header.fileType()) {
                .directory => {
                    const file_name = try stripComponents(unstripped_file_name, options.strip_components);
                    if (file_name.len != 0) {
                        try dir.makePath(file_name);
                    }
                },
                .normal => {
                    if (file_size == 0 and unstripped_file_name.len == 0) return;
                    const file_name = try stripComponents(unstripped_file_name, options.strip_components);

                    if (std.fs.path.dirname(file_name)) |dir_name| {
                        try dir.makePath(dir_name);
                    }
                    var file = try dir.createFile(file_name, .{});
                    defer file.close();

                    var file_off: usize = 0;
                    while (true) {
                        if (buffer.len - start < 1024) {
                            const dest_end = end - start;
                            @memcpy(buffer[0..dest_end], buffer[start..end]);
                            end = dest_end;
                            start = 0;
                        }
                        // Ask for the rounded up file size + 512 for the next header.

                        // TODO: https://github.com/ziglang/zig/issues/14039

                        const ask = @as(usize, @intCast(@min(
                            buffer.len - end,
                            rounded_file_size + 512 - file_off -| (end - start),
                        )));
                        end += try reader.readAtLeast(buffer[end..], ask);
                        if (end - start < ask) return error.UnexpectedEndOfStream;
                        // TODO: https://github.com/ziglang/zig/issues/14039

                        const slice = buffer[start..@as(usize, @intCast(@min(file_size - file_off + start, end)))];
                        try file.writeAll(slice);
                        file_off += slice.len;
                        start += slice.len;
                        if (file_off >= file_size) {
                            start += pad_len;
                            // Guaranteed since we use a buffer divisible by 512.

                            assert(start <= end);
                            continue :header;
                        }
                    }
                },
                .global_extended_header, .extended_header => {
                    if (start + rounded_file_size > end) return error.TarHeadersTooBig;
                    start = @as(usize, @intCast(start + rounded_file_size));
                },
                .hard_link => return error.TarUnsupportedFileType,
                .symbolic_link => return error.TarUnsupportedFileType,
                else => return error.TarUnsupportedFileType,
            }
        }
    }

    fn stripComponents(path: []const u8, count: u32) ![]const u8 {
        var i: usize = 0;
        var c = count;
        while (c > 0) : (c -= 1) {
            if (std.mem.indexOfScalarPos(u8, path, i, '/')) |pos| {
                i = pos + 1;
            } else {
                return error.TarComponentsOutsideStrippedPrefix;
            }
        }
        return path[i..];
    }

    test stripComponents {
        const expectEqualStrings = std.testing.expectEqualStrings;
        try expectEqualStrings("a/b/c", try stripComponents("a/b/c", 0));
        try expectEqualStrings("b/c", try stripComponents("a/b/c", 1));
        try expectEqualStrings("c", try stripComponents("a/b/c", 2));
    }

    const std = @import("std.zig");
    const assert = std.debug.assert;