Artifact 5addd5fdb51bdc09b4b3f6c155ed493f006849d3bad8566a3735601a1db614a0:
- File
src/delta.zig
— part of check-in
[0ae7fb2622]
at
2026-07-08 21:53:01
on branch trunk
— Scaffold zit: Zig project + RFC references + module layout
Fossil-in-Zig, ported from the four RFC drafts (docs/rfcs/). Layout mirrors the RFC layering: delta -> artifact -> repostate -> sync. delta int codec + checksum and artifact naming/Z-card implemented with RFC-derived test vectors; everything else stubbed against its section. Builds clean; zig build test green. See docs/HANDOFF.md. (user: dmestas, size: 5487) [annotate] [blame] [check-ins using]
//! 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). pub fn decodeInt(s: []const u8) Decoded { var v: u32 = 0; var i: usize = 0; while (i < s.len) : (i += 1) { const d = value64[s[i]]; if (d < 0) break; v = (v << 6) +% @as(u32, @intCast(d)); } return .{ .value = v, .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 "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 = decodeInt(enc); try std.testing.expectEqual(v, dec.value); try std.testing.expectEqual(enc.len, dec.len); } } test "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 }