1use std::fs::File;
2use std::path::Path;
3use std::sync::Arc;
4
5use memmap2::MmapOptions;
6
7use super::{FileInfo, FileRange};
8use crate::Error;
9use crate::mincore::PageMap;
10
11pub trait FileProcessed {
12 type Output;
13 fn into_output(self) -> Self::Output;
14 fn output_ref(&self) -> &Self::Output;
15 fn total_pages(&self) -> usize;
16 fn pages_in_core_before(&self) -> Option<usize> {
17 None
18 }
19 fn pages_in_core_after(&self) -> usize;
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct FullResult<O, PM> {
24 pub output: O,
25 pub total_pages: usize,
26 pub pages_in_core_before: usize,
27 pub pages_in_core_after: usize,
28 pub residency_before: Option<PM>,
29 pub residency_after: Option<PM>,
30}
31
32impl<O, PM> FileProcessed for FullResult<O, PM> {
33 type Output = O;
34 fn into_output(self) -> O {
35 self.output
36 }
37 fn output_ref(&self) -> &O {
38 &self.output
39 }
40 fn total_pages(&self) -> usize {
41 self.total_pages
42 }
43 fn pages_in_core_before(&self) -> Option<usize> {
44 Some(self.pages_in_core_before)
45 }
46 fn pages_in_core_after(&self) -> usize {
47 self.pages_in_core_after
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub struct CountsResult<O> {
53 pub output: O,
54 pub total_pages: usize,
55 pub pages_in_core_after: usize,
56}
57
58impl<O> FileProcessed for CountsResult<O> {
59 type Output = O;
60 fn into_output(self) -> O {
61 self.output
62 }
63 fn output_ref(&self) -> &O {
64 &self.output
65 }
66 fn total_pages(&self) -> usize {
67 self.total_pages
68 }
69 fn pages_in_core_after(&self) -> usize {
70 self.pages_in_core_after
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub struct SkipResult<O> {
76 pub output: O,
77 pub total_pages: usize,
78}
79
80impl<O> FileProcessed for SkipResult<O> {
81 type Output = O;
82 fn into_output(self) -> O {
83 self.output
84 }
85 fn output_ref(&self) -> &O {
86 &self.output
87 }
88 fn total_pages(&self) -> usize {
89 self.total_pages
90 }
91 fn pages_in_core_after(&self) -> usize {
92 0
93 }
94}
95
96pub(crate) struct PreparedFile {
97 pub file: File,
98 pub offset: u64,
99 pub len: usize,
100 pub total_pages: usize,
101 pub mmap: Arc<memmap2::Mmap>,
102}
103
104pub(crate) fn prepare_file(path: &Path, range: &FileRange) -> crate::Result<Option<PreparedFile>> {
105 let io_err = |e| Error::io(path.display().to_string(), e);
106
107 let file = fs_err::File::open(path).map_err(io_err)?;
108 let file_len = file.metadata().map_err(io_err)?.len();
109
110 if file_len == 0 {
111 return Ok(None);
112 }
113
114 let (offset, len) =
115 effective_range(file_len, range).ok_or_else(|| Error::OffsetBeyondFile {
116 path: path.to_path_buf(),
117 offset: range.offset,
118 file_len,
119 })?;
120
121 let total_pages = len.div_ceil(*crate::pagesize::PAGE_SIZE);
122
123 let mmap = Arc::new(unsafe {
124 MmapOptions::new()
125 .offset(offset)
126 .len(len)
127 .map(file.file())
128 .map_err(io_err)?
129 });
130
131 Ok(Some(PreparedFile {
132 file: file.into_file(),
133 offset,
134 len,
135 total_pages,
136 mmap,
137 }))
138}
139
140fn effective_range(file_len: u64, range: &FileRange) -> Option<(u64, usize)> {
141 if file_len == 0 {
142 return None;
143 }
144 let offset = range.offset;
145 if offset >= file_len {
146 return None;
147 }
148 let len = match range.max_len {
149 Some(max) if (offset + max) < file_len => max as usize,
150 _ => (file_len - offset) as usize,
151 };
152 Some((offset, len))
153}
154
155pub fn file_info<PM: PageMap>(
156 path: &Path,
157 range: &FileRange,
158) -> crate::Result<Option<FileInfo<PM>>> {
159 let io_err = |e| Error::io(path.display().to_string(), e);
160
161 let file = fs_err::File::open(path).map_err(io_err)?;
162 let file_len = file.metadata().map_err(io_err)?.len();
163
164 let Some((offset, len)) = effective_range(file_len, range) else {
165 return Ok(None);
166 };
167
168 let total_pages = len.div_ceil(*crate::pagesize::PAGE_SIZE);
169 let mmap = unsafe {
170 MmapOptions::new()
171 .offset(offset)
172 .len(len)
173 .map(file.file())
174 .map_err(io_err)?
175 };
176 let residency: PM = crate::mincore::residency(&mmap, len)?;
177 Ok(Some(FileInfo {
178 total_pages,
179 residency,
180 }))
181}
182
183#[cfg(test)]
184mod tests {
185 use crate::mincore::PageMapSlice as _;
186 use crate::mode;
187
188 use super::*;
189 use std::io::Write;
190 use std::sync::atomic::Ordering;
191
192 use super::super::*;
193
194 fn create_temp_file(pages: usize) -> (tempfile::NamedTempFile, usize) {
195 let page_size = *crate::pagesize::PAGE_SIZE;
196 let size = page_size * pages;
197 let mut f = tempfile::NamedTempFile::new().unwrap();
198 f.write_all(&vec![0xABu8; size]).unwrap();
199 f.flush().unwrap();
200 (f, size)
201 }
202
203 macro_rules! process_tests {
204 ($t:ty, $mod:ident) => {
205 mod $mod {
206 use super::*;
207
208 #[test]
209 fn query_counts_pages() {
210 let (f, _) = create_temp_file(4);
211 let range = FileRange {
212 offset: 0,
213 max_len: None,
214 };
215 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
216 .unwrap()
217 .unwrap();
218 assert_eq!(result.total_pages, 4);
219 }
220
221 #[test]
222 fn empty_file_returns_none() {
223 let f = tempfile::NamedTempFile::new().unwrap();
224 let range = FileRange {
225 offset: 0,
226 max_len: None,
227 };
228 let result: Option<CountsResult<()>> =
229 mode::counts_process_file::<Query, $t>(&Query, f.path(), &range).unwrap();
230 assert!(result.is_none());
231 }
232
233 #[test]
234 fn offset_beyond_file() {
235 let (f, _) = create_temp_file(1);
236 let range = FileRange {
237 offset: 1_000_000,
238 max_len: None,
239 };
240 let result: crate::Result<Option<CountsResult<()>>> =
241 mode::counts_process_file::<Query, $t>(&Query, f.path(), &range);
242 assert!(result.is_err());
243 let err = result.unwrap_err();
244 assert!(
245 matches!(err, Error::OffsetBeyondFile { .. }),
246 "expected OffsetBeyondFile, got: {err}"
247 );
248 }
249
250 #[test]
251 fn with_max_len() {
252 let (f, _) = create_temp_file(8);
253 let page_size = *crate::pagesize::PAGE_SIZE;
254 let range = FileRange {
255 offset: 0,
256 max_len: Some((page_size * 2) as u64),
257 };
258 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
259 .unwrap()
260 .unwrap();
261 assert_eq!(result.total_pages, 2);
262 }
263
264 #[test]
265 fn touch_makes_resident() {
266 let (f, size) = create_temp_file(4);
267 let range = FileRange {
268 offset: 0,
269 max_len: None,
270 };
271
272 mode::counts_process_file::<Evict, $t>(&Evict, f.path(), &range).unwrap();
273 mode::counts_process_file::<Touch, $t>(&Touch, f.path(), &range).unwrap();
274
275 let file = fs_err::File::open(f.path()).unwrap();
276 let mmap_check = unsafe {
277 memmap2::MmapOptions::new()
278 .len(size)
279 .map(file.file())
280 .unwrap()
281 };
282 let residency: $t = crate::mincore::residency(&mmap_check, size).unwrap();
283 assert!(
284 (0..residency.len()).all(|i| residency[i..i + 1].count_filled() == 1),
285 "expected all pages resident after touch"
286 );
287 }
288
289 #[test]
290 fn evict_succeeds() {
291 let (f, _) = create_temp_file(4);
292 let range = FileRange {
293 offset: 0,
294 max_len: None,
295 };
296 let result: crate::Result<Option<CountsResult<()>>> =
297 mode::counts_process_file::<Evict, $t>(&Evict, f.path(), &range);
298 assert!(result.is_ok());
299 }
300
301 #[test]
302 fn full_residency() {
303 let (f, _) = create_temp_file(4);
304 let range = FileRange {
305 offset: 0,
306 max_len: None,
307 };
308 let result: FullResult<(), $t> =
309 mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None, None)
310 .unwrap()
311 .unwrap();
312 assert!(result.residency_after.is_some());
313 assert_eq!(result.residency_after.unwrap().len(), 4);
314 }
315
316 #[test]
317 fn query_full_reuses_before() {
318 let (f, _) = create_temp_file(4);
319 let range = FileRange {
320 offset: 0,
321 max_len: None,
322 };
323 let result: FullResult<(), $t> =
324 mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None, None)
325 .unwrap()
326 .unwrap();
327 assert_eq!(result.pages_in_core_before, result.pages_in_core_after);
328 assert!(result.residency_before.is_none());
329 assert!(result.residency_after.is_some());
330 }
331
332 #[test]
333 fn counts_without_bitmap() {
334 let (f, _) = create_temp_file(4);
335 let range = FileRange {
336 offset: 0,
337 max_len: None,
338 };
339 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
340 .unwrap()
341 .unwrap();
342 assert_eq!(result.total_pages, 4);
343 }
344
345 #[test]
346 fn nonexistent_returns_error() {
347 let range = FileRange {
348 offset: 0,
349 max_len: None,
350 };
351 let result: crate::Result<Option<CountsResult<()>>> =
352 mode::counts_process_file::<Query, $t>(
353 &Query,
354 std::path::Path::new("/nonexistent/file.dat"),
355 &range,
356 );
357 assert!(result.is_err());
358 }
359
360 #[test]
361 fn file_info_pages() {
362 let (f, _) = create_temp_file(4);
363 let range = FileRange {
364 offset: 0,
365 max_len: None,
366 };
367 let info: FileInfo<$t> = file_info(f.path(), &range).unwrap().unwrap();
368 assert_eq!(info.total_pages, 4);
369 assert_eq!(info.residency.len(), 4);
370 }
371 }
372 };
373 }
374
375 process_tests!(Vec<bool>, vec_bool_impl);
376
377 #[cfg(feature = "bitvec")]
378 process_tests!(::bitvec::vec::BitVec, bitvec_impl);
379
380 #[test]
381 fn test_stats_default() {
382 let stats = Stats::default();
383 assert_eq!(stats.total_pages.load(Ordering::Relaxed), 0);
384 assert_eq!(stats.initial_pages_in_core.load(Ordering::Relaxed), 0);
385 assert_eq!(stats.action_pages.load(Ordering::Relaxed), 0);
386 assert_eq!(stats.total_files.load(Ordering::Relaxed), 0);
387 assert_eq!(stats.total_dirs.load(Ordering::Relaxed), 0);
388 }
389}