winx_code_agent/utils/mmap.rs
1// File lengths are validated against explicit u64 ceilings before these casts;
2// mmap APIs then require platform-sized usize lengths.
3#![allow(unsafe_code, clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4
5use memmap2::{Mmap, MmapOptions};
6use rayon::prelude::*;
7use std::cmp::min;
8use std::fs::File;
9use std::io::{BufReader, Read, Seek, SeekFrom};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use tracing::{debug, info, trace, warn};
13
14use crate::errors::{Result, WinxError};
15
16/// Maximum file size for direct reading (10MB)
17pub const DIRECT_READ_THRESHOLD: u64 = 10_000_000;
18
19/// Maximum file size for single memory mapping (1GB)
20pub const MAX_MMAP_SIZE: u64 = 1_000_000_000;
21
22/// Maximum file size for segmented memory mapping (4GB)
23pub const MAX_SEGMENTED_MMAP_SIZE: u64 = 4_000_000_000;
24
25/// Segment size for large file memory mapping (256MB)
26pub const SEGMENT_SIZE: u64 = 256_000_000;
27
28const DIRECT_READ_CHUNK_SIZE: usize = 1_048_576;
29const STREAMING_CHUNK_SIZE: usize = 4_194_304;
30
31/// Upper bound on the up-front `Vec::with_capacity` hint derived from (untrusted)
32/// file metadata. A sparse or racing file can report a size far larger than the
33/// bytes it will actually yield; pre-allocating that size lets a caller force a
34/// multi-GB allocation before a single byte is read, and under `panic = "abort"`
35/// an allocation failure takes down the whole server. We cap the hint and let
36/// the `Vec` grow as bytes actually arrive (this mirrors `read_streaming`).
37const MAX_PREALLOC_BYTES: usize = 64 * 1024 * 1024;
38
39/// Read file contents optimally based on file size
40///
41/// This function chooses the optimal reading strategy based on file size:
42/// - Small files: Direct read with standard File I/O
43/// - Medium files: Memory-mapped reading for performance
44/// - Large files: Segmented memory-mapped reading
45/// - Extreme files: Windowed access with streaming
46///
47/// # Arguments
48///
49/// * `path` - Path to the file to read
50/// * `max_file_size` - Maximum allowed file size
51///
52/// # Returns
53///
54/// A vector containing the file contents
55///
56/// # Errors
57///
58/// Returns an error if the file cannot be read or exceeds the size limit
59pub fn read_file_optimized(path: &Path, max_file_size: u64) -> Result<Vec<u8>> {
60 // Get file metadata
61 let file = File::open(path).map_err(|e| WinxError::FileAccessError {
62 path: path.to_path_buf(),
63 message: format!("Error opening file: {e}"),
64 })?;
65
66 let metadata = file.metadata().map_err(|e| WinxError::FileAccessError {
67 path: path.to_path_buf(),
68 message: format!("Failed to get file metadata: {e}"),
69 })?;
70
71 // Check file size
72 let file_size = metadata.len();
73 if file_size > max_file_size {
74 return Err(WinxError::FileTooLarge {
75 path: path.to_path_buf(),
76 size: file_size,
77 max_size: max_file_size,
78 });
79 }
80
81 // Choose reading strategy based on file size
82 if file_size < DIRECT_READ_THRESHOLD {
83 debug!("Using direct read for file: {}", path.display());
84 read_direct(&file, file_size, path)
85 } else if file_size < MAX_MMAP_SIZE {
86 debug!("Using memory-mapped read for file: {}", path.display());
87 read_mmap(&file, path)
88 } else if file_size < MAX_SEGMENTED_MMAP_SIZE {
89 debug!("Using segmented memory-mapped read for file: {}", path.display());
90 read_segmented_mmap(&file, file_size, path)
91 } else {
92 debug!("Using streaming read for extremely large file: {}", path.display());
93 read_streaming(&file, file_size, path)
94 }
95}
96
97/// Read file contents directly using standard I/O
98///
99/// This is efficient for small files.
100///
101/// # Arguments
102///
103/// * `file` - Open file handle
104/// * `file_size` - Size of the file
105/// * `path` - Path to the file (for error reporting)
106///
107/// # Returns
108///
109/// A vector containing the file contents
110///
111/// # Errors
112///
113/// Returns an error if the file cannot be read
114fn read_direct(file: &File, file_size: u64, path: &Path) -> Result<Vec<u8>> {
115 // For very small files (< 1MB), use an optimized approach
116 if file_size < 1_000_000 {
117 // Pre-allocate an exact-sized buffer
118 let mut buffer = Vec::with_capacity(min(file_size as usize, MAX_PREALLOC_BYTES));
119
120 // Create a mutable file handle and seek to the beginning
121 let mut file_handle = file.try_clone().map_err(|e| WinxError::FileAccessError {
122 path: path.to_path_buf(),
123 message: format!("Error cloning file handle: {e}"),
124 })?;
125
126 file_handle.seek(SeekFrom::Start(0)).map_err(|e| WinxError::FileAccessError {
127 path: path.to_path_buf(),
128 message: format!("Error seeking to start of file: {e}"),
129 })?;
130
131 // Use a BufReader with an appropriate buffer size (4K-64K)
132 let mut reader = BufReader::with_capacity(min(file_size as usize, 64 * 1024), file_handle);
133
134 // Read directly to the end
135 reader.read_to_end(&mut buffer).map_err(|e| WinxError::FileAccessError {
136 path: path.to_path_buf(),
137 message: format!("Error reading file: {e}"),
138 })?;
139
140 return Ok(buffer);
141 }
142
143 // For larger files, use a chunked reading approach with progress tracking.
144 // Cap the up-front hint to MAX_PREALLOC_BYTES like the small-file branch above
145 // (line 114) and read_streaming: file_size comes from untrusted metadata and a
146 // sparse/racing file can report a size far bigger than it yields, turning this
147 // into a forced multi-GB allocation (OOM = abort under panic = "abort").
148 let mut buffer = Vec::with_capacity(min(file_size as usize, MAX_PREALLOC_BYTES));
149
150 // Create a mutable file handle and seek to the beginning
151 let mut file_handle = file.try_clone().map_err(|e| WinxError::FileAccessError {
152 path: path.to_path_buf(),
153 message: format!("Error cloning file handle: {e}"),
154 })?;
155
156 file_handle.seek(SeekFrom::Start(0)).map_err(|e| WinxError::FileAccessError {
157 path: path.to_path_buf(),
158 message: format!("Error seeking to start of file: {e}"),
159 })?;
160
161 let mut reader = BufReader::with_capacity(262_144, file_handle); // 256KB buffer
162
163 let mut chunk = vec![0; DIRECT_READ_CHUNK_SIZE];
164 let mut bytes_read = 0;
165
166 loop {
167 match reader.read(&mut chunk) {
168 Ok(0) => break, // EOF
169 Ok(n) => {
170 buffer.extend_from_slice(&chunk[..n]);
171 bytes_read += n as u64;
172
173 // Log progress for larger files
174 if file_size > 5_000_000 && bytes_read % 5_000_000 < DIRECT_READ_CHUNK_SIZE as u64 {
175 trace!(
176 "Read progress for {}: {}MB/{}MB ({}%)",
177 path.display(),
178 bytes_read / 1_000_000,
179 file_size / 1_000_000,
180 bytes_read * 100 / file_size
181 );
182 }
183 }
184 Err(e) => {
185 return Err(WinxError::FileAccessError {
186 path: path.to_path_buf(),
187 message: format!("Error reading file chunk: {e}"),
188 });
189 }
190 }
191 }
192
193 Ok(buffer)
194}
195
196/// Read file contents using memory mapping
197///
198/// This is efficient for larger files as it avoids loading the entire
199/// file into memory at once.
200///
201/// # Arguments
202///
203/// * `file` - Open file handle
204/// * `path` - Path to the file (for error reporting)
205///
206/// # Returns
207///
208/// A vector containing the file contents
209///
210/// # Errors
211///
212/// Returns an error if the file cannot be mapped
213fn read_mmap(file: &File, path: &Path) -> Result<Vec<u8>> {
214 // Check for empty file to avoid mmap error
215 if file.metadata().map_or(0, |m| m.len()) == 0 {
216 return Ok(Vec::new());
217 }
218
219 // SAFETY: Memory mapping a file is inherently unsafe because:
220 // - The file could be modified by another process during access
221 // - The file could be truncated, causing access to invalid memory
222 // We mitigate these risks by:
223 // - Using the mapped data immediately and converting to Vec<u8>
224 // - Not holding the mmap across async boundaries
225 // - File size was verified before this call
226 let mmap = unsafe { MmapOptions::new().map(file) }.map_err(|e| WinxError::FileAccessError {
227 path: path.to_path_buf(),
228 message: format!("Failed to memory-map file: {e}"),
229 })?;
230
231 // Copy the mapped bytes into an owned Vec. Copying out of an mmap is
232 // bandwidth-bound (a memcpy), so the old Rayon "parallel" path bought
233 // nothing: it allocated the file twice — a zero-filled `result` plus a
234 // Vec of per-chunk `to_vec()`s — only to memcpy between them. `to_vec()`
235 // allocates exactly `mmap.len()` (the real mapped size, not an untrusted
236 // metadata hint) and copies once.
237 Ok(mmap.to_vec())
238}
239
240/// Read large file with segmented memory mapping
241///
242/// This function reads a large file using multiple memory mapped segments,
243/// which allows handling files larger than the maximum mapping size.
244///
245/// # Arguments
246///
247/// * `file` - Open file handle
248/// * `file_size` - Size of the file
249/// * `path` - Path to the file (for error reporting)
250///
251/// # Returns
252///
253/// A vector containing the file contents
254///
255/// # Errors
256///
257/// Returns an error if the file cannot be read or mapped
258fn read_segmented_mmap(file: &File, file_size: u64, path: &Path) -> Result<Vec<u8>> {
259 // Calculate number of segments needed
260 let segment_count = file_size.div_ceil(SEGMENT_SIZE);
261 debug!(
262 "Reading file {} in {} segments of {}MB each",
263 path.display(),
264 segment_count,
265 SEGMENT_SIZE / 1_000_000
266 );
267
268 // Pre-allocate result vector (capped — see MAX_PREALLOC_BYTES)
269 let mut result = Vec::with_capacity(min(file_size as usize, MAX_PREALLOC_BYTES));
270
271 // Process each segment
272 for i in 0..segment_count {
273 let segment_start = i * SEGMENT_SIZE;
274 let segment_size = min(SEGMENT_SIZE, file_size - segment_start);
275
276 info!(
277 "Processing segment {}/{} of file {} ({:.1}%)",
278 i + 1,
279 segment_count,
280 path.display(),
281 (segment_start as f64 / file_size as f64) * 100.0
282 );
283
284 // SAFETY: the shared file handle stays valid for the whole loop; mmap
285 // uses the explicit `.offset()`, not the fd seek position, so segments
286 // never conflict and no per-segment open()/seek() is needed. Bounds come
287 // from the verified file size; data is copied to Vec, not held.
288 let segment_mmap = unsafe {
289 MmapOptions::new().offset(segment_start).len(segment_size as usize).map(file)
290 }
291 .map_err(|e| WinxError::FileAccessError {
292 path: path.to_path_buf(),
293 message: format!("Failed to memory-map file segment {i}: {e}"),
294 })?;
295
296 // Append segment data to result
297 result.extend_from_slice(&segment_mmap);
298 }
299
300 Ok(result)
301}
302
303/// Read extremely large file with streaming
304///
305/// This function reads an extremely large file using a streaming approach,
306/// which minimizes memory usage by processing the file in small chunks.
307///
308/// # Arguments
309///
310/// * `file` - Open file handle
311/// * `file_size` - Size of the file
312/// * `path` - Path to the file (for error reporting)
313///
314/// # Returns
315///
316/// A vector containing the file contents
317///
318/// # Errors
319///
320/// Returns an error if the file cannot be read
321fn read_streaming(file: &File, file_size: u64, path: &Path) -> Result<Vec<u8>> {
322 warn!(
323 "Reading extremely large file ({}GB) with streaming approach: {}",
324 file_size / 1_000_000_000,
325 path.display()
326 );
327
328 // For extreme files, pre-allocate a bounded vector and grow as bytes arrive.
329 // Cap at MAX_PREALLOC_BYTES (not file_size, not 1GB): file_size is untrusted
330 // metadata, and a multi-GB up-front Vec is exactly the OOM-via-abort vector the
331 // cap exists to close. The extra reallocs on a genuinely huge file are rare and
332 // cheap next to crashing the server.
333 let initial_capacity = min(file_size as usize, MAX_PREALLOC_BYTES);
334 let mut buffer = Vec::with_capacity(initial_capacity);
335
336 let mut reader = BufReader::with_capacity(STREAMING_CHUNK_SIZE, file);
337 let mut chunk = vec![0; STREAMING_CHUNK_SIZE];
338 let mut bytes_read = 0;
339
340 loop {
341 match reader.read(&mut chunk) {
342 Ok(0) => break, // EOF
343 Ok(n) => {
344 buffer.extend_from_slice(&chunk[..n]);
345 bytes_read += n as u64;
346
347 // Log progress every 100MB
348 if bytes_read % 100_000_000 < STREAMING_CHUNK_SIZE as u64 {
349 info!(
350 "Read progress for large file {}: {:.2}GB/{:.2}GB ({:.1}%)",
351 path.display(),
352 bytes_read as f64 / 1_000_000_000.0,
353 file_size as f64 / 1_000_000_000.0,
354 bytes_read as f64 * 100.0 / file_size as f64
355 );
356 }
357 }
358 Err(e) => {
359 return Err(WinxError::FileAccessError {
360 path: path.to_path_buf(),
361 message: format!("Error reading file chunk at position {bytes_read}: {e}"),
362 });
363 }
364 }
365 }
366
367 Ok(buffer)
368}
369
370/// Read a specific segment of a file
371///
372/// This function reads a specific segment of a file using memory mapping
373/// or direct I/O, depending on the segment size.
374///
375/// # Arguments
376///
377/// * `path` - Path to the file
378/// * `offset` - Starting offset in bytes
379/// * `length` - Length of segment to read in bytes
380/// * `max_file_size` - Maximum allowed file size
381///
382/// # Returns
383///
384/// A vector containing the file segment contents
385///
386/// # Errors
387///
388/// Returns an error if the file cannot be read or exceeds the size limit
389pub fn read_file_segment(
390 path: &Path,
391 offset: u64,
392 length: u64,
393 max_file_size: u64,
394) -> Result<Vec<u8>> {
395 // Get file metadata
396 let file = File::open(path).map_err(|e| WinxError::FileAccessError {
397 path: path.to_path_buf(),
398 message: format!("Error opening file: {e}"),
399 })?;
400
401 let metadata = file.metadata().map_err(|e| WinxError::FileAccessError {
402 path: path.to_path_buf(),
403 message: format!("Failed to get file metadata: {e}"),
404 })?;
405
406 // Check file size
407 let file_size = metadata.len();
408 if file_size > max_file_size {
409 return Err(WinxError::FileTooLarge {
410 path: path.to_path_buf(),
411 size: file_size,
412 max_size: max_file_size,
413 });
414 }
415
416 // Validate offset and length
417 if offset >= file_size {
418 return Err(WinxError::FileAccessError {
419 path: path.to_path_buf(),
420 message: format!("Offset {offset} exceeds file size {file_size}"),
421 });
422 }
423
424 // Adjust length if needed to stay within file bounds
425 let length = min(length, file_size - offset);
426
427 // Choose reading strategy based on segment size
428 if length < DIRECT_READ_THRESHOLD {
429 debug!("Using direct read for file segment: {}", path.display());
430 read_segment_direct(&file, offset, length, path)
431 } else {
432 debug!("Using memory-mapped read for file segment: {}", path.display());
433 read_segment_mmap(&file, offset, length, path)
434 }
435}
436
437/// Read a file segment directly using standard I/O
438///
439/// # Arguments
440///
441/// * `file` - Open file handle
442/// * `offset` - Starting offset in bytes
443/// * `length` - Length of segment to read in bytes
444/// * `path` - Path to the file (for error reporting)
445///
446/// # Returns
447///
448/// A vector containing the file segment contents
449///
450/// # Errors
451///
452/// Returns an error if the file segment cannot be read
453fn read_segment_direct(file: &File, offset: u64, length: u64, path: &Path) -> Result<Vec<u8>> {
454 // Create a new file object that can be seeked
455 let mut seekable_file = file.try_clone().map_err(|e| WinxError::FileAccessError {
456 path: path.to_path_buf(),
457 message: format!("Failed to clone file handle: {e}"),
458 })?;
459
460 // Seek to the specified offset
461 seekable_file.seek(SeekFrom::Start(offset)).map_err(|e| WinxError::FileAccessError {
462 path: path.to_path_buf(),
463 message: format!("Failed to seek to offset {offset}: {e}"),
464 })?;
465
466 // Read the specified length
467 let mut buffer = Vec::with_capacity(length as usize);
468 let reader = BufReader::with_capacity(min(length as usize, 64 * 1024), seekable_file);
469
470 // Use take to limit the read to the specified length
471 reader.take(length).read_to_end(&mut buffer).map_err(|e| WinxError::FileAccessError {
472 path: path.to_path_buf(),
473 message: format!("Error reading file segment: {e}"),
474 })?;
475
476 Ok(buffer)
477}
478
479/// Read a file segment using memory mapping
480///
481/// # Arguments
482///
483/// * `file` - Open file handle
484/// * `offset` - Starting offset in bytes
485/// * `length` - Length of segment to read in bytes
486/// * `path` - Path to the file (for error reporting)
487///
488/// # Returns
489///
490/// A vector containing the file segment contents
491///
492/// # Errors
493///
494/// Returns an error if the file segment cannot be mapped
495fn read_segment_mmap(file: &File, offset: u64, length: u64, path: &Path) -> Result<Vec<u8>> {
496 // SAFETY: Memory mapping is safe here because:
497 // - Offset and length were validated against file size by caller
498 // - Data is immediately copied to Vec<u8>, not held
499 // - File handle remains valid for duration of the map operation
500 let segment_mmap = unsafe { MmapOptions::new().offset(offset).len(length as usize).map(file) }
501 .map_err(|e| WinxError::FileAccessError {
502 path: path.to_path_buf(),
503 message: format!("Failed to memory-map file segment: {e}"),
504 })?;
505
506 // Copy segment data to result
507 Ok(segment_mmap.to_vec())
508}
509
510/// Read a text file as a string using the optimal reading strategy
511///
512/// This function reads a file as text, using the most efficient strategy
513/// based on the file size.
514///
515/// # Arguments
516///
517/// * `path` - Path to the file to read
518/// * `max_file_size` - Maximum allowed file size
519///
520/// # Returns
521///
522/// A string containing the file contents
523///
524/// # Errors
525///
526/// Returns an error if the file cannot be read or exceeds the size limit
527pub fn read_file_to_string(path: &Path, max_file_size: u64) -> Result<String> {
528 let bytes = read_file_optimized(path, max_file_size)?;
529
530 String::from_utf8(bytes).map_err(|e| WinxError::FileAccessError {
531 path: path.to_path_buf(),
532 message: format!("Failed to decode file as UTF-8: {e}"),
533 })
534}
535
536/// Read a text file in a parallel, line-by-line fashion
537///
538/// This processes lines in parallel using Rayon for faster processing
539/// of large text files.
540///
541/// # Arguments
542///
543/// * `path` - Path to the file to read
544/// * `max_file_size` - Maximum allowed file size
545/// * `line_processor` - Function to process each line
546///
547/// # Returns
548///
549/// Result indicating success or failure
550///
551/// # Errors
552///
553/// Returns an error if the file cannot be read or exceeds the size limit
554pub fn process_text_file_parallel<F>(
555 path: &Path,
556 max_file_size: u64,
557 line_processor: F,
558) -> Result<()>
559where
560 F: Fn(&str) + Sync,
561{
562 let content = read_file_to_string(path, max_file_size)?;
563
564 // For larger files, use parallel processing
565 if content.len() > 1_000_000 {
566 // 1MB
567 content.par_lines().for_each(|line| {
568 line_processor(line);
569 });
570 } else {
571 // For smaller files, process sequentially
572 content.lines().for_each(|line| {
573 line_processor(line);
574 });
575 }
576
577 Ok(())
578}
579
580/// Read a text file segment as a string
581///
582/// # Arguments
583///
584/// * `path` - Path to the file to read
585/// * `offset` - Starting offset in bytes
586/// * `length` - Length of segment to read in bytes
587/// * `max_file_size` - Maximum allowed file size
588///
589/// # Returns
590///
591/// A string containing the file segment contents
592///
593/// # Errors
594///
595/// Returns an error if the file segment cannot be read
596pub fn read_file_segment_to_string(
597 path: &Path,
598 offset: u64,
599 length: u64,
600 max_file_size: u64,
601) -> Result<String> {
602 let bytes = read_file_segment(path, offset, length, max_file_size)?;
603
604 String::from_utf8(bytes).map_err(|e| WinxError::FileAccessError {
605 path: path.to_path_buf(),
606 message: format!("Failed to decode file segment as UTF-8: {e}"),
607 })
608}
609
610/// `ShareableMap` provides a thread-safe memory-mapped file access
611///
612/// This is useful for providing read-only access to multiple threads
613/// without copying the data, especially for large files.
614#[derive(Clone)]
615pub struct ShareableMap {
616 /// The memory-mapped file data
617 data: Arc<Mmap>,
618 /// The path to the mapped file
619 path: PathBuf,
620}
621
622impl ShareableMap {
623 /// Create a new `ShareableMap` from a file
624 ///
625 /// # Arguments
626 ///
627 /// * `path` - Path to the file to map
628 ///
629 /// # Returns
630 ///
631 /// A Result containing the `ShareableMap` or an error
632 ///
633 /// # Errors
634 ///
635 /// Returns an error if the file cannot be mapped
636 pub fn new(path: &Path) -> Result<Self> {
637 let file = File::open(path).map_err(|e| WinxError::FileAccessError {
638 path: path.to_path_buf(),
639 message: format!("Error opening file: {e}"),
640 })?;
641
642 // Check for empty file
643 if file
644 .metadata()
645 .map_err(|e| WinxError::FileAccessError {
646 path: path.to_path_buf(),
647 message: format!("Failed to get metadata: {e}"),
648 })?
649 .len()
650 == 0
651 {
652 return Err(WinxError::FileAccessError {
653 path: path.to_path_buf(),
654 message: "Cannot memory map empty file".to_string(),
655 });
656 }
657
658 // SAFETY: ShareableMap wraps the Mmap in Arc for thread-safe sharing.
659 // The mapped data is read-only and the Arc ensures the Mmap outlives
660 // all references. Users must be aware the underlying file should not
661 // be modified while ShareableMap is in use.
662 let mmap =
663 unsafe { MmapOptions::new().map(&file) }.map_err(|e| WinxError::FileAccessError {
664 path: path.to_path_buf(),
665 message: format!("Failed to memory-map file: {e}"),
666 })?;
667
668 Ok(Self { data: Arc::new(mmap), path: path.to_path_buf() })
669 }
670
671 /// Create a new `ShareableMap` for a segment of a file
672 ///
673 /// # Arguments
674 ///
675 /// * `path` - Path to the file to map
676 /// * `offset` - Starting offset in bytes
677 /// * `length` - Length of segment to map in bytes
678 ///
679 /// # Returns
680 ///
681 /// A Result containing the `ShareableMap` or an error
682 ///
683 /// # Errors
684 ///
685 /// Returns an error if the file segment cannot be mapped
686 pub fn new_segment(path: &Path, offset: u64, length: u64) -> Result<Self> {
687 if length == 0 {
688 return Err(WinxError::FileAccessError {
689 path: path.to_path_buf(),
690 message: "Cannot memory map segment of length 0".to_string(),
691 });
692 }
693
694 let file = File::open(path).map_err(|e| WinxError::FileAccessError {
695 path: path.to_path_buf(),
696 message: format!("Error opening file: {e}"),
697 })?;
698
699 // SAFETY: Same as ShareableMap::new, plus:
700 // - Caller is responsible for ensuring offset+length is within file bounds
701 // - The segment mapping is wrapped in Arc for safe sharing
702 let mmap = unsafe { MmapOptions::new().offset(offset).len(length as usize).map(&file) }
703 .map_err(|e| WinxError::FileAccessError {
704 path: path.to_path_buf(),
705 message: format!("Failed to memory-map file segment: {e}"),
706 })?;
707
708 Ok(Self { data: Arc::new(mmap), path: path.to_path_buf() })
709 }
710
711 /// Get the data as a byte slice
712 pub fn as_slice(&self) -> &[u8] {
713 &self.data
714 }
715
716 /// Get the path to the mapped file
717 pub fn path(&self) -> &Path {
718 &self.path
719 }
720
721 /// Get the size of the mapped data
722 pub fn len(&self) -> usize {
723 self.data.len()
724 }
725
726 /// Check if the mapped data is empty
727 pub fn is_empty(&self) -> bool {
728 self.data.is_empty()
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use std::io::Write;
736 use tempfile::NamedTempFile;
737
738 fn create_test_file(size: usize) -> Result<(NamedTempFile, Vec<u8>)> {
739 let mut file = NamedTempFile::new()?;
740 let mut data = Vec::with_capacity(size);
741
742 // Fill with pattern data (more realistic than zeros)
743 for i in 0..size {
744 data.push((i % 256) as u8);
745 }
746
747 file.write_all(&data)?;
748 file.flush()?;
749
750 Ok((file, data))
751 }
752
753 #[test]
754 fn test_direct_read_small_file() -> Result<()> {
755 let size = 10 * 1024; // 10KB
756 let (file, expected_data) = create_test_file(size)?;
757
758 let result = read_direct(file.as_file(), size as u64, file.path())?;
759 assert_eq!(result, expected_data);
760 Ok(())
761 }
762
763 #[test]
764 fn test_mmap_read() -> Result<()> {
765 let size = 1024 * 1024; // 1MB
766 let (file, expected_data) = create_test_file(size)?;
767
768 let result = read_mmap(file.as_file(), file.path())?;
769 assert_eq!(result, expected_data);
770 Ok(())
771 }
772
773 #[test]
774 fn test_file_segment_read() -> Result<()> {
775 let size = 1024 * 1024; // 1MB
776 let (file, data) = create_test_file(size)?;
777
778 // Read a segment from the middle
779 let offset = 100 * 1024; // 100KB
780 let length = 200 * 1024; // 200KB
781 let expected_segment = &data[offset as usize..(offset + length) as usize];
782
783 let result = read_segment_direct(file.as_file(), offset, length, file.path())?;
784 assert_eq!(result, expected_segment);
785
786 let result = read_segment_mmap(file.as_file(), offset, length, file.path())?;
787 assert_eq!(result, expected_segment);
788 Ok(())
789 }
790
791 #[test]
792 fn test_shareable_map() -> Result<()> {
793 let size = 100 * 1024; // 100KB
794 let (file, data) = create_test_file(size)?;
795
796 let map = ShareableMap::new(file.path())?;
797 assert_eq!(map.as_slice(), &data);
798
799 // Test segment
800 let offset = 10 * 1024; // 10KB
801 let length = 20 * 1024; // 20KB
802 let segment_map = ShareableMap::new_segment(file.path(), offset, length)?;
803 assert_eq!(segment_map.as_slice(), &data[offset as usize..(offset + length) as usize]);
804 Ok(())
805 }
806
807 #[test]
808 fn test_parallel_processing() -> Result<()> {
809 // Create a test file with lines
810 let mut file = NamedTempFile::new()?;
811 let mut lines = Vec::new();
812
813 for i in 0..1000 {
814 let line = format!("Line {i}\n");
815 file.write_all(line.as_bytes())?;
816 lines.push(format!("Line {i}"));
817 }
818 file.flush()?;
819
820 // Test parallel processing
821 let processed_lines = std::sync::Mutex::new(Vec::new());
822
823 process_text_file_parallel(file.path(), 1_000_000, |line| {
824 if let Ok(mut lines) = processed_lines.lock() {
825 lines.push(line.to_string());
826 }
827 })?;
828
829 // Verify results (order may differ due to parallel processing)
830 let result =
831 processed_lines.lock().map_err(|error| WinxError::ResourceAllocationError {
832 message: format!("Failed to lock processed lines: {error}"),
833 })?;
834 assert_eq!(result.len(), lines.len());
835
836 // Check that all lines are present
837 for line in &lines {
838 assert!(result.contains(line));
839 }
840 Ok(())
841 }
842}