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}