summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main.zig104
1 files changed, 104 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
index d123e7f..34cb59c 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -4,6 +4,13 @@ const std = @import("std");
// See spec.txt.
const ZipFile = struct {
+ const CompressionMethod = enum(u16) {
+ store = 0,
+ deflate = 8,
+ lzma = 14,
+ zstd = 93,
+ xz = 95,
+ };
// [local file header 1]
// [encryption header 1]
// [file data 1]
@@ -82,6 +89,51 @@ const ZipFile = struct {
fn file_name(self: ZipFile, index: u16) []const u8 {
return self.central_directory_headers[index].file_name;
}
+ fn extract(self: ZipFile, index: u16, stream_or_file_in: anytype, stream_or_file_out: anytype) !void {
+ const cdh = self.central_directory_headers[index];
+ try stream_or_file_in.seekTo(cdh.relative_offset_of_local_header);
+ var lfh = try LocalFileHeader.read(self.allocator, stream_or_file_in);
+ defer lfh.deinit(self.allocator);
+ const is_encrypted = lfh.general_purpose_bit_flag.isSet(0);
+ if (is_encrypted) return error.EncryptionNotSupported;
+
+ var reader = stream_or_file_in.reader();
+ var lr = std.io.limitedReader(reader, lfh.compressed_size);
+ var limited_reader = lr.reader();
+ switch (lfh.compression_method) {
+ .store => { // no compression (store)
+ var writer = stream_or_file_out.writer();
+ try pump(limited_reader, writer, lfh.uncompressed_size, lfh.crc32);
+ },
+ .deflate => { // deflate
+ var decomp = try std.compress.deflate.decompressor(self.allocator, limited_reader, null);
+ defer decomp.deinit();
+ var decomp_reader = decomp.reader();
+ var writer = stream_or_file_out.writer();
+ try pump(decomp_reader, writer, lfh.uncompressed_size, lfh.crc32);
+ },
+ else => {
+ std.log.err("compression method {} not supported", .{lfh.compression_method});
+ return error.CompressionMethodNotSupported;
+ },
+ }
+ }
+ fn pump(reader: anytype, writer: anytype, expected_size_written: usize, expected_crc32: u32) !void {
+ var buf = [_]u8{0} ** 1024;
+ var crc32 = std.hash.Crc32.init();
+ var written: usize = 0;
+ while (true) {
+ const read = try reader.read(&buf);
+ if (read == 0) break;
+ const write = buf[0..read];
+ try writer.writeAll(write);
+
+ crc32.update(write);
+ written += read;
+ }
+ if (written != expected_size_written) return error.WrongUncompressedSize;
+ if (crc32.final() != expected_crc32) return error.WrongChecksum;
+ }
};
const CentralDirectoryHeader = struct {
@@ -143,6 +195,32 @@ const EndOfCentralDirectoryRecord = struct {
}
};
+const LocalFileHeader = struct {
+ const SIG: u32 = @as(u32, 0x04034b50);
+ version_needed_to_extract: u16,
+ general_purpose_bit_flag: std.bit_set.IntegerBitSet(16),
+ compression_method: ZipFile.CompressionMethod,
+ 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_name: []const u8,
+ extra_field: []const u8,
+ fn read(allocator: std.mem.Allocator, stream_or_file: anytype) !LocalFileHeader {
+ return read2(allocator, stream_or_file, LocalFileHeader, &[_]Dynamic{
+ .{ .field_name = "file_name", .length_field_name = "file_name_length" },
+ .{ .field_name = "extra_field", .length_field_name = "extra_field_length" },
+ });
+ }
+ fn deinit(self: *LocalFileHeader, allocator: std.mem.Allocator) void {
+ allocator.free(self.file_name);
+ allocator.free(self.extra_field);
+ }
+};
+
const Dynamic = struct {
field_name: []const u8,
length_field_name: []const u8,
@@ -185,6 +263,18 @@ fn read2(
.Int => {
@field(t, field.name) = try reader.readIntLittle(field.type);
},
+ .Struct => |fsi| {
+ if (fsi.backing_integer) |bi| {
+ const int = try reader.readIntLittle(bi);
+ @field(t, field.name) = @bitCast(int);
+ } else @compileError("only packed struct with backing integer are supported, field " ++ field.name ++ " type " ++ @typeName(field.type) ++ "is not such a struct");
+ },
+ .Enum => |fei| {
+ if (@typeInfo(fei.tag_type) == .Int) {
+ const int = try reader.readIntLittle(fei.tag_type);
+ @field(t, field.name) = @enumFromInt(int);
+ } else @compileError("only enum with integer tag type are supported field " ++ field.name ++ " type " ++ @typeName(field.type) ++ "is not such a struct");
+ },
else => @compileError("don't know how to handle field " ++ field.name ++ " of type " ++ @tagName(fti)),
}
}
@@ -210,3 +300,17 @@ test "foo2" {
try std.testing.expectEqualStrings(zf.file_name(0), "hello.txt");
try std.testing.expectEqualStrings(zf.file_name(1), "foo.txt");
}
+
+test "extract" {
+ 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();
+ var out = [_]u8{0} ** 1024;
+ var fbs_out = std.io.fixedBufferStream(&out);
+ try zf.extract(0, &fbs, &fbs_out);
+ try std.testing.expectEqualStrings("Hello, Zip!", fbs_out.getWritten());
+ fbs_out.reset();
+ try zf.extract(1, &fbs, &fbs_out);
+ try std.testing.expectEqualStrings("hi there\n", fbs_out.getWritten());
+}