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/// Errors that can occur during file I/O operations
24#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum IoError {
27    /// File could not be opened for reading
28    #[error("Failed to open file '{path}': {source}")]
29    FileOpenError {
30        /// Path to the file that could not be opened
31        path: PathBuf,
32        /// Underlying I/O error
33        #[source]
34        source: std::io::Error,
35    },
36
37    /// File could not be memory-mapped
38    #[error("Failed to memory-map file '{path}': {source}")]
39    MmapError {
40        /// Path to the file that could not be mapped
41        path: PathBuf,
42        /// Underlying I/O error
43        #[source]
44        source: std::io::Error,
45    },
46
47    /// File is empty and cannot be processed
48    #[error("File '{path}' is empty")]
49    EmptyFile {
50        /// Path to the empty file
51        path: PathBuf,
52    },
53
54    /// File is too large to be processed safely
55    #[error("File '{path}' is too large ({size} bytes, maximum {max_size} bytes)")]
56    FileTooLarge {
57        /// Path to the file that is too large
58        path: PathBuf,
59        /// Actual file size in bytes
60        size: FileSize,
61        /// Maximum allowed file size in bytes
62        max_size: FileSize,
63    },
64
65    /// File metadata could not be read
66    #[error("Failed to read metadata for file '{path}': {source}")]
67    MetadataError {
68        /// Path to the file whose metadata could not be read
69        path: PathBuf,
70        /// Underlying I/O error
71        #[source]
72        source: std::io::Error,
73    },
74
75    /// Buffer access out of bounds
76    #[error(
77        "Buffer access out of bounds: offset {offset} + length {length} > buffer size {buffer_size}"
78    )]
79    BufferOverrun {
80        /// Requested offset
81        offset: BufferOffset,
82        /// Requested length
83        length: BufferLength,
84        /// Actual buffer size
85        buffer_size: BufferLength,
86    },
87
88    /// Invalid offset or length parameter
89    #[error("Invalid buffer access parameters: offset {offset}, length {length}")]
90    InvalidAccess {
91        /// Requested offset
92        offset: BufferOffset,
93        /// Requested length
94        length: BufferLength,
95    },
96
97    /// File is not a regular file (e.g., device node, FIFO, symlink to special file)
98    #[error("File '{path}' is not a regular file (file type: {file_type})")]
99    InvalidFileType {
100        /// Path to the file that is not a regular file
101        path: PathBuf,
102        /// Description of the file type
103        file_type: String,
104    },
105}
106
107/// A memory-mapped file buffer for efficient file access
108///
109/// This struct provides safe access to file contents through memory mapping,
110/// which avoids loading the entire file into memory while providing fast
111/// random access to file data.
112///
113/// # Examples
114///
115/// ```no_run
116/// use libmagic_rs::io::FileBuffer;
117/// use std::path::Path;
118///
119/// let buffer = FileBuffer::new(Path::new("example.bin"))?;
120/// let data = buffer.as_slice();
121/// println!("File size: {} bytes", data.len());
122/// # Ok::<(), Box<dyn std::error::Error>>(())
123/// ```
124#[derive(Debug)]
125pub struct FileBuffer {
126    /// Memory-mapped file data
127    mmap: Mmap,
128    /// Path to the file for error reporting
129    path: PathBuf,
130}
131
132impl FileBuffer {
133    /// Maximum file size that can be processed (1 GB)
134    ///
135    /// This limit prevents memory exhaustion attacks and ensures reasonable
136    /// processing times. Files larger than this are likely not suitable for
137    /// magic rule evaluation and may indicate malicious input.
138    pub const MAX_FILE_SIZE: FileSize = 1024 * 1024 * 1024;
139
140    /// Creates a new memory-mapped file buffer
141    ///
142    /// # Arguments
143    ///
144    /// * `path` - Path to the file to be mapped
145    ///
146    /// # Returns
147    ///
148    /// Returns a `FileBuffer` on success, or an `IoError` if the file cannot
149    /// be opened or mapped.
150    ///
151    /// # Errors
152    ///
153    /// This function will return an error if:
154    /// - The file does not exist or cannot be opened
155    /// - The file cannot be memory-mapped
156    /// - The file is empty
157    /// - The file is larger than the maximum allowed size
158    /// - File metadata cannot be read
159    ///
160    /// # Examples
161    ///
162    /// ```no_run
163    /// use libmagic_rs::io::FileBuffer;
164    /// use std::path::Path;
165    ///
166    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
167    /// # Ok::<(), Box<dyn std::error::Error>>(())
168    /// ```
169    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    /// Creates a new `FileBuffer` using caller-supplied metadata.
183    ///
184    /// This is a performance-focused alternative to [`FileBuffer::new`] for
185    /// callers that have already called `std::fs::metadata` on `path` (for
186    /// example, to check the empty-file case before constructing the buffer).
187    /// It skips the internal `std::fs::canonicalize` + second `metadata`
188    /// round-trip that [`FileBuffer::new`] performs, eliminating two
189    /// redundant syscalls on the hot path of
190    /// [`MagicDatabase::evaluate_file`](crate::MagicDatabase::evaluate_file).
191    ///
192    /// # Security
193    ///
194    /// This constructor deliberately skips `std::fs::canonicalize` for
195    /// performance. Symlink resolution and path canonicalization are the
196    /// caller's responsibility. In adversarial environments (untrusted file
197    /// paths), prefer [`FileBuffer::new`] or [`MagicDatabase::evaluate_buffer`]
198    /// instead.
199    ///
200    /// The caller is responsible for having read `metadata` via a path that
201    /// makes sense for their security model. The same structural checks
202    /// (regular file, non-empty, under `MAX_FILE_SIZE`) are still applied
203    /// against the supplied metadata.
204    ///
205    /// # Errors
206    ///
207    /// Returns the same `IoError` variants as [`FileBuffer::new`] for
208    /// validation failures, file open failures, and mmap failures.
209    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    /// Opens a file for reading with proper error handling
225    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    /// Validates file metadata on the already-open file descriptor.
233    ///
234    /// Uses `File::metadata()` (fstat) rather than re-resolving the path to
235    /// close the TOCTOU window between `open_file` and validation. An attacker
236    /// cannot swap the path for a symlink, directory, or device after open
237    /// because fstat operates on the kernel file-table entry.
238    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    /// Apply the regular-file/size structural checks to an already-read
248    /// [`std::fs::Metadata`] value.
249    ///
250    /// Shared between [`FileBuffer::new`] (which re-reads metadata via
251    /// canonicalize) and [`FileBuffer::from_path_and_metadata`] (which reuses
252    /// caller-supplied metadata). The `reported_path` is the path to include
253    /// in any returned error.
254    fn check_metadata(metadata: &std::fs::Metadata, reported_path: &Path) -> Result<(), IoError> {
255        // Check if the target is a regular file
256        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                // Check for other special file types (cross-platform)
263                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        // Check if file is empty
275        if file_size == 0 {
276            return Err(IoError::EmptyFile {
277                path: reported_path.to_path_buf(),
278            });
279        }
280
281        // Check if file is too large
282        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    /// Detects special file types in a cross-platform manner
294    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    /// Creates a symlink in a cross-platform manner (test helper only).
325    ///
326    /// # Arguments
327    /// * `original` - The path to the original file or directory
328    /// * `link` - The path where the symlink should be created
329    ///
330    /// # Errors
331    /// * Returns `std::io::Error` if symlink creation fails (e.g., insufficient permissions)
332    /// * On Windows, may require admin privileges or developer mode enabled
333    /// * On non-Unix/Windows platforms, returns an "Unsupported" error
334    #[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    /// Creates memory mapping for the file
363    ///
364    /// # Security
365    ///
366    /// This function uses memory mapping which provides several security benefits:
367    /// - Avoids loading entire files into memory, reducing memory exhaustion attacks
368    /// - Provides read-only access to file contents
369    /// - Leverages OS-level memory protection mechanisms
370    fn create_memory_mapping(file: &File, path_buf: &Path) -> Result<Mmap, IoError> {
371        // SAFETY: We use safe memory mapping through memmap2, which handles
372        // the unsafe operations internally with proper error checking.
373        // The memmap2 crate is a vetted dependency that provides safe abstractions
374        // over unsafe memory mapping operations.
375        #[allow(unsafe_code)]
376        unsafe {
377            MmapOptions::new().map(file).map_err(|source| {
378                // Sanitize error message to avoid leaking sensitive path information
379                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    /// Returns the file contents as a byte slice
393    ///
394    /// This provides safe access to the memory-mapped file data without
395    /// copying the contents.
396    ///
397    /// # Examples
398    ///
399    /// ```no_run
400    /// use libmagic_rs::io::FileBuffer;
401    /// use std::path::Path;
402    ///
403    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
404    /// let data = buffer.as_slice();
405    /// println!("First byte: 0x{:02x}", data[0]);
406    /// # Ok::<(), Box<dyn std::error::Error>>(())
407    /// ```
408    #[must_use]
409    pub fn as_slice(&self) -> &[u8] {
410        &self.mmap
411    }
412
413    /// Returns the path of the file
414    ///
415    /// # Examples
416    ///
417    /// ```no_run
418    /// use libmagic_rs::io::FileBuffer;
419    /// use std::path::Path;
420    ///
421    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
422    /// println!("File path: {}", buffer.path().display());
423    /// # Ok::<(), Box<dyn std::error::Error>>(())
424    /// ```
425    #[must_use]
426    pub fn path(&self) -> &Path {
427        &self.path
428    }
429
430    /// Returns the size of the file in bytes
431    ///
432    /// # Examples
433    ///
434    /// ```no_run
435    /// use libmagic_rs::io::FileBuffer;
436    /// use std::path::Path;
437    ///
438    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
439    /// println!("File size: {} bytes", buffer.len());
440    /// # Ok::<(), Box<dyn std::error::Error>>(())
441    /// ```
442    #[must_use]
443    pub fn len(&self) -> usize {
444        self.mmap.len()
445    }
446
447    /// Returns true if the file is empty
448    ///
449    /// Note: This should never return true for a successfully created `FileBuffer`,
450    /// as empty files are rejected during construction.
451    ///
452    /// # Examples
453    ///
454    /// ```no_run
455    /// use libmagic_rs::io::FileBuffer;
456    /// use std::path::Path;
457    ///
458    /// let buffer = FileBuffer::new(Path::new("example.bin"))?;
459    /// assert!(!buffer.is_empty()); // Should always be false for valid buffers
460    /// # Ok::<(), Box<dyn std::error::Error>>(())
461    /// ```
462    #[must_use]
463    pub fn is_empty(&self) -> bool {
464        self.mmap.is_empty()
465    }
466}
467
468/// Safely reads bytes from a buffer with bounds checking
469///
470/// This function provides safe access to buffer data with comprehensive
471/// bounds checking to prevent buffer overruns and invalid access patterns.
472///
473/// # Arguments
474///
475/// * `buffer` - The buffer to read from
476/// * `offset` - Starting offset in the buffer
477/// * `length` - Number of bytes to read
478///
479/// # Returns
480///
481/// Returns a slice of the requested bytes on success, or an `IoError` if
482/// the access would be out of bounds.
483///
484/// # Errors
485///
486/// This function will return an error if:
487/// - The offset is beyond the buffer size
488/// - The length would cause an overflow
489/// - The offset + length exceeds the buffer size
490/// - The length is zero (invalid access)
491///
492/// # Examples
493///
494/// ```
495/// use libmagic_rs::io::safe_read_bytes;
496///
497/// let buffer = b"Hello, World!";
498/// let result = safe_read_bytes(buffer, 0, 5)?;
499/// assert_eq!(result, b"Hello");
500///
501/// let result = safe_read_bytes(buffer, 7, 6)?;
502/// assert_eq!(result, b"World!");
503/// # Ok::<(), Box<dyn std::error::Error>>(())
504/// ```
505pub 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; // Safe: validate_buffer_access proved bounds
512    // Use .get() for defense-in-depth; the validate call above guarantees
513    // this range is in-bounds, so the unwrap_or fallback is unreachable.
514    Ok(buffer.get(offset..end_offset).unwrap_or(&[]))
515}
516
517/// Safely reads a single byte from a buffer with bounds checking
518///
519/// This is a convenience function for reading a single byte with proper
520/// bounds checking.
521///
522/// # Arguments
523///
524/// * `buffer` - The buffer to read from
525/// * `offset` - Offset of the byte to read
526///
527/// # Returns
528///
529/// Returns the byte at the specified offset on success, or an `IoError` if
530/// the access would be out of bounds.
531///
532/// # Errors
533///
534/// This function will return an error if the offset is beyond the buffer size.
535///
536/// # Examples
537///
538/// ```
539/// use libmagic_rs::io::safe_read_byte;
540///
541/// let buffer = b"Hello";
542/// let byte = safe_read_byte(buffer, 0)?;
543/// assert_eq!(byte, b'H');
544/// # Ok::<(), Box<dyn std::error::Error>>(())
545/// ```
546pub 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
554/// Validates buffer access parameters without performing the actual read
555///
556/// This function can be used to validate buffer access parameters before
557/// performing the actual read operation.
558///
559/// # Arguments
560///
561/// * `buffer_size` - Size of the buffer
562/// * `offset` - Starting offset
563/// * `length` - Number of bytes to access
564///
565/// # Returns
566///
567/// Returns `Ok(())` if the access is valid, or an `IoError` if it would
568/// be out of bounds.
569///
570/// # Errors
571///
572/// This function will return an error if:
573/// - The offset is beyond the buffer size
574/// - The length would cause an overflow
575/// - The offset + length exceeds the buffer size
576/// - The length is zero (invalid access)
577///
578/// # Examples
579///
580/// ```
581/// use libmagic_rs::io::validate_buffer_access;
582///
583/// // Valid access
584/// validate_buffer_access(100, 10, 20)?;
585///
586/// // Invalid access - would go beyond buffer
587/// let result = validate_buffer_access(100, 90, 20);
588/// assert!(result.is_err());
589/// # Ok::<(), Box<dyn std::error::Error>>(())
590/// ```
591pub fn validate_buffer_access(
592    buffer_size: BufferLength,
593    offset: BufferOffset,
594    length: BufferLength,
595) -> Result<(), IoError> {
596    // Check for zero length (invalid access)
597    if length == 0 {
598        return Err(IoError::InvalidAccess { offset, length });
599    }
600
601    // Check if offset is within buffer bounds
602    if offset >= buffer_size {
603        return Err(IoError::BufferOverrun {
604            offset,
605            length,
606            buffer_size,
607        });
608    }
609
610    // Check for potential overflow in offset + length calculation
611    let end_offset = offset
612        .checked_add(length)
613        .ok_or(IoError::InvalidAccess { offset, length })?;
614
615    // Check if the end offset is within buffer bounds
616    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    use super::*;
630    use std::fs;
631    use std::io::Write;
632    use std::sync::atomic::{AtomicU64, Ordering};
633
634    /// Monotonic counter for unique temp file names across parallel tests.
635    static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
636
637    /// Helper function to create a temporary file with given content.
638    /// Uses a monotonic counter + process ID for guaranteed uniqueness.
639    fn create_temp_file(content: &[u8]) -> PathBuf {
640        let temp_dir = std::env::temp_dir();
641        let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
642        let file_path = temp_dir.join(format!("libmagic_test_{}_{id}", std::process::id()));
643
644        {
645            let mut file = File::create(&file_path).expect("Failed to create temp file");
646            file.write_all(content).expect("Failed to write temp file");
647            file.sync_all().expect("Failed to sync temp file");
648        } // File is closed here when it goes out of scope
649
650        file_path
651    }
652
653    /// Helper function to clean up temporary file
654    fn cleanup_temp_file(path: &Path) {
655        let _ = fs::remove_file(path);
656    }
657
658    #[test]
659    fn test_file_buffer_creation_success() {
660        let content = b"Hello, World!";
661        let temp_path = create_temp_file(content);
662
663        let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
664
665        assert_eq!(buffer.as_slice(), content);
666        assert_eq!(buffer.len(), content.len());
667        assert!(!buffer.is_empty());
668        assert_eq!(buffer.path(), temp_path.as_path());
669
670        cleanup_temp_file(&temp_path);
671    }
672
673    #[test]
674    fn test_file_buffer_nonexistent_file() {
675        let nonexistent_path = Path::new("/nonexistent/file.bin");
676
677        let result = FileBuffer::new(nonexistent_path);
678
679        assert!(result.is_err());
680        match result.unwrap_err() {
681            IoError::FileOpenError { path, .. } => {
682                assert_eq!(path, nonexistent_path);
683            }
684            other => panic!("Expected FileOpenError, got {other:?}"),
685        }
686    }
687
688    #[test]
689    fn test_file_buffer_empty_file() {
690        let temp_path = create_temp_file(&[]);
691
692        let result = FileBuffer::new(&temp_path);
693
694        assert!(result.is_err());
695        match result.unwrap_err() {
696            IoError::EmptyFile { path } => {
697                // `validate_file_metadata` now uses `file.metadata()` on the
698                // open descriptor rather than re-canonicalizing the path,
699                // so the reported path is the caller-supplied path as-is.
700                assert_eq!(path, temp_path);
701            }
702            other => panic!("Expected EmptyFile error, got {other:?}"),
703        }
704
705        cleanup_temp_file(&temp_path);
706    }
707
708    #[test]
709    fn test_file_buffer_large_file() {
710        // Create a file with some content to test normal operation
711        let content = vec![0u8; 1024]; // 1KB file
712        let temp_path = create_temp_file(&content);
713
714        let buffer =
715            FileBuffer::new(&temp_path).expect("Failed to create FileBuffer for normal file");
716        assert_eq!(buffer.len(), 1024);
717
718        cleanup_temp_file(&temp_path);
719    }
720
721    #[test]
722    fn test_file_buffer_binary_content() {
723        let content = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
724        let temp_path = create_temp_file(&content);
725
726        let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
727
728        assert_eq!(buffer.as_slice(), content.as_slice());
729        assert_eq!(buffer.as_slice()[0], 0x00);
730        assert_eq!(buffer.as_slice()[7], 0xFC);
731
732        cleanup_temp_file(&temp_path);
733    }
734
735    #[test]
736    fn test_io_error_display() {
737        let path = PathBuf::from("/test/path");
738        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
739
740        let error = IoError::FileOpenError {
741            path,
742            source: io_err,
743        };
744
745        let error_string = format!("{error}");
746        assert!(error_string.contains("/test/path"));
747        assert!(error_string.contains("Failed to open file"));
748    }
749
750    #[test]
751    fn test_empty_file_error_display() {
752        let path = PathBuf::from("/test/empty.bin");
753        let error = IoError::EmptyFile { path };
754
755        let error_string = format!("{error}");
756        assert!(error_string.contains("/test/empty.bin"));
757        assert!(error_string.contains("is empty"));
758    }
759
760    #[test]
761    fn test_file_too_large_error_display() {
762        let path = PathBuf::from("/test/large.bin");
763        let error = IoError::FileTooLarge {
764            path,
765            size: 2_000_000_000,
766            max_size: 1_000_000_000,
767        };
768
769        let error_string = format!("{error}");
770        assert!(error_string.contains("/test/large.bin"));
771        assert!(error_string.contains("too large"));
772        assert!(error_string.contains("2000000000"));
773        assert!(error_string.contains("1000000000"));
774    }
775
776    #[test]
777    fn test_safe_read_bytes_success() {
778        let buffer = b"Hello, World!";
779
780        // Read from beginning
781        let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
782        assert_eq!(result, b"Hello");
783
784        // Read from middle
785        let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
786        assert_eq!(result, b"World");
787
788        // Read single byte
789        let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
790        assert_eq!(result, b"H");
791
792        // Read entire buffer
793        let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
794        assert_eq!(result, buffer);
795
796        // Read from end
797        let result = safe_read_bytes(buffer, buffer.len() - 1, 1).expect("Failed to read bytes");
798        assert_eq!(result, b"!");
799    }
800
801    #[test]
802    fn test_safe_read_bytes_out_of_bounds() {
803        let buffer = b"Hello";
804
805        // Offset beyond buffer
806        let result = safe_read_bytes(buffer, 10, 1);
807        assert!(result.is_err());
808        match result.unwrap_err() {
809            IoError::BufferOverrun {
810                offset,
811                length,
812                buffer_size,
813            } => {
814                assert_eq!(offset, 10);
815                assert_eq!(length, 1);
816                assert_eq!(buffer_size, 5);
817            }
818            other => panic!("Expected BufferOverrun, got {other:?}"),
819        }
820
821        // Length extends beyond buffer
822        let result = safe_read_bytes(buffer, 3, 5);
823        assert!(result.is_err());
824        match result.unwrap_err() {
825            IoError::BufferOverrun {
826                offset,
827                length,
828                buffer_size,
829            } => {
830                assert_eq!(offset, 3);
831                assert_eq!(length, 5);
832                assert_eq!(buffer_size, 5);
833            }
834            other => panic!("Expected BufferOverrun, got {other:?}"),
835        }
836
837        // Offset at buffer boundary
838        let result = safe_read_bytes(buffer, 5, 1);
839        assert!(result.is_err());
840    }
841
842    #[test]
843    fn test_safe_read_bytes_zero_length() {
844        let buffer = b"Hello";
845
846        let result = safe_read_bytes(buffer, 0, 0);
847        assert!(result.is_err());
848        match result.unwrap_err() {
849            IoError::InvalidAccess { offset, length } => {
850                assert_eq!(offset, 0);
851                assert_eq!(length, 0);
852            }
853            other => panic!("Expected InvalidAccess, got {other:?}"),
854        }
855    }
856
857    #[test]
858    fn test_safe_read_bytes_overflow() {
859        let buffer = b"Hello";
860
861        // Test potential overflow in offset + length
862        // When offset is usize::MAX, it's beyond buffer bounds, so we get BufferOverrun
863        let result = safe_read_bytes(buffer, usize::MAX, 1);
864        assert!(result.is_err());
865        match result.unwrap_err() {
866            IoError::BufferOverrun { .. } => {
867                // This is expected since usize::MAX > buffer.len()
868            }
869            other => panic!("Expected BufferOverrun, got {other:?}"),
870        }
871
872        // Test overflow with valid offset but huge length
873        let result = safe_read_bytes(buffer, 1, usize::MAX);
874        assert!(result.is_err());
875        match result.unwrap_err() {
876            IoError::InvalidAccess { .. } => {
877                // This should trigger overflow in checked_add
878            }
879            other => panic!("Expected InvalidAccess, got {other:?}"),
880        }
881
882        // Test a case that would overflow but with smaller numbers
883        let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
884        assert!(result.is_err());
885        match result.unwrap_err() {
886            IoError::InvalidAccess { .. } => {
887                // This should trigger overflow in checked_add
888            }
889            other => panic!("Expected InvalidAccess, got {other:?}"),
890        }
891    }
892
893    #[test]
894    fn test_safe_read_byte_success() {
895        let buffer = b"Hello";
896
897        assert_eq!(safe_read_byte(buffer, 0).unwrap(), b'H');
898        assert_eq!(safe_read_byte(buffer, 1).unwrap(), b'e');
899        assert_eq!(safe_read_byte(buffer, 4).unwrap(), b'o');
900    }
901
902    #[test]
903    fn test_safe_read_byte_out_of_bounds() {
904        let buffer = b"Hello";
905
906        let result = safe_read_byte(buffer, 5);
907        assert!(result.is_err());
908        match result.unwrap_err() {
909            IoError::BufferOverrun {
910                offset,
911                length,
912                buffer_size,
913            } => {
914                assert_eq!(offset, 5);
915                assert_eq!(length, 1);
916                assert_eq!(buffer_size, 5);
917            }
918            other => panic!("Expected BufferOverrun, got {other:?}"),
919        }
920
921        let result = safe_read_byte(buffer, 100);
922        assert!(result.is_err());
923    }
924
925    #[test]
926    fn test_validate_buffer_access_success() {
927        // Valid accesses
928        validate_buffer_access(100, 0, 50).expect("Should be valid");
929        validate_buffer_access(100, 50, 50).expect("Should be valid");
930        validate_buffer_access(100, 99, 1).expect("Should be valid");
931        validate_buffer_access(10, 0, 10).expect("Should be valid");
932        validate_buffer_access(1, 0, 1).expect("Should be valid");
933    }
934
935    #[test]
936    fn test_validate_buffer_access_invalid() {
937        // Zero length
938        let result = validate_buffer_access(100, 0, 0);
939        assert!(result.is_err());
940
941        // Offset beyond buffer
942        let result = validate_buffer_access(100, 100, 1);
943        assert!(result.is_err());
944
945        // Length extends beyond buffer
946        let result = validate_buffer_access(100, 50, 51);
947        assert!(result.is_err());
948
949        // Overflow conditions
950        let result = validate_buffer_access(100, usize::MAX, 1);
951        assert!(result.is_err());
952
953        let result = validate_buffer_access(100, 1, usize::MAX);
954        assert!(result.is_err());
955    }
956
957    #[test]
958    fn test_validate_buffer_access_edge_cases() {
959        // Empty buffer
960        let result = validate_buffer_access(0, 0, 1);
961        assert!(result.is_err());
962
963        // Large buffer, valid access
964        let large_size = 1_000_000;
965        validate_buffer_access(large_size, 0, large_size).expect("Should be valid");
966        validate_buffer_access(large_size, large_size - 1, 1).expect("Should be valid");
967
968        // Large buffer, invalid access
969        let result = validate_buffer_access(large_size, large_size - 1, 2);
970        assert!(result.is_err());
971    }
972
973    #[test]
974    fn test_buffer_access_security_patterns() {
975        // Test patterns that could indicate security vulnerabilities
976        let buffer_size = 1024;
977
978        // Test potential integer overflow patterns
979        let overflow_patterns = vec![
980            (usize::MAX, 1),           // Maximum offset
981            (buffer_size, usize::MAX), // Maximum length
982            (usize::MAX - 1, 2),       // Near-overflow offset
983        ];
984
985        for (offset, length) in overflow_patterns {
986            let result = validate_buffer_access(buffer_size, offset, length);
987            assert!(
988                result.is_err(),
989                "Should reject potentially dangerous access pattern: offset={offset}, length={length}"
990            );
991        }
992
993        // Test boundary conditions that should be safe
994        let safe_patterns = vec![
995            (0, 1),               // Start of buffer
996            (buffer_size - 1, 1), // End of buffer
997            (buffer_size / 2, 1), // Middle of buffer
998        ];
999
1000        for (offset, length) in safe_patterns {
1001            let result = validate_buffer_access(buffer_size, offset, length);
1002            assert!(
1003                result.is_ok(),
1004                "Should accept safe access pattern: offset={offset}, length={length}"
1005            );
1006        }
1007    }
1008
1009    #[test]
1010    fn test_buffer_overrun_error_display() {
1011        let error = IoError::BufferOverrun {
1012            offset: 10,
1013            length: 5,
1014            buffer_size: 12,
1015        };
1016
1017        let error_string = format!("{error}");
1018        assert!(error_string.contains("Buffer access out of bounds"));
1019        assert!(error_string.contains("offset 10"));
1020        assert!(error_string.contains("length 5"));
1021        assert!(error_string.contains("buffer size 12"));
1022    }
1023
1024    #[test]
1025    fn test_invalid_access_error_display() {
1026        let error = IoError::InvalidAccess {
1027            offset: 0,
1028            length: 0,
1029        };
1030
1031        let error_string = format!("{error}");
1032        assert!(error_string.contains("Invalid buffer access parameters"));
1033        assert!(error_string.contains("offset 0"));
1034        assert!(error_string.contains("length 0"));
1035    }
1036
1037    #[test]
1038    fn test_invalid_file_type_error_display() {
1039        let error = IoError::InvalidFileType {
1040            path: std::path::PathBuf::from("/dev/null"),
1041            file_type: "character device".to_string(),
1042        };
1043
1044        let error_string = format!("{error}");
1045        assert!(error_string.contains("is not a regular file"));
1046        assert!(error_string.contains("/dev/null"));
1047        assert!(error_string.contains("character device"));
1048    }
1049
1050    #[test]
1051    fn test_file_buffer_directory_rejection() {
1052        // Create a temporary directory
1053        let temp_dir = std::env::temp_dir().join("test_dir_12345");
1054        std::fs::create_dir_all(&temp_dir).unwrap();
1055
1056        let result = FileBuffer::new(&temp_dir);
1057
1058        assert!(result.is_err());
1059        match result.unwrap_err() {
1060            IoError::InvalidFileType { path, file_type } => {
1061                assert_eq!(file_type, "directory");
1062                // `validate_file_metadata` now uses the open descriptor,
1063                // so the reported path is the caller-supplied path.
1064                assert_eq!(path, temp_dir);
1065            }
1066            IoError::FileOpenError { .. } => {
1067                // On Windows, we can't open directories as files, so we get a FileOpenError
1068                // This is expected behavior, so we'll consider this test passed
1069                println!(
1070                    "Directory test skipped on this platform (can't open directories as files)"
1071                );
1072            }
1073            other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1074        }
1075
1076        // Cleanup
1077        std::fs::remove_dir(&temp_dir).unwrap();
1078    }
1079
1080    #[test]
1081    fn test_file_buffer_symlink_to_directory_rejection() {
1082        // Create a temporary directory and a symlink to it
1083        let temp_dir = std::env::temp_dir().join("test_dir_symlink_12345");
1084        let symlink_path = std::env::temp_dir().join("test_symlink_12345");
1085
1086        std::fs::create_dir_all(&temp_dir).unwrap();
1087
1088        // Create symlink (cross-platform approach)
1089        let symlink_result = FileBuffer::create_symlink(&temp_dir, &symlink_path);
1090
1091        match symlink_result {
1092            Ok(()) => {
1093                let result = FileBuffer::new(&symlink_path);
1094
1095                assert!(result.is_err());
1096                match result.unwrap_err() {
1097                    IoError::InvalidFileType { path, file_type } => {
1098                        assert_eq!(file_type, "directory");
1099                        // Post-TOCTOU fix: reported path is the caller-supplied
1100                        // symlink path, not the canonicalized target.
1101                        assert_eq!(path, symlink_path);
1102                    }
1103                    IoError::FileOpenError { .. } => {
1104                        // On Windows, we can't open directories as files, so we get a FileOpenError
1105                        // This is expected behavior, so we'll consider this test passed
1106                        println!(
1107                            "Directory symlink test skipped on this platform (can't open directories as files)"
1108                        );
1109                    }
1110                    other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1111                }
1112
1113                // Cleanup
1114                let _ = std::fs::remove_file(&symlink_path);
1115            }
1116            Err(_) => {
1117                // Symlink creation failed (e.g., no admin privileges on Windows)
1118                println!(
1119                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1120                );
1121            }
1122        }
1123
1124        // Cleanup
1125        std::fs::remove_dir(&temp_dir).unwrap();
1126    }
1127
1128    #[test]
1129    fn test_file_buffer_symlink_to_regular_file_success() {
1130        // Create a temporary file and a symlink to it
1131        let temp_file = std::env::temp_dir().join("test_file_symlink_12345");
1132        let symlink_path = std::env::temp_dir().join("test_symlink_file_12345");
1133
1134        let content = b"test content";
1135        std::fs::write(&temp_file, content).unwrap();
1136
1137        // Create symlink (cross-platform approach)
1138        let symlink_result = FileBuffer::create_symlink(&temp_file, &symlink_path);
1139
1140        match symlink_result {
1141            Ok(()) => {
1142                let result = FileBuffer::new(&symlink_path);
1143
1144                assert!(result.is_ok());
1145                let buffer = result.unwrap();
1146                assert_eq!(buffer.as_slice(), content);
1147
1148                // Cleanup
1149                let _ = std::fs::remove_file(&symlink_path);
1150            }
1151            Err(_) => {
1152                // Symlink creation failed (e.g., no admin privileges on Windows)
1153                println!(
1154                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1155                );
1156            }
1157        }
1158
1159        // Cleanup
1160        std::fs::remove_file(&temp_file).unwrap();
1161    }
1162
1163    #[test]
1164    fn test_file_buffer_special_files_rejection() {
1165        // Test rejection of special files that exist on Unix systems
1166        #[cfg(unix)]
1167        {
1168            // Test /dev/null (character device)
1169            let result = FileBuffer::new(std::path::Path::new("/dev/null"));
1170            assert!(result.is_err());
1171            match result.unwrap_err() {
1172                IoError::InvalidFileType { path, file_type } => {
1173                    assert_eq!(file_type, "character device");
1174                    assert_eq!(path, std::path::PathBuf::from("/dev/null"));
1175                }
1176                other => panic!("Expected InvalidFileType error, got {other:?}"),
1177            }
1178
1179            // Test /dev/zero (character device)
1180            let result = FileBuffer::new(std::path::Path::new("/dev/zero"));
1181            assert!(result.is_err());
1182            match result.unwrap_err() {
1183                IoError::InvalidFileType { path, file_type } => {
1184                    assert_eq!(file_type, "character device");
1185                    assert_eq!(path, std::path::PathBuf::from("/dev/zero"));
1186                }
1187                other => panic!("Expected InvalidFileType error, got {other:?}"),
1188            }
1189
1190            // Test /dev/random (character device)
1191            let result = FileBuffer::new(std::path::Path::new("/dev/random"));
1192            assert!(result.is_err());
1193            match result.unwrap_err() {
1194                IoError::InvalidFileType { path, file_type } => {
1195                    assert_eq!(file_type, "character device");
1196                    assert_eq!(path, std::path::PathBuf::from("/dev/random"));
1197                }
1198                other => panic!("Expected InvalidFileType error, got {other:?}"),
1199            }
1200        }
1201
1202        #[cfg(not(unix))]
1203        {
1204            // On non-Unix systems, these special files don't exist
1205            println!("Skipping special file tests on non-Unix platform");
1206        }
1207    }
1208
1209    #[test]
1210    fn test_file_buffer_cross_platform_special_files() {
1211        // Test cross-platform special file detection
1212        // This test works on all platforms by creating temporary special files
1213
1214        // Test with a directory (works on all platforms)
1215        let temp_dir = std::env::temp_dir().join("test_special_dir_12345");
1216        std::fs::create_dir_all(&temp_dir).unwrap();
1217
1218        let result = FileBuffer::new(&temp_dir);
1219        assert!(result.is_err());
1220        match result.unwrap_err() {
1221            IoError::InvalidFileType { path, file_type } => {
1222                assert_eq!(file_type, "directory");
1223                // Caller-supplied path (post-TOCTOU fix).
1224                assert_eq!(path, temp_dir);
1225            }
1226            IoError::FileOpenError { .. } => {
1227                // On Windows, we can't open directories as files
1228                println!(
1229                    "Directory test skipped on this platform (can't open directories as files)"
1230                );
1231            }
1232            other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1233        }
1234
1235        // Cleanup
1236        std::fs::remove_dir(&temp_dir).unwrap();
1237    }
1238
1239    #[test]
1240    #[ignore = "FIFOs can cause hanging issues in CI environments"]
1241    fn test_file_buffer_fifo_rejection() {
1242        // Create a FIFO (named pipe) and test rejection
1243        #[cfg(unix)]
1244        {
1245            use nix::unistd;
1246
1247            let fifo_path = std::env::temp_dir().join("test_fifo_12345");
1248
1249            // Create a FIFO using nix crate
1250            match unistd::mkfifo(
1251                &fifo_path,
1252                nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
1253            ) {
1254                Ok(()) => {
1255                    let result = FileBuffer::new(&fifo_path);
1256
1257                    assert!(result.is_err());
1258                    match result.unwrap_err() {
1259                        IoError::InvalidFileType { path, file_type } => {
1260                            assert_eq!(file_type, "FIFO/pipe");
1261                            // Post-TOCTOU fix: reported path is the caller-supplied path.
1262                            assert_eq!(path, fifo_path);
1263                        }
1264                        other => panic!("Expected InvalidFileType error, got {other:?}"),
1265                    }
1266
1267                    // Cleanup
1268                    std::fs::remove_file(&fifo_path).unwrap();
1269                }
1270                Err(_) => {
1271                    // If we can't create a FIFO, skip this test
1272                    println!("Skipping FIFO test - unable to create FIFO");
1273                }
1274            }
1275        }
1276
1277        #[cfg(not(unix))]
1278        {
1279            // On non-Unix systems, we can't create FIFOs easily, so we'll skip this test
1280            println!("Skipping FIFO test on non-Unix platform");
1281        }
1282    }
1283}