1use memmap2::{Mmap, MmapOptions};
10use std::fs::File;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14type BufferOffset = usize;
16
17type BufferLength = usize;
19
20type FileSize = u64;
22
23trait SafeBufferAccess {
25 fn validate_access(&self, offset: BufferOffset, length: BufferLength) -> Result<(), IoError>;
27
28 fn get_safe_slice(&self, offset: BufferOffset, length: BufferLength) -> Result<&[u8], IoError>;
30}
31
32impl SafeBufferAccess for [u8] {
33 fn validate_access(&self, offset: BufferOffset, length: BufferLength) -> Result<(), IoError> {
34 validate_buffer_access(self.len(), offset, length)
35 }
36
37 fn get_safe_slice(&self, offset: BufferOffset, length: BufferLength) -> Result<&[u8], IoError> {
38 self.validate_access(offset, length)?;
39 let end_offset = offset + length; Ok(&self[offset..end_offset])
41 }
42}
43
44#[derive(Debug, Error)]
46pub enum IoError {
47 #[error("Failed to open file '{path}': {source}")]
49 FileOpenError {
50 path: PathBuf,
52 #[source]
54 source: std::io::Error,
55 },
56
57 #[error("Failed to memory-map file '{path}': {source}")]
59 MmapError {
60 path: PathBuf,
62 #[source]
64 source: std::io::Error,
65 },
66
67 #[error("File '{path}' is empty")]
69 EmptyFile {
70 path: PathBuf,
72 },
73
74 #[error("File '{path}' is too large ({size} bytes, maximum {max_size} bytes)")]
76 FileTooLarge {
77 path: PathBuf,
79 size: FileSize,
81 max_size: FileSize,
83 },
84
85 #[error("Failed to read metadata for file '{path}': {source}")]
87 MetadataError {
88 path: PathBuf,
90 #[source]
92 source: std::io::Error,
93 },
94
95 #[error(
97 "Buffer access out of bounds: offset {offset} + length {length} > buffer size {buffer_size}"
98 )]
99 BufferOverrun {
100 offset: BufferOffset,
102 length: BufferLength,
104 buffer_size: BufferLength,
106 },
107
108 #[error("Invalid buffer access parameters: offset {offset}, length {length}")]
110 InvalidAccess {
111 offset: BufferOffset,
113 length: BufferLength,
115 },
116
117 #[error("File '{path}' is not a regular file (file type: {file_type})")]
119 InvalidFileType {
120 path: PathBuf,
122 file_type: String,
124 },
125}
126
127#[derive(Debug)]
145pub struct FileBuffer {
146 mmap: Mmap,
148 path: PathBuf,
150}
151
152impl FileBuffer {
153 const MAX_FILE_SIZE: FileSize = 1024 * 1024 * 1024;
159
160 #[allow(dead_code)]
163 const MAX_CONCURRENT_MAPPINGS: usize = 100;
164
165 #[allow(dead_code)]
169 const SMALL_FILE_THRESHOLD: u64 = 4096;
170
171 pub fn new(path: &Path) -> Result<Self, IoError> {
201 let path_buf = path.to_path_buf();
207
208 let file = Self::open_file(path, &path_buf)?;
209 Self::validate_file_metadata(&file, &path_buf)?;
210 let mmap = Self::create_memory_mapping(&file, &path_buf)?;
211
212 Ok(Self {
213 mmap,
214 path: path_buf,
215 })
216 }
217
218 fn open_file(path: &Path, path_buf: &Path) -> Result<File, IoError> {
220 File::open(path).map_err(|source| IoError::FileOpenError {
221 path: path_buf.to_path_buf(),
222 source,
223 })
224 }
225
226 fn validate_file_metadata(_file: &File, path_buf: &Path) -> Result<(), IoError> {
228 let canonical_path =
230 std::fs::canonicalize(path_buf).map_err(|source| IoError::MetadataError {
231 path: path_buf.to_path_buf(),
232 source,
233 })?;
234
235 let metadata =
237 std::fs::metadata(&canonical_path).map_err(|source| IoError::MetadataError {
238 path: canonical_path.clone(),
239 source,
240 })?;
241
242 if !metadata.is_file() {
244 let file_type = if metadata.is_dir() {
245 "directory".to_string()
246 } else if metadata.is_symlink() {
247 "symlink".to_string()
248 } else {
249 Self::detect_special_file_type(&metadata)
251 };
252
253 return Err(IoError::InvalidFileType {
254 path: canonical_path,
255 file_type,
256 });
257 }
258
259 let file_size = metadata.len();
260
261 if file_size == 0 {
268 return Err(IoError::EmptyFile {
269 path: canonical_path,
270 });
271 }
272
273 if file_size > Self::MAX_FILE_SIZE {
275 return Err(IoError::FileTooLarge {
276 path: canonical_path,
277 size: file_size,
278 max_size: Self::MAX_FILE_SIZE,
279 });
280 }
281
282 Ok(())
283 }
284
285 fn detect_special_file_type(metadata: &std::fs::Metadata) -> String {
287 #[cfg(unix)]
288 {
289 use std::os::unix::fs::FileTypeExt;
290 if metadata.file_type().is_block_device() {
291 "block device".to_string()
292 } else if metadata.file_type().is_char_device() {
293 "character device".to_string()
294 } else if metadata.file_type().is_fifo() {
295 "FIFO/pipe".to_string()
296 } else if metadata.file_type().is_socket() {
297 "socket".to_string()
298 } else {
299 "special file".to_string()
300 }
301 }
302 #[cfg(windows)]
303 {
304 if metadata.file_type().is_symlink() {
305 "symlink".to_string()
306 } else {
307 "special file".to_string()
308 }
309 }
310 #[cfg(not(any(unix, windows)))]
311 {
312 "special file".to_string()
313 }
314 }
315
316 pub fn create_symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
327 original: P,
328 link: Q,
329 ) -> Result<(), std::io::Error> {
330 #[cfg(unix)]
331 {
332 std::os::unix::fs::symlink(original, link)
333 }
334 #[cfg(windows)]
335 {
336 let original_path = original.as_ref();
337
338 if original_path.is_dir() {
339 std::os::windows::fs::symlink_dir(original, link)
340 } else {
341 std::os::windows::fs::symlink_file(original, link)
342 }
343 }
344 #[cfg(not(any(unix, windows)))]
345 {
346 Err(std::io::Error::new(
347 std::io::ErrorKind::Unsupported,
348 "Symlinks not supported on this platform",
349 ))
350 }
351 }
352
353 fn create_memory_mapping(file: &File, path_buf: &Path) -> Result<Mmap, IoError> {
362 #[allow(unsafe_code)]
367 unsafe {
368 MmapOptions::new().map(file).map_err(|source| {
369 let sanitized_path = path_buf.file_name().map_or_else(
371 || "<unknown>".to_string(),
372 |name| name.to_string_lossy().into_owned(),
373 );
374
375 IoError::MmapError {
376 path: PathBuf::from(sanitized_path),
377 source,
378 }
379 })
380 }
381 }
382
383 #[must_use]
400 pub fn as_slice(&self) -> &[u8] {
401 &self.mmap
402 }
403
404 #[must_use]
417 pub fn path(&self) -> &Path {
418 &self.path
419 }
420
421 #[must_use]
434 pub fn len(&self) -> usize {
435 self.mmap.len()
436 }
437
438 #[must_use]
454 pub fn is_empty(&self) -> bool {
455 self.mmap.is_empty()
456 }
457}
458
459pub fn safe_read_bytes(
497 buffer: &[u8],
498 offset: BufferOffset,
499 length: BufferLength,
500) -> Result<&[u8], IoError> {
501 buffer.get_safe_slice(offset, length)
506}
507
508pub fn safe_read_byte(buffer: &[u8], offset: BufferOffset) -> Result<u8, IoError> {
538 buffer.get(offset).copied().ok_or(IoError::BufferOverrun {
539 offset,
540 length: 1,
541 buffer_size: buffer.len(),
542 })
543}
544
545pub fn validate_buffer_access(
583 buffer_size: BufferLength,
584 offset: BufferOffset,
585 length: BufferLength,
586) -> Result<(), IoError> {
587 if length == 0 {
589 return Err(IoError::InvalidAccess { offset, length });
590 }
591
592 if offset >= buffer_size {
594 return Err(IoError::BufferOverrun {
595 offset,
596 length,
597 buffer_size,
598 });
599 }
600
601 let end_offset = offset
603 .checked_add(length)
604 .ok_or(IoError::InvalidAccess { offset, length })?;
605
606 if end_offset > buffer_size {
608 return Err(IoError::BufferOverrun {
609 offset,
610 length,
611 buffer_size,
612 });
613 }
614
615 Ok(())
616}
617
618impl Drop for FileBuffer {
622 fn drop(&mut self) {
623 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use std::fs;
633 use std::io::Write;
634 use std::sync::atomic::{AtomicU64, Ordering};
635
636 static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
638
639 fn create_temp_file(content: &[u8]) -> PathBuf {
642 let temp_dir = std::env::temp_dir();
643 let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
644 let file_path = temp_dir.join(format!("libmagic_test_{}_{id}", std::process::id()));
645
646 {
647 let mut file = File::create(&file_path).expect("Failed to create temp file");
648 file.write_all(content).expect("Failed to write temp file");
649 file.sync_all().expect("Failed to sync temp file");
650 } file_path
653 }
654
655 fn cleanup_temp_file(path: &Path) {
657 let _ = fs::remove_file(path);
658 }
659
660 #[test]
661 fn test_file_buffer_creation_success() {
662 let content = b"Hello, World!";
663 let temp_path = create_temp_file(content);
664
665 let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
666
667 assert_eq!(buffer.as_slice(), content);
668 assert_eq!(buffer.len(), content.len());
669 assert!(!buffer.is_empty());
670 assert_eq!(buffer.path(), temp_path.as_path());
671
672 cleanup_temp_file(&temp_path);
673 }
674
675 #[test]
676 fn test_file_buffer_nonexistent_file() {
677 let nonexistent_path = Path::new("/nonexistent/file.bin");
678
679 let result = FileBuffer::new(nonexistent_path);
680
681 assert!(result.is_err());
682 match result.unwrap_err() {
683 IoError::FileOpenError { path, .. } => {
684 assert_eq!(path, nonexistent_path);
685 }
686 other => panic!("Expected FileOpenError, got {other:?}"),
687 }
688 }
689
690 #[test]
691 fn test_file_buffer_empty_file() {
692 let temp_path = create_temp_file(&[]);
693
694 let result = FileBuffer::new(&temp_path);
695
696 assert!(result.is_err());
697 match result.unwrap_err() {
698 IoError::EmptyFile { path } => {
699 let canonical_temp_path = std::fs::canonicalize(&temp_path).unwrap();
701 assert_eq!(path, canonical_temp_path);
702 }
703 other => panic!("Expected EmptyFile error, got {other:?}"),
704 }
705
706 cleanup_temp_file(&temp_path);
707 }
708
709 #[test]
710 fn test_file_buffer_large_file() {
711 let content = vec![0u8; 1024]; let temp_path = create_temp_file(&content);
714
715 let buffer =
716 FileBuffer::new(&temp_path).expect("Failed to create FileBuffer for normal file");
717 assert_eq!(buffer.len(), 1024);
718
719 cleanup_temp_file(&temp_path);
720 }
721
722 #[test]
723 fn test_file_buffer_binary_content() {
724 let content = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
725 let temp_path = create_temp_file(&content);
726
727 let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
728
729 assert_eq!(buffer.as_slice(), content.as_slice());
730 assert_eq!(buffer.as_slice()[0], 0x00);
731 assert_eq!(buffer.as_slice()[7], 0xFC);
732
733 cleanup_temp_file(&temp_path);
734 }
735
736 #[test]
737 fn test_io_error_display() {
738 let path = PathBuf::from("/test/path");
739 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
740
741 let error = IoError::FileOpenError {
742 path,
743 source: io_err,
744 };
745
746 let error_string = format!("{error}");
747 assert!(error_string.contains("/test/path"));
748 assert!(error_string.contains("Failed to open file"));
749 }
750
751 #[test]
752 fn test_empty_file_error_display() {
753 let path = PathBuf::from("/test/empty.bin");
754 let error = IoError::EmptyFile { path };
755
756 let error_string = format!("{error}");
757 assert!(error_string.contains("/test/empty.bin"));
758 assert!(error_string.contains("is empty"));
759 }
760
761 #[test]
762 fn test_file_too_large_error_display() {
763 let path = PathBuf::from("/test/large.bin");
764 let error = IoError::FileTooLarge {
765 path,
766 size: 2_000_000_000,
767 max_size: 1_000_000_000,
768 };
769
770 let error_string = format!("{error}");
771 assert!(error_string.contains("/test/large.bin"));
772 assert!(error_string.contains("too large"));
773 assert!(error_string.contains("2000000000"));
774 assert!(error_string.contains("1000000000"));
775 }
776
777 #[test]
778 fn test_safe_read_bytes_success() {
779 let buffer = b"Hello, World!";
780
781 let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
783 assert_eq!(result, b"Hello");
784
785 let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
787 assert_eq!(result, b"World");
788
789 let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
791 assert_eq!(result, b"H");
792
793 let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
795 assert_eq!(result, buffer);
796
797 let result = safe_read_bytes(buffer, buffer.len() - 1, 1).expect("Failed to read bytes");
799 assert_eq!(result, b"!");
800 }
801
802 #[test]
803 fn test_safe_read_bytes_out_of_bounds() {
804 let buffer = b"Hello";
805
806 let result = safe_read_bytes(buffer, 10, 1);
808 assert!(result.is_err());
809 match result.unwrap_err() {
810 IoError::BufferOverrun {
811 offset,
812 length,
813 buffer_size,
814 } => {
815 assert_eq!(offset, 10);
816 assert_eq!(length, 1);
817 assert_eq!(buffer_size, 5);
818 }
819 other => panic!("Expected BufferOverrun, got {other:?}"),
820 }
821
822 let result = safe_read_bytes(buffer, 3, 5);
824 assert!(result.is_err());
825 match result.unwrap_err() {
826 IoError::BufferOverrun {
827 offset,
828 length,
829 buffer_size,
830 } => {
831 assert_eq!(offset, 3);
832 assert_eq!(length, 5);
833 assert_eq!(buffer_size, 5);
834 }
835 other => panic!("Expected BufferOverrun, got {other:?}"),
836 }
837
838 let result = safe_read_bytes(buffer, 5, 1);
840 assert!(result.is_err());
841 }
842
843 #[test]
844 fn test_safe_read_bytes_zero_length() {
845 let buffer = b"Hello";
846
847 let result = safe_read_bytes(buffer, 0, 0);
848 assert!(result.is_err());
849 match result.unwrap_err() {
850 IoError::InvalidAccess { offset, length } => {
851 assert_eq!(offset, 0);
852 assert_eq!(length, 0);
853 }
854 other => panic!("Expected InvalidAccess, got {other:?}"),
855 }
856 }
857
858 #[test]
859 fn test_safe_read_bytes_overflow() {
860 let buffer = b"Hello";
861
862 let result = safe_read_bytes(buffer, usize::MAX, 1);
865 assert!(result.is_err());
866 match result.unwrap_err() {
867 IoError::BufferOverrun { .. } => {
868 }
870 other => panic!("Expected BufferOverrun, got {other:?}"),
871 }
872
873 let result = safe_read_bytes(buffer, 1, usize::MAX);
875 assert!(result.is_err());
876 match result.unwrap_err() {
877 IoError::InvalidAccess { .. } => {
878 }
880 other => panic!("Expected InvalidAccess, got {other:?}"),
881 }
882
883 let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
885 assert!(result.is_err());
886 match result.unwrap_err() {
887 IoError::InvalidAccess { .. } => {
888 }
890 other => panic!("Expected InvalidAccess, got {other:?}"),
891 }
892 }
893
894 #[test]
895 fn test_safe_read_byte_success() {
896 let buffer = b"Hello";
897
898 assert_eq!(safe_read_byte(buffer, 0).unwrap(), b'H');
899 assert_eq!(safe_read_byte(buffer, 1).unwrap(), b'e');
900 assert_eq!(safe_read_byte(buffer, 4).unwrap(), b'o');
901 }
902
903 #[test]
904 fn test_safe_read_byte_out_of_bounds() {
905 let buffer = b"Hello";
906
907 let result = safe_read_byte(buffer, 5);
908 assert!(result.is_err());
909 match result.unwrap_err() {
910 IoError::BufferOverrun {
911 offset,
912 length,
913 buffer_size,
914 } => {
915 assert_eq!(offset, 5);
916 assert_eq!(length, 1);
917 assert_eq!(buffer_size, 5);
918 }
919 other => panic!("Expected BufferOverrun, got {other:?}"),
920 }
921
922 let result = safe_read_byte(buffer, 100);
923 assert!(result.is_err());
924 }
925
926 #[test]
927 fn test_validate_buffer_access_success() {
928 validate_buffer_access(100, 0, 50).expect("Should be valid");
930 validate_buffer_access(100, 50, 50).expect("Should be valid");
931 validate_buffer_access(100, 99, 1).expect("Should be valid");
932 validate_buffer_access(10, 0, 10).expect("Should be valid");
933 validate_buffer_access(1, 0, 1).expect("Should be valid");
934 }
935
936 #[test]
937 fn test_validate_buffer_access_invalid() {
938 let result = validate_buffer_access(100, 0, 0);
940 assert!(result.is_err());
941
942 let result = validate_buffer_access(100, 100, 1);
944 assert!(result.is_err());
945
946 let result = validate_buffer_access(100, 50, 51);
948 assert!(result.is_err());
949
950 let result = validate_buffer_access(100, usize::MAX, 1);
952 assert!(result.is_err());
953
954 let result = validate_buffer_access(100, 1, usize::MAX);
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn test_validate_buffer_access_edge_cases() {
960 let result = validate_buffer_access(0, 0, 1);
962 assert!(result.is_err());
963
964 let large_size = 1_000_000;
966 validate_buffer_access(large_size, 0, large_size).expect("Should be valid");
967 validate_buffer_access(large_size, large_size - 1, 1).expect("Should be valid");
968
969 let result = validate_buffer_access(large_size, large_size - 1, 2);
971 assert!(result.is_err());
972 }
973
974 #[test]
975 fn test_buffer_access_security_patterns() {
976 let buffer_size = 1024;
978
979 let overflow_patterns = vec![
981 (usize::MAX, 1), (buffer_size, usize::MAX), (usize::MAX - 1, 2), ];
985
986 for (offset, length) in overflow_patterns {
987 let result = validate_buffer_access(buffer_size, offset, length);
988 assert!(
989 result.is_err(),
990 "Should reject potentially dangerous access pattern: offset={offset}, length={length}"
991 );
992 }
993
994 let safe_patterns = vec![
996 (0, 1), (buffer_size - 1, 1), (buffer_size / 2, 1), ];
1000
1001 for (offset, length) in safe_patterns {
1002 let result = validate_buffer_access(buffer_size, offset, length);
1003 assert!(
1004 result.is_ok(),
1005 "Should accept safe access pattern: offset={offset}, length={length}"
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn test_buffer_overrun_error_display() {
1012 let error = IoError::BufferOverrun {
1013 offset: 10,
1014 length: 5,
1015 buffer_size: 12,
1016 };
1017
1018 let error_string = format!("{error}");
1019 assert!(error_string.contains("Buffer access out of bounds"));
1020 assert!(error_string.contains("offset 10"));
1021 assert!(error_string.contains("length 5"));
1022 assert!(error_string.contains("buffer size 12"));
1023 }
1024
1025 #[test]
1026 fn test_invalid_access_error_display() {
1027 let error = IoError::InvalidAccess {
1028 offset: 0,
1029 length: 0,
1030 };
1031
1032 let error_string = format!("{error}");
1033 assert!(error_string.contains("Invalid buffer access parameters"));
1034 assert!(error_string.contains("offset 0"));
1035 assert!(error_string.contains("length 0"));
1036 }
1037
1038 #[test]
1039 fn test_invalid_file_type_error_display() {
1040 let error = IoError::InvalidFileType {
1041 path: std::path::PathBuf::from("/dev/null"),
1042 file_type: "character device".to_string(),
1043 };
1044
1045 let error_string = format!("{error}");
1046 assert!(error_string.contains("is not a regular file"));
1047 assert!(error_string.contains("/dev/null"));
1048 assert!(error_string.contains("character device"));
1049 }
1050
1051 #[test]
1052 fn test_file_buffer_directory_rejection() {
1053 let temp_dir = std::env::temp_dir().join("test_dir_12345");
1055 std::fs::create_dir_all(&temp_dir).unwrap();
1056
1057 let result = FileBuffer::new(&temp_dir);
1058
1059 assert!(result.is_err());
1060 match result.unwrap_err() {
1061 IoError::InvalidFileType { path, file_type } => {
1062 assert_eq!(file_type, "directory");
1063 let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
1065 assert_eq!(path, canonical_temp_dir);
1066 }
1067 IoError::FileOpenError { .. } => {
1068 println!(
1071 "Directory test skipped on this platform (can't open directories as files)"
1072 );
1073 }
1074 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1075 }
1076
1077 std::fs::remove_dir(&temp_dir).unwrap();
1079 }
1080
1081 #[test]
1082 fn test_file_buffer_symlink_to_directory_rejection() {
1083 let temp_dir = std::env::temp_dir().join("test_dir_symlink_12345");
1085 let symlink_path = std::env::temp_dir().join("test_symlink_12345");
1086
1087 std::fs::create_dir_all(&temp_dir).unwrap();
1088
1089 let symlink_result = FileBuffer::create_symlink(&temp_dir, &symlink_path);
1091
1092 match symlink_result {
1093 Ok(()) => {
1094 let result = FileBuffer::new(&symlink_path);
1095
1096 assert!(result.is_err());
1097 match result.unwrap_err() {
1098 IoError::InvalidFileType { path, file_type } => {
1099 assert_eq!(file_type, "directory");
1100 let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
1102 assert_eq!(path, canonical_temp_dir);
1103 }
1104 IoError::FileOpenError { .. } => {
1105 println!(
1108 "Directory symlink test skipped on this platform (can't open directories as files)"
1109 );
1110 }
1111 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1112 }
1113
1114 let _ = std::fs::remove_file(&symlink_path);
1116 }
1117 Err(_) => {
1118 println!(
1120 "Skipping symlink test - unable to create symlink (may need admin privileges)"
1121 );
1122 }
1123 }
1124
1125 std::fs::remove_dir(&temp_dir).unwrap();
1127 }
1128
1129 #[test]
1130 fn test_file_buffer_symlink_to_regular_file_success() {
1131 let temp_file = std::env::temp_dir().join("test_file_symlink_12345");
1133 let symlink_path = std::env::temp_dir().join("test_symlink_file_12345");
1134
1135 let content = b"test content";
1136 std::fs::write(&temp_file, content).unwrap();
1137
1138 let symlink_result = FileBuffer::create_symlink(&temp_file, &symlink_path);
1140
1141 match symlink_result {
1142 Ok(()) => {
1143 let result = FileBuffer::new(&symlink_path);
1144
1145 assert!(result.is_ok());
1146 let buffer = result.unwrap();
1147 assert_eq!(buffer.as_slice(), content);
1148
1149 let _ = std::fs::remove_file(&symlink_path);
1151 }
1152 Err(_) => {
1153 println!(
1155 "Skipping symlink test - unable to create symlink (may need admin privileges)"
1156 );
1157 }
1158 }
1159
1160 std::fs::remove_file(&temp_file).unwrap();
1162 }
1163
1164 #[test]
1165 fn test_file_buffer_special_files_rejection() {
1166 #[cfg(unix)]
1168 {
1169 let result = FileBuffer::new(std::path::Path::new("/dev/null"));
1171 assert!(result.is_err());
1172 match result.unwrap_err() {
1173 IoError::InvalidFileType { path, file_type } => {
1174 assert_eq!(file_type, "character device");
1175 assert_eq!(path, std::path::PathBuf::from("/dev/null"));
1176 }
1177 other => panic!("Expected InvalidFileType error, got {other:?}"),
1178 }
1179
1180 let result = FileBuffer::new(std::path::Path::new("/dev/zero"));
1182 assert!(result.is_err());
1183 match result.unwrap_err() {
1184 IoError::InvalidFileType { path, file_type } => {
1185 assert_eq!(file_type, "character device");
1186 assert_eq!(path, std::path::PathBuf::from("/dev/zero"));
1187 }
1188 other => panic!("Expected InvalidFileType error, got {other:?}"),
1189 }
1190
1191 let result = FileBuffer::new(std::path::Path::new("/dev/random"));
1193 assert!(result.is_err());
1194 match result.unwrap_err() {
1195 IoError::InvalidFileType { path, file_type } => {
1196 assert_eq!(file_type, "character device");
1197 assert_eq!(path, std::path::PathBuf::from("/dev/random"));
1198 }
1199 other => panic!("Expected InvalidFileType error, got {other:?}"),
1200 }
1201 }
1202
1203 #[cfg(not(unix))]
1204 {
1205 println!("Skipping special file tests on non-Unix platform");
1207 }
1208 }
1209
1210 #[test]
1211 fn test_file_buffer_cross_platform_special_files() {
1212 let temp_dir = std::env::temp_dir().join("test_special_dir_12345");
1217 std::fs::create_dir_all(&temp_dir).unwrap();
1218
1219 let result = FileBuffer::new(&temp_dir);
1220 assert!(result.is_err());
1221 match result.unwrap_err() {
1222 IoError::InvalidFileType { path, file_type } => {
1223 assert_eq!(file_type, "directory");
1224 let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
1225 assert_eq!(path, canonical_temp_dir);
1226 }
1227 IoError::FileOpenError { .. } => {
1228 println!(
1230 "Directory test skipped on this platform (can't open directories as files)"
1231 );
1232 }
1233 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1234 }
1235
1236 std::fs::remove_dir(&temp_dir).unwrap();
1238 }
1239
1240 #[test]
1241 #[ignore = "FIFOs can cause hanging issues in CI environments"]
1242 fn test_file_buffer_fifo_rejection() {
1243 #[cfg(unix)]
1245 {
1246 use nix::unistd;
1247
1248 let fifo_path = std::env::temp_dir().join("test_fifo_12345");
1249
1250 match unistd::mkfifo(
1252 &fifo_path,
1253 nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
1254 ) {
1255 Ok(()) => {
1256 let result = FileBuffer::new(&fifo_path);
1257
1258 assert!(result.is_err());
1259 match result.unwrap_err() {
1260 IoError::InvalidFileType { path, file_type } => {
1261 assert_eq!(file_type, "FIFO/pipe");
1262 let canonical_fifo_path = std::fs::canonicalize(&fifo_path).unwrap();
1263 assert_eq!(path, canonical_fifo_path);
1264 }
1265 other => panic!("Expected InvalidFileType error, got {other:?}"),
1266 }
1267
1268 std::fs::remove_file(&fifo_path).unwrap();
1270 }
1271 Err(_) => {
1272 println!("Skipping FIFO test - unable to create FIFO");
1274 }
1275 }
1276 }
1277
1278 #[cfg(not(unix))]
1279 {
1280 println!("Skipping FIFO test on non-Unix platform");
1282 }
1283 }
1284}