//! Delta Format — draft-fossil-delta-format-00.
//!
//! A delta encodes one byte sequence (the *target*) as instructions for
//! reconstructing it from another (the *source*): a header giving the target
//! byte-count, a stream of copy/literal segments, and a trailing checksum.
//!
//! This module implements the two primitives that the rest of the format is
//! built on — the base-64 integer codec (Section 3.1) and the checksum
//! (Section 3.4) — and stubs the apply/create procedures (Section 4).
const std = @import("std");
pub const Error = error{
/// The delta is malformed (truncated, bad opcode, bounds violation, ...).
/// The specific rejection conditions are enumerated in Section 4.
MalformedDelta,
/// Reconstructed output did not match the trailing checksum (Section 3.4).
ChecksumMismatch,
NotImplemented,
};
// ---------------------------------------------------------------------------
// Base-64 integers (Section 3.1)
//
// Integers are written most-significant digit first (Section 3.1). Internally
// digits are collected least-significant-first, then reversed on output.
// ---------------------------------------------------------------------------
pub const digit64 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
/// Reverse map: byte -> 0..63, or -1 if not a DIGIT64 character.
const value64: [256]i8 = blk: {
var t = [_]i8{-1} ** 256;
for (digit64, 0..) |c, i| t[c] = @intCast(i);
break :blk t;
};
/// Encode `v` MSB-first into `buf`, returning the written slice.
/// A 32-bit value needs at most 6 base-64 digits.
pub fn encodeInt(v: u32, buf: *[6]u8) []u8 {
if (v == 0) {
buf[0] = digit64[0];
return buf[0..1];
}
var tmp: [6]u8 = undefined;
var n: usize = 0;
var x = v;
while (x > 0) : (n += 1) {
tmp[n] = digit64[@intCast(x & 0x3f)];
x >>= 6;
}
var j: usize = 0;
while (j < n) : (j += 1) buf[j] = tmp[n - 1 - j];
return buf[0..n];
}
pub const Decoded = struct { value: u32, len: usize };
/// Decode a leading base-64 integer from `s`. Consumes DIGIT64 characters
/// until the first non-digit byte (or end of input). Per Section 3.1 a value
/// that reaches 2^32 is not interoperable and its handling is otherwise
/// implementation-defined; we take the strict posture and reject it rather than
/// let a 32-bit accumulator wrap silently.
pub fn decodeInt(s: []const u8) Error!Decoded {
var acc: u64 = 0;
var i: usize = 0;
while (i < s.len) : (i += 1) {
const d = value64[s[i]];
if (d < 0) break;
acc = (acc << 6) | @as(u64, @intCast(d));
if (acc > std.math.maxInt(u32)) return Error.MalformedDelta;
}
std.debug.assert(acc <= std.math.maxInt(u32));
return .{ .value = @intCast(acc), .len = i };
}
// ---------------------------------------------------------------------------
// Checksum (Section 3.4)
//
// Treat the target as big-endian 32-bit words, sum them modulo 2^32 (natural
// unsigned wraparound). A trailing run shorter than four bytes is added as
// z[0]<<24, z[1]<<16, z[2]<<8. NOTE: the modulus is 2^32, not 2^32-1 — the
// Fossil prose docs say otherwise and are in error (Anomalies).
// ---------------------------------------------------------------------------
pub fn checksum(data: []const u8) u32 {
var sum: u32 = 0;
var i: usize = 0;
while (i + 4 <= data.len) : (i += 4) {
const w = (@as(u32, data[i]) << 24) |
(@as(u32, data[i + 1]) << 16) |
(@as(u32, data[i + 2]) << 8) |
@as(u32, data[i + 3]);
sum +%= w;
}
const rem = data.len - i;
if (rem >= 1) sum +%= @as(u32, data[i]) << 24;
if (rem >= 2) sum +%= @as(u32, data[i + 1]) << 16;
if (rem >= 3) sum +%= @as(u32, data[i + 2]) << 8;
return sum;
}
// ---------------------------------------------------------------------------
// Apply / create (Section 4, Section 5) — TODO
// ---------------------------------------------------------------------------
/// Reconstruct the target by applying `delta` to `source`.
/// Caller owns the returned slice. See Section 4 for the procedure and the
/// exhaustive rejection conditions.
pub fn apply(allocator: std.mem.Allocator, source: []const u8, delta_bytes: []const u8) Error![]u8 {
_ = allocator;
_ = source;
_ = delta_bytes;
return Error.NotImplemented;
}
/// Produce a delta that reconstructs `target` from `source`.
pub fn create(allocator: std.mem.Allocator, source: []const u8, target: []const u8) Error![]u8 {
_ = allocator;
_ = source;
_ = target;
return Error.NotImplemented;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test "delta §3.1: base-64 integer round-trips (MSB-first)" {
var buf: [6]u8 = undefined;
for ([_]u32{ 0, 1, 63, 64, 4095, 100000, 0xFFFF_FFFF }) |v| {
const enc = encodeInt(v, &buf);
const dec = try decodeInt(enc);
try std.testing.expectEqual(v, dec.value);
try std.testing.expectEqual(enc.len, dec.len);
}
}
test "delta §3.1: decodeInt rejects an integer that reaches 2^32" {
// "400000" = 4*64^5 = 2^32: §3.1 lets a decoder reject >= 2^32, and the
// strict posture avoids the silent u32 wrap the naive accumulator would do.
try std.testing.expectError(Error.MalformedDelta, decodeInt("400000"));
// 2^32 - 1 is the largest in-range value and must still decode.
const max = try decodeInt("3~~~~~");
try std.testing.expectEqual(@as(u32, 0xFFFF_FFFF), max.value);
}
test "delta §3.1: encodeInt is most-significant-digit first" {
var buf: [6]u8 = undefined;
// 64 = 1*64 + 0 -> "10" (MSB first), not "01".
try std.testing.expectEqualStrings("10", encodeInt(64, &buf));
try std.testing.expectEqualStrings("0", encodeInt(0, &buf));
}
test "checksum: modulo 2^32, big-endian words + tail (Section 3.4)" {
try std.testing.expectEqual(@as(u32, 0), checksum(""));
try std.testing.expectEqual(@as(u32, 42), checksum(&[_]u8{ 0, 0, 0, 42 }));
try std.testing.expectEqual(@as(u32, 0x0100_0000), checksum(&[_]u8{1})); // 1-byte tail
try std.testing.expectEqual(@as(u32, 0x0000_2A00), checksum(&[_]u8{ 0, 0, 42 })); // 3-byte tail
}