Skip to main content

vfs/engine/engines/
chunked.rs

1use crate::engine::block::BlockStore;
2use crate::engine::error::{VfsError, VfsResult};
3use crate::engine::metadata::MetadataStore;
4use crate::engine::types::{
5    decode_unwritten_extents, encode_unwritten_extents, normalize_path, set_xattr_value,
6    unwritten_after_allocate, unwritten_after_collapse, unwritten_after_insert,
7    unwritten_after_truncate, unwritten_after_write, unwritten_after_zero, unwritten_byte_ranges,
8    validate_xattr_name, BlockKey, ChunkEdit, ChunkRange, CreateInodeAttrs, Dentry, InodeMeta,
9    InodePatch, InodeType, SnapshotId, Storage, Timespec, VirtualStat, DEFAULT_CHUNK_SIZE,
10    DEFAULT_INLINE_THRESHOLD, INTERNAL_XATTR_PREFIX,
11};
12use crate::engine::vfs::{Snapshottable, VirtualFileSystem};
13use async_trait::async_trait;
14use std::collections::BTreeMap;
15
16#[derive(Debug, Clone)]
17pub struct ChunkedFsOptions {
18    pub inline_threshold: usize,
19    pub chunk_size: u32,
20    pub uid: u32,
21    pub gid: u32,
22    pub file_mode: u32,
23    pub dir_mode: u32,
24}
25
26impl Default for ChunkedFsOptions {
27    fn default() -> Self {
28        Self {
29            inline_threshold: DEFAULT_INLINE_THRESHOLD,
30            chunk_size: DEFAULT_CHUNK_SIZE,
31            uid: 0,
32            gid: 0,
33            file_mode: 0o644,
34            dir_mode: 0o755,
35        }
36    }
37}
38
39#[derive(Debug, Clone)]
40pub struct ChunkedFs<M, B> {
41    metadata: M,
42    blocks: B,
43    options: ChunkedFsOptions,
44    adaptive_chunk_size: bool,
45}
46
47const MAX_ADAPTIVE_CHUNK_SIZE: u32 = 1024 * 1024;
48
49impl<M, B> ChunkedFs<M, B> {
50    pub fn new(metadata: M, blocks: B) -> Self {
51        Self::with_options(metadata, blocks, ChunkedFsOptions::default())
52    }
53
54    pub fn with_options(metadata: M, blocks: B, options: ChunkedFsOptions) -> Self {
55        Self {
56            metadata,
57            blocks,
58            options,
59            adaptive_chunk_size: false,
60        }
61    }
62
63    pub fn with_adaptive_chunk_size(metadata: M, blocks: B, options: ChunkedFsOptions) -> Self {
64        Self {
65            metadata,
66            blocks,
67            options,
68            adaptive_chunk_size: true,
69        }
70    }
71
72    pub fn metadata(&self) -> &M {
73        &self.metadata
74    }
75
76    pub fn blocks(&self) -> &B {
77        &self.blocks
78    }
79}
80
81impl<M: MetadataStore, B: BlockStore> ChunkedFs<M, B> {
82    fn initial_chunk_size(&self, write_len: usize) -> u32 {
83        if !self.adaptive_chunk_size {
84            return self.options.chunk_size;
85        }
86        let write_len = u32::try_from(write_len).unwrap_or(u32::MAX);
87        write_len
88            .max(self.options.chunk_size)
89            .min(MAX_ADAPTIVE_CHUNK_SIZE.max(self.options.chunk_size))
90    }
91
92    async fn write_existing_or_create(&self, path: &str, content: &[u8]) -> VfsResult<()> {
93        let (parent, name) = self.metadata.resolve_parent(path).await?;
94        let existing = self.metadata.lstat(path).await.ok();
95        let (ino, mut xattrs) = match existing {
96            Some(meta) => {
97                if meta.kind == InodeType::Directory {
98                    return Err(VfsError::eisdir(path));
99                }
100                (meta.ino, meta.xattrs)
101            }
102            None => {
103                let chunk_size = self.initial_chunk_size(content.len());
104                let storage = if content.len() <= self.options.inline_threshold {
105                    Storage::Inline(content.to_vec())
106                } else {
107                    Storage::Chunked { chunk_size }
108                };
109                let meta = self
110                    .metadata
111                    .create(
112                        parent.ino,
113                        &name,
114                        CreateInodeAttrs::file(
115                            self.options.file_mode,
116                            self.options.uid,
117                            self.options.gid,
118                            storage,
119                        ),
120                    )
121                    .await?;
122                if content.len() <= self.options.inline_threshold {
123                    return Ok(());
124                }
125                (meta.ino, meta.xattrs)
126            }
127        };
128        encode_unwritten_extents(&mut xattrs, &[]);
129
130        if content.len() <= self.options.inline_threshold {
131            let freed = self
132                .metadata
133                .set_attr(
134                    ino,
135                    InodePatch {
136                        storage: Some(Storage::Inline(content.to_vec())),
137                        size: Some(content.len() as u64),
138                        xattrs: Some(xattrs),
139                        ..InodePatch::default()
140                    },
141                )
142                .await?;
143            self.blocks.delete_many(&freed).await?;
144            return Ok(());
145        }
146
147        let chunk_size = self.initial_chunk_size(content.len());
148        let mut edits = Vec::new();
149        for (index, chunk) in content.chunks(chunk_size as usize).enumerate() {
150            let key = BlockKey::from_content(chunk);
151            if !self.blocks.exists(&key).await? {
152                self.blocks.put(&key, chunk).await?;
153            }
154            edits.push(ChunkEdit {
155                index: index as u64,
156                key,
157                len: chunk.len() as u32,
158            });
159        }
160        self.metadata
161            .set_attr(
162                ino,
163                InodePatch {
164                    storage: Some(Storage::Chunked { chunk_size }),
165                    size: Some(content.len() as u64),
166                    xattrs: Some(xattrs),
167                    ..InodePatch::default()
168                },
169            )
170            .await?;
171        let freed = self
172            .metadata
173            .commit_write(
174                ino,
175                edits,
176                content.len() as u64,
177                dense_allocation(content.len() as u64),
178            )
179            .await?;
180        self.blocks.delete_many(&freed).await?;
181        Ok(())
182    }
183
184    fn file_chunk_size(&self, storage: &Storage) -> u32 {
185        match storage {
186            Storage::Chunked { chunk_size } => *chunk_size,
187            Storage::Inline(_) | Storage::None => self.options.chunk_size,
188        }
189    }
190
191    fn ensure_file<'a>(&self, path: &str, meta: &'a InodeMeta) -> VfsResult<&'a InodeMeta> {
192        match meta.kind {
193            InodeType::File => Ok(meta),
194            InodeType::Directory => Err(VfsError::eisdir(path)),
195            InodeType::Symlink => Err(VfsError::eopnotsupp("resolved symlink without target file")),
196            InodeType::CharacterDevice | InodeType::BlockDevice | InodeType::Fifo => {
197                Err(VfsError::new(
198                    "ENXIO",
199                    format!("device I/O requires kernel dispatch: {path}"),
200                ))
201            }
202        }
203    }
204
205    async fn read_file_range(
206        &self,
207        meta: &InodeMeta,
208        offset: u64,
209        length: usize,
210    ) -> VfsResult<Vec<u8>> {
211        if length == 0 || offset >= meta.size {
212            return Ok(Vec::new());
213        }
214        let available = meta.size.saturating_sub(offset).min(length as u64);
215        let output_len = usize::try_from(available)
216            .map_err(|_| VfsError::einval(format!("range length is too large: {available}")))?;
217
218        match &meta.storage {
219            Storage::Inline(data) => {
220                let start = usize::try_from(offset).map_err(|_| {
221                    VfsError::einval(format!("range offset is too large: {offset}"))
222                })?;
223                if start >= data.len() {
224                    return Ok(vec![0; output_len]);
225                }
226                let end = start.saturating_add(output_len).min(data.len());
227                let mut output = vec![0; output_len];
228                output[..end - start].copy_from_slice(&data[start..end]);
229                Ok(output)
230            }
231            Storage::None => Ok(vec![0; output_len]),
232            Storage::Chunked { chunk_size } => {
233                let chunk_size = u64::from(*chunk_size);
234                let end_offset = offset
235                    .checked_add(available)
236                    .ok_or_else(|| VfsError::einval("range end overflows"))?;
237                let start_index = offset / chunk_size;
238                let end_index = end_offset.div_ceil(chunk_size);
239                let chunks = self
240                    .metadata
241                    .get_chunks(
242                        meta.ino,
243                        ChunkRange {
244                            start: start_index,
245                            end: Some(end_index),
246                        },
247                    )
248                    .await?;
249                let mut output = vec![0; output_len];
250                for chunk in chunks {
251                    let chunk_start = chunk.index.saturating_mul(chunk_size);
252                    let block = self.blocks.get(&chunk.key).await?;
253                    let copy_start = offset.max(chunk_start);
254                    let copy_end = end_offset.min(chunk_start.saturating_add(block.len() as u64));
255                    if copy_start >= copy_end {
256                        continue;
257                    }
258                    let output_start = usize::try_from(copy_start - offset)
259                        .map_err(|_| VfsError::einval("range output offset is too large"))?;
260                    let block_start = usize::try_from(copy_start - chunk_start)
261                        .map_err(|_| VfsError::einval("range block offset is too large"))?;
262                    let len = usize::try_from(copy_end - copy_start)
263                        .map_err(|_| VfsError::einval("range copy length is too large"))?;
264                    output[output_start..output_start + len]
265                        .copy_from_slice(&block[block_start..block_start + len]);
266                }
267                Ok(output)
268            }
269        }
270    }
271
272    async fn put_chunk_edit(&self, index: u64, data: Vec<u8>) -> VfsResult<ChunkEdit> {
273        let len = u32::try_from(data.len())
274            .map_err(|_| VfsError::einval(format!("chunk is too large: {}", data.len())))?;
275        let key = BlockKey::from_content(&data);
276        if !self.blocks.exists(&key).await? {
277            self.blocks.put(&key, &data).await?;
278        }
279        Ok(ChunkEdit { index, key, len })
280    }
281
282    async fn set_unwritten_extents(
283        &self,
284        meta: &InodeMeta,
285        extents: &[(u64, u64)],
286    ) -> VfsResult<()> {
287        let mut xattrs = meta.xattrs.clone();
288        encode_unwritten_extents(&mut xattrs, extents);
289        let freed = self
290            .metadata
291            .set_attr(
292                meta.ino,
293                InodePatch {
294                    xattrs: Some(xattrs),
295                    ..InodePatch::default()
296                },
297            )
298            .await?;
299        self.blocks.delete_many(&freed).await
300    }
301
302    async fn write_chunked_range(
303        &self,
304        meta: &InodeMeta,
305        content: &[u8],
306        offset: u64,
307    ) -> VfsResult<u64> {
308        if content.is_empty() {
309            return Ok(meta.size);
310        }
311        let content_len = u64::try_from(content.len()).map_err(|_| {
312            VfsError::einval(format!("pwrite content is too large: {}", content.len()))
313        })?;
314        let end_offset = offset
315            .checked_add(content_len)
316            .ok_or_else(|| VfsError::einval("pwrite end offset overflows"))?;
317        let new_size = meta.size.max(end_offset);
318
319        if !matches!(meta.storage, Storage::Chunked { .. })
320            && usize::try_from(new_size)
321                .ok()
322                .is_some_and(|len| len <= self.options.inline_threshold)
323        {
324            let old_len = usize::try_from(meta.size)
325                .map_err(|_| VfsError::einval(format!("file is too large: {}", meta.size)))?;
326            let mut data = self.read_file_range(meta, 0, old_len).await?;
327            let start = usize::try_from(offset)
328                .map_err(|_| VfsError::einval(format!("pwrite offset is too large: {offset}")))?;
329            let end = start.saturating_add(content.len());
330            if start > data.len() {
331                data.resize(start, 0);
332            }
333            if end > data.len() {
334                data.resize(end, 0);
335            }
336            data[start..end].copy_from_slice(content);
337            let mut xattrs = meta.xattrs.clone();
338            encode_unwritten_extents(
339                &mut xattrs,
340                &unwritten_after_write(
341                    &decode_unwritten_extents(&meta.xattrs)?,
342                    offset,
343                    content_len,
344                ),
345            );
346            let freed = self
347                .metadata
348                .set_attr(
349                    meta.ino,
350                    InodePatch {
351                        storage: Some(Storage::Inline(data)),
352                        size: Some(new_size),
353                        allocated_extents: Some(allocation_after_write(
354                            &meta.allocated_extents,
355                            offset,
356                            content_len,
357                        )),
358                        xattrs: Some(xattrs),
359                        ..InodePatch::default()
360                    },
361                )
362                .await?;
363            self.blocks.delete_many(&freed).await?;
364            return Ok(new_size);
365        }
366
367        let chunk_size = u64::from(match &meta.storage {
368            Storage::Chunked { chunk_size } => *chunk_size,
369            Storage::Inline(_) | Storage::None => self.initial_chunk_size(content.len()),
370        });
371        let start_index = offset / chunk_size;
372        let end_index = end_offset.div_ceil(chunk_size);
373        let existing_chunks = if matches!(meta.storage, Storage::Chunked { .. }) {
374            self.metadata
375                .get_chunks(
376                    meta.ino,
377                    ChunkRange {
378                        start: start_index,
379                        end: Some(end_index),
380                    },
381                )
382                .await?
383                .into_iter()
384                .map(|chunk| (chunk.index, chunk.key))
385                .collect::<BTreeMap<_, _>>()
386        } else {
387            BTreeMap::new()
388        };
389
390        let mut edits = Vec::new();
391        if let Storage::Inline(data) = &meta.storage {
392            let inline_chunks = u64::try_from(data.len())
393                .map_err(|_| VfsError::einval("inline data is too large"))?
394                .div_ceil(chunk_size);
395            for index in 0..inline_chunks.min(start_index) {
396                let chunk_start = index.saturating_mul(chunk_size);
397                let chunk_len = chunk_size.min(new_size.saturating_sub(chunk_start));
398                let mut chunk_data = vec![
399                    0;
400                    usize::try_from(chunk_len).map_err(|_| {
401                        VfsError::einval("inline prefix chunk is too large")
402                    })?
403                ];
404                let copy_start = usize::try_from(chunk_start)
405                    .map_err(|_| VfsError::einval("inline prefix offset is too large"))?;
406                let copy_end = data.len().min(copy_start.saturating_add(chunk_data.len()));
407                chunk_data[..copy_end - copy_start].copy_from_slice(&data[copy_start..copy_end]);
408                edits.push(self.put_chunk_edit(index, chunk_data).await?);
409            }
410        }
411        for index in start_index..end_index {
412            let chunk_start = index.saturating_mul(chunk_size);
413            let chunk_len = chunk_size.min(new_size.saturating_sub(chunk_start));
414            let mut chunk_data = vec![
415                0;
416                usize::try_from(chunk_len).map_err(|_| {
417                    VfsError::einval("chunk length is too large")
418                })?
419            ];
420
421            match &meta.storage {
422                Storage::Inline(data) => {
423                    let copy_start = chunk_start.min(data.len() as u64);
424                    let copy_end = chunk_start.saturating_add(chunk_len).min(data.len() as u64);
425                    if copy_start < copy_end {
426                        let dst = usize::try_from(copy_start - chunk_start)
427                            .map_err(|_| VfsError::einval("inline chunk offset is too large"))?;
428                        let src = usize::try_from(copy_start)
429                            .map_err(|_| VfsError::einval("inline source offset is too large"))?;
430                        let len = usize::try_from(copy_end - copy_start)
431                            .map_err(|_| VfsError::einval("inline copy length is too large"))?;
432                        chunk_data[dst..dst + len].copy_from_slice(&data[src..src + len]);
433                    }
434                }
435                Storage::Chunked { .. } => {
436                    if let Some(key) = existing_chunks.get(&index) {
437                        let old = self.blocks.get(key).await?;
438                        let len = old.len().min(chunk_data.len());
439                        chunk_data[..len].copy_from_slice(&old[..len]);
440                    }
441                }
442                Storage::None => {}
443            }
444
445            let write_start = offset.max(chunk_start);
446            let write_end = end_offset.min(chunk_start.saturating_add(chunk_len));
447            if write_start < write_end {
448                let dst = usize::try_from(write_start - chunk_start)
449                    .map_err(|_| VfsError::einval("chunk write offset is too large"))?;
450                let src = usize::try_from(write_start - offset)
451                    .map_err(|_| VfsError::einval("content write offset is too large"))?;
452                let len = usize::try_from(write_end - write_start)
453                    .map_err(|_| VfsError::einval("chunk write length is too large"))?;
454                chunk_data[dst..dst + len].copy_from_slice(&content[src..src + len]);
455            }
456
457            edits.push(self.put_chunk_edit(index, chunk_data).await?);
458        }
459
460        if !matches!(meta.storage, Storage::Chunked { .. }) {
461            self.metadata
462                .set_attr(
463                    meta.ino,
464                    InodePatch {
465                        storage: Some(Storage::Chunked {
466                            chunk_size: u32::try_from(chunk_size)
467                                .map_err(|_| VfsError::einval("chunk size is too large"))?,
468                        }),
469                        size: Some(new_size),
470                        ..InodePatch::default()
471                    },
472                )
473                .await?;
474        }
475        let freed = self
476            .metadata
477            .commit_write(
478                meta.ino,
479                edits,
480                new_size,
481                allocation_after_write(&meta.allocated_extents, offset, content_len),
482            )
483            .await?;
484        self.blocks.delete_many(&freed).await?;
485        self.set_unwritten_extents(
486            meta,
487            &unwritten_after_write(
488                &decode_unwritten_extents(&meta.xattrs)?,
489                offset,
490                content_len,
491            ),
492        )
493        .await?;
494        Ok(new_size)
495    }
496}
497
498#[async_trait]
499impl<M: MetadataStore, B: BlockStore> VirtualFileSystem for ChunkedFs<M, B> {
500    async fn read_file(&self, path: &str) -> VfsResult<Vec<u8>> {
501        let meta = self.metadata.resolve(path).await?;
502        self.ensure_file(path, &meta)?;
503        let len = usize::try_from(meta.size)
504            .map_err(|_| VfsError::einval(format!("file is too large: {}", meta.size)))?;
505        self.read_file_range(&meta, 0, len).await
506    }
507
508    async fn read_dir(&self, path: &str) -> VfsResult<Vec<String>> {
509        let meta = self.metadata.resolve(path).await?;
510        Ok(self
511            .metadata
512            .list_dir(meta.ino)
513            .await?
514            .into_iter()
515            .map(|entry| entry.name)
516            .collect())
517    }
518
519    async fn read_dir_with_types(&self, path: &str) -> VfsResult<Vec<Dentry>> {
520        let meta = self.metadata.resolve(path).await?;
521        Ok(self
522            .metadata
523            .list_dir(meta.ino)
524            .await?
525            .into_iter()
526            .map(|entry| Dentry {
527                name: entry.name,
528                ino: entry.meta.ino,
529                kind: entry.meta.kind,
530            })
531            .collect())
532    }
533
534    async fn write_file(&self, path: &str, content: &[u8]) -> VfsResult<()> {
535        self.write_existing_or_create(path, content).await
536    }
537
538    async fn create_dir(&self, path: &str) -> VfsResult<()> {
539        let (parent, name) = self.metadata.resolve_parent(path).await?;
540        self.metadata
541            .create(
542                parent.ino,
543                &name,
544                CreateInodeAttrs::directory(
545                    self.options.dir_mode,
546                    self.options.uid,
547                    self.options.gid,
548                ),
549            )
550            .await?;
551        Ok(())
552    }
553
554    async fn mkdir(&self, path: &str, recursive: bool) -> VfsResult<()> {
555        if !recursive {
556            return self.create_dir(path).await;
557        }
558        let normalized = normalize_path(path)?;
559        let mut current = String::new();
560        for part in normalized
561            .trim_start_matches('/')
562            .split('/')
563            .filter(|p| !p.is_empty())
564        {
565            current.push('/');
566            current.push_str(part);
567            if !self.exists(&current).await {
568                self.create_dir(&current).await?;
569            }
570        }
571        Ok(())
572    }
573
574    async fn mknod(&self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
575        let (parent, name) = self.metadata.resolve_parent(path).await?;
576        self.metadata
577            .create(
578                parent.ino,
579                &name,
580                CreateInodeAttrs::special_node(mode, self.options.uid, self.options.gid, rdev)?,
581            )
582            .await?;
583        Ok(())
584    }
585
586    async fn exists(&self, path: &str) -> bool {
587        self.metadata.resolve(path).await.is_ok()
588    }
589
590    async fn stat(&self, path: &str) -> VfsResult<VirtualStat> {
591        Ok(self.metadata.resolve(path).await?.to_stat())
592    }
593
594    async fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
595        Ok(self.metadata.lstat(path).await?.to_stat())
596    }
597
598    async fn remove_file(&self, path: &str) -> VfsResult<()> {
599        let meta = self.metadata.lstat(path).await?;
600        if meta.kind == InodeType::Directory {
601            return Err(VfsError::eisdir(path));
602        }
603        let (parent, name) = self.metadata.resolve_parent(path).await?;
604        let freed = self.metadata.remove(parent.ino, &name).await?;
605        self.blocks.delete_many(&freed).await
606    }
607
608    async fn remove_dir(&self, path: &str) -> VfsResult<()> {
609        let meta = self.metadata.lstat(path).await?;
610        if meta.kind != InodeType::Directory {
611            return Err(VfsError::enotdir(path));
612        }
613        let (parent, name) = self.metadata.resolve_parent(path).await?;
614        let freed = self.metadata.remove(parent.ino, &name).await?;
615        self.blocks.delete_many(&freed).await
616    }
617
618    async fn rename(&self, old_path: &str, new_path: &str) -> VfsResult<()> {
619        let (src_parent, src) = self.metadata.resolve_parent(old_path).await?;
620        let (dst_parent, dst) = self.metadata.resolve_parent(new_path).await?;
621        let freed = self
622            .metadata
623            .rename(src_parent.ino, &src, dst_parent.ino, &dst)
624            .await?;
625        self.blocks.delete_many(&freed).await
626    }
627
628    async fn realpath(&self, path: &str) -> VfsResult<String> {
629        self.metadata.resolve(path).await?;
630        normalize_path(path)
631    }
632
633    async fn symlink(&self, target: &str, link_path: &str) -> VfsResult<()> {
634        let (parent, name) = self.metadata.resolve_parent(link_path).await?;
635        self.metadata
636            .create(
637                parent.ino,
638                &name,
639                CreateInodeAttrs::symlink(target.to_string(), self.options.uid, self.options.gid),
640            )
641            .await?;
642        Ok(())
643    }
644
645    async fn readlink(&self, path: &str) -> VfsResult<String> {
646        let meta = self.metadata.lstat(path).await?;
647        if meta.kind != InodeType::Symlink {
648            return Err(VfsError::einval(format!("not a symlink: {path}")));
649        }
650        Ok(meta.symlink_target.unwrap_or_default())
651    }
652
653    async fn link(&self, old_path: &str, new_path: &str) -> VfsResult<()> {
654        let target = self.metadata.resolve(old_path).await?;
655        let (parent, name) = self.metadata.resolve_parent(new_path).await?;
656        self.metadata.link(parent.ino, &name, target.ino).await
657    }
658
659    async fn chmod(&self, path: &str, mode: u32) -> VfsResult<()> {
660        let meta = self.metadata.resolve(path).await?;
661        self.metadata
662            .set_attr(
663                meta.ino,
664                InodePatch {
665                    mode: Some(mode),
666                    ..InodePatch::default()
667                },
668            )
669            .await?;
670        Ok(())
671    }
672
673    async fn chown(&self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
674        let meta = self.metadata.resolve(path).await?;
675        self.metadata
676            .set_attr(
677                meta.ino,
678                InodePatch {
679                    uid: Some(uid),
680                    gid: Some(gid),
681                    ..InodePatch::default()
682                },
683            )
684            .await?;
685        Ok(())
686    }
687
688    async fn lchown(&self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
689        let meta = self.metadata.lstat(path).await?;
690        self.metadata
691            .set_attr(
692                meta.ino,
693                InodePatch {
694                    uid: Some(uid),
695                    gid: Some(gid),
696                    ..InodePatch::default()
697                },
698            )
699            .await?;
700        Ok(())
701    }
702
703    async fn get_xattr(&self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
704        validate_xattr_name(name)?;
705        let meta = if follow_symlinks {
706            self.metadata.resolve(path).await?
707        } else {
708            self.metadata.lstat(path).await?
709        };
710        meta.xattrs.get(name).cloned().ok_or_else(|| {
711            VfsError::new(
712                "ENODATA",
713                format!("extended attribute does not exist: {name}"),
714            )
715        })
716    }
717
718    async fn list_xattrs(&self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
719        let meta = if follow_symlinks {
720            self.metadata.resolve(path).await?
721        } else {
722            self.metadata.lstat(path).await?
723        };
724        Ok(meta
725            .xattrs
726            .into_keys()
727            .filter(|name| !name.starts_with(INTERNAL_XATTR_PREFIX))
728            .collect())
729    }
730
731    async fn set_xattr(
732        &self,
733        path: &str,
734        name: &str,
735        value: &[u8],
736        flags: u32,
737        follow_symlinks: bool,
738    ) -> VfsResult<()> {
739        let meta = if follow_symlinks {
740            self.metadata.resolve(path).await?
741        } else {
742            self.metadata.lstat(path).await?
743        };
744        let mut xattrs = meta.xattrs;
745        set_xattr_value(&mut xattrs, name, value, flags)?;
746        self.metadata
747            .set_attr(
748                meta.ino,
749                InodePatch {
750                    xattrs: Some(xattrs),
751                    ..InodePatch::default()
752                },
753            )
754            .await?;
755        Ok(())
756    }
757
758    async fn remove_xattr(&self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
759        validate_xattr_name(name)?;
760        let meta = if follow_symlinks {
761            self.metadata.resolve(path).await?
762        } else {
763            self.metadata.lstat(path).await?
764        };
765        let mut xattrs = meta.xattrs;
766        if xattrs.remove(name).is_none() {
767            return Err(VfsError::new(
768                "ENODATA",
769                format!("extended attribute does not exist: {name}"),
770            ));
771        }
772        self.metadata
773            .set_attr(
774                meta.ino,
775                InodePatch {
776                    xattrs: Some(xattrs),
777                    ..InodePatch::default()
778                },
779            )
780            .await?;
781        Ok(())
782    }
783
784    async fn utimes(&self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
785        let meta = self.metadata.resolve(path).await?;
786        self.metadata
787            .set_attr(
788                meta.ino,
789                InodePatch {
790                    atime: Some(ms_to_timespec(atime_ms)),
791                    mtime: Some(ms_to_timespec(mtime_ms)),
792                    ..InodePatch::default()
793                },
794            )
795            .await?;
796        Ok(())
797    }
798
799    async fn set_atime(&self, path: &str, atime_ms: u64) -> VfsResult<()> {
800        let meta = self.metadata.resolve(path).await?;
801        self.metadata
802            .set_attr(
803                meta.ino,
804                InodePatch {
805                    atime: Some(ms_to_timespec(atime_ms)),
806                    ..InodePatch::default()
807                },
808            )
809            .await?;
810        Ok(())
811    }
812
813    async fn truncate(&self, path: &str, length: u64) -> VfsResult<()> {
814        let meta = self.metadata.resolve(path).await?;
815        self.ensure_file(path, &meta)?;
816        if length == meta.size {
817            return Ok(());
818        }
819
820        if usize::try_from(length)
821            .ok()
822            .is_some_and(|len| len <= self.options.inline_threshold)
823        {
824            let data = self
825                .read_file_range(&meta, 0, usize::try_from(length).unwrap_or(0))
826                .await?;
827            let mut xattrs = meta.xattrs.clone();
828            encode_unwritten_extents(
829                &mut xattrs,
830                &unwritten_after_truncate(&decode_unwritten_extents(&meta.xattrs)?, length),
831            );
832            let freed = self
833                .metadata
834                .set_attr(
835                    meta.ino,
836                    InodePatch {
837                        storage: Some(Storage::Inline(data)),
838                        size: Some(length),
839                        allocated_extents: Some(allocation_after_truncate(
840                            &meta.allocated_extents,
841                            length,
842                        )),
843                        xattrs: Some(xattrs),
844                        ..InodePatch::default()
845                    },
846                )
847                .await?;
848            self.blocks.delete_many(&freed).await?;
849            return Ok(());
850        }
851
852        let chunk_size = u64::from(self.file_chunk_size(&meta.storage));
853        let mut edits = Vec::new();
854        if !matches!(meta.storage, Storage::Chunked { .. }) {
855            let existing_len = meta.size.min(length);
856            let mut offset = 0;
857            while offset < existing_len {
858                let len = (existing_len - offset).min(chunk_size);
859                let data = self
860                    .read_file_range(
861                        &meta,
862                        offset,
863                        usize::try_from(len)
864                            .map_err(|_| VfsError::einval("truncate chunk is too large"))?,
865                    )
866                    .await?;
867                edits.push(self.put_chunk_edit(offset / chunk_size, data).await?);
868                offset = offset.saturating_add(len);
869            }
870            self.metadata
871                .set_attr(
872                    meta.ino,
873                    InodePatch {
874                        storage: Some(Storage::Chunked {
875                            chunk_size: self.options.chunk_size,
876                        }),
877                        size: Some(length),
878                        ..InodePatch::default()
879                    },
880                )
881                .await?;
882        } else if length < meta.size && !length.is_multiple_of(chunk_size) {
883            let final_index = length / chunk_size;
884            let final_start = final_index.saturating_mul(chunk_size);
885            let final_len = length - final_start;
886            let data = self
887                .read_file_range(
888                    &meta,
889                    final_start,
890                    usize::try_from(final_len)
891                        .map_err(|_| VfsError::einval("truncate final chunk is too large"))?,
892                )
893                .await?;
894            edits.push(self.put_chunk_edit(final_index, data).await?);
895        }
896
897        let freed = self
898            .metadata
899            .commit_write(
900                meta.ino,
901                edits,
902                length,
903                allocation_after_truncate(&meta.allocated_extents, length),
904            )
905            .await?;
906        self.blocks.delete_many(&freed).await?;
907        self.set_unwritten_extents(
908            &meta,
909            &unwritten_after_truncate(&decode_unwritten_extents(&meta.xattrs)?, length),
910        )
911        .await
912    }
913
914    async fn allocate(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
915        let end = offset
916            .checked_add(length)
917            .ok_or_else(|| VfsError::einval("allocation range overflows"))?;
918        let meta = self.metadata.resolve(path).await?;
919        self.ensure_file(path, &meta)?;
920        let mut xattrs = meta.xattrs.clone();
921        encode_unwritten_extents(
922            &mut xattrs,
923            &unwritten_after_allocate(
924                &decode_unwritten_extents(&meta.xattrs)?,
925                &meta.allocated_extents,
926                offset,
927                length,
928            ),
929        );
930        let freed = self
931            .metadata
932            .set_attr(
933                meta.ino,
934                InodePatch {
935                    size: Some(meta.size.max(end)),
936                    allocated_extents: Some(allocation_after_write(
937                        &meta.allocated_extents,
938                        offset,
939                        length,
940                    )),
941                    xattrs: Some(xattrs),
942                    ..InodePatch::default()
943                },
944            )
945            .await?;
946        self.blocks.delete_many(&freed).await
947    }
948
949    async fn punch_hole(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
950        const PUNCH_CHUNK_BYTES: u64 = 64 * 1024;
951
952        let requested_end = offset
953            .checked_add(length)
954            .ok_or_else(|| VfsError::einval("hole-punch range overflows"))?;
955        let meta = self.metadata.resolve(path).await?;
956        self.ensure_file(path, &meta)?;
957        let end = requested_end.min(meta.size);
958        let mut cursor = offset.min(meta.size);
959        while cursor < end {
960            let chunk_len = usize::try_from((end - cursor).min(PUNCH_CHUNK_BYTES))
961                .map_err(|_| VfsError::einval("hole-punch chunk is too large"))?;
962            self.write_chunked_range(&meta, &vec![0; chunk_len], cursor)
963                .await?;
964            cursor += chunk_len as u64;
965        }
966        let freed = self
967            .metadata
968            .set_attr(
969                meta.ino,
970                InodePatch {
971                    allocated_extents: Some(allocation_after_punch(
972                        &meta.allocated_extents,
973                        offset,
974                        length,
975                    )),
976                    xattrs: Some({
977                        let mut xattrs = meta.xattrs.clone();
978                        encode_unwritten_extents(
979                            &mut xattrs,
980                            &unwritten_after_write(
981                                &decode_unwritten_extents(&meta.xattrs)?,
982                                offset,
983                                length,
984                            ),
985                        );
986                        xattrs
987                    }),
988                    ..InodePatch::default()
989                },
990            )
991            .await?;
992        self.blocks.delete_many(&freed).await
993    }
994
995    async fn zero_range(
996        &self,
997        path: &str,
998        offset: u64,
999        length: u64,
1000        keep_size: bool,
1001    ) -> VfsResult<()> {
1002        const ZERO_CHUNK_BYTES: u64 = 64 * 1024;
1003
1004        let end = offset
1005            .checked_add(length)
1006            .ok_or_else(|| VfsError::einval("zero range overflows"))?;
1007        if length == 0 {
1008            return Err(VfsError::einval("zero range length must be nonzero"));
1009        }
1010        let original = self.metadata.resolve(path).await?;
1011        self.ensure_file(path, &original)?;
1012        let zero_end = if keep_size {
1013            end.min(original.size)
1014        } else {
1015            end
1016        };
1017        let mut cursor = offset.min(zero_end);
1018        while cursor < zero_end {
1019            let chunk_len = usize::try_from((zero_end - cursor).min(ZERO_CHUNK_BYTES))
1020                .map_err(|_| VfsError::einval("zero range chunk is too large"))?;
1021            self.pwrite(path, &vec![0; chunk_len], cursor).await?;
1022            cursor += chunk_len as u64;
1023        }
1024        let updated = self.metadata.resolve(path).await?;
1025        let freed = self
1026            .metadata
1027            .set_attr(
1028                updated.ino,
1029                InodePatch {
1030                    size: keep_size.then_some(original.size),
1031                    allocated_extents: Some(allocation_after_write(
1032                        &updated.allocated_extents,
1033                        offset,
1034                        length,
1035                    )),
1036                    xattrs: Some({
1037                        let mut xattrs = original.xattrs.clone();
1038                        encode_unwritten_extents(
1039                            &mut xattrs,
1040                            &unwritten_after_zero(
1041                                &decode_unwritten_extents(&original.xattrs)?,
1042                                &original.allocated_extents,
1043                                offset,
1044                                length,
1045                            ),
1046                        );
1047                        xattrs
1048                    }),
1049                    ..InodePatch::default()
1050                },
1051            )
1052            .await?;
1053        self.blocks.delete_many(&freed).await
1054    }
1055
1056    async fn insert_range(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1057        validate_shift_range(offset, length)?;
1058        let meta = self.metadata.resolve(path).await?;
1059        self.ensure_file(path, &meta)?;
1060        if offset >= meta.size {
1061            return Err(VfsError::einval("insert range offset must be before EOF"));
1062        }
1063        let mut data = self
1064            .read_file_range(
1065                &meta,
1066                0,
1067                usize::try_from(meta.size)
1068                    .map_err(|_| VfsError::einval("insert source is too large"))?,
1069            )
1070            .await?;
1071        let start = usize::try_from(offset)
1072            .map_err(|_| VfsError::einval("insert range offset is too large"))?;
1073        let insert_len = usize::try_from(length)
1074            .map_err(|_| VfsError::einval("insert range length is too large"))?;
1075        data.splice(start..start, std::iter::repeat_n(0, insert_len));
1076        let extents = allocation_after_insert(&meta.allocated_extents, offset, length);
1077        let unwritten =
1078            unwritten_after_insert(&decode_unwritten_extents(&meta.xattrs)?, offset, length);
1079        self.write_existing_or_create(path, &data).await?;
1080        let updated = self.metadata.resolve(path).await?;
1081        let freed = self
1082            .metadata
1083            .set_attr(
1084                updated.ino,
1085                InodePatch {
1086                    allocated_extents: Some(extents),
1087                    xattrs: Some({
1088                        let mut xattrs = meta.xattrs.clone();
1089                        encode_unwritten_extents(&mut xattrs, &unwritten);
1090                        xattrs
1091                    }),
1092                    ..InodePatch::default()
1093                },
1094            )
1095            .await?;
1096        self.blocks.delete_many(&freed).await
1097    }
1098
1099    async fn collapse_range(&self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1100        validate_shift_range(offset, length)?;
1101        let end = offset
1102            .checked_add(length)
1103            .ok_or_else(|| VfsError::einval("collapse range overflows"))?;
1104        let meta = self.metadata.resolve(path).await?;
1105        self.ensure_file(path, &meta)?;
1106        if end >= meta.size {
1107            return Err(VfsError::einval("collapse range must end before EOF"));
1108        }
1109        let mut data = self
1110            .read_file_range(
1111                &meta,
1112                0,
1113                usize::try_from(meta.size)
1114                    .map_err(|_| VfsError::einval("collapse source is too large"))?,
1115            )
1116            .await?;
1117        let start = usize::try_from(offset)
1118            .map_err(|_| VfsError::einval("collapse range offset is too large"))?;
1119        let end = usize::try_from(end)
1120            .map_err(|_| VfsError::einval("collapse range end is too large"))?;
1121        data.drain(start..end);
1122        let extents = allocation_after_collapse(&meta.allocated_extents, offset, length);
1123        let unwritten =
1124            unwritten_after_collapse(&decode_unwritten_extents(&meta.xattrs)?, offset, length);
1125        self.write_existing_or_create(path, &data).await?;
1126        let updated = self.metadata.resolve(path).await?;
1127        let freed = self
1128            .metadata
1129            .set_attr(
1130                updated.ino,
1131                InodePatch {
1132                    allocated_extents: Some(extents),
1133                    xattrs: Some({
1134                        let mut xattrs = meta.xattrs.clone();
1135                        encode_unwritten_extents(&mut xattrs, &unwritten);
1136                        xattrs
1137                    }),
1138                    ..InodePatch::default()
1139                },
1140            )
1141            .await?;
1142        self.blocks.delete_many(&freed).await
1143    }
1144
1145    async fn allocated_ranges(&self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
1146        let meta = self.metadata.resolve(path).await?;
1147        self.ensure_file(path, &meta)?;
1148        Ok(allocation_byte_ranges(&meta.allocated_extents, meta.size))
1149    }
1150
1151    async fn unwritten_ranges(&self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
1152        let meta = self.metadata.resolve(path).await?;
1153        self.ensure_file(path, &meta)?;
1154        unwritten_byte_ranges(&meta.xattrs, meta.size)
1155    }
1156
1157    async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1158        let meta = self.metadata.resolve(path).await?;
1159        self.ensure_file(path, &meta)?;
1160        self.read_file_range(&meta, offset, length).await
1161    }
1162
1163    async fn pwrite(&self, path: &str, content: &[u8], offset: u64) -> VfsResult<()> {
1164        let meta = self.metadata.resolve(path).await?;
1165        self.ensure_file(path, &meta)?;
1166        self.write_chunked_range(&meta, content, offset).await?;
1167        Ok(())
1168    }
1169
1170    async fn append(&self, path: &str, content: &[u8]) -> VfsResult<u64> {
1171        let meta = self.metadata.resolve(path).await?;
1172        self.ensure_file(path, &meta)?;
1173        let len = meta
1174            .size
1175            .checked_add(u64::try_from(content.len()).map_err(|_| {
1176                VfsError::einval(format!("append content is too large: {}", content.len()))
1177            })?)
1178            .ok_or_else(|| VfsError::einval("append size overflows"))?;
1179        self.write_chunked_range(&meta, content, meta.size).await?;
1180        Ok(len)
1181    }
1182
1183    async fn sync(&self, _path: &str) -> VfsResult<()> {
1184        self.blocks.sync().await?;
1185        self.metadata.flush().await
1186    }
1187}
1188
1189#[async_trait]
1190impl<M: MetadataStore, B: BlockStore> Snapshottable for ChunkedFs<M, B> {
1191    async fn snapshot(&self, root: u64) -> VfsResult<SnapshotId> {
1192        self.metadata.snapshot(root).await
1193    }
1194
1195    async fn fork(&self, snap: SnapshotId) -> VfsResult<u64> {
1196        self.metadata.fork(snap).await
1197    }
1198}
1199
1200fn ms_to_timespec(ms: u64) -> Timespec {
1201    Timespec {
1202        sec: (ms / 1_000) as i64,
1203        nsec: ((ms % 1_000) * 1_000_000) as u32,
1204    }
1205}
1206
1207fn dense_allocation(size: u64) -> Vec<(u64, u64)> {
1208    if size == 0 {
1209        Vec::new()
1210    } else {
1211        vec![(0, size.div_ceil(512))]
1212    }
1213}
1214
1215fn allocation_after_write(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
1216    if length == 0 {
1217        return existing.to_vec();
1218    }
1219    let mut pending = (offset / 512, offset.saturating_add(length).div_ceil(512));
1220    let mut merged = Vec::with_capacity(existing.len() + 1);
1221    for &(start, end) in existing {
1222        if end < pending.0 {
1223            merged.push((start, end));
1224        } else if pending.1 < start {
1225            merged.push(pending);
1226            pending = (start, end);
1227        } else {
1228            pending.0 = pending.0.min(start);
1229            pending.1 = pending.1.max(end);
1230        }
1231    }
1232    merged.push(pending);
1233    merged
1234}
1235
1236fn allocation_after_truncate(existing: &[(u64, u64)], size: u64) -> Vec<(u64, u64)> {
1237    let end = size.div_ceil(512);
1238    existing
1239        .iter()
1240        .filter_map(|(start, extent_end)| {
1241            let extent_end = (*extent_end).min(end);
1242            (*start < extent_end).then_some((*start, extent_end))
1243        })
1244        .collect()
1245}
1246
1247fn allocation_after_punch(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
1248    let start = offset.div_ceil(512);
1249    let end = offset.saturating_add(length) / 512;
1250    if start >= end {
1251        return existing.to_vec();
1252    }
1253    existing
1254        .iter()
1255        .flat_map(|&(extent_start, extent_end)| {
1256            [
1257                (extent_start, extent_end.min(start)),
1258                (extent_start.max(end), extent_end),
1259            ]
1260            .into_iter()
1261            .filter(|(part_start, part_end)| part_start < part_end)
1262        })
1263        .collect()
1264}
1265
1266fn validate_shift_range(offset: u64, length: u64) -> VfsResult<()> {
1267    if length == 0 || !offset.is_multiple_of(512) || !length.is_multiple_of(512) {
1268        return Err(VfsError::einval(
1269            "insert/collapse range requires a nonzero 512-byte-aligned range",
1270        ));
1271    }
1272    Ok(())
1273}
1274
1275fn allocation_after_insert(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
1276    let start = offset / 512;
1277    let shift = length / 512;
1278    normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
1279        if extent_end <= start {
1280            vec![(extent_start, extent_end)]
1281        } else if extent_start >= start {
1282            vec![(extent_start + shift, extent_end + shift)]
1283        } else {
1284            vec![(extent_start, start), (start + shift, extent_end + shift)]
1285        }
1286    }))
1287}
1288
1289fn allocation_after_collapse(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
1290    let start = offset / 512;
1291    let end = start + length / 512;
1292    normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
1293        let mut parts = Vec::with_capacity(2);
1294        if extent_start < start {
1295            parts.push((extent_start, extent_end.min(start)));
1296        }
1297        if extent_end > end {
1298            parts.push((
1299                extent_start.max(end) - (end - start),
1300                extent_end - (end - start),
1301            ));
1302        }
1303        parts
1304    }))
1305}
1306
1307fn normalize_extents(extents: impl IntoIterator<Item = (u64, u64)>) -> Vec<(u64, u64)> {
1308    let mut merged = Vec::<(u64, u64)>::new();
1309    for (start, end) in extents.into_iter().filter(|(start, end)| start < end) {
1310        if let Some(last) = merged.last_mut().filter(|last| start <= last.1) {
1311            last.1 = last.1.max(end);
1312        } else {
1313            merged.push((start, end));
1314        }
1315    }
1316    merged
1317}
1318
1319fn allocation_byte_ranges(extents: &[(u64, u64)], size: u64) -> Vec<(u64, u64)> {
1320    extents
1321        .iter()
1322        .filter_map(|&(start, end)| {
1323            let start = start.saturating_mul(512).min(size);
1324            let end = end.saturating_mul(512).min(size);
1325            (start < end).then_some((start, end))
1326        })
1327        .collect()
1328}