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