From 08c3c0c8dd29378e151046977b9ffde76964c90c Mon Sep 17 00:00:00 2001 From: nuts_rice Date: Fri, 19 Sep 2025 11:42:35 -0600 Subject: [PATCH 1/9] feat: add sprite dir errors + warns + validation --- martin-core/src/resources/sprites/error.rs | 12 + martin-core/src/resources/sprites/mod.rs | 262 ++++++++++++++++++++- 2 files changed, 265 insertions(+), 9 deletions(-) diff --git a/martin-core/src/resources/sprites/error.rs b/martin-core/src/resources/sprites/error.rs index d6fafcb9c..00d9b4c28 100644 --- a/martin-core/src/resources/sprites/error.rs +++ b/martin-core/src/resources/sprites/error.rs @@ -47,4 +47,16 @@ pub enum SpriteError { /// Failed to create sprite from SVG file. #[error("Unable to create a sprite from file {0}")] SpriteInstError(PathBuf), + + /// Empty sprite directory. + #[error("Empty sprite directory: {0}")] + EmptyDirectory(PathBuf), + + /// Invalid SVG format. + #[error("Invalid SVG format in {0}: {1}")] + InvalidSvgFormat(PathBuf, String), + + /// Directory validation failed. + #[error("Directory validation failed for {0}: {1}")] + DirectoryValidationFailed(PathBuf, String), } diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index 62e50d6ed..60100d2fe 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -71,27 +71,271 @@ impl SpriteSources { /// Adds a sprite source directory containing SVG files. /// Files are ignored - only directories accepted. Duplicates ignored with warning. + /// Performs basic validation of SVG format before adding the source. pub fn add_source(&mut self, id: String, path: PathBuf) { let disp_path = path.display(); + if path.is_file() { warn!("Ignoring non-directory sprite source {id} from {disp_path}"); - } else { - match self.0.entry(id) { - Entry::Occupied(v) => { + return; + } + if !path.exists() { + warn!("Sprite source {id} path doesn't exist: {disp_path}"); + return; + } + + match std::fs::read_dir(&path) { + Ok(entries) => { + let mut file_count = 0; + let mut svg_count = 0; + let mut sprite_output_files = Vec::new(); + + for entry in entries { + if let Ok(entry) = entry { + let entry_path = entry.path(); + if entry_path.is_file() { + file_count += 1; + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "svg" { + svg_count += 1; + } else if ext == "png" || ext == "json" { + let filename = entry_path.file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if filename.contains("sprite") || filename.contains("@2x") { + sprite_output_files.push(entry_path.file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default()); + } + } + } + } + } + } + + if file_count == 0 { + warn!("Sprite source {id} directory is empty: {disp_path}"); + } else if svg_count == 0 && !sprite_output_files.is_empty() { warn!( - "Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", - v.key(), - v.get().path.display() + "Sprite source {id} contains files ({}) but no valid SVG sources: {disp_path}. \ + Martin requires source SVG files.", + sprite_output_files.join(", ") ); + } else if svg_count == 0 { + warn!("Sprite source {id} contains no SVG files: {disp_path}"); } - Entry::Vacant(v) => { - info!("Configured sprite source {} from {disp_path}", v.key()); - v.insert(SpriteSource { path }); + } + Err(e) => { + warn!("Cannot read sprite source {id} directory {disp_path}: {e}"); + return; + } + } + + match self.0.entry(id) { + Entry::Occupied(v) => { + warn!( + "Ignoring duplicate sprite source {} from {disp_path}: Already configured from {}", + v.key(), + v.get().path.display() + ); + } + Entry::Vacant(v) => { + info!("Configured sprite source {} from {disp_path}", v.key()); + v.insert(SpriteSource { path }); + } + } + } + + /// Validates a sprite source directory to ensure it contains valid SVG files. + /// Checks include: + /// - Directory existence and accessibility + /// - Presence of SVG files + /// - Basic SVG format validation + pub async fn validate_source_directory(&self, path: &PathBuf) -> Result<(), SpriteError> { + let disp_path = path.display(); + let on_err = |e| SpriteError::IoError(e, path.clone()); + + // Check if path exists and get metadata + let metadata = tokio::fs::metadata(path).await.map_err(on_err)?; + + if !metadata.is_dir() { + warn!("Sprite source is not a directory: {disp_path}"); + return Err(SpriteError::DirectoryValidationFailed( + path.clone(), + "Path is not a directory".to_string() + )); + } + + // Check directory permissions by trying to read it + let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; + let mut svg_count = 0; + let mut total_files = 0; + let mut sprite_output_files = Vec::new(); // Track pre-generated sprite files + + while let Some(entry) = entries.next_entry().await.map_err(on_err)? { + let entry_path = entry.path(); + total_files += 1; + + if entry_path.is_file() { + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "svg" { + svg_count += 1; + // Validate individual SVG file + if let Err(e) = self.validate_svg_file(&entry_path).await { + warn!("Invalid SVG file {}: {}", entry_path.display(), e); + } + } else if ext == "png" || ext == "json" { + let filename = entry_path.file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if filename.contains("sprite") || filename.contains("@2x") { + sprite_output_files.push(entry_path.file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default()); + } + } } } } + + if total_files == 0 { + warn!("Empty sprite directory: {disp_path}"); + return Err(SpriteError::EmptyDirectory(path.clone())); + } + + if svg_count == 0 { + if !sprite_output_files.is_empty() { + warn!( + "No SVG source files found in sprite directory: {disp_path}. \ + Found pre-generated sprite files: {}. \ + Martin requires source SVG files, not pre-generated sprite outputs.", + sprite_output_files.join(", ") + ); + return Err(SpriteError::DirectoryValidationFailed( + path.clone(), + format!( + "Directory contains pre-generated sprite files ({}) but no source SVG files. \ + Please provide a directory with .svg files instead.", + sprite_output_files.join(", ") + ) + )); + } else { + warn!("No SVG files found in sprite directory: {disp_path}. Please ensure the directory contains .svg files."); + return Err(SpriteError::NoSpriteFilesFound(path.clone())); + } + } + + info!("Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files"); + Ok(()) } + /// Validates an individual SVG file for format. + async fn validate_svg_file(&self, path: &PathBuf) -> Result<(), SpriteError> { + let on_err = |e| SpriteError::IoError(e, path.clone()); + + let content = tokio::fs::read_to_string(path).await.map_err(on_err)?; + let content = content.trim(); + + if content.is_empty() { + return Err(SpriteError::InvalidSvgFormat( + path.clone(), + "File is empty".to_string() + )); + } + + if !content.starts_with(" Result<(), SpriteError> { + if self.0.is_empty() { + info!("No sprite sources configured"); + return Ok(()); + } + + let mut total_sources = 0; + let mut valid_sources = 0; + let mut total_svg_files = 0; + let mut validation_errors = Vec::new(); + + info!("Validating {} configured sprite sources...", self.0.len()); + + for source in &self.0 { + total_sources += 1; + let id = source.key(); + let path = &source.value().path; + + match self.validate_source_directory(path).await { + Ok(()) => { + valid_sources += 1; + if let Ok(svg_count) = count_svg_files(path).await { + total_svg_files += svg_count; + } + } + Err(e) => { + warn!("Validation failed for sprite source {id}: {e}"); + validation_errors.push((id.clone(), e)); + } + } + } + + if validation_errors.is_empty() { + info!( + "Sprite source validation completed successfully: {}/{} sources valid, {} total SVG files", + valid_sources, total_sources, total_svg_files + ); + } else { + warn!( + "Sprite source validation completed with errors: {}/{} sources valid, {} errors encountered", + valid_sources, total_sources, validation_errors.len() + ); + for (id, error) in &validation_errors { + warn!(" - {}: {}", id, error); + } + } + + Ok(()) + } +} + +/// Helper function to count SVG files in a directory. +async fn count_svg_files(path: &PathBuf) -> Result { + let on_err = |e| SpriteError::IoError(e, path.clone()); + let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; + let mut count = 0; + + while let Some(entry) = entries.next_entry().await.map_err(on_err)? { + let entry_path = entry.path(); + if entry_path.is_file() { + if let Some(extension) = entry_path.extension() { + if extension.to_string_lossy().to_lowercase() == "svg" { + count += 1; + } + } + } + } + + Ok(count) +} + +impl SpriteSources { /// Generates a spritesheet from comma-separated sprite source IDs. /// /// Append "@2x" for high-DPI sprites. From 7cff982f6138630b70f1577655b191ca606ef951 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:56:04 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- martin-core/src/resources/sprites/mod.rs | 48 +++++++++++++++--------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index 60100d2fe..d9799f62d 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -100,13 +100,17 @@ impl SpriteSources { if ext == "svg" { svg_count += 1; } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() + let filename = entry_path + .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(); if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); + sprite_output_files.push( + entry_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + ); } } } @@ -146,7 +150,7 @@ impl SpriteSources { } } } - + /// Validates a sprite source directory to ensure it contains valid SVG files. /// Checks include: /// - Directory existence and accessibility @@ -163,7 +167,7 @@ impl SpriteSources { warn!("Sprite source is not a directory: {disp_path}"); return Err(SpriteError::DirectoryValidationFailed( path.clone(), - "Path is not a directory".to_string() + "Path is not a directory".to_string(), )); } @@ -187,13 +191,17 @@ impl SpriteSources { warn!("Invalid SVG file {}: {}", entry_path.display(), e); } } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() + let filename = entry_path + .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(); if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); + sprite_output_files.push( + entry_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + ); } } } @@ -219,15 +227,19 @@ impl SpriteSources { "Directory contains pre-generated sprite files ({}) but no source SVG files. \ Please provide a directory with .svg files instead.", sprite_output_files.join(", ") - ) + ), )); } else { - warn!("No SVG files found in sprite directory: {disp_path}. Please ensure the directory contains .svg files."); + warn!( + "No SVG files found in sprite directory: {disp_path}. Please ensure the directory contains .svg files." + ); return Err(SpriteError::NoSpriteFilesFound(path.clone())); } } - info!("Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files"); + info!( + "Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files" + ); Ok(()) } @@ -241,14 +253,14 @@ impl SpriteSources { if content.is_empty() { return Err(SpriteError::InvalidSvgFormat( path.clone(), - "File is empty".to_string() + "File is empty".to_string(), )); } if !content.starts_with(" Date: Fri, 19 Sep 2025 12:14:37 -0600 Subject: [PATCH 3/9] chore: clippy + fmt --- martin-core/src/resources/sprites/mod.rs | 100 ++++++++++++----------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index 60100d2fe..187ba8c27 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -90,24 +90,26 @@ impl SpriteSources { let mut svg_count = 0; let mut sprite_output_files = Vec::new(); - for entry in entries { - if let Ok(entry) = entry { - let entry_path = entry.path(); - if entry_path.is_file() { - file_count += 1; - if let Some(extension) = entry_path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - if ext == "svg" { - svg_count += 1; - } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default(); - if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() + for entry in entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_file() { + file_count += 1; + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "svg" { + svg_count += 1; + } else if ext == "png" || ext == "json" { + let filename = entry_path + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if filename.contains("sprite") || filename.contains("@2x") { + sprite_output_files.push( + entry_path + .file_name() .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); - } + .unwrap_or_default(), + ); } } } @@ -146,7 +148,7 @@ impl SpriteSources { } } } - + /// Validates a sprite source directory to ensure it contains valid SVG files. /// Checks include: /// - Directory existence and accessibility @@ -163,7 +165,7 @@ impl SpriteSources { warn!("Sprite source is not a directory: {disp_path}"); return Err(SpriteError::DirectoryValidationFailed( path.clone(), - "Path is not a directory".to_string() + "Path is not a directory".to_string(), )); } @@ -187,13 +189,17 @@ impl SpriteSources { warn!("Invalid SVG file {}: {}", entry_path.display(), e); } } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() + let filename = entry_path + .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(); if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); + sprite_output_files.push( + entry_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + ); } } } @@ -205,29 +211,26 @@ impl SpriteSources { return Err(SpriteError::EmptyDirectory(path.clone())); } - if svg_count == 0 { - if !sprite_output_files.is_empty() { - warn!( - "No SVG source files found in sprite directory: {disp_path}. \ + if svg_count == 0 && sprite_output_files.is_empty() { + warn!( + "No SVG source files found in sprite directory: {disp_path}. \ Found pre-generated sprite files: {}. \ Martin requires source SVG files, not pre-generated sprite outputs.", - sprite_output_files.join(", ") - ); - return Err(SpriteError::DirectoryValidationFailed( - path.clone(), - format!( - "Directory contains pre-generated sprite files ({}) but no source SVG files. \ + sprite_output_files.join(", ") + ); + return Err(SpriteError::DirectoryValidationFailed( + path.clone(), + format!( + "Directory contains pre-generated sprite files ({}) but no source SVG files. \ Please provide a directory with .svg files instead.", - sprite_output_files.join(", ") - ) - )); - } else { - warn!("No SVG files found in sprite directory: {disp_path}. Please ensure the directory contains .svg files."); - return Err(SpriteError::NoSpriteFilesFound(path.clone())); - } + sprite_output_files.join(", ") + ), + )); } - info!("Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files"); + info!( + "Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files" + ); Ok(()) } @@ -241,14 +244,14 @@ impl SpriteSources { if content.is_empty() { return Err(SpriteError::InvalidSvgFormat( path.clone(), - "File is empty".to_string() + "File is empty".to_string(), )); } if !content.starts_with(" Date: Fri, 19 Sep 2025 18:23:59 +0000 Subject: [PATCH 4/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- martin-core/src/resources/sprites/mod.rs | 89 +++++++++++++----------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index eda36b87f..187ba8c27 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -91,27 +91,30 @@ impl SpriteSources { let mut sprite_output_files = Vec::new(); for entry in entries.flatten() { - let entry_path = entry.path(); - if entry_path.is_file() { - file_count += 1; - if let Some(extension) = entry_path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - if ext == "svg" { - svg_count += 1; - } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default(); - if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() + let entry_path = entry.path(); + if entry_path.is_file() { + file_count += 1; + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "svg" { + svg_count += 1; + } else if ext == "png" || ext == "json" { + let filename = entry_path + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if filename.contains("sprite") || filename.contains("@2x") { + sprite_output_files.push( + entry_path + .file_name() .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); - } + .unwrap_or_default(), + ); } } } } - + } if file_count == 0 { warn!("Sprite source {id} directory is empty: {disp_path}"); @@ -145,7 +148,7 @@ impl SpriteSources { } } } - + /// Validates a sprite source directory to ensure it contains valid SVG files. /// Checks include: /// - Directory existence and accessibility @@ -162,7 +165,7 @@ impl SpriteSources { warn!("Sprite source is not a directory: {disp_path}"); return Err(SpriteError::DirectoryValidationFailed( path.clone(), - "Path is not a directory".to_string() + "Path is not a directory".to_string(), )); } @@ -186,13 +189,17 @@ impl SpriteSources { warn!("Invalid SVG file {}: {}", entry_path.display(), e); } } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() + let filename = entry_path + .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(); if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); + sprite_output_files.push( + entry_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + ); } } } @@ -204,26 +211,26 @@ impl SpriteSources { return Err(SpriteError::EmptyDirectory(path.clone())); } - if svg_count == 0 && - sprite_output_files.is_empty() { - warn!( - "No SVG source files found in sprite directory: {disp_path}. \ + if svg_count == 0 && sprite_output_files.is_empty() { + warn!( + "No SVG source files found in sprite directory: {disp_path}. \ Found pre-generated sprite files: {}. \ Martin requires source SVG files, not pre-generated sprite outputs.", - sprite_output_files.join(", ") - ); - return Err(SpriteError::DirectoryValidationFailed( - path.clone(), - format!( - "Directory contains pre-generated sprite files ({}) but no source SVG files. \ + sprite_output_files.join(", ") + ); + return Err(SpriteError::DirectoryValidationFailed( + path.clone(), + format!( + "Directory contains pre-generated sprite files ({}) but no source SVG files. \ Please provide a directory with .svg files instead.", - sprite_output_files.join(", ") - ) - )); - } - + sprite_output_files.join(", ") + ), + )); + } - info!("Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files"); + info!( + "Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files" + ); Ok(()) } @@ -237,14 +244,14 @@ impl SpriteSources { if content.is_empty() { return Err(SpriteError::InvalidSvgFormat( path.clone(), - "File is empty".to_string() + "File is empty".to_string(), )); } if !content.starts_with(" Date: Sun, 21 Sep 2025 09:42:27 -0600 Subject: [PATCH 5/9] feat: move sprite validation + unclutter --- martin-core/src/resources/sprites/mod.rs | 182 ++++++++++------------- 1 file changed, 76 insertions(+), 106 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index eda36b87f..6e3352eb6 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -72,63 +72,23 @@ impl SpriteSources { /// Adds a sprite source directory containing SVG files. /// Files are ignored - only directories accepted. Duplicates ignored with warning. /// Performs basic validation of SVG format before adding the source. - pub fn add_source(&mut self, id: String, path: PathBuf) { + pub fn add_source(&mut self, id: String, path: PathBuf) -> Result<(), SpriteError> { let disp_path = path.display(); if path.is_file() { warn!("Ignoring non-directory sprite source {id} from {disp_path}"); - return; + return Err(SpriteError::DirectoryValidationFailed( + path, + "Path is not a directory".to_string(), + )); } + if !path.exists() { warn!("Sprite source {id} path doesn't exist: {disp_path}"); - return; - } - - match std::fs::read_dir(&path) { - Ok(entries) => { - let mut file_count = 0; - let mut svg_count = 0; - let mut sprite_output_files = Vec::new(); - - for entry in entries.flatten() { - let entry_path = entry.path(); - if entry_path.is_file() { - file_count += 1; - if let Some(extension) = entry_path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - if ext == "svg" { - svg_count += 1; - } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default(); - if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); - } - } - } - } - } - - - if file_count == 0 { - warn!("Sprite source {id} directory is empty: {disp_path}"); - } else if svg_count == 0 && !sprite_output_files.is_empty() { - warn!( - "Sprite source {id} contains files ({}) but no valid SVG sources: {disp_path}. \ - Martin requires source SVG files.", - sprite_output_files.join(", ") - ); - } else if svg_count == 0 { - warn!("Sprite source {id} contains no SVG files: {disp_path}"); - } - } - Err(e) => { - warn!("Cannot read sprite source {id} directory {disp_path}: {e}"); - return; - } + return Err(SpriteError::DirectoryValidationFailed( + path, + "Path does not exist".to_string(), + )); } match self.0.entry(id) { @@ -144,8 +104,10 @@ impl SpriteSources { v.insert(SpriteSource { path }); } } + + Ok(()) } - + /// Validates a sprite source directory to ensure it contains valid SVG files. /// Checks include: /// - Directory existence and accessibility @@ -162,38 +124,22 @@ impl SpriteSources { warn!("Sprite source is not a directory: {disp_path}"); return Err(SpriteError::DirectoryValidationFailed( path.clone(), - "Path is not a directory".to_string() + "Path is not a directory".to_string(), )); } - // Check directory permissions by trying to read it - let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; - let mut svg_count = 0; - let mut total_files = 0; - let mut sprite_output_files = Vec::new(); // Track pre-generated sprite files + let (total_files, svg_count, sprite_output_files) = + Self::scan_directory_files(path).await?; + let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; while let Some(entry) = entries.next_entry().await.map_err(on_err)? { let entry_path = entry.path(); - total_files += 1; - if entry_path.is_file() { if let Some(extension) = entry_path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - if ext == "svg" { - svg_count += 1; - // Validate individual SVG file + if extension.to_string_lossy().to_lowercase() == "svg" { if let Err(e) = self.validate_svg_file(&entry_path).await { warn!("Invalid SVG file {}: {}", entry_path.display(), e); } - } else if ext == "png" || ext == "json" { - let filename = entry_path.file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default(); - if filename.contains("sprite") || filename.contains("@2x") { - sprite_output_files.push(entry_path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default()); - } } } } @@ -204,26 +150,26 @@ impl SpriteSources { return Err(SpriteError::EmptyDirectory(path.clone())); } - if svg_count == 0 && - sprite_output_files.is_empty() { - warn!( - "No SVG source files found in sprite directory: {disp_path}. \ + if svg_count == 0 && sprite_output_files.is_empty() { + warn!( + "No SVG source files found in sprite directory: {disp_path}. \ Found pre-generated sprite files: {}. \ Martin requires source SVG files, not pre-generated sprite outputs.", - sprite_output_files.join(", ") - ); - return Err(SpriteError::DirectoryValidationFailed( - path.clone(), - format!( - "Directory contains pre-generated sprite files ({}) but no source SVG files. \ + sprite_output_files.join(", ") + ); + return Err(SpriteError::DirectoryValidationFailed( + path.clone(), + format!( + "Directory contains pre-generated sprite files ({}) but no source SVG files. \ Please provide a directory with .svg files instead.", - sprite_output_files.join(", ") - ) - )); - } - + sprite_output_files.join(", ") + ), + )); + } - info!("Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files"); + info!( + "Validated sprite directory {disp_path}: found {svg_count} SVG files out of {total_files} total files" + ); Ok(()) } @@ -237,14 +183,14 @@ impl SpriteSources { if content.is_empty() { return Err(SpriteError::InvalidSvgFormat( path.clone(), - "File is empty".to_string() + "File is empty".to_string(), )); } if !content.starts_with(" { valid_sources += 1; - if let Ok(svg_count) = count_svg_files(path).await { + if let Ok(svg_count) = Self::count_svg_files(path).await { total_svg_files += svg_count; } } @@ -308,26 +254,50 @@ impl SpriteSources { Ok(()) } -} -/// Helper function to count SVG files in a directory. -async fn count_svg_files(path: &PathBuf) -> Result { - let on_err = |e| SpriteError::IoError(e, path.clone()); - let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; - let mut count = 0; - - while let Some(entry) = entries.next_entry().await.map_err(on_err)? { - let entry_path = entry.path(); - if entry_path.is_file() { - if let Some(extension) = entry_path.extension() { - if extension.to_string_lossy().to_lowercase() == "svg" { - count += 1; + /// Scans a directory and returns file counts. + async fn scan_directory_files( + path: &PathBuf, + ) -> Result<(usize, usize, Vec), SpriteError> { + let on_err = |e| SpriteError::IoError(e, path.clone()); + let mut entries = tokio::fs::read_dir(path).await.map_err(on_err)?; + let mut total_files = 0; + let mut svg_count = 0; + let mut sprite_output_files = Vec::new(); + + while let Some(entry) = entries.next_entry().await.map_err(on_err)? { + let entry_path = entry.path(); + if entry_path.is_file() { + total_files += 1; + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "svg" { + svg_count += 1; + } else if ext == "png" || ext == "json" { + let filename = entry_path + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if filename.contains("sprite") || filename.contains("@2x") { + sprite_output_files.push( + entry_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + ); + } + } } } } + + Ok((total_files, svg_count, sprite_output_files)) } - Ok(count) + async fn count_svg_files(path: &PathBuf) -> Result { + let (_, svg_count, _) = Self::scan_directory_files(path).await?; + Ok(svg_count) + } } impl SpriteSources { From 5519fb3ddc2440e9929241c3b1c40a1d59ba9ab3 Mon Sep 17 00:00:00 2001 From: nuts_rice Date: Mon, 22 Sep 2025 09:38:19 -0600 Subject: [PATCH 6/9] chore: clippy --- martin-core/src/resources/sprites/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index 2365d39e2..ea19d2c75 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -134,9 +134,7 @@ impl SpriteSources { if entry_path.is_file() { if let Some(extension) = entry_path.extension() { if extension.to_string_lossy().to_lowercase() == "svg" { - if let Err(e) = self.validate_svg_file(&entry_path).await { - return Err(e); - } + self.validate_svg_file(&entry_path).await?; } } } @@ -331,11 +329,11 @@ mod tests { #[tokio::test] async fn test_sprites() { let mut sprites = SpriteSources::default(); - sprites.add_source( + let _ = sprites.add_source( "src1".to_string(), PathBuf::from("../tests/fixtures/sprites/src1"), ); - sprites.add_source( + let _ = sprites.add_source( "src2".to_string(), PathBuf::from("../tests/fixtures/sprites/src2"), ); From abb1e01dc91b6f1703eabacb007048495f45cfec Mon Sep 17 00:00:00 2001 From: nuts_rice Date: Mon, 22 Sep 2025 11:30:28 -0600 Subject: [PATCH 7/9] feat: add sprite validation dedicated errors --- martin-core/src/resources/sprites/error.rs | 12 ++++++++++++ martin-core/src/resources/sprites/mod.rs | 22 +++++----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/martin-core/src/resources/sprites/error.rs b/martin-core/src/resources/sprites/error.rs index 00d9b4c28..69caf32be 100644 --- a/martin-core/src/resources/sprites/error.rs +++ b/martin-core/src/resources/sprites/error.rs @@ -56,6 +56,18 @@ pub enum SpriteError { #[error("Invalid SVG format in {0}: {1}")] InvalidSvgFormat(PathBuf, String), + /// File is empty. + #[error("File is empty: {0}")] + EmptyFile(PathBuf), + + /// Directory does not exist. + #[error("Directory does not exist: {0}")] + DirectoryNotFound(PathBuf), + + /// Path is not a directory. + #[error("Path is not a directory: {0}")] + NotADirectory(PathBuf), + /// Directory validation failed. #[error("Directory validation failed for {0}: {1}")] DirectoryValidationFailed(PathBuf, String), diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index ea19d2c75..76fece929 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -76,17 +76,11 @@ impl SpriteSources { let disp_path = path.display(); if path.is_file() { - return Err(SpriteError::DirectoryValidationFailed( - path, - "Path is not a directory".to_string(), - )); + return Err(SpriteError::NotADirectory(path)); } if !path.exists() { - return Err(SpriteError::DirectoryValidationFailed( - path, - "Path does not exist".to_string(), - )); + return Err(SpriteError::DirectoryNotFound(path)); } match self.0.entry(id) { @@ -119,10 +113,7 @@ impl SpriteSources { let metadata = tokio::fs::metadata(path).await.map_err(on_err)?; if !metadata.is_dir() { - return Err(SpriteError::DirectoryValidationFailed( - path.clone(), - "Path is not a directory".to_string(), - )); + return Err(SpriteError::NotADirectory(path.clone())); } let (total_files, svg_count, sprite_output_files) = @@ -165,16 +156,13 @@ impl SpriteSources { let content = content.trim(); if content.is_empty() { - return Err(SpriteError::InvalidSvgFormat( - path.clone(), - "File is empty".to_string(), - )); + return Err(SpriteError::EmptyFile(path.clone())); } if !content.starts_with(" Date: Wed, 24 Sep 2025 09:45:09 -0600 Subject: [PATCH 8/9] tests: add sprite validation tests --- martin-core/src/resources/sprites/mod.rs | 56 ++++++++++++++++-- tests/fixtures/sprites/notsrc2/ferris.png | Bin 0 -> 61362 bytes tests/fixtures/sprites/notsrc2/notasprite.txt | 0 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/sprites/notsrc2/ferris.png create mode 100644 tests/fixtures/sprites/notsrc2/notasprite.txt diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index 76fece929..f1fdf6b2b 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -317,14 +317,14 @@ mod tests { #[tokio::test] async fn test_sprites() { let mut sprites = SpriteSources::default(); - let _ = sprites.add_source( + sprites.add_source( "src1".to_string(), PathBuf::from("../tests/fixtures/sprites/src1"), - ); - let _ = sprites.add_source( + ).unwrap(); + sprites.add_source( "src2".to_string(), PathBuf::from("../tests/fixtures/sprites/src2"), - ); + ).unwrap(); assert_eq!(sprites.0.len(), 2); @@ -366,3 +366,51 @@ mod tests { insta::assert_binary_snapshot!(&format!("{filename}.png"), png); } } + +#[tokio::test] +async fn test_directory_not_found() { + let mut sprites = SpriteSources::default(); + let result = sprites.add_source("nothere".to_string(), PathBuf::from("/path/to/nowhere")); + assert!(matches!(result, Err(SpriteError::DirectoryNotFound(..)))); +} + +#[tokio::test] +async fn test_not_a_directory() { + let mut sprites = SpriteSources::default(); + let result = sprites.add_source( + "notadir".to_string(), + PathBuf::from("../tests/fixtures/sprites/notsrc2/ferris.png"), + ); + assert!(matches!(result, Err(SpriteError::NotADirectory(..)))); +} + +#[tokio::test] +async fn test_empty_directory() { + let sprites = SpriteSources::default(); + let result = sprites + .validate_source_directory(&PathBuf::from("../tests/fixtures/sprites/notsrc1")) + .await; + assert!(matches!(result, Err(SpriteError::EmptyDirectory(..)))); +} + +#[tokio::test] +async fn test_sprite_source_scan() { + use crate::sprites::SpriteSources; + let result = + SpriteSources::scan_directory_files(&PathBuf::from("../tests/fixtures/sprites/notsrc2")) + .await; + assert_eq!(result.as_ref().unwrap().0, 2); + assert_eq!(result.unwrap().1, 0); +} + +#[tokio::test] +async fn test_empty_file() { + use crate::sprites::SpriteSources; + let sprites = SpriteSources::default(); + let result = SpriteSources::validate_svg_file( + &sprites, + &PathBuf::from("../tests/fixtures/sprites/notsrc2/notasprite.txt"), + ) + .await; + assert!(matches!(result, Err(SpriteError::EmptyFile(..)))); +} diff --git a/tests/fixtures/sprites/notsrc2/ferris.png b/tests/fixtures/sprites/notsrc2/ferris.png new file mode 100644 index 0000000000000000000000000000000000000000..9a39db49f8d71467430953cd977e830964e2f221 GIT binary patch literal 61362 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 Tsflp#JPdgmW$6-0qk#Va_>HUB literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/notsrc2/notasprite.txt b/tests/fixtures/sprites/notsrc2/notasprite.txt new file mode 100644 index 000000000..e69de29bb From 53cb0d8e4bef1dbe6bd88d1e07b7b94313c56fba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:26:15 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- martin-core/src/resources/sprites/mod.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/martin-core/src/resources/sprites/mod.rs b/martin-core/src/resources/sprites/mod.rs index f1fdf6b2b..0f85df0e9 100644 --- a/martin-core/src/resources/sprites/mod.rs +++ b/martin-core/src/resources/sprites/mod.rs @@ -317,14 +317,18 @@ mod tests { #[tokio::test] async fn test_sprites() { let mut sprites = SpriteSources::default(); - sprites.add_source( - "src1".to_string(), - PathBuf::from("../tests/fixtures/sprites/src1"), - ).unwrap(); - sprites.add_source( - "src2".to_string(), - PathBuf::from("../tests/fixtures/sprites/src2"), - ).unwrap(); + sprites + .add_source( + "src1".to_string(), + PathBuf::from("../tests/fixtures/sprites/src1"), + ) + .unwrap(); + sprites + .add_source( + "src2".to_string(), + PathBuf::from("../tests/fixtures/sprites/src2"), + ) + .unwrap(); assert_eq!(sprites.0.len(), 2);