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 maybe_async::maybe_async;
15use parking_lot::RwLock;
16use std::fmt::{self, Display, Formatter};
17use std::io::{self, Write};
18#[cfg(any(target_os = "linux", target_os = "macos"))]
19use std::os::fd::AsRawFd;
20#[cfg(unix)]
21use std::os::unix::fs::FileTypeExt;
22#[cfg(all(unix, not(target_os = "macos")))]
23use std::os::unix::fs::OpenOptionsExt;
24#[cfg(windows)]
25use std::os::windows::fs::{FileExt, OpenOptionsExt};
26#[cfg(windows)]
27use std::os::windows::io::AsRawHandle;
28use std::path::{Path, PathBuf};
29use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30use std::{cmp, fs};
31#[cfg(unix)]
32use tracing::{debug, warn};
33#[cfg(windows)]
34use windows_sys::Win32;
35#[cfg(windows)]
36use windows_sys::Win32::System::Ioctl::{FILE_ZERO_DATA_INFORMATION, FSCTL_SET_ZERO_DATA};
37#[cfg(windows)]
38use windows_sys::Win32::System::IO::DeviceIoControl;
39
40/// Use a plain file or host block device as a storage object.
41#[derive(Debug)]
42pub struct File {
43    /// The file.
44    file: RwLock<fs::File>,
45
46    /// For debug purposes, and to resolve relative filenames.
47    filename: Option<PathBuf>,
48
49    /// Minimal I/O alignment for requests.
50    req_align: usize,
51
52    /// Minimal memory buffer alignment.
53    mem_align: usize,
54
55    /// Minimum required alignment for zero writes.
56    zero_align: usize,
57
58    /// Minimum required alignment for effective discards.
59    discard_align: usize,
60
61    /// Cached file length.
62    ///
63    /// Third parties changing the length concurrently is pretty certain to break things anyway.
64    size: AtomicU64,
65
66    /// Storage helper.
67    common_storage_helper: CommonStorageHelper,
68
69    /// macOS-only: Use fsync() instead of F_FULLFSYNC on `sync()` method.
70    #[cfg(target_os = "macos")]
71    relaxed_sync: bool,
72
73    /// Set once we know that discard is unsupported and we can skip trying.
74    discard_unsupported: AtomicBool,
75}
76
77impl TryFrom<fs::File> for File {
78    type Error = io::Error;
79
80    /// Use the given existing `std::fs::File`.
81    ///
82    /// Convert the given existing `std::fs::File` object into an imago storage object.
83    ///
84    /// When using this, the resulting object will not know its own filename.  That makes it
85    /// impossible to auto-resolve relative paths to it, e.g. qcow2 backing file names.
86    fn try_from(file: fs::File) -> io::Result<Self> {
87        let open_mode = Self::detect_open_mode(&file);
88        Self::new(
89            file,
90            None,
91            open_mode.0,
92            #[cfg(unix)]
93            open_mode.1,
94            #[cfg(target_os = "macos")]
95            false,
96        )
97    }
98}
99
100#[maybe_async(AFIT)]
101impl Storage for File {
102    async fn open(opts: StorageOpenOptions) -> io::Result<Self> {
103        Self::do_open_sync(opts, fs::OpenOptions::new())
104    }
105
106    #[cfg(feature = "sync-wrappers")]
107    fn open_sync(opts: StorageOpenOptions) -> io::Result<Self> {
108        Self::do_open_sync(opts, fs::OpenOptions::new())
109    }
110
111    async fn create_open(opts: StorageCreateOptions) -> io::Result<Self> {
112        // Always allow writing for new files
113        let opts = opts.modify_open_opts(|o| o.write(true));
114        let size = opts.size;
115        let prealloc_mode = opts.prealloc_mode;
116
117        let mut file_opts = fs::OpenOptions::new();
118        if opts.overwrite {
119            file_opts.create(true).truncate(true);
120        } else {
121            file_opts.create_new(true);
122        };
123
124        let file = Self::do_open_sync(opts.get_open_options(), file_opts)?;
125        if size > 0 {
126            file.resize(size, prealloc_mode)
127                .await
128                .err_context(|| "Resizing file")?;
129        }
130
131        Ok(file)
132    }
133
134    fn mem_align(&self) -> usize {
135        self.mem_align
136    }
137
138    fn req_align(&self) -> usize {
139        self.req_align
140    }
141
142    fn zero_align(&self) -> usize {
143        cmp::max(self.zero_align, self.req_align)
144    }
145
146    fn discard_align(&self) -> usize {
147        cmp::max(self.discard_align, self.req_align)
148    }
149
150    fn size(&self) -> io::Result<u64> {
151        Ok(self.size.load(Ordering::Relaxed))
152    }
153
154    fn resolve_relative_path<P: AsRef<Path>>(&self, relative: P) -> io::Result<PathBuf> {
155        let relative = relative.as_ref();
156
157        if relative.is_absolute() {
158            return Ok(relative.to_path_buf());
159        }
160
161        let filename = self
162            .filename
163            .as_ref()
164            .ok_or_else(|| io::Error::other("No filename set for base image"))?;
165
166        let dirname = filename
167            .parent()
168            .ok_or_else(|| io::Error::other("Invalid base image filename set"))?;
169
170        Ok(dirname.join(relative))
171    }
172
173    fn get_filename(&self) -> Option<PathBuf> {
174        self.filename.as_ref().cloned()
175    }
176
177    #[cfg(unix)]
178    async unsafe fn pure_readv(
179        &self,
180        mut bufv: IoVectorMut<'_>,
181        mut offset: u64,
182    ) -> io::Result<()> {
183        while !bufv.is_empty() {
184            let iovec = unsafe { bufv.as_iovec() };
185            let preadv_offset = offset
186                .try_into()
187                .map_err(|_| io::Error::other("Read offset overflow"))?;
188
189            let len = while_eintr(|| unsafe {
190                libc::preadv(
191                    self.file.read().as_raw_fd(),
192                    iovec.as_ptr(),
193                    iovec.len() as libc::c_int,
194                    preadv_offset,
195                )
196            })? as u64;
197
198            if len == 0 {
199                // End of file
200                bufv.fill(0);
201                break;
202            }
203
204            bufv = bufv.split_tail_at(len);
205            offset = offset
206                .checked_add(len)
207                .ok_or_else(|| io::Error::other("Read offset overflow"))?;
208        }
209
210        Ok(())
211    }
212
213    #[cfg(windows)]
214    async unsafe fn pure_readv(&self, bufv: IoVectorMut<'_>, mut offset: u64) -> io::Result<()> {
215        for mut buffer in bufv.into_inner() {
216            let mut buffer: &mut [u8] = &mut buffer;
217            while !buffer.is_empty() {
218                let len = if offset >= self.size.load(Ordering::Relaxed) {
219                    buffer.fill(0);
220                    buffer.len()
221                } else {
222                    match self.file.write().seek_read(buffer, offset)? {
223                        0 => {
224                            // End of file (file is shorter than `self.size`)
225                            buffer.fill(0);
226                            buffer.len()
227                        }
228                        len => len,
229                    }
230                };
231                offset = offset
232                    .checked_add(len as u64)
233                    .ok_or_else(|| io::Error::other("Read offset overflow"))?;
234                buffer = buffer.split_at_mut(len).1;
235            }
236        }
237        Ok(())
238    }
239
240    #[cfg(unix)]
241    async unsafe fn pure_writev(&self, mut bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
242        while !bufv.is_empty() {
243            let iovec = unsafe { bufv.as_iovec() };
244            let pwritev_offset = offset
245                .try_into()
246                .map_err(|_| io::Error::other("Write offset overflow"))?;
247
248            // Keep holding the lock until `self.size` is updated, so that `resize()` (which takes
249            // the write lock) always sees the current size
250            let file = self.file.read();
251            let len = while_eintr(|| unsafe {
252                libc::pwritev(
253                    file.as_raw_fd(),
254                    iovec.as_ptr(),
255                    iovec.len() as libc::c_int,
256                    pwritev_offset,
257                )
258            })? as u64;
259
260            if len == 0 {
261                // Should not happen, i.e. is an error
262                return Err(io::ErrorKind::WriteZero.into());
263            }
264
265            bufv = bufv.split_tail_at(len);
266            offset = offset
267                .checked_add(len)
268                .ok_or_else(|| io::Error::other("Write offset overflow"))?;
269            self.size.fetch_max(offset, Ordering::Relaxed);
270        }
271
272        Ok(())
273    }
274
275    #[cfg(windows)]
276    async unsafe fn pure_writev(&self, bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
277        for buffer in bufv.into_inner() {
278            let mut buffer: &[u8] = &buffer;
279            while !buffer.is_empty() {
280                // Keep holding the lock until `self.size` is updated, so that `resize()` always
281                // sees the current size
282                #[allow(clippy::readonly_write_lock)]
283                let file = self.file.write();
284                let len = file.seek_write(buffer, offset)?;
285                if len == 0 {
286                    // Should not happen, i.e. is an error
287                    return Err(io::ErrorKind::WriteZero.into());
288                }
289                offset = offset
290                    .checked_add(len as u64)
291                    .ok_or_else(|| io::Error::other("Write offset overflow"))?;
292                self.size.fetch_max(offset, Ordering::Relaxed);
293                buffer = buffer.split_at(len).1;
294            }
295        }
296        Ok(())
297    }
298
299    async unsafe fn pure_write_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
300        let end = offset
301            .checked_add(length)
302            .ok_or_else(|| io::Error::other("Write-zeroes wrap-around"))?;
303
304        // Only zeroes the part inside of the file
305        self.discard_to_zero(offset, length).await?;
306
307        // Unlike discarding, writing zeroes must grow the file if the range reaches past its end.
308        // Everything that grows the file updates `self.size` while holding `self.file`, so under
309        // the write lock, `self.size` is current, and growing (never shrinking) the file to `end`
310        // cannot cut off data written concurrently.
311        #[allow(clippy::readonly_write_lock)]
312        let file = self.file.write();
313        if end > self.size.load(Ordering::Relaxed) {
314            file.set_len(end)?;
315            self.size.fetch_max(end, Ordering::Relaxed);
316        }
317
318        Ok(())
319    }
320
321    #[cfg(target_os = "linux")]
322    async unsafe fn pure_write_allocated_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
323        let offset: libc::off_t = offset
324            .try_into()
325            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
326        let length: libc::off_t = length
327            .try_into()
328            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
329
330        let file = self.file.read();
331        // Safe: File descriptor is valid, and the rest are simple integer parameters.
332        while_eintr(|| unsafe {
333            libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_ZERO_RANGE, offset, length)
334        })
335        .map_err(Self::map_os_err)?;
336
337        // Without `FALLOC_FL_KEEP_SIZE`, this may have grown the file
338        self.size
339            .fetch_max(offset as u64 + length as u64, Ordering::Relaxed);
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().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().as_raw_fd()) })?;
368            return Ok(());
369        }
370        self.file.write().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();
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
452#[maybe_async]
453impl File {
454    /// Use an existing file with explicitly known open options.
455    ///
456    /// The filename is not used to reopen the file, but is retained to resolve relative paths.
457    /// The writable and direct I/O options must match how the file was opened.
458    pub fn from_open_file(file: fs::File, opts: StorageOpenOptions) -> io::Result<Self> {
459        Self::new(
460            file,
461            opts.filename,
462            opts.direct,
463            #[cfg(unix)]
464            opts.writable,
465            #[cfg(target_os = "macos")]
466            opts.relaxed_sync,
467        )
468    }
469
470    /// Central internal function to create a `File` object.
471    fn new(
472        mut file: fs::File,
473        filename: Option<PathBuf>,
474        direct_io: bool,
475        #[cfg(unix)] writable: bool,
476        #[cfg(target_os = "macos")] relaxed_sync: bool,
477    ) -> io::Result<Self> {
478        let size = get_file_size(&file).err_context(|| "Failed to determine file size")?;
479
480        let (req_align, mem_align, zero_align, discard_align) = if direct_io {
481            #[cfg(unix)]
482            let (min_req_align, min_mem_align) = (
483                Self::get_min_dio_req_align(&file),
484                Self::get_min_dio_mem_align(&file),
485            );
486            #[cfg(not(unix))]
487            let (min_req_align, min_mem_align) = (1, 1);
488
489            Self::probe_alignments(
490                &mut file,
491                min_req_align,
492                min_mem_align,
493                #[cfg(unix)]
494                writable,
495            )
496        } else {
497            // Buffered I/O has no request/memory alignment (page cache).
498            // Write-zeroes/discard still need the filesystem block size when
499            // writable; read-only files never issue those operations.
500            #[cfg(unix)]
501            let (zero_align, discard_align) = if writable {
502                Self::get_fs_zero_discard_align(&file)
503            } else {
504                (1, 1)
505            };
506            #[cfg(not(unix))]
507            let (zero_align, discard_align) = (1, 1);
508            (1, 1, zero_align, discard_align)
509        };
510        assert!(req_align.is_power_of_two());
511        assert!(mem_align.is_power_of_two());
512
513        Ok(File {
514            file: RwLock::new(file),
515            filename,
516            req_align,
517            mem_align,
518            zero_align,
519            discard_align,
520            size: size.into(),
521            common_storage_helper: Default::default(),
522            #[cfg(target_os = "macos")]
523            relaxed_sync,
524            discard_unsupported: AtomicBool::new(false),
525        })
526    }
527
528    /// Detect access and direct I/O modes for an externally supplied file.
529    ///
530    /// Returns `(direct_io, writable)`, indicating whether direct I/O is enabled and whether the
531    /// file is open for writing.
532    #[cfg(unix)]
533    fn detect_open_mode(file: &fs::File) -> (bool, bool) {
534        // Safe: No argument, returns result.
535        let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
536        let writable = flags < 0 || (flags & libc::O_ACCMODE) != libc::O_RDONLY;
537        #[cfg(not(target_os = "macos"))]
538        let direct_io = flags >= 0 && (flags & libc::O_DIRECT) != 0;
539        #[cfg(target_os = "macos")]
540        let direct_io = false;
541        (direct_io, writable)
542    }
543
544    /// Conservatively assume externally supplied files are writable on non-Unix systems.
545    ///
546    /// Returns `(direct_io, writable)`, indicating whether direct I/O is enabled and whether the
547    /// file is open for writing.
548    #[cfg(not(unix))]
549    fn detect_open_mode(_file: &fs::File) -> (bool, bool) {
550        (false, true)
551    }
552
553    /// Host page size, rounded up to a power of two.
554    #[cfg(unix)]
555    fn host_page_size() -> usize {
556        let page_size = page_size::get();
557        if page_size.is_power_of_two() {
558            return page_size;
559        }
560        let assume = page_size.checked_next_power_of_two().unwrap_or(4096);
561        let assume = cmp::max(4096, assume);
562        warn!("Reported page size of {page_size} is not a power of two, assuming {assume}");
563        assume
564    }
565
566    /// Filesystem block size used as write-zeroes / discard alignment.
567    #[cfg(unix)]
568    fn get_fs_zero_discard_align(file: &fs::File) -> (usize, usize) {
569        let page_size = Self::host_page_size();
570
571        #[cfg(any(target_os = "linux", target_os = "macos"))]
572        let (mut zero_align, mut discard_align) = {
573            let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
574            // Safe: FD is valid, passed pointer is valid and its type matches the call.
575            match while_eintr(|| unsafe { libc::fstatfs(file.as_raw_fd(), &mut statfs) }) {
576                // On macOS, `f_bsize` is the fundamental block size.  On Linux, `f_bsize` is the
577                // optimal transfer block size and `f_frsize` is the actual block size.
578                #[cfg(target_os = "linux")]
579                Ok(_) => (statfs.f_frsize as usize, statfs.f_frsize as usize),
580                #[cfg(target_os = "macos")]
581                Ok(_) => (statfs.f_bsize as usize, statfs.f_bsize as usize),
582
583                Err(_) => (page_size, page_size),
584            }
585        };
586
587        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
588        let (mut zero_align, mut discard_align) = {
589            let _ = file;
590            (page_size, page_size)
591        };
592
593        // Double-check to make absolutely sure both are powers of two
594        if !zero_align.is_power_of_two() {
595            zero_align = page_size;
596        }
597        if !discard_align.is_power_of_two() {
598            discard_align = page_size;
599        }
600
601        (zero_align, discard_align)
602    }
603
604    /// Probe minimal direct I/O request, memory, zero and discard alignments.
605    ///
606    /// Start at `min_req_align` and `min_mem_align`.
607    #[cfg(unix)]
608    fn probe_alignments(
609        file: &mut fs::File,
610        min_req_align: usize,
611        min_mem_align: usize,
612        mut writable: bool,
613    ) -> (usize, usize, usize, usize) {
614        let page_size = Self::host_page_size();
615        let (zero_align, discard_align) = Self::get_fs_zero_discard_align(file);
616
617        let max_req_align = 65536;
618        let max_mem_align = cmp::max(page_size, max_req_align);
619
620        // Minimum fallbacks in case something goes wrong.
621        let safe_req_align = 4096;
622        let safe_mem_align = cmp::max(page_size, safe_req_align);
623
624        let mut test_buf = match IoBuffer::new(max_mem_align, max_mem_align) {
625            Ok(buf) => buf,
626            Err(err) => {
627                warn!(
628                    "Failed to allocate memory to probe request alignment ({err}), \
629                    falling back to {safe_req_align}/{safe_mem_align}"
630                );
631                return (safe_req_align, safe_mem_align, zero_align, discard_align);
632            }
633        };
634
635        let mut req_align: usize = min_req_align;
636        let result = loop {
637            assert!(req_align <= max_mem_align);
638            match Self::probe_access(
639                file,
640                test_buf.as_mut_range(0..req_align).into_slice(),
641                req_align.try_into().unwrap(),
642                &mut writable,
643            ) {
644                Ok(true) => break Ok(req_align),
645                Ok(false) => {
646                    if req_align >= max_req_align {
647                        break Err(io::Error::other(format!(
648                            "Maximum I/O alignment ({max_req_align}) exceeded"
649                        )));
650                    }
651                    // No reason to probe anything between 1 and 512
652                    if req_align == min_req_align {
653                        req_align = cmp::max(min_req_align << 1, 512);
654                    } else {
655                        req_align <<= 1;
656                    }
657                }
658                Err(err) => break Err(err),
659            }
660        };
661
662        let req_align = match result {
663            Ok(align) => {
664                debug!("Probed request alignment: {align}");
665                align
666            }
667            Err(err) => {
668                // Failed to determine request alignment, use a presumably safe value
669                let align = cmp::max(req_align, safe_req_align);
670                warn!(
671                    "Failed to probe request alignment ({err}; {}), falling back to {align} bytes",
672                    err.kind(),
673                );
674                align
675            }
676        };
677
678        let mut mem_align: usize = min_mem_align;
679        let result = loop {
680            assert!(mem_align <= max_mem_align);
681            let range = (max_mem_align - mem_align)..max_mem_align;
682            match Self::probe_access(
683                file,
684                test_buf.as_mut_range(range).into_slice(),
685                0,
686                &mut writable,
687            ) {
688                Ok(true) => break Ok(mem_align),
689                Ok(false) => {
690                    // Not aligned
691                    if mem_align >= max_mem_align {
692                        break Err(io::Error::other(format!(
693                            "Maximum memory alignment ({max_mem_align}) exceeded"
694                        )));
695                    }
696                    // No reason to probe anything between 1 and the page size (or 4096 at least)
697                    if mem_align == min_mem_align {
698                        mem_align = cmp::max(min_mem_align << 1, cmp::min(page_size, 4096));
699                    } else {
700                        mem_align <<= 1;
701                    }
702                }
703                Err(err) => break Err(err),
704            }
705        };
706
707        let mem_align = match result {
708            Ok(align) => {
709                debug!("Probed memory alignment: {align}");
710                align
711            }
712            Err(err) => {
713                // Failed to determine memory alignment, use a presumably safe value
714                let align = cmp::max(mem_align, safe_mem_align);
715                warn!(
716                    "Failed to probe memory alignment ({err}; {}), falling back to {align} bytes",
717                    err.kind(),
718                );
719                align
720            }
721        };
722
723        (req_align, mem_align, zero_align, discard_align)
724    }
725
726    /// Do an alignment-probing I/O access.
727    ///
728    /// Return `Ok(true)` if everything was OK, and `Ok(false)` if the request was reported to be
729    /// misaligned.
730    ///
731    /// `may_write` is a boolean that controls whether this is allowed to write (the same data read
732    /// before) to improve reliability.  Is automatically set to `false` if writing is found to not
733    /// be possible.
734    #[cfg(unix)]
735    fn probe_access(
736        file: &mut fs::File,
737        slice: &mut [u8],
738        offset: libc::off_t,
739        may_write: &mut bool,
740    ) -> io::Result<bool> {
741        // Use `libc::pread` so we get well-defined errors.
742        // Safe: Passing the slice as the buffer it is.
743        let ret = while_eintr(|| unsafe {
744            libc::pread(
745                file.as_raw_fd(),
746                slice.as_mut_ptr() as *mut libc::c_void,
747                slice.len(),
748                offset,
749            )
750        });
751
752        if let Err(err) = ret {
753            if err.raw_os_error() == Some(libc::EINVAL) {
754                return Ok(false);
755            } else {
756                return Err(err);
757            }
758        }
759
760        if !*may_write {
761            return Ok(true);
762        }
763
764        // Safe: Passing the slice as the buffer it is.
765        let ret = while_eintr(|| unsafe {
766            libc::pwrite(
767                file.as_raw_fd(),
768                slice.as_ptr() as *const libc::c_void,
769                slice.len(),
770                offset,
771            )
772        });
773
774        if let Err(err) = ret {
775            if err.raw_os_error() == Some(libc::EINVAL) {
776                Ok(false)
777            } else if err.raw_os_error() == Some(libc::EBADF) {
778                *may_write = false;
779                Ok(true)
780            } else {
781                Err(err)
782            }
783        } else {
784            Ok(true)
785        }
786    }
787
788    /// Get system-reported minimum request alignment for direct I/O.
789    #[cfg(unix)]
790    fn get_min_dio_req_align(file: &fs::File) -> usize {
791        #[cfg(target_os = "linux")]
792        {
793            let mut alignment = 0;
794            let res = unsafe { ioctl::blksszget(file.as_raw_fd(), &mut alignment) };
795            if res.is_ok() && alignment > 0 {
796                let alignment = alignment as usize;
797                if alignment.is_power_of_two() {
798                    return alignment;
799                }
800            }
801        }
802
803        #[cfg(target_os = "macos")]
804        {
805            let mut alignment = 0;
806            let res = unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut alignment) };
807            if res.is_ok() && alignment.is_power_of_two() {
808                return alignment as usize;
809            }
810        }
811
812        #[cfg(target_os = "freebsd")]
813        {
814            let mut alignment = 0;
815            let res = unsafe { ioctl::diocgsectorsize(file.as_raw_fd(), &mut alignment) };
816            if res.is_ok() && alignment.is_power_of_two() {
817                return alignment as usize;
818            }
819        }
820
821        // Then we’ll probe.
822        1
823    }
824
825    /// Get system-reported minimum memory alignment for direct I/O.
826    #[cfg(unix)]
827    fn get_min_dio_mem_align(_file: &fs::File) -> usize {
828        // I don’t think there’s a reliable way to get this.
829        1
830    }
831
832    /// Probe minimal request and memory alignments.
833    ///
834    /// Start at `min_req_align` and `min_mem_align`.
835    #[cfg(windows)]
836    fn probe_alignments(
837        _file: &mut fs::File,
838        min_req_align: usize,
839        min_mem_align: usize,
840    ) -> (usize, usize, usize, usize) {
841        // TODO: Need to find out how Windows indicates unaligned I/O
842        (
843            cmp::max(min_req_align, 4096),
844            cmp::max(min_mem_align, 4096),
845            1,
846            1,
847        )
848    }
849
850    /// Implementation for anything that opens a file.
851    fn do_open_sync(opts: StorageOpenOptions, base_fs_opts: fs::OpenOptions) -> io::Result<Self> {
852        let Some(filename) = opts.filename else {
853            return Err(io::Error::new(
854                io::ErrorKind::InvalidInput,
855                "Filename required",
856            ));
857        };
858
859        let mut file_opts = base_fs_opts;
860        file_opts.read(true).write(opts.writable);
861        #[cfg(not(target_os = "macos"))]
862        if opts.direct {
863            file_opts.custom_flags(
864                #[cfg(unix)]
865                libc::O_DIRECT,
866                #[cfg(windows)]
867                windows_sys::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING,
868            );
869        }
870
871        let filename_owned = filename.to_owned();
872        let file = file_opts.open(filename)?;
873
874        #[cfg(target_os = "macos")]
875        if opts.direct {
876            // Safe: We check the return value.
877            while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) })
878                .err_context(|| "Failed to disable host cache")?;
879        }
880
881        Self::new(
882            file,
883            Some(filename_owned),
884            opts.direct,
885            #[cfg(unix)]
886            opts.writable,
887            #[cfg(target_os = "macos")]
888            opts.relaxed_sync,
889        )
890    }
891
892    /// For special operations, ensure the error kind is usable.
893    ///
894    /// When invoking OS I/O operations directly, we turn the returned raw OS error code into an
895    /// `io::Error` object via `io::Error::last_os_error()`.  To differentiate between different
896    /// error cases, in generic imago code, we then don’t use that raw error code, but the error
897    /// kind (`io::ErrorKind`) instead, specifically it’s important to properly return an error of
898    /// kind `io::ErrorKind::Unsupported` when an operation is unsupported, so fall-backs can be
899    /// employed.
900    ///
901    /// Rust’s standard library only assigns this error kind (`Unsupported`) to `EOPNOTSUPP` (=
902    /// `ENOTSUP`) and `ENOSYS`.  However, some “special” operations (`fallocate()`,
903    /// `fcntl(F_PUNCHHOLE)`, `ioctl()`, ...) can return other error codes for when an operation is
904    /// not supported on a specific file, e.g. `ENODEV` or `ENXIO`.
905    ///
906    /// Assign the appropriate error kind to such errors so the generic code can handle them.
907    #[cfg(unix)]
908    fn map_os_err(err: io::Error) -> io::Error {
909        let Some(raw) = err.raw_os_error() else {
910            return err;
911        };
912
913        let has_kind = err.kind();
914        let want_kind = match raw {
915            #[allow(unreachable_patterns)] // `ENOTSUP` may be equal to `EOPNOTSUPP`
916            libc::ENOTSUP | libc::EOPNOTSUPP | libc::ENODEV | libc::ENXIO | libc::ENOTTY => {
917                io::ErrorKind::Unsupported
918            }
919            _ => has_kind,
920        };
921
922        if has_kind != want_kind {
923            io::Error::new(want_kind, err)
924        } else {
925            err
926        }
927    }
928
929    /// For special operations, ensure the error kind is usable.
930    ///
931    /// Rust’s std maps neither `ERROR_NOT_SUPPORTED` nor `ERROR_INVALID_FUNCTION` (which is what
932    /// file systems generally return for control codes they do not support) to
933    /// [`io::ErrorKind::Unsupported`].
934    ///
935    /// Assign the appropriate error kind to such errors so the generic code can handle them.
936    #[cfg(windows)]
937    fn map_os_err(err: io::Error) -> io::Error {
938        let Some(raw) = err.raw_os_error() else {
939            return err;
940        };
941
942        let has_kind = err.kind();
943        let want_kind = match raw as u32 {
944            Win32::Foundation::ERROR_NOT_SUPPORTED | Win32::Foundation::ERROR_INVALID_FUNCTION => {
945                io::ErrorKind::Unsupported
946            }
947            _ => has_kind,
948        };
949
950        if has_kind != want_kind {
951            io::Error::new(want_kind, err)
952        } else {
953            err
954        }
955    }
956
957    /// For special operations, ensure the error kind is usable.
958    ///
959    /// For systems other than UNIX and Windows, this is an identity map.
960    #[cfg(not(any(unix, windows)))]
961    fn map_os_err(err: io::Error) -> io::Error {
962        err
963    }
964
965    /// Ensure the given range reads back as zeroes, or return an error.
966    async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
967        if offset >= self.size.load(Ordering::Relaxed) {
968            // Nothing to do
969            return Ok(());
970        }
971
972        if self.discard_unsupported.load(Ordering::Relaxed) {
973            Err(io::ErrorKind::Unsupported.into())
974        } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
975            if err.kind() == io::ErrorKind::Unsupported {
976                self.discard_unsupported.store(true, Ordering::Relaxed);
977            }
978            Err(err)
979        } else {
980            Ok(())
981        }
982    }
983
984    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
985    #[cfg(target_os = "linux")]
986    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
987        let offset: libc::off_t = offset
988            .try_into()
989            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
990        let length: libc::off_t = length
991            .try_into()
992            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
993
994        let file = self.file.read();
995        // Safe: File descriptor is valid, and the rest are simple integer parameters.
996        while_eintr(|| unsafe {
997            libc::fallocate(
998                file.as_raw_fd(),
999                libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
1000                offset,
1001                length,
1002            )
1003        })
1004        .map_err(Self::map_os_err)?;
1005
1006        Ok(())
1007    }
1008
1009    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
1010    #[cfg(windows)]
1011    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1012        let offset: i64 = offset
1013            .try_into()
1014            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
1015        let length: i64 = length
1016            .try_into()
1017            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1018
1019        let end = offset.saturating_add(length);
1020        let params = FILE_ZERO_DATA_INFORMATION {
1021            FileOffset: offset,
1022            BeyondFinalZero: end,
1023        };
1024        let mut _returned = 0;
1025        let file = self.file.read();
1026        // Safe: File handle is valid, mandatory pointers (input, returned length) are passed and
1027        // valid, the parameter type matches the call, and the input size matches the object
1028        // passed.
1029        let ret = unsafe {
1030            DeviceIoControl(
1031                file.as_raw_handle(),
1032                FSCTL_SET_ZERO_DATA,
1033                (&params as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
1034                size_of_val(&params) as u32,
1035                std::ptr::null_mut(),
1036                0,
1037                &mut _returned,
1038                std::ptr::null_mut(),
1039            )
1040        };
1041        if ret == 0 {
1042            return Err(Self::map_os_err(io::Error::last_os_error()));
1043        }
1044
1045        Ok(())
1046    }
1047
1048    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
1049    #[cfg(target_os = "macos")]
1050    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1051        let offset: libc::off_t = offset
1052            .try_into()
1053            .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
1054        let length: libc::off_t = length
1055            .try_into()
1056            .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1057
1058        let params = libc::fpunchhole_t {
1059            fp_flags: 0,
1060            reserved: 0,
1061            fp_offset: offset,
1062            fp_length: length,
1063        };
1064        let file = self.file.read();
1065        // Safe: FD is valid, passed pointer is valid and its type matches the call.
1066        while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, &params) })
1067            .map_err(Self::map_os_err)?;
1068
1069        Ok(())
1070    }
1071
1072    /// Via OS-specific means, ensure the given range reads back as zeroes, or return an error.
1073    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1074    async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1075        Err(io::ErrorKind::Unsupported.into())
1076    }
1077}
1078
1079impl Display for File {
1080    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1081        if let Some(filename) = self.filename.as_ref() {
1082            write!(f, "file:{filename:?}")
1083        } else {
1084            write!(f, "file:<unknown path>")
1085        }
1086    }
1087}
1088
1089/// Get total size in bytes of the given file.
1090///
1091/// If the file is a block or character device, use get_device_size() instead of
1092/// reading len from metadata which doesn't work on some platforms like macOS.
1093fn get_file_size(file: &fs::File) -> io::Result<u64> {
1094    #[allow(clippy::bind_instead_of_map)]
1095    file.metadata().and_then(|m| {
1096        #[cfg(unix)]
1097        if m.file_type().is_block_device() || m.file_type().is_char_device() {
1098            return get_device_size(file);
1099        }
1100        Ok(m.len())
1101    })
1102}
1103
1104cfg_if! {
1105    if #[cfg(target_os = "linux")] {
1106        /// Get total size in bytes of the given block or character device.
1107        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1108            let mut size = 0;
1109            unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1110            Ok(size)
1111        }
1112    } else if #[cfg(target_os = "macos")] {
1113        /// Get total size in bytes of the given block or character device.
1114        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1115            let mut block_size = 0;
1116            unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1117            let mut block_count = 0;
1118            unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1119            Ok(u64::from(block_size) * block_count)
1120        }
1121    } else if #[cfg(target_os = "freebsd")] {
1122        /// Get total size in bytes of the given block or character device.
1123        fn get_device_size(file: &fs::File) -> io::Result<u64> {
1124            let mut size = 0;
1125            unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1126            Ok(size as u64)
1127        }
1128    } else if #[cfg(unix)] {
1129        /// Get total size in bytes of the given block or character device - unsupported platform.
1130        fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1131            Err(io::ErrorKind::Unsupported.into())
1132        }
1133    }
1134}
1135
1136/// This module generates type-safe wrappers for chosen ioctls
1137mod ioctl {
1138    #[cfg(unix)]
1139    use nix::ioctl_read;
1140    #[cfg(target_os = "linux")]
1141    use nix::ioctl_read_bad;
1142
1143    // https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h#L200
1144
1145    #[cfg(target_os = "linux")]
1146    ioctl_read!(blkgetsize64, 0x12, 114, u64);
1147
1148    #[cfg(target_os = "linux")]
1149    ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1150
1151    // https://github.com/apple-oss-distributions/xnu/blob/main/bsd/sys/disk.h#L198-L199
1152
1153    #[cfg(target_os = "macos")]
1154    ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1155
1156    #[cfg(target_os = "macos")]
1157    ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1158
1159    // https://web.mit.edu/freebsd/head/sys/sys/disk.h
1160
1161    #[cfg(target_os = "freebsd")]
1162    ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1163
1164    #[cfg(target_os = "freebsd")]
1165    ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170    use super::File;
1171    use crate::{Storage, StorageCreateOptions, StorageExt};
1172    use std::path::PathBuf;
1173    #[cfg(unix)]
1174    use std::time::{SystemTime, UNIX_EPOCH};
1175    use std::{cmp, env, fs, io};
1176
1177    #[cfg(windows)]
1178    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1179    async fn zero_data_preserves_the_byte_at_the_exclusive_end() {
1180        let path = test_dir().join(format!(
1181            "imago-zero-data-boundary-{}-{}",
1182            std::process::id(),
1183            std::time::SystemTime::now()
1184                .duration_since(std::time::UNIX_EPOCH)
1185                .unwrap()
1186                .as_nanos()
1187        ));
1188        fs::write(&path, vec![0x5a; 12288]).unwrap();
1189        let host = fs::OpenOptions::new()
1190            .read(true)
1191            .write(true)
1192            .open(&path)
1193            .unwrap();
1194        let storage = File::try_from(host).unwrap();
1195        let zero_result = storage.discard_to_zero_os_specific(4096, 4096).await;
1196        drop(storage);
1197
1198        let contents = fs::read(&path);
1199        let _ = fs::remove_file(&path);
1200        if zero_result
1201            .as_ref()
1202            .is_err_and(|err| err.kind() == io::ErrorKind::Unsupported)
1203        {
1204            // Nothing to test (e.g. on Wine)
1205            return;
1206        }
1207        zero_result.unwrap();
1208        let contents = contents.unwrap();
1209        assert_eq!(contents[4095], 0x5a);
1210        assert!(contents[4096..8192].iter().all(|byte| *byte == 0));
1211        assert_eq!(contents[8192], 0x5a);
1212    }
1213
1214    #[cfg(windows)]
1215    #[test]
1216    fn map_os_err_unsupported() {
1217        use windows_sys::Win32::Foundation::{
1218            ERROR_ACCESS_DENIED, ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED,
1219        };
1220
1221        for code in [ERROR_NOT_SUPPORTED, ERROR_INVALID_FUNCTION] {
1222            let err = File::map_os_err(io::Error::from_raw_os_error(code as i32));
1223            assert_eq!(err.kind(), io::ErrorKind::Unsupported);
1224        }
1225
1226        // Other errors must keep their kind
1227        let err = File::map_os_err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as i32));
1228        assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
1229    }
1230
1231    #[cfg(unix)]
1232    #[test]
1233    fn buffered_writable_file_is_not_modified_during_construction() {
1234        let unique = SystemTime::now()
1235            .duration_since(UNIX_EPOCH)
1236            .unwrap()
1237            .as_nanos();
1238        let path = test_dir().join(format!(
1239            "imago-buffered-probe-{}-{unique}",
1240            std::process::id()
1241        ));
1242        let file = fs::OpenOptions::new()
1243            .read(true)
1244            .write(true)
1245            .create_new(true)
1246            .open(&path)
1247            .unwrap();
1248
1249        let storage = File::try_from(file).unwrap();
1250        let alignments = (
1251            storage.req_align,
1252            storage.mem_align,
1253            storage.zero_align,
1254            storage.discard_align,
1255        );
1256        let len = fs::metadata(&path).unwrap().len();
1257        let expected_fs_align = {
1258            let probe = fs::File::open(&path).unwrap();
1259            super::File::get_fs_zero_discard_align(&probe)
1260        };
1261        drop(storage);
1262        fs::remove_file(path).unwrap();
1263
1264        assert_eq!(alignments, (1, 1, expected_fs_align.0, expected_fs_align.1));
1265        assert_eq!(len, 0);
1266    }
1267
1268    /// Directory for test files.
1269    ///
1270    /// Taken from the `TEST_DIR` environment variable, falling back to [`env::temp_dir()`].  The
1271    /// latter is often a tmpfs, which does not support all operations (e.g. `fallocate()` with
1272    /// `FALLOC_FL_ZERO_RANGE`), so some code paths are only tested with fallbacks there.
1273    fn test_dir() -> PathBuf {
1274        env::var_os("TEST_DIR")
1275            .map(PathBuf::from)
1276            .unwrap_or_else(env::temp_dir)
1277    }
1278
1279    struct TempPath(PathBuf);
1280
1281    impl Drop for TempPath {
1282        fn drop(&mut self) {
1283            let _ = std::fs::remove_file(&self.0);
1284        }
1285    }
1286
1287    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
1288    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1289    async fn tail_discard_keeps_file_length() -> io::Result<()> {
1290        let path = test_dir().join(format!("imago-tail-discard-{}.raw", std::process::id()));
1291        let _temp_path = TempPath(path.clone());
1292
1293        let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1294        // To actually exercise the discard path, use the reported discard alignment as the test
1295        // granularity (at least 4096).
1296        let granularity = cmp::max(file.discard_align(), 4096) as u64;
1297        file.write(&vec![0xabu8; 2 * granularity as usize][..], 0)
1298            .await?;
1299        assert_eq!(std::fs::metadata(&path)?.len(), 2 * granularity);
1300
1301        file.discard(granularity, granularity).await?;
1302        assert_eq!(std::fs::metadata(&path)?.len(), 2 * granularity);
1303
1304        let mut prefix = vec![0u8; granularity as usize];
1305        file.read(&mut prefix, 0).await?;
1306        assert_eq!(prefix, vec![0xabu8; granularity as usize]);
1307
1308        // If discarding is not supported (e.g. on Wine), it is a no-op
1309        if !file
1310            .discard_unsupported
1311            .load(std::sync::atomic::Ordering::Relaxed)
1312        {
1313            let mut tail = vec![0xffu8; granularity as usize];
1314            file.read(&mut tail, granularity).await?;
1315            assert!(tail.iter().all(|&byte| byte == 0));
1316        }
1317
1318        io::Result::Ok(())
1319    }
1320
1321    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1322    async fn write_allocated_zeroes_past_eof_updates_size() -> io::Result<()> {
1323        let path = test_dir().join(format!(
1324            "imago-allocated-zeroes-past-eof-{}.raw",
1325            std::process::id()
1326        ));
1327        let _temp_path = TempPath(path.clone());
1328
1329        let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1330        // To actually exercise the efficient zero-write path, use the reported zero alignment as
1331        // the test granularity (at least 4096).
1332        let granularity = cmp::max(file.zero_align(), 4096) as u64;
1333
1334        file.write(&vec![0xabu8; granularity as usize][..], 0)
1335            .await?;
1336        assert_eq!(fs::metadata(&path)?.len(), granularity);
1337        assert_eq!(file.size()?, granularity);
1338
1339        file.write_allocated_zeroes(granularity, granularity)
1340            .await?;
1341        assert_eq!(fs::metadata(&path)?.len(), 2 * granularity);
1342        assert_eq!(file.size()?, 2 * granularity);
1343
1344        let mut tail = vec![0xffu8; granularity as usize];
1345        file.read(&mut tail, granularity).await?;
1346        assert!(tail.iter().all(|&byte| byte == 0));
1347
1348        io::Result::Ok(())
1349    }
1350
1351    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1352    async fn write_zeroes_past_eof_grows_file() -> io::Result<()> {
1353        let path = test_dir().join(format!(
1354            "imago-write-zeroes-past-eof-{}.raw",
1355            std::process::id()
1356        ));
1357        let _temp_path = TempPath(path.clone());
1358
1359        let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1360        // To actually exercise the efficient zero-write path, use the reported zero alignment as
1361        // the test granularity (at least 4096).
1362        let granularity = cmp::max(file.zero_align(), 4096) as u64;
1363        file.write(&vec![0xabu8; granularity as usize][..], 0)
1364            .await?;
1365        assert_eq!(fs::metadata(&path)?.len(), granularity);
1366        assert_eq!(file.size()?, granularity);
1367
1368        // Range starting at EOF
1369        file.write_zeroes(granularity, granularity).await?;
1370        assert_eq!(fs::metadata(&path)?.len(), 2 * granularity);
1371        assert_eq!(file.size()?, 2 * granularity);
1372
1373        let mut buf = vec![0u8; 2 * granularity as usize];
1374        file.read(&mut buf, 0).await?;
1375        assert!(buf[..granularity as usize].iter().all(|&byte| byte == 0xab));
1376        assert!(buf[granularity as usize..].iter().all(|&byte| byte == 0));
1377
1378        // Range crossing EOF
1379        file.write(&vec![0xcdu8; granularity as usize][..], granularity)
1380            .await?;
1381        file.write_zeroes(granularity, 2 * granularity).await?;
1382        assert_eq!(fs::metadata(&path)?.len(), 3 * granularity);
1383        assert_eq!(file.size()?, 3 * granularity);
1384
1385        let mut buf = vec![0xffu8; 3 * granularity as usize];
1386        file.read(&mut buf, 0).await?;
1387        assert!(buf[..granularity as usize].iter().all(|&byte| byte == 0xab));
1388        assert!(buf[granularity as usize..].iter().all(|&byte| byte == 0));
1389
1390        io::Result::Ok(())
1391    }
1392
1393    /// Reading past the actual end of file must work even if `File::size` is larger.
1394    #[cfg(windows)]
1395    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1396    async fn read_past_external_truncation() -> io::Result<()> {
1397        use crate::StorageOpenOptions;
1398        use std::sync::atomic::{AtomicBool, Ordering};
1399        use std::sync::Arc;
1400        use std::time::Duration;
1401
1402        let path = test_dir().join(format!(
1403            "imago-read-past-external-truncation-{}.raw",
1404            std::process::id()
1405        ));
1406        let _temp_path = TempPath(path.clone());
1407        std::fs::write(&path, vec![0xabu8; 8192])?;
1408
1409        let file = File::open(StorageOpenOptions::new().filename(&path)).await?;
1410        // Truncate behind `file`'s back, so its cached size is too large
1411        fs::OpenOptions::new()
1412            .write(true)
1413            .open(&path)?
1414            .set_len(4096)?;
1415
1416        // When broken, reading spins forever; abort instead of hanging
1417        let done = Arc::new(AtomicBool::new(false));
1418        let watchdog_done = Arc::clone(&done);
1419        std::thread::spawn(move || {
1420            std::thread::sleep(Duration::from_secs(10));
1421            if !watchdog_done.load(Ordering::Relaxed) {
1422                eprintln!("read_past_external_truncation: Read hangs, aborting");
1423                std::process::abort();
1424            }
1425        });
1426
1427        let mut buf = vec![0xffu8; 8192];
1428        file.read(&mut buf[..], 0).await?;
1429        done.store(true, Ordering::Relaxed);
1430
1431        assert!(buf[..4096].iter().all(|&byte| byte == 0xab));
1432        assert!(buf[4096..].iter().all(|&byte| byte == 0));
1433
1434        io::Result::Ok(())
1435    }
1436}