diff --git a/core/fs/lfs_bridge.nim b/core/fs/lfs_bridge.nim new file mode 100644 index 0000000..8cd2eaf --- /dev/null +++ b/core/fs/lfs_bridge.nim @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LSL-1.0 +# Copyright (c) 2026 Markus Maiwald +# Stewardship: Self Sovereign Society Foundation +# +# This file is part of the Nexus Sovereign Core. +# See legal/LICENSE_SOVEREIGN.md for license terms. + +## Rumpk Layer 1: LittleFS Bridge +## +## Nim FFI wrapper for the Zig-side LittleFS HAL (littlefs_hal.zig). +## Provides the API that VFS delegates to for /nexus mount point. +## +## All calls cross the Nim→Zig→C boundary: +## Nim (this file) → Zig (littlefs_hal.zig) → C (lfs.c) → VirtIO-Block + +# --- FFI imports from littlefs_hal.zig (exported as C ABI) --- +proc nexus_lfs_format(): int32 {.importc, cdecl.} +proc nexus_lfs_mount(): int32 {.importc, cdecl.} +proc nexus_lfs_unmount(): int32 {.importc, cdecl.} +proc nexus_lfs_open(path: cstring, flags: int32): int32 {.importc, cdecl.} +proc nexus_lfs_read(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.} +proc nexus_lfs_write(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.} +proc nexus_lfs_close(handle: int32): int32 {.importc, cdecl.} +proc nexus_lfs_seek(handle: int32, off: int32, whence: int32): int32 {.importc, cdecl.} +proc nexus_lfs_size(handle: int32): int32 {.importc, cdecl.} +proc nexus_lfs_remove(path: cstring): int32 {.importc, cdecl.} +proc nexus_lfs_mkdir(path: cstring): int32 {.importc, cdecl.} +proc nexus_lfs_is_mounted(): int32 {.importc, cdecl.} + +# --- LFS open flags (match lfs.h) --- +const + LFS_O_RDONLY* = 1'i32 + LFS_O_WRONLY* = 2'i32 + LFS_O_RDWR* = 3'i32 + LFS_O_CREAT* = 0x0100'i32 + LFS_O_EXCL* = 0x0200'i32 + LFS_O_TRUNC* = 0x0400'i32 + LFS_O_APPEND* = 0x0800'i32 + +# --- LFS seek flags --- +const + LFS_SEEK_SET* = 0'i32 + LFS_SEEK_CUR* = 1'i32 + LFS_SEEK_END* = 2'i32 + +# --- Public API for VFS --- + +proc lfs_mount_fs*(): bool = + ## Mount the LittleFS filesystem. Auto-formats on first boot. + return nexus_lfs_mount() == 0 + +proc lfs_unmount_fs*(): bool = + return nexus_lfs_unmount() == 0 + +proc lfs_format_fs*(): bool = + return nexus_lfs_format() == 0 + +proc lfs_is_mounted*(): bool = + return nexus_lfs_is_mounted() != 0 + +proc lfs_open_file*(path: cstring, flags: int32): int32 = + ## Open a file. Returns handle >= 0 on success, < 0 on error. + return nexus_lfs_open(path, flags) + +proc lfs_read_file*(handle: int32, buf: pointer, size: uint32): int32 = + ## Read from file. Returns bytes read or negative error. + return nexus_lfs_read(handle, buf, size) + +proc lfs_write_file*(handle: int32, buf: pointer, size: uint32): int32 = + ## Write to file. Returns bytes written or negative error. + return nexus_lfs_write(handle, buf, size) + +proc lfs_close_file*(handle: int32): int32 = + return nexus_lfs_close(handle) + +proc lfs_seek_file*(handle: int32, off: int32, whence: int32): int32 = + return nexus_lfs_seek(handle, off, whence) + +proc lfs_file_size*(handle: int32): int32 = + return nexus_lfs_size(handle) + +proc lfs_remove_path*(path: cstring): int32 = + return nexus_lfs_remove(path) + +proc lfs_mkdir_path*(path: cstring): int32 = + return nexus_lfs_mkdir(path) + +# --- Convenience: VFS-compatible read/write (path-based, like SFS) --- + +proc lfs_vfs_read*(path: cstring, buf: pointer, max_len: int): int = + ## Read entire file into buffer. Returns bytes read or -1. + let h = nexus_lfs_open(path, LFS_O_RDONLY) + if h < 0: return -1 + let n = nexus_lfs_read(h, buf, uint32(max_len)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int(n) + +proc lfs_vfs_write*(path: cstring, buf: pointer, len: int) = + ## Write buffer to file (create/truncate). + let h = nexus_lfs_open(path, LFS_O_WRONLY or LFS_O_CREAT or LFS_O_TRUNC) + if h < 0: return + discard nexus_lfs_write(h, buf, uint32(len)) + discard nexus_lfs_close(h) + +proc lfs_vfs_read_at*(path: cstring, buf: pointer, count: uint64, + offset: uint64): int64 = + ## Read `count` bytes starting at `offset`. Returns bytes read. + let h = nexus_lfs_open(path, LFS_O_RDONLY) + if h < 0: return -1 + if offset > 0: + discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET) + let n = nexus_lfs_read(h, buf, uint32(count)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int64(n) + +proc lfs_vfs_write_at*(path: cstring, buf: pointer, count: uint64, + offset: uint64): int64 = + ## Write `count` bytes at `offset`. Returns bytes written. + let flags = LFS_O_WRONLY or LFS_O_CREAT + let h = nexus_lfs_open(path, flags) + if h < 0: return -1 + if offset > 0: + discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET) + let n = nexus_lfs_write(h, buf, uint32(count)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int64(n) diff --git a/core/fs/vfs.nim b/core/fs/vfs.nim index bd996d3..157ddb5 100644 --- a/core/fs/vfs.nim +++ b/core/fs/vfs.nim @@ -10,11 +10,11 @@ ## Freestanding implementation (No OS module dependencies). ## Uses fixed-size arrays for descriptors to ensure deterministic latency. -import tar, sfs +import tar, sfs, lfs_bridge type VFSMode = enum - MODE_TAR, MODE_SFS, MODE_RAM, MODE_TTY + MODE_TAR, MODE_SFS, MODE_RAM, MODE_TTY, MODE_LFS MountPoint = object prefix: array[32, char] @@ -25,6 +25,7 @@ type offset: uint64 mode: VFSMode active: bool + lfs_handle: int32 ## LFS file handle (-1 = not open) const MAX_MOUNTS = 8 const MAX_FDS = 32 @@ -64,9 +65,16 @@ proc vfs_add_mount(prefix: cstring, mode: VFSMode) = mnt_table[mnt_count].mode = mode mnt_count += 1 +proc kprintln(s: cstring) {.importc, cdecl.} + proc vfs_mount_init*() = - # Restore the SPEC-502 baseline - vfs_add_mount("/nexus", MODE_SFS) + # Mount LittleFS for /nexus (persistent sovereign storage) + if lfs_bridge.lfs_mount_fs(): + vfs_add_mount("/nexus", MODE_LFS) + else: + # Fallback to SFS if LittleFS mount fails (no block device?) + kprintln("[VFS] LFS mount failed, falling back to SFS for /nexus") + vfs_add_mount("/nexus", MODE_SFS) vfs_add_mount("/sysro", MODE_TAR) vfs_add_mount("/state", MODE_RAM) vfs_add_mount("/dev/tty", MODE_TTY) @@ -83,22 +91,36 @@ proc resolve_path(path: cstring): (VFSMode, int) = proc ion_vfs_open*(path: cstring, flags: int32): int32 {.exportc, cdecl.} = let (mode, prefix_len) = resolve_path(path) - + # Delegate internal open let sub_path = cast[cstring](cast[uint64](path) + uint64(prefix_len)) var internal_fd: int32 = -1 - + + # Map VFS flags to LFS flags for MODE_LFS + var lfs_h: int32 = -1 + case mode: of MODE_TAR, MODE_RAM: internal_fd = tar.vfs_open(sub_path, flags) of MODE_SFS: internal_fd = 0 # Shim + of MODE_LFS: + # Convert POSIX-ish flags to LFS flags + var lfs_flags = lfs_bridge.LFS_O_RDONLY + if (flags and 3) == 1: lfs_flags = lfs_bridge.LFS_O_WRONLY + elif (flags and 3) == 2: lfs_flags = lfs_bridge.LFS_O_RDWR + if (flags and 0x40) != 0: lfs_flags = lfs_flags or lfs_bridge.LFS_O_CREAT # O_CREAT + if (flags and 0x200) != 0: lfs_flags = lfs_flags or lfs_bridge.LFS_O_TRUNC # O_TRUNC + if (flags and 0x400) != 0: lfs_flags = lfs_flags or lfs_bridge.LFS_O_APPEND # O_APPEND + lfs_h = lfs_bridge.lfs_open_file(sub_path, lfs_flags) + if lfs_h >= 0: internal_fd = 0 of MODE_TTY: internal_fd = 1 # Shim - + if internal_fd >= 0: for i in 0..= 0: discard lfs_bridge.lfs_close_file(lfs_h) return -1 proc ion_vfs_read*(fd: int32, buf: pointer, count: uint64): int64 {.exportc, cdecl.} = let idx = int(fd - 3) if idx < 0 or idx >= MAX_FDS or not fd_table[idx].active: return -1 let fh = addr fd_table[idx] - + case fh.mode: of MODE_TTY: return -2 of MODE_TAR, MODE_RAM: @@ -132,12 +156,17 @@ proc ion_vfs_read*(fd: int32, buf: pointer, count: uint64): int64 {.exportc, cde fh.offset += actual return int64(actual) return 0 + of MODE_LFS: + if fh.lfs_handle < 0: return -1 + let n = lfs_bridge.lfs_read_file(fh.lfs_handle, buf, uint32(count)) + if n > 0: fh.offset += uint64(n) + return int64(n) proc ion_vfs_write*(fd: int32, buf: pointer, count: uint64): int64 {.exportc, cdecl.} = let idx = int(fd - 3) if idx < 0 or idx >= MAX_FDS or not fd_table[idx].active: return -1 let fh = addr fd_table[idx] - + case fh.mode: of MODE_TTY: return -2 of MODE_TAR, MODE_RAM: @@ -149,14 +178,47 @@ proc ion_vfs_write*(fd: int32, buf: pointer, count: uint64): int64 {.exportc, cd let path = cast[cstring](addr fh.path[0]) sfs.sfs_write_file(path, buf, int(count)) return int64(count) + of MODE_LFS: + if fh.lfs_handle < 0: return -1 + let n = lfs_bridge.lfs_write_file(fh.lfs_handle, buf, uint32(count)) + if n > 0: fh.offset += uint64(n) + return int64(n) proc ion_vfs_close*(fd: int32): int32 {.exportc, cdecl.} = let idx = int(fd - 3) if idx >= 0 and idx < MAX_FDS: + if fd_table[idx].mode == MODE_LFS and fd_table[idx].lfs_handle >= 0: + discard lfs_bridge.lfs_close_file(fd_table[idx].lfs_handle) + fd_table[idx].lfs_handle = -1 fd_table[idx].active = false return 0 return -1 +proc ion_vfs_dup*(fd: int32, min_fd: int32 = 0): int32 {.exportc, cdecl.} = + let idx = int(fd - 3) + if idx < 0 or idx >= MAX_FDS or not fd_table[idx].active: return -1 + + # F_DUPFD needs to find first fd >= min_fd + let start_idx = if min_fd > 3: int(min_fd - 3) else: 0 + + for i in start_idx..= MAX_FDS or not fd_table[old_idx].active: return -1 + if new_idx < 0 or new_idx >= MAX_FDS: return -1 + + if old_idx == new_idx: return new_fd + + fd_table[new_idx] = fd_table[old_idx] + fd_table[new_idx].active = true + return new_fd + proc ion_vfs_list*(buf: pointer, max_len: uint64): int64 {.exportc, cdecl.} = # Hardcoded baseline for now to avoid string/os dependency let msg = "/nexus\n/sysro\n/state\n" diff --git a/hal/littlefs_hal.zig b/hal/littlefs_hal.zig new file mode 100644 index 0000000..f3f1b92 --- /dev/null +++ b/hal/littlefs_hal.zig @@ -0,0 +1,362 @@ +// 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); +} diff --git a/vendor/littlefs/lfs_rumpk.h b/vendor/littlefs/lfs_rumpk.h new file mode 100644 index 0000000..60340da --- /dev/null +++ b/vendor/littlefs/lfs_rumpk.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSD-3-Clause +// LittleFS freestanding configuration for Rumpk unikernel. +// +// Replaces lfs_util.h entirely via -DLFS_CONFIG=lfs_rumpk.h. +// Provides minimal stubs using kernel-provided malloc/free/memcpy/memset. +#ifndef LFS_RUMPK_H +#define LFS_RUMPK_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// --- NULL (freestanding stddef.h may not always provide it) --- +#ifndef NULL +#define NULL ((void *)0) +#endif + +// --- Logging: all disabled in freestanding kernel --- +#define LFS_TRACE(...) +#define LFS_DEBUG(...) +#define LFS_WARN(...) +#define LFS_ERROR(...) + +// --- Assertions: disabled --- +#define LFS_ASSERT(test) ((void)(test)) + +// --- Memory functions (provided by clib.c / libc_shim.zig) --- +extern void *malloc(unsigned long size); +extern void free(void *ptr); +extern void *memcpy(void *dest, const void *src, unsigned long n); +extern void *memset(void *s, int c, unsigned long n); +extern int memcmp(const void *s1, const void *s2, unsigned long n); +extern unsigned long strlen(const char *s); + +// strchr — needed by lfs.c for path parsing +static inline char *strchr(const char *s, int c) { + while (*s) { + if (*s == (char)c) return (char *)s; + s++; + } + return (c == '\0') ? (char *)s : NULL; +} + +static inline void *lfs_malloc(unsigned long size) { + return malloc(size); +} + +static inline void lfs_free(void *p) { + free(p); +} + +// --- Builtins --- +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) { + return a - (a % alignment); +} + +static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) { + return lfs_aligndown(a + alignment - 1, alignment); +} + +static inline uint32_t lfs_npw2(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return 32 - __builtin_clz(a - 1); +#else + uint32_t r = 0, s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif +} + +static inline uint32_t lfs_ctz(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_ctz(a); +#else + return lfs_npw2((a & -a) + 1) - 1; +#endif +} + +static inline uint32_t lfs_popc(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +#endif +} + +static inline int lfs_scmp(uint32_t a, uint32_t b) { + return (int)(unsigned)(a - b); +} + +// --- Endianness (RISC-V / ARM64 / x86_64 are all little-endian) --- +static inline uint32_t lfs_fromle32(uint32_t a) { return a; } +static inline uint32_t lfs_tole32(uint32_t a) { return a; } + +static inline uint32_t lfs_frombe32(uint32_t a) { + return __builtin_bswap32(a); +} +static inline uint32_t lfs_tobe32(uint32_t a) { + return __builtin_bswap32(a); +} + +// --- CRC-32 (implementation in lfs.c) --- +uint32_t lfs_crc(uint32_t crc, const void *buffer, unsigned long size); + +#ifdef __cplusplus +} +#endif + +#endif // LFS_RUMPK_H