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}