ove/fs.rs
1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Filesystem abstraction for oveRTOS.
8//!
9//! Provides RAII [`File`] and [`Dir`] handles for reading, writing, and
10//! directory traversal. All paths must be null-terminated byte slices
11//! (e.g. `b"/data/config.bin\0"`).
12
13use crate::bindings;
14use crate::error::{Error, Result};
15
16// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
17// calls below): any handle passed to the C API is non-null and refers to a
18// live RTOS object — wrapper constructors establish validity via
19// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
20// a handle is released. Pointer and slice arguments reference caller-owned
21// memory valid for the duration of the call; the C side copies whatever it
22// retains and does not alias them past return (verified against the
23// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
24// pointer casts from user data, slice reconstruction via `from_raw_parts`,
25// or storing a callback across the FFI boundary — carry their own
26// `// SAFETY:` comment.
27
28/// Open flag: open file for reading.
29pub const O_READ: i32 = bindings::OVE_FS_O_READ as i32;
30/// Open flag: open file for writing.
31pub const O_WRITE: i32 = bindings::OVE_FS_O_WRITE as i32;
32/// Open flag: create the file if it does not exist.
33pub const O_CREATE: i32 = bindings::OVE_FS_O_CREATE as i32;
34/// Open flag: all writes append to the end of the file.
35pub const O_APPEND: i32 = bindings::OVE_FS_O_APPEND as i32;
36
37/// Mount a filesystem.
38///
39/// Pass `None` for `dev` or `mnt` to use platform defaults.
40pub fn mount(dev: Option<&[u8]>, mnt: Option<&[u8]>) -> Result<()> {
41 let dev_ptr = dev.map_or(core::ptr::null(), |d| d.as_ptr() as *const _);
42 let mnt_ptr = mnt.map_or(core::ptr::null(), |m| m.as_ptr() as *const _);
43 let rc = unsafe { bindings::ove_fs_mount(dev_ptr, mnt_ptr) };
44 Error::from_code(rc)
45}
46
47/// RAII file handle.
48pub struct File {
49 handle: bindings::ove_file_t,
50}
51
52impl File {
53 /// Open a file. `path` must be `\0`-terminated.
54 pub fn open(path: &[u8], flags: i32) -> Result<Self> {
55 let mut handle: bindings::ove_file_t = core::ptr::null_mut();
56 let rc = unsafe { bindings::ove_fs_open(&mut handle, path.as_ptr() as *const _, flags) };
57 Error::from_code(rc)?;
58 Ok(Self { handle })
59 }
60
61 /// Read from the file into `buf`. Returns the number of bytes read.
62 pub fn read(&self, buf: &mut [u8]) -> Result<usize> {
63 let mut bytes_read: usize = 0;
64 let rc = unsafe {
65 bindings::ove_fs_read(
66 self.handle,
67 buf.as_mut_ptr() as *mut _,
68 buf.len(),
69 &mut bytes_read,
70 )
71 };
72 Error::from_code(rc)?;
73 Ok(bytes_read)
74 }
75
76 /// Write `buf` to the file. Returns the number of bytes written.
77 pub fn write(&self, buf: &[u8]) -> Result<usize> {
78 let mut bytes_written: usize = 0;
79 let rc = unsafe {
80 bindings::ove_fs_write(
81 self.handle,
82 buf.as_ptr() as *const _,
83 buf.len(),
84 &mut bytes_written,
85 )
86 };
87 Error::from_code(rc)?;
88 Ok(bytes_written)
89 }
90}
91
92impl Drop for File {
93 fn drop(&mut self) {
94 unsafe { bindings::ove_fs_close(self.handle) };
95 }
96}
97
98// SAFETY: File wraps an opaque RTOS handle. Access is single-threaded by
99// application design (only one thread owns the File at a time).
100unsafe impl Send for File {}
101
102/// A directory entry returned by `Dir::read_entry`.
103pub struct DirEntry {
104 inner: bindings::ove_dirent,
105}
106
107impl DirEntry {
108 /// The entry name as a byte slice (without trailing `\0`).
109 pub fn name(&self) -> &[u8] {
110 // SAFETY: `self.inner.name` is an inline fixed-size array owned by this
111 // `DirEntry`; the slice borrows it for `&self`'s lifetime.
112 let name_bytes = unsafe {
113 core::slice::from_raw_parts(
114 self.inner.name.as_ptr() as *const u8,
115 self.inner.name.len(),
116 )
117 };
118 cstr_to_slice(name_bytes)
119 }
120
121 /// The file size in bytes.
122 pub fn size(&self) -> u32 {
123 self.inner.size
124 }
125
126 /// Whether this entry is a directory.
127 pub fn is_dir(&self) -> bool {
128 self.inner.is_dir != 0
129 }
130}
131
132/// RAII directory handle.
133pub struct Dir {
134 handle: bindings::ove_dir_t,
135}
136
137impl Dir {
138 /// Open a directory. `path` must be `\0`-terminated.
139 pub fn open(path: &[u8]) -> Result<Self> {
140 let mut handle: bindings::ove_dir_t = core::ptr::null_mut();
141 let rc = unsafe { bindings::ove_fs_opendir(&mut handle, path.as_ptr() as *const _) };
142 Error::from_code(rc)?;
143 Ok(Self { handle })
144 }
145
146 /// Read the next directory entry.
147 ///
148 /// Returns `Ok(None)` at end-of-directory — signalled either by the
149 /// `OVE_ERR_EOF` code or (on some backends) an empty entry name. Any
150 /// other negative code is surfaced as an `Err`, so a genuine I/O failure
151 /// mid-iteration is not silently mistaken for end-of-directory.
152 pub fn read_entry(&mut self) -> Result<Option<DirEntry>> {
153 let mut entry: bindings::ove_dirent = unsafe { core::mem::zeroed() };
154 let rc = unsafe { bindings::ove_fs_readdir(self.handle, &mut entry) };
155 match Error::from_code(rc) {
156 Ok(()) => {}
157 Err(Error::Eof) => return Ok(None),
158 Err(e) => return Err(e),
159 }
160 // Some backends signal end-of-directory with an empty name instead
161 // of OVE_ERR_EOF.
162 if entry.name[0] == 0 {
163 return Ok(None);
164 }
165 Ok(Some(DirEntry { inner: entry }))
166 }
167}
168
169impl Drop for Dir {
170 fn drop(&mut self) {
171 unsafe { bindings::ove_fs_closedir(self.handle) };
172 }
173}
174
175// SAFETY: Dir wraps an opaque RTOS handle. Access is single-threaded by
176// application design.
177unsafe impl Send for Dir {}
178
179fn cstr_to_slice(buf: &[u8]) -> &[u8] {
180 let mut len = 0;
181 while len < buf.len() && buf[len] != 0 {
182 len += 1;
183 }
184 &buf[..len]
185}