Skip to main content

vfs/engine/
vfs.rs

1use crate::engine::error::{VfsError, VfsResult};
2use crate::engine::types::{Dentry, SnapshotId, VirtualStat};
3use async_trait::async_trait;
4
5#[async_trait]
6pub trait VirtualFileSystem: Send + Sync {
7    async fn read_file(&self, path: &str) -> VfsResult<Vec<u8>>;
8
9    async fn read_text(&self, path: &str) -> VfsResult<String> {
10        String::from_utf8(self.read_file(path).await?)
11            .map_err(|_| VfsError::einval(format!("file is not valid UTF-8: {path}")))
12    }
13
14    async fn read_dir(&self, path: &str) -> VfsResult<Vec<String>>;
15    async fn read_dir_with_types(&self, path: &str) -> VfsResult<Vec<Dentry>>;
16    async fn write_file(&self, path: &str, content: &[u8]) -> VfsResult<()>;
17    async fn create_dir(&self, path: &str) -> VfsResult<()>;
18    async fn mkdir(&self, path: &str, recursive: bool) -> VfsResult<()>;
19    async fn mknod(&self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
20        let _ = (mode, rdev);
21        Err(VfsError::eopnotsupp(format!(
22            "special inode creation is not supported for {path}"
23        )))
24    }
25    async fn exists(&self, path: &str) -> bool;
26    async fn stat(&self, path: &str) -> VfsResult<VirtualStat>;
27    async fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
28    async fn remove_file(&self, path: &str) -> VfsResult<()>;
29    async fn remove_dir(&self, path: &str) -> VfsResult<()>;
30    async fn rename(&self, old_path: &str, new_path: &str) -> VfsResult<()>;
31    async fn realpath(&self, path: &str) -> VfsResult<String>;
32    async fn symlink(&self, target: &str, link_path: &str) -> VfsResult<()>;
33    async fn readlink(&self, path: &str) -> VfsResult<String>;
34    async fn link(&self, old_path: &str, new_path: &str) -> VfsResult<()>;
35    async fn chmod(&self, path: &str, mode: u32) -> VfsResult<()>;
36    async fn chown(&self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
37    async fn lchown(&self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
38        self.chown(path, uid, gid).await
39    }
40    async fn get_xattr(&self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
41        let _ = (name, follow_symlinks);
42        Err(VfsError::eopnotsupp(format!(
43            "extended attributes are not supported for {path}"
44        )))
45    }
46    async fn list_xattrs(&self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
47        let _ = follow_symlinks;
48        Err(VfsError::eopnotsupp(format!(
49            "extended attributes are not supported for {path}"
50        )))
51    }
52    async fn set_xattr(
53        &self,
54        path: &str,
55        name: &str,
56        value: &[u8],
57        flags: u32,
58        follow_symlinks: bool,
59    ) -> VfsResult<()> {
60        let _ = (name, value, flags, follow_symlinks);
61        Err(VfsError::eopnotsupp(format!(
62            "extended attributes are not supported for {path}"
63        )))
64    }
65    async fn remove_xattr(&self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
66        let _ = (name, follow_symlinks);
67        Err(VfsError::eopnotsupp(format!(
68            "extended attributes are not supported for {path}"
69        )))
70    }
71    async fn utimes(&self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
72    /// Update access time as a read side effect without changing ctime or mtime.
73    async fn set_atime(&self, path: &str, atime_ms: u64) -> VfsResult<()> {
74        let stat = self.stat(path).await?;
75        let mtime_ms = if stat.mtime.sec < 0 {
76            0
77        } else {
78            (stat.mtime.sec as u64)
79                .saturating_mul(1_000)
80                .saturating_add(u64::from(stat.mtime.nsec / 1_000_000))
81        };
82        self.utimes(path, atime_ms, mtime_ms).await
83    }
84    async fn truncate(&self, path: &str, length: u64) -> VfsResult<()>;
85    async fn allocate(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
86        const ALLOCATION_CHUNK_BYTES: u64 = 64 * 1024;
87
88        let end = offset
89            .checked_add(length)
90            .ok_or_else(|| VfsError::einval("allocation range overflows"))?;
91        if length == 0 {
92            return Ok(());
93        }
94        let stat = self.stat(path).await?;
95        if end > stat.size {
96            self.truncate(path, end).await?;
97        }
98        let mut cursor = offset;
99        while cursor < end {
100            let chunk_len = (end - cursor).min(ALLOCATION_CHUNK_BYTES);
101            let chunk_len = usize::try_from(chunk_len)
102                .map_err(|_| VfsError::einval("allocation chunk is too large"))?;
103            let mut bytes = self.pread(path, cursor, chunk_len).await?;
104            bytes.resize(chunk_len, 0);
105            self.pwrite(path, &bytes, cursor).await?;
106            cursor += chunk_len as u64;
107        }
108        Ok(())
109    }
110    async fn insert_range(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
111        validate_shift_range(offset, length)?;
112        let size = self.stat(path).await?.size;
113        if offset >= size {
114            return Err(VfsError::einval("insert range offset must be before EOF"));
115        }
116        let tail_len = usize::try_from(size - offset)
117            .map_err(|_| VfsError::einval("insert range tail is too large"))?;
118        let tail = self.pread(path, offset, tail_len).await?;
119        self.truncate(
120            path,
121            size.checked_add(length)
122                .ok_or_else(|| VfsError::einval("insert range size overflows"))?,
123        )
124        .await?;
125        self.pwrite(path, &tail, offset + length).await?;
126        self.punch_hole(path, offset, length).await
127    }
128    async fn collapse_range(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
129        validate_shift_range(offset, length)?;
130        let size = self.stat(path).await?.size;
131        let end = offset
132            .checked_add(length)
133            .ok_or_else(|| VfsError::einval("collapse range overflows"))?;
134        if end >= size {
135            return Err(VfsError::einval("collapse range must end before EOF"));
136        }
137        let tail_len = usize::try_from(size - end)
138            .map_err(|_| VfsError::einval("collapse range tail is too large"))?;
139        let tail = self.pread(path, end, tail_len).await?;
140        self.pwrite(path, &tail, offset).await?;
141        self.truncate(path, size - length).await
142    }
143    /// Zeroes and allocates a byte range, optionally preserving the file size.
144    async fn zero_range(
145        &self,
146        path: &str,
147        offset: u64,
148        length: u64,
149        keep_size: bool,
150    ) -> VfsResult<()> {
151        let end = offset
152            .checked_add(length)
153            .ok_or_else(|| VfsError::einval("zero range overflows"))?;
154        if length == 0 {
155            return Err(VfsError::einval("zero range length must be nonzero"));
156        }
157        let original_size = self.stat(path).await?.size;
158        self.allocate(path, offset, length).await?;
159        let zero_end = if keep_size {
160            end.min(original_size)
161        } else {
162            end
163        };
164        let mut cursor = offset.min(zero_end);
165        while cursor < zero_end {
166            let chunk_len = usize::try_from((zero_end - cursor).min(64 * 1024))
167                .map_err(|_| VfsError::einval("zero range chunk is too large"))?;
168            self.pwrite(path, &vec![0; chunk_len], cursor).await?;
169            cursor += chunk_len as u64;
170        }
171        if keep_size && self.stat(path).await?.size != original_size {
172            self.truncate(path, original_size).await?;
173        }
174        Ok(())
175    }
176    /// Deallocates a byte range while preserving the file size. Bytes in the
177    /// intersecting range read back as zeroes.
178    async fn punch_hole(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
179        const PUNCH_CHUNK_BYTES: u64 = 64 * 1024;
180
181        let requested_end = offset
182            .checked_add(length)
183            .ok_or_else(|| VfsError::einval("hole-punch range overflows"))?;
184        let size = self.stat(path).await?.size;
185        let end = requested_end.min(size);
186        let mut cursor = offset.min(size);
187        while cursor < end {
188            let chunk_len = (end - cursor).min(PUNCH_CHUNK_BYTES) as usize;
189            self.pwrite(path, &vec![0; chunk_len], cursor).await?;
190            cursor += chunk_len as u64;
191        }
192        Ok(())
193    }
194    /// Returns allocated byte ranges as half-open `(start, end)` intervals.
195    async fn allocated_ranges(&self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
196        Err(VfsError::eopnotsupp(format!(
197            "extent mapping is not supported for {path}"
198        )))
199    }
200    /// Returns unwritten allocated byte ranges as half-open `(start, end)` intervals.
201    async fn unwritten_ranges(&self, _path: &str) -> VfsResult<Vec<(u64, u64)>> {
202        Ok(Vec::new())
203    }
204    async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
205    async fn pwrite(&self, path: &str, content: &[u8], offset: u64) -> VfsResult<()>;
206    async fn append(&self, path: &str, content: &[u8]) -> VfsResult<u64>;
207
208    async fn sync(&self, _path: &str) -> VfsResult<()> {
209        Ok(())
210    }
211
212    async fn shutdown(&self) -> VfsResult<()> {
213        Ok(())
214    }
215}
216
217fn validate_shift_range(offset: u64, length: u64) -> VfsResult<()> {
218    const ALIGNMENT: u64 = 512;
219    if length == 0 || !offset.is_multiple_of(ALIGNMENT) || !length.is_multiple_of(ALIGNMENT) {
220        return Err(VfsError::einval(
221            "insert/collapse range requires a nonzero 512-byte-aligned range",
222        ));
223    }
224    Ok(())
225}
226
227#[async_trait]
228pub trait Snapshottable: Send + Sync {
229    async fn snapshot(&self, root: u64) -> VfsResult<SnapshotId>;
230    async fn fork(&self, snap: SnapshotId) -> VfsResult<u64>;
231}