Skip to main content

dolang_vfs/
file.rs

1//! File-specific values shared by VFS implementations.
2
3use std::{
4    future::Future,
5    io,
6    mem::MaybeUninit,
7    pin::Pin,
8    task::{Context, Poll},
9};
10
11use bytes::{Bytes, BytesMut};
12use dolang_winterop::security::SecDesc;
13use serde::{Deserialize, Serialize};
14use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
15
16use crate::{
17    client, direct,
18    error::{Error, ErrorKind, HandoffError, Result},
19    metadata::{FsMetadata, Metadata},
20    path,
21    process::{StdioRecv, StdioSend},
22    security::{Acl, AclKind},
23};
24
25mod copy;
26
27pub(crate) use copy::{COPY_LIMIT, FileId};
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/// An asynchronous file handle backed by either a remote [`Client`] or local [`Direct`].
307///
308/// File handles implement Tokio's asynchronous read, write, and seek traits.
309#[derive(Debug)]
310pub(crate) enum FileInner {
311    Client(client::File),
312    Direct(direct::File),
313}
314
315#[derive(Debug)]
316pub struct File {
317    pub(crate) inner: FileInner,
318}
319
320impl File {
321    pub(crate) fn client(file: client::File) -> Self {
322        Self {
323            inner: FileInner::Client(file),
324        }
325    }
326
327    pub(crate) fn direct(file: direct::File) -> Self {
328        Self {
329            inner: FileInner::Direct(file),
330        }
331    }
332}
333
334/// One of two futures, chosen at dispatch time.
335///
336/// The positional operations return `impl Future`, so a backend that dispatches
337/// between two implementations has two distinct future types to reconcile and
338/// cannot simply `match` inside an `async` block: the returned future captures
339/// no lifetimes, so it cannot borrow the handle it came from. Boxing would
340/// work; this avoids the allocation on what is meant to be the hot path.
341pub(crate) enum EitherFuture<L, R> {
342    Left(L),
343    Right(R),
344}
345
346impl<T, L: Future<Output = T>, R: Future<Output = T>> Future for EitherFuture<L, R> {
347    type Output = T;
348
349    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
350        // SAFETY: the projection is structural and neither variant is ever
351        // moved out of, so the pinning guarantee carries through to whichever
352        // future is inside.
353        unsafe {
354            match self.get_unchecked_mut() {
355                Self::Left(future) => Pin::new_unchecked(future).poll(cx),
356                Self::Right(future) => Pin::new_unchecked(future).poll(cx),
357            }
358        }
359    }
360}
361
362macro_rules! dispatch_file_mut {
363    ($self:expr, $method:ident($($arg:expr),* $(,)?)) => {{
364        match &mut $self.inner {
365            FileInner::Client(file) => Pin::new(file).$method($($arg),*),
366            FileInner::Direct(file) => Pin::new(file).$method($($arg),*),
367        }
368    }};
369}
370
371macro_rules! match_file {
372    (move $self:expr, $file:ident => $body:expr) => {{
373        match $self.inner {
374            FileInner::Client($file) => $body,
375            FileInner::Direct($file) => $body,
376        }
377    }};
378    ($self:expr, $file:ident => $body:expr) => {{
379        match &$self.inner {
380            FileInner::Client($file) => $body,
381            FileInner::Direct($file) => $body,
382        }
383    }};
384}
385
386impl AsyncRead for File {
387    fn poll_read(
388        mut self: Pin<&mut Self>,
389        cx: &mut Context<'_>,
390        buf: &mut ReadBuf<'_>,
391    ) -> Poll<io::Result<()>> {
392        dispatch_file_mut!(self.as_mut().get_mut(), poll_read(cx, buf))
393    }
394}
395
396impl AsyncWrite for File {
397    fn poll_write(
398        mut self: Pin<&mut Self>,
399        cx: &mut Context<'_>,
400        buf: &[u8],
401    ) -> Poll<io::Result<usize>> {
402        dispatch_file_mut!(self.as_mut().get_mut(), poll_write(cx, buf))
403    }
404
405    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
406        dispatch_file_mut!(self.as_mut().get_mut(), poll_flush(cx))
407    }
408
409    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
410        dispatch_file_mut!(self.as_mut().get_mut(), poll_shutdown(cx))
411    }
412}
413
414impl AsyncSeek for File {
415    fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> {
416        dispatch_file_mut!(self.as_mut().get_mut(), start_seek(position))
417    }
418
419    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
420        dispatch_file_mut!(self.as_mut().get_mut(), poll_complete(cx))
421    }
422}
423
424/// Puts a handle recovered from a failed handoff back in the wrapper it was
425/// dispatched out of.
426pub(crate) fn rewrap<I, O>(error: HandoffError<I>, wrap: impl FnOnce(I) -> O) -> HandoffError<O> {
427    let (handle, error) = error.into_parts();
428    HandoffError::new(wrap(handle), error)
429}
430
431impl File {
432    /// Consumes this handle, converting it into a standard-output or
433    /// standard-error endpoint positioned at `offset`.
434    ///
435    /// The endpoint carries a position of its own and the cursor is kept on
436    /// this side rather than in the kernel, so the position has to be stated
437    /// explicitly: this handle's own is only the right answer when the caller
438    /// is the one holding it, which is not the case for anything relaying on
439    /// someone else's behalf.
440    ///
441    /// The handle is consumed because two live handles onto one description
442    /// would each believe a cursor the other moves. Callers that want to keep
443    /// reading or writing should open the file again instead.
444    ///
445    /// # Errors
446    ///
447    /// Returns [`HandoffError`], which carries the handle back. Nothing has
448    /// been surrendered when it does — most importantly on the busy path,
449    /// where operations are still in flight against the descriptor and the
450    /// caller may simply retry once they finish.
451    pub async fn into_stdio_send(
452        self,
453        offset: u64,
454    ) -> std::result::Result<StdioSend, HandoffError<Self>> {
455        match self.inner {
456            FileInner::Client(file) => file
457                .into_stdio_send(offset)
458                .await
459                .map_err(|error| rewrap(error, Self::client)),
460            FileInner::Direct(file) => file
461                .into_stdio_send(offset)
462                .await
463                .map_err(|error| rewrap(error, Self::direct)),
464        }
465    }
466
467    /// Consumes this handle, converting it into a standard-input endpoint
468    /// positioned at `offset`. See [`into_stdio_send`](Self::into_stdio_send).
469    pub async fn into_stdio_recv(
470        self,
471        offset: u64,
472    ) -> std::result::Result<StdioRecv, HandoffError<Self>> {
473        match self.inner {
474            FileInner::Client(file) => file
475                .into_stdio_recv(offset)
476                .await
477                .map_err(|error| rewrap(error, Self::client)),
478            FileInner::Direct(file) => file
479                .into_stdio_recv(offset)
480                .await
481                .map_err(|error| rewrap(error, Self::direct)),
482        }
483    }
484
485    /// Closes this handle.
486    pub async fn close(self) -> Result<()> {
487        match_file!(move self, file => file.close().await)
488    }
489
490    /// Reads into `buf`'s spare capacity starting at `offset`, returning the
491    /// transfer count.
492    ///
493    /// Cancelling the read may leave `buf` empty.
494    ///
495    /// Fewer bytes than the spare capacity available may be read. `0` indicates
496    /// either end-of-file or that `buf` had no spare capacity.
497    pub fn read_at<'b>(
498        &self,
499        buf: &'b mut BytesMut,
500        offset: u64,
501    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
502        match &self.inner {
503            FileInner::Client(file) => EitherFuture::Left(file.read_at(buf, offset)),
504            FileInner::Direct(file) => EitherFuture::Right(file.read_at(buf, offset)),
505        }
506    }
507
508    /// Writes `data` at `offset`, returning the byte count.
509    ///
510    /// May write less than all of `data` on success, but at least 1 byte unless `data` is empty.
511    /// Use on an append-mode handle is an error; use [`append`](Self::append) there.
512    pub fn write_at(
513        &self,
514        data: Bytes,
515        offset: u64,
516    ) -> impl Future<Output = Result<usize>> + Send + use<> {
517        match &self.inner {
518            FileInner::Client(file) => EitherFuture::Left(file.write_at(data, offset)),
519            FileInner::Direct(file) => EitherFuture::Right(file.write_at(data, offset)),
520        }
521    }
522
523    /// Appends `data`, returning the byte count and the position just past
524    /// what was written.  Less data may be written than requested on success,
525    /// but at least 1 byte will be written unless `data` was empty.
526    pub fn append(&self, data: Bytes) -> impl Future<Output = Result<(usize, u64)>> + Send + use<> {
527        match &self.inner {
528            FileInner::Client(file) => EitherFuture::Left(file.append(data)),
529            FileInner::Direct(file) => EitherFuture::Right(file.append(data)),
530        }
531    }
532
533    /// Reads into the possibly uninitialized `buf``, returning how
534    /// many bytes at its front were filled.
535    pub fn read_at_into<'b>(
536        &self,
537        buf: &'b mut [MaybeUninit<u8>],
538        offset: u64,
539    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
540        match &self.inner {
541            FileInner::Client(file) => EitherFuture::Left(file.read_at_into(buf, offset)),
542            FileInner::Direct(file) => EitherFuture::Right(file.read_at_into(buf, offset)),
543        }
544    }
545
546    /// Writes `data` at `offset` from borrowed storage, returning the byte
547    /// count.
548    ///
549    /// Less data may be written than requested on success, but always at least 1
550    /// byte unless `data` is empty.
551    pub fn write_at_from<'b>(
552        &self,
553        data: &'b [u8],
554        offset: u64,
555    ) -> impl Future<Output = Result<usize>> + Send + use<'b> {
556        match &self.inner {
557            FileInner::Client(file) => EitherFuture::Left(file.write_at_from(data, offset)),
558            FileInner::Direct(file) => EitherFuture::Right(file.write_at_from(data, offset)),
559        }
560    }
561
562    /// Copies at most 2 MiB from this file into `dst`.
563    ///
564    /// `len` of `None` copies toward end of source. A positive result may be
565    /// short for any reason; callers that want a complete operation must loop.
566    /// Zero indicates end of source (or an empty request).
567    ///
568    /// Nothing is promised about physical allocation: an accelerated route may
569    /// preserve holes, and the fallback does not.
570    pub async fn copy_data(
571        &self,
572        dst: &File,
573        src_offset: u64,
574        target: CopyDest,
575        len: Option<u64>,
576        mode: CopyMode,
577    ) -> Result<CopyDataResult> {
578        self.check_overlap(dst, src_offset, target, len).await?;
579        let len = len.map(|len| len.min(copy::COPY_LIMIT));
580        match (&self.inner, &dst.inner) {
581            // `Never` still takes this route: it forbids block sharing, not
582            // running the copy on the side that owns both files.
583            (FileInner::Client(src), FileInner::Client(dst)) if src.can_copy_data_with(dst) => {
584                src.copy_data(dst, src_offset, target, len, mode).await
585            }
586            #[cfg(any(
587                target_os = "linux",
588                target_os = "freebsd",
589                target_os = "macos",
590                windows
591            ))]
592            (FileInner::Direct(src_direct), FileInner::Direct(dst_direct))
593                if matches!(target, CopyDest::At(_)) =>
594            {
595                if src_direct.is_regular().await? && dst_direct.is_regular().await? {
596                    src_direct
597                        .copy_data(dst_direct, src_offset, target, len, mode)
598                        .await
599                } else if mode == CopyMode::Require {
600                    Err(Error::new(
601                        ErrorKind::Unsupported,
602                        "block sharing is not supported for this copy",
603                    ))
604                } else {
605                    copy::copy_chunked(self, dst, src_offset, target, len).await
606                }
607            }
608            _ => {
609                // The refusal sits here, immediately above the fallback,
610                // rather than at the top of the function: once a platform
611                // route exists it is inserted above this point and the refusal
612                // becomes "sharing was attempted and could not be had", with
613                // nothing else to re-plumb.
614                if mode == CopyMode::Require {
615                    return Err(Error::new(
616                        ErrorKind::Unsupported,
617                        "block sharing is not supported for this copy",
618                    ));
619                }
620                copy::copy_chunked(self, dst, src_offset, target, len).await
621            }
622        }
623    }
624
625    /// Rejects a copy whose source and destination regions overlap within one
626    /// file.
627    ///
628    /// Best effort by necessity: identity is knowable for two local handles
629    /// and for two citations of one opaque file, but a mixed local/opaque pair
630    /// — which one session can hand out, since `Open` may answer with either —
631    /// has nothing cheap in common to compare. An unknown identity is treated
632    /// as "cannot tell" and lets the copy proceed; a same-session opaque pair
633    /// with distinct citations is checked on the server, where both handles
634    /// are local.
635    async fn check_overlap(
636        &self,
637        dst: &File,
638        src_offset: u64,
639        target: CopyDest,
640        len: Option<u64>,
641    ) -> Result<()> {
642        let same = copy::same_opaque(self, dst)
643            || match (copy::identity(self).await, copy::identity(dst).await) {
644                (Some(src), Some(dst)) => src == dst,
645                _ => false,
646            };
647        if !same {
648            return Ok(());
649        }
650        // Only now, when the regions are known to share a file, is the length
651        // worth a round trip: an open-ended copy runs to end of source, and an
652        // append lands there too.
653        let size = match (len, target) {
654            (Some(_), CopyDest::At(_)) => 0,
655            _ => self.metadata().await?.len(),
656        };
657        let src_end = match len {
658            Some(len) => src_offset.saturating_add(len),
659            None => size.max(src_offset),
660        };
661        let dst_start = match target {
662            CopyDest::At(offset) => offset,
663            CopyDest::Append => size,
664        };
665        let dst_end = dst_start.saturating_add(src_end - src_offset);
666        if dst_start < src_end && src_offset < dst_end {
667            return Err(Error::new(
668                ErrorKind::InvalidInput,
669                "source and destination regions of the same file overlap",
670            ));
671        }
672        Ok(())
673    }
674
675    /// Changes the file length to `size` bytes.
676    pub async fn set_size(&self, size: u64) -> Result<()> {
677        match_file!(self, file => file.set_size(size).await)
678    }
679
680    /// Flushes the file's written data to durable storage, returning once the
681    /// device reports it committed.
682    ///
683    /// `data` selects a data-only flush (`fdatasync`), which may skip metadata
684    /// the caller does not need — notably the modification time — and so can
685    /// avoid a second write to the inode. Size changes are still flushed,
686    /// since a reader could not find the data without them.
687    ///
688    /// This durably places the *contents*. It says nothing about the
689    /// directory entry naming the file, which is a separate inode and needs
690    /// its own flush to survive a crash.
691    pub async fn sync(&self, data: bool) -> Result<()> {
692        match_file!(self, file => file.sync(data).await)
693    }
694
695    /// Returns metadata for the open file.
696    pub async fn metadata(&self) -> Result<Metadata> {
697        match_file!(self, file => file.metadata().await)
698    }
699
700    /// Returns metadata for the filesystem containing the open file.
701    pub async fn fs_metadata(&self) -> Result<FsMetadata> {
702        match_file!(self, file => file.fs_metadata().await)
703    }
704
705    /// Returns the ACL of the requested `kind`. For a POSIX ACL, `default`
706    /// selects the directory's default ACL rather than its access ACL; it
707    /// must be `false` for `AclKind::Nfs4`.
708    pub async fn acl(&self, kind: AclKind, default: bool) -> Result<Option<Acl>> {
709        match_file!(self, file => file.acl(kind, default).await)
710    }
711
712    /// Sets or removes the ACL of `kind`. `acl`, if present, must match
713    /// `kind`. `default` selects the POSIX default ACL, as in
714    /// [`acl`](Self::acl); it must be `false` for `AclKind::Nfs4`.
715    pub async fn set_acl(&self, kind: AclKind, acl: Option<&Acl>, default: bool) -> Result<()> {
716        match_file!(self, file => file.set_acl(kind, acl, default).await)
717    }
718
719    /// Returns the Windows security descriptor selected by `mask`.
720    pub async fn sec_desc(&self, mask: dolang_winterop::security::SecInfo) -> Result<SecDesc> {
721        match_file!(self, file => file.sec_desc(mask).await)
722    }
723
724    /// Replaces the Windows security descriptor.
725    pub async fn update_sec_desc(&self, sec_desc: &SecDesc) -> Result<()> {
726        match_file!(self, file => file.update_sec_desc(sec_desc).await)
727    }
728
729    /// Lists extended attributes in `namespace`.
730    pub async fn xattrs(&self, namespace: XattrNamespace<'_>) -> Result<Vec<XattrEntry>> {
731        match_file!(self, file => file.xattrs(namespace).await)
732    }
733
734    /// Reads one extended attribute.
735    pub async fn xattr(&self, name: &str, namespace: Option<&str>) -> Result<Vec<u8>> {
736        match_file!(self, file => file.xattr(name, namespace).await)
737    }
738
739    /// Lists alternate data streams.
740    pub async fn streams(&self) -> Result<Vec<StreamEntry>> {
741        match_file!(self, file => file.streams().await)
742    }
743
744    /// Creates or replaces an extended attribute.
745    pub async fn set_xattr(&self, name: &str, namespace: Option<&str>, value: &[u8]) -> Result<()> {
746        match_file!(self, file => file.set_xattr(name, namespace, value).await)
747    }
748
749    /// Removes an extended attribute.
750    pub async fn remove_xattr(&self, name: &str, namespace: Option<&str>) -> Result<()> {
751        match_file!(self, file => file.remove_xattr(name, namespace).await)
752    }
753
754    /// Acquires a byte-range lock with the requested mode and behavior.
755    pub async fn lock(
756        &self,
757        range: FileLockRange,
758        mode: FileLockMode,
759        behavior: FileLockBehavior,
760    ) -> Result<Option<FileLock>> {
761        let request = FileLockRequest::new(range, mode, behavior);
762        match_file!(self, file => file.lock(request).await)
763    }
764
765    /// Converts this handle into a local standard-library file when possible.
766    pub async fn try_into_std(self) -> std::result::Result<std::fs::File, Self> {
767        match self.inner {
768            FileInner::Client(file) => file.try_into_std().await.map_err(Self::client),
769            FileInner::Direct(file) => file.try_into_std().await.map_err(Self::direct),
770        }
771    }
772}
773
774/// Configures and opens a file on one [`Vfs`] backend.
775enum OpenOptionsInner<'a> {
776    Client(client::OpenOptions<'a>),
777    Direct(direct::OpenOptions),
778}
779
780pub struct OpenOptions<'a> {
781    inner: OpenOptionsInner<'a>,
782}
783
784impl<'a> OpenOptions<'a> {
785    pub(crate) fn client(options: client::OpenOptions<'a>) -> Self {
786        Self {
787            inner: OpenOptionsInner::Client(options),
788        }
789    }
790
791    pub(crate) fn direct(options: direct::OpenOptions) -> Self {
792        Self {
793            inner: OpenOptionsInner::Direct(options),
794        }
795    }
796}
797
798impl OpenOptions<'_> {
799    /// Enables or disables read access.
800    pub fn read(&mut self, read: bool) -> &mut Self {
801        match &mut self.inner {
802            OpenOptionsInner::Client(opts) => {
803                opts.read(read);
804            }
805            OpenOptionsInner::Direct(opts) => {
806                opts.read(read);
807            }
808        }
809        self
810    }
811
812    /// Enables or disables write access.
813    pub fn write(&mut self, write: bool) -> &mut Self {
814        match &mut self.inner {
815            OpenOptionsInner::Client(opts) => {
816                opts.write(write);
817            }
818            OpenOptionsInner::Direct(opts) => {
819                opts.write(write);
820            }
821        }
822        self
823    }
824
825    /// Enables or disables append mode.
826    pub fn append(&mut self, append: bool) -> &mut Self {
827        match &mut self.inner {
828            OpenOptionsInner::Client(opts) => {
829                opts.append(append);
830            }
831            OpenOptionsInner::Direct(opts) => {
832                opts.append(append);
833            }
834        }
835        self
836    }
837
838    /// Enables or disables creation when the file is absent.
839    pub fn create(&mut self, create: bool) -> &mut Self {
840        match &mut self.inner {
841            OpenOptionsInner::Client(opts) => {
842                opts.create(create);
843            }
844            OpenOptionsInner::Direct(opts) => {
845                opts.create(create);
846            }
847        }
848        self
849    }
850
851    /// Enables or disables exclusive creation.
852    pub fn create_new(&mut self, create_new: bool) -> &mut Self {
853        match &mut self.inner {
854            OpenOptionsInner::Client(opts) => {
855                opts.create_new(create_new);
856            }
857            OpenOptionsInner::Direct(opts) => {
858                opts.create_new(create_new);
859            }
860        }
861        self
862    }
863
864    /// Enables or disables truncation when opening.
865    pub fn truncate(&mut self, truncate: bool) -> &mut Self {
866        match &mut self.inner {
867            OpenOptionsInner::Client(opts) => {
868                opts.truncate(truncate);
869            }
870            OpenOptionsInner::Direct(opts) => {
871                opts.truncate(truncate);
872            }
873        }
874        self
875    }
876
877    /// Enables or disables following the final path component when it is a link.
878    pub fn no_follow(&mut self, no_follow: bool) -> &mut Self {
879        match &mut self.inner {
880            OpenOptionsInner::Client(opts) => {
881                opts.no_follow(no_follow);
882            }
883            OpenOptionsInner::Direct(opts) => {
884                opts.no_follow(no_follow);
885            }
886        }
887        self
888    }
889
890    /// Opens `path` using the configured options.
891    pub async fn open(&self, path: path::Path<'_>) -> Result<File> {
892        match &self.inner {
893            OpenOptionsInner::Client(opts) => client::OpenOptions::open(opts, path).await,
894            OpenOptionsInner::Direct(opts) => direct::OpenOptions::open(opts, path)
895                .await
896                .map(File::direct),
897        }
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    use super::FileLockRange;
904    use crate::error::ErrorKind;
905
906    #[test]
907    fn lock_range_construction_validates_order() {
908        let range = FileLockRange::new(4, Some(8)).unwrap();
909        assert_eq!(range.start(), 4);
910        assert_eq!(range.end(), Some(8));
911        assert!(!range.is_empty());
912        assert!(
913            FileLockRange::new(8, Some(4))
914                .is_err_and(|error| error.kind() == ErrorKind::InvalidInput)
915        );
916        assert_eq!(FileLockRange::to_eof(4).end(), None);
917    }
918}