Skip to content
zit Artifact [e8f3a6297d]
Login

Artifact [e8f3a6297d]

Artifact e8f3a6297d47549aea146222732ccf514c39688414f0a74ba396ac91066686f3:


//! 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 base-64 integer codec (Section 3.1) and
//! checksum (Section 3.4) that the rest of the format is built on, the
//! decoder (`apply`, Section 4), and a minimal conforming producer
//! (`create`, Section 5).

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,
};

/// Errors specific to the producer (Section 5). Kept separate from `Error`
/// (decoder rejections) so `apply`'s error set stays clean.
pub const CreateError = error{
    /// §3.1: every integer a conforming producer emits MUST be less than
    /// 2^32; a target this large cannot be encoded.
    TargetTooLarge,
};

// ---------------------------------------------------------------------------
// 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.
///
/// §6: bytes 0x80-0xFF are not in the alphabet and MUST terminate here, same
/// as any other non-digit byte. The lookup below indexes `value64` with the
/// raw byte and applies no high-bit mask, by design: masking would fold
/// 0x80-0xBF onto '0'-'?' and accept them as digits, the exact anti-pattern
/// §6 calls out.
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)
// ---------------------------------------------------------------------------

/// Parsed and validated delta header (§4 step 2).
const Header = struct {
    /// Declared length of the reconstructed target, in bytes.
    target_len: usize,
    /// Byte offset of the first segment, immediately past the header's LF.
    pos: usize,
};

/// Parse the header: a base-64 integer target length followed by a
/// mandatory 0x0a (§4 step 2).
fn parseHeader(delta_bytes: []const u8) Error!Header {
    const header = try decodeInt(delta_bytes);
    const p = header.len;
    if (p >= delta_bytes.len) return Error.MalformedDelta; // matrix #2: int ran to buffer end, no LF.
    if (delta_bytes[p] != '\n') return Error.MalformedDelta; // matrix #1: header not followed by 0x0a.
    return .{ .target_len = header.value, .pos = p + 1 }; // +1: past the header LF.
}

/// Read only the declared target byte-count from a delta (§4.1). Reads the
/// header integer and its terminating 0x0a and nothing more: no segment or
/// trailer byte is read, required, or validated, and no bytes are required
/// after that LF for the query to succeed. Returns MalformedDelta when the
/// buffer is empty or the header integer is not immediately followed by
/// 0x0a (§4.1's two rejection conditions; both funnel through parseHeader).
pub fn targetSize(delta_bytes: []const u8) Error!usize {
    const header = try parseHeader(delta_bytes);
    // Restates decodeInt's u32 cap, which parseHeader inherits; not a new guarantee.
    std.debug.assert(header.target_len <= std.math.maxInt(u32));
    return header.target_len;
}

/// Apply one `@`-copy segment starting at the opcode byte `q`. Returns the
/// delta position of the next segment. See §4 step 3, matrix entries #3-#5.
fn applyCopy(
    source: []const u8,
    delta_bytes: []const u8,
    q: usize,
    count: usize,
    out: []u8,
    out_pos: *usize,
    target_len: usize,
) Error!usize {
    const o = try decodeInt(delta_bytes[q + 1 ..]);
    const r = q + 1 + o.len;
    if (r >= delta_bytes.len) return Error.MalformedDelta; // matrix #2
    if (delta_bytes[r] != ',') return Error.MalformedDelta; // matrix #3: no comma.
    const offset: usize = o.value;
    // u64 widening: each operand is < 2^32 (decodeInt's cap), so the sum is
    // < 2^33 and cannot wrap — satisfies the "does not overflow for large
    // values" requirement (§4 step 3 `@`).
    if (@as(u64, offset) + @as(u64, count) > source.len) return Error.MalformedDelta; // matrix #5
    if (@as(u64, out_pos.*) + @as(u64, count) > target_len) return Error.MalformedDelta; // matrix #4
    std.debug.assert(offset + count <= source.len);
    std.debug.assert(out_pos.* + count <= target_len);
    @memcpy(out[out_pos.*..][0..count], source[offset..][0..count]);
    out_pos.* += count;
    return r + 1;
}

/// Apply one `:`-literal segment starting at the opcode byte `q`. Returns
/// the delta position of the next segment. See §4 step 3, matrix entries
/// #6-#7.
fn applyLiteral(
    delta_bytes: []const u8,
    q: usize,
    count: usize,
    out: []u8,
    out_pos: *usize,
    target_len: usize,
) Error!usize {
    const rem = delta_bytes.len - (q + 1);
    if (count > rem) return Error.MalformedDelta; // matrix #7: count exceeds remaining delta.
    if (@as(u64, out_pos.*) + @as(u64, count) > target_len) return Error.MalformedDelta; // matrix #6
    std.debug.assert(out_pos.* + count <= target_len);
    @memcpy(out[out_pos.*..][0..count], delta_bytes[q + 1 ..][0..count]);
    out_pos.* += count;
    return q + 1 + count;
}

/// Validate the `;` trailer: output must be exactly `target_len` bytes and
/// `count` (the trailer's integer) must equal the checksum of `out`. See §4
/// step 3, matrix entries #8-#9.
fn checkTrailer(out: []const u8, out_pos: usize, target_len: usize, count: usize) Error!void {
    if (out_pos != target_len) return Error.MalformedDelta; // matrix #9
    if (count != checksum(out)) return Error.ChecksumMismatch; // matrix #8
}

/// Reconstruct the target by applying `delta` to `source`.
/// Caller owns the returned slice; on any rejection the output buffer is
/// freed before the error returns, so a caller that discards the error per
/// Section 6 cannot leak or read a partial buffer. See Section 4 for the
/// procedure and the exhaustive rejection conditions (matrix entries 1-11).
/// `source` must be the complete, resident source bytes: this decoder has no
/// mechanism to request or await an absent source, so the caller MUST supply
/// it in full before calling (§4.2). A repository layer that does not yet
/// hold the source should not invoke this function at all.
pub fn apply(
    allocator: std.mem.Allocator,
    source: []const u8,
    delta_bytes: []const u8,
) (Error || std.mem.Allocator.Error)![]u8 {
    if (delta_bytes.len == 0) return Error.MalformedDelta; // §4 step 1: zero-length delta, rejected ahead of the numbered matrix (§6).

    const header = try parseHeader(delta_bytes);
    const target_len = header.target_len;
    var p: usize = header.pos;
    const out = try allocator.alloc(u8, target_len);
    errdefer allocator.free(out);
    var out_pos: usize = 0;

    // Segment loop (§4 step 3). Every branch that continues the loop
    // advances `p` by at least one byte, so `iters` cannot exceed the delta
    // length; the assert makes that bound explicit rather than assumed.
    var iters: usize = 0;
    while (p < delta_bytes.len) {
        iters += 1;
        std.debug.assert(iters <= delta_bytes.len);
        std.debug.assert(p <= delta_bytes.len);
        std.debug.assert(out_pos <= target_len);

        const c = try decodeInt(delta_bytes[p..]);
        const count: usize = c.value;
        const q = p + c.len;
        if (q >= delta_bytes.len) return Error.MalformedDelta; // matrix #2: count field ran off the end.

        switch (delta_bytes[q]) {
            '@' => p = try applyCopy(source, delta_bytes, q, count, out, &out_pos, target_len),
            ':' => p = try applyLiteral(delta_bytes, q, count, out, &out_pos, target_len),
            ';' => { // trailer: `count` is the checksum integer, not a byte count.
                try checkTrailer(out, out_pos, target_len, count);
                std.debug.assert(out_pos == target_len);
                std.debug.assert(out.len == target_len);
                return out; // ownership transfers; errdefer does not fire.
            },
            else => return Error.MalformedDelta, // matrix #10: opcode not in {@, :, ;}.
        }
    }
    return Error.MalformedDelta; // matrix #11: ran off the end without a trailer.
}

/// Source-block size for the encoder's hash index. §5 notes that for a source
/// of 16 bytes or fewer a copy can never beat the equivalent literal, so this is
/// both the index granularity and the minimum match length worth emitting.
const encoder_block = 16;

/// Emit a literal segment for `lit` (count, ':', payload), or nothing when `lit`
/// is empty (§3.3.2).
fn flushLiteral(allocator: std.mem.Allocator, out: *std.ArrayList(u8), lit: []const u8, ibuf: *[6]u8) std.mem.Allocator.Error!void {
    if (lit.len == 0) return;
    try out.appendSlice(allocator, encodeInt(@intCast(lit.len), ibuf));
    try out.append(allocator, ':');
    try out.appendSlice(allocator, lit);
}

/// Source offset of a byte-verified `encoder_block`-length match for
/// `target[t..]`, or null. Verifying the bytes (not just the hash) means a hash
/// collision costs only a missed match, never a wrong copy.
fn matchAt(source: []const u8, target: []const u8, t: usize, index: *std.AutoHashMap(u64, u32)) ?usize {
    if (t + encoder_block > target.len) return null;
    const h = std.hash.Wyhash.hash(0, target[t..][0..encoder_block]);
    const off: usize = index.get(h) orelse return null;
    if (!std.mem.eql(u8, source[off..][0..encoder_block], target[t..][0..encoder_block])) return null;
    return off;
}

/// Produce a delta that reconstructs `target` from `source` (Section 5, which is
/// informative: any copy/literal segmentation that reproduces the target is a
/// valid delta). This encoder indexes non-overlapping `encoder_block`-byte source
/// blocks by hash, greedily grows each byte-verified hash hit into the longest
/// copy, and emits the gaps as literals; the trailer carries the target checksum
/// (§3.4). A source of `encoder_block` bytes or fewer is emitted whole as one
/// literal (§5's last paragraph). Caller owns the returned slice.
pub fn create(
    allocator: std.mem.Allocator,
    source: []const u8,
    target: []const u8,
) (CreateError || std.mem.Allocator.Error)![]u8 {
    // Every integer the format carries (target length, count, offset, checksum)
    // must be < 2^32 (§3.1); refuse inputs whose sizes we could not represent.
    if (source.len > std.math.maxInt(u32) or target.len > std.math.maxInt(u32))
        return CreateError.TargetTooLarge;

    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(allocator);
    var ibuf: [6]u8 = undefined;

    // Header: target length then the mandatory LF (§3.2).
    try out.appendSlice(allocator, encodeInt(@intCast(target.len), &ibuf));
    try out.append(allocator, '\n');

    // Index non-overlapping source blocks by hash (latest offset per hash; the
    // byte-verification in matchAt tolerates the resulting overwrites). §5: skip
    // this for a source of encoder_block bytes or fewer — a literal is as good.
    var index = std.AutoHashMap(u64, u32).init(allocator);
    defer index.deinit();
    if (source.len > encoder_block) {
        var i: usize = 0;
        while (i + encoder_block <= source.len) : (i += encoder_block) {
            try index.put(std.hash.Wyhash.hash(0, source[i..][0..encoder_block]), @intCast(i));
        }
    }

    // Greedy scan: each verified run becomes a copy; the bytes between runs
    // accumulate as one pending literal, flushed before each copy and at the end.
    // Every iteration advances `t`, so the loop is bounded by target.len.
    var t: usize = 0;
    var lit_start: usize = 0;
    while (t < target.len) {
        if (matchAt(source, target, t, &index)) |o| {
            var len: usize = encoder_block;
            while (o + len < source.len and t + len < target.len and
                source[o + len] == target[t + len]) : (len += 1)
            {}
            std.debug.assert(o + len <= source.len);
            std.debug.assert(t + len <= target.len);
            try flushLiteral(allocator, &out, target[lit_start..t], &ibuf);
            try out.appendSlice(allocator, encodeInt(@intCast(len), &ibuf)); // count
            try out.append(allocator, '@');
            try out.appendSlice(allocator, encodeInt(@intCast(o), &ibuf)); // offset
            try out.append(allocator, ',');
            t += len;
            lit_start = t;
        } else {
            t += 1;
        }
    }
    try flushLiteral(allocator, &out, target[lit_start..], &ibuf);

    // Trailer: checksum of the target, then ';' (§3.4).
    try out.appendSlice(allocator, encodeInt(checksum(target), &ibuf));
    try out.append(allocator, ';');

    std.debug.assert(out.items.len >= 4); // minimum valid delta is "0\n0;"
    std.debug.assert(out.items[out.items.len - 1] == ';');
    return out.toOwnedSlice(allocator);
}