Skip to main content

libmagic_rs/io/
mod.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! I/O utilities module
5//!
6//! This module provides efficient file access utilities including memory-mapped
7//! file I/O for optimal performance.
8
9use memmap2::{Mmap, MmapOptions};
10use std::fs::File;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14/// Type alias for buffer offset positions
15type BufferOffset = usize;
16
17/// Type alias for buffer lengths
18type BufferLength = usize;
19
20/// Type alias for file sizes in bytes
21type FileSize = u64;
22
23/// Internal trait for safe buffer operations
24trait SafeBufferAccess {
25    /// Validates that an access operation is within bounds
26    fn validate_access(&self, offset: BufferOffset, length: BufferLength) -> Result<(), IoError>;
27
28    /// Gets a safe slice from the buffer
29    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; // Safe after validation
40        Ok(&self[offset..end_offset])
41    }
42}
43
44/// Errors that can occur during file I/O operations
45#[derive(Debug, Error)]
46pub enum IoError {
47    /// File could not be opened for reading
48    #[error("Failed to open file '{path}': {source}")]
49    FileOpenError {
50        /// Path to the file that could not be opened
51        path: PathBuf,
52        /// Underlying I/O error
53        #[source]
54        source: std::io::Error,
55    },
56
57    /// File could not be memory-mapped
58    #[error("Failed to memory-map file '{path}': {source}")]
59    MmapError {
60        /// Path to the file that could not be mapped
61        path: PathBuf,
62        /// Underlying I/O error
63        #[source]
64        source: std::io::Error,
65    },
66
67    /// File is empty and cannot be processed
68    #[error("File '{path}' is empty")]
69    EmptyFile {
70        /// Path to the empty file
71        path: PathBuf,
72    },
73
74    /// File is too large to be processed safely
75    #[error("File '{path}' is too large ({size} bytes, maximum {max_size} bytes)")]
76    FileTooLarge {
77        /// Path to the file that is too large
78        path: PathBuf,
79        /// Actual file size in bytes
80        size: FileSize,
81        /// Maximum allowed file size in bytes
82        max_size: FileSize,
83    },
84
85    /// File metadata could not be read
86    #[error("Failed to read metadata for file '{path}': {source}")]
87    MetadataError {
88        /// Path to the file whose metadata could not be read
89        path: PathBuf,
90        /// Underlying I/O error
91        #[source]
92        source: std::io::Error,
93    },
94
95    /// Buffer access out of bounds
96    #[error(
97        "Buffer access out of bounds: offset {offset} + length {length} > buffer size {buffer_size}"
98    )]
99    BufferOverrun {
100        /// Requested offset
101        offset: BufferOffset,
102        /// Requested length
103        length: BufferLength,
104        /// Actual buffer size
105        buffer_size: BufferLength,
106    },
107
108    /// Invalid offset or length parameter
109    #[error("Invalid buffer access parameters: offset {offset}, length {length}")]
110    InvalidAccess {
111        /// Requested offset
112        offset: BufferOffset,
113        /// Requested length
114        length: BufferLength,
115    },
116
117    /// File is not a regular file (e.g., device node, FIFO, symlink to special file)
118    #[error("File '{path}' is not a regular file (file type: {file_type})")]
119    InvalidFileType {
120        /// Path to the file that is not a regular file
121        path: PathBuf,
122        /// Description of the file type
123        file_type: String,
124    },
125}
126
127/// A memory-mapped file buffer for efficient file access
128///
129/// This struct provides safe access to file contents through memory mapping,
130/// which avoids loading the entire file into memory while providing fast
131/// random access to file data.
132///
133/// # Examples
134///
135/// ```no_run
136/// use libmagic_rs::io::FileBuffer;
137/// use std::path::Path;
138///
139/// let buffer = FileBuffer::new(Path::new("example.bin"))?;
140/// let data = buffer.as_slice();
141/// println!("File size: {} bytes", data.len());
142/// # Ok::<(), Box<dyn std::error::Error>>(())
143/// ```
144#[derive(Debug)]
145pub struct FileBuffer {
146    /// Memory-mapped file data
147    mmap: Mmap,
148    /// Path to the file for error reporting
149    path: PathBuf,
150}
151
152impl FileBuffer {
153    /// Maximum file size that can be processed (1 GB)
154    ///
155    /// This limit prevents memory exhaustion attacks and ensures reasonable
156    /// processing times. Files larger than this are likely not suitable for
157    /// magic rule evaluation and may indicate malicious input.
158    const MAX_FILE_SIZE: FileSize = 1024 * 1024 * 1024;
159
160    /// Maximum number of concurrent file mappings to prevent resource exhaustion
161    /// TODO: Implement concurrent mapping tracking in future versions
162    #[allow(dead_code)]
163    const MAX_CONCURRENT_MAPPINGS: usize = 100;
164
165    // TODO: Consider implementing adaptive I/O strategy for small files
166    // Files smaller than 4KB might benefit from regular read() instead of mmap
167    // This would require benchmarking to determine the optimal threshold
168    #[allow(dead_code)]
169    const SMALL_FILE_THRESHOLD: u64 = 4096;
170
171    /// Creates a new memory-mapped file buffer
172    ///
173    /// # Arguments
174    ///
175    /// * `path` - Path to the file to be mapped
176    ///
177    /// # Returns
178    ///
179    /// Returns a `FileBuffer` on success, or an `IoError` if the file cannot
180    /// be opened or mapped.
181    ///
182    /// # Errors
183    ///
184    /// This function will return an error if:
185    /// - The file does not exist or cannot be opened
186    /// - The file cannot be memory-mapped
187    /// - The file is empty
188    /// - The file is larger than the maximum allowed size
189    /// - File metadata cannot be read
190    ///
191    /// # Examples
192    ///
193    /// ```no_run
194    /// use libmagic_rs::io::FileBuffer;
195    /// use std::path::Path;
196    ///
197    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
198    /// # Ok::<(), Box<dyn std::error::Error>>(())
199    /// ```
200    pub fn new(path: &Path) -> Result<Self, IoError> {
201        // TODO: Add additional error handling for edge cases:
202        // - Handle symbolic links and their resolution
203        // - Add validation for path length limits on different platforms
204        // - Handle special files (devices, pipes, etc.) gracefully
205        // - Add retry logic for transient I/O errors
206        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    /// Opens a file for reading with proper error handling
219    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    /// Validates file metadata and ensures file is suitable for memory mapping
227    fn validate_file_metadata(_file: &File, path_buf: &Path) -> Result<(), IoError> {
228        // Resolve symlinks to get the actual target file
229        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        // Get metadata for the canonical path to ensure we're checking the actual file
236        let metadata =
237            std::fs::metadata(&canonical_path).map_err(|source| IoError::MetadataError {
238                path: canonical_path.clone(),
239                source,
240            })?;
241
242        // Check if the target is a regular file
243        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                // Check for other special file types (cross-platform)
250                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        // TODO: Add more comprehensive file validation:
262        // - Validate file permissions for reading
263        // - Handle sparse files and their actual disk usage
264        // - Add warnings for files that might be too small for meaningful analysis
265
266        // Check if file is empty
267        if file_size == 0 {
268            return Err(IoError::EmptyFile {
269                path: canonical_path,
270            });
271        }
272
273        // Check if file is too large
274        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    /// Detects special file types in a cross-platform manner
286    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    /// Creates a symlink in a cross-platform manner
317    ///
318    /// # Arguments
319    /// * `original` - The path to the original file or directory
320    /// * `link` - The path where the symlink should be created
321    ///
322    /// # Errors
323    /// * Returns `std::io::Error` if symlink creation fails (e.g., insufficient permissions)
324    /// * On Windows, may require admin privileges or developer mode enabled
325    /// * On non-Unix/Windows platforms, returns an "Unsupported" error
326    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    /// Creates memory mapping for the file
354    ///
355    /// # Security
356    ///
357    /// This function uses memory mapping which provides several security benefits:
358    /// - Avoids loading entire files into memory, reducing memory exhaustion attacks
359    /// - Provides read-only access to file contents
360    /// - Leverages OS-level memory protection mechanisms
361    fn create_memory_mapping(file: &File, path_buf: &Path) -> Result<Mmap, IoError> {
362        // SAFETY: We use safe memory mapping through memmap2, which handles
363        // the unsafe operations internally with proper error checking.
364        // The memmap2 crate is a vetted dependency that provides safe abstractions
365        // over unsafe memory mapping operations.
366        #[allow(unsafe_code)]
367        unsafe {
368            MmapOptions::new().map(file).map_err(|source| {
369                // Sanitize error message to avoid leaking sensitive path information
370                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    /// Returns the file contents as a byte slice
384    ///
385    /// This provides safe access to the memory-mapped file data without
386    /// copying the contents.
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// use libmagic_rs::io::FileBuffer;
392    /// use std::path::Path;
393    ///
394    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
395    /// let data = buffer.as_slice();
396    /// println!("First byte: 0x{:02x}", data[0]);
397    /// # Ok::<(), Box<dyn std::error::Error>>(())
398    /// ```
399    #[must_use]
400    pub fn as_slice(&self) -> &[u8] {
401        &self.mmap
402    }
403
404    /// Returns the path of the file
405    ///
406    /// # Examples
407    ///
408    /// ```no_run
409    /// use libmagic_rs::io::FileBuffer;
410    /// use std::path::Path;
411    ///
412    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
413    /// println!("File path: {}", buffer.path().display());
414    /// # Ok::<(), Box<dyn std::error::Error>>(())
415    /// ```
416    #[must_use]
417    pub fn path(&self) -> &Path {
418        &self.path
419    }
420
421    /// Returns the size of the file in bytes
422    ///
423    /// # Examples
424    ///
425    /// ```no_run
426    /// use libmagic_rs::io::FileBuffer;
427    /// use std::path::Path;
428    ///
429    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
430    /// println!("File size: {} bytes", buffer.len());
431    /// # Ok::<(), Box<dyn std::error::Error>>(())
432    /// ```
433    #[must_use]
434    pub fn len(&self) -> usize {
435        self.mmap.len()
436    }
437
438    /// Returns true if the file is empty
439    ///
440    /// Note: This should never return true for a successfully created `FileBuffer`,
441    /// as empty files are rejected during construction.
442    ///
443    /// # Examples
444    ///
445    /// ```no_run
446    /// use libmagic_rs::io::FileBuffer;
447    /// use std::path::Path;
448    ///
449    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
450    /// assert!(!buffer.is_empty()); // Should always be false for valid buffers
451    /// # Ok::<(), Box<dyn std::error::Error>>(())
452    /// ```
453    #[must_use]
454    pub fn is_empty(&self) -> bool {
455        self.mmap.is_empty()
456    }
457}
458
459/// Safely reads bytes from a buffer with bounds checking
460///
461/// This function provides safe access to buffer data with comprehensive
462/// bounds checking to prevent buffer overruns and invalid access patterns.
463///
464/// # Arguments
465///
466/// * `buffer` - The buffer to read from
467/// * `offset` - Starting offset in the buffer
468/// * `length` - Number of bytes to read
469///
470/// # Returns
471///
472/// Returns a slice of the requested bytes on success, or an `IoError` if
473/// the access would be out of bounds.
474///
475/// # Errors
476///
477/// This function will return an error if:
478/// - The offset is beyond the buffer size
479/// - The length would cause an overflow
480/// - The offset + length exceeds the buffer size
481/// - The length is zero (invalid access)
482///
483/// # Examples
484///
485/// ```
486/// use libmagic_rs::io::safe_read_bytes;
487///
488/// let buffer = b"Hello, World!";
489/// let result = safe_read_bytes(buffer, 0, 5)?;
490/// assert_eq!(result, b"Hello");
491///
492/// let result = safe_read_bytes(buffer, 7, 6)?;
493/// assert_eq!(result, b"World!");
494/// # Ok::<(), Box<dyn std::error::Error>>(())
495/// ```
496pub fn safe_read_bytes(
497    buffer: &[u8],
498    offset: BufferOffset,
499    length: BufferLength,
500) -> Result<&[u8], IoError> {
501    // TODO: Add performance monitoring and warnings:
502    // - Log warnings for very large read operations that might impact performance
503    // - Add metrics collection for buffer access patterns
504    // - Consider caching frequently accessed buffer regions
505    buffer.get_safe_slice(offset, length)
506}
507
508/// Safely reads a single byte from a buffer with bounds checking
509///
510/// This is a convenience function for reading a single byte with proper
511/// bounds checking.
512///
513/// # Arguments
514///
515/// * `buffer` - The buffer to read from
516/// * `offset` - Offset of the byte to read
517///
518/// # Returns
519///
520/// Returns the byte at the specified offset on success, or an `IoError` if
521/// the access would be out of bounds.
522///
523/// # Errors
524///
525/// This function will return an error if the offset is beyond the buffer size.
526///
527/// # Examples
528///
529/// ```
530/// use libmagic_rs::io::safe_read_byte;
531///
532/// let buffer = b"Hello";
533/// let byte = safe_read_byte(buffer, 0)?;
534/// assert_eq!(byte, b'H');
535/// # Ok::<(), Box<dyn std::error::Error>>(())
536/// ```
537pub 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
545/// Validates buffer access parameters without performing the actual read
546///
547/// This function can be used to validate buffer access parameters before
548/// performing the actual read operation.
549///
550/// # Arguments
551///
552/// * `buffer_size` - Size of the buffer
553/// * `offset` - Starting offset
554/// * `length` - Number of bytes to access
555///
556/// # Returns
557///
558/// Returns `Ok(())` if the access is valid, or an `IoError` if it would
559/// be out of bounds.
560///
561/// # Errors
562///
563/// This function will return an error if:
564/// - The offset is beyond the buffer size
565/// - The length would cause an overflow
566/// - The offset + length exceeds the buffer size
567/// - The length is zero (invalid access)
568///
569/// # Examples
570///
571/// ```
572/// use libmagic_rs::io::validate_buffer_access;
573///
574/// // Valid access
575/// validate_buffer_access(100, 10, 20)?;
576///
577/// // Invalid access - would go beyond buffer
578/// let result = validate_buffer_access(100, 90, 20);
579/// assert!(result.is_err());
580/// # Ok::<(), Box<dyn std::error::Error>>(())
581/// ```
582pub fn validate_buffer_access(
583    buffer_size: BufferLength,
584    offset: BufferOffset,
585    length: BufferLength,
586) -> Result<(), IoError> {
587    // Check for zero length (invalid access)
588    if length == 0 {
589        return Err(IoError::InvalidAccess { offset, length });
590    }
591
592    // Check if offset is within buffer bounds
593    if offset >= buffer_size {
594        return Err(IoError::BufferOverrun {
595            offset,
596            length,
597            buffer_size,
598        });
599    }
600
601    // Check for potential overflow in offset + length calculation
602    let end_offset = offset
603        .checked_add(length)
604        .ok_or(IoError::InvalidAccess { offset, length })?;
605
606    // Check if the end offset is within buffer bounds
607    if end_offset > buffer_size {
608        return Err(IoError::BufferOverrun {
609            offset,
610            length,
611            buffer_size,
612        });
613    }
614
615    Ok(())
616}
617
618// RAII cleanup is handled automatically by the Drop trait implementation
619// of Mmap, which properly unmaps the memory and closes file handles.
620// This implementation is kept explicit for documentation purposes.
621impl Drop for FileBuffer {
622    fn drop(&mut self) {
623        // Mmap handles cleanup automatically through its Drop implementation
624        // The memory mapping is safely unmapped and file handles are closed
625        // No explicit cleanup needed here due to RAII patterns
626    }
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    /// Monotonic counter for unique temp file names across parallel tests.
637    static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
638
639    /// Helper function to create a temporary file with given content.
640    /// Uses a monotonic counter + process ID for guaranteed uniqueness.
641    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 is closed here when it goes out of scope
651
652        file_path
653    }
654
655    /// Helper function to clean up temporary file
656    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                // The path should be canonicalized, so we need to canonicalize the temp_path for comparison
700                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        // Create a file with some content to test normal operation
712        let content = vec![0u8; 1024]; // 1KB file
713        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        // Read from beginning
782        let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
783        assert_eq!(result, b"Hello");
784
785        // Read from middle
786        let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
787        assert_eq!(result, b"World");
788
789        // Read single byte
790        let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
791        assert_eq!(result, b"H");
792
793        // Read entire buffer
794        let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
795        assert_eq!(result, buffer);
796
797        // Read from end
798        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        // Offset beyond buffer
807        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        // Length extends beyond buffer
823        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        // Offset at buffer boundary
839        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        // Test potential overflow in offset + length
863        // When offset is usize::MAX, it's beyond buffer bounds, so we get BufferOverrun
864        let result = safe_read_bytes(buffer, usize::MAX, 1);
865        assert!(result.is_err());
866        match result.unwrap_err() {
867            IoError::BufferOverrun { .. } => {
868                // This is expected since usize::MAX > buffer.len()
869            }
870            other => panic!("Expected BufferOverrun, got {other:?}"),
871        }
872
873        // Test overflow with valid offset but huge length
874        let result = safe_read_bytes(buffer, 1, usize::MAX);
875        assert!(result.is_err());
876        match result.unwrap_err() {
877            IoError::InvalidAccess { .. } => {
878                // This should trigger overflow in checked_add
879            }
880            other => panic!("Expected InvalidAccess, got {other:?}"),
881        }
882
883        // Test a case that would overflow but with smaller numbers
884        let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
885        assert!(result.is_err());
886        match result.unwrap_err() {
887            IoError::InvalidAccess { .. } => {
888                // This should trigger overflow in checked_add
889            }
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        // Valid accesses
929        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        // Zero length
939        let result = validate_buffer_access(100, 0, 0);
940        assert!(result.is_err());
941
942        // Offset beyond buffer
943        let result = validate_buffer_access(100, 100, 1);
944        assert!(result.is_err());
945
946        // Length extends beyond buffer
947        let result = validate_buffer_access(100, 50, 51);
948        assert!(result.is_err());
949
950        // Overflow conditions
951        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        // Empty buffer
961        let result = validate_buffer_access(0, 0, 1);
962        assert!(result.is_err());
963
964        // Large buffer, valid access
965        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        // Large buffer, invalid access
970        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        // Test patterns that could indicate security vulnerabilities
977        let buffer_size = 1024;
978
979        // Test potential integer overflow patterns
980        let overflow_patterns = vec![
981            (usize::MAX, 1),           // Maximum offset
982            (buffer_size, usize::MAX), // Maximum length
983            (usize::MAX - 1, 2),       // Near-overflow offset
984        ];
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        // Test boundary conditions that should be safe
995        let safe_patterns = vec![
996            (0, 1),               // Start of buffer
997            (buffer_size - 1, 1), // End of buffer
998            (buffer_size / 2, 1), // Middle of buffer
999        ];
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        // Create a temporary directory
1054        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                // The path should be canonicalized
1064                let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
1065                assert_eq!(path, canonical_temp_dir);
1066            }
1067            IoError::FileOpenError { .. } => {
1068                // On Windows, we can't open directories as files, so we get a FileOpenError
1069                // This is expected behavior, so we'll consider this test passed
1070                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        // Cleanup
1078        std::fs::remove_dir(&temp_dir).unwrap();
1079    }
1080
1081    #[test]
1082    fn test_file_buffer_symlink_to_directory_rejection() {
1083        // Create a temporary directory and a symlink to it
1084        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        // Create symlink (cross-platform approach)
1090        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                        // The path should be canonicalized to the target directory
1101                        let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
1102                        assert_eq!(path, canonical_temp_dir);
1103                    }
1104                    IoError::FileOpenError { .. } => {
1105                        // On Windows, we can't open directories as files, so we get a FileOpenError
1106                        // This is expected behavior, so we'll consider this test passed
1107                        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                // Cleanup
1115                let _ = std::fs::remove_file(&symlink_path);
1116            }
1117            Err(_) => {
1118                // Symlink creation failed (e.g., no admin privileges on Windows)
1119                println!(
1120                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1121                );
1122            }
1123        }
1124
1125        // Cleanup
1126        std::fs::remove_dir(&temp_dir).unwrap();
1127    }
1128
1129    #[test]
1130    fn test_file_buffer_symlink_to_regular_file_success() {
1131        // Create a temporary file and a symlink to it
1132        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        // Create symlink (cross-platform approach)
1139        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                // Cleanup
1150                let _ = std::fs::remove_file(&symlink_path);
1151            }
1152            Err(_) => {
1153                // Symlink creation failed (e.g., no admin privileges on Windows)
1154                println!(
1155                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1156                );
1157            }
1158        }
1159
1160        // Cleanup
1161        std::fs::remove_file(&temp_file).unwrap();
1162    }
1163
1164    #[test]
1165    fn test_file_buffer_special_files_rejection() {
1166        // Test rejection of special files that exist on Unix systems
1167        #[cfg(unix)]
1168        {
1169            // Test /dev/null (character device)
1170            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            // Test /dev/zero (character device)
1181            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            // Test /dev/random (character device)
1192            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            // On non-Unix systems, these special files don't exist
1206            println!("Skipping special file tests on non-Unix platform");
1207        }
1208    }
1209
1210    #[test]
1211    fn test_file_buffer_cross_platform_special_files() {
1212        // Test cross-platform special file detection
1213        // This test works on all platforms by creating temporary special files
1214
1215        // Test with a directory (works on all platforms)
1216        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                // On Windows, we can't open directories as files
1229                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        // Cleanup
1237        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        // Create a FIFO (named pipe) and test rejection
1244        #[cfg(unix)]
1245        {
1246            use nix::unistd;
1247
1248            let fifo_path = std::env::temp_dir().join("test_fifo_12345");
1249
1250            // Create a FIFO using nix crate
1251            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                    // Cleanup
1269                    std::fs::remove_file(&fifo_path).unwrap();
1270                }
1271                Err(_) => {
1272                    // If we can't create a FIFO, skip this test
1273                    println!("Skipping FIFO test - unable to create FIFO");
1274                }
1275            }
1276        }
1277
1278        #[cfg(not(unix))]
1279        {
1280            // On non-Unix systems, we can't create FIFOs easily, so we'll skip this test
1281            println!("Skipping FIFO test on non-Unix platform");
1282        }
1283    }
1284}