Skip to main content

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}