diff --git a/src/application/services/folder_service.rs b/src/application/services/folder_service.rs index f6fbf1e9..dd84e52f 100644 --- a/src/application/services/folder_service.rs +++ b/src/application/services/folder_service.rs @@ -4,7 +4,7 @@ use crate::application::dtos::folder_dto::{ use crate::application::ports::inbound::FolderUseCase; use crate::common::errors::{DomainError, ErrorKind}; use crate::domain::repositories::folder_repository::FolderRepository; -use crate::domain::services::path_service::StoragePath; +use crate::domain::services::path_service::{StoragePath, validate_storage_name}; use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository; use std::sync::Arc; use uuid::Uuid; @@ -136,12 +136,11 @@ impl FolderUseCase for FolderService { /// Creates a new folder async fn create_folder(&self, dto: CreateFolderDto) -> Result { // Input validation - if dto.name.is_empty() { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Folder", - "Folder name cannot be empty", - )); + if let Err(reason) = validate_storage_name(&dto.name) { + return Err(DomainError::validation_error(format!( + "Invalid folder name '{}': {reason}", + dto.name + ))); } // If a parent_id is provided, verify it exists @@ -397,12 +396,11 @@ impl FolderUseCase for FolderService { caller_id: Uuid, ) -> Result { // Input validation - if dto.name.is_empty() { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Folder", - "New folder name cannot be empty", - )); + if let Err(reason) = validate_storage_name(&dto.name) { + return Err(DomainError::validation_error(format!( + "Invalid folder name '{}': {reason}", + dto.name + ))); } // Verify the folder exists and belongs to the caller diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index de54d0e6..57728d4d 100644 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -1,6 +1,6 @@ use uuid::Uuid; -use crate::domain::services::path_service::StoragePath; +use crate::domain::services::path_service::{StoragePath, validate_storage_name}; // Re-export entity errors from the centralized module pub use super::entity_errors::{FileError, FileResult}; @@ -100,9 +100,8 @@ impl File { mime_type: String, folder_id: Option, ) -> FileResult { - // Validate file name - if name.is_empty() || name.contains('/') || name.contains('\\') { - return Err(FileError::InvalidFileName(name)); + if let Err(reason) = validate_storage_name(&name) { + return Err(FileError::InvalidFileName(format!("{name}: {reason}"))); } let now = std::time::SystemTime::now() @@ -137,9 +136,8 @@ impl File { created_at: u64, modified_at: u64, ) -> FileResult { - // Validate folder name - if name.is_empty() || name.contains('/') || name.contains('\\') { - return Err(FileError::InvalidFileName(name)); + if let Err(reason) = validate_storage_name(&name) { + return Err(FileError::InvalidFileName(format!("{name}: {reason}"))); } // Store the path string for serialization compatibility @@ -199,9 +197,8 @@ impl File { owner_id: Option, etag: String, ) -> FileResult { - // Validate file name - if name.is_empty() || name.contains('/') || name.contains('\\') { - return Err(FileError::InvalidFileName(name)); + if let Err(reason) = validate_storage_name(&name) { + return Err(FileError::InvalidFileName(format!("{name}: {reason}"))); } // Store the path string for serialization compatibility @@ -321,9 +318,8 @@ impl File { /// Creates a new version of the file with updated name pub fn with_name(&self, new_name: String) -> FileResult { - // Validate file name - if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { - return Err(FileError::InvalidFileName(new_name)); + if let Err(reason) = validate_storage_name(&new_name) { + return Err(FileError::InvalidFileName(format!("{new_name}: {reason}"))); } // Update path based on name diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index fdf82bb4..d4ebe33c 100644 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -1,6 +1,6 @@ use uuid::Uuid; -use crate::domain::services::path_service::StoragePath; +use crate::domain::services::path_service::{StoragePath, validate_storage_name}; // Re-export entity errors from the centralized module pub use super::entity_errors::{FolderError, FolderResult}; @@ -71,8 +71,8 @@ impl Folder { owner_id: Option, ) -> FolderResult { // Validate folder name - if name.is_empty() || name.contains('/') || name.contains('\\') { - return Err(FolderError::InvalidFolderName(name)); + if let Err(reason) = validate_storage_name(&name) { + return Err(FolderError::InvalidFolderName(format!("{name}: {reason}"))); } let now = std::time::SystemTime::now() @@ -126,8 +126,8 @@ impl Folder { modified_at: u64, ) -> FolderResult { // Validate folder name - if name.is_empty() || name.contains('/') || name.contains('\\') { - return Err(FolderError::InvalidFolderName(name)); + if let Err(reason) = validate_storage_name(&name) { + return Err(FolderError::InvalidFolderName(format!("{name}: {reason}"))); } // Store the path string for serialization compatibility @@ -208,9 +208,10 @@ impl Folder { /// Creates a new version of the folder with updated name pub fn with_name(&self, new_name: String) -> FolderResult { - // Validate folder name - if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { - return Err(FolderError::InvalidFolderName(new_name)); + if let Err(reason) = validate_storage_name(&new_name) { + return Err(FolderError::InvalidFolderName(format!( + "{new_name}: {reason}" + ))); } // Update path based on the name diff --git a/src/domain/services/path_service.rs b/src/domain/services/path_service.rs index f3f54190..5c147ba7 100644 --- a/src/domain/services/path_service.rs +++ b/src/domain/services/path_service.rs @@ -6,6 +6,26 @@ use std::path::PathBuf; +/// Validates a single file or folder name component. +/// +/// Returns `Err` with a human-readable reason if the name is rejected. +/// Callers should wrap the reason into their own error type. +pub fn validate_storage_name(name: &str) -> Result<(), &'static str> { + if name.is_empty() { + return Err("name cannot be empty"); + } + if name.contains('/') || name.contains('\\') { + return Err("name must not contain '/' or '\\'"); + } + if name.contains('\0') { + return Err("name must not contain null bytes"); + } + if name == "." || name == ".." { + return Err("'.' and '..' are not valid names"); + } + Ok(()) +} + /// Represents a storage path in the domain (Value Object) #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct StoragePath {