Skip to main content

smb_server_vfs/
lib.rs

1//! Dialect-neutral virtual filesystem interface.
2//!
3//! Protocol dispatchers (SMB1/SMB2) translate incoming commands into the
4//! operations of the [`Vfs`] trait; storage backends implement those
5//! operations. This keeps information-level encoding, access-mask mapping
6//! and status translation entirely inside the protocol layers.
7//!
8//! Every method receives an [`IoCtxView`](crate::IoCtxView)-free simple
9//! signature on purpose: backends only need the operation arguments plus the
10//! [`OpenFile`] handle whose opaque `inner` they own.
11
12#![forbid(unsafe_code)]
13#![deny(missing_docs)]
14#![warn(missing_debug_implementations)]
15
16mod vfs;
17
18pub use vfs::Vfs;
19
20use smb_server_proto::types::{AttrFlags, FileTime};
21
22/// An open file or directory handle owned by a backend.
23///
24/// The server tracks common bookkeeping (path, access flags, delete state);
25/// backend-private state lives in [`OpenFile::inner`].
26#[derive(Debug)]
27pub struct OpenFile {
28    /// Backend-relative path this handle was opened on (kept in sync across
29    /// rename-by-handle).
30    pub path: String,
31    /// Share-relative client path the handle was opened on (backslashes
32    /// normalized to `\`), empty for the share root. Lets protocol layers
33    /// re-enumerate or re-stat without re-deriving paths from storage.
34    pub rel: String,
35    /// True when the handle refers to a directory.
36    pub is_dir: bool,
37    /// Read data access was granted at open time.
38    pub can_read: bool,
39    /// Write/append/delete-data access was granted at open time.
40    pub can_write: bool,
41    /// Delete-on-close requested via create options.
42    pub delete_on_close: bool,
43    /// Deletion requested through SET_INFORMATION; applied on close.
44    pub delete_pending: bool,
45    /// Opaque backend state (file descriptor wrapper, cached cursor, …).
46    ///
47    /// Not `Send`/`Sync`: io_uring resources (`tokio_uring::fs::File`) are
48    /// `!Send`, and per-connection state is driven entirely on one thread.
49    pub inner: Box<dyn std::any::Any>,
50}
51
52impl OpenFile {
53    /// Downcast the backend-private state to a concrete type.
54    pub fn inner_as<T: 'static>(&self) -> Option<&T> {
55        self.inner.downcast_ref::<T>()
56    }
57
58    /// Immutably downcast the backend-private state (alias of [`Self::inner_as`]).
59    pub fn inner_as_ref<T: 'static>(&self) -> Option<&T> {
60        self.inner.downcast_ref::<T>()
61    }
62
63    /// Mutably downcast the backend-private state to a concrete type.
64    pub fn inner_as_mut<T: 'static>(&mut self) -> Option<&mut T> {
65        self.inner.downcast_mut::<T>()
66    }
67}
68
69/// Snapshot of file metadata shared between protocol layers and backends.
70#[derive(Debug, Clone, Default)]
71pub struct FileMeta {
72    /// Creation / last-access / last-write / change times.
73    pub times: [FileTime; 4],
74    /// Attribute flags.
75    pub attrs: AttrFlags,
76    /// Allocation size in bytes (block rounded).
77    pub alloc: u64,
78    /// End of file in bytes.
79    pub eof: u64,
80    /// True when the object is a directory.
81    pub is_dir: bool,
82}
83
84/// One directory enumeration entry.
85#[derive(Debug, Clone)]
86pub struct Entry {
87    /// File name as presented to clients.
88    pub name: String,
89    /// Metadata snapshot for the entry.
90    pub meta: FileMeta,
91}
92
93/// Neutralised set-information operations. Dialect layers translate their
94/// wire levels into these before calling [`Vfs::set_info`].
95#[derive(Debug, Clone)]
96pub enum SetOp {
97    /// Mark the object for deletion (`FILE_DISPOSITION_INFORMATION`).
98    Disposition {
99        /// True = delete when the last handle closes.
100        delete: bool,
101    },
102    /// Set allocation size.
103    Allocation(u64),
104    /// Set end-of-file (truncate/extend).
105    EndOfFile(u64),
106    /// Update timestamps; `None` fields are left untouched
107    /// (`FILE_BASIC_INFORMATION`).
108    Basic {
109        /// Last-access time to apply, if any.
110        access: Option<FileTime>,
111        /// Last-write time to apply, if any.
112        write: Option<FileTime>,
113    },
114    /// Rename the target to `name` relative to the share root.
115    Rename {
116        /// Whether an existing destination may be replaced.
117        replace_if_exists: bool,
118        /// New path relative to the share root.
119        name: String,
120    },
121    /// Set a single extended attribute (`FILE_FULL_EA_INFORMATION`).
122    Ea {
123        /// Extended-attribute name.
124        name: String,
125        /// Raw attribute value.
126        value: Vec<u8>,
127    },
128}
129
130/// Errors returned by backends. Backends should map their native failures to
131/// these; the protocol layer translates them into NT status codes.
132#[derive(Debug, thiserror::Error)]
133pub enum VfsError {
134    /// Underlying I/O failure.
135    #[error("vfs i/o: {0}")]
136    Io(#[from] std::io::Error),
137    /// Object does not exist.
138    #[error("not found")]
139    NotFound,
140    /// Object already exists.
141    #[error("already exists")]
142    AlreadyExists,
143    /// Operation not permitted for this principal.
144    #[error("access denied")]
145    AccessDenied,
146    /// Directory not empty during removal.
147    #[error("directory not empty")]
148    DirectoryNotEmpty,
149    /// A parameter was out of range or malformed.
150    #[error("invalid argument")]
151    InvalidArgument,
152    /// The backend does not implement the requested operation.
153    #[error("not supported")]
154    NotSupported,
155    /// A path component is a symbolic link, so traversal stops and the client
156    /// resolves it ([MS-SMB2] §2.2.2.2.1, STATUS_STOPPED_ON_SYMLINK).
157    #[error("stopped on symlink")]
158    StoppedOnSymlink {
159        /// Symlink target, reported as the substitute and print name.
160        target: String,
161        /// UTF-16 byte length of the request path following the symlink.
162        unparsed_len: u16,
163        /// The target is a relative path (`SYMLINK_FLAG_RELATIVE`).
164        relative: bool,
165    },
166}
167
168/// Result alias used throughout the VFS surface.
169pub type VfsResult<T> = Result<T, VfsError>;
170
171/// Map an [`std::io::Error`] onto the closest [`VfsError`].
172pub fn map_io(e: std::io::Error) -> VfsError {
173    match e.kind() {
174        std::io::ErrorKind::NotFound => VfsError::NotFound,
175        std::io::ErrorKind::PermissionDenied => VfsError::AccessDenied,
176        std::io::ErrorKind::AlreadyExists => VfsError::AlreadyExists,
177        std::io::ErrorKind::DirectoryNotEmpty => VfsError::DirectoryNotEmpty,
178        _ => e.into(),
179    }
180}