Skip to main content

mmap_io/
iterator.rs

1//! Zero-copy iterator-based access to a memory-mapped file.
2//!
3//! [`ChunkIterator`] and [`PageIterator`] yield [`MappedSlice<'a>`]
4//! items that borrow directly from the underlying mapping (no
5//! allocation, no copy). On RW mappings the iterator holds a read
6//! guard for its entire lifetime, which blocks any concurrent
7//! `resize()` until iteration completes.
8//!
9//! The owned variants ([`ChunkIteratorOwned`], [`PageIteratorOwned`])
10//! yield `Result<Vec<u8>>` for callers that genuinely need owned
11//! buffers (e.g. when handing data to a thread that outlives the
12//! mapping borrow). They allocate one `Vec<u8>` per item.
13
14use crate::errors::{MmapIoError, Result};
15use crate::mmap::{MapVariant, MappedSlice, MemoryMappedFile};
16use crate::utils::page_size;
17use memmap2::MmapMut;
18use parking_lot::RwLockReadGuard;
19use std::marker::PhantomData;
20
21/// Internal guard variant: holds the RW read lock alive (so the
22/// underlying mapping cannot be remapped via `resize()`), or is
23/// `None` for RO / COW mappings whose mappings are inherently
24/// immutable.
25///
26/// The `Held` variant's field is never read; it exists for its
27/// destructor only (drops the read lock when the iterator is
28/// dropped).
29#[allow(dead_code)]
30enum IterGuard<'a> {
31    /// RW mapping: read guard kept alive for the iterator's life.
32    Held(RwLockReadGuard<'a, MmapMut>),
33    /// RO / COW mapping: no lock to hold.
34    None,
35}
36
37/// Iterator over fixed-size chunks of a memory-mapped file.
38///
39/// Yields [`MappedSlice<'a>`] items that borrow directly from the
40/// mapped region. The iterator holds the underlying read lock (on RW
41/// mappings) for its lifetime, so calls to `resize()` from another
42/// thread will block until the iterator is dropped.
43///
44/// # Examples
45///
46/// ```no_run
47/// use mmap_io::MemoryMappedFile;
48///
49/// let mmap = MemoryMappedFile::open_ro("data.bin")?;
50///
51/// // Iterate over 4 KiB chunks. Each `chunk` is `MappedSlice<'_>`,
52/// // which derefs to `&[u8]`.
53/// for (i, chunk) in mmap.chunks(4096).enumerate() {
54///     let _ = chunk.len();
55///     let _first_byte = chunk[0];
56///     println!("Chunk {} has {} bytes", i, chunk.len());
57/// }
58/// # Ok::<(), mmap_io::MmapIoError>(())
59/// ```
60pub struct ChunkIterator<'a> {
61    /// Base pointer to the mapped region. Valid for `'a` because the
62    /// guard (or the immutable underlying mapping for RO/COW) keeps
63    /// the address space stable.
64    base: *const u8,
65    /// Total bytes in the mapping. Captured at iterator construction
66    /// and not re-checked; the guard (RW) or immutable mapping
67    /// (RO/COW) ensures the length cannot change during iteration.
68    total_len: usize,
69    /// Bytes per yielded chunk. The final chunk may be shorter.
70    chunk_size: usize,
71    /// Offset into the mapped region of the next chunk to yield.
72    current_offset: usize,
73    /// Lifetime / unmap guard.
74    _guard: IterGuard<'a>,
75    /// Notional borrow tying the raw pointer to `'a`.
76    _marker: PhantomData<&'a [u8]>,
77}
78
79// SAFETY: ChunkIterator is `Send` because:
80// - For `Held`, parking_lot's `RwLockReadGuard` is `Send` and `Sync`.
81// - For `None`, no lock is held; the pointer targets an immutable
82//   mapping that lives at least as long as `'a`.
83// - The pointer itself targets `u8`, which is `Send + Sync`.
84// The iterator yields immutable slices, so multiple yielded items
85// can coexist as standard shared borrows.
86unsafe impl<'a> Send for ChunkIterator<'a> {}
87// SAFETY: same justification as `Send`; sharing `&ChunkIterator`
88// across threads only allows calling `next()` from one thread at a
89// time (Iterator's contract requires `&mut self`), and the read-only
90// access pattern matches.
91unsafe impl<'a> Sync for ChunkIterator<'a> {}
92
93impl<'a> ChunkIterator<'a> {
94    pub(crate) fn new(mmap: &'a MemoryMappedFile, chunk_size: usize) -> Result<Self> {
95        let total_len = usize::try_from(mmap.current_len()?)
96            .map_err(|_| MmapIoError::ResizeFailed("mapping length exceeds usize::MAX".into()))?;
97
98        let (base, guard) = match &mmap.inner.map {
99            MapVariant::Ro(m) => (m.as_ptr(), IterGuard::None),
100            MapVariant::Rw(lock) => {
101                let g = lock.read();
102                let ptr = g.as_ptr();
103                (ptr, IterGuard::Held(g))
104            }
105            MapVariant::Cow(m) => (m.as_ptr(), IterGuard::None),
106        };
107
108        Ok(Self {
109            base,
110            total_len,
111            chunk_size,
112            current_offset: 0,
113            _guard: guard,
114            _marker: PhantomData,
115        })
116    }
117}
118
119impl<'a> Iterator for ChunkIterator<'a> {
120    type Item = MappedSlice<'a>;
121
122    fn next(&mut self) -> Option<Self::Item> {
123        if self.chunk_size == 0 || self.current_offset >= self.total_len {
124            return None;
125        }
126        let remaining = self.total_len - self.current_offset;
127        let chunk_len = remaining.min(self.chunk_size);
128
129        // SAFETY: `base.add(current_offset)` produces a pointer
130        // inside the mapped region because `current_offset <
131        // total_len <= mapping length` (the mapping is stable for
132        // `'a` per the guard / immutable variant in construction).
133        // `slice::from_raw_parts` with `chunk_len <= remaining`
134        // produces a slice that does not escape the mapped region.
135        // The resulting `&'a [u8]` shares the lifetime of the guard
136        // (`'a`), so multiple yielded chunks can coexist as
137        // immutable borrows. No mutation of the mapping is possible
138        // while the guard is alive (write lock would be required and
139        // is blocked by the held read guard).
140        let slice: &'a [u8] =
141            unsafe { std::slice::from_raw_parts(self.base.add(self.current_offset), chunk_len) };
142        self.current_offset += chunk_len;
143        Some(MappedSlice::owned(slice))
144    }
145
146    fn size_hint(&self) -> (usize, Option<usize>) {
147        if self.chunk_size == 0 {
148            return (0, Some(0));
149        }
150        let remaining = self.total_len.saturating_sub(self.current_offset);
151        let chunks = remaining.div_ceil(self.chunk_size);
152        (chunks, Some(chunks))
153    }
154}
155
156impl<'a> ExactSizeIterator for ChunkIterator<'a> {}
157
158/// Iterator over page-aligned chunks of a memory-mapped file.
159///
160/// Equivalent to `mmap.chunks(page_size())` with the same zero-copy
161/// guarantees.
162///
163/// # Examples
164///
165/// ```no_run
166/// use mmap_io::MemoryMappedFile;
167///
168/// let mmap = MemoryMappedFile::open_ro("data.bin")?;
169/// for page in mmap.pages() {
170///     let _ = page.len();
171/// }
172/// # Ok::<(), mmap_io::MmapIoError>(())
173/// ```
174pub struct PageIterator<'a> {
175    inner: ChunkIterator<'a>,
176}
177
178impl<'a> PageIterator<'a> {
179    pub(crate) fn new(mmap: &'a MemoryMappedFile) -> Result<Self> {
180        let ps = page_size();
181        Ok(Self {
182            inner: ChunkIterator::new(mmap, ps)?,
183        })
184    }
185}
186
187impl<'a> Iterator for PageIterator<'a> {
188    type Item = MappedSlice<'a>;
189
190    fn next(&mut self) -> Option<Self::Item> {
191        self.inner.next()
192    }
193
194    fn size_hint(&self) -> (usize, Option<usize>) {
195        self.inner.size_hint()
196    }
197}
198
199impl<'a> ExactSizeIterator for PageIterator<'a> {}
200
201/// Migration-aid iterator that yields owned `Vec<u8>` chunks. Each
202/// chunk is allocated and copied from the mapping. Prefer
203/// [`ChunkIterator`] (via `chunks()`) for zero-copy access.
204///
205/// # Examples
206///
207/// ```no_run
208/// use mmap_io::MemoryMappedFile;
209///
210/// let mmap = MemoryMappedFile::open_ro("data.bin")?;
211/// for chunk in mmap.chunks_owned(4096) {
212///     let bytes: Vec<u8> = chunk?;
213///     let _ = bytes;
214/// }
215/// # Ok::<(), mmap_io::MmapIoError>(())
216/// ```
217pub struct ChunkIteratorOwned<'a> {
218    inner: ChunkIterator<'a>,
219}
220
221impl<'a> ChunkIteratorOwned<'a> {
222    pub(crate) fn new(mmap: &'a MemoryMappedFile, chunk_size: usize) -> Result<Self> {
223        Ok(Self {
224            inner: ChunkIterator::new(mmap, chunk_size)?,
225        })
226    }
227}
228
229impl<'a> Iterator for ChunkIteratorOwned<'a> {
230    type Item = Result<Vec<u8>>;
231
232    fn next(&mut self) -> Option<Self::Item> {
233        self.inner.next().map(|slice| Ok(slice.as_slice().to_vec()))
234    }
235
236    fn size_hint(&self) -> (usize, Option<usize>) {
237        self.inner.size_hint()
238    }
239}
240
241impl<'a> ExactSizeIterator for ChunkIteratorOwned<'a> {}
242
243/// Migration-aid iterator that yields owned page-sized `Vec<u8>`
244/// buffers. Prefer [`PageIterator`] (via `pages()`) for zero-copy.
245pub struct PageIteratorOwned<'a> {
246    inner: PageIterator<'a>,
247}
248
249impl<'a> PageIteratorOwned<'a> {
250    pub(crate) fn new(mmap: &'a MemoryMappedFile) -> Result<Self> {
251        Ok(Self {
252            inner: PageIterator::new(mmap)?,
253        })
254    }
255}
256
257impl<'a> Iterator for PageIteratorOwned<'a> {
258    type Item = Result<Vec<u8>>;
259
260    fn next(&mut self) -> Option<Self::Item> {
261        self.inner.next().map(|slice| Ok(slice.as_slice().to_vec()))
262    }
263
264    fn size_hint(&self) -> (usize, Option<usize>) {
265        self.inner.size_hint()
266    }
267}
268
269impl<'a> ExactSizeIterator for PageIteratorOwned<'a> {}
270
271/// Mutable chunk iterator: callback-based because Rust's borrow
272/// checker does not allow yielding multiple mutable references from
273/// one iterator. The iterator acquires the underlying RW write lock
274/// once and holds it for the entire iteration, then drives the
275/// caller's closure on each chunk in order.
276pub struct ChunkIteratorMut<'a> {
277    mmap: &'a MemoryMappedFile,
278    chunk_size: usize,
279    total_len: u64,
280    _phantom: PhantomData<&'a mut [u8]>,
281}
282
283impl<'a> ChunkIteratorMut<'a> {
284    pub(crate) fn new(mmap: &'a MemoryMappedFile, chunk_size: usize) -> Result<Self> {
285        let total_len = mmap.current_len()?;
286        Ok(Self {
287            mmap,
288            chunk_size,
289            total_len,
290            _phantom: PhantomData,
291        })
292    }
293
294    /// Process each chunk under a single held write guard. The
295    /// closure receives `(offset, &mut [u8])` for each chunk in
296    /// order. Returning `Err` aborts iteration and surfaces the
297    /// error to the caller.
298    ///
299    /// The closure's error type is the crate's [`MmapIoError`].
300    /// Callers carrying a foreign error type should map into
301    /// `MmapIoError` before returning (e.g. via `.map_err(|e|
302    /// MmapIoError::Io(...))`).
303    ///
304    /// # Errors
305    ///
306    /// Returns [`MmapIoError::InvalidMode`] on read-only or COW
307    /// mappings (mutable iteration requires `ReadWrite`). Returns any
308    /// error propagated from the user closure.
309    pub fn for_each_mut<F>(self, mut f: F) -> Result<()>
310    where
311        F: FnMut(u64, &mut [u8]) -> Result<()>,
312    {
313        if self.chunk_size == 0 || self.total_len == 0 {
314            return Ok(());
315        }
316        match &self.mmap.inner.map {
317            MapVariant::Ro(_) => Err(MmapIoError::InvalidMode(
318                "chunks_mut requires ReadWrite mode",
319            )),
320            MapVariant::Cow(_) => Err(MmapIoError::InvalidMode(
321                "chunks_mut on copy-on-write mapping is not supported (phase-1 read-only)",
322            )),
323            MapVariant::Rw(lock) => {
324                let mut guard = lock.write();
325                let total = self.total_len as usize;
326                let chunk_size = self.chunk_size;
327                let mut offset = 0usize;
328                while offset < total {
329                    let remaining = total - offset;
330                    let chunk_len = remaining.min(chunk_size);
331                    let end = offset + chunk_len;
332                    f(offset as u64, &mut guard[offset..end])?;
333                    offset = end;
334                }
335                Ok(())
336            }
337        }
338    }
339
340    /// Migration shim that mirrors the 0.9.6 `for_each_mut`
341    /// signature: returns `Result<std::result::Result<(), E>>`
342    /// where `E` is the closure's error type.
343    ///
344    /// **Prefer [`for_each_mut`](Self::for_each_mut)** for new
345    /// code; that method was flattened to `Result<()>` (using
346    /// the crate's `MmapIoError`) in 0.9.7. Foreign error types
347    /// should be mapped via `.map_err(|e| MmapIoError::Io(...))`
348    /// before returning.
349    ///
350    /// This shim exists for callers migrating off the 0.9.6
351    /// signature. Internally it still uses the new single-held-
352    /// guard implementation (the H2 perf win is preserved); only
353    /// the return shape is back-compat.
354    ///
355    /// # Errors
356    ///
357    /// Returns the outer `Err(MmapIoError)` for any mmap-side
358    /// failure (e.g. RW lock unavailable, OOB chunk during a
359    /// concurrent resize). Returns `Ok(Err(E))` for closure
360    /// errors. Returns `Ok(Ok(()))` when iteration completes
361    /// cleanly.
362    pub fn for_each_mut_legacy<F, E>(self, mut f: F) -> Result<std::result::Result<(), E>>
363    where
364        F: FnMut(u64, &mut [u8]) -> std::result::Result<(), E>,
365    {
366        if self.chunk_size == 0 || self.total_len == 0 {
367            return Ok(Ok(()));
368        }
369        match &self.mmap.inner.map {
370            MapVariant::Ro(_) => Err(MmapIoError::InvalidMode(
371                "chunks_mut requires ReadWrite mode",
372            )),
373            MapVariant::Cow(_) => Err(MmapIoError::InvalidMode(
374                "chunks_mut on copy-on-write mapping is not supported (phase-1 read-only)",
375            )),
376            MapVariant::Rw(lock) => {
377                let mut guard = lock.write();
378                let total = self.total_len as usize;
379                let chunk_size = self.chunk_size;
380                let mut offset = 0usize;
381                while offset < total {
382                    let remaining = total - offset;
383                    let chunk_len = remaining.min(chunk_size);
384                    let end = offset + chunk_len;
385                    match f(offset as u64, &mut guard[offset..end]) {
386                        Ok(()) => offset = end,
387                        Err(e) => return Ok(Err(e)),
388                    }
389                }
390                Ok(Ok(()))
391            }
392        }
393    }
394}
395
396impl MemoryMappedFile {
397    /// Zero-copy chunk iterator. Yields [`MappedSlice<'_>`] of size
398    /// `chunk_size` (final chunk may be shorter).
399    ///
400    /// For RW mappings, the iterator holds a read guard for its
401    /// lifetime; concurrent `resize()` blocks until the iterator is
402    /// dropped.
403    ///
404    /// # Examples
405    ///
406    /// ```no_run
407    /// use mmap_io::MemoryMappedFile;
408    /// let mmap = MemoryMappedFile::open_ro("data.bin")?;
409    /// for chunk in mmap.chunks(64 * 1024) {
410    ///     let _ = chunk.len();
411    /// }
412    /// # Ok::<(), mmap_io::MmapIoError>(())
413    /// ```
414    ///
415    /// # Panics
416    ///
417    /// Panics if iterator construction fails. This is unreachable for
418    /// supported inputs: the constructor's only failure mode is
419    /// `chunk_size == 0`, which the type-level contract documents as
420    /// invalid usage. Empty mappings produce an already-exhausted
421    /// iterator rather than an error.
422    #[cfg(feature = "iterator")]
423    #[must_use]
424    pub fn chunks(&self, chunk_size: usize) -> ChunkIterator<'_> {
425        ChunkIterator::new(self, chunk_size).expect("chunk iterator creation should not fail")
426    }
427
428    /// Zero-copy page-aligned iterator.
429    ///
430    /// # Panics
431    ///
432    /// Unreachable in practice; see [`chunks`](Self::chunks).
433    #[cfg(feature = "iterator")]
434    #[must_use]
435    pub fn pages(&self) -> PageIterator<'_> {
436        PageIterator::new(self).expect("page iterator creation should not fail")
437    }
438
439    /// Migration-aid: chunk iterator yielding owned `Vec<u8>` items.
440    /// Allocates one `Vec<u8>` per chunk and copies the data into it.
441    /// Prefer `chunks()` for zero-copy.
442    ///
443    /// # Panics
444    ///
445    /// Unreachable in practice; see [`chunks`](Self::chunks).
446    #[cfg(feature = "iterator")]
447    #[must_use]
448    pub fn chunks_owned(&self, chunk_size: usize) -> ChunkIteratorOwned<'_> {
449        ChunkIteratorOwned::new(self, chunk_size)
450            .expect("owned chunk iterator creation should not fail")
451    }
452
453    /// Migration-aid: page iterator yielding owned `Vec<u8>` items.
454    /// Prefer `pages()` for zero-copy.
455    ///
456    /// # Panics
457    ///
458    /// Unreachable in practice; see [`chunks`](Self::chunks).
459    #[cfg(feature = "iterator")]
460    #[must_use]
461    pub fn pages_owned(&self) -> PageIteratorOwned<'_> {
462        PageIteratorOwned::new(self).expect("owned page iterator creation should not fail")
463    }
464
465    /// Callback-driven mutable iterator. Acquires a single write
466    /// guard for the entire iteration. Available only on
467    /// `ReadWrite` mappings.
468    ///
469    /// # Examples
470    ///
471    /// ```no_run
472    /// use mmap_io::MemoryMappedFile;
473    ///
474    /// let mmap = MemoryMappedFile::open_rw("data.bin")?;
475    /// mmap.chunks_mut(4096).for_each_mut(|_offset, chunk| {
476    ///     chunk.fill(0);
477    ///     Ok(())
478    /// })?;
479    /// # Ok::<(), mmap_io::MmapIoError>(())
480    /// ```
481    ///
482    /// # Panics
483    ///
484    /// Unreachable in practice; see [`chunks`](Self::chunks).
485    #[cfg(feature = "iterator")]
486    #[must_use]
487    pub fn chunks_mut(&self, chunk_size: usize) -> ChunkIteratorMut<'_> {
488        ChunkIteratorMut::new(self, chunk_size)
489            .expect("mutable chunk iterator creation should not fail")
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::create_mmap;
497    use std::fs;
498    use std::path::PathBuf;
499
500    fn tmp_path(name: &str) -> PathBuf {
501        let mut p = std::env::temp_dir();
502        p.push(format!(
503            "mmap_io_iterator_test_{}_{}",
504            name,
505            std::process::id()
506        ));
507        p
508    }
509
510    #[test]
511    #[cfg(feature = "iterator")]
512    fn test_chunk_iterator_zero_copy() {
513        let path = tmp_path("chunk_iter");
514        let _ = fs::remove_file(&path);
515
516        let mmap = create_mmap(&path, 10240).expect("create");
517        for i in 0..10 {
518            let data = vec![i as u8; 1024];
519            mmap.update_region(i * 1024, &data).expect("write");
520        }
521        mmap.flush().expect("flush");
522
523        // Aligned chunks: 10 x 1024.
524        let chunks: Vec<Vec<u8>> = mmap.chunks(1024).map(|s| s.as_slice().to_vec()).collect();
525        assert_eq!(chunks.len(), 10);
526        for (i, chunk) in chunks.iter().enumerate() {
527            assert_eq!(chunk.len(), 1024);
528            assert!(chunk.iter().all(|&b| b == i as u8));
529        }
530
531        // Unaligned chunks: 3000 / 3000 / 3000 / 1240.
532        let chunks: Vec<Vec<u8>> = mmap.chunks(3000).map(|s| s.as_slice().to_vec()).collect();
533        assert_eq!(chunks.len(), 4);
534        assert_eq!(chunks[3].len(), 1240);
535
536        drop(mmap);
537        fs::remove_file(&path).expect("cleanup");
538    }
539
540    #[test]
541    #[cfg(feature = "iterator")]
542    fn test_page_iterator_zero_copy() {
543        let path = tmp_path("page_iter");
544        let _ = fs::remove_file(&path);
545
546        let ps = page_size();
547        let file_size = ps * 3 + 100;
548
549        let mmap = create_mmap(&path, file_size as u64).expect("create");
550
551        let pages: Vec<usize> = mmap.pages().map(|p| p.len()).collect();
552        assert_eq!(pages.len(), 4);
553        assert_eq!(pages[0], ps);
554        assert_eq!(pages[1], ps);
555        assert_eq!(pages[2], ps);
556        assert_eq!(pages[3], 100);
557
558        drop(mmap);
559        fs::remove_file(&path).expect("cleanup");
560    }
561
562    #[test]
563    #[cfg(feature = "iterator")]
564    fn test_chunks_owned_compat() {
565        let path = tmp_path("chunks_owned");
566        let _ = fs::remove_file(&path);
567
568        let mmap = create_mmap(&path, 4096).expect("create");
569        mmap.update_region(0, &vec![0x11u8; 4096]).expect("write");
570
571        let owned: Vec<Vec<u8>> = mmap
572            .chunks_owned(1024)
573            .collect::<Result<Vec<_>>>()
574            .expect("collect");
575        assert_eq!(owned.len(), 4);
576        for v in &owned {
577            assert_eq!(v.len(), 1024);
578            assert!(v.iter().all(|&b| b == 0x11));
579        }
580
581        drop(mmap);
582        fs::remove_file(&path).expect("cleanup");
583    }
584
585    #[test]
586    #[cfg(feature = "iterator")]
587    fn test_mutable_chunk_iterator_single_guard() {
588        let path = tmp_path("mut_chunk_iter");
589        let _ = fs::remove_file(&path);
590
591        let mmap = create_mmap(&path, 4096).expect("create");
592
593        mmap.chunks_mut(1024)
594            .for_each_mut(|offset, chunk| {
595                let value = (offset / 1024) as u8;
596                chunk.fill(value);
597                Ok(())
598            })
599            .expect("for_each_mut");
600
601        mmap.flush().expect("flush");
602
603        let mut buf = [0u8; 1024];
604        for i in 0..4 {
605            mmap.read_into(i * 1024, &mut buf).expect("read");
606            assert!(buf.iter().all(|&b| b == i as u8));
607        }
608
609        drop(mmap);
610        fs::remove_file(&path).expect("cleanup");
611    }
612
613    #[test]
614    #[cfg(feature = "iterator")]
615    fn test_iterator_size_hint() {
616        let path = tmp_path("size_hint");
617        let _ = fs::remove_file(&path);
618
619        let mmap = create_mmap(&path, 10000).expect("create");
620
621        {
622            let iter = mmap.chunks(1000);
623            assert_eq!(iter.size_hint(), (10, Some(10)));
624        }
625        {
626            let iter = mmap.chunks(3000);
627            assert_eq!(iter.size_hint(), (4, Some(4)));
628        }
629
630        drop(mmap);
631        fs::remove_file(&path).expect("cleanup");
632    }
633
634    #[test]
635    #[cfg(feature = "iterator")]
636    fn test_iterator_zero_chunk_size_yields_nothing() {
637        let path = tmp_path("zero_chunk");
638        let _ = fs::remove_file(&path);
639
640        let mmap = create_mmap(&path, 4096).expect("create");
641        assert_eq!(mmap.chunks(0).count(), 0);
642
643        drop(mmap);
644        fs::remove_file(&path).expect("cleanup");
645    }
646
647    #[test]
648    #[cfg(feature = "iterator")]
649    fn test_one_byte_file_iteration() {
650        let path = tmp_path("one_byte_iter");
651        let _ = fs::remove_file(&path);
652
653        let mmap = create_mmap(&path, 1).expect("create");
654        let chunks: Vec<usize> = mmap.chunks(1024).map(|s| s.len()).collect();
655        assert_eq!(chunks.len(), 1);
656        assert_eq!(chunks[0], 1);
657
658        drop(mmap);
659        fs::remove_file(&path).expect("cleanup");
660    }
661}