const std = @import("std"); // ZIP file implementation // See spec.txt. const ZipFile = struct { allocator: std.mem.Allocator, is_zip_64: bool = false, end_of_central_directory_record: EndOfCentralDirectoryRecord, central_directory_headers: []CentralDirectoryHeader, fn from(allocator: std.mem.Allocator, file_or_stream: anytype) !ZipFile { // Find the EndOfCentralDirectoryRecord. It must be in the last 64k of the file const eocdr_search_width_max: usize = 64_000; const epos = try file_or_stream.getEndPos(); const eocdr_search_width: usize = @min(epos, eocdr_search_width_max); const eocdr_seek_start: usize = epos - eocdr_search_width; try file_or_stream.seekTo(eocdr_seek_start); var reader = file_or_stream.reader(); const needle = @byteSwap(EndOfCentralDirectoryRecord.SIG); var window: u32 = try reader.readIntLittle(u32); while (true) { if (window == needle) { try file_or_stream.seekBy(-4); break; } const nb = try reader.readByte(); window <<= 8; window |= nb; } else { return error.EndOfCentralDirectoryRecordNotFound; } var eocdr = try EndOfCentralDirectoryRecord.read(allocator, file_or_stream); errdefer eocdr.deinit(allocator); if (eocdr.disk_number_this != 0 or eocdr.disk_number_central_dir_start != 0) return error.SpansNotSupported; if (eocdr.total_central_dir_entries != eocdr.total_central_dir_entries_on_this_disk) return error.SpansNotSupported; var central_directory_headers = try allocator.alloc(CentralDirectoryHeader, eocdr.total_central_dir_entries); errdefer allocator.free(central_directory_headers); try file_or_stream.seekTo(eocdr.central_dir_offset); for (0..eocdr.total_central_dir_entries) |i| { central_directory_headers[i] = try CentralDirectoryHeader.read(allocator, file_or_stream); } return ZipFile{ .allocator = allocator, .end_of_central_directory_record = eocdr, .central_directory_headers = central_directory_headers, }; } fn deinit(self: *ZipFile) void { self.end_of_central_directory_record.deinit(self.allocator); for (0..self.central_directory_headers.len) |i| { self.central_directory_headers[i].deinit(self.allocator); } self.allocator.free(self.central_directory_headers); } // [local file header 1] // [encryption header 1] // [file data 1] // [data descriptor 1] // . // . // . // [local file header n] // [encryption header n] // [file data n] // [data descriptor n] // [archive decryption header] // [archive extra data record] // [central directory header 1] // . // . // . // [central directory header n] // [zip64 end of central directory record] // [zip64 end of central directory locator] // [end of central directory record] }; const Dynamic = struct { field_name: []const u8, length_field_name: []const u8, }; fn read2( allocator: std.mem.Allocator, stream_or_file: anytype, comptime T: type, comptime sig: u32, comptime dynamics: []const Dynamic, ) !T { const ti = @typeInfo(T); if (ti != .Struct) @compileError("read2 expects type parameter T to be a struct, but it was a " ++ @typeName(T)); const si = ti.Struct; var reader = stream_or_file.reader(); const sig_actual = try reader.readIntLittle(u32); if (sig_actual != sig) { std.log.err("invalid signature expected {x} got {x}", .{ sig, sig_actual }); return error.InvalidSignature; } var t: T = undefined; inline for (si.fields) |field| { const fti = @typeInfo(field.type); dynamic: inline for (dynamics) |dyn| { if (comptime std.mem.eql(u8, dyn.field_name, field.name)) { if (fti != .Pointer) @compileError("field " ++ field.name ++ " is marked dynamic but isn't a pointer. Instead it's a " ++ @typeName(field.type)); const pi = fti.Pointer; if (pi.size != .Slice) @compileError("field " ++ field.name ++ " is marked dynamic, but isn't a slice, instead it's sized " ++ @tagName(pi.size)); const len = @field(t, dyn.length_field_name); var buf = try allocator.alloc(pi.child, len); // TODO how to errdefer in a loop, not sure where the scope ends. _ = try reader.readAll(buf); @field(t, field.name) = buf; break :dynamic; } } else { switch (fti) { .Int => { @field(t, field.name) = try reader.readIntLittle(field.type); }, else => @compileError("don't know how to handle field " ++ field.name ++ " of type " ++ @tagName(fti)), } } } return t; } const CentralDirectoryHeader = struct { const SIG: u32 = @as(u32, 0x02014b50); version_made_by: u16, version_needed_to_extract: u16, general_purpose_bit_flag: u16, compression_method: u16, last_mod_file_time: u16, last_mod_file_date: u16, crc32: u32, compressed_size: u32, uncompressed_size: u32, file_name_length: u16, extra_field_length: u16, file_comment_length: u16, disk_number_start: u16, internal_file_attributes: u16, external_file_attributes: u32, relative_offset_of_local_header: u32, file_name: []const u8, extra_field: []const u8, file_comment: []const u8, fn read(allocator: std.mem.Allocator, stream_or_file: anytype) !CentralDirectoryHeader { return read2(allocator, stream_or_file, CentralDirectoryHeader, CentralDirectoryHeader.SIG, &[_]Dynamic{ .{ .field_name = "file_name", .length_field_name = "file_name_length" }, .{ .field_name = "extra_field", .length_field_name = "extra_field_length" }, .{ .field_name = "file_comment", .length_field_name = "file_comment_length" }, }); } fn deinit(self: *CentralDirectoryHeader, allocator: std.mem.Allocator) void { allocator.free(self.file_name); allocator.free(self.extra_field); allocator.free(self.file_comment); } }; const EndOfCentralDirectoryRecord = struct { const SIG: u32 = @as(u32, 0x06054b50); disk_number_this: u16, disk_number_central_dir_start: u16, total_central_dir_entries_on_this_disk: u16, total_central_dir_entries: u16, size_of_central_dir: u32, central_dir_offset: u32, comment_length: u16, comment: []const u8, fn read(allocator: std.mem.Allocator, file_or_stream: anytype) !EndOfCentralDirectoryRecord { return read2(allocator, file_or_stream, EndOfCentralDirectoryRecord, EndOfCentralDirectoryRecord.SIG, &[_]Dynamic{ .{ .field_name = "comment", .length_field_name = "comment_length" }, }); } fn deinit(self: *EndOfCentralDirectoryRecord, allocator: std.mem.Allocator) void { allocator.free(self.comment); } }; test "foo" { const test_zip = @embedFile("hello.zip"); var fbs = std.io.fixedBufferStream(test_zip); var zf = try ZipFile.from(std.testing.allocator, &fbs); defer zf.deinit(); try std.testing.expectEqual(zf.central_directory_headers.len, 2); try std.testing.expectEqualStrings(zf.central_directory_headers[0].file_name, "hello.txt"); try std.testing.expectEqualStrings(zf.central_directory_headers[1].file_name, "foo.txt"); }