Skip to main content

hd_mount/
fuse.rs

1//! FUSE filesystem adapter. Bridges fuser::Filesystem to ProjectedFs.
2//!
3//! This module is not tested with inline tests because FUSE operations
4//! require root privileges and a mounted filesystem. The core logic
5//! is tested via ProjectedFs in projected.rs.
6
7use std::ffi::OsStr;
8use std::time::{Duration, UNIX_EPOCH};
9
10use fuser::{
11    FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
12    Request,
13};
14
15use crate::projected::ProjectedFs;
16
17const TTL: Duration = Duration::from_secs(1);
18
19/// FUSE filesystem backed by ProjectedFs.
20pub struct FuseFs {
21    fs: ProjectedFs,
22    // inode -> path mapping for FUSE (FUSE uses inodes, not paths)
23    inodes: Vec<String>, // index = inode number (1-based, 0 unused)
24}
25
26impl FuseFs {
27    pub fn new(fs: ProjectedFs) -> Self {
28        // Build inode table from the DAG
29        let mut inodes = vec![String::new()]; // inode 0 unused
30        inodes.push(String::new()); // inode 1 = root ("")
31
32        // Walk the DAG to assign inodes
33        if let Ok(root_entries) = fs.list_dir("") {
34            for entry in &root_entries {
35                inodes.push(entry.clone());
36                // If it's a directory, add its children
37                if let Ok(children) = fs.list_dir(entry) {
38                    for child in &children {
39                        inodes.push(format!("{}/{}", entry, child));
40                    }
41                }
42            }
43        }
44
45        FuseFs { fs, inodes }
46    }
47
48    fn path_to_inode(&self, path: &str) -> Option<u64> {
49        self.inodes.iter().position(|p| p == path).map(|i| i as u64)
50    }
51
52    fn inode_to_path(&self, inode: u64) -> Option<&str> {
53        self.inodes.get(inode as usize).map(|s| s.as_str())
54    }
55
56    fn make_attr(&self, inode: u64, path: &str) -> FileAttr {
57        let is_dir = self.fs.list_dir(path).is_ok();
58        let size = if is_dir {
59            0
60        } else {
61            self.fs.read_file(path).map(|d| d.len() as u64).unwrap_or(0)
62        };
63
64        FileAttr {
65            ino: inode,
66            size,
67            blocks: size.div_ceil(512),
68            atime: UNIX_EPOCH,
69            mtime: UNIX_EPOCH,
70            ctime: UNIX_EPOCH,
71            crtime: UNIX_EPOCH,
72            kind: if is_dir { FileType::Directory } else { FileType::RegularFile },
73            perm: if is_dir { 0o755 } else { 0o644 },
74            nlink: 1,
75            uid: 0,
76            gid: 0,
77            rdev: 0,
78            blksize: 512,
79            flags: 0,
80        }
81    }
82}
83
84impl Filesystem for FuseFs {
85    fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
86        if let Some(path) = self.inode_to_path(ino) {
87            let path = path.to_string();
88            if ino == 1 || self.fs.exists(&path) {
89                reply.attr(&TTL, &self.make_attr(ino, &path));
90            } else {
91                reply.error(libc::ENOENT);
92            }
93        } else {
94            reply.error(libc::ENOENT);
95        }
96    }
97
98    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
99        let parent_path = match self.inode_to_path(parent) {
100            Some(p) => p.to_string(),
101            None => { reply.error(libc::ENOENT); return; }
102        };
103
104        let child_path = if parent_path.is_empty() {
105            name.to_string_lossy().to_string()
106        } else {
107            format!("{}/{}", parent_path, name.to_string_lossy())
108        };
109
110        if self.fs.exists(&child_path) {
111            // Ensure inode exists
112            let inode = if let Some(ino) = self.path_to_inode(&child_path) {
113                ino
114            } else {
115                self.inodes.push(child_path.clone());
116                (self.inodes.len() - 1) as u64
117            };
118            reply.entry(&TTL, &self.make_attr(inode, &child_path), 0);
119        } else {
120            reply.error(libc::ENOENT);
121        }
122    }
123
124    fn readdir(
125        &mut self,
126        _req: &Request,
127        ino: u64,
128        _fh: u64,
129        offset: i64,
130        mut reply: ReplyDirectory,
131    ) {
132        let path = match self.inode_to_path(ino) {
133            Some(p) => p.to_string(),
134            None => { reply.error(libc::ENOENT); return; }
135        };
136
137        let entries = match self.fs.list_dir(&path) {
138            Ok(e) => e,
139            Err(_) => { reply.error(libc::ENOENT); return; }
140        };
141
142        let mut full_entries = vec![
143            (ino, FileType::Directory, ".".to_string()),
144            (ino, FileType::Directory, "..".to_string()),
145        ];
146
147        for entry_name in entries {
148            let child_path = if path.is_empty() {
149                entry_name.clone()
150            } else {
151                format!("{}/{}", path, entry_name)
152            };
153
154            let inode = if let Some(ino) = self.path_to_inode(&child_path) {
155                ino
156            } else {
157                self.inodes.push(child_path.clone());
158                (self.inodes.len() - 1) as u64
159            };
160
161            let kind = if self.fs.list_dir(&child_path).is_ok() {
162                FileType::Directory
163            } else {
164                FileType::RegularFile
165            };
166
167            full_entries.push((inode, kind, entry_name));
168        }
169
170        for (i, (inode, kind, name)) in full_entries.iter().enumerate().skip(offset as usize) {
171            if reply.add(*inode, (i + 1) as i64, *kind, name) {
172                break;
173            }
174        }
175        reply.ok();
176    }
177
178    fn read(
179        &mut self,
180        _req: &Request,
181        ino: u64,
182        _fh: u64,
183        offset: i64,
184        size: u32,
185        _flags: i32,
186        _lock_owner: Option<u64>,
187        reply: ReplyData,
188    ) {
189        let path = match self.inode_to_path(ino) {
190            Some(p) => p.to_string(),
191            None => { reply.error(libc::ENOENT); return; }
192        };
193
194        match self.fs.read_file(&path) {
195            Ok(data) => {
196                let start = offset as usize;
197                if start >= data.len() {
198                    reply.data(&[]);
199                } else {
200                    let end = (start + size as usize).min(data.len());
201                    reply.data(&data[start..end]);
202                }
203            }
204            Err(_) => reply.error(libc::ENOENT),
205        }
206    }
207}