// 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); }