feat(zig): native Arboricx bundle parser and C ABI
This commit is contained in:
479
ext/zig/src/bundle.zig
Normal file
479
ext/zig/src/bundle.zig
Normal file
@@ -0,0 +1,479 @@
|
||||
const std = @import("std");
|
||||
const tree = @import("tree.zig");
|
||||
const Arena = @import("arena.zig").Arena;
|
||||
|
||||
pub const Hash = [32]u8;
|
||||
|
||||
pub const Error = error{
|
||||
InvalidMagic,
|
||||
InvalidVersion,
|
||||
Truncated,
|
||||
InvalidManifest,
|
||||
InvalidNodePayload,
|
||||
HashMismatch,
|
||||
ExportNotFound,
|
||||
MissingChild,
|
||||
UnexpectedFormat,
|
||||
DigestMismatch,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
const Parser = struct {
|
||||
bytes: []const u8,
|
||||
pos: usize,
|
||||
|
||||
fn init(bytes: []const u8) Parser {
|
||||
return .{ .bytes = bytes, .pos = 0 };
|
||||
}
|
||||
|
||||
fn remaining(self: *const Parser) usize {
|
||||
return self.bytes.len - self.pos;
|
||||
}
|
||||
|
||||
fn expect(self: *Parser, n: usize) Error![]const u8 {
|
||||
if (self.remaining() < n) return error.Truncated;
|
||||
const result = self.bytes[self.pos .. self.pos + n];
|
||||
self.pos += n;
|
||||
return result;
|
||||
}
|
||||
|
||||
fn readU8(self: *Parser) Error!u8 {
|
||||
const b = try self.expect(1);
|
||||
return b[0];
|
||||
}
|
||||
|
||||
fn readU16(self: *Parser) Error!u16 {
|
||||
const b = try self.expect(2);
|
||||
return std.mem.readInt(u16, b[0..2], .big);
|
||||
}
|
||||
|
||||
fn readU32(self: *Parser) Error!u32 {
|
||||
const b = try self.expect(4);
|
||||
return std.mem.readInt(u32, b[0..4], .big);
|
||||
}
|
||||
|
||||
fn readU64(self: *Parser) Error!u64 {
|
||||
const b = try self.expect(8);
|
||||
return std.mem.readInt(u64, b[0..8], .big);
|
||||
}
|
||||
|
||||
fn readHash(self: *Parser) Error!Hash {
|
||||
const b = try self.expect(32);
|
||||
var h: Hash = undefined;
|
||||
@memcpy(&h, b);
|
||||
return h;
|
||||
}
|
||||
|
||||
fn readLengthPrefixedBytes(self: *Parser, allocator: std.mem.Allocator) Error![]const u8 {
|
||||
const len = try self.readU32();
|
||||
const bytes = try self.expect(len);
|
||||
const copy = try allocator.alloc(u8, bytes.len);
|
||||
@memcpy(copy, bytes);
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
const SectionEntry = struct {
|
||||
section_type: u32,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
digest: Hash,
|
||||
};
|
||||
|
||||
fn parseHeader(p: *Parser) Error!struct { major: u16, minor: u16, section_count: u32, dir_offset: u64 } {
|
||||
const magic = try p.expect(8);
|
||||
if (!std.mem.eql(u8, magic, "ARBORICX")) return error.InvalidMagic;
|
||||
|
||||
const major = try p.readU16();
|
||||
const minor = try p.readU16();
|
||||
const section_count = try p.readU32();
|
||||
_ = try p.readU64(); // flags
|
||||
const dir_offset = try p.readU64();
|
||||
|
||||
if (major != 1) return error.InvalidVersion;
|
||||
|
||||
return .{ .major = major, .minor = minor, .section_count = section_count, .dir_offset = dir_offset };
|
||||
}
|
||||
|
||||
fn parseSectionEntries(p: *Parser, count: u32, allocator: std.mem.Allocator) Error![]SectionEntry {
|
||||
const entries = try allocator.alloc(SectionEntry, count);
|
||||
errdefer allocator.free(entries);
|
||||
|
||||
for (entries) |*entry| {
|
||||
entry.section_type = try p.readU32();
|
||||
_ = try p.readU16(); // section_version
|
||||
_ = try p.readU16(); // section_flags
|
||||
const compression = try p.readU16();
|
||||
const digest_alg = try p.readU16();
|
||||
entry.offset = try p.readU64();
|
||||
entry.length = try p.readU64();
|
||||
entry.digest = try p.readHash();
|
||||
|
||||
if (compression != 0) return error.UnexpectedFormat;
|
||||
if (digest_alg != 1) return error.UnexpectedFormat;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
fn sha256Digest(data: []const u8) Hash {
|
||||
var h = std.crypto.hash.sha2.Sha256.init(.{});
|
||||
h.update(data);
|
||||
var out: Hash = undefined;
|
||||
h.final(&out);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { exports: []Export, roots: []Root } {
|
||||
const magic = try p.expect(8);
|
||||
if (!std.mem.eql(u8, magic, "ARBMNFST")) return error.InvalidManifest;
|
||||
|
||||
const major = try p.readU16();
|
||||
_ = try p.readU16(); // minor
|
||||
if (major != 1) return error.InvalidVersion;
|
||||
|
||||
const schema = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(schema);
|
||||
if (!std.mem.eql(u8, schema, "arboricx.bundle.manifest.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const bundle_type = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(bundle_type);
|
||||
if (!std.mem.eql(u8, bundle_type, "tree-calculus-executable-object")) return error.UnexpectedFormat;
|
||||
|
||||
const calc = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(calc);
|
||||
if (!std.mem.eql(u8, calc, "tree-calculus.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const hash_alg = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(hash_alg);
|
||||
if (!std.mem.eql(u8, hash_alg, "sha256")) return error.UnexpectedFormat;
|
||||
|
||||
const hash_domain = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(hash_domain);
|
||||
if (!std.mem.eql(u8, hash_domain, "arboricx.merkle.node.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const payload_type = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(payload_type);
|
||||
if (!std.mem.eql(u8, payload_type, "arboricx.merkle.payload.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const sem = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(sem);
|
||||
if (!std.mem.eql(u8, sem, "tree-calculus.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const eval_mode = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(eval_mode);
|
||||
if (!std.mem.eql(u8, eval_mode, "normal-order")) return error.UnexpectedFormat;
|
||||
|
||||
const abi = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(abi);
|
||||
if (!std.mem.eql(u8, abi, "arboricx.abi.tree.v1")) return error.UnexpectedFormat;
|
||||
|
||||
const cap_count = try p.readU32();
|
||||
var i: u32 = 0;
|
||||
while (i < cap_count) : (i += 1) {
|
||||
const cap = try p.readLengthPrefixedBytes(allocator);
|
||||
defer allocator.free(cap);
|
||||
if (cap.len != 0) return error.UnexpectedFormat;
|
||||
}
|
||||
|
||||
const closure = try p.readU8();
|
||||
if (closure != 0) return error.UnexpectedFormat;
|
||||
|
||||
const root_count = try p.readU32();
|
||||
const roots = try allocator.alloc(Root, root_count);
|
||||
errdefer allocator.free(roots);
|
||||
for (roots) |*r| {
|
||||
r.hash = try p.readHash();
|
||||
r.role = try p.readLengthPrefixedBytes(allocator);
|
||||
}
|
||||
|
||||
const export_count = try p.readU32();
|
||||
const exports = try allocator.alloc(Export, export_count);
|
||||
errdefer {
|
||||
for (exports) |*e| {
|
||||
allocator.free(e.name);
|
||||
allocator.free(e.kind);
|
||||
allocator.free(e.abi);
|
||||
}
|
||||
allocator.free(exports);
|
||||
}
|
||||
for (exports) |*e| {
|
||||
e.name = try p.readLengthPrefixedBytes(allocator);
|
||||
e.root = try p.readHash();
|
||||
e.kind = try p.readLengthPrefixedBytes(allocator);
|
||||
e.abi = try p.readLengthPrefixedBytes(allocator);
|
||||
if (!std.mem.eql(u8, e.abi, "arboricx.abi.tree.v1")) return error.UnexpectedFormat;
|
||||
}
|
||||
|
||||
const metadata_count = try p.readU32();
|
||||
var m: u32 = 0;
|
||||
while (m < metadata_count) : (m += 1) {
|
||||
_ = try p.readU16(); // tag
|
||||
const len = try p.readU32();
|
||||
_ = try p.expect(len);
|
||||
}
|
||||
|
||||
const ext_count = try p.readU32();
|
||||
var e_idx: u32 = 0;
|
||||
while (e_idx < ext_count) : (e_idx += 1) {
|
||||
_ = try p.readU16(); // tag
|
||||
const len = try p.readU32();
|
||||
_ = try p.expect(len);
|
||||
}
|
||||
|
||||
return .{ .exports = exports, .roots = roots };
|
||||
}
|
||||
|
||||
const Export = struct {
|
||||
name: []const u8,
|
||||
root: Hash,
|
||||
kind: []const u8,
|
||||
abi: []const u8,
|
||||
};
|
||||
|
||||
const Root = struct {
|
||||
hash: Hash,
|
||||
role: []const u8,
|
||||
};
|
||||
|
||||
fn parseNodeSection(p: *Parser, allocator: std.mem.Allocator) Error!std.AutoHashMap(Hash, []const u8) {
|
||||
const node_count = try p.readU64();
|
||||
var map = std.AutoHashMap(Hash, []const u8).init(allocator);
|
||||
errdefer map.deinit();
|
||||
|
||||
var i: u64 = 0;
|
||||
while (i < node_count) : (i += 1) {
|
||||
const hash = try p.readHash();
|
||||
const plen = try p.readU32();
|
||||
const payload = try p.expect(plen);
|
||||
|
||||
const expected_hash = blk: {
|
||||
var h = std.crypto.hash.sha2.Sha256.init(.{});
|
||||
h.update("arboricx.merkle.node.v1");
|
||||
h.update(&[_]u8{0});
|
||||
h.update(payload);
|
||||
var out: Hash = undefined;
|
||||
h.final(&out);
|
||||
break :blk out;
|
||||
};
|
||||
if (!std.mem.eql(u8, &hash, &expected_hash)) return error.HashMismatch;
|
||||
|
||||
try map.put(hash, payload);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
fn loadNode(
|
||||
arena: *Arena,
|
||||
payloads: std.AutoHashMap(Hash, []const u8),
|
||||
cache: *std.AutoHashMap(Hash, u32),
|
||||
root_hash: Hash,
|
||||
) Error!u32 {
|
||||
const Frame = struct {
|
||||
hash: Hash,
|
||||
state: u2,
|
||||
};
|
||||
|
||||
const max_stack = payloads.count() * 2;
|
||||
var stack = try arena.allocator.alloc(Frame, max_stack);
|
||||
defer arena.allocator.free(stack);
|
||||
var sp: usize = 0;
|
||||
|
||||
stack[sp] = .{ .hash = root_hash, .state = 0 };
|
||||
sp += 1;
|
||||
|
||||
while (sp > 0) {
|
||||
const frame = &stack[sp - 1];
|
||||
|
||||
if (cache.get(frame.hash)) |_| {
|
||||
sp -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.state == 0) {
|
||||
frame.state = 1;
|
||||
const payload = payloads.get(frame.hash) orelse return error.MissingChild;
|
||||
if (payload.len == 0) return error.InvalidNodePayload;
|
||||
|
||||
switch (payload[0]) {
|
||||
0x00 => {
|
||||
if (payload.len != 1) return error.InvalidNodePayload;
|
||||
},
|
||||
0x01 => {
|
||||
if (payload.len != 33) return error.InvalidNodePayload;
|
||||
var child_hash: Hash = undefined;
|
||||
@memcpy(&child_hash, payload[1..33]);
|
||||
if (cache.get(child_hash) == null) {
|
||||
stack[sp] = .{ .hash = child_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
},
|
||||
0x02 => {
|
||||
if (payload.len != 65) return error.InvalidNodePayload;
|
||||
var left_hash: Hash = undefined;
|
||||
var right_hash: Hash = undefined;
|
||||
@memcpy(&left_hash, payload[1..33]);
|
||||
@memcpy(&right_hash, payload[33..65]);
|
||||
const need_right = cache.get(right_hash) == null;
|
||||
const need_left = cache.get(left_hash) == null;
|
||||
if (need_right) {
|
||||
stack[sp] = .{ .hash = right_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
if (need_left) {
|
||||
stack[sp] = .{ .hash = left_hash, .state = 0 };
|
||||
sp += 1;
|
||||
}
|
||||
},
|
||||
else => return error.InvalidNodePayload,
|
||||
}
|
||||
} else {
|
||||
const payload = payloads.get(frame.hash).?;
|
||||
const idx: u32 = switch (payload[0]) {
|
||||
0x00 => try arena.alloc(.leaf),
|
||||
0x01 => blk: {
|
||||
var child_hash: Hash = undefined;
|
||||
@memcpy(&child_hash, payload[1..33]);
|
||||
const child_idx = cache.get(child_hash).?;
|
||||
break :blk try arena.alloc(.{ .stem = .{ .child = child_idx } });
|
||||
},
|
||||
0x02 => blk: {
|
||||
var left_hash: Hash = undefined;
|
||||
var right_hash: Hash = undefined;
|
||||
@memcpy(&left_hash, payload[1..33]);
|
||||
@memcpy(&right_hash, payload[33..65]);
|
||||
const left_idx = cache.get(left_hash).?;
|
||||
const right_idx = cache.get(right_hash).?;
|
||||
break :blk try arena.alloc(.{ .fork = .{ .left = left_idx, .right = right_idx } });
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
try cache.put(frame.hash, idx);
|
||||
sp -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return cache.get(root_hash) orelse return error.MissingChild;
|
||||
}
|
||||
|
||||
/// Parse an Arboricx bundle and load the named export into the arena.
|
||||
/// Returns the arena index of the exported term tree.
|
||||
pub fn loadBundleExport(
|
||||
arena: *Arena,
|
||||
bundle_bytes: []const u8,
|
||||
export_name: []const u8,
|
||||
) Error!u32 {
|
||||
var p = Parser.init(bundle_bytes);
|
||||
|
||||
const header = try parseHeader(&p);
|
||||
|
||||
p.pos = @intCast(header.dir_offset);
|
||||
const allocator = arena.allocator;
|
||||
const entries = try parseSectionEntries(&p, header.section_count, allocator);
|
||||
defer allocator.free(entries);
|
||||
|
||||
var manifest_entry: ?SectionEntry = null;
|
||||
var nodes_entry: ?SectionEntry = null;
|
||||
for (entries) |entry| {
|
||||
if (entry.section_type == 1) manifest_entry = entry;
|
||||
if (entry.section_type == 2) nodes_entry = entry;
|
||||
}
|
||||
const manifest_section = manifest_entry orelse return error.InvalidManifest;
|
||||
const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
|
||||
|
||||
const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
|
||||
|
||||
const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
|
||||
|
||||
var mp = Parser.init(manifest_bytes);
|
||||
const manifest = try parseManifest(&mp, allocator);
|
||||
defer {
|
||||
for (manifest.exports) |e| {
|
||||
allocator.free(e.name);
|
||||
allocator.free(e.kind);
|
||||
allocator.free(e.abi);
|
||||
}
|
||||
allocator.free(manifest.exports);
|
||||
for (manifest.roots) |r| {
|
||||
allocator.free(r.role);
|
||||
}
|
||||
allocator.free(manifest.roots);
|
||||
}
|
||||
|
||||
var export_hash: ?Hash = null;
|
||||
for (manifest.exports) |e| {
|
||||
if (std.mem.eql(u8, e.name, export_name)) {
|
||||
export_hash = e.root;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const root_hash = export_hash orelse return error.ExportNotFound;
|
||||
|
||||
var np = Parser.init(nodes_bytes);
|
||||
var payloads = try parseNodeSection(&np, allocator);
|
||||
defer payloads.deinit();
|
||||
|
||||
var cache = std.AutoHashMap(Hash, u32).init(allocator);
|
||||
defer cache.deinit();
|
||||
|
||||
return try loadNode(arena, payloads, &cache, root_hash);
|
||||
}
|
||||
|
||||
/// Parse an Arboricx bundle and load the default (first) root into the arena.
|
||||
pub fn loadBundleDefaultRoot(
|
||||
arena: *Arena,
|
||||
bundle_bytes: []const u8,
|
||||
) Error!u32 {
|
||||
var p = Parser.init(bundle_bytes);
|
||||
|
||||
const header = try parseHeader(&p);
|
||||
|
||||
p.pos = @intCast(header.dir_offset);
|
||||
const allocator = arena.allocator;
|
||||
const entries = try parseSectionEntries(&p, header.section_count, allocator);
|
||||
defer allocator.free(entries);
|
||||
|
||||
var manifest_entry: ?SectionEntry = null;
|
||||
var nodes_entry: ?SectionEntry = null;
|
||||
for (entries) |entry| {
|
||||
if (entry.section_type == 1) manifest_entry = entry;
|
||||
if (entry.section_type == 2) nodes_entry = entry;
|
||||
}
|
||||
const manifest_section = manifest_entry orelse return error.InvalidManifest;
|
||||
const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
|
||||
|
||||
const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
|
||||
|
||||
const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
|
||||
if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
|
||||
|
||||
var mp = Parser.init(manifest_bytes);
|
||||
const manifest = try parseManifest(&mp, allocator);
|
||||
defer {
|
||||
for (manifest.exports) |e| {
|
||||
allocator.free(e.name);
|
||||
allocator.free(e.kind);
|
||||
allocator.free(e.abi);
|
||||
}
|
||||
allocator.free(manifest.exports);
|
||||
for (manifest.roots) |r| {
|
||||
allocator.free(r.role);
|
||||
}
|
||||
allocator.free(manifest.roots);
|
||||
}
|
||||
|
||||
if (manifest.roots.len == 0) return error.ExportNotFound;
|
||||
const root_hash = manifest.roots[0].hash;
|
||||
|
||||
var np = Parser.init(nodes_bytes);
|
||||
var payloads = try parseNodeSection(&np, allocator);
|
||||
defer payloads.deinit();
|
||||
|
||||
var cache = std.AutoHashMap(Hash, u32).init(allocator);
|
||||
defer cache.deinit();
|
||||
|
||||
return try loadNode(arena, payloads, &cache, root_hash);
|
||||
}
|
||||
Reference in New Issue
Block a user