Skip to main content

dolang_vfs/
file.rs

1//! File-specific values shared by VFS implementations.
2
3use std::{
4    future::Future,
5    mem::MaybeUninit,
6    pin::Pin,
7    sync::atomic::Ordering,
8    task::{Context, Poll},
9};
10
11use bytes::{Bytes, BytesMut};
12use dolang_winterop::security::SecDesc;
13use serde::{Deserialize, Serialize};
14
15use crate::{
16    client, direct,
17    error::{Error, ErrorKind, Result},
18    metadata::{FsMetadata, Metadata},
19    path,
20    security::{Acl, AclKind},
21};
22
23mod copy;
24mod cursor;
25
26pub(crate) use copy::{COPY_LIMIT, FileId};
27pub(crate) use cursor::WriteOp;
28
29/// Selects an extended-attribute namespace when listing attributes.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum XattrNamespace<'a> {
32    /// The target's default namespace.
33    Default,
34    /// One named target-specific namespace.
35    Named(&'a str),
36    /// Every namespace supported by the target.
37    Any,
38}
39
40/// Describes one extended attribute without reading its value.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct XattrEntry {
43    /// Attribute name within its namespace.
44    pub(crate) name: String,
45    /// Namespace, when the target reports one separately.
46    pub(crate) namespace: Option<String>,
47    /// Value size, when available without reading it.
48    pub(crate) size: Option<u64>,
49    /// Target-specific attribute flags.
50    pub(crate) flags: Option<u8>,
51}
52
53impl XattrEntry {
54    /// Returns the attribute name without its namespace prefix.
55    pub fn name(&self) -> &str {
56        &self.name
57    }
58    /// Returns the attribute namespace, if one was reported separately.
59    pub fn namespace(&self) -> Option<&str> {
60        self.namespace.as_deref()
61    }
62    /// Returns the attribute value size in bytes, if available.
63    pub const fn size(&self) -> Option<u64> {
64        self.size
65    }
66    /// Returns the platform-specific attribute flags, if available.
67    pub const fn flags(&self) -> Option<u8> {
68        self.flags
69    }
70}
71
72/// Describes one alternate data stream associated with a file.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct StreamEntry {
75    /// Stream name.
76    pub(crate) name: String,
77    /// Stream type reported by the target.
78    pub(crate) r#type: String,
79    /// Logical stream length in bytes.
80    pub(crate) size: u64,
81    /// Allocated stream size in bytes.
82    pub(crate) alloc_size: u64,
83}
84
85impl StreamEntry {
86    /// Returns the stream name.
87    pub fn name(&self) -> &str {
88        &self.name
89    }
90    /// Returns the stream type reported by the target.
91    pub fn stream_type(&self) -> &str {
92        &self.r#type
93    }
94    /// Returns the logical stream length in bytes.
95    pub const fn size(&self) -> u64 {
96        self.size
97    }
98    /// Returns the allocated stream size in bytes.
99    pub const fn alloc_size(&self) -> u64 {
100        self.alloc_size
101    }
102}
103
104bitflags::bitflags! {
105    /// Permissions checked by [`Vfs::access`](crate::Vfs::access).
106    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
107    pub struct AccessFlags: i32 {
108        /// Checks execute permission.
109        const X_OK = 1;
110        /// Checks write permission.
111        const W_OK = 2;
112        /// Checks read permission.
113        const R_OK = 4;
114        /// Checks only whether the path exists.
115        const F_OK = 0;
116    }
117}
118
119/// Lock access requested for a byte range of a file.
120#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
121pub enum FileLockMode {
122    /// Prevents other exclusive or shared locks from overlapping this range.
123    Exclusive,
124    /// Allows other shared locks but not exclusive locks to overlap this range.
125    Shared,
126}
127
128/// Whether acquiring a file lock may wait.
129#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
130pub enum FileLockBehavior {
131    /// Waits until the lock can be acquired.
132    Blocking,
133    /// Returns without waiting when the lock cannot be acquired.
134    Try,
135}
136
137/// A half-open byte range used for a file lock.
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
139pub struct FileLockRange {
140    /// Inclusive byte offset at which the range starts.
141    pub(crate) start: u64,
142    /// Exclusive byte offset at which the range ends, or no end for EOF.
143    pub(crate) end: Option<u64>,
144}
145
146impl FileLockRange {
147    /// Creates a range from `start` to the exclusive `end`, or to EOF.
148    ///
149    /// Returns an error if `end` precedes `start`.
150    pub fn new(start: u64, end: Option<u64>) -> Result<Self> {
151        if end.is_some_and(|end| end < start) {
152            return Err(crate::error::Error::new(
153                crate::error::ErrorKind::InvalidInput,
154                "lock range end precedes its start",
155            ));
156        }
157        Ok(Self { start, end })
158    }
159
160    /// Creates a range extending from `start` to EOF.
161    pub const fn to_eof(start: u64) -> Self {
162        Self { start, end: None }
163    }
164    /// Returns the inclusive starting offset.
165    pub const fn start(self) -> u64 {
166        self.start
167    }
168    /// Returns the exclusive ending offset, or `None` for EOF.
169    pub const fn end(self) -> Option<u64> {
170        self.end
171    }
172    /// Returns whether this range contains no bytes.
173    pub fn is_empty(self) -> bool {
174        self.end == Some(self.start)
175    }
176
177    pub(crate) fn conflicts(self, other: Self) -> bool {
178        match (self.is_empty(), other.is_empty()) {
179            (true, true) => return false,
180            (true, false) => {
181                return other.start < self.start && self.start < other.end.unwrap_or(u64::MAX);
182            }
183            (false, true) => {
184                return self.start < other.start && other.start < self.end.unwrap_or(u64::MAX);
185            }
186            (false, false) => {}
187        }
188        self.start < other.end.unwrap_or(u64::MAX) && other.start < self.end.unwrap_or(u64::MAX)
189    }
190}
191
192/// A complete request to acquire a file lock.
193#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
194pub(crate) struct FileLockRequest {
195    /// Byte range to lock.
196    pub(crate) range: FileLockRange,
197    /// Access mode to acquire.
198    pub(crate) mode: FileLockMode,
199    /// Whether acquisition may block.
200    pub(crate) behavior: FileLockBehavior,
201}
202
203impl FileLockRequest {
204    pub(crate) const fn new(
205        range: FileLockRange,
206        mode: FileLockMode,
207        behavior: FileLockBehavior,
208    ) -> Self {
209        Self {
210            range,
211            mode,
212            behavior,
213        }
214    }
215}
216
217/// A held file lock released explicitly or when dropped.
218pub struct FileLock {
219    inner: Option<FileLockInner>,
220}
221
222enum FileLockInner {
223    Direct(direct::FileLock),
224    Remote(client::FileLock),
225}
226
227impl FileLock {
228    pub(crate) fn direct(lock: direct::FileLock) -> Self {
229        Self {
230            inner: Some(FileLockInner::Direct(lock)),
231        }
232    }
233
234    pub(crate) fn remote(lock: client::FileLock) -> Self {
235        Self {
236            inner: Some(FileLockInner::Remote(lock)),
237        }
238    }
239
240    /// Releases the lock. Calling this after a successful release is a no-op.
241    pub async fn release(&mut self) -> Result<()> {
242        let Some(lock) = self.inner.as_mut() else {
243            return Ok(());
244        };
245        let result = match lock {
246            FileLockInner::Direct(lock) => lock.release().await,
247            FileLockInner::Remote(lock) => lock.release().await,
248        };
249        if result.is_ok() {
250            self.inner = None;
251        }
252        result
253    }
254}
255
256impl std::fmt::Debug for FileLock {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        f.debug_struct("FileLock")
259            .field("released", &self.inner.is_none())
260            .finish()
261    }
262}
263/// Whether a copy may — or must — share blocks with its source rather than
264/// duplicating them.
265///
266/// The distinction is not observable in the byte content either way, which is
267/// why it needs asking for explicitly: a caller copying for deduplication has
268/// no other way to learn whether it got sharing or a full duplicate.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
270pub enum CopyMode {
271    /// Share blocks when the platform and both files allow it, and copy the
272    /// data outright when they do not.
273    Auto,
274    /// Fail rather than copy the data outright.
275    Require,
276    /// Copy the data outright even where sharing is available.
277    Never,
278}
279
280/// Where a copy's bytes land in the destination.
281///
282/// Not a bare offset: an append-mode handle has no offset to name — both
283/// backends refuse a positional write on one, and the position is chosen by
284/// the platform at write time — yet copying to the current position of such a
285/// handle is perfectly meaningful.
286#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
287pub enum CopyDest {
288    /// At an absolute offset.
289    At(u64),
290    /// Through [`File::append`], which places bytes at the end of a handle
291    /// opened for appending. Like `append` itself, this belongs to such a
292    /// handle: on any other one it writes wherever the platform's own position
293    /// happens to be.
294    Append,
295}
296
297/// Result of one bounded [`File::copy_data`] operation.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub struct CopyDataResult {
300    /// Number of logical bytes copied, including preserved holes.
301    pub count: u64,
302    /// Actual destination end for an append copy.
303    pub destination_end: Option<u64>,
304}
305
306#[derive(Debug, Clone)]
307pub(crate) enum FileInner {
308    Client(client::File),
309    Direct(direct::File),
310}
311
312/// An asynchronous file handle backed by either a remote VFS client or the local process.
313///
314/// File handles implement Tokio's asynchronous read, write, and seek traits.
315///
316/// Clones share one open file, as duplicated descriptors do: one cursor
317/// position, one set of locks, and one closed state. Closing or handing off
318/// the file through any clone closes it for all of them, and their later
319/// operations fail with [`ErrorKind::Closed`]. Operations in flight are not
320/// shared: a clone starts with none, and its reads and writes do not wait for
321/// another clone's.
322#[derive(Debug)]
323pub struct File {
324    pub(crate) inner: FileInner,
325    cursor: Option<Box<cursor::Cursor>>,
326}
327
328impl Clone for File {
329    fn clone(&self) -> Self {
330        Self::with_cursor(self.inner.clone(), None)
331    }
332}
333
334impl File {
335    pub(crate) fn client(file: client::File) -> Self {
336        Self::with_cursor(FileInner::Client(file), None)
337    }
338
339    pub(crate) fn direct(file: direct::File) -> Self {
340        Self::with_cursor(FileInner::Direct(file), None)
341    }
342
343    fn with_cursor(inner: FileInner, cursor: Option<Box<cursor::Cursor>>) -> Self {
344        Self { inner, cursor }
345    }
346}
347
348/// One of two futures, chosen at dispatch time.
349///
350/// The positional operations return `impl Future`, so a backend that dispatches
351/// between two implementations has two distinct future types to reconcile and
352/// cannot simply `match` inside an `async` block: the returned future captures
353/// no lifetimes, so it cannot borrow the handle it came from. Boxing would
354/// work; this avoids the allocation on what is meant to be the hot path.
355pub(crate) enum EitherFuture<L, R> {
356    Left(L),
357    Right(R),
358}
359
360impl<T, L: Future<Output = T>, R: Future<Output = T>> Future for EitherFuture<L, R> {
361    type Output = T;
362
363    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
364        // SAFETY: the projection is structural and neither variant is ever
365        // moved out of, so the pinning guarantee carries through to whichever
366        // future is inside.
367        unsafe {
368            match self.get_unchecked_mut() {
369                Self::Left(future) => Pin::new_unchecked(future).poll(cx),
370                Self::Right(future) => Pin::new_unchecked(future).poll(cx),
371            }
372        }
373    }
374}
375
376macro_rules! match_file {
377    (move $self:expr, $file:ident => $body:expr) => {{
378        match $self.inner {
379            FileInner::Client($file) => $body,
380            FileInner::Direct($file) => $body,
381        }
382    }};
383    ($self:expr, $file:ident => $body:expr) => {{
384        match &$self.inner {
385            FileInner::Client($file) => $body,
386            FileInner::Direct($file) => $body,
387        }
388    }};
389}
390
391impl File {
392    /// Closes the file, for every clone. Closing a closed file does nothing.
393    pub async fn close(mut self) -> Result<()> {
394        self.cancel_cursor().await;
395        match_file!(move self, file => file.close().await)
396    }
397
398    /// Reads into `buf`'s spare capacity starting at `offset`, returning the
399    /// transfer count.
400    ///
401    /// Cancelling the read may leave `buf` empty.
402    ///
403    /// Fewer bytes than the spare capacity available may be read. `0` indicates
404    /// either end-of-file or that `buf` had no spare capacity.
405    pub fn read_at<'b>(
406        &self,
407        buf: &'b mut BytesMut,
408        offset: u64,
409    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
410        match &self.inner {
411            FileInner::Client(file) => EitherFuture::Left(file.read_at(buf, offset)),
412            FileInner::Direct(file) => EitherFuture::Right(file.read_at(buf, offset)),
413        }
414    }
415
416    /// Writes `data` at `offset`, returning the byte count.
417    ///
418    /// May write less than all of `data` on success, but at least 1 byte unless `data` is empty.
419    /// Use on an append-mode handle is an error; use [`append`](Self::append) there.
420    pub fn write_at(
421        &self,
422        data: Bytes,
423        offset: u64,
424    ) -> impl Future<Output = Result<usize>> + Send + use<> {
425        match &self.inner {
426            FileInner::Client(file) => EitherFuture::Left(file.write_at(data, offset)),
427            FileInner::Direct(file) => EitherFuture::Right(file.write_at(data, offset)),
428        }
429    }
430
431    /// Appends `data`, returning the byte count and the position just past
432    /// what was written.  Less data may be written than requested on success,
433    /// but at least 1 byte will be written unless `data` was empty.
434    pub fn append(&self, data: Bytes) -> impl Future<Output = Result<(usize, u64)>> + Send + use<> {
435        match &self.inner {
436            FileInner::Client(file) => EitherFuture::Left(file.append(data)),
437            FileInner::Direct(file) => EitherFuture::Right(file.append(data)),
438        }
439    }
440
441    /// Reads into the possibly uninitialized `buf``, returning how
442    /// many bytes at its front were filled.
443    pub fn read_at_into<'b>(
444        &self,
445        buf: &'b mut [MaybeUninit<u8>],
446        offset: u64,
447    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
448        match &self.inner {
449            FileInner::Client(file) => EitherFuture::Left(file.read_at_into(buf, offset)),
450            FileInner::Direct(file) => EitherFuture::Right(file.read_at_into(buf, offset)),
451        }
452    }
453
454    /// Writes `data` at `offset` from borrowed storage, returning the byte
455    /// count.
456    ///
457    /// Less data may be written than requested on success, but always at least 1
458    /// byte unless `data` is empty.
459    pub fn write_at_from<'b>(
460        &self,
461        data: &'b [u8],
462        offset: u64,
463    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
464        match &self.inner {
465            FileInner::Client(file) => EitherFuture::Left(file.write_at_from(data, offset)),
466            FileInner::Direct(file) => EitherFuture::Right(file.write_at_from(data, offset)),
467        }
468    }
469
470    /// Copies at most 2 MiB from this file into `dst`.
471    ///
472    /// `len` of `None` copies toward end of source. A positive result may be
473    /// short for any reason; callers that want a complete operation must loop.
474    /// Zero indicates end of source (or an empty request).
475    ///
476    /// Nothing is promised about physical allocation: an accelerated route may
477    /// preserve holes, and the fallback does not.
478    pub async fn copy_data(
479        &self,
480        dst: &File,
481        src_offset: u64,
482        target: CopyDest,
483        len: Option<u64>,
484        mode: CopyMode,
485    ) -> Result<CopyDataResult> {
486        self.check_overlap(dst, src_offset, target, len).await?;
487        let len = len.map(|len| len.min(copy::COPY_LIMIT));
488        match (&self.inner, &dst.inner) {
489            // `Never` still takes this route: it forbids block sharing, not
490            // running the copy on the side that owns both files.
491            (FileInner::Client(src), FileInner::Client(dst)) if src.can_copy_data_with(dst) => {
492                src.copy_data(dst, src_offset, target, len, mode).await
493            }
494            #[cfg(any(
495                target_os = "linux",
496                target_os = "freebsd",
497                target_os = "macos",
498                windows
499            ))]
500            (FileInner::Direct(src_direct), FileInner::Direct(dst_direct))
501                if matches!(target, CopyDest::At(_)) =>
502            {
503                if src_direct.is_regular().await? && dst_direct.is_regular().await? {
504                    src_direct
505                        .copy_data(dst_direct, src_offset, target, len, mode)
506                        .await
507                } else if mode == CopyMode::Require {
508                    Err(Error::new(
509                        ErrorKind::Unsupported,
510                        "block sharing is not supported for this copy",
511                    ))
512                } else {
513                    copy::copy_chunked(self, dst, src_offset, target, len).await
514                }
515            }
516            _ => {
517                // The refusal sits here, immediately above the fallback,
518                // rather than at the top of the function: once a platform
519                // route exists it is inserted above this point and the refusal
520                // becomes "sharing was attempted and could not be had", with
521                // nothing else to re-plumb.
522                if mode == CopyMode::Require {
523                    return Err(Error::new(
524                        ErrorKind::Unsupported,
525                        "block sharing is not supported for this copy",
526                    ));
527                }
528                copy::copy_chunked(self, dst, src_offset, target, len).await
529            }
530        }
531    }
532
533    /// Rejects a copy whose source and destination regions overlap within one
534    /// file.
535    ///
536    /// Best effort by necessity: identity is knowable for two local handles
537    /// and for two citations of one opaque file, but a mixed local/opaque pair
538    /// — which one session can hand out, since `Open` may answer with either —
539    /// has nothing cheap in common to compare. An unknown identity is treated
540    /// as "cannot tell" and lets the copy proceed; a same-session opaque pair
541    /// with distinct citations is checked on the server, where both handles
542    /// are local.
543    async fn check_overlap(
544        &self,
545        dst: &File,
546        src_offset: u64,
547        target: CopyDest,
548        len: Option<u64>,
549    ) -> Result<()> {
550        let same = copy::same_opaque(self, dst)
551            || match (copy::identity(self).await, copy::identity(dst).await) {
552                (Some(src), Some(dst)) => src == dst,
553                _ => false,
554            };
555        if !same {
556            return Ok(());
557        }
558        // Only now, when the regions are known to share a file, is the length
559        // worth a round trip: an open-ended copy runs to end of source, and an
560        // append lands there too.
561        let size = match (len, target) {
562            (Some(_), CopyDest::At(_)) => 0,
563            _ => self.metadata().await?.len(),
564        };
565        let src_end = match len {
566            Some(len) => src_offset.saturating_add(len),
567            None => size.max(src_offset),
568        };
569        let dst_start = match target {
570            CopyDest::At(offset) => offset,
571            CopyDest::Append => size,
572        };
573        let dst_end = dst_start.saturating_add(src_end - src_offset);
574        if dst_start < src_end && src_offset < dst_end {
575            return Err(Error::new(
576                ErrorKind::InvalidInput,
577                "source and destination regions of the same file overlap",
578            ));
579        }
580        Ok(())
581    }
582
583    /// Changes the file length to `size` bytes.
584    pub async fn set_size(&self, size: u64) -> Result<()> {
585        match_file!(self, file => file.set_size(size).await)
586    }
587
588    /// Flushes the file's written data to durable storage, returning once the
589    /// device reports it committed.
590    ///
591    /// `data` selects a data-only flush (`fdatasync`), which may skip metadata
592    /// the caller does not need — notably the modification time — and so can
593    /// avoid a second write to the inode. Size changes are still flushed,
594    /// since a reader could not find the data without them.
595    ///
596    /// This durably places the *contents*. It says nothing about the
597    /// directory entry naming the file, which is a separate inode and needs
598    /// its own flush to survive a crash.
599    pub async fn sync(&self, data: bool) -> Result<()> {
600        match_file!(self, file => file.sync(data).await)
601    }
602
603    /// Returns metadata for the open file.
604    pub async fn metadata(&self) -> Result<Metadata> {
605        match_file!(self, file => file.metadata().await)
606    }
607
608    /// Returns metadata for the filesystem containing the open file.
609    pub async fn fs_metadata(&self) -> Result<FsMetadata> {
610        match_file!(self, file => file.fs_metadata().await)
611    }
612
613    /// Returns the ACL of the requested `kind`. For a POSIX ACL, `default`
614    /// selects the directory's default ACL rather than its access ACL; it
615    /// must be `false` for `AclKind::Nfs4`.
616    pub async fn acl(&self, kind: AclKind, default: bool) -> Result<Option<Acl>> {
617        match_file!(self, file => file.acl(kind, default).await)
618    }
619
620    /// Sets or removes the ACL of `kind`. `acl`, if present, must match
621    /// `kind`. `default` selects the POSIX default ACL, as in
622    /// [`acl`](Self::acl); it must be `false` for `AclKind::Nfs4`.
623    pub async fn set_acl(&self, kind: AclKind, acl: Option<&Acl>, default: bool) -> Result<()> {
624        match_file!(self, file => file.set_acl(kind, acl, default).await)
625    }
626
627    /// Returns the Windows security descriptor selected by `mask`.
628    pub async fn sec_desc(&self, mask: dolang_winterop::security::SecInfo) -> Result<SecDesc> {
629        match_file!(self, file => file.sec_desc(mask).await)
630    }
631
632    /// Replaces the Windows security descriptor.
633    pub async fn update_sec_desc(&self, sec_desc: &SecDesc) -> Result<()> {
634        match_file!(self, file => file.update_sec_desc(sec_desc).await)
635    }
636
637    /// Lists extended attributes in `namespace`.
638    pub async fn xattrs(&self, namespace: XattrNamespace<'_>) -> Result<Vec<XattrEntry>> {
639        match_file!(self, file => file.xattrs(namespace).await)
640    }
641
642    /// Reads one extended attribute.
643    pub async fn xattr(&self, name: &str, namespace: Option<&str>) -> Result<Vec<u8>> {
644        match_file!(self, file => file.xattr(name, namespace).await)
645    }
646
647    /// Lists alternate data streams.
648    pub async fn streams(&self) -> Result<Vec<StreamEntry>> {
649        match_file!(self, file => file.streams().await)
650    }
651
652    /// Creates or replaces an extended attribute.
653    pub async fn set_xattr(&self, name: &str, namespace: Option<&str>, value: &[u8]) -> Result<()> {
654        match_file!(self, file => file.set_xattr(name, namespace, value).await)
655    }
656
657    /// Removes an extended attribute.
658    pub async fn remove_xattr(&self, name: &str, namespace: Option<&str>) -> Result<()> {
659        match_file!(self, file => file.remove_xattr(name, namespace).await)
660    }
661
662    /// Acquires a byte-range lock with the requested mode and behavior.
663    pub async fn lock(
664        &self,
665        range: FileLockRange,
666        mode: FileLockMode,
667        behavior: FileLockBehavior,
668    ) -> Result<Option<FileLock>> {
669        let request = FileLockRequest::new(range, mode, behavior);
670        match_file!(self, file => file.lock(request).await)
671    }
672
673    /// Converts this handle into a local standard-library file when possible,
674    /// taking it from every clone.
675    ///
676    /// Surrendering the descriptor means surrendering the cursor with it, so
677    /// the file comes back positioned where this handle is.
678    pub async fn try_into_std(self) -> std::result::Result<std::fs::File, Self> {
679        if self.check_idle().is_err() {
680            return Err(self);
681        }
682        let pos = self.inner.position().load(Ordering::Relaxed);
683        let File { inner, cursor } = self;
684        match inner {
685            FileInner::Client(file) => file
686                .try_into_std()
687                .await
688                .map_err(|file| Self::with_cursor(FileInner::Client(file), cursor)),
689            FileInner::Direct(file) => file
690                .try_into_std(pos)
691                .await
692                .map_err(|file| Self::with_cursor(FileInner::Direct(file), cursor)),
693        }
694    }
695}
696
697/// Configures and opens a file on one [`Vfs`] backend.
698enum OpenOptionsInner<'a> {
699    Client(client::OpenOptions<'a>),
700    Direct(direct::OpenOptions),
701}
702
703pub struct OpenOptions<'a> {
704    inner: OpenOptionsInner<'a>,
705}
706
707impl<'a> OpenOptions<'a> {
708    pub(crate) fn client(options: client::OpenOptions<'a>) -> Self {
709        Self {
710            inner: OpenOptionsInner::Client(options),
711        }
712    }
713
714    pub(crate) fn direct(options: direct::OpenOptions) -> Self {
715        Self {
716            inner: OpenOptionsInner::Direct(options),
717        }
718    }
719}
720
721impl OpenOptions<'_> {
722    /// Enables or disables read access.
723    pub fn read(&mut self, read: bool) -> &mut Self {
724        match &mut self.inner {
725            OpenOptionsInner::Client(opts) => {
726                opts.read(read);
727            }
728            OpenOptionsInner::Direct(opts) => {
729                opts.read(read);
730            }
731        }
732        self
733    }
734
735    /// Enables or disables write access.
736    pub fn write(&mut self, write: bool) -> &mut Self {
737        match &mut self.inner {
738            OpenOptionsInner::Client(opts) => {
739                opts.write(write);
740            }
741            OpenOptionsInner::Direct(opts) => {
742                opts.write(write);
743            }
744        }
745        self
746    }
747
748    /// Enables or disables append mode.
749    pub fn append(&mut self, append: bool) -> &mut Self {
750        match &mut self.inner {
751            OpenOptionsInner::Client(opts) => {
752                opts.append(append);
753            }
754            OpenOptionsInner::Direct(opts) => {
755                opts.append(append);
756            }
757        }
758        self
759    }
760
761    /// Enables or disables creation when the file is absent.
762    pub fn create(&mut self, create: bool) -> &mut Self {
763        match &mut self.inner {
764            OpenOptionsInner::Client(opts) => {
765                opts.create(create);
766            }
767            OpenOptionsInner::Direct(opts) => {
768                opts.create(create);
769            }
770        }
771        self
772    }
773
774    /// Enables or disables exclusive creation.
775    pub fn create_new(&mut self, create_new: bool) -> &mut Self {
776        match &mut self.inner {
777            OpenOptionsInner::Client(opts) => {
778                opts.create_new(create_new);
779            }
780            OpenOptionsInner::Direct(opts) => {
781                opts.create_new(create_new);
782            }
783        }
784        self
785    }
786
787    /// Enables or disables truncation when opening.
788    pub fn truncate(&mut self, truncate: bool) -> &mut Self {
789        match &mut self.inner {
790            OpenOptionsInner::Client(opts) => {
791                opts.truncate(truncate);
792            }
793            OpenOptionsInner::Direct(opts) => {
794                opts.truncate(truncate);
795            }
796        }
797        self
798    }
799
800    /// Enables or disables following the final path component when it is a link.
801    pub fn no_follow(&mut self, no_follow: bool) -> &mut Self {
802        match &mut self.inner {
803            OpenOptionsInner::Client(opts) => {
804                opts.no_follow(no_follow);
805            }
806            OpenOptionsInner::Direct(opts) => {
807                opts.no_follow(no_follow);
808            }
809        }
810        self
811    }
812
813    /// Opens `path` using the configured options.
814    pub async fn open(&self, path: path::Path<'_>) -> Result<File> {
815        match &self.inner {
816            OpenOptionsInner::Client(opts) => client::OpenOptions::open(opts, path).await,
817            OpenOptionsInner::Direct(opts) => direct::OpenOptions::open(opts, path)
818                .await
819                .map(File::direct),
820        }
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use super::FileLockRange;
827    use crate::error::ErrorKind;
828
829    #[test]
830    fn lock_range_construction_validates_order() {
831        let range = FileLockRange::new(4, Some(8)).unwrap();
832        assert_eq!(range.start(), 4);
833        assert_eq!(range.end(), Some(8));
834        assert!(!range.is_empty());
835        assert!(
836            FileLockRange::new(8, Some(4))
837                .is_err_and(|error| error.kind() == ErrorKind::InvalidInput)
838        );
839        assert_eq!(FileLockRange::to_eof(4).end(), None);
840    }
841}