wow_mpq/io/
memory_map.rs

1//! Cross-platform memory-mapped file support with security boundaries
2//!
3//! Provides high-performance memory-mapped access to MPQ archives while
4//! maintaining security and cross-platform compatibility.
5//!
6//! Security features:
7//! - Cross-platform safety (Windows, Linux, macOS)
8//! - Resource limits to prevent memory exhaustion
9//! - Access protection with bounds checking
10//! - Graceful fallback to regular I/O
11
12#[cfg(feature = "mmap")]
13use crate::security::{SecurityLimits, SessionTracker};
14#[cfg(feature = "mmap")]
15use crate::{Error, Result};
16#[cfg(feature = "mmap")]
17use memmap2::{Mmap, MmapOptions};
18#[cfg(feature = "mmap")]
19use std::fs::File;
20#[cfg(feature = "mmap")]
21use std::path::Path;
22#[cfg(feature = "mmap")]
23use std::sync::Arc;
24
25/// Configuration for memory mapping operations
26#[derive(Debug, Clone)]
27pub struct MemoryMapConfig {
28    /// Maximum size for memory mapping (security limit)
29    pub max_map_size: u64,
30    /// Enable memory mapping (can be disabled for compatibility)
31    pub enable_mapping: bool,
32    /// Use read-ahead optimization
33    pub read_ahead: bool,
34    /// Enable advisory locking
35    pub advisory_locking: bool,
36}
37
38impl Default for MemoryMapConfig {
39    fn default() -> Self {
40        Self {
41            max_map_size: 2 * 1024 * 1024 * 1024, // 2GB limit
42            enable_mapping: true,
43            read_ahead: true,
44            advisory_locking: false,
45        }
46    }
47}
48
49impl MemoryMapConfig {
50    /// Create strict memory mapping configuration with lower limits
51    pub fn strict() -> Self {
52        Self {
53            max_map_size: 256 * 1024 * 1024, // 256MB limit
54            enable_mapping: true,
55            read_ahead: false,
56            advisory_locking: true,
57        }
58    }
59
60    /// Create permissive memory mapping configuration with higher limits
61    pub fn permissive() -> Self {
62        Self {
63            max_map_size: 8 * 1024 * 1024 * 1024, // 8GB limit
64            enable_mapping: true,
65            read_ahead: true,
66            advisory_locking: false,
67        }
68    }
69
70    /// Disable memory mapping for compatibility mode
71    pub fn disabled() -> Self {
72        Self {
73            max_map_size: 0,
74            enable_mapping: false,
75            read_ahead: false,
76            advisory_locking: false,
77        }
78    }
79}
80
81/// Memory mapping statistics
82#[derive(Debug, Clone, Default)]
83pub struct MemoryMapStats {
84    /// Total bytes mapped
85    pub bytes_mapped: u64,
86    /// Number of active mappings
87    pub active_mappings: usize,
88    /// Number of failed mapping attempts
89    pub failed_mappings: usize,
90    /// Number of fallback operations to regular I/O
91    pub fallback_operations: usize,
92}
93
94/// Cross-platform memory-mapped file wrapper with security boundaries
95#[cfg(feature = "mmap")]
96pub struct MemoryMappedArchive {
97    /// Memory mapping
98    mmap: Mmap,
99    /// Configuration
100    #[allow(dead_code)] // Used for future functionality
101    config: MemoryMapConfig,
102    /// Security limits
103    security_limits: SecurityLimits,
104    /// Session tracker for resource monitoring
105    #[allow(dead_code)] // Used for future functionality
106    session_tracker: Arc<SessionTracker>,
107    /// File size
108    file_size: u64,
109    /// Statistics
110    stats: MemoryMapStats,
111}
112
113#[cfg(feature = "mmap")]
114impl std::fmt::Debug for MemoryMappedArchive {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("MemoryMappedArchive")
117            .field("file_size", &self.file_size)
118            .field("config", &self.config)
119            .field("security_limits", &self.security_limits)
120            .field("stats", &self.stats)
121            .finish()
122    }
123}
124
125#[cfg(feature = "mmap")]
126impl MemoryMappedArchive {
127    /// Create a new memory-mapped archive from a file
128    pub fn new<P: AsRef<Path>>(
129        path: P,
130        config: MemoryMapConfig,
131        security_limits: SecurityLimits,
132        session_tracker: Arc<SessionTracker>,
133    ) -> Result<Self> {
134        // Check if memory mapping is enabled
135        if !config.enable_mapping {
136            return Err(Error::unsupported_feature(
137                "Memory mapping is disabled in configuration",
138            ));
139        }
140
141        // Open the file
142        let file = File::open(&path).map_err(|e| {
143            Error::io_error(format!("Failed to open file for memory mapping: {}", e))
144        })?;
145
146        // Get file size
147        let file_size = file
148            .metadata()
149            .map_err(|e| Error::io_error(format!("Failed to get file metadata: {}", e)))?
150            .len();
151
152        // Validate file size against security limits
153        Self::validate_file_size(file_size, &config, &security_limits)?;
154
155        // Create memory mapping with security options
156        let mmap = Self::create_secure_mmap(&file, file_size, &config)?;
157
158        // Create statistics with proper initialization
159        let stats = MemoryMapStats {
160            bytes_mapped: file_size,
161            active_mappings: 1,
162            ..Default::default()
163        };
164
165        Ok(Self {
166            mmap,
167            config,
168            security_limits,
169            session_tracker,
170            file_size,
171            stats,
172        })
173    }
174
175    /// Create a memory-mapped archive from an existing file handle
176    pub fn from_file(
177        file: File,
178        config: MemoryMapConfig,
179        security_limits: SecurityLimits,
180        session_tracker: Arc<SessionTracker>,
181    ) -> Result<Self> {
182        if !config.enable_mapping {
183            return Err(Error::unsupported_feature(
184                "Memory mapping is disabled in configuration",
185            ));
186        }
187
188        let file_size = file
189            .metadata()
190            .map_err(|e| Error::io_error(format!("Failed to get file metadata: {}", e)))?
191            .len();
192
193        Self::validate_file_size(file_size, &config, &security_limits)?;
194
195        let mmap = Self::create_secure_mmap(&file, file_size, &config)?;
196
197        let stats = MemoryMapStats {
198            bytes_mapped: file_size,
199            active_mappings: 1,
200            ..Default::default()
201        };
202
203        Ok(Self {
204            mmap,
205            config,
206            security_limits,
207            session_tracker,
208            file_size,
209            stats,
210        })
211    }
212
213    /// Validate file size against security limits
214    fn validate_file_size(
215        file_size: u64,
216        config: &MemoryMapConfig,
217        security_limits: &SecurityLimits,
218    ) -> Result<()> {
219        if file_size == 0 {
220            return Err(Error::invalid_format("Cannot memory map empty file"));
221        }
222
223        if file_size > config.max_map_size {
224            return Err(Error::resource_exhaustion(format!(
225                "File size {} exceeds memory mapping limit {}",
226                file_size, config.max_map_size
227            )));
228        }
229
230        if file_size > security_limits.max_archive_size {
231            return Err(Error::resource_exhaustion(format!(
232                "File size {} exceeds security archive size limit {}",
233                file_size, security_limits.max_archive_size
234            )));
235        }
236
237        Ok(())
238    }
239
240    /// Create secure memory mapping with platform-specific optimizations
241    fn create_secure_mmap(file: &File, file_size: u64, config: &MemoryMapConfig) -> Result<Mmap> {
242        let mut mmap_options = MmapOptions::new();
243
244        // Configure read-ahead if enabled
245        if config.read_ahead {
246            // Platform-specific read-ahead hints will be applied by memmap2
247        }
248
249        // Create the memory mapping
250        let mmap = unsafe {
251            mmap_options
252                .len(file_size as usize)
253                .map(file)
254                .map_err(|e| Error::io_error(format!("Failed to create memory mapping: {}", e)))?
255        };
256
257        // Apply platform-specific optimizations
258        #[cfg(unix)]
259        {
260            Self::apply_unix_optimizations(&mmap, config)?;
261        }
262
263        #[cfg(windows)]
264        {
265            Self::apply_windows_optimizations(&mmap, config)?;
266        }
267
268        Ok(mmap)
269    }
270
271    /// Apply Unix-specific optimizations
272    #[cfg(unix)]
273    fn apply_unix_optimizations(mmap: &Mmap, config: &MemoryMapConfig) -> Result<()> {
274        // Apply madvise hints for better performance
275        // Set appropriate madvise flags based on configuration
276        let advice = if config.read_ahead {
277            libc::MADV_SEQUENTIAL | libc::MADV_WILLNEED
278        } else {
279            libc::MADV_RANDOM
280        };
281
282        unsafe {
283            let result = libc::madvise(mmap.as_ptr() as *mut libc::c_void, mmap.len(), advice);
284
285            if result != 0 {
286                log::warn!(
287                    "Failed to apply madvise optimization: {}",
288                    std::io::Error::last_os_error()
289                );
290                // Don't fail on madvise errors - they're just hints
291            }
292        }
293
294        Ok(())
295    }
296
297    /// Apply Windows-specific optimizations
298    #[cfg(windows)]
299    fn apply_windows_optimizations(_mmap: &Mmap, _config: &MemoryMapConfig) -> Result<()> {
300        // Windows-specific memory mapping optimizations could be added here
301        // For now, we rely on memmap2's cross-platform defaults
302        Ok(())
303    }
304
305    /// Safely read data from memory mapping with bounds checking
306    pub fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
307        self.validate_read_bounds(offset, buf.len())?;
308
309        let start = offset as usize;
310        let end = start + buf.len();
311
312        // Safe bounds-checked copy
313        buf.copy_from_slice(&self.mmap[start..end]);
314
315        Ok(())
316    }
317
318    /// Get a safe slice from memory mapping with bounds checking
319    pub fn get_slice(&self, offset: u64, len: usize) -> Result<&[u8]> {
320        self.validate_read_bounds(offset, len)?;
321
322        let start = offset as usize;
323        let end = start + len;
324
325        Ok(&self.mmap[start..end])
326    }
327
328    /// Validate read bounds against memory mapping size
329    fn validate_read_bounds(&self, offset: u64, len: usize) -> Result<()> {
330        let start = offset;
331        let end = offset.saturating_add(len as u64);
332
333        if start >= self.file_size {
334            return Err(Error::invalid_bounds(format!(
335                "Read offset {} beyond file size {}",
336                start, self.file_size
337            )));
338        }
339
340        if end > self.file_size {
341            return Err(Error::invalid_bounds(format!(
342                "Read end {} beyond file size {}",
343                end, self.file_size
344            )));
345        }
346
347        if len > self.security_limits.max_decompressed_size as usize {
348            return Err(Error::resource_exhaustion(format!(
349                "Read length {} exceeds security limit {}",
350                len, self.security_limits.max_decompressed_size
351            )));
352        }
353
354        Ok(())
355    }
356
357    /// Get file size
358    pub fn file_size(&self) -> u64 {
359        self.file_size
360    }
361
362    /// Get memory mapping statistics
363    pub fn stats(&self) -> &MemoryMapStats {
364        &self.stats
365    }
366
367    /// Check if memory mapping is active and healthy
368    pub fn is_healthy(&self) -> bool {
369        self.mmap.len() == self.file_size as usize
370    }
371
372    /// Synchronize memory mapping with underlying file (if writable)
373    pub fn sync(&self) -> Result<()> {
374        // For read-only mappings, this is a no-op
375        // Future versions could support writable mappings
376        Ok(())
377    }
378
379    /// Get raw memory mapping for advanced use cases
380    pub fn as_slice(&self) -> &[u8] {
381        &self.mmap
382    }
383}
384
385#[cfg(feature = "mmap")]
386impl Drop for MemoryMappedArchive {
387    fn drop(&mut self) {
388        // Update statistics
389        self.stats.active_mappings = 0;
390
391        // Memory mapping will be automatically unmapped by memmap2
392        log::debug!("Unmapping memory-mapped archive ({} bytes)", self.file_size);
393    }
394}
395
396/// Memory mapping manager for handling multiple archives
397#[cfg(feature = "mmap")]
398#[derive(Debug)]
399pub struct MemoryMapManager {
400    config: MemoryMapConfig,
401    security_limits: SecurityLimits,
402    session_tracker: Arc<SessionTracker>,
403    global_stats: MemoryMapStats,
404}
405
406#[cfg(feature = "mmap")]
407impl MemoryMapManager {
408    /// Create a new memory mapping manager
409    pub fn new(
410        config: MemoryMapConfig,
411        security_limits: SecurityLimits,
412        session_tracker: Arc<SessionTracker>,
413    ) -> Self {
414        Self {
415            config,
416            security_limits,
417            session_tracker,
418            global_stats: MemoryMapStats::default(),
419        }
420    }
421
422    /// Create a memory-mapped archive
423    pub fn create_mapping<P: AsRef<Path>>(&mut self, path: P) -> Result<MemoryMappedArchive> {
424        match MemoryMappedArchive::new(
425            path,
426            self.config.clone(),
427            self.security_limits.clone(),
428            self.session_tracker.clone(),
429        ) {
430            Ok(mmap) => {
431                self.global_stats.bytes_mapped += mmap.file_size();
432                self.global_stats.active_mappings += 1;
433                Ok(mmap)
434            }
435            Err(e) => {
436                self.global_stats.failed_mappings += 1;
437                Err(e)
438            }
439        }
440    }
441
442    /// Record a fallback operation
443    pub fn record_fallback(&mut self) {
444        self.global_stats.fallback_operations += 1;
445    }
446
447    /// Get global statistics
448    pub fn global_stats(&self) -> &MemoryMapStats {
449        &self.global_stats
450    }
451
452    /// Check if memory mapping should be attempted based on current state
453    pub fn should_attempt_mapping(&self, file_size: u64) -> bool {
454        if !self.config.enable_mapping {
455            return false;
456        }
457
458        if file_size > self.config.max_map_size {
459            return false;
460        }
461
462        if file_size > self.security_limits.max_archive_size {
463            return false;
464        }
465
466        // Consider fallback rate - if too many mappings are failing,
467        // temporarily disable to avoid overhead
468        let total_attempts = self.global_stats.active_mappings + self.global_stats.failed_mappings;
469        if total_attempts > 10 {
470            let failure_rate = self.global_stats.failed_mappings as f64 / total_attempts as f64;
471            if failure_rate > 0.5 {
472                log::warn!(
473                    "High memory mapping failure rate ({:.1}%), temporarily disabling",
474                    failure_rate * 100.0
475                );
476                return false;
477            }
478        }
479
480        true
481    }
482}
483
484// Provide stub implementations when mmap feature is disabled
485/// Stub implementation of memory-mapped archive when feature is disabled
486///
487/// This implementation always returns an error indicating that memory mapping
488/// support was not compiled in and the feature needs to be enabled.
489#[cfg(not(feature = "mmap"))]
490#[derive(Debug)]
491pub struct MemoryMappedArchive;
492
493#[cfg(not(feature = "mmap"))]
494impl MemoryMappedArchive {
495    /// Create a new memory-mapped archive - stub implementation
496    ///
497    /// # Errors
498    /// Always returns an error indicating memory mapping is not available
499    pub fn new<P: AsRef<std::path::Path>>(
500        _path: P,
501        _config: MemoryMapConfig,
502        _security_limits: crate::security::SecurityLimits,
503        _session_tracker: std::sync::Arc<crate::security::SessionTracker>,
504    ) -> crate::Result<Self> {
505        Err(crate::Error::unsupported_feature(
506            "Memory mapping support not compiled in - enable 'mmap' feature",
507        ))
508    }
509}
510
511/// Stub implementation of memory mapping manager when feature is disabled
512///
513/// This provides a no-op implementation for compatibility when memory mapping
514/// is not available.
515#[cfg(not(feature = "mmap"))]
516#[derive(Debug)]
517pub struct MemoryMapManager;
518
519#[cfg(not(feature = "mmap"))]
520impl MemoryMapManager {
521    /// Create a new memory mapping manager - stub implementation
522    pub fn new(
523        _config: MemoryMapConfig,
524        _security_limits: crate::security::SecurityLimits,
525        _session_tracker: std::sync::Arc<crate::security::SessionTracker>,
526    ) -> Self {
527        Self
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    #[cfg(feature = "mmap")]
535    use crate::security::SecurityLimits;
536    #[cfg(feature = "mmap")]
537    use std::io::Write;
538    #[cfg(feature = "mmap")]
539    use tempfile::NamedTempFile;
540
541    #[test]
542    fn test_memory_map_config_defaults() {
543        let config = MemoryMapConfig::default();
544        assert!(config.enable_mapping);
545        assert!(config.read_ahead);
546        assert!(!config.advisory_locking);
547        assert_eq!(config.max_map_size, 2 * 1024 * 1024 * 1024);
548    }
549
550    #[test]
551    fn test_memory_map_config_variants() {
552        let strict = MemoryMapConfig::strict();
553        let permissive = MemoryMapConfig::permissive();
554        let disabled = MemoryMapConfig::disabled();
555
556        assert!(strict.max_map_size < permissive.max_map_size);
557        assert!(strict.advisory_locking);
558        assert!(!strict.read_ahead);
559
560        assert!(!disabled.enable_mapping);
561        assert_eq!(disabled.max_map_size, 0);
562    }
563
564    #[test]
565    fn test_memory_map_stats_default() {
566        let stats = MemoryMapStats::default();
567        assert_eq!(stats.bytes_mapped, 0);
568        assert_eq!(stats.active_mappings, 0);
569        assert_eq!(stats.failed_mappings, 0);
570        assert_eq!(stats.fallback_operations, 0);
571    }
572
573    #[cfg(feature = "mmap")]
574    #[test]
575    fn test_memory_mapped_archive_creation() -> Result<()> {
576        let mut temp_file = NamedTempFile::new().unwrap();
577        let test_data = b"Hello, memory mapped world! This is test data for validation.";
578        temp_file.write_all(test_data).unwrap();
579        temp_file.flush().unwrap();
580
581        let config = MemoryMapConfig::default();
582        let security_limits = SecurityLimits::default();
583        let session_tracker = Arc::new(crate::security::SessionTracker::new());
584
585        let mmap_archive =
586            MemoryMappedArchive::new(temp_file.path(), config, security_limits, session_tracker)?;
587
588        assert_eq!(mmap_archive.file_size(), test_data.len() as u64);
589        assert!(mmap_archive.is_healthy());
590        assert_eq!(mmap_archive.stats().bytes_mapped, test_data.len() as u64);
591        assert_eq!(mmap_archive.stats().active_mappings, 1);
592
593        Ok(())
594    }
595
596    #[cfg(feature = "mmap")]
597    #[test]
598    fn test_memory_mapped_archive_read_at() -> Result<()> {
599        let mut temp_file = NamedTempFile::new().unwrap();
600        let test_data = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
601        temp_file.write_all(test_data).unwrap();
602        temp_file.flush().unwrap();
603
604        let config = MemoryMapConfig::default();
605        let security_limits = SecurityLimits::default();
606        let session_tracker = Arc::new(crate::security::SessionTracker::new());
607
608        let mmap_archive =
609            MemoryMappedArchive::new(temp_file.path(), config, security_limits, session_tracker)?;
610
611        // Test reading from different positions
612        let mut buf = [0u8; 5];
613        mmap_archive.read_at(0, &mut buf)?;
614        assert_eq!(&buf, b"01234");
615
616        mmap_archive.read_at(10, &mut buf)?;
617        assert_eq!(&buf, b"ABCDE");
618
619        mmap_archive.read_at(30, &mut buf)?;
620        assert_eq!(&buf, b"UVWXY");
621
622        Ok(())
623    }
624
625    #[cfg(feature = "mmap")]
626    #[test]
627    fn test_memory_mapped_archive_bounds_checking() -> Result<()> {
628        let mut temp_file = NamedTempFile::new().unwrap();
629        let test_data = b"Short data";
630        temp_file.write_all(test_data).unwrap();
631        temp_file.flush().unwrap();
632
633        let config = MemoryMapConfig::default();
634        let security_limits = SecurityLimits::default();
635        let session_tracker = Arc::new(crate::security::SessionTracker::new());
636
637        let mmap_archive =
638            MemoryMappedArchive::new(temp_file.path(), config, security_limits, session_tracker)?;
639
640        // Test reading beyond file size
641        let mut buf = [0u8; 5];
642        let result = mmap_archive.read_at(test_data.len() as u64, &mut buf);
643        assert!(result.is_err());
644
645        // Test reading past end
646        let result = mmap_archive.read_at(8, &mut buf);
647        assert!(result.is_err());
648
649        Ok(())
650    }
651
652    #[cfg(feature = "mmap")]
653    #[test]
654    fn test_memory_mapped_archive_get_slice() -> Result<()> {
655        let mut temp_file = NamedTempFile::new().unwrap();
656        let test_data = b"Memory mapped slice test data";
657        temp_file.write_all(test_data).unwrap();
658        temp_file.flush().unwrap();
659
660        let config = MemoryMapConfig::default();
661        let security_limits = SecurityLimits::default();
662        let session_tracker = Arc::new(crate::security::SessionTracker::new());
663
664        let mmap_archive =
665            MemoryMappedArchive::new(temp_file.path(), config, security_limits, session_tracker)?;
666
667        // Test getting slices
668        let slice = mmap_archive.get_slice(0, 6)?;
669        assert_eq!(slice, b"Memory");
670
671        let slice = mmap_archive.get_slice(7, 6)?;
672        assert_eq!(slice, b"mapped");
673
674        let slice = mmap_archive.get_slice(14, 5)?;
675        assert_eq!(slice, b"slice");
676
677        Ok(())
678    }
679
680    #[cfg(feature = "mmap")]
681    #[test]
682    fn test_file_size_validation() {
683        let config = MemoryMapConfig::strict(); // 256MB limit
684        let security_limits = SecurityLimits::strict(); // 1GB limit
685
686        // Valid size
687        let result =
688            MemoryMappedArchive::validate_file_size(100 * 1024 * 1024, &config, &security_limits);
689        assert!(result.is_ok());
690
691        // Exceeds config limit
692        let result =
693            MemoryMappedArchive::validate_file_size(300 * 1024 * 1024, &config, &security_limits);
694        assert!(result.is_err());
695
696        // Exceeds security limit
697        let large_config = MemoryMapConfig::permissive(); // 8GB limit
698        let result = MemoryMappedArchive::validate_file_size(
699            2 * 1024 * 1024 * 1024,
700            &large_config,
701            &security_limits,
702        );
703        assert!(result.is_err());
704
705        // Empty file
706        let result = MemoryMappedArchive::validate_file_size(0, &config, &security_limits);
707        assert!(result.is_err());
708    }
709
710    #[cfg(feature = "mmap")]
711    #[test]
712    fn test_memory_map_manager() -> Result<()> {
713        let config = MemoryMapConfig::default();
714        let security_limits = SecurityLimits::default();
715        let session_tracker = Arc::new(crate::security::SessionTracker::new());
716
717        let mut manager = MemoryMapManager::new(config, security_limits, session_tracker);
718
719        // Test should_attempt_mapping
720        assert!(manager.should_attempt_mapping(1024 * 1024)); // 1MB
721        assert!(!manager.should_attempt_mapping(10 * 1024 * 1024 * 1024)); // 10GB
722
723        // Test fallback recording
724        manager.record_fallback();
725        assert_eq!(manager.global_stats().fallback_operations, 1);
726
727        Ok(())
728    }
729
730    #[test]
731    fn test_disabled_feature_stubs() {
732        #[cfg(not(feature = "mmap"))]
733        {
734            let config = MemoryMapConfig::default();
735            let security_limits = crate::security::SecurityLimits::default();
736            let session_tracker = std::sync::Arc::new(crate::security::SessionTracker::new());
737
738            let result = MemoryMappedArchive::new(
739                "/nonexistent/path",
740                config.clone(),
741                security_limits.clone(),
742                session_tracker.clone(),
743            );
744            assert!(result.is_err());
745
746            let _manager = MemoryMapManager::new(config, security_limits, session_tracker);
747        }
748    }
749}