puzzlefs_lib/reader/
fuse.rs

1use log::{debug, warn};
2use os_pipe::PipeWriter;
3use std::ffi::CString;
4use std::ffi::OsStr;
5use std::ffi::OsString;
6use std::fs;
7use std::fs::OpenOptions;
8use std::io::Write;
9use std::os::raw::c_int;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::ffi::OsStringExt;
12use std::os::unix::fs::FileTypeExt;
13use std::path::{Path, PathBuf};
14use std::thread;
15
16use fuser::{
17    FileAttr, FileType, Filesystem, KernelConfig, ReplyData, ReplyEntry, ReplyOpen, Request,
18    TimeOrNow,
19};
20use nix::errno::Errno;
21use nix::fcntl::OFlag;
22use std::time::{Duration, SystemTime};
23
24use crate::format::{DirEnt, Inode, InodeMode, Result, WireFormatError};
25
26use super::puzzlefs::{file_read, PuzzleFS};
27
28pub enum PipeDescriptor {
29    UnnamedPipe(PipeWriter),
30    NamedPipe(PathBuf),
31}
32
33pub struct Fuse {
34    pfs: PuzzleFS,
35    sender: Option<std::sync::mpsc::Sender<()>>,
36    init_notify: Option<PipeDescriptor>,
37    // TODO: LRU cache inodes or something. I had problems fiddling with the borrow checker for the
38    // cache, so for now we just do each lookup every time.
39}
40
41fn mode_to_fuse_type(inode: &Inode) -> Result<FileType> {
42    Ok(match inode.mode {
43        InodeMode::File { .. } => FileType::RegularFile,
44        InodeMode::Dir { .. } => FileType::Directory,
45        InodeMode::Fifo { .. } => FileType::NamedPipe,
46        InodeMode::Chr { .. } => FileType::CharDevice,
47        InodeMode::Blk { .. } => FileType::BlockDevice,
48        InodeMode::Lnk { .. } => FileType::Symlink,
49        InodeMode::Sock { .. } => FileType::Socket,
50        _ => return Err(WireFormatError::from_errno(Errno::EINVAL)),
51    })
52}
53
54impl Fuse {
55    pub fn new(
56        pfs: PuzzleFS,
57        sender: Option<std::sync::mpsc::Sender<()>>,
58        init_notify: Option<PipeDescriptor>,
59    ) -> Fuse {
60        Fuse {
61            pfs,
62            sender,
63            init_notify,
64        }
65    }
66
67    fn _lookup(&mut self, parent: u64, name: &OsStr) -> Result<FileAttr> {
68        let dir = self.pfs.find_inode(parent)?;
69        let ino = dir.dir_lookup(name.as_bytes())?;
70        self._getattr(ino)
71    }
72
73    fn _getattr(&mut self, ino: u64) -> Result<FileAttr> {
74        let ic = self.pfs.find_inode(ino)?;
75        let kind = mode_to_fuse_type(&ic)?;
76        let len = ic.file_len().unwrap_or(0);
77        Ok(FileAttr {
78            ino: ic.ino,
79            size: len,
80            blocks: 0,
81            atime: SystemTime::UNIX_EPOCH,
82            mtime: SystemTime::UNIX_EPOCH,
83            ctime: SystemTime::UNIX_EPOCH,
84            crtime: SystemTime::UNIX_EPOCH,
85            kind,
86            perm: ic.permissions,
87            nlink: 0,
88            uid: ic.uid,
89            gid: ic.gid,
90            rdev: 0,
91            blksize: 0,
92            flags: 0,
93        })
94    }
95
96    fn _open(&self, flags_i: i32, reply: ReplyOpen) {
97        let allowed_flags = OFlag::O_RDONLY
98            | OFlag::O_PATH
99            | OFlag::O_NONBLOCK
100            | OFlag::O_DIRECTORY
101            | OFlag::O_NOFOLLOW
102            | OFlag::O_NOATIME;
103        let flags = OFlag::from_bits_truncate(flags_i);
104        if !allowed_flags.contains(flags) {
105            warn!("invalid flags {flags:?}, only allowed {allowed_flags:?}");
106            reply.error(Errno::EROFS as i32)
107        } else {
108            // stateless open for now, slower maybe
109            reply.opened(0, flags_i.try_into().unwrap());
110        }
111    }
112
113    fn _read(&mut self, ino: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
114        let inode = self.pfs.find_inode(ino)?;
115        let mut buf = vec![0_u8; size as usize];
116        let read = file_read(
117            &self.pfs.oci,
118            &inode,
119            offset as usize,
120            &mut buf,
121            &self.pfs.verity_data,
122        )?;
123        buf.truncate(read);
124        Ok(buf)
125    }
126
127    fn _readdir(&mut self, ino: u64, offset: i64, reply: &mut fuser::ReplyDirectory) -> Result<()> {
128        let inode = self.pfs.find_inode(ino)?;
129        let entries = inode.dir_entries()?;
130        for (index, DirEnt { name, ino: ino_r }) in entries.iter().enumerate().skip(offset as usize)
131        {
132            let ino = *ino_r;
133            let inode = self.pfs.find_inode(ino)?;
134            let kind = mode_to_fuse_type(&inode)?;
135
136            // if the buffer is full, let's skip the extra lookups
137            if reply.add(ino, (index + 1) as i64, kind, OsStr::from_bytes(name)) {
138                break;
139            }
140        }
141
142        Ok(())
143    }
144
145    fn _readlink(&mut self, ino: u64) -> Result<OsString> {
146        let inode = self.pfs.find_inode(ino)?;
147        let error = WireFormatError::from_errno(Errno::EINVAL);
148        let kind = mode_to_fuse_type(&inode)?;
149        match kind {
150            FileType::Symlink => inode
151                .additional
152                .and_then(|add| add.symlink_target.map(OsString::from_vec))
153                .ok_or(error),
154            _ => Err(error),
155        }
156    }
157
158    fn _listxattr(&mut self, ino: u64) -> Result<Vec<u8>> {
159        let inode = self.pfs.find_inode(ino)?;
160        let xattr_list = inode
161            .additional
162            .map(|add| {
163                add.xattrs
164                    .iter()
165                    .flat_map(|x| {
166                        CString::new(x.key.as_slice())
167                            .expect("xattr is a valid string")
168                            .as_bytes_with_nul()
169                            .to_vec()
170                    })
171                    .collect::<Vec<u8>>()
172            })
173            .unwrap_or_else(Vec::<u8>::new);
174
175        Ok(xattr_list)
176    }
177
178    fn _getxattr(&mut self, ino: u64, name: &OsStr) -> Result<Vec<u8>> {
179        let inode = self.pfs.find_inode(ino)?;
180        inode
181            .additional
182            .and_then(|add| {
183                add.xattrs
184                    .into_iter()
185                    .find(|elem| elem.key == name.as_bytes())
186            })
187            .map(|xattr| xattr.val)
188            .ok_or_else(|| WireFormatError::from_errno(Errno::ENODATA))
189    }
190}
191
192impl Drop for Fuse {
193    fn drop(&mut self) {
194        // This code should be in the destroy function inside the Filesystem implementation
195        // Unfortunately, destroy is not getting called: https://github.com/zargony/fuse-rs/issues/151
196        // This is fixed in fuser, which we're not using right now: https://github.com/cberner/fuser/issues/153
197        if let Some(sender) = &self.sender {
198            sender.send(()).unwrap();
199        }
200    }
201}
202
203impl Filesystem for Fuse {
204    fn init(
205        &mut self,
206        _req: &Request<'_>,
207        _config: &mut KernelConfig,
208    ) -> std::result::Result<(), c_int> {
209        if let Some(init_notify) = self.init_notify.take() {
210            match init_notify {
211                PipeDescriptor::UnnamedPipe(mut pipe_writer) => {
212                    if let Err(e) = pipe_writer.write_all(b"s") {
213                        warn!("unsuccessful send! {e}");
214                    }
215                }
216                PipeDescriptor::NamedPipe(named_pipe) => {
217                    // since opening a pipe for writing blocks until the reading end is opened
218                    // create a new thread so the filesystem can be used even if nobody is reading from the pipe
219                    thread::spawn(move || {
220                        let md = fs::metadata(&named_pipe);
221                        match md {
222                            Err(e) => {
223                                warn!("cannot get file metadata, {e}");
224                                return;
225                            }
226                            Ok(md) => {
227                                if !md.file_type().is_fifo() {
228                                    warn!(
229                                        "the provided file {} is not a fifo!",
230                                        named_pipe.display()
231                                    );
232                                    return;
233                                }
234                            }
235                        }
236                        let file = OpenOptions::new().write(true).open(&named_pipe);
237                        match file {
238                            Ok(mut file) => {
239                                if let Err(e) = file.write_all(b"s") {
240                                    warn!("cannot write to pipe {}, {e}", named_pipe.display());
241                                }
242                            }
243                            Err(e) => {
244                                warn!("cannot open pipe {}, {e}", named_pipe.display());
245                            }
246                        }
247                    });
248                }
249            }
250        }
251        Ok(())
252    }
253
254    fn destroy(&mut self) {}
255    fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {}
256
257    // puzzlefs is readonly, so we can ignore a bunch of requests
258    fn setattr(
259        &mut self,
260        _req: &Request<'_>,
261        _ino: u64,
262        _mode: Option<u32>,
263        _uid: Option<u32>,
264        _gid: Option<u32>,
265        _size: Option<u64>,
266        _atime: Option<TimeOrNow>,
267        _mtime: Option<TimeOrNow>,
268        _ctime: Option<SystemTime>,
269        _fh: Option<u64>,
270        _crtime: Option<SystemTime>,
271        _chgtime: Option<SystemTime>,
272        _bkuptime: Option<SystemTime>,
273        _flags: Option<u32>,
274        reply: fuser::ReplyAttr,
275    ) {
276        debug!("setattr not supported!");
277        reply.error(Errno::EROFS as i32)
278    }
279
280    fn mknod(
281        &mut self,
282        _req: &Request<'_>,
283        _parent: u64,
284        _name: &OsStr,
285        _mode: u32,
286        _umask: u32,
287        _rdev: u32,
288        reply: ReplyEntry,
289    ) {
290        debug!("mknod not supported!");
291        reply.error(Errno::EROFS as i32)
292    }
293
294    fn mkdir(
295        &mut self,
296        _req: &Request<'_>,
297        _parent: u64,
298        _name: &OsStr,
299        _mode: u32,
300        _umask: u32,
301        reply: ReplyEntry,
302    ) {
303        debug!("mkdir not supported!");
304        reply.error(Errno::EROFS as i32)
305    }
306
307    fn unlink(
308        &mut self,
309        _req: &Request<'_>,
310        _parent: u64,
311        _name: &OsStr,
312        reply: fuser::ReplyEmpty,
313    ) {
314        debug!("unlink not supported!");
315        reply.error(Errno::EROFS as i32)
316    }
317
318    fn rmdir(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: fuser::ReplyEmpty) {
319        debug!("rmdir not supported!");
320        reply.error(Errno::EROFS as i32)
321    }
322
323    fn symlink(
324        &mut self,
325        _req: &Request<'_>,
326        _parent: u64,
327        _name: &OsStr,
328        _link: &Path,
329        reply: ReplyEntry,
330    ) {
331        debug!("symlink not supported!");
332        reply.error(Errno::EROFS as i32)
333    }
334
335    fn rename(
336        &mut self,
337        _req: &Request<'_>,
338        _parent: u64,
339        _name: &OsStr,
340        _newparent: u64,
341        _newname: &OsStr,
342        _flags: u32,
343        reply: fuser::ReplyEmpty,
344    ) {
345        debug!("rename not supported!");
346        reply.error(Errno::EROFS as i32)
347    }
348
349    fn link(
350        &mut self,
351        _req: &Request<'_>,
352        _ino: u64,
353        _newparent: u64,
354        _newname: &OsStr,
355        reply: ReplyEntry,
356    ) {
357        debug!("link not supported!");
358        reply.error(Errno::EROFS as i32)
359    }
360
361    fn write(
362        &mut self,
363        _req: &Request<'_>,
364        _ino: u64,
365        _fh: u64,
366        _offset: i64,
367        _data: &[u8],
368        _write_flags: u32,
369        _flags: i32,
370        _lock_owner: Option<u64>,
371        reply: fuser::ReplyWrite,
372    ) {
373        debug!("write not supported!");
374        reply.error(Errno::EROFS as i32)
375    }
376
377    fn flush(
378        &mut self,
379        _req: &Request<'_>,
380        _ino: u64,
381        _fh: u64,
382        _lock_owner: u64,
383        reply: fuser::ReplyEmpty,
384    ) {
385        debug!("flush not supported!");
386        reply.error(Errno::ENOSYS as i32)
387    }
388
389    fn fsync(
390        &mut self,
391        _req: &Request<'_>,
392        _ino: u64,
393        _fh: u64,
394        _datasync: bool,
395        reply: fuser::ReplyEmpty,
396    ) {
397        debug!("fsync not supported!");
398        reply.error(Errno::EROFS as i32)
399    }
400
401    fn fsyncdir(
402        &mut self,
403        _req: &Request<'_>,
404        _ino: u64,
405        _fh: u64,
406        _datasync: bool,
407        reply: fuser::ReplyEmpty,
408    ) {
409        debug!("fsyncdir not supported!");
410        reply.error(Errno::EROFS as i32)
411    }
412
413    fn setxattr(
414        &mut self,
415        _req: &Request<'_>,
416        _ino: u64,
417        _name: &OsStr,
418        _value: &[u8],
419        _flags: i32,
420        _position: u32,
421        reply: fuser::ReplyEmpty,
422    ) {
423        reply.error(Errno::EROFS as i32)
424    }
425
426    fn removexattr(
427        &mut self,
428        _req: &Request<'_>,
429        _ino: u64,
430        _name: &OsStr,
431        reply: fuser::ReplyEmpty,
432    ) {
433        debug!("removexattr not supported!");
434        reply.error(Errno::EROFS as i32)
435    }
436
437    fn create(
438        &mut self,
439        _req: &Request<'_>,
440        _parent: u64,
441        _name: &OsStr,
442        _mode: u32,
443        _umask: u32,
444        _flags: i32,
445        reply: fuser::ReplyCreate,
446    ) {
447        debug!("create not supported!");
448        reply.error(Errno::EROFS as i32)
449    }
450
451    fn getlk(
452        &mut self,
453        _req: &Request<'_>,
454        _ino: u64,
455        _fh: u64,
456        _lock_owner: u64,
457        _start: u64,
458        _end: u64,
459        _typ: i32,
460        _pid: u32,
461        reply: fuser::ReplyLock,
462    ) {
463        debug!("getlk not supported!");
464        reply.error(Errno::EROFS as i32)
465    }
466
467    fn setlk(
468        &mut self,
469        _req: &Request<'_>,
470        _ino: u64,
471        _fh: u64,
472        _lock_owner: u64,
473        _start: u64,
474        _end: u64,
475        _typ: i32,
476        _pid: u32,
477        _sleep: bool,
478        reply: fuser::ReplyEmpty,
479    ) {
480        debug!("setlk not supported!");
481        reply.error(Errno::EROFS as i32)
482    }
483
484    fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
485        match self._lookup(parent, name) {
486            Ok(attr) => {
487                // http://libfuse.github.io/doxygen/structfuse__entry__param.html
488                let ttl = Duration::new(u64::MAX, 0);
489                let generation = 0;
490                reply.entry(&ttl, &attr, generation)
491            }
492            Err(e) => {
493                debug!("cannot lookup parent: {parent}, name {name:?} {e}!");
494                reply.error(e.to_errno());
495            }
496        }
497    }
498
499    fn getattr(&mut self, _req: &Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
500        match self._getattr(ino) {
501            Ok(attr) => {
502                // http://libfuse.github.io/doxygen/structfuse__entry__param.html
503                let ttl = Duration::new(u64::MAX, 0);
504                reply.attr(&ttl, &attr)
505            }
506            Err(e) => {
507                debug!("cannot getattr for ino {ino} {e}!");
508                reply.error(e.to_errno())
509            }
510        }
511    }
512
513    fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
514        match self._readlink(ino) {
515            Ok(symlink) => reply.data(symlink.as_bytes()),
516            Err(e) => {
517                debug!("cannot readlink ino: {ino} {e}!");
518                reply.error(e.to_errno())
519            }
520        }
521    }
522
523    fn open(&mut self, _req: &Request<'_>, _ino: u64, flags: i32, reply: ReplyOpen) {
524        self._open(flags, reply)
525    }
526
527    fn read(
528        &mut self,
529        _req: &Request<'_>,
530        ino: u64,
531        _fh: u64,
532        offset: i64,
533        size: u32,
534        _flags: i32,
535        _lock_owner: Option<u64>,
536        reply: ReplyData,
537    ) {
538        // TODO: why i64 from the fuse API here?
539        let uoffset: u64 = offset.try_into().unwrap();
540        match self._read(ino, uoffset, size) {
541            Ok(data) => reply.data(data.as_slice()),
542            Err(e) => {
543                debug!("cannot read ino {ino}, offset: {uoffset} {e}!");
544                reply.error(e.to_errno())
545            }
546        }
547    }
548
549    fn release(
550        &mut self,
551        _req: &Request<'_>,
552        _ino: u64,
553        _fh: u64,
554        _flags: i32,
555        _lock_owner: Option<u64>,
556        _flush: bool,
557        reply: fuser::ReplyEmpty,
558    ) {
559        // TODO: purge from our cache here? dcache should save us too...
560        reply.ok()
561    }
562
563    fn opendir(&mut self, _req: &Request<'_>, _ino: u64, flags: i32, reply: ReplyOpen) {
564        self._open(flags, reply)
565    }
566
567    fn readdir(
568        &mut self,
569        _req: &Request<'_>,
570        ino: u64,
571        _fh: u64,
572        offset: i64,
573        mut reply: fuser::ReplyDirectory,
574    ) {
575        match self._readdir(ino, offset, &mut reply) {
576            Ok(_) => reply.ok(),
577            Err(e) => {
578                debug!("cannot readdir ino: {ino}, offset {offset} {e}!");
579                reply.error(e.to_errno())
580            }
581        }
582    }
583
584    fn releasedir(
585        &mut self,
586        _req: &Request<'_>,
587        _ino: u64,
588        _fh: u64,
589        _flags: i32,
590        reply: fuser::ReplyEmpty,
591    ) {
592        // TODO: again maybe purge from cache?
593        reply.ok()
594    }
595
596    fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
597        reply.statfs(
598            0,   // blocks
599            0,   // bfree
600            0,   // bavail
601            0,   // files
602            0,   // ffree
603            0,   // bsize
604            256, // namelen
605            0,   // frsize
606        )
607    }
608
609    fn getxattr(
610        &mut self,
611        _req: &Request<'_>,
612        ino: u64,
613        name: &OsStr,
614        size: u32,
615        reply: fuser::ReplyXattr,
616    ) {
617        match self._getxattr(ino, name) {
618            Ok(xattr) => {
619                let xattr_len: u32 = xattr
620                    .len()
621                    .try_into()
622                    .expect("xattrs should not exceed u32");
623                if size == 0 {
624                    reply.size(xattr_len)
625                } else if xattr_len <= size {
626                    reply.data(&xattr)
627                } else {
628                    reply.error(Errno::ERANGE as i32)
629                }
630            }
631            Err(e) => {
632                debug!("cannot getxattr, ino: {ino}, name {name:?} {e}!");
633                reply.error(e.to_errno())
634            }
635        }
636    }
637
638    fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: fuser::ReplyXattr) {
639        match self._listxattr(ino) {
640            Ok(xattr) => {
641                let xattr_len: u32 = xattr
642                    .len()
643                    .try_into()
644                    .expect("xattrs should not exceed u32");
645                if size == 0 {
646                    reply.size(xattr_len)
647                } else if xattr_len <= size {
648                    reply.data(&xattr)
649                } else {
650                    reply.error(Errno::ERANGE as i32)
651                }
652            }
653            Err(e) => {
654                debug!("cannot listxattr, ino {ino}, size {size} {e}!");
655                reply.error(e.to_errno())
656            }
657        }
658    }
659
660    fn access(&mut self, _req: &Request<'_>, _ino: u64, _mask: i32, reply: fuser::ReplyEmpty) {
661        reply.ok()
662    }
663
664    fn bmap(
665        &mut self,
666        _req: &Request<'_>,
667        _ino: u64,
668        _blocksize: u32,
669        _idx: u64,
670        reply: fuser::ReplyBmap,
671    ) {
672        reply.error(Errno::ENOLCK as i32)
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use std::fs;
679    use std::io;
680    use std::path::Path;
681
682    use sha2::{Digest, Sha256};
683    use tempfile::tempdir;
684
685    use crate::builder::build_test_fs;
686    use crate::oci::Image;
687
688    #[test]
689    fn test_fuse() {
690        let dir = tempdir().unwrap();
691        let image = Image::new(dir.path()).unwrap();
692        build_test_fs(Path::new("src/builder/test/test-1"), &image, "test").unwrap();
693        let mountpoint = tempdir().unwrap();
694        let _bg = crate::reader::spawn_mount::<&str>(
695            image,
696            "test",
697            Path::new(mountpoint.path()),
698            &[],
699            None,
700            None,
701            None,
702        )
703        .unwrap();
704        let ents = fs::read_dir(mountpoint.path())
705            .unwrap()
706            .collect::<io::Result<Vec<fs::DirEntry>>>()
707            .unwrap();
708        assert_eq!(ents.len(), 1);
709        assert_eq!(
710            ents[0].path().strip_prefix(mountpoint.path()).unwrap(),
711            Path::new("SekienAkashita.jpg")
712        );
713
714        let mut hasher = Sha256::new();
715        let mut f = fs::File::open(ents[0].path()).unwrap();
716        io::copy(&mut f, &mut hasher).unwrap();
717        let digest = hasher.finalize();
718        const FILE_DIGEST: &str =
719            "d9e749d9367fc908876749d6502eb212fee88c9a94892fb07da5ef3ba8bc39ed";
720        assert_eq!(hex::encode(digest), FILE_DIGEST);
721    }
722}