From 9124459939665ae694aa351909fbee5fef2a1bb0 Mon Sep 17 00:00:00 2001 From: quexeky Date: Tue, 13 May 2025 06:53:00 +1000 Subject: [PATCH] chore: Initial path normalisation & parsing with backup generation Signed-off-by: quexeky --- src-tauri/Cargo.lock | 292 +++++++++++++++++--- src-tauri/Cargo.toml | 19 +- src-tauri/src/cloud_saves/backup_manager.rs | 97 +++++++ src-tauri/src/cloud_saves/conditions.rs | 6 + src-tauri/src/cloud_saves/metadata.rs | 35 +++ src-tauri/src/cloud_saves/mod.rs | 8 + src-tauri/src/cloud_saves/normalise.rs | 162 +++++++++++ src-tauri/src/cloud_saves/parse.rs | 0 src-tauri/src/cloud_saves/path.rs | 48 ++++ src-tauri/src/cloud_saves/placeholder.rs | 54 ++++ src-tauri/src/cloud_saves/resolver.rs | 125 +++++++++ src-tauri/src/cloud_saves/strict_path.rs | 0 src-tauri/src/database/db.rs | 6 +- src-tauri/src/error/backup_error.rs | 21 ++ src-tauri/src/error/mod.rs | 1 + src-tauri/src/lib.rs | 1 + src-tauri/src/process/process_manager.rs | 51 +++- 17 files changed, 871 insertions(+), 55 deletions(-) create mode 100644 src-tauri/src/cloud_saves/backup_manager.rs create mode 100644 src-tauri/src/cloud_saves/conditions.rs create mode 100644 src-tauri/src/cloud_saves/metadata.rs create mode 100644 src-tauri/src/cloud_saves/mod.rs create mode 100644 src-tauri/src/cloud_saves/normalise.rs create mode 100644 src-tauri/src/cloud_saves/parse.rs create mode 100644 src-tauri/src/cloud_saves/path.rs create mode 100644 src-tauri/src/cloud_saves/placeholder.rs create mode 100644 src-tauri/src/cloud_saves/resolver.rs create mode 100644 src-tauri/src/cloud_saves/strict_path.rs create mode 100644 src-tauri/src/error/backup_error.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 37d0771..ecb2d83 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -569,9 +569,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byte-tools" @@ -722,6 +722,8 @@ version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -1156,15 +1158,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys 0.4.1", -] - [[package]] name = "dirs" version = "4.0.0" @@ -1183,6 +1176,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1190,7 +1192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1202,10 +1204,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1289,29 +1303,36 @@ dependencies = [ "cacache 13.1.0", "chrono", "deranged", - "directories", + "dirs 6.0.0", "droplet-rs", "dynfmt", + "filetime", "gethostname 1.0.1", "hex 0.4.3", "http 1.2.0", "http-serde 2.1.1", + "known-folders", "log", "log4rs", "md5", "parking_lot 0.12.3", + "rand 0.9.1", "rayon", + "regex", "reqwest 0.12.9", "reqwest-middleware 0.4.0", "reqwest-middleware-cache", "rustbreak", "rustix 0.38.42", + "schemars", "serde", "serde-binary", "serde_json", "serde_with", + "sha1", "shared_child", "slice-deque", + "tar", "tauri", "tauri-build", "tauri-plugin-autostart", @@ -1320,13 +1341,17 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-shell", "tauri-plugin-single-instance", + "tempfile", "throttle_my_fn", "tokio", "umu-wrapper-lib", "url", "urlencoding", "uuid", + "walkdir", "webbrowser", + "whoami", + "zstd", ] [[package]] @@ -1540,6 +1565,18 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.0.35" @@ -1888,6 +1925,18 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2667,10 +2716,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] -name = "js-sys" -version = "0.3.76" +name = "jobserver" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -2709,6 +2767,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "known-folders" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d9a1740cc8b46e259a0eb787d79d855e79ff10b9855a5eba58868d5da7927c" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -2795,6 +2862,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.8", ] [[package]] @@ -2827,9 +2895,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" dependencies = [ "serde", "value-bag", @@ -3971,6 +4039,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.7.3" @@ -3996,6 +4070,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -4016,6 +4100,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -4034,6 +4128,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -4121,6 +4224,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.9", +] + [[package]] name = "reflink-copy" version = "0.1.23" @@ -4444,6 +4558,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.18" @@ -4470,9 +4590,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -4485,9 +4605,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", @@ -5200,6 +5320,17 @@ dependencies = [ "syn 2.0.91", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5569,14 +5700,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.2", "once_cell", - "rustix 0.38.42", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6171,21 +6302,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.99" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -6197,9 +6344,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -6210,9 +6357,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6220,9 +6367,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -6233,9 +6380,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -6312,9 +6462,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -6428,6 +6578,17 @@ dependencies = [ "windows-core 0.58.0", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall 0.5.8", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6965,6 +7126,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -7077,6 +7247,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix 1.0.5", +] + [[package]] name = "xdg-home" version = "1.3.0" @@ -7261,6 +7441,34 @@ dependencies = [ "syn 2.0.91", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 676a31a..bd6204a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,7 +29,6 @@ tauri-plugin-shell = "2.2.1" serde_json = "1" serde-binary = "0.5.0" rayon = "1.10.0" -directories = "5.0.1" webbrowser = "1.0.2" url = "2.5.2" tauri-plugin-deep-link = "2" @@ -58,6 +57,18 @@ reqwest-middleware-cache = "0.1.1" deranged = "=0.4.0" droplet-rs = "0.7.3" gethostname = "1.0.1" +zstd = "0.13.3" +tar = "0.4.44" +rand = "0.9.1" +regex = "1.11.1" +tempfile = "3.19.1" +schemars = "0.8.22" +sha1 = "0.10.6" +dirs = "6.0.0" +whoami = "1.6.0" +filetime = "0.2.25" +walkdir = "2.5.0" +known-folders = "1.2.0" [dependencies.dynfmt] version = "0.1.5" @@ -81,11 +92,7 @@ features = ["fs"] [dependencies.uuid] version = "1.10.0" -features = [ - "v4", # Lets you generate random UUIDs - "fast-rng", # Use a faster (but still sufficiently random) RNG - "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs -] +features = ["v4", "fast-rng", "macro-diagnostics"] [dependencies.rustbreak] version = "2" diff --git a/src-tauri/src/cloud_saves/backup_manager.rs b/src-tauri/src/cloud_saves/backup_manager.rs new file mode 100644 index 0000000..8c9ceb6 --- /dev/null +++ b/src-tauri/src/cloud_saves/backup_manager.rs @@ -0,0 +1,97 @@ +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +use log::warn; + +use crate::{database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform}; + +use super::path::CommonPath; + +pub struct BackupManager<'a> { + pub current_platform: Platform, + pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>, +} + +impl BackupManager<'_> { + pub fn new() -> Self { + BackupManager { + #[cfg(target_os = "windows")] + current_platform: Platform::Windows, + + #[cfg(target_os = "macos")] + current_platform: Platform::MacOs, + + #[cfg(target_os = "linux")] + current_platform: Platform::Linux, + + sources: HashMap::from([ + // Current platform to target platform + ( + (Platform::Windows, Platform::Windows), + &WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send), + ), + ( + (Platform::Linux, Platform::Linux), + &LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send), + ), + ( + (Platform::MacOs, Platform::MacOs), + &MacBackupManager {} as &(dyn BackupHandler + Sync + Send), + ), + + ]), + } + } + +} + +pub trait BackupHandler: Send + Sync { + fn root_translate(&self, path: &PathBuf, game: &GameVersion) -> Result; + fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result { Ok(PathBuf::from_str(&game.game_id).unwrap()) } + fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) } + fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c } + fn store_user_id_translate(&self, path: &PathBuf, game: &GameVersion) -> Result; + fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { Ok(PathBuf::from_str(&whoami::username()).unwrap()) } + fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected Windows Reference in Backup "); Err(BackupError::InvalidSystem) } + fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected XDG Reference in Backup "); Err(BackupError::InvalidSystem) } + fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result { warn!("Unexpected XDG Reference in Backup "); Err(BackupError::InvalidSystem) } + fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result { Ok(PathBuf::new()) } +} + +pub struct LinuxBackupManager {} +impl BackupHandler for LinuxBackupManager { + fn root_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + println!("Root translate"); + PathBuf::from_str("~").map_err(|_| BackupError::ParseError) + } + + fn store_user_id_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + println!("Store user id translate"); + PathBuf::from_str("ID").map_err(|_| BackupError::ParseError) + } +} +pub struct WindowsBackupManager {} +impl BackupHandler for WindowsBackupManager { + fn root_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + todo!() + } + + fn store_user_id_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + todo!() + } +} +pub struct MacBackupManager {} +impl BackupHandler for MacBackupManager { + fn root_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + todo!() + } + + fn store_user_id_translate(&self, path: &PathBuf, game: &GameVersion) -> Result { + todo!() + } +} \ No newline at end of file diff --git a/src-tauri/src/cloud_saves/conditions.rs b/src-tauri/src/cloud_saves/conditions.rs new file mode 100644 index 0000000..1b3af87 --- /dev/null +++ b/src-tauri/src/cloud_saves/conditions.rs @@ -0,0 +1,6 @@ +use crate::process::process_manager::Platform; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Condition { + Os(Platform) +} diff --git a/src-tauri/src/cloud_saves/metadata.rs b/src-tauri/src/cloud_saves/metadata.rs new file mode 100644 index 0000000..eefa587 --- /dev/null +++ b/src-tauri/src/cloud_saves/metadata.rs @@ -0,0 +1,35 @@ +use crate::database::db::GameVersion; + +use super::conditions::{Condition}; + + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CloudSaveMetadata { + pub files: Vec, + pub game_version: GameVersion, + pub save_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct GameFile { + pub path: String, + pub id: Option, + pub data_type: DataType, + pub tags: Vec, + pub conditions: Vec +} +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +pub enum DataType { + Registry, + File, + Other +} +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Tag { + Config, + Save, + #[default] + #[serde(other)] + Other, +} \ No newline at end of file diff --git a/src-tauri/src/cloud_saves/mod.rs b/src-tauri/src/cloud_saves/mod.rs new file mode 100644 index 0000000..0051a40 --- /dev/null +++ b/src-tauri/src/cloud_saves/mod.rs @@ -0,0 +1,8 @@ +pub mod conditions; +pub mod metadata; +pub mod resolver; +pub mod placeholder; +pub mod normalise; +pub mod parse; +pub mod path; +pub mod backup_manager; \ No newline at end of file diff --git a/src-tauri/src/cloud_saves/normalise.rs b/src-tauri/src/cloud_saves/normalise.rs new file mode 100644 index 0000000..67da752 --- /dev/null +++ b/src-tauri/src/cloud_saves/normalise.rs @@ -0,0 +1,162 @@ +use std::sync::LazyLock; + +use regex::Regex; +use crate::process::process_manager::Platform; + +use super::placeholder::*; + + +pub fn normalize(path: &str, os: Platform) -> String { + let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/"); + + if path == "~" || path.starts_with("~/") { + path = path.replacen('~', HOME, 1); + } + + static CONSECUTIVE_SLASHES: LazyLock = LazyLock::new(|| Regex::new(r"/{2,}").unwrap()); + static UNNECESSARY_DOUBLE_STAR_1: LazyLock = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap()); + static UNNECESSARY_DOUBLE_STAR_2: LazyLock = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap()); + static ENDING_WILDCARD: LazyLock = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap()); + static ENDING_DOT: LazyLock = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap()); + static INTERMEDIATE_DOT: LazyLock = LazyLock::new(|| Regex::new(r"(/\./)").unwrap()); + static BLANK_SEGMENT: LazyLock = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap()); + static APP_DATA: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap()); + static APP_DATA_ROAMING: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap()); + static APP_DATA_LOCAL: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap()); + static APP_DATA_LOCAL_2: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap()); + static USER_PROFILE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap()); + static DOCUMENTS: LazyLock = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap()); + + for (pattern, replacement) in [ + (&CONSECUTIVE_SLASHES, "/"), + (&UNNECESSARY_DOUBLE_STAR_1, "${1}*"), + (&UNNECESSARY_DOUBLE_STAR_2, "*${1}"), + (&ENDING_WILDCARD, ""), + (&ENDING_DOT, ""), + (&INTERMEDIATE_DOT, "/"), + (&BLANK_SEGMENT, "/"), + (&APP_DATA, WIN_APP_DATA), + (&APP_DATA_ROAMING, WIN_APP_DATA), + (&APP_DATA_LOCAL, WIN_LOCAL_APP_DATA), + (&APP_DATA_LOCAL_2, &format!("{}/", WIN_LOCAL_APP_DATA)), + (&USER_PROFILE, HOME), + (&DOCUMENTS, WIN_DOCUMENTS), + ] { + path = pattern.replace_all(&path, replacement).to_string(); + } + + if os == Platform::Windows { + let documents_2: Regex = Regex::new(r"(?i)/Documents").unwrap(); + + #[allow(clippy::single_element_loop)] + for (pattern, replacement) in [(&documents_2, WIN_DOCUMENTS)] { + path = pattern.replace_all(&path, replacement).to_string(); + } + } + + for (pattern, replacement) in [ + ("{64BitSteamID}", STORE_USER_ID), + ("{Steam3AccountID}", STORE_USER_ID), + ] { + path = path.replace(pattern, replacement); + } + + path +} + +fn too_broad(path: &str) -> bool { + println!("Path: {}", path); + use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA}; + + let path_lower = path.to_lowercase(); + + for item in ALL { + if path == *item { + return true; + } + } + + for item in AVOID_WILDCARDS { + if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) { + return true; + } + } + + // These paths are present whether or not the game is installed. + // If possible, they should be narrowed down on the wiki. + for item in [ + format!("{}/{}", BASE, STORE_USER_ID), // because `` is handled as `*` + format!("{}/Documents", HOME), + format!("{}/Saved Games", HOME), + format!("{}/AppData", HOME), + format!("{}/AppData/Local", HOME), + format!("{}/AppData/Local/Packages", HOME), + format!("{}/AppData/LocalLow", HOME), + format!("{}/AppData/Roaming", HOME), + format!("{}/Documents/My Games", HOME), + format!("{}/Library/Application Support", HOME), + format!("{}/Library/Application Support/UserData", HOME), + format!("{}/Library/Preferences", HOME), + format!("{}/.renpy", HOME), + format!("{}/.renpy/persistent", HOME), + format!("{}/Library", HOME), + format!("{}/Library/RenPy", HOME), + format!("{}/Telltale Games", HOME), + format!("{}/config", ROOT), + format!("{}/MMFApplications", WIN_APP_DATA), + format!("{}/RenPy", WIN_APP_DATA), + format!("{}/RenPy/persistent", WIN_APP_DATA), + format!("{}/win.ini", WIN_DIR), + format!("{}/SysWOW64", WIN_DIR), + format!("{}/My Games", WIN_DOCUMENTS), + format!("{}/Telltale Games", WIN_DOCUMENTS), + format!("{}/unity3d", XDG_CONFIG), + format!("{}/unity3d", XDG_DATA), + "C:/Program Files".to_string(), + "C:/Program Files (x86)".to_string(), + ] { + let item = item.to_lowercase(); + if path_lower == item + || path_lower.starts_with(&format!("{}/*", item)) + || path_lower.starts_with(&format!("{}/{}", item, STORE_USER_ID.to_lowercase())) + || path_lower.starts_with(&format!("{}/savesdir", item)) + { + return true; + } + } + + + // Drive letters: + let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap(); + if drives.is_match(path) { + return true; + } + + // Colon not for a drive letter + if path.get(2..).is_some_and(|path| path.contains(':')) { + return true; + } + + // Root: + if path == "/" { + return true; + } + + // Relative path wildcard: + if path.starts_with('*') { + return true; + } + + false +} + +pub fn usable(path: &str) -> bool { + let unprintable: Regex = Regex::new(r"(\p{Cc}|\p{Cf})").unwrap(); + + !path.is_empty() + && !path.contains("{{") + && !path.starts_with("./") + && !path.starts_with("../") + && !too_broad(path) + && !unprintable.is_match(path) +} \ No newline at end of file diff --git a/src-tauri/src/cloud_saves/parse.rs b/src-tauri/src/cloud_saves/parse.rs new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/src/cloud_saves/path.rs b/src-tauri/src/cloud_saves/path.rs new file mode 100644 index 0000000..92aaaf9 --- /dev/null +++ b/src-tauri/src/cloud_saves/path.rs @@ -0,0 +1,48 @@ +use std::{path::PathBuf, sync::LazyLock}; + +pub enum CommonPath { + Config, + Data, + DataLocal, + DataLocalLow, + Document, + Home, + Public, + SavedGames, +} + +impl CommonPath { + pub fn get(&self) -> Option { + static CONFIG: LazyLock> = LazyLock::new(|| dirs::config_dir()); + static DATA: LazyLock> = LazyLock::new(|| dirs::data_dir()); + static DATA_LOCAL: LazyLock> = LazyLock::new(|| dirs::data_local_dir()); + static DOCUMENT: LazyLock> = LazyLock::new(|| dirs::document_dir()); + static HOME: LazyLock> = LazyLock::new(|| dirs::home_dir()); + static PUBLIC: LazyLock> = LazyLock::new(|| dirs::public_dir()); + + #[cfg(windows)] + static DATA_LOCAL_LOW: LazyLock> = LazyLock::new(|| { + known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow) + }); + #[cfg(not(windows))] + static DATA_LOCAL_LOW: Option = None; + + #[cfg(windows)] + static SAVED_GAMES: LazyLock> = LazyLock::new(|| { + known_folders::get_known_folder_path(known_folders::KnownFolder::SavedGames) + }); + #[cfg(not(windows))] + static SAVED_GAMES: Option = None; + + match self { + Self::Config => CONFIG.clone(), + Self::Data => DATA.clone(), + Self::DataLocal => DATA_LOCAL.clone(), + Self::DataLocalLow => DATA_LOCAL_LOW.clone(), + Self::Document => DOCUMENT.clone(), + Self::Home => HOME.clone(), + Self::Public => PUBLIC.clone(), + Self::SavedGames => SAVED_GAMES.clone(), + } + } +} diff --git a/src-tauri/src/cloud_saves/placeholder.rs b/src-tauri/src/cloud_saves/placeholder.rs new file mode 100644 index 0000000..2d8c7ab --- /dev/null +++ b/src-tauri/src/cloud_saves/placeholder.rs @@ -0,0 +1,54 @@ +use std::sync::LazyLock; + +use crate::process::process_manager::Platform; + +pub const ALL: &[&str] = &[ + ROOT, + GAME, + BASE, + HOME, + STORE_USER_ID, + OS_USER_NAME, + WIN_APP_DATA, + WIN_LOCAL_APP_DATA, + WIN_DOCUMENTS, + WIN_PUBLIC, + WIN_PROGRAM_DATA, + WIN_DIR, + XDG_DATA, + XDG_CONFIG, +]; + +/// These are paths where `/*/` is suspicious. +pub const AVOID_WILDCARDS: &[&str] = &[ + ROOT, + HOME, + WIN_APP_DATA, + WIN_LOCAL_APP_DATA, + WIN_DOCUMENTS, + WIN_PUBLIC, + WIN_PROGRAM_DATA, + WIN_DIR, + XDG_DATA, + XDG_CONFIG, +]; + +pub const ROOT: &str = ""; +pub const GAME: &str = ""; +pub const BASE: &str = ""; +pub const HOME: &str = ""; +pub const STORE_USER_ID: &str = ""; +pub const OS_USER_NAME: &str = ""; +pub const WIN_APP_DATA: &str = ""; +pub const WIN_LOCAL_APP_DATA: &str = ""; +pub const WIN_LOCAL_APP_DATA_LOW: &str = ""; +pub const WIN_DOCUMENTS: &str = ""; +pub const WIN_PUBLIC: &str = ""; +pub const WIN_PROGRAM_DATA: &str = ""; +pub const WIN_DIR: &str = ""; +pub const XDG_DATA: &str = ""; +pub const XDG_CONFIG: &str = ""; +pub const SKIP: &str = ""; + +pub static OS_USERNAME: LazyLock = LazyLock::new(|| whoami::username()); + diff --git a/src-tauri/src/cloud_saves/resolver.rs b/src-tauri/src/cloud_saves/resolver.rs new file mode 100644 index 0000000..3f27975 --- /dev/null +++ b/src-tauri/src/cloud_saves/resolver.rs @@ -0,0 +1,125 @@ +use std::{fs::File, io::Write, path::{Component, PathBuf}}; + +use super::{backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*}; +use log::warn; +use rustix::path::Arg; +use tempfile::tempfile; + +use crate::{ + database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform, +}; + +use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize}; + +pub fn resolve(meta: &mut CloudSaveMetadata) -> File { + let f = File::create_new("save").unwrap(); + let compressor = zstd::Encoder::new(f, 22).unwrap(); + let mut tarball = tar::Builder::new(compressor); + let manager = BackupManager::new(); + for file in meta.files.iter_mut() { + let id = uuid::Uuid::new_v4().to_string(); + let os = match file + .conditions + .iter() + .find_map(|p| match p { + super::conditions::Condition::Os(os) => Some(os), + _ => None, + }) + .cloned() + { + Some(os) => os, + None => { + warn!( + "File {:?} could not be backed up because it did not provide an OS", + &file + ); + continue; + } + }; + let handler = match manager.sources.get(&(manager.current_platform, os)) { + Some(h) => *h, + None => continue, + }; + let t_path = PathBuf::from(normalize(&file.path, os)); + println!("{:?}", &t_path); + let path = parse_path(t_path, handler, &meta.game_version).unwrap(); + let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here + if f.is_dir() { + tarball.append_dir(&id, path).unwrap(); + } else if f.is_file() { + tarball + .append_file(&id, &mut File::open(path).unwrap()) + .unwrap(); + } + file.id = Some(id); + } + let binding = serde_json::to_string(meta).unwrap(); + println!("Binding: {}", &binding); + let serialized = binding.as_bytes(); + let mut file = tempfile().unwrap(); + file.write(serialized).unwrap(); + tarball.append_file("metadata", &mut file).unwrap(); + tarball.into_inner().unwrap().finish().unwrap() +} + +pub fn parse_path( + path: PathBuf, + backup_handler: &dyn BackupHandler, + game: &GameVersion, +) -> Result { + println!("Parsing: {:?}", &path); + let mut s = PathBuf::new(); + for component in path.components() { + match component.as_str().unwrap() { + ROOT => { s.push(backup_handler.root_translate(&path, game)?)}, + GAME => { s.push(backup_handler.game_translate(&path, game)?)}, + BASE => { s.push(backup_handler.base_translate(&path, game)?)}, + HOME => { s.push(backup_handler.home_translate(&path, game)?)}, + STORE_USER_ID => { s.push(backup_handler.store_user_id_translate(&path, game)?)}, + OS_USER_NAME => { s.push(backup_handler.os_user_name_translate(&path, game)?)}, + WIN_APP_DATA => { s.push(backup_handler.win_app_data_translate(&path, game)?)}, + WIN_LOCAL_APP_DATA => { s.push(backup_handler.win_local_app_data_translate(&path, game)?)}, + WIN_LOCAL_APP_DATA_LOW => { s.push(backup_handler.win_local_app_data_low_translate(&path, game)?)}, + WIN_DOCUMENTS => { s.push(backup_handler.win_documents_translate(&path, game)?)}, + WIN_PUBLIC => { s.push(backup_handler.win_public_translate(&path, game)?)}, + WIN_PROGRAM_DATA => { s.push(backup_handler.win_program_data_translate(&path, game)?)}, + WIN_DIR => { s.push(backup_handler.win_dir_translate(&path, game)?)}, + XDG_DATA => { s.push(backup_handler.xdg_data_translate(&path, game)?)}, + XDG_CONFIG => { s.push(backup_handler.xdg_config_translate(&path, game)?)}, + SKIP => { }, + _ => s.push(PathBuf::from(component.as_os_str())) + } + } + + println!("Final line: {:?}", &s); + Ok(s) +} + +pub fn test() { + let mut meta = CloudSaveMetadata { + files: vec![GameFile { + path: String::from("/favicon.png"), + id: None, + data_type: super::metadata::DataType::File, + tags: Vec::new(), + conditions: vec![Condition::Os(Platform::Linux)], + }], + game_version: GameVersion { + game_id: String::new(), + version_name: String::new(), + platform: Platform::Linux, + launch_command: String::new(), + launch_args: Vec::new(), + launch_command_template: String::new(), + setup_command: String::new(), + setup_args: Vec::new(), + setup_command_template: String::new(), + only_setup: true, + version_index: 0, + delta: false, + umu_id_override: None, + }, + save_id: String::from("aaaaaaa"), + }; + let file = resolve(&mut meta); +} diff --git a/src-tauri/src/cloud_saves/strict_path.rs b/src-tauri/src/cloud_saves/strict_path.rs new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/src/database/db.rs b/src-tauri/src/database/db.rs index 4ba210e..40fab04 100644 --- a/src-tauri/src/database/db.rs +++ b/src-tauri/src/database/db.rs @@ -7,7 +7,6 @@ use std::{ }; use chrono::Utc; -use directories::BaseDirs; use log::{debug, error, info}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -59,7 +58,7 @@ fn default_template() -> String { "{}".to_owned() } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct GameVersion { pub game_id: String, @@ -132,7 +131,8 @@ impl Database { } } pub static DATA_ROOT_DIR: LazyLock> = - LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); + LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop"))); + // Custom JSON serializer to support everything we need #[derive(Debug, Default, Clone)] diff --git a/src-tauri/src/error/backup_error.rs b/src-tauri/src/error/backup_error.rs new file mode 100644 index 0000000..17a76c5 --- /dev/null +++ b/src-tauri/src/error/backup_error.rs @@ -0,0 +1,21 @@ +use std::fmt::Display; + +use serde_with::SerializeDisplay; + +#[derive(Debug, SerializeDisplay, Clone, Copy)] +pub enum BackupError { + InvalidSystem, + NotFound, + ParseError +} + +impl Display for BackupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + BackupError::InvalidSystem => "Attempted to generate path for invalid system", + BackupError::NotFound => "Could not generate or find path", + BackupError::ParseError => "Failed to parse path", + }; + write!(f, "{}", s) + } +} \ No newline at end of file diff --git a/src-tauri/src/error/mod.rs b/src-tauri/src/error/mod.rs index 89b74ae..e698787 100644 --- a/src-tauri/src/error/mod.rs +++ b/src-tauri/src/error/mod.rs @@ -4,3 +4,4 @@ pub mod library_error; pub mod process_error; pub mod remote_access_error; pub mod setup_error; +pub mod backup_error; \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3d7e6a5..458ef0f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod games; mod autostart; mod cleanup; +mod cloud_saves; mod commands; mod download_manager; mod error; diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index 2bcfd01..8b00251 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -47,7 +47,7 @@ impl ProcessManager<'_> { current_platform: Platform::Windows, #[cfg(target_os = "macos")] - current_platform: Platform::macOS, + current_platform: Platform::MacOs, #[cfg(target_os = "linux")] current_platform: Platform::Linux, @@ -66,7 +66,7 @@ impl ProcessManager<'_> { &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), ), ( - (Platform::macOS, Platform::macOS), + (Platform::MacOs, Platform::MacOs), &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), ), ( @@ -324,11 +324,54 @@ impl ProcessManager<'_> { } } -#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)] +#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)] pub enum Platform { Windows, Linux, - macOS, + MacOs, +} + +impl Platform { + const WINDOWS: bool = cfg!(target_os = "windows"); + const MAC: bool = cfg!(target_os = "macos"); + const LINUX: bool = cfg!(target_os = "linux"); + #[cfg(target_os = "windows")] + pub const HOST: Platform = Self::Windows; + #[cfg(target_os = "macos")] + pub const HOST: Platform = Self::MacOs; + #[cfg(target_os = "linux")] + pub const HOST: Platform = Self::Linux; + + + + pub fn is_case_sensitive(&self) -> bool { + match self { + Self::Windows | Self::MacOs => false, + Self::Linux => true, + } + } +} + +impl From<&str> for Platform { + fn from(value: &str) -> Self { + match value.to_lowercase().trim() { + "windows" => Self::Windows, + "linux" => Self::Linux, + "mac" | "macos" => Self::MacOs, + _ => unimplemented!(), + } + } +} + +impl From for Platform { + fn from(value: whoami::Platform) -> Self { + match value { + whoami::Platform::Windows => Platform::Windows, + whoami::Platform::Linux => Platform::Linux, + whoami::Platform::MacOS => Platform::MacOs, + _ => unimplemented!() + } + } } pub trait ProcessHandler: Send + 'static {