From 0c52085be7738b80a28fbced6f4801c171c533d7 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Fri, 15 Sep 2023 21:08:12 +0100 Subject: Initial decompression --- src/main.zig | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) (limited to 'src') 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()); +} -- cgit v1.2.3-ZIG