]> git.proxmox.com Git - proxmox-backup.git/blob - src/pxar/fuse.rs
ab9d2e85c813261b7cf48bf7a95237563a323e74
[proxmox-backup.git] / src / pxar / fuse.rs
1 //! Low level FUSE implementation for pxar.
2 //!
3 //! Allows to mount the archive as read-only filesystem to inspect its contents.
4
5 use std::collections::HashMap;
6 use std::convert::TryFrom;
7 use std::ffi::{CStr, CString, OsStr};
8 use std::fs::File;
9 use std::io::BufReader;
10 use std::os::unix::ffi::{OsStrExt, OsStringExt};
11 use std::path::Path;
12 use std::sync::Mutex;
13
14 use failure::{bail, format_err, Error};
15 use lazy_static::lazy_static;
16 use libc;
17 use libc::{c_char, c_int, c_void, size_t};
18
19 use super::binary_search_tree::search_binary_tree_by;
20 use super::decoder::Decoder;
21 use super::format_definition::{PxarAttributes, PxarEntry, PxarGoodbyeItem};
22
23 /// Node ID of the root i-node
24 ///
25 /// Offsets in the archive are used as i-node for the fuse implementation, as
26 /// they are unique and enough to reference each item in the pxar archive.
27 /// The only exception to this is the `FUSE_ROOT_ID`, which is defined as 1 by
28 /// the fuse library.
29 /// This is okay since offset 1 is part of the root directory entry header and
30 /// will therefore not occur again, but remapping to the correct offset of 0 is
31 /// required.
32 const FUSE_ROOT_ID: u64 = 1;
33
34 const GOODBYE_ITEM_SIZE: u64 = std::mem::size_of::<PxarGoodbyeItem>() as u64;
35 lazy_static! {
36 /// HashMap holding the mapping from the child offsets to their parent
37 /// offsets.
38 ///
39 /// In order to include the parent directory entry '..' in the response for
40 /// readdir callback, this mapping is needed.
41 /// Calling the lookup callback will insert the offsets into the HashMap.
42 static ref CHILD_PARENT: Mutex<HashMap<u64, u64>> = Mutex::new(HashMap::new());
43 }
44
45 type Inode = u64;
46 type Offset = u64;
47 /// FFI types for easier readability
48 type Request = *mut c_void;
49 type MutPtr = *mut c_void;
50 type ConstPtr = *const c_void;
51 type StrPtr = *const c_char;
52 type MutStrPtr = *mut c_char;
53
54 #[rustfmt::skip]
55 #[link(name = "fuse3")]
56 extern "C" {
57 fn fuse_session_new(args: Option<&FuseArgs>, oprs: Option<&Operations>, size: size_t, op: ConstPtr) -> MutPtr;
58 fn fuse_set_signal_handlers(session: ConstPtr) -> c_int;
59 fn fuse_remove_signal_handlers(session: ConstPtr);
60 fn fuse_daemonize(foreground: c_int) -> c_int;
61 fn fuse_session_mount(session: ConstPtr, mountpoint: StrPtr) -> c_int;
62 fn fuse_session_unmount(session: ConstPtr);
63 fn fuse_session_loop(session: ConstPtr) -> c_int;
64 fn fuse_session_loop_mt_31(session: ConstPtr, clone_fd: c_int) -> c_int;
65 fn fuse_session_destroy(session: ConstPtr);
66 fn fuse_reply_attr(req: Request, attr: Option<&libc::stat>, timeout: f64) -> c_int;
67 fn fuse_reply_err(req: Request, errno: c_int) -> c_int;
68 fn fuse_reply_open(req: Request, fileinfo: ConstPtr) -> c_int;
69 fn fuse_reply_buf(req: Request, buf: MutStrPtr, size: size_t) -> c_int;
70 fn fuse_reply_entry(req: Request, entry: Option<&EntryParam>) -> c_int;
71 fn fuse_reply_xattr(req: Request, size: size_t) -> c_int;
72 fn fuse_reply_readlink(req: Request, link: StrPtr) -> c_int;
73 fn fuse_req_userdata(req: Request) -> MutPtr;
74 fn fuse_add_direntry(req: Request, buf: MutStrPtr, bufsize: size_t, name: StrPtr, stbuf: Option<&libc::stat>, off: c_int) -> c_int;
75 }
76
77 /// Command line arguments passed to fuse.
78 #[repr(C)]
79 #[derive(Debug)]
80 struct FuseArgs {
81 argc: c_int,
82 argv: *const StrPtr,
83 allocated: c_int,
84 }
85
86 /// `Context` for callback functions providing the decoder, caches and the
87 /// offset within the archive for the i-node given by the caller.
88 struct Context {
89 decoder: Decoder,
90 goodbye_cache: HashMap<Inode, Vec<(PxarGoodbyeItem, Offset, Offset)>>,
91 attr_cache: Option<(Inode, PxarAttributes)>,
92 ino_offset: Offset,
93 }
94
95 impl Context {
96 /// Lookup the goodbye item identified by `filename` and its corresponding `hash`
97 ///
98 /// Updates the goodbye table cache to contain the table for the directory given
99 /// by the i-node in the provided `Context`.
100 /// Search the first matching `hash` in the goodbye table, allowing for a fast
101 /// comparison with the items.
102 /// As there could be a hash collision, the found items filename is then compared
103 /// by seek to the corresponding item in the archive and reading its attributes
104 /// (which the lookup callback needs to do anyway).
105 /// If the filename does not match, the function is called recursively with the
106 /// rest of the goodbye table to lookup the next match.
107 /// The matching items archive offset, entry and payload size are returned.
108 /// If there is no entry with matching `filename` and `hash` a `libc::ENOENT` is
109 /// returned.
110 fn find_goodbye_entry(
111 &mut self,
112 filename: &CStr,
113 hash: u64,
114 ) -> Result<(u64, PxarEntry, PxarAttributes, u64), i32> {
115 let gbt = self.goodbye_cache.get(&self.ino_offset)
116 .ok_or_else(|| libc::EIO)?;
117 let mut start_idx = 0;
118 let mut skip_multiple = 0;
119 loop {
120 // Search for the next goodbye entry with matching hash.
121 let idx = search_binary_tree_by(
122 start_idx,
123 gbt.len(),
124 skip_multiple,
125 |idx| hash.cmp(&gbt[idx].0.hash),
126 ).ok_or(libc::ENOENT)?;
127
128 let (_item, start, end) = &gbt[idx];
129
130 // At this point it is not clear if the item is a directory or not, this
131 // has to be decided based on the entry mode.
132 // `Decoder`s attributes function accepts both, offsets pointing to
133 // the start of an item (PXAR_FILENAME) or the GOODBYE_TAIL_MARKER in case
134 // of directories, so the use of start offset is fine for both cases.
135 let (entry_name, entry, attr, payload_size) =
136 self.decoder.attributes(*start).map_err(|_| libc::EIO)?;
137
138 // Possible hash collision, need to check if the found entry is indeed
139 // the filename to lookup.
140 if entry_name.as_bytes() == filename.to_bytes() {
141 let child_offset = find_offset(&entry, *start, *end);
142 return Ok((child_offset, entry, attr, payload_size));
143 }
144 // Hash collision, check the next entry in the goodbye table by starting
145 // from given index but skipping one more match (so hash at index itself).
146 start_idx = idx;
147 skip_multiple = 1;
148 }
149 }
150 }
151
152 /// `Session` stores a pointer to the session context and is used to mount the
153 /// archive to the given mountpoint.
154 pub struct Session {
155 ptr: MutPtr,
156 verbose: bool,
157 }
158
159 /// `Operations` defines the callback function table of supported operations.
160 #[repr(C)]
161 #[derive(Default)]
162 #[rustfmt::skip]
163 struct Operations {
164 // The order in which the functions are listed matters, as the offset in the
165 // struct defines what function the fuse driver uses.
166 // It should therefore not be altered!
167 init: Option<extern fn(userdata: MutPtr)>,
168 destroy: Option<extern fn(userdata: MutPtr)>,
169 lookup: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
170 forget: Option<extern fn(req: Request, inode: u64, nlookup: u64)>,
171 getattr: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
172 setattr: Option<extern fn(req: Request, inode: u64, attr: MutPtr, to_set: c_int, fileinfo: MutPtr)>,
173 readlink: Option<extern fn(req: Request, inode: u64)>,
174 mknod: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, rdev: c_int)>,
175 mkdir: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int)>,
176 unlink: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
177 rmdir: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
178 symlink: Option<extern fn(req: Request, link: StrPtr, parent: u64, name: StrPtr)>,
179 rename: Option<extern fn(req: Request, parent: u64, name: StrPtr, newparent: u64, newname: StrPtr, flags: c_int)>,
180 link: Option<extern fn(req: Request, inode: u64, newparent: u64, newname: StrPtr)>,
181 open: Option<extern fn(req: Request, indoe: u64, fileinfo: MutPtr)>,
182 read: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
183 write: Option<extern fn(req: Request, inode: u64, buffer: StrPtr, size: size_t, offset: c_void, fileinfo: MutPtr)>,
184 flush: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
185 release: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
186 fsync: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
187 opendir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
188 readdir: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
189 releasedir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
190 fsyncdir: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
191 statfs: Option<extern fn(req: Request, inode: u64)>,
192 setxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, value: StrPtr, size: size_t, flags: c_int)>,
193 getxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, size: size_t)>,
194 listxattr: Option<extern fn(req: Request, inode: u64, size: size_t)>,
195 removexattr: Option<extern fn(req: Request, inode: u64, name: StrPtr)>,
196 access: Option<extern fn(req: Request, inode: u64, mask: i32)>,
197 create: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, fileinfo: MutPtr)>,
198 getlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr)>,
199 setlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr, sleep: c_int)>,
200 bmap: Option<extern fn(req: Request, inode: u64, blocksize: size_t, idx: u64)>,
201 ioctl: Option<extern fn(req: Request, inode: u64, cmd: c_int, arg: MutPtr, fileinfo: MutPtr, flags: c_int, in_buf: ConstPtr, in_bufsz: size_t, out_bufsz: size_t)>,
202 poll: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, pollhandle: MutPtr)>,
203 write_buf: Option<extern fn(req: Request, inode: u64, bufv: MutPtr, offset: c_int, fileinfo: MutPtr)>,
204 retrieve_reply: Option<extern fn(req: Request, cookie: ConstPtr, inode: u64, offset: c_int, bufv: MutPtr)>,
205 forget_multi: Option<extern fn(req: Request, count: size_t, forgets: MutPtr)>,
206 flock: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, op: c_int)>,
207 fallocate: Option<extern fn(req: Request, inode: u64, mode: c_int, offset: c_int, length: c_int, fileinfo: MutPtr)>,
208 readdirplus: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
209 copy_file_range: Option<extern fn(req: Request, ino_in: u64, off_in: c_int, fi_in: MutPtr, ino_out: u64, off_out: c_int, fi_out: MutPtr, len: size_t, flags: c_int)>,
210 }
211
212
213 impl Session {
214
215 /// Create a new low level fuse session.
216 ///
217 /// `Session` is created using the provided mount options and sets the
218 /// default signal handlers.
219 /// Options have to be provided as comma separated OsStr, e.g.
220 /// ("ro,default_permissions").
221 pub fn from_path(archive_path: &Path, options: &OsStr, verbose: bool) -> Result<Self, Error> {
222 let file = File::open(archive_path)?;
223 let reader = BufReader::new(file);
224 let decoder = Decoder::new(reader)?;
225 Self::new(decoder, options, verbose)
226 }
227
228 /// Create a new low level fuse session using the given `Decoder`.
229 ///
230 /// `Session` is created using the provided mount options and sets the
231 /// default signal handlers.
232 /// Options have to be provided as comma separated OsStr, e.g.
233 /// ("ro,default_permissions").
234 pub fn new(
235 mut decoder: Decoder,
236 options: &OsStr,
237 verbose: bool,
238 ) -> Result<Self, Error> {
239 let args = Self::setup_args(options, verbose)?;
240 let oprs = Self::setup_callbacks();
241
242 let root_ino_offset = decoder.root_end_offset() - GOODBYE_ITEM_SIZE;
243 let root_goodbye_table = decoder.goodbye_table(None, root_ino_offset + GOODBYE_ITEM_SIZE)?;
244 let mut goodbye_cache = HashMap::new();
245 goodbye_cache.insert(root_ino_offset, root_goodbye_table);
246
247 let ctx = Context {
248 decoder,
249 goodbye_cache,
250 attr_cache: None,
251 ino_offset: 0,
252 };
253
254 let session_ctx = Box::new(Mutex::new(ctx));
255 let arg_ptrs: Vec<_> = args.iter().map(|opt| opt.as_ptr()).collect();
256 let fuse_args = FuseArgs {
257 argc: arg_ptrs.len() as i32,
258 argv: arg_ptrs.as_ptr(),
259 allocated: 0,
260 };
261 let session_ptr = unsafe {
262 fuse_session_new(
263 Some(&fuse_args),
264 Some(&oprs),
265 std::mem::size_of::<Operations>(),
266 // Ownership of session_ctx is passed to the session here.
267 // It has to be reclaimed before dropping the session to free
268 // the `Context` and close the underlying file. This is done inside
269 // the destroy callback function.
270 Box::into_raw(session_ctx) as ConstPtr,
271 )
272 };
273
274 if session_ptr.is_null() {
275 bail!("error while creating new fuse session");
276 }
277
278 if unsafe { fuse_set_signal_handlers(session_ptr) } != 0 {
279 bail!("error while setting signal handlers");
280 }
281
282 Ok(Self { ptr: session_ptr, verbose })
283 }
284
285 fn setup_args(options: &OsStr, verbose: bool) -> Result<Vec<CString>, Error> {
286 // First argument should be the executable name
287 let mut arguments = vec![
288 CString::new("pxar-mount").unwrap(),
289 CString::new("-o").unwrap(),
290 CString::new(options.as_bytes())?,
291 ];
292 if verbose {
293 arguments.push(CString::new("--debug").unwrap());
294 }
295
296 Ok(arguments)
297 }
298
299 fn setup_callbacks() -> Operations {
300 // Register the callback functions for the session
301 let mut oprs = Operations::default();
302 oprs.init = Some(Self::init);
303 oprs.destroy = Some(Self::destroy);
304 oprs.lookup = Some(Self::lookup);
305 oprs.getattr = Some(Self::getattr);
306 oprs.readlink = Some(Self::readlink);
307 oprs.open = Some(Self::open);
308 oprs.read = Some(Self::read);
309 oprs.opendir = Some(Self::opendir);
310 oprs.readdir = Some(Self::readdir);
311 oprs.releasedir = Some(Self::releasedir);
312 oprs.getxattr = Some(Self::getxattr);
313 oprs.listxattr = Some(Self::listxattr);
314 oprs
315 }
316
317 /// Mount the filesystem on the given mountpoint.
318 ///
319 /// Actually mount the filesystem for this session on the provided mountpoint
320 /// and daemonize process.
321 pub fn mount(&mut self, mountpoint: &Path, deamonize: bool) -> Result<(), Error> {
322 if self.verbose {
323 println!("Mounting archive to {:#?}", mountpoint);
324 }
325 let mountpoint = mountpoint.canonicalize()?;
326 let path_cstr = CString::new(mountpoint.as_os_str().as_bytes())
327 .map_err(|err| format_err!("invalid mountpoint - {}", err))?;
328 if unsafe { fuse_session_mount(self.ptr, path_cstr.as_ptr()) } != 0 {
329 bail!("mounting on {:#?} failed", mountpoint);
330 }
331
332 // Send process to background if deamonize is set
333 if deamonize && unsafe { fuse_daemonize(0) } != 0 {
334 bail!("could not send process to background");
335 }
336
337 Ok(())
338 }
339
340 /// Execute session loop which handles requests from kernel.
341 ///
342 /// The multi_threaded flag controls if the session loop runs in
343 /// single-threaded or multi-threaded mode.
344 /// Single-threaded mode is intended for debugging only.
345 pub fn run_loop(&mut self, multi_threaded: bool) -> Result<(), Error> {
346 if self.verbose {
347 println!("Executing fuse session loop");
348 }
349 let result = match multi_threaded {
350 true => unsafe { fuse_session_loop_mt_31(self.ptr, 1) },
351 false => unsafe { fuse_session_loop(self.ptr) },
352 };
353 if result < 0 {
354 bail!("fuse session loop exited with - {}", result);
355 }
356 if result > 0 {
357 eprintln!("fuse session loop received signal - {}", result);
358 }
359
360 Ok(())
361 }
362
363 /// Creates a context providing an exclusive mutable reference to the `Context`.
364 ///
365 /// Each callback function needing access to the `Context` can easily get an
366 /// exclusive handle by running the code inside this context.
367 /// Responses with error code can easily be generated by returning with the
368 /// error code.
369 /// The error code will be used to reply to libfuse.
370 fn run_in_context<F>(req: Request, inode: u64, code: F)
371 where
372 F: FnOnce(&mut Context) -> Result<(), i32>,
373 {
374 let boxed_ctx = unsafe {
375 let ptr = fuse_req_userdata(req) as *mut Mutex<Context>;
376 Box::from_raw(ptr)
377 };
378 let result = boxed_ctx
379 .lock()
380 .map(|mut ctx| {
381 ctx.ino_offset = match inode {
382 FUSE_ROOT_ID => ctx.decoder.root_end_offset() - GOODBYE_ITEM_SIZE,
383 _ => inode,
384 };
385 code(&mut ctx)
386 })
387 .unwrap_or(Err(libc::EIO));
388
389 if let Err(err) = result {
390 unsafe {
391 let _res = fuse_reply_err(req, err);
392 }
393 }
394
395 // Release ownership of boxed context, do not drop it.
396 let _ = Box::into_raw(boxed_ctx);
397 }
398
399 /// Callback functions for fuse kernel driver.
400 extern "C" fn init(_decoder: MutPtr) {
401 // Notting to do here for now
402 }
403
404 /// Cleanup the userdata created while creating the session, which is the `Context`
405 extern "C" fn destroy(ctx: MutPtr) {
406 // Get ownership of the `Context` and drop it when Box goes out of scope.
407 unsafe { Box::from_raw(ctx) };
408 }
409
410 /// Lookup `name` in the directory referenced by `parent` i-node.
411 ///
412 /// Inserts also the child and parent file offset in the hashmap to quickly
413 /// obtain the parent offset based on the child offset.
414 /// Caches goodbye table of parent and attributes of child, if found.
415 extern "C" fn lookup(req: Request, parent: u64, name: StrPtr) {
416 let filename = unsafe { CStr::from_ptr(name) };
417 let hash = super::format_definition::compute_goodbye_hash(filename.to_bytes());
418
419 Self::run_in_context(req, parent, |mut ctx| {
420 // find_ goodbye_entry() will also update the goodbye cache
421 let (child_offset, entry, attr, payload_size) =
422 ctx.find_goodbye_entry(&filename, hash)?;
423 ctx.attr_cache = Some((child_offset, attr));
424 let child_inode = calculate_inode(child_offset, ctx.decoder.root_end_offset());
425
426 let e = EntryParam {
427 inode: child_inode,
428 generation: 1,
429 attr: stat(child_inode, &entry, payload_size)?,
430 attr_timeout: std::f64::MAX,
431 entry_timeout: std::f64::MAX,
432 };
433
434 // Update the parent for this child entry. Used to get parent offset if
435 // only child offset is known.
436 CHILD_PARENT
437 .lock()
438 .map_err(|_| libc::EIO)?
439 .insert(child_offset, ctx.ino_offset);
440 let _res = unsafe { fuse_reply_entry(req, Some(&e)) };
441
442 Ok(())
443 });
444 }
445
446 extern "C" fn getattr(req: Request, inode: u64, _fileinfo: MutPtr) {
447 Self::run_in_context(req, inode, |ctx| {
448 let (_, entry, attr, payload_size) = ctx
449 .decoder
450 .attributes(ctx.ino_offset)
451 .map_err(|_| libc::EIO)?;
452 ctx.attr_cache = Some((ctx.ino_offset, attr));
453 let attr = stat(inode, &entry, payload_size)?;
454 let _res = unsafe {
455 // Since fs is read-only, the timeout can be max.
456 let timeout = std::f64::MAX;
457 fuse_reply_attr(req, Some(&attr), timeout)
458 };
459
460 Ok(())
461 });
462 }
463
464 extern "C" fn readlink(req: Request, inode: u64) {
465 Self::run_in_context(req, inode, |ctx| {
466 let (target, _) = ctx
467 .decoder
468 .read_link(ctx.ino_offset)
469 .map_err(|_| libc::EIO)?;
470 let link = CString::new(target.into_os_string().into_vec()).map_err(|_| libc::EIO)?;
471 let _ret = unsafe { fuse_reply_readlink(req, link.as_ptr()) };
472
473 Ok(())
474 });
475 }
476
477 extern "C" fn open(req: Request, inode: u64, fileinfo: MutPtr) {
478 Self::run_in_context(req, inode, |ctx| {
479 ctx.decoder.open(ctx.ino_offset).map_err(|_| libc::ENOENT)?;
480 let _ret = unsafe { fuse_reply_open(req, fileinfo) };
481
482 Ok(())
483 });
484 }
485
486 extern "C" fn read(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) {
487 Self::run_in_context(req, inode, |ctx| {
488 let mut data = ctx
489 .decoder
490 .read(ctx.ino_offset, size, offset as u64)
491 .map_err(|_| libc::EIO)?;
492
493 let _res = unsafe {
494 let len = data.len();
495 let dptr = data.as_mut_ptr() as *mut c_char;
496 fuse_reply_buf(req, dptr, len)
497 };
498
499 Ok(())
500 });
501 }
502
503 /// Open the directory referenced by the given inode for reading.
504 ///
505 /// This simply checks if the inode references a valid directory, no internal
506 /// state identifies the directory as opened.
507 extern "C" fn opendir(req: Request, inode: u64, fileinfo: MutPtr) {
508 Self::run_in_context(req, inode, |ctx| {
509 let gbt = ctx.decoder
510 .goodbye_table(None, ctx.ino_offset + GOODBYE_ITEM_SIZE)
511 .map_err(|_| libc::EIO)?;
512 ctx.goodbye_cache.insert(ctx.ino_offset, gbt);
513
514 let _ret = unsafe { fuse_reply_open(req, fileinfo as MutPtr) };
515
516 Ok(())
517 });
518 }
519
520 /// Read and return the entries of the directory referenced by i-node.
521 ///
522 /// Replies to the request with the entries fitting into a buffer of length
523 /// `size`, as requested by the caller.
524 /// `offset` identifies the start index of entries to return. This is used on
525 /// repeated calls, occurring if not all entries fitted into the buffer.
526 /// The goodbye table of the directory is cached in order to speedup repeated
527 /// calls occurring when not all entries fitted in the reply buffer.
528 extern "C" fn readdir(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) {
529 let offset = offset as usize;
530
531 Self::run_in_context(req, inode, |ctx| {
532 let gb_table = ctx.goodbye_cache.get(&ctx.ino_offset)
533 .ok_or_else(|| libc::EIO)?;
534 let n_entries = gb_table.len();
535 let mut buf = ReplyBuf::new(req, size, offset);
536
537 if offset < n_entries {
538 for e in gb_table[offset..gb_table.len()].iter() {
539 let (filename, entry, _, payload_size) =
540 ctx.decoder.attributes(e.1).map_err(|_| libc::EIO)?;
541 let name = CString::new(filename.as_bytes()).map_err(|_| libc::EIO)?;
542 let item_offset = find_offset(&entry, e.1, e.2);
543 let item_inode = calculate_inode(item_offset, ctx.decoder.root_end_offset());
544 let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?;
545 match buf.add_entry(&name, &attr) {
546 Ok(ReplyBufState::Okay) => {}
547 Ok(ReplyBufState::Overfull) => return buf.reply_filled(),
548 Err(_) => return Err(libc::EIO),
549 }
550 }
551 }
552
553 // Add current directory entry "."
554 if offset <= n_entries {
555 let (_, entry, _, payload_size) = ctx
556 .decoder
557 .attributes(ctx.ino_offset)
558 .map_err(|_| libc::EIO)?;
559 // No need to calculate i-node for current dir, since it is given as parameter
560 let attr = stat(inode, &entry, payload_size).map_err(|_| libc::EIO)?;
561 let name = CString::new(".").unwrap();
562 match buf.add_entry(&name, &attr) {
563 Ok(ReplyBufState::Okay) => {}
564 Ok(ReplyBufState::Overfull) => return buf.reply_filled(),
565 Err(_) => return Err(libc::EIO),
566 }
567 }
568
569 // Add parent directory entry ".."
570 if offset <= n_entries + 1 {
571 let parent_off = if inode == FUSE_ROOT_ID {
572 ctx.decoder.root_end_offset() - GOODBYE_ITEM_SIZE
573 } else {
574 let guard = CHILD_PARENT.lock().map_err(|_| libc::EIO)?;
575 *guard.get(&ctx.ino_offset).ok_or_else(|| libc::EIO)?
576 };
577 let (_, entry, _, payload_size) =
578 ctx.decoder.attributes(parent_off).map_err(|_| libc::EIO)?;
579 let item_inode = calculate_inode(parent_off, ctx.decoder.root_end_offset());
580 let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?;
581 let name = CString::new("..").unwrap();
582 match buf.add_entry(&name, &attr) {
583 Ok(ReplyBufState::Okay) => {}
584 Ok(ReplyBufState::Overfull) => return buf.reply_filled(),
585 Err(_) => return Err(libc::EIO),
586 }
587 }
588
589 buf.reply_filled()
590 });
591 }
592
593 extern "C" fn releasedir(req: Request, inode: u64, _fileinfo: MutPtr) {
594 Self::run_in_context(req, inode, |ctx| {
595 let _gbt = ctx.goodbye_cache.remove(&ctx.ino_offset);
596 Ok(())
597 });
598 }
599
600 /// Get the value of the extended attribute of `inode` identified by `name`.
601 extern "C" fn getxattr(req: Request, inode: u64, name: StrPtr, size: size_t) {
602 let name = unsafe { CStr::from_ptr(name) };
603
604 Self::run_in_context(req, inode, |ctx| {
605 let (_, _, xattrs, _) = ctx
606 .decoder
607 .attributes(ctx.ino_offset)
608 .map_err(|_| libc::EIO)?;
609 // security.capability is stored separately, check it first
610 if name.to_bytes() == b"security.capability" {
611 match xattrs.fcaps {
612 None => return Err(libc::ENODATA),
613 Some(mut fcaps) => return Self::xattr_reply_value(req, &mut fcaps.data, size),
614 }
615 }
616
617 for mut xattr in xattrs.xattrs {
618 if name.to_bytes() == xattr.name.as_slice() {
619 return Self::xattr_reply_value(req, &mut xattr.value, size);
620 }
621 }
622
623 Err(libc::ENODATA)
624 });
625 }
626
627 /// Get a list of the extended attribute of `inode`.
628 extern "C" fn listxattr(req: Request, inode: u64, size: size_t) {
629 Self::run_in_context(req, inode, |ctx| {
630 let (_, _, xattrs, _) = ctx
631 .decoder
632 .attributes(ctx.ino_offset)
633 .map_err(|_| libc::EIO)?;
634 let mut buffer = Vec::new();
635 if xattrs.fcaps.is_some() {
636 buffer.extend_from_slice(b"security.capability\0");
637 }
638 for mut xattr in xattrs.xattrs {
639 buffer.append(&mut xattr.name);
640 buffer.push(b'\0');
641 }
642 Self::xattr_reply_value(req, &mut buffer, size)
643 });
644 }
645
646 /// Helper function used to respond to get- and listxattr calls in order to
647 /// de-duplicate code.
648 fn xattr_reply_value(req: Request, value: &mut Vec<u8>, size: size_t) -> Result<(), i32> {
649 let len = value.len();
650
651 if size == 0 {
652 // reply the needed buffer size to fit value
653 let _res = unsafe { fuse_reply_xattr(req, len) };
654 } else if size < len {
655 // value does not fit into requested buffer size
656 return Err(libc::ERANGE);
657 } else {
658 // value fits into requested buffer size, send value
659 let _res = unsafe {
660 let vptr = value.as_mut_ptr() as *mut c_char;
661 fuse_reply_buf(req, vptr, len)
662 };
663 }
664
665 Ok(())
666 }
667 }
668
669 impl Drop for Session {
670 fn drop(&mut self) {
671 unsafe {
672 fuse_session_unmount(self.ptr);
673 fuse_remove_signal_handlers(self.ptr);
674 fuse_session_destroy(self.ptr);
675 }
676 }
677 }
678
679 /// Return the correct offset for the item based on its `PxarEntry` mode
680 ///
681 /// For directories, the offset for the corresponding `GOODBYE_TAIL_MARKER`
682 /// is returned.
683 /// If it is not a directory, the start offset is returned.
684 fn find_offset(entry: &PxarEntry, start: u64, end: u64) -> u64 {
685 if (entry.mode as u32 & libc::S_IFMT) == libc::S_IFDIR {
686 end - GOODBYE_ITEM_SIZE
687 } else {
688 start
689 }
690 }
691
692 /// Calculate the i-node based on the given `offset`
693 ///
694 /// This maps the `offset` to the correct i-node, which is simply the offset.
695 /// The root directory is an exception, as it has per definition `FUSE_ROOT_ID`.
696 /// `root_end` is the end offset of the root directory (archive end).
697 fn calculate_inode(offset: u64, root_end: u64) -> u64 {
698 // check for root offset which has to be mapped to `FUSE_ROOT_ID`
699 if offset == root_end - GOODBYE_ITEM_SIZE {
700 FUSE_ROOT_ID
701 } else {
702 offset
703 }
704 }
705
706 /// FUSE entry for fuse_reply_entry in lookup callback
707 #[repr(C)]
708 struct EntryParam {
709 inode: u64,
710 generation: u64,
711 attr: libc::stat,
712 attr_timeout: f64,
713 entry_timeout: f64,
714 }
715
716 /// Create a `libc::stat` with the provided i-node, entry and payload size
717 fn stat(inode: u64, entry: &PxarEntry, payload_size: u64) -> Result<libc::stat, i32> {
718 let nlink = match (entry.mode as u32) & libc::S_IFMT {
719 libc::S_IFDIR => 2,
720 _ => 1,
721 };
722 let time = i64::try_from(entry.mtime).map_err(|_| libc::EIO)?;
723 let sec = time / 1_000_000_000;
724 let nsec = time % 1_000_000_000;
725
726 let mut attr: libc::stat = unsafe { std::mem::zeroed() };
727 attr.st_ino = inode;
728 attr.st_nlink = nlink;
729 attr.st_mode = u32::try_from(entry.mode).map_err(|_| libc::EIO)?;
730 attr.st_size = i64::try_from(payload_size).map_err(|_| libc::EIO)?;
731 attr.st_uid = entry.uid;
732 attr.st_gid = entry.gid;
733 attr.st_atime = sec;
734 attr.st_atime_nsec = nsec;
735 attr.st_mtime = sec;
736 attr.st_mtime_nsec = nsec;
737 attr.st_ctime = sec;
738 attr.st_ctime_nsec = nsec;
739
740 Ok(attr)
741 }
742
743 /// State of ReplyBuf after last add_entry call
744 enum ReplyBufState {
745 /// Entry was successfully added to ReplyBuf
746 Okay,
747 /// Entry did not fit into ReplyBuf, was not added
748 Overfull,
749 }
750
751 /// Used to correctly fill and reply the buffer for the readdir callback
752 struct ReplyBuf {
753 /// internal buffer holding the binary data
754 buffer: Vec<u8>,
755 /// offset up to which the buffer is filled already
756 filled: usize,
757 /// fuse request the buffer is used to reply to
758 req: Request,
759 /// index of the next item, telling from were to start on the next readdir callback in
760 /// case not everything fitted in the buffer on the first reply.
761 next: usize,
762 }
763
764 impl ReplyBuf {
765 /// Create a new empty `ReplyBuf` of `size` with element counting index at `next`.
766 fn new(req: Request, size: usize, next: usize) -> Self {
767 Self {
768 buffer: vec![0; size],
769 filled: 0,
770 req,
771 next,
772 }
773 }
774
775 /// Reply to the `Request` with the filled buffer
776 fn reply_filled(&mut self) -> Result<(), i32> {
777 let _res = unsafe {
778 let ptr = self.buffer.as_mut_ptr() as *mut c_char;
779 fuse_reply_buf(self.req, ptr, self.filled)
780 };
781
782 Ok(())
783 }
784
785 /// Fill the buffer for the fuse reply with the next entry
786 fn add_entry(&mut self, name: &CString, attr: &libc::stat) -> Result<ReplyBufState, Error> {
787 self.next += 1;
788 let size = self.buffer.len();
789 let bytes = unsafe {
790 let bptr = self.buffer.as_mut_ptr() as *mut c_char;
791 let nptr = name.as_ptr();
792 fuse_add_direntry(
793 self.req,
794 bptr.offset(self.filled as isize),
795 size - self.filled,
796 nptr,
797 Some(&attr),
798 i32::try_from(self.next)?,
799 ) as usize
800 };
801 self.filled += bytes;
802 // Never exceed the max size requested in the callback (=buffer.len())
803 if self.filled > size {
804 // Entry did not fit, so go back to previous state
805 self.filled -= bytes;
806 self.next -= 1;
807 return Ok(ReplyBufState::Overfull);
808 }
809
810 Ok(ReplyBufState::Okay)
811 }
812 }