Skip to main content

secure_exec_vfs_core/engine/engines/
object.rs

1use crate::engine::block::ObjectBackend;
2use crate::engine::error::{VfsError, VfsResult};
3use crate::engine::types::{normalize_path, Dentry, InodeType, ObjectMeta, Timespec, VirtualStat};
4use crate::engine::vfs::VirtualFileSystem;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone)]
8pub struct ObjectFsOptions {
9    pub prefix: String,
10    pub uid: u32,
11    pub gid: u32,
12    pub file_mode: u32,
13    pub dir_mode: u32,
14}
15
16impl Default for ObjectFsOptions {
17    fn default() -> Self {
18        Self {
19            prefix: String::new(),
20            uid: 0,
21            gid: 0,
22            file_mode: 0o644,
23            dir_mode: 0o755,
24        }
25    }
26}
27
28#[derive(Debug, Clone)]
29pub struct ObjectFs<B> {
30    backend: B,
31    options: ObjectFsOptions,
32}
33
34impl<B> ObjectFs<B> {
35    pub fn new(backend: B) -> Self {
36        Self::with_options(backend, ObjectFsOptions::default())
37    }
38
39    pub fn with_options(backend: B, options: ObjectFsOptions) -> Self {
40        Self { backend, options }
41    }
42
43    fn key_for(&self, path: &str) -> VfsResult<String> {
44        let normalized = normalize_path(path)?;
45        let relative = normalized.trim_start_matches('/');
46        Ok(format!("{}{}", self.options.prefix, relative))
47    }
48
49    fn dir_prefix_for(&self, path: &str) -> VfsResult<String> {
50        let mut key = self.key_for(path)?;
51        if !key.is_empty() && !key.ends_with('/') {
52            key.push('/');
53        }
54        Ok(key)
55    }
56
57    fn file_meta(&self, size: u64) -> ObjectMeta {
58        ObjectMeta {
59            size,
60            mtime: Timespec::now(),
61            mode: self.options.file_mode,
62            uid: self.options.uid,
63            gid: self.options.gid,
64            kind: InodeType::File,
65            symlink_target: None,
66        }
67    }
68
69    fn dir_meta(&self) -> ObjectMeta {
70        ObjectMeta {
71            size: 0,
72            mtime: Timespec::now(),
73            mode: self.options.dir_mode,
74            uid: self.options.uid,
75            gid: self.options.gid,
76            kind: InodeType::Directory,
77            symlink_target: None,
78        }
79    }
80}
81
82impl<B: ObjectBackend> ObjectFs<B> {
83    async fn collect_objects_under(&self, prefix: &str) -> VfsResult<Vec<String>> {
84        let mut pending = vec![prefix.to_string()];
85        let mut objects = Vec::new();
86        while let Some(current) = pending.pop() {
87            for entry in self.backend.list(&current).await? {
88                if entry.is_prefix {
89                    pending.push(entry.name);
90                } else {
91                    objects.push(entry.name);
92                }
93            }
94        }
95        Ok(objects)
96    }
97}
98
99#[async_trait]
100impl<B: ObjectBackend> VirtualFileSystem for ObjectFs<B> {
101    async fn read_file(&self, path: &str) -> VfsResult<Vec<u8>> {
102        let key = self.key_for(path)?;
103        let meta = self
104            .backend
105            .head(&key)
106            .await?
107            .ok_or_else(|| VfsError::enoent(path))?;
108        if meta.kind == InodeType::Directory {
109            return Err(VfsError::eisdir(path));
110        }
111        if meta.kind == InodeType::Symlink {
112            let target = meta.symlink_target.ok_or_else(|| VfsError::enoent(path))?;
113            return self.read_file(&target).await;
114        }
115        self.backend.get_range(&key, 0, meta.size).await
116    }
117
118    async fn read_dir(&self, path: &str) -> VfsResult<Vec<String>> {
119        Ok(self
120            .read_dir_with_types(path)
121            .await?
122            .into_iter()
123            .map(|entry| entry.name)
124            .collect())
125    }
126
127    async fn read_dir_with_types(&self, path: &str) -> VfsResult<Vec<Dentry>> {
128        let prefix = self.dir_prefix_for(path)?;
129        let entries = self.backend.list(&prefix).await?;
130        let mut result = Vec::new();
131        for entry in entries {
132            let name = entry
133                .name
134                .trim_start_matches(&prefix)
135                .trim_end_matches('/')
136                .to_string();
137            if name.is_empty() || name.contains('/') {
138                continue;
139            }
140            result.push(Dentry {
141                name,
142                ino: 0,
143                kind: if entry.is_prefix {
144                    InodeType::Directory
145                } else {
146                    InodeType::File
147                },
148            });
149        }
150        Ok(result)
151    }
152
153    async fn write_file(&self, path: &str, content: &[u8]) -> VfsResult<()> {
154        let key = self.key_for(path)?;
155        self.backend
156            .put(&key, content, self.file_meta(content.len() as u64))
157            .await
158    }
159
160    async fn create_dir(&self, path: &str) -> VfsResult<()> {
161        let key = self.dir_prefix_for(path)?;
162        self.backend.put(&key, &[], self.dir_meta()).await
163    }
164
165    async fn mkdir(&self, path: &str, recursive: bool) -> VfsResult<()> {
166        if !recursive {
167            return self.create_dir(path).await;
168        }
169        let normalized = normalize_path(path)?;
170        let mut current = String::new();
171        for part in normalized
172            .trim_start_matches('/')
173            .split('/')
174            .filter(|p| !p.is_empty())
175        {
176            current.push('/');
177            current.push_str(part);
178            self.create_dir(&current).await?;
179        }
180        Ok(())
181    }
182
183    async fn exists(&self, path: &str) -> bool {
184        let Ok(key) = self.key_for(path) else {
185            return false;
186        };
187        if self.backend.head(&key).await.ok().flatten().is_some() {
188            return true;
189        }
190        let Ok(prefix) = self.dir_prefix_for(path) else {
191            return false;
192        };
193        self.backend
194            .list(&prefix)
195            .await
196            .map(|entries| !entries.is_empty())
197            .unwrap_or(false)
198    }
199
200    async fn stat(&self, path: &str) -> VfsResult<VirtualStat> {
201        let key = self.key_for(path)?;
202        if let Some(meta) = self.backend.head(&key).await? {
203            return Ok(object_stat(meta));
204        }
205        let entries = self.backend.list(&self.dir_prefix_for(path)?).await?;
206        if entries.is_empty() {
207            return Err(VfsError::enoent(path));
208        }
209        Ok(object_stat(self.dir_meta()))
210    }
211
212    async fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
213        self.stat(path).await
214    }
215
216    async fn remove_file(&self, path: &str) -> VfsResult<()> {
217        self.backend.delete(&self.key_for(path)?).await
218    }
219
220    async fn remove_dir(&self, path: &str) -> VfsResult<()> {
221        let prefix = self.dir_prefix_for(path)?;
222        let entries = self.backend.list(&prefix).await?;
223        if entries.iter().any(|entry| entry.name != prefix) {
224            return Err(VfsError::enotempty(path));
225        }
226        self.backend.delete(&prefix).await
227    }
228
229    async fn rename(&self, old_path: &str, new_path: &str) -> VfsResult<()> {
230        let old_key = self.key_for(old_path)?;
231        if self.backend.head(&old_key).await?.is_some() {
232            let new_key = self.key_for(new_path)?;
233            self.backend.copy(&old_key, &new_key).await?;
234            self.backend.delete(&old_key).await?;
235            return Ok(());
236        }
237        let old_prefix = self.dir_prefix_for(old_path)?;
238        let new_prefix = self.dir_prefix_for(new_path)?;
239        let objects = self.collect_objects_under(&old_prefix).await?;
240        if objects.is_empty() {
241            return Err(VfsError::enoent(old_path));
242        }
243        for key in &objects {
244            let dst = format!("{new_prefix}{}", key.trim_start_matches(&old_prefix));
245            self.backend.copy(key, &dst).await?;
246        }
247        for key in objects {
248            self.backend.delete(&key).await?;
249        }
250        Ok(())
251    }
252
253    async fn realpath(&self, path: &str) -> VfsResult<String> {
254        if !self.exists(path).await {
255            return Err(VfsError::enoent(path));
256        }
257        normalize_path(path)
258    }
259
260    async fn symlink(&self, target: &str, link_path: &str) -> VfsResult<()> {
261        let key = self.key_for(link_path)?;
262        let meta = ObjectMeta {
263            size: 0,
264            mtime: Timespec::now(),
265            mode: 0o777,
266            uid: self.options.uid,
267            gid: self.options.gid,
268            kind: InodeType::Symlink,
269            symlink_target: Some(target.to_string()),
270        };
271        self.backend.put(&key, &[], meta).await
272    }
273
274    async fn readlink(&self, path: &str) -> VfsResult<String> {
275        let key = self.key_for(path)?;
276        let meta = self
277            .backend
278            .head(&key)
279            .await?
280            .ok_or_else(|| VfsError::enoent(path))?;
281        if meta.kind != InodeType::Symlink {
282            return Err(VfsError::einval(format!("not a symlink: {path}")));
283        }
284        Ok(meta.symlink_target.unwrap_or_default())
285    }
286
287    async fn link(&self, _old_path: &str, _new_path: &str) -> VfsResult<()> {
288        Err(VfsError::eopnotsupp("ObjectFs does not support hard links"))
289    }
290
291    async fn chmod(&self, _path: &str, _mode: u32) -> VfsResult<()> {
292        Ok(())
293    }
294
295    async fn chown(&self, _path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
296        Ok(())
297    }
298
299    async fn utimes(&self, _path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
300        Ok(())
301    }
302
303    async fn truncate(&self, path: &str, length: u64) -> VfsResult<()> {
304        let mut data = self.read_file(path).await?;
305        let length = usize::try_from(length)
306            .map_err(|_| VfsError::einval(format!("truncate length is too large: {length}")))?;
307        data.resize(length, 0);
308        self.write_file(path, &data).await
309    }
310
311    async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
312        let key = self.key_for(path)?;
313        self.backend.get_range(&key, offset, length as u64).await
314    }
315
316    async fn pwrite(&self, path: &str, content: &[u8], offset: u64) -> VfsResult<()> {
317        let mut data = self.read_file(path).await?;
318        let start = usize::try_from(offset)
319            .map_err(|_| VfsError::einval(format!("pwrite offset is too large: {offset}")))?;
320        if start > data.len() {
321            data.resize(start, 0);
322        }
323        let end = start.saturating_add(content.len());
324        if end > data.len() {
325            data.resize(end, 0);
326        }
327        data[start..end].copy_from_slice(content);
328        self.write_file(path, &data).await
329    }
330
331    async fn append(&self, path: &str, content: &[u8]) -> VfsResult<u64> {
332        let mut data = self.read_file(path).await?;
333        data.extend_from_slice(content);
334        let len = data.len() as u64;
335        self.write_file(path, &data).await?;
336        Ok(len)
337    }
338}
339
340fn object_stat(meta: ObjectMeta) -> VirtualStat {
341    let type_bits = match meta.kind {
342        InodeType::File => crate::engine::types::S_IFREG,
343        InodeType::Directory => crate::engine::types::S_IFDIR,
344        InodeType::Symlink => crate::engine::types::S_IFLNK,
345    };
346    VirtualStat {
347        mode: type_bits | (meta.mode & 0o7777),
348        size: meta.size,
349        blocks: meta.size.div_ceil(512),
350        is_directory: meta.kind == InodeType::Directory,
351        is_symbolic_link: meta.kind == InodeType::Symlink,
352        atime: meta.mtime,
353        mtime: meta.mtime,
354        ctime: meta.mtime,
355        birthtime: meta.mtime,
356        ino: 0,
357        nlink: 1,
358        uid: meta.uid,
359        gid: meta.gid,
360    }
361}