secure_exec_vfs_core/engine/engines/
object.rs1use 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(¤t).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(¤t).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}