Skip to content
zit Artifact [f50d210223]
Login

Artifact [f50d210223]

Artifact f50d210223a26e5ecce6237496ee132b107c7568900076576c15efbbfd124157:


//! 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 L905-909, 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, .{});
}