diff --git a/Cargo.lock b/Cargo.lock
index e1136fc7..65c12cbb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2553,6 +2553,12 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+[[package]]
+name = "iter-read"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b"
+
[[package]]
name = "itertools"
version = "0.11.0"
@@ -3040,6 +3046,7 @@ dependencies = [
"egui",
"flume",
"indexed_db_futures",
+ "iter-read",
"itertools",
"js-sys",
"luminol-config",
@@ -3047,12 +3054,14 @@ dependencies = [
"once_cell",
"oneshot",
"parking_lot",
+ "qp-trie",
"rand",
"rfd",
"ron",
"rust-ini",
"serde",
"slab",
+ "tempfile",
"thiserror",
"tracing",
"wasm-bindgen",
@@ -3471,6 +3480,12 @@ dependencies = [
"jni-sys",
]
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
[[package]]
name = "nix"
version = "0.23.2"
@@ -4327,6 +4342,16 @@ dependencies = [
"bytemuck",
]
+[[package]]
+name = "qp-trie"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ec628a7d1fc2c5f5a551eb34e01e08df62d55203640959a79a9a2859c797a97"
+dependencies = [
+ "new_debug_unreachable",
+ "unreachable",
+]
+
[[package]]
name = "quick-xml"
version = "0.31.0"
@@ -5819,6 +5844,15 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+dependencies = [
+ "void",
+]
+
[[package]]
name = "url"
version = "2.5.0"
@@ -5897,6 +5931,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
[[package]]
name = "vtparse"
version = "0.6.2"
diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml
index 53f3f209..74f471bd 100644
--- a/crates/filesystem/Cargo.toml
+++ b/crates/filesystem/Cargo.toml
@@ -39,12 +39,19 @@ tracing.workspace = true
luminol-config.workspace = true
+rand.workspace = true
+
+iter-read = "1.0.1"
+qp-trie = "0.8.2"
+
[target.'cfg(windows)'.dependencies]
winreg = "0.51.0"
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tempfile = "3.8.1"
+
[target.'cfg(target_arch = "wasm32")'.dependencies]
once_cell.workspace = true
-rand.workspace = true
slab.workspace = true
luminol-web = { version = "0.4.0", path = "../web/" }
@@ -69,4 +76,8 @@ web-sys = { version = "0.3", features = [
"FileSystemRemoveOptions",
"FileSystemWritableFileStream",
"WritableStream",
+
+ "Navigator",
+ "StorageManager",
+ "Window",
] }
diff --git a/crates/filesystem/src/archiver.rs b/crates/filesystem/src/archiver.rs
deleted file mode 100644
index f18a1726..00000000
--- a/crates/filesystem/src/archiver.rs
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright (C) 2023 Lily Lyons
-//
-// This file is part of Luminol.
-//
-// Luminol is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Luminol is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Luminol. If not, see .
-
-use itertools::Itertools;
-use std::io::{prelude::*, Cursor, SeekFrom};
-
-use crate::{DirEntry, Error, Metadata, OpenFlags};
-
-#[derive(Debug, Default)]
-pub struct FileSystem
-where
- T: Read + Write + Seek + Send + Sync,
-{
- files: dashmap::DashMap,
- directories: dashmap::DashMap>,
- archive: parking_lot::Mutex,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-struct Entry {
- offset: u64,
- size: u64,
- start_magic: u32,
-}
-
-const MAGIC: u32 = 0xDEADCAFE;
-const HEADER: &[u8] = b"RGSSAD\0";
-
-impl FileSystem
-where
- T: Read + Write + Seek + Send + Sync,
-{
- pub fn new(mut file: T) -> Result {
- let version = Self::read_header(&mut file)?;
-
- let files = dashmap::DashMap::new();
- let directories = dashmap::DashMap::new();
-
- fn read_u32(file: &mut F) -> Result
- where
- F: Read + Write + Seek + Send + Sync,
- {
- let mut buffer = [0; 4];
- file.read_exact(&mut buffer)?;
- Ok(u32::from_le_bytes(buffer))
- }
-
- fn read_u32_xor(file: &mut F, key: u32) -> Result
- where
- F: Read + Write + Seek + Send + Sync,
- {
- let result = read_u32(file)?;
- Ok(result ^ key)
- }
-
- match version {
- 1 | 2 => {
- let mut magic = MAGIC;
-
- while let Ok(name_len) = read_u32_xor(&mut file, Self::advance_magic(&mut magic)) {
- let mut name = vec![0; name_len as usize];
- file.read_exact(&mut name).unwrap();
- for byte in name.iter_mut() {
- let char = *byte ^ Self::advance_magic(&mut magic) as u8;
- if char == b'\\' {
- *byte = b'/';
- } else {
- *byte = char;
- }
- }
- let name = camino::Utf8PathBuf::from(String::from_utf8(name)?);
-
- Self::process_path(&directories, &name);
-
- let entry_len = read_u32_xor(&mut file, Self::advance_magic(&mut magic))?;
-
- let entry = Entry {
- size: entry_len as u64,
- offset: file.stream_position()?,
- start_magic: magic,
- };
-
- files.insert(name, entry);
-
- file.seek(SeekFrom::Start(entry.offset + entry.size))?;
- }
- }
- 3 => {
- let mut u32_buf = [0; 4];
- file.read_exact(&mut u32_buf)?;
-
- let base_magic = u32::from_le_bytes(u32_buf);
- let base_magic = (base_magic * 9) + 3;
-
- while let Ok(offset) = read_u32_xor(&mut file, base_magic) {
- if offset == 0 {
- break;
- }
-
- let entry_len = read_u32_xor(&mut file, base_magic)?;
- let magic = read_u32_xor(&mut file, base_magic)?;
- let name_len = read_u32_xor(&mut file, base_magic)?;
-
- let mut name = vec![0; name_len as usize];
- file.read_exact(&mut name)?;
- for (i, byte) in name.iter_mut().enumerate() {
- let char = *byte ^ (base_magic >> (8 * (i % 4))) as u8;
- if char == b'\\' {
- *byte = b'/';
- } else {
- *byte = char;
- }
- }
- let name = camino::Utf8PathBuf::from(String::from_utf8(name)?);
-
- Self::process_path(&directories, &name);
-
- let entry = Entry {
- size: entry_len as u64,
- offset: offset as u64,
- start_magic: magic,
- };
- files.insert(name, entry);
- }
- }
- _ => return Err(Error::InvalidHeader),
- }
-
- /*
- for dir in directories.iter() {
- println!("===========");
- println!("{}", dir.key());
- for i in dir.value().iter() {
- println!("{}", &*i);
- }
- println!("----------");
- }
- */
-
- Ok(FileSystem {
- files,
- directories,
- archive: parking_lot::Mutex::new(file),
- })
- }
-
- fn process_path(
- directories: &dashmap::DashMap>,
- path: impl AsRef,
- ) {
- for (a, b) in path.as_ref().ancestors().tuple_windows() {
- directories
- .entry(b.to_path_buf())
- .or_default()
- .insert(a.strip_prefix(b).unwrap_or(a).to_path_buf());
- }
- }
-
- fn advance_magic(magic: &mut u32) -> u32 {
- let old = *magic;
-
- *magic = magic.wrapping_mul(7).wrapping_add(3);
-
- old
- }
-
- fn read_header(file: &mut T) -> Result {
- let mut header_buf = [0; 8];
-
- file.read_exact(&mut header_buf)?;
-
- if !header_buf.starts_with(HEADER) {
- return Err(Error::InvalidHeader);
- }
-
- Ok(header_buf[7])
- }
-}
-
-#[derive(Debug)]
-pub struct File {
- cursor: Cursor>, // TODO WRITE
-}
-
-impl std::io::Write for File {
- fn write(&mut self, buf: &[u8]) -> std::io::Result {
- self.cursor.write(buf)
- }
-
- fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result {
- self.cursor.write_vectored(bufs)
- }
-
- fn flush(&mut self) -> std::io::Result<()> {
- Ok(())
- }
-}
-
-impl std::io::Read for File {
- fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
- self.cursor.read(buf)
- }
-
- fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result {
- self.cursor.read_vectored(bufs)
- }
-
- fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
- self.cursor.read_exact(buf)
- }
-}
-
-impl std::io::Seek for File {
- fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result {
- self.cursor.seek(pos)
- }
-
- fn stream_position(&mut self) -> std::io::Result {
- self.cursor.stream_position()
- }
-}
-
-impl crate::File for File {
- fn metadata(&self) -> crate::Result {
- Ok(Metadata {
- is_file: true,
- size: self.cursor.get_ref().len() as u64,
- })
- }
-}
-
-impl crate::FileSystem for FileSystem
-where
- T: Read + Write + Seek + Send + Sync + 'static,
-{
- type File = File;
-
- fn open_file(
- &self,
- path: impl AsRef,
- flags: OpenFlags,
- ) -> Result {
- if flags.contains(OpenFlags::Write) {
- return Err(Error::NotSupported);
- }
-
- let entry = self.files.get(path.as_ref()).ok_or(Error::NotExist)?;
- let mut buf = vec![0; entry.size as usize];
-
- {
- let mut archive = self.archive.lock();
- archive.seek(SeekFrom::Start(entry.offset))?;
- archive.read_exact(&mut buf)?;
- }
-
- let mut magic = entry.start_magic;
- let mut j = 0;
- for byte in buf.iter_mut() {
- if j == 4 {
- j = 0;
- magic = magic.wrapping_mul(7).wrapping_add(3);
- }
-
- *byte ^= magic.to_le_bytes()[j];
-
- j += 1;
- }
-
- let cursor = Cursor::new(buf);
- Ok(File { cursor })
- }
-
- fn metadata(&self, path: impl AsRef) -> Result {
- let path = path.as_ref();
- if let Some(entry) = self.files.get(path) {
- return Ok(Metadata {
- is_file: true,
- size: entry.size,
- });
- }
-
- if let Some(directory) = self.directories.get(path) {
- return Ok(Metadata {
- is_file: false,
- size: directory.len() as u64,
- });
- }
-
- Err(Error::NotExist)
- }
-
- fn rename(
- &self,
- _from: impl AsRef,
- _to: impl AsRef,
- ) -> std::result::Result<(), Error> {
- Err(Error::NotSupported)
- }
-
- fn create_dir(&self, _path: impl AsRef) -> Result<(), Error> {
- Err(Error::NotSupported)
- }
-
- fn exists(&self, path: impl AsRef) -> Result {
- let path = path.as_ref();
- Ok(self.files.contains_key(path) || self.directories.contains_key(path))
- }
-
- fn remove_dir(&self, _path: impl AsRef) -> Result<(), Error> {
- Err(Error::NotSupported)
- }
-
- fn remove_file(&self, _path: impl AsRef) -> Result<(), Error> {
- Err(Error::NotSupported)
- }
-
- fn read_dir(&self, path: impl AsRef) -> Result, Error> {
- let path = path.as_ref();
- let directory = self.directories.get(path).ok_or(Error::NotExist)?;
- directory
- .iter()
- .map(|entry| {
- let path = path.join(&*entry);
- let metadata = self.metadata(&path)?;
- Ok(DirEntry { path, metadata })
- })
- .try_collect()
- }
-}
diff --git a/crates/filesystem/src/archiver/file.rs b/crates/filesystem/src/archiver/file.rs
new file mode 100644
index 00000000..0a761d76
--- /dev/null
+++ b/crates/filesystem/src/archiver/file.rs
@@ -0,0 +1,224 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+use std::io::{
+ prelude::*,
+ BufReader,
+ ErrorKind::{InvalidData, PermissionDenied},
+ SeekFrom,
+};
+
+use super::util::{move_file_and_truncate, read_file_xor, regress_magic};
+use super::Trie;
+use crate::File as _;
+use crate::Metadata;
+
+#[derive(Debug)]
+pub struct File
+where
+ T: crate::File,
+{
+ pub(super) archive: Option>>,
+ pub(super) trie: Option>>,
+ pub(super) path: camino::Utf8PathBuf,
+ pub(super) read_allowed: bool,
+ pub(super) tmp: crate::host::File,
+ pub(super) modified: parking_lot::Mutex,
+ pub(super) version: u8,
+ pub(super) base_magic: u32,
+}
+
+impl Drop for File
+where
+ T: crate::File,
+{
+ fn drop(&mut self) {
+ if self.archive.is_some() {
+ let _ = self.flush();
+ }
+ }
+}
+
+impl std::io::Write for File
+where
+ T: crate::File,
+{
+ fn write(&mut self, buf: &[u8]) -> std::io::Result {
+ if self.archive.is_some() {
+ let mut modified = self.modified.lock();
+ *modified = true;
+ let count = self.tmp.write(buf)?;
+ Ok(count)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+
+ fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result {
+ if self.archive.is_some() {
+ let mut modified = self.modified.lock();
+ *modified = true;
+ let count = self.tmp.write_vectored(bufs)?;
+ Ok(count)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ let mut modified = self.modified.lock();
+ if !*modified {
+ return Ok(());
+ }
+
+ let Some(archive) = &self.archive else {
+ return Err(PermissionDenied.into());
+ };
+ let Some(trie) = &self.trie else {
+ return Err(PermissionDenied.into());
+ };
+ let mut archive = archive.lock();
+ let mut trie = trie.write();
+ let archive_len = archive.metadata()?.size;
+
+ let tmp_stream_position = self.tmp.stream_position()?;
+ self.tmp.flush()?;
+ self.tmp.seek(SeekFrom::Start(0))?;
+
+ // If the size of the file has changed, rotate the archive to place the file at the end of
+ // the archive before writing the new contents of the file
+ let mut entry = *trie.get_file(&self.path).ok_or(InvalidData)?;
+ let old_size = entry.size;
+ let new_size = self.tmp.metadata()?.size;
+ if old_size != new_size {
+ move_file_and_truncate(
+ &mut archive,
+ &mut trie,
+ &self.path,
+ self.version,
+ self.base_magic,
+ )?;
+ entry = *trie.get_file(&self.path).ok_or(InvalidData)?;
+
+ // Write the new length of the file to the archive
+ match self.version {
+ 1 | 2 => {
+ let mut magic = entry.start_magic;
+ regress_magic(&mut magic);
+ archive.seek(SeekFrom::Start(
+ entry.body_offset.checked_sub(4).ok_or(InvalidData)?,
+ ))?;
+ archive.write_all(&(new_size as u32 ^ magic).to_le_bytes())?;
+ }
+
+ 3 => {
+ archive.seek(SeekFrom::Start(entry.header_offset + 4))?;
+ archive.write_all(&(new_size as u32 ^ self.base_magic).to_le_bytes())?;
+ }
+
+ _ => return Err(InvalidData.into()),
+ }
+
+ // Write the new length of the file to the trie
+ trie.get_mut_file(&self.path).ok_or(InvalidData)?.size = new_size;
+ }
+
+ // Now write the new contents of the file
+ archive.seek(SeekFrom::Start(entry.body_offset))?;
+ let mut reader = BufReader::new(&mut self.tmp);
+ std::io::copy(
+ &mut read_file_xor(&mut reader, entry.start_magic),
+ archive.as_file(),
+ )?;
+ drop(reader);
+ self.tmp.seek(SeekFrom::Start(tmp_stream_position))?;
+
+ if old_size > new_size {
+ archive.set_len(
+ archive_len
+ .checked_sub(old_size)
+ .ok_or(InvalidData)?
+ .checked_add(new_size)
+ .ok_or(InvalidData)?,
+ )?;
+ }
+ archive.flush()?;
+ *modified = false;
+ Ok(())
+ }
+}
+
+impl std::io::Read for File
+where
+ T: crate::File,
+{
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ if self.read_allowed {
+ self.tmp.read(buf)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+
+ fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result {
+ if self.read_allowed {
+ self.tmp.read_vectored(bufs)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+
+ fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
+ if self.read_allowed {
+ self.tmp.read_exact(buf)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+}
+
+impl std::io::Seek for File
+where
+ T: crate::File,
+{
+ fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result {
+ self.tmp.seek(pos)
+ }
+
+ fn stream_position(&mut self) -> std::io::Result {
+ self.tmp.stream_position()
+ }
+}
+
+impl crate::File for File
+where
+ T: crate::File,
+{
+ fn metadata(&self) -> std::io::Result {
+ self.tmp.metadata()
+ }
+
+ fn set_len(&self, new_size: u64) -> std::io::Result<()> {
+ if self.archive.is_some() {
+ let mut modified = self.modified.lock();
+ *modified = true;
+ self.tmp.set_len(new_size)
+ } else {
+ Err(PermissionDenied.into())
+ }
+ }
+}
diff --git a/crates/filesystem/src/archiver/filesystem.rs b/crates/filesystem/src/archiver/filesystem.rs
new file mode 100644
index 00000000..f63c7c37
--- /dev/null
+++ b/crates/filesystem/src/archiver/filesystem.rs
@@ -0,0 +1,656 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+use itertools::Itertools;
+use rand::Rng;
+use std::io::{
+ prelude::*,
+ BufReader, BufWriter,
+ ErrorKind::{AlreadyExists, InvalidData},
+ SeekFrom,
+};
+
+use super::util::{
+ advance_magic, move_file_and_truncate, read_file_xor, read_header, read_u32_xor, regress_magic,
+};
+use super::{Entry, File, Trie, MAGIC};
+use crate::{DirEntry, Error, Metadata, OpenFlags};
+
+#[derive(Debug, Default)]
+pub struct FileSystem {
+ pub(super) trie: std::sync::Arc>,
+ pub(super) archive: std::sync::Arc>,
+ pub(super) version: u8,
+ pub(super) base_magic: u32,
+}
+
+impl FileSystem
+where
+ T: crate::File,
+{
+ pub fn new(mut file: T) -> Result {
+ let mut reader = BufReader::new(&mut file);
+
+ let version = read_header(&mut reader)?;
+
+ let mut trie = crate::FileSystemTrie::new();
+
+ let mut base_magic = MAGIC;
+
+ match version {
+ 1 | 2 => {
+ let mut magic = MAGIC;
+
+ while let Ok(path_len) = read_u32_xor(&mut reader, advance_magic(&mut magic)) {
+ let mut path = vec![0; path_len as usize];
+ reader.read_exact(&mut path)?;
+ for byte in path.iter_mut() {
+ let char = *byte ^ advance_magic(&mut magic) as u8;
+ if char == b'\\' {
+ *byte = b'/';
+ } else {
+ *byte = char;
+ }
+ }
+ let path = camino::Utf8PathBuf::from(String::from_utf8(path)?);
+
+ let entry_len = read_u32_xor(&mut reader, advance_magic(&mut magic))?;
+
+ let stream_position = reader.stream_position()?;
+ let entry = Entry {
+ size: entry_len as u64,
+ header_offset: stream_position
+ .checked_sub(path_len as u64 + 8)
+ .ok_or(Error::IoError(InvalidData.into()))?,
+ body_offset: stream_position,
+ start_magic: magic,
+ };
+
+ trie.create_file(path, entry);
+
+ reader.seek(SeekFrom::Start(entry.body_offset + entry.size))?;
+ }
+ }
+ 3 => {
+ let mut u32_buf = [0; 4];
+ reader.read_exact(&mut u32_buf)?;
+
+ base_magic = u32::from_le_bytes(u32_buf);
+ base_magic = (base_magic * 9) + 3;
+
+ while let Ok(body_offset) = read_u32_xor(&mut reader, base_magic) {
+ if body_offset == 0 {
+ break;
+ }
+ let header_offset = reader
+ .stream_position()?
+ .checked_sub(4)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+
+ let entry_len = read_u32_xor(&mut reader, base_magic)?;
+ let magic = read_u32_xor(&mut reader, base_magic)?;
+ let path_len = read_u32_xor(&mut reader, base_magic)?;
+
+ let mut path = vec![0; path_len as usize];
+ reader.read_exact(&mut path)?;
+ for (i, byte) in path.iter_mut().enumerate() {
+ let char = *byte ^ (base_magic >> (8 * (i % 4))) as u8;
+ if char == b'\\' {
+ *byte = b'/';
+ } else {
+ *byte = char;
+ }
+ }
+ let path = camino::Utf8PathBuf::from(String::from_utf8(path)?);
+
+ let entry = Entry {
+ size: entry_len as u64,
+ header_offset,
+ body_offset: body_offset as u64,
+ start_magic: magic,
+ };
+ trie.create_file(path, entry);
+ }
+ }
+ _ => return Err(Error::InvalidHeader),
+ }
+
+ Ok(FileSystem {
+ trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)),
+ archive: std::sync::Arc::new(parking_lot::Mutex::new(file)),
+ version,
+ base_magic,
+ })
+ }
+}
+
+impl crate::FileSystem for FileSystem
+where
+ T: crate::File,
+{
+ type File = File;
+
+ fn open_file(
+ &self,
+ path: impl AsRef,
+ flags: OpenFlags,
+ ) -> Result {
+ let path = path.as_ref();
+ let mut tmp = crate::host::File::new()?;
+ let mut created = false;
+
+ {
+ let mut archive = self.archive.lock();
+ let mut trie = self.trie.write();
+ let entry = *trie.get_file(path).ok_or(Error::NotExist)?;
+ archive.seek(SeekFrom::Start(entry.body_offset))?;
+
+ if flags.contains(OpenFlags::Create) && !trie.contains_file(path) {
+ created = true;
+ match self.version {
+ 1 | 2 => {
+ archive.seek(SeekFrom::Start(8))?;
+ let mut reader = BufReader::new(archive.as_file());
+ let mut magic = MAGIC;
+ while let Ok(path_len) =
+ read_u32_xor(&mut reader, advance_magic(&mut magic))
+ {
+ for _ in 0..path_len {
+ advance_magic(&mut magic);
+ }
+ reader.seek(SeekFrom::Current(path_len as i64))?;
+ let entry_len = read_u32_xor(&mut reader, advance_magic(&mut magic))?;
+ reader.seek(SeekFrom::Current(entry_len as i64))?;
+ }
+ drop(reader);
+
+ let archive_len = archive.seek(SeekFrom::End(0))?;
+ let mut writer = BufWriter::new(archive.as_file());
+ writer.write_all(
+ &(path.as_str().bytes().len() as u32 ^ advance_magic(&mut magic))
+ .to_le_bytes(),
+ )?;
+ writer.write_all(
+ &path
+ .as_str()
+ .bytes()
+ .map(|b| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ advance_magic(&mut magic) as u8
+ })
+ .collect_vec(),
+ )?;
+ writer.write_all(&advance_magic(&mut magic).to_le_bytes())?;
+ writer.flush()?;
+ drop(writer);
+
+ trie.create_file(
+ path,
+ Entry {
+ header_offset: archive_len,
+ body_offset: archive_len + path.as_str().bytes().len() as u64 + 8,
+ size: 0,
+ start_magic: magic,
+ },
+ );
+ }
+
+ 3 => {
+ let mut tmp = crate::host::File::new()?;
+
+ let extra_data_len = path.as_str().bytes().len() as u32 + 16;
+ let mut headers = Vec::new();
+
+ archive.seek(SeekFrom::Start(12))?;
+ let mut reader = BufReader::new(archive.as_file());
+ let mut position = 12;
+ while let Ok(offset) = read_u32_xor(&mut reader, self.base_magic) {
+ if offset == 0 {
+ break;
+ }
+ headers.push((position, offset));
+ reader.seek(SeekFrom::Current(8))?;
+ let path_len = read_u32_xor(&mut reader, self.base_magic)?;
+ position = reader.seek(SeekFrom::Current(path_len as i64))?;
+ }
+ drop(reader);
+
+ archive.seek(SeekFrom::Start(position))?;
+ std::io::copy(archive.as_file(), &mut tmp)?;
+ tmp.flush()?;
+
+ let magic: u32 = rand::thread_rng().gen();
+ let archive_len = archive.metadata()?.size as u32 + extra_data_len;
+ let mut writer = BufWriter::new(archive.as_file());
+ for (position, offset) in headers {
+ writer.seek(SeekFrom::Start(position))?;
+ writer.write_all(
+ &((offset + extra_data_len) ^ self.base_magic).to_le_bytes(),
+ )?;
+ }
+ writer.seek(SeekFrom::Start(position))?;
+ writer.write_all(&(archive_len ^ self.base_magic).to_le_bytes())?;
+ writer.write_all(&self.base_magic.to_le_bytes())?;
+ writer.write_all(&(magic ^ self.base_magic).to_le_bytes())?;
+ writer.write_all(
+ &(path.as_str().bytes().len() as u32 ^ self.base_magic).to_le_bytes(),
+ )?;
+ writer.write_all(
+ &path
+ .as_str()
+ .bytes()
+ .enumerate()
+ .map(|(i, b)| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ (self.base_magic >> (8 * (i % 4))) as u8
+ })
+ .collect_vec(),
+ )?;
+ tmp.seek(SeekFrom::Start(0))?;
+ std::io::copy(&mut tmp, &mut writer)?;
+ writer.flush()?;
+ drop(writer);
+
+ trie.create_file(
+ path,
+ Entry {
+ header_offset: position,
+ body_offset: archive_len as u64,
+ size: 0,
+ start_magic: magic,
+ },
+ );
+ }
+
+ _ => return Err(Error::NotSupported),
+ }
+ } else if !flags.contains(OpenFlags::Truncate) {
+ let mut adapter = BufReader::new(archive.as_file().take(entry.size));
+ std::io::copy(
+ &mut read_file_xor(&mut adapter, entry.start_magic),
+ &mut tmp,
+ )?;
+ tmp.flush()?;
+ }
+ }
+
+ tmp.seek(SeekFrom::Start(0))?;
+ Ok(File {
+ archive: flags
+ .contains(OpenFlags::Write)
+ .then(|| self.archive.clone()),
+ trie: flags.contains(OpenFlags::Write).then(|| self.trie.clone()),
+ path: path.to_owned(),
+ read_allowed: flags.contains(OpenFlags::Read),
+ tmp,
+ modified: parking_lot::Mutex::new(
+ !created && flags.contains(OpenFlags::Write) && flags.contains(OpenFlags::Truncate),
+ ),
+ version: self.version,
+ base_magic: self.base_magic,
+ })
+ }
+
+ fn metadata(&self, path: impl AsRef) -> Result {
+ let path = path.as_ref();
+ let trie = self.trie.read();
+ if let Some(entry) = trie.get_file(path) {
+ Ok(Metadata {
+ is_file: true,
+ size: entry.size,
+ })
+ } else if let Some(size) = trie.get_dir_size(path) {
+ Ok(Metadata {
+ is_file: false,
+ size: size as u64,
+ })
+ } else {
+ Err(Error::NotExist)
+ }
+ }
+
+ fn rename(
+ &self,
+ from: impl AsRef,
+ to: impl AsRef,
+ ) -> std::result::Result<(), Error> {
+ let from = from.as_ref();
+ let to = to.as_ref();
+
+ let mut archive = self.archive.lock();
+ let mut trie = self.trie.write();
+
+ if trie.contains_dir(from) {
+ return Err(Error::NotSupported);
+ }
+ if trie.contains(to) {
+ return Err(Error::IoError(AlreadyExists.into()));
+ }
+ if !trie.contains_dir(from.parent().ok_or(Error::NotExist)?) {
+ return Err(Error::NotExist);
+ }
+ let Some(old_entry) = trie.get_file(from).copied() else {
+ return Err(Error::NotExist);
+ };
+
+ let archive_len = archive.metadata()?.size;
+ let from_len = from.as_str().bytes().len();
+ let to_len = to.as_str().bytes().len();
+
+ if from_len != to_len {
+ match self.version {
+ 1 | 2 => {
+ // Move the file contents into a temporary file
+ let mut tmp = crate::host::File::new()?;
+ archive.seek(SeekFrom::Start(old_entry.body_offset))?;
+ let mut reader = BufReader::new(archive.as_file().take(old_entry.size));
+ std::io::copy(
+ &mut read_file_xor(&mut reader, old_entry.start_magic),
+ &mut tmp,
+ )?;
+ tmp.flush()?;
+ drop(reader);
+
+ // Move the file to the end so that we can change the header size
+ move_file_and_truncate(
+ &mut archive,
+ &mut trie,
+ from,
+ self.version,
+ self.base_magic,
+ )?;
+ let mut new_entry = *trie
+ .get_file(from)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+ trie.remove_file(from)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+ new_entry.size = old_entry.size;
+
+ let mut magic = new_entry.start_magic;
+ regress_magic(&mut magic);
+ regress_magic(&mut magic);
+ for _ in from.as_str().bytes() {
+ regress_magic(&mut magic);
+ }
+
+ // Regenerate the header
+ archive.seek(SeekFrom::Start(new_entry.header_offset))?;
+ let mut writer = BufWriter::new(archive.as_file());
+ writer.write_all(&(to_len as u32 ^ advance_magic(&mut magic)).to_le_bytes())?;
+ writer.write_all(
+ &to.as_str()
+ .bytes()
+ .map(|b| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ advance_magic(&mut magic) as u8
+ })
+ .collect_vec(),
+ )?;
+ writer.write_all(
+ &(old_entry.size as u32 ^ advance_magic(&mut magic)).to_le_bytes(),
+ )?;
+
+ new_entry.start_magic = magic;
+
+ // Move the file contents to the end
+ tmp.seek(SeekFrom::Start(0))?;
+ let mut reader = BufReader::new(&mut tmp);
+ std::io::copy(&mut read_file_xor(&mut reader, magic), &mut writer)?;
+ writer.flush()?;
+ drop(writer);
+
+ trie.create_file(to, new_entry);
+ }
+
+ 3 => {
+ // Move everything after the header into a temporary file
+ let mut tmp = crate::host::File::new()?;
+ archive.seek(SeekFrom::Start(
+ old_entry.header_offset + from_len as u64 + 16,
+ ))?;
+ std::io::copy(archive.as_file(), &mut tmp)?;
+ tmp.flush()?;
+
+ // Change the path
+ archive.seek(SeekFrom::Start(old_entry.header_offset + 12))?;
+ let mut writer = BufWriter::new(archive.as_file());
+ writer.write_all(&(to_len as u32 ^ self.base_magic).to_le_bytes())?;
+ writer.write_all(
+ &to.as_str()
+ .bytes()
+ .enumerate()
+ .map(|(i, b)| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ (self.base_magic >> (8 * (i % 4))) as u8
+ })
+ .collect_vec(),
+ )?;
+ trie.remove_file(from)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+ trie.create_file(to, old_entry);
+
+ // Move everything else back
+ tmp.seek(SeekFrom::Start(0))?;
+ std::io::copy(&mut tmp, &mut writer)?;
+ writer.flush()?;
+ drop(writer);
+
+ // Update all of the offsets in the headers
+ archive.seek(SeekFrom::Start(12))?;
+ let mut reader = BufReader::new(archive.as_file());
+ let mut headers = Vec::new();
+ while let Ok(current_body_offset) = read_u32_xor(&mut reader, self.base_magic) {
+ if current_body_offset == 0 {
+ break;
+ }
+ let current_header_offset = reader
+ .stream_position()?
+ .checked_sub(4)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+ reader.seek(SeekFrom::Current(8))?;
+ let current_path_len = read_u32_xor(&mut reader, self.base_magic)?;
+
+ let mut current_path = vec![0; current_path_len as usize];
+ reader.read_exact(&mut current_path)?;
+ for (i, byte) in current_path.iter_mut().enumerate() {
+ let char = *byte ^ (self.base_magic >> (8 * (i % 4))) as u8;
+ if char == b'\\' {
+ *byte = b'/';
+ } else {
+ *byte = char;
+ }
+ }
+ let current_path = String::from_utf8(current_path)
+ .map_err(|_| Error::IoError(InvalidData.into()))?;
+
+ let current_body_offset = (current_body_offset as u64)
+ .checked_add_signed(to_len as i64 - from_len as i64)
+ .ok_or(Error::IoError(InvalidData.into()))?;
+ trie.get_mut_file(current_path)
+ .ok_or(Error::IoError(InvalidData.into()))?
+ .body_offset = current_body_offset;
+ headers.push((current_header_offset, current_body_offset as u32));
+ }
+ drop(reader);
+ let mut writer = BufWriter::new(archive.as_file());
+ for (position, offset) in headers {
+ writer.seek(SeekFrom::Start(position))?;
+ writer.write_all(&(offset ^ self.base_magic).to_le_bytes())?;
+ }
+ writer.flush()?;
+ drop(writer);
+ }
+
+ _ => return Err(Error::IoError(InvalidData.into())),
+ }
+
+ if to_len < from_len {
+ archive.set_len(
+ archive_len
+ .checked_add_signed(to_len as i64 - from_len as i64)
+ .ok_or(Error::IoError(InvalidData.into()))?,
+ )?;
+ archive.flush()?;
+ }
+ } else {
+ match self.version {
+ 1 | 2 => {
+ let mut magic = old_entry.start_magic;
+ for _ in from.as_str().bytes() {
+ regress_magic(&mut magic);
+ }
+ archive.seek(SeekFrom::Start(old_entry.header_offset + 4))?;
+ archive.write_all(
+ &to.as_str()
+ .bytes()
+ .map(|b| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ advance_magic(&mut magic) as u8
+ })
+ .collect_vec(),
+ )?;
+ archive.flush()?;
+ }
+
+ 3 => {
+ archive.seek(SeekFrom::Start(old_entry.header_offset + 16))?;
+ archive.write_all(
+ &to.as_str()
+ .bytes()
+ .enumerate()
+ .map(|(i, b)| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ (self.base_magic >> (8 * (i % 4))) as u8
+ })
+ .collect_vec(),
+ )?;
+ archive.flush()?;
+ }
+
+ _ => return Err(Error::IoError(InvalidData.into())),
+ }
+ }
+
+ Ok(())
+ }
+
+ fn create_dir(&self, path: impl AsRef) -> Result<(), Error> {
+ let path = path.as_ref();
+ let mut trie = self.trie.write();
+ if trie.contains(path) {
+ return Err(Error::IoError(AlreadyExists.into()));
+ }
+ if let Some(parent) = path.parent() {
+ if !trie.contains_dir(parent) {
+ return Err(Error::NotExist);
+ }
+ }
+ trie.create_dir(path);
+ Ok(())
+ }
+
+ fn exists(&self, path: impl AsRef) -> Result {
+ let trie = self.trie.read();
+ Ok(trie.contains(path))
+ }
+
+ fn remove_dir(&self, path: impl AsRef) -> Result<(), Error> {
+ let path = path.as_ref();
+ if !self.trie.read().contains_dir(path) {
+ return Err(Error::NotExist);
+ }
+
+ let paths = self
+ .trie
+ .read()
+ .iter_prefix(path)
+ .ok_or(Error::NotExist)?
+ .map(|(k, _)| k)
+ .collect_vec();
+ for file_path in paths {
+ self.remove_file(file_path)?;
+ }
+
+ self.trie
+ .write()
+ .remove_dir(path)
+ .then_some(())
+ .ok_or(Error::NotExist)?;
+ Ok(())
+ }
+
+ fn remove_file(&self, path: impl AsRef) -> Result<(), Error> {
+ let path = path.as_ref();
+ let path_len = path.as_str().bytes().len() as u64;
+ let mut archive = self.archive.lock();
+ let mut trie = self.trie.write();
+
+ let entry = *trie.get_file(path).ok_or(Error::NotExist)?;
+ let archive_len = archive.metadata()?.size;
+
+ move_file_and_truncate(&mut archive, &mut trie, path, self.version, self.base_magic)?;
+
+ match self.version {
+ 1 | 2 => {
+ archive.set_len(
+ archive_len
+ .checked_sub(entry.size + path_len + 8)
+ .ok_or(Error::IoError(InvalidData.into()))?,
+ )?;
+ archive.flush()?;
+ }
+
+ 3 => {
+ // Remove the header of the deleted file
+ let mut tmp = crate::host::File::new()?;
+ archive.seek(SeekFrom::Start(entry.header_offset + path_len + 16))?;
+ std::io::copy(archive.as_file(), &mut tmp)?;
+ tmp.flush()?;
+ tmp.seek(SeekFrom::Start(0))?;
+ archive.seek(SeekFrom::Start(entry.header_offset))?;
+ std::io::copy(&mut tmp, archive.as_file())?;
+
+ archive.set_len(
+ archive_len
+ .checked_sub(entry.size + path_len + 16)
+ .ok_or(Error::IoError(InvalidData.into()))?,
+ )?;
+ archive.flush()?;
+ }
+
+ _ => return Err(Error::NotSupported),
+ }
+
+ trie.remove_file(path);
+ Ok(())
+ }
+
+ fn read_dir(&self, path: impl AsRef) -> Result, Error> {
+ let path = path.as_ref();
+ let trie = self.trie.read();
+ if let Some(iter) = trie.iter_dir(path) {
+ iter.map(|(name, _)| {
+ let path = path.join(name);
+ let metadata = self.metadata(&path)?;
+ Ok(DirEntry { path, metadata })
+ })
+ .try_collect()
+ } else {
+ Err(Error::NotExist)
+ }
+ }
+}
diff --git a/crates/filesystem/src/archiver/mod.rs b/crates/filesystem/src/archiver/mod.rs
new file mode 100644
index 00000000..a47e2d20
--- /dev/null
+++ b/crates/filesystem/src/archiver/mod.rs
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+mod filesystem;
+pub use filesystem::FileSystem;
+
+mod file;
+pub use file::File;
+
+mod util;
+
+type Trie = crate::FileSystemTrie;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct Entry {
+ header_offset: u64,
+ body_offset: u64,
+ size: u64,
+ start_magic: u32,
+}
+
+const MAGIC: u32 = 0xDEADCAFE;
+const HEADER: &[u8] = b"RGSSAD\0";
diff --git a/crates/filesystem/src/archiver/util.rs b/crates/filesystem/src/archiver/util.rs
new file mode 100644
index 00000000..5ebdfb93
--- /dev/null
+++ b/crates/filesystem/src/archiver/util.rs
@@ -0,0 +1,295 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+use itertools::Itertools;
+use std::io::{prelude::*, BufReader, BufWriter, ErrorKind::InvalidData, SeekFrom};
+
+use super::{Trie, HEADER};
+use crate::Error;
+
+fn read_u32(mut file: impl Read) -> std::io::Result {
+ let mut buffer = [0; 4];
+ file.read_exact(&mut buffer)?;
+ Ok(u32::from_le_bytes(buffer))
+}
+
+pub(super) fn read_u32_xor(file: impl Read, key: u32) -> std::io::Result {
+ let result = read_u32(file)?;
+ Ok(result ^ key)
+}
+
+pub(super) fn read_file_xor(file: impl Read, start_magic: u32) -> impl Read {
+ let iter = file.bytes().scan((start_magic, 0), |state, maybe_byte| {
+ let Ok(byte) = maybe_byte else { return None };
+ let (mut magic, mut j) = *state;
+
+ if j == 4 {
+ j = 0;
+ magic = magic.wrapping_mul(7).wrapping_add(3);
+ }
+ let byte = byte ^ magic.to_le_bytes()[j];
+ j += 1;
+
+ *state = (magic, j);
+ Some(byte)
+ });
+ iter_read::IterRead::new(iter)
+}
+
+pub(super) fn advance_magic(magic: &mut u32) -> u32 {
+ let old = *magic;
+
+ *magic = magic.wrapping_mul(7).wrapping_add(3);
+
+ old
+}
+
+pub(super) fn regress_magic(magic: &mut u32) -> u32 {
+ let old = *magic;
+
+ *magic = magic.wrapping_sub(3).wrapping_mul(3067833783);
+
+ old
+}
+
+pub(super) fn read_header(mut file: impl Read) -> Result {
+ let mut header_buf = [0; 8];
+
+ file.read_exact(&mut header_buf)?;
+
+ if !header_buf.starts_with(HEADER) {
+ return Err(Error::InvalidHeader);
+ }
+
+ Ok(header_buf[7])
+}
+
+/// Moves a file within an archive to the end of the archive and truncates the file's length to 0.
+/// Does NOT truncate the actual archive to the correct length afterwards.
+pub(super) fn move_file_and_truncate(
+ archive: &mut parking_lot::MutexGuard<'_, T>,
+ trie: &mut parking_lot::RwLockWriteGuard<'_, Trie>,
+ path: impl AsRef,
+ version: u8,
+ base_magic: u32,
+) -> std::io::Result<()>
+where
+ T: crate::File,
+{
+ let path = path.as_ref();
+ let path_len = path.as_str().bytes().len() as u64;
+ let archive_len = archive.metadata()?.size;
+ archive.seek(SeekFrom::Start(0))?;
+
+ let mut entry = *trie.get_file(path).ok_or(InvalidData)?;
+ match version {
+ 1 | 2 => {
+ let mut tmp = crate::host::File::new()?;
+ archive.seek(SeekFrom::Start(entry.body_offset + entry.size))?;
+ let mut reader = BufReader::new(archive.as_file());
+ let mut writer = BufWriter::new(&mut tmp);
+
+ let mut reader_magic = entry.start_magic;
+
+ // Determine what the magic value was for the beginning of the modified file's
+ // header
+ let mut writer_magic = entry.start_magic;
+ regress_magic(&mut writer_magic);
+ regress_magic(&mut writer_magic);
+ for _ in path.as_str().bytes() {
+ regress_magic(&mut writer_magic);
+ }
+
+ // Re-encrypt the headers and data for the files after the modified file into a
+ // temporary file
+ while let Ok(current_path_len) =
+ read_u32_xor(&mut reader, advance_magic(&mut reader_magic))
+ {
+ let mut current_path = vec![0; current_path_len as usize];
+ reader.read_exact(&mut current_path)?;
+ for byte in current_path.iter_mut() {
+ let char = *byte ^ advance_magic(&mut reader_magic) as u8;
+ if char == b'\\' {
+ *byte = b'/';
+ } else {
+ *byte = char;
+ }
+ }
+ let current_path = String::from_utf8(current_path).map_err(|_| InvalidData)?;
+ let current_entry = trie.get_mut_file(¤t_path).ok_or(InvalidData)?;
+ reader.seek(SeekFrom::Start(current_entry.body_offset))?;
+ advance_magic(&mut reader_magic);
+
+ writer.write_all(
+ &(current_path_len ^ advance_magic(&mut writer_magic)).to_le_bytes(),
+ )?;
+ writer.write_all(
+ ¤t_path
+ .as_str()
+ .bytes()
+ .map(|b| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ advance_magic(&mut writer_magic) as u8
+ })
+ .collect_vec(),
+ )?;
+ writer.write_all(
+ &(current_entry.size as u32 ^ advance_magic(&mut writer_magic)).to_le_bytes(),
+ )?;
+ std::io::copy(
+ &mut read_file_xor(
+ &mut read_file_xor(
+ &mut (&mut reader).take(current_entry.size),
+ reader_magic,
+ ),
+ writer_magic,
+ ),
+ &mut writer,
+ )?;
+
+ current_entry.header_offset = current_entry
+ .header_offset
+ .checked_sub(entry.size + path_len + 8)
+ .ok_or(InvalidData)?;
+ current_entry.body_offset = current_entry
+ .body_offset
+ .checked_sub(entry.size + path_len + 8)
+ .ok_or(InvalidData)?;
+ current_entry.start_magic = writer_magic;
+ }
+
+ // Write the header of the modified file at the end of the temporary file
+ writer
+ .write_all(&(path_len as u32 ^ advance_magic(&mut writer_magic)).to_le_bytes())?;
+ writer.write_all(
+ &path
+ .as_str()
+ .bytes()
+ .map(|b| {
+ let b = if b == b'/' { b'\\' } else { b };
+ b ^ advance_magic(&mut writer_magic) as u8
+ })
+ .collect_vec(),
+ )?;
+ writer.write_all(&advance_magic(&mut writer_magic).to_le_bytes())?;
+ writer.flush()?;
+ drop(reader);
+ drop(writer);
+
+ // Write the contents of the temporary file into the archive, starting from
+ // where the modified file's header was
+ tmp.seek(SeekFrom::Start(0))?;
+ archive.seek(SeekFrom::Start(entry.header_offset))?;
+ std::io::copy(&mut tmp, archive.as_file())?;
+
+ entry.start_magic = writer_magic;
+ entry.body_offset = archive_len.checked_sub(entry.size).ok_or(InvalidData)?;
+ entry.header_offset = entry
+ .body_offset
+ .checked_sub(path_len + 8)
+ .ok_or(InvalidData)?;
+ entry.size = 0;
+ *trie.get_mut_file(path).ok_or(InvalidData)? = entry;
+
+ Ok(())
+ }
+
+ 3 => {
+ let mut tmp = crate::host::File::new()?;
+
+ // Copy the contents of the files after the modified file into a temporary file
+ archive.seek(SeekFrom::Start(entry.body_offset + entry.size))?;
+ std::io::copy(archive.as_file(), &mut tmp)?;
+ tmp.flush()?;
+
+ // Copy the contents of the temporary file back into the archive starting from where
+ // the modified file was
+ tmp.seek(SeekFrom::Start(0))?;
+ archive.seek(SeekFrom::Start(entry.body_offset))?;
+ std::io::copy(&mut tmp, archive.as_file())?;
+
+ // Find all of the files in the archive with offsets greater than the original
+ // offset of the modified file and decrement them accordingly
+ archive.seek(SeekFrom::Start(12))?;
+ let mut reader = BufReader::new(archive.as_file());
+ let mut headers = Vec::new();
+ while let Ok(current_body_offset) = read_u32_xor(&mut reader, base_magic) {
+ if current_body_offset == 0 {
+ break;
+ }
+ let current_header_offset = reader
+ .stream_position()?
+ .checked_sub(4)
+ .ok_or(InvalidData)?;
+ let current_body_offset = current_body_offset as u64;
+ reader.seek(SeekFrom::Current(8))?;
+ let current_path_len = read_u32_xor(&mut reader, base_magic)?;
+ let should_truncate = current_header_offset == entry.header_offset;
+ if current_body_offset <= entry.body_offset && !should_truncate {
+ reader.seek(SeekFrom::Current(current_path_len as i64))?;
+ continue;
+ }
+
+ let mut current_path = vec![0; current_path_len as usize];
+ reader.read_exact(&mut current_path)?;
+ for (i, byte) in current_path.iter_mut().enumerate() {
+ let char = *byte ^ (base_magic >> (8 * (i % 4))) as u8;
+ if char == b'\\' {
+ *byte = b'/';
+ } else {
+ *byte = char;
+ }
+ }
+ let current_path = String::from_utf8(current_path).map_err(|_| InvalidData)?;
+
+ let current_body_offset = if should_truncate {
+ archive_len.checked_sub(entry.size).ok_or(InvalidData)?
+ } else {
+ current_body_offset
+ .checked_sub(entry.size)
+ .ok_or(InvalidData)?
+ };
+
+ trie.get_mut_file(current_path)
+ .ok_or(InvalidData)?
+ .body_offset = current_body_offset;
+ headers.push((
+ current_header_offset,
+ current_body_offset as u32,
+ should_truncate,
+ ));
+ }
+ drop(reader);
+ let mut writer = BufWriter::new(archive.as_file());
+ for (position, offset, should_truncate) in headers {
+ writer.seek(SeekFrom::Start(position))?;
+ writer.write_all(&(offset ^ base_magic).to_le_bytes())?;
+ if should_truncate {
+ writer.write_all(&base_magic.to_le_bytes())?;
+ }
+ }
+ writer.flush()?;
+ drop(writer);
+
+ trie.get_mut_file(path).ok_or(InvalidData)?.size = 0;
+
+ Ok(())
+ }
+
+ _ => Err(InvalidData.into()),
+ }
+}
diff --git a/crates/filesystem/src/erased.rs b/crates/filesystem/src/erased.rs
index e92b79a4..606dfb6c 100644
--- a/crates/filesystem/src/erased.rs
+++ b/crates/filesystem/src/erased.rs
@@ -98,9 +98,13 @@ where
}
impl File for Box {
- fn metadata(&self) -> Result {
+ fn metadata(&self) -> std::io::Result {
self.as_ref().metadata()
}
+
+ fn set_len(&self, new_size: u64) -> std::io::Result<()> {
+ self.as_ref().set_len(new_size)
+ }
}
impl crate::FileSystem for dyn ErasedFilesystem {
diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs
index e970e54d..7871edaa 100644
--- a/crates/filesystem/src/lib.rs
+++ b/crates/filesystem/src/lib.rs
@@ -22,6 +22,9 @@ pub mod list;
pub mod path_cache;
pub mod project;
+mod trie;
+pub use trie::*;
+
#[cfg(not(target_arch = "wasm32"))]
pub mod native;
#[cfg(target_arch = "wasm32")]
@@ -111,7 +114,20 @@ bitflags::bitflags! {
}
pub trait File: std::io::Read + std::io::Write + std::io::Seek + Send + Sync + 'static {
- fn metadata(&self) -> Result;
+ fn metadata(&self) -> std::io::Result;
+
+ /// Truncates or extends the size of the file. If the file is extended, the file will be
+ /// null-padded at the end. The file cursor never changes when truncating or extending, even if
+ /// the cursor would be put outside the file bounds by this operation.
+ fn set_len(&self, new_size: u64) -> std::io::Result<()>;
+
+ /// Casts a mutable reference to this file into `&mut luminol_filesystem::File`.
+ fn as_file(&mut self) -> &mut Self
+ where
+ Self: Sized,
+ {
+ self
+ }
}
pub trait FileSystem: Send + Sync + 'static {
diff --git a/crates/filesystem/src/list.rs b/crates/filesystem/src/list.rs
index 63608d2b..57b3259a 100644
--- a/crates/filesystem/src/list.rs
+++ b/crates/filesystem/src/list.rs
@@ -42,8 +42,9 @@ impl crate::FileSystem for FileSystem {
flags: OpenFlags,
) -> Result {
let path = path.as_ref();
+ let parent = path.parent().unwrap_or(path);
for fs in self.filesystems.iter() {
- if fs.exists(path)? || flags.contains(OpenFlags::Create) {
+ if fs.exists(path)? || (flags.contains(OpenFlags::Create) && fs.exists(parent)?) {
return fs.open_file(path, flags);
}
}
diff --git a/crates/filesystem/src/native.rs b/crates/filesystem/src/native.rs
index cbda94cf..31e9b326 100644
--- a/crates/filesystem/src/native.rs
+++ b/crates/filesystem/src/native.rs
@@ -16,13 +16,16 @@
// along with Luminol. If not, see .
use itertools::Itertools;
-use crate::{DirEntry, File, Metadata, OpenFlags, Result};
+use crate::{DirEntry, Metadata, OpenFlags, Result};
#[derive(Debug, Clone)]
pub struct FileSystem {
root_path: camino::Utf8PathBuf,
}
+#[derive(Debug)]
+pub struct File(std::fs::File);
+
impl FileSystem {
pub fn new(root_path: impl AsRef) -> Self {
Self {
@@ -61,7 +64,7 @@ impl FileSystem {
}
impl crate::FileSystem for FileSystem {
- type File = std::fs::File;
+ type File = File;
fn open_file(
&self,
@@ -76,6 +79,7 @@ impl crate::FileSystem for FileSystem {
.truncate(flags.contains(OpenFlags::Truncate))
.open(path)
.map_err(Into::into)
+ .map(File)
}
fn metadata(&self, path: impl AsRef) -> Result {
@@ -139,12 +143,53 @@ impl crate::FileSystem for FileSystem {
}
}
-impl File for std::fs::File {
- fn metadata(&self) -> Result {
- let metdata = self.metadata()?;
+impl File {
+ /// Creates a new empty temporary file with read-write permissions.
+ pub fn new() -> std::io::Result {
+ tempfile::tempfile().map(File)
+ }
+}
+
+impl crate::File for File {
+ fn metadata(&self) -> std::io::Result {
+ let metdata = self.0.metadata()?;
Ok(Metadata {
is_file: metdata.is_file(),
size: metdata.len(),
})
}
+
+ fn set_len(&self, new_size: u64) -> std::io::Result<()> {
+ self.0.set_len(new_size)
+ }
+}
+
+impl std::io::Read for File {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ self.0.read(buf)
+ }
+
+ fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result {
+ self.0.read_vectored(bufs)
+ }
+}
+
+impl std::io::Write for File {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result {
+ self.0.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.0.flush()
+ }
+
+ fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result {
+ self.0.write_vectored(bufs)
+ }
+}
+
+impl std::io::Seek for File {
+ fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result {
+ self.0.seek(pos)
+ }
}
diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs
index 6fd164f6..74f114be 100644
--- a/crates/filesystem/src/project.rs
+++ b/crates/filesystem/src/project.rs
@@ -558,12 +558,19 @@ impl std::io::Seek for File {
}
impl crate::File for File {
- fn metadata(&self) -> Result {
+ fn metadata(&self) -> std::io::Result {
match self {
File::Host(h) => crate::File::metadata(h),
File::Loaded(l) => l.metadata(),
}
}
+
+ fn set_len(&self, new_size: u64) -> std::io::Result<()> {
+ match self {
+ File::Host(f) => f.set_len(new_size),
+ File::Loaded(f) => f.set_len(new_size),
+ }
+ }
}
impl crate::FileSystem for FileSystem {
diff --git a/crates/filesystem/src/trie.rs b/crates/filesystem/src/trie.rs
new file mode 100644
index 00000000..26ec283b
--- /dev/null
+++ b/crates/filesystem/src/trie.rs
@@ -0,0 +1,392 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+use qp_trie::wrapper::{BStr, BString};
+
+type Trie = qp_trie::Trie>;
+type DirTrie = qp_trie::Trie>;
+
+pub struct FileSystemTrieDirIter<'a, T>(FileSystemTrieDirIterInner<'a, T>);
+
+enum FileSystemTrieDirIterInner<'a, T> {
+ Direct(qp_trie::Iter<'a, BString, Option>, usize),
+ Prefix(std::iter::Once<(&'a str, Option<&'a T>)>),
+}
+
+impl<'a, T> std::iter::FusedIterator for FileSystemTrieDirIter<'a, T> {}
+
+impl<'a, T> std::iter::ExactSizeIterator for FileSystemTrieDirIter<'a, T> {
+ fn len(&self) -> usize {
+ match &self.0 {
+ FileSystemTrieDirIterInner::Direct(_, len) => *len,
+ FileSystemTrieDirIterInner::Prefix(_) => 1,
+ }
+ }
+}
+
+impl<'a, T> Iterator for FileSystemTrieDirIter<'a, T> {
+ type Item = (&'a str, Option<&'a T>);
+ fn next(&mut self) -> Option {
+ match &mut self.0 {
+ FileSystemTrieDirIterInner::Direct(iter, _) => iter
+ .next()
+ .map(|(key, value)| (key.as_str(), value.as_ref())),
+ FileSystemTrieDirIterInner::Prefix(iter) => iter.next(),
+ }
+ }
+}
+
+pub struct FileSystemTrieIter<'a, T> {
+ path: camino::Utf8PathBuf,
+ trie: &'a Trie,
+ root_done: bool,
+ trie_iter: Option>>,
+ dir_iter: Option<(camino::Utf8PathBuf, qp_trie::Iter<'a, BString, Option>)>,
+}
+
+impl<'a, T> std::iter::FusedIterator for FileSystemTrieIter<'a, T> {}
+
+impl<'a, T> Iterator for FileSystemTrieIter<'a, T> {
+ type Item = (camino::Utf8PathBuf, &'a T);
+ fn next(&mut self) -> Option {
+ loop {
+ if let Some((prefix, dir_iter)) = &mut self.dir_iter {
+ match dir_iter.next() {
+ Some((filename, Some(value))) => {
+ return Some((prefix.join(filename.as_str()), value));
+ }
+ None => {
+ self.dir_iter = None;
+ self.root_done = true;
+ }
+ _ => {}
+ }
+ } else if let Some(trie_iter) = &mut self.trie_iter {
+ let (prefix, dir_trie) = trie_iter.next()?;
+ self.dir_iter = Some((prefix.as_str().to_owned().into(), dir_trie.iter()));
+ } else if self.path.as_str() == "" {
+ self.root_done = true;
+ self.trie_iter = Some(self.trie.iter())
+ } else if self.root_done {
+ self.trie_iter = Some(
+ self.trie
+ .iter_prefix::(format!("{}/", self.path).as_str().into()),
+ );
+ } else {
+ self.dir_iter = self
+ .trie
+ .get_str(self.path.as_str())
+ .map(|t| (self.path.clone(), t.iter()));
+ }
+ }
+ }
+}
+
+/// A container for a directory tree-like cache where the "files" are a data type of your choice.
+#[derive(Debug, Clone)]
+pub struct FileSystemTrie(Trie);
+
+impl Default for FileSystemTrie {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl FileSystemTrie {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Adds a directory to the trie. If parent directories do not exist, they will be created.
+ pub fn create_dir(&mut self, path: impl AsRef) {
+ let path = path.as_ref();
+
+ // Nothing to do if the path is already in the trie
+ if self.0.contains_key_str(path.as_str()) {
+ return;
+ }
+
+ // Otherwise, find the longest prefix in the trie that is shared with some directory
+ // path that is in the trie
+ let prefix = self.get_dir_prefix(path).as_str().to_string();
+
+ // Check if the trie already contains an entry for this prefix, and if not, create one
+ if !self.0.contains_key_str(&prefix) {
+ let mut dir_trie = DirTrie::new();
+ let prefix_with_trailing_slash = format!("{}/", &prefix);
+ if let Some((child_path, _)) = self
+ .0
+ .iter_prefix_str(if prefix.is_empty() {
+ ""
+ } else {
+ &prefix_with_trailing_slash
+ })
+ .nth(1)
+ {
+ // If there is a different path in the trie that has this as a prefix (there can be
+ // at most one), add it to this directory entry
+ dir_trie.insert_str(
+ camino::Utf8Path::new(child_path.as_str())
+ .strip_prefix(&prefix)
+ .unwrap()
+ .iter()
+ .next()
+ .unwrap(),
+ None,
+ );
+ }
+ self.0.insert_str(&prefix, dir_trie);
+ }
+
+ // Add the new path to the entry at this prefix
+ let prefix_trie = self.0.get_mut_str(&prefix).unwrap();
+ prefix_trie.insert_str(
+ path.strip_prefix(&prefix).unwrap().iter().next().unwrap(),
+ None,
+ );
+
+ // Add the actual entry for the new path
+ self.0.insert_str(path.as_str(), DirTrie::new());
+ }
+
+ /// Adds a file to the trie with the given value. If parent directories do not exist, they will
+ /// be created. If there already was a file at the given path, the original value will be
+ /// returned.
+ pub fn create_file(&mut self, path: impl AsRef, value: T) -> Option {
+ let path = path.as_ref().to_owned();
+
+ let dir = path.parent().unwrap_or(camino::Utf8Path::new(""));
+ let filename = path.iter().next_back().unwrap();
+
+ // Add the parent directory to the trie
+ self.create_dir(dir);
+ let dir_trie = self.0.get_mut_str(dir.as_str()).unwrap();
+
+ if let Some(option) = dir_trie.get_mut_str(filename) {
+ // If the path is already in the trie, replace the value and return the original
+ option.replace(value)
+ } else {
+ // Add the file to the parent directory's entry
+ dir_trie.insert_str(filename, Some(value));
+ None
+ }
+ }
+
+ /// Returns whether or not a directory exists at the given path.
+ pub fn contains_dir(&self, path: impl AsRef) -> bool {
+ let path = path.as_ref().as_str();
+ if path.is_empty() {
+ return true;
+ }
+ self.0.contains_key_str(path)
+ || (path.is_empty() && self.0.count() != 0)
+ || self
+ .0
+ .longest_common_prefix::(format!("{path}/").as_str().into())
+ .as_str()
+ .len()
+ == path.len() + 1
+ }
+
+ /// Returns whether or not a file exists at the given path.
+ pub fn contains_file(&self, path: impl AsRef) -> bool {
+ let path = path.as_ref();
+ let Some(filename) = path.iter().next_back() else {
+ return false;
+ };
+ let dir = path.parent().unwrap_or(camino::Utf8Path::new(""));
+ self.0
+ .get_str(dir.as_str())
+ .map_or(false, |dir_trie| dir_trie.contains_key_str(filename))
+ }
+
+ /// Returns whether or not a file or directory exists at the given path.
+ pub fn contains(&self, path: impl AsRef) -> bool {
+ self.contains_file(&path) || self.contains_dir(&path)
+ }
+
+ /// Gets the number of direct children in the directory at the given path, if it exists.
+ pub fn get_dir_size(&self, path: impl AsRef) -> Option {
+ let path = path.as_ref().as_str();
+ if let Some(dir_trie) = self.0.get_str(path) {
+ Some(dir_trie.count())
+ } else if (path.is_empty() && self.0.count() != 0)
+ || self
+ .0
+ .longest_common_prefix::(format!("{path}/").as_str().into())
+ .as_str()
+ .len()
+ == path.len() + 1
+ {
+ Some(1)
+ } else {
+ None
+ }
+ }
+
+ /// Gets an immutable reference to the value in the file at the given path, if it exists.
+ pub fn get_file(&self, path: impl AsRef) -> Option<&T> {
+ let path = path.as_ref();
+ let Some(filename) = path.iter().next_back() else {
+ return None;
+ };
+ let dir = path.parent().unwrap_or(camino::Utf8Path::new(""));
+ self.0
+ .get_str(dir.as_str())
+ .and_then(|dir_trie| dir_trie.get_str(filename))
+ .and_then(|o| o.as_ref())
+ }
+
+ /// Gets a mutable reference to the value in the file at the given path, if it exists.
+ pub fn get_mut_file(&mut self, path: impl AsRef) -> Option<&mut T> {
+ let path = path.as_ref();
+ let Some(filename) = path.iter().next_back() else {
+ return None;
+ };
+ let dir = path.parent().unwrap_or(camino::Utf8Path::new(""));
+ self.0
+ .get_mut_str(dir.as_str())
+ .and_then(|dir_trie| dir_trie.get_mut_str(filename))
+ .and_then(|o| o.as_mut())
+ }
+
+ /// Gets the longest prefix of the given path that is the path of a directory in the trie.
+ pub fn get_dir_prefix(&self, path: impl AsRef) -> &camino::Utf8Path {
+ let path = path.as_ref().as_str();
+ if self.0.contains_key_str(path) {
+ return self
+ .0
+ .longest_common_prefix::(path.into())
+ .as_str()
+ .into();
+ }
+ let prefix = self
+ .0
+ .longest_common_prefix::(format!("{path}/").as_str().into())
+ .as_str();
+ let prefix = if !self.0.contains_key_str(prefix) {
+ prefix.rsplit_once('/').map(|(s, _)| s).unwrap_or(prefix)
+ } else {
+ prefix
+ };
+ prefix.into()
+ }
+
+ /// Removes the file at the given path if it exists, and returns the original value.
+ pub fn remove_file(&mut self, path: impl AsRef) -> Option {
+ let path = path.as_ref();
+ let Some(filename) = path.iter().next_back() else {
+ return None;
+ };
+ let dir = path.parent().unwrap_or(camino::Utf8Path::new(""));
+ self.0
+ .get_mut_str(dir.as_str())
+ .and_then(|dir_trie| dir_trie.remove_str(filename))
+ .flatten()
+ }
+
+ /// Removes the directory at the given path and all of its contents if it exists, and returns
+ /// whether or not it existed.
+ pub fn remove_dir(&mut self, path: impl AsRef) -> bool {
+ let path = path.as_ref();
+ let path_str = path.as_str();
+ if path_str.is_empty() {
+ self.0.clear();
+ true
+ } else if self.0.contains_key_str(path_str) {
+ self.0.remove_prefix_str(&format!("{path_str}/"));
+ self.0.remove_str(path_str);
+ if let Some(parent) = path.parent() {
+ self.create_dir(parent);
+ }
+ true
+ } else if self
+ .0
+ .longest_common_prefix::(format!("{path_str}/").as_str().into())
+ .as_str()
+ .len()
+ == path_str.len() + 1
+ {
+ self.0.remove_prefix_str(&format!("{path_str}/"));
+ if let Some(parent) = path.parent() {
+ self.create_dir(parent);
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Given the path to a directory, returns an iterator over its children if it exists.
+ /// The iterator's items are of the form `(key, value)` where `key` is the name of the child as
+ /// `&str` and `value` is the data of the child if it's a file, as `Option<&T>`.
+ pub fn iter_dir(
+ &self,
+ path: impl AsRef,
+ ) -> Option> {
+ let path = path.as_ref();
+ let prefix_with_trailing_slash = format!("{}/", path.as_str());
+ if let Some(dir_trie) = self.0.get_str(path.as_str()) {
+ Some(FileSystemTrieDirIter(FileSystemTrieDirIterInner::Direct(
+ dir_trie.iter(),
+ dir_trie.count(),
+ )))
+ } else if let Some((key, _)) = self
+ .0
+ .iter_prefix_str(if path.as_str().is_empty() {
+ ""
+ } else {
+ &prefix_with_trailing_slash
+ })
+ .next()
+ {
+ Some(FileSystemTrieDirIter(FileSystemTrieDirIterInner::Prefix(
+ std::iter::once((
+ camino::Utf8Path::new(key.as_str())
+ .strip_prefix(path)
+ .unwrap()
+ .iter()
+ .next()
+ .unwrap(),
+ None,
+ )),
+ )))
+ } else {
+ None
+ }
+ }
+
+ /// Given the path to a directory, returns an iterator over immutable references to its
+ /// descendant files if it exists.
+ pub fn iter_prefix(
+ &self,
+ path: impl AsRef,
+ ) -> Option> {
+ let path = path.as_ref();
+ if self.contains_dir(path) {
+ Some(FileSystemTrieIter {
+ path: path.to_owned(),
+ trie: &self.0,
+ root_done: false,
+ trie_iter: None,
+ dir_iter: None,
+ })
+ } else {
+ None
+ }
+ }
+}
diff --git a/crates/filesystem/src/web/events.rs b/crates/filesystem/src/web/events.rs
index ac1eb71d..f65df28a 100644
--- a/crates/filesystem/src/web/events.rs
+++ b/crates/filesystem/src/web/events.rs
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see .
-use super::util::{generate_key, get_subdir, handle_event, idb, to_future};
+use super::util::{generate_key, get_subdir, get_tmp_dir, handle_event, idb, to_future};
use super::FileSystemCommand;
use crate::{DirEntry, Error, Metadata, OpenFlags};
use indexed_db_futures::prelude::*;
@@ -24,7 +24,10 @@ use wasm_bindgen::prelude::*;
pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
wasm_bindgen_futures::spawn_local(async move {
- web_sys::window().expect("cannot run `setup_main_thread_hooks()` outside of main thread");
+ let storage = web_sys::window()
+ .expect("cannot run `setup_main_thread_hooks()` outside of main thread")
+ .navigator()
+ .storage();
struct FileHandle {
offset: usize,
@@ -402,8 +405,12 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
let mut vec = Vec::new();
loop {
- let Ok(entry) =
- to_future::(entry_iter.next().unwrap()).await
+ let Ok(entry) = to_future::(
+ entry_iter
+ .next()
+ .map_err(|_| Error::IoError(PermissionDenied.into()))?,
+ )
+ .await
else {
break;
};
@@ -462,6 +469,53 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
handle_event(tx, async { dirs.insert(dirs.get(key).unwrap().clone()) }).await;
}
+ FileSystemCommand::FileCreateTemp(tx) => {
+ handle_event(tx, async {
+ let tmp_dir = get_tmp_dir(&storage).await.ok_or(PermissionDenied)?;
+
+ let filename = generate_key();
+
+ let mut options = web_sys::FileSystemGetFileOptions::new();
+ options.create(true);
+ let file_handle = to_future::(
+ tmp_dir.get_file_handle_with_options(&filename, &options),
+ )
+ .await
+ .map_err(|_| PermissionDenied)?;
+
+ let write_handle = to_future(file_handle.create_writable())
+ .await
+ .map_err(|_| PermissionDenied)?;
+
+ Ok((
+ files.insert(FileHandle {
+ offset: 0,
+ file_handle,
+ read_allowed: true,
+ write_handle: Some(write_handle),
+ }),
+ filename,
+ ))
+ })
+ .await;
+ }
+
+ FileSystemCommand::FileSetLength(key, new_size, tx) => {
+ handle_event(tx, async {
+ let file = files.get_mut(key).unwrap();
+ let write_handle = file.write_handle.as_ref().ok_or(PermissionDenied)?;
+ to_future::(
+ write_handle
+ .truncate_with_f64(new_size as f64)
+ .map_err(|_| PermissionDenied)?,
+ )
+ .await
+ .map_err(|_| PermissionDenied)?;
+ Ok(())
+ })
+ .await;
+ }
+
FileSystemCommand::FileRead(key, max_length, tx) => {
handle_event(tx, async {
let file = files.get_mut(key).unwrap();
@@ -504,12 +558,16 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
js_sys::Uint8Array::new(&JsValue::from_f64(vec.len() as f64));
u8_array.copy_from(&vec[..]);
if to_future::(
- write_handle.seek_with_f64(file.offset as f64).unwrap(),
+ write_handle
+ .seek_with_f64(file.offset as f64)
+ .map_err(|_| PermissionDenied)?,
)
.await
.is_ok()
&& to_future::(
- write_handle.write_with_buffer_source(&u8_array).unwrap(),
+ write_handle
+ .write_with_buffer_source(&u8_array)
+ .map_err(|_| PermissionDenied)?,
)
.await
.is_ok()
@@ -529,9 +587,11 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
// Closing and reopening the handle is the only way to flush
if file.write_handle.is_none()
- || to_future::(file.write_handle.as_ref().unwrap().close())
- .await
- .is_err()
+ || to_future::(
+ file.write_handle.as_ref().ok_or(PermissionDenied)?.close(),
+ )
+ .await
+ .is_err()
{
return Err(PermissionDenied.into());
}
@@ -588,7 +648,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
.await;
}
- FileSystemCommand::FileDrop(key, tx) => {
+ FileSystemCommand::FileDrop(key, temp_file_name, tx) => {
handle_event(tx, async {
if files.contains(key) {
let file = files.remove(key);
@@ -597,6 +657,14 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) {
if let Some(write_handle) = &file.write_handle {
let _ = to_future::(write_handle.close()).await;
}
+
+ if let Some(temp_file_name) = temp_file_name {
+ if let Some(tmp_dir) = get_tmp_dir(&storage).await {
+ let _ =
+ to_future::(tmp_dir.remove_entry(&temp_file_name))
+ .await;
+ }
+ }
true
} else {
false
diff --git a/crates/filesystem/src/web/mod.rs b/crates/filesystem/src/web/mod.rs
index 6e1e07d1..d8a5fc36 100644
--- a/crates/filesystem/src/web/mod.rs
+++ b/crates/filesystem/src/web/mod.rs
@@ -58,6 +58,7 @@ pub struct FileSystem {
#[derive(Debug)]
pub struct File {
key: usize,
+ temp_file_name: Option,
}
#[derive(Debug)]
@@ -93,6 +94,8 @@ enum FileSystemCommand {
),
DirDrop(usize, oneshot::Sender),
DirClone(usize, oneshot::Sender),
+ FileCreateTemp(oneshot::Sender>),
+ FileSetLength(usize, u64, oneshot::Sender>),
FileRead(usize, usize, oneshot::Sender>>),
FileWrite(usize, Vec, oneshot::Sender>),
FileFlush(usize, oneshot::Sender>),
@@ -102,7 +105,7 @@ enum FileSystemCommand {
oneshot::Sender>,
),
FileSize(usize, oneshot::Sender>),
- FileDrop(usize, oneshot::Sender),
+ FileDrop(usize, Option, oneshot::Sender),
}
fn worker_channels_or_die() -> &'static WorkerChannels {
@@ -203,7 +206,10 @@ impl FileSystemTrait for FileSystem {
send_and_recv(|tx| {
FileSystemCommand::DirOpenFile(self.key, path.as_ref().to_path_buf(), flags, tx)
})
- .map(|key| File { key })
+ .map(|key| File {
+ key,
+ temp_file_name: None,
+ })
}
fn metadata(&self, path: impl AsRef) -> Result {
@@ -249,20 +255,38 @@ impl FileSystemTrait for FileSystem {
}
}
+impl File {
+ /// Creates a new empty temporary file with read-write permissions.
+ pub fn new() -> std::io::Result {
+ send_and_recv(|tx| FileSystemCommand::FileCreateTemp(tx)).map(|(key, temp_file_name)| {
+ Self {
+ key,
+ temp_file_name: Some(temp_file_name),
+ }
+ })
+ }
+}
+
impl Drop for File {
fn drop(&mut self) {
- let _ = send_and_recv(|tx| FileSystemCommand::FileDrop(self.key, tx));
+ let _ = send_and_recv(|tx| {
+ FileSystemCommand::FileDrop(self.key, self.temp_file_name.take(), tx)
+ });
}
}
impl crate::File for File {
- fn metadata(&self) -> crate::Result {
+ fn metadata(&self) -> std::io::Result {
let size = send_and_recv(|tx| FileSystemCommand::FileSize(self.key, tx))?;
Ok(Metadata {
is_file: true,
size,
})
}
+
+ fn set_len(&self, new_size: u64) -> std::io::Result<()> {
+ send_and_recv(|tx| FileSystemCommand::FileSetLength(self.key, new_size, tx))
+ }
}
impl std::io::Read for File {
diff --git a/crates/filesystem/src/web/util.rs b/crates/filesystem/src/web/util.rs
index 60335cd5..a3d9b5da 100644
--- a/crates/filesystem/src/web/util.rs
+++ b/crates/filesystem/src/web/util.rs
@@ -32,6 +32,8 @@ where
}
/// Returns a subdirectory of a given directory given the relative path of the subdirectory.
+/// If one of the parent directories along the path does not exist, this will return `None`.
+/// Use `get_subdir_create` if you want to create the missing parent directories.
pub(super) async fn get_subdir(
dir: &web_sys::FileSystemDirectoryHandle,
path_iter: &mut camino::Iter<'_>,
@@ -49,6 +51,41 @@ pub(super) async fn get_subdir(
}
}
+/// Returns a subdirectory of a given directory given the relative path of the subdirectory.
+/// If one of the parent directories along the path does not exist, this will create the missing
+/// directories.
+pub(super) async fn get_subdir_create(
+ dir: &web_sys::FileSystemDirectoryHandle,
+ path_iter: &mut camino::Iter<'_>,
+) -> Option {
+ let mut dir = dir.clone();
+ loop {
+ let Some(path_element) = path_iter.next() else {
+ return Some(dir);
+ };
+ let mut options = web_sys::FileSystemGetDirectoryOptions::new();
+ options.create(true);
+ if let Ok(subdir) =
+ to_future(dir.get_directory_handle_with_options(path_element, &options)).await
+ {
+ dir = subdir;
+ } else {
+ return None;
+ }
+ }
+}
+
+/// Returns a handle to a directory for temporary files in the Origin Private File System.
+pub(super) async fn get_tmp_dir(
+ storage: &web_sys::StorageManager,
+) -> Option {
+ let opfs_root = to_future::(storage.get_directory())
+ .await
+ .ok()?;
+ let mut iter = camino::Utf8Path::new("astrabit.luminol/tmp").iter();
+ get_subdir_create(&opfs_root, &mut iter).await
+}
+
/// Generates a random string suitable for use as a unique identifier.
pub(super) fn generate_key() -> String {
rand::thread_rng()