feat(fs): LittleFS integration — VFS, HAL bridge, persistent /nexus
This commit is contained in:
parent
0c598ce0bd
commit
8d64fe2180
|
|
@ -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)
|
||||
|
|
@ -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..<MAX_FDS:
|
||||
if not fd_table[i].active:
|
||||
fd_table[i].active = true
|
||||
fd_table[i].mode = mode
|
||||
fd_table[i].offset = 0
|
||||
fd_table[i].lfs_handle = lfs_h
|
||||
let p = cast[ptr UncheckedArray[char]](sub_path)
|
||||
var j = 0
|
||||
while p[j] != '\0' and j < 63:
|
||||
|
|
@ -106,13 +128,15 @@ proc ion_vfs_open*(path: cstring, flags: int32): int32 {.exportc, cdecl.} =
|
|||
j += 1
|
||||
fd_table[i].path[j] = '\0'
|
||||
return int32(i + 3) # FDs start at 3
|
||||
# No free slot — close LFS handle if we opened one
|
||||
if lfs_h >= 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:
|
||||
if not fd_table[i].active:
|
||||
fd_table[i] = fd_table[idx]
|
||||
return int32(i + 3)
|
||||
return -1
|
||||
|
||||
proc ion_vfs_dup2*(old_fd: int32, new_fd: int32): int32 {.exportc, cdecl.} =
|
||||
let old_idx = int(old_fd - 3)
|
||||
let new_idx = int(new_fd - 3)
|
||||
if old_idx < 0 or old_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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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
|
||||
Loading…
Reference in New Issue