170 lines
5.8 KiB
Zig
170 lines
5.8 KiB
Zig
const std = @import("std");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub const Codec = enum(u8) {
|
|
none = 0,
|
|
rle = 1,
|
|
};
|
|
|
|
const maxRun = 130;
|
|
const maxLiteral = 128;
|
|
|
|
pub fn rleCompress(allocator: Allocator, input: []const u8) ![]u8 {
|
|
var out = std.ArrayList(u8).init(allocator);
|
|
errdefer out.deinit();
|
|
|
|
var i: usize = 0;
|
|
while (i < input.len) {
|
|
var runLen: usize = 1;
|
|
while (i + runLen < input.len and input[i + runLen] == input[i] and runLen < maxRun) : (runLen += 1) {}
|
|
|
|
if (runLen >= 3) {
|
|
try out.append(0x80 | @as(u8, @intCast(runLen - 3)));
|
|
try out.append(input[i]);
|
|
i += runLen;
|
|
continue;
|
|
}
|
|
|
|
const litStart = i;
|
|
var litLen: usize = 0;
|
|
while (i < input.len and litLen < maxLiteral) {
|
|
var ahead: usize = 1;
|
|
while (i + ahead < input.len and input[i + ahead] == input[i] and ahead < 3) : (ahead += 1) {}
|
|
if (ahead >= 3) break;
|
|
i += 1;
|
|
litLen += 1;
|
|
}
|
|
try out.append(@as(u8, @intCast(litLen - 1)));
|
|
try out.appendSlice(input[litStart .. litStart + litLen]);
|
|
}
|
|
|
|
return out.toOwnedSlice();
|
|
}
|
|
|
|
pub fn rleDecompress(allocator: Allocator, input: []const u8, expectedLen: usize) ![]u8 {
|
|
var out = try allocator.alloc(u8, expectedLen);
|
|
errdefer allocator.free(out);
|
|
|
|
var written: usize = 0;
|
|
var i: usize = 0;
|
|
while (i < input.len) {
|
|
const control = input[i];
|
|
i += 1;
|
|
if (control & 0x80 != 0) {
|
|
const runLen: usize = @as(usize, control & 0x7f) + 3;
|
|
if (i >= input.len) return error.CorruptBlock;
|
|
const byte = input[i];
|
|
i += 1;
|
|
if (written + runLen > expectedLen) return error.CorruptBlock;
|
|
@memset(out[written .. written + runLen], byte);
|
|
written += runLen;
|
|
} else {
|
|
const litLen: usize = @as(usize, control) + 1;
|
|
if (i + litLen > input.len) return error.CorruptBlock;
|
|
if (written + litLen > expectedLen) return error.CorruptBlock;
|
|
@memcpy(out[written .. written + litLen], input[i .. i + litLen]);
|
|
written += litLen;
|
|
i += litLen;
|
|
}
|
|
}
|
|
if (written != expectedLen) return error.CorruptBlock;
|
|
return out;
|
|
}
|
|
|
|
pub const Encoded = struct {
|
|
codec: Codec,
|
|
bytes: []u8,
|
|
|
|
pub fn deinit(self: Encoded, allocator: Allocator) void {
|
|
allocator.free(self.bytes);
|
|
}
|
|
};
|
|
|
|
pub fn encode(allocator: Allocator, input: []const u8) !Encoded {
|
|
const compressed = try rleCompress(allocator, input);
|
|
if (compressed.len < input.len) {
|
|
return .{ .codec = .rle, .bytes = compressed };
|
|
}
|
|
allocator.free(compressed);
|
|
return .{ .codec = .none, .bytes = try allocator.dupe(u8, input) };
|
|
}
|
|
|
|
pub fn decode(allocator: Allocator, codec: Codec, payload: []const u8, originalLen: usize) ![]u8 {
|
|
return switch (codec) {
|
|
.none => allocator.dupe(u8, payload),
|
|
.rle => rleDecompress(allocator, payload, originalLen),
|
|
};
|
|
}
|
|
|
|
fn expectRoundtrip(allocator: Allocator, input: []const u8) !void {
|
|
const compressed = try rleCompress(allocator, input);
|
|
defer allocator.free(compressed);
|
|
const restored = try rleDecompress(allocator, compressed, input.len);
|
|
defer allocator.free(restored);
|
|
try std.testing.expectEqualSlices(u8, input, restored);
|
|
}
|
|
|
|
test "rle roundtrip on runs literals and mixed" {
|
|
const allocator = std.testing.allocator;
|
|
try expectRoundtrip(allocator, "");
|
|
try expectRoundtrip(allocator, "a");
|
|
try expectRoundtrip(allocator, "aaaaaaaaaa");
|
|
try expectRoundtrip(allocator, "abcdefghij");
|
|
try expectRoundtrip(allocator, "aaabbbcccdddeeefff");
|
|
try expectRoundtrip(allocator, "aaaaaXbbbbbYcccccZ");
|
|
try expectRoundtrip(allocator, &[_]u8{0} ** 300);
|
|
try expectRoundtrip(allocator, &[_]u8{0xff} ** 300);
|
|
}
|
|
|
|
test "rle roundtrip on random data" {
|
|
const allocator = std.testing.allocator;
|
|
var prng = std.Random.DefaultPrng.init(0x1234abcd);
|
|
const random = prng.random();
|
|
var buf: [4096]u8 = undefined;
|
|
var trial: usize = 0;
|
|
while (trial < 40) : (trial += 1) {
|
|
const len = random.intRangeAtMost(usize, 0, buf.len);
|
|
for (buf[0..len]) |*byte| byte.* = random.intRangeAtMost(u8, 0, 5);
|
|
try expectRoundtrip(allocator, buf[0..len]);
|
|
}
|
|
}
|
|
|
|
test "rle compresses repetitive payload" {
|
|
const allocator = std.testing.allocator;
|
|
const payload = "x" ** 1000;
|
|
const compressed = try rleCompress(allocator, payload);
|
|
defer allocator.free(compressed);
|
|
try std.testing.expect(compressed.len < payload.len / 10);
|
|
}
|
|
|
|
test "encode picks smaller representation" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const compressible = "y" ** 500;
|
|
const enc1 = try encode(allocator, compressible);
|
|
defer enc1.deinit(allocator);
|
|
try std.testing.expectEqual(Codec.rle, enc1.codec);
|
|
const back1 = try decode(allocator, enc1.codec, enc1.bytes, compressible.len);
|
|
defer allocator.free(back1);
|
|
try std.testing.expectEqualSlices(u8, compressible, back1);
|
|
|
|
var incompressible: [256]u8 = undefined;
|
|
var prng = std.Random.DefaultPrng.init(0xfeed);
|
|
prng.random().bytes(&incompressible);
|
|
const enc2 = try encode(allocator, &incompressible);
|
|
defer enc2.deinit(allocator);
|
|
try std.testing.expectEqual(Codec.none, enc2.codec);
|
|
const back2 = try decode(allocator, enc2.codec, enc2.bytes, incompressible.len);
|
|
defer allocator.free(back2);
|
|
try std.testing.expectEqualSlices(u8, &incompressible, back2);
|
|
}
|
|
|
|
test "decompress rejects truncated stream" {
|
|
const allocator = std.testing.allocator;
|
|
const payload = "z" ** 200;
|
|
const compressed = try rleCompress(allocator, payload);
|
|
defer allocator.free(compressed);
|
|
try std.testing.expectError(error.CorruptBlock, rleDecompress(allocator, compressed[0 .. compressed.len - 1], payload.len));
|
|
}
|