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