Skip to main content

threatflux_binary_analysis/utils/
mmap.rs

1//! Memory mapping utilities for efficient binary analysis
2//!
3//! This module provides safe memory mapping functionality for reading large binary files
4//! efficiently without loading them entirely into memory.
5
6use crate::{BinaryError, Result};
7use memmap2::{Mmap, MmapOptions};
8use std::fs::File;
9use std::ops::{Deref, Range};
10use std::path::Path;
11
12/// Memory-mapped binary file
13#[derive(Debug)]
14pub struct MappedBinary {
15    _file: File,
16    mmap: Mmap,
17    size: usize,
18}
19
20impl MappedBinary {
21    /// Create a new memory-mapped binary from a file path.
22    ///
23    /// # Safety
24    ///
25    /// The caller must ensure that the mapped file's contents and length are not
26    /// modified for the lifetime of the returned mapping, including through other
27    /// file handles or processes. Violating this requirement can invalidate shared
28    /// references into the map or trigger platform-specific faults.
29    pub unsafe fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
30        let file = File::open(path)
31            .map_err(|e| BinaryError::memory_map(format!("Failed to open file: {}", e)))?;
32
33        // SAFETY: the caller accepts the backing-file stability contract above.
34        let mmap = unsafe {
35            MmapOptions::new().map(&file).map_err(|e| {
36                BinaryError::memory_map(format!("Failed to create memory map: {}", e))
37            })?
38        };
39
40        let size = mmap.len();
41
42        Ok(Self {
43            _file: file,
44            mmap,
45            size,
46        })
47    }
48
49    /// Create a memory-mapped binary from an open file.
50    ///
51    /// # Safety
52    ///
53    /// The caller must ensure that the mapped file's contents and length are not
54    /// modified for the lifetime of the returned mapping, including through other
55    /// file handles or processes.
56    pub unsafe fn from_file(file: File) -> Result<Self> {
57        // SAFETY: the caller accepts the backing-file stability contract above.
58        let mmap = unsafe {
59            MmapOptions::new().map(&file).map_err(|e| {
60                BinaryError::memory_map(format!("Failed to create memory map: {}", e))
61            })?
62        };
63
64        let size = mmap.len();
65
66        Ok(Self {
67            _file: file,
68            mmap,
69            size,
70        })
71    }
72
73    /// Get the size of the mapped file
74    pub fn size(&self) -> usize {
75        self.size
76    }
77
78    /// Get a slice of the mapped data
79    pub fn slice(&self, range: Range<usize>) -> crate::types::ByteSliceResult<'_> {
80        if range.start > range.end {
81            return Err(BinaryError::memory_map("Invalid reversed range"));
82        }
83        self.mmap
84            .get(range)
85            .ok_or_else(|| BinaryError::memory_map("Range exceeds file size"))
86    }
87
88    /// Get data at a specific offset with a given length
89    pub fn read_at(&self, offset: usize, length: usize) -> crate::types::ByteSliceResult<'_> {
90        let end = offset
91            .checked_add(length)
92            .ok_or_else(|| BinaryError::memory_map("Read range overflows usize"))?;
93        self.mmap
94            .get(offset..end)
95            .ok_or_else(|| BinaryError::memory_map("Read exceeds file size"))
96    }
97
98    /// Read a specific number of bytes starting from an offset
99    pub fn read_bytes(&self, offset: usize, count: usize) -> Result<Vec<u8>> {
100        let data = self.read_at(offset, count)?;
101        Ok(data.to_vec())
102    }
103
104    /// Read a u8 value at the specified offset
105    pub fn read_u8(&self, offset: usize) -> Result<u8> {
106        if offset >= self.size {
107            return Err(BinaryError::memory_map(
108                "Offset exceeds file size".to_string(),
109            ));
110        }
111        Ok(self.mmap[offset])
112    }
113
114    /// Read a u16 value at the specified offset (little endian)
115    pub fn read_u16_le(&self, offset: usize) -> Result<u16> {
116        let bytes = self.read_at(offset, 2)?;
117        Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
118    }
119
120    /// Read a u16 value at the specified offset (big endian)
121    pub fn read_u16_be(&self, offset: usize) -> Result<u16> {
122        let bytes = self.read_at(offset, 2)?;
123        Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
124    }
125
126    /// Read a u32 value at the specified offset (little endian)
127    pub fn read_u32_le(&self, offset: usize) -> Result<u32> {
128        let bytes = self.read_at(offset, 4)?;
129        Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
130    }
131
132    /// Read a u32 value at the specified offset (big endian)
133    pub fn read_u32_be(&self, offset: usize) -> Result<u32> {
134        let bytes = self.read_at(offset, 4)?;
135        Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
136    }
137
138    /// Read a u64 value at the specified offset (little endian)
139    pub fn read_u64_le(&self, offset: usize) -> Result<u64> {
140        let bytes = self.read_at(offset, 8)?;
141        Ok(u64::from_le_bytes([
142            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
143        ]))
144    }
145
146    /// Read a u64 value at the specified offset (big endian)
147    pub fn read_u64_be(&self, offset: usize) -> Result<u64> {
148        let bytes = self.read_at(offset, 8)?;
149        Ok(u64::from_be_bytes([
150            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
151        ]))
152    }
153
154    /// Read a null-terminated string at the specified offset
155    pub fn read_cstring(&self, offset: usize, max_length: usize) -> Result<String> {
156        if offset > self.size {
157            return Err(BinaryError::memory_map("Offset exceeds file size"));
158        }
159
160        let mut end = offset;
161        let limit = offset.saturating_add(max_length).min(self.size);
162
163        while end < limit && self.mmap[end] != 0 {
164            end += 1;
165        }
166
167        let bytes = &self.mmap[offset..end];
168        String::from_utf8(bytes.to_vec())
169            .map_err(|e| BinaryError::memory_map(format!("Invalid UTF-8 string: {}", e)))
170    }
171
172    /// Find the first occurrence of a pattern in the mapped data
173    pub fn find_pattern(&self, pattern: &[u8]) -> Option<usize> {
174        if pattern.is_empty() {
175            return Some(0);
176        }
177
178        self.mmap
179            .windows(pattern.len())
180            .position(|window| window == pattern)
181    }
182
183    /// Find all occurrences of a pattern in the mapped data
184    pub fn find_all_patterns(&self, pattern: &[u8]) -> Vec<usize> {
185        // Returning every boundary for an empty pattern would allocate in
186        // proportion to the file size and has no useful binary-analysis meaning.
187        if pattern.is_empty() || pattern.len() > self.size {
188            return Vec::new();
189        }
190
191        let mut positions = Vec::new();
192        let mut start = 0;
193
194        while start <= self.size - pattern.len() {
195            if let Some(pos) = self.mmap[start..]
196                .windows(pattern.len())
197                .position(|window| window == pattern)
198            {
199                positions.push(start + pos);
200                start += pos + 1;
201            } else {
202                break;
203            }
204        }
205
206        positions
207    }
208
209    /// Check if the mapped data starts with a specific magic signature
210    pub fn starts_with(&self, signature: &[u8]) -> bool {
211        if signature.len() > self.size {
212            return false;
213        }
214
215        &self.mmap[..signature.len()] == signature
216    }
217
218    /// Get a hexdump of a specific region
219    pub fn hexdump(&self, offset: usize, length: usize) -> Result<String> {
220        let data = self.read_at(offset, length)?;
221        Ok(format_hexdump(data, offset))
222    }
223
224    /// Create a safe view into a portion of the mapped data
225    pub fn view(&self, range: Range<usize>) -> Result<MappedView<'_>> {
226        if range.start > range.end {
227            return Err(BinaryError::memory_map("Invalid reversed range"));
228        }
229        let offset = range.start;
230        let data = self
231            .mmap
232            .get(range)
233            .ok_or_else(|| BinaryError::memory_map("Range exceeds file size"))?;
234
235        Ok(MappedView { data, offset })
236    }
237}
238
239impl Deref for MappedBinary {
240    type Target = [u8];
241
242    fn deref(&self) -> &Self::Target {
243        &self.mmap
244    }
245}
246
247/// A view into a portion of a memory-mapped binary
248#[derive(Debug)]
249pub struct MappedView<'a> {
250    data: &'a [u8],
251    offset: usize,
252}
253
254impl<'a> MappedView<'a> {
255    /// Get the offset of this view within the original file
256    pub fn offset(&self) -> usize {
257        self.offset
258    }
259
260    /// Get the size of this view
261    pub fn size(&self) -> usize {
262        self.data.len()
263    }
264
265    /// Convert to a byte vector
266    pub fn to_vec(&self) -> Vec<u8> {
267        self.data.to_vec()
268    }
269}
270
271impl<'a> Deref for MappedView<'a> {
272    type Target = [u8];
273
274    fn deref(&self) -> &Self::Target {
275        self.data
276    }
277}
278
279/// Format data as a hexdump
280fn format_hexdump(data: &[u8], base_offset: usize) -> String {
281    let mut result = String::new();
282
283    for (i, chunk) in data.chunks(16).enumerate() {
284        let offset = base_offset.saturating_add(i.saturating_mul(16));
285        result.push_str(&format!("{:08x}: ", offset));
286
287        // Hex bytes
288        for (j, byte) in chunk.iter().enumerate() {
289            if j == 8 {
290                result.push(' ');
291            }
292            result.push_str(&format!("{:02x} ", byte));
293        }
294
295        // Padding for incomplete lines
296        for j in chunk.len()..16 {
297            if j == 8 {
298                result.push(' ');
299            }
300            result.push_str("   ");
301        }
302
303        // ASCII representation
304        result.push_str(" |");
305        for byte in chunk {
306            if byte.is_ascii_graphic() || *byte == b' ' {
307                result.push(*byte as char);
308            } else {
309                result.push('.');
310            }
311        }
312        result.push_str("|\n");
313    }
314
315    result
316}
317
318/// Memory mapping configuration
319#[derive(Debug, Clone, Default)]
320pub struct MmapConfig {
321    /// Whether to use huge pages if available
322    pub use_huge_pages: bool,
323    /// Whether to populate the mapping (fault pages immediately). Supported on
324    /// Linux and Android; requests are rejected on platforms where this is a no-op.
325    pub populate: bool,
326    /// Whether to lock the mapping in memory
327    pub lock_memory: bool,
328}
329
330/// Advanced memory mapping with configuration
331#[derive(Debug)]
332pub struct AdvancedMmap {
333    _file: File,
334    mmap: Mmap,
335    config: MmapConfig,
336}
337
338impl AdvancedMmap {
339    /// Create an advanced memory map with configuration.
340    ///
341    /// Huge pages are not supported for file-backed mappings and are rejected
342    /// instead of being silently ignored.
343    ///
344    /// # Safety
345    ///
346    /// The caller must ensure that the mapped file's contents and length are not
347    /// modified for the lifetime of the returned mapping, including through other
348    /// file handles or processes.
349    pub unsafe fn new<P: AsRef<Path>>(path: P, config: MmapConfig) -> Result<Self> {
350        if config.use_huge_pages {
351            return Err(BinaryError::config(
352                "huge pages are not supported for file-backed memory maps",
353            ));
354        }
355
356        #[cfg(not(any(target_os = "linux", target_os = "android")))]
357        if config.populate {
358            return Err(BinaryError::config(
359                "populating mapped pages is unsupported on this platform",
360            ));
361        }
362
363        #[cfg(not(unix))]
364        if config.lock_memory {
365            return Err(BinaryError::config(
366                "locking mapped memory is unsupported on this platform",
367            ));
368        }
369
370        let file = File::open(path)
371            .map_err(|e| BinaryError::memory_map(format!("Failed to open file: {}", e)))?;
372
373        let mut options = MmapOptions::new();
374
375        if config.populate {
376            options.populate();
377        }
378
379        // SAFETY: the caller accepts the backing-file stability contract above.
380        let mmap = unsafe {
381            options.map(&file).map_err(|e| {
382                BinaryError::memory_map(format!("Failed to create memory map: {}", e))
383            })?
384        };
385
386        #[cfg(unix)]
387        if config.lock_memory {
388            mmap.lock().map_err(|error| {
389                BinaryError::memory_map(format!("Failed to lock memory map: {error}"))
390            })?;
391        }
392
393        Ok(Self {
394            _file: file,
395            mmap,
396            config,
397        })
398    }
399
400    /// Get the underlying mapped data
401    pub fn data(&self) -> &[u8] {
402        &self.mmap
403    }
404
405    /// Get the configuration used
406    pub fn config(&self) -> &MmapConfig {
407        &self.config
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use std::io::Write;
415    use tempfile::NamedTempFile;
416
417    fn mapped_binary(path: impl AsRef<Path>) -> Result<super::MappedBinary> {
418        // SAFETY: each test owns its temporary file and does not mutate it after
419        // creating the mapping.
420        unsafe { super::MappedBinary::new(path) }
421    }
422
423    fn mapped_binary_from_file(file: File) -> Result<super::MappedBinary> {
424        // SAFETY: each test retains the temporary file solely to keep its path
425        // alive and does not mutate the mapped contents.
426        unsafe { super::MappedBinary::from_file(file) }
427    }
428
429    fn advanced_mmap(path: impl AsRef<Path>, config: MmapConfig) -> Result<super::AdvancedMmap> {
430        // SAFETY: each test owns its temporary file and does not mutate it after
431        // creating the mapping.
432        unsafe { super::AdvancedMmap::new(path, config) }
433    }
434
435    fn create_test_file() -> NamedTempFile {
436        let mut file = NamedTempFile::new().unwrap();
437        file.write_all(b"Hello, World! This is a test file.")
438            .unwrap();
439        file.flush().unwrap();
440        file
441    }
442
443    fn create_binary_test_file() -> NamedTempFile {
444        let mut file = NamedTempFile::new().unwrap();
445        // Create binary data with various byte values including null terminators
446        let data = vec![
447            0x12, 0x34, 0x56, 0x78, // u32 little endian: 0x78563412, big endian: 0x12345678
448            0xAB, 0xCD, 0xEF, 0x01, // u32 continuation
449            0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00, // "Hello\0"
450            0x57, 0x6F, 0x72, 0x6C, 0x64, 0x00, // "World\0"
451            0xFF, 0xFE, 0xFD, 0xFC, // More binary data
452            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 8 bytes for u64 test
453            0x41, 0x42, 0x43, // "ABC"
454            0x80, 0x81, 0x82, 0x83, // Non-ASCII bytes
455        ];
456        file.write_all(&data).unwrap();
457        file.flush().unwrap();
458        file
459    }
460
461    fn create_empty_file() -> NamedTempFile {
462        NamedTempFile::new().unwrap()
463    }
464
465    #[test]
466    fn test_mapped_binary_creation() {
467        let file = create_test_file();
468        let mapped = mapped_binary(file.path());
469        assert!(mapped.is_ok());
470
471        let mapped = mapped.unwrap();
472        assert_eq!(mapped.size(), 34);
473    }
474
475    #[test]
476    fn test_mapped_binary_from_file() {
477        let temp_file = create_test_file();
478        let file = File::open(temp_file.path()).unwrap();
479        let mapped = mapped_binary_from_file(file);
480        assert!(mapped.is_ok());
481
482        let mapped = mapped.unwrap();
483        assert_eq!(mapped.size(), 34);
484        assert_eq!(&mapped[0..5], b"Hello");
485    }
486
487    #[test]
488    fn test_mapped_binary_deref() {
489        let file = create_test_file();
490        let mapped = mapped_binary(file.path()).unwrap();
491
492        // Test Deref implementation
493        assert_eq!(&mapped[0..5], b"Hello");
494        assert_eq!(mapped.len(), 34);
495    }
496
497    #[test]
498    fn test_slice_method() {
499        let file = create_test_file();
500        let mapped = mapped_binary(file.path()).unwrap();
501
502        // Test successful slice
503        let slice = mapped.slice(0..5).unwrap();
504        assert_eq!(slice, b"Hello");
505
506        let slice = mapped.slice(7..12).unwrap();
507        assert_eq!(slice, b"World");
508
509        // Test error case - range exceeds file size
510        let result = mapped.slice(0..100);
511        assert!(result.is_err());
512        assert!(
513            result
514                .unwrap_err()
515                .to_string()
516                .contains("Range exceeds file size")
517        );
518    }
519
520    #[test]
521    fn test_read_operations() {
522        let file = create_test_file();
523        let mapped = mapped_binary(file.path()).unwrap();
524
525        // Test read_at
526        let data = mapped.read_at(0, 5).unwrap();
527        assert_eq!(data, b"Hello");
528
529        // Test read_bytes
530        let bytes = mapped.read_bytes(7, 5).unwrap();
531        assert_eq!(bytes, b"World".to_vec());
532
533        // Test read_u8
534        let byte = mapped.read_u8(0).unwrap();
535        assert_eq!(byte, b'H');
536    }
537
538    #[test]
539    fn test_read_operations_errors() {
540        let file = create_test_file();
541        let mapped = mapped_binary(file.path()).unwrap();
542
543        // Test read_at with out of bounds
544        let result = mapped.read_at(0, 100);
545        assert!(result.is_err());
546        assert!(
547            result
548                .unwrap_err()
549                .to_string()
550                .contains("Read exceeds file size")
551        );
552
553        // Test read_at with offset out of bounds
554        let result = mapped.read_at(50, 5);
555        assert!(result.is_err());
556
557        // Test read_bytes with out of bounds
558        let result = mapped.read_bytes(0, 100);
559        assert!(result.is_err());
560
561        // Test read_u8 with offset out of bounds
562        let result = mapped.read_u8(100);
563        assert!(result.is_err());
564        assert!(
565            result
566                .unwrap_err()
567                .to_string()
568                .contains("Offset exceeds file size")
569        );
570
571        // Overflow must be rejected before constructing a slice range.
572        let result = mapped.read_at(usize::MAX, 2);
573        assert!(result.unwrap_err().to_string().contains("overflows usize"));
574    }
575
576    #[test]
577    fn test_integer_read_operations() {
578        let file = create_binary_test_file();
579        let mapped = mapped_binary(file.path()).unwrap();
580
581        // Test u16 reads (bytes 0-1: 0x12, 0x34)
582        let val_le = mapped.read_u16_le(0).unwrap();
583        assert_eq!(val_le, 0x3412); // Little endian
584
585        let val_be = mapped.read_u16_be(0).unwrap();
586        assert_eq!(val_be, 0x1234); // Big endian
587
588        // Test u32 reads (bytes 0-3: 0x12, 0x34, 0x56, 0x78)
589        let val_le = mapped.read_u32_le(0).unwrap();
590        assert_eq!(val_le, 0x78563412); // Little endian
591
592        let val_be = mapped.read_u32_be(0).unwrap();
593        assert_eq!(val_be, 0x12345678); // Big endian
594
595        // Test u64 reads at position 24 where our 8-byte sequence 0x00..0x07 is located
596        let val_le = mapped.read_u64_le(24).unwrap();
597        assert_eq!(val_le, 0x0706050403020100); // Little endian
598
599        let val_be = mapped.read_u64_be(24).unwrap();
600        assert_eq!(val_be, 0x0001020304050607); // Big endian
601    }
602
603    #[test]
604    fn test_integer_read_operations_errors() {
605        let file = create_binary_test_file();
606        let mapped = mapped_binary(file.path()).unwrap();
607        let file_size = mapped.size();
608
609        // Test u16 read errors
610        assert!(mapped.read_u16_le(file_size).is_err());
611        assert!(mapped.read_u16_be(file_size - 1).is_err());
612
613        // Test u32 read errors
614        assert!(mapped.read_u32_le(file_size).is_err());
615        assert!(mapped.read_u32_be(file_size - 3).is_err());
616
617        // Test u64 read errors
618        assert!(mapped.read_u64_le(file_size).is_err());
619        assert!(mapped.read_u64_be(file_size - 7).is_err());
620    }
621
622    #[test]
623    fn test_read_cstring() {
624        let file = create_binary_test_file();
625        let mapped = mapped_binary(file.path()).unwrap();
626
627        // Test reading "Hello\0" starting at byte 8
628        let s = mapped.read_cstring(8, 10).unwrap();
629        assert_eq!(s, "Hello");
630
631        // Test reading "World\0" starting at byte 14
632        let s = mapped.read_cstring(14, 10).unwrap();
633        assert_eq!(s, "World");
634
635        // Test with max_length limit
636        let s = mapped.read_cstring(8, 3).unwrap();
637        assert_eq!(s, "Hel");
638
639        // Test reading from offset that would go beyond file
640        let result = mapped.read_cstring(8, 1000);
641        assert!(result.is_ok()); // Should succeed but be limited by file size
642    }
643
644    #[test]
645    fn test_read_cstring_errors() {
646        let mut file = NamedTempFile::new().unwrap();
647        // Create data with invalid UTF-8
648        file.write_all(&[0xFF, 0xFE, 0x00]).unwrap();
649        file.flush().unwrap();
650
651        let mapped = mapped_binary(file.path()).unwrap();
652
653        // Test invalid UTF-8 string
654        let result = mapped.read_cstring(0, 10);
655        assert!(result.is_err());
656        assert!(
657            result
658                .unwrap_err()
659                .to_string()
660                .contains("Invalid UTF-8 string")
661        );
662
663        assert!(mapped.read_cstring(usize::MAX, 1).is_err());
664    }
665
666    #[test]
667    fn test_pattern_finding() {
668        let file = create_test_file();
669        let mapped = mapped_binary(file.path()).unwrap();
670
671        // Test find_pattern
672        let pos = mapped.find_pattern(b"World");
673        assert_eq!(pos, Some(7));
674
675        let pos = mapped.find_pattern(b"xyz");
676        assert_eq!(pos, None);
677
678        // Test find_pattern with single byte pattern
679        let pos = mapped.find_pattern(b"H");
680        assert_eq!(pos, Some(0));
681    }
682
683    #[test]
684    fn test_find_all_patterns() {
685        let mut file = NamedTempFile::new().unwrap();
686        file.write_all(b"ababcabab").unwrap();
687        file.flush().unwrap();
688
689        let mapped = mapped_binary(file.path()).unwrap();
690
691        // Test find_all_patterns with multiple occurrences
692        let positions = mapped.find_all_patterns(b"ab");
693        assert_eq!(positions, vec![0, 2, 5, 7]);
694
695        // Test with single occurrence
696        let positions = mapped.find_all_patterns(b"abc");
697        assert_eq!(positions, vec![2]);
698
699        // Test with no occurrences
700        let positions = mapped.find_all_patterns(b"xyz");
701        assert_eq!(positions, Vec::<usize>::new());
702
703        // Test with single byte pattern
704        let positions = mapped.find_all_patterns(b"a");
705        assert_eq!(positions, vec![0, 2, 5, 7]);
706    }
707
708    #[test]
709    fn test_pattern_edge_cases() {
710        let file = create_test_file();
711        let mapped = mapped_binary(file.path()).unwrap();
712
713        // Test pattern longer than file
714        let long_pattern = vec![b'A'; 1000];
715        let result = mapped.find_pattern(&long_pattern);
716        assert_eq!(result, None);
717
718        // Test pattern at end of file
719        let result = mapped.find_pattern(b"file.");
720        assert!(result.is_some());
721
722        assert_eq!(mapped.find_pattern(b""), Some(0));
723        assert!(mapped.find_all_patterns(b"").is_empty());
724    }
725
726    #[test]
727    fn test_starts_with() {
728        let file = create_test_file();
729        let mapped = mapped_binary(file.path()).unwrap();
730
731        assert!(mapped.starts_with(b"Hello"));
732        assert!(!mapped.starts_with(b"World"));
733        assert!(mapped.starts_with(b"")); // Empty pattern should match
734
735        // Test with signature longer than file
736        let mut long_signature = vec![0; 100];
737        long_signature[0] = b'H';
738        assert!(!mapped.starts_with(&long_signature));
739    }
740
741    #[test]
742    fn test_hexdump_method() {
743        let file = create_binary_test_file();
744        let mapped = mapped_binary(file.path()).unwrap();
745
746        // Test hexdump method
747        let hexdump = mapped.hexdump(0, 8).unwrap();
748        assert!(hexdump.contains("00000000:"));
749        assert!(hexdump.contains("12 34 56 78"));
750
751        // Test error case
752        let result = mapped.hexdump(0, 1000);
753        assert!(result.is_err());
754    }
755
756    #[test]
757    fn test_view_creation() {
758        let file = create_test_file();
759        let mapped = mapped_binary(file.path()).unwrap();
760
761        let view = mapped.view(0..5).unwrap();
762        assert_eq!(view.size(), 5);
763        assert_eq!(view.offset(), 0);
764        assert_eq!(&*view, b"Hello");
765
766        // Test error case
767        let result = mapped.view(0..100);
768        assert!(result.is_err());
769        assert!(
770            result
771                .unwrap_err()
772                .to_string()
773                .contains("Range exceeds file size")
774        );
775
776        let reversed_start = 5;
777        let reversed_end = 4;
778        assert!(mapped.view(reversed_start..reversed_end).is_err());
779        assert!(mapped.slice(reversed_start..reversed_end).is_err());
780    }
781
782    #[test]
783    fn test_mapped_view_methods() {
784        let file = create_test_file();
785        let mapped = mapped_binary(file.path()).unwrap();
786
787        let view = mapped.view(7..12).unwrap();
788        assert_eq!(view.offset(), 7);
789        assert_eq!(view.size(), 5);
790
791        // Test to_vec method
792        let vec = view.to_vec();
793        assert_eq!(vec, b"World".to_vec());
794
795        // Test Deref implementation
796        assert_eq!(&*view, b"World");
797        assert_eq!(view.len(), 5);
798    }
799
800    #[test]
801    fn test_hexdump_function() {
802        // Test basic hexdump
803        let data = b"Hello, World!";
804        let hexdump = format_hexdump(data, 0);
805        assert!(hexdump.contains("48 65 6c 6c 6f 2c 20 57"));
806        assert!(hexdump.contains("Hello, W"));
807
808        // Test empty data
809        let hexdump = format_hexdump(&[], 0);
810        assert_eq!(hexdump, "");
811
812        // Test data with non-printable characters
813        let data = &[
814            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
815            0x0E, 0x0F,
816        ];
817        let hexdump = format_hexdump(data, 0x1000);
818        assert!(hexdump.contains("00001000:"));
819        assert!(hexdump.contains("00 01 02 03 04 05 06 07"));
820        assert!(hexdump.contains("................"));
821
822        // Test data longer than 16 bytes
823        let data = b"This is a longer string that spans multiple lines";
824        let hexdump = format_hexdump(data, 0);
825        let lines: Vec<&str> = hexdump.lines().collect();
826        assert!(lines.len() > 1); // Should have multiple lines
827
828        // Test data with mixed printable and non-printable
829        let data = &[b'A', b'B', 0xFF, b'C', b'D'];
830        let hexdump = format_hexdump(data, 0);
831        assert!(hexdump.contains("AB.CD"));
832
833        let hexdump = format_hexdump(&[0; 32], usize::MAX - 4);
834        assert_eq!(hexdump.lines().count(), 2);
835    }
836
837    #[test]
838    fn test_empty_file_handling() {
839        let file = create_empty_file();
840        let mapped = mapped_binary(file.path()).unwrap();
841
842        assert_eq!(mapped.size(), 0);
843
844        // Test operations on empty file
845        assert!(mapped.read_u8(0).is_err());
846        assert!(mapped.read_at(0, 1).is_err());
847        assert_eq!(mapped.find_pattern(b"test"), None);
848        assert_eq!(mapped.find_all_patterns(b"test"), Vec::<usize>::new());
849        assert!(mapped.starts_with(b"")); // Empty pattern should match empty file
850        assert!(!mapped.starts_with(b"test"));
851
852        let result = mapped.view(0..1);
853        assert!(result.is_err());
854    }
855
856    #[test]
857    fn test_mmap_config_default() {
858        let config = MmapConfig::default();
859        assert!(!config.use_huge_pages);
860        assert!(!config.populate);
861        assert!(!config.lock_memory);
862    }
863
864    #[test]
865    fn test_mmap_config_debug() {
866        let config = MmapConfig {
867            use_huge_pages: true,
868            populate: false,
869            lock_memory: true,
870        };
871        let debug_str = format!("{:?}", config);
872        assert!(debug_str.contains("use_huge_pages: true"));
873        assert!(debug_str.contains("populate: false"));
874        assert!(debug_str.contains("lock_memory: true"));
875    }
876
877    #[test]
878    fn test_mmap_config_clone() {
879        let config = MmapConfig {
880            use_huge_pages: true,
881            populate: true,
882            lock_memory: false,
883        };
884        let cloned = config.clone();
885        assert!(cloned.use_huge_pages);
886        assert!(cloned.populate);
887        assert!(!cloned.lock_memory);
888    }
889
890    #[test]
891    fn test_advanced_mmap() {
892        let file = create_test_file();
893        let config = MmapConfig::default();
894        let advanced = advanced_mmap(file.path(), config.clone()).unwrap();
895
896        assert_eq!(advanced.data().len(), 34);
897        assert_eq!(advanced.data()[0..5], *b"Hello");
898
899        let returned_config = advanced.config();
900        assert_eq!(returned_config.use_huge_pages, config.use_huge_pages);
901        assert_eq!(returned_config.populate, config.populate);
902        assert_eq!(returned_config.lock_memory, config.lock_memory);
903    }
904
905    #[cfg(any(target_os = "linux", target_os = "android"))]
906    #[test]
907    fn test_advanced_mmap_with_populate() {
908        let file = create_test_file();
909        let config = MmapConfig {
910            use_huge_pages: false,
911            populate: true,
912            lock_memory: false,
913        };
914        let advanced = advanced_mmap(file.path(), config).unwrap();
915
916        assert_eq!(advanced.data().len(), 34);
917        assert!(advanced.config().populate);
918    }
919
920    #[cfg(not(any(target_os = "linux", target_os = "android")))]
921    #[test]
922    fn test_advanced_mmap_rejects_populate_when_unsupported() {
923        let file = create_test_file();
924        let config = MmapConfig {
925            use_huge_pages: false,
926            populate: true,
927            lock_memory: false,
928        };
929
930        let error = advanced_mmap(file.path(), config).unwrap_err();
931        assert!(matches!(error, BinaryError::ConfigError(_)));
932        assert!(error.to_string().contains("unsupported on this platform"));
933    }
934
935    #[test]
936    fn test_advanced_mmap_rejects_ignored_huge_page_request() {
937        let file = create_test_file();
938        let config = MmapConfig {
939            use_huge_pages: true,
940            ..Default::default()
941        };
942
943        let error = advanced_mmap(file.path(), config).unwrap_err();
944        assert!(error.to_string().contains("not supported"));
945    }
946
947    #[test]
948    fn test_file_not_found_error() {
949        let result = mapped_binary("/nonexistent/file/path");
950        assert!(result.is_err());
951        let error = result.unwrap_err();
952        assert!(error.to_string().contains("Failed to open file"));
953    }
954
955    #[test]
956    fn test_advanced_mmap_file_not_found_error() {
957        let config = MmapConfig::default();
958        let result = advanced_mmap("/nonexistent/file/path", config);
959        assert!(result.is_err());
960        let error = result.unwrap_err();
961        assert!(error.to_string().contains("Failed to open file"));
962    }
963
964    #[test]
965    fn test_large_data_operations() {
966        let mut file = NamedTempFile::new().unwrap();
967        let large_data = vec![0xAB; 10000]; // 10KB of 0xAB
968        file.write_all(&large_data).unwrap();
969        file.flush().unwrap();
970
971        let mapped = mapped_binary(file.path()).unwrap();
972
973        // Test reading from various positions
974        assert_eq!(mapped.read_u8(5000).unwrap(), 0xAB);
975        assert_eq!(mapped.read_bytes(1000, 100).unwrap().len(), 100);
976
977        // Test pattern finding in large data
978        let positions = mapped.find_all_patterns(&[0xAB, 0xAB]);
979        assert!(positions.len() > 1000); // Should find many overlapping patterns
980
981        // Test hexdump with large offset
982        let hexdump = mapped.hexdump(8000, 32).unwrap();
983        assert!(hexdump.contains("00001f40:")); // 8000 in hex
984    }
985
986    #[test]
987    fn test_boundary_conditions() {
988        let file = create_test_file(); // 34 bytes: "Hello, World! This is a test file."
989        let mapped = mapped_binary(file.path()).unwrap();
990
991        // Test reading at exact file boundary
992        assert!(mapped.read_u8(33).is_ok()); // Last byte
993        assert!(mapped.read_u8(34).is_err()); // One past end
994
995        // Test reading exactly to the end
996        assert!(mapped.read_at(30, 4).is_ok()); // Last 4 bytes
997        assert!(mapped.read_at(30, 5).is_err()); // One byte too many
998
999        // Test slice at boundary
1000        assert!(mapped.slice(0..34).is_ok()); // Entire file
1001        assert!(mapped.slice(0..35).is_err()); // One byte too many
1002
1003        // Test view at boundary
1004        assert!(mapped.view(0..34).is_ok()); // Entire file
1005        assert!(mapped.view(0..35).is_err()); // One byte too many
1006    }
1007}