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