363 lines
13 KiB
Zig
363 lines
13 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_SOVEREIGN.md for license terms.
|
|
|
|
//! Rumpk Layer 0: LittleFS ↔ VirtIO-Block HAL
|
|
//!
|
|
//! Translates LittleFS block operations into VirtIO-Block sector I/O.
|
|
//! Exports C-ABI functions for Nim L1 to call: nexus_lfs_mount, nexus_lfs_format,
|
|
//! nexus_lfs_open, nexus_lfs_read, nexus_lfs_write, nexus_lfs_close, etc.
|
|
//!
|
|
//! Block geometry:
|
|
//! - LFS block size: 4096 bytes (8 sectors)
|
|
//! - Sector size: 512 bytes (VirtIO standard)
|
|
//! - 32MB disk: 8192 blocks
|
|
|
|
const BLOCK_SIZE: u32 = 4096;
|
|
const SECTOR_SIZE: u32 = 512;
|
|
const SECTORS_PER_BLOCK: u32 = BLOCK_SIZE / SECTOR_SIZE;
|
|
const BLOCK_COUNT: u32 = 8192; // 32MB / 4096
|
|
const CACHE_SIZE: u32 = 512;
|
|
const LOOKAHEAD_SIZE: u32 = 64;
|
|
|
|
// --- VirtIO-Block FFI (from virtio_block.zig) ---
|
|
extern fn virtio_blk_read(sector: u64, buf: [*]u8) void;
|
|
extern fn virtio_blk_write(sector: u64, buf: [*]const u8) void;
|
|
|
|
// --- Kernel print (from Nim L1 kernel.nim, exported as C ABI) ---
|
|
extern fn kprint(s: [*:0]const u8) void;
|
|
|
|
// --- LittleFS C types (must match lfs.h layout exactly) ---
|
|
// We use opaque pointers and only declare what we need for the config struct.
|
|
|
|
const LfsConfig = extern struct {
|
|
context: ?*anyopaque,
|
|
read: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32,
|
|
prog: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32,
|
|
erase: *const fn (*LfsConfig, u32) callconv(.c) i32,
|
|
sync: *const fn (*LfsConfig) callconv(.c) i32,
|
|
read_size: u32,
|
|
prog_size: u32,
|
|
block_size: u32,
|
|
block_count: u32,
|
|
block_cycles: i32,
|
|
cache_size: u32,
|
|
lookahead_size: u32,
|
|
compact_thresh: u32,
|
|
read_buffer: ?*anyopaque,
|
|
prog_buffer: ?*anyopaque,
|
|
lookahead_buffer: ?*anyopaque,
|
|
name_max: u32,
|
|
file_max: u32,
|
|
attr_max: u32,
|
|
metadata_max: u32,
|
|
inline_max: u32,
|
|
};
|
|
|
|
// Opaque LittleFS types — we let lfs.c manage the internals
|
|
const LfsT = opaque {};
|
|
const LfsFileT = opaque {};
|
|
const LfsInfo = opaque {};
|
|
|
|
// --- LittleFS C API (linked from lfs.o) ---
|
|
extern fn lfs_format(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32;
|
|
extern fn lfs_mount(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32;
|
|
extern fn lfs_unmount(lfs: *LfsT) callconv(.c) i32;
|
|
extern fn lfs_file_open(lfs: *LfsT, file: *LfsFileT, path: [*:0]const u8, flags: i32) callconv(.c) i32;
|
|
extern fn lfs_file_close(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
|
extern fn lfs_file_read(lfs: *LfsT, file: *LfsFileT, buf: [*]u8, size: u32) callconv(.c) i32;
|
|
extern fn lfs_file_write(lfs: *LfsT, file: *LfsFileT, buf: [*]const u8, size: u32) callconv(.c) i32;
|
|
extern fn lfs_file_sync(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
|
extern fn lfs_file_seek(lfs: *LfsT, file: *LfsFileT, off: i32, whence: i32) callconv(.c) i32;
|
|
extern fn lfs_file_size(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
|
extern fn lfs_remove(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32;
|
|
extern fn lfs_mkdir(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32;
|
|
extern fn lfs_stat(lfs: *LfsT, path: [*:0]const u8, info: *LfsInfo) callconv(.c) i32;
|
|
|
|
// --- Static state ---
|
|
// LittleFS requires ~800 bytes for lfs_t. We over-allocate to be safe.
|
|
var lfs_state: [2048]u8 align(8) = [_]u8{0} ** 2048;
|
|
var lfs_mounted: bool = false;
|
|
|
|
// Static buffers to avoid malloc for cache/lookahead
|
|
var read_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE;
|
|
var prog_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE;
|
|
var lookahead_buf: [LOOKAHEAD_SIZE]u8 = [_]u8{0} ** LOOKAHEAD_SIZE;
|
|
|
|
// File handles: pre-allocated pool (LittleFS lfs_file_t is ~100 bytes, over-allocate)
|
|
const MAX_LFS_FILES = 8;
|
|
var file_slots: [MAX_LFS_FILES][512]u8 align(8) = [_][512]u8{[_]u8{0} ** 512} ** MAX_LFS_FILES;
|
|
var file_active: [MAX_LFS_FILES]bool = [_]bool{false} ** MAX_LFS_FILES;
|
|
|
|
var cfg: LfsConfig = .{
|
|
.context = null,
|
|
.read = &lfsRead,
|
|
.prog = &lfsProg,
|
|
.erase = &lfsErase,
|
|
.sync = &lfsSync,
|
|
.read_size = SECTOR_SIZE,
|
|
.prog_size = SECTOR_SIZE,
|
|
.block_size = BLOCK_SIZE,
|
|
.block_count = BLOCK_COUNT,
|
|
.block_cycles = 500,
|
|
.cache_size = CACHE_SIZE,
|
|
.lookahead_size = LOOKAHEAD_SIZE,
|
|
.compact_thresh = 0,
|
|
.read_buffer = &read_cache,
|
|
.prog_buffer = &prog_cache,
|
|
.lookahead_buffer = &lookahead_buf,
|
|
.name_max = 0,
|
|
.file_max = 0,
|
|
.attr_max = 0,
|
|
.metadata_max = 0,
|
|
.inline_max = 0,
|
|
};
|
|
|
|
// =========================================================
|
|
// LittleFS Config Callbacks
|
|
// =========================================================
|
|
|
|
/// Read a region from a block via VirtIO-Block.
|
|
fn lfsRead(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 {
|
|
const buf: [*]u8 = @ptrCast(@alignCast(buffer orelse return -5));
|
|
const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE;
|
|
const sector_offset = off % SECTOR_SIZE;
|
|
|
|
if (sector_offset == 0 and size % SECTOR_SIZE == 0) {
|
|
// Aligned: direct sector reads
|
|
var i: u32 = 0;
|
|
while (i < size / SECTOR_SIZE) : (i += 1) {
|
|
virtio_blk_read(base_sector + i, buf + i * SECTOR_SIZE);
|
|
}
|
|
} else {
|
|
// Unaligned: bounce buffer
|
|
var tmp: [SECTOR_SIZE]u8 = undefined;
|
|
var remaining: u32 = size;
|
|
var buf_off: u32 = 0;
|
|
var cur_off: u32 = off;
|
|
|
|
while (remaining > 0) {
|
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE;
|
|
const sec_off = cur_off % SECTOR_SIZE;
|
|
virtio_blk_read(sec, &tmp);
|
|
|
|
const avail = SECTOR_SIZE - sec_off;
|
|
const chunk = if (remaining < avail) remaining else avail;
|
|
for (0..chunk) |j| {
|
|
buf[buf_off + @as(u32, @intCast(j))] = tmp[sec_off + @as(u32, @intCast(j))];
|
|
}
|
|
buf_off += chunk;
|
|
cur_off += chunk;
|
|
remaining -= chunk;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Program (write) a region in a block via VirtIO-Block.
|
|
fn lfsProg(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 {
|
|
const buf: [*]const u8 = @ptrCast(@alignCast(buffer orelse return -5));
|
|
const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE;
|
|
const sector_offset = off % SECTOR_SIZE;
|
|
|
|
if (sector_offset == 0 and size % SECTOR_SIZE == 0) {
|
|
// Aligned: direct sector writes
|
|
var i: u32 = 0;
|
|
while (i < size / SECTOR_SIZE) : (i += 1) {
|
|
virtio_blk_write(base_sector + i, buf + i * SECTOR_SIZE);
|
|
}
|
|
} else {
|
|
// Unaligned: read-modify-write via bounce buffer
|
|
var tmp: [SECTOR_SIZE]u8 = undefined;
|
|
var remaining: u32 = size;
|
|
var buf_off: u32 = 0;
|
|
var cur_off: u32 = off;
|
|
|
|
while (remaining > 0) {
|
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE;
|
|
const sec_off = cur_off % SECTOR_SIZE;
|
|
|
|
// Read existing sector if partial write
|
|
if (sec_off != 0 or remaining < SECTOR_SIZE) {
|
|
virtio_blk_read(sec, &tmp);
|
|
}
|
|
|
|
const avail = SECTOR_SIZE - sec_off;
|
|
const chunk = if (remaining < avail) remaining else avail;
|
|
for (0..chunk) |j| {
|
|
tmp[sec_off + @as(u32, @intCast(j))] = buf[buf_off + @as(u32, @intCast(j))];
|
|
}
|
|
virtio_blk_write(sec, &tmp);
|
|
buf_off += chunk;
|
|
cur_off += chunk;
|
|
remaining -= chunk;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Erase a block. VirtIO-Block has no erase concept, so we zero-fill.
|
|
fn lfsErase(_: *LfsConfig, block: u32) callconv(.c) i32 {
|
|
const zeros = [_]u8{0xFF} ** SECTOR_SIZE; // LFS expects 0xFF after erase
|
|
var i: u32 = 0;
|
|
while (i < SECTORS_PER_BLOCK) : (i += 1) {
|
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + i;
|
|
virtio_blk_write(sec, &zeros);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Sync — VirtIO-Block is synchronous, nothing to flush.
|
|
fn lfsSync(_: *LfsConfig) callconv(.c) i32 {
|
|
return 0;
|
|
}
|
|
|
|
// =========================================================
|
|
// Exported C-ABI for Nim L1
|
|
// =========================================================
|
|
|
|
/// Format the block device with LittleFS.
|
|
export fn nexus_lfs_format() i32 {
|
|
kprint("[LFS] Formatting sovereign filesystem...\n");
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const rc = lfs_format(lfs_ptr, &cfg);
|
|
if (rc == 0) {
|
|
kprint("[LFS] Format OK\n");
|
|
} else {
|
|
kprint("[LFS] Format FAILED\n");
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/// Mount the LittleFS filesystem. Auto-formats if mount fails (first boot).
|
|
export fn nexus_lfs_mount() i32 {
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
var rc = lfs_mount(lfs_ptr, &cfg);
|
|
if (rc != 0) {
|
|
// First boot or corrupt — format and retry
|
|
kprint("[LFS] Mount failed, formatting (first boot)...\n");
|
|
rc = lfs_format(lfs_ptr, &cfg);
|
|
if (rc != 0) {
|
|
kprint("[LFS] Format FAILED\n");
|
|
return rc;
|
|
}
|
|
rc = lfs_mount(lfs_ptr, &cfg);
|
|
}
|
|
if (rc == 0) {
|
|
lfs_mounted = true;
|
|
kprint("[LFS] Sovereign filesystem mounted on /nexus\n");
|
|
} else {
|
|
kprint("[LFS] Mount FAILED after format\n");
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/// Unmount the filesystem.
|
|
export fn nexus_lfs_unmount() i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const rc = lfs_unmount(lfs_ptr);
|
|
lfs_mounted = false;
|
|
return rc;
|
|
}
|
|
|
|
/// Open a file. Returns a file handle index (0..MAX_LFS_FILES-1) or -1 on error.
|
|
/// flags: 1=RDONLY, 2=WRONLY, 3=RDWR, 0x0100=CREAT, 0x0400=TRUNC, 0x0800=APPEND
|
|
export fn nexus_lfs_open(path: [*:0]const u8, flags: i32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
// Find free slot
|
|
var slot: usize = 0;
|
|
while (slot < MAX_LFS_FILES) : (slot += 1) {
|
|
if (!file_active[slot]) break;
|
|
}
|
|
if (slot >= MAX_LFS_FILES) return -1; // No free handles
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[slot]));
|
|
const rc = lfs_file_open(lfs_ptr, file_ptr, path, flags);
|
|
if (rc == 0) {
|
|
file_active[slot] = true;
|
|
return @intCast(slot);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/// Read from a file. Returns bytes read or negative error.
|
|
export fn nexus_lfs_read(handle: i32, buf: [*]u8, size: u32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const idx: usize = @intCast(handle);
|
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
|
return lfs_file_read(lfs_ptr, file_ptr, buf, size);
|
|
}
|
|
|
|
/// Write to a file. Returns bytes written or negative error.
|
|
export fn nexus_lfs_write(handle: i32, buf: [*]const u8, size: u32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const idx: usize = @intCast(handle);
|
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
|
return lfs_file_write(lfs_ptr, file_ptr, buf, size);
|
|
}
|
|
|
|
/// Close a file handle.
|
|
export fn nexus_lfs_close(handle: i32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const idx: usize = @intCast(handle);
|
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
|
const rc = lfs_file_close(lfs_ptr, file_ptr);
|
|
file_active[idx] = false;
|
|
return rc;
|
|
}
|
|
|
|
/// Seek within a file.
|
|
export fn nexus_lfs_seek(handle: i32, off: i32, whence: i32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const idx: usize = @intCast(handle);
|
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
|
return lfs_file_seek(lfs_ptr, file_ptr, off, whence);
|
|
}
|
|
|
|
/// Get file size.
|
|
export fn nexus_lfs_size(handle: i32) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const idx: usize = @intCast(handle);
|
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
|
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
|
return lfs_file_size(lfs_ptr, file_ptr);
|
|
}
|
|
|
|
/// Remove a file or empty directory.
|
|
export fn nexus_lfs_remove(path: [*:0]const u8) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
return lfs_remove(lfs_ptr, path);
|
|
}
|
|
|
|
/// Create a directory.
|
|
export fn nexus_lfs_mkdir(path: [*:0]const u8) i32 {
|
|
if (!lfs_mounted) return -1;
|
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
|
return lfs_mkdir(lfs_ptr, path);
|
|
}
|
|
|
|
/// Check if mounted.
|
|
export fn nexus_lfs_is_mounted() i32 {
|
|
return if (lfs_mounted) @as(i32, 1) else @as(i32, 0);
|
|
}
|