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
23#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum IoError {
27 #[error("Failed to open file '{path}': {source}")]
29 FileOpenError {
30 path: PathBuf,
32 #[source]
34 source: std::io::Error,
35 },
36
37 #[error("Failed to memory-map file '{path}': {source}")]
39 MmapError {
40 path: PathBuf,
42 #[source]
44 source: std::io::Error,
45 },
46
47 #[error("File '{path}' is empty")]
49 EmptyFile {
50 path: PathBuf,
52 },
53
54 #[error("File '{path}' is too large ({size} bytes, maximum {max_size} bytes)")]
56 FileTooLarge {
57 path: PathBuf,
59 size: FileSize,
61 max_size: FileSize,
63 },
64
65 #[error("Failed to read metadata for file '{path}': {source}")]
67 MetadataError {
68 path: PathBuf,
70 #[source]
72 source: std::io::Error,
73 },
74
75 #[error(
77 "Buffer access out of bounds: offset {offset} + length {length} > buffer size {buffer_size}"
78 )]
79 BufferOverrun {
80 offset: BufferOffset,
82 length: BufferLength,
84 buffer_size: BufferLength,
86 },
87
88 #[error("Invalid buffer access parameters: offset {offset}, length {length}")]
90 InvalidAccess {
91 offset: BufferOffset,
93 length: BufferLength,
95 },
96
97 #[error("File '{path}' is not a regular file (file type: {file_type})")]
99 InvalidFileType {
100 path: PathBuf,
102 file_type: String,
104 },
105}
106
107#[derive(Debug)]
125pub struct FileBuffer {
126 mmap: Mmap,
128 path: PathBuf,
130}
131
132impl FileBuffer {
133 pub const MAX_FILE_SIZE: FileSize = 1024 * 1024 * 1024;
139
140 pub fn new(path: &Path) -> Result<Self, IoError> {
170 let path_buf = path.to_path_buf();
171
172 let file = Self::open_file(path, &path_buf)?;
173 Self::validate_file_metadata(&file, &path_buf)?;
174 let mmap = Self::create_memory_mapping(&file, &path_buf)?;
175
176 Ok(Self {
177 mmap,
178 path: path_buf,
179 })
180 }
181
182 pub fn from_path_and_metadata(
210 path: &Path,
211 metadata: &std::fs::Metadata,
212 ) -> Result<Self, IoError> {
213 let path_buf = path.to_path_buf();
214 Self::check_metadata(metadata, &path_buf)?;
215 let file = Self::open_file(path, &path_buf)?;
216 let mmap = Self::create_memory_mapping(&file, &path_buf)?;
217
218 Ok(Self {
219 mmap,
220 path: path_buf,
221 })
222 }
223
224 fn open_file(path: &Path, path_buf: &Path) -> Result<File, IoError> {
226 File::open(path).map_err(|source| IoError::FileOpenError {
227 path: path_buf.to_path_buf(),
228 source,
229 })
230 }
231
232 fn validate_file_metadata(file: &File, path_buf: &Path) -> Result<(), IoError> {
239 let metadata = file.metadata().map_err(|source| IoError::MetadataError {
240 path: path_buf.to_path_buf(),
241 source,
242 })?;
243
244 Self::check_metadata(&metadata, path_buf)
245 }
246
247 fn check_metadata(metadata: &std::fs::Metadata, reported_path: &Path) -> Result<(), IoError> {
255 if !metadata.is_file() {
257 let file_type = if metadata.is_dir() {
258 "directory".to_string()
259 } else if metadata.is_symlink() {
260 "symlink".to_string()
261 } else {
262 Self::detect_special_file_type(metadata)
264 };
265
266 return Err(IoError::InvalidFileType {
267 path: reported_path.to_path_buf(),
268 file_type,
269 });
270 }
271
272 let file_size = metadata.len();
273
274 if file_size == 0 {
276 return Err(IoError::EmptyFile {
277 path: reported_path.to_path_buf(),
278 });
279 }
280
281 if file_size > Self::MAX_FILE_SIZE {
283 return Err(IoError::FileTooLarge {
284 path: reported_path.to_path_buf(),
285 size: file_size,
286 max_size: Self::MAX_FILE_SIZE,
287 });
288 }
289
290 Ok(())
291 }
292
293 fn detect_special_file_type(metadata: &std::fs::Metadata) -> String {
295 #[cfg(unix)]
296 {
297 use std::os::unix::fs::FileTypeExt;
298 if metadata.file_type().is_block_device() {
299 "block device".to_string()
300 } else if metadata.file_type().is_char_device() {
301 "character device".to_string()
302 } else if metadata.file_type().is_fifo() {
303 "FIFO/pipe".to_string()
304 } else if metadata.file_type().is_socket() {
305 "socket".to_string()
306 } else {
307 "special file".to_string()
308 }
309 }
310 #[cfg(windows)]
311 {
312 if metadata.file_type().is_symlink() {
313 "symlink".to_string()
314 } else {
315 "special file".to_string()
316 }
317 }
318 #[cfg(not(any(unix, windows)))]
319 {
320 "special file".to_string()
321 }
322 }
323
324 #[cfg(test)]
335 pub(crate) fn create_symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
336 original: P,
337 link: Q,
338 ) -> Result<(), std::io::Error> {
339 #[cfg(unix)]
340 {
341 std::os::unix::fs::symlink(original, link)
342 }
343 #[cfg(windows)]
344 {
345 let original_path = original.as_ref();
346
347 if original_path.is_dir() {
348 std::os::windows::fs::symlink_dir(original, link)
349 } else {
350 std::os::windows::fs::symlink_file(original, link)
351 }
352 }
353 #[cfg(not(any(unix, windows)))]
354 {
355 Err(std::io::Error::new(
356 std::io::ErrorKind::Unsupported,
357 "Symlinks not supported on this platform",
358 ))
359 }
360 }
361
362 fn create_memory_mapping(file: &File, path_buf: &Path) -> Result<Mmap, IoError> {
371 #[allow(unsafe_code)]
376 unsafe {
377 MmapOptions::new().map(file).map_err(|source| {
378 let sanitized_path = path_buf.file_name().map_or_else(
380 || "<unknown>".to_string(),
381 |name| name.to_string_lossy().into_owned(),
382 );
383
384 IoError::MmapError {
385 path: PathBuf::from(sanitized_path),
386 source,
387 }
388 })
389 }
390 }
391
392 #[must_use]
409 pub fn as_slice(&self) -> &[u8] {
410 &self.mmap
411 }
412
413 #[must_use]
426 pub fn path(&self) -> &Path {
427 &self.path
428 }
429
430 #[must_use]
443 pub fn len(&self) -> usize {
444 self.mmap.len()
445 }
446
447 #[must_use]
463 pub fn is_empty(&self) -> bool {
464 self.mmap.is_empty()
465 }
466}
467
468pub fn safe_read_bytes(
506 buffer: &[u8],
507 offset: BufferOffset,
508 length: BufferLength,
509) -> Result<&[u8], IoError> {
510 validate_buffer_access(buffer.len(), offset, length)?;
511 let end_offset = offset + length; Ok(buffer.get(offset..end_offset).unwrap_or(&[]))
515}
516
517pub fn safe_read_byte(buffer: &[u8], offset: BufferOffset) -> Result<u8, IoError> {
547 buffer.get(offset).copied().ok_or(IoError::BufferOverrun {
548 offset,
549 length: 1,
550 buffer_size: buffer.len(),
551 })
552}
553
554pub fn validate_buffer_access(
592 buffer_size: BufferLength,
593 offset: BufferOffset,
594 length: BufferLength,
595) -> Result<(), IoError> {
596 if length == 0 {
598 return Err(IoError::InvalidAccess { offset, length });
599 }
600
601 if offset >= buffer_size {
603 return Err(IoError::BufferOverrun {
604 offset,
605 length,
606 buffer_size,
607 });
608 }
609
610 let end_offset = offset
612 .checked_add(length)
613 .ok_or(IoError::InvalidAccess { offset, length })?;
614
615 if end_offset > buffer_size {
617 return Err(IoError::BufferOverrun {
618 offset,
619 length,
620 buffer_size,
621 });
622 }
623
624 Ok(())
625}
626
627#[cfg(test)]
628mod tests {
629 #![allow(
632 clippy::integer_division,
633 clippy::let_underscore_must_use,
634 clippy::semicolon_outside_block
635 )]
636
637 use super::*;
638 use std::fs;
639 use std::io::Write;
640 use std::sync::atomic::{AtomicU64, Ordering};
641
642 static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
644
645 fn create_temp_file(content: &[u8]) -> PathBuf {
648 let temp_dir = std::env::temp_dir();
649 let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
650 let file_path = temp_dir.join(format!("libmagic_test_{}_{id}", std::process::id()));
651
652 {
653 let mut file = File::create(&file_path).expect("Failed to create temp file");
654 file.write_all(content).expect("Failed to write temp file");
655 file.sync_all().expect("Failed to sync temp file");
656 } file_path
659 }
660
661 fn cleanup_temp_file(path: &Path) {
663 let _ = fs::remove_file(path);
664 }
665
666 #[test]
667 fn test_file_buffer_creation_success() {
668 let content = b"Hello, World!";
669 let temp_path = create_temp_file(content);
670
671 let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
672
673 assert_eq!(buffer.as_slice(), content);
674 assert_eq!(buffer.len(), content.len());
675 assert!(!buffer.is_empty());
676 assert_eq!(buffer.path(), temp_path.as_path());
677
678 cleanup_temp_file(&temp_path);
679 }
680
681 #[test]
682 fn test_file_buffer_nonexistent_file() {
683 let nonexistent_path = Path::new("/nonexistent/file.bin");
684
685 let result = FileBuffer::new(nonexistent_path);
686
687 assert!(result.is_err());
688 match result.unwrap_err() {
689 IoError::FileOpenError { path, .. } => {
690 assert_eq!(path, nonexistent_path);
691 }
692 other => panic!("Expected FileOpenError, got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn test_file_buffer_empty_file() {
698 let temp_path = create_temp_file(&[]);
699
700 let result = FileBuffer::new(&temp_path);
701
702 assert!(result.is_err());
703 match result.unwrap_err() {
704 IoError::EmptyFile { path } => {
705 assert_eq!(path, temp_path);
709 }
710 other => panic!("Expected EmptyFile error, got {other:?}"),
711 }
712
713 cleanup_temp_file(&temp_path);
714 }
715
716 #[test]
717 fn test_file_buffer_large_file() {
718 let content = vec![0u8; 1024]; let temp_path = create_temp_file(&content);
721
722 let buffer =
723 FileBuffer::new(&temp_path).expect("Failed to create FileBuffer for normal file");
724 assert_eq!(buffer.len(), 1024);
725
726 cleanup_temp_file(&temp_path);
727 }
728
729 #[test]
730 fn test_file_buffer_binary_content() {
731 let content = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
732 let temp_path = create_temp_file(&content);
733
734 let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
735
736 assert_eq!(buffer.as_slice(), content.as_slice());
737 assert_eq!(buffer.as_slice()[0], 0x00);
738 assert_eq!(buffer.as_slice()[7], 0xFC);
739
740 cleanup_temp_file(&temp_path);
741 }
742
743 #[test]
744 fn test_io_error_display() {
745 let path = PathBuf::from("/test/path");
746 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
747
748 let error = IoError::FileOpenError {
749 path,
750 source: io_err,
751 };
752
753 let error_string = format!("{error}");
754 assert!(error_string.contains("/test/path"));
755 assert!(error_string.contains("Failed to open file"));
756 }
757
758 #[test]
759 fn test_empty_file_error_display() {
760 let path = PathBuf::from("/test/empty.bin");
761 let error = IoError::EmptyFile { path };
762
763 let error_string = format!("{error}");
764 assert!(error_string.contains("/test/empty.bin"));
765 assert!(error_string.contains("is empty"));
766 }
767
768 #[test]
769 fn test_file_too_large_error_display() {
770 let path = PathBuf::from("/test/large.bin");
771 let error = IoError::FileTooLarge {
772 path,
773 size: 2_000_000_000,
774 max_size: 1_000_000_000,
775 };
776
777 let error_string = format!("{error}");
778 assert!(error_string.contains("/test/large.bin"));
779 assert!(error_string.contains("too large"));
780 assert!(error_string.contains("2000000000"));
781 assert!(error_string.contains("1000000000"));
782 }
783
784 #[test]
785 fn test_safe_read_bytes_success() {
786 let buffer = b"Hello, World!";
787
788 let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
790 assert_eq!(result, b"Hello");
791
792 let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
794 assert_eq!(result, b"World");
795
796 let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
798 assert_eq!(result, b"H");
799
800 let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
802 assert_eq!(result, buffer);
803
804 let result = safe_read_bytes(buffer, buffer.len() - 1, 1).expect("Failed to read bytes");
806 assert_eq!(result, b"!");
807 }
808
809 #[test]
810 fn test_safe_read_bytes_out_of_bounds() {
811 let buffer = b"Hello";
812
813 let result = safe_read_bytes(buffer, 10, 1);
815 assert!(result.is_err());
816 match result.unwrap_err() {
817 IoError::BufferOverrun {
818 offset,
819 length,
820 buffer_size,
821 } => {
822 assert_eq!(offset, 10);
823 assert_eq!(length, 1);
824 assert_eq!(buffer_size, 5);
825 }
826 other => panic!("Expected BufferOverrun, got {other:?}"),
827 }
828
829 let result = safe_read_bytes(buffer, 3, 5);
831 assert!(result.is_err());
832 match result.unwrap_err() {
833 IoError::BufferOverrun {
834 offset,
835 length,
836 buffer_size,
837 } => {
838 assert_eq!(offset, 3);
839 assert_eq!(length, 5);
840 assert_eq!(buffer_size, 5);
841 }
842 other => panic!("Expected BufferOverrun, got {other:?}"),
843 }
844
845 let result = safe_read_bytes(buffer, 5, 1);
847 assert!(result.is_err());
848 }
849
850 #[test]
851 fn test_safe_read_bytes_zero_length() {
852 let buffer = b"Hello";
853
854 let result = safe_read_bytes(buffer, 0, 0);
855 assert!(result.is_err());
856 match result.unwrap_err() {
857 IoError::InvalidAccess { offset, length } => {
858 assert_eq!(offset, 0);
859 assert_eq!(length, 0);
860 }
861 other => panic!("Expected InvalidAccess, got {other:?}"),
862 }
863 }
864
865 #[test]
866 fn test_safe_read_bytes_overflow() {
867 let buffer = b"Hello";
868
869 let result = safe_read_bytes(buffer, usize::MAX, 1);
872 assert!(result.is_err());
873 match result.unwrap_err() {
874 IoError::BufferOverrun { .. } => {
875 }
877 other => panic!("Expected BufferOverrun, got {other:?}"),
878 }
879
880 let result = safe_read_bytes(buffer, 1, usize::MAX);
882 assert!(result.is_err());
883 match result.unwrap_err() {
884 IoError::InvalidAccess { .. } => {
885 }
887 other => panic!("Expected InvalidAccess, got {other:?}"),
888 }
889
890 let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
892 assert!(result.is_err());
893 match result.unwrap_err() {
894 IoError::InvalidAccess { .. } => {
895 }
897 other => panic!("Expected InvalidAccess, got {other:?}"),
898 }
899 }
900
901 #[test]
902 fn test_safe_read_byte_success() {
903 let buffer = b"Hello";
904
905 assert_eq!(safe_read_byte(buffer, 0).unwrap(), b'H');
906 assert_eq!(safe_read_byte(buffer, 1).unwrap(), b'e');
907 assert_eq!(safe_read_byte(buffer, 4).unwrap(), b'o');
908 }
909
910 #[test]
911 fn test_safe_read_byte_out_of_bounds() {
912 let buffer = b"Hello";
913
914 let result = safe_read_byte(buffer, 5);
915 assert!(result.is_err());
916 match result.unwrap_err() {
917 IoError::BufferOverrun {
918 offset,
919 length,
920 buffer_size,
921 } => {
922 assert_eq!(offset, 5);
923 assert_eq!(length, 1);
924 assert_eq!(buffer_size, 5);
925 }
926 other => panic!("Expected BufferOverrun, got {other:?}"),
927 }
928
929 let result = safe_read_byte(buffer, 100);
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn test_validate_buffer_access_success() {
935 validate_buffer_access(100, 0, 50).expect("Should be valid");
937 validate_buffer_access(100, 50, 50).expect("Should be valid");
938 validate_buffer_access(100, 99, 1).expect("Should be valid");
939 validate_buffer_access(10, 0, 10).expect("Should be valid");
940 validate_buffer_access(1, 0, 1).expect("Should be valid");
941 }
942
943 #[test]
944 fn test_validate_buffer_access_invalid() {
945 let result = validate_buffer_access(100, 0, 0);
947 assert!(result.is_err());
948
949 let result = validate_buffer_access(100, 100, 1);
951 assert!(result.is_err());
952
953 let result = validate_buffer_access(100, 50, 51);
955 assert!(result.is_err());
956
957 let result = validate_buffer_access(100, usize::MAX, 1);
959 assert!(result.is_err());
960
961 let result = validate_buffer_access(100, 1, usize::MAX);
962 assert!(result.is_err());
963 }
964
965 #[test]
966 fn test_validate_buffer_access_edge_cases() {
967 let result = validate_buffer_access(0, 0, 1);
969 assert!(result.is_err());
970
971 let large_size = 1_000_000;
973 validate_buffer_access(large_size, 0, large_size).expect("Should be valid");
974 validate_buffer_access(large_size, large_size - 1, 1).expect("Should be valid");
975
976 let result = validate_buffer_access(large_size, large_size - 1, 2);
978 assert!(result.is_err());
979 }
980
981 #[test]
982 fn test_buffer_access_security_patterns() {
983 let buffer_size = 1024;
985
986 let overflow_patterns = vec![
988 (usize::MAX, 1), (buffer_size, usize::MAX), (usize::MAX - 1, 2), ];
992
993 for (offset, length) in overflow_patterns {
994 let result = validate_buffer_access(buffer_size, offset, length);
995 assert!(
996 result.is_err(),
997 "Should reject potentially dangerous access pattern: offset={offset}, length={length}"
998 );
999 }
1000
1001 let safe_patterns = vec![
1003 (0, 1), (buffer_size - 1, 1), (buffer_size / 2, 1), ];
1007
1008 for (offset, length) in safe_patterns {
1009 let result = validate_buffer_access(buffer_size, offset, length);
1010 assert!(
1011 result.is_ok(),
1012 "Should accept safe access pattern: offset={offset}, length={length}"
1013 );
1014 }
1015 }
1016
1017 #[test]
1018 fn test_buffer_overrun_error_display() {
1019 let error = IoError::BufferOverrun {
1020 offset: 10,
1021 length: 5,
1022 buffer_size: 12,
1023 };
1024
1025 let error_string = format!("{error}");
1026 assert!(error_string.contains("Buffer access out of bounds"));
1027 assert!(error_string.contains("offset 10"));
1028 assert!(error_string.contains("length 5"));
1029 assert!(error_string.contains("buffer size 12"));
1030 }
1031
1032 #[test]
1033 fn test_invalid_access_error_display() {
1034 let error = IoError::InvalidAccess {
1035 offset: 0,
1036 length: 0,
1037 };
1038
1039 let error_string = format!("{error}");
1040 assert!(error_string.contains("Invalid buffer access parameters"));
1041 assert!(error_string.contains("offset 0"));
1042 assert!(error_string.contains("length 0"));
1043 }
1044
1045 #[test]
1046 fn test_invalid_file_type_error_display() {
1047 let error = IoError::InvalidFileType {
1048 path: std::path::PathBuf::from("/dev/null"),
1049 file_type: "character device".to_string(),
1050 };
1051
1052 let error_string = format!("{error}");
1053 assert!(error_string.contains("is not a regular file"));
1054 assert!(error_string.contains("/dev/null"));
1055 assert!(error_string.contains("character device"));
1056 }
1057
1058 #[test]
1059 fn test_file_buffer_directory_rejection() {
1060 let temp_dir = std::env::temp_dir().join("test_dir_12345");
1062 std::fs::create_dir_all(&temp_dir).unwrap();
1063
1064 let result = FileBuffer::new(&temp_dir);
1065
1066 assert!(result.is_err());
1067 match result.unwrap_err() {
1068 IoError::InvalidFileType { path, file_type } => {
1069 assert_eq!(file_type, "directory");
1070 assert_eq!(path, temp_dir);
1073 }
1074 IoError::FileOpenError { .. } => {
1075 println!(
1078 "Directory test skipped on this platform (can't open directories as files)"
1079 );
1080 }
1081 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1082 }
1083
1084 std::fs::remove_dir(&temp_dir).unwrap();
1086 }
1087
1088 #[test]
1089 fn test_file_buffer_symlink_to_directory_rejection() {
1090 let temp_dir = std::env::temp_dir().join("test_dir_symlink_12345");
1092 let symlink_path = std::env::temp_dir().join("test_symlink_12345");
1093
1094 std::fs::create_dir_all(&temp_dir).unwrap();
1095
1096 let symlink_result = FileBuffer::create_symlink(&temp_dir, &symlink_path);
1098
1099 match symlink_result {
1100 Ok(()) => {
1101 let result = FileBuffer::new(&symlink_path);
1102
1103 assert!(result.is_err());
1104 match result.unwrap_err() {
1105 IoError::InvalidFileType { path, file_type } => {
1106 assert_eq!(file_type, "directory");
1107 assert_eq!(path, symlink_path);
1110 }
1111 IoError::FileOpenError { .. } => {
1112 println!(
1115 "Directory symlink test skipped on this platform (can't open directories as files)"
1116 );
1117 }
1118 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1119 }
1120
1121 let _ = std::fs::remove_file(&symlink_path);
1123 }
1124 Err(_) => {
1125 println!(
1127 "Skipping symlink test - unable to create symlink (may need admin privileges)"
1128 );
1129 }
1130 }
1131
1132 std::fs::remove_dir(&temp_dir).unwrap();
1134 }
1135
1136 #[test]
1137 fn test_file_buffer_symlink_to_regular_file_success() {
1138 let temp_file = std::env::temp_dir().join("test_file_symlink_12345");
1140 let symlink_path = std::env::temp_dir().join("test_symlink_file_12345");
1141
1142 let content = b"test content";
1143 std::fs::write(&temp_file, content).unwrap();
1144
1145 let symlink_result = FileBuffer::create_symlink(&temp_file, &symlink_path);
1147
1148 match symlink_result {
1149 Ok(()) => {
1150 let result = FileBuffer::new(&symlink_path);
1151
1152 assert!(result.is_ok());
1153 let buffer = result.unwrap();
1154 assert_eq!(buffer.as_slice(), content);
1155
1156 let _ = std::fs::remove_file(&symlink_path);
1158 }
1159 Err(_) => {
1160 println!(
1162 "Skipping symlink test - unable to create symlink (may need admin privileges)"
1163 );
1164 }
1165 }
1166
1167 std::fs::remove_file(&temp_file).unwrap();
1169 }
1170
1171 #[test]
1172 fn test_file_buffer_special_files_rejection() {
1173 #[cfg(unix)]
1175 {
1176 let result = FileBuffer::new(std::path::Path::new("/dev/null"));
1178 assert!(result.is_err());
1179 match result.unwrap_err() {
1180 IoError::InvalidFileType { path, file_type } => {
1181 assert_eq!(file_type, "character device");
1182 assert_eq!(path, std::path::PathBuf::from("/dev/null"));
1183 }
1184 other => panic!("Expected InvalidFileType error, got {other:?}"),
1185 }
1186
1187 let result = FileBuffer::new(std::path::Path::new("/dev/zero"));
1189 assert!(result.is_err());
1190 match result.unwrap_err() {
1191 IoError::InvalidFileType { path, file_type } => {
1192 assert_eq!(file_type, "character device");
1193 assert_eq!(path, std::path::PathBuf::from("/dev/zero"));
1194 }
1195 other => panic!("Expected InvalidFileType error, got {other:?}"),
1196 }
1197
1198 let result = FileBuffer::new(std::path::Path::new("/dev/random"));
1200 assert!(result.is_err());
1201 match result.unwrap_err() {
1202 IoError::InvalidFileType { path, file_type } => {
1203 assert_eq!(file_type, "character device");
1204 assert_eq!(path, std::path::PathBuf::from("/dev/random"));
1205 }
1206 other => panic!("Expected InvalidFileType error, got {other:?}"),
1207 }
1208 }
1209
1210 #[cfg(not(unix))]
1211 {
1212 println!("Skipping special file tests on non-Unix platform");
1214 }
1215 }
1216
1217 #[test]
1218 fn test_file_buffer_cross_platform_special_files() {
1219 let temp_dir = std::env::temp_dir().join("test_special_dir_12345");
1224 std::fs::create_dir_all(&temp_dir).unwrap();
1225
1226 let result = FileBuffer::new(&temp_dir);
1227 assert!(result.is_err());
1228 match result.unwrap_err() {
1229 IoError::InvalidFileType { path, file_type } => {
1230 assert_eq!(file_type, "directory");
1231 assert_eq!(path, temp_dir);
1233 }
1234 IoError::FileOpenError { .. } => {
1235 println!(
1237 "Directory test skipped on this platform (can't open directories as files)"
1238 );
1239 }
1240 other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1241 }
1242
1243 std::fs::remove_dir(&temp_dir).unwrap();
1245 }
1246
1247 #[test]
1248 #[ignore = "FIFOs can cause hanging issues in CI environments"]
1249 fn test_file_buffer_fifo_rejection() {
1250 #[cfg(unix)]
1252 {
1253 use nix::unistd;
1254
1255 let fifo_path = std::env::temp_dir().join("test_fifo_12345");
1256
1257 match unistd::mkfifo(
1259 &fifo_path,
1260 nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
1261 ) {
1262 Ok(()) => {
1263 let result = FileBuffer::new(&fifo_path);
1264
1265 assert!(result.is_err());
1266 match result.unwrap_err() {
1267 IoError::InvalidFileType { path, file_type } => {
1268 assert_eq!(file_type, "FIFO/pipe");
1269 assert_eq!(path, fifo_path);
1271 }
1272 other => panic!("Expected InvalidFileType error, got {other:?}"),
1273 }
1274
1275 std::fs::remove_file(&fifo_path).unwrap();
1277 }
1278 Err(_) => {
1279 println!("Skipping FIFO test - unable to create FIFO");
1281 }
1282 }
1283 }
1284
1285 #[cfg(not(unix))]
1286 {
1287 println!("Skipping FIFO test on non-Unix platform");
1289 }
1290 }
1291}