diff --git a/Cargo.lock b/Cargo.lock index 2e6eb268..01654f67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,13 +958,30 @@ dependencies = [ ] [[package]] -name = "color-backtrace" -version = "0.6.1" +name = "color-eyre" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "150fd80a270c0671379f388c8204deb6a746bb4eac8a6c03fe2460b2c0127ea0" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" dependencies = [ "backtrace", - "termcolor", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", ] [[package]] @@ -1111,21 +1128,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.3.2" @@ -1712,6 +1714,16 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "eyre" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1881,9 +1893,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1908,9 +1920,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1942,9 +1954,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -1953,21 +1965,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2088,18 +2100,18 @@ dependencies = [ [[package]] name = "git-version" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ad01ffa8221f7fe8b936d6ffb2a3e7ad428885a04fad51866a5f33eafda57c" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" dependencies = [ "git-version-macro", ] [[package]] name = "git-version-macro" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84488ccbdb24ad6f56dc1863b4a8154a7856cd3c6c7610401634fab3cb588dae" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", @@ -2567,6 +2579,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df19da1e92fbfec043ca97d622955381b1f3ee72a180ec999912df31b1ccd951" +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexed_db_futures" version = "0.4.1" @@ -2949,19 +2967,19 @@ dependencies = [ name = "luminol" version = "0.4.0" dependencies = [ + "async-std", "camino", - "color-backtrace", - "crc", + "color-eyre", "egui", "egui_extras", - "epaint", "flume", + "futures-lite 2.1.0", + "git-version", "image 0.24.7", "js-sys", "luminol-audio", "luminol-config", "luminol-core", - "luminol-data", "luminol-eframe", "luminol-egui-wgpu", "luminol-filesystem", @@ -2993,6 +3011,7 @@ name = "luminol-audio" version = "0.4.0" dependencies = [ "camino", + "color-eyre", "flume", "luminol-filesystem", "once_cell", @@ -3011,7 +3030,7 @@ dependencies = [ name = "luminol-components" version = "0.4.0" dependencies = [ - "anyhow", + "color-eyre", "egui", "glam", "indextree", @@ -3024,11 +3043,7 @@ dependencies = [ "luminol-egui-wgpu", "luminol-filesystem", "luminol-graphics", - "once_cell", - "parking_lot", "qp-trie", - "serde", - "slab", "strum", "syntect", "wgpu", @@ -3051,24 +3066,24 @@ name = "luminol-core" version = "0.4.0" dependencies = [ "alox-48", - "anyhow", - "bitflags 2.4.1", "camino", + "color-eyre", "egui", "egui-modal", "egui-notify", "egui_dock", - "getrandom", + "git-version", + "itertools", "luminol-audio", "luminol-config", "luminol-data", "luminol-filesystem", "luminol-graphics", - "parking_lot", "poll-promise", "rand", "serde", "strum", + "tracing", ] [[package]] @@ -3079,13 +3094,10 @@ dependencies = [ "bytemuck", "camino", "flate2", - "getrandom", - "num-derive 0.4.1", "num_enum 0.7.1", "paste", "rand", "serde", - "slab", "strum", ] @@ -3155,6 +3167,7 @@ dependencies = [ "async_io_stream", "bitflags 2.4.1", "camino", + "color-eyre", "dashmap", "egui", "flume", @@ -3174,7 +3187,6 @@ dependencies = [ "rfd", "ron", "rust-ini", - "serde", "slab", "tempfile", "thiserror", @@ -3189,13 +3201,12 @@ dependencies = [ name = "luminol-graphics" version = "0.4.0" dependencies = [ - "anyhow", "bytemuck", "camino", + "color-eyre", "crossbeam", "dashmap", "egui", - "egui_extras", "glam", "image 0.24.7", "itertools", @@ -3204,8 +3215,6 @@ dependencies = [ "luminol-filesystem", "naga", "naga_oil", - "once_cell", - "slab", "wgpu", ] @@ -3228,8 +3237,10 @@ dependencies = [ name = "luminol-term" version = "0.4.0" dependencies = [ + "color-eyre", "crossbeam-channel", "egui", + "luminol-core", "portable-pty", "termwiz", "wezterm-term", @@ -3239,14 +3250,12 @@ dependencies = [ name = "luminol-ui" version = "0.4.0" dependencies = [ - "anyhow", "async-std", "camino", "catppuccin-egui", + "color-eyre", "egui", - "egui_extras", - "futures", - "futures-lite 2.1.0", + "futures-util", "git-version", "itertools", "luminol-audio", @@ -3259,7 +3268,6 @@ dependencies = [ "luminol-modals", "luminol-term", "once_cell", - "pin-project", "poll-promise", "qp-trie", "reqwest", @@ -3271,13 +3279,10 @@ dependencies = [ name = "luminol-web" version = "0.4.0" dependencies = [ - "egui", "js-sys", - "luminol-egui-wgpu", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu", ] [[package]] @@ -3695,17 +3700,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "num-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -3892,7 +3886,7 @@ dependencies = [ "jni 0.20.0", "ndk", "ndk-context", - "num-derive 0.3.3", + "num-derive", "num-traits", "oboe-sys", ] @@ -4033,6 +4027,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "pango-sys" version = "0.18.0" @@ -5538,7 +5538,7 @@ dependencies = [ "log", "memmem", "nix 0.24.3", - "num-derive 0.3.3", + "num-derive", "num-traits", "ordered-float", "pest", @@ -5714,6 +5714,7 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", "socket2 0.5.5", "tokio-macros", @@ -5858,6 +5859,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 098e9874..e98a5149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,6 @@ image = "0.24.7" serde = { version = "1.0", features = ["derive"] } alox-48 = { version = "0.4.2", default-features = false } ron = "0.8.1" -serde_json = "1.0" rust-ini = "0.20.0" bytemuck = { version = "1.14.0", features = [ @@ -98,7 +97,7 @@ strum = { version = "0.25.0", features = ["derive"] } paste = "1.0.14" thiserror = "1.0.37" bitflags = "2.4.0" -anyhow = "1.0" +color-eyre = "0.6.2" puffin = "0.18" raw-window-handle = "0.5.0" @@ -127,7 +126,8 @@ itertools = "0.11.0" rfd = "0.12.0" rand = "0.8.5" -getrandom = { version = "0.2", features = ["js"] } + +git-version = "0.3.9" luminol-audio = { version = "0.4.0", path = "crates/audio/" } luminol-components = { version = "0.4.0", path = "crates/components/" } @@ -148,7 +148,6 @@ luminol-eframe.workspace = true luminol-egui-wgpu.workspace = true egui.workspace = true egui_extras.workspace = true -epaint.workspace = true wgpu.workspace = true @@ -159,29 +158,29 @@ once_cell.workspace = true image.workspace = true -crc = { version = "3.0.1", optional = true } - tracing-subscriber = "0.3.17" -color-backtrace = "0.6.0" +color-eyre.workspace = true luminol-audio.workspace = true luminol-core.workspace = true luminol-config.workspace = true -luminol-data.workspace = true luminol-filesystem.workspace = true luminol-graphics.workspace = true luminol-ui.workspace = true # luminol-windows = { version = "0.1.0", path = "../windows/" } # luminol-tabs = { version = "0.1.0", path = "../tabs/" } -poll-promise.workspace = true - camino.workspace = true strum.workspace = true zstd = "0.13.0" +async-std.workspace = true +futures-lite.workspace = true + +git-version.workspace = true + # Native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] steamworks = { version = "0.10.0", optional = true } @@ -190,7 +189,7 @@ tokio = { version = "1.33", features = [ "macros", "io-util", "rt-multi-thread", - "time", + "parking_lot", ] } # *sigh* luminol-term.workspace = true @@ -237,7 +236,7 @@ optional = true features = ["webgl"] [features] -steamworks = ["dep:steamworks", "crc"] +steamworks = ["dep:steamworks"] webgl = ["dep:wgpu"] [target.'cfg(windows)'.build-dependencies] @@ -284,6 +283,10 @@ opt-level = 3 [profile.dev.package.glam] opt-level = 3 +# Backtraces for color-eyre errors and panics +[profile.dev.package.backtrace] +opt-level = 3 + # See why config is set up this way. # https://bevy-cheatbook.github.io/pitfalls/performance.html#why-not-use---release diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 9d3cf1ad..5ecd1a09 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -24,6 +24,7 @@ parking_lot.workspace = true camino.workspace = true once_cell.workspace = true +color-eyre.workspace = true thiserror.workspace = true luminol-filesystem.workspace = true diff --git a/crates/audio/src/error.rs b/crates/audio/src/error.rs index 5cb99738..8be66af0 100644 --- a/crates/audio/src/error.rs +++ b/crates/audio/src/error.rs @@ -36,4 +36,4 @@ pub enum Error { FileSystem(#[from] luminol_filesystem::Error), } -pub type Result = std::result::Result; +pub use color_eyre::Result; diff --git a/crates/components/Cargo.toml b/crates/components/Cargo.toml index 7280f676..6f86213f 100644 --- a/crates/components/Cargo.toml +++ b/crates/components/Cargo.toml @@ -36,17 +36,10 @@ syntect = { version = "5.1.0", default-features = false, features = [ "default-fancy", ] } -parking_lot.workspace = true - itertools.workspace = true -serde.workspace = true +color-eyre.workspace = true -once_cell.workspace = true -slab.workspace = true qp-trie.workspace = true - -anyhow.workspace = true - indextree = "4.6.0" lexical-sort = "0.3.1" diff --git a/crates/components/src/filesystem_view.rs b/crates/components/src/filesystem_view.rs index 8e8655b4..3b858a61 100644 --- a/crates/components/src/filesystem_view.rs +++ b/crates/components/src/filesystem_view.rs @@ -192,8 +192,13 @@ where ancestors.reverse(); let path = ancestors.join("/"); - let mut subentries = self.filesystem.read_dir(path).unwrap_or_else(|e| { - update_state.toasts.error(e.to_string()); + let mut subentries = self.filesystem.read_dir(&path).unwrap_or_else(|e| { + luminol_core::error!( + update_state.toasts, + e.wrap_err(format!( + "Error reading contents of directory {path} in filesystem view" + )) + ); Vec::new() }); subentries.sort_unstable_by(|a, b| { diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index 6b7ab11e..7e64748d 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -66,7 +66,7 @@ impl MapView { pub fn new( update_state: &luminol_core::UpdateState<'_>, map_id: usize, - ) -> anyhow::Result { + ) -> color_eyre::Result { let map = update_state .data .get_or_load_map(map_id, update_state.filesystem); @@ -91,7 +91,7 @@ impl MapView { let events = map .events .iter() - .map(|(id, e)| -> anyhow::Result<_> { + .map(|(id, e)| -> color_eyre::Result<_> { let sprite = luminol_graphics::Event::new( &update_state.graphics, update_state.filesystem, diff --git a/crates/components/src/sound_tab.rs b/crates/components/src/sound_tab.rs index b6ffe696..66ff116c 100644 --- a/crates/components/src/sound_tab.rs +++ b/crates/components/src/sound_tab.rs @@ -68,7 +68,10 @@ impl SoundTab { pitch, source, ) { - update_state.toasts.error(e.to_string()); + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error playing from audio file") + ); } } @@ -146,7 +149,10 @@ impl SoundTab { pitch, source, ) { - update_state.toasts.error(e.to_string()); + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error playing from audio file"), + ); } }; } diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index c4fedf6e..52a49e7b 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -104,7 +104,7 @@ impl Tilepicker { pub fn new( update_state: &luminol_core::UpdateState<'_>, map_id: usize, // FIXME - ) -> anyhow::Result { + ) -> color_eyre::Result { let map = update_state .data .get_or_load_map(map_id, update_state.filesystem); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index a7430f35..5dbdd825 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -18,7 +18,6 @@ workspace = true [dependencies] rust-ini.workspace = true - serde.workspace = true strum.workspace = true diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index a57cafb3..b26979e3 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -23,19 +23,22 @@ egui_dock = "0.9.0" egui-notify = "0.11.0" egui-modal = "0.3.1" -parking_lot.workspace = true poll-promise.workspace = true -anyhow.workspace = true +tracing.workspace = true +color-eyre.workspace = true + camino.workspace = true -bitflags.workspace = true + +itertools.workspace = true strum.workspace = true serde.workspace = true alox-48.workspace = true rand.workspace = true -getrandom.workspace = true + +git-version.workspace = true luminol-audio.workspace = true luminol-config.workspace = true diff --git a/crates/core/src/data_cache.rs b/crates/core/src/data_cache.rs index 243442fd..15fd206c 100644 --- a/crates/core/src/data_cache.rs +++ b/crates/core/src/data_cache.rs @@ -22,7 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use anyhow::Context; +use color_eyre::eyre::WrapErr; use luminol_data::rpg; use std::{ cell::{RefCell, RefMut}, @@ -59,31 +59,33 @@ pub enum Data { fn read_data( filesystem: &impl luminol_filesystem::FileSystem, filename: impl AsRef, -) -> anyhow::Result +) -> color_eyre::Result where T: serde::de::DeserializeOwned, { let path = camino::Utf8PathBuf::from("Data").join(filename); let data = filesystem.read(path)?; - alox_48::from_bytes(&data).map_err(anyhow::Error::from) + alox_48::from_bytes(&data).map_err(color_eyre::Report::from) } fn write_data( data: &impl serde::Serialize, filesystem: &impl luminol_filesystem::FileSystem, filename: impl AsRef, -) -> anyhow::Result<()> { +) -> color_eyre::Result<()> { let path = camino::Utf8PathBuf::from("Data").join(filename); let bytes = alox_48::to_bytes(data)?; - filesystem.write(path, bytes).map_err(anyhow::Error::from) + filesystem + .write(path, bytes) + .map_err(color_eyre::Report::from) } fn read_nil_padded( filesystem: &impl luminol_filesystem::FileSystem, filename: impl AsRef, -) -> anyhow::Result> +) -> color_eyre::Result> where T: serde::de::DeserializeOwned, { @@ -92,14 +94,14 @@ where let mut de = alox_48::Deserializer::new(&data)?; - luminol_data::helpers::nil_padded::deserialize(&mut de).map_err(anyhow::Error::from) + luminol_data::helpers::nil_padded::deserialize(&mut de).map_err(color_eyre::Report::from) } fn write_nil_padded( data: &[impl serde::Serialize], filesystem: &impl luminol_filesystem::FileSystem, filename: impl AsRef, -) -> anyhow::Result<()> { +) -> color_eyre::Result<()> { let path = camino::Utf8PathBuf::from("Data").join(filename); let mut ser = alox_48::Serializer::new(); @@ -107,14 +109,14 @@ fn write_nil_padded( luminol_data::helpers::nil_padded::serialize(data, &mut ser)?; filesystem .write(path, ser.output) - .map_err(anyhow::Error::from) + .map_err(color_eyre::Report::from) } macro_rules! load { ($fs:ident, $type:ident) => { RefCell::new(rpg::$type { data: read_nil_padded($fs, format!("{}.rxdata", stringify!($type))) - .context(format!("while reading {}.rxdata", stringify!($type)))?, + .wrap_err_with(|| format!("While reading {}.rxdata", stringify!($type)))?, ..Default::default() }) }; @@ -130,14 +132,12 @@ macro_rules! from_defaults { macro_rules! save { ($fs:ident, $type:ident, $field:ident) => {{ - let mut borrowed = $field.borrow_mut(); - let modified = borrowed.modified; - if modified { - borrowed.modified = false; + let borrowed = $field.borrow(); + if borrowed.modified { write_nil_padded(&borrowed.data, $fs, format!("{}.rxdata", stringify!($type))) - .context(format!("while saving {}.rxdata", stringify!($type)))?; + .wrap_err_with(|| format!("While saving {}.rxdata", stringify!($type)))?; } - modified + borrowed.modified }}; } impl Data { @@ -147,15 +147,15 @@ impl Data { &mut self, filesystem: &impl luminol_filesystem::FileSystem, config: &mut luminol_config::project::Config, - ) -> anyhow::Result<()> { + ) -> color_eyre::Result<()> { let map_infos = RefCell::new(rpg::MapInfos { data: read_data(filesystem, "MapInfos.rxdata") - .context("while reading MapInfos.rxdata")?, + .wrap_err("While reading MapInfos.rxdata")?, ..Default::default() }); let mut system = read_data::(filesystem, "System.rxdata") - .context("while reading System.rxdata")?; + .wrap_err("While reading System.rxdata")?; system.magic_number = rand::random(); let system = RefCell::new(system); @@ -181,7 +181,7 @@ impl Data { } } let Some(scripts) = scripts else { - anyhow::bail!( + color_eyre::eyre::bail!( "Unable to load scripts (tried {}, xScripts, and Scripts first)", config.project.scripts_path ); @@ -270,7 +270,7 @@ impl Data { &mut self, filesystem: &impl luminol_filesystem::FileSystem, config: &luminol_config::project::Config, - ) -> anyhow::Result<()> { + ) -> color_eyre::Result<()> { let Self::Loaded { actors, animations, @@ -309,20 +309,18 @@ impl Data { modified |= save!(filesystem, Weapons, weapons); { - let mut map_infos = map_infos.borrow_mut(); + let map_infos = map_infos.borrow(); if map_infos.modified { modified = true; - map_infos.modified = false; write_data(&map_infos.data, filesystem, "MapInfos.rxdata") - .context("while saving MapInfos.rxdata")?; + .wrap_err("While saving MapInfos.rxdata")?; } } { - let mut scripts = scripts.borrow_mut(); + let scripts = scripts.borrow(); if scripts.modified { modified = true; - scripts.modified = false; write_data( &scripts.data, filesystem, @@ -332,13 +330,12 @@ impl Data { } { - let mut maps = maps.borrow_mut(); - maps.iter_mut().try_for_each(|(id, map)| { + let maps = maps.borrow(); + maps.iter().try_for_each(|(id, map)| { if map.modified { modified = true; - map.modified = false; write_data(map, filesystem, format!("Map{id:0>3}.rxdata")) - .with_context(|| format!("while saving map {id:0>3}")) + .wrap_err_with(|| format!("While saving map {id:0>3}")) } else { Ok(()) } @@ -348,13 +345,30 @@ impl Data { { let system = system.get_mut(); if system.modified || modified { - system.modified = false; system.magic_number = rand::random(); write_data(system, filesystem, "System.rxdata") - .context("while saving System.rxdata")?; + .wrap_err("While saving System.rxdata")?; + system.modified = false; } } + actors.borrow_mut().modified = false; + animations.borrow_mut().modified = false; + armors.borrow_mut().modified = false; + classes.borrow_mut().modified = false; + common_events.borrow_mut().modified = false; + enemies.borrow_mut().modified = false; + items.borrow_mut().modified = false; + skills.borrow_mut().modified = false; + states.borrow_mut().modified = false; + tilesets.borrow_mut().modified = false; + troops.borrow_mut().modified = false; + weapons.borrow_mut().modified = false; + map_infos.borrow_mut().modified = false; + scripts.borrow_mut().modified = false; + for (_, map) in maps.borrow_mut().iter_mut() { + map.modified = false; + } Ok(()) } } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4b4ddbf1..ae6ff149 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -23,6 +23,7 @@ // Program grant you additional permission to convey the resulting work. use std::sync::Arc; +pub use tracing; mod tab; pub use tab::{EditTabs, Tab, Tabs}; @@ -231,11 +232,11 @@ impl<'res> UpdateState<'res> { { Ok(_) => { self.modified.set(false); - self.toasts.info("Saved project successfully!") + info!(self.toasts, "Saved project successfully!"); } Err(e) => { should_run_closure = false; - self.toasts.error(e.to_string()) + error!(self.toasts, e.wrap_err("Error saving project")); } } } @@ -266,7 +267,15 @@ impl<'res> UpdateState<'res> { self.global_config, )); } - Ok(Err(error)) => self.toasts.error(error.to_string()), + Ok(Err(error)) + if !matches!( + error.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) => + { + error!(self.toasts, error.wrap_err("Error locating project files")); + } + Ok(Err(_)) => {} Err(p) => self.project_manager.load_filesystem_promise = Some(p), } } @@ -278,36 +287,43 @@ impl<'res> UpdateState<'res> { match filesystem_open_result { Some(Ok(load_result)) => { for missing_rtp in load_result.missing_rtps { - self.toasts.warning(format!( - "Failed to find suitable path for the RTP {missing_rtp}" - )); + warn!( + self.toasts, + format!("Failed to find suitable path for the RTP {missing_rtp}") + ); // FIXME we should probably load rtps from the RTP/ paths on non-wasm targets #[cfg(not(target_arch = "wasm32"))] - self.toasts - .info(format!("You may want to set an RTP path for {missing_rtp}")); + info!( + self.toasts, + format!("You may want to set an RTP path for {missing_rtp}") + ); #[cfg(target_arch = "wasm32")] - self - .toasts - .info(format!("Please place the {missing_rtp} RTP in the 'RTP/{missing_rtp}' subdirectory in your project directory")); + info!(self.toasts, format!("Please place the {missing_rtp} RTP in the 'RTP/{missing_rtp}' subdirectory in your project directory")); } - if let Err(why) = self.data.load( + if let Err(error) = self.data.load( self.filesystem, // TODO code jank self.project_config.as_mut().unwrap(), ) { - self.toasts - .error(format!("Error loading the project data: {why}")); + error!( + self.toasts, + error.wrap_err("Error loading the project data") + ); + self.close_project(); } else { - self.toasts.info(format!( - "Successfully opened {:?}", - self.filesystem.project_path().expect("project not open") - )); + info!( + self.toasts, + format!( + "Successfully opened {:?}", + self.filesystem.project_path().expect("project not open") + ) + ); } } - Some(Err(why)) => { - self.toasts - .error(format!("Error opening the project: {why}")); + Some(Err(error)) => { + error!(self.toasts, error.wrap_err("Error opening the project")); + self.close_project(); } None => {} } @@ -331,10 +347,15 @@ impl<'res> UpdateState<'res> { *self.data = data_cache; self.project_config.replace(config); } - Err(error) => self.toasts.error(format!("{error:#}")), + Err(error) => { + error!(self.toasts, error.wrap_err("Error creating new project")) + } } } - Ok(Err(error)) => self.toasts.error(format!("{error:#}")), + Ok(Err(error)) => error!( + self.toasts, + error.wrap_err("Error locating destination directory for project"), + ), Err(p) => self.project_manager.create_project_promise = Some(p), } } diff --git a/crates/core/src/project_manager.rs b/crates/core/src/project_manager.rs index 1d8594c6..ed40bcb7 100644 --- a/crates/core/src/project_manager.rs +++ b/crates/core/src/project_manager.rs @@ -38,7 +38,7 @@ pub struct CreateProjectResult { } type ProjectManagerClosure = dyn FnOnce(&mut crate::UpdateState<'_>); -pub type CreateProjectPromiseResult = anyhow::Result; +pub type CreateProjectPromiseResult = color_eyre::Result; pub type FileSystemPromiseResult = luminol_filesystem::Result; pub type FileSystemOpenResult = luminol_filesystem::Result; diff --git a/crates/core/src/toasts.rs b/crates/core/src/toasts.rs index 8af6d31c..eac712bd 100644 --- a/crates/core/src/toasts.rs +++ b/crates/core/src/toasts.rs @@ -22,6 +22,9 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +use color_eyre::Section; +use itertools::Itertools; + /// A toasts management struct. #[derive(Default)] pub struct Toasts { @@ -36,28 +39,94 @@ impl Toasts { self.inner.add(toast); } - /// Display an info toast. - pub fn info(&mut self, caption: impl Into) { - self.inner.info(caption); + /// Display all toasts. + pub fn show(&mut self, ctx: &egui::Context) { + self.inner.show(ctx); } - /// Display a warning toast. - pub fn warning(&mut self, caption: impl Into) { - self.inner.warning(caption); + #[doc(hidden)] + pub fn _i_inner(&mut self, caption: impl Into) { + self.inner + .info(caption) + .set_duration(Some(std::time::Duration::from_secs(7))); } - /// Display an error toast. - pub fn error(&mut self, caption: impl Into) { - self.inner.error(caption); + #[doc(hidden)] + pub fn _w_inner(&mut self, caption: impl Into) { + self.inner + .warning(caption) + .set_duration(Some(std::time::Duration::from_secs(7))); } - /// Display a generic toast. - pub fn basic(&mut self, caption: impl Into) { - self.inner.basic(caption); + #[doc(hidden)] + pub fn _b_inner(&mut self, caption: impl Into) { + self.inner + .basic(caption) + .set_duration(Some(std::time::Duration::from_secs(7))); } - /// Display all toasts. - pub fn show(&mut self, ctx: &egui::Context) { - self.inner.show(ctx); + #[doc(hidden)] + pub fn _e_add_version_section(error: color_eyre::Report) -> color_eyre::Report { + error.section(format!("Luminol version: {}", git_version::git_version!())) + } + + #[doc(hidden)] + pub fn _e_inner(&mut self, error: color_eyre::Report) { + #[cfg(not(target_arch = "wasm32"))] + let help = "Check the log (Debug > Log) for more details"; + #[cfg(target_arch = "wasm32")] + let help = "Check the browser developer console for more details"; + + if error.chain().len() <= 1 { + self.inner.error(format!("{}\n\n{}", error, help,)) + } else { + self.inner.error(format!( + "{}\n\n{}\n\n{}", + error, + error.chain().skip(1).map(|e| e.to_string()).join("\n"), + help + )) + } + .set_duration(Some(std::time::Duration::from_secs(7))); } } + +/// Display an info toast. +#[macro_export] +macro_rules! info { + ($toasts:expr, $caption:expr $(,)?) => {{ + let caption = String::from($caption); + $crate::tracing::info!("{caption}"); + $crate::Toasts::_i_inner(&mut $toasts, $caption); + }}; +} + +/// Display a warning toast. +#[macro_export] +macro_rules! warn { + ($toasts:expr, $caption:expr $(,)?) => {{ + let caption = String::from($caption); + $crate::tracing::warn!("{caption}"); + $crate::Toasts::_w_inner(&mut $toasts, caption); + }}; +} + +/// Display a generic toast. +#[macro_export] +macro_rules! basic { + ($toasts:expr, $caption:expr $(,)?) => {{ + let caption = String::from($caption); + $crate::tracing::info!("{caption}"); + $crate::Toasts::_b_inner(&mut $toasts, caption); + }}; +} + +/// Format a `color_eyre::Report` and display it as an error toast. +#[macro_export] +macro_rules! error { + ($toasts:expr, $error:expr $(,)?) => {{ + let error = $crate::Toasts::_e_add_version_section($error); + $crate::tracing::error!("Luminol error:{error:?}"); + $crate::Toasts::_e_inner(&mut $toasts, error); + }}; +} diff --git a/crates/data/Cargo.toml b/crates/data/Cargo.toml index b1350c0b..10b4d099 100644 --- a/crates/data/Cargo.toml +++ b/crates/data/Cargo.toml @@ -17,18 +17,15 @@ categories.workspace = true workspace = true [dependencies] -num-derive = "0.4.0" num_enum = "0.7.0" rand.workspace = true -getrandom.workspace = true flate2 = "1.0" serde.workspace = true alox-48.workspace = true bytemuck.workspace = true -slab.workspace = true strum.workspace = true paste.workspace = true camino.workspace = true diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml index a34595b5..71aa3104 100644 --- a/crates/filesystem/Cargo.toml +++ b/crates/filesystem/Cargo.toml @@ -19,6 +19,7 @@ workspace = true [dependencies] rfd.workspace = true +color-eyre.workspace = true thiserror.workspace = true bitflags.workspace = true @@ -34,7 +35,6 @@ pin-project.workspace = true egui.workspace = true -serde.workspace = true ron.workspace = true rust-ini.workspace = true diff --git a/crates/filesystem/src/archiver/file.rs b/crates/filesystem/src/archiver/file.rs index f3636b44..a0047828 100644 --- a/crates/filesystem/src/archiver/file.rs +++ b/crates/filesystem/src/archiver/file.rs @@ -26,8 +26,8 @@ use std::{pin::Pin, task::Poll}; use super::util::{move_file_and_truncate, read_file_xor, regress_magic}; use super::Trie; -use crate::File as _; use crate::Metadata; +use crate::{File as _, StdIoErrorExt}; #[derive(Debug)] #[pin_project] @@ -51,24 +51,38 @@ where T: crate::File, { fn write(&mut self, buf: &[u8]) -> std::io::Result { + let c = format!( + "While writing to file {:?} within a version {} archive", + self.path, self.version + ); if self.archive.is_some() { let mut modified = self.modified.lock(); *modified = true; - let count = self.tmp.write(buf)?; - Ok(count) + self.tmp.write(buf).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )) + .wrap_io_err_with(|| c.clone()) } } fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + let c = format!( + "While writing (vectored) to file {:?} within a version {} archive", + self.path, self.version + ); if self.archive.is_some() { let mut modified = self.modified.lock(); *modified = true; - let count = self.tmp.write_vectored(bufs)?; - Ok(count) + self.tmp.write_vectored(bufs).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )) + .wrap_io_err_with(|| c.clone()) } } @@ -77,26 +91,48 @@ where if !*modified { return Ok(()); } + let c = format!( + "While flushing file {:?} within a version {} archive", + self.path, self.version + ); - 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 mut archive = self + .archive + .as_ref() + .ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )) + .wrap_io_err_with(|| c.clone())? + .lock(); + let mut trie = self + .trie + .as_ref() + .ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )) + .wrap_io_err_with(|| c.clone())? + .write(); let archive_len = archive.metadata()?.size; - let tmp_stream_position = self.tmp.stream_position()?; - self.tmp.flush()?; - self.tmp.seek(SeekFrom::Start(0))?; + let tmp_stream_position = self.tmp.stream_position().wrap_io_err_with(|| c.clone())?; + self.tmp.flush().wrap_io_err_with(|| c.clone())?; + self.tmp + .seek(SeekFrom::Start(0)) + .wrap_io_err_with(|| c.clone())?; // 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 mut entry = *trie + .get_file(&self.path) + .ok_or(std::io::Error::new( + InvalidData, + "Could not find the file within the archive", + )) + .wrap_io_err_with(|| c.clone())?; let old_size = entry.size; - let new_size = self.tmp.metadata()?.size; + let new_size = self.tmp.metadata().wrap_io_err_with(|| c.clone())?.size; if old_size != new_size { move_file_and_truncate( &mut archive, @@ -104,52 +140,115 @@ where &self.path, self.version, self.base_magic, - )?; - entry = *trie.get_file(&self.path).ok_or(InvalidData)?; + ) + .wrap_io_err("While relocating the file header to the end of the archive") + .wrap_io_err_with(|| c.clone())?; + entry = *trie + .get_file(&self.path) + .ok_or(std::io::Error::new( + InvalidData, + "Could not find the file within the archive", + )) + .wrap_io_err_with(|| c.clone())?; // 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())?; + archive + .seek(SeekFrom::Start( + entry.body_offset.checked_sub(4).ok_or(InvalidData)?, + )) + .wrap_io_err("While writing the file length to the archive") + .wrap_io_err_with(|| c.clone())?; + archive + .write_all(&(new_size as u32 ^ magic).to_le_bytes()) + .wrap_io_err( + "While writing the base magic value of the file to the archive", + ) + .wrap_io_err_with(|| c.clone())?; } 3 => { - archive.seek(SeekFrom::Start(entry.header_offset + 4))?; - archive.write_all(&(new_size as u32 ^ self.base_magic).to_le_bytes())?; + archive + .seek(SeekFrom::Start(entry.header_offset + 4)) + .wrap_io_err("While writing the file length to the archive") + .wrap_io_err_with(|| c.clone())?; + archive + .write_all(&(new_size as u32 ^ self.base_magic).to_le_bytes()) + .wrap_io_err( + "While writing the base magic value of the file to the archive", + ) + .wrap_io_err_with(|| c.clone())?; } - _ => return Err(InvalidData.into()), + _ => { + return Err(std::io::Error::new( + InvalidData, + format!( + "Invalid archive version: {} (supported versions are 1, 2 and 3)", + self.version + ), + )) + } } // Write the new length of the file to the trie - trie.get_file_mut(&self.path).ok_or(InvalidData)?.size = new_size; + trie.get_file_mut(&self.path) + .ok_or(std::io::Error::new( + InvalidData, + "Could not find the file within the archive", + )) + .wrap_io_err("After changing the file length within the archive") + .wrap_io_err_with(|| c.clone())? + .size = new_size; } // Now write the new contents of the file - archive.seek(SeekFrom::Start(entry.body_offset))?; + archive + .seek(SeekFrom::Start(entry.body_offset)) + .wrap_io_err("While writing the file contents to the archive") + .wrap_io_err_with(|| c.clone())?; let mut reader = BufReader::new(&mut self.tmp); std::io::copy( &mut read_file_xor(&mut reader, entry.start_magic), archive.as_file(), - )?; + ) + .wrap_io_err("While writing the file contents to the archive") + .wrap_io_err_with(|| c.clone())?; drop(reader); - self.tmp.seek(SeekFrom::Start(tmp_stream_position))?; + self.tmp + .seek(SeekFrom::Start(tmp_stream_position)) + .wrap_io_err("While writing the file contents to the archive") + .wrap_io_err_with(|| c.clone())?; 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 + .set_len( + archive_len + .checked_sub(old_size) + .ok_or(std::io::Error::new( + InvalidData, + "Archive header is corrupt", + )) + .wrap_io_err("While truncating the archive") + .wrap_io_err_with(|| c.clone())? + .checked_add(new_size) + .ok_or(std::io::Error::new( + InvalidData, + "Archive header is corrupt", + )) + .wrap_io_err("While truncating the archive") + .wrap_io_err_with(|| c.clone())?, + ) + .wrap_io_err("While truncating the archive") + .wrap_io_err_with(|| c.clone())?; } - archive.flush()?; + archive + .flush() + .wrap_io_err("While flushing the archive after writing its contents") + .wrap_io_err_with(|| c.clone())?; *modified = false; Ok(()) } @@ -160,26 +259,50 @@ where T: crate::File, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let c = format!( + "While reading from file {:?} within a version {} archive", + self.path, self.version + ); if self.read_allowed { - self.tmp.read(buf) + self.tmp.read(buf).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + )) + .wrap_io_err_with(|| c.clone()) } } fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + let c = format!( + "While reading (vectored) from file {:?} within a version {} archive", + self.path, self.version + ); if self.read_allowed { - self.tmp.read_vectored(bufs) + self.tmp.read_vectored(bufs).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + )) + .wrap_io_err_with(|| c.clone()) } } fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + let c = format!( + "While reading (exact) from file {:?} within a version {} archive", + self.path, self.version + ); if self.read_allowed { - self.tmp.read_exact(buf) + self.tmp.read_exact(buf).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + )) + .wrap_io_err_with(|| c.clone()) } } } @@ -193,10 +316,23 @@ where cx: &mut std::task::Context<'_>, buf: &mut [u8], ) -> Poll> { + let c = format!( + "While asynchronously reading from file {:?} within a version {} archive", + self.path, self.version + ); if self.read_allowed { - self.project().tmp.poll_read(cx, buf) + self.project() + .tmp + .poll_read(cx, buf) + .map(|r| r.wrap_io_err_with(|| c.clone())) } else { - Poll::Ready(Err(PermissionDenied.into())) + Poll::Ready( + Err(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + )) + .wrap_io_err_with(|| c.clone()), + ) } } @@ -205,10 +341,23 @@ where cx: &mut std::task::Context<'_>, bufs: &mut [std::io::IoSliceMut<'_>], ) -> Poll> { + let c = format!( + "While asynchronously reading (vectored) from file {:?} within a version {} archive", + self.path, self.version + ); if self.read_allowed { - self.project().tmp.poll_read_vectored(cx, bufs) + self.project() + .tmp + .poll_read_vectored(cx, bufs) + .map(|r| r.wrap_io_err_with(|| c.clone())) } else { - Poll::Ready(Err(PermissionDenied.into())) + Poll::Ready( + Err(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + )) + .wrap_io_err_with(|| c.clone()), + ) } } } @@ -218,11 +367,19 @@ where T: crate::File, { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { - self.tmp.seek(pos) + let c = format!( + "While asynchronously seeking file {:?} within a version {} archive", + self.path, self.version + ); + self.tmp.seek(pos).wrap_io_err(c) } fn stream_position(&mut self) -> std::io::Result { - self.tmp.stream_position() + let c = format!( + "While getting stream position for file {:?} within a version {} archive", + self.path, self.version + ); + self.tmp.stream_position().wrap_io_err(c) } } @@ -235,7 +392,14 @@ where cx: &mut std::task::Context<'_>, pos: SeekFrom, ) -> Poll> { - self.project().tmp.poll_seek(cx, pos) + let c = format!( + "While asynchronously seeking file {:?} within a version {} archive", + self.path, self.version + ); + self.project() + .tmp + .poll_seek(cx, pos) + .map(|r| r.wrap_io_err(c)) } } @@ -244,16 +408,28 @@ where T: crate::File, { fn metadata(&self) -> std::io::Result { - self.tmp.metadata() + let c = format!( + "While getting metadata for file {:?} within a version {} archive", + self.path, self.version + ); + self.tmp.metadata().wrap_io_err(c) } fn set_len(&self, new_size: u64) -> std::io::Result<()> { + let c = format!( + "While setting length for file {:?} within a version {} archive", + self.path, self.version + ); if self.archive.is_some() { let mut modified = self.modified.lock(); *modified = true; - self.tmp.set_len(new_size) + self.tmp.set_len(new_size).wrap_io_err_with(|| c.clone()) } else { - Err(PermissionDenied.into()) + Err(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )) + .wrap_io_err_with(|| c.clone()) } } } diff --git a/crates/filesystem/src/archiver/filesystem.rs b/crates/filesystem/src/archiver/filesystem.rs deleted file mode 100644 index 4377d927..00000000 --- a/crates/filesystem/src/archiver/filesystem.rs +++ /dev/null @@ -1,859 +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 async_std::io::{BufReader as AsyncBufReader, BufWriter as AsyncBufWriter}; -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_file_xor_async, read_header, - read_u32_xor, regress_magic, -}; -use super::{Entry, File, Trie, HEADER, 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 Clone for FileSystem { - fn clone(&self) -> Self { - Self { - trie: self.trie.clone(), - archive: self.archive.clone(), - version: self.version, - base_magic: self.base_magic, - } - } -} - -impl FileSystem -where - T: crate::File, -{ - /// Creates a new archiver filesystem from a file containing an existing archive. - pub fn new(mut file: T) -> Result { - file.seek(SeekFrom::Start(0))?; - 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.wrapping_mul(9).wrapping_add(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(Self { - trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), - archive: std::sync::Arc::new(parking_lot::Mutex::new(file)), - version, - base_magic, - }) - } - - /// Creates a new archiver filesystem from the given files. - /// The contents of the archive itself will be stored in `buffer`. - pub async fn from_buffer_and_files<'a, I, P, R>( - mut buffer: T, - version: u8, - files: I, - ) -> Result - where - T: futures_lite::AsyncWrite + futures_lite::AsyncSeek + Unpin, - I: Iterator>, - P: AsRef + 'a, - R: futures_lite::AsyncRead + Unpin, - { - use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; - - buffer.set_len(0)?; - AsyncSeekExt::seek(&mut buffer, SeekFrom::Start(0)).await?; - - let mut writer = AsyncBufWriter::new(&mut buffer); - writer.write_all(HEADER).await?; - writer.write_all(&[version]).await?; - - let mut trie = Trie::new(); - - match version { - 1 | 2 => { - let mut magic = MAGIC; - let mut header_offset = 8; - - for result in files { - let (path, size, file) = result?; - let reader = AsyncBufReader::new(file.take(size as u64)); - let path = path.as_ref(); - let header_size = path.as_str().bytes().len() as u64 + 8; - - // Write the header - writer - .write_all( - &(path.as_str().bytes().len() as u32 ^ advance_magic(&mut magic)) - .to_le_bytes(), - ) - .await?; - 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(), - ) - .await?; - writer - .write_all(&(size ^ advance_magic(&mut magic)).to_le_bytes()) - .await?; - - // Write the file contents - async_std::io::copy(&mut read_file_xor_async(reader, magic), &mut writer) - .await?; - - trie.create_file( - path, - Entry { - header_offset, - body_offset: header_offset + header_size, - size: size as u64, - start_magic: magic, - }, - ); - - header_offset += header_size + size as u64; - } - - writer.flush().await?; - drop(writer); - Ok(Self { - trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), - archive: std::sync::Arc::new(parking_lot::Mutex::new(buffer)), - version, - base_magic: MAGIC, - }) - } - - 3 => { - let mut tmp = crate::host::File::new()?; - let mut tmp_writer = AsyncBufWriter::new(&mut tmp); - let mut entries = if let (_, Some(upper_bound)) = files.size_hint() { - Vec::with_capacity(upper_bound) - } else { - Vec::new() - }; - - let base_magic: u32 = rand::thread_rng().gen(); - writer - .write_all(&(base_magic.wrapping_sub(3).wrapping_mul(954437177)).to_le_bytes()) - .await?; - let mut header_offset = 12; - let mut body_offset = 0; - - for result in files { - let (path, size, file) = result?; - let reader = AsyncBufReader::new(file.take(size as u64)); - let path = path.as_ref(); - let entry_magic: u32 = rand::thread_rng().gen(); - - // Write the header to the buffer, except for the offset - writer.seek(SeekFrom::Current(4)).await?; - writer.write_all(&(size ^ base_magic).to_le_bytes()).await?; - writer - .write_all(&(entry_magic ^ base_magic).to_le_bytes()) - .await?; - writer - .write_all(&(path.as_str().bytes().len() as u32 ^ base_magic).to_le_bytes()) - .await?; - writer - .write_all( - &path - .as_str() - .bytes() - .enumerate() - .map(|(i, b)| { - let b = if b == b'/' { b'\\' } else { b }; - b ^ (base_magic >> (8 * (i % 4))) as u8 - }) - .collect_vec(), - ) - .await?; - - // Write the actual file contents to a temporary file - async_std::io::copy( - &mut read_file_xor_async(reader, entry_magic), - &mut tmp_writer, - ) - .await?; - - entries.push(( - path.to_owned(), - Entry { - header_offset, - body_offset, - size: size as u64, - start_magic: entry_magic, - }, - )); - - header_offset += path.as_str().bytes().len() as u64 + 16; - body_offset += size as u64; - } - - // Write the terminator at the end of the buffer - writer.write_all(&base_magic.to_le_bytes()).await?; - - // Write the contents of the temporary file to the buffer after the terminator - tmp_writer.flush().await?; - drop(tmp_writer); - AsyncSeekExt::seek(&mut tmp, SeekFrom::Start(0)).await?; - async_std::io::copy(&mut tmp, &mut writer).await?; - - // Write the offsets into the header now that we know the total size of the files - let header_size = header_offset + 4; - for (path, mut entry) in entries { - entry.body_offset += header_size; - writer.seek(SeekFrom::Start(entry.header_offset)).await?; - writer - .write_all(&(entry.body_offset as u32 ^ base_magic).to_le_bytes()) - .await?; - trie.create_file(path, entry); - } - - writer.flush().await?; - drop(writer); - Ok(Self { - trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), - archive: std::sync::Arc::new(parking_lot::Mutex::new(buffer)), - version, - base_magic, - }) - } - - _ => Err(Error::NotSupported), - } - } -} - -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(); - - 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); - regress_magic(&mut magic); - - 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 entry = *trie.get_file(path).ok_or(Error::NotExist)?; - archive.seek(SeekFrom::Start(entry.body_offset))?; - - 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()?; - } else if !trie.contains_file(path) { - return Err(Error::NotExist); - } - } - - 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_file_mut(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_file(path) { - return Err(Error::IoError(AlreadyExists.into())); - } - 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 = if path == "" { - name.into() - } else { - format!("{path}/{name}").into() - }; - let metadata = self.metadata(&path)?; - Ok(DirEntry { path, metadata }) - }) - .try_collect() - } else { - Err(Error::NotExist) - } - } -} diff --git a/crates/filesystem/src/archiver/filesystem/impls.rs b/crates/filesystem/src/archiver/filesystem/impls.rs new file mode 100644 index 00000000..b487751c --- /dev/null +++ b/crates/filesystem/src/archiver/filesystem/impls.rs @@ -0,0 +1,839 @@ +// 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 color_eyre::eyre::WrapErr; +use itertools::Itertools; +use rand::Rng; +use std::io::{ + prelude::*, + BufReader, BufWriter, + ErrorKind::{AlreadyExists, InvalidData}, + SeekFrom, +}; + +use super::super::util::{ + advance_magic, move_file_and_truncate, read_file_xor, read_u32_xor, regress_magic, +}; +use super::{Entry, File, FileSystem, MAGIC}; +use crate::{DirEntry, Error, Metadata, OpenFlags, Result}; + +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 c = format!( + "While opening file {path:?} in a version {} archive", + self.version + ); + let mut tmp = crate::host::File::new() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + let mut created = false; + + { + let mut archive = self.archive.lock(); + let mut trie = self.trie.write(); + + if flags.contains(OpenFlags::Create) && !trie.contains_file(path) { + created = true; + match self.version { + 1 | 2 => { + archive + .seek(SeekFrom::Start(8)) + .wrap_err("While reading the header of the archive") + .wrap_err_with(|| c.clone())?; + let mut reader = BufReader::new(archive.as_file()); + let mut magic = MAGIC; + let mut i = 0; + 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)).wrap_err_with(|| format!("While reading the file length (path length = {path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + let entry_len = read_u32_xor(&mut reader, advance_magic(&mut magic)).wrap_err_with(|| format!("While reading the file length (path length = {path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + reader.seek(SeekFrom::Current(entry_len as i64)).wrap_err_with(|| format!("While seeking forward by {entry_len} bytes to read file #{} in the archive", i + 1)).wrap_err_with(|| c.clone())?; + i += 1; + } + drop(reader); + regress_magic(&mut magic); + + let archive_len = archive + .seek(SeekFrom::End(0)) + .wrap_err( + "While writing the path length of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err( + "While writing the path length of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While writing the path of the new file to the archive") + .wrap_err_with(|| c.clone())?; + writer + .write_all(&advance_magic(&mut magic).to_le_bytes()) + .wrap_err( + "While writing the file length of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + writer + .flush() + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + 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() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + + let extra_data_len = path.as_str().bytes().len() as u32 + 16; + let mut headers = Vec::new(); + + archive + .seek(SeekFrom::Start(12)) + .wrap_err("While reading the header of the archive") + .wrap_err_with(|| c.clone())?; + let mut reader = BufReader::new(archive.as_file()); + let mut position = 12; + let mut i = 0; + 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)).wrap_err_with(|| format!("While reading the path length (file offset = {offset}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + let path_len = read_u32_xor(&mut reader, self.base_magic).wrap_err_with(|| format!("While reading the path length (file offset = {offset}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + position = reader.seek(SeekFrom::Current(path_len as i64)).wrap_err_with(|| format!("While seeking forward by {path_len} bytes to read file #{} in the archive", i + 1)).wrap_err_with(|| c.clone())?; + i += 1; + } + drop(reader); + + archive + .seek(SeekFrom::Start(position)) + .wrap_err("While copying the archive body into a temporary file") + .wrap_err_with(|| c.clone())?; + std::io::copy(archive.as_file(), &mut tmp) + .wrap_err("While copying the archive body into a temporary file") + .wrap_err_with(|| c.clone())?; + tmp.flush() + .wrap_err("While copying the archive body into a temporary file") + .wrap_err_with(|| c.clone())?; + + let magic: u32 = rand::thread_rng().gen(); + let archive_len = archive + .metadata() + .wrap_err("While getting the size of the archive") + .wrap_err_with(|| c.clone())? + .size as u32 + + extra_data_len; + let mut writer = BufWriter::new(archive.as_file()); + for (i, (position, offset)) in headers.into_iter().enumerate() { + writer + .seek(SeekFrom::Start(position)) + .wrap_err_with(|| { + format!("While rewriting the file offset of file #{i} to the archive") + }) + .wrap_err_with(|| c.clone())?; + writer + .write_all( + &((offset + extra_data_len) ^ self.base_magic).to_le_bytes(), + ) + .wrap_err_with(|| { + format!("While rewriting the file offset of file #{i} to the archive") + }) + .wrap_err_with(|| c.clone())?; + } + writer + .seek(SeekFrom::Start(position)) + .wrap_err( + "While writing the file offset of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + writer + .write_all(&(archive_len ^ self.base_magic).to_le_bytes()) + .wrap_err( + "While writing the file offset of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + writer + .write_all(&self.base_magic.to_le_bytes()) + .wrap_err( + "While writing the file length of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + writer + .write_all(&(magic ^ self.base_magic).to_le_bytes()) + .wrap_err( + "While writing the base magic value of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + writer + .write_all( + &(path.as_str().bytes().len() as u32 ^ self.base_magic) + .to_le_bytes(), + ) + .wrap_err( + "While writing the path length of the new file to the archive", + ) + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While writing the path of the new file to the archive") + .wrap_err_with(|| c.clone())?; + tmp.seek(SeekFrom::Start(0)).wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + std::io::copy(&mut tmp, &mut writer).wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + writer.flush().wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + drop(writer); + + trie.create_file( + path, + Entry { + header_offset: position, + body_offset: archive_len as u64, + size: 0, + start_magic: magic, + }, + ); + } + + _ => return Err(Error::InvalidArchiveVersion(self.version).into()), + } + } else if !flags.contains(OpenFlags::Truncate) { + let entry = *trie + .get_file(path) + .ok_or(Error::NotExist) + .wrap_err("While copying the file within the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + archive + .seek(SeekFrom::Start(entry.body_offset)) + .wrap_err("While copying the file within the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + + 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, + ) + .wrap_err("While copying the file within the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + tmp.flush() + .wrap_err("While copying the file within the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + } else if !trie.contains_file(path) { + return Err(Error::NotExist.into()); + } + } + + tmp.seek(SeekFrom::Start(0)) + .wrap_err("While copying the file within the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + 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.into()) + } + } + + fn rename( + &self, + from: impl AsRef, + to: impl AsRef, + ) -> Result<()> { + let from = from.as_ref(); + let to = to.as_ref(); + + let mut archive = self.archive.lock(); + let mut trie = self.trie.write(); + let c = format!( + "While renaming {from:?} to {to:?} in a version {} archive", + self.version + ); + + if trie.contains_dir(from) { + return Err(Error::NotSupported.into()); + } + if trie.contains(to) { + return Err(Error::IoError(AlreadyExists.into()).into()); + } + if !trie.contains_dir( + from.parent() + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?, + ) { + return Err(Error::NotExist.into()); + } + let Some(old_entry) = trie.get_file(from).copied() else { + return Err(Error::NotExist.into()); + }; + + let archive_len = archive + .metadata() + .wrap_err("While getting the length of the archive") + .wrap_err_with(|| c.clone())? + .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() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + archive.seek(SeekFrom::Start(old_entry.body_offset)).wrap_err("While copying the contents of the file within the archive into a temporary file").wrap_err_with(|| c.clone())?; + 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, + ).wrap_err("While copying the contents of the file within the archive into a temporary file").wrap_err_with(|| c.clone())?; + tmp.flush().wrap_err("While copying the contents of the file within the archive into a temporary file").wrap_err_with(|| c.clone())?; + 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, + ) + .wrap_err("While relocating the file header to the end of the archive") + .wrap_err_with(|| c.clone())?; + let mut new_entry = *trie + .get_file(from) + .ok_or(Error::InvalidHeader) + .wrap_err("While relocating the file header to the end of the archive") + .wrap_err_with(|| c.clone())?; + trie.remove_file(from) + .ok_or(Error::InvalidHeader) + .wrap_err("While relocating the file header to the end of the archive") + .wrap_err_with(|| c.clone())?; + 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)) + .wrap_err("While rewriting the path length of the file to the archive") + .wrap_err_with(|| c.clone())?; + let mut writer = BufWriter::new(archive.as_file()); + writer + .write_all(&(to_len as u32 ^ advance_magic(&mut magic)).to_le_bytes()) + .wrap_err("While rewriting the path length of the file to the archive") + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While rewriting the path of the file to the archive") + .wrap_err_with(|| c.clone())?; + writer + .write_all( + &(old_entry.size as u32 ^ advance_magic(&mut magic)).to_le_bytes(), + ) + .wrap_err("While rewriting the file length of the file to the archive") + .wrap_err_with(|| c.clone())?; + + new_entry.start_magic = magic; + + // Move the file contents to the end + tmp.seek(SeekFrom::Start(0)) + .wrap_err("While relocating the file contents to the end of the archive") + .wrap_err_with(|| c.clone())?; + let mut reader = BufReader::new(&mut tmp); + std::io::copy(&mut read_file_xor(&mut reader, magic), &mut writer) + .wrap_err("While relocating the file contents to the end of the archive") + .wrap_err_with(|| c.clone())?; + writer + .flush() + .wrap_err("While relocating the file contents to the end of the archive") + .wrap_err_with(|| c.clone())?; + 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() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + archive + .seek(SeekFrom::Start( + old_entry.header_offset + from_len as u64 + 16, + )) + .wrap_err("While copying the contents of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + std::io::copy(archive.as_file(), &mut tmp) + .wrap_err("While copying the contents of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + tmp.flush() + .wrap_err("While copying the contents of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + + // Change the path + archive + .seek(SeekFrom::Start(old_entry.header_offset + 12)) + .wrap_err("While rewriting the path length of the file to the archive") + .wrap_err_with(|| c.clone())?; + let mut writer = BufWriter::new(archive.as_file()); + writer + .write_all(&(to_len as u32 ^ self.base_magic).to_le_bytes()) + .wrap_err("While rewriting the path length of the file to the archive") + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While rewriting the path of the file to the archive") + .wrap_err_with(|| c.clone())?; + trie.remove_file(from) + .ok_or(Error::InvalidHeader) + .wrap_err("While rewriting the header of the file to the archive") + .wrap_err_with(|| c.clone())?; + trie.create_file(to, old_entry); + + // Move everything else back + tmp.seek(SeekFrom::Start(0)).wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + std::io::copy(&mut tmp, &mut writer).wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + writer.flush().wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + drop(writer); + + // Update all of the offsets in the headers + archive + .seek(SeekFrom::Start(12)) + .wrap_err("While rewriting the header of the archive") + .wrap_err_with(|| c.clone())?; + let mut reader = BufReader::new(archive.as_file()); + let mut headers = Vec::new(); + let mut i = 0; + 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() + .wrap_err_with(|| { + format!("While reading the path length of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())? + .checked_sub(4) + .ok_or(Error::InvalidHeader) + .wrap_err_with(|| { + format!("While reading the path length of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + reader + .seek(SeekFrom::Current(8)) + .wrap_err_with(|| { + format!("While reading the path length of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + let current_path_len = read_u32_xor(&mut reader, self.base_magic) + .wrap_err_with(|| { + format!("While reading the path length of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + + let mut current_path = vec![0; current_path_len as usize]; + reader.read_exact(&mut current_path).wrap_err_with(|| format!("While reading the path (path length = {current_path_len} of file #{i}) in the archive")).wrap_err_with(|| c.clone())?; + 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::PathUtf8Error).wrap_err_with(|| format!("While reading the path (path length = {current_path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + + let current_body_offset = (current_body_offset as u64) + .checked_add_signed(to_len as i64 - from_len as i64) + .ok_or(Error::InvalidHeader) + .wrap_err_with(|| { + format!( + "While reading the header (path = {current_path}) of file #{i} in the archive" + ) + }) + .wrap_err_with(|| c.clone())?; + trie.get_file_mut(¤t_path) + .ok_or(Error::InvalidHeader) + .wrap_err_with(|| { + format!( + "While reading the header (path = {current_path}) of file #{i} in the archive" + ) + }) + .wrap_err_with(|| c.clone())? + .body_offset = current_body_offset; + headers.push((current_header_offset, current_body_offset as u32)); + i += 1; + } + drop(reader); + let mut writer = BufWriter::new(archive.as_file()); + for (i, (position, offset)) in headers.into_iter().enumerate() { + writer.seek(SeekFrom::Start(position))?; + writer + .write_all(&(offset ^ self.base_magic).to_le_bytes()) + .wrap_err_with(|| { + format!( + "While rewriting the file offset of file #{i} to the archive" + ) + }) + .wrap_err_with(|| c.clone())?; + } + writer + .flush() + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + drop(writer); + } + + _ => return Err(Error::InvalidHeader.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::InvalidHeader) + .wrap_err("While truncating the archive") + .wrap_err_with(|| c.clone())?, + )?; + archive + .flush() + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + } + } 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)) + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + archive + .flush() + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + } + + 3 => { + archive + .seek(SeekFrom::Start(old_entry.header_offset + 16)) + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + 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(), + ) + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + archive + .flush() + .wrap_err("While rewriting the path of the file in-place to the archive") + .wrap_err_with(|| c.clone())?; + } + + _ => return Err(Error::InvalidHeader.into()), + } + } + + Ok(()) + } + + fn create_dir(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let mut trie = self.trie.write(); + if trie.contains_file(path) { + return Err(Error::IoError(AlreadyExists.into())).wrap_err_with(|| { + format!( + "While creating a directory at {path:?} within a version {} archive", + self.version + ) + }); + } + 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<()> { + let path = path.as_ref(); + let c = format!( + "While removing a directory at {path:?} within a version {} archive", + self.version + ); + if !self.trie.read().contains_dir(path) { + return Err(Error::NotExist).wrap_err_with(|| c.clone()); + } + + let paths = self + .trie + .read() + .iter_prefix(path) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())? + .map(|(k, _)| k) + .collect_vec(); + for file_path in paths { + self.remove_file(&file_path) + .wrap_err_with(|| format!("While removing a file {file_path:?} within the archive")) + .wrap_err_with(|| c.clone())?; + } + + self.trie + .write() + .remove_dir(path) + .then_some(()) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?; + Ok(()) + } + + fn remove_file(&self, path: impl AsRef) -> Result<()> { + 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 c = format!( + "While removing a file at {path:?} within a version {} archive", + self.version + ); + + let entry = *trie + .get_file(path) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?; + let archive_len = archive.metadata().wrap_err_with(|| c.clone())?.size; + + move_file_and_truncate(&mut archive, &mut trie, path, self.version, self.base_magic) + .wrap_err("While relocating the file header to the end of the archive") + .wrap_err_with(|| c.clone())?; + + match self.version { + 1 | 2 => { + archive + .set_len( + archive_len + .checked_sub(entry.size + path_len + 8) + .ok_or(Error::IoError(InvalidData.into())) + .wrap_err("While truncating the archive") + .wrap_err_with(|| c.clone())?, + ) + .wrap_err("While truncating the archive") + .wrap_err_with(|| c.clone())?; + archive + .flush() + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + } + + 3 => { + // Remove the header of the deleted file + let mut tmp = crate::host::File::new() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + archive + .seek(SeekFrom::Start(entry.header_offset + path_len + 16)) + .wrap_err("While copying the header of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + std::io::copy(archive.as_file(), &mut tmp) + .wrap_err("While copying the header of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + tmp.flush() + .wrap_err("While copying the header of the archive into a temporary file") + .wrap_err_with(|| c.clone())?; + tmp.seek(SeekFrom::Start(0)).wrap_err("While copying a temporary file containing the archive header into the archive").wrap_err_with(|| c.clone())?; + archive.seek(SeekFrom::Start(entry.header_offset)).wrap_err("While copying a temporary file containing the archive header into the archive").wrap_err_with(|| c.clone())?; + std::io::copy(&mut tmp, archive.as_file()).wrap_err("While copying a temporary file containing the archive header into the archive").wrap_err_with(|| c.clone())?; + + archive + .set_len( + archive_len + .checked_sub(entry.size + path_len + 16) + .ok_or(Error::IoError(InvalidData.into())) + .wrap_err("While truncating the archive") + .wrap_err_with(|| c.clone())?, + ) + .wrap_err("While truncating the archive") + .wrap_err_with(|| c.clone())?; + archive + .flush() + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + } + + _ => { + return Err(Error::InvalidArchiveVersion(self.version)).wrap_err_with(|| c.clone()) + } + } + + trie.remove_file(path); + Ok(()) + } + + fn read_dir(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + let trie = self.trie.read(); + let c = format!( + "While reading the contents of the directory {path:?} in a version {} archive", + self.version + ); + if let Some(iter) = trie.iter_dir(path) { + iter.map(|(name, _)| { + let path = if path == "" { + name.into() + } else { + format!("{path}/{name}").into() + }; + let metadata = self + .metadata(&path) + .wrap_err_with(|| { + format!("While getting the metadata of {path:?} in the archive") + }) + .wrap_err_with(|| c.clone())?; + Ok(DirEntry { path, metadata }) + }) + .try_collect() + } else { + Err(Error::NotExist).wrap_err_with(|| c.clone()) + } + } +} diff --git a/crates/filesystem/src/archiver/filesystem/mod.rs b/crates/filesystem/src/archiver/filesystem/mod.rs new file mode 100644 index 00000000..12870314 --- /dev/null +++ b/crates/filesystem/src/archiver/filesystem/mod.rs @@ -0,0 +1,422 @@ +// 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 async_std::io::{BufReader as AsyncBufReader, BufWriter as AsyncBufWriter}; +use color_eyre::eyre::WrapErr; +use itertools::Itertools; +use rand::Rng; +use std::io::{prelude::*, BufReader, SeekFrom}; + +use super::util::{advance_magic, read_file_xor_async, read_header, read_u32_xor}; +use super::{Entry, File, Trie, HEADER, MAGIC}; +use crate::{Error, Result}; + +mod impls; + +#[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 Clone for FileSystem { + fn clone(&self) -> Self { + Self { + trie: self.trie.clone(), + archive: self.archive.clone(), + version: self.version, + base_magic: self.base_magic, + } + } +} + +impl FileSystem +where + T: crate::File, +{ + /// Creates a new archiver filesystem from a file containing an existing archive. + pub fn new(mut file: T) -> Result { + file.seek(SeekFrom::Start(0)) + .wrap_err("While detecting archive version")?; + let mut reader = BufReader::new(&mut file); + + let version = read_header(&mut reader).wrap_err("While detecting archive version")?; + + let mut trie = crate::FileSystemTrie::new(); + + let mut base_magic = MAGIC; + + let c = format!( + "While performing initial parsing of the header of a version {version} archive" + ); + + match version { + 1 | 2 => { + let mut magic = MAGIC; + + let mut i = 0; + + 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).wrap_err("").wrap_err_with(|| format!("While reading the path (path length = {path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + 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).wrap_err_with(|| format!("While reading the path (path length = {path_len}) of file #{i} in the archive)")).wrap_err_with(|| c.clone())?); + + let entry_len = read_u32_xor(&mut reader, advance_magic(&mut magic)) + .wrap_err_with(|| { + format!("While reading the file length (path = {path:?}) of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + + let stream_position = reader + .stream_position() + .wrap_err_with(|| { + format!("While reading the file length (path = {path:?}) of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + let entry = Entry { + size: entry_len as u64, + header_offset: stream_position + .checked_sub(path_len as u64 + 8) + .ok_or(Error::InvalidHeader).wrap_err_with(|| format!("While reading the file length (path = {path:?}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?, + body_offset: stream_position, + start_magic: magic, + }; + + trie.create_file(path, entry); + + reader + .seek(SeekFrom::Start(entry.body_offset + entry.size)) + .wrap_err_with(|| { + format!( + "While seeking to offset {} to read file #{} in the archive", + entry.body_offset + entry.size, + i + 1 + ) + }) + .wrap_err_with(|| c.clone())?; + i += 1; + } + } + 3 => { + let mut u32_buf = [0; 4]; + reader + .read_exact(&mut u32_buf) + .wrap_err("While reading the base magic value of the archive") + .wrap_err_with(|| c.clone())?; + + base_magic = u32::from_le_bytes(u32_buf); + base_magic = base_magic.wrapping_mul(9).wrapping_add(3); + + let mut i = 0; + + while let Ok(body_offset) = read_u32_xor(&mut reader, base_magic) { + if body_offset == 0 { + break; + } + let header_offset = reader + .stream_position() + .wrap_err_with(|| { + format!("While reading the file offset of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())? + .checked_sub(4) + .ok_or(Error::InvalidHeader) + .wrap_err_with(|| { + format!("While reading the file offset of file #{i} in the archive") + }) + .wrap_err_with(|| c.clone())?; + + let entry_len = read_u32_xor(&mut reader, base_magic).wrap_err_with(|| format!("While reading the file length (file offset = {body_offset}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + let magic = read_u32_xor(&mut reader, base_magic).wrap_err_with(|| format!("While reading the magic value (file offset = {body_offset}, file length = {entry_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + let path_len = read_u32_xor(&mut reader, base_magic).wrap_err_with(|| format!("While reading the path length (file offset = {body_offset}, file length = {entry_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + + let mut path = vec![0; path_len as usize]; + reader.read_exact(&mut path).wrap_err_with(|| format!("While reading the path (file offset = {body_offset}, file length = {entry_len}, path length = {path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?; + 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).wrap_err_with(|| format!("While reading the path (file offset = {body_offset}, file length = {entry_len}, path length = {path_len}) of file #{i} in the archive")).wrap_err_with(|| c.clone())?); + + let entry = Entry { + size: entry_len as u64, + header_offset, + body_offset: body_offset as u64, + start_magic: magic, + }; + trie.create_file(path, entry); + i += 1; + } + } + _ => return Err(Error::InvalidArchiveVersion(version).into()), + } + + Ok(Self { + trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), + archive: std::sync::Arc::new(parking_lot::Mutex::new(file)), + version, + base_magic, + }) + } + + /// Creates a new archiver filesystem from the given files. + /// The contents of the archive itself will be stored in `buffer`. + pub async fn from_buffer_and_files<'a, I, P, R>( + mut buffer: T, + version: u8, + files: I, + ) -> Result + where + T: futures_lite::AsyncWrite + futures_lite::AsyncSeek + Unpin, + I: Iterator>, + P: AsRef + 'a, + R: futures_lite::AsyncRead + Unpin, + { + use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; + + let c = format!("While creating a new version {version} archive"); + + buffer + .set_len(0) + .wrap_err("While clearing the archive") + .wrap_err_with(|| c.clone())?; + AsyncSeekExt::seek(&mut buffer, SeekFrom::Start(0)) + .await + .wrap_err("While clearing the archive") + .wrap_err_with(|| c.clone())?; + + let mut writer = AsyncBufWriter::new(&mut buffer); + writer + .write_all(HEADER) + .await + .wrap_err("While writing the archive version") + .wrap_err_with(|| c.clone())?; + writer + .write_all(&[version]) + .await + .wrap_err("While writing the archive version") + .wrap_err_with(|| c.clone())?; + + let mut trie = Trie::new(); + + match version { + 1 | 2 => { + let mut magic = MAGIC; + let mut header_offset = 8; + + for (i, result) in files.enumerate() { + let (path, size, file) = result + .wrap_err_with(|| { + format!( + "While getting file #{i} to add to the archive from the iterator" + ) + }) + .wrap_err_with(|| c.clone())?; + let reader = AsyncBufReader::new(file.take(size as u64)); + let path = path.as_ref(); + let header_size = path.as_str().bytes().len() as u64 + 8; + + // Write the header + writer + .write_all( + &(path.as_str().bytes().len() as u32 ^ advance_magic(&mut magic)) + .to_le_bytes(), + ) + .await.wrap_err_with(|| format!("While writing the path length of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + 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(), + ) + .await.wrap_err_with(|| format!("While writing the path of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + writer + .write_all(&(size ^ advance_magic(&mut magic)).to_le_bytes()) + .await.wrap_err_with(|| format!("While writing the file length of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + + // Write the file contents + async_std::io::copy(&mut read_file_xor_async(reader, magic), &mut writer) + .await.wrap_err_with(|| format!("While writing the contents of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + + trie.create_file( + path, + Entry { + header_offset, + body_offset: header_offset + header_size, + size: size as u64, + start_magic: magic, + }, + ); + + header_offset += header_size + size as u64; + } + + writer + .flush() + .await + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + drop(writer); + Ok(Self { + trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), + archive: std::sync::Arc::new(parking_lot::Mutex::new(buffer)), + version, + base_magic: MAGIC, + }) + } + + 3 => { + let mut tmp = crate::host::File::new() + .wrap_err("While creating a temporary file") + .wrap_err_with(|| c.clone())?; + let mut tmp_writer = AsyncBufWriter::new(&mut tmp); + let mut entries = if let (_, Some(upper_bound)) = files.size_hint() { + Vec::with_capacity(upper_bound) + } else { + Vec::new() + }; + + let base_magic: u32 = rand::thread_rng().gen(); + writer + .write_all(&(base_magic.wrapping_sub(3).wrapping_mul(954437177)).to_le_bytes()) + .await + .wrap_err("While writing the archive base magic value") + .wrap_err_with(|| c.clone())?; + let mut header_offset = 12; + let mut body_offset = 0; + + for (i, result) in files.enumerate() { + let (path, size, file) = result + .wrap_err_with(|| { + format!( + "While getting file #{i} to write to the archive from the iterator" + ) + }) + .wrap_err_with(|| c.clone())?; + let reader = AsyncBufReader::new(file.take(size as u64)); + let path = path.as_ref(); + let entry_magic: u32 = rand::thread_rng().gen(); + + // Write the header to the buffer, except for the offset + writer.seek(SeekFrom::Current(4)).await.wrap_err_with(|| format!("While writing the file length of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + writer.write_all(&(size ^ base_magic).to_le_bytes()).await.wrap_err_with(|| format!("While writing the file length of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + writer + .write_all(&(entry_magic ^ base_magic).to_le_bytes()) + .await.wrap_err_with(|| format!("While writing the magic value of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + writer + .write_all(&(path.as_str().bytes().len() as u32 ^ base_magic).to_le_bytes()) + .await.wrap_err_with(|| format!("While writing the path length of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + writer + .write_all( + &path + .as_str() + .bytes() + .enumerate() + .map(|(i, b)| { + let b = if b == b'/' { b'\\' } else { b }; + b ^ (base_magic >> (8 * (i % 4))) as u8 + }) + .collect_vec(), + ) + .await.wrap_err_with(|| format!("While writing the path of file #{i} (path = {path:?}, file length = {size}) to the archive")).wrap_err_with(|| c.clone())?; + + // Write the actual file contents to a temporary file + async_std::io::copy( + &mut read_file_xor_async(reader, entry_magic), + &mut tmp_writer, + ) + .await.wrap_err_with(|| format!("While writing the contents of file #{i} (path = {path:?}, file length = {size}) to a temporary file before writing it to the archive")).wrap_err_with(|| c.clone())?; + + entries.push(( + path.to_owned(), + Entry { + header_offset, + body_offset, + size: size as u64, + start_magic: entry_magic, + }, + )); + + header_offset += path.as_str().bytes().len() as u64 + 16; + body_offset += size as u64; + } + + // Write the terminator at the end of the buffer + writer + .write_all(&base_magic.to_le_bytes()) + .await + .wrap_err("While writing the header terminator to the archive") + .wrap_err_with(|| c.clone())?; + + // Write the contents of the temporary file to the buffer after the terminator + tmp_writer + .flush() + .await + .wrap_err("While flushing a temporary file containing the archive body") + .wrap_err_with(|| c.clone())?; + drop(tmp_writer); + AsyncSeekExt::seek(&mut tmp, SeekFrom::Start(0)).await.wrap_err("While copying a temporary file containing the archive body into the archive").wrap_err_with(|| c.clone())?; + async_std::io::copy(&mut tmp, &mut writer).await.wrap_err("While copying a temporary file containin the archive body into the archive").wrap_err_with(|| c.clone())?; + + // Write the offsets into the header now that we know the total size of the files + let header_size = header_offset + 4; + for (i, (path, mut entry)) in entries.into_iter().enumerate() { + entry.body_offset += header_size; + writer.seek(SeekFrom::Start(entry.header_offset)).await.wrap_err_with(|| format!("While writing the file offset of file #{i} (path = {path:?}, file length = {}, file offset = {})", entry.size, entry.body_offset)).wrap_err_with(|| c.clone())?; + writer + .write_all(&(entry.body_offset as u32 ^ base_magic).to_le_bytes()) + .await.wrap_err_with(|| format!("While writing the file offset of file #{i} (path = {path:?}, file length = {}, file offset = {}) to the archive", entry.size, entry.body_offset)).wrap_err_with(|| c.clone())?; + trie.create_file(path, entry); + } + + writer + .flush() + .await + .wrap_err("While flushing the archive after writing its contents") + .wrap_err_with(|| c.clone())?; + drop(writer); + Ok(Self { + trie: std::sync::Arc::new(parking_lot::RwLock::new(trie)), + archive: std::sync::Arc::new(parking_lot::Mutex::new(buffer)), + version, + base_magic, + }) + } + + _ => Err(Error::NotSupported.into()), + } + } +} diff --git a/crates/filesystem/src/egui_bytes_loader.rs b/crates/filesystem/src/egui_bytes_loader.rs index 56e52d2e..482e0a7d 100644 --- a/crates/filesystem/src/egui_bytes_loader.rs +++ b/crates/filesystem/src/egui_bytes_loader.rs @@ -27,12 +27,10 @@ use std::sync::Arc; use dashmap::{DashMap, DashSet}; use egui::load::{Bytes, BytesPoll, LoadError}; -use crate::Error; - #[derive(Default)] pub struct Loader { loaded_files: DashMap>, - errored_files: DashMap, + errored_files: DashMap, unloaded_files: DashSet, } diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs index f645eb61..2323f367 100644 --- a/crates/filesystem/src/lib.rs +++ b/crates/filesystem/src/lib.rs @@ -53,6 +53,8 @@ pub enum Error { NotSupported, #[error("Archive header is incorrect")] InvalidHeader, + #[error("Invalid archive version: {0} (supported versions are 1, 2 and 3)")] + InvalidArchiveVersion(u8), #[error("No filesystems are loaded to perform this operation")] NoFilesystems, #[error("Unable to detect the project's RPG Maker version (perhaps you did not open an RPG Maker project?")] @@ -67,7 +69,51 @@ pub enum Error { MissingIDB, } -pub type Result = std::result::Result; +pub use color_eyre::Result; + +pub trait StdIoErrorExt { + // Add additional context to a `std::io::Result`. + fn wrap_io_err_with(self, c: impl FnOnce() -> C) -> Self + where + Self: Sized, + C: std::fmt::Display + Send + Sync + 'static; + + // Add additional context to a `std::io::Result`. + fn wrap_io_err(self, c: C) -> Self + where + Self: Sized, + C: std::fmt::Display + Send + Sync + 'static, + { + self.wrap_io_err_with(|| c) + } + + // Add additional context to a `std::io::Result`. This is an alias for `.wrap_io_err_with`. + fn with_io_context(self, c: impl FnOnce() -> C) -> Self + where + Self: Sized, + C: std::fmt::Display + Send + Sync + 'static, + { + self.wrap_io_err_with(c) + } + + // Add additional context to a `std::io::Result`. This is an alias for `.wrap_io_err`. + fn io_context(self, c: C) -> Self + where + Self: Sized, + C: std::fmt::Display + Send + Sync + 'static, + { + self.wrap_io_err(c) + } +} + +impl StdIoErrorExt for std::io::Result { + fn wrap_io_err_with(self, c: impl FnOnce() -> C) -> Self + where + C: std::fmt::Display + Send + Sync + 'static, + { + self.map_err(|e| std::io::Error::new(e.kind(), color_eyre::eyre::eyre!(e).wrap_err(c()))) + } +} #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct Metadata { diff --git a/crates/filesystem/src/list.rs b/crates/filesystem/src/list.rs index 57b3259a..0f5ca4c0 100644 --- a/crates/filesystem/src/list.rs +++ b/crates/filesystem/src/list.rs @@ -15,7 +15,8 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crate::{erased::ErasedFilesystem, DirEntry, Error, File, Metadata, OpenFlags}; +use crate::{erased::ErasedFilesystem, DirEntry, Error, File, Metadata, OpenFlags, Result}; +use color_eyre::eyre::WrapErr; use itertools::Itertools; #[derive(Default)] @@ -40,78 +41,112 @@ impl crate::FileSystem for FileSystem { &self, path: impl AsRef, flags: OpenFlags, - ) -> Result { + ) -> Result { let path = path.as_ref(); + let c = format!("While opening file {path:?} in a list filesystem"); let parent = path.parent().unwrap_or(path); for fs in self.filesystems.iter() { - if fs.exists(path)? || (flags.contains(OpenFlags::Create) && fs.exists(parent)?) { - return fs.open_file(path, flags); + if fs.exists(path).wrap_err_with(|| c.clone())? + || (flags.contains(OpenFlags::Create) + && fs.exists(parent).wrap_err_with(|| c.clone())?) + { + return fs.open_file(path, flags).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } - fn metadata(&self, path: impl AsRef) -> Result { + fn metadata(&self, path: impl AsRef) -> Result { let path = path.as_ref(); + let c = format!("While getting metadata for {path:?} in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(path)? { - return fs.metadata(path); + if fs.exists(path).wrap_err_with(|| c.clone())? { + return fs.metadata(path).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } fn rename( &self, from: impl AsRef, to: impl AsRef, - ) -> Result<(), Error> { + ) -> Result<()> { let from = from.as_ref(); + let to = to.as_ref(); + let c = format!("While renaming {from:?} to {to:?} in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(from)? { - return fs.rename(from, to.as_ref()); + if fs.exists(from).wrap_err_with(|| c.clone())? { + return fs.rename(from, to.as_ref()).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } - fn exists(&self, path: impl AsRef) -> Result { + fn exists(&self, path: impl AsRef) -> Result { let path = path.as_ref(); + let c = format!("While checking if {path:?} exists in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(path)? { + if fs.exists(path).wrap_err_with(|| c.clone())? { return Ok(true); } } Ok(false) } - fn create_dir(&self, path: impl AsRef) -> Result<(), Error> { - let fs = self.filesystems.first().ok_or(Error::NoFilesystems)?; - fs.create_dir(path.as_ref()) + fn create_dir(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let c = format!("While creating a directory at {path:?} in a list filesystem"); + let fs = self + .filesystems + .first() + .ok_or(Error::NoFilesystems) + .wrap_err_with(|| c.clone())?; + fs.create_dir(path).wrap_err_with(|| c.clone()) } - fn remove_dir(&self, path: impl AsRef) -> Result<(), Error> { - let fs = self.filesystems.first().ok_or(Error::NoFilesystems)?; - fs.remove_dir(path.as_ref()) + fn remove_dir(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let c = format!("While removing a directory at {path:?} in a list filesystem"); + let fs = self + .filesystems + .first() + .ok_or(Error::NoFilesystems) + .wrap_err_with(|| c.clone())?; + fs.remove_dir(path).wrap_err_with(|| c.clone()) } - fn remove_file(&self, path: impl AsRef) -> Result<(), Error> { - let fs = self.filesystems.first().ok_or(Error::NoFilesystems)?; - fs.remove_file(path.as_ref()) + fn remove_file(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let c = format!("While removing a file at {path:?} in a list filesystem"); + let fs = self + .filesystems + .first() + .ok_or(Error::NoFilesystems) + .wrap_err_with(|| c.clone())?; + fs.remove_file(path).wrap_err_with(|| c.clone()) } - fn remove(&self, path: impl AsRef) -> Result<(), Error> { - let fs = self.filesystems.first().ok_or(Error::NoFilesystems)?; - fs.remove(path.as_ref()) + fn remove(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let c = format!("While removing {path:?} in a list filesystem"); + let fs = self + .filesystems + .first() + .ok_or(Error::NoFilesystems) + .wrap_err_with(|| c.clone())?; + fs.remove(path).wrap_err_with(|| c.clone()) } - fn read_dir(&self, path: impl AsRef) -> Result, Error> { + fn read_dir(&self, path: impl AsRef) -> Result> { let path = path.as_ref(); + let c = + format!("While reading the contents of the directory {path:?} in a list filesystem"); let mut entries = Vec::new(); for fs in self.filesystems.iter() { - if fs.exists(path)? { - entries.extend(fs.read_dir(path)?) + if fs.exists(path).wrap_err_with(|| c.clone())? { + entries.extend(fs.read_dir(path).wrap_err_with(|| c.clone())?) } } // FIXME: remove duplicates in a more efficient manner @@ -120,37 +155,36 @@ impl crate::FileSystem for FileSystem { Ok(entries) } - fn read(&self, path: impl AsRef) -> Result, Error> { + fn read(&self, path: impl AsRef) -> Result> { let path = path.as_ref(); + let c = format!("While reading from the file {path:?} in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(path)? { - return fs.read(path); + if fs.exists(path).wrap_err_with(|| c.clone())? { + return fs.read(path).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } - fn read_to_string(&self, path: impl AsRef) -> Result { + fn read_to_string(&self, path: impl AsRef) -> Result { let path = path.as_ref(); + let c = format!("While reading from the file {path:?} in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(path)? { - return fs.read_to_string(path); + if fs.exists(path).wrap_err_with(|| c.clone())? { + return fs.read_to_string(path).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } - fn write( - &self, - path: impl AsRef, - data: impl AsRef<[u8]>, - ) -> Result<(), Error> { + fn write(&self, path: impl AsRef, data: impl AsRef<[u8]>) -> Result<()> { let path = path.as_ref(); + let c = format!("While writing to the file {path:?} in a list filesystem"); for fs in self.filesystems.iter() { - if fs.exists(path)? { - return fs.write(path, data.as_ref()); + if fs.exists(path).wrap_err_with(|| c.clone())? { + return fs.write(path, data.as_ref()).wrap_err_with(|| c.clone()); } } - Err(Error::NotExist) + Err(Error::NotExist).wrap_err_with(|| c.clone()) } } diff --git a/crates/filesystem/src/native.rs b/crates/filesystem/src/native.rs index 247a4a3c..04795f2e 100644 --- a/crates/filesystem/src/native.rs +++ b/crates/filesystem/src/native.rs @@ -14,9 +14,11 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . + +use color_eyre::eyre::WrapErr; use itertools::Itertools; -use crate::{DirEntry, Metadata, OpenFlags, Result}; +use crate::{DirEntry, Metadata, OpenFlags, Result, StdIoErrorExt}; use pin_project::pin_project; use std::{ io::ErrorKind::{InvalidInput, PermissionDenied}, @@ -34,6 +36,7 @@ pub struct FileSystem { pub struct File { file: Inner, path: camino::Utf8PathBuf, + stripped_path: Option, #[pin] async_file: async_fs::File, } @@ -56,28 +59,32 @@ impl FileSystem { } pub async fn from_folder_picker() -> Result { + let c = "While picking a folder from the host filesystem"; if let Some(path) = rfd::AsyncFileDialog::default().pick_folder().await { - let path = - camino::Utf8Path::from_path(path.path()).ok_or(crate::Error::PathUtf8Error)?; + let path = camino::Utf8Path::from_path(path.path()) + .ok_or(crate::Error::PathUtf8Error) + .wrap_err(c)?; Ok(Self::new(path)) } else { - Err(crate::Error::CancelledLoading) + Err(crate::Error::CancelledLoading).wrap_err(c) } } pub async fn from_file_picker() -> Result { + let c = "While picking a folder from the host filesystem"; if let Some(path) = rfd::AsyncFileDialog::default() .add_filter("project file", &["rxproj", "rvproj", "rvproj2", "lumproj"]) .pick_file() .await { let path = camino::Utf8Path::from_path(path.path()) - .ok_or(crate::Error::PathUtf8Error)? + .ok_or(crate::Error::PathUtf8Error) + .wrap_err(c)? .parent() .expect("path does not have parent"); Ok(Self::new(path)) } else { - Err(crate::Error::CancelledLoading) + Err(crate::Error::CancelledLoading).wrap_err(c) } } } @@ -90,7 +97,9 @@ impl crate::FileSystem for FileSystem { path: impl AsRef, flags: OpenFlags, ) -> Result { - let path = self.root_path.join(path); + let stripped_path = path.as_ref(); + let path = self.root_path.join(path.as_ref()); + let c = format!("While opening file {path:?} in a host folder"); std::fs::OpenOptions::new() .create(flags.contains(OpenFlags::Create)) .write(flags.contains(OpenFlags::Write)) @@ -98,18 +107,24 @@ impl crate::FileSystem for FileSystem { .truncate(flags.contains(OpenFlags::Truncate)) .open(&path) .map(|file| { - let clone = file.try_clone()?; + let clone = file.try_clone().wrap_err_with(|| c.clone())?; Ok(File { file: Inner::StdFsFile(file), path, + stripped_path: Some(stripped_path.to_owned()), async_file: clone.into(), }) - })? + }) + .wrap_err_with(|| c.clone())? } fn metadata(&self, path: impl AsRef) -> Result { + let c = format!( + "While getting metadata for {:?} in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - let metadata = std::fs::metadata(path)?; + let metadata = std::fs::metadata(path).wrap_err_with(|| c.clone())?; Ok(Metadata { is_file: metadata.is_file(), size: metadata.len(), @@ -121,34 +136,60 @@ impl crate::FileSystem for FileSystem { from: impl AsRef, to: impl AsRef, ) -> Result<()> { + let c = format!( + "While renaming {:?} to {:?} in a host folder", + from.as_ref(), + to.as_ref() + ); let from = self.root_path.join(from); let to = self.root_path.join(to); - std::fs::rename(from, to).map_err(Into::into) + std::fs::rename(from, to).wrap_err(c) } fn exists(&self, path: impl AsRef) -> Result { + let c = format!( + "While checking if {:?} exists in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - path.try_exists().map_err(Into::into) + path.try_exists().wrap_err(c) } fn create_dir(&self, path: impl AsRef) -> Result<()> { + let c = format!( + "While creating a directory at {:?} in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - std::fs::create_dir_all(path).map_err(Into::into) + std::fs::create_dir_all(path).wrap_err(c) } fn remove_dir(&self, path: impl AsRef) -> Result<()> { + let c = format!( + "While removing a directory at {:?} in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - std::fs::remove_dir_all(path).map_err(Into::into) + std::fs::remove_dir_all(path).wrap_err(c) } fn remove_file(&self, path: impl AsRef) -> Result<()> { + let c = format!( + "While removing a file at {:?} in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - std::fs::remove_file(path).map_err(Into::into) + std::fs::remove_file(path).wrap_err(c) } fn read_dir(&self, path: impl AsRef) -> Result> { + let c = format!( + "While reading the contents of the directory {:?} in a host folder", + path.as_ref() + ); let path = self.root_path.join(path); - path.read_dir_utf8()? + path.read_dir_utf8() + .wrap_err_with(|| c.clone())? .map_ok(|entry| { let path = entry.into_path(); let path = path @@ -160,7 +201,7 @@ impl crate::FileSystem for FileSystem { #[cfg(windows)] let path = path.into_string().replace('\\', "/").into(); - let metadata = self.metadata(&path)?; + let metadata = self.metadata(&path).wrap_err_with(|| c.clone())?; Ok(DirEntry::new(path, metadata)) }) .flatten() @@ -171,12 +212,22 @@ impl crate::FileSystem for FileSystem { impl File { /// Creates a new empty temporary file with read-write permissions. pub fn new() -> std::io::Result { + let c = "While creating a temporary file on the host filesystem"; let file = tempfile::NamedTempFile::new()?; - let path = file.path().to_str().ok_or(InvalidInput)?.into(); - let clone = file.as_file().try_clone()?; + let path = file + .path() + .to_str() + .ok_or(std::io::Error::new( + InvalidInput, + "Tried to create a temporary file, but its path was not valid UTF-8", + )) + .wrap_io_err(c)? + .into(); + let clone = file.as_file().try_clone().wrap_io_err(c)?; Ok(Self { file: Inner::NamedTempFile(file), path, + stripped_path: None, async_file: clone.into(), }) } @@ -191,6 +242,7 @@ impl File { filter_name: &str, extensions: &[impl ToString], ) -> Result<(Self, String)> { + let c = "While picking a file on the host filesystem"; if let Some(path) = rfd::AsyncFileDialog::default() .add_filter(filter_name, extensions) .pick_file() @@ -199,7 +251,8 @@ impl File { let file = std::fs::OpenOptions::new() .read(true) .open(path.path()) - .map_err(crate::Error::IoError)?; + .map_err(crate::Error::IoError) + .wrap_err(c)?; let path = path .path() .iter() @@ -207,18 +260,26 @@ impl File { .unwrap() .to_os_string() .into_string() - .map_err(|_| crate::Error::PathUtf8Error)?; - let clone = file.try_clone()?; + .map_err(|_| crate::Error::PathUtf8Error) + .wrap_err(c)?; + let clone = file.try_clone().wrap_err(c)?; Ok(( File { file: Inner::StdFsFile(file), path: path.clone().into(), + stripped_path: Some( + camino::Utf8Path::new(&path) + .iter() + .next_back() + .unwrap() + .into(), + ), async_file: clone.into(), }, path, )) } else { - Err(crate::Error::CancelledLoading) + Err(crate::Error::CancelledLoading).wrap_err(c) } } @@ -240,6 +301,15 @@ impl File { /// file picker where the user selects a file extension. `filter_name` works only in native /// builds; it is ignored in web builds. pub async fn save(&self, filename: &str, filter_name: &str) -> Result<()> { + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While saving the file {:?} in a host folder to disk", + stripped_path + ); let mut dialog = rfd::AsyncFileDialog::default().set_file_name(filename); if let Some((_, extension)) = filename.rsplit_once('.') { dialog = dialog.add_filter(filter_name, &[extension]); @@ -247,15 +317,25 @@ impl File { let path = dialog .save_file() .await - .ok_or(crate::Error::CancelledLoading)?; - std::fs::copy(&self.path, path.path())?; + .ok_or(crate::Error::CancelledLoading) + .wrap_err_with(|| c.clone())?; + std::fs::copy(&self.path, path.path()).wrap_err_with(|| c.clone())?; Ok(()) } } impl crate::File for File { fn metadata(&self) -> std::io::Result { - let metdata = self.file.as_file().metadata()?; + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While getting metadata for file {:?} in a host folder", + stripped_path + ); + let metdata = self.file.as_file().metadata().wrap_io_err(c)?; Ok(Metadata { is_file: metdata.is_file(), size: metdata.len(), @@ -263,17 +343,44 @@ impl crate::File for File { } fn set_len(&self, new_size: u64) -> std::io::Result<()> { - self.file.as_file().set_len(new_size) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While setting length of file {:?} in a host folder", + stripped_path + ); + self.file.as_file().set_len(new_size).wrap_io_err(c) } } impl std::io::Read for File { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.file.as_file().read(buf) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While reading from file {:?} in a host folder", + stripped_path + ); + self.file.as_file().read(buf).wrap_io_err(c) } fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { - self.file.as_file().read_vectored(bufs) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While reading (vectored) from file {:?} in a host folder", + stripped_path + ); + self.file.as_file().read_vectored(bufs).wrap_io_err(c) } } @@ -283,7 +390,19 @@ impl futures_lite::AsyncRead for File { cx: &mut std::task::Context<'_>, buf: &mut [u8], ) -> Poll> { - self.project().async_file.poll_read(cx, buf) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously reading from file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_read(cx, buf) + .map(|p| p.wrap_io_err(c)) } fn poll_read_vectored( @@ -291,21 +410,54 @@ impl futures_lite::AsyncRead for File { cx: &mut std::task::Context<'_>, bufs: &mut [std::io::IoSliceMut<'_>], ) -> Poll> { - self.project().async_file.poll_read_vectored(cx, bufs) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously reading (vectored) from file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_read_vectored(cx, bufs) + .map(|p| p.wrap_io_err(c)) } } impl std::io::Write for File { fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.file.as_file().write(buf) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While writing to file {:?} in a host folder", stripped_path); + self.file.as_file().write(buf).wrap_io_err(c) } fn flush(&mut self) -> std::io::Result<()> { - self.file.as_file().flush() + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While flushing file {:?} in a host folder", stripped_path); + self.file.as_file().flush().wrap_io_err(c) } fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { - self.file.as_file().write_vectored(bufs) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While writing (vectored) to file {:?} in a host folder", + stripped_path + ); + self.file.as_file().write_vectored(bufs).wrap_io_err(c) } } @@ -315,7 +467,19 @@ impl futures_lite::AsyncWrite for File { cx: &mut std::task::Context<'_>, buf: &[u8], ) -> Poll> { - self.project().async_file.poll_write(cx, buf) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously writing to file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_write(cx, buf) + .map(|r| r.wrap_io_err(c)) } fn poll_write_vectored( @@ -323,27 +487,66 @@ impl futures_lite::AsyncWrite for File { cx: &mut std::task::Context<'_>, bufs: &[std::io::IoSlice<'_>], ) -> Poll> { - self.project().async_file.poll_write_vectored(cx, bufs) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously writing (vectored) to file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_write_vectored(cx, bufs) + .map(|r| r.wrap_io_err(c)) } fn poll_flush( self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - self.project().async_file.poll_flush(cx) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously flushing file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_flush(cx) + .map(|r| r.wrap_io_err(c)) } fn poll_close( self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> Poll> { - Poll::Ready(Err(PermissionDenied.into())) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously closing file {:?} in a host folder", + stripped_path + ); + Poll::Ready(Err(std::io::Error::new(PermissionDenied, "Attempted to asynchronously close a `luminol_filesystem::host::File`, which is not allowed")).wrap_io_err(c)) } } impl std::io::Seek for File { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { - self.file.as_file().seek(pos) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While seeking file {:?} in a host folder", stripped_path); + self.file.as_file().seek(pos).wrap_io_err(c) } } @@ -353,7 +556,19 @@ impl futures_lite::AsyncSeek for File { cx: &mut std::task::Context<'_>, pos: std::io::SeekFrom, ) -> Poll> { - self.project().async_file.poll_seek(cx, pos) + let stripped_path = self + .stripped_path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously seeking file {:?} in a host folder", + stripped_path + ); + self.project() + .async_file + .poll_seek(cx, pos) + .map(|r| r.wrap_io_err(c)) } } diff --git a/crates/filesystem/src/path_cache.rs b/crates/filesystem/src/path_cache.rs index 4550981e..a7facf7f 100644 --- a/crates/filesystem/src/path_cache.rs +++ b/crates/filesystem/src/path_cache.rs @@ -16,6 +16,7 @@ // along with Luminol. If not, see . use crate::{DirEntry, Error, Metadata, OpenFlags, Result}; +use color_eyre::eyre::WrapErr; #[derive(Debug, Clone)] pub struct FileSystem { @@ -41,6 +42,8 @@ where } pub fn regen_cache(&self) -> Result<()> { + let c = "While regenerating path cache"; + fn read_dir_recursive( fs: &(impl crate::FileSystem + ?Sized), path: impl AsRef, @@ -73,8 +76,8 @@ where lowercase.set_extension(""); self.cache.insert(lowercase, path.to_path_buf()); - })?; - Ok(()) + }) + .wrap_err(c) } pub fn debug_ui(&self, ui: &mut egui::Ui) { @@ -124,18 +127,27 @@ where flags: OpenFlags, ) -> Result { let path = path.as_ref(); + let c = format!("While opening file {path:?} in a path cache"); if flags.contains(OpenFlags::Create) { let mut lower_path = to_lowercase(path); lower_path.set_extension(""); self.cache.insert(lower_path, path.to_path_buf()); } - let path = self.desensitize(path).ok_or(Error::NotExist)?; - self.fs.open_file(path, flags) + let path = self + .desensitize(path) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?; + self.fs.open_file(path, flags).wrap_err_with(|| c.clone()) } fn metadata(&self, path: impl AsRef) -> Result { - let path = self.desensitize(path).ok_or(Error::NotExist)?; - self.fs.metadata(path) + let path = path.as_ref(); + let c = format!("While getting metadata for {path:?} in a path cache"); + let path = self + .desensitize(path) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?; + self.fs.metadata(path).wrap_err_with(|| c.clone()) } fn rename( @@ -143,10 +155,18 @@ where from: impl AsRef, to: impl AsRef, ) -> Result<()> { - let from = self.desensitize(from).ok_or(Error::NotExist)?; + let c = format!( + "While renaming {:?} to {:?} in a path cache", + from.as_ref(), + to.as_ref() + ); + let from = self + .desensitize(from) + .ok_or(Error::NotExist) + .wrap_err_with(|| c.clone())?; let to = to.as_ref().to_path_buf(); - self.fs.rename(&from, &to)?; + self.fs.rename(&from, &to).wrap_err_with(|| c.clone())?; self.cache.remove(&from); self.cache.insert(to_lowercase(&to), to); @@ -160,8 +180,9 @@ where fn create_dir(&self, path: impl AsRef) -> Result<()> { let path = path.as_ref().to_path_buf(); + let c = format!("While creating directory {path:?} in a path cache"); - self.fs.create_dir(&path)?; + self.fs.create_dir(&path).wrap_err_with(|| c.clone())?; self.cache.insert(to_lowercase(&path), path); @@ -170,8 +191,9 @@ where fn remove_dir(&self, path: impl AsRef) -> Result<()> { let path = self.desensitize(path).ok_or(Error::NotExist)?; + let c = format!("While removing directory {path:?} in a path cache"); - self.fs.remove_dir(&path)?; + self.fs.remove_dir(&path).wrap_err_with(|| c.clone())?; self.cache.remove(&to_lowercase(path)); @@ -180,8 +202,9 @@ where fn remove_file(&self, path: impl AsRef) -> Result<()> { let path = self.desensitize(path).ok_or(Error::NotExist)?; + let c = format!("While removing file {path:?} in a path cache"); - self.fs.remove_file(&path)?; + self.fs.remove_file(&path).wrap_err_with(|| c.clone())?; self.cache.remove(&to_lowercase(path)); @@ -190,6 +213,7 @@ where fn read_dir(&self, path: impl AsRef) -> Result> { let path = self.desensitize(path).ok_or(Error::NotExist)?; - self.fs.read_dir(path) + let c = format!("While reading the contents of the directory {path:?} in a path cache"); + self.fs.read_dir(path).wrap_err_with(|| c.clone()) } } diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs index d6c19328..4f51fb39 100644 --- a/crates/filesystem/src/project.rs +++ b/crates/filesystem/src/project.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +use color_eyre::eyre::WrapErr; #[cfg(target_arch = "wasm32")] use itertools::Itertools; @@ -100,7 +101,8 @@ impl FileSystem { } fn load_project_config(&self) -> Result { - self.create_dir(".luminol")?; + let c = "While loading project configuration"; + self.create_dir(".luminol").wrap_err(c)?; let project = match self .read_to_string(".luminol/config") @@ -110,13 +112,14 @@ impl FileSystem { Some(c) => c, None => { let Some(editor_ver) = self.detect_rm_ver() else { - return Err(Error::UnableToDetectRMVer); + return Err(Error::UnableToDetectRMVer).wrap_err(c); }; let config = luminol_config::project::Project { editor_ver, ..Default::default() }; - self.write(".luminol/config", ron::to_string(&config).unwrap())?; + self.write(".luminol/config", ron::to_string(&config).wrap_err(c)?) + .wrap_err(c)?; config } }; @@ -129,7 +132,11 @@ impl FileSystem { Some(c) => c, None => { let command_db = luminol_config::command_db::CommandDB::new(project.editor_ver); - self.write(".luminol/commands", ron::to_string(&command_db).unwrap())?; + self.write( + ".luminol/commands", + ron::to_string(&command_db).wrap_err(c)?, + ) + .wrap_err(c)?; command_db } }; @@ -186,14 +193,22 @@ impl FileSystem { project_config: &mut Option, global_config: &mut luminol_config::global::Config, ) -> Result { + let c = "While loading project data"; + *self = FileSystem::HostLoaded(host); - let config = self.load_project_config()?; + let config = self.load_project_config().wrap_err(c)?; let Self::HostLoaded(host) = std::mem::take(self) else { - panic!("unable to fetch host filesystem") + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Unable to fetch host filesystem", + ) + .into()); }; - let result = self.load_partially_loaded_project(host, &config, global_config)?; + let result = self + .load_partially_loaded_project(host, &config, global_config) + .wrap_err(c)?; *project_config = Some(config); @@ -450,7 +465,7 @@ impl FileSystem { }) .is_none() { - return Err(Error::InvalidProjectFolder); + return Err(Error::InvalidProjectFolder.into()); }; let root_path = host.root_path().to_path_buf(); @@ -593,7 +608,7 @@ impl crate::FileSystem for FileSystem { flags: OpenFlags, ) -> Result { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.open_file(path, flags).map(File::Host), FileSystem::Loaded { filesystem: f, .. } => f.open_file(path, flags).map(File::Loaded), } @@ -601,7 +616,7 @@ impl crate::FileSystem for FileSystem { fn metadata(&self, path: impl AsRef) -> Result { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.metadata(path), FileSystem::Loaded { filesystem: f, .. } => f.metadata(path), } @@ -613,7 +628,7 @@ impl crate::FileSystem for FileSystem { to: impl AsRef, ) -> Result<()> { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.rename(from, to), FileSystem::Loaded { filesystem, .. } => filesystem.rename(from, to), } @@ -621,7 +636,7 @@ impl crate::FileSystem for FileSystem { fn exists(&self, path: impl AsRef) -> Result { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.exists(path), FileSystem::Loaded { filesystem, .. } => filesystem.exists(path), } @@ -629,7 +644,7 @@ impl crate::FileSystem for FileSystem { fn create_dir(&self, path: impl AsRef) -> Result<()> { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.create_dir(path), FileSystem::Loaded { filesystem, .. } => filesystem.create_dir(path), } @@ -637,7 +652,7 @@ impl crate::FileSystem for FileSystem { fn remove_dir(&self, path: impl AsRef) -> Result<()> { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.remove_dir(path), FileSystem::Loaded { filesystem, .. } => filesystem.remove_dir(path), } @@ -645,7 +660,7 @@ impl crate::FileSystem for FileSystem { fn remove_file(&self, path: impl AsRef) -> Result<()> { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.remove_file(path), FileSystem::Loaded { filesystem, .. } => filesystem.remove_file(path), } @@ -653,7 +668,7 @@ impl crate::FileSystem for FileSystem { fn read_dir(&self, path: impl AsRef) -> Result> { match self { - FileSystem::Unloaded => Err(Error::NotLoaded), + FileSystem::Unloaded => Err(Error::NotLoaded.into()), FileSystem::HostLoaded(f) => f.read_dir(path), FileSystem::Loaded { filesystem, .. } => filesystem.read_dir(path), } diff --git a/crates/filesystem/src/web/events.rs b/crates/filesystem/src/web/events.rs index dfef6f49..f648bab0 100644 --- a/crates/filesystem/src/web/events.rs +++ b/crates/filesystem/src/web/events.rs @@ -81,7 +81,16 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { is_file: true, size: blob.size() as u64, }) - .map_err(|_| Error::IoError(PermissionDenied.into())) + .map_err(|e| { + Error::IoError(std::io::Error::new( + PermissionDenied, + format!( + "Failed to get read handle to file: {}", + e.to_string() + ), + )) + .into() + }) } else if to_future::( subdir.get_directory_handle(name), ) @@ -95,7 +104,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { }) } else { // If the path is neither a file nor a directory - Err(Error::NotExist) + Err(Error::NotExist.into()) } }) .await; @@ -167,7 +176,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let mut iter = path.iter(); let filename = iter .next_back() - .ok_or(Error::IoError(PermissionDenied.into()))?; + .ok_or(Error::IoError(std::io::Error::new(PermissionDenied, "Tried to open a file but the given path corresponded to a directory")))?; let subdir = get_subdir(dirs.get(key).unwrap(), &mut iter) .await .ok_or(Error::NotExist)?; @@ -226,7 +235,8 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { if (flags.contains(OpenFlags::Write) && handle.write_handle.is_none()) || !close_result { - Err(Error::IoError(std::io::ErrorKind::PermissionDenied.into())) + Err(Error::IoError(std::io::Error::new(PermissionDenied, "Failed to obtain write permissions for file")) + .into()) } else { Ok(files.insert(handle)) } @@ -237,10 +247,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { .is_ok() { // If the path is a directory - Err(Error::IoError(PermissionDenied.into())) + Err(std::io::Error::new(PermissionDenied, "Tried to open a file but the given path corresponded to a directory").into()) } else { // If the path is neither a file nor a directory - Err(Error::NotExist) + Err(Error::NotExist.into()) } }) .await; @@ -273,7 +283,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let mut iter = path.iter(); get_subdir_create(dirs.get(key).unwrap(), &mut iter) .await - .ok_or(Error::IoError(PermissionDenied.into()))?; + .ok_or(Error::IoError(std::io::Error::new( + PermissionDenied, + "Failed to create directory", + )))?; Ok(()) }) .await; @@ -284,7 +297,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let mut iter = path.iter(); let dirname = iter .next_back() - .ok_or(Error::IoError(PermissionDenied.into()))?; + .ok_or(Error::IoError(std::io::Error::new(PermissionDenied, "Failed to remove directory")))?; let subdir = get_subdir(dirs.get(key).unwrap(), &mut iter) .await .ok_or(Error::NotExist)?; @@ -296,7 +309,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { .is_ok() { // If the path is a file - Err(Error::IoError(PermissionDenied.into())) + Err(std::io::Error::new(PermissionDenied, "Tried to remove a directory but the given path corresponded to a file").into()) } else if to_future::( subdir.get_directory_handle(dirname), ) @@ -311,10 +324,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { ) .await .map(|_| ()) - .map_err(|_| Error::IoError(PermissionDenied.into())) + .map_err(|e| std::io::Error::new(PermissionDenied, format!("Failed to remove directory: {}", e.to_string())).into()) } else { // If the path is neither a file nor a directory - Err(Error::NotExist) + Err(Error::NotExist.into()) } }) .await; @@ -325,7 +338,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let mut iter = path.iter(); let filename = iter .next_back() - .ok_or(Error::IoError(PermissionDenied.into()))?; + .ok_or(std::io::Error::new(PermissionDenied, "Tried to remove a file but the given path corresponded to a directory"))?; let subdir = get_subdir(dirs.get(key).unwrap(), &mut iter) .await .ok_or(Error::NotExist)?; @@ -340,7 +353,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { to_future::(subdir.remove_entry(filename)) .await .map(|_| ()) - .map_err(|_| Error::IoError(PermissionDenied.into())) + .map_err(|e| std::io::Error::new(PermissionDenied, format!("Failed to remove file: {}", e.to_string())).into()) } else if to_future::( subdir.get_directory_handle(filename), ) @@ -348,10 +361,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { .is_ok() { // If the path is a directory - Err(Error::IoError(PermissionDenied.into())) + Err(std::io::Error::new(PermissionDenied, "Tried to remove a file but the given path corresponded to a directory").into()) } else { // If the path is neither a file nor a directory - Err(Error::NotExist) + Err(Error::NotExist.into()) } }) .await; @@ -368,9 +381,15 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let mut vec = Vec::new(); loop { let Ok(entry) = to_future::( - entry_iter - .next() - .map_err(|_| Error::IoError(PermissionDenied.into()))?, + entry_iter.next().map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to read directory contents: {}", + e.unchecked_into::().to_string() + ), + ) + })?, ) .await else { @@ -433,7 +452,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { FileSystemCommand::FileCreateTemp(tx) => { handle_event(tx, async { - let tmp_dir = get_tmp_dir(&storage).await.ok_or(PermissionDenied)?; + let tmp_dir = get_tmp_dir(&storage).await?; let filename = generate_key(); @@ -443,11 +462,25 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { tmp_dir.get_file_handle_with_options(&filename, &options), ) .await - .map_err(|_| PermissionDenied)?; + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!("Failed to create temporary file: {}", e.to_string()), + ) + })?; - let write_handle = to_future(file_handle.create_writable()) - .await - .map_err(|_| PermissionDenied)?; + let write_handle = + to_future(file_handle.create_writable()) + .await + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to obtain write permissions for temporary file: {}", + e.to_string() + ), + ) + })?; Ok(( files.insert(FileHandle { @@ -465,14 +498,34 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { 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)?; + let write_handle = + file.write_handle.as_ref().ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + ))?; to_future::( write_handle .truncate_with_f64(new_size as f64) - .map_err(|_| PermissionDenied)?, + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to set file length: {}", + e.unchecked_into::().to_string() + ), + ) + })?, ) .await - .map_err(|_| PermissionDenied)?; + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to set file length: {}", + e.unchecked_into::().to_string() + ), + ) + })?; Ok(()) }) .await; @@ -502,16 +555,43 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { FileSystemCommand::FileSave(key, filename, tx) => { handle_event(tx, async { let file = files.get(key).unwrap(); - let file_handle = file.read_allowed.then_some(&file.file_handle)?; + let file_handle = file.read_allowed.then_some(&file.file_handle).ok_or( + std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + ), + )?; let blob = to_future::(file_handle.get_file()) .await - .ok()?; - let url = web_sys::Url::create_object_url_with_blob(&blob).ok()?; + .map_err(|e| { + Error::IoError(std::io::Error::new( + PermissionDenied, + format!("Failed to get read handle to file: {}", e.to_string()), + )) + })?; + let url = + web_sys::Url::create_object_url_with_blob(&blob).map_err(|e| { + Error::IoError(std::io::Error::new( + PermissionDenied, + format!( + "Failed to create object URL from blob: {}", + e.unchecked_into::().to_string() + ), + )) + })?; let anchor = document .create_element("a") - .ok()? + .map_err(|e| { + Error::IoError(std::io::Error::new( + PermissionDenied, + format!( + "Failed to create anchor element for downloading files: {}", + e.unchecked_into::().to_string() + ), + )) + })? .unchecked_into::(); anchor.set_href(&url); anchor.set_download(&filename); @@ -519,7 +599,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { anchor.remove(); let _ = web_sys::Url::revoke_object_url(&url); - Some(()) + Ok(()) }) .await; } @@ -535,18 +615,18 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { } else { None }) - .ok_or(PermissionDenied)?; + .ok_or(std::io::Error::new(PermissionDenied, "Attempted to read from file with no read permissions"))?; let blob = read_handle .slice_with_f64_and_f64( file.offset as f64, (file.offset + max_length) as f64, ) - .map_err(|_| PermissionDenied)?; + .map_err(|e| std::io::Error::new(PermissionDenied, format!("Failed to read from file: {}", e.unchecked_into::().to_string())))?; let buffer = to_future::(blob.array_buffer()) .await - .map_err(|_| PermissionDenied)?; + .map_err(|e| std::io::Error::new(PermissionDenied, format!("Failed to convert file contents into array buffer while reading file: {}", e.unchecked_into::().to_string())))?; let u8_array = js_sys::Uint8Array::new(&buffer); let vec = u8_array.to_vec(); @@ -559,32 +639,65 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { FileSystemCommand::FileWrite(key, vec, tx) => { handle_event(tx, async { let file = files.get_mut(key).unwrap(); - let write_handle = file.write_handle.as_ref().ok_or(PermissionDenied)?; + let write_handle = + file.write_handle.as_ref().ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + ))?; // We can't use `write_handle.write_with_u8_array()` when shared memory is enabled let u8_array = js_sys::Uint8Array::new(&JsValue::from_f64(vec.len() as f64)); u8_array.copy_from(&vec[..]); - if to_future::( + to_future::( write_handle .seek_with_f64(file.offset as f64) - .map_err(|_| PermissionDenied)?, + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to seek file handle: {}", + e.unchecked_into::().to_string() + ), + ) + })?, ) .await - .is_ok() - && to_future::( - write_handle - .write_with_buffer_source(&u8_array) - .map_err(|_| PermissionDenied)?, + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to seek file handle: {}", + e.unchecked_into::().to_string() + ), ) - .await - .is_ok() - { - file.offset += vec.len(); - Ok(()) - } else { - Err(PermissionDenied.into()) - } + })?; + to_future::( + write_handle + .write_with_buffer_source(&u8_array) + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to write to file: {}", + e.unchecked_into::().to_string() + ), + ) + })?, + ) + .await + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to write to file: {}", + e.unchecked_into::().to_string() + ), + ) + })?; + + file.offset += vec.len(); + Ok(()) }) .await; } @@ -594,25 +707,44 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { let file = files.get_mut(key).unwrap(); // Closing and reopening the handle is the only way to flush - if file.write_handle.is_none() - || to_future::( - file.write_handle.as_ref().ok_or(PermissionDenied)?.close(), - ) - .await - .is_err() - { - return Err(PermissionDenied.into()); + if file.write_handle.is_none() { + return Err(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + )); } + to_future::( + file.write_handle + .as_ref() + .ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to write to file with no write permissions", + ))? + .close(), + ) + .await + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!("Failed to flush file: {}", e.to_string()), + ) + })?; let mut options = web_sys::FileSystemCreateWritableOptions::new(); options.keep_existing_data(true); - if let Ok(write_handle) = - to_future(file.file_handle.create_writable_with_options(&options)).await - { - file.write_handle = Some(write_handle); - Ok(()) - } else { - Err(PermissionDenied.into()) - } + let write_handle = + to_future(file.file_handle.create_writable_with_options(&options)) + .await + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!( + "Failed to reopen write handle for file after flushing: {}", + e.to_string() + ), + ) + })?; + file.write_handle = Some(write_handle); + Ok(()) }) .await; } @@ -627,7 +759,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { } else { None }) - .ok_or(PermissionDenied)?; + .ok_or(std::io::Error::new( + PermissionDenied, + "Attempted to read from file with no read permissions", + ))?; let size = read_handle.size(); let new_offset = match seek_from { @@ -639,7 +774,10 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { file.offset = new_offset as usize; Ok(new_offset as u64) } else { - Err(InvalidInput.into()) + Err(std::io::Error::new( + InvalidInput, + "Tried to seek to negative offset in file", + )) } }) .await; @@ -651,7 +789,12 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { to_future::(file.file_handle.get_file()) .await .map(|file| file.size() as u64) - .map_err(|_| PermissionDenied.into()) + .map_err(|e| { + std::io::Error::new( + PermissionDenied, + format!("Failed to get file size: {}", e.to_string()), + ) + }) }) .await; } @@ -667,7 +810,7 @@ pub fn setup_main_thread_hooks(main_channels: super::MainChannels) { } if let Some(temp_file_name) = temp_file_name { - if let Some(tmp_dir) = get_tmp_dir(&storage).await { + if let Ok(tmp_dir) = get_tmp_dir(&storage).await { let _ = to_future::(tmp_dir.remove_entry(&temp_file_name)) .await; diff --git a/crates/filesystem/src/web/mod.rs b/crates/filesystem/src/web/mod.rs index b5aadc6b..45e3299e 100644 --- a/crates/filesystem/src/web/mod.rs +++ b/crates/filesystem/src/web/mod.rs @@ -15,12 +15,15 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +use color_eyre::eyre::WrapErr; use itertools::Itertools; mod events; mod util; pub use events::setup_main_thread_hooks; +use crate::StdIoErrorExt; + use super::FileSystem as FileSystemTrait; use super::{DirEntry, Error, Metadata, OpenFlags, Result}; use std::io::ErrorKind::PermissionDenied; @@ -62,6 +65,7 @@ pub struct FileSystem { #[derive(Debug)] pub struct File { key: usize, + path: Option, temp_file_name: Option, futures: parking_lot::Mutex, } @@ -114,7 +118,7 @@ enum FileSystemCommand { Vec, oneshot::Sender>, ), - FileSave(usize, String, oneshot::Sender>), + FileSave(usize, String, oneshot::Sender>), FileRead(usize, usize, oneshot::Sender>>), FileWrite(usize, Vec, oneshot::Sender>), FileFlush(usize, oneshot::Sender>), @@ -151,8 +155,9 @@ impl FileSystem { /// successfully. /// If the File System API is not supported, this always returns `None` without doing anything. pub async fn from_folder_picker() -> Result { + let c = "While picking a folder from the host filesystem"; if !Self::filesystem_supported() { - return Err(Error::Wasm32FilesystemNotSupported); + return Err(Error::Wasm32FilesystemNotSupported).wrap_err(c); } send_and_await(|tx| FileSystemCommand::DirPicker(tx)) .await @@ -162,13 +167,15 @@ impl FileSystem { idb_key: None, }) .ok_or(Error::CancelledLoading) + .wrap_err(c) } /// Attempts to restore a previously created `FileSystem` using its IndexedDB key returned by /// `.save_to_idb()`. pub async fn from_idb_key(idb_key: String) -> Result { + let c = "While restoring a directory handle from IndexedDB"; if !Self::filesystem_supported() { - return Err(Error::Wasm32FilesystemNotSupported); + return Err(Error::Wasm32FilesystemNotSupported).wrap_err(c); } send_and_await(|tx| FileSystemCommand::DirFromIdb(idb_key.clone(), tx)) .await @@ -178,16 +185,20 @@ impl FileSystem { idb_key: Some(idb_key), }) .ok_or(Error::MissingIDB) + .wrap_err(c) } /// Creates a new `FileSystem` from a subdirectory of this one. pub fn subdir(&self, path: impl AsRef) -> Result { - send_and_recv(|tx| FileSystemCommand::DirSubdir(self.key, path.as_ref().to_path_buf(), tx)) + let path = path.as_ref(); + let c = format!("While getting a subdirectory {path:?} of a host folder"); + send_and_recv(|tx| FileSystemCommand::DirSubdir(self.key, path.to_path_buf(), tx)) .map(|(key, name)| FileSystem { key, name, idb_key: None, }) + .wrap_err(c) } /// Stores this `FileSystem` to IndexedDB. If successful, consumes this `Filesystem` and @@ -235,28 +246,34 @@ impl FileSystemTrait for FileSystem { path: impl AsRef, flags: OpenFlags, ) -> Result { - send_and_recv(|tx| { - FileSystemCommand::DirOpenFile(self.key, path.as_ref().to_path_buf(), flags, tx) - }) - .map(|key| File { - key, - temp_file_name: None, - futures: Default::default(), - }) + let path = path.as_ref(); + let c = format!("While opening file {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirOpenFile(self.key, path.to_path_buf(), flags, tx)) + .map(|key| File { + key, + path: Some(path.to_owned()), + temp_file_name: None, + futures: Default::default(), + }) + .wrap_err(c) } fn metadata(&self, path: impl AsRef) -> Result { - send_and_recv(|tx| { - FileSystemCommand::DirEntryMetadata(self.key, path.as_ref().to_path_buf(), tx) - }) + let path = path.as_ref(); + let c = format!("While getting metadata for {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirEntryMetadata(self.key, path.to_path_buf(), tx)) + .wrap_err(c) } fn rename( &self, - _from: impl AsRef, - _to: impl AsRef, + from: impl AsRef, + to: impl AsRef, ) -> Result<()> { - Err(Error::NotSupported) + let from = from.as_ref(); + let to = to.as_ref(); + let c = format!("While renaming {from:?} to {to:?} in a host folder"); + Err(Error::NotSupported).wrap_err(c) } fn exists(&self, path: impl AsRef) -> Result { @@ -266,38 +283,46 @@ impl FileSystemTrait for FileSystem { } fn create_dir(&self, path: impl AsRef) -> Result<()> { - send_and_recv(|tx| { - FileSystemCommand::DirCreateDir(self.key, path.as_ref().to_path_buf(), tx) - }) + let path = path.as_ref(); + let c = format!("While creating a directory at {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirCreateDir(self.key, path.to_path_buf(), tx)) + .wrap_err(c) } fn remove_dir(&self, path: impl AsRef) -> Result<()> { - send_and_recv(|tx| { - FileSystemCommand::DirRemoveDir(self.key, path.as_ref().to_path_buf(), tx) - }) + let path = path.as_ref(); + let c = format!("While removing a directory at {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirRemoveDir(self.key, path.to_path_buf(), tx)) + .wrap_err(c) } fn remove_file(&self, path: impl AsRef) -> Result<()> { - send_and_recv(|tx| { - FileSystemCommand::DirRemoveFile(self.key, path.as_ref().to_path_buf(), tx) - }) + let path = path.as_ref(); + let c = format!("While removing a file at {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirRemoveFile(self.key, path.to_path_buf(), tx)) + .wrap_err(c) } fn read_dir(&self, path: impl AsRef) -> Result> { - send_and_recv(|tx| FileSystemCommand::DirReadDir(self.key, path.as_ref().to_path_buf(), tx)) + let path = path.as_ref(); + let c = format!("While reading the contents of the directory {path:?} in a host folder"); + send_and_recv(|tx| FileSystemCommand::DirReadDir(self.key, path.to_path_buf(), tx)) + .wrap_err(c) } } 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 { + let c = "While creating a temporary file on a host filesystem"; + send_and_recv(|tx| FileSystemCommand::FileCreateTemp(tx)) + .map(|(key, temp_file_name)| Self { key, + path: None, temp_file_name: Some(temp_file_name), futures: Default::default(), - } - }) + }) + .wrap_io_err(c) } /// Attempts to prompt the user to choose a file from their local machine using the @@ -312,8 +337,9 @@ impl File { filter_name: &str, extensions: &[impl ToString], ) -> Result<(Self, String)> { + let c = "While picking a file on a host filesystem"; if !FileSystem::filesystem_supported() { - return Err(Error::Wasm32FilesystemNotSupported); + return Err(Error::Wasm32FilesystemNotSupported).wrap_err(c); } send_and_await(|tx| { FileSystemCommand::FilePicker( @@ -327,6 +353,7 @@ impl File { ( Self { key, + path: Some(name.clone().into()), temp_file_name: None, futures: Default::default(), }, @@ -334,6 +361,7 @@ impl File { ) }) .ok_or(Error::CancelledLoading) + .wrap_err(c) } /// Saves this file to a location of the user's choice. @@ -354,9 +382,18 @@ impl File { /// file picker where the user selects a file extension. `filter_name` works only in native /// builds; it is ignored in web builds. pub async fn save(&self, filename: &str, _filter_name: &str) -> Result<()> { + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While saving the file {:?} in a host folder to disk", + stripped_path + ); send_and_await(|tx| FileSystemCommand::FileSave(self.key, filename.to_string(), tx)) .await - .ok_or(Error::IoError(PermissionDenied.into())) + .wrap_err(c) } } @@ -370,7 +407,16 @@ impl Drop for File { impl crate::File for File { fn metadata(&self) -> std::io::Result { - let size = send_and_recv(|tx| FileSystemCommand::FileSize(self.key, tx))?; + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While getting metadata for file {:?} in a host folder", + stripped_path + ); + let size = send_and_recv(|tx| FileSystemCommand::FileSize(self.key, tx)).wrap_io_err(c)?; Ok(Metadata { is_file: true, size, @@ -378,13 +424,32 @@ impl crate::File for File { } fn set_len(&self, new_size: u64) -> std::io::Result<()> { - send_and_recv(|tx| FileSystemCommand::FileSetLength(self.key, new_size, tx)) + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While setting length of file {:?} in a host folder", + stripped_path + ); + send_and_recv(|tx| FileSystemCommand::FileSetLength(self.key, new_size, tx)).wrap_io_err(c) } } impl std::io::Read for File { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let vec = send_and_recv(|tx| FileSystemCommand::FileRead(self.key, buf.len(), tx))?; + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While reading from file {:?} in a host folder", + stripped_path + ); + let vec = send_and_recv(|tx| FileSystemCommand::FileRead(self.key, buf.len(), tx)) + .wrap_io_err(c)?; let length = vec.len(); buf[..length].copy_from_slice(&vec[..]); Ok(length) @@ -397,6 +462,15 @@ impl futures_lite::AsyncRead for File { cx: &mut std::task::Context<'_>, buf: &mut [u8], ) -> Poll> { + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously reading from file {:?} in a host folder", + stripped_path + ); let mut futures = self.futures.lock(); if futures.read.is_none() { futures.read = Some(send_and_wake(cx, |tx| { @@ -412,7 +486,7 @@ impl futures_lite::AsyncRead for File { } Ok(Err(e)) => { futures.read = None; - Poll::Ready(Err(e)) + Poll::Ready(Err(e).wrap_io_err(c)) } Err(_) => Poll::Pending, } @@ -421,12 +495,25 @@ impl futures_lite::AsyncRead for File { impl std::io::Write for File { fn write(&mut self, buf: &[u8]) -> std::io::Result { - send_and_recv(|tx| FileSystemCommand::FileWrite(self.key, buf.to_vec(), tx))?; + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While writing to file {:?} in a host folder", stripped_path); + send_and_recv(|tx| FileSystemCommand::FileWrite(self.key, buf.to_vec(), tx)) + .wrap_io_err(c)?; Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { - send_and_recv(|tx| FileSystemCommand::FileFlush(self.key, tx)) + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While flushing file {:?} in a host folder", stripped_path); + send_and_recv(|tx| FileSystemCommand::FileFlush(self.key, tx)).wrap_io_err(c) } } @@ -436,6 +523,15 @@ impl futures_lite::AsyncWrite for File { cx: &mut std::task::Context<'_>, buf: &[u8], ) -> Poll> { + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously writing to file {:?} in a host folder", + stripped_path + ); let mut futures = self.futures.lock(); if futures.write.is_none() { futures.write = Some(send_and_wake(cx, |tx| { @@ -449,7 +545,7 @@ impl futures_lite::AsyncWrite for File { } Ok(Err(e)) => { futures.write = None; - Poll::Ready(Err(e)) + Poll::Ready(Err(e).wrap_io_err(c)) } Err(_) => Poll::Pending, } @@ -459,6 +555,15 @@ impl futures_lite::AsyncWrite for File { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously flushing file {:?} in a host folder", + stripped_path + ); let mut futures = self.futures.lock(); if futures.flush.is_none() { futures.flush = Some(send_and_wake(cx, |tx| { @@ -472,7 +577,7 @@ impl futures_lite::AsyncWrite for File { } Ok(Err(e)) => { futures.flush = None; - Poll::Ready(Err(e)) + Poll::Ready(Err(e).wrap_io_err(c)) } Err(_) => Poll::Pending, } @@ -482,13 +587,28 @@ impl futures_lite::AsyncWrite for File { self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> Poll> { - Poll::Ready(Err(PermissionDenied.into())) + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously closing file {:?} in a host folder", + stripped_path + ); + Poll::Ready(Err(std::io::Error::new(PermissionDenied, "Attempted to asynchronously close a `luminol_filesystem::host::File`, which is not allowed")).wrap_io_err(c)) } } impl std::io::Seek for File { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { - send_and_recv(|tx| FileSystemCommand::FileSeek(self.key, pos, tx)) + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!("While seeking file {:?} in a host folder", stripped_path); + send_and_recv(|tx| FileSystemCommand::FileSeek(self.key, pos, tx)).wrap_io_err(c) } } @@ -498,6 +618,15 @@ impl futures_lite::AsyncSeek for File { cx: &mut std::task::Context<'_>, pos: std::io::SeekFrom, ) -> Poll> { + let stripped_path = self + .path + .as_ref() + .map(|p| p.as_str()) + .unwrap_or(""); + let c = format!( + "While asynchronously seeking file {:?} in a host folder", + stripped_path + ); let mut futures = self.futures.lock(); if futures.seek.is_none() { futures.seek = Some(send_and_wake(cx, |tx| { @@ -511,7 +640,7 @@ impl futures_lite::AsyncSeek for File { } Ok(Err(e)) => { futures.seek = None; - Poll::Ready(Err(e)) + Poll::Ready(Err(e).wrap_io_err(c)) } Err(_) => Poll::Pending, } diff --git a/crates/filesystem/src/web/util.rs b/crates/filesystem/src/web/util.rs index 549cdc7e..bd2af6c8 100644 --- a/crates/filesystem/src/web/util.rs +++ b/crates/filesystem/src/web/util.rs @@ -78,12 +78,22 @@ pub(super) async fn get_subdir_create( /// 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 { +) -> std::io::Result { let opfs_root = to_future::(storage.get_directory()) .await - .ok()?; + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + format!("Failed to get handle to OPFS root: {}", e.to_string()), + ) + })?; let mut iter = camino::Utf8Path::new("astrabit.luminol/tmp").iter(); - get_subdir_create(&opfs_root, &mut iter).await + get_subdir_create(&opfs_root, &mut iter) + .await + .ok_or(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Failed to get handle to temporary directory", + )) } /// Generates a random string suitable for use as a unique identifier. diff --git a/crates/graphics/Cargo.toml b/crates/graphics/Cargo.toml index c3c45876..023a7bdd 100644 --- a/crates/graphics/Cargo.toml +++ b/crates/graphics/Cargo.toml @@ -20,7 +20,6 @@ workspace = true image = "0.24.7" egui.workspace = true -egui_extras.workspace = true luminol-egui-wgpu.workspace = true wgpu.workspace = true glam.workspace = true @@ -28,13 +27,10 @@ glam.workspace = true naga_oil = "0.11.0" naga = "0.14.1" -once_cell.workspace = true crossbeam.workspace = true dashmap.workspace = true -anyhow.workspace = true - -slab.workspace = true +color-eyre.workspace = true bytemuck.workspace = true diff --git a/crates/graphics/src/atlas_loader.rs b/crates/graphics/src/atlas_loader.rs index 5e54d0bc..9bbe8fec 100644 --- a/crates/graphics/src/atlas_loader.rs +++ b/crates/graphics/src/atlas_loader.rs @@ -27,7 +27,7 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, - ) -> anyhow::Result { + ) -> color_eyre::Result { Ok(self .atlases .entry(tileset.id) @@ -40,7 +40,7 @@ impl Loader { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, - ) -> anyhow::Result { + ) -> color_eyre::Result { Ok(self .atlases .entry(tileset.id) diff --git a/crates/graphics/src/event.rs b/crates/graphics/src/event.rs index 91ae1488..bcfaa483 100644 --- a/crates/graphics/src/event.rs +++ b/crates/graphics/src/event.rs @@ -56,9 +56,9 @@ impl Event { filesystem: &impl luminol_filesystem::FileSystem, event: &luminol_data::rpg::Event, atlas: &Atlas, - ) -> anyhow::Result> { + ) -> color_eyre::Result> { let Some(page) = event.pages.first() else { - anyhow::bail!("event does not have first page"); + color_eyre::eyre::bail!("event does not have first page"); }; let texture = if let Some(ref filename) = page.graphic.character_name { diff --git a/crates/graphics/src/map.rs b/crates/graphics/src/map.rs index a2e8b658..9bade0ce 100644 --- a/crates/graphics/src/map.rs +++ b/crates/graphics/src/map.rs @@ -118,7 +118,7 @@ impl Map { map: &luminol_data::rpg::Map, tileset: &luminol_data::rpg::Tileset, passages: &luminol_data::Table2, - ) -> anyhow::Result { + ) -> color_eyre::Result { let atlas = graphics_state .atlas_loader .load_atlas(graphics_state, filesystem, tileset)?; diff --git a/crates/graphics/src/texture_loader.rs b/crates/graphics/src/texture_loader.rs index e5efcd60..aff7c5c4 100644 --- a/crates/graphics/src/texture_loader.rs +++ b/crates/graphics/src/texture_loader.rs @@ -54,7 +54,7 @@ fn load_wgpu_texture_from_path( device: &wgpu::Device, queue: &wgpu::Queue, path: &str, -) -> anyhow::Result { +) -> color_eyre::Result { let file = filesystem.read(path)?; let texture_data = image::load_from_memory(&file)?.to_rgba8(); @@ -112,7 +112,7 @@ impl Loader { filesystem: &impl luminol_filesystem::FileSystem, directory: impl AsRef, file: impl AsRef, - ) -> anyhow::Result> { + ) -> color_eyre::Result> { let path = directory.as_ref().join(file.as_ref()); self.load_now(filesystem, path) } @@ -121,7 +121,7 @@ impl Loader { &self, filesystem: &impl luminol_filesystem::FileSystem, path: impl AsRef, - ) -> anyhow::Result> { + ) -> color_eyre::Result> { let path = path.as_ref().as_str(); let texture = load_wgpu_texture_from_path( diff --git a/crates/graphics/src/tiles/atlas.rs b/crates/graphics/src/tiles/atlas.rs index f974e965..2fda03b2 100644 --- a/crates/graphics/src/tiles/atlas.rs +++ b/crates/graphics/src/tiles/atlas.rs @@ -1,4 +1,3 @@ -use anyhow::Context; // Copyright (C) 2023 Lily Lyons // // This file is part of Luminol. @@ -15,6 +14,8 @@ use anyhow::Context; // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . + +use color_eyre::eyre::WrapErr; use itertools::Itertools; use super::autotile_ids::AUTOTILES; @@ -56,7 +57,7 @@ impl Atlas { graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, - ) -> anyhow::Result { + ) -> color_eyre::Result { let tileset_img = match &tileset.tileset_name { Some(tileset_name) => { let file = filesystem @@ -86,7 +87,7 @@ impl Atlas { } }) .try_collect() - .context("while loading atlas autotiles")?; + .wrap_err("While loading atlas autotiles")?; let autotile_frames = std::array::from_fn(|i| { autotiles[i] diff --git a/crates/term/Cargo.toml b/crates/term/Cargo.toml index 2e75421e..27e7316f 100644 --- a/crates/term/Cargo.toml +++ b/crates/term/Cargo.toml @@ -18,7 +18,9 @@ workspace = true [dependencies] egui.workspace = true -# termwiz = "0.20.0" + +luminol-core.workspace = true +color-eyre.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] crossbeam-channel = "0.5" diff --git a/crates/term/src/_impl/mod.rs b/crates/term/src/_impl/mod.rs index 6a277bde..5f6c66a1 100644 --- a/crates/term/src/_impl/mod.rs +++ b/crates/term/src/_impl/mod.rs @@ -22,32 +22,50 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -use crossbeam_channel::{unbounded, Receiver}; +use color_eyre::eyre::WrapErr; +pub use crossbeam_channel::unbounded; +use crossbeam_channel::{Receiver, Sender}; use std::io::prelude::*; use std::sync::Arc; +pub use termwiz; mod into; use into::{IntoEgui, IntoWez, TryIntoWez}; +pub type TermSender = Sender>; +pub type TermReceiver = Receiver>; +pub type ByteSender = Sender>; +pub type ByteReceiver = Receiver>; + pub use portable_pty::CommandBuilder; + pub use termwiz::Error; pub struct Terminal { terminal: wezterm_term::Terminal, - reader: Receiver>, + reader: TermReceiver, + process: Option, + id: Option, + title: Option, + first_render: bool, +} +struct Process { child: Box, pair: portable_pty::PtyPair, } impl Drop for Terminal { fn drop(&mut self) { - self.kill() + let _ = self.kill(); } } impl Terminal { - pub fn new(command: portable_pty::CommandBuilder) -> Result { + pub fn new( + ctx: &egui::Context, + command: portable_pty::CommandBuilder, + ) -> Result { let pty_system = portable_pty::native_pty_system(); let pair = pty_system.openpty(portable_pty::PtySize::default())?; let child = pair.slave.spawn_command(command)?; @@ -63,6 +81,7 @@ impl Terminal { writer, ); + let ctx = ctx.clone(); let (sender, reciever) = unbounded(); std::thread::spawn(move || { let mut buf = [0; 2usize.pow(10)]; @@ -74,6 +93,9 @@ impl Terminal { return; }; let actions = parser.parse_as_vec(&buf[0..len]); + if !actions.is_empty() { + ctx.request_repaint(); + } let Ok(_) = sender.send(actions) else { return }; } }); @@ -81,40 +103,157 @@ impl Terminal { Ok(Self { terminal, reader: reciever, - child, - pair, + process: Some(Process { pair, child }), + id: None, + title: None, + first_render: true, }) } + pub fn new_readonly( + ctx: &egui::Context, + id: egui::Id, + title: impl Into, + receiver: Receiver>, + default_cols: usize, + default_rows: usize, + ) -> Self { + let (cols, rows) = ctx.memory_mut(|m| { + *m.data + .get_persisted_mut_or_insert_with(id, move || (default_cols, default_rows)) + }); + Self { + terminal: wezterm_term::Terminal::new( + wezterm_term::TerminalSize { + cols, + rows, + ..Default::default() + }, + Arc::new(Config), + "luminol-term", + "1.0", + Box::new(std::io::sink()), + ), + reader: receiver, + process: None, + id: Some(id), + title: Some(title.into()), + first_render: true, + } + } + pub fn title(&self) -> String { - self.terminal.get_title().replace("wezterm", "luminol-term") + self.title + .clone() + .unwrap_or_else(|| self.terminal.get_title().replace("wezterm", "luminol-term")) } pub fn id(&self) -> egui::Id { - if let Some(id) = self.child.process_id() { + if let Some(id) = self.id { + id + } else if let Some(id) = self.process.as_ref().and_then(|p| p.child.process_id()) { egui::Id::new(id) } else { egui::Id::new(self.title()) } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> std::io::Result<()> { - while let Ok(actions) = self.reader.try_recv() { + pub fn set_size( + &mut self, + update_state: &mut luminol_core::UpdateState<'_>, + cols: usize, + rows: usize, + ) { + let mut size = self.terminal.get_size(); + size.cols = cols; + size.rows = rows; + self.terminal.resize(size); + update_state + .ctx + .memory_mut(|m| m.data.insert_persisted(self.id(), (size.cols, size.rows))); + if let Some(process) = &mut self.process { + if let Err(e) = process.pair.master.resize(portable_pty::PtySize { + rows: size.rows as u16, + cols: size.cols as u16, + ..Default::default() + }) { + luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e).wrap_err("Error resizing terminal") + ); + } + } + } + + pub fn set_cols(&mut self, update_state: &mut luminol_core::UpdateState<'_>, cols: usize) { + self.set_size(update_state, cols, self.terminal.get_size().rows); + } + + pub fn set_rows(&mut self, update_state: &mut luminol_core::UpdateState<'_>, rows: usize) { + self.set_size(update_state, self.terminal.get_size().cols, rows); + } + + pub fn size(&self) -> (usize, usize) { + let size = self.terminal.get_size(); + (size.cols, size.rows) + } + + pub fn cols(&self) -> usize { + self.terminal.get_size().cols + } + + pub fn rows(&self) -> usize { + self.terminal.get_size().rows + } + + pub fn erase_scrollback(&mut self) { + self.terminal.erase_scrollback(); + } + + pub fn erase_scrollback_and_viewport(&mut self) { + self.terminal.erase_scrollback_and_viewport(); + self.terminal + .perform_actions(vec![termwiz::escape::Action::CSI( + termwiz::escape::CSI::Edit(termwiz::escape::csi::Edit::EraseInDisplay( + termwiz::escape::csi::EraseInDisplay::EraseDisplay, + )), + )]) + } + + pub fn update(&mut self) { + for actions in self.reader.try_iter() { self.terminal.perform_actions(actions); } + } - let mut size = self.terminal.get_size(); + pub fn ui(&mut self, ui: &mut egui::Ui) -> color_eyre::Result<()> { + // Forget the scroll position from the last time the user opened the application so that + // the terminal immediately scrolls to the bottom + let scroll_area_id_source = "scroll_area"; + if self.first_render { + self.first_render = false; + let scroll_area_id = ui.make_persistent_id(egui::Id::new(scroll_area_id_source)); + egui::scroll_area::State::default().store(ui.ctx(), scroll_area_id); + } + + self.update(); + + let size = self.terminal.get_size(); let cursor_pos = self.terminal.cursor_pos(); let palette = self.terminal.get_config().color_palette(); - let prev_spacing = ui.spacing_mut().item_spacing; + let prev_spacing = ui.spacing().item_spacing; ui.spacing_mut().item_spacing = egui::Vec2::ZERO; let text_width = ui.fonts(|f| f.glyph_width(&egui::FontId::monospace(12.0), '?')); let text_height = ui.text_style_height(&egui::TextStyle::Monospace); + let scroll_area_height = (size.rows + 1) as f32 * text_height; + let mut inner_result = Ok(()); egui::ScrollArea::vertical() - .max_height((size.rows + 1) as f32 * text_height) + .id_source(scroll_area_id_source) + .max_height(scroll_area_height) + .min_scrolled_height(scroll_area_height) .stick_to_bottom(true) .show_rows( ui, @@ -192,9 +331,9 @@ impl Terminal { let (response, painter) = ui.allocate_painter(galley_rect.size(), egui::Sense::click_and_drag()); - // if response.clicked() && !response.has_focus() { - // ui.memory_mut(|mem| mem.request_focus(response.id)); - // } + if response.clicked() && !response.has_focus() { + ui.memory_mut(|mem| mem.request_focus(response.id)); + } painter.rect_filled( galley_rect.translate(response.rect.min.to_vec2()), @@ -213,13 +352,17 @@ impl Terminal { egui::Stroke::new(1.0, egui::Color32::WHITE), ); - // if ui.memory(|mem| mem.has_focus(response.id)) { - - ui.output_mut(|o| o.mutable_text_under_cursor = true); - ui.ctx().set_cursor_icon(egui::CursorIcon::Text); - // ui.memory_mut(|mem| mem.lock_focus(response.id, true)); + if response.hovered() { + ui.output_mut(|o| o.mutable_text_under_cursor = true); + ui.ctx().set_cursor_icon(egui::CursorIcon::Text); + } + let focused = response.has_focus(); ui.input(|i| { + if !focused { + return; + } + for e in i.events.iter() { let result = match e { egui::Event::PointerMoved(pos) => { @@ -313,53 +456,25 @@ impl Terminal { _ => Ok(()), }; if let Err(e) = result { - eprintln!("terminal input error {e:?}"); - } + inner_result = Err(color_eyre::eyre::eyre!(e)) + .wrap_err("Terminal input error"); + break; + }; } }); }, ); ui.spacing_mut().item_spacing = prev_spacing; - - ui.separator(); - - ui.horizontal(|ui| { - if ui - .button(egui::RichText::new("KILL").color(egui::Color32::RED)) - .clicked() - { - self.kill() - } - - let mut resize = false; - resize |= ui.add(egui::DragValue::new(&mut size.rows)).changed(); - - ui.label("x"); - resize |= ui.add(egui::DragValue::new(&mut size.cols)).changed(); - - if resize { - self.terminal.resize(size); - if let Err(e) = self.pair.master.resize(portable_pty::PtySize { - rows: size.rows as u16, - cols: size.cols as u16, - ..Default::default() - }) { - eprintln!("error resizing terminal: {e}"); - } - } - }); - - ui.ctx() - .request_repaint_after(std::time::Duration::from_millis(16)); - - Ok(()) + inner_result } #[inline(never)] - pub fn kill(&mut self) { - if let Err(e) = self.child.kill() { - eprintln!("error killing child: {e}"); + pub fn kill(&mut self) -> color_eyre::Result<()> { + if let Some(process) = &mut self.process { + process.child.kill().map_err(|e| e.into()) + } else { + Ok(()) } } } diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index b194e50e..b80e1fa6 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -27,7 +27,6 @@ luminol-components.workspace = true luminol-modals.workspace = true egui.workspace = true -egui_extras.workspace = true catppuccin-egui = { version = "3.1.0", git = "https://github.com/catppuccin/egui", rev = "bcb5849b6f96b56aa4982ec3366e238371de473e" } @@ -35,13 +34,11 @@ camino.workspace = true strum.workspace = true -git-version = "0.3.5" +git-version.workspace = true poll-promise.workspace = true async-std.workspace = true -pin-project.workspace = true -futures-lite.workspace = true -futures = "0.3.28" +futures-util = "0.3.30" reqwest = "0.11.22" zip = { version = "0.6.6", default-features = false, features = ["deflate"] } @@ -51,7 +48,7 @@ qp-trie.workspace = true itertools.workspace = true -anyhow.workspace = true +color-eyre.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] luminol-term = { version = "0.4.0", path = "../term/" } diff --git a/crates/ui/src/tabs/map/mod.rs b/crates/ui/src/tabs/map/mod.rs index 5ecea3ac..8f571cf3 100644 --- a/crates/ui/src/tabs/map/mod.rs +++ b/crates/ui/src/tabs/map/mod.rs @@ -111,7 +111,7 @@ impl Tab { pub fn new( id: usize, update_state: &mut luminol_core::UpdateState<'_>, - ) -> anyhow::Result { + ) -> color_eyre::Result { // *sigh* // borrow checker. let view = luminol_components::MapView::new(update_state, id)?; diff --git a/crates/ui/src/windows/archive_manager.rs b/crates/ui/src/windows/archive_manager.rs index 2da6e488..9b852a70 100644 --- a/crates/ui/src/windows/archive_manager.rs +++ b/crates/ui/src/windows/archive_manager.rs @@ -266,12 +266,21 @@ impl Window { name, )) } - Err(e) => update_state.toasts.error(e.to_string()), + Err(e) => luminol_core::error!( + update_state.toasts, + e.wrap_err("Error parsing archive contents") + ), } } Ok(Err(e)) => { - if !matches!(e, luminol_filesystem::Error::CancelledLoading) { - update_state.toasts.error(e.to_string()) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Unable to read archive file") + ); } } Err(p) => *load_promise = Some(p), @@ -350,7 +359,7 @@ impl Window { Ok(()) })); } - Err(e) => update_state.toasts.error(e.to_string()), + Err(e) => luminol_core::error!(update_state.toasts, e.wrap_err("Error enumerating files to extract from archive")), } } else if save_promise.is_some() { ui.spinner(); @@ -371,10 +380,18 @@ impl Window { if let Some(p) = save_promise.take() { match p.try_take() { - Ok(Ok(())) => update_state.toasts.info("Extracted successfully!"), + Ok(Ok(())) => { + luminol_core::info!(update_state.toasts, "Extracted successfully!") + } Ok(Err(e)) => { - if !matches!(e, luminol_filesystem::Error::CancelledLoading) { - update_state.toasts.error(e.to_string()) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error extracting archive") + ); } } Err(p) => *save_promise = Some(p), @@ -400,8 +417,14 @@ impl Window { )); } Ok(Err(e)) => { - if !matches!(e, luminol_filesystem::Error::CancelledLoading) { - update_state.toasts.error(e.to_string()) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Unable to read contents of source directory"), + ); } } Err(p) => *load_promise = Some(p), @@ -511,7 +534,7 @@ impl Window { .await })); } - Err(e) => update_state.toasts.error(e.to_string()), + Err(e) => luminol_core::error!(update_state.toasts, e.wrap_err("Error enumerating files to create archive from")), } } } else if save_promise.is_some() { @@ -534,11 +557,20 @@ impl Window { if let Some(p) = save_promise.take() { match p.try_take() { Ok(Ok(())) => { - update_state.toasts.info("Created archive successfully!"); + luminol_core::info!( + update_state.toasts, + "Created archive successfully!" + ); } Ok(Err(e)) => { - if !matches!(e, luminol_filesystem::Error::CancelledLoading) { - update_state.toasts.error(e.to_string()) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error creating archive") + ); } } Err(p) => *save_promise = Some(p), diff --git a/crates/ui/src/windows/console.rs b/crates/ui/src/windows/console.rs index bc88491b..13e96dce 100644 --- a/crates/ui/src/windows/console.rs +++ b/crates/ui/src/windows/console.rs @@ -27,9 +27,12 @@ pub struct Window { } impl Window { - pub fn new(command: luminol_term::CommandBuilder) -> Result { + pub fn new( + ctx: &egui::Context, + command: luminol_term::CommandBuilder, + ) -> Result { Ok(Self { - term: luminol_term::Terminal::new(command)?, + term: luminol_term::Terminal::new(ctx, command)?, }) } } @@ -40,7 +43,7 @@ impl luminol_core::Window for Window { } fn id(&self) -> egui::Id { - egui::Id::new("Console") + self.term.id() } fn requires_filesystem(&self) -> bool { @@ -58,10 +61,38 @@ impl luminol_core::Window for Window { .open(open) .resizable(false) .show(ctx, |ui| { + ui.horizontal(|ui| { + if ui + .button(egui::RichText::new("KILL").color(egui::Color32::RED)) + .clicked() + { + if let Err(e) = self.term.kill() { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error killing child"), + ); + } + } + + let mut resize = false; + let (mut cols, mut rows) = self.term.size(); + + resize |= ui.add(egui::DragValue::new(&mut cols)).changed(); + ui.label("×"); + resize |= ui.add(egui::DragValue::new(&mut rows)).changed(); + + if resize { + self.term.set_size(update_state, cols, rows); + } + }); + + ui.add_space(ui.spacing().item_spacing.y); + if let Err(e) = self.term.ui(ui) { - update_state - .toasts - .error(format!("error displaying terminal: {e:?}")); + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error displaying terminal"), + ); } }); } diff --git a/crates/ui/src/windows/map_picker.rs b/crates/ui/src/windows/map_picker.rs index 413062ac..ce257b70 100644 --- a/crates/ui/src/windows/map_picker.rs +++ b/crates/ui/src/windows/map_picker.rs @@ -134,7 +134,10 @@ impl luminol_core::Window for Window { if let Some(id) = open_map_id { match crate::tabs::map::Tab::new(id, update_state) { Ok(tab) => update_state.edit_tabs.add_tab(tab), - Err(e) => update_state.toasts.error(e.to_string()), + Err(e) => luminol_core::error!( + update_state.toasts, + e.wrap_err("Error enumerating maps") + ), } } }) diff --git a/crates/ui/src/windows/new_project.rs b/crates/ui/src/windows/new_project.rs index 416cbde7..53516fdd 100644 --- a/crates/ui/src/windows/new_project.rs +++ b/crates/ui/src/windows/new_project.rs @@ -25,7 +25,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use anyhow::Context; +use color_eyre::eyre::WrapErr; use luminol_filesystem::FileSystem; use strum::IntoEnumIterator; @@ -231,7 +231,7 @@ impl Window { if download_executable { Self::download_executable(&config, &host_fs, progress) .await - .with_context(|| format!("while downloading {}", config.project.rgss_ver))?; + .wrap_err_with(|| format!("While downloading {}", config.project.rgss_ver))?; } if let Some(branch_name) = git_branch_name { @@ -243,7 +243,7 @@ impl Window { .spawn() .and_then(|mut c| c.wait()) { - anyhow::bail!("Failed to initialize git repository: {e}"); + color_eyre::eyre::bail!("Failed to initialize git repository: {e}"); } } @@ -258,7 +258,7 @@ impl Window { config: &luminol_config::project::Config, filesystem: &impl luminol_filesystem::FileSystem, progress: Arc, - ) -> anyhow::Result<()> { + ) -> color_eyre::Result<()> { let zip_url: &[_] = match config.project.rgss_ver { luminol_config:: RGSSVer::ModShot => &[ "https://github.com/thehatkid/ModShot/releases/download/latest/ModShot_Windows_bb6bcbc_Ruby-3.1-ucrt64_Steam-false.zip", @@ -276,20 +276,21 @@ impl Window { progress.zip_total.store(zip_url.len(), Ordering::Relaxed); - let zips = futures::future::join_all(zip_url.iter().map(|url| reqwest::get(*url))).await; + let zips = + futures_util::future::join_all(zip_url.iter().map(|url| reqwest::get(*url))).await; for (index, zip_response) in zips.into_iter().enumerate() { progress.zip_current.store(index, Ordering::Relaxed); progress.total_progress.store(0, Ordering::Relaxed); let response = zip_response - .map_err(anyhow::Error::from) - .context("while downloading the zip")?; + .map_err(color_eyre::Report::from) + .wrap_err("While downloading the zip")?; let bytes = response.bytes().await?; let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)) - .context("while reading the zip archive")?; + .wrap_err("While reading the zip archive")?; progress .total_progress .store(archive.len(), Ordering::Relaxed); @@ -308,7 +309,7 @@ impl Window { .unwrap_or(&file_path); let file_path = file_path .to_str() - .ok_or(anyhow::anyhow!("invalid file path {file_path:#?}"))?; + .ok_or(color_eyre::eyre::eyre!("Invalid file path {file_path:#?}"))?; if file_path.is_empty() || filesystem.exists(file_path)? { continue; @@ -317,14 +318,14 @@ impl Window { if file.is_dir() { filesystem .create_dir(file_path) - .with_context(|| format!("creating the directory {file_path}"))?; + .wrap_err_with(|| format!("While creating the directory {file_path}"))?; } else { let mut bytes = Vec::new(); file.read_to_end(&mut bytes) - .with_context(|| format!("reading the file {file_path}"))?; + .wrap_err_with(|| format!("While reading the file {file_path}"))?; filesystem .write(file_path, bytes) - .with_context(|| format!("writing the file {file_path}"))?; + .wrap_err_with(|| format!("While writing the file {file_path}"))?; } } } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index a5346621..d5092fa0 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -16,10 +16,6 @@ workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -egui.workspace = true -luminol-egui-wgpu.workspace = true -wgpu.workspace = true - wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4" diff --git a/src/app/log_window.rs b/src/app/log_window.rs new file mode 100644 index 00000000..7362bde2 --- /dev/null +++ b/src/app/log_window.rs @@ -0,0 +1,138 @@ +// 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 . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use std::collections::VecDeque; + +static BUFFER_CAPACITY: usize = 1 << 24; + +pub struct LogWindow { + pub(super) term_shown: bool, + term: luminol_term::Terminal, + save_promise: Option>>, + buffer: VecDeque, + buffer_entry_sizes: VecDeque, + byte_rx: luminol_term::ByteReceiver, +} + +impl LogWindow { + pub fn new(term: luminol_term::Terminal, byte_rx: luminol_term::ByteReceiver) -> Self { + Self { + term_shown: false, + save_promise: None, + buffer: VecDeque::new(), + buffer_entry_sizes: VecDeque::new(), + term, + byte_rx, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>) { + // We update the log terminal even if it's not open so that we don't encounter + // performance problems when the terminal has to parse all the new input at once + self.term.update(); + + for bytes in self.byte_rx.try_iter() { + while self.buffer.len() + bytes.len() > BUFFER_CAPACITY { + for _ in 0..self.buffer_entry_sizes.pop_front().unwrap() { + self.buffer.pop_front(); + } + } + self.buffer_entry_sizes.push_back(bytes.len()); + self.buffer.extend(bytes); + } + + egui::Window::new("Log") + .id(self.term.id()) + .open(&mut self.term_shown) + .resizable(false) + .show(ui.ctx(), |ui| { + ui.horizontal(|ui| { + let mut resize = false; + let (mut cols, mut rows) = self.term.size(); + + resize |= ui.add(egui::DragValue::new(&mut cols)).changed(); + ui.label("×"); + resize |= ui.add(egui::DragValue::new(&mut rows)).changed(); + + if resize { + self.term.set_size(update_state, cols, rows); + } + + ui.add_space(ui.style().spacing.indent); + + if ui.button("Clear").clicked() { + self.buffer.clear(); + self.term.erase_scrollback_and_viewport(); + } + + if let Some(p) = self.save_promise.take() { + ui.spinner(); + + match p.try_take() { + Ok(Ok(())) => { + luminol_core::info!(update_state.toasts, "Successfully saved log!") + } + Ok(Err(e)) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) => + { + luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e) + .wrap_err("Error saving the log to a file") + ) + } + Ok(Err(_)) => {} + Err(p) => self.save_promise = Some(p), + } + } else if ui.button("Save to file").clicked() { + self.buffer.make_contiguous(); + let buffer = self.buffer.clone(); + + self.save_promise = Some(luminol_core::spawn_future(async move { + use futures_lite::AsyncWriteExt; + + let mut tmp = luminol_filesystem::host::File::new()?; + let mut cursor = async_std::io::Cursor::new(buffer.as_slices().0); + async_std::io::copy(&mut cursor, &mut tmp).await?; + tmp.flush().await?; + tmp.save("luminol.log", "Log files").await?; + Ok(()) + })); + } + }); + + ui.add_space(ui.spacing().item_spacing.y); + + if let Err(e) = self.term.ui(ui) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error displaying log window"), + ); + } + }); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 6de7c5b8..a937f44b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -28,11 +28,15 @@ use crate::lumi::Lumi; #[cfg(feature = "steamworks")] use crate::steam::Steamworks; +#[cfg(not(target_arch = "wasm32"))] +mod log_window; mod top_bar; /// The main Luminol struct. Handles rendering, GUI state, that sort of thing. pub struct App { top_bar: top_bar::TopBar, + #[cfg(not(target_arch = "wasm32"))] + log: log_window::LogWindow, lumi: Lumi, #[cfg(not(target_arch = "wasm32"))] @@ -80,6 +84,8 @@ impl App { pub fn new( cc: &luminol_eframe::CreationContext<'_>, modified: luminol_core::ModifiedState, + #[cfg(not(target_arch = "wasm32"))] log_term_rx: luminol_term::TermReceiver, + #[cfg(not(target_arch = "wasm32"))] log_byte_rx: luminol_term::ByteReceiver, #[cfg(not(target_arch = "wasm32"))] try_load_path: Option, #[cfg(target_arch = "wasm32")] audio: luminol_audio::AudioWrapper, #[cfg(feature = "steamworks")] steamworks: Steamworks, @@ -178,10 +184,10 @@ impl App { match filesystem.load_project_from_path(&mut project_config, &mut global_config, path) { Ok(_) => { if let Err(e) = data.load(&filesystem, project_config.as_mut().unwrap()) { - toasts.error(e.to_string()) + luminol_core::error!(toasts, e) } } - Err(e) => toasts.error(e.to_string()), + Err(e) => luminol_core::error!(toasts, e), } } @@ -211,6 +217,18 @@ impl App { Self { top_bar: top_bar::TopBar::default(), + #[cfg(not(target_arch = "wasm32"))] + log: log_window::LogWindow::new( + luminol_term::Terminal::new_readonly( + &cc.egui_ctx, + "luminol_log".into(), + "Log", + log_term_rx, + 132, + 43, + ), + log_byte_rx, + ), lumi, audio, @@ -256,13 +274,18 @@ impl luminol_eframe::App for App { &mut self.global_config, path, ) { - self.toasts - .error(format!("Error opening dropped project: {e}")) + luminol_core::error!( + self.toasts, + color_eyre::eyre::eyre!(e).wrap_err("Error opening dropped project") + ) } else { - self.toasts.info(format!( - "Successfully opened {:?}", - self.filesystem.project_path().expect("project not open") - )); + luminol_core::info!( + self.toasts, + format!( + "Successfully opened {:?}", + self.filesystem.project_path().expect("project not open") + ) + ); } } }); @@ -324,6 +347,16 @@ impl luminol_eframe::App for App { .frame(egui::Frame::central_panel(&ctx.style()).inner_margin(0.)) .show(ctx, |ui| { ui.group(|ui| self.tabs.ui_without_edit(ui, &mut update_state)); + + // Show the log window if it's open. + #[cfg(not(target_arch = "wasm32"))] + { + if self.top_bar.show_log { + self.top_bar.show_log = false; + self.log.term_shown = true; + } + self.log.ui(ui, &mut update_state); + } }); // Update all windows. diff --git a/src/app/top_bar.rs b/src/app/top_bar.rs index efc3c2e4..24dcca77 100644 --- a/src/app/top_bar.rs +++ b/src/app/top_bar.rs @@ -29,6 +29,8 @@ use strum::IntoEnumIterator; pub struct TopBar { #[cfg(not(target_arch = "wasm32"))] fullscreen: bool, + #[cfg(not(target_arch = "wasm32"))] + pub(super) show_log: bool, } impl TopBar { @@ -264,6 +266,11 @@ impl TopBar { .edit_windows .add_window(luminol_ui::windows::misc::FilesystemDebug::default()); } + + #[cfg(not(target_arch = "wasm32"))] + if ui.button("Log").clicked() { + self.show_log = true; + } }); #[cfg(not(target_arch = "wasm32"))] @@ -280,23 +287,27 @@ impl TopBar { .expect("project not loaded"), ); - let result = luminol_ui::windows::console::Window::new(cmd).or_else(|_| { - let mut cmd = luminol_term::CommandBuilder::new("game"); - cmd.cwd( - update_state - .filesystem - .project_path() - .expect("project not loaded"), - ); + let result = + luminol_ui::windows::console::Window::new(ui.ctx(), cmd).or_else(|_| { + let mut cmd = luminol_term::CommandBuilder::new("game"); + cmd.cwd( + update_state + .filesystem + .project_path() + .expect("project not loaded"), + ); - luminol_ui::windows::console::Window::new(cmd) - }); + luminol_ui::windows::console::Window::new(ui.ctx(), cmd) + }); match result { Ok(w) => update_state.edit_windows.add_window(w), - Err(e) => update_state.toasts.error(format!( - "error starting game (tried steamshim.exe and then game.exe): {e}" - )), + Err(e) => luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e).wrap_err( + "Error starting game (tried steamshim.exe and then game.exe)" + ) + ), } } @@ -313,11 +324,12 @@ impl TopBar { .expect("project not loaded"), ); - match luminol_ui::windows::console::Window::new(cmd) { + match luminol_ui::windows::console::Window::new(ui.ctx(), cmd) { Ok(w) => update_state.edit_windows.add_window(w), - Err(e) => update_state - .toasts - .error(format!("error starting shell: {e}")), + Err(e) => luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e).wrap_err("Error starting shell") + ), } } }); @@ -340,9 +352,9 @@ impl TopBar { match update_state.data.save(update_state.filesystem, config) { Ok(_) => { update_state.modified.set(false); - update_state.toasts.info("Saved project successfully!") + luminol_core::info!(update_state.toasts, "Saved project successfully!"); } - Err(e) => update_state.toasts.error(e.to_string()), + Err(e) => luminol_core::error!(update_state.toasts, e), } } } diff --git a/src/main.rs b/src/main.rs index c5b308d7..b0e2b024 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -39,6 +40,86 @@ compile_error!("Steamworks is not supported on webassembly"); #[cfg(feature = "steamworks")] mod steam; +#[cfg(not(target_arch = "wasm32"))] +/// A writer that copies whatever is written to it to two other writers. +struct CopyWriter(A, B); + +#[cfg(not(target_arch = "wasm32"))] +impl std::io::Write for CopyWriter +where + A: std::io::Write, + B: std::io::Write, +{ + fn write(&mut self, buf: &[u8]) -> std::io::Result { + Ok(self.0.write(buf)?.min(self.1.write(buf)?)) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.0.flush()?; + self.1.flush()?; + Ok(()) + } +} + +#[cfg(not(target_arch = "wasm32"))] +static LOG_TERM_SENDER: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + +#[cfg(not(target_arch = "wasm32"))] +static LOG_BYTE_SENDER: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + +#[cfg(not(target_arch = "wasm32"))] +static CONTEXT: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + +#[cfg(not(target_arch = "wasm32"))] +/// A writer that writes to Luminol's log window. +struct LogWriter(luminol_term::termwiz::escape::parser::Parser); + +#[cfg(not(target_arch = "wasm32"))] +impl std::io::Write for LogWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + LOG_BYTE_SENDER + .get() + .unwrap() + .try_send(buf.into()) + .map_err(std::io::Error::other)?; + + let parsed = self.0.parse_as_vec(buf); + + // Convert from LF line endings to CRLF so that wezterm will display them properly + let mut vec = Vec::with_capacity(2 * parsed.len()); + for action in parsed { + if action + == luminol_term::termwiz::escape::Action::Control( + luminol_term::termwiz::escape::ControlCode::LineFeed, + ) + { + vec.push(luminol_term::termwiz::escape::Action::Control( + luminol_term::termwiz::escape::ControlCode::CarriageReturn, + )); + } + vec.push(action); + } + + LOG_TERM_SENDER + .get() + .unwrap() + .try_send(vec) + .map_err(std::io::Error::other)?; + + if let Some(ctx) = CONTEXT.get() { + ctx.request_repaint(); + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + #[cfg(not(target_arch = "wasm32"))] fn main() { #[cfg(feature = "steamworks")] @@ -91,12 +172,59 @@ fn main() { std::process::abort(); }); - // Log to stdout (if you run with `RUST_LOG=debug`). - tracing_subscriber::fmt::init(); + // Enable full backtraces unless the user manually set the RUST_BACKTRACE environment variable + if std::env::var("RUST_BACKTRACE").is_err() { + std::env::set_var("RUST_BACKTRACE", "full"); + } - color_backtrace::BacktracePrinter::new() - .verbosity(color_backtrace::Verbosity::Full) - .install(color_backtrace::default_output_stream()); + // Set up hooks for formatting errors and panics + color_eyre::config::HookBuilder::default() + .panic_section(format!("Luminol version: {}", git_version::git_version!())) + .add_frame_filter(Box::new(|frames| { + let filters = &[ + "_", + "core::", + "alloc::", + "tokio::", + "winit::", + "std::rt::", + "std::sys_", + "egui::ui::", + "E as eyre::", + "T as core::", + "egui_dock::", + "std::panic::", + "egui::context::", + "luminol_eframe::", + "std::panicking::", + "egui::containers::", + "std::thread::local::", + ]; + frames.retain(|frame| { + !filters.iter().any(|f| { + frame + .name + .as_ref() + .is_some_and(|name| name.strip_prefix('<').unwrap_or(name).starts_with(f)) + }) + }) + })) + .install() + .expect("failed to install color-eyre hooks"); + + // Log to stderr as well as Luminol's log. + let (log_term_tx, log_term_rx) = luminol_term::unbounded(); + let (log_byte_tx, log_byte_rx) = luminol_term::unbounded(); + LOG_TERM_SENDER.set(log_term_tx).unwrap(); + LOG_BYTE_SENDER.set(log_byte_tx).unwrap(); + tracing_subscriber::fmt() + .with_writer(|| { + CopyWriter( + std::io::stderr(), + LogWriter(luminol_term::termwiz::escape::parser::Parser::new()), + ) + }) + .init(); let image = image::load_from_memory(ICON).expect("Failed to load Icon data."); @@ -131,9 +259,12 @@ fn main() { "Luminol", native_options, Box::new(|cc| { + CONTEXT.set(cc.egui_ctx.clone()).unwrap(); Box::new(app::App::new( cc, Default::default(), + log_term_rx, + log_byte_rx, std::env::args_os().nth(1), #[cfg(feature = "steamworks")] steamworks, @@ -143,9 +274,6 @@ fn main() { .expect("failed to start luminol"); } -#[cfg(target_arch = "wasm32")] -fn main() {} - #[cfg(target_arch = "wasm32")] const CANVAS_ID: &str = "luminol-canvas"; @@ -172,28 +300,20 @@ pub fn luminol_main_start(fallback: bool) { } }); + // Set up hooks for formatting errors and panics + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!("Luminol version: {}", git_version::git_version!())) + .into_hooks(); + eyre_hook + .install() + .expect("failed to install color-eyre hooks"); std::panic::set_hook(Box::new(move |info| { - let backtrace_printer = - color_backtrace::BacktracePrinter::new().verbosity(color_backtrace::Verbosity::Full); - let mut buffer = color_backtrace::termcolor::Ansi::new(vec![]); - let _ = backtrace_printer.print_panic_info(info, &mut buffer); - let report = String::from_utf8(buffer.into_inner()).expect("panic report not valid utf-8"); - - web_sys::console::log_1(&js_sys::JsString::from(report)); - + web_sys::console::log_1(&js_sys::JsString::from( + panic_hook.panic_report(info).to_string(), + )); let _ = panic_tx.send(()); })); - // Redirect tracing to console.log and friends: - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_max_level(tracing::Level::INFO) - .build(), - ); - - // Redirect log (currently used by egui) to tracing - tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); - let window = web_sys::window().expect("could not get `window` object (make sure you're running this in the main thread of a web browser)"); let prefers_color_scheme_dark = window .match_media("(prefers-color-scheme: dark)") @@ -210,6 +330,16 @@ pub fn luminol_main_start(fallback: bool) { .transfer_control_to_offscreen() .expect("could not transfer canvas control to offscreen"); + // Redirect tracing to console.log and friends: + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_max_level(tracing::Level::INFO) + .build(), + ); + + // Redirect log (currently used by egui) to tracing + tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); + if !luminol_web::bindings::cross_origin_isolated() { tracing::error!("Luminol requires Cross-Origin Isolation to be enabled in order to run."); return;