Skip to main content

imago/
file.rs

1//! Use a plain file or host block device as storage.
2
3#[cfg(unix)]
4use crate::io_buffers::IoBuffer;
5use crate::io_buffers::{IoVector, IoVectorMut};
6#[cfg(unix)]
7use crate::misc_helpers::while_eintr;
8use crate::misc_helpers::ResultErrorContext;
9use crate::storage::drivers::CommonStorageHelper;
10use crate::storage::ext::write_full_zeroes;
11use crate::storage::PreallocateMode;
12use crate::{Storage, StorageCreateOptions, StorageOpenOptions};
13use cfg_if::cfg_if;
14use std::fmt::{self, Display, Formatter};
15use std::io::{self, Write};
16#[cfg(any(target_os = "linux", target_os = "macos"))]
17use std::os::fd::AsRawFd;
18#[cfg(unix)]
19use std::os::unix::fs::FileTypeExt;
20#[cfg(all(unix, not(target_os = "macos")))]
21use std::os::unix::fs::OpenOptionsExt;
22#[cfg(windows)]
23use std::os::windows::fs::{FileExt, OpenOptionsExt};
24#[cfg(windows)]
25use std::os::windows::io::AsRawHandle;
26use std::path::{Path, PathBuf};
27use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
28use std::sync::RwLock;
29use std::{cmp, fs};
30#[cfg(unix)]
31use tracing::{debug, warn};
32#[cfg(windows)]
33use windows_sys::Win32::System::Ioctl::{FILE_ZERO_DATA_INFORMATION, FSCTL_SET_ZERO_DATA};
34#[cfg(windows)]
35use windows_sys::Win32::System::IO::DeviceIoControl;
36
37/// Linux UAPI value for `RWF_DONTCACHE`.
38///
39/// Keeping the stable UAPI bit here avoids raising Imago's minimum `libc` crate version solely
40/// for a newly exposed constant. Both supported Linux libc targets already expose `pwritev2()`.
41#[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
42const RWF_DONTCACHE_FLAG: libc::c_int = 0x0000_0080;
43
44/// Use a plain file or host block device as a storage object.
45#[derive(Debug)]
46pub struct File {
47    /// The file.
48    file: RwLock<fs::File>,
49
50    /// For debug purposes, and to resolve relative filenames.
51    filename: Option<PathBuf>,
52
53    /// Minimal I/O alignment for requests.
54    req_align: usize,
55
56    /// Minimal memory buffer alignment.
57    mem_align: usize,
58
59    /// Minimum required alignment for zero writes.
60    zero_align: usize,
61
62    /// Minimum required alignment for effective discards.
63    discard_align: usize,
64
65    /// Cached file length.
66    ///
67    /// Third parties changing the length concurrently is pretty certain to break things anyway.
68    size: AtomicU64,
69
70    /// Storage helper.
71    common_storage_helper: CommonStorageHelper,
72
73    /// macOS-only: Use fsync() instead of F_FULLFSYNC on `sync()` method.
74    #[cfg(target_os = "macos")]
75    relaxed_sync: bool,
76
77    /// GNU/musl Linux-only: Whether to try `RWF_DONTCACHE` for buffered writes.
78    ///
79    /// The flag is cleared permanently after the kernel or filesystem reports the hint as
80    /// unsupported, avoiding repeated failed syscalls while preserving ordinary buffered writes.
81    #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
82    write_dontcache: AtomicBool,
83
84    /// Set once we know that discard is unsupported and we can skip trying.
85    discard_unsupported: AtomicBool,
86}
87
88impl TryFrom<fs::File> for File {
89    type Error = io::Error;
90
91    /// Use the given existing `std::fs::File`.
92    ///
93    /// Convert the given existing `std::fs::File` object into an imago storage object.
94    ///
95    /// When using this, the resulting object will not know its own filename.  That makes it
96    /// impossible to auto-resolve relative paths to it, e.g. qcow2 backing file names.
97    fn try_from(file: fs::File) -> io::Result<Self> {
98        Self::new(
99            file,
100            None,
101            false,
102            #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
103            false,
104            #[cfg(target_os = "macos")]
105            false,
106        )
107    }
108}
109
110impl Storage for File {
111    async fn open(opts: StorageOpenOptions) -> io::Result<Self> {
112        Self::do_open_sync(opts, fs::OpenOptions::new())
113    }
114
115    #[cfg(feature = "sync-wrappers")]
116    fn open_sync(opts: StorageOpenOptions) -> io::Result<Self> {
117        Self::do_open_sync(opts, fs::OpenOptions::new())
118    }
119
120    async fn create_open(opts: StorageCreateOptions) -> io::Result<Self> {
121        // Always allow writing for new files
122        let opts = opts.modify_open_opts(|o| o.write(true));
123        let size = opts.size;
124        let prealloc_mode = opts.prealloc_mode;
125
126        let mut file_opts = fs::OpenOptions::new();
127        if opts.overwrite {
128            file_opts.create(true).truncate(true);
129        } else {
130            file_opts.create_new(true);
131        };
132
133        let file = Self::do_open_sync(opts.get_open_options(), file_opts)?;
134        if size > 0 {
135            file.resize(size, prealloc_mode)
136                .await
137                .err_context(|| "Resizing file")?;
138        }
139
140        Ok(file)
141    }
142
143    fn mem_align(&self) -> usize {
144        self.mem_align
145    }
146
147    fn req_align(&self) -> usize {
148        self.req_align
149    }
150
151    fn zero_align(&self) -> usize {
152        self.zero_align
153    }
154
155    fn discard_align(&self) -> usize {
156        self.discard_align
157    }
158
159    fn size(&self) -> io::Result<u64> {
160        Ok(self.size.load(Ordering::Relaxed))
161    }
162
163    fn resolve_relative_path<P: AsRef<Path>>(&self, relative: P) -> io::Result<PathBuf> {
164        let relative = relative.as_ref();
165
166        if relative.is_absolute() {
167            return Ok(relative.to_path_buf());
168        }
169
170        let filename = self
171            .filename
172            .as_ref()
173            .ok_or_else(|| io::Error::other("No filename set for base image"))?;
174
175        let dirname = filename
176            .parent()
177            .ok_or_else(|| io::Error::other("Invalid base image filename set"))?;
178
179        Ok(dirname.join(relative))
180    }
181
182    fn get_filename(&self) -> Option<PathBuf> {
183        self.filename.as_ref().cloned()
184    }
185
186    #[cfg(unix)]
187    async unsafe fn pure_readv(
188        &self,
189        mut bufv: IoVectorMut<'_>,
190        mut offset: u64,
191    ) -> io::Result<()> {
192        while !bufv.is_empty() {
193            let iovec = unsafe { bufv.as_iovec() };
194            let preadv_offset = offset
195                .try_into()
196                .map_err(|_| io::Error::other("Read offset overflow"))?;
197
198            let len = while_eintr(|| unsafe {
199                libc::preadv(
200                    self.file.read().unwrap().as_raw_fd(),
201                    iovec.as_ptr(),
202                    iovec.len() as libc::c_int,
203                    preadv_offset,
204                )
205            })? as u64;
206
207            if len == 0 {
208                // End of file
209                bufv.fill(0);
210                break;
211            }
212
213            bufv = bufv.split_tail_at(len);
214            offset = offset
215                .checked_add(len)
216                .ok_or_else(|| io::Error::other("Read offset overflow"))?;
217        }
218
219        Ok(())
220    }
221
222    #[cfg(windows)]
223    async unsafe fn pure_readv(&self, bufv: IoVectorMut<'_>, mut offset: u64) -> io::Result<()> {
224        for mut buffer in bufv.into_inner() {
225            let mut buffer: &mut [u8] = &mut buffer;
226            while !buffer.is_empty() {
227                let len = if offset >= self.size.load(Ordering::Relaxed) {
228                    buffer.fill(0);
229                    buffer.len()
230                } else {
231                    self.file.write().unwrap().seek_read(buffer, offset)?
232                };
233                offset = offset
234                    .checked_add(len as u64)
235                    .ok_or_else(|| io::Error::other("Read offset overflow"))?;
236                buffer = buffer.split_at_mut(len).1;
237            }
238        }
239        Ok(())
240    }
241
242    #[cfg(unix)]
243    async unsafe fn pure_writev(&self, mut bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
244        while !bufv.is_empty() {
245            let iovec = unsafe { bufv.as_iovec() };
246            let pwritev_offset = offset
247                .try_into()
248                .map_err(|_| io::Error::other("Write offset overflow"))?;
249
250            #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
251            let len = write_with_optional_dontcache(
252                &self.write_dontcache,
253                || {
254                    // Safe: The descriptor and iovec remain valid for the duration of the call;
255                    // the offset was checked above and the flag has no pointer arguments.
256                    syscall_result(unsafe {
257                        libc::pwritev2(
258                            self.file.read().unwrap().as_raw_fd(),
259                            iovec.as_ptr(),
260                            iovec.len() as libc::c_int,
261                            pwritev_offset,
262                            RWF_DONTCACHE_FLAG,
263                        )
264                    })
265                },
266                || {
267                    // This fallback receives the exact same unconsumed iovec and offset.  It is
268                    // used only when the kernel explicitly rejects `RWF_DONTCACHE`.
269                    syscall_result(unsafe {
270                        libc::pwritev(
271                            self.file.read().unwrap().as_raw_fd(),
272                            iovec.as_ptr(),
273                            iovec.len() as libc::c_int,
274                            pwritev_offset,
275                        )
276                    })
277                },
278            )?;
279
280            #[cfg(all(
281                unix,
282                not(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))
283            ))]
284            let len = while_eintr(|| unsafe {
285                libc::pwritev(
286                    self.file.read().unwrap().as_raw_fd(),
287                    iovec.as_ptr(),
288                    iovec.len() as libc::c_int,
289                    pwritev_offset,
290                )
291            })?;
292
293            let len = require_write_progress(len)?;
294
295            bufv = bufv.split_tail_at(len);
296            offset = offset
297                .checked_add(len)
298                .ok_or_else(|| io::Error::other("Write offset overflow"))?;
299            self.size.fetch_max(offset, Ordering::Relaxed);
300        }
301
302        Ok(())
303    }
304
305    #[cfg(windows)]
306    async unsafe fn pure_writev(&self, bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
307        for buffer in bufv.into_inner() {
308            let mut buffer: &[u8] = &buffer;
309            while !buffer.is_empty() {
310                let len = self.file.write().unwrap().seek_write(buffer, offset)?;
311                offset = offset
312                    .checked_add(len as u64)
313                    .ok_or_else(|| io::Error::other("Write offset overflow"))?;
314                self.size.fetch_max(offset, Ordering::Relaxed);
315                buffer = buffer.split_at(len).1;
316            }
317        }
318        Ok(())
319    }
320
321    async unsafe fn pure_write_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
322        self.discard_to_zero(offset, length).await
323    }
324
325    #[cfg(target_os = "linux")]
326    async unsafe fn pure_write_allocated_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
327        let offset: libc::off_t = offset
328            .try_into()
329            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
330        let length: libc::off_t = length
331            .try_into()
332            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
333
334        let file = self.file.read().unwrap();
335        // Safe: File descriptor is valid, and the rest are simple integer parameters.
336        while_eintr(|| unsafe {
337            libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_ZERO_RANGE, offset, length)
338        })
339        .map_err(Self::map_os_err)?;
340
341        Ok(())
342    }
343
344    async unsafe fn pure_discard(&self, offset: u64, length: u64) -> io::Result<()> {
345        if let Err(err) = self.discard_to_zero(offset, length).await {
346            // Ignore `Unsupported` errors: As per the `pure_discard` documentation, a no-op
347            // implementation is acceptable.  In addition, the default implementation returns
348            // `Ok(())`, and it makes no sense to be harsher than that here.
349            if err.kind() == io::ErrorKind::Unsupported {
350                Ok(())
351            } else {
352                Err(err)
353            }
354        } else {
355            Ok(())
356        }
357    }
358
359    async fn flush(&self) -> io::Result<()> {
360        self.file.write().unwrap().flush()
361    }
362
363    async fn sync(&self) -> io::Result<()> {
364        #[cfg(target_os = "macos")]
365        if self.relaxed_sync {
366            // Safe: File descriptor is valid and there aren't any other arguments.
367            while_eintr(|| unsafe { libc::fsync(self.file.write().unwrap().as_raw_fd()) })?;
368            return Ok(());
369        }
370        self.file.write().unwrap().sync_all()
371    }
372
373    async unsafe fn invalidate_cache(&self) -> io::Result<()> {
374        // TODO: Figure out what to do.  Generally, `std::fs::File` does not have internal buffers,
375        // so we don’t need to invalidate anything; we could close and reopen, but that would still
376        // flush, and is difficult to do in a platform-independent way (/proc/self/fd would allow
377        // this on Linux).  Using e.g. the filename is not safe.
378        // Right now, it’s best not to do anything.
379        Ok(())
380    }
381
382    fn get_storage_helper(&self) -> &CommonStorageHelper {
383        &self.common_storage_helper
384    }
385
386    async fn resize(&self, new_size: u64, prealloc_mode: PreallocateMode) -> io::Result<()> {
387        let file = self.file.write().unwrap();
388        let current_size = self.size.load(Ordering::Relaxed);
389
390        match new_size.cmp(&current_size) {
391            std::cmp::Ordering::Equal => return Ok(()),
392            std::cmp::Ordering::Less => {
393                file.set_len(new_size)?;
394                self.size.fetch_min(new_size, Ordering::Relaxed);
395                return Ok(());
396            }
397            std::cmp::Ordering::Greater => (), // handled below
398        }
399
400        match prealloc_mode {
401            PreallocateMode::None | PreallocateMode::Zero => file.set_len(new_size)?,
402            PreallocateMode::Allocate => {
403                #[cfg(not(unix))]
404                return Err(io::ErrorKind::Unsupported.into());
405
406                #[cfg(all(unix, not(target_os = "macos")))]
407                {
408                    let ofs = current_size.try_into().map_err(io::Error::other)?;
409                    let len = (new_size - current_size)
410                        .try_into()
411                        .map_err(io::Error::other)?;
412                    while_eintr(|| unsafe { libc::fallocate(file.as_raw_fd(), 0, ofs, len) })
413                        .map_err(Self::map_os_err)?;
414                }
415
416                #[cfg(target_os = "macos")]
417                {
418                    // Best-effort.  PEOFPOSMODE allocates from the “physical” EOF, wherever that
419                    // may be, but the only alternative would be VOLPOSMODE, which nobody knows the
420                    // meaning of.  Also doesn’t change the file length, we need to truncate
421                    // afterwards still.
422                    let mut params = libc::fstore_t {
423                        fst_flags: libc::F_ALLOCATEALL,
424                        fst_posmode: libc::F_PEOFPOSMODE,
425                        fst_offset: 0,
426                        fst_length: (new_size - current_size)
427                            .try_into()
428                            .map_err(io::Error::other)?,
429                        fst_bytesalloc: 0, // output
430                    };
431                    while_eintr(|| unsafe {
432                        libc::fcntl(file.as_raw_fd(), libc::F_PREALLOCATE, &mut params)
433                    })
434                    .map_err(Self::map_os_err)?;
435
436                    file.set_len(new_size)?;
437                }
438            }
439            PreallocateMode::WriteData => {
440                // FIXME: Keeping the lock would be nice, but resizing concurrently with I/O is
441                // pretty risky anyway.
442                drop(file);
443                write_full_zeroes(self, current_size, new_size - current_size).await?;
444            }
445        }
446
447        self.size.fetch_max(new_size, Ordering::Relaxed);
448        Ok(())
449    }
450}
451
452impl File {
453    /// Central internal function to create a `File` object.
454    ///
455    /// `direct_io` should be `true` if direct I/O was requested, and can be `false` if that status
456    /// is unknown.
457    fn new(
458        mut file: fs::File,
459        filename: Option<PathBuf>,
460        direct_io: bool,
461        #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
462        write_dontcache: bool,
463        #[cfg(target_os = "macos")] relaxed_sync: bool,
464    ) -> io::Result<Self> {
465        let size = get_file_size(&file).err_context(|| "Failed to determine file size")?;
466
467        #[cfg(all(unix, not(target_os = "macos")))]
468        let direct_io = direct_io || {
469            // Safe: No argument, returns result.
470            let res = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
471            res > 0 && (res & libc::O_DIRECT) != 0
472        };
473
474        let (min_req_align, min_mem_align) = if direct_io {
475            #[cfg(unix)]
476            {
477                (
478                    Self::get_min_dio_req_align(&file),
479                    Self::get_min_dio_mem_align(&file),
480                )
481            }
482
483            #[cfg(not(unix))]
484            {
485                (1, 1)
486            } // probe it then
487        } else {
488            (1, 1)
489        };
490
491        let (req_align, mem_align, zero_align, discard_align) =
492            Self::probe_alignments(&mut file, min_req_align, min_mem_align);
493        assert!(req_align.is_power_of_two());
494        assert!(mem_align.is_power_of_two());
495
496        Ok(File {
497            file: RwLock::new(file),
498            filename,
499            req_align,
500            mem_align,
501            zero_align,
502            discard_align,
503            size: size.into(),
504            common_storage_helper: Default::default(),
505            #[cfg(target_os = "macos")]
506            relaxed_sync,
507            #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
508            write_dontcache: AtomicBool::new(write_dontcache),
509            discard_unsupported: AtomicBool::new(false),
510        })
511    }
512
513    /// Probe minimal request, memory, zero and discard alignments.
514    ///
515    /// Start at `min_req_align` and `min_mem_align`.
516    #[cfg(unix)]
517    fn probe_alignments(
518        file: &mut fs::File,
519        min_req_align: usize,
520        min_mem_align: usize,
521    ) -> (usize, usize, usize, usize) {
522        let mut page_size = page_size::get();
523        if !page_size.is_power_of_two() {
524            let assume = page_size.checked_next_power_of_two().unwrap_or(4096);
525            let assume = cmp::max(4096, assume);
526            warn!("Reported page size of {page_size} is not a power of two, assuming {assume}");
527            page_size = assume;
528        }
529
530        #[cfg(not(target_os = "macos"))]
531        let (zero_align, discard_align) = (1, 1);
532        #[cfg(target_os = "macos")]
533        let (zero_align, discard_align) = {
534            let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
535            // Safe: FD is valid, passed pointer is valid and its type matches the call.
536            match while_eintr(|| unsafe { libc::fstatfs(file.as_raw_fd(), &mut statfs) }) {
537                Ok(_) => (statfs.f_bsize as usize, statfs.f_bsize as usize),
538                Err(_) => (page_size, page_size),
539            }
540        };
541
542        let mut writable = true;
543
544        let max_req_align = 65536;
545        let max_mem_align = cmp::max(page_size, max_req_align);
546
547        // Minimum fallbacks in case something goes wrong.
548        let safe_req_align = 4096;
549        let safe_mem_align = cmp::max(page_size, safe_req_align);
550
551        let mut test_buf = match IoBuffer::new(max_mem_align, max_mem_align) {
552            Ok(buf) => buf,
553            Err(err) => {
554                warn!(
555                    "Failed to allocate memory to probe request alignment ({err}), \
556                    falling back to {safe_req_align}/{safe_mem_align}"
557                );
558                return (safe_req_align, safe_mem_align, zero_align, discard_align);
559            }
560        };
561
562        let mut req_align: usize = min_req_align;
563        let result = loop {
564            assert!(req_align <= max_mem_align);
565            match Self::probe_access(
566                file,
567                test_buf.as_mut_range(0..req_align).into_slice(),
568                req_align.try_into().unwrap(),
569                &mut writable,
570            ) {
571                Ok(true) => break Ok(req_align),
572                Ok(false) => {
573                    if req_align >= max_req_align {
574                        break Err(io::Error::other(format!(
575                            "Maximum I/O alignment ({max_req_align}) exceeded"
576                        )));
577                    }
578                    // No reason to probe anything between 1 and 512
579                    if req_align == min_req_align {
580                        req_align = cmp::max(min_req_align << 1, 512);
581                    } else {
582                        req_align <<= 1;
583                    }
584                }
585                Err(err) => break Err(err),
586            }
587        };
588
589        let req_align = match result {
590            Ok(align) => {
591                debug!("Probed request alignment: {align}");
592                align
593            }
594            Err(err) => {
595                // Failed to determine request alignment, use a presumably safe value
596                let align = cmp::max(req_align, safe_req_align);
597                warn!(
598                    "Failed to probe request alignment ({err}; {}), falling back to {align} bytes",
599                    err.kind(),
600                );
601                align
602            }
603        };
604
605        let mut mem_align: usize = min_mem_align;
606        let result = loop {
607            assert!(mem_align <= max_mem_align);
608            let range = (max_mem_align - mem_align)..max_mem_align;
609            match Self::probe_access(
610                file,
611                test_buf.as_mut_range(range).into_slice(),
612                0,
613                &mut writable,
614            ) {
615                Ok(true) => break Ok(mem_align),
616                Ok(false) => {
617                    // Not aligned
618                    if mem_align >= max_mem_align {
619                        break Err(io::Error::other(format!(
620                            "Maximum memory alignment ({max_mem_align}) exceeded"
621                        )));
622                    }
623                    // No reason to probe anything between 1 and the page size (or 4096 at least)
624                    if mem_align == min_mem_align {
625                        mem_align = cmp::max(min_mem_align << 1, cmp::min(page_size, 4096));
626                    } else {
627                        mem_align <<= 1;
628                    }
629                }
630                Err(err) => break Err(err),
631            }
632        };
633
634        let mem_align = match result {
635            Ok(align) => {
636                debug!("Probed memory alignment: {align}");
637                align
638            }
639            Err(err) => {
640                // Failed to determine memory alignment, use a presumably safe value
641                let align = cmp::max(mem_align, safe_mem_align);
642                warn!(
643                    "Failed to probe memory alignment ({err}; {}), falling back to {align} bytes",
644                    err.kind(),
645                );
646                align
647            }
648        };
649
650        (req_align, mem_align, zero_align, discard_align)
651    }
652
653    /// Do an alignment-probing I/O access.
654    ///
655    /// Return `Ok(true)` if everything was OK, and `Ok(false)` if the request was reported to be
656    /// misaligned.
657    ///
658    /// `may_write` is a boolean that controls whether this is allowed to write (the same data read
659    /// before) to improve reliability.  Is automatically set to `false` if writing is found to not
660    /// be possible.
661    #[cfg(unix)]
662    fn probe_access(
663        file: &mut fs::File,
664        slice: &mut [u8],
665        offset: libc::off_t,
666        may_write: &mut bool,
667    ) -> io::Result<bool> {
668        // Use `libc::pread` so we get well-defined errors.
669        // Safe: Passing the slice as the buffer it is.
670        let ret = while_eintr(|| unsafe {
671            libc::pread(
672                file.as_raw_fd(),
673                slice.as_mut_ptr() as *mut libc::c_void,
674                slice.len(),
675                offset,
676            )
677        });
678
679        if let Err(err) = ret {
680            if err.raw_os_error() == Some(libc::EINVAL) {
681                return Ok(false);
682            } else {
683                return Err(err);
684            }
685        }
686
687        if !*may_write {
688            return Ok(true);
689        }
690
691        // Safe: Passing the slice as the buffer it is.
692        let ret = while_eintr(|| unsafe {
693            libc::pwrite(
694                file.as_raw_fd(),
695                slice.as_ptr() as *const libc::c_void,
696                slice.len(),
697                offset,
698            )
699        });
700
701        if let Err(err) = ret {
702            if err.raw_os_error() == Some(libc::EINVAL) {
703                Ok(false)
704            } else if err.raw_os_error() == Some(libc::EBADF) {
705                *may_write = false;
706                Ok(true)
707            } else {
708                Err(err)
709            }
710        } else {
711            Ok(true)
712        }
713    }
714
715    /// Get system-reported minimum request alignment for direct I/O.
716    #[cfg(unix)]
717    fn get_min_dio_req_align(file: &fs::File) -> usize {
718        #[cfg(target_os = "linux")]
719        {
720            let mut alignment = 0;
721            let res = unsafe { ioctl::blksszget(file.as_raw_fd(), &mut alignment) };
722            if res.is_ok() && alignment > 0 {
723                let alignment = alignment as usize;
724                if alignment.is_power_of_two() {
725                    return alignment;
726                }
727            }
728        }
729
730        #[cfg(target_os = "macos")]
731        {
732            let mut alignment = 0;
733            let res = unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut alignment) };
734            if res.is_ok() && alignment.is_power_of_two() {
735                return alignment as usize;
736            }
737        }
738
739        #[cfg(target_os = "freebsd")]
740        {
741            let mut alignment = 0;
742            let res = unsafe { ioctl::diocgsectorsize(file.as_raw_fd(), &mut alignment) };
743            if res.is_ok() && alignment.is_power_of_two() {
744                return alignment as usize;
745            }
746        }
747
748        // Then we’ll probe.
749        1
750    }
751
752    /// Get system-reported minimum memory alignment for direct I/O.
753    #[cfg(unix)]
754    fn get_min_dio_mem_align(_file: &fs::File) -> usize {
755        // I don’t think there’s a reliable way to get this.
756        1
757    }
758
759    /// Probe minimal request and memory alignments.
760    ///
761    /// Start at `min_req_align` and `min_mem_align`.
762    #[cfg(windows)]
763    fn probe_alignments(
764        _file: &mut fs::File,
765        min_req_align: usize,
766        min_mem_align: usize,
767    ) -> (usize, usize, usize, usize) {
768        // TODO: Need to find out how Windows indicates unaligned I/O
769        (
770            cmp::max(min_req_align, 4096),
771            cmp::max(min_mem_align, 4096),
772            1,
773            1,
774        )
775    }
776
777    /// Implementation for anything that opens a file.
778    fn do_open_sync(opts: StorageOpenOptions, base_fs_opts: fs::OpenOptions) -> io::Result<Self> {
779        #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
780        if opts.write_dontcache && !opts.writable {
781            return Err(io::Error::new(
782                io::ErrorKind::InvalidInput,
783                "RWF_DONTCACHE requires writable storage",
784            ));
785        }
786
787        #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
788        if opts.write_dontcache && opts.direct {
789            return Err(io::Error::new(
790                io::ErrorKind::InvalidInput,
791                "RWF_DONTCACHE is incompatible with direct I/O",
792            ));
793        }
794
795        let Some(filename) = opts.filename else {
796            return Err(io::Error::new(
797                io::ErrorKind::InvalidInput,
798                "Filename required",
799            ));
800        };
801
802        let mut file_opts = base_fs_opts;
803        file_opts.read(true).write(opts.writable);
804        #[cfg(not(target_os = "macos"))]
805        if opts.direct {
806            file_opts.custom_flags(
807                #[cfg(unix)]
808                libc::O_DIRECT,
809                #[cfg(windows)]
810                windows_sys::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING,
811            );
812        }
813
814        let filename_owned = filename.to_owned();
815        let file = file_opts.open(filename)?;
816
817        #[cfg(target_os = "macos")]
818        if opts.direct {
819            // Safe: We check the return value.
820            while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) })
821                .err_context(|| "Failed to disable host cache")?;
822        }
823
824        Self::new(
825            file,
826            Some(filename_owned),
827            opts.direct,
828            #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
829            opts.write_dontcache,
830            #[cfg(target_os = "macos")]
831            opts.relaxed_sync,
832        )
833    }
834
835    /// For special operations, ensure the error kind is usable.
836    ///
837    /// When invoking OS I/O operations directly, we turn the returned raw OS error code into an
838    /// `io::Error` object via `io::Error::last_os_error()`.  To differentiate between different
839    /// error cases, in generic imago code, we then don’t use that raw error code, but the error
840    /// kind (`io::ErrorKind`) instead, specifically it’s important to properly return an error of
841    /// kind `io::ErrorKind::Unsupported` when an operation is unsupported, so fall-backs can be
842    /// employed.
843    ///
844    /// Rust’s standard library only assigns this error kind (`Unsupported`) to `EOPNOTSUPP` (=
845    /// `ENOTSUP`) and `ENOSYS`.  However, some “special” operations (`fallocate()`,
846    /// `fcntl(F_PUNCHHOLE)`, `ioctl()`, ...) can return other error codes for when an operation is
847    /// not supported on a specific file, e.g. `ENODEV` or `ENXIO`.
848    ///
849    /// Assign the appropriate error kind to such errors so the generic code can handle them.
850    #[cfg(unix)]
851    fn map_os_err(err: io::Error) -> io::Error {
852        let Some(raw) = err.raw_os_error() else {
853            return err;
854        };
855
856        let has_kind = err.kind();
857        let want_kind = match raw {
858            #[allow(unreachable_patterns)] // `ENOTSUP` may be equal to `EOPNOTSUPP`
859            libc::ENOTSUP | libc::EOPNOTSUPP | libc::ENODEV | libc::ENXIO | libc::ENOTTY => {
860                io::ErrorKind::Unsupported
861            }
862            _ => has_kind,
863        };
864
865        if has_kind != want_kind {
866            io::Error::new(want_kind, err)
867        } else {
868            err
869        }
870    }
871
872    /// For special operations, ensure the error kind is usable.
873    ///
874    /// For non-UNIX systems, this is an identity map.
875    #[cfg(not(unix))]
876    fn map_os_err(err: io::Error) -> io::Error {
877        err
878    }
879
880    /// Attempt to discard range by truncating the file.
881    ///
882    /// If the range reaches the end of the file, truncate and restore the original file length.
883    /// Return `true` on success.
884    ///
885    /// If the range is not at the end of the file, i.e. another method of discarding is needed,
886    /// return `false`.
887    fn try_discard_by_truncate(&self, offset: u64, length: u64) -> io::Result<bool> {
888        // Prevent modifications to the file length
889        #[allow(clippy::readonly_write_lock)]
890        let file = self.file.write().unwrap();
891
892        let size = self.size.load(Ordering::Relaxed);
893        if offset >= size {
894            // Nothing to do
895            return Ok(true);
896        }
897
898        // If `offset + length` overflows, we can just assume it ends at `size`.  (Anything past
899        // `size is irrelevant anyway.)
900        let end = offset.checked_add(length).unwrap_or(size);
901        if end < size {
902            return Ok(false);
903        }
904
905        file.set_len(offset)?;
906        // Release the tail without changing the disk capacity seen after reopening the image.
907        file.set_len(size)?;
908        Ok(true)
909    }
910
911    /// Ensure the given range reads back as zeroes, or return an error.
912    async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
913        if self.try_discard_by_truncate(offset, length)? {
914            return Ok(());
915        }
916
917        if self.discard_unsupported.load(Ordering::Relaxed) {
918            Err(io::ErrorKind::Unsupported.into())
919        } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
920            if err.kind() == io::ErrorKind::Unsupported {
921                self.discard_unsupported.store(true, Ordering::Relaxed);
922            }
923            Err(err)
924        } else {
925            Ok(())
926        }
927    }
928
929    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
930    #[cfg(target_os = "linux")]
931    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
932        let offset: libc::off_t = offset
933            .try_into()
934            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
935        let length: libc::off_t = length
936            .try_into()
937            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
938
939        let file = self.file.read().unwrap();
940        // Safe: File descriptor is valid, and the rest are simple integer parameters.
941        while_eintr(|| unsafe {
942            libc::fallocate(
943                file.as_raw_fd(),
944                libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
945                offset,
946                length,
947            )
948        })
949        .map_err(Self::map_os_err)?;
950
951        Ok(())
952    }
953
954    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
955    #[cfg(windows)]
956    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
957        let offset: i64 = offset
958            .try_into()
959            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
960        let length: i64 = length
961            .try_into()
962            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
963
964        let end = offset.saturating_add(length).saturating_add(1);
965        let params = FILE_ZERO_DATA_INFORMATION {
966            FileOffset: offset,
967            BeyondFinalZero: end,
968        };
969        let mut _returned = 0;
970        let file = self.file.read().unwrap();
971        // Safe: File handle is valid, mandatory pointers (input, returned length) are passed and
972        // valid, the parameter type matches the call, and the input size matches the object
973        // passed.
974        let ret = unsafe {
975            DeviceIoControl(
976                file.as_raw_handle(),
977                FSCTL_SET_ZERO_DATA,
978                (&params as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
979                size_of_val(&params) as u32,
980                std::ptr::null_mut(),
981                0,
982                &mut _returned,
983                std::ptr::null_mut(),
984            )
985        };
986        if ret == 0 {
987            return Err(Self::map_os_err(io::Error::last_os_error()));
988        }
989
990        Ok(())
991    }
992
993    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
994    #[cfg(target_os = "macos")]
995    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
996        let offset: libc::off_t = offset
997            .try_into()
998            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
999        let length: libc::off_t = length
1000            .try_into()
1001            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1002
1003        let params = libc::fpunchhole_t {
1004            fp_flags: 0,
1005            reserved: 0,
1006            fp_offset: offset,
1007            fp_length: length,
1008        };
1009        let file = self.file.read().unwrap();
1010        // Safe: FD is valid, passed pointer is valid and its type matches the call.
1011        while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, &params) })
1012            .map_err(Self::map_os_err)?;
1013
1014        Ok(())
1015    }
1016
1017    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
1018    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1019    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1020        Err(io::ErrorKind::Unsupported.into())
1021    }
1022}
1023
1024impl Display for File {
1025    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1026        if let Some(filename) = self.filename.as_ref() {
1027            write!(f, "file:{filename:?}")
1028        } else {
1029            write!(f, "file:<unknown path>")
1030        }
1031    }
1032}
1033
1034/// Convert a successful vectored-write result into observable forward progress.
1035#[cfg(unix)]
1036fn require_write_progress(length: libc::ssize_t) -> io::Result<u64> {
1037    if length == 0 {
1038        Err(io::ErrorKind::WriteZero.into())
1039    } else {
1040        debug_assert!(length > 0);
1041        Ok(length as u64)
1042    }
1043}
1044
1045/// Turn a Linux vectored-write syscall return value into an I/O result.
1046#[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1047fn syscall_result(result: libc::ssize_t) -> io::Result<libc::ssize_t> {
1048    if result == -1 {
1049        Err(io::Error::last_os_error())
1050    } else {
1051        Ok(result)
1052    }
1053}
1054
1055/// Retry an I/O operation only when it was interrupted before making observable progress.
1056#[cfg(any(
1057    all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1058    all(test, unix)
1059))]
1060fn retry_interrupted<F>(mut operation: F) -> io::Result<libc::ssize_t>
1061where
1062    F: FnMut() -> io::Result<libc::ssize_t>,
1063{
1064    loop {
1065        match operation() {
1066            Err(error) if error.raw_os_error() == Some(libc::EINTR) => continue,
1067            result => return result,
1068        }
1069    }
1070}
1071
1072/// Try a `RWF_DONTCACHE` write and fall back only when that hint is unsupported.
1073///
1074/// Both operations are supplied by the caller so an unsupported hinted write can be retried with
1075/// the ordinary syscall using the exact same unconsumed iovec and offset.  All other errors,
1076/// including `EINVAL`, remain visible to the caller rather than silently changing I/O semantics.
1077#[cfg(any(
1078    all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1079    all(test, unix)
1080))]
1081fn write_with_optional_dontcache<H, P>(
1082    write_dontcache: &AtomicBool,
1083    hinted_write: H,
1084    plain_write: P,
1085) -> io::Result<libc::ssize_t>
1086where
1087    H: FnMut() -> io::Result<libc::ssize_t>,
1088    P: FnMut() -> io::Result<libc::ssize_t>,
1089{
1090    if !write_dontcache.load(Ordering::Relaxed) {
1091        return retry_interrupted(plain_write);
1092    }
1093
1094    match retry_interrupted(hinted_write) {
1095        Err(error)
1096            if matches!(
1097                error.raw_os_error(),
1098                Some(libc::EOPNOTSUPP) | Some(libc::ENOSYS)
1099            ) =>
1100        {
1101            // Kernel and filesystem support are authoritative.  Once either rejects the flag,
1102            // future writes avoid paying for another syscall that is known not to work.
1103            write_dontcache.store(false, Ordering::Relaxed);
1104            retry_interrupted(plain_write)
1105        }
1106        result => result,
1107    }
1108}
1109
1110/// Get total size in bytes of the given file.
1111///
1112/// If the file is a block or character device, use get_device_size() instead of
1113/// reading len from metadata which doesn't work on some platforms like macOS.
1114fn get_file_size(file: &fs::File) -> io::Result<u64> {
1115    #[allow(clippy::bind_instead_of_map)]
1116    file.metadata().and_then(|m| {
1117        #[cfg(unix)]
1118        if m.file_type().is_block_device() || m.file_type().is_char_device() {
1119            return get_device_size(file);
1120        }
1121        Ok(m.len())
1122    })
1123}
1124
1125cfg_if! {
1126    if #[cfg(target_os = "linux")] {
1127        /// Get total size in bytes of the given block or character device.
1128        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1129            let mut size = 0;
1130            unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1131            Ok(size)
1132        }
1133    } else if #[cfg(target_os = "macos")] {
1134        /// Get total size in bytes of the given block or character device.
1135        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1136            let mut block_size = 0;
1137            unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1138            let mut block_count = 0;
1139            unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1140            Ok(u64::from(block_size) * block_count)
1141        }
1142    } else if #[cfg(target_os = "freebsd")] {
1143        /// Get total size in bytes of the given block or character device.
1144        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1145            let mut size = 0;
1146            unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1147            Ok(size as u64)
1148        }
1149    } else if #[cfg(unix)] {
1150        /// Get total size in bytes of the given block or character device - unsupported platform.
1151        fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1152            Err(io::ErrorKind::Unsupported.into())
1153        }
1154    }
1155}
1156
1157/// This module generates type-safe wrappers for chosen ioctls
1158mod ioctl {
1159    #[cfg(unix)]
1160    use nix::ioctl_read;
1161    #[cfg(target_os = "linux")]
1162    use nix::ioctl_read_bad;
1163
1164    // https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h#L200
1165
1166    #[cfg(target_os = "linux")]
1167    ioctl_read!(blkgetsize64, 0x12, 114, u64);
1168
1169    #[cfg(target_os = "linux")]
1170    ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1171
1172    // https://github.com/apple-oss-distributions/xnu/blob/main/bsd/sys/disk.h#L198-L199
1173
1174    #[cfg(target_os = "macos")]
1175    ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1176
1177    #[cfg(target_os = "macos")]
1178    ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1179
1180    // https://web.mit.edu/freebsd/head/sys/sys/disk.h
1181
1182    #[cfg(target_os = "freebsd")]
1183    ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1184
1185    #[cfg(target_os = "freebsd")]
1186    ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1187}
1188
1189#[cfg(all(test, unix))]
1190mod tests {
1191    use super::*;
1192
1193    use std::cell::Cell;
1194
1195    #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1196    #[test]
1197    fn write_dontcache_options_default_to_disabled() {
1198        let options = StorageOpenOptions::new();
1199        assert!(!options.get_write_dontcache());
1200
1201        let options = options.write_dontcache(true);
1202        assert!(options.get_write_dontcache());
1203    }
1204
1205    #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1206    #[test]
1207    fn write_dontcache_rejects_read_only_files() {
1208        let options = StorageOpenOptions::new().write_dontcache(true);
1209        let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1210
1211        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1212        assert!(error.to_string().contains("writable storage"));
1213    }
1214
1215    #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1216    #[test]
1217    fn write_dontcache_rejects_direct_io() {
1218        let options = StorageOpenOptions::new()
1219            .write(true)
1220            .direct(true)
1221            .write_dontcache(true);
1222        let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1223
1224        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1225        assert!(error.to_string().contains("direct I/O"));
1226    }
1227
1228    #[test]
1229    fn unsupported_hint_retries_the_same_tail_and_disables_future_hints() {
1230        for unsupported_error in [libc::EOPNOTSUPP, libc::ENOSYS] {
1231            let enabled = AtomicBool::new(true);
1232            let hinted_calls = Cell::new(0);
1233            let plain_calls = Cell::new(0);
1234
1235            let length = write_with_optional_dontcache(
1236                &enabled,
1237                || {
1238                    hinted_calls.set(hinted_calls.get() + 1);
1239                    if hinted_calls.get() == 1 {
1240                        Err(io::Error::from_raw_os_error(libc::EINTR))
1241                    } else {
1242                        Err(io::Error::from_raw_os_error(unsupported_error))
1243                    }
1244                },
1245                || {
1246                    plain_calls.set(plain_calls.get() + 1);
1247                    Ok(37)
1248                },
1249            )
1250            .unwrap();
1251
1252            assert_eq!(length, 37);
1253            assert_eq!(hinted_calls.get(), 2);
1254            assert_eq!(plain_calls.get(), 1);
1255            assert!(!enabled.load(Ordering::Relaxed));
1256
1257            let length = write_with_optional_dontcache(
1258                &enabled,
1259                || panic!("a disabled hint must not be retried"),
1260                || {
1261                    plain_calls.set(plain_calls.get() + 1);
1262                    Ok(11)
1263                },
1264            )
1265            .unwrap();
1266
1267            assert_eq!(length, 11);
1268            assert_eq!(plain_calls.get(), 2);
1269        }
1270    }
1271
1272    #[test]
1273    fn invalid_hint_error_does_not_fall_back() {
1274        let enabled = AtomicBool::new(true);
1275        let plain_calls = Cell::new(0);
1276
1277        let error = write_with_optional_dontcache(
1278            &enabled,
1279            || Err(io::Error::from_raw_os_error(libc::EINVAL)),
1280            || {
1281                plain_calls.set(plain_calls.get() + 1);
1282                Ok(1)
1283            },
1284        )
1285        .unwrap_err();
1286
1287        assert_eq!(error.raw_os_error(), Some(libc::EINVAL));
1288        assert_eq!(plain_calls.get(), 0);
1289        assert!(enabled.load(Ordering::Relaxed));
1290    }
1291
1292    #[test]
1293    fn successful_partial_hint_write_advances_without_fallback() {
1294        let enabled = AtomicBool::new(true);
1295        let plain_calls = Cell::new(0);
1296
1297        let length = write_with_optional_dontcache(
1298            &enabled,
1299            || Ok(13),
1300            || {
1301                plain_calls.set(plain_calls.get() + 1);
1302                Ok(99)
1303            },
1304        )
1305        .unwrap();
1306
1307        assert_eq!(length, 13);
1308        assert_eq!(plain_calls.get(), 0);
1309        assert!(enabled.load(Ordering::Relaxed));
1310    }
1311
1312    #[test]
1313    fn zero_length_write_is_write_zero() {
1314        let error = require_write_progress(0).unwrap_err();
1315        assert_eq!(error.kind(), io::ErrorKind::WriteZero);
1316    }
1317}
1318
1319#[cfg(test)]
1320mod tail_discard_tests {
1321    use super::File;
1322    use crate::{Storage, StorageExt, StorageOpenOptions};
1323    use std::{fs, io};
1324
1325    // Keep cleanup alive until all storage handles have been dropped, including on assertion failure.
1326    struct TempPath(std::path::PathBuf);
1327
1328    impl Drop for TempPath {
1329        fn drop(&mut self) {
1330            let _ = fs::remove_file(&self.0);
1331        }
1332    }
1333
1334    #[test]
1335    fn tail_discard_keeps_file_length() -> io::Result<()> {
1336        let runtime = tokio::runtime::Builder::new_current_thread().build()?;
1337        runtime.block_on(async {
1338            let unique = std::time::SystemTime::now()
1339                .duration_since(std::time::UNIX_EPOCH)
1340                .unwrap()
1341                .as_nanos();
1342            let path = std::env::temp_dir().join(format!(
1343                "msb-imago-tail-discard-{}-{unique}.raw",
1344                std::process::id()
1345            ));
1346            let _temp_path = TempPath(path.clone());
1347            fs::write(&path, vec![0xabu8; 8192])?;
1348
1349            let file = File::open(StorageOpenOptions::new().write(true).filename(&path)).await?;
1350            file.discard(4096, 4096).await?;
1351            assert_eq!(fs::metadata(&path)?.len(), 8192);
1352            assert_eq!(file.size()?, 8192);
1353
1354            let mut prefix = vec![0u8; 4096];
1355            file.read(&mut prefix, 0).await?;
1356            assert_eq!(prefix, vec![0xabu8; 4096]);
1357
1358            let mut tail = vec![0xffu8; 4096];
1359            file.read(&mut tail, 4096).await?;
1360            assert!(tail.iter().all(|&byte| byte == 0));
1361
1362            // Reopening must discover the same capacity, rather than relying on the cached size.
1363            drop(file);
1364            let reopened = File::open(StorageOpenOptions::new().filename(&path)).await?;
1365            assert_eq!(reopened.size()?, 8192);
1366            Ok(())
1367        })
1368    }
1369}