Skip to content
zit Artifact [461ab62e21]
Login

Artifact [461ab62e21]

Artifact 461ab62e218d4d215f4012afca4f9b94f9cf0ecef9fa71abf1c88e4ac8d18292:


//! The quarry attachment stub.
//!
//! quarry never attaches file bytes to a hosted repository. It attaches a
//! small text blob that says where the bytes are — a content hash, a size,
//! a mimetype, a name — so the A card stays a valid fossil artifact, the
//! store stays small, and nothing is a phantom on sync. This module only
//! RECOGNISES that shape: `attach list` and `zit artifact` report such a
//! blob as external instead of as its own hundred-odd bytes. A reader that
//! does not recognise it sees a small text file under the attachment's name,
//! which is the degraded-but-honest case. Nothing here writes one; quarry
//! writes it through `zit attach add - --name NAME --mimetype TYPE < stub`.
//!
//! ```
//! quarry-attachment 1
//! sha256 4f9a…e01c
//! size 18349022
//! type video/mp4
//! name screen-recording.mp4
//! ```
//!
//! One header line naming the scheme and version, then `key value` lines.
//! Unknown keys are ignored (forward compatibility); a missing required key
//! or a malformed value is not a stub.

const std = @import("std");
const repeat = @import("repeat.zig").repeat;

pub const header = "quarry-attachment 1\n";

/// A stub is a pointer, so it is small; anything larger that happens to
/// begin with the header is a text file, not a stub.
pub const max_len: usize = 4096;

pub const Stub = struct {
    /// 64 lowercase hex — the external bytes' SHA-256.
    sha256: []const u8,
    size: u64,
    /// May be empty when the stub carries no `type` line.
    mimetype: []const u8,
    name: []const u8,
};

/// Recognise `bytes` as a stub, borrowing its fields, or null.
pub fn parse(bytes: []const u8) ?Stub {
    if (bytes.len > max_len or !std.mem.startsWith(u8, bytes, header)) return null;
    var sha: ?[]const u8 = null;
    var size: ?u64 = null;
    var mimetype: []const u8 = "";
    var name: ?[]const u8 = null;
    var it = std.mem.splitScalar(u8, bytes[header.len..], '\n');
    while (it.next()) |line| {
        if (line.len == 0) continue;
        const sp = std.mem.indexOfScalar(u8, line, ' ') orelse return null;
        const key = line[0..sp];
        const value = line[sp + 1 ..];
        if (value.len == 0) return null;
        if (std.mem.eql(u8, key, "sha256")) {
            if (value.len != 64) return null;
            for (value) |c| if (!std.ascii.isHex(c) or std.ascii.isUpper(c)) return null;
            sha = value;
        } else if (std.mem.eql(u8, key, "size")) {
            size = std.fmt.parseInt(u64, value, 10) catch return null;
        } else if (std.mem.eql(u8, key, "type")) {
            mimetype = value;
        } else if (std.mem.eql(u8, key, "name")) {
            name = value;
        }
    }
    return .{
        .sha256 = sha orelse return null,
        .size = size orelse return null,
        .mimetype = mimetype,
        .name = name orelse return null,
    };
}

test "a well-formed stub parses; unknown keys are ignored" {
    const s = parse(header ++ "sha256 " ++ (repeat("4f", 32)) ++ "\nsize 18349022\ntype video/mp4\nname screen-recording.mp4\nfuture yes\n").?;
    try std.testing.expectEqualStrings(repeat("4f", 32), s.sha256);
    try std.testing.expectEqual(@as(u64, 18349022), s.size);
    try std.testing.expectEqualStrings("video/mp4", s.mimetype);
    try std.testing.expectEqualStrings("screen-recording.mp4", s.name);
}

test "not a stub: wrong header, missing key, bad hash, bad size, too large" {
    try std.testing.expect(parse("quarry-attachment 2\nsha256 " ++ (repeat("4f", 32)) ++ "\nsize 1\nname a\n") == null);
    try std.testing.expect(parse(header ++ "size 1\nname a\n") == null); // no sha256
    try std.testing.expect(parse(header ++ "sha256 abc\nsize 1\nname a\n") == null);
    try std.testing.expect(parse(header ++ "sha256 " ++ (repeat("4F", 32)) ++ "\nsize 1\nname a\n") == null); // uppercase
    try std.testing.expect(parse(header ++ "sha256 " ++ (repeat("4f", 32)) ++ "\nsize big\nname a\n") == null);
    try std.testing.expect(parse(header ++ "sha256 " ++ (repeat("4f", 32)) ++ "\nsize 1\n") == null); // no name
    try std.testing.expect(parse("PNG\x89 not text") == null);
    const big = header ++ "sha256 " ++ (repeat("4f", 32)) ++ "\nsize 1\nname a\n" ++ (repeat("x", max_len));
    try std.testing.expect(parse(big) == null);
}