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 [`crate::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    // Restriction lints without an allow-*-in-tests config option;
630    // test-only helpers: fixture math and best-effort temp-file cleanup.
631    #![allow(
632        clippy::integer_division,
633        clippy::let_underscore_must_use,
634        clippy::semicolon_outside_block
635    )]
636
637    use super::*;
638    use std::fs;
639    use std::io::Write;
640    use std::sync::atomic::{AtomicU64, Ordering};
641
642    /// Monotonic counter for unique temp file names across parallel tests.
643    static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
644
645    /// Helper function to create a temporary file with given content.
646    /// Uses a monotonic counter + process ID for guaranteed uniqueness.
647    fn create_temp_file(content: &[u8]) -> PathBuf {
648        let temp_dir = std::env::temp_dir();
649        let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
650        let file_path = temp_dir.join(format!("libmagic_test_{}_{id}", std::process::id()));
651
652        {
653            let mut file = File::create(&file_path).expect("Failed to create temp file");
654            file.write_all(content).expect("Failed to write temp file");
655            file.sync_all().expect("Failed to sync temp file");
656        } // File is closed here when it goes out of scope
657
658        file_path
659    }
660
661    /// Helper function to clean up temporary file
662    fn cleanup_temp_file(path: &Path) {
663        let _ = fs::remove_file(path);
664    }
665
666    #[test]
667    fn test_file_buffer_creation_success() {
668        let content = b"Hello, World!";
669        let temp_path = create_temp_file(content);
670
671        let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
672
673        assert_eq!(buffer.as_slice(), content);
674        assert_eq!(buffer.len(), content.len());
675        assert!(!buffer.is_empty());
676        assert_eq!(buffer.path(), temp_path.as_path());
677
678        cleanup_temp_file(&temp_path);
679    }
680
681    #[test]
682    fn test_file_buffer_nonexistent_file() {
683        let nonexistent_path = Path::new("/nonexistent/file.bin");
684
685        let result = FileBuffer::new(nonexistent_path);
686
687        assert!(result.is_err());
688        match result.unwrap_err() {
689            IoError::FileOpenError { path, .. } => {
690                assert_eq!(path, nonexistent_path);
691            }
692            other => panic!("Expected FileOpenError, got {other:?}"),
693        }
694    }
695
696    #[test]
697    fn test_file_buffer_empty_file() {
698        let temp_path = create_temp_file(&[]);
699
700        let result = FileBuffer::new(&temp_path);
701
702        assert!(result.is_err());
703        match result.unwrap_err() {
704            IoError::EmptyFile { path } => {
705                // `validate_file_metadata` now uses `file.metadata()` on the
706                // open descriptor rather than re-canonicalizing the path,
707                // so the reported path is the caller-supplied path as-is.
708                assert_eq!(path, temp_path);
709            }
710            other => panic!("Expected EmptyFile error, got {other:?}"),
711        }
712
713        cleanup_temp_file(&temp_path);
714    }
715
716    #[test]
717    fn test_file_buffer_large_file() {
718        // Create a file with some content to test normal operation
719        let content = vec![0u8; 1024]; // 1KB file
720        let temp_path = create_temp_file(&content);
721
722        let buffer =
723            FileBuffer::new(&temp_path).expect("Failed to create FileBuffer for normal file");
724        assert_eq!(buffer.len(), 1024);
725
726        cleanup_temp_file(&temp_path);
727    }
728
729    #[test]
730    fn test_file_buffer_binary_content() {
731        let content = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
732        let temp_path = create_temp_file(&content);
733
734        let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
735
736        assert_eq!(buffer.as_slice(), content.as_slice());
737        assert_eq!(buffer.as_slice()[0], 0x00);
738        assert_eq!(buffer.as_slice()[7], 0xFC);
739
740        cleanup_temp_file(&temp_path);
741    }
742
743    #[test]
744    fn test_io_error_display() {
745        let path = PathBuf::from("/test/path");
746        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
747
748        let error = IoError::FileOpenError {
749            path,
750            source: io_err,
751        };
752
753        let error_string = format!("{error}");
754        assert!(error_string.contains("/test/path"));
755        assert!(error_string.contains("Failed to open file"));
756    }
757
758    #[test]
759    fn test_empty_file_error_display() {
760        let path = PathBuf::from("/test/empty.bin");
761        let error = IoError::EmptyFile { path };
762
763        let error_string = format!("{error}");
764        assert!(error_string.contains("/test/empty.bin"));
765        assert!(error_string.contains("is empty"));
766    }
767
768    #[test]
769    fn test_file_too_large_error_display() {
770        let path = PathBuf::from("/test/large.bin");
771        let error = IoError::FileTooLarge {
772            path,
773            size: 2_000_000_000,
774            max_size: 1_000_000_000,
775        };
776
777        let error_string = format!("{error}");
778        assert!(error_string.contains("/test/large.bin"));
779        assert!(error_string.contains("too large"));
780        assert!(error_string.contains("2000000000"));
781        assert!(error_string.contains("1000000000"));
782    }
783
784    #[test]
785    fn test_safe_read_bytes_success() {
786        let buffer = b"Hello, World!";
787
788        // Read from beginning
789        let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
790        assert_eq!(result, b"Hello");
791
792        // Read from middle
793        let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
794        assert_eq!(result, b"World");
795
796        // Read single byte
797        let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
798        assert_eq!(result, b"H");
799
800        // Read entire buffer
801        let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
802        assert_eq!(result, buffer);
803
804        // Read from end
805        let result = safe_read_bytes(buffer, buffer.len() - 1, 1).expect("Failed to read bytes");
806        assert_eq!(result, b"!");
807    }
808
809    #[test]
810    fn test_safe_read_bytes_out_of_bounds() {
811        let buffer = b"Hello";
812
813        // Offset beyond buffer
814        let result = safe_read_bytes(buffer, 10, 1);
815        assert!(result.is_err());
816        match result.unwrap_err() {
817            IoError::BufferOverrun {
818                offset,
819                length,
820                buffer_size,
821            } => {
822                assert_eq!(offset, 10);
823                assert_eq!(length, 1);
824                assert_eq!(buffer_size, 5);
825            }
826            other => panic!("Expected BufferOverrun, got {other:?}"),
827        }
828
829        // Length extends beyond buffer
830        let result = safe_read_bytes(buffer, 3, 5);
831        assert!(result.is_err());
832        match result.unwrap_err() {
833            IoError::BufferOverrun {
834                offset,
835                length,
836                buffer_size,
837            } => {
838                assert_eq!(offset, 3);
839                assert_eq!(length, 5);
840                assert_eq!(buffer_size, 5);
841            }
842            other => panic!("Expected BufferOverrun, got {other:?}"),
843        }
844
845        // Offset at buffer boundary
846        let result = safe_read_bytes(buffer, 5, 1);
847        assert!(result.is_err());
848    }
849
850    #[test]
851    fn test_safe_read_bytes_zero_length() {
852        let buffer = b"Hello";
853
854        let result = safe_read_bytes(buffer, 0, 0);
855        assert!(result.is_err());
856        match result.unwrap_err() {
857            IoError::InvalidAccess { offset, length } => {
858                assert_eq!(offset, 0);
859                assert_eq!(length, 0);
860            }
861            other => panic!("Expected InvalidAccess, got {other:?}"),
862        }
863    }
864
865    #[test]
866    fn test_safe_read_bytes_overflow() {
867        let buffer = b"Hello";
868
869        // Test potential overflow in offset + length
870        // When offset is usize::MAX, it's beyond buffer bounds, so we get BufferOverrun
871        let result = safe_read_bytes(buffer, usize::MAX, 1);
872        assert!(result.is_err());
873        match result.unwrap_err() {
874            IoError::BufferOverrun { .. } => {
875                // This is expected since usize::MAX > buffer.len()
876            }
877            other => panic!("Expected BufferOverrun, got {other:?}"),
878        }
879
880        // Test overflow with valid offset but huge length
881        let result = safe_read_bytes(buffer, 1, usize::MAX);
882        assert!(result.is_err());
883        match result.unwrap_err() {
884            IoError::InvalidAccess { .. } => {
885                // This should trigger overflow in checked_add
886            }
887            other => panic!("Expected InvalidAccess, got {other:?}"),
888        }
889
890        // Test a case that would overflow but with smaller numbers
891        let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
892        assert!(result.is_err());
893        match result.unwrap_err() {
894            IoError::InvalidAccess { .. } => {
895                // This should trigger overflow in checked_add
896            }
897            other => panic!("Expected InvalidAccess, got {other:?}"),
898        }
899    }
900
901    #[test]
902    fn test_safe_read_byte_success() {
903        let buffer = b"Hello";
904
905        assert_eq!(safe_read_byte(buffer, 0).unwrap(), b'H');
906        assert_eq!(safe_read_byte(buffer, 1).unwrap(), b'e');
907        assert_eq!(safe_read_byte(buffer, 4).unwrap(), b'o');
908    }
909
910    #[test]
911    fn test_safe_read_byte_out_of_bounds() {
912        let buffer = b"Hello";
913
914        let result = safe_read_byte(buffer, 5);
915        assert!(result.is_err());
916        match result.unwrap_err() {
917            IoError::BufferOverrun {
918                offset,
919                length,
920                buffer_size,
921            } => {
922                assert_eq!(offset, 5);
923                assert_eq!(length, 1);
924                assert_eq!(buffer_size, 5);
925            }
926            other => panic!("Expected BufferOverrun, got {other:?}"),
927        }
928
929        let result = safe_read_byte(buffer, 100);
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_validate_buffer_access_success() {
935        // Valid accesses
936        validate_buffer_access(100, 0, 50).expect("Should be valid");
937        validate_buffer_access(100, 50, 50).expect("Should be valid");
938        validate_buffer_access(100, 99, 1).expect("Should be valid");
939        validate_buffer_access(10, 0, 10).expect("Should be valid");
940        validate_buffer_access(1, 0, 1).expect("Should be valid");
941    }
942
943    #[test]
944    fn test_validate_buffer_access_invalid() {
945        // Zero length
946        let result = validate_buffer_access(100, 0, 0);
947        assert!(result.is_err());
948
949        // Offset beyond buffer
950        let result = validate_buffer_access(100, 100, 1);
951        assert!(result.is_err());
952
953        // Length extends beyond buffer
954        let result = validate_buffer_access(100, 50, 51);
955        assert!(result.is_err());
956
957        // Overflow conditions
958        let result = validate_buffer_access(100, usize::MAX, 1);
959        assert!(result.is_err());
960
961        let result = validate_buffer_access(100, 1, usize::MAX);
962        assert!(result.is_err());
963    }
964
965    #[test]
966    fn test_validate_buffer_access_edge_cases() {
967        // Empty buffer
968        let result = validate_buffer_access(0, 0, 1);
969        assert!(result.is_err());
970
971        // Large buffer, valid access
972        let large_size = 1_000_000;
973        validate_buffer_access(large_size, 0, large_size).expect("Should be valid");
974        validate_buffer_access(large_size, large_size - 1, 1).expect("Should be valid");
975
976        // Large buffer, invalid access
977        let result = validate_buffer_access(large_size, large_size - 1, 2);
978        assert!(result.is_err());
979    }
980
981    #[test]
982    fn test_buffer_access_security_patterns() {
983        // Test patterns that could indicate security vulnerabilities
984        let buffer_size = 1024;
985
986        // Test potential integer overflow patterns
987        let overflow_patterns = vec![
988            (usize::MAX, 1),           // Maximum offset
989            (buffer_size, usize::MAX), // Maximum length
990            (usize::MAX - 1, 2),       // Near-overflow offset
991        ];
992
993        for (offset, length) in overflow_patterns {
994            let result = validate_buffer_access(buffer_size, offset, length);
995            assert!(
996                result.is_err(),
997                "Should reject potentially dangerous access pattern: offset={offset}, length={length}"
998            );
999        }
1000
1001        // Test boundary conditions that should be safe
1002        let safe_patterns = vec![
1003            (0, 1),               // Start of buffer
1004            (buffer_size - 1, 1), // End of buffer
1005            (buffer_size / 2, 1), // Middle of buffer
1006        ];
1007
1008        for (offset, length) in safe_patterns {
1009            let result = validate_buffer_access(buffer_size, offset, length);
1010            assert!(
1011                result.is_ok(),
1012                "Should accept safe access pattern: offset={offset}, length={length}"
1013            );
1014        }
1015    }
1016
1017    #[test]
1018    fn test_buffer_overrun_error_display() {
1019        let error = IoError::BufferOverrun {
1020            offset: 10,
1021            length: 5,
1022            buffer_size: 12,
1023        };
1024
1025        let error_string = format!("{error}");
1026        assert!(error_string.contains("Buffer access out of bounds"));
1027        assert!(error_string.contains("offset 10"));
1028        assert!(error_string.contains("length 5"));
1029        assert!(error_string.contains("buffer size 12"));
1030    }
1031
1032    #[test]
1033    fn test_invalid_access_error_display() {
1034        let error = IoError::InvalidAccess {
1035            offset: 0,
1036            length: 0,
1037        };
1038
1039        let error_string = format!("{error}");
1040        assert!(error_string.contains("Invalid buffer access parameters"));
1041        assert!(error_string.contains("offset 0"));
1042        assert!(error_string.contains("length 0"));
1043    }
1044
1045    #[test]
1046    fn test_invalid_file_type_error_display() {
1047        let error = IoError::InvalidFileType {
1048            path: std::path::PathBuf::from("/dev/null"),
1049            file_type: "character device".to_string(),
1050        };
1051
1052        let error_string = format!("{error}");
1053        assert!(error_string.contains("is not a regular file"));
1054        assert!(error_string.contains("/dev/null"));
1055        assert!(error_string.contains("character device"));
1056    }
1057
1058    #[test]
1059    fn test_file_buffer_directory_rejection() {
1060        // Create a temporary directory
1061        let temp_dir = std::env::temp_dir().join("test_dir_12345");
1062        std::fs::create_dir_all(&temp_dir).unwrap();
1063
1064        let result = FileBuffer::new(&temp_dir);
1065
1066        assert!(result.is_err());
1067        match result.unwrap_err() {
1068            IoError::InvalidFileType { path, file_type } => {
1069                assert_eq!(file_type, "directory");
1070                // `validate_file_metadata` now uses the open descriptor,
1071                // so the reported path is the caller-supplied path.
1072                assert_eq!(path, temp_dir);
1073            }
1074            IoError::FileOpenError { .. } => {
1075                // On Windows, we can't open directories as files, so we get a FileOpenError
1076                // This is expected behavior, so we'll consider this test passed
1077                println!(
1078                    "Directory test skipped on this platform (can't open directories as files)"
1079                );
1080            }
1081            other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1082        }
1083
1084        // Cleanup
1085        std::fs::remove_dir(&temp_dir).unwrap();
1086    }
1087
1088    #[test]
1089    fn test_file_buffer_symlink_to_directory_rejection() {
1090        // Create a temporary directory and a symlink to it
1091        let temp_dir = std::env::temp_dir().join("test_dir_symlink_12345");
1092        let symlink_path = std::env::temp_dir().join("test_symlink_12345");
1093
1094        std::fs::create_dir_all(&temp_dir).unwrap();
1095
1096        // Create symlink (cross-platform approach)
1097        let symlink_result = FileBuffer::create_symlink(&temp_dir, &symlink_path);
1098
1099        match symlink_result {
1100            Ok(()) => {
1101                let result = FileBuffer::new(&symlink_path);
1102
1103                assert!(result.is_err());
1104                match result.unwrap_err() {
1105                    IoError::InvalidFileType { path, file_type } => {
1106                        assert_eq!(file_type, "directory");
1107                        // Post-TOCTOU fix: reported path is the caller-supplied
1108                        // symlink path, not the canonicalized target.
1109                        assert_eq!(path, symlink_path);
1110                    }
1111                    IoError::FileOpenError { .. } => {
1112                        // On Windows, we can't open directories as files, so we get a FileOpenError
1113                        // This is expected behavior, so we'll consider this test passed
1114                        println!(
1115                            "Directory symlink test skipped on this platform (can't open directories as files)"
1116                        );
1117                    }
1118                    other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1119                }
1120
1121                // Cleanup
1122                let _ = std::fs::remove_file(&symlink_path);
1123            }
1124            Err(_) => {
1125                // Symlink creation failed (e.g., no admin privileges on Windows)
1126                println!(
1127                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1128                );
1129            }
1130        }
1131
1132        // Cleanup
1133        std::fs::remove_dir(&temp_dir).unwrap();
1134    }
1135
1136    #[test]
1137    fn test_file_buffer_symlink_to_regular_file_success() {
1138        // Create a temporary file and a symlink to it
1139        let temp_file = std::env::temp_dir().join("test_file_symlink_12345");
1140        let symlink_path = std::env::temp_dir().join("test_symlink_file_12345");
1141
1142        let content = b"test content";
1143        std::fs::write(&temp_file, content).unwrap();
1144
1145        // Create symlink (cross-platform approach)
1146        let symlink_result = FileBuffer::create_symlink(&temp_file, &symlink_path);
1147
1148        match symlink_result {
1149            Ok(()) => {
1150                let result = FileBuffer::new(&symlink_path);
1151
1152                assert!(result.is_ok());
1153                let buffer = result.unwrap();
1154                assert_eq!(buffer.as_slice(), content);
1155
1156                // Cleanup
1157                let _ = std::fs::remove_file(&symlink_path);
1158            }
1159            Err(_) => {
1160                // Symlink creation failed (e.g., no admin privileges on Windows)
1161                println!(
1162                    "Skipping symlink test - unable to create symlink (may need admin privileges)"
1163                );
1164            }
1165        }
1166
1167        // Cleanup
1168        std::fs::remove_file(&temp_file).unwrap();
1169    }
1170
1171    #[test]
1172    fn test_file_buffer_special_files_rejection() {
1173        // Test rejection of special files that exist on Unix systems
1174        #[cfg(unix)]
1175        {
1176            // Test /dev/null (character device)
1177            let result = FileBuffer::new(std::path::Path::new("/dev/null"));
1178            assert!(result.is_err());
1179            match result.unwrap_err() {
1180                IoError::InvalidFileType { path, file_type } => {
1181                    assert_eq!(file_type, "character device");
1182                    assert_eq!(path, std::path::PathBuf::from("/dev/null"));
1183                }
1184                other => panic!("Expected InvalidFileType error, got {other:?}"),
1185            }
1186
1187            // Test /dev/zero (character device)
1188            let result = FileBuffer::new(std::path::Path::new("/dev/zero"));
1189            assert!(result.is_err());
1190            match result.unwrap_err() {
1191                IoError::InvalidFileType { path, file_type } => {
1192                    assert_eq!(file_type, "character device");
1193                    assert_eq!(path, std::path::PathBuf::from("/dev/zero"));
1194                }
1195                other => panic!("Expected InvalidFileType error, got {other:?}"),
1196            }
1197
1198            // Test /dev/random (character device)
1199            let result = FileBuffer::new(std::path::Path::new("/dev/random"));
1200            assert!(result.is_err());
1201            match result.unwrap_err() {
1202                IoError::InvalidFileType { path, file_type } => {
1203                    assert_eq!(file_type, "character device");
1204                    assert_eq!(path, std::path::PathBuf::from("/dev/random"));
1205                }
1206                other => panic!("Expected InvalidFileType error, got {other:?}"),
1207            }
1208        }
1209
1210        #[cfg(not(unix))]
1211        {
1212            // On non-Unix systems, these special files don't exist
1213            println!("Skipping special file tests on non-Unix platform");
1214        }
1215    }
1216
1217    #[test]
1218    fn test_file_buffer_cross_platform_special_files() {
1219        // Test cross-platform special file detection
1220        // This test works on all platforms by creating temporary special files
1221
1222        // Test with a directory (works on all platforms)
1223        let temp_dir = std::env::temp_dir().join("test_special_dir_12345");
1224        std::fs::create_dir_all(&temp_dir).unwrap();
1225
1226        let result = FileBuffer::new(&temp_dir);
1227        assert!(result.is_err());
1228        match result.unwrap_err() {
1229            IoError::InvalidFileType { path, file_type } => {
1230                assert_eq!(file_type, "directory");
1231                // Caller-supplied path (post-TOCTOU fix).
1232                assert_eq!(path, temp_dir);
1233            }
1234            IoError::FileOpenError { .. } => {
1235                // On Windows, we can't open directories as files
1236                println!(
1237                    "Directory test skipped on this platform (can't open directories as files)"
1238                );
1239            }
1240            other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
1241        }
1242
1243        // Cleanup
1244        std::fs::remove_dir(&temp_dir).unwrap();
1245    }
1246
1247    #[test]
1248    #[ignore = "FIFOs can cause hanging issues in CI environments"]
1249    fn test_file_buffer_fifo_rejection() {
1250        // Create a FIFO (named pipe) and test rejection
1251        #[cfg(unix)]
1252        {
1253            use nix::unistd;
1254
1255            let fifo_path = std::env::temp_dir().join("test_fifo_12345");
1256
1257            // Create a FIFO using nix crate
1258            match unistd::mkfifo(
1259                &fifo_path,
1260                nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
1261            ) {
1262                Ok(()) => {
1263                    let result = FileBuffer::new(&fifo_path);
1264
1265                    assert!(result.is_err());
1266                    match result.unwrap_err() {
1267                        IoError::InvalidFileType { path, file_type } => {
1268                            assert_eq!(file_type, "FIFO/pipe");
1269                            // Post-TOCTOU fix: reported path is the caller-supplied path.
1270                            assert_eq!(path, fifo_path);
1271                        }
1272                        other => panic!("Expected InvalidFileType error, got {other:?}"),
1273                    }
1274
1275                    // Cleanup
1276                    std::fs::remove_file(&fifo_path).unwrap();
1277                }
1278                Err(_) => {
1279                    // If we can't create a FIFO, skip this test
1280                    println!("Skipping FIFO test - unable to create FIFO");
1281                }
1282            }
1283        }
1284
1285        #[cfg(not(unix))]
1286        {
1287            // On non-Unix systems, we can't create FIFOs easily, so we'll skip this test
1288            println!("Skipping FIFO test on non-Unix platform");
1289        }
1290    }
1291}