-
-
Notifications
You must be signed in to change notification settings - Fork 298
Bad sprite dir warnings and svg validation #2211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
08c3c0c
7cff982
8f339a1
0b46368
ef0fe33
3db4484
1631a89
5519fb3
abb1e01
af5b91d
53cb0d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,27 +71,153 @@ impl SpriteSources { | |
|
|
||
| /// Adds a sprite source directory containing SVG files. | ||
| /// Files are ignored - only directories accepted. Duplicates ignored with warning. | ||
| pub fn add_source(&mut self, id: String, path: PathBuf) { | ||
| /// Performs basic validation of SVG format before adding the source. | ||
| 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}"); | ||
| } else { | ||
| match self.0.entry(id) { | ||
| Entry::Occupied(v) => { | ||
| warn!( | ||
| "Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", | ||
| v.key(), | ||
| v.get().path.display() | ||
| ); | ||
| return Err(SpriteError::NotADirectory(path)); | ||
| } | ||
|
|
||
| if !path.exists() { | ||
| return Err(SpriteError::DirectoryNotFound(path)); | ||
| } | ||
|
|
||
| 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 }); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// 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> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not add dead code. |
||
| 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() { | ||
| return Err(SpriteError::NotADirectory(path.clone())); | ||
| } | ||
|
|
||
| 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(); | ||
| if entry_path.is_file() { | ||
| if let Some(extension) = entry_path.extension() { | ||
| if extension.to_string_lossy().to_lowercase() == "svg" { | ||
| self.validate_svg_file(&entry_path).await?; | ||
| } | ||
| } | ||
| Entry::Vacant(v) => { | ||
| info!("Configured sprite source {} from {disp_path}", v.key()); | ||
| v.insert(SpriteSource { path }); | ||
| } | ||
| } | ||
|
|
||
| if total_files == 0 { | ||
| return Err(SpriteError::EmptyDirectory(path.clone())); | ||
| } | ||
|
|
||
| if svg_count == 0 && sprite_output_files.is_empty() { | ||
| return Err(SpriteError::DirectoryValidationFailed( | ||
| path.clone(), | ||
| "Directory contains no SVG files".to_string(), | ||
| )); | ||
| } | ||
|
|
||
| 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::EmptyFile(path.clone())); | ||
| } | ||
|
|
||
| if !content.starts_with("<?xml") && !content.starts_with("<svg") { | ||
| return Err(SpriteError::InvalidSvgFormat( | ||
| path.clone(), | ||
| "Missing SVG declaration".to_string(), | ||
| )); | ||
| } | ||
|
|
||
| // Check if it contains an SVG tag | ||
| if !content.contains("<svg") { | ||
| return Err(SpriteError::InvalidSvgFormat( | ||
| path.clone(), | ||
| "No SVG element found".to_string(), | ||
| )); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Scans a directory and returns file counts. | ||
| async fn scan_directory_files( | ||
| path: &PathBuf, | ||
| ) -> Result<(usize, usize, Vec<String>), 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)) | ||
| } | ||
| } | ||
|
|
||
| impl SpriteSources { | ||
| /// Generates a spritesheet from comma-separated sprite source IDs. | ||
| /// | ||
| /// Append "@2x" for high-DPI sprites. | ||
|
|
@@ -191,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"), | ||
| ); | ||
| sprites.add_source( | ||
| "src2".to_string(), | ||
| PathBuf::from("../tests/fixtures/sprites/src2"), | ||
| ); | ||
| 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); | ||
|
|
||
|
|
@@ -240,3 +370,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(..)))); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter should be
path: &Pathinstead ofpath: &PathBuf. Taking a reference to PathBuf is unnecessary since Path is the borrowed version that should be used in function parameters.