Filename sanitization

master
hheik 2024-09-04 16:08:41 +03:00
parent cd905cece8
commit 259e867575
2 changed files with 61 additions and 25 deletions

View File

@ -9,6 +9,8 @@ use yaml_rust2::Yaml;
pub const BASE_URL: &str = "http://thunderstore.io"; pub const BASE_URL: &str = "http://thunderstore.io";
pub type DependencyString = String;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ModInfo { pub struct ModInfo {
pub name: String, pub name: String,
@ -16,7 +18,7 @@ pub struct ModInfo {
pub version: (i64, i64, i64), pub version: (i64, i64, i64),
pub website_url: String, pub website_url: String,
pub enabled: bool, pub enabled: bool,
pub dependencies: Vec<Option<String>>, pub dependencies: Vec<Option<DependencyString>>,
} }
impl ModInfo { impl ModInfo {
@ -46,11 +48,30 @@ impl ModInfo {
format!("{url}{}", self.version_string(),) 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<String, String> {
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!( format!(
"{name}_{author}_{version}", "{author}-{name}-{version}",
name = self.name,
author = self.author, author = self.author,
name = self.name,
version = self.version_string(), version = self.version_string(),
) )
} }
@ -72,25 +93,39 @@ impl FetchExtractor {
.join(PathBuf::from(format!("{}.zip", unique_name))) .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 self.base_dir
.join(PathBuf::from(format!("{}", unique_name))) .join(PathBuf::from(format!("{}", unique_name)))
} }
pub fn is_fetched(&self, unique_name: &str) -> Option<PathBuf> { /// Returns `Some(path)` if the item is fetched (archive path exists), or `None` otherwise
match self.archive_path(unique_name).exists() { pub fn try_get_fetched(&self, unique_name: &str) -> Option<PathBuf> {
true => Some(self.archive_path(unique_name)), let path = self.archive_path(unique_name);
match path.exists() {
true => Some(path),
false => None, false => None,
} }
} }
pub fn is_extracted(&self, unique_name: &str) -> Option<PathBuf> { /// Returns if the item in fetched
match self.extracted_path(unique_name).exists() { pub fn is_fetched(&self, unique_name: &str) -> bool {
true => Some(self.extracted_path(unique_name)), 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<PathBuf> {
let path = self.extract_path(unique_name);
match path.exists() {
true => Some(path),
false => None, 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 /// Removes the archive and extracted files
pub fn clean(&self, unique_name: &str) -> std::io::Result<()> { pub fn clean(&self, unique_name: &str) -> std::io::Result<()> {
if unique_name.is_empty() { if unique_name.is_empty() {
@ -104,11 +139,11 @@ impl FetchExtractor {
std::fs::create_dir_all(parent_dir)?; 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)?; 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() { if archive_path.is_file() {
std::fs::remove_file(self.archive_path(unique_name))?; std::fs::remove_file(self.archive_path(unique_name))?;
} else { } 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() { 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 { } else {
return Err(std::io::Error::new( return Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
@ -136,7 +171,7 @@ impl FetchExtractor {
/// Fetches the archive and writes it to `self.archive_path()` /// Fetches the archive and writes it to `self.archive_path()`
pub fn fetch(&self, url: &str, unique_name: &str) -> reqwest::Result<()> { pub fn fetch(&self, url: &str, unique_name: &str) -> reqwest::Result<()> {
// Don't re-download if it already exists // Don't re-download if it already exists
if self.is_fetched(unique_name).is_some() { if self.is_fetched(unique_name) {
return Ok(()); return Ok(());
} }
@ -153,19 +188,19 @@ impl FetchExtractor {
Ok(()) 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<()> { pub fn extract(&self, unique_name: &str) -> std::io::Result<()> {
// Don't re-extract if it already exists // Don't re-extract if it already exists
if self.is_extracted(unique_name).is_some() { if self.is_extracted(unique_name) {
return Ok(()); 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)?; std::fs::create_dir_all(parent_dir)?;
} }
unzip_to( unzip_to(
&self.archive_path(unique_name), &self.archive_path(unique_name),
&self.extracted_path(unique_name), &self.extract_path(unique_name),
) )
.unwrap(); .unwrap();
Ok(()) Ok(())

View File

@ -39,7 +39,7 @@ fn main() {
println!("Installing mods"); println!("Installing mods");
for mod_info in mod_list.iter() { for mod_info in mod_list.iter() {
installer::install( installer::install(
&mod_fetcher.extracted_path(&mod_info.unique_name()), &mod_fetcher.extract_path(&mod_info.unique_name().unwrap()),
&game_dir(), &game_dir(),
&mod_info, &mod_info,
) )
@ -73,7 +73,7 @@ fn fetch_mod_list(
fetcher.extract(profile_uuid)?; fetcher.extract(profile_uuid)?;
let mods_yaml_path = fetcher let mods_yaml_path = fetcher
.extracted_path(profile_uuid) .extract_path(profile_uuid)
.join(PathBuf::from("mods.yml")); .join(PathBuf::from("mods.yml"));
let mods_yaml = std::fs::read_to_string(&mods_yaml_path)?; let mods_yaml = std::fs::read_to_string(&mods_yaml_path)?;
@ -113,10 +113,11 @@ fn fetch_mods(fetcher: &FetchExtractor, mod_list: &Vec<ModInfo>) -> Result<(), B
author = mod_info.author, author = mod_info.author,
version = mod_info.version_string(), version = mod_info.version_string(),
); );
let unique_name = mod_info.unique_name().unwrap();
// Skip download & extract if the mod is already extracted // Skip download & extract if the mod is already extracted
if fetcher.is_extracted(&mod_info.unique_name()).is_none() { if !fetcher.is_extracted(&unique_name) {
fetcher.fetch(&mod_info.download_url(), &mod_info.unique_name())?; fetcher.fetch(&mod_info.download_url(), &unique_name)?;
fetcher.extract(&mod_info.unique_name())?; fetcher.extract(&unique_name)?;
println!(""); println!("");
} else { } else {
println!("📦"); println!("📦");