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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 JJiN_e008Aa$sPaz