Skip to main content

hashtree_fuse/
lib.rs

1use std::collections::{HashMap, HashSet};
2use std::hash::{Hash as StdHash, Hasher};
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, RwLock};
5
6use futures::executor::block_on;
7use hashtree_core::{
8    sha256, to_hex, Cid, HashTree, HashTreeConfig, HashTreeError, LinkType, Store,
9};
10use thiserror::Error;
11
12pub const ROOT_INODE: u64 = 1;
13// Virtual entry used only to wake Linux directory monitors after remote root updates.
14pub const DIRECTORY_REFRESH_SENTINEL_NAME: &str = "iris-drive-refresh";
15const WHOLE_FILE_HASH_META_KEY: &str = "whole_file_hash";
16
17#[derive(Debug, Error)]
18pub enum FsError {
19    #[error("root hash is not a directory")]
20    InvalidRoot,
21    #[error("entry not found")]
22    NotFound,
23    #[error("not a directory")]
24    NotDir,
25    #[error("is a directory")]
26    IsDir,
27    #[error("entry already exists")]
28    AlreadyExists,
29    #[error("directory not empty")]
30    NotEmpty,
31    #[error("invalid entry name")]
32    InvalidName,
33    #[error("tree error: {0}")]
34    Tree(String),
35    #[error("publish error: {0}")]
36    Publish(String),
37}
38
39impl From<HashTreeError> for FsError {
40    fn from(err: HashTreeError) -> Self {
41        FsError::Tree(err.to_string())
42    }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum EntryKind {
47    File,
48    Directory,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct EntryAttr {
53    pub inode: u64,
54    pub size: u64,
55    pub kind: EntryKind,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DirEntry {
60    pub inode: u64,
61    pub name: String,
62    pub kind: EntryKind,
63}
64
65pub trait RootPublisher: Send + Sync {
66    fn publish(&self, cid: &Cid) -> Result<(), FsError>;
67}
68
69#[derive(Debug, Clone, Eq)]
70struct ChildKey {
71    parent: u64,
72    name: String,
73}
74
75impl PartialEq for ChildKey {
76    fn eq(&self, other: &Self) -> bool {
77        self.parent == other.parent && self.name == other.name
78    }
79}
80
81impl StdHash for ChildKey {
82    fn hash<H: Hasher>(&self, state: &mut H) {
83        self.parent.hash(state);
84        self.name.hash(state);
85    }
86}
87
88#[derive(Debug, Clone, PartialEq)]
89struct ResolvedEntry {
90    cid: Cid,
91    link_type: LinkType,
92    size: u64,
93    meta: Option<HashMap<String, serde_json::Value>>,
94}
95
96#[cfg(feature = "fuse")]
97#[derive(Debug, Clone, PartialEq, Eq)]
98enum FuseInvalidation {
99    Entry {
100        parent: u64,
101        name: String,
102    },
103    Delete {
104        parent: u64,
105        child: u64,
106        name: String,
107    },
108}
109
110pub struct HashtreeFuseInner<S: Store> {
111    tree: HashTree<S>,
112    root: RwLock<Cid>,
113    paths: RwLock<HashMap<u64, Vec<String>>>,
114    children: RwLock<HashMap<ChildKey, u64>>,
115    parents: RwLock<HashMap<u64, u64>>,
116    next_inode: AtomicU64,
117    publisher: Option<Arc<dyn RootPublisher>>,
118    refresh_unlinks: Mutex<HashSet<Vec<String>>>,
119    modify_lock: Mutex<()>,
120    #[cfg(feature = "fuse")]
121    notifier: Mutex<Option<fuser::Notifier>>,
122}
123
124pub struct HashtreeFuse<S: Store> {
125    inner: Arc<HashtreeFuseInner<S>>,
126}
127
128impl<S: Store> Clone for HashtreeFuse<S> {
129    fn clone(&self) -> Self {
130        Self {
131            inner: self.inner.clone(),
132        }
133    }
134}
135
136impl<S: Store> std::ops::Deref for HashtreeFuse<S> {
137    type Target = HashtreeFuseInner<S>;
138
139    fn deref(&self) -> &Self::Target {
140        &self.inner
141    }
142}
143
144impl<S: Store> HashtreeFuse<S> {
145    pub fn new(store: Arc<S>, root: Cid) -> Result<Self, FsError> {
146        Self::new_with_publisher(store, root, None)
147    }
148
149    pub fn new_with_publisher(
150        store: Arc<S>,
151        root: Cid,
152        publisher: Option<Arc<dyn RootPublisher>>,
153    ) -> Result<Self, FsError> {
154        let mut config = HashTreeConfig::new(store);
155        if root.key.is_none() {
156            config = config.public();
157        }
158        let tree = HashTree::new(config);
159
160        let is_dir = block_on(tree.get_directory_node(&root))?.is_some();
161        if !is_dir {
162            return Err(FsError::InvalidRoot);
163        }
164
165        let mut paths = HashMap::new();
166        paths.insert(ROOT_INODE, Vec::new());
167        let mut parents = HashMap::new();
168        parents.insert(ROOT_INODE, ROOT_INODE);
169
170        Ok(Self {
171            inner: Arc::new(HashtreeFuseInner {
172                tree,
173                root: RwLock::new(root),
174                paths: RwLock::new(paths),
175                children: RwLock::new(HashMap::new()),
176                parents: RwLock::new(parents),
177                next_inode: AtomicU64::new(ROOT_INODE + 1),
178                publisher,
179                refresh_unlinks: Mutex::new(HashSet::new()),
180                modify_lock: Mutex::new(()),
181                #[cfg(feature = "fuse")]
182                notifier: Mutex::new(None),
183            }),
184        })
185    }
186
187    pub fn current_root(&self) -> Cid {
188        self.root.read().unwrap().clone()
189    }
190
191    pub fn replace_root(&self, root: Cid) -> Result<(), FsError> {
192        let _guard = self.modify_lock.lock().unwrap();
193        self.replace_root_locked(root)
194    }
195
196    pub fn replace_root_if_current(
197        &self,
198        expected_current: &Cid,
199        root: Cid,
200    ) -> Result<bool, FsError> {
201        let _guard = self.modify_lock.lock().unwrap();
202        if self.current_root() != *expected_current {
203            return Ok(false);
204        }
205        self.replace_root_locked(root)?;
206        Ok(true)
207    }
208
209    fn replace_root_locked(&self, root: Cid) -> Result<(), FsError> {
210        let is_dir = block_on(self.tree.get_directory_node(&root))?.is_some();
211        if !is_dir {
212            return Err(FsError::InvalidRoot);
213        }
214
215        #[cfg(feature = "fuse")]
216        let invalidations = self.changed_known_entries_for_root(&root);
217        #[cfg(feature = "fuse")]
218        let paths_needing_new_inode = self.changed_known_paths_requiring_new_inode_for_root(&root);
219
220        *self.root.write().unwrap() = root;
221
222        self.retain_existing_paths_after_root_update()?;
223        #[cfg(feature = "fuse")]
224        self.drop_paths(paths_needing_new_inode);
225
226        #[cfg(feature = "fuse")]
227        self.notify_entries_invalidated(&invalidations);
228
229        Ok(())
230    }
231
232    #[cfg(feature = "fuse")]
233    pub fn removed_known_entry_paths_for_root(&self, root: &Cid) -> Vec<Vec<String>> {
234        self.changed_known_entries_for_root(root)
235            .into_iter()
236            .filter_map(|invalidation| match invalidation {
237                FuseInvalidation::Delete { parent, name, .. } => {
238                    let mut path = self.path_for_inode(parent).ok()?;
239                    path.push(name);
240                    Some(path)
241                }
242                _ => None,
243            })
244            .collect()
245    }
246
247    #[cfg(not(feature = "fuse"))]
248    pub fn removed_known_entry_paths_for_root(&self, _root: &Cid) -> Vec<Vec<String>> {
249        Vec::new()
250    }
251
252    pub fn begin_refresh_unlinks(&self, paths: impl IntoIterator<Item = Vec<String>>) {
253        self.refresh_unlinks.lock().unwrap().extend(paths);
254    }
255
256    pub fn clear_refresh_unlinks(&self) {
257        self.refresh_unlinks.lock().unwrap().clear();
258    }
259
260    #[cfg(feature = "fuse")]
261    fn set_notifier(&self, notifier: fuser::Notifier) {
262        *self.notifier.lock().unwrap() = Some(notifier);
263    }
264
265    #[cfg(feature = "fuse")]
266    fn clear_notifier(&self) {
267        *self.notifier.lock().unwrap() = None;
268    }
269
270    #[cfg(feature = "fuse")]
271    fn notify_entries_invalidated(&self, invalidations: &[FuseInvalidation]) {
272        let guard = self.notifier.lock().unwrap();
273        let Some(notifier) = guard.as_ref() else {
274            return;
275        };
276        for invalidation in invalidations {
277            match invalidation {
278                FuseInvalidation::Entry { parent, name } => {
279                    let _ = notifier.inval_entry(*parent, std::ffi::OsStr::new(name));
280                }
281                FuseInvalidation::Delete {
282                    parent,
283                    child,
284                    name,
285                } => {
286                    let _ = notifier.delete(*parent, *child, std::ffi::OsStr::new(name));
287                }
288            }
289        }
290    }
291
292    #[cfg(feature = "fuse")]
293    fn changed_known_entries_for_root(&self, new_root: &Cid) -> Vec<FuseInvalidation> {
294        let old_root = self.current_root();
295        let paths = self.paths.read().unwrap().clone();
296        let children = self.children.read().unwrap().clone();
297        let mut invalidations = Vec::new();
298
299        for (inode, path) in paths {
300            let old_entries = self.directory_entry_names_at_root(&old_root, &path);
301            let new_entries = self.directory_entry_names_at_root(new_root, &path);
302            if old_entries.is_none() && new_entries.is_none() {
303                continue;
304            }
305
306            let old_entries = old_entries.unwrap_or_default();
307            let new_entries = new_entries.unwrap_or_default();
308            for removed in old_entries.difference(&new_entries) {
309                invalidations.push(FuseInvalidation::Entry {
310                    parent: inode,
311                    name: removed.clone(),
312                });
313                if let Some(child) = children
314                    .get(&ChildKey {
315                        parent: inode,
316                        name: removed.clone(),
317                    })
318                    .copied()
319                {
320                    invalidations.push(FuseInvalidation::Delete {
321                        parent: inode,
322                        child,
323                        name: removed.clone(),
324                    });
325                }
326            }
327
328            for retained in old_entries.intersection(&new_entries) {
329                if self.entry_changed_between_roots(&old_root, new_root, &path, retained) {
330                    invalidations.push(FuseInvalidation::Entry {
331                        parent: inode,
332                        name: retained.clone(),
333                    });
334                }
335            }
336
337            for added in new_entries.difference(&old_entries) {
338                invalidations.push(FuseInvalidation::Entry {
339                    parent: inode,
340                    name: added.clone(),
341                });
342            }
343        }
344
345        invalidations
346    }
347
348    #[cfg(feature = "fuse")]
349    fn changed_known_paths_requiring_new_inode_for_root(&self, new_root: &Cid) -> Vec<Vec<String>> {
350        let old_root = self.current_root();
351        let paths: Vec<Vec<String>> = self.paths.read().unwrap().values().cloned().collect();
352
353        paths
354            .into_iter()
355            .filter(|path| {
356                !path.is_empty()
357                    && !Self::is_directory_refresh_sentinel_path(path)
358                    && self.entry_path_changed_between_roots(&old_root, new_root, path)
359                    && self.changed_entry_should_get_new_inode(&old_root, new_root, path)
360            })
361            .collect()
362    }
363
364    #[cfg(feature = "fuse")]
365    fn changed_entry_should_get_new_inode(
366        &self,
367        old_root: &Cid,
368        new_root: &Cid,
369        path: &[String],
370    ) -> bool {
371        match (
372            self.resolve_entry_at_root(old_root, path),
373            self.resolve_entry_at_root(new_root, path),
374        ) {
375            (Ok(old), Ok(new)) => {
376                !old.link_type.is_directory_like() || !new.link_type.is_directory_like()
377            }
378            _ => true,
379        }
380    }
381
382    #[cfg(feature = "fuse")]
383    fn entry_path_changed_between_roots(
384        &self,
385        old_root: &Cid,
386        new_root: &Cid,
387        path: &[String],
388    ) -> bool {
389        match (
390            self.resolve_entry_at_root(old_root, path),
391            self.resolve_entry_at_root(new_root, path),
392        ) {
393            (Ok(old), Ok(new)) => old != new,
394            (Err(_), Err(_)) => false,
395            _ => true,
396        }
397    }
398
399    #[cfg(feature = "fuse")]
400    fn entry_changed_between_roots(
401        &self,
402        old_root: &Cid,
403        new_root: &Cid,
404        parent_path: &[String],
405        name: &str,
406    ) -> bool {
407        let mut child_path = parent_path.to_vec();
408        child_path.push(name.to_string());
409        match (
410            self.resolve_entry_at_root(old_root, &child_path),
411            self.resolve_entry_at_root(new_root, &child_path),
412        ) {
413            (Ok(old), Ok(new)) => old != new,
414            (Err(_), Err(_)) => false,
415            _ => true,
416        }
417    }
418
419    pub fn lookup_child(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
420        if name.is_empty() {
421            return Err(FsError::NotFound);
422        }
423        if name == "." {
424            return self.get_attr(parent);
425        }
426        if name == ".." {
427            let parent_inode = self.parent_inode(parent);
428            return self.get_attr(parent_inode);
429        }
430        if Self::is_directory_refresh_sentinel(name) {
431            if self.get_attr(parent)?.kind != EntryKind::Directory {
432                return Err(FsError::NotDir);
433            }
434            let inode = self.get_or_create_child_inode(parent, name)?;
435            return Ok(Self::directory_refresh_sentinel_attr(inode));
436        }
437
438        let child_inode = self.get_or_create_child_inode(parent, name)?;
439        let path = self.path_for_inode(child_inode)?;
440
441        match self.resolve_entry(&path) {
442            Ok(entry) => self.entry_attr_from_resolved(child_inode, entry),
443            Err(FsError::NotFound) => {
444                self.drop_inode(child_inode);
445                Err(FsError::NotFound)
446            }
447            Err(err) => Err(err),
448        }
449    }
450
451    pub fn get_attr(&self, inode: u64) -> Result<EntryAttr, FsError> {
452        if inode == ROOT_INODE {
453            return Ok(EntryAttr {
454                inode,
455                size: 0,
456                kind: EntryKind::Directory,
457            });
458        }
459
460        let path = self.path_for_inode(inode)?;
461        if Self::is_directory_refresh_sentinel_path(&path) {
462            let parent_path = &path[..path.len() - 1];
463            self.resolve_dir_cid(parent_path)?;
464            return Ok(Self::directory_refresh_sentinel_attr(inode));
465        }
466        let entry = self.resolve_entry(&path)?;
467        self.entry_attr_from_resolved(inode, entry)
468    }
469
470    pub fn read_file(&self, inode: u64, offset: u64, size: u32) -> Result<Vec<u8>, FsError> {
471        let path = self.path_for_inode(inode)?;
472        if Self::is_directory_refresh_sentinel_path(&path) {
473            return Ok(Vec::new());
474        }
475        let entry = self.resolve_entry(&path)?;
476        if entry.link_type.is_directory_like() {
477            return Err(FsError::IsDir);
478        }
479
480        let file_size = self.entry_size(&entry)?;
481        if offset >= file_size {
482            return Ok(vec![]);
483        }
484        let read_len = (size as u64).min(file_size - offset);
485        if read_len == 0 {
486            return Ok(vec![]);
487        }
488
489        if entry.cid.key.is_some() {
490            let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
491            let start = usize::try_from(offset).unwrap_or(usize::MAX);
492            if start >= data.len() {
493                return Ok(vec![]);
494            }
495            let end_u64 = offset.saturating_add(read_len);
496            let mut end = usize::try_from(end_u64).unwrap_or(data.len());
497            if end > data.len() {
498                end = data.len();
499            }
500            return Ok(data[start..end].to_vec());
501        }
502
503        let end = offset.saturating_add(read_len);
504        let data = block_on(
505            self.tree
506                .read_file_range(&entry.cid.hash, offset, Some(end)),
507        )?
508        .ok_or(FsError::NotFound)?;
509        Ok(data)
510    }
511
512    pub fn read_dir(&self, inode: u64) -> Result<Vec<DirEntry>, FsError> {
513        let path = self.path_for_inode(inode)?;
514        let dir_cid = self.resolve_dir_cid(&path)?;
515
516        let entries = block_on(self.tree.list_directory(&dir_cid))?;
517        let mut out = Vec::with_capacity(entries.len());
518
519        for entry in entries {
520            if Self::is_directory_refresh_sentinel(&entry.name) {
521                continue;
522            }
523            let child_inode = self.get_or_create_child_inode(inode, &entry.name)?;
524            out.push(DirEntry {
525                inode: child_inode,
526                name: entry.name,
527                kind: Self::kind_from_link(entry.link_type),
528            });
529        }
530
531        Ok(out)
532    }
533
534    pub fn create_file(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
535        self.ensure_valid_name(name)?;
536        let _guard = self.modify_lock.lock().unwrap();
537
538        let parent_path = self.path_for_inode(parent)?;
539        let mut child_path = parent_path.clone();
540        child_path.push(name.to_string());
541
542        if self.resolve_entry(&child_path).is_ok() {
543            return Err(FsError::AlreadyExists);
544        }
545
546        let (cid, size) = block_on(self.tree.put(&[]))?;
547        let link_type = self.link_type_for_size(size);
548        let meta = Self::file_entry_meta(&[]);
549        let new_root = block_on(self.tree.set_entry_with_meta(
550            &self.current_root(),
551            &self.path_refs(&parent_path),
552            name,
553            &cid,
554            size,
555            link_type,
556            Some(meta),
557        ))?;
558
559        self.apply_root_update(new_root)?;
560
561        let inode = self.insert_path(parent, name.to_string(), child_path);
562        Ok(EntryAttr {
563            inode,
564            size,
565            kind: EntryKind::File,
566        })
567    }
568
569    pub fn mkdir(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
570        self.ensure_valid_name(name)?;
571        let _guard = self.modify_lock.lock().unwrap();
572
573        let parent_path = self.path_for_inode(parent)?;
574        let mut child_path = parent_path.clone();
575        child_path.push(name.to_string());
576
577        if self.resolve_entry(&child_path).is_ok() {
578            return Err(FsError::AlreadyExists);
579        }
580
581        let dir_cid = block_on(self.tree.put_directory(Vec::new()))?;
582        let new_root = block_on(self.tree.set_entry(
583            &self.current_root(),
584            &self.path_refs(&parent_path),
585            name,
586            &dir_cid,
587            0,
588            LinkType::Dir,
589        ))?;
590
591        self.apply_root_update(new_root)?;
592
593        let inode = self.insert_path(parent, name.to_string(), child_path);
594        Ok(EntryAttr {
595            inode,
596            size: 0,
597            kind: EntryKind::Directory,
598        })
599    }
600
601    pub fn write_file(&self, inode: u64, offset: u64, data: &[u8]) -> Result<u32, FsError> {
602        let _guard = self.modify_lock.lock().unwrap();
603        let path = self.path_for_inode(inode)?;
604        let existing = self.read_file_full(&path)?;
605        let new_data = Self::apply_write(existing, offset, data);
606        self.update_file_at_path(&path, new_data)?;
607        Ok(data.len() as u32)
608    }
609
610    pub fn truncate_file(&self, inode: u64, size: u64) -> Result<(), FsError> {
611        let _guard = self.modify_lock.lock().unwrap();
612        let path = self.path_for_inode(inode)?;
613        let existing = self.read_file_full(&path)?;
614        let new_data = Self::apply_truncate(existing, size);
615        self.update_file_at_path(&path, new_data)?;
616        Ok(())
617    }
618
619    pub fn unlink(&self, parent: u64, name: &str) -> Result<(), FsError> {
620        if Self::is_directory_refresh_sentinel(name) {
621            return Ok(());
622        }
623        if self.take_refresh_unlink(parent, name)? {
624            return Ok(());
625        }
626        self.ensure_valid_name(name)?;
627        let _guard = self.modify_lock.lock().unwrap();
628
629        let parent_path = self.path_for_inode(parent)?;
630        let mut child_path = parent_path.clone();
631        child_path.push(name.to_string());
632        let entry = self.resolve_entry(&child_path)?;
633        if entry.link_type.is_directory_like() {
634            return Err(FsError::IsDir);
635        }
636
637        let new_root = block_on(self.tree.remove_entry(
638            &self.current_root(),
639            &self.path_refs(&parent_path),
640            name,
641        ))?;
642
643        self.apply_root_update(new_root)?;
644        self.remove_paths_prefix(&child_path);
645        self.children.write().unwrap().remove(&ChildKey {
646            parent,
647            name: name.to_string(),
648        });
649
650        Ok(())
651    }
652
653    pub fn rmdir(&self, parent: u64, name: &str) -> Result<(), FsError> {
654        if self.take_refresh_unlink(parent, name)? {
655            return Ok(());
656        }
657        self.ensure_valid_name(name)?;
658        let _guard = self.modify_lock.lock().unwrap();
659
660        let parent_path = self.path_for_inode(parent)?;
661        let mut child_path = parent_path.clone();
662        child_path.push(name.to_string());
663        let entry = self.resolve_entry(&child_path)?;
664        if !entry.link_type.is_directory_like() {
665            return Err(FsError::NotDir);
666        }
667
668        let dir_entries = block_on(self.tree.list_directory(&entry.cid))?;
669        if !dir_entries.is_empty() {
670            return Err(FsError::NotEmpty);
671        }
672
673        let new_root = block_on(self.tree.remove_entry(
674            &self.current_root(),
675            &self.path_refs(&parent_path),
676            name,
677        ))?;
678
679        self.apply_root_update(new_root)?;
680        self.remove_paths_prefix(&child_path);
681        self.children.write().unwrap().remove(&ChildKey {
682            parent,
683            name: name.to_string(),
684        });
685
686        Ok(())
687    }
688
689    pub fn rename(
690        &self,
691        parent: u64,
692        name: &str,
693        new_parent: u64,
694        new_name: &str,
695    ) -> Result<(), FsError> {
696        self.ensure_valid_name(name)?;
697        self.ensure_valid_name(new_name)?;
698        let _guard = self.modify_lock.lock().unwrap();
699
700        if parent == new_parent && name == new_name {
701            return Ok(());
702        }
703
704        let parent_path = self.path_for_inode(parent)?;
705        let new_parent_path = self.path_for_inode(new_parent)?;
706
707        let mut old_path = parent_path.clone();
708        old_path.push(name.to_string());
709        let entry = self.resolve_entry(&old_path)?;
710
711        let new_root = block_on(self.tree.set_entry_with_meta(
712            &self.current_root(),
713            &self.path_refs(&new_parent_path),
714            new_name,
715            &entry.cid,
716            entry.size,
717            entry.link_type,
718            entry.meta.clone(),
719        ))?;
720        let new_root = block_on(self.tree.remove_entry(
721            &new_root,
722            &self.path_refs(&parent_path),
723            name,
724        ))?;
725
726        self.apply_root_update(new_root)?;
727
728        let inode = self.get_or_create_child_inode(parent, name)?;
729        let mut new_path = new_parent_path.clone();
730        new_path.push(new_name.to_string());
731
732        self.children.write().unwrap().remove(&ChildKey {
733            parent,
734            name: name.to_string(),
735        });
736        self.children.write().unwrap().insert(
737            ChildKey {
738                parent: new_parent,
739                name: new_name.to_string(),
740            },
741            inode,
742        );
743
744        self.parents.write().unwrap().insert(inode, new_parent);
745        self.update_paths_prefix(&old_path, &new_path);
746
747        Ok(())
748    }
749
750    fn ensure_valid_name(&self, name: &str) -> Result<(), FsError> {
751        if name.is_empty() || name.contains('/') || Self::is_directory_refresh_sentinel(name) {
752            return Err(FsError::InvalidName);
753        }
754        Ok(())
755    }
756
757    fn is_directory_refresh_sentinel(name: &str) -> bool {
758        name == DIRECTORY_REFRESH_SENTINEL_NAME
759    }
760
761    fn is_directory_refresh_sentinel_path(path: &[String]) -> bool {
762        path.last()
763            .is_some_and(|name| Self::is_directory_refresh_sentinel(name))
764    }
765
766    fn directory_refresh_sentinel_attr(inode: u64) -> EntryAttr {
767        EntryAttr {
768            inode,
769            size: 0,
770            kind: EntryKind::File,
771        }
772    }
773
774    fn path_for_inode(&self, inode: u64) -> Result<Vec<String>, FsError> {
775        self.paths
776            .read()
777            .unwrap()
778            .get(&inode)
779            .cloned()
780            .ok_or(FsError::NotFound)
781    }
782
783    fn take_refresh_unlink(&self, parent: u64, name: &str) -> Result<bool, FsError> {
784        let parent_path = self.path_for_inode(parent)?;
785        let mut child_path = parent_path;
786        child_path.push(name.to_string());
787        Ok(self.refresh_unlinks.lock().unwrap().remove(&child_path))
788    }
789
790    fn resolve_entry(&self, path: &[String]) -> Result<ResolvedEntry, FsError> {
791        self.resolve_entry_at_root(&self.current_root(), path)
792    }
793
794    fn resolve_entry_at_root(&self, root: &Cid, path: &[String]) -> Result<ResolvedEntry, FsError> {
795        if path.is_empty() {
796            return Ok(ResolvedEntry {
797                cid: root.clone(),
798                link_type: LinkType::Dir,
799                size: 0,
800                meta: None,
801            });
802        }
803
804        let (parent_path, name) = path.split_at(path.len() - 1);
805        let parent_cid = self.resolve_dir_cid_at_root(root, parent_path)?;
806        let entries = block_on(self.tree.list_directory(&parent_cid))?;
807        let entry = entries
808            .into_iter()
809            .find(|e| e.name == name[0])
810            .ok_or(FsError::NotFound)?;
811
812        Ok(ResolvedEntry {
813            cid: Cid {
814                hash: entry.hash,
815                key: entry.key,
816            },
817            link_type: entry.link_type,
818            size: entry.size,
819            meta: entry.meta,
820        })
821    }
822
823    fn resolve_dir_cid(&self, path: &[String]) -> Result<Cid, FsError> {
824        self.resolve_dir_cid_at_root(&self.current_root(), path)
825    }
826
827    fn resolve_dir_cid_at_root(&self, root: &Cid, path: &[String]) -> Result<Cid, FsError> {
828        if path.is_empty() {
829            return Ok(root.clone());
830        }
831
832        let path_str = path.join("/");
833        let cid = block_on(self.tree.resolve(root, &path_str))?.ok_or(FsError::NotFound)?;
834
835        let is_dir = block_on(self.tree.is_dir(&cid))?;
836        if !is_dir {
837            return Err(FsError::NotDir);
838        }
839
840        Ok(cid)
841    }
842
843    #[cfg(feature = "fuse")]
844    fn directory_entry_names_at_root(
845        &self,
846        root: &Cid,
847        path: &[String],
848    ) -> Option<HashSet<String>> {
849        let dir_cid = self.resolve_dir_cid_at_root(root, path).ok()?;
850        let entries = block_on(self.tree.list_directory(&dir_cid)).ok()?;
851        Some(
852            entries
853                .into_iter()
854                .map(|entry| entry.name)
855                .filter(|name| !Self::is_directory_refresh_sentinel(name))
856                .collect(),
857        )
858    }
859
860    fn retain_existing_paths_after_root_update(&self) -> Result<(), FsError> {
861        let known: Vec<(u64, Vec<String>)> = self
862            .paths
863            .read()
864            .unwrap()
865            .iter()
866            .map(|(inode, path)| (*inode, path.clone()))
867            .collect();
868        let mut keep = HashSet::new();
869        keep.insert(ROOT_INODE);
870
871        for (inode, path) in known {
872            if inode == ROOT_INODE {
873                continue;
874            }
875            if Self::is_directory_refresh_sentinel_path(&path) {
876                if self.resolve_dir_cid(&path[..path.len() - 1]).is_ok() {
877                    keep.insert(inode);
878                }
879                continue;
880            }
881            match self.resolve_entry(&path) {
882                Ok(_) => {
883                    keep.insert(inode);
884                }
885                Err(FsError::NotFound) | Err(FsError::NotDir) => {}
886                Err(error) => return Err(error),
887            }
888        }
889
890        self.paths
891            .write()
892            .unwrap()
893            .retain(|inode, _| keep.contains(inode));
894        self.parents
895            .write()
896            .unwrap()
897            .retain(|inode, parent| keep.contains(inode) && keep.contains(parent));
898        self.children
899            .write()
900            .unwrap()
901            .retain(|key, inode| keep.contains(&key.parent) && keep.contains(inode));
902        Ok(())
903    }
904
905    fn entry_attr_from_resolved(
906        &self,
907        inode: u64,
908        entry: ResolvedEntry,
909    ) -> Result<EntryAttr, FsError> {
910        let kind = Self::kind_from_link(entry.link_type);
911        let size = if kind == EntryKind::Directory {
912            0
913        } else {
914            self.entry_size(&entry)?
915        };
916
917        Ok(EntryAttr { inode, size, kind })
918    }
919
920    fn entry_size(&self, entry: &ResolvedEntry) -> Result<u64, FsError> {
921        if entry.link_type.is_directory_like() {
922            return Ok(0);
923        }
924        if entry.size > 0 {
925            return Ok(entry.size);
926        }
927
928        let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
929        Ok(data.len() as u64)
930    }
931
932    fn read_file_full(&self, path: &[String]) -> Result<Vec<u8>, FsError> {
933        let entry = self.resolve_entry(path)?;
934        if entry.link_type.is_directory_like() {
935            return Err(FsError::IsDir);
936        }
937        let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
938        Ok(data)
939    }
940
941    fn update_file_at_path(&self, path: &[String], data: Vec<u8>) -> Result<(), FsError> {
942        let (parent_path, name) = path.split_at(path.len() - 1);
943        let (cid, size) = block_on(self.tree.put(&data))?;
944        let link_type = self.link_type_for_size(size);
945        let meta = Self::file_entry_meta(&data);
946
947        let new_root = block_on(self.tree.set_entry_with_meta(
948            &self.current_root(),
949            &self.path_refs(parent_path),
950            name[0].as_str(),
951            &cid,
952            size,
953            link_type,
954            Some(meta),
955        ))?;
956
957        self.apply_root_update(new_root)
958    }
959
960    fn file_entry_meta(data: &[u8]) -> HashMap<String, serde_json::Value> {
961        HashMap::from([(
962            WHOLE_FILE_HASH_META_KEY.to_string(),
963            serde_json::Value::String(to_hex(&sha256(data))),
964        )])
965    }
966
967    fn apply_root_update(&self, new_root: Cid) -> Result<(), FsError> {
968        if let Some(publisher) = &self.publisher {
969            publisher.publish(&new_root)?;
970        }
971        *self.root.write().unwrap() = new_root;
972        Ok(())
973    }
974
975    fn link_type_for_size(&self, size: u64) -> LinkType {
976        if size as usize > self.tree.chunk_size() {
977            LinkType::File
978        } else {
979            LinkType::Blob
980        }
981    }
982
983    fn get_or_create_child_inode(&self, parent: u64, name: &str) -> Result<u64, FsError> {
984        let key = ChildKey {
985            parent,
986            name: name.to_string(),
987        };
988        if let Some(inode) = self.children.read().unwrap().get(&key).copied() {
989            return Ok(inode);
990        }
991
992        let parent_path = self.path_for_inode(parent)?;
993        let mut child_path = parent_path.clone();
994        child_path.push(name.to_string());
995
996        if let Some(existing) = self.find_inode_by_path(&child_path) {
997            self.children.write().unwrap().insert(key, existing);
998            return Ok(existing);
999        }
1000
1001        Ok(self.insert_path(parent, name.to_string(), child_path))
1002    }
1003
1004    fn insert_path(&self, parent: u64, name: String, path: Vec<String>) -> u64 {
1005        let inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
1006        self.paths.write().unwrap().insert(inode, path);
1007        self.parents.write().unwrap().insert(inode, parent);
1008        self.children
1009            .write()
1010            .unwrap()
1011            .insert(ChildKey { parent, name }, inode);
1012        inode
1013    }
1014
1015    fn find_inode_by_path(&self, path: &[String]) -> Option<u64> {
1016        self.paths
1017            .read()
1018            .unwrap()
1019            .iter()
1020            .find_map(|(inode, inode_path)| {
1021                if inode_path == path {
1022                    Some(*inode)
1023                } else {
1024                    None
1025                }
1026            })
1027    }
1028
1029    fn update_paths_prefix(&self, old_prefix: &[String], new_prefix: &[String]) {
1030        let mut paths = self.paths.write().unwrap();
1031        for path in paths.values_mut() {
1032            if Self::path_has_prefix(path, old_prefix) {
1033                let mut updated = new_prefix.to_vec();
1034                updated.extend_from_slice(&path[old_prefix.len()..]);
1035                *path = updated;
1036            }
1037        }
1038    }
1039
1040    fn remove_paths_prefix(&self, prefix: &[String]) {
1041        let mut to_remove = Vec::new();
1042        {
1043            let paths = self.paths.read().unwrap();
1044            for (inode, path) in paths.iter() {
1045                if *inode == ROOT_INODE {
1046                    continue;
1047                }
1048                if Self::path_has_prefix(path, prefix) {
1049                    to_remove.push(*inode);
1050                }
1051            }
1052        }
1053
1054        if to_remove.is_empty() {
1055            return;
1056        }
1057
1058        let remove_set: std::collections::HashSet<u64> = to_remove.into_iter().collect();
1059        self.paths
1060            .write()
1061            .unwrap()
1062            .retain(|inode, _| !remove_set.contains(inode));
1063        self.parents
1064            .write()
1065            .unwrap()
1066            .retain(|inode, _| !remove_set.contains(inode));
1067        self.children
1068            .write()
1069            .unwrap()
1070            .retain(|_, inode| !remove_set.contains(inode));
1071    }
1072
1073    #[cfg(feature = "fuse")]
1074    fn drop_paths(&self, paths: impl IntoIterator<Item = Vec<String>>) {
1075        for path in paths {
1076            if let Some(inode) = self.find_inode_by_path(&path) {
1077                self.drop_inode(inode);
1078            }
1079        }
1080    }
1081
1082    fn drop_inode(&self, inode: u64) {
1083        if inode == ROOT_INODE {
1084            return;
1085        }
1086        let mut paths = self.paths.write().unwrap();
1087        let removed_path = paths.remove(&inode);
1088        drop(paths);
1089        self.parents.write().unwrap().remove(&inode);
1090        if let Some(path) = removed_path {
1091            if let Some((name, parent_path)) = path.split_last() {
1092                if let Some(parent_inode) = self.find_inode_by_path(parent_path) {
1093                    self.children.write().unwrap().remove(&ChildKey {
1094                        parent: parent_inode,
1095                        name: name.to_string(),
1096                    });
1097                }
1098            }
1099        }
1100    }
1101
1102    fn parent_inode(&self, inode: u64) -> u64 {
1103        self.parents
1104            .read()
1105            .unwrap()
1106            .get(&inode)
1107            .copied()
1108            .unwrap_or(ROOT_INODE)
1109    }
1110
1111    fn kind_from_link(link_type: LinkType) -> EntryKind {
1112        match link_type {
1113            LinkType::Dir | LinkType::Fanout => EntryKind::Directory,
1114            LinkType::Blob | LinkType::File => EntryKind::File,
1115        }
1116    }
1117
1118    fn apply_write(mut existing: Vec<u8>, offset: u64, data: &[u8]) -> Vec<u8> {
1119        let offset_usize = offset as usize;
1120        if existing.len() < offset_usize {
1121            existing.resize(offset_usize, 0);
1122        }
1123        if existing.len() < offset_usize + data.len() {
1124            existing.resize(offset_usize + data.len(), 0);
1125        }
1126        existing[offset_usize..offset_usize + data.len()].copy_from_slice(data);
1127        existing
1128    }
1129
1130    fn apply_truncate(mut existing: Vec<u8>, size: u64) -> Vec<u8> {
1131        let size = size as usize;
1132        if existing.len() > size {
1133            existing.truncate(size);
1134        } else if existing.len() < size {
1135            existing.resize(size, 0);
1136        }
1137        existing
1138    }
1139
1140    fn path_refs<'a>(&self, path: &'a [String]) -> Vec<&'a str> {
1141        path.iter().map(|p| p.as_str()).collect()
1142    }
1143
1144    fn path_has_prefix(path: &[String], prefix: &[String]) -> bool {
1145        if prefix.len() > path.len() {
1146            return false;
1147        }
1148        path.iter().zip(prefix.iter()).all(|(a, b)| a == b)
1149    }
1150}
1151
1152#[cfg(feature = "fuse")]
1153mod fuse_impl {
1154    use super::*;
1155    use fuser::{
1156        FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyCreate, ReplyData,
1157        ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyStatfs, ReplyWrite, Request,
1158    };
1159    use std::ffi::CString;
1160    use std::ffi::OsStr;
1161    use std::path::Path;
1162    use std::time::{Duration, SystemTime};
1163
1164    const TTL: Duration = Duration::ZERO;
1165    const FALLBACK_BLOCK_SIZE: u32 = 4096;
1166    const FALLBACK_TOTAL_BYTES: u64 = 1 << 40;
1167    const FALLBACK_FREE_BYTES: u64 = 1 << 39;
1168    const FALLBACK_TOTAL_FILES: u64 = 1_000_000;
1169    const FALLBACK_FREE_FILES: u64 = 900_000;
1170
1171    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1172    pub(crate) struct FsStats {
1173        pub(crate) blocks: u64,
1174        pub(crate) bfree: u64,
1175        pub(crate) bavail: u64,
1176        pub(crate) files: u64,
1177        pub(crate) ffree: u64,
1178        pub(crate) bsize: u32,
1179        pub(crate) namelen: u32,
1180        pub(crate) frsize: u32,
1181    }
1182
1183    impl FsError {
1184        fn errno(&self) -> i32 {
1185            match self {
1186                FsError::InvalidRoot | FsError::InvalidName => libc::EINVAL,
1187                FsError::NotFound => libc::ENOENT,
1188                FsError::NotDir => libc::ENOTDIR,
1189                FsError::IsDir => libc::EISDIR,
1190                FsError::AlreadyExists => libc::EEXIST,
1191                FsError::NotEmpty => libc::ENOTEMPTY,
1192                FsError::Tree(_) | FsError::Publish(_) => libc::EIO,
1193            }
1194        }
1195    }
1196
1197    pub(crate) fn fallback_fs_stats() -> FsStats {
1198        let blocks = FALLBACK_TOTAL_BYTES / u64::from(FALLBACK_BLOCK_SIZE);
1199        let free_blocks = FALLBACK_FREE_BYTES / u64::from(FALLBACK_BLOCK_SIZE);
1200        FsStats {
1201            blocks,
1202            bfree: free_blocks,
1203            bavail: free_blocks,
1204            files: FALLBACK_TOTAL_FILES,
1205            ffree: FALLBACK_FREE_FILES,
1206            bsize: FALLBACK_BLOCK_SIZE,
1207            namelen: 255,
1208            frsize: FALLBACK_BLOCK_SIZE,
1209        }
1210    }
1211
1212    fn checked_u32<T: TryInto<u32>>(value: T) -> Option<u32> {
1213        value.try_into().ok()
1214    }
1215
1216    fn host_fs_stats(path: &Path) -> Option<FsStats> {
1217        let path = CString::new(path.as_os_str().as_encoded_bytes()).ok()?;
1218        let mut stat = std::mem::MaybeUninit::<libc::statfs>::uninit();
1219        let result = unsafe { libc::statfs(path.as_ptr(), stat.as_mut_ptr()) };
1220        if result != 0 {
1221            return None;
1222        }
1223
1224        let stat = unsafe { stat.assume_init() };
1225        let bsize = checked_u32(stat.f_bsize)?;
1226        Some(FsStats {
1227            blocks: stat.f_blocks,
1228            bfree: stat.f_bfree,
1229            bavail: stat.f_bavail,
1230            files: stat.f_files,
1231            ffree: stat.f_ffree,
1232            bsize,
1233            namelen: 255,
1234            frsize: bsize,
1235        })
1236    }
1237
1238    pub(crate) fn current_fs_stats() -> FsStats {
1239        host_fs_stats(Path::new("/")).unwrap_or_else(fallback_fs_stats)
1240    }
1241
1242    impl<S: Store + Send + Sync + 'static> HashtreeFuse<S> {
1243        pub fn mount(
1244            self,
1245            mountpoint: impl AsRef<Path>,
1246            options: &[MountOption],
1247        ) -> std::io::Result<()> {
1248            let mut session = fuser::Session::new(self.clone(), mountpoint.as_ref(), options)?;
1249            self.set_notifier(session.notifier());
1250            let result = session.run();
1251            self.clear_notifier();
1252            result
1253        }
1254
1255        fn file_attr(&self, attr: &EntryAttr) -> FileAttr {
1256            let (kind, perm, nlink) = match attr.kind {
1257                EntryKind::Directory => (FileType::Directory, 0o755, 2),
1258                EntryKind::File => (FileType::RegularFile, 0o644, 1),
1259            };
1260            let uid = unsafe { libc::geteuid() };
1261            let gid = unsafe { libc::getegid() };
1262            let blocks = attr.size.div_ceil(512);
1263
1264            FileAttr {
1265                ino: attr.inode,
1266                size: attr.size,
1267                blocks,
1268                atime: SystemTime::UNIX_EPOCH,
1269                mtime: SystemTime::UNIX_EPOCH,
1270                ctime: SystemTime::UNIX_EPOCH,
1271                crtime: SystemTime::UNIX_EPOCH,
1272                kind,
1273                perm,
1274                nlink,
1275                uid,
1276                gid,
1277                rdev: 0,
1278                blksize: 512,
1279                flags: 0,
1280            }
1281        }
1282
1283        fn file_type(kind: EntryKind) -> FileType {
1284            match kind {
1285                EntryKind::Directory => FileType::Directory,
1286                EntryKind::File => FileType::RegularFile,
1287            }
1288        }
1289    }
1290
1291    impl<S: Store + Send + Sync + 'static> Filesystem for HashtreeFuse<S> {
1292        fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
1293            let name = match name.to_str() {
1294                Some(value) => value,
1295                None => {
1296                    reply.error(libc::ENOENT);
1297                    return;
1298                }
1299            };
1300
1301            match self.lookup_child(parent, name) {
1302                Ok(attr) => reply.entry(&TTL, &self.file_attr(&attr), 0),
1303                Err(err) => reply.error(err.errno()),
1304            }
1305        }
1306
1307        fn getattr(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) {
1308            match self.get_attr(ino) {
1309                Ok(attr) => reply.attr(&TTL, &self.file_attr(&attr)),
1310                Err(err) => reply.error(err.errno()),
1311            }
1312        }
1313
1314        fn open(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen) {
1315            reply.opened(0, 0);
1316        }
1317
1318        fn read(
1319            &mut self,
1320            _req: &Request<'_>,
1321            ino: u64,
1322            _fh: u64,
1323            offset: i64,
1324            size: u32,
1325            _flags: i32,
1326            _lock_owner: Option<u64>,
1327            reply: ReplyData,
1328        ) {
1329            let offset = if offset < 0 { 0 } else { offset as u64 };
1330            match self.read_file(ino, offset, size) {
1331                Ok(data) => reply.data(&data),
1332                Err(err) => reply.error(err.errno()),
1333            }
1334        }
1335
1336        fn write(
1337            &mut self,
1338            _req: &Request<'_>,
1339            ino: u64,
1340            _fh: u64,
1341            offset: i64,
1342            data: &[u8],
1343            _write_flags: u32,
1344            _flags: i32,
1345            _lock_owner: Option<u64>,
1346            reply: ReplyWrite,
1347        ) {
1348            let offset = if offset < 0 { 0 } else { offset as u64 };
1349            match self.write_file(ino, offset, data) {
1350                Ok(written) => reply.written(written),
1351                Err(err) => reply.error(err.errno()),
1352            }
1353        }
1354
1355        fn create(
1356            &mut self,
1357            _req: &Request<'_>,
1358            parent: u64,
1359            name: &OsStr,
1360            _mode: u32,
1361            _umask: u32,
1362            _flags: i32,
1363            reply: ReplyCreate,
1364        ) {
1365            let name = match name.to_str() {
1366                Some(value) => value,
1367                None => {
1368                    reply.error(libc::EINVAL);
1369                    return;
1370                }
1371            };
1372
1373            match self.create_file(parent, name) {
1374                Ok(attr) => reply.created(&TTL, &self.file_attr(&attr), 0, 0, 0),
1375                Err(err) => reply.error(err.errno()),
1376            }
1377        }
1378
1379        fn mkdir(
1380            &mut self,
1381            _req: &Request<'_>,
1382            parent: u64,
1383            name: &OsStr,
1384            _mode: u32,
1385            _umask: u32,
1386            reply: ReplyEntry,
1387        ) {
1388            let name = match name.to_str() {
1389                Some(value) => value,
1390                None => {
1391                    reply.error(libc::EINVAL);
1392                    return;
1393                }
1394            };
1395
1396            match HashtreeFuse::mkdir(self, parent, name) {
1397                Ok(attr) => reply.entry(&TTL, &self.file_attr(&attr), 0),
1398                Err(err) => reply.error(err.errno()),
1399            }
1400        }
1401
1402        fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1403            let name = match name.to_str() {
1404                Some(value) => value,
1405                None => {
1406                    reply.error(libc::EINVAL);
1407                    return;
1408                }
1409            };
1410
1411            match HashtreeFuse::unlink(self, parent, name) {
1412                Ok(()) => reply.ok(),
1413                Err(err) => reply.error(err.errno()),
1414            }
1415        }
1416
1417        fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1418            let name = match name.to_str() {
1419                Some(value) => value,
1420                None => {
1421                    reply.error(libc::EINVAL);
1422                    return;
1423                }
1424            };
1425
1426            match HashtreeFuse::rmdir(self, parent, name) {
1427                Ok(()) => reply.ok(),
1428                Err(err) => reply.error(err.errno()),
1429            }
1430        }
1431
1432        fn rename(
1433            &mut self,
1434            _req: &Request<'_>,
1435            parent: u64,
1436            name: &OsStr,
1437            newparent: u64,
1438            newname: &OsStr,
1439            _flags: u32,
1440            reply: ReplyEmpty,
1441        ) {
1442            let name = match name.to_str() {
1443                Some(value) => value,
1444                None => {
1445                    reply.error(libc::EINVAL);
1446                    return;
1447                }
1448            };
1449            let newname = match newname.to_str() {
1450                Some(value) => value,
1451                None => {
1452                    reply.error(libc::EINVAL);
1453                    return;
1454                }
1455            };
1456
1457            match HashtreeFuse::rename(self, parent, name, newparent, newname) {
1458                Ok(()) => reply.ok(),
1459                Err(err) => reply.error(err.errno()),
1460            }
1461        }
1462
1463        fn setattr(
1464            &mut self,
1465            _req: &Request<'_>,
1466            ino: u64,
1467            _mode: Option<u32>,
1468            _uid: Option<u32>,
1469            _gid: Option<u32>,
1470            size: Option<u64>,
1471            _atime: Option<fuser::TimeOrNow>,
1472            _mtime: Option<fuser::TimeOrNow>,
1473            _ctime: Option<SystemTime>,
1474            _fh: Option<u64>,
1475            _crtime: Option<SystemTime>,
1476            _chgtime: Option<SystemTime>,
1477            _bkuptime: Option<SystemTime>,
1478            _flags: Option<u32>,
1479            reply: ReplyAttr,
1480        ) {
1481            if let Some(size) = size {
1482                match self.truncate_file(ino, size) {
1483                    Ok(()) => {
1484                        if let Ok(attr) = self.get_attr(ino) {
1485                            reply.attr(&TTL, &self.file_attr(&attr));
1486                        } else {
1487                            reply.error(libc::EIO);
1488                        }
1489                    }
1490                    Err(err) => reply.error(err.errno()),
1491                }
1492            } else {
1493                match self.get_attr(ino) {
1494                    Ok(attr) => reply.attr(&TTL, &self.file_attr(&attr)),
1495                    Err(err) => reply.error(err.errno()),
1496                }
1497            }
1498        }
1499
1500        fn readdir(
1501            &mut self,
1502            _req: &Request<'_>,
1503            ino: u64,
1504            _fh: u64,
1505            offset: i64,
1506            mut reply: ReplyDirectory,
1507        ) {
1508            let mut entries = Vec::new();
1509            entries.push((ino, EntryKind::Directory, ".".to_string()));
1510            let parent = self.parent_inode(ino);
1511            entries.push((parent, EntryKind::Directory, "..".to_string()));
1512
1513            match self.read_dir(ino) {
1514                Ok(children) => {
1515                    for entry in children {
1516                        entries.push((entry.inode, entry.kind, entry.name));
1517                    }
1518                }
1519                Err(err) => {
1520                    reply.error(err.errno());
1521                    return;
1522                }
1523            }
1524
1525            let start = if offset < 0 { 0 } else { offset as usize };
1526            for (index, (inode, kind, name)) in entries.into_iter().enumerate().skip(start) {
1527                let next_offset = (index + 1) as i64;
1528                let full = reply.add(inode, next_offset, Self::file_type(kind), name);
1529                if full {
1530                    break;
1531                }
1532            }
1533
1534            reply.ok();
1535        }
1536
1537        fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) {
1538            let stats = current_fs_stats();
1539            reply.statfs(
1540                stats.blocks,
1541                stats.bfree,
1542                stats.bavail,
1543                stats.files,
1544                stats.ffree,
1545                stats.bsize,
1546                stats.namelen,
1547                stats.frsize,
1548            );
1549        }
1550    }
1551}
1552
1553#[cfg(test)]
1554mod tests {
1555    use super::*;
1556    use hashtree_core::store::MemoryStore;
1557
1558    #[cfg(feature = "fuse")]
1559    use super::fuse_impl::{current_fs_stats, fallback_fs_stats};
1560
1561    struct RecordingPublisher {
1562        updates: Mutex<Vec<Cid>>,
1563    }
1564
1565    impl RecordingPublisher {
1566        fn new() -> Self {
1567            Self {
1568                updates: Mutex::new(Vec::new()),
1569            }
1570        }
1571
1572        fn updates(&self) -> Vec<Cid> {
1573            self.updates.lock().unwrap().clone()
1574        }
1575    }
1576
1577    impl RootPublisher for RecordingPublisher {
1578        fn publish(&self, cid: &Cid) -> Result<(), FsError> {
1579            self.updates.lock().unwrap().push(cid.clone());
1580            Ok(())
1581        }
1582    }
1583
1584    async fn empty_root(store: Arc<MemoryStore>) -> Cid {
1585        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1586        tree.put_directory(Vec::new()).await.unwrap()
1587    }
1588
1589    #[tokio::test]
1590    async fn test_create_write_read_file() {
1591        let store = Arc::new(MemoryStore::new());
1592        let root = empty_root(store.clone()).await;
1593        let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1594
1595        let attr = fs.create_file(ROOT_INODE, "hello.txt").unwrap();
1596        assert_eq!(attr.kind, EntryKind::File);
1597
1598        fs.write_file(attr.inode, 0, b"hello").unwrap();
1599        let read = fs.read_file(attr.inode, 0, 5).unwrap();
1600        assert_eq!(read, b"hello");
1601
1602        let tree = HashTree::new(HashTreeConfig::new(store));
1603        let entries = tree.list_directory(&fs.current_root()).await.unwrap();
1604        let file = entries
1605            .iter()
1606            .find(|entry| entry.name == "hello.txt")
1607            .unwrap();
1608        assert_eq!(
1609            file.meta
1610                .as_ref()
1611                .and_then(|meta| meta.get("whole_file_hash"))
1612                .and_then(serde_json::Value::as_str),
1613            Some("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
1614        );
1615    }
1616
1617    #[tokio::test]
1618    async fn test_mkdir_and_rename() {
1619        let store = Arc::new(MemoryStore::new());
1620        let root = empty_root(store.clone()).await;
1621        let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1622
1623        let dir = fs.mkdir(ROOT_INODE, "docs").unwrap();
1624        let file = fs.create_file(dir.inode, "draft.txt").unwrap();
1625        fs.write_file(file.inode, 0, b"data").unwrap();
1626
1627        fs.rename(dir.inode, "draft.txt", dir.inode, "final.txt")
1628            .unwrap();
1629        let entries = fs.read_dir(dir.inode).unwrap();
1630        let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
1631        assert!(names.contains(&"final.txt".to_string()));
1632        assert!(!names.contains(&"draft.txt".to_string()));
1633
1634        let tree = HashTree::new(HashTreeConfig::new(store));
1635        let docs = tree
1636            .resolve(&fs.current_root(), "docs")
1637            .await
1638            .unwrap()
1639            .unwrap();
1640        let entries = tree.list_directory(&docs).await.unwrap();
1641        let file = entries
1642            .iter()
1643            .find(|entry| entry.name == "final.txt")
1644            .unwrap();
1645        let expected_hash = to_hex(&sha256(b"data"));
1646        assert_eq!(
1647            file.meta
1648                .as_ref()
1649                .and_then(|meta| meta.get("whole_file_hash"))
1650                .and_then(serde_json::Value::as_str),
1651            Some(expected_hash.as_str())
1652        );
1653    }
1654
1655    #[tokio::test]
1656    async fn test_truncate_file() {
1657        let store = Arc::new(MemoryStore::new());
1658        let root = empty_root(store.clone()).await;
1659        let fs = HashtreeFuse::new(store, root).unwrap();
1660
1661        let file = fs.create_file(ROOT_INODE, "file.bin").unwrap();
1662        fs.write_file(file.inode, 0, b"abcdef").unwrap();
1663        fs.truncate_file(file.inode, 3).unwrap();
1664        let read = fs.read_file(file.inode, 0, 10).unwrap();
1665        assert_eq!(read, b"abc");
1666    }
1667
1668    #[tokio::test]
1669    async fn test_directory_refresh_sentinel_is_virtual() {
1670        let store = Arc::new(MemoryStore::new());
1671        let root = empty_root(store.clone()).await;
1672        let fs = HashtreeFuse::new(store, root.clone()).unwrap();
1673
1674        let entries = fs.read_dir(ROOT_INODE).unwrap();
1675        assert!(!entries
1676            .iter()
1677            .any(|entry| entry.name == DIRECTORY_REFRESH_SENTINEL_NAME));
1678        let sentinel = fs
1679            .lookup_child(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1680            .unwrap();
1681        assert_eq!(sentinel.kind, EntryKind::File);
1682        assert!(fs.read_file(sentinel.inode, 0, 10).unwrap().is_empty());
1683        assert!(fs
1684            .create_file(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1685            .is_err());
1686        fs.unlink(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1687            .unwrap();
1688        assert_eq!(fs.current_root(), root);
1689    }
1690
1691    #[test]
1692    fn test_directory_refresh_sentinel_is_not_hidden() {
1693        assert!(!DIRECTORY_REFRESH_SENTINEL_NAME.starts_with('.'));
1694    }
1695
1696    #[tokio::test]
1697    async fn test_publisher_invoked() {
1698        let store = Arc::new(MemoryStore::new());
1699        let root = empty_root(store.clone()).await;
1700        let publisher = Arc::new(RecordingPublisher::new());
1701        let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1702
1703        let file = fs.create_file(ROOT_INODE, "note.txt").unwrap();
1704        fs.write_file(file.inode, 0, b"note").unwrap();
1705
1706        let updates = publisher.updates();
1707        assert!(!updates.is_empty());
1708        assert_eq!(updates.last().unwrap(), &fs.current_root());
1709    }
1710
1711    #[tokio::test]
1712    async fn test_replace_root_refreshes_visible_tree_without_publishing() {
1713        let store = Arc::new(MemoryStore::new());
1714        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1715        let root = empty_root(store.clone()).await;
1716        let publisher = Arc::new(RecordingPublisher::new());
1717        let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1718
1719        let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1720        fs.write_file(old_file.inode, 0, b"old").unwrap();
1721        assert!(!publisher.updates().is_empty());
1722        publisher.updates.lock().unwrap().clear();
1723
1724        let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1725        let new_root = tree
1726            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1727                "new.txt", &new_blob,
1728            )
1729            .with_size(new_size)
1730            .with_link_type(LinkType::Blob)])
1731            .await
1732            .unwrap();
1733
1734        let old_lookup = fs.lookup_child(ROOT_INODE, "old.txt").unwrap();
1735        fs.replace_root(new_root.clone()).unwrap();
1736
1737        assert_eq!(fs.current_root(), new_root);
1738        assert!(fs.lookup_child(ROOT_INODE, "old.txt").is_err());
1739        let new_lookup = fs.lookup_child(ROOT_INODE, "new.txt").unwrap();
1740        assert_ne!(old_lookup.inode, new_lookup.inode);
1741        assert_eq!(fs.read_file(new_lookup.inode, 0, 3).unwrap(), b"new");
1742        assert!(publisher.updates().is_empty());
1743    }
1744
1745    #[tokio::test]
1746    async fn test_replace_root_if_current_preserves_dirty_root() {
1747        let store = Arc::new(MemoryStore::new());
1748        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1749        let root = empty_root(store.clone()).await;
1750        let fs = HashtreeFuse::new(store, root.clone()).unwrap();
1751
1752        let local = fs.create_file(ROOT_INODE, "local.txt").unwrap();
1753        fs.write_file(local.inode, 0, b"local").unwrap();
1754        let dirty_root = fs.current_root();
1755
1756        let (remote_blob, remote_size) = tree.put(b"remote").await.unwrap();
1757        let remote_root = tree
1758            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1759                "remote.txt",
1760                &remote_blob,
1761            )
1762            .with_size(remote_size)
1763            .with_link_type(LinkType::Blob)])
1764            .await
1765            .unwrap();
1766
1767        let replaced = fs
1768            .replace_root_if_current(&root, remote_root.clone())
1769            .unwrap();
1770
1771        assert!(!replaced);
1772        assert_eq!(fs.current_root(), dirty_root);
1773        assert!(fs.lookup_child(ROOT_INODE, "local.txt").is_ok());
1774        assert!(fs.lookup_child(ROOT_INODE, "remote.txt").is_err());
1775
1776        let replaced = fs
1777            .replace_root_if_current(&dirty_root, remote_root.clone())
1778            .unwrap();
1779        assert!(replaced);
1780        assert_eq!(fs.current_root(), remote_root);
1781    }
1782
1783    #[cfg(feature = "fuse")]
1784    #[tokio::test]
1785    async fn test_replace_root_invalidates_removed_entries_without_inode_notification() {
1786        let store = Arc::new(MemoryStore::new());
1787        let root = empty_root(store.clone()).await;
1788        let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1789
1790        let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1791        fs.write_file(old_file.inode, 0, b"old").unwrap();
1792        let new_root = empty_root(store).await;
1793
1794        let invalidations = fs.changed_known_entries_for_root(&new_root);
1795
1796        assert!(invalidations.contains(&FuseInvalidation::Entry {
1797            parent: ROOT_INODE,
1798            name: "old.txt".to_string(),
1799        }));
1800        assert_eq!(invalidations.len(), 2);
1801    }
1802
1803    #[cfg(feature = "fuse")]
1804    #[tokio::test]
1805    async fn test_replace_root_emits_delete_invalidation_for_removed_entries() {
1806        let store = Arc::new(MemoryStore::new());
1807        let root = empty_root(store.clone()).await;
1808        let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1809
1810        let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1811        fs.write_file(old_file.inode, 0, b"old").unwrap();
1812        let new_root = empty_root(store).await;
1813
1814        let invalidations = fs.changed_known_entries_for_root(&new_root);
1815
1816        assert!(invalidations.contains(&FuseInvalidation::Delete {
1817            parent: ROOT_INODE,
1818            child: old_file.inode,
1819            name: "old.txt".to_string(),
1820        }));
1821        assert_eq!(
1822            fs.removed_known_entry_paths_for_root(&new_root),
1823            vec![vec!["old.txt".to_string()]]
1824        );
1825        let delete_index = invalidations
1826            .iter()
1827            .position(|invalidation| {
1828                matches!(invalidation, FuseInvalidation::Delete { name, .. } if name == "old.txt")
1829            })
1830            .unwrap();
1831        let entry_index = invalidations
1832            .iter()
1833            .position(|invalidation| {
1834                matches!(invalidation, FuseInvalidation::Entry { name, .. } if name == "old.txt")
1835            })
1836            .unwrap();
1837        assert!(entry_index < delete_index);
1838    }
1839
1840    #[cfg(feature = "fuse")]
1841    #[tokio::test]
1842    async fn test_replace_root_invalidates_changed_known_entry_by_name_and_new_inode() {
1843        let store = Arc::new(MemoryStore::new());
1844        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1845        let (old_blob, old_size) = tree.put(b"old").await.unwrap();
1846        let root = tree
1847            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1848                "note.txt", &old_blob,
1849            )
1850            .with_size(old_size)
1851            .with_link_type(LinkType::Blob)])
1852            .await
1853            .unwrap();
1854        let fs = HashtreeFuse::new(store, root).unwrap();
1855        let old_file = fs.lookup_child(ROOT_INODE, "note.txt").unwrap();
1856
1857        let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1858        let new_root = tree
1859            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1860                "note.txt", &new_blob,
1861            )
1862            .with_size(new_size)
1863            .with_link_type(LinkType::Blob)])
1864            .await
1865            .unwrap();
1866
1867        let invalidations = fs.changed_known_entries_for_root(&new_root);
1868
1869        assert!(invalidations.contains(&FuseInvalidation::Entry {
1870            parent: ROOT_INODE,
1871            name: "note.txt".to_string(),
1872        }));
1873        assert_eq!(invalidations.len(), 1);
1874
1875        fs.replace_root(new_root).unwrap();
1876        let new_file = fs.lookup_child(ROOT_INODE, "note.txt").unwrap();
1877        assert_ne!(old_file.inode, new_file.inode);
1878        assert_eq!(fs.read_file(new_file.inode, 0, 3).unwrap(), b"new");
1879    }
1880
1881    #[cfg(feature = "fuse")]
1882    #[tokio::test]
1883    async fn test_replace_root_invalidates_removed_name_without_known_child_inode() {
1884        let store = Arc::new(MemoryStore::new());
1885        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1886        let (old_blob, old_size) = tree.put(b"old").await.unwrap();
1887        let root = tree
1888            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1889                "old.txt", &old_blob,
1890            )
1891            .with_size(old_size)
1892            .with_link_type(LinkType::Blob)])
1893            .await
1894            .unwrap();
1895        let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1896        let new_root = empty_root(store).await;
1897
1898        let invalidations = fs.changed_known_entries_for_root(&new_root);
1899
1900        assert!(invalidations.contains(&FuseInvalidation::Entry {
1901            parent: ROOT_INODE,
1902            name: "old.txt".to_string(),
1903        }));
1904    }
1905
1906    #[tokio::test]
1907    async fn test_refresh_unlink_does_not_publish_or_change_root() {
1908        let store = Arc::new(MemoryStore::new());
1909        let root = empty_root(store.clone()).await;
1910        let publisher = Arc::new(RecordingPublisher::new());
1911        let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1912
1913        let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1914        fs.write_file(old_file.inode, 0, b"old").unwrap();
1915        let old_root = fs.current_root();
1916        publisher.updates.lock().unwrap().clear();
1917
1918        fs.begin_refresh_unlinks(vec![vec!["old.txt".to_string()]]);
1919        fs.unlink(ROOT_INODE, "old.txt").unwrap();
1920
1921        assert_eq!(fs.current_root(), old_root);
1922        assert!(publisher.updates().is_empty());
1923        assert!(fs.lookup_child(ROOT_INODE, "old.txt").is_ok());
1924    }
1925
1926    #[cfg(feature = "fuse")]
1927    #[tokio::test]
1928    async fn test_replace_root_invalidates_added_entries_by_name() {
1929        let store = Arc::new(MemoryStore::new());
1930        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1931        let root = empty_root(store.clone()).await;
1932        let fs = HashtreeFuse::new(store, root).unwrap();
1933
1934        let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1935        let new_root = tree
1936            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1937                "new.txt", &new_blob,
1938            )
1939            .with_size(new_size)
1940            .with_link_type(LinkType::Blob)])
1941            .await
1942            .unwrap();
1943
1944        let invalidations = fs.changed_known_entries_for_root(&new_root);
1945
1946        assert!(invalidations.contains(&FuseInvalidation::Entry {
1947            parent: ROOT_INODE,
1948            name: "new.txt".to_string(),
1949        }));
1950    }
1951
1952    #[tokio::test]
1953    async fn test_replace_root_preserves_existing_directory_inodes() {
1954        let store = Arc::new(MemoryStore::new());
1955        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1956        let root = empty_root(store.clone()).await;
1957        let fs = HashtreeFuse::new(store, root).unwrap();
1958
1959        let docs = fs.mkdir(ROOT_INODE, "docs").unwrap();
1960        let old_file = fs.create_file(docs.inode, "old.txt").unwrap();
1961        fs.write_file(old_file.inode, 0, b"old").unwrap();
1962        let old_entries = fs.read_dir(docs.inode).unwrap();
1963        assert!(old_entries.iter().any(|entry| entry.name == "old.txt"));
1964
1965        let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1966        let new_docs = tree
1967            .put_directory(vec![hashtree_core::DirEntry::from_cid(
1968                "new.txt", &new_blob,
1969            )
1970            .with_size(new_size)
1971            .with_link_type(LinkType::Blob)])
1972            .await
1973            .unwrap();
1974        let new_root = tree
1975            .put_directory(vec![
1976                hashtree_core::DirEntry::from_cid("docs", &new_docs).with_link_type(LinkType::Dir)
1977            ])
1978            .await
1979            .unwrap();
1980
1981        fs.replace_root(new_root).unwrap();
1982
1983        assert_eq!(
1984            fs.lookup_child(ROOT_INODE, "docs").unwrap().inode,
1985            docs.inode
1986        );
1987        assert_eq!(fs.get_attr(docs.inode).unwrap().kind, EntryKind::Directory);
1988        let new_entries = fs.read_dir(docs.inode).unwrap();
1989        let names: Vec<String> = new_entries.into_iter().map(|entry| entry.name).collect();
1990        assert!(names.contains(&"new.txt".to_string()));
1991        assert!(!names.contains(&"old.txt".to_string()));
1992    }
1993
1994    #[tokio::test]
1995    async fn test_fanout_directory_entry_behaves_as_directory() {
1996        let store = Arc::new(MemoryStore::new());
1997        let tree = HashTree::new(HashTreeConfig::new(store.clone()).with_max_links(2));
1998        let mut fanout_entries = Vec::new();
1999        for index in 0..5 {
2000            let body = format!("file-{index}");
2001            let (cid, size) = tree.put(body.as_bytes()).await.unwrap();
2002            fanout_entries.push(
2003                hashtree_core::DirEntry::from_cid(format!("file-{index}.txt"), &cid)
2004                    .with_size(size)
2005                    .with_link_type(LinkType::Blob),
2006            );
2007        }
2008        let fanout_dir = tree.put_directory(fanout_entries).await.unwrap();
2009        let root = tree
2010            .put_directory(vec![hashtree_core::DirEntry::from_cid("big", &fanout_dir)
2011                .with_link_type(LinkType::Fanout)])
2012            .await
2013            .unwrap();
2014        let fs = HashtreeFuse::new(store, root).unwrap();
2015
2016        let big = fs.lookup_child(ROOT_INODE, "big").unwrap();
2017        assert_eq!(big.kind, EntryKind::Directory);
2018        assert!(matches!(fs.read_file(big.inode, 0, 8), Err(FsError::IsDir)));
2019        assert!(matches!(fs.unlink(ROOT_INODE, "big"), Err(FsError::IsDir)));
2020        assert!(matches!(
2021            fs.rmdir(ROOT_INODE, "big"),
2022            Err(FsError::NotEmpty)
2023        ));
2024
2025        let names: Vec<String> = fs
2026            .read_dir(big.inode)
2027            .unwrap()
2028            .into_iter()
2029            .map(|entry| entry.name)
2030            .collect();
2031        assert!(names.contains(&"file-3.txt".to_string()));
2032
2033        let file = fs.lookup_child(big.inode, "file-3.txt").unwrap();
2034        assert_eq!(file.kind, EntryKind::File);
2035        assert_eq!(fs.read_file(file.inode, 0, 6).unwrap(), b"file-3");
2036    }
2037
2038    #[cfg(feature = "fuse")]
2039    #[test]
2040    fn test_fallback_fs_stats_reports_free_space() {
2041        let stats = fallback_fs_stats();
2042        assert!(stats.blocks > 0);
2043        assert!(stats.bfree > 0);
2044        assert!(stats.bavail > 0);
2045        assert!(stats.bsize > 0);
2046        assert_eq!(stats.bfree, stats.bavail);
2047    }
2048
2049    #[cfg(feature = "fuse")]
2050    #[test]
2051    fn test_current_fs_stats_reports_free_space() {
2052        let stats = current_fs_stats();
2053        assert!(stats.blocks > 0);
2054        assert!(stats.bfree > 0);
2055        assert!(stats.bavail > 0);
2056        assert!(stats.bsize > 0);
2057    }
2058}