From 9fe4c89230df2d78c8bf37b4b1d7a9bedb92677b Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Fri, 11 Sep 2020 22:17:08 +0200 Subject: [PATCH] std: Add a gzip decoder --- build.zig | 1 + lib/std/compress.zig | 2 + lib/std/compress/gzip.zig | 248 ++++++++++++++++++++++++++++++++ lib/std/compress/rfc1952.txt.gz | Bin 0 -> 8059 bytes 4 files changed, 251 insertions(+) create mode 100644 lib/std/compress/gzip.zig create mode 100644 lib/std/compress/rfc1952.txt.gz diff --git a/build.zig b/build.zig index 3f7f1a9038..a6a2d87371 100644 --- a/build.zig +++ b/build.zig @@ -128,6 +128,7 @@ pub fn build(b: *Builder) !void { "README.md", ".z.0", ".z.9", + ".gz", "rfc1951.txt", }, }); diff --git a/lib/std/compress.zig b/lib/std/compress.zig index 5518f807df..95f496021e 100644 --- a/lib/std/compress.zig +++ b/lib/std/compress.zig @@ -6,8 +6,10 @@ const std = @import("std.zig"); pub const deflate = @import("compress/deflate.zig"); +pub const gzip = @import("compress/gzip.zig"); pub const zlib = @import("compress/zlib.zig"); test "" { + _ = gzip; _ = zlib; } diff --git a/lib/std/compress/gzip.zig b/lib/std/compress/gzip.zig new file mode 100644 index 0000000000..aad1731393 --- /dev/null +++ b/lib/std/compress/gzip.zig @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2015-2020 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. +// +// Decompressor for GZIP data streams (RFC1952) + +const std = @import("std"); +const io = std.io; +const fs = std.fs; +const testing = std.testing; +const mem = std.mem; +const deflate = std.compress.deflate; + +// Flags for the FLG field in the header +const FTEXT = 1 << 0; +const FHCRC = 1 << 1; +const FEXTRA = 1 << 2; +const FNAME = 1 << 3; +const FCOMMENT = 1 << 4; + +pub fn GzipStream(comptime ReaderType: type) type { + return struct { + const Self = @This(); + + pub const Error = ReaderType.Error || + deflate.InflateStream(ReaderType).Error || + error{ CorruptedData, WrongChecksum }; + pub const Reader = io.Reader(*Self, Error, read); + + allocator: *mem.Allocator, + inflater: deflate.InflateStream(ReaderType), + in_reader: ReaderType, + hasher: std.hash.Crc32, + window_slice: []u8, + read_amt: usize, + + info: struct { + filename: ?[]const u8, + comment: ?[]const u8, + modification_time: u32, + }, + + fn init(allocator: *mem.Allocator, source: ReaderType) !Self { + // gzip header format is specified in RFC1952 + const header = try source.readBytesNoEof(10); + + // Check the ID1/ID2 fields + if (header[0] != 0x1f or header[1] != 0x8b) + return error.BadHeader; + + const CM = header[2]; + // The CM field must be 8 to indicate the use of DEFLATE + if (CM != 8) return error.InvalidCompression; + // Flags + const FLG = header[3]; + // Modification time, as a Unix timestamp. + // If zero there's no timestamp available. + const MTIME = mem.readIntLittle(u32, header[4..8]); + // Extra flags + const XFL = header[8]; + // Operating system where the compression took place + const OS = header[9]; + + if (FLG & FEXTRA != 0) { + // Skip the extra data, we could read and expose it to the user + // if somebody needs it. + const len = try source.readIntLittle(u16); + try source.skipBytes(len, .{}); + } + + var filename: ?[]const u8 = null; + if (FLG & FNAME != 0) { + filename = try source.readUntilDelimiterAlloc( + allocator, + 0, + std.math.maxInt(usize), + ); + } + errdefer if (filename) |p| allocator.free(p); + + var comment: ?[]const u8 = null; + if (FLG & FCOMMENT != 0) { + comment = try source.readUntilDelimiterAlloc( + allocator, + 0, + std.math.maxInt(usize), + ); + } + errdefer if (comment) |p| allocator.free(p); + + if (FLG & FHCRC != 0) { + // TODO: Evaluate and check the header checksum. The stdlib has + // no CRC16 yet :( + _ = try source.readIntLittle(u16); + } + + // The RFC doesn't say anything about the DEFLATE window size to be + // used, default to 32K. + var window_slice = try allocator.alloc(u8, 32 * 1024); + + return Self{ + .allocator = allocator, + .inflater = deflate.inflateStream(source, window_slice), + .in_reader = source, + .hasher = std.hash.Crc32.init(), + .window_slice = window_slice, + .info = .{ + .filename = filename, + .comment = comment, + .modification_time = MTIME, + }, + .read_amt = 0, + }; + } + + pub fn deinit(self: *Self) void { + self.allocator.free(self.window_slice); + if (self.info.filename) |filename| + self.allocator.free(filename); + if (self.info.comment) |comment| + self.allocator.free(comment); + } + + // Implements the io.Reader interface + pub fn read(self: *Self, buffer: []u8) Error!usize { + if (buffer.len == 0) + return 0; + + // Read from the compressed stream and update the computed checksum + const r = try self.inflater.read(buffer); + if (r != 0) { + self.hasher.update(buffer[0..r]); + self.read_amt += r; + return r; + } + + // We've reached the end of stream, check if the checksum matches + const hash = try self.in_reader.readIntLittle(u32); + if (hash != self.hasher.final()) + return error.WrongChecksum; + + // The ISIZE field is the size of the uncompressed input modulo 2^32 + const input_size = try self.in_reader.readIntLittle(u32); + if (self.read_amt & 0xffffffff != input_size) + return error.CorruptedData; + + return 0; + } + + pub fn reader(self: *Self) Reader { + return .{ .context = self }; + } + }; +} + +pub fn gzipStream(allocator: *mem.Allocator, reader: anytype) !GzipStream(@TypeOf(reader)) { + return GzipStream(@TypeOf(reader)).init(allocator, reader); +} + +fn testReader(data: []const u8, comptime expected: []const u8) !void { + var in_stream = io.fixedBufferStream(data); + + var gzip_stream = try gzipStream(testing.allocator, in_stream.reader()); + defer gzip_stream.deinit(); + + // Read and decompress the whole file + const buf = try gzip_stream.reader().readAllAlloc(testing.allocator, std.math.maxInt(usize)); + defer testing.allocator.free(buf); + // Calculate its SHA256 hash and check it against the reference + var hash: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(buf, hash[0..], .{}); + + assertEqual(expected, &hash); +} + +// Assert `expected` == `input` where `input` is a bytestring. +pub fn assertEqual(comptime expected: []const u8, input: []const u8) void { + var expected_bytes: [expected.len / 2]u8 = undefined; + for (expected_bytes) |*r, i| { + r.* = std.fmt.parseInt(u8, expected[2 * i .. 2 * i + 2], 16) catch unreachable; + } + + testing.expectEqualSlices(u8, &expected_bytes, input); +} + +// 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("rfc1952.txt.gz"), + "164ef0897b4cbec63abf1b57f069f3599bd0fb7c72c2a4dee21bd7e03ec9af67", + ); +} + +test "sanity checks" { + // Truncated header + testing.expectError( + error.EndOfStream, + testReader(&[_]u8{ 0x1f, 0x8B }, ""), + ); + // Wrong CM + testing.expectError( + error.InvalidCompression, + testReader(&[_]u8{ + 0x1f, 0x8b, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, + }, ""), + ); + // Wrong checksum + testing.expectError( + error.WrongChecksum, + testReader(&[_]u8{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + }, ""), + ); + // Truncated checksum + testing.expectError( + error.EndOfStream, + testReader(&[_]u8{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, + }, ""), + ); + // Wrong initial size + testing.expectError( + error.CorruptedData, + testReader(&[_]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 + testing.expectError( + error.EndOfStream, + testReader(&[_]u8{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, ""), + ); +} diff --git a/lib/std/compress/rfc1952.txt.gz b/lib/std/compress/rfc1952.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..be43b90a7917a993933c3266db50882f77a5662e GIT binary patch literal 8059 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 oc!TCNeMp0OdZ(9smFU literal 0 HcmV?d00001