| ... | ... | @@ -0,0 +1,248 @@ |
| 1 | // SPDX-License-Identifier: MIT |
| 2 | // Copyright (c) 2015-2020 Zig Contributors |
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. |
| 4 | // The MIT license requires this copyright notice to be included in all copies |
| 5 | // and substantial portions of the software. |
| 6 | // |
| 7 | // Decompressor for GZIP data streams (RFC1952) |
| 8 | |
| 9 | const std = @import("std"); |
| 10 | const io = std.io; |
| 11 | const fs = std.fs; |
| 12 | const testing = std.testing; |
| 13 | const mem = std.mem; |
| 14 | const deflate = std.compress.deflate; |
| 15 | |
| 16 | // Flags for the FLG field in the header |
| 17 | const FTEXT = 1 << 0; |
| 18 | const FHCRC = 1 << 1; |
| 19 | const FEXTRA = 1 << 2; |
| 20 | const FNAME = 1 << 3; |
| 21 | const FCOMMENT = 1 << 4; |
| 22 | |
| 23 | pub fn GzipStream(comptime ReaderType: type) type { |
| 24 | return struct { |
| 25 | const Self = @This(); |
| 26 | |
| 27 | pub const Error = ReaderType.Error || |
| 28 | deflate.InflateStream(ReaderType).Error || |
| 29 | error{ CorruptedData, WrongChecksum }; |
| 30 | pub const Reader = io.Reader(*Self, Error, read); |
| 31 | |
| 32 | allocator: *mem.Allocator, |
| 33 | inflater: deflate.InflateStream(ReaderType), |
| 34 | in_reader: ReaderType, |
| 35 | hasher: std.hash.Crc32, |
| 36 | window_slice: []u8, |
| 37 | read_amt: usize, |
| 38 | |
| 39 | info: struct { |
| 40 | filename: ?[]const u8, |
| 41 | comment: ?[]const u8, |
| 42 | modification_time: u32, |
| 43 | }, |
| 44 | |
| 45 | fn init(allocator: *mem.Allocator, source: ReaderType) !Self { |
| 46 | // gzip header format is specified in RFC1952 |
| 47 | const header = try source.readBytesNoEof(10); |
| 48 | |
| 49 | // Check the ID1/ID2 fields |
| 50 | if (header[0] != 0x1f or header[1] != 0x8b) |
| 51 | return error.BadHeader; |
| 52 | |
| 53 | const CM = header[2]; |
| 54 | // The CM field must be 8 to indicate the use of DEFLATE |
| 55 | if (CM != 8) return error.InvalidCompression; |
| 56 | // Flags |
| 57 | const FLG = header[3]; |
| 58 | // Modification time, as a Unix timestamp. |
| 59 | // If zero there's no timestamp available. |
| 60 | const MTIME = mem.readIntLittle(u32, header[4..8]); |
| 61 | // Extra flags |
| 62 | const XFL = header[8]; |
| 63 | // Operating system where the compression took place |
| 64 | const OS = header[9]; |
| 65 | |
| 66 | if (FLG & FEXTRA != 0) { |
| 67 | // Skip the extra data, we could read and expose it to the user |
| 68 | // if somebody needs it. |
| 69 | const len = try source.readIntLittle(u16); |
| 70 | try source.skipBytes(len, .{}); |
| 71 | } |
| 72 | |
| 73 | var filename: ?[]const u8 = null; |
| 74 | if (FLG & FNAME != 0) { |
| 75 | filename = try source.readUntilDelimiterAlloc( |
| 76 | allocator, |
| 77 | 0, |
| 78 | std.math.maxInt(usize), |
| 79 | ); |
| 80 | } |
| 81 | errdefer if (filename) |p| allocator.free(p); |
| 82 | |
| 83 | var comment: ?[]const u8 = null; |
| 84 | if (FLG & FCOMMENT != 0) { |
| 85 | comment = try source.readUntilDelimiterAlloc( |
| 86 | allocator, |
| 87 | 0, |
| 88 | std.math.maxInt(usize), |
| 89 | ); |
| 90 | } |
| 91 | errdefer if (comment) |p| allocator.free(p); |
| 92 | |
| 93 | if (FLG & FHCRC != 0) { |
| 94 | // TODO: Evaluate and check the header checksum. The stdlib has |
| 95 | // no CRC16 yet :( |
| 96 | _ = try source.readIntLittle(u16); |
| 97 | } |
| 98 | |
| 99 | // The RFC doesn't say anything about the DEFLATE window size to be |
| 100 | // used, default to 32K. |
| 101 | var window_slice = try allocator.alloc(u8, 32 * 1024); |
| 102 | |
| 103 | return Self{ |
| 104 | .allocator = allocator, |
| 105 | .inflater = deflate.inflateStream(source, window_slice), |
| 106 | .in_reader = source, |
| 107 | .hasher = std.hash.Crc32.init(), |
| 108 | .window_slice = window_slice, |
| 109 | .info = .{ |
| 110 | .filename = filename, |
| 111 | .comment = comment, |
| 112 | .modification_time = MTIME, |
| 113 | }, |
| 114 | .read_amt = 0, |
| 115 | }; |
| 116 | } |
| 117 | |
| 118 | pub fn deinit(self: *Self) void { |
| 119 | self.allocator.free(self.window_slice); |
| 120 | if (self.info.filename) |filename| |
| 121 | self.allocator.free(filename); |
| 122 | if (self.info.comment) |comment| |
| 123 | self.allocator.free(comment); |
| 124 | } |
| 125 | |
| 126 | // Implements the io.Reader interface |
| 127 | pub fn read(self: *Self, buffer: []u8) Error!usize { |
| 128 | if (buffer.len == 0) |
| 129 | return 0; |
| 130 | |
| 131 | // Read from the compressed stream and update the computed checksum |
| 132 | const r = try self.inflater.read(buffer); |
| 133 | if (r != 0) { |
| 134 | self.hasher.update(buffer[0..r]); |
| 135 | self.read_amt += r; |
| 136 | return r; |
| 137 | } |
| 138 | |
| 139 | // We've reached the end of stream, check if the checksum matches |
| 140 | const hash = try self.in_reader.readIntLittle(u32); |
| 141 | if (hash != self.hasher.final()) |
| 142 | return error.WrongChecksum; |
| 143 | |
| 144 | // The ISIZE field is the size of the uncompressed input modulo 2^32 |
| 145 | const input_size = try self.in_reader.readIntLittle(u32); |
| 146 | if (self.read_amt & 0xffffffff != input_size) |
| 147 | return error.CorruptedData; |
| 148 | |
| 149 | return 0; |
| 150 | } |
| 151 | |
| 152 | pub fn reader(self: *Self) Reader { |
| 153 | return .{ .context = self }; |
| 154 | } |
| 155 | }; |
| 156 | } |
| 157 | |
| 158 | pub fn gzipStream(allocator: *mem.Allocator, reader: anytype) !GzipStream(@TypeOf(reader)) { |
| 159 | return GzipStream(@TypeOf(reader)).init(allocator, reader); |
| 160 | } |
| 161 | |
| 162 | fn testReader(data: []const u8, comptime expected: []const u8) !void { |
| 163 | var in_stream = io.fixedBufferStream(data); |
| 164 | |
| 165 | var gzip_stream = try gzipStream(testing.allocator, in_stream.reader()); |
| 166 | defer gzip_stream.deinit(); |
| 167 | |
| 168 | // Read and decompress the whole file |
| 169 | const buf = try gzip_stream.reader().readAllAlloc(testing.allocator, std.math.maxInt(usize)); |
| 170 | defer testing.allocator.free(buf); |
| 171 | // Calculate its SHA256 hash and check it against the reference |
| 172 | var hash: [32]u8 = undefined; |
| 173 | std.crypto.hash.sha2.Sha256.hash(buf, hash[0..], .{}); |
| 174 | |
| 175 | assertEqual(expected, &hash); |
| 176 | } |
| 177 | |
| 178 | // Assert `expected` == `input` where `input` is a bytestring. |
| 179 | pub fn assertEqual(comptime expected: []const u8, input: []const u8) void { |
| 180 | var expected_bytes: [expected.len / 2]u8 = undefined; |
| 181 | for (expected_bytes) |*r, i| { |
| 182 | r.* = std.fmt.parseInt(u8, expected[2 * i .. 2 * i + 2], 16) catch unreachable; |
| 183 | } |
| 184 | |
| 185 | testing.expectEqualSlices(u8, &expected_bytes, input); |
| 186 | } |
| 187 | |
| 188 | // All the test cases are obtained by compressing the RFC1952 text |
| 189 | // |
| 190 | // https://tools.ietf.org/rfc/rfc1952.txt length=25037 bytes |
| 191 | // SHA256=164ef0897b4cbec63abf1b57f069f3599bd0fb7c72c2a4dee21bd7e03ec9af67 |
| 192 | test "compressed data" { |
| 193 | try testReader( |
| 194 | @embedFile("rfc1952.txt.gz"), |
| 195 | "164ef0897b4cbec63abf1b57f069f3599bd0fb7c72c2a4dee21bd7e03ec9af67", |
| 196 | ); |
| 197 | } |
| 198 | |
| 199 | test "sanity checks" { |
| 200 | // Truncated header |
| 201 | testing.expectError( |
| 202 | error.EndOfStream, |
| 203 | testReader(&[_]u8{ 0x1f, 0x8B }, ""), |
| 204 | ); |
| 205 | // Wrong CM |
| 206 | testing.expectError( |
| 207 | error.InvalidCompression, |
| 208 | testReader(&[_]u8{ |
| 209 | 0x1f, 0x8b, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 210 | 0x00, 0x03, |
| 211 | }, ""), |
| 212 | ); |
| 213 | // Wrong checksum |
| 214 | testing.expectError( |
| 215 | error.WrongChecksum, |
| 216 | testReader(&[_]u8{ |
| 217 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 218 | 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, |
| 219 | 0x00, 0x00, 0x00, 0x00, |
| 220 | }, ""), |
| 221 | ); |
| 222 | // Truncated checksum |
| 223 | testing.expectError( |
| 224 | error.EndOfStream, |
| 225 | testReader(&[_]u8{ |
| 226 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 227 | 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, |
| 228 | }, ""), |
| 229 | ); |
| 230 | // Wrong initial size |
| 231 | testing.expectError( |
| 232 | error.CorruptedData, |
| 233 | testReader(&[_]u8{ |
| 234 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 235 | 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 236 | 0x00, 0x00, 0x00, 0x01, |
| 237 | }, ""), |
| 238 | ); |
| 239 | // Truncated initial size field |
| 240 | testing.expectError( |
| 241 | error.EndOfStream, |
| 242 | testReader(&[_]u8{ |
| 243 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 244 | 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 245 | 0x00, 0x00, 0x00, |
| 246 | }, ""), |
| 247 | ); |
| 248 | } |