Skip to main content

submilli_engine/runtime/
filesystem.rs

1//! Filesystem services supplied by the embedding runtime.
2//!
3//! Capability checks and fuel admission belong to the host caller. Embedders
4//! supply an effect ID and optional work budget with each call.
5use super::durable::{EffectId, RunId};
6use super::fs::ContainError;
7use super::fuel::WorkBudget;
8use super::vfs::{Access, Placement, VfsMode};
9use super::{QuotaExceeded, Vfs};
10use std::io::{Read, Seek, SeekFrom};
11use std::sync::Arc;
12
13#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14pub enum ReaderOptions {
15    Bytes { chunk_size: usize },
16    Lines { maximum_bytes: u64 },
17}
18#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct ReadPosition {
20    pub offset: u64,
21    pub bom_handled: bool,
22    pub closed: bool,
23}
24#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub enum ResourceKind {
26    Reader(ReaderOptions),
27    Directory,
28    Writer,
29}
30/// Only this descriptor crosses execution history. Native handles never do.
31#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct ResourceDescriptor {
33    #[serde(rename = "scope")]
34    pub run_id: RunId,
35    pub id: EffectId,
36    pub kind: ResourceKind,
37}
38pub enum ReadItem {
39    Bytes(Vec<u8>),
40    Line(String),
41}
42pub struct ReadOutput {
43    pub item: Option<ReadItem>,
44    pub(crate) _charge: super::limits::HostBytes,
45}
46impl ReadOutput {
47    /// Keep a pre-admitted allocation charged until output marshalling finishes.
48    pub fn new(item: Option<ReadItem>, reservation: super::limits::HostBytes) -> Self {
49        Self {
50            item,
51            _charge: reservation,
52        }
53    }
54}
55
56#[derive(Debug, thiserror::Error)]
57pub enum FilesystemError {
58    #[error(transparent)]
59    Containment(#[from] ContainError),
60    #[error(transparent)]
61    Io(#[from] std::io::Error),
62    #[error(transparent)]
63    Quota(#[from] QuotaExceeded),
64    #[error("unsupported filesystem operation: {0}")]
65    Unsupported(&'static str),
66    #[error("{0}")]
67    Execution(wasmtime::Error),
68    #[error("{0}")]
69    Rejected(String),
70    #[error(transparent)]
71    Exhausted(#[from] super::fuel::Exhausted),
72    #[error("filesystem backend failed: {0}")]
73    Backend(Box<dyn std::error::Error + Send + Sync>),
74}
75
76#[derive(Clone)]
77pub struct MountInfo {
78    pub path: String,
79    pub volume: String,
80    pub access: Access,
81    pub maximum_bytes: Option<u64>,
82}
83#[derive(Clone)]
84pub struct FilesystemInfo {
85    pub cwd: String,
86    pub mode: VfsMode,
87    pub access: Access,
88    pub volume: Option<String>,
89    pub maximum_bytes: Option<u64>,
90    pub mounts: Vec<MountInfo>,
91}
92impl FilesystemInfo {
93    pub fn from_vfs(vfs: &Vfs) -> Self {
94        Self {
95            cwd: vfs.cwd().into(),
96            mode: vfs.mode(),
97            access: vfs.access(),
98            volume: vfs.volume().map(str::to_owned),
99            maximum_bytes: vfs.quota().map(|q| q.limit()),
100            mounts: vfs
101                .mounts()
102                .iter()
103                .map(|m| MountInfo {
104                    path: m.guest_path().into(),
105                    volume: m.volume().into(),
106                    access: m.access(),
107                    maximum_bytes: m.quota().map(|q| q.limit()),
108                })
109                .collect(),
110        }
111    }
112}
113
114#[async_trait::async_trait]
115pub trait FileReader: Send + Sync {
116    fn descriptor(&self) -> Option<ResourceDescriptor> {
117        None
118    }
119    async fn next(
120        &mut self,
121        effect_id: &EffectId,
122        work: Option<Arc<WorkBudget>>,
123        limits: &super::limits::TenantLimits,
124    ) -> Result<ReadOutput, FilesystemError>;
125    async fn close(
126        &mut self,
127        effect_id: &EffectId,
128        work: Option<Arc<WorkBudget>>,
129    ) -> Result<(), FilesystemError>;
130}
131
132#[async_trait::async_trait]
133pub trait FileWriter: Send + Sync {
134    fn descriptor(&self) -> Option<ResourceDescriptor> {
135        None
136    }
137    fn is_closed(&self) -> bool;
138    async fn write(
139        &mut self,
140        effect_id: &EffectId,
141        work: Option<Arc<WorkBudget>>,
142        bytes: Vec<u8>,
143    ) -> Result<(), FilesystemError>;
144    /// Only explicit close publishes. Dropping a writer never publishes it.
145    async fn close(
146        &mut self,
147        effect_id: &EffectId,
148        work: Option<Arc<WorkBudget>>,
149    ) -> Result<(), FilesystemError>;
150}
151
152#[derive(Clone)]
153pub struct DirectoryEntry {
154    pub name: String,
155    pub path: String,
156    pub kind: &'static str,
157    pub bytes: u64,
158}
159#[async_trait::async_trait]
160pub trait DirectoryReader: Send + Sync {
161    fn descriptor(&self) -> Option<ResourceDescriptor> {
162        None
163    }
164    async fn next_entry(
165        &mut self,
166        effect_id: &EffectId,
167        work: Option<Arc<WorkBudget>>,
168        limits: &super::limits::TenantLimits,
169    ) -> Result<Option<DirectoryEntry>, FilesystemError>;
170    fn retained_bytes(&self) -> u64;
171    async fn close(
172        &mut self,
173        effect_id: &EffectId,
174        work: Option<Arc<WorkBudget>>,
175        limits: &super::limits::TenantLimits,
176    ) -> Result<(), FilesystemError>;
177}
178
179#[async_trait::async_trait]
180pub trait Filesystem: Send + Sync {
181    fn info(&self) -> FilesystemInfo;
182    fn cwd(&self) -> &str;
183    /// Additional fuel to admit before resolving a guest path.
184    fn path_cost(&self, _path: &str) -> u64 {
185        0
186    }
187    fn placement(&self, path: &str) -> Result<Placement, FilesystemError>;
188    async fn metadata(
189        &self,
190        effect_id: &EffectId,
191        work: Option<Arc<WorkBudget>>,
192        path: &str,
193        follow: bool,
194    ) -> Result<cap_std::fs::Metadata, FilesystemError>;
195    async fn read(
196        &self,
197        effect_id: &EffectId,
198        work: Option<Arc<WorkBudget>>,
199        path: &str,
200        offset: u64,
201        maximum_bytes: u64,
202    ) -> Result<Vec<u8>, FilesystemError>;
203    /// Atomically replace the file contents, creating the file if it is missing.
204    async fn write(
205        &self,
206        effect_id: &EffectId,
207        work: Option<Arc<WorkBudget>>,
208        path: &str,
209        bytes: Vec<u8>,
210    ) -> Result<(), FilesystemError>;
211    /// Append bytes to the file, creating the file if it is missing.
212    async fn append(
213        &self,
214        effect_id: &EffectId,
215        work: Option<Arc<WorkBudget>>,
216        path: &str,
217        bytes: Vec<u8>,
218    ) -> Result<(), FilesystemError>;
219    async fn mkdir(
220        &self,
221        effect_id: &EffectId,
222        work: Option<Arc<WorkBudget>>,
223        path: &str,
224        recursive: bool,
225    ) -> Result<(), FilesystemError>;
226    async fn remove(
227        &self,
228        effect_id: &EffectId,
229        work: Option<Arc<WorkBudget>>,
230        path: &str,
231        recursive: bool,
232        budget: &mut dyn MutationBudget,
233    ) -> Result<(), FilesystemError>;
234    async fn move_entry(
235        &self,
236        effect_id: &EffectId,
237        work: Option<Arc<WorkBudget>>,
238        from: &str,
239        to: &str,
240        budget: &mut dyn MutationBudget,
241    ) -> Result<(), FilesystemError>;
242    async fn copy(
243        &self,
244        effect_id: &EffectId,
245        work: Option<Arc<WorkBudget>>,
246        from: &str,
247        to: &str,
248        recursive: bool,
249        budget: &mut dyn MutationBudget,
250    ) -> Result<(), FilesystemError>;
251    /// Construct an ID-only resource without accessing storage during replay.
252    fn restore_reader(
253        &self,
254        _descriptor: ResourceDescriptor,
255    ) -> Result<Box<dyn FileReader>, FilesystemError> {
256        Err(FilesystemError::Unsupported("reader restoration"))
257    }
258    fn restore_directory(
259        &self,
260        _descriptor: ResourceDescriptor,
261    ) -> Result<Box<dyn DirectoryReader>, FilesystemError> {
262        Err(FilesystemError::Unsupported("directory restoration"))
263    }
264    fn restore_writer(
265        &self,
266        _descriptor: ResourceDescriptor,
267    ) -> Result<Box<dyn FileWriter>, FilesystemError> {
268        Err(FilesystemError::Unsupported("writer restoration"))
269    }
270    async fn open_reader(
271        &self,
272        effect_id: &EffectId,
273        work: Option<Arc<WorkBudget>>,
274        path: &str,
275        options: ReaderOptions,
276    ) -> Result<Box<dyn FileReader>, FilesystemError>;
277    async fn open_directory(
278        &self,
279        effect_id: &EffectId,
280        work: Option<Arc<WorkBudget>>,
281        path: &str,
282        recursive: bool,
283        limits: &super::limits::TenantLimits,
284    ) -> Result<Box<dyn DirectoryReader>, FilesystemError>;
285    async fn open_writer(
286        &self,
287        effect_id: &EffectId,
288        work: Option<Arc<WorkBudget>>,
289        path: &str,
290    ) -> Result<Box<dyn FileWriter>, FilesystemError>;
291}
292
293pub fn bounded_read(
294    mut reader: impl Read + Seek,
295    offset: u64,
296    maximum_bytes: u64,
297) -> Result<Vec<u8>, FilesystemError> {
298    reader.seek(SeekFrom::Start(offset))?;
299    let mut bytes = Vec::new();
300    let mut bounded = reader.take(maximum_bytes);
301    let mut chunk = [0; 8192];
302    loop {
303        let read = bounded.read(&mut chunk)?;
304        if read == 0 {
305            return Ok(bytes);
306        }
307        bytes
308            .try_reserve(read)
309            .map_err(|e| FilesystemError::Backend(Box::new(e)))?;
310        bytes.extend_from_slice(&chunk[..read]);
311    }
312}
313/// Resource accounting for native tree operations. Once a mutation succeeds,
314/// subsequent work must settle fuel rather than refuse. Heap admission stays fallible.
315pub trait MutationBudget: Send {
316    fn begin(&mut self) -> wasmtime::Result<()>;
317    fn reserve(&mut self, bytes: u64) -> wasmtime::Result<()>;
318    fn end(&mut self);
319    fn effect(&mut self);
320    fn syscalls(&mut self, count: u64, settled: bool) -> wasmtime::Result<()>;
321    fn copied(&mut self, entries: u64, bytes: u64) -> wasmtime::Result<()>;
322}