diff --git a/src/CircularBuffer.zig b/src/CircularBuffer.zig index dd97aec..4bf0c56 100644 --- a/src/CircularBuffer.zig +++ b/src/CircularBuffer.zig @@ -1,19 +1,22 @@ -const std = @import("std"); -const assert = std.debug.assert; -const testing = std.testing; - -/// Sliding window of decoded data. Or maybe better described as circular buffer. -/// Contains 64K bytes. Deflate limits: +/// 64K buffer of uncompressed data created in inflate (decompression). Has enough +/// history to support writing match; copying length of bytes +/// from the position distance backward from current. +/// +/// Reads can return less than available bytes if they are spread across +/// different circles. So reads should repeat until get required number of bytes +/// or until returned slice is zero length. +/// +/// Note on deflate limits: /// * non-compressible block is limited to 65,535 bytes. /// * backward pointer is limited in distance to 32K bytes and in length to 258 bytes. /// /// Whole non-compressed block can be written without overlap. We always have /// history of up to 64K, more then 32K needed. /// -/// Reads can return less than available bytes if they are spread across -/// different circles. So reads should repeat until get required number of bytes -/// or until returned slice is zero length. -/// +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + const mask = 0xffff; // 64K - 1 const buffer_len = mask + 1; // 64K buffer @@ -34,7 +37,7 @@ pub inline fn write(self: *Self, b: u8) void { self.wp += 1; } -// Write match (backreference to the same data slice) starting at `distance` +// Write match (back-reference to the same data slice) starting at `distance` // back from current write position, and `length` of bytes. pub fn writeMatch(self: *Self, length: u16, distance: u16) void { assert(self.wp - self.rp < mask); @@ -64,7 +67,7 @@ pub fn writeMatch(self: *Self, length: u16, distance: u16) void { } } -// Retruns writable part of the internal buffer of size `n` at most. Advanjces +// Returns writable part of the internal buffer of size `n` at most. Advances // write pointer, assumes that returned buffer will be filled with data. pub fn getWritable(self: *Self, n: usize) []u8 { const wp = self.wp & mask; @@ -113,6 +116,12 @@ pub inline fn free(self: *Self) usize { return buffer_len - (self.wp - self.rp); } +// Full if largest match can't fit. 258 is largest match length. That much bytes +// can be produced in single decode step. +pub inline fn full(self: *Self) bool { + return self.free() < 258 + 1; +} + // example from: https://youtu.be/SJPvNi4HrWQ?t=3558 test "CircularBuffer copy" { var sw: Self = .{}; diff --git a/src/SlidingWindow.zig b/src/SlidingWindow.zig index 48612a7..d8eec6d 100644 --- a/src/SlidingWindow.zig +++ b/src/SlidingWindow.zig @@ -1,3 +1,6 @@ +/// Used in deflate (compression), holds uncompressed data form which Tokens are +/// produces. In combination with Lookup it is used to find matches in history data. +/// const std = @import("std"); const consts = @import("consts.zig"); @@ -5,8 +8,6 @@ const expect = testing.expect; const assert = std.debug.assert; const testing = std.testing; -// Buffer of history data. - const hist_len = consts.history.len; const buffer_len = 2 * hist_len; const min_lookahead = consts.match.min_length + consts.match.max_length; @@ -17,7 +18,7 @@ const Self = @This(); buffer: [buffer_len]u8 = undefined, wp: usize = 0, // write position rp: usize = 0, // read position -fp: isize = 0, // flush position, tokens are build from fp..rp +fp: isize = 0, // last flush position, tokens are build from fp..rp // Returns number of bytes written, or 0 if buffer is full and need to slide. pub fn write(self: *Self, buf: []const u8) usize { @@ -30,7 +31,7 @@ pub fn write(self: *Self, buf: []const u8) usize { } // Slide buffer for hist_len. -// Drops old history, preserves bwtween hist_len and hist_len - min_lookahead. +// Drops old history, preserves between hist_len and hist_len - min_lookahead. // Returns number of bytes removed. pub fn slide(self: *Self) u16 { assert(self.rp >= max_rp and self.wp >= self.rp); @@ -42,34 +43,41 @@ pub fn slide(self: *Self) u16 { return @intCast(n); } -// flush - process all data from window -// If not flush preserve enough data for the loghest match. -// Returns null if there is not enough data. -pub fn activeLookahead(self: *Self, flush: bool) ?[]const u8 { - const min: usize = if (flush) 0 else min_lookahead; +// Data from the current position (read position). Those part of the buffer is +// not converted to tokens yet. +inline fn lookahead(self: *Self) []const u8 { + assert(self.wp >= self.rp); + return self.buffer[self.rp..self.wp]; +} + +// Returns part of the lookahead buffer. If should_flush is set no lookahead is +// preserved otherwise preserves enough data for the longest match. Returns +// null if there is not enough data. +pub fn activeLookahead(self: *Self, should_flush: bool) ?[]const u8 { + const min: usize = if (should_flush) 0 else min_lookahead; const lh = self.lookahead(); return if (lh.len > min) lh else null; } -pub inline fn lookahead(self: *Self) []const u8 { - assert(self.wp >= self.rp); - return self.buffer[self.rp..self.wp]; +// Advances read position, shrinks lookahead. +pub fn advance(self: *Self, n: u16) void { + assert(self.wp >= self.rp + n); + self.rp += n; } +// Returns writable part of the buffer, where new uncompressed data can be +// written. pub fn writable(self: *Self) []u8 { return self.buffer[self.wp..]; } +// Notification of what part of writable buffer is filled with data. pub fn written(self: *Self, n: usize) void { self.wp += n; } -pub fn advance(self: *Self, n: u16) void { - assert(self.wp >= self.rp + n); - self.rp += n; -} - // Finds match length between previous and current position. +// Used in hot path! pub fn match(self: *Self, prev_pos: u16, curr_pos: u16, min_len: u16) u16 { const max_len: usize = @min(self.wp - curr_pos, consts.match.max_length); // lookahead buffers from previous and current positions @@ -95,14 +103,19 @@ pub fn match(self: *Self, prev_pos: u16, curr_pos: u16, min_len: u16) u16 { return if (i >= consts.match.min_length) @intCast(i) else 0; } +// Current position of non-compressed data. Data before rp are already converted +// to tokens. pub fn pos(self: *Self) u16 { return @intCast(self.rp); } -pub fn flushed(self: *Self) void { +// Notification that token list is cleared. +pub fn flush(self: *Self) void { self.fp = @intCast(self.rp); } +// Part of the buffer since last flush or null if there was slide in between (so +// fp becomes negative). pub fn tokensBuffer(self: *Self) ?[]const u8 { assert(self.fp <= self.rp); if (self.fp < 0) return null; diff --git a/src/Token.zig b/src/Token.zig index 85be652..5b6b735 100644 --- a/src/Token.zig +++ b/src/Token.zig @@ -1,7 +1,115 @@ +/// Token cat be literal: single byte of data or match; reference to the slice of +/// data in the same stream represented with . Where length +/// can be 3 - 258 bytes, and distance 1 - 32768 bytes. +/// const std = @import("std"); const assert = std.debug.assert; +const print = std.debug.print; +const expect = std.testing.expect; const consts = @import("consts.zig").match; +const Token = @This(); + +pub const Kind = enum(u1) { + literal, + match, +}; + +// Distance range 1 - 32768, stored in dist as 0 - 32767 (fits u15) +dist: u15 = 0, +// Length range 3 - 258, stored in len_lit as 0 - 255 (fits u8) +len_lit: u8 = 0, +kind: Kind = .literal, + +pub fn literal(t: Token) u8 { + return t.len_lit; +} + +pub fn distance(t: Token) u16 { + return @as(u16, t.dist) + consts.min_distance; +} + +pub fn length(t: Token) u16 { + return @as(u16, t.len_lit) + consts.base_length; +} + +pub fn initLiteral(lit: u8) Token { + return .{ .kind = .literal, .len_lit = lit }; +} + +// distance range 1 - 32768, stored in dist as 0 - 32767 (u15) +// length range 3 - 258, stored in len_lit as 0 - 255 (u8) +pub fn initMatch(dist: u16, len: u16) Token { + assert(len >= consts.min_length and len <= consts.max_length); + assert(dist >= consts.min_distance and dist <= consts.max_distance); + return .{ + .kind = .match, + .dist = @intCast(dist - consts.min_distance), + .len_lit = @intCast(len - consts.base_length), + }; +} + +pub fn eql(t: Token, o: Token) bool { + return t.kind == o.kind and + t.dist == o.dist and + t.len_lit == o.len_lit; +} + +pub fn lengthCode(t: Token) u16 { + return match_lengths[match_lengths_index[t.len_lit]].code; +} + +pub fn lengthEncoding(t: Token) MatchLength { + var c = match_lengths[match_lengths_index[t.len_lit]]; + c.extra_length = t.len_lit - c.base_scaled; + return c; +} + +// Returns the distance code corresponding to a specific distance. +// Distance code is in range: 0 - 29. +pub fn distanceCode(t: Token) u8 { + var dist: u16 = t.dist; + if (dist < match_distances_index.len) { + return match_distances_index[dist]; + } + dist >>= 7; + if (dist < match_distances_index.len) { + return match_distances_index[dist] + 14; + } + dist >>= 7; + return match_distances_index[dist] + 28; +} + +pub fn distanceEncoding(t: Token) MatchDistance { + var c = match_distances[t.distanceCode()]; + c.extra_distance = t.dist - c.base_scaled; + return c; +} + +pub fn lengthExtraBits(code: u32) u8 { + return match_lengths[code - length_codes_start].extra_bits; +} + +pub fn matchLength(code: u8) MatchLength { + return match_lengths[code]; +} + +pub fn matchDistance(code: u8) MatchDistance { + return match_distances[code]; +} + +pub fn distanceExtraBits(code: u32) u8 { + return match_distances[code].extra_bits; +} + +pub fn show(t: Token) void { + if (t.kind == .literal) { + print("L('{c}'), ", .{t.literal()}); + } else { + print("M({d}, {d}), ", .{ t.distance(), t.length() }); + } +} + // Retruns index in match_lengths table for each length in range 0-255. const match_lengths_index = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, @@ -167,111 +275,6 @@ const match_distances = [_]MatchDistance{ .{ .extra_bits = 13, .base_scaled = 0x6000, .code = 29, .base = 24577 }, }; -const Token = @This(); - -pub const Kind = enum(u1) { - literal, - match, -}; - -// distance range 1 - 32768, stored in dist as 0 - 32767 (u15) -dist: u15 = 0, -// length range 3 - 258, stored in len_lit as 0 - 255 (u8) -len_lit: u8 = 0, -kind: Kind = .literal, - -pub fn literal(t: Token) u8 { - return t.len_lit; -} - -pub fn distance(t: Token) u16 { - return @as(u16, t.dist) + consts.min_distance; -} - -pub fn length(t: Token) u16 { - return @as(u16, t.len_lit) + consts.base_length; -} - -pub fn initLiteral(lit: u8) Token { - return .{ .kind = .literal, .len_lit = lit }; -} - -// distance range 1 - 32768, stored in dist as 0 - 32767 (u15) -// length range 3 - 258, stored in len_lit as 0 - 255 (u8) -pub fn initMatch(dist: u16, len: u16) Token { - assert(len >= consts.min_length and len <= consts.max_length); - assert(dist >= consts.min_distance and dist <= consts.max_distance); - return .{ - .kind = .match, - .dist = @intCast(dist - consts.min_distance), - .len_lit = @intCast(len - consts.base_length), - }; -} - -pub fn eql(t: Token, o: Token) bool { - return t.kind == o.kind and - t.dist == o.dist and - t.len_lit == o.len_lit; -} - -pub fn lengthCode(t: Token) u16 { - return match_lengths[match_lengths_index[t.len_lit]].code; -} - -pub fn lengthEncoding(t: Token) MatchLength { - var c = match_lengths[match_lengths_index[t.len_lit]]; - c.extra_length = t.len_lit - c.base_scaled; - return c; -} - -// Returns the distance code corresponding to a specific distance. -// Distance code is in range: 0 - 29. -pub fn distanceCode(t: Token) u8 { - var dist: u16 = t.dist; - if (dist < match_distances_index.len) { - return match_distances_index[dist]; - } - dist >>= 7; - if (dist < match_distances_index.len) { - return match_distances_index[dist] + 14; - } - dist >>= 7; - return match_distances_index[dist] + 28; -} - -pub fn distanceEncoding(t: Token) MatchDistance { - var c = match_distances[t.distanceCode()]; - c.extra_distance = t.dist - c.base_scaled; - return c; -} - -pub fn lengthExtraBits(code: u32) u8 { - return match_lengths[code - length_codes_start].extra_bits; -} - -pub fn matchLength(code: u8) MatchLength { - return match_lengths[code]; -} - -pub fn matchDistance(code: u8) MatchDistance { - return match_distances[code]; -} - -pub fn distanceExtraBits(code: u32) u8 { - return match_distances[code].extra_bits; -} - -pub fn show(t: Token) void { - if (t.kind == .literal) { - print("L('{c}'), ", .{t.literal()}); - } else { - print("M({d}, {d}), ", .{ t.distance(), t.length() }); - } -} - -const print = std.debug.print; -const expect = std.testing.expect; - test "Token size" { try expect(@sizeOf(Token) == 4); } diff --git a/src/bit_reader.zig b/src/bit_reader.zig index 09871ea..8a7a6d8 100644 --- a/src/bit_reader.zig +++ b/src/bit_reader.zig @@ -6,7 +6,15 @@ pub fn bitReader(reader: anytype) BitReader(@TypeOf(reader)) { return BitReader(@TypeOf(reader)).init(reader); } -/// Bit reader used during inflate. +/// Bit reader used during inflate (decompression). Has internal buffer of 64 +/// bits which shifts right after bits are consumed. Uses forward_reader to fill +/// that internal buffer when needed. +/// +/// readF is the core function. Supports few different ways of getting bits +/// controlled by flags. In hot path we try to avoid checking whether we need to +/// fill buffer from forward_reader by calling fill in advance and readF with +/// buffered flag set. +/// pub fn BitReader(comptime ReaderType: type) type { return struct { // Underlying reader used for filling internal bits buffer diff --git a/src/bit_writer.zig b/src/bit_writer.zig index 848b399..1fc29d1 100644 --- a/src/bit_writer.zig +++ b/src/bit_writer.zig @@ -1,6 +1,12 @@ const std = @import("std"); const assert = std.debug.assert; +/// Bit writer for use in deflate (compression). +/// +/// Has internal bits buffer of 64 bits and internal bytes buffer of 248 bytes. +/// When we accumulate 48 bits 6 bytes are moved to the bytes buffer. When we +/// accumulate 240 bytes they are flushed to the underlying inner_writer. +/// pub fn BitWriter(comptime WriterType: type) type { // buffer_flush_size indicates the buffer size // after which bytes are flushed to the writer. diff --git a/src/container.zig b/src/container.zig index 9cf3a75..def3082 100644 --- a/src/container.zig +++ b/src/container.zig @@ -1,5 +1,19 @@ const std = @import("std"); +/// Container of the deflate bit stream body. Container adds header before +/// deflate bit stream and footer after. It can bi gzip, zlib or raw (no header, +/// no footer, raw bit stream). +/// +/// Zlib format is defined in rfc 1950. Header has 2 bytes and footer 4 bytes +/// addler 32 checksum. +/// +/// Gzip format is defined in rfc 1952. Header has 10+ bytes and footer 4 bytes +/// crc32 checksum and 4 bytes of uncompressed data length. +/// +/// +/// rfc 1950: https://datatracker.ietf.org/doc/html/rfc1950#page-4 +/// rfc 1952: https://datatracker.ietf.org/doc/html/rfc1952#page-5 +/// pub const Container = enum { raw, // no header or footer gzip, // gzip header and footer diff --git a/src/inflate.zig b/src/inflate.zig index 2eb06af..95fecb8 100644 --- a/src/inflate.zig +++ b/src/inflate.zig @@ -60,12 +60,6 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { return .{ .bits = BitReaderType.init(rt) }; } - inline fn histFull(self: *Self) bool { - // 258 is largest match length. That much bytes can be produced in - // single decode step. - return self.hist.free() < 258 + 1; - } - fn blockHeader(self: *Self) !void { self.bfinal = try self.bits.read(u1); self.block_type = try self.bits.read(u2); @@ -86,7 +80,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { } fn fixedBlock(self: *Self) !bool { - while (!self.histFull()) { + while (!self.hist.full()) { const code = try self.bits.readFixedCode(); switch (code) { 0...255 => self.hist.write(@intCast(code)), @@ -160,8 +154,8 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { self.dst_h.build(&dst_l); } - // Decode code length symbol to code length. Writes decoded lendth into - // lens slice starting at position pos. Returns number of postitions + // Decode code length symbol to code length. Writes decoded length into + // lens slice starting at position pos. Returns number of positions // advanced. fn dynamicCodeLength(self: *Self, code: u16, lens: []u4, pos: usize) !usize { assert(code <= 18); @@ -188,8 +182,11 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { } } + // In larger archives most blocks are usually dynamic, so decompression + // performance depends on this function. fn dynamicBlock(self: *Self) !bool { - while (!self.histFull()) { + // Hot path loop! + while (!self.hist.full()) { try self.bits.fill(15); // optimization so other bit reads can be buffered (avoiding one `if` in hot path) const sym = try self.decodeSymbol(&self.lit_h);