Merge pull request #353 from EdouardVanbelle/fix/sanity_check_files_and_folder_names

fix(storage names): enure folder and file names are not using reserved chars
This commit is contained in:
Dionisio Pozo
2026-05-08 01:21:05 +02:00
committed by GitHub
4 changed files with 49 additions and 34 deletions
+11 -13
View File
@@ -4,7 +4,7 @@ use crate::application::dtos::folder_dto::{
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
use crate::common::errors::{DomainError, ErrorKind}; use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::repositories::folder_repository::FolderRepository; 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 crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@@ -136,12 +136,11 @@ impl FolderUseCase for FolderService {
/// Creates a new folder /// Creates a new folder
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> { async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
// Input validation // Input validation
if dto.name.is_empty() { if let Err(reason) = validate_storage_name(&dto.name) {
return Err(DomainError::new( return Err(DomainError::validation_error(format!(
ErrorKind::InvalidInput, "Invalid folder name '{}': {reason}",
"Folder", dto.name
"Folder name cannot be empty", )));
));
} }
// If a parent_id is provided, verify it exists // If a parent_id is provided, verify it exists
@@ -397,12 +396,11 @@ impl FolderUseCase for FolderService {
caller_id: Uuid, caller_id: Uuid,
) -> Result<FolderDto, DomainError> { ) -> Result<FolderDto, DomainError> {
// Input validation // Input validation
if dto.name.is_empty() { if let Err(reason) = validate_storage_name(&dto.name) {
return Err(DomainError::new( return Err(DomainError::validation_error(format!(
ErrorKind::InvalidInput, "Invalid folder name '{}': {reason}",
"Folder", dto.name
"New folder name cannot be empty", )));
));
} }
// Verify the folder exists and belongs to the caller // Verify the folder exists and belongs to the caller
+9 -13
View File
@@ -1,6 +1,6 @@
use uuid::Uuid; 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 // Re-export entity errors from the centralized module
pub use super::entity_errors::{FileError, FileResult}; pub use super::entity_errors::{FileError, FileResult};
@@ -100,9 +100,8 @@ impl File {
mime_type: String, mime_type: String,
folder_id: Option<String>, folder_id: Option<String>,
) -> FileResult<Self> { ) -> FileResult<Self> {
// Validate file name if let Err(reason) = validate_storage_name(&name) {
if name.is_empty() || name.contains('/') || name.contains('\\') { return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
return Err(FileError::InvalidFileName(name));
} }
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
@@ -137,9 +136,8 @@ impl File {
created_at: u64, created_at: u64,
modified_at: u64, modified_at: u64,
) -> FileResult<Self> { ) -> FileResult<Self> {
// Validate folder name if let Err(reason) = validate_storage_name(&name) {
if name.is_empty() || name.contains('/') || name.contains('\\') { return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
return Err(FileError::InvalidFileName(name));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
@@ -199,9 +197,8 @@ impl File {
owner_id: Option<Uuid>, owner_id: Option<Uuid>,
etag: String, etag: String,
) -> FileResult<Self> { ) -> FileResult<Self> {
// Validate file name if let Err(reason) = validate_storage_name(&name) {
if name.is_empty() || name.contains('/') || name.contains('\\') { return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
return Err(FileError::InvalidFileName(name));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
@@ -321,9 +318,8 @@ impl File {
/// Creates a new version of the file with updated name /// Creates a new version of the file with updated name
pub fn with_name(&self, new_name: String) -> FileResult<Self> { pub fn with_name(&self, new_name: String) -> FileResult<Self> {
// Validate file name if let Err(reason) = validate_storage_name(&new_name) {
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { return Err(FileError::InvalidFileName(format!("{new_name}: {reason}")));
return Err(FileError::InvalidFileName(new_name));
} }
// Update path based on name // Update path based on name
+9 -8
View File
@@ -1,6 +1,6 @@
use uuid::Uuid; 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 // Re-export entity errors from the centralized module
pub use super::entity_errors::{FolderError, FolderResult}; pub use super::entity_errors::{FolderError, FolderResult};
@@ -71,8 +71,8 @@ impl Folder {
owner_id: Option<Uuid>, owner_id: Option<Uuid>,
) -> FolderResult<Self> { ) -> FolderResult<Self> {
// Validate folder name // Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') { if let Err(reason) = validate_storage_name(&name) {
return Err(FolderError::InvalidFolderName(name)); return Err(FolderError::InvalidFolderName(format!("{name}: {reason}")));
} }
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
@@ -126,8 +126,8 @@ impl Folder {
modified_at: u64, modified_at: u64,
) -> FolderResult<Self> { ) -> FolderResult<Self> {
// Validate folder name // Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') { if let Err(reason) = validate_storage_name(&name) {
return Err(FolderError::InvalidFolderName(name)); return Err(FolderError::InvalidFolderName(format!("{name}: {reason}")));
} }
// Store the path string for serialization compatibility // Store the path string for serialization compatibility
@@ -208,9 +208,10 @@ impl Folder {
/// Creates a new version of the folder with updated name /// Creates a new version of the folder with updated name
pub fn with_name(&self, new_name: String) -> FolderResult<Self> { pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
// Validate folder name if let Err(reason) = validate_storage_name(&new_name) {
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { return Err(FolderError::InvalidFolderName(format!(
return Err(FolderError::InvalidFolderName(new_name)); "{new_name}: {reason}"
)));
} }
// Update path based on the name // Update path based on the name
+20
View File
@@ -6,6 +6,26 @@
use std::path::PathBuf; 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) /// Represents a storage path in the domain (Value Object)
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoragePath { pub struct StoragePath {