Skip to main content

leodos_libcfs/os/
fs.rs

1//! Safe, idiomatic wrappers for CFE File Services (CFE_FS) and OSAL file APIs.
2//!
3//! This module provides a `File` struct that acts as a safe handle for
4//! file operations, ensuring that files are closed when they go out of scope.
5//! It also provides wrappers for standalone filesystem operations like `mkdir` and `remove`.
6
7use crate::cfe::time::SysTime;
8use crate::error::{CfsError, OsalError, Result};
9use crate::ffi;
10use crate::os::id::OsalId;
11use crate::os::util::c_path_from_str;
12use crate::status::{check, Status};
13use bitflags::bitflags;
14use core::ffi::CStr;
15use core::future::Future;
16use core::mem::MaybeUninit;
17use core::ops::Drop;
18use core::task::Poll;
19use heapless::CString;
20
21#[cfg(not(nos3_cfe))]
22use crate::os::time::OsTime;
23
24bitflags! {
25    /// File permission modes for `fs::chmod`.
26    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
27    pub struct FileMode: u32 {
28        /// Read permission.
29        const READ = ffi::OS_FILESTAT_MODE_READ;
30        /// Write permission.
31        const WRITE = ffi::OS_FILESTAT_MODE_WRITE;
32        /// Execute permission.
33        const EXEC = ffi::OS_FILESTAT_MODE_EXEC;
34    }
35}
36
37/// Generalized file types/categories known to FS.
38///
39/// This is used by `parse_input_filename` to apply default paths and extensions.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[repr(u32)]
42pub enum FileCategory {
43    /// An unknown or unspecified file category.
44    Unknown = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_UNKNOWN,
45    /// A dynamically loadable module file (e.g., .so).
46    DynamicModule = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_DYNAMIC_MODULE,
47    /// A binary data dump file.
48    BinaryDataDump = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_BINARY_DATA_DUMP,
49    /// A text-based log file.
50    TextLog = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_TEXT_LOG,
51    /// A script file (e.g., an ES startup script).
52    Script = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_SCRIPT,
53    /// A temporary or ephemeral file.
54    Temp = ffi::CFE_FS_FileCategory_t_CFE_FS_FileCategory_TEMP,
55}
56
57/// Metadata and state for a background file write operation.
58///
59/// This struct should be stored in a persistent memory location (e.g., a `static`)
60/// for the duration of the asynchronous file write.
61pub struct FileWriteMetaData(pub(crate) ffi::CFE_FS_FileWriteMetaData_t);
62
63/// A structure containing metadata about a file.
64#[derive(Debug, Clone, Copy)]
65pub struct FileStat {
66    inner: ffi::os_fstat_t,
67}
68
69impl FileStat {
70    /// Returns the size of the file in bytes.
71    pub fn size(&self) -> usize {
72        self.inner.FileSize
73    }
74
75    /// Returns `true` if this metadata is for a directory.
76    pub fn is_dir(&self) -> bool {
77        (self.inner.FileModeBits & ffi::OS_FILESTAT_MODE_DIR) != 0
78    }
79}
80
81/// A safe, idiomatic wrapper for a cFE file header.
82#[derive(Debug, Clone)]
83#[repr(transparent)]
84pub struct FsHeader(pub(crate) ffi::CFE_FS_Header_t);
85
86/// Statistics about the overall file system.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct FsInfo {
89    /// Total number of file descriptors.
90    pub max_fds: u32,
91    /// Total number that are free.
92    pub free_fds: u32,
93    /// Maximum number of volumes.
94    pub max_volumes: u32,
95    /// Total number of volumes free.
96    pub free_volumes: u32,
97}
98
99impl From<ffi::os_fsinfo_t> for FsInfo {
100    fn from(info: ffi::os_fsinfo_t) -> Self {
101        Self {
102            max_fds: info.MaxFds,
103            free_fds: info.FreeFds,
104            max_volumes: info.MaxVolumes,
105            free_volumes: info.FreeVolumes,
106        }
107    }
108}
109
110impl FsHeader {
111    /// Creates and initializes a new cFE file header with the specified description and subtype.
112    ///
113    /// The description will be truncated if it is longer than the header field allows.
114    pub fn new(description: &str, subtype: u32) -> Result<Self> {
115        let mut header_uninit = MaybeUninit::<ffi::CFE_FS_Header_t>::uninit();
116
117        // CFE_FS_HDR_DESC_MAX_LEN includes the null terminator.
118        let mut c_desc = CString::<{ ffi::CFE_FS_HDR_DESC_MAX_LEN as usize }>::new();
119        // This will truncate if the description is too long, which is acceptable.
120        c_desc
121            .extend_from_bytes(description.as_bytes())
122            .map_err(|_| CfsError::Osal(OsalError::FsPathTooLong))?;
123
124        unsafe {
125            // CFE_FS_InitHeader takes a pointer and initializes the memory it points to.
126            ffi::CFE_FS_InitHeader(header_uninit.as_mut_ptr(), c_desc.as_ptr(), subtype);
127            // Now that the C function has initialized it, we can safely assume it's initialized.
128            Ok(Self(header_uninit.assume_init()))
129        }
130    }
131}
132
133/// Defines the origin for a seek operation, used by `File::seek`.
134#[derive(Debug, Clone, Copy)]
135pub enum SeekFrom {
136    /// Seek from the beginning of the stream.
137    Start(u32),
138    /// Seek from the end of the stream.
139    End(i32),
140    /// Seek from the current position.
141    Current(i32),
142}
143
144/// A structure containing statistics about a filesystem volume.
145#[derive(Debug, Clone, Copy)]
146pub struct StatVfs {
147    inner: ffi::OS_statvfs_t,
148}
149
150impl StatVfs {
151    /// Returns the fundamental file system block size.
152    pub fn block_size(&self) -> usize {
153        self.inner.block_size
154    }
155
156    /// Returns the total number of blocks in the file system.
157    pub fn total_blocks(&self) -> ffi::osal_blockcount_t {
158        self.inner.total_blocks
159    }
160
161    /// Returns the number of free blocks available.
162    pub fn blocks_free(&self) -> ffi::osal_blockcount_t {
163        self.inner.blocks_free
164    }
165}
166
167/// Properties of an open file, returned by `File::info`.
168#[derive(Debug, Clone)]
169pub struct FileProp {
170    /// The full path of the open file.
171    pub path: CString<{ ffi::OS_MAX_PATH_LEN as usize }>,
172    /// The OSAL ID of the task that opened the file.
173    pub user: OsalId,
174    /// Indicates if the file descriptor is valid.
175    pub is_valid: bool,
176}
177
178/// A handle to an open directory.
179///
180/// This is an iterator that yields the entries within the directory. It is
181/// a wrapper around an `osal_id_t` that will automatically call `OS_DirectoryClose` when
182/// it goes out of scope, preventing resource leaks.
183#[derive(Debug)]
184pub struct Directory {
185    id: ffi::osal_id_t,
186}
187
188impl Directory {
189    /// Opens a directory for reading.
190    pub fn open(path: &str) -> Result<Self> {
191        let c_path = c_path_from_str(path)?;
192        let mut dir_id = MaybeUninit::uninit();
193        check(unsafe { ffi::OS_DirectoryOpen(dir_id.as_mut_ptr(), c_path.as_ptr()) })?;
194        Ok(Self {
195            id: unsafe { dir_id.assume_init() },
196        })
197    }
198
199    /// Rewinds the directory stream to the beginning.
200    pub fn rewind(&mut self) -> Result<()> {
201        check(unsafe { ffi::OS_DirectoryRewind(self.id) })?;
202        Ok(())
203    }
204}
205
206impl Iterator for Directory {
207    type Item = Result<CString<{ ffi::OS_MAX_FILE_NAME as usize }>>;
208
209    /// Reads the next directory entry.
210    ///
211    /// Returns `Some(Ok(name))` for the next entry, `None` when the directory
212    /// has been fully read, or `Some(Err(e))` if an error occurs.
213    fn next(&mut self) -> Option<Self::Item> {
214        let mut dirent = MaybeUninit::<ffi::os_dirent_t>::uninit();
215        let status = check(unsafe { ffi::OS_DirectoryRead(self.id, dirent.as_mut_ptr()) });
216
217        match status {
218            Ok(_) => {
219                let dirent = unsafe { dirent.assume_init() };
220                let c_str = unsafe { CStr::from_ptr(dirent.FileName.as_ptr()) };
221                let mut s = CString::new();
222                match s.extend_from_bytes(c_str.to_bytes()) {
223                    Ok(_) => Some(Ok(s)),
224                    Err(_) => Some(Err(CfsError::Osal(OsalError::NameTooLong))),
225                }
226            }
227            Err(CfsError::Osal(OsalError::Error)) => None,
228            Err(err) => Some(Err(err)),
229        }
230    }
231}
232
233impl Drop for Directory {
234    /// Closes the directory when the `Directory` object goes out of scope.
235    fn drop(&mut self) {
236        // OS_DirectoryClose returns an osal_status_t, which can be an error.
237        // But we ignore it in drop, as with other resources in this crate.
238        let _ = unsafe { ffi::OS_DirectoryClose(self.id) };
239    }
240}
241
242/// A handle to an open file.
243///
244/// This is a wrapper around an `osal_id_t` that will automatically call
245/// `OS_close` when it goes out of scope, preventing resource leaks.
246#[derive(Debug)]
247pub struct File {
248    id: OsalId,
249}
250
251/// File access modes for opening or creating a file.
252#[derive(Debug, Clone, Copy)]
253#[repr(u32)]
254pub enum AccessMode {
255    /// Open for reading only.
256    ReadOnly = ffi::OS_READ_ONLY,
257    /// Open for writing only.
258    WriteOnly = ffi::OS_WRITE_ONLY,
259    /// Open for reading and writing.
260    ReadWrite = ffi::OS_READ_WRITE,
261}
262
263impl File {
264    /// Returns the underlying OSAL file ID.
265    pub fn id(&self) -> OsalId {
266        self.id
267    }
268    /// Opens a file with the specified access mode.
269    ///
270    /// # Arguments
271    /// * `path`: The virtual path to the file (e.g., "/ram/my_file.txt").
272    /// * `access`: The access mode, e.g., `ffi::OS_READ_ONLY`, `ffi::OS_WRITE_ONLY`,
273    ///   or `ffi::OS_READ_WRITE`.
274    pub fn open(path: &str, access: AccessMode) -> Result<Self> {
275        let c_path = c_path_from_str(path)?;
276        let mut file_id = MaybeUninit::uninit();
277        let status = unsafe {
278            ffi::OS_OpenCreate(
279                file_id.as_mut_ptr(),
280                c_path.as_ptr(),
281                ffi::OS_file_flag_t_OS_FILE_FLAG_NONE as i32,
282                access as i32,
283            )
284        };
285        check(status)?;
286        Ok(Self {
287            id: OsalId(unsafe { file_id.assume_init() }),
288        })
289    }
290
291    /// Creates a new file, or truncates an existing one, with read/write access.
292    ///
293    /// # Arguments
294    /// * `path`: The virtual path to the file to create (e.g., "/ram/new_file.dat").
295    pub fn create(path: &str) -> Result<Self> {
296        let c_path = c_path_from_str(path)?;
297        let mut file_id = MaybeUninit::uninit();
298        let flags = ffi::OS_file_flag_t_OS_FILE_FLAG_CREATE as i32
299            | ffi::OS_file_flag_t_OS_FILE_FLAG_TRUNCATE as i32;
300
301        let status = unsafe {
302            ffi::OS_OpenCreate(
303                file_id.as_mut_ptr(),
304                c_path.as_ptr(),
305                flags,
306                ffi::OS_READ_WRITE as i32,
307            )
308        };
309        check(status)?;
310        Ok(Self {
311            id: OsalId(unsafe { file_id.assume_init() }),
312        })
313    }
314
315    /// Reads some bytes from the file into the specified buffer.
316    ///
317    /// Returns the number of bytes read.
318    pub fn sync_read(&mut self, buf: &mut [u8]) -> Result<usize> {
319        let bytes_read = unsafe { ffi::OS_read(self.id.0, buf.as_mut_ptr() as *mut _, buf.len()) };
320        if bytes_read < 0 {
321            Err(CfsError::from(bytes_read))
322        } else {
323            Ok(bytes_read as usize)
324        }
325    }
326
327    /// Writes a buffer to the file.
328    ///
329    /// Returns the number of bytes written.
330    pub fn sync_write(&mut self, buf: &[u8]) -> Result<usize> {
331        let bytes_written =
332            unsafe { ffi::OS_write(self.id.0, buf.as_ptr() as *const _, buf.len()) };
333        if bytes_written < 0 {
334            Err(CfsError::from(bytes_written))
335        } else {
336            Ok(bytes_written as usize)
337        }
338    }
339
340    /// Seeks to an offset, in bytes, in a stream.
341    ///
342    /// Returns the new position from the start of the file.
343    pub fn seek(&mut self, pos: SeekFrom) -> Result<u32> {
344        let (offset, whence) = match pos {
345            SeekFrom::Start(offset) => (offset as i32, ffi::OS_SEEK_SET),
346            SeekFrom::End(offset) => (offset, ffi::OS_SEEK_END),
347            SeekFrom::Current(offset) => (offset, ffi::OS_SEEK_CUR),
348        };
349
350        let new_pos = unsafe { ffi::OS_lseek(self.id.0, offset, whence) };
351        if new_pos < 0 {
352            Err(CfsError::from(new_pos))
353        } else {
354            Ok(new_pos as u32)
355        }
356    }
357
358    /// Reads the standard cFE File Header from the file.
359    pub fn read_header(&mut self) -> Result<FsHeader> {
360        let mut hdr = MaybeUninit::uninit();
361        let status = unsafe { ffi::CFE_FS_ReadHeader(hdr.as_mut_ptr(), self.id.0) };
362        check(status)?;
363        Ok(FsHeader(unsafe { hdr.assume_init() }))
364    }
365
366    /// Writes the standard cFE File Header to the file.
367    pub fn write_header(&mut self, hdr: &mut FsHeader) -> Result<()> {
368        let status = unsafe { ffi::CFE_FS_WriteHeader(self.id.0, &mut hdr.0) };
369        check(status)?;
370        Ok(())
371    }
372
373    /// Modifies the timestamp field in the cFE File Header.
374    pub fn set_timestamp(&mut self, time: SysTime) -> Result<()> {
375        let status = unsafe { ffi::CFE_FS_SetTimestamp(self.id.0, time.0) };
376        check(status)?;
377        Ok(())
378    }
379
380    /// Retrieves information about this open file.
381    pub fn info(&self) -> Result<FileProp> {
382        let mut prop = MaybeUninit::<ffi::OS_file_prop_t>::uninit();
383        check(unsafe { ffi::OS_FDGetInfo(self.id.0, prop.as_mut_ptr()) })?;
384        let prop = unsafe { prop.assume_init() };
385
386        let mut path_str = CString::new();
387        let c_str = unsafe { CStr::from_ptr(prop.Path.as_ptr()) };
388        path_str
389            .extend_from_bytes(c_str.to_bytes())
390            .map_err(|_| CfsError::Osal(OsalError::FsPathTooLong))?;
391
392        Ok(FileProp {
393            path: path_str,
394            user: OsalId(prop.User),
395            is_valid: prop.IsValid != 0,
396        })
397    }
398
399    /// Reads from the file with a relative timeout.
400    pub fn timed_read(&mut self, buf: &mut [u8], timeout_ms: i32) -> Result<usize> {
401        let bytes_read = unsafe {
402            ffi::OS_TimedRead(self.id.0, buf.as_mut_ptr() as *mut _, buf.len(), timeout_ms)
403        };
404        if bytes_read < 0 {
405            Err(CfsError::from(bytes_read))
406        } else {
407            Ok(bytes_read as usize)
408        }
409    }
410
411    /// Reads from the file with an absolute timeout.
412    #[cfg(not(nos3_cfe))]
413    pub fn timed_read_abs(&mut self, buf: &mut [u8], abstime: OsTime) -> Result<usize> {
414        let bytes_read = unsafe {
415            ffi::OS_TimedReadAbs(self.id.0, buf.as_mut_ptr() as *mut _, buf.len(), abstime.0)
416        };
417        if bytes_read < 0 {
418            Err(CfsError::from(bytes_read))
419        } else {
420            Ok(bytes_read as usize)
421        }
422    }
423
424    /// Writes to the file with a relative timeout.
425    pub fn timed_write(&mut self, buf: &[u8], timeout_ms: i32) -> Result<usize> {
426        let bytes_written = unsafe {
427            ffi::OS_TimedWrite(self.id.0, buf.as_ptr() as *const _, buf.len(), timeout_ms)
428        };
429        if bytes_written < 0 {
430            Err(CfsError::from(bytes_written))
431        } else {
432            Ok(bytes_written as usize)
433        }
434    }
435
436    /// Writes to the file with an absolute timeout.
437    #[cfg(not(nos3_cfe))]
438    pub fn timed_write_abs(&mut self, buf: &[u8], abstime: OsTime) -> Result<usize> {
439        let bytes_written = unsafe {
440            ffi::OS_TimedWriteAbs(self.id.0, buf.as_ptr() as *const _, buf.len(), abstime.0)
441        };
442        if bytes_written < 0 {
443            Err(CfsError::from(bytes_written))
444        } else {
445            Ok(bytes_written as usize)
446        }
447    }
448}
449
450impl Drop for File {
451    /// Closes the file when the `File` object goes out of scope.
452    fn drop(&mut self) {
453        let _ = unsafe { ffi::OS_close(self.id.0) };
454    }
455}
456
457// --- Standalone Functions ---
458
459/// Retrieves metadata for a file or directory at a given path.
460pub fn stat(path: &str) -> Result<FileStat> {
461    let c_path = c_path_from_str(path)?;
462    let mut filestats = MaybeUninit::uninit();
463    check(unsafe { ffi::OS_stat(c_path.as_ptr(), filestats.as_mut_ptr()) })?;
464    Ok(FileStat {
465        inner: unsafe { filestats.assume_init() },
466    })
467}
468
469/// Changes the permission mode of a file.
470///
471/// # Errors
472///
473/// Returns an error if the path is invalid or the underlying OS call fails.
474pub fn chmod(path: &str, mode: FileMode) -> Result<()> {
475    let c_path = c_path_from_str(path)?;
476    check(unsafe { ffi::OS_chmod(c_path.as_ptr(), mode.bits()) })?;
477    Ok(())
478}
479/// Removes a file from the file system.
480///
481/// Behavior on an open file is undefined at the OSAL level.
482/// Ensure the file is closed first.
483pub fn remove(path: &str) -> Result<()> {
484    let c_path = c_path_from_str(path)?;
485    let status = unsafe { ffi::OS_remove(c_path.as_ptr()) };
486    check(status)?;
487    Ok(())
488}
489
490/// Renames a file.
491///
492/// Behavior on an open file is undefined at the OSAL level.
493/// Ensure the file is closed first.
494pub fn rename(old: &str, new: &str) -> Result<()> {
495    let c_old = c_path_from_str(old)?;
496    let c_new = c_path_from_str(new)?;
497    let status = unsafe { ffi::OS_rename(c_old.as_ptr(), c_new.as_ptr()) };
498    check(status)?;
499    Ok(())
500}
501
502/// Creates a new directory.
503pub fn mkdir(path: &str) -> Result<()> {
504    let c_path = c_path_from_str(path)?;
505    // `access` is currently unused by OSAL but we pass a reasonable default.
506    let status = unsafe { ffi::OS_mkdir(c_path.as_ptr(), ffi::OS_READ_WRITE) };
507    check(status)?;
508    Ok(())
509}
510
511/// Removes an empty directory.
512pub fn rmdir(path: &str) -> Result<()> {
513    let c_path = c_path_from_str(path)?;
514    let status = unsafe { ffi::OS_rmdir(c_path.as_ptr()) };
515    check(status)?;
516    Ok(())
517}
518
519/// Copies a single file from `src` to `dest`.
520///
521/// Behavior on an open file is undefined at the OSAL level.
522/// Ensure the file is closed first.
523pub fn cp(src: &str, dest: &str) -> Result<()> {
524    let c_src = c_path_from_str(src)?;
525    let c_dest = c_path_from_str(dest)?;
526    check(unsafe { ffi::OS_cp(c_src.as_ptr(), c_dest.as_ptr()) })?;
527    Ok(())
528}
529
530/// Moves a single file from `src` to `dest`.
531///
532/// This will first attempt a rename, and if that fails (e.g.,
533/// across different filesystems), it will fall back to a
534/// copy-then-delete operation.
535///
536/// Behavior on an open file is undefined at the OSAL level.
537/// Ensure the file is closed first.
538pub fn mv(src: &str, dest: &str) -> Result<()> {
539    let c_src = c_path_from_str(src)?;
540    let c_dest = c_path_from_str(dest)?;
541    check(unsafe { ffi::OS_mv(c_src.as_ptr(), c_dest.as_ptr()) })?;
542    Ok(())
543}
544
545/// Retrieves statistics about a filesystem volume.
546pub fn statvfs(path: &str) -> Result<StatVfs> {
547    let c_path = c_path_from_str(path)?;
548    let mut statbuf = MaybeUninit::uninit();
549    check(unsafe { ffi::OS_FileSysStatVolume(c_path.as_ptr(), statbuf.as_mut_ptr()) })?;
550    Ok(StatVfs {
551        inner: unsafe { statbuf.assume_init() },
552    })
553}
554
555/// Extracts the filename from a unix style path and filename string.
556///
557/// Returns a `&str` slice of the valid filename within the provided buffer.
558pub fn extract_filename_from_path<'a>(
559    original_path: &str,
560    filename_buf: &'a mut [u8; ffi::OS_MAX_PATH_LEN as usize],
561) -> Result<&'a str> {
562    let c_path = c_path_from_str(original_path)?;
563    let status = unsafe {
564        ffi::CFE_FS_ExtractFilenameFromPath(
565            c_path.as_ptr(),
566            filename_buf.as_mut_ptr() as *mut libc::c_char,
567        )
568    };
569    check(status)?;
570
571    let c_str = unsafe { CStr::from_ptr(filename_buf.as_ptr() as *const libc::c_char) };
572    c_str.to_str().map_err(|_| CfsError::InvalidString)
573}
574
575/// Parses a filename from an input string, applying default path and extension.
576///
577/// This function uses cFE's logic to construct a complete, platform-correct
578/// file path. If the `input_name` omits a path or extension, defaults
579/// appropriate for the `category` are applied.
580///
581/// # Arguments
582/// * `output_buf`: A buffer to store the resulting fully-qualified path.
583/// * `input_name`: The (potentially partial) filename to parse.
584/// * `category`: The `FileCategory` which determines the default path and extension.
585///
586/// # Returns
587/// On success, returns a `&str` slice of the valid, null-terminated path
588/// within `output_buf`.
589pub fn parse_input_filename<'a>(
590    output_buf: &'a mut [u8; ffi::OS_MAX_PATH_LEN as usize],
591    input_name: &str,
592    category: FileCategory,
593) -> Result<&'a str> {
594    let c_input = c_path_from_str(input_name)?;
595    let status = unsafe {
596        ffi::CFE_FS_ParseInputFileName(
597            output_buf.as_mut_ptr() as *mut libc::c_char,
598            c_input.as_ptr(),
599            output_buf.len(),
600            category as u32,
601        )
602    };
603    check(status)?;
604
605    // Find the null terminator to determine the actual length of the output string.
606    let len = output_buf.iter().position(|&b| b == 0).unwrap_or(0);
607    core::str::from_utf8(&output_buf[..len]).map_err(|_| CfsError::InvalidString)
608}
609
610/// Checks if a file with the given name is currently open.
611pub fn is_file_open(filename: &str) -> bool {
612    if let Ok(c_path) = c_path_from_str(filename) {
613        matches!(
614            check(unsafe { ffi::OS_FileOpenCheck(c_path.as_ptr()) }),
615            Ok(Status::Success)
616        )
617    } else {
618        false
619    }
620}
621
622/// Closes all files that were opened through OSAL.
623pub fn close_all_files() -> Result<()> {
624    check(unsafe { ffi::OS_CloseAllFiles() })?;
625    Ok(())
626}
627
628/// Closes a file by its filename.
629///
630/// Only works if the name matches the one used to open the file.
631pub fn close_file_by_name(filename: &str) -> Result<()> {
632    let c_path = c_path_from_str(filename)?;
633    check(unsafe { ffi::OS_CloseFileByName(c_path.as_ptr()) })?;
634    Ok(())
635}
636
637/// Creates a new file system on a block device or in memory.
638///
639/// For RAM disks, `volname` must begin with `"RAM"` (e.g.
640/// `"RAM0"`). If `address` is null, the OS allocates the memory.
641pub fn make_fs(
642    address: *mut u8,
643    devname: &str,
644    volname: &str,
645    blocksize: usize,
646    numblocks: usize,
647) -> Result<()> {
648    let c_dev = c_path_from_str(devname)?;
649    let c_vol = c_path_from_str(volname)?;
650    check(unsafe {
651        ffi::OS_mkfs(
652            address as *mut libc::c_char,
653            c_dev.as_ptr(),
654            c_vol.as_ptr(),
655            blocksize,
656            numblocks,
657        )
658    })?;
659    Ok(())
660}
661
662/// Mounts a file system to a virtual mount point.
663pub fn mount(devname: &str, mountpoint: &str) -> Result<()> {
664    let c_dev = c_path_from_str(devname)?;
665    let c_mount = c_path_from_str(mountpoint)?;
666    check(unsafe { ffi::OS_mount(c_dev.as_ptr(), c_mount.as_ptr()) })?;
667    Ok(())
668}
669
670/// Unmounts a file system.
671///
672/// All open file descriptors on this volume become invalid after
673/// unmount. Close them first.
674pub fn unmount(mountpoint: &str) -> Result<()> {
675    let c_mount = c_path_from_str(mountpoint)?;
676    check(unsafe { ffi::OS_unmount(c_mount.as_ptr()) })?;
677    Ok(())
678}
679
680/// Gets the physical drive name associated with a virtual mount point.
681pub fn get_phys_drive_name(mountpoint: &str) -> Result<CString<{ ffi::OS_MAX_PATH_LEN as usize }>> {
682    let c_mount = c_path_from_str(mountpoint)?;
683    let mut buffer = [0u8; ffi::OS_MAX_PATH_LEN as usize];
684    check(unsafe {
685        ffi::OS_FS_GetPhysDriveName(buffer.as_mut_ptr() as *mut libc::c_char, c_mount.as_ptr())
686    })?;
687    let len = buffer.iter().position(|&b| b == 0).unwrap_or(0);
688    let mut s = CString::new();
689    s.extend_from_bytes(&buffer[..len])
690        .map_err(|_| CfsError::Osal(OsalError::NameTooLong))?;
691    Ok(s)
692}
693
694/// Translates an OSAL virtual path to a host-specific local path.
695pub fn translate_path(
696    virtual_path: &str,
697) -> Result<CString<{ ffi::OS_MAX_LOCAL_PATH_LEN as usize }>> {
698    let c_virt = c_path_from_str(virtual_path)?;
699    let mut buffer = [0u8; ffi::OS_MAX_LOCAL_PATH_LEN as usize];
700    check(unsafe {
701        ffi::OS_TranslatePath(c_virt.as_ptr(), buffer.as_mut_ptr() as *mut libc::c_char)
702    })?;
703    let len = buffer.iter().position(|&b| b == 0).unwrap_or(0);
704    let mut s = CString::new();
705    s.extend_from_bytes(&buffer[..len])
706        .map_err(|_| CfsError::Osal(OsalError::NameTooLong))?;
707    Ok(s)
708}
709
710/// Retrieves information about the overall file system.
711pub fn get_fs_info() -> Result<FsInfo> {
712    let mut info = MaybeUninit::uninit();
713    check(unsafe { ffi::OS_GetFsInfo(info.as_mut_ptr()) })?;
714    Ok(unsafe { info.assume_init() }.into())
715}
716
717/// Gets the default virtual mount point for a file category (e.g., "/ram" or "/cf").
718pub fn get_default_mount_point(category: FileCategory) -> Option<&'static str> {
719    let ptr = unsafe { ffi::CFE_FS_GetDefaultMountPoint(category as u32) };
720    if ptr.is_null() {
721        None
722    } else {
723        Some(unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or(""))
724    }
725}
726
727/// Gets the default filename extension for a file category (e.g., ".so" or ".log").
728pub fn get_default_extension(category: FileCategory) -> Option<&'static str> {
729    let ptr = unsafe { ffi::CFE_FS_GetDefaultExtension(category as u32) };
730    if ptr.is_null() {
731        None
732    } else {
733        Some(unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or(""))
734    }
735}
736
737/// Registers a background file dump request with Executive Services.
738pub fn background_file_dump_request(meta: &mut FileWriteMetaData) -> Result<()> {
739    check(unsafe { ffi::CFE_FS_BackgroundFileDumpRequest(&mut meta.0) })?;
740    Ok(())
741}
742
743/// Checks if a background file dump request is currently pending.
744pub fn is_background_file_dump_pending(meta: &FileWriteMetaData) -> bool {
745    unsafe { ffi::CFE_FS_BackgroundFileDumpIsPending(&meta.0) }
746}
747
748/// Creates a fixed mapping between a physical host path and a virtual OSAL mount point.
749///
750/// This is typically called by the PSP/BSP before application startup to configure
751/// the virtual filesystem.
752pub fn add_fixed_map(phys_path: &str, virt_path: &str) -> Result<OsalId> {
753    let c_phys = c_path_from_str(phys_path)?;
754    let c_virt = c_path_from_str(virt_path)?;
755    let mut filesys_id = MaybeUninit::uninit();
756    check(unsafe {
757        ffi::OS_FileSysAddFixedMap(filesys_id.as_mut_ptr(), c_phys.as_ptr(), c_virt.as_ptr())
758    })?;
759    Ok(OsalId(unsafe { filesys_id.assume_init() }))
760}
761
762/// Initializes an existing file system on the target.
763///
764/// This is a low-level function for preparing a block device or memory region for use,
765/// but without creating it from scratch like `make_fs`.
766pub fn init_fs(
767    address: *mut u8,
768    devname: &str,
769    volname: &str,
770    blocksize: usize,
771    numblocks: usize,
772) -> Result<()> {
773    let c_dev = c_path_from_str(devname)?;
774    let c_vol = c_path_from_str(volname)?;
775    check(unsafe {
776        ffi::OS_initfs(
777            address as *mut libc::c_char,
778            c_dev.as_ptr(),
779            c_vol.as_ptr(),
780            blocksize,
781            numblocks,
782        )
783    })?;
784    Ok(())
785}
786
787/// Removes a file system mapping from OSAL.
788///
789/// This does not unmount the filesystem, but rather removes the OSAL entry for it.
790pub fn remove_fs(devname: &str) -> Result<()> {
791    let c_dev = c_path_from_str(devname)?;
792    check(unsafe { ffi::OS_rmfs(c_dev.as_ptr()) })?;
793    Ok(())
794}
795
796/// Checks the health of a file system and optionally repairs it.
797///
798/// Note: This functionality may not be implemented on all underlying operating systems.
799pub fn check_fs(path: &str, repair: bool) -> Result<()> {
800    let c_path = c_path_from_str(path)?;
801    check(unsafe { ffi::OS_chkfs(c_path.as_ptr(), repair) })?;
802    Ok(())
803}
804
805impl File {
806    /// Asynchronously reads from the file into the provided buffer.
807    pub fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> impl Future<Output = Result<usize>> + 'a {
808        core::future::poll_fn(|_| match self.timed_read(buf, 0) {
809            Ok(n) => core::task::Poll::Ready(Ok(n)),
810            Err(CfsError::Osal(OsalError::Timeout | OsalError::QueueEmpty)) => Poll::Pending,
811            Err(e) => core::task::Poll::Ready(Err(e)),
812        })
813    }
814
815    /// Asynchronously writes the provided buffer to the file.
816    pub fn write<'a>(&'a mut self, buf: &'a [u8]) -> impl Future<Output = Result<usize>> + 'a {
817        core::future::poll_fn(|_| match self.timed_write(buf, 0) {
818            Ok(n) => core::task::Poll::Ready(Ok(n)),
819            Err(CfsError::Osal(OsalError::Timeout | OsalError::QueueFull)) => Poll::Pending,
820            Err(e) => core::task::Poll::Ready(Err(e)),
821        })
822    }
823}