From 4dfca01de4fda9a195048011b3339686dce4e936 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 29 Jan 2024 14:12:19 +0100 Subject: [PATCH] gzip: implement compression --- lib/std/compress.zig | 32 ++- lib/std/compress/deflate/compressor.zig | 6 +- lib/std/compress/deflate/compressor_test.zig | 8 +- lib/std/compress/deflate/decompressor.zig | 13 +- .../compress/deflate/deflate_fast_test.zig | 4 +- .../compress/deflate/huffman_bit_writer.zig | 3 +- lib/std/compress/gzip.zig | 211 +++++++++++++++--- lib/std/compress/testdata/rfc1952.txt.gz | Bin 8059 -> 8056 bytes 8 files changed, 221 insertions(+), 56 deletions(-) diff --git a/lib/std/compress.zig b/lib/std/compress.zig index 7e81d9deba..e56008cefe 100644 --- a/lib/std/compress.zig +++ b/lib/std/compress.zig @@ -21,7 +21,7 @@ pub fn HashedReader( pub fn read(self: *@This(), buf: []u8) Error!usize { const amt = try self.child_reader.read(buf); - self.hasher.update(buf); + self.hasher.update(buf[0..amt]); return amt; } @@ -38,6 +38,36 @@ pub fn hashedReader( return .{ .child_reader = reader, .hasher = hasher }; } +pub fn HashedWriter( + comptime WriterType: anytype, + comptime HasherType: anytype, +) type { + return struct { + child_writer: WriterType, + hasher: HasherType, + + pub const Error = WriterType.Error; + pub const Writer = std.io.Writer(*@This(), Error, write); + + pub fn write(self: *@This(), buf: []const u8) Error!usize { + const amt = try self.child_writer.write(buf); + self.hasher.update(buf[0..amt]); + return amt; + } + + pub fn writer(self: *@This()) Writer { + return .{ .context = self }; + } + }; +} + +pub fn hashedWriter( + writer: anytype, + hasher: anytype, +) HashedWriter(@TypeOf(writer), @TypeOf(hasher)) { + return .{ .child_writer = writer, .hasher = hasher }; +} + test { _ = deflate; _ = gzip; diff --git a/lib/std/compress/deflate/compressor.zig b/lib/std/compress/deflate/compressor.zig index e41b097636..0326668793 100644 --- a/lib/std/compress/deflate/compressor.zig +++ b/lib/std/compress/deflate/compressor.zig @@ -733,7 +733,7 @@ pub fn Compressor(comptime WriterType: anytype) type { } /// Writes the compressed form of `input` to the underlying writer. - pub fn write(self: *Self, input: []const u8) !usize { + pub fn write(self: *Self, input: []const u8) Error!usize { var buf = input; // writes data to hm_bw, which will eventually write the @@ -756,7 +756,7 @@ pub fn Compressor(comptime WriterType: anytype) type { /// If the underlying writer returns an error, `flush()` returns that error. /// /// In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH. - pub fn flush(self: *Self) !void { + pub fn flush(self: *Self) Error!void { self.sync = true; try self.step(); try self.hm_bw.writeStoredHeader(0, false); @@ -956,7 +956,7 @@ pub fn Compressor(comptime WriterType: anytype) type { } /// Writes any pending data to the underlying writer. - pub fn close(self: *Self) !void { + pub fn close(self: *Self) Error!void { self.sync = true; try self.step(); try self.hm_bw.writeStoredHeader(0, true); diff --git a/lib/std/compress/deflate/compressor_test.zig b/lib/std/compress/deflate/compressor_test.zig index 1f765e3fdc..f7f5b34a9a 100644 --- a/lib/std/compress/deflate/compressor_test.zig +++ b/lib/std/compress/deflate/compressor_test.zig @@ -86,7 +86,7 @@ fn testSync(level: deflate.Compression, input: []const u8) !void { read = try decomp.reader().readAll(&final); try testing.expectEqual(@as(usize, 0), read); // expect ended stream to return 0 bytes - _ = decomp.close(); + try decomp.close(); } } @@ -102,7 +102,7 @@ fn testSync(level: deflate.Compression, input: []const u8) !void { defer testing.allocator.free(decompressed); _ = try decomp.reader().readAll(decompressed); - _ = decomp.close(); + try decomp.close(); try testing.expectEqualSlices(u8, input, decompressed); } @@ -477,7 +477,7 @@ test "inflate reset" { .readAllAlloc(testing.allocator, math.maxInt(usize)); defer testing.allocator.free(decompressed_1); - _ = decomp.close(); + try decomp.close(); try testing.expectEqualSlices(u8, strings[0], decompressed_0); try testing.expectEqualSlices(u8, strings[1], decompressed_1); @@ -524,7 +524,7 @@ test "inflate reset dictionary" { .readAllAlloc(testing.allocator, math.maxInt(usize)); defer testing.allocator.free(decompressed_1); - _ = decomp.close(); + try decomp.close(); try testing.expectEqualSlices(u8, strings[0], decompressed_0); try testing.expectEqualSlices(u8, strings[1], decompressed_1); diff --git a/lib/std/compress/deflate/decompressor.zig b/lib/std/compress/deflate/decompressor.zig index 8e86bc09f3..896f931a66 100644 --- a/lib/std/compress/deflate/decompressor.zig +++ b/lib/std/compress/deflate/decompressor.zig @@ -477,11 +477,10 @@ pub fn Decompressor(comptime ReaderType: type) type { } } - pub fn close(self: *Self) ?Error { - if (self.err == @as(?Error, error.EndOfStreamWithNoError)) { - return null; + pub fn close(self: *Self) Error!void { + if (self.err) |err| { + if (err != error.EndOfStreamWithNoError) return err; } - return self.err; } // RFC 1951 section 3.2.7. @@ -880,7 +879,7 @@ pub fn Decompressor(comptime ReaderType: type) type { /// Replaces the inner reader and dictionary with new_reader and new_dict. /// new_reader must be of the same type as the reader being replaced. - pub fn reset(s: *Self, new_reader: ReaderType, new_dict: ?[]const u8) !void { + pub fn reset(s: *Self, new_reader: ReaderType, new_dict: ?[]const u8) Error!void { s.inner_reader = new_reader; s.step = nextBlock; s.err = null; @@ -920,9 +919,7 @@ test "confirm decompressor resets" { const buf = try decomp.reader().readAllAlloc(std.testing.allocator, 1024 * 100); defer std.testing.allocator.free(buf); - if (decomp.close()) |err| { - return err; - } + try decomp.close(); try decomp.reset(stream.reader(), null); } diff --git a/lib/std/compress/deflate/deflate_fast_test.zig b/lib/std/compress/deflate/deflate_fast_test.zig index ca9a978ad1..fdb8e3fd6a 100644 --- a/lib/std/compress/deflate/deflate_fast_test.zig +++ b/lib/std/compress/deflate/deflate_fast_test.zig @@ -83,7 +83,7 @@ test "best speed" { defer decomp.deinit(); const read = try decomp.reader().readAll(decompressed); - _ = decomp.close(); + try decomp.close(); try testing.expectEqual(want.items.len, read); try testing.expectEqualSlices(u8, want.items, decompressed); @@ -150,7 +150,7 @@ test "best speed max match offset" { var decomp = try inflate.decompressor(testing.allocator, fib.reader(), null); defer decomp.deinit(); const read = try decomp.reader().readAll(decompressed); - _ = decomp.close(); + try decomp.close(); try testing.expectEqual(src.len, read); try testing.expectEqualSlices(u8, src, decompressed); diff --git a/lib/std/compress/deflate/huffman_bit_writer.zig b/lib/std/compress/deflate/huffman_bit_writer.zig index 27c8b0a7af..a79dc91aa8 100644 --- a/lib/std/compress/deflate/huffman_bit_writer.zig +++ b/lib/std/compress/deflate/huffman_bit_writer.zig @@ -124,7 +124,8 @@ pub fn HuffmanBitWriter(comptime WriterType: type) type { if (self.err) { return; } - self.bytes_written += try self.inner_writer.write(b); + try self.inner_writer.writeAll(b); + self.bytes_written += b.len; } fn writeBits(self: *Self, b: u32, nb: u32) Error!void { diff --git a/lib/std/compress/gzip.zig b/lib/std/compress/gzip.zig index bb94687114..0576812a09 100644 --- a/lib/std/compress/gzip.zig +++ b/lib/std/compress/gzip.zig @@ -1,5 +1,5 @@ // -// Decompressor for GZIP data streams (RFC1952) +// Compressor/Decompressor for GZIP data streams (RFC1952) const std = @import("../std.zig"); const io = std.io; @@ -8,6 +8,8 @@ const testing = std.testing; const mem = std.mem; const deflate = std.compress.deflate; +const magic = &[2]u8{ 0x1f, 0x8b }; + // Flags for the FLG field in the header const FTEXT = 1 << 0; const FHCRC = 1 << 1; @@ -17,6 +19,14 @@ const FCOMMENT = 1 << 4; const max_string_len = 1024; +pub const Header = struct { + extra: ?[]const u8 = null, + filename: ?[]const u8 = null, + comment: ?[]const u8 = null, + modification_time: u32 = 0, + operating_system: u8 = 255, +}; + pub fn Decompress(comptime ReaderType: type) type { return struct { const Self = @This(); @@ -30,25 +40,19 @@ pub fn Decompress(comptime ReaderType: type) type { inflater: deflate.Decompressor(ReaderType), in_reader: ReaderType, hasher: std.hash.Crc32, - read_amt: usize, + read_amt: u32, - info: struct { - extra: ?[]const u8, - filename: ?[]const u8, - comment: ?[]const u8, - modification_time: u32, - operating_system: u8, - }, + info: Header, - fn init(allocator: mem.Allocator, source: ReaderType) !Self { - var hasher = std.compress.hashedReader(source, std.hash.Crc32.init()); + fn init(allocator: mem.Allocator, in_reader: ReaderType) !Self { + var hasher = std.compress.hashedReader(in_reader, std.hash.Crc32.init()); const hashed_reader = hasher.reader(); // gzip header format is specified in RFC1952 const header = try hashed_reader.readBytesNoEof(10); // Check the ID1/ID2 fields - if (header[0] != 0x1f or header[1] != 0x8b) + if (!std.mem.eql(u8, header[0..2], magic)) return error.BadHeader; const CM = header[2]; @@ -88,15 +92,15 @@ pub fn Decompress(comptime ReaderType: type) type { errdefer if (comment) |p| allocator.free(p); if (FLG & FHCRC != 0) { - const hash = try source.readInt(u16, .little); + const hash = try in_reader.readInt(u16, .little); if (hash != @as(u16, @truncate(hasher.hasher.final()))) return error.WrongChecksum; } - return Self{ + return .{ .allocator = allocator, - .inflater = try deflate.decompressor(allocator, source, null), - .in_reader = source, + .inflater = try deflate.decompressor(allocator, in_reader, null), + .in_reader = in_reader, .hasher = std.hash.Crc32.init(), .info = .{ .filename = filename, @@ -119,7 +123,7 @@ pub fn Decompress(comptime ReaderType: type) type { self.allocator.free(comment); } - // Implements the io.Reader interface + /// Implements the io.Reader interface pub fn read(self: *Self, buffer: []u8) Error!usize { if (buffer.len == 0) return 0; @@ -128,10 +132,12 @@ pub fn Decompress(comptime ReaderType: type) type { const r = try self.inflater.read(buffer); if (r != 0) { self.hasher.update(buffer[0..r]); - self.read_amt += r; + self.read_amt +%= @truncate(r); return r; } + try self.inflater.close(); + // We've reached the end of stream, check if the checksum matches const hash = try self.in_reader.readInt(u32, .little); if (hash != self.hasher.final()) @@ -139,7 +145,7 @@ pub fn Decompress(comptime ReaderType: type) type { // The ISIZE field is the size of the uncompressed input modulo 2^32 const input_size = try self.in_reader.readInt(u32, .little); - if (self.read_amt & 0xffffffff != input_size) + if (self.read_amt != input_size) return error.CorruptedData; return 0; @@ -155,7 +161,117 @@ pub fn decompress(allocator: mem.Allocator, reader: anytype) !Decompress(@TypeOf return Decompress(@TypeOf(reader)).init(allocator, reader); } -fn testReader(data: []const u8, comptime expected: []const u8) !void { +pub const CompressOptions = struct { + header: Header = .{}, + hash_header: bool = true, + level: deflate.Compression = .default_compression, +}; + +pub fn Compress(comptime WriterType: type) type { + return struct { + const Self = @This(); + + pub const Error = WriterType.Error || + deflate.Compressor(WriterType).Error; + pub const Writer = io.Writer(*Self, Error, write); + + allocator: mem.Allocator, + deflater: deflate.Compressor(WriterType), + out_writer: WriterType, + hasher: std.hash.Crc32, + write_amt: u32, + + fn init(allocator: mem.Allocator, out_writer: WriterType, options: CompressOptions) !Self { + var hasher = std.compress.hashedWriter(out_writer, std.hash.Crc32.init()); + const hashed_writer = hasher.writer(); + + // ID1/ID2 + try hashed_writer.writeAll(magic); + // CM + try hashed_writer.writeByte(8); + // Flags + try hashed_writer.writeByte( + @as(u8, if (options.hash_header) FHCRC else 0) | + @as(u8, if (options.header.extra) |_| FEXTRA else 0) | + @as(u8, if (options.header.filename) |_| FNAME else 0) | + @as(u8, if (options.header.comment) |_| FCOMMENT else 0), + ); + // Modification time + try hashed_writer.writeInt(u32, options.header.modification_time, .little); + // Extra flags + try hashed_writer.writeByte(0); + // Operating system + try hashed_writer.writeByte(options.header.operating_system); + + if (options.header.extra) |extra| { + try hashed_writer.writeInt(u16, @intCast(extra.len), .little); + try hashed_writer.writeAll(extra); + } + + if (options.header.filename) |filename| { + try hashed_writer.writeAll(filename); + try hashed_writer.writeByte(0); + } + + if (options.header.comment) |comment| { + try hashed_writer.writeAll(comment); + try hashed_writer.writeByte(0); + } + + if (options.hash_header) { + try out_writer.writeInt( + u16, + @truncate(hasher.hasher.final()), + .little, + ); + } + + return .{ + .allocator = allocator, + .deflater = try deflate.compressor(allocator, out_writer, .{ .level = options.level }), + .out_writer = out_writer, + .hasher = std.hash.Crc32.init(), + .write_amt = 0, + }; + } + + pub fn deinit(self: *Self) void { + self.deflater.deinit(); + } + + /// Implements the io.Writer interface + pub fn write(self: *Self, buffer: []const u8) Error!usize { + if (buffer.len == 0) + return 0; + + // Write to the compressed stream and update the computed checksum + const r = try self.deflater.write(buffer); + self.hasher.update(buffer[0..r]); + self.write_amt +%= @truncate(r); + return r; + } + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + pub fn flush(self: *Self) Error!void { + try self.deflater.flush(); + } + + pub fn close(self: *Self) Error!void { + try self.deflater.close(); + try self.out_writer.writeInt(u32, self.hasher.final(), .little); + try self.out_writer.writeInt(u32, self.write_amt, .little); + } + }; +} + +pub fn compress(allocator: mem.Allocator, writer: anytype, options: CompressOptions) !Compress(@TypeOf(writer)) { + return Compress(@TypeOf(writer)).init(allocator, writer, options); +} + +fn testReader(expected: []const u8, data: []const u8) !void { var in_stream = io.fixedBufferStream(data); var gzip_stream = try decompress(testing.allocator, in_stream.reader()); @@ -169,70 +285,91 @@ fn testReader(data: []const u8, comptime expected: []const u8) !void { try testing.expectEqualSlices(u8, expected, buf); } +fn testWriter(expected: []const u8, data: []const u8, options: CompressOptions) !void { + var actual = std.ArrayList(u8).init(testing.allocator); + defer actual.deinit(); + + var gzip_stream = try compress(testing.allocator, actual.writer(), options); + defer gzip_stream.deinit(); + + // Write and compress the whole file + try gzip_stream.writer().writeAll(data); + try gzip_stream.close(); + + // Check against the reference + try testing.expectEqualSlices(u8, expected, actual.items); +} + // All the test cases are obtained by compressing the RFC1952 text // // https://tools.ietf.org/rfc/rfc1952.txt length=25037 bytes // SHA256=164ef0897b4cbec63abf1b57f069f3599bd0fb7c72c2a4dee21bd7e03ec9af67 test "compressed data" { - try testReader( - @embedFile("testdata/rfc1952.txt.gz"), - @embedFile("testdata/rfc1952.txt"), - ); + const plain = @embedFile("testdata/rfc1952.txt"); + const compressed = @embedFile("testdata/rfc1952.txt.gz"); + try testReader(plain, compressed); + try testWriter(compressed, plain, .{ + .header = .{ + .filename = "rfc1952.txt", + .modification_time = 1706533053, + .operating_system = 3, + }, + }); } test "sanity checks" { // Truncated header try testing.expectError( error.EndOfStream, - testReader(&[_]u8{ 0x1f, 0x8B }, ""), + testReader(undefined, &[_]u8{ 0x1f, 0x8B }), ); // Wrong CM try testing.expectError( error.InvalidCompression, - testReader(&[_]u8{ + testReader(undefined, &[_]u8{ 0x1f, 0x8b, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, - }, ""), + }), ); // Wrong checksum try testing.expectError( error.WrongChecksum, - testReader(&[_]u8{ + testReader(undefined, &[_]u8{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - }, ""), + }), ); // Truncated checksum try testing.expectError( error.EndOfStream, - testReader(&[_]u8{ + testReader(undefined, &[_]u8{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, - }, ""), + }), ); // Wrong initial size try testing.expectError( error.CorruptedData, - testReader(&[_]u8{ + testReader(undefined, &[_]u8{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - }, ""), + }), ); // Truncated initial size field try testing.expectError( error.EndOfStream, - testReader(&[_]u8{ + testReader(undefined, &[_]u8{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, ""), + }), ); } test "header checksum" { - try testReader(&[_]u8{ + try testReader("", &[_]u8{ // GZIP header 0x1f, 0x8b, 0x08, 0x12, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, @@ -241,5 +378,5 @@ test "header checksum" { // GZIP data 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, ""); + }); } diff --git a/lib/std/compress/testdata/rfc1952.txt.gz b/lib/std/compress/testdata/rfc1952.txt.gz index be43b90a7917a993933c3266db50882f77a5662e..17958d64f33341401979a71ec7617cd65ab8a05c 100644 GIT binary patch literal 8056 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 oc!TCNeMp0OdZ(9smFU