//! Public-surface unit tests for the §4.1 zlib container codec
//! (`container.zig`). The oracle is draft-fossil-sync-protocol-00 §4.1; every
//! expected value is transcribed from the spec text and its RFC1950 references,
//! never captured from a running Fossil.
const std = @import("std");
const container = @import("container.zig");
/// A generous decode capacity for the round-trip tests; the codec's `max_output`
/// bound is exercised directly by the "over max_output" test below. Sync passes
/// its `reply_body_cap` here in production; the codec itself is cap-agnostic.
const cap: usize = 1 << 24;
test "container §4.1 r18: the container is BE32 length then a zlib stream (worked example)" {
const encoded = try container.encode(std.testing.allocator, "abc", .default);
defer std.testing.allocator.free(encoded);
// BE32 uncompressed length (r18).
try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 0, 3 }, encoded[0..4]);
// RFC1950 §2.2 MUST: CM (low nibble of CMF) is 8 (deflate).
try std.testing.expectEqual(@as(u8, 8), encoded[4] & 0x0f);
// RFC1950 §2.2 MUST: (CMF*256 + FLG) % 31 == 0 (FCHECK).
const cmf_flg = @as(u16, encoded[4]) * 256 + @as(u16, encoded[5]);
try std.testing.expectEqual(@as(u16, 0), cmf_flg % 31);
// FDICT must be 0 — no preset dictionary.
try std.testing.expectEqual(@as(u8, 0), encoded[5] & 0x20);
// RFC1950 §9 Adler-32 of "abc": s1=1+97+98+99=295=0x0127,
// s2=0+98+196+295=589=0x024D, stored MSB-first as the trailer.
try std.testing.expectEqualSlices(u8, &[_]u8{ 0x02, 0x4D, 0x01, 0x27 }, encoded[encoded.len - 4 ..]);
}
test "container §4.1 r19: the ordinary single-input path uses the zlib default level" {
const encoded = try container.encode(std.testing.allocator, "hello fossil sync", .default);
defer std.testing.allocator.free(encoded);
const decoded = try container.decode(std.testing.allocator, encoded, cap);
defer std.testing.allocator.free(decoded);
try std.testing.expectEqualStrings("hello fossil sync", decoded);
}
test "container §4.1 r20: an inline-login prepend compresses the concatenation at level 9" {
const login = "login foo bar baz\n";
const body = "gimme 0123456789012345678901234567890123456789\n";
const combined = login ++ body;
const encoded = try container.encode(std.testing.allocator, combined, .best);
defer std.testing.allocator.free(encoded);
try std.testing.expectEqual(@as(u32, combined.len), std.mem.readInt(u32, encoded[0..4], .big));
const decoded = try container.decode(std.testing.allocator, encoded, cap);
defer std.testing.allocator.free(decoded);
try std.testing.expectEqualStrings(combined, decoded);
}
test "container §4.1 r21: decoding does not depend on the compression level" {
const input = "the quick brown fox jumps over the lazy dog";
const enc_default = try container.encode(std.testing.allocator, input, .default);
defer std.testing.allocator.free(enc_default);
const enc_best = try container.encode(std.testing.allocator, input, .best);
defer std.testing.allocator.free(enc_best);
const dec_default = try container.decode(std.testing.allocator, enc_default, cap);
defer std.testing.allocator.free(dec_default);
const dec_best = try container.decode(std.testing.allocator, enc_best, cap);
defer std.testing.allocator.free(dec_best);
try std.testing.expectEqualStrings(input, dec_default);
try std.testing.expectEqualStrings(input, dec_best);
}
test "container §4.1: the empty input round-trips" {
const encoded = try container.encode(std.testing.allocator, "", .default);
defer std.testing.allocator.free(encoded);
try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 0, 0 }, encoded[0..4]);
const decoded = try container.decode(std.testing.allocator, encoded, cap);
defer std.testing.allocator.free(decoded);
try std.testing.expectEqual(@as(usize, 0), decoded.len);
}
test "container §4.1: a truncated container header is rejected" {
try std.testing.expectError(error.TruncatedHeader, container.decode(std.testing.allocator, "\x00\x00", cap));
}
test "container §4.1: a decode that fits within declared capacity is accepted" {
// §4.1 L1200-1201, L1210-1211: the length field is an output-capacity
// value, not an exact length — a decode that succeeds within capacity is
// accepted. Declared 99, decoded 11 <= 99, so the shorter stream is
// accepted as-is and the actual bytes are returned.
const encoded = try container.encode(std.testing.allocator, "mismatch me", .default);
defer std.testing.allocator.free(encoded);
var lied: [64]u8 = undefined;
@memcpy(lied[0..encoded.len], encoded);
std.mem.writeInt(u32, lied[0..4], 99, .big); // declares 99 (capacity), stream carries 11
const decoded = try container.decode(std.testing.allocator, lied[0..encoded.len], cap);
defer std.testing.allocator.free(decoded);
try std.testing.expectEqualStrings("mismatch me", decoded);
}
test "container §4.1: output exceeding declared capacity is rejected" {
// §4.1 L1209-1210: reject when the decoder cannot fit its output within the
// declared capacity — declared 5, actual 11 > 5.
const encoded = try container.encode(std.testing.allocator, "mismatch me", .default);
defer std.testing.allocator.free(encoded);
var lied: [64]u8 = undefined;
@memcpy(lied[0..encoded.len], encoded);
std.mem.writeInt(u32, lied[0..4], 5, .big); // declares capacity 5, stream carries 11
try std.testing.expectError(error.OutputTooLarge, container.decode(std.testing.allocator, lied[0..encoded.len], cap));
}
test "container §4.1: a declared length over max_output is rejected" {
const encoded = try container.encode(std.testing.allocator, "capped", .default);
defer std.testing.allocator.free(encoded);
try std.testing.expectError(error.OutputTooLarge, container.decode(std.testing.allocator, encoded, 2));
}
test "container §4.1: garbage after the length prefix is rejected" {
const garbage = [_]u8{ 0, 0, 0, 5, 0xff, 0xff, 0xff, 0xff, 0xff };
try std.testing.expectError(error.InvalidStream, container.decode(std.testing.allocator, &garbage, cap));
}
fn encodeNoLeak(gpa: std.mem.Allocator) !void {
const out = try container.encode(gpa, "allocation failure probe", .default);
gpa.free(out);
}
test "container §4.1: encode survives allocation failure" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, encodeNoLeak, .{});
}
fn decodeNoLeak(gpa: std.mem.Allocator) !void {
const encoded = try container.encode(gpa, "allocation failure probe", .default);
defer gpa.free(encoded);
const out = try container.decode(gpa, encoded, cap);
gpa.free(out);
}
test "container §4.1: decode survives allocation failure" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, decodeNoLeak, .{});
}