513 lines
16 KiB
Zig
513 lines
16 KiB
Zig
// SPDX-License-Identifier: LCL-1.0
|
|
// Copyright (c) 2026 Markus Maiwald
|
|
// Stewardship: Self Sovereign Society Foundation
|
|
//
|
|
// This file is part of the Nexus Commonwealth.
|
|
// See legal/LICENSE_COMMONWEALTH.md for license terms.
|
|
|
|
//! Project LibWeb: LWF Adapter for Rumpk Kernel
|
|
//!
|
|
//! Freestanding LWF header parser for kernel-side routing decisions.
|
|
//! Zero-copy — operates directly on ION slab buffers.
|
|
//! Does NOT use std.mem.Allocator — all parsing is in-place.
|
|
//!
|
|
//! The full LWF codec (with allocation, encode, checksum) runs in
|
|
//! the Membrane (userland) where std is available. The kernel only
|
|
//! needs to parse the header to decide routing.
|
|
//!
|
|
//! Wire Format (after Ethernet header):
|
|
//! [Eth 14B][LWF Header 88B][Payload ...][LWF Trailer 36B]
|
|
//!
|
|
//! Integration Points:
|
|
//! - NetSwitch: EtherType 0x4C57 ("LW") → route to chan_lwf_rx
|
|
//! - Membrane: Full LWF codec via upstream lwf.zig (uses std)
|
|
|
|
// =========================================================
|
|
// LWF Constants (RFC-0000 v0.3.1)
|
|
// =========================================================
|
|
|
|
pub const ETHERTYPE_LWF: u16 = 0x4C57; // "LW" in ASCII — Sovereign EtherType
|
|
pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 };
|
|
pub const LWF_VERSION: u8 = 0x02;
|
|
pub const HEADER_SIZE: usize = 88;
|
|
pub const TRAILER_SIZE: usize = 36;
|
|
pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE; // 124 bytes
|
|
|
|
// =========================================================
|
|
// Frame Classes (RFC-0000 Section 4.2)
|
|
// =========================================================
|
|
|
|
pub const FrameClass = enum(u8) {
|
|
micro = 0x00, // 128 bytes total
|
|
mini = 0x01, // 512 bytes total
|
|
standard = 0x02, // 1350 bytes total
|
|
big = 0x03, // 4096 bytes total (exceeds ION slab!)
|
|
jumbo = 0x04, // 9000 bytes total (exceeds ION slab!)
|
|
_,
|
|
|
|
pub fn maxTotal(self: FrameClass) u16 {
|
|
return switch (self) {
|
|
.micro => 128,
|
|
.mini => 512,
|
|
.standard => 1350,
|
|
.big => 4096,
|
|
.jumbo => 9000,
|
|
_ => 0,
|
|
};
|
|
}
|
|
|
|
/// Check if frame class fits in an ION slab (2048 bytes)
|
|
pub fn fitsInSlab(self: FrameClass) bool {
|
|
return switch (self) {
|
|
.micro, .mini, .standard => true,
|
|
.big, .jumbo => false,
|
|
_ => false,
|
|
};
|
|
}
|
|
};
|
|
|
|
// =========================================================
|
|
// Service Types (RFC-0121)
|
|
// =========================================================
|
|
|
|
pub const ServiceType = struct {
|
|
pub const DATA_TRANSPORT: u16 = 0x0001;
|
|
pub const SLASH_PROTOCOL: u16 = 0x0002;
|
|
pub const IDENTITY_SIGNAL: u16 = 0x0003;
|
|
pub const ECONOMIC_SETTLEMENT: u16 = 0x0004;
|
|
pub const RELAY_FORWARD: u16 = 0x0005;
|
|
pub const STREAM_AUDIO: u16 = 0x0800;
|
|
pub const STREAM_VIDEO: u16 = 0x0801;
|
|
pub const STREAM_DATA: u16 = 0x0802;
|
|
pub const SWARM_MANIFEST: u16 = 0x0B00;
|
|
pub const SWARM_HAVE: u16 = 0x0B01;
|
|
pub const SWARM_REQUEST: u16 = 0x0B02;
|
|
pub const SWARM_BLOCK: u16 = 0x0B03;
|
|
};
|
|
|
|
// =========================================================
|
|
// LWF Flags (RFC-0000 Section 4.3)
|
|
// =========================================================
|
|
|
|
pub const Flags = struct {
|
|
pub const ENCRYPTED: u8 = 0x01;
|
|
pub const SIGNED: u8 = 0x02;
|
|
pub const RELAYABLE: u8 = 0x04;
|
|
pub const HAS_ENTROPY: u8 = 0x08;
|
|
pub const FRAGMENTED: u8 = 0x10;
|
|
pub const PRIORITY: u8 = 0x20;
|
|
};
|
|
|
|
// =========================================================
|
|
// LWF Header View (Zero-Copy over ION slab)
|
|
// =========================================================
|
|
|
|
/// Parsed header fields from a raw buffer. No allocation.
|
|
/// All multi-byte integers are stored big-endian on the wire.
|
|
pub const HeaderView = struct {
|
|
/// Pointer to the start of the LWF header in the ION slab
|
|
raw: [*]const u8,
|
|
|
|
// Pre-parsed routing fields (hot path)
|
|
service_type: u16,
|
|
payload_len: u16,
|
|
frame_class: FrameClass,
|
|
version: u8,
|
|
flags: u8,
|
|
sequence: u32,
|
|
timestamp: u64,
|
|
|
|
/// Parse header from raw bytes. Returns null if invalid.
|
|
/// Does NOT copy — references the original buffer.
|
|
pub fn parse(data: [*]const u8, data_len: u16) ?HeaderView {
|
|
if (data_len < HEADER_SIZE) return null;
|
|
|
|
// Fast reject: Magic check (4 bytes at offset 0)
|
|
if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0)
|
|
return null;
|
|
|
|
// Version check (offset 77)
|
|
const ver = data[77];
|
|
if (ver != LWF_VERSION) return null;
|
|
|
|
// Parse routing-critical fields
|
|
const service = readU16Big(data[72..74]);
|
|
const plen = readU16Big(data[74..76]);
|
|
const fclass = data[76];
|
|
const flg = data[78];
|
|
const seq = readU32Big(data[68..72]);
|
|
const ts = readU64Big(data[80..88]);
|
|
|
|
// Sanity: payload_len must fit in remaining buffer
|
|
const total_needed = HEADER_SIZE + @as(usize, plen) + TRAILER_SIZE;
|
|
if (total_needed > data_len) return null;
|
|
|
|
return HeaderView{
|
|
.raw = data,
|
|
.service_type = service,
|
|
.payload_len = plen,
|
|
.frame_class = @enumFromInt(fclass),
|
|
.version = ver,
|
|
.flags = flg,
|
|
.sequence = seq,
|
|
.timestamp = ts,
|
|
};
|
|
}
|
|
|
|
/// Get destination hint (24 bytes at offset 4)
|
|
pub fn destHint(self: *const HeaderView) *const [24]u8 {
|
|
return @ptrCast(self.raw[4..28]);
|
|
}
|
|
|
|
/// Get source hint (24 bytes at offset 28)
|
|
pub fn sourceHint(self: *const HeaderView) *const [24]u8 {
|
|
return @ptrCast(self.raw[28..52]);
|
|
}
|
|
|
|
/// Get session ID (16 bytes at offset 52)
|
|
pub fn sessionId(self: *const HeaderView) *const [16]u8 {
|
|
return @ptrCast(self.raw[52..68]);
|
|
}
|
|
|
|
/// Get pointer to payload data (starts at offset 88)
|
|
pub fn payloadPtr(self: *const HeaderView) [*]const u8 {
|
|
return self.raw + HEADER_SIZE;
|
|
}
|
|
|
|
/// Check if frame has PRIORITY flag
|
|
pub fn isPriority(self: *const HeaderView) bool {
|
|
return (self.flags & Flags.PRIORITY) != 0;
|
|
}
|
|
|
|
/// Check if frame is encrypted
|
|
pub fn isEncrypted(self: *const HeaderView) bool {
|
|
return (self.flags & Flags.ENCRYPTED) != 0;
|
|
}
|
|
|
|
/// Total frame size (header + payload + trailer)
|
|
pub fn totalSize(self: *const HeaderView) usize {
|
|
return HEADER_SIZE + @as(usize, self.payload_len) + TRAILER_SIZE;
|
|
}
|
|
};
|
|
|
|
// =========================================================
|
|
// Fast Path: Validation for NetSwitch
|
|
// =========================================================
|
|
|
|
/// Quick magic-byte check. Use before full parse for early rejection.
|
|
/// Expects data to point past the Ethernet header (14 bytes).
|
|
pub fn isLwfMagic(data: [*]const u8, len: u16) bool {
|
|
if (len < 4) return false;
|
|
return data[0] == 'L' and data[1] == 'W' and data[2] == 'F' and data[3] == 0;
|
|
}
|
|
|
|
/// Validate and parse an LWF frame from an ION slab.
|
|
/// Returns the parsed header view, or null if invalid.
|
|
/// The ION slab data should start at the LWF header (after Ethernet strip).
|
|
pub fn validateFrame(data: [*]const u8, len: u16) ?HeaderView {
|
|
return HeaderView.parse(data, len);
|
|
}
|
|
|
|
// =========================================================
|
|
// C ABI Exports (for Nim FFI)
|
|
// =========================================================
|
|
|
|
/// Check if a raw buffer contains a valid LWF frame.
|
|
/// Called from netswitch.nim to decide routing.
|
|
/// Returns 1 if valid LWF, 0 otherwise.
|
|
export fn lwf_validate(data: [*]const u8, len: u16) u8 {
|
|
if (HeaderView.parse(data, len)) |_| {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Get the service type from a validated LWF frame.
|
|
/// Returns 0 on invalid input.
|
|
export fn lwf_get_service_type(data: [*]const u8, len: u16) u16 {
|
|
if (HeaderView.parse(data, len)) |hdr| {
|
|
return hdr.service_type;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Get the payload length from a validated LWF frame.
|
|
export fn lwf_get_payload_len(data: [*]const u8, len: u16) u16 {
|
|
if (HeaderView.parse(data, len)) |hdr| {
|
|
return hdr.payload_len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Check if frame has PRIORITY flag set.
|
|
export fn lwf_is_priority(data: [*]const u8, len: u16) u8 {
|
|
if (HeaderView.parse(data, len)) |hdr| {
|
|
return if (hdr.isPriority()) 1 else 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// =========================================================
|
|
// Freestanding Integer Helpers (no std)
|
|
// =========================================================
|
|
|
|
inline fn readU16Big(bytes: *const [2]u8) u16 {
|
|
return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]);
|
|
}
|
|
|
|
inline fn readU32Big(bytes: *const [4]u8) u32 {
|
|
return (@as(u32, bytes[0]) << 24) |
|
|
(@as(u32, bytes[1]) << 16) |
|
|
(@as(u32, bytes[2]) << 8) |
|
|
@as(u32, bytes[3]);
|
|
}
|
|
|
|
inline fn readU64Big(bytes: *const [8]u8) u64 {
|
|
return (@as(u64, bytes[0]) << 56) |
|
|
(@as(u64, bytes[1]) << 48) |
|
|
(@as(u64, bytes[2]) << 40) |
|
|
(@as(u64, bytes[3]) << 32) |
|
|
(@as(u64, bytes[4]) << 24) |
|
|
(@as(u64, bytes[5]) << 16) |
|
|
(@as(u64, bytes[6]) << 8) |
|
|
@as(u64, bytes[7]);
|
|
}
|
|
|
|
inline fn writeU16Big(buf: *[2]u8, val: u16) void {
|
|
buf[0] = @truncate(val >> 8);
|
|
buf[1] = @truncate(val);
|
|
}
|
|
|
|
inline fn writeU32Big(buf: *[4]u8, val: u32) void {
|
|
buf[0] = @truncate(val >> 24);
|
|
buf[1] = @truncate(val >> 16);
|
|
buf[2] = @truncate(val >> 8);
|
|
buf[3] = @truncate(val);
|
|
}
|
|
|
|
inline fn writeU64Big(buf: *[8]u8, val: u64) void {
|
|
buf[0] = @truncate(val >> 56);
|
|
buf[1] = @truncate(val >> 48);
|
|
buf[2] = @truncate(val >> 40);
|
|
buf[3] = @truncate(val >> 32);
|
|
buf[4] = @truncate(val >> 24);
|
|
buf[5] = @truncate(val >> 16);
|
|
buf[6] = @truncate(val >> 8);
|
|
buf[7] = @truncate(val);
|
|
}
|
|
|
|
// =========================================================
|
|
// Test Frame Builder (for unit tests only)
|
|
// =========================================================
|
|
|
|
/// Build a minimal valid LWF frame in a buffer for testing.
|
|
/// Returns the total frame size written.
|
|
fn buildTestFrame(buf: []u8, payload: []const u8, service: u16, flags: u8) usize {
|
|
const total = HEADER_SIZE + payload.len + TRAILER_SIZE;
|
|
if (buf.len < total) return 0;
|
|
|
|
// Zero the buffer
|
|
for (buf[0..total]) |*b| b.* = 0;
|
|
|
|
// Magic
|
|
buf[0] = 'L';
|
|
buf[1] = 'W';
|
|
buf[2] = 'F';
|
|
buf[3] = 0;
|
|
|
|
// dest_hint (4..28) — leave zeros
|
|
// source_hint (28..52) — leave zeros
|
|
// session_id (52..68) — leave zeros
|
|
|
|
// Sequence (68..72)
|
|
writeU32Big(buf[68..72], 1);
|
|
|
|
// Service type (72..74)
|
|
writeU16Big(buf[72..74], service);
|
|
|
|
// Payload len (74..76)
|
|
writeU16Big(buf[74..76], @truncate(payload.len));
|
|
|
|
// Frame class (76)
|
|
buf[76] = @intFromEnum(FrameClass.standard);
|
|
|
|
// Version (77)
|
|
buf[77] = LWF_VERSION;
|
|
|
|
// Flags (78)
|
|
buf[78] = flags;
|
|
|
|
// entropy_difficulty (79) — 0
|
|
|
|
// Timestamp (80..88)
|
|
writeU64Big(buf[80..88], 0xDEADBEEF);
|
|
|
|
// Payload
|
|
for (payload, 0..) |byte, i| {
|
|
buf[HEADER_SIZE + i] = byte;
|
|
}
|
|
|
|
// Trailer (zeros = no signature, no checksum)
|
|
|
|
return total;
|
|
}
|
|
|
|
// =========================================================
|
|
// Tests
|
|
// =========================================================
|
|
|
|
const testing = @import("std").testing;
|
|
|
|
test "valid LWF frame parses correctly" {
|
|
var buf: [512]u8 = undefined;
|
|
const payload = "Hello LWF";
|
|
const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0);
|
|
try testing.expect(sz > 0);
|
|
|
|
const hdr = HeaderView.parse(&buf, @truncate(sz));
|
|
try testing.expect(hdr != null);
|
|
|
|
const h = hdr.?;
|
|
try testing.expectEqual(ServiceType.DATA_TRANSPORT, h.service_type);
|
|
try testing.expectEqual(@as(u16, 9), h.payload_len);
|
|
try testing.expectEqual(LWF_VERSION, h.version);
|
|
try testing.expectEqual(@as(u8, 0), h.flags);
|
|
try testing.expectEqual(@as(u32, 1), h.sequence);
|
|
try testing.expectEqual(@as(u64, 0xDEADBEEF), h.timestamp);
|
|
try testing.expectEqual(FrameClass.standard, h.frame_class);
|
|
}
|
|
|
|
test "invalid magic rejected" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Corrupt magic
|
|
buf[0] = 'X';
|
|
const hdr = HeaderView.parse(&buf, 160);
|
|
try testing.expect(hdr == null);
|
|
}
|
|
|
|
test "wrong version rejected" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Set wrong version
|
|
buf[77] = 0x01;
|
|
const hdr = HeaderView.parse(&buf, 160);
|
|
try testing.expect(hdr == null);
|
|
}
|
|
|
|
test "buffer too small rejected" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Pass length smaller than header
|
|
const hdr = HeaderView.parse(&buf, 80);
|
|
try testing.expect(hdr == null);
|
|
}
|
|
|
|
test "payload overflow rejected" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Claim huge payload that doesn't fit
|
|
writeU16Big(buf[74..76], 5000);
|
|
const hdr = HeaderView.parse(&buf, 160);
|
|
try testing.expect(hdr == null);
|
|
}
|
|
|
|
test "priority flag detection" {
|
|
var buf: [512]u8 = undefined;
|
|
const sz = buildTestFrame(&buf, "urgent", ServiceType.SLASH_PROTOCOL, Flags.PRIORITY);
|
|
|
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
|
try testing.expect(hdr.isPriority());
|
|
try testing.expect(!hdr.isEncrypted());
|
|
}
|
|
|
|
test "encrypted flag detection" {
|
|
var buf: [512]u8 = undefined;
|
|
const sz = buildTestFrame(&buf, "secret", ServiceType.IDENTITY_SIGNAL, Flags.ENCRYPTED | Flags.SIGNED);
|
|
|
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
|
try testing.expect(hdr.isEncrypted());
|
|
try testing.expect(!hdr.isPriority());
|
|
}
|
|
|
|
test "isLwfMagic fast path" {
|
|
var buf: [8]u8 = .{ 'L', 'W', 'F', 0, 0, 0, 0, 0 };
|
|
try testing.expect(isLwfMagic(&buf, 8));
|
|
|
|
buf[2] = 'X';
|
|
try testing.expect(!isLwfMagic(&buf, 8));
|
|
|
|
// Too short
|
|
try testing.expect(!isLwfMagic(&buf, 3));
|
|
}
|
|
|
|
test "C ABI lwf_validate matches HeaderView.parse" {
|
|
var buf: [512]u8 = undefined;
|
|
const sz = buildTestFrame(&buf, "abi_test", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
try testing.expectEqual(@as(u8, 1), lwf_validate(&buf, @truncate(sz)));
|
|
|
|
// Corrupt magic
|
|
buf[0] = 0;
|
|
try testing.expectEqual(@as(u8, 0), lwf_validate(&buf, @truncate(sz)));
|
|
}
|
|
|
|
test "C ABI lwf_get_service_type" {
|
|
var buf: [512]u8 = undefined;
|
|
const sz = buildTestFrame(&buf, "svc", ServiceType.ECONOMIC_SETTLEMENT, 0);
|
|
|
|
try testing.expectEqual(ServiceType.ECONOMIC_SETTLEMENT, lwf_get_service_type(&buf, @truncate(sz)));
|
|
}
|
|
|
|
test "frame class slab fit check" {
|
|
try testing.expect(FrameClass.micro.fitsInSlab());
|
|
try testing.expect(FrameClass.mini.fitsInSlab());
|
|
try testing.expect(FrameClass.standard.fitsInSlab());
|
|
try testing.expect(!FrameClass.big.fitsInSlab());
|
|
try testing.expect(!FrameClass.jumbo.fitsInSlab());
|
|
}
|
|
|
|
test "totalSize calculation" {
|
|
var buf: [512]u8 = undefined;
|
|
const payload = "12345";
|
|
const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
|
try testing.expectEqual(@as(usize, 88 + 5 + 36), hdr.totalSize());
|
|
}
|
|
|
|
test "dest and source hint accessors" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "hint", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Write a known dest hint at offset 4
|
|
buf[4] = 0xAA;
|
|
buf[27] = 0xBB;
|
|
// Write a known source hint at offset 28
|
|
buf[28] = 0xCC;
|
|
buf[51] = 0xDD;
|
|
|
|
const hdr = HeaderView.parse(&buf, 160).?;
|
|
try testing.expectEqual(@as(u8, 0xAA), hdr.destHint()[0]);
|
|
try testing.expectEqual(@as(u8, 0xBB), hdr.destHint()[23]);
|
|
try testing.expectEqual(@as(u8, 0xCC), hdr.sourceHint()[0]);
|
|
try testing.expectEqual(@as(u8, 0xDD), hdr.sourceHint()[23]);
|
|
}
|
|
|
|
test "session ID accessor" {
|
|
var buf: [512]u8 = undefined;
|
|
_ = buildTestFrame(&buf, "sess", ServiceType.DATA_TRANSPORT, 0);
|
|
|
|
// Write session ID at offset 52
|
|
buf[52] = 0x42;
|
|
buf[67] = 0x99;
|
|
|
|
const hdr = HeaderView.parse(&buf, 160).?;
|
|
try testing.expectEqual(@as(u8, 0x42), hdr.sessionId()[0]);
|
|
try testing.expectEqual(@as(u8, 0x99), hdr.sessionId()[15]);
|
|
}
|