vexide_core/fs/
mod.rs

1//! Filesystem manipulation operations.
2//!
3//! This module contains basic methods to manipulate the contents of the brain's
4//! micro SDCard. All methods in this module represent VEXos filesystem operations.
5//!
6//! # VEXos Limitations
7//!
8//! While this module largely mimicks Rust's `std::fs` API, there are several major
9//! limitations in the VEXos filesystem. This module only provides a small subset of
10//! what would normally be expected in a typical Rust enviornment. Notably:
11//!
12//! - Files cannot be opened as read and write at the same time (only one). To read a
13//!   file that you’ve written to, you’ll need to drop your written file descriptor and
14//!   reopen it as readonly.
15//! - Files can be created, but not deleted or renamed.
16//! - Directories cannot be created or enumerated from the Brain, only top-level files.
17
18use alloc::{boxed::Box, ffi::CString, format, string::String, vec, vec::Vec};
19
20use no_std_io::io::{Read, Seek, Write};
21
22use crate::{
23    io,
24    path::{Path, PathBuf},
25};
26
27mod fs_str;
28
29pub use fs_str::{Display, FsStr, FsString};
30
31/// Options and flags which can be used to configure how a file is opened.
32///
33/// This builder exposes the ability to configure how a [`File`] is opened and
34/// what operations are permitted on the open file. The [`File::open`] and
35/// [`File::create`] methods are aliases for commonly used options using this
36/// builder.
37///
38/// Generally speaking, when using `OpenOptions`, you'll first call
39/// [`OpenOptions::new`], then chain calls to methods to set each option, then
40/// call [`OpenOptions::open`], passing the path of the file you're trying to
41/// open. This will give you a [`io::Result`] with a [`File`] inside that you
42/// can further operate on.
43///
44/// # Limitations
45///
46/// - Files MUST be opened in either `read` XOR `write` mode.
47/// - VEXos does not allow you to open a file configured as `read` and `write`
48///   at the same time. Doing so will return an error with `File::open`. This is
49///   a fundamental limtiation of the OS.
50///
51/// # Examples
52///
53/// Opening a file to read:
54///
55/// ```no_run
56/// use vexide::fs::OpenOptions;
57///
58/// let file = OpenOptions::new().read(true).open("foo.txt");
59/// ```
60///
61/// Opening a file for writing, as well as creating it if it doesn't exist:
62///
63/// ```no_run
64/// use vexide::fs::OpenOptions;
65///
66/// let file = OpenOptions::new()
67///             .write(true)
68///             .create(true)
69///             .open("foo.txt");
70/// ```
71#[allow(clippy::struct_excessive_bools)]
72#[derive(Clone, Debug)]
73pub struct OpenOptions {
74    read: bool,
75    write: bool,
76    append: bool,
77    truncate: bool,
78    create_new: bool,
79}
80
81impl OpenOptions {
82    /// Creates a blank new set of options ready for configuration.
83    ///
84    /// All options are initially set to `false`.
85    ///
86    /// # Examples
87    ///
88    /// ```no_run
89    /// use vexide::fs::OpenOptions;
90    ///
91    /// let mut options = OpenOptions::new();
92    /// let file = options.read(true).open("foo.txt");
93    /// ```
94    #[allow(clippy::new_without_default)]
95    #[must_use]
96    pub const fn new() -> OpenOptions {
97        OpenOptions {
98            read: false,
99            write: false,
100            append: false,
101            truncate: false,
102            create_new: false,
103        }
104    }
105
106    /// Sets the option for read access.
107    ///
108    /// This option, when true, will indicate that the file should be
109    /// `read`-able if opened.
110    ///
111    /// # Examples
112    ///
113    /// ```no_run
114    /// use vexide::fs::OpenOptions;
115    ///
116    /// let file = OpenOptions::new().read(true).open("foo.txt");
117    /// ```
118    pub const fn read(&mut self, read: bool) -> &mut Self {
119        self.read = read;
120        self
121    }
122
123    /// Sets the option for write access.
124    ///
125    /// This option, when true, will indicate that the file should be
126    /// `write`-able if opened.
127    ///
128    /// If the file already exists, any write calls on it will overwrite its
129    /// contents, without truncating it.
130    ///
131    /// # Examples
132    ///
133    /// ```no_run
134    /// use vexide::fs::OpenOptions;
135    ///
136    /// let file = OpenOptions::new().write(true).open("foo.txt");
137    /// ```
138    pub const fn write(&mut self, write: bool) -> &mut Self {
139        self.write = write;
140        self
141    }
142
143    /// Sets the option for the append mode.
144    ///
145    /// This option, when true, means that writes will append to a file instead
146    /// of overwriting previous contents.
147    /// Note that setting `.write(true).append(true)` has the same effect as
148    /// setting only `.append(true)`.
149    ///
150    /// Append mode guarantees that writes will be positioned at the current end of file,
151    /// even when there are other processes or threads appending to the same file. This is
152    /// unlike <code>[seek]\([SeekFrom]::[End]\(0))</code> followed by `write()`, which
153    /// has a race between seeking and writing during which another writer can write, with
154    /// our `write()` overwriting their data.
155    ///
156    /// Keep in mind that this does not necessarily guarantee that data appended by
157    /// different processes or threads does not interleave. The amount of data accepted a
158    /// single `write()` call depends on the operating system and file system. A
159    /// successful `write()` is allowed to write only part of the given data, so even if
160    /// you're careful to provide the whole message in a single call to `write()`, there
161    /// is no guarantee that it will be written out in full. If you rely on the filesystem
162    /// accepting the message in a single write, make sure that all data that belongs
163    /// together is written in one operation. This can be done by concatenating strings
164    /// before passing them to [`write()`].
165    ///
166    /// [SeekFrom]: io::SeekFrom
167    /// [Start]: io::SeekFrom::End
168    /// [End]: io::SeekFrom::End
169    /// [Seek]: io::Seek::seek
170    ///
171    /// ## Note
172    ///
173    /// This function doesn't create the file if it doesn't exist. Use the
174    /// [`OpenOptions::create`] method to do so.
175    ///
176    /// [`write()`]: Write::write "io::Write::write"
177    /// [`flush()`]: Write::flush "io::Write::flush"
178    ///
179    /// # Examples
180    ///
181    /// ```no_run
182    /// use vexide::fs::OpenOptions;
183    ///
184    /// let file = OpenOptions::new().append(true).open("foo.txt");
185    /// ```
186    pub const fn append(&mut self, append: bool) -> &mut Self {
187        self.append = append;
188        self
189    }
190
191    /// Sets the option for truncating a previous file.
192    ///
193    /// If a file is successfully opened with this option set it will truncate
194    /// the file to 0 length if it already exists.
195    ///
196    /// The file must be opened with write access for truncate to work.
197    ///
198    /// # Examples
199    ///
200    /// ```no_run
201    /// use vexide::fs::OpenOptions;
202    ///
203    /// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt");
204    /// ```
205    pub const fn truncate(&mut self, truncate: bool) -> &mut Self {
206        self.truncate = truncate;
207        self
208    }
209
210    /// Sets the option to create a new file, or open it if it already exists.
211    ///
212    /// In order for the file to be created, [`OpenOptions::write`] or
213    /// [`OpenOptions::append`] access must be used.
214    ///
215    /// See also [`write()`][self::write] for a simple function to create a file
216    /// with some given data.
217    ///
218    /// # Examples
219    ///
220    /// ```no_run
221    /// use vexide::fs::OpenOptions;
222    ///
223    /// let file = OpenOptions::new().write(true).create(true).open("foo.txt");
224    /// ```
225    pub const fn create(&mut self, create: bool) -> &mut Self {
226        self.write = create;
227        self
228    }
229
230    /// Sets the option to create a new file, failing if it already exists.
231    ///
232    /// No file is allowed to exist at the target location. In this way, if the call succeeds,
233    /// the file returned is guaranteed to be new. If a file exists at the target location,
234    /// creating a new file will fail with [`AlreadyExists`] or another error based on the
235    /// situation. See [`OpenOptions::open`] for a non-exhaustive list of likely errors.
236    ///
237    /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are
238    /// ignored.
239    ///
240    /// The file must be opened with write or append access in order to create
241    /// a new file.
242    ///
243    /// [`.create()`]: OpenOptions::create
244    /// [`.truncate()`]: OpenOptions::truncate
245    /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
246    ///
247    /// # Examples
248    ///
249    /// ```no_run
250    /// use vexide::fs::OpenOptions;
251    ///
252    /// let file = OpenOptions::new().write(true)
253    ///                              .create_new(true)
254    ///                              .open("foo.txt");
255    /// ```
256    pub const fn create_new(&mut self, create_new: bool) -> &mut Self {
257        self.create_new = create_new;
258        self
259    }
260
261    /// Opens a file at `path` with the options specified by `self`.
262    ///
263    /// # Errors
264    ///
265    /// This function will return an error under a number of different
266    /// circumstances. Some of these error conditions are listed here, together
267    /// with their [`io::ErrorKind`]. The mapping to [`io::ErrorKind`]s is not
268    /// part of the compatibility contract of the function.
269    ///
270    /// * [`NotFound`]: The specified file does not exist and neither `create`
271    ///   or `create_new` is set.
272    /// * [`AlreadyExists`]: `create_new` was specified and the file already
273    ///   exists.
274    /// * [`InvalidInput`]: Invalid combinations of open options (read/write
275    ///   access both specified, truncate without write access, no access mode
276    ///   set, etc.).
277    ///
278    /// The following errors don't match any existing [`io::ErrorKind`] at the moment:
279    /// * Filesystem-level errors: full disk, write permission
280    ///   requested on a read-only file system, exceeded disk quota, too many
281    ///   open files, too long filename.
282    ///
283    /// # Examples
284    ///
285    /// ```no_run
286    /// use vexide::fs::OpenOptions;
287    ///
288    /// let file = OpenOptions::new().read(true).open("foo.txt");
289    /// ```
290    ///
291    /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
292    /// [`InvalidInput`]: io::ErrorKind::InvalidInput
293    /// [`NotFound`]: io::ErrorKind::NotFound
294    /// [`PermissionDenied`]: io::ErrorKind::PermissionDenied
295    pub fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
296        // Mount sdcard volume as FAT filesystem
297        map_fresult(unsafe { vex_sdk::vexFileMountSD() })?;
298
299        let path = path.as_ref();
300
301        let path = CString::new(path.as_fs_str().as_encoded_bytes()).map_err(|_| {
302            io::Error::new(io::ErrorKind::InvalidData, "Path contained a null byte")
303        })?;
304
305        if self.write && self.read {
306            return Err(io::Error::new(
307                io::ErrorKind::InvalidInput,
308                "Files cannot be opened with read and write access",
309            ));
310        }
311        if self.create_new {
312            let file_exists = unsafe { vex_sdk::vexFileStatus(path.as_ptr()) };
313            if file_exists != 0 {
314                return Err(io::Error::new(
315                    io::ErrorKind::AlreadyExists,
316                    "File already exists",
317                ));
318            }
319        }
320
321        let file = if self.read && !self.write {
322            // The second argument to this function is ignored.
323            // Open in read only mode
324            unsafe { vex_sdk::vexFileOpen(path.as_ptr(), c"".as_ptr()) }
325        } else if self.write && self.append {
326            // Open in read/write and append mode
327            unsafe { vex_sdk::vexFileOpenWrite(path.as_ptr()) }
328        } else if self.write && self.truncate {
329            // Open in read/write mode
330            unsafe { vex_sdk::vexFileOpenCreate(path.as_ptr()) }
331        } else if self.write {
332            // Open in read/write and overwrite mode
333            unsafe {
334                // Open in read/write and append mode
335                let fd = vex_sdk::vexFileOpenWrite(path.as_ptr());
336                // Seek to beginning of the file
337                vex_sdk::vexFileSeek(fd, 0, 0);
338
339                fd
340            }
341        } else {
342            return Err(io::Error::new(
343                io::ErrorKind::InvalidInput,
344                "Files cannot be opened without read or write access",
345            ));
346        };
347
348        if file.is_null() {
349            Err(io::Error::new(
350                io::ErrorKind::NotFound,
351                "Could not open file",
352            ))
353        } else {
354            Ok(File {
355                fd: file,
356                write: self.write,
357            })
358        }
359    }
360}
361
362/// A structure representing a type of file with accessors for each file type.
363/// It is returned by [`Metadata::file_type`] method.
364#[derive(Copy, Clone, PartialEq, Eq, Hash)]
365pub struct FileType {
366    is_dir: bool,
367}
368
369impl FileType {
370    /// Tests whether this file type represents a directory. The
371    /// result is mutually exclusive to the results of [`is_file`];
372    /// only one of these tests may pass.
373    ///
374    /// [`is_file`]: FileType::is_file
375    ///
376    /// # Examples
377    ///
378    /// ```no_run
379    /// use vexide::fs;
380    ///
381    /// let metadata = fs::metadata("foo.txt")?;
382    /// let file_type = metadata.file_type();
383    ///
384    /// assert_eq!(file_type.is_dir(), false);
385    /// ```
386    #[must_use]
387    pub const fn is_dir(&self) -> bool {
388        self.is_dir
389    }
390
391    /// Tests whether this file type represents a regular file. The
392    /// result is mutually exclusive to the results of [`is_dir`];
393    /// only one of these tests may pass.
394    ///
395    /// When the goal is simply to read from (or write to) the source, the most
396    /// reliable way to test the source can be read (or written to) is to open
397    /// it. See [`File::open`] or [`OpenOptions::open`] for more information.
398    ///
399    /// [`is_dir`]: FileType::is_dir
400    ///
401    /// # Examples
402    ///
403    /// ```no_run
404    /// use vexide::fs;
405    ///
406    /// let metadata = fs::metadata("foo.txt")?;
407    /// let file_type = metadata.file_type();
408    ///
409    /// assert_eq!(file_type.is_file(), true);
410    /// ```
411    #[must_use]
412    pub const fn is_file(&self) -> bool {
413        !self.is_dir
414    }
415}
416
417/// Metadata information about a file.
418///
419/// This structure is returned from the [`metadata`] function or method
420/// and represents known metadata about a file such as its size and type.
421#[derive(Clone)]
422pub struct Metadata {
423    file_type: FileType,
424    size: u64,
425}
426
427impl Metadata {
428    fn from_fd(fd: *mut vex_sdk::FIL) -> io::Result<Self> {
429        let size = unsafe { vex_sdk::vexFileSize(fd) };
430
431        if size >= 0 {
432            Ok(Self {
433                size: size as u64,
434                file_type: FileType { is_dir: false },
435            })
436        } else {
437            Err(io::Error::new(
438                io::ErrorKind::InvalidData,
439                "Failed to get file size",
440            ))
441        }
442    }
443
444    fn from_path(path: &Path) -> io::Result<Self> {
445        let c_path = CString::new(path.as_fs_str().as_encoded_bytes()).map_err(|_| {
446            io::Error::new(io::ErrorKind::InvalidData, "Path contained a null byte")
447        })?;
448
449        let file_type = unsafe { vex_sdk::vexFileStatus(c_path.as_ptr()) };
450        let is_dir = file_type == 3;
451
452        // We can't get the size if its a directory because we cant open it as a file
453        if is_dir {
454            Ok(Self {
455                size: 0,
456                file_type: FileType { is_dir: true },
457            })
458        } else {
459            let mut opts = OpenOptions::new();
460            opts.read(true);
461            let file = opts.open(path)?;
462            let fd = file.fd;
463
464            Self::from_fd(fd)
465        }
466    }
467
468    /// Returns the file type for this metadata.
469    ///
470    /// # Examples
471    ///
472    /// ```no_run
473    /// fn main() -> std::io::Result<()> {
474    ///     use vexide::fs;
475    ///
476    ///     let metadata = fs::metadata("foo.txt")?;
477    ///
478    ///     println!("{:?}", metadata.file_type());
479    ///     Ok(())
480    /// }
481    /// ```
482    #[must_use]
483    pub const fn file_type(&self) -> FileType {
484        self.file_type
485    }
486
487    /// Tests whether this file type represents a directory. The
488    /// result is mutually exclusive to the results of [`is_file`];
489    /// only one of these tests may pass.
490    ///
491    /// [`is_file`]: FileType::is_file
492    ///
493    /// # Examples
494    ///
495    /// ```no_run
496    /// use vexide::fs;
497    ///
498    /// let metadata = fs::metadata("foo.txt")?;
499    ///
500    /// assert!(!metadata.is_dir());
501    /// ```
502    #[must_use]
503    pub const fn is_dir(&self) -> bool {
504        self.file_type.is_dir
505    }
506
507    /// Tests whether this file type represents a regular file. The
508    /// result is mutually exclusive to the results of [`is_dir`];
509    /// only one of these tests may pass.
510    ///
511    /// When the goal is simply to read from (or write to) the source, the most
512    /// reliable way to test the source can be read (or written to) is to open
513    /// it. See [`File::open`] or [`OpenOptions::open`] for more information.
514    ///
515    /// [`is_dir`]: FileType::is_dir
516    ///
517    /// # Examples
518    ///
519    /// ```no_run
520    /// use vexide::fs;
521    ///
522    /// let metadata = fs::metadata("foo.txt")?;
523    ///
524    /// assert!(metadata.is_file());
525    /// ```
526    #[must_use]
527    pub const fn is_file(&self) -> bool {
528        !self.file_type.is_dir
529    }
530
531    /// Returns the size of the file, in bytes, this metadata is for.
532    ///
533    /// # Examples
534    ///
535    /// ```no_run
536    /// use vexide::fs;
537    ///
538    /// let metadata = fs::metadata("foo.txt")?;
539    ///
540    /// assert_eq!(0, metadata.len());
541    /// ```
542    #[allow(clippy::len_without_is_empty)]
543    #[must_use]
544    pub fn len(&self) -> Option<u64> {
545        self.file_type.is_dir.then_some(self.size)
546    }
547}
548
549/// Represents a file in the file system.
550pub struct File {
551    fd: *mut vex_sdk::FIL,
552    write: bool,
553}
554impl File {
555    fn flush(&self) {
556        unsafe {
557            vex_sdk::vexFileSync(self.fd);
558        }
559    }
560
561    fn tell(&self) -> io::Result<u64> {
562        let position = unsafe { vex_sdk::vexFileTell(self.fd) };
563        position.try_into().map_err(|_| {
564            io::Error::new(
565                io::ErrorKind::InvalidData,
566                "Failed to get current location in file",
567            )
568        })
569    }
570
571    /// Attempts to open a file in read-only mode.
572    ///
573    /// See the [`OpenOptions::open`] method for more details.
574    ///
575    /// If you only need to read the entire file contents, consider
576    /// [`fs::read()`][self::read] or [`fs::read_to_string()`][self::read_to_string]
577    /// instead.
578    ///
579    /// # Errors
580    ///
581    /// This function will return an error if `path` does not already exist.
582    /// Other errors may also be returned according to [`OpenOptions::open`].
583    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
584        OpenOptions::new().read(true).open(path.as_ref())
585    }
586
587    /// Opens a file in write-only mode.
588    ///
589    /// This function will create a file if it does not exist,
590    /// and will truncate it if it does.
591    ///
592    /// Depending on the platform, this function may fail if the
593    /// full directory path does not exist.
594    /// See the [`OpenOptions::open`] function for more details.
595    ///
596    /// See also [`fs::write()`][self::write] for a simple function to
597    /// create a file with some given data.
598    ///
599    /// # Errors
600    ///
601    /// See [`OpenOptions::open`].
602    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
603        OpenOptions::new()
604            .write(true)
605            .create(true)
606            .truncate(true)
607            .open(path.as_ref())
608    }
609
610    /// Creates a new file in read-write mode; error if the file exists.
611    ///
612    /// This function will create a file if it does not exist, or return an error if it does. This
613    /// way, if the call succeeds, the file returned is guaranteed to be new.
614    /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`]
615    /// or another error based on the situation. See [`OpenOptions::open`] for a
616    /// non-exhaustive list of likely errors.
617    ///
618    /// This option is useful because it is atomic. Otherwise between checking whether a file
619    /// exists and creating a new one, the file may have been created by another process (a TOCTOU
620    /// race condition / attack).
621    ///
622    /// This can also be written using
623    /// `File::options().read(true).write(true).create_new(true).open(...)`.
624    ///
625    /// [`AlreadyExists`]: crate::io::ErrorKind::AlreadyExists
626    ///
627    /// # Errors
628    ///
629    /// See [`OpenOptions::open`].
630    pub fn create_new<P: AsRef<Path>>(path: P) -> io::Result<File> {
631        OpenOptions::new()
632            .read(true)
633            .write(true)
634            .create_new(true)
635            .open(path.as_ref())
636    }
637
638    /// Returns a new OpenOptions object.
639    ///
640    /// This function returns a new OpenOptions object that you can use to
641    /// open or create a file with specific options if `open()` or `create()`
642    /// are not appropriate.
643    ///
644    /// It is equivalent to `OpenOptions::new()`, but allows you to write more
645    /// readable code. Instead of
646    /// `OpenOptions::new().append(true).open("example.log")`,
647    /// you can write `File::options().append(true).open("example.log")`. This
648    /// also avoids the need to import `OpenOptions`.
649    ///
650    /// See the [`OpenOptions::new`] function for more details.
651    #[must_use]
652    pub const fn options() -> OpenOptions {
653        OpenOptions::new()
654    }
655
656    /// Queries metadata about the underlying file.
657    ///
658    /// # Errors
659    ///
660    /// * [`InvalidData`]: Internal filesystem error occurred.
661    ///
662    /// [`InvalidData`]: io::ErrorKind::InvalidData
663    pub fn metadata(&self) -> io::Result<Metadata> {
664        Metadata::from_fd(self.fd)
665    }
666
667    /// Attempts to sync all OS-internal file content and metadata to disk.
668    ///
669    /// This function will attempt to ensure that all in-memory data reaches the
670    /// filesystem before returning.
671    ///
672    /// This can be used to handle errors that would otherwise only be caught
673    /// when the `File` is closed, as dropping a `File` will ignore all errors.
674    /// Note, however, that `sync_all` is generally more expensive than closing
675    /// a file by dropping it, because the latter is not required to block until
676    /// the data has been written to the filesystem.
677    ///
678    /// If synchronizing the metadata is not required, use [`sync_data`] instead.
679    ///
680    /// [`sync_data`]: File::sync_data
681    ///
682    /// # Errors
683    ///
684    /// This function is infallible.
685    pub fn sync_all(&self) -> io::Result<()> {
686        self.flush();
687        Ok(())
688    }
689
690    /// This function is similar to [`sync_all`], except that it might not
691    /// synchronize file metadata to the filesystem.
692    ///
693    /// This is intended for use cases that must synchronize content, but don't
694    /// need the metadata on disk. The goal of this method is to reduce disk
695    /// operations.
696    ///
697    /// Note that some platforms may simply implement this in terms of
698    /// [`sync_all`].
699    ///
700    /// [`sync_all`]: File::sync_all
701    ///
702    /// # Errors
703    ///
704    /// This function is infallible.
705    pub fn sync_data(&self) -> io::Result<()> {
706        self.flush();
707        Ok(())
708    }
709}
710impl io::Write for File {
711    fn write(&mut self, buf: &[u8]) -> no_std_io::io::Result<usize> {
712        if !self.write {
713            return Err(io::Error::new(
714                io::ErrorKind::PermissionDenied,
715                "Files opened in read mode cannot be written to.",
716            ));
717        }
718
719        let len = buf.len();
720        let buf_ptr = buf.as_ptr();
721        let written =
722            unsafe { vex_sdk::vexFileWrite(buf_ptr.cast_mut().cast(), 1, len as _, self.fd) };
723        if written < 0 {
724            Err(io::Error::new(
725                io::ErrorKind::Other,
726                "Could not write to file",
727            ))
728        } else {
729            Ok(written as usize)
730        }
731    }
732
733    fn flush(&mut self) -> no_std_io::io::Result<()> {
734        File::flush(self);
735        Ok(())
736    }
737}
738impl io::Read for File {
739    fn read(&mut self, buf: &mut [u8]) -> no_std_io::io::Result<usize> {
740        if self.write {
741            return Err(io::Error::new(
742                io::ErrorKind::PermissionDenied,
743                "Files opened in write mode cannot be read from.",
744            ));
745        }
746
747        let len = buf.len() as _;
748        let buf_ptr = buf.as_mut_ptr();
749        let read = unsafe { vex_sdk::vexFileRead(buf_ptr.cast(), 1, len, self.fd) };
750        if read < 0 {
751            Err(io::Error::new(
752                io::ErrorKind::Other,
753                "Could not read from file",
754            ))
755        } else {
756            Ok(read as usize)
757        }
758    }
759}
760
761impl Seek for File {
762    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
763        const SEEK_SET: i32 = 0;
764        const SEEK_CUR: i32 = 1;
765        const SEEK_END: i32 = 2;
766
767        fn try_convert_offset<T: TryInto<u32>>(offset: T) -> io::Result<u32> {
768            offset.try_into().map_err(|_| {
769                io::Error::new(
770                    io::ErrorKind::InvalidInput,
771                    "Cannot seek to an offset too large to fit in a 32 bit integer",
772                )
773            })
774        }
775
776        match pos {
777            io::SeekFrom::Start(offset) => unsafe {
778                map_fresult(vex_sdk::vexFileSeek(
779                    self.fd,
780                    try_convert_offset(offset)?,
781                    SEEK_SET,
782                ))?;
783            },
784            io::SeekFrom::End(offset) => unsafe {
785                if offset >= 0 {
786                    map_fresult(vex_sdk::vexFileSeek(
787                        self.fd,
788                        try_convert_offset(offset)?,
789                        SEEK_END,
790                    ))?;
791                } else {
792                    // `vexFileSeek` does not support seeking with negative offset, meaning
793                    // we have to calculate the offset from the end of the file ourselves.
794                    map_fresult(vex_sdk::vexFileSeek(
795                        self.fd,
796                        try_convert_offset((self.metadata()?.size as i64) + offset)?,
797                        SEEK_SET,
798                    ))?;
799                }
800            },
801            io::SeekFrom::Current(offset) => unsafe {
802                if offset >= 0 {
803                    map_fresult(vex_sdk::vexFileSeek(
804                        self.fd,
805                        try_convert_offset(offset)?,
806                        SEEK_CUR,
807                    ))?;
808                } else {
809                    // `vexFileSeek` does not support seeking with negative offset, meaning
810                    // we have to calculate the offset from the stream position ourselves.
811                    map_fresult(vex_sdk::vexFileSeek(
812                        self.fd,
813                        try_convert_offset((self.tell()? as i64) + offset)?,
814                        SEEK_SET,
815                    ))?;
816                }
817            },
818        }
819
820        self.tell()
821    }
822}
823
824impl Drop for File {
825    fn drop(&mut self) {
826        // We do not need to sync because vexFileClose will do that for us
827        unsafe {
828            vex_sdk::vexFileClose(self.fd);
829        }
830    }
831}
832
833/// An entry returned by the [`ReadDir`] iterator.
834///
835/// A `DirEntry` represents an item within a directory on the Brain's Micro SD card.
836/// The Brain provides very little metadata on files, so only the base std `DirEntry` methods are supported.
837#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
838pub struct DirEntry {
839    base: FsString,
840    name: FsString,
841}
842impl DirEntry {
843    /// Returns the full path to the directory item.
844    ///
845    /// This path is creeated by joining the path of the call to [`read_dir`] to the name of the file.
846    ///
847    /// # Examples
848    ///
849    /// ```
850    /// for entry in fs::read_dir(".").unwrap() {
851    ///    println!("{:?}", entry.path());
852    /// }
853    /// ```
854    ///
855    /// This example will lead to output like:
856    /// ```text
857    /// "somefile.txt"
858    /// "breakingbadseason1.mp4"
859    /// "badapple.mp3"
860    /// ```
861    #[must_use]
862    pub fn path(&self) -> PathBuf {
863        PathBuf::from(format!("{}/{}", self.base.display(), self.name.display()))
864    }
865
866    /// Returns the metadata for the full path to the item.
867    ///
868    /// This is equivalent to calling [`metadata`] with the output from [`DirEntry::path`].
869    ///
870    /// # Errors
871    ///
872    /// This function will error if the path does not exist.
873    ///
874    /// # Examples
875    ///
876    /// ```
877    /// for entry in read_dir("somepath") {
878    ///     println!(
879    ///         "{:?} is a {}.",
880    ///         entry.path(),
881    ///         match entry.metadata().is_file() {
882    ///             true => "file",
883    ///             false => "folder"
884    ///         }
885    ///     );
886    /// }
887    /// ```
888    pub fn metadata(&self) -> io::Result<Metadata> {
889        let path = self.path();
890        Metadata::from_path(&path)
891    }
892
893    /// Returns the file type of the file that this [`DirEntry`] points to.
894    ///
895    /// This function is equivalent to getting the [`FileType`] from the metadata returned by [`DirEntry::metadata`].
896    ///
897    /// # Errors
898    ///
899    /// This function will error if the path does not exist.
900    ///
901    /// # Examples
902    ///
903    /// ```
904    /// for entry in read_dir("somepath") {
905    ///     println!(
906    ///         "{:?} is a {}.",
907    ///         entry.path(),
908    ///         match entry.file_type().is_file() {
909    ///             true => "file",
910    ///             false => "folder"
911    ///         }
912    ///     );
913    /// }
914    /// ```
915    pub fn file_type(&self) -> io::Result<FileType> {
916        Ok(self.metadata()?.file_type)
917    }
918
919    /// Returns the name of the file not including any leading components.
920    ///
921    /// The following paths will all lead to a file name of `foo`:
922    /// - `./foo`
923    /// - `../foo`
924    /// - `/some/global/foo`
925    #[must_use]
926    pub fn file_name(&self) -> FsString {
927        self.name.clone()
928    }
929}
930
931/// An iterator over the entries of a directory.
932///
933/// This iterator is returned from [`read_dir`] and will yield items of type [`DirEntry`].
934/// Information about the path is exposed through the [`DirEntry`]
935///
936/// Unlike the equivalent iterator in the standard library,
937/// this iterator does not return results as it is infallible.
938#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
939pub struct ReadDir {
940    idx: usize,
941    filenames: Box<[Option<FsString>]>,
942    base: FsString,
943}
944impl Iterator for ReadDir {
945    type Item = DirEntry;
946
947    fn next(&mut self) -> Option<Self::Item> {
948        if self.idx >= self.filenames.len() {
949            return None;
950        }
951        let entry = DirEntry {
952            base: self.base.clone(),
953            name: self.filenames[self.idx].take().unwrap(),
954        };
955        self.idx += 1;
956        Some(entry)
957    }
958}
959
960/// Returns an iterator over the items in a directory.
961///
962/// The returned [`ReadDir`] iterator will yield items of type [`DirEntry`].
963/// This is slightly different from the standard library API which yields items of type `io::Result<DirEntry>`.
964/// This is due to the fact that all directory items are gathered at iterator creation and the iterator itself is infallible.
965///
966/// # Errors
967///
968/// This function will error if:
969/// - The given path does not exist
970/// - The given path does not point to a directory.
971///
972/// # Examples
973///
974/// ```
975/// for entry in vexide::fs::read_dir("somefolder") {
976///     println!("{:?}", entry.path);
977/// }
978/// ```
979pub fn read_dir<P: AsRef<Path>>(path: P) -> io::Result<ReadDir> {
980    let path = path.as_ref();
981    let meta = metadata(path)?;
982    if meta.is_file() {
983        return Err(io::Error::new(
984            no_std_io::io::ErrorKind::InvalidInput,
985            "Cannot read the entries of a path that is not a directory.",
986        ));
987    }
988
989    let c_path = CString::new(path.as_fs_str().as_encoded_bytes())
990        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Path contained a null byte"))?;
991
992    let mut size_guess = 1024;
993    let mut last_buffer_size = None;
994
995    let mut filename_buffer;
996    loop {
997        filename_buffer = vec![0; size_guess];
998
999        unsafe {
1000            map_fresult(vex_sdk::vexFileDirectoryGet(
1001                c_path.as_ptr(),
1002                filename_buffer.as_mut_ptr().cast(),
1003                size_guess as _,
1004            ))?;
1005        }
1006
1007        let mut len = 0;
1008        for (i, byte) in filename_buffer.iter().enumerate().rev() {
1009            if *byte != 0 {
1010                len = i;
1011                break;
1012            }
1013        }
1014
1015        if last_buffer_size == Some(len) {
1016            break;
1017        }
1018
1019        last_buffer_size.replace(len);
1020        size_guess *= 2;
1021    }
1022
1023    let mut file_names = vec![];
1024
1025    let fs_str = unsafe { FsStr::from_inner(&filename_buffer) };
1026
1027    let mut filename_start_idx = 0;
1028    for (i, byte) in fs_str.as_encoded_bytes().iter().enumerate() {
1029        if *byte == b'\n' {
1030            let filename = &fs_str.as_encoded_bytes()[filename_start_idx..i];
1031            let filename = unsafe { FsString::from_encoded_bytes_unchecked(filename.to_vec()) };
1032            file_names.push(Some(filename));
1033            filename_start_idx = i + 1;
1034        }
1035    }
1036
1037    let base = path.inner.to_fs_string();
1038
1039    Ok(ReadDir {
1040        idx: 0,
1041        filenames: file_names.into_boxed_slice(),
1042        base,
1043    })
1044}
1045
1046fn map_fresult(fresult: vex_sdk::FRESULT) -> io::Result<()> {
1047    // VEX presumably uses a derivative of FatFs (most likely the xilffs library)
1048    // for sdcard filesystem functions.
1049    //
1050    // Documentation for each FRESULT originates from here:
1051    // <http://elm-chan.org/fsw/ff/doc/rc.html>
1052    match fresult {
1053        vex_sdk::FRESULT::FR_OK => Ok(()),
1054        vex_sdk::FRESULT::FR_DISK_ERR => Err(io::Error::new(
1055            io::ErrorKind::Uncategorized,
1056            "internal function reported an unrecoverable hard error",
1057        )),
1058        vex_sdk::FRESULT::FR_INT_ERR => Err(io::Error::new(
1059            io::ErrorKind::Uncategorized,
1060            "assertion failed and an insanity is detected in the internal process",
1061        )),
1062        vex_sdk::FRESULT::FR_NOT_READY => Err(io::Error::new(
1063            io::ErrorKind::Uncategorized,
1064            "the storage device could not be prepared to work",
1065        )),
1066        vex_sdk::FRESULT::FR_NO_FILE => Err(io::Error::new(
1067            io::ErrorKind::NotFound,
1068            "could not find the file in the directory",
1069        )),
1070        vex_sdk::FRESULT::FR_NO_PATH => Err(io::Error::new(
1071            io::ErrorKind::NotFound,
1072            "a directory in the path name could not be found",
1073        )),
1074        vex_sdk::FRESULT::FR_INVALID_NAME => Err(io::Error::new(
1075            io::ErrorKind::InvalidInput,
1076            "the given string is invalid as a path name",
1077        )),
1078        vex_sdk::FRESULT::FR_DENIED => Err(io::Error::new(
1079            io::ErrorKind::PermissionDenied,
1080            "the required access for this operation was denied",
1081        )),
1082        vex_sdk::FRESULT::FR_EXIST => Err(io::Error::new(
1083            io::ErrorKind::AlreadyExists,
1084            "an object with the same name already exists in the directory",
1085        )),
1086        vex_sdk::FRESULT::FR_INVALID_OBJECT => Err(io::Error::new(
1087            io::ErrorKind::Uncategorized,
1088            "invalid or null file/directory object",
1089        )),
1090        vex_sdk::FRESULT::FR_WRITE_PROTECTED => Err(io::Error::new(
1091            io::ErrorKind::PermissionDenied,
1092            "a write operation was performed on write-protected media",
1093        )),
1094        vex_sdk::FRESULT::FR_INVALID_DRIVE => Err(io::Error::new(
1095            io::ErrorKind::InvalidInput,
1096            "an invalid drive number was specified in the path name",
1097        )),
1098        vex_sdk::FRESULT::FR_NOT_ENABLED => Err(io::Error::new(
1099            io::ErrorKind::Uncategorized,
1100            "work area for the logical drive has not been registered",
1101        )),
1102        vex_sdk::FRESULT::FR_NO_FILESYSTEM => Err(io::Error::new(
1103            io::ErrorKind::Uncategorized,
1104            "valid FAT volume could not be found on the drive",
1105        )),
1106        vex_sdk::FRESULT::FR_MKFS_ABORTED => Err(io::Error::new(
1107            io::ErrorKind::Uncategorized,
1108            "failed to create filesystem volume",
1109        )),
1110        vex_sdk::FRESULT::FR_TIMEOUT => Err(io::Error::new(
1111            io::ErrorKind::TimedOut,
1112            "the function was canceled due to a timeout of thread-safe control",
1113        )),
1114        vex_sdk::FRESULT::FR_LOCKED => Err(io::Error::new(
1115            io::ErrorKind::Uncategorized,
1116            "the operation to the object was rejected by file sharing control",
1117        )),
1118        vex_sdk::FRESULT::FR_NOT_ENOUGH_CORE => Err(io::Error::new(
1119            io::ErrorKind::Uncategorized,
1120            "not enough memory for the operation",
1121        )),
1122        vex_sdk::FRESULT::FR_TOO_MANY_OPEN_FILES => Err(io::Error::new(
1123            io::ErrorKind::Uncategorized,
1124            "maximum number of open files has been reached",
1125        )),
1126        vex_sdk::FRESULT::FR_INVALID_PARAMETER => Err(io::Error::new(
1127            io::ErrorKind::InvalidInput,
1128            "a given parameter was invalid",
1129        )),
1130        _ => unreachable!(), // C-style enum
1131    }
1132}
1133
1134/// Copies the contents of one file to another.
1135///
1136/// If the destination file does not exist, it will be created.
1137/// If it does exist, it will be overwritten.
1138///
1139/// # Errors
1140///
1141/// This function will error if the source file does not exist
1142/// or any other error according to [`OpenOptions::open`].
1143pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<u64> {
1144    let from = read(from)?;
1145    let mut to = File::create(to)?;
1146    // Not completely accurate to std, but this is the best we've got
1147    let len = from.len() as u64;
1148
1149    to.write_all(&from)?;
1150
1151    Ok(len)
1152}
1153
1154/// Returns true if the path points to a file that exists on the filesystem.
1155///
1156/// Unlike in the standard library, this function cannot fail because there are not permissions.
1157///
1158/// # Examples
1159///
1160/// ```
1161/// use vexide::fs::*;
1162///
1163/// assert!(exists("existent.txt"));
1164/// assert!(!exists("nonexistent.txt"));
1165/// ```
1166pub fn exists<P: AsRef<Path>>(path: P) -> bool {
1167    let file_exists = unsafe { vex_sdk::vexFileStatus(path.as_ref().as_fs_str().as_ptr().cast()) };
1168    // Woop woop we've got a nullptr!
1169    file_exists != 0
1170}
1171
1172/// Gets the metadata for a file or path.
1173///
1174/// # Errors
1175///
1176/// This function will error if the path doesn't exist.
1177pub fn metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> {
1178    Metadata::from_path(path.as_ref())
1179}
1180
1181/// Reads the entire contents of a file into a vector.
1182///
1183/// This is a convenience function for using [`File::open`] and [`Read::read_to_end`].
1184///
1185/// # Errors
1186///
1187/// This function will error if the path doesn't exist
1188/// or any other error according to [`OpenOptions::open`].
1189pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
1190    let mut file = File::open(path)?;
1191    let mut buf = Vec::new();
1192    file.read_to_end(&mut buf)?;
1193    Ok(buf)
1194}
1195
1196/// Reads the entire contents of a file into a string.
1197///
1198/// This is a convenience function for using [`File::open`], [`Read::read_to_end`], and [`String::from_utf8`] (no_std_io does not support read_to_string).
1199///
1200/// # Errors
1201///
1202/// This function will error if the path doesn't exist, if the file is not valid UTF-8,
1203/// or any other error according to [`OpenOptions::open`].
1204pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
1205    let mut file = File::open(path)?;
1206    let mut buf = Vec::new();
1207    file.read_to_end(&mut buf)?;
1208    let string = String::from_utf8(buf)
1209        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "File was not valid UTF-8"))?;
1210    Ok(string)
1211}
1212
1213/// Writes an entire buffer to a file, replacing its contents.
1214///
1215/// This function will create a new file if it does not exist.
1216///
1217/// This is a convenience function for using [`File::create`] and [`Write::write_all`].
1218///
1219/// # Errors
1220///
1221/// This function will error if the path is invalid or for any other error according to [`OpenOptions::open`].
1222pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
1223    let mut file = File::create(path)?;
1224    file.write_all(contents.as_ref())
1225}