From 259e86757588c08a5c789734cfd78d9b7b596823 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:08:41 +0300 Subject: [PATCH] Filename sanitization --- src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++++-------------- src/main.rs | 11 ++++---- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bbe797b..7838394 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ use yaml_rust2::Yaml; pub const BASE_URL: &str = "http://thunderstore.io"; +pub type DependencyString = String; + #[derive(Debug, Clone)] pub struct ModInfo { pub name: String, @@ -16,7 +18,7 @@ pub struct ModInfo { pub version: (i64, i64, i64), pub website_url: String, pub enabled: bool, - pub dependencies: Vec>, + pub dependencies: Vec>, } impl ModInfo { @@ -46,11 +48,30 @@ impl ModInfo { format!("{url}{}", self.version_string(),) } - pub fn unique_name(&self) -> String { + /// Get a unique identifier for given mod version. + /// Used as part of the filepath in archives & extracted files. + /// + /// Returns an error if it contains slashes or if the path contains no filename (such as ..) + pub fn unique_name(&self) -> Result { + let sanitized = self.dependency_string().replace(&[':', '/'], "_"); + let path = PathBuf::from(&sanitized); + if path.file_name() != Some(path.as_os_str()) { + return Err(format!( + "Mod name {:?} contains invalid characters for a filepath", + sanitized + )); + } + Ok(sanitized) + } + + /// Get the dependecy string for this mod that can be compared to other mods dependencies. + /// + /// This follows the mod naming format from profile + pub fn dependency_string(&self) -> DependencyString { format!( - "{name}_{author}_{version}", - name = self.name, + "{author}-{name}-{version}", author = self.author, + name = self.name, version = self.version_string(), ) } @@ -72,25 +93,39 @@ impl FetchExtractor { .join(PathBuf::from(format!("{}.zip", unique_name))) } - pub fn extracted_path(&self, unique_name: &str) -> PathBuf { + pub fn extract_path(&self, unique_name: &str) -> PathBuf { self.base_dir .join(PathBuf::from(format!("{}", unique_name))) } - pub fn is_fetched(&self, unique_name: &str) -> Option { - match self.archive_path(unique_name).exists() { - true => Some(self.archive_path(unique_name)), + /// Returns `Some(path)` if the item is fetched (archive path exists), or `None` otherwise + pub fn try_get_fetched(&self, unique_name: &str) -> Option { + let path = self.archive_path(unique_name); + match path.exists() { + true => Some(path), false => None, } } - pub fn is_extracted(&self, unique_name: &str) -> Option { - match self.extracted_path(unique_name).exists() { - true => Some(self.extracted_path(unique_name)), + /// Returns if the item in fetched + pub fn is_fetched(&self, unique_name: &str) -> bool { + self.try_get_fetched(unique_name).is_some() + } + + /// Returns `Some(path)` if the item is extracted (extraction path exists), or `None` otherwise + pub fn try_get_extracted(&self, unique_name: &str) -> Option { + let path = self.extract_path(unique_name); + match path.exists() { + true => Some(path), false => None, } } + /// Returns if the item is extracted + pub fn is_extracted(&self, unique_name: &str) -> bool { + self.try_get_extracted(unique_name).is_some() + } + /// Removes the archive and extracted files pub fn clean(&self, unique_name: &str) -> std::io::Result<()> { if unique_name.is_empty() { @@ -104,11 +139,11 @@ impl FetchExtractor { std::fs::create_dir_all(parent_dir)?; } - if let Some(parent_dir) = self.extracted_path(unique_name).parent() { + if let Some(parent_dir) = self.extract_path(unique_name).parent() { std::fs::create_dir_all(parent_dir)?; } - if let Some(archive_path) = self.is_fetched(unique_name) { + if let Some(archive_path) = self.try_get_fetched(unique_name) { if archive_path.is_file() { std::fs::remove_file(self.archive_path(unique_name))?; } else { @@ -119,9 +154,9 @@ impl FetchExtractor { } } - if let Some(extract_path) = self.is_extracted(unique_name) { + if let Some(extract_path) = self.try_get_extracted(unique_name) { if extract_path.is_dir() { - std::fs::remove_dir_all(self.extracted_path(unique_name))?; + std::fs::remove_dir_all(self.extract_path(unique_name))?; } else { return Err(std::io::Error::new( std::io::ErrorKind::Other, @@ -136,7 +171,7 @@ impl FetchExtractor { /// Fetches the archive and writes it to `self.archive_path()` pub fn fetch(&self, url: &str, unique_name: &str) -> reqwest::Result<()> { // Don't re-download if it already exists - if self.is_fetched(unique_name).is_some() { + if self.is_fetched(unique_name) { return Ok(()); } @@ -153,19 +188,19 @@ impl FetchExtractor { Ok(()) } - /// Extracts the archive to `self.extracted_path()` + /// Extracts the archive to `self.extract_path()` pub fn extract(&self, unique_name: &str) -> std::io::Result<()> { // Don't re-extract if it already exists - if self.is_extracted(unique_name).is_some() { + if self.is_extracted(unique_name) { return Ok(()); } - if let Some(parent_dir) = self.extracted_path(unique_name).parent() { + if let Some(parent_dir) = self.extract_path(unique_name).parent() { std::fs::create_dir_all(parent_dir)?; } unzip_to( &self.archive_path(unique_name), - &self.extracted_path(unique_name), + &self.extract_path(unique_name), ) .unwrap(); Ok(()) diff --git a/src/main.rs b/src/main.rs index bff6004..d5a6797 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ fn main() { println!("Installing mods"); for mod_info in mod_list.iter() { installer::install( - &mod_fetcher.extracted_path(&mod_info.unique_name()), + &mod_fetcher.extract_path(&mod_info.unique_name().unwrap()), &game_dir(), &mod_info, ) @@ -73,7 +73,7 @@ fn fetch_mod_list( fetcher.extract(profile_uuid)?; let mods_yaml_path = fetcher - .extracted_path(profile_uuid) + .extract_path(profile_uuid) .join(PathBuf::from("mods.yml")); let mods_yaml = std::fs::read_to_string(&mods_yaml_path)?; @@ -113,10 +113,11 @@ fn fetch_mods(fetcher: &FetchExtractor, mod_list: &Vec) -> Result<(), B author = mod_info.author, version = mod_info.version_string(), ); + let unique_name = mod_info.unique_name().unwrap(); // Skip download & extract if the mod is already extracted - if fetcher.is_extracted(&mod_info.unique_name()).is_none() { - fetcher.fetch(&mod_info.download_url(), &mod_info.unique_name())?; - fetcher.extract(&mod_info.unique_name())?; + if !fetcher.is_extracted(&unique_name) { + fetcher.fetch(&mod_info.download_url(), &unique_name)?; + fetcher.extract(&unique_name)?; println!("✅"); } else { println!("📦");