Skip to main content

reinhardt_http/
upload.rs

1//! File upload handling functionality
2//!
3//! This module provides file upload processing including handlers,
4//! temporary file management, and memory-based uploads.
5
6use percent_encoding::percent_decode_str;
7use sha2::{Digest, Sha256};
8use std::fs;
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11
12/// Errors that can occur during file upload operations
13#[derive(Debug, thiserror::Error)]
14pub enum FileUploadError {
15	#[error("File too large: {0} bytes (max: {1} bytes)")]
16	FileTooLarge(usize, usize),
17	#[error("Invalid file type: {0}")]
18	InvalidFileType(String),
19	#[error("IO error: {0}")]
20	Io(#[from] io::Error),
21	#[error("Upload error: {0}")]
22	Upload(String),
23	#[error("Checksum verification failed")]
24	ChecksumMismatch,
25	#[error("MIME type detection failed")]
26	MimeDetectionFailed,
27	#[error("Path traversal detected in filename")]
28	PathTraversal,
29}
30
31/// Validate that a filename does not contain path traversal sequences
32/// or other unsafe characters that could escape the upload directory.
33///
34/// Checks both raw and URL-decoded forms of the filename to prevent
35/// bypasses via percent-encoding (e.g. `%2e%2e%2f`).
36pub fn validate_safe_filename(filename: &str) -> Result<(), FileUploadError> {
37	if filename.is_empty() {
38		return Err(FileUploadError::Upload("Empty filename".to_string()));
39	}
40
41	// Check both the raw filename and its URL-decoded form to prevent
42	// bypass via percent-encoded traversal sequences like %2e%2e%2f
43	let decoded = percent_decode_str(filename).decode_utf8_lossy();
44	for candidate in [filename, decoded.as_ref()] {
45		if candidate.contains('\0') {
46			return Err(FileUploadError::PathTraversal);
47		}
48		if candidate.contains("..") {
49			return Err(FileUploadError::PathTraversal);
50		}
51		if candidate.contains('/') || candidate.contains('\\') {
52			return Err(FileUploadError::PathTraversal);
53		}
54		// Reject absolute paths (Unix and Windows)
55		if candidate.starts_with('/') || candidate.starts_with('\\') {
56			return Err(FileUploadError::PathTraversal);
57		}
58		if candidate.len() >= 2
59			&& candidate.as_bytes()[0].is_ascii_alphabetic()
60			&& candidate.as_bytes()[1] == b':'
61		{
62			return Err(FileUploadError::PathTraversal);
63		}
64	}
65	Ok(())
66}
67
68/// FileUploadHandler processes file uploads
69///
70/// Handles file upload operations including validation, storage,
71/// and cleanup of temporary files.
72pub struct FileUploadHandler {
73	upload_dir: PathBuf,
74	max_size: usize,
75	allowed_extensions: Option<Vec<String>>,
76	verify_checksum: bool,
77	allowed_mime_types: Option<Vec<String>>,
78}
79
80impl FileUploadHandler {
81	/// Create a new FileUploadHandler
82	///
83	/// # Arguments
84	///
85	/// * `upload_dir` - Directory where uploaded files will be stored
86	///
87	/// # Examples
88	///
89	/// ```
90	/// use reinhardt_http::upload::FileUploadHandler;
91	/// use std::path::PathBuf;
92	///
93	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
94	/// assert_eq!(handler.max_size(), 10 * 1024 * 1024); // 10MB default
95	/// ```
96	pub fn new(upload_dir: PathBuf) -> Self {
97		Self {
98			upload_dir,
99			max_size: 10 * 1024 * 1024, // 10MB default
100			allowed_extensions: None,
101			verify_checksum: false,
102			allowed_mime_types: None,
103		}
104	}
105
106	/// Set maximum file size
107	///
108	/// # Examples
109	///
110	/// ```
111	/// use reinhardt_http::upload::FileUploadHandler;
112	/// use std::path::PathBuf;
113	///
114	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
115	///     .with_max_size(5 * 1024 * 1024); // 5MB
116	/// assert_eq!(handler.max_size(), 5 * 1024 * 1024);
117	/// ```
118	pub fn with_max_size(mut self, max_size: usize) -> Self {
119		self.max_size = max_size;
120		self
121	}
122
123	/// Set allowed file extensions
124	///
125	/// # Examples
126	///
127	/// ```
128	/// use reinhardt_http::upload::FileUploadHandler;
129	/// use std::path::PathBuf;
130	///
131	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
132	///     .with_allowed_extensions(vec!["jpg".to_string(), "png".to_string()]);
133	/// ```
134	pub fn with_allowed_extensions(mut self, extensions: Vec<String>) -> Self {
135		self.allowed_extensions = Some(extensions);
136		self
137	}
138
139	/// Enable checksum verification
140	///
141	/// # Examples
142	///
143	/// ```
144	/// use reinhardt_http::upload::FileUploadHandler;
145	/// use std::path::PathBuf;
146	///
147	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
148	///     .with_checksum_verification(true);
149	/// ```
150	pub fn with_checksum_verification(mut self, enabled: bool) -> Self {
151		self.verify_checksum = enabled;
152		self
153	}
154
155	/// Set allowed MIME types
156	///
157	/// # Examples
158	///
159	/// ```
160	/// use reinhardt_http::upload::FileUploadHandler;
161	/// use std::path::PathBuf;
162	///
163	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
164	///     .with_allowed_mime_types(vec![
165	///         "image/jpeg".to_string(),
166	///         "image/png".to_string()
167	///     ]);
168	/// ```
169	pub fn with_allowed_mime_types(mut self, mime_types: Vec<String>) -> Self {
170		self.allowed_mime_types = Some(mime_types);
171		self
172	}
173
174	/// Get the maximum file size
175	pub fn max_size(&self) -> usize {
176		self.max_size
177	}
178
179	/// Get the upload directory
180	pub fn upload_dir(&self) -> &Path {
181		&self.upload_dir
182	}
183
184	/// Calculate SHA-256 checksum of file content
185	///
186	/// # Examples
187	///
188	/// ```
189	/// use reinhardt_http::upload::FileUploadHandler;
190	/// use std::path::PathBuf;
191	///
192	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
193	/// let checksum = handler.calculate_checksum(b"test data");
194	/// assert_eq!(checksum.len(), 64); // SHA-256 produces 64 hex characters
195	/// ```
196	pub fn calculate_checksum(&self, content: &[u8]) -> String {
197		let mut hasher = Sha256::new();
198		hasher.update(content);
199		let result = hasher.finalize();
200		// Convert bytes to hex string
201		result.iter().map(|b| format!("{:02x}", b)).collect()
202	}
203
204	/// Verify file checksum
205	///
206	/// # Examples
207	///
208	/// ```
209	/// use reinhardt_http::upload::FileUploadHandler;
210	/// use std::path::PathBuf;
211	///
212	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
213	/// let content = b"test data";
214	/// let checksum = handler.calculate_checksum(content);
215	/// assert!(handler.verify_file_checksum(content, &checksum).is_ok());
216	/// ```
217	pub fn verify_file_checksum(
218		&self,
219		content: &[u8],
220		expected_checksum: &str,
221	) -> Result<(), FileUploadError> {
222		let actual_checksum = self.calculate_checksum(content);
223		if actual_checksum == expected_checksum {
224			Ok(())
225		} else {
226			Err(FileUploadError::ChecksumMismatch)
227		}
228	}
229
230	/// Detect MIME type from file content
231	///
232	/// Basic MIME type detection based on file signatures (magic numbers).
233	///
234	/// # Examples
235	///
236	/// ```
237	/// use reinhardt_http::upload::FileUploadHandler;
238	/// use std::path::PathBuf;
239	///
240	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
241	///
242	/// // PNG signature
243	/// let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
244	/// assert_eq!(handler.detect_mime_type(&png_data), Some("image/png".to_string()));
245	///
246	/// // JPEG signature
247	/// let jpeg_data = vec![0xFF, 0xD8, 0xFF];
248	/// assert_eq!(handler.detect_mime_type(&jpeg_data), Some("image/jpeg".to_string()));
249	/// ```
250	pub fn detect_mime_type(&self, content: &[u8]) -> Option<String> {
251		if content.is_empty() {
252			return None;
253		}
254
255		// Check common file signatures
256		if content.len() >= 8 && content[0..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] {
257			return Some("image/png".to_string());
258		}
259
260		if content.len() >= 3 && content[0..3] == [0xFF, 0xD8, 0xFF] {
261			return Some("image/jpeg".to_string());
262		}
263
264		if content.len() >= 4 && content[0..4] == [0x47, 0x49, 0x46, 0x38] {
265			return Some("image/gif".to_string());
266		}
267
268		if content.len() >= 4 && content[0..4] == [0x25, 0x50, 0x44, 0x46] {
269			return Some("application/pdf".to_string());
270		}
271
272		if content.len() >= 4
273			&& (content[0..4] == [0x50, 0x4B, 0x03, 0x04]
274				|| content[0..4] == [0x50, 0x4B, 0x05, 0x06])
275		{
276			return Some("application/zip".to_string());
277		}
278
279		None
280	}
281
282	/// Validate MIME type
283	fn validate_mime_type(&self, content: &[u8]) -> Result<(), FileUploadError> {
284		if let Some(ref allowed) = self.allowed_mime_types {
285			let detected_mime = self
286				.detect_mime_type(content)
287				.ok_or(FileUploadError::MimeDetectionFailed)?;
288
289			if !allowed.contains(&detected_mime) {
290				return Err(FileUploadError::InvalidFileType(detected_mime));
291			}
292		}
293		Ok(())
294	}
295
296	/// Handle a file upload
297	///
298	/// # Arguments
299	///
300	/// * `field_name` - Name of the form field
301	/// * `filename` - Original filename
302	/// * `content` - File content as bytes
303	///
304	/// # Returns
305	///
306	/// Returns the path to the saved file
307	///
308	/// # Examples
309	///
310	/// ```no_run
311	/// use reinhardt_http::upload::FileUploadHandler;
312	/// use std::path::PathBuf;
313	///
314	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
315	/// let result = handler.handle_upload("avatar", "photo.jpg", b"image data");
316	/// assert!(result.is_ok());
317	/// ```
318	pub fn handle_upload(
319		&self,
320		field_name: &str,
321		filename: &str,
322		content: &[u8],
323	) -> Result<String, FileUploadError> {
324		// Validate field_name to prevent path traversal via form field names
325		validate_safe_filename(field_name)?;
326
327		// Check file size
328		if content.len() > self.max_size {
329			return Err(FileUploadError::FileTooLarge(content.len(), self.max_size));
330		}
331
332		// Validate file extension
333		if let Some(ref allowed) = self.allowed_extensions {
334			let extension = Path::new(filename)
335				.extension()
336				.and_then(|e| e.to_str())
337				.unwrap_or("");
338
339			if !allowed.iter().any(|ext| ext == extension) {
340				return Err(FileUploadError::InvalidFileType(extension.to_string()));
341			}
342		}
343
344		// Validate MIME type
345		self.validate_mime_type(content)?;
346
347		// Create upload directory if it doesn't exist
348		fs::create_dir_all(&self.upload_dir)?;
349
350		// Generate unique filename
351		let unique_filename = self.generate_unique_filename(field_name, filename);
352		let file_path = self.upload_dir.join(&unique_filename);
353
354		// Write file
355		let mut file = fs::File::create(&file_path)?;
356		file.write_all(content)?;
357
358		Ok(unique_filename)
359	}
360
361	/// Handle upload with checksum verification
362	///
363	/// # Examples
364	///
365	/// ```no_run
366	/// use reinhardt_http::upload::FileUploadHandler;
367	/// use std::path::PathBuf;
368	///
369	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
370	///     .with_checksum_verification(true);
371	///
372	/// let content = b"test data";
373	/// let checksum = handler.calculate_checksum(content);
374	/// let result = handler.handle_upload_with_checksum(
375	///     "file",
376	///     "test.txt",
377	///     content,
378	///     &checksum
379	/// );
380	/// assert!(result.is_ok());
381	/// ```
382	pub fn handle_upload_with_checksum(
383		&self,
384		field_name: &str,
385		filename: &str,
386		content: &[u8],
387		expected_checksum: &str,
388	) -> Result<String, FileUploadError> {
389		// Verify checksum if enabled
390		if self.verify_checksum {
391			self.verify_file_checksum(content, expected_checksum)?;
392		}
393
394		// Handle the upload normally
395		self.handle_upload(field_name, filename, content)
396	}
397
398	/// Generate a unique filename using a cryptographically random UUID v4
399	///
400	/// Extracts only the file extension from the original filename,
401	/// discarding the original name to prevent path traversal.
402	/// Uses UUID v4 (CSPRNG-based) instead of timestamps to prevent
403	/// predictable filename enumeration.
404	fn generate_unique_filename(&self, field_name: &str, original_filename: &str) -> String {
405		let unique_id = uuid::Uuid::new_v4();
406
407		// Extract only the extension from the basename (strip any directory components)
408		let basename = Path::new(original_filename)
409			.file_name()
410			.and_then(|n| n.to_str())
411			.unwrap_or(original_filename);
412
413		let extension = Path::new(basename)
414			.extension()
415			.and_then(|e| e.to_str())
416			.unwrap_or("");
417
418		if extension.is_empty() {
419			format!("{}_{}", field_name, unique_id)
420		} else {
421			format!("{}_{}.{}", field_name, unique_id, extension)
422		}
423	}
424
425	/// Delete an uploaded file
426	///
427	/// # Examples
428	///
429	/// ```no_run
430	/// use reinhardt_http::upload::FileUploadHandler;
431	/// use std::path::PathBuf;
432	///
433	/// let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
434	/// let result = handler.delete_upload("avatar_123456.jpg");
435	/// assert!(result.is_ok());
436	/// ```
437	pub fn delete_upload(&self, filename: &str) -> Result<(), FileUploadError> {
438		// Validate filename to prevent path traversal attacks
439		validate_safe_filename(filename)?;
440		let file_path = self.upload_dir.join(filename);
441		fs::remove_file(file_path)?;
442		Ok(())
443	}
444}
445
446/// TemporaryFileUpload manages temporary uploaded files
447///
448/// Automatically cleans up temporary files when dropped.
449pub struct TemporaryFileUpload {
450	path: PathBuf,
451	auto_delete: bool,
452}
453
454impl TemporaryFileUpload {
455	/// Create a new temporary file upload
456	///
457	/// # Examples
458	///
459	/// ```
460	/// use reinhardt_http::upload::TemporaryFileUpload;
461	/// use std::path::PathBuf;
462	///
463	/// let temp = TemporaryFileUpload::new(PathBuf::from("/tmp/temp_file.dat"));
464	/// assert_eq!(temp.path().to_str().unwrap(), "/tmp/temp_file.dat");
465	/// ```
466	pub fn new(path: PathBuf) -> Self {
467		Self {
468			path,
469			auto_delete: true,
470		}
471	}
472
473	/// Create a temporary file with content
474	///
475	/// # Examples
476	///
477	/// ```no_run
478	/// use reinhardt_http::upload::TemporaryFileUpload;
479	/// use std::path::PathBuf;
480	///
481	/// let temp = TemporaryFileUpload::with_content(
482	///     PathBuf::from("/tmp/temp.txt"),
483	///     b"Hello, World!"
484	/// ).unwrap();
485	/// ```
486	pub fn with_content(path: PathBuf, content: &[u8]) -> Result<Self, FileUploadError> {
487		let mut file = fs::File::create(&path)?;
488		file.write_all(content)?;
489		Ok(Self {
490			path,
491			auto_delete: true,
492		})
493	}
494
495	/// Disable automatic deletion
496	///
497	/// # Examples
498	///
499	/// ```
500	/// use reinhardt_http::upload::TemporaryFileUpload;
501	/// use std::path::PathBuf;
502	///
503	/// let mut temp = TemporaryFileUpload::new(PathBuf::from("/tmp/keep_me.txt"));
504	/// temp.keep();
505	/// assert!(!temp.auto_delete());
506	/// ```
507	pub fn keep(&mut self) {
508		self.auto_delete = false;
509	}
510
511	/// Get the file path
512	pub fn path(&self) -> &Path {
513		&self.path
514	}
515
516	/// Check if auto-delete is enabled
517	pub fn auto_delete(&self) -> bool {
518		self.auto_delete
519	}
520
521	/// Read file content
522	///
523	/// # Examples
524	///
525	/// ```no_run
526	/// use reinhardt_http::upload::TemporaryFileUpload;
527	/// use std::path::PathBuf;
528	///
529	/// let temp = TemporaryFileUpload::with_content(
530	///     PathBuf::from("/tmp/test.txt"),
531	///     b"content"
532	/// ).unwrap();
533	/// let content = temp.read_content().unwrap();
534	/// assert_eq!(content, b"content");
535	/// ```
536	pub fn read_content(&self) -> Result<Vec<u8>, FileUploadError> {
537		Ok(fs::read(&self.path)?)
538	}
539}
540
541impl Drop for TemporaryFileUpload {
542	fn drop(&mut self) {
543		if self.auto_delete && self.path.exists() {
544			let _ = fs::remove_file(&self.path);
545		}
546	}
547}
548
549/// MemoryFileUpload stores uploaded files in memory
550///
551/// Useful for small files or testing scenarios where
552/// disk I/O should be avoided.
553pub struct MemoryFileUpload {
554	filename: String,
555	content: Vec<u8>,
556	content_type: Option<String>,
557}
558
559impl MemoryFileUpload {
560	/// Create a new memory-based file upload
561	///
562	/// # Examples
563	///
564	/// ```
565	/// use reinhardt_http::upload::MemoryFileUpload;
566	///
567	/// let upload = MemoryFileUpload::new(
568	///     "document.pdf".to_string(),
569	///     vec![0x25, 0x50, 0x44, 0x46]
570	/// );
571	/// assert_eq!(upload.filename(), "document.pdf");
572	/// assert_eq!(upload.size(), 4);
573	/// ```
574	pub fn new(filename: String, content: Vec<u8>) -> Self {
575		Self {
576			filename,
577			content,
578			content_type: None,
579		}
580	}
581
582	/// Create a memory upload with content type
583	///
584	/// # Examples
585	///
586	/// ```
587	/// use reinhardt_http::upload::MemoryFileUpload;
588	///
589	/// let upload = MemoryFileUpload::with_content_type(
590	///     "image.png".to_string(),
591	///     vec![0x89, 0x50, 0x4E, 0x47],
592	///     "image/png".to_string()
593	/// );
594	/// assert_eq!(upload.content_type(), Some("image/png"));
595	/// ```
596	pub fn with_content_type(filename: String, content: Vec<u8>, content_type: String) -> Self {
597		Self {
598			filename,
599			content,
600			content_type: Some(content_type),
601		}
602	}
603
604	/// Get the filename
605	pub fn filename(&self) -> &str {
606		&self.filename
607	}
608
609	/// Get the file content
610	pub fn content(&self) -> &[u8] {
611		&self.content
612	}
613
614	/// Get the content type
615	pub fn content_type(&self) -> Option<&str> {
616		self.content_type.as_deref()
617	}
618
619	/// Get the file size in bytes
620	///
621	/// # Examples
622	///
623	/// ```
624	/// use reinhardt_http::upload::MemoryFileUpload;
625	///
626	/// let upload = MemoryFileUpload::new("test.txt".to_string(), vec![1, 2, 3, 4, 5]);
627	/// assert_eq!(upload.size(), 5);
628	/// ```
629	pub fn size(&self) -> usize {
630		self.content.len()
631	}
632
633	/// Check if the upload is empty
634	///
635	/// # Examples
636	///
637	/// ```
638	/// use reinhardt_http::upload::MemoryFileUpload;
639	///
640	/// let empty = MemoryFileUpload::new("empty.txt".to_string(), vec![]);
641	/// assert!(empty.is_empty());
642	///
643	/// let non_empty = MemoryFileUpload::new("data.txt".to_string(), vec![1, 2, 3]);
644	/// assert!(!non_empty.is_empty());
645	/// ```
646	pub fn is_empty(&self) -> bool {
647		self.content.is_empty()
648	}
649
650	/// Save to disk
651	///
652	/// # Examples
653	///
654	/// ```no_run
655	/// use reinhardt_http::upload::MemoryFileUpload;
656	/// use std::path::PathBuf;
657	///
658	/// let upload = MemoryFileUpload::new("test.txt".to_string(), vec![1, 2, 3]);
659	/// let result = upload.save_to_disk(PathBuf::from("/tmp/test.txt"));
660	/// assert!(result.is_ok());
661	/// ```
662	pub fn save_to_disk(&self, path: PathBuf) -> Result<(), FileUploadError> {
663		let mut file = fs::File::create(path)?;
664		file.write_all(&self.content)?;
665		Ok(())
666	}
667}
668
669#[cfg(test)]
670mod tests {
671	use super::*;
672
673	#[test]
674	fn test_file_upload_handler_creation() {
675		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
676		assert_eq!(handler.max_size(), 10 * 1024 * 1024);
677		assert_eq!(handler.upload_dir(), Path::new("/tmp/uploads"));
678	}
679
680	#[test]
681	fn test_file_upload_handler_with_max_size() {
682		let handler =
683			FileUploadHandler::new(PathBuf::from("/tmp/uploads")).with_max_size(5 * 1024 * 1024);
684		assert_eq!(handler.max_size(), 5 * 1024 * 1024);
685	}
686
687	#[test]
688	fn test_file_upload_handler_size_validation() {
689		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads")).with_max_size(100);
690
691		let large_content = vec![0u8; 200];
692		let result = handler.handle_upload("test", "large.txt", &large_content);
693
694		assert!(result.is_err());
695		if let Err(FileUploadError::FileTooLarge(size, max)) = result {
696			assert_eq!(size, 200);
697			assert_eq!(max, 100);
698		} else {
699			panic!("Expected FileTooLarge error");
700		}
701	}
702
703	#[test]
704	fn test_file_upload_handler_extension_validation() {
705		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"))
706			.with_allowed_extensions(vec!["jpg".to_string(), "png".to_string()]);
707
708		let content = b"test content";
709		let result = handler.handle_upload("test", "document.pdf", content);
710
711		assert!(result.is_err());
712		if let Err(FileUploadError::InvalidFileType(ext)) = result {
713			assert_eq!(ext, "pdf");
714		} else {
715			panic!("Expected InvalidFileType error");
716		}
717	}
718
719	#[test]
720	fn test_temporary_file_upload_creation() {
721		let temp = TemporaryFileUpload::new(PathBuf::from("/tmp/test_temp.txt"));
722		assert_eq!(temp.path(), Path::new("/tmp/test_temp.txt"));
723		assert!(temp.auto_delete());
724	}
725
726	#[test]
727	fn test_temporary_file_upload_keep() {
728		let mut temp = TemporaryFileUpload::new(PathBuf::from("/tmp/test_keep.txt"));
729		temp.keep();
730		assert!(!temp.auto_delete());
731	}
732
733	#[test]
734	fn test_temporary_file_upload_with_content() {
735		let temp_path = PathBuf::from("/tmp/test_content_temp.txt");
736		let content = b"Test content";
737
738		let temp = TemporaryFileUpload::with_content(temp_path.clone(), content).unwrap();
739		assert!(temp_path.exists());
740
741		let read_content = temp.read_content().unwrap();
742		assert_eq!(read_content, content);
743
744		drop(temp);
745		assert!(!temp_path.exists());
746	}
747
748	#[test]
749	fn test_temporary_file_upload_auto_delete() {
750		let temp_path = PathBuf::from("/tmp/test_auto_delete.txt");
751		fs::write(&temp_path, b"test").unwrap();
752
753		{
754			let _temp = TemporaryFileUpload::new(temp_path.clone());
755			assert!(temp_path.exists());
756		}
757
758		assert!(!temp_path.exists());
759	}
760
761	#[test]
762	fn test_memory_file_upload_creation() {
763		let upload = MemoryFileUpload::new("test.txt".to_string(), vec![1, 2, 3, 4, 5]);
764
765		assert_eq!(upload.filename(), "test.txt");
766		assert_eq!(upload.content(), &[1, 2, 3, 4, 5]);
767		assert_eq!(upload.size(), 5);
768		assert!(!upload.is_empty());
769	}
770
771	#[test]
772	fn test_memory_file_upload_with_content_type() {
773		let upload = MemoryFileUpload::with_content_type(
774			"image.png".to_string(),
775			vec![0x89, 0x50, 0x4E, 0x47],
776			"image/png".to_string(),
777		);
778
779		assert_eq!(upload.filename(), "image.png");
780		assert_eq!(upload.content_type(), Some("image/png"));
781	}
782
783	#[test]
784	fn test_memory_file_upload_is_empty() {
785		let empty = MemoryFileUpload::new("empty.txt".to_string(), vec![]);
786		assert!(empty.is_empty());
787		assert_eq!(empty.size(), 0);
788
789		let non_empty = MemoryFileUpload::new("data.txt".to_string(), vec![1, 2, 3]);
790		assert!(!non_empty.is_empty());
791		assert_eq!(non_empty.size(), 3);
792	}
793
794	#[test]
795	fn test_memory_file_upload_save_to_disk() {
796		let temp_path = PathBuf::from("/tmp/test_memory_save.txt");
797		let upload = MemoryFileUpload::new("test.txt".to_string(), vec![1, 2, 3, 4, 5]);
798
799		let result = upload.save_to_disk(temp_path.clone());
800		assert!(result.is_ok());
801		assert!(temp_path.exists());
802
803		let content = fs::read(&temp_path).unwrap();
804		assert_eq!(content, vec![1, 2, 3, 4, 5]);
805
806		fs::remove_file(temp_path).unwrap();
807	}
808
809	// =================================================================
810	// Path traversal prevention tests (Issue #355)
811	// =================================================================
812
813	#[rstest::rstest]
814	#[case("../../../etc/passwd")]
815	#[case("foo/../../bar")]
816	#[case("/etc/passwd")]
817	#[case("test\0file.txt")]
818	#[case("..%2f..%2fetc%2fpasswd")]
819	#[case("%2e%2e/%2e%2e/etc/passwd")]
820	fn test_delete_upload_rejects_path_traversal(#[case] filename: &str) {
821		// Arrange
822		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
823
824		// Act
825		let result = handler.delete_upload(filename);
826
827		// Assert
828		assert!(
829			matches!(result, Err(FileUploadError::PathTraversal)),
830			"Expected PathTraversal error for filename: {}",
831			filename
832		);
833	}
834
835	#[rstest::rstest]
836	fn test_delete_upload_allows_safe_filenames() {
837		// Arrange
838		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
839
840		// Act - these should not return PathTraversal error
841		// (they may return IO NotFound since files don't exist, which is expected)
842		let result = handler.delete_upload("safe_file.txt");
843
844		// Assert
845		assert!(
846			!matches!(result, Err(FileUploadError::PathTraversal)),
847			"Safe filename should not trigger path traversal error"
848		);
849	}
850
851	#[rstest::rstest]
852	#[case("normal.txt", true)]
853	#[case("my-file_123.jpg", true)]
854	#[case("report.pdf", true)]
855	#[case("image_2024.png", true)]
856	#[case("../../../etc/passwd", false)]
857	#[case("foo/../bar.txt", false)]
858	#[case("/absolute/path.txt", false)]
859	#[case("null\0byte.txt", false)]
860	#[case("", false)]
861	#[case("back\\slash.txt", false)]
862	#[case("C:\\Windows\\system32", false)]
863	#[case("..%2f..%2fetc%2fpasswd", false)]
864	#[case("%2e%2e%2f%2e%2e%2f", false)]
865	fn test_validate_safe_filename(#[case] filename: &str, #[case] should_pass: bool) {
866		// Act
867		let result = validate_safe_filename(filename);
868
869		// Assert
870		assert_eq!(
871			result.is_ok(),
872			should_pass,
873			"validate_safe_filename({:?}) expected {} but got {}",
874			filename,
875			if should_pass { "Ok" } else { "Err" },
876			if result.is_ok() { "Ok" } else { "Err" },
877		);
878	}
879
880	#[rstest::rstest]
881	#[case("../malicious")]
882	#[case("foo/../../bar")]
883	#[case("..%2fmalicious")]
884	fn test_handle_upload_rejects_traversal_in_field_name(#[case] field_name: &str) {
885		// Arrange
886		let handler = FileUploadHandler::new(PathBuf::from("/tmp/uploads"));
887
888		// Act
889		let result = handler.handle_upload(field_name, "safe.txt", b"content");
890
891		// Assert
892		assert!(
893			matches!(result, Err(FileUploadError::PathTraversal)),
894			"Expected PathTraversal error for field_name: {}",
895			field_name
896		);
897	}
898
899	#[rstest::rstest]
900	fn test_handle_upload_accepts_safe_field_name() {
901		// Arrange
902		let handler = FileUploadHandler::new(PathBuf::from("/tmp/reinhardt_upload_test"));
903
904		// Act
905		let result = handler.handle_upload("avatar", "photo.jpg", b"image data");
906
907		// Assert
908		assert!(result.is_ok());
909
910		// Cleanup
911		let _ = fs::remove_dir_all("/tmp/reinhardt_upload_test");
912	}
913}