luminol_filesystem/
lib.rs

1// Copyright (C) 2024 Melody Madeline Lyons
2//
3// This file is part of Luminol.
4//
5// Luminol is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Luminol is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Luminol.  If not, see <http://www.gnu.org/licenses/>.
17
18pub mod archiver;
19pub mod egui_bytes_loader;
20pub mod erased;
21pub mod list;
22pub mod path_cache;
23pub mod project;
24
25mod trie;
26pub use trie::*;
27
28#[cfg(not(target_arch = "wasm32"))]
29pub mod native;
30#[cfg(target_arch = "wasm32")]
31pub mod web;
32
33// Re-export platform specific filesystem as "host"
34// This means we need can use less #[cfg]s!
35#[cfg(not(target_arch = "wasm32"))]
36pub use native as host;
37#[cfg(target_arch = "wasm32")]
38pub use web as host;
39
40#[derive(thiserror::Error, Debug)]
41pub enum Error {
42    #[error("File or directory does not exist")]
43    NotExist,
44    #[error("Io error {0}")]
45    IoError(#[from] std::io::Error),
46    #[error("UTF-8 Error {0}")]
47    Utf8Error(#[from] std::string::FromUtf8Error),
48    #[error("Path is not valid UTF-8")]
49    PathUtf8Error,
50    #[error("Project not loaded")]
51    NotLoaded,
52    #[error("Operation not supported by this filesystem")]
53    NotSupported,
54    #[error("Archive header is incorrect")]
55    InvalidHeader,
56    #[error("Invalid archive version: {0} (supported versions are 1, 2 and 3)")]
57    InvalidArchiveVersion(u8),
58    #[error("No filesystems are loaded to perform this operation")]
59    NoFilesystems,
60    #[error("Unable to detect the project's RPG Maker version (perhaps you did not open an RPG Maker project?")]
61    UnableToDetectRMVer,
62    #[error("Cancelled loading project")]
63    CancelledLoading,
64    #[error("Your browser does not support File System Access API")]
65    Wasm32FilesystemNotSupported,
66    #[error("Invalid project folder")]
67    InvalidProjectFolder,
68}
69
70pub use color_eyre::Result;
71
72pub trait StdIoErrorExt {
73    // Add additional context to a `std::io::Result`.
74    fn wrap_io_err_with<C>(self, c: impl FnOnce() -> C) -> Self
75    where
76        Self: Sized,
77        C: std::fmt::Display + Send + Sync + 'static;
78
79    // Add additional context to a `std::io::Result`.
80    fn wrap_io_err<C>(self, c: C) -> Self
81    where
82        Self: Sized,
83        C: std::fmt::Display + Send + Sync + 'static,
84    {
85        self.wrap_io_err_with(|| c)
86    }
87
88    // Add additional context to a `std::io::Result`. This is an alias for `.wrap_io_err_with`.
89    fn with_io_context<C>(self, c: impl FnOnce() -> C) -> Self
90    where
91        Self: Sized,
92        C: std::fmt::Display + Send + Sync + 'static,
93    {
94        self.wrap_io_err_with(c)
95    }
96
97    // Add additional context to a `std::io::Result`. This is an alias for `.wrap_io_err`.
98    fn io_context<C>(self, c: C) -> Self
99    where
100        Self: Sized,
101        C: std::fmt::Display + Send + Sync + 'static,
102    {
103        self.wrap_io_err(c)
104    }
105}
106
107impl<T> StdIoErrorExt for std::io::Result<T> {
108    fn wrap_io_err_with<C>(self, c: impl FnOnce() -> C) -> Self
109    where
110        C: std::fmt::Display + Send + Sync + 'static,
111    {
112        self.map_err(|e| std::io::Error::new(e.kind(), color_eyre::eyre::eyre!(e).wrap_err(c())))
113    }
114}
115
116#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
117pub struct Metadata {
118    pub is_file: bool,
119    pub size: u64,
120}
121
122#[derive(Clone, PartialEq, Eq, Hash, Debug)]
123pub struct DirEntry {
124    pub path: camino::Utf8PathBuf,
125    pub metadata: Metadata,
126}
127
128impl DirEntry {
129    pub fn new(path: camino::Utf8PathBuf, metadata: Metadata) -> Self {
130        Self { path, metadata }
131    }
132
133    pub fn path(&self) -> &camino::Utf8Path {
134        &self.path
135    }
136
137    pub fn metadata(&self) -> Metadata {
138        self.metadata
139    }
140
141    pub fn file_name(&self) -> &str {
142        self.path
143            .file_name()
144            .expect("path created through DirEntry must have a filename")
145    }
146
147    pub fn into_path(self) -> camino::Utf8PathBuf {
148        self.path
149    }
150}
151
152bitflags::bitflags! {
153    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154    pub struct OpenFlags: u8 {
155        const Read = 0b00000001;
156        const Write = 0b00000010;
157        const Truncate = 0b00000100;
158        const Create = 0b00001000;
159    }
160}
161
162pub trait File: std::io::Read + std::io::Write + std::io::Seek + Send + Sync {
163    fn metadata(&self) -> std::io::Result<Metadata>;
164
165    /// Truncates or extends the size of the file. If the file is extended, the file will be
166    /// null-padded at the end. The file cursor never changes when truncating or extending, even if
167    /// the cursor would be put outside the file bounds by this operation.
168    fn set_len(&self, new_size: u64) -> std::io::Result<()>;
169
170    /// Casts a mutable reference to this file into `&mut luminol_filesystem::File`.
171    fn as_file(&mut self) -> &mut Self
172    where
173        Self: Sized,
174    {
175        self
176    }
177}
178
179impl<T> File for &mut T
180where
181    T: File + ?Sized,
182{
183    fn metadata(&self) -> std::io::Result<Metadata> {
184        (**self).metadata()
185    }
186
187    fn set_len(&self, new_size: u64) -> std::io::Result<()> {
188        (**self).set_len(new_size)
189    }
190}
191
192pub trait FileSystem: Send + Sync {
193    type File: File;
194
195    fn open_file(&self, path: impl AsRef<camino::Utf8Path>, flags: OpenFlags)
196        -> Result<Self::File>;
197
198    fn create_file(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Self::File> {
199        self.open_file(path, OpenFlags::Create | OpenFlags::Write)
200    }
201
202    fn metadata(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Metadata>;
203
204    fn rename(
205        &self,
206        from: impl AsRef<camino::Utf8Path>,
207        to: impl AsRef<camino::Utf8Path>,
208    ) -> Result<()>;
209
210    fn exists(&self, path: impl AsRef<camino::Utf8Path>) -> Result<bool>;
211
212    fn create_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()>;
213
214    fn remove_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()>;
215
216    fn remove_file(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()>;
217
218    fn remove(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()> {
219        let path = path.as_ref();
220        let metadata = self.metadata(path)?;
221        if metadata.is_file {
222            self.remove_file(path)
223        } else {
224            self.remove_dir(path)
225        }
226    }
227
228    fn read_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Vec<DirEntry>>;
229
230    /// Corresponds to [`std::fs::read()`].
231    /// Will open a file at the path and read the entire file into a buffer.
232    fn read(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Vec<u8>> {
233        use std::io::Read;
234
235        let path = path.as_ref();
236
237        let mut buf = Vec::with_capacity(self.metadata(path)?.size as usize);
238        let mut file = self.open_file(path, OpenFlags::Read)?;
239        file.read_to_end(&mut buf)?;
240
241        Ok(buf)
242    }
243
244    fn read_to_string(&self, path: impl AsRef<camino::Utf8Path>) -> Result<String> {
245        let buf = self.read(path)?;
246        String::from_utf8(buf).map_err(Into::into)
247    }
248
249    /// Corresponds to [`std::fs::write()`].
250    /// Will open a file at the path, create it if it does not exist (and truncate it)
251    /// and then write the provided bytes.
252    fn write(&self, path: impl AsRef<camino::Utf8Path>, data: impl AsRef<[u8]>) -> Result<()> {
253        use std::io::Write;
254
255        let mut file = self.open_file(
256            path,
257            OpenFlags::Write | OpenFlags::Truncate | OpenFlags::Create,
258        )?;
259        file.write_all(data.as_ref())?;
260        file.flush()?;
261
262        Ok(())
263    }
264}
265
266pub trait ReadDir {
267    fn read_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Vec<DirEntry>>;
268}
269
270impl<T> ReadDir for T
271where
272    T: FileSystem,
273{
274    fn read_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Vec<DirEntry>> {
275        self.read_dir(path)
276    }
277}