Artifact 1cb0d210636000e572ee1f039181b501a4b6e88747700deb978ea07c3145b7f1:
- File
src/attachstub.zig
— part of check-in
[170a2e88d0]
at
2026-09-11 20:29:07
on branch trunk
— attach: add/rm/list verbs and the quarry stub (zit#186, zit#187)
The write side of the A card. add stores the blob then the attachment artifact in one commit and prints both hashes; rm ends the association with a sourceless card; list folds latest-per-(target, name). The new attachment-size-limit setting (config key 17) caps add, fossil's semantics with a 1 MiB default. A quarry attachment stub lists as external and zit artifact describes it. Smoke S15 pushes an attachment to real fossil and reads it back from fossil's attachment table. (user: dmestas, size: 4217) [annotate] [blame] [check-ins using]
//! zit#187 — 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"); 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 " ++ ("4f" ** 32) ++ "\nsize 18349022\ntype video/mp4\nname screen-recording.mp4\nfuture yes\n").?; try std.testing.expectEqualStrings("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 " ++ ("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 " ++ ("4F" ** 32) ++ "\nsize 1\nname a\n") == null); // uppercase try std.testing.expect(parse(header ++ "sha256 " ++ ("4f" ** 32) ++ "\nsize big\nname a\n") == null); try std.testing.expect(parse(header ++ "sha256 " ++ ("4f" ** 32) ++ "\nsize 1\n") == null); // no name try std.testing.expect(parse("PNG\x89 not text") == null); const big = header ++ "sha256 " ++ ("4f" ** 32) ++ "\nsize 1\nname a\n" ++ ("x" ** max_len); try std.testing.expect(parse(big) == null); }