zip-zig

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

main.zig (27902B)


      1 const std = @import("std");
      2 
      3 // ZIP file implementation
      4 // See spec.txt.
      5 
      6 pub const CompressionMethod = enum(u16) {
      7     store = 0,
      8     shrink = 1,
      9     reduce_1 = 2,
     10     reduce_2 = 3,
     11     reduce_3 = 4,
     12     reduce_4 = 5,
     13     implode = 6,
     14     reserved_1 = 7,
     15     deflate = 8,
     16     deflate64 = 9,
     17     pkware_implode = 10,
     18     reserved_2 = 11,
     19     bzip2 = 12,
     20     reserved_3 = 13,
     21     lzma = 14,
     22     reserved_4 = 15,
     23     ibm_zos_zmpsc = 16,
     24     reserved_5 = 17,
     25     ibm_terse = 18,
     26     ibm_lz77_z = 19,
     27     zstd_deprecated = 20,
     28     zstd = 93,
     29     mp3 = 94,
     30     xz = 95,
     31     jpeg = 96,
     32     wavpack = 97,
     33     ppmd_version_i_rev1 = 98,
     34     aex_encryption_marker = 99,
     35 };
     36 // [local file header 1]
     37 // [encryption header 1]
     38 // [file data 1]
     39 // [data descriptor 1]
     40 // .
     41 // .
     42 // .
     43 // [local file header n]
     44 // [encryption header n]
     45 // [file data n]
     46 // [data descriptor n]
     47 // [archive decryption header]
     48 // [archive extra data record]
     49 // [central directory header 1]
     50 // .
     51 // .
     52 // .
     53 // [central directory header n]
     54 // [zip64 end of central directory record]
     55 // [zip64 end of central directory locator]
     56 // [end of central directory record]
     57 allocator: std.mem.Allocator,
     58 end_of_central_directory_record: EndOfCentralDirectoryRecord,
     59 central_directory_headers: std.ArrayList(CentralDirectoryHeader),
     60 
     61 pub fn empty(allocator: std.mem.Allocator) @This() {
     62     return .{
     63         .allocator = allocator,
     64         .end_of_central_directory_record = EndOfCentralDirectoryRecord.empty(),
     65         .central_directory_headers = std.ArrayList(CentralDirectoryHeader).init(allocator),
     66     };
     67 }
     68 
     69 pub fn from(allocator: std.mem.Allocator, file_or_stream: anytype) !@This() {
     70     // Find the EndOfCentralDirectoryRecord. It must be in the last 64k of the file
     71     const eocdr_search_width_max: usize = 64_000;
     72     const epos = try file_or_stream.getEndPos();
     73     const eocdr_search_width: usize = @min(epos, eocdr_search_width_max);
     74     const eocdr_seek_start: usize = epos - eocdr_search_width;
     75     try file_or_stream.seekTo(eocdr_seek_start);
     76     var reader = file_or_stream.reader();
     77     const sb = try read_to_sig(reader, EndOfCentralDirectoryRecord.SIG);
     78     try file_or_stream.seekBy(sb);
     79 
     80     var eocdr = try EndOfCentralDirectoryRecord.read(allocator, reader);
     81     errdefer eocdr.deinit(allocator);
     82     if (eocdr.disk_number_this != 0 or eocdr.disk_number_central_dir_start != 0) return error.SpansNotSupported;
     83     if (eocdr.total_central_dir_entries != eocdr.total_central_dir_entries_on_this_disk) return error.SpansNotSupported;
     84 
     85     try file_or_stream.seekTo(eocdr.central_dir_offset);
     86 
     87     var central_directory_headers = try std.ArrayList(CentralDirectoryHeader).initCapacity(allocator, eocdr.total_central_dir_entries);
     88     errdefer {
     89         // while (central_directory_headers.popOrNull()) |cdh| {
     90         //     cdh.deinit(allocator);
     91         // }
     92         var cdh: ?CentralDirectoryHeader = central_directory_headers.popOrNull();
     93         while (cdh != null) {
     94             cdh.?.deinit(allocator);
     95             cdh = central_directory_headers.popOrNull();
     96         }
     97         central_directory_headers.deinit();
     98     }
     99     for (0..eocdr.total_central_dir_entries) |_| {
    100         central_directory_headers.appendAssumeCapacity(try CentralDirectoryHeader.read(allocator, reader));
    101     }
    102 
    103     return .{
    104         .allocator = allocator,
    105         .end_of_central_directory_record = eocdr,
    106         .central_directory_headers = central_directory_headers,
    107     };
    108 }
    109 
    110 pub fn deinit(self: *@This()) void {
    111     self.end_of_central_directory_record.deinit(self.allocator);
    112     var cdh: ?CentralDirectoryHeader = self.central_directory_headers.popOrNull();
    113     while (cdh != null) {
    114         cdh.?.deinit(self.allocator);
    115         cdh = self.central_directory_headers.popOrNull();
    116     }
    117     self.central_directory_headers.deinit();
    118 }
    119 
    120 /// returns how much to seekBy after the signature  is found (becuase we'll now have read past it.)
    121 fn read_to_sig(reader: anytype, sig: u32) !i32 {
    122     const needle = @byteSwap(sig);
    123     var window: u32 = try reader.readIntLittle(u32);
    124     while (true) {
    125         if (window == needle) {
    126             return -4;
    127         }
    128         const nb = try reader.readByte();
    129         window <<= 8;
    130         window |= nb;
    131     } else {
    132         return error.SignatureNotFound;
    133     }
    134 }
    135 
    136 pub fn count_files(self: @This()) u16 {
    137     return self.end_of_central_directory_record.total_central_dir_entries;
    138 }
    139 pub fn file_name(self: @This(), index: usize) []const u8 {
    140     return self.central_directory_headers.items[index].file_name;
    141 }
    142 pub fn file_comment(self: @This(), index: usize) []const u8 {
    143     return self.central_directory_headers.items[index].file_comment;
    144 }
    145 pub fn is_dir(self: *@This(), index: usize) bool {
    146     return std.mem.endsWith(u8, self.central_directory_headers.items[index].file_name, "/"); // This is what the java stdlib does
    147 }
    148 pub fn extract(self: @This(), index: usize, stream_or_file_in: anytype, writer: anytype) !void {
    149     const cdh = self.central_directory_headers.items[index];
    150     try stream_or_file_in.seekTo(cdh.relative_offset_of_local_header);
    151     var reader = stream_or_file_in.reader();
    152     var lfh = try LocalFileHeader.read(self.allocator, reader);
    153     defer lfh.deinit(self.allocator);
    154     return try lfh.extract(self.allocator, reader, writer);
    155 }
    156 
    157 fn pump(reader: anytype, writer: anytype, expected_size_written: usize, expected_crc32: u32) !void {
    158     var buf = [_]u8{0} ** 1024;
    159     var crc32 = std.hash.Crc32.init();
    160     var written: usize = 0;
    161     while (true) {
    162         const read = try reader.read(&buf);
    163         if (read == 0) break;
    164         const write = buf[0..read];
    165         try writer.writeAll(write);
    166 
    167         crc32.update(write);
    168         written += read;
    169     }
    170     if (written != expected_size_written) return error.WrongUncompressedSize;
    171     if (crc32.final() != expected_crc32) return error.WrongChecksum;
    172 }
    173 
    174 fn pump_returning(reader: anytype, writer: anytype) !struct { u64, u32 } {
    175     var cw = std.io.countingWriter(writer);
    176     var cww = cw.writer();
    177     var buf = [_]u8{0} ** 1024;
    178     var crc32 = std.hash.Crc32.init();
    179     while (true) {
    180         std.log.err("reading...", .{});
    181         const read = try reader.read(&buf);
    182         if (read == 0) break;
    183         const write = buf[0..read];
    184         try cww.writeAll(write);
    185 
    186         crc32.update(write);
    187     }
    188     const res = .{ cw.bytes_written, crc32.final() };
    189     std.log.err("res... {any}", .{res});
    190     return res;
    191 }
    192 
    193 pub const CentralDirectoryHeader = struct {
    194     const SIG: u32 = @as(u32, 0x02014b50);
    195     version_made_by: u16,
    196     version_needed_to_extract: u16,
    197     general_purpose_bit_flag: std.bit_set.IntegerBitSet(16),
    198     compression_method: CompressionMethod,
    199     last_mod_file_time: u16,
    200     last_mod_file_date: u16,
    201     crc32: u32,
    202     compressed_size: u32,
    203     uncompressed_size: u32,
    204     file_name_length: u16,
    205     extra_field_length: u16,
    206     file_comment_length: u16,
    207     disk_number_start: u16,
    208     internal_file_attributes: u16,
    209     external_file_attributes: u32,
    210     relative_offset_of_local_header: u32,
    211     file_name: []const u8,
    212     extra_field: []const u8,
    213     file_comment: []const u8,
    214 
    215     pub fn read(allocator: std.mem.Allocator, reader: anytype) !CentralDirectoryHeader {
    216         return read2(allocator, reader, CentralDirectoryHeader, &[_]Dynamic{
    217             .{ .field_name = "file_name", .length_field_name = "file_name_length" },
    218             .{ .field_name = "extra_field", .length_field_name = "extra_field_length" },
    219             .{ .field_name = "file_comment", .length_field_name = "file_comment_length" },
    220         });
    221     }
    222 
    223     pub fn write(self: *CentralDirectoryHeader, writer: anytype) !void {
    224         // TODO generics
    225         self.file_name_length = @intCast(self.file_name.len);
    226         self.extra_field_length = @intCast(self.extra_field.len);
    227         self.file_comment_length = @intCast(self.file_comment.len);
    228 
    229         try writer.writeIntLittle(u32, SIG);
    230         try writer.writeIntLittle(u16, self.version_made_by);
    231         try writer.writeIntLittle(u16, self.version_needed_to_extract);
    232         try writer.writeIntLittle(u16, @bitCast(self.general_purpose_bit_flag));
    233         try writer.writeIntLittle(u16, @intFromEnum(self.compression_method));
    234         try writer.writeIntLittle(u16, self.last_mod_file_time);
    235         try writer.writeIntLittle(u16, self.last_mod_file_date);
    236         try writer.writeIntLittle(u32, self.crc32);
    237         try writer.writeIntLittle(u32, self.compressed_size);
    238         try writer.writeIntLittle(u32, self.uncompressed_size);
    239         try writer.writeIntLittle(u16, self.file_name_length);
    240         try writer.writeIntLittle(u16, self.extra_field_length);
    241         try writer.writeIntLittle(u16, self.file_comment_length);
    242         try writer.writeIntLittle(u16, self.disk_number_start);
    243         try writer.writeIntLittle(u16, self.internal_file_attributes);
    244         try writer.writeIntLittle(u32, self.external_file_attributes);
    245         try writer.writeIntLittle(u32, self.relative_offset_of_local_header);
    246         try writer.writeAll(self.file_name);
    247         try writer.writeAll(self.extra_field);
    248         try writer.writeAll(self.file_comment);
    249     }
    250 
    251     pub fn from(allocator: std.mem.Allocator, lfh: LocalFileHeader, offset: u32) !CentralDirectoryHeader {
    252         // TODO generics
    253         return .{
    254             .version_made_by = 0,
    255             .version_needed_to_extract = lfh.version_needed_to_extract,
    256             .general_purpose_bit_flag = lfh.general_purpose_bit_flag,
    257             .compression_method = lfh.compression_method,
    258             .last_mod_file_time = lfh.last_mod_file_time,
    259             .last_mod_file_date = lfh.last_mod_file_date,
    260             .crc32 = lfh.crc32,
    261             .compressed_size = lfh.compressed_size,
    262             .uncompressed_size = lfh.uncompressed_size,
    263             .file_name_length = lfh.file_name_length,
    264             .extra_field_length = lfh.extra_field_length,
    265             .file_comment_length = 0,
    266             .disk_number_start = 0,
    267             .internal_file_attributes = 0,
    268             .external_file_attributes = 0,
    269             .relative_offset_of_local_header = offset,
    270             .file_name = try allocator.dupe(u8, lfh.file_name),
    271             .extra_field = try allocator.dupe(u8, lfh.extra_field),
    272             .file_comment = &[_]u8{},
    273         };
    274     }
    275 
    276     pub fn deinit(self: *CentralDirectoryHeader, allocator: std.mem.Allocator) void {
    277         allocator.free(self.file_name);
    278         allocator.free(self.extra_field);
    279         allocator.free(self.file_comment);
    280     }
    281 };
    282 
    283 pub const EndOfCentralDirectoryRecord = struct {
    284     const SIG: u32 = @as(u32, 0x06054b50);
    285     disk_number_this: u16,
    286     disk_number_central_dir_start: u16,
    287     total_central_dir_entries_on_this_disk: u16,
    288     total_central_dir_entries: u16,
    289     size_of_central_dir: u32,
    290     central_dir_offset: u32,
    291     comment_length: u16,
    292     comment: []const u8,
    293 
    294     pub fn read(allocator: std.mem.Allocator, reader: anytype) !EndOfCentralDirectoryRecord {
    295         return read2(allocator, reader, EndOfCentralDirectoryRecord, &[_]Dynamic{
    296             .{ .field_name = "comment", .length_field_name = "comment_length" },
    297         });
    298     }
    299 
    300     pub fn empty() EndOfCentralDirectoryRecord {
    301         return .{
    302             .disk_number_this = 0,
    303             .disk_number_central_dir_start = 0,
    304             .total_central_dir_entries_on_this_disk = 0,
    305             .total_central_dir_entries = 0,
    306             .size_of_central_dir = 0,
    307             .central_dir_offset = 0,
    308             .comment_length = 0,
    309             .comment = &[_]u8{},
    310         };
    311     }
    312 
    313     pub fn write(self: *EndOfCentralDirectoryRecord, writer: anytype) !void {
    314         self.comment_length = @intCast(self.comment.len);
    315         try writer.writeIntLittle(u32, SIG);
    316         try writer.writeIntLittle(u16, self.disk_number_this);
    317         try writer.writeIntLittle(u16, self.disk_number_central_dir_start);
    318         try writer.writeIntLittle(u16, self.total_central_dir_entries_on_this_disk);
    319         try writer.writeIntLittle(u16, self.total_central_dir_entries);
    320         try writer.writeIntLittle(u32, self.size_of_central_dir);
    321         try writer.writeIntLittle(u32, self.central_dir_offset);
    322         try writer.writeIntLittle(u16, self.comment_length);
    323         try writer.writeAll(self.comment);
    324     }
    325 
    326     pub fn deinit(self: *EndOfCentralDirectoryRecord, allocator: std.mem.Allocator) void {
    327         allocator.free(self.comment);
    328     }
    329 };
    330 
    331 pub const LocalFileHeader = struct {
    332     const SIG: u32 = @as(u32, 0x04034b50);
    333     version_needed_to_extract: u16,
    334     general_purpose_bit_flag: std.bit_set.IntegerBitSet(16),
    335     compression_method: CompressionMethod,
    336     last_mod_file_time: u16,
    337     last_mod_file_date: u16,
    338     crc32: u32,
    339     compressed_size: u32,
    340     uncompressed_size: u32,
    341     file_name_length: u16,
    342     extra_field_length: u16,
    343     file_name: []const u8,
    344     extra_field: []const u8,
    345 
    346     pub fn read(allocator: std.mem.Allocator, reader: anytype) !LocalFileHeader {
    347         return read2(allocator, reader, LocalFileHeader, &[_]Dynamic{
    348             .{ .field_name = "file_name", .length_field_name = "file_name_length" },
    349             .{ .field_name = "extra_field", .length_field_name = "extra_field_length" },
    350         });
    351     }
    352 
    353     pub fn from(allocator: std.mem.Allocator, path: []const u8) !LocalFileHeader {
    354         std.log.err("lfh.from {s}", .{path});
    355         const file = try std.fs.cwd().openFile(path, .{});
    356         defer file.close();
    357         const md = try file.metadata();
    358         var rdr = file.reader();
    359         var cw = std.io.countingWriter(std.io.null_writer);
    360         var comp = try std.compress.deflate.compressor(allocator, cw.writer(), .{});
    361         defer comp.deinit();
    362         const written, const crc32 = try pump_returning(rdr, comp.writer());
    363 
    364         return .{
    365             .version_needed_to_extract = 1,
    366             .general_purpose_bit_flag = std.bit_set.IntegerBitSet(16).initEmpty(),
    367             .compression_method = .deflate,
    368             .last_mod_file_time = 0, // TODO
    369             .last_mod_file_date = 0, // TODO
    370             .crc32 = crc32,
    371             .compressed_size = @intCast(written),
    372             .uncompressed_size = @intCast(md.size()), // TODO zip64 support
    373             .file_name_length = @intCast(path.len),
    374             .extra_field_length = 0,
    375             .file_name = try allocator.dupe(u8, path),
    376             .extra_field = &[_]u8{},
    377         };
    378     }
    379 
    380     pub fn write(self: *LocalFileHeader, writer: anytype) !void {
    381         self.file_name_length = @intCast(self.file_name.len);
    382         self.extra_field_length = @intCast(self.extra_field.len);
    383         try writer.writeIntLittle(u32, SIG);
    384         try writer.writeIntLittle(u16, self.version_needed_to_extract);
    385         try writer.writeIntLittle(u16, @bitCast(self.general_purpose_bit_flag.mask));
    386         try writer.writeIntLittle(u16, @intFromEnum(self.compression_method));
    387         try writer.writeIntLittle(u16, self.last_mod_file_time);
    388         try writer.writeIntLittle(u16, self.last_mod_file_date);
    389         try writer.writeIntLittle(u32, self.crc32);
    390         try writer.writeIntLittle(u32, self.compressed_size);
    391         try writer.writeIntLittle(u32, self.uncompressed_size);
    392         try writer.writeIntLittle(u16, self.file_name_length);
    393         try writer.writeIntLittle(u16, self.extra_field_length);
    394         try writer.writeAll(self.file_name);
    395         try writer.writeAll(self.extra_field);
    396     }
    397 
    398     pub fn deinit(self: *LocalFileHeader, allocator: std.mem.Allocator) void {
    399         allocator.free(self.file_name);
    400         allocator.free(self.extra_field);
    401     }
    402 
    403     pub fn is_dir(self: *LocalFileHeader) bool {
    404         return std.mem.endsWith(u8, self.file_name, "/"); // This is what the java stdlib does
    405     }
    406 
    407     pub fn extract(self: *LocalFileHeader, allocator: std.mem.Allocator, reader: anytype, writer: anytype) !void {
    408         const is_encrypted = self.general_purpose_bit_flag.isSet(0);
    409         if (is_encrypted) return error.EncryptionNotSupported;
    410 
    411         if (self.is_dir()) {
    412             if (self.compressed_size != 0) {
    413                 // directories can't have a size, this is very likely wrong.
    414                 return error.InvalidFileName;
    415             }
    416             return; // Do nothing here. If we were definitely writing to a filesystem we could make an empty dir I guess.
    417         }
    418 
    419         var lr = std.io.limitedReader(reader, self.compressed_size);
    420         var limited_reader = lr.reader();
    421         switch (self.compression_method) {
    422             .store => {
    423                 try pump(limited_reader, writer, self.uncompressed_size, self.crc32);
    424             },
    425             .deflate => {
    426                 var decomp = try std.compress.deflate.decompressor(allocator, limited_reader, null);
    427                 defer decomp.deinit();
    428                 var decomp_reader = decomp.reader();
    429                 try pump(decomp_reader, writer, self.uncompressed_size, self.crc32);
    430             },
    431             .lzma => {
    432                 var decomp = try std.compress.lzma.decompress(allocator, limited_reader);
    433                 defer decomp.deinit();
    434                 var decomp_reader = decomp.reader();
    435                 try pump(decomp_reader, writer, self.uncompressed_size, self.crc32);
    436             },
    437             else => {
    438                 std.log.err("compression method {} not supported", .{self.compression_method});
    439                 return error.CompressionMethodNotSupported;
    440             },
    441         }
    442     }
    443 
    444     pub fn write_data(self: *LocalFileHeader, allocator: std.mem.Allocator, reader: anytype, writer: anytype) !void {
    445         const is_encrypted = self.general_purpose_bit_flag.isSet(0);
    446         if (is_encrypted) return error.EncryptionNotSupported;
    447 
    448         if (self.is_dir()) {
    449             if (self.compressed_size != 0) {
    450                 // directories can't have a size, this is very likely wrong.
    451                 return error.InvalidFileName;
    452             }
    453             return; // Do nothing here. If we were definitely writing to a filesystem we could make an empty dir I guess.
    454         }
    455 
    456         switch (self.compression_method) {
    457             .store => {
    458                 _ = try pump_returning(reader, writer);
    459             },
    460             .deflate => {
    461                 var comp = try std.compress.deflate.compressor(allocator, writer, .{});
    462                 defer comp.deinit();
    463                 var decomp_writer = comp.writer();
    464                 _ = try pump_returning(reader, decomp_writer);
    465             },
    466             else => {
    467                 std.log.err("compression method {} not supported", .{self.compression_method});
    468                 return error.CompressionMethodNotSupported;
    469             },
    470         }
    471     }
    472 };
    473 
    474 const Dynamic = struct {
    475     field_name: []const u8,
    476     length_field_name: []const u8,
    477 };
    478 
    479 fn read2(
    480     allocator: std.mem.Allocator,
    481     reader: anytype,
    482     comptime T: type,
    483     comptime dynamics: []const Dynamic,
    484 ) !T {
    485     if (!@hasDecl(T, "SIG")) @compileError("Expected decl SIG:u32 on type " ++ @typeName(T));
    486     const ti = @typeInfo(T);
    487     if (ti != .Struct) @compileError("read2 expects type parameter T to be a struct, but it was a " ++ @typeName(T));
    488     const si = ti.Struct;
    489 
    490     const sig_actual = try reader.readIntLittle(u32);
    491     if (sig_actual != T.SIG) {
    492         std.log.err("invalid signature expected {x} got {x}", .{ T.SIG, sig_actual });
    493         return error.InvalidSignature;
    494     }
    495     var t: T = undefined;
    496     inline for (si.fields) |field| {
    497         const fti = @typeInfo(field.type);
    498         dynamic: inline for (dynamics) |dyn| {
    499             if (comptime std.mem.eql(u8, dyn.field_name, field.name)) {
    500                 if (fti != .Pointer) @compileError("field " ++ field.name ++ " is marked dynamic but isn't a pointer. Instead it's a " ++ @typeName(field.type));
    501                 const pi = fti.Pointer;
    502                 if (pi.size != .Slice) @compileError("field " ++ field.name ++ " is marked dynamic, but isn't a slice, instead it's sized " ++ @tagName(pi.size));
    503                 const len = @field(t, dyn.length_field_name);
    504                 var buf = try allocator.alloc(pi.child, len);
    505                 // TODO how to errdefer in a loop, not sure where the scope ends.
    506                 _ = try reader.readAll(buf);
    507                 @field(t, field.name) = buf;
    508                 break :dynamic;
    509             }
    510         } else {
    511             switch (fti) {
    512                 .Int => {
    513                     @field(t, field.name) = try reader.readIntLittle(field.type);
    514                 },
    515                 .Struct => |fsi| {
    516                     if (fsi.backing_integer) |bi| {
    517                         const int = try reader.readIntLittle(bi);
    518                         @field(t, field.name) = @bitCast(int);
    519                     } else @compileError("only packed struct with backing integer are supported, field " ++ field.name ++ " type " ++ @typeName(field.type) ++ "is not such a struct");
    520                 },
    521                 .Enum => |fei| {
    522                     if (@typeInfo(fei.tag_type) == .Int) {
    523                         const int = try reader.readIntLittle(fei.tag_type);
    524                         @field(t, field.name) = @enumFromInt(int);
    525                     } else @compileError("only enum with integer tag type are supported field " ++ field.name ++ " type " ++ @typeName(field.type) ++ "is not such a struct");
    526                 },
    527                 else => @compileError("don't know  how to handle field " ++ field.name ++ " of type " ++ @tagName(fti)),
    528             }
    529         }
    530     }
    531     return t;
    532 }
    533 
    534 pub const Options = struct {
    535     allocator: std.mem.Allocator,
    536 };
    537 
    538 /// tar.zig compatibility, ish. It does a forwards only pass of a zipfile
    539 pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void {
    540     const allocator = options.allocator;
    541     // var peek_stream = std.io.peekStream(4, reader);
    542     // var peek_reader = peek_stream.reader();
    543     var i: u32 = 0;
    544     while (true) {
    545         std.log.err("reading file {}", .{i});
    546         var lfh = LocalFileHeader.read(allocator, reader) catch |e| switch (e) {
    547             error.InvalidSignature => return, // done
    548             else => return e,
    549         };
    550         defer lfh.deinit(allocator);
    551         if (std.fs.path.dirname(lfh.file_name)) |dn| {
    552             try dir.makePath(dn);
    553         }
    554         if (!lfh.is_dir()) {
    555             var f = try dir.createFile(lfh.file_name, .{});
    556             defer f.close();
    557             var writer = f.writer();
    558             try lfh.extract(allocator, reader, writer);
    559         }
    560         // TODO skip data descriptor
    561         i += 1;
    562     }
    563 }
    564 
    565 pub const WriteOptions = struct {};
    566 
    567 /// Write a whole zip file
    568 pub fn write_zip(allocator: std.mem.Allocator, path_iterator: anytype, writer: anytype) !void {
    569     var cw = std.io.countingWriter(writer);
    570     var cww = cw.writer();
    571     var zf = @This().empty(allocator);
    572     defer zf.deinit();
    573     while (path_iterator.next()) |path| {
    574         var lfh = try LocalFileHeader.from(allocator, path);
    575         defer lfh.deinit(allocator);
    576         const file = try std.fs.cwd().openFile(path, .{});
    577         defer file.close();
    578         var cdh = try CentralDirectoryHeader.from(allocator, lfh, @intCast(cw.bytes_written));
    579         try zf.central_directory_headers.append(cdh);
    580         std.log.err("lfh {any}", .{lfh});
    581         try lfh.write(cww);
    582         try lfh.write_data(allocator, file.reader(), cww);
    583     }
    584     const central_dir_offset = cw.bytes_written;
    585     for (0..zf.central_directory_headers.items.len) |i| {
    586         var cdh = zf.central_directory_headers.items[i];
    587         std.log.err("cdh {any}", .{cdh});
    588         try cdh.write(cww);
    589     }
    590     const size_of_central_dir = cw.bytes_written - central_dir_offset;
    591     var eocdr = EndOfCentralDirectoryRecord{
    592         .disk_number_this = 0,
    593         .disk_number_central_dir_start = 0,
    594         .total_central_dir_entries_on_this_disk = @intCast(zf.central_directory_headers.items.len),
    595         .total_central_dir_entries = @intCast(zf.central_directory_headers.items.len),
    596         .size_of_central_dir = @intCast(size_of_central_dir),
    597         .central_dir_offset = @intCast(central_dir_offset), // TODO zip64 support
    598         .comment_length = 0,
    599         .comment = &[_]u8{},
    600     };
    601     std.log.err("eocdr {any}", .{eocdr});
    602     try eocdr.write(cww);
    603 }
    604 
    605 const TestFileIter = struct {
    606     files: [2][]const u8,
    607     ix: usize,
    608     fn next(self: *TestFileIter) ?[]const u8 {
    609         if (self.ix < 2) {
    610             const res = self.files[self.ix];
    611             self.ix += 1;
    612             return res;
    613         }
    614         return null;
    615     }
    616 };
    617 
    618 test "write" {
    619     // TODO this isn't finished.
    620     return error.SkipZigTest;
    621     // const allocator = std.testing.allocator;
    622     // {
    623     //     const out = try std.fs.cwd().createFile("test_out.zip", .{});
    624     //     defer out.close();
    625     //     var tfi = TestFileIter{
    626     //         .ix = 0,
    627     //         .files = [_][]const u8{ "src/foo.txt", "src/bar.txt" },
    628     //     };
    629     //     try write_zip(allocator, &tfi, out.writer());
    630     // }
    631 
    632     // {
    633     //     const out = try std.fs.cwd().openFile("test_out.zip", .{});
    634     //     defer out.close();
    635     //     try pipeToFileSystem(try std.fs.cwd().makeOpenPath("test_out", .{}), out.reader(), .{ .allocator = allocator });
    636     // }
    637 }
    638 
    639 // test "open stream" {
    640 //     const test_zip = @embedFile("hello.zip");
    641 //     var fbs = std.io.fixedBufferStream(test_zip);
    642 //     var zf = try @This().from(std.testing.allocator, &fbs);
    643 //     defer zf.deinit();
    644 //     try std.testing.expectEqual(zf.count_files(), 2);
    645 //     try std.testing.expectEqualStrings(zf.file_name(0), "hello.txt");
    646 //     try std.testing.expectEqualStrings(zf.file_name(1), "foo.txt");
    647 // }
    648 
    649 // test "open file" {
    650 //     const test_zip = try std.fs.cwd().openFile("src/hello.zip", .{});
    651 //     var zf = try @This().from(std.testing.allocator, &test_zip);
    652 //     defer zf.deinit();
    653 //     try std.testing.expectEqual(zf.count_files(), 2);
    654 //     try std.testing.expectEqualStrings(zf.file_name(0), "hello.txt");
    655 //     try std.testing.expectEqualStrings(zf.file_name(1), "foo.txt");
    656 // }
    657 
    658 // test "extract stored" {
    659 //     const test_zip = @embedFile("hello.zip");
    660 //     var fbs = std.io.fixedBufferStream(test_zip);
    661 //     var zf = try @This().from(std.testing.allocator, &fbs);
    662 //     defer zf.deinit();
    663 //     var out = [_]u8{0} ** 1024;
    664 //     var fbs_out = std.io.fixedBufferStream(&out);
    665 //     var writer = fbs_out.writer();
    666 //     try zf.extract(0, &fbs, writer);
    667 //     try std.testing.expectEqualStrings("Hello, Zip!", fbs_out.getWritten());
    668 //     fbs_out.reset();
    669 //     try zf.extract(1, &fbs, writer);
    670 //     try std.testing.expectEqualStrings("hi there\n", fbs_out.getWritten());
    671 // }
    672 
    673 // test "extract deflate" {
    674 //     const test_zip = @embedFile("deflate.zip");
    675 //     var fbs = std.io.fixedBufferStream(test_zip);
    676 //     var zf = try @This().from(std.testing.allocator, &fbs);
    677 //     defer zf.deinit();
    678 //     var out = [_]u8{0} ** 1024;
    679 //     var fbs_out = std.io.fixedBufferStream(&out);
    680 //     var writer = fbs_out.writer();
    681 //     try std.testing.expectEqualStrings("Here is a comment :)", zf.file_comment(0));
    682 //     try std.testing.expectEqual(@This().CompressionMethod.deflate, zf.central_directory_headers.items[0].compression_method);
    683 //     try zf.extract(0, &fbs, writer);
    684 //     try std.testing.expectEqualStrings(@embedFile("foo.txt"), fbs_out.getWritten());
    685 // }
    686 // test "subdir" {
    687 //     const test_zip = @embedFile("subfolder.zip");
    688 //     var fbs = std.io.fixedBufferStream(test_zip);
    689 //     var zf = try @This().from(std.testing.allocator, &fbs);
    690 //     defer zf.deinit();
    691 //     try std.testing.expectEqual(true, zf.is_dir(0));
    692 //     try std.testing.expectEqual(false, zf.is_dir(1));
    693 // }
    694 // test "pipe to filesystem" {
    695 //     var f = try std.fs.cwd().openFile("src/subfolder.zip", .{});
    696 //     defer f.close();
    697 //     try pipeToFileSystem(try std.fs.cwd().makeOpenPath("test", .{}), f.reader(), .{ .allocator = std.testing.allocator });
    698 // }