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