From 7f00f6ed56d8e9006e06fc26d6a9d77e8d56efbc Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Tue, 9 Jan 2024 23:12:04 +0100 Subject: [PATCH] use virtual FS for mount --- Cargo.toml | 1 + src/commands/mount.rs | 48 ++++- src/commands/mount/format.rs | 37 ++++ src/commands/mount/fs.rs | 381 +++++++++++++++++----------------- src/commands/mount/mountfs.rs | 255 +++++++++++++++++++++++ 5 files changed, 525 insertions(+), 197 deletions(-) create mode 100644 src/commands/mount/format.rs create mode 100644 src/commands/mount/mountfs.rs diff --git a/Cargo.toml b/Cargo.toml index 72432aa31..3d1c0bb83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ jemallocator-global = { version = "0.3.2", optional = true } mimalloc = { version = "0.1.39", default_features = false, optional = true } rhai = { workspace = true } simplelog = { workspace = true } +runtime-format = "0.1.3" [dev-dependencies] aho-corasick = { workspace = true } diff --git a/src/commands/mount.rs b/src/commands/mount.rs index bf606239d..20e518fbb 100644 --- a/src/commands/mount.rs +++ b/src/commands/mount.rs @@ -1,8 +1,11 @@ //! `mount` subcommand -mod fs; -use std::{ffi::OsStr, path::PathBuf}; +mod format; +pub mod fs; +mod mountfs; + +use mountfs::RusticFS; -use fs::RusticFS; +use std::{ffi::OsStr, path::PathBuf}; use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; @@ -10,14 +13,23 @@ use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; use fuse_mt::{mount, FuseMT}; -/// `dump` subcommand #[derive(clap::Parser, Command, Debug)] pub(crate) struct MountCmd { - /// file from snapshot to dump - #[clap(value_name = "SNAPSHOT[:PATH]")] - snap: String, + /// The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"] + #[clap(long)] + path_template: Option, + + /// The time template to use to display times in the path template. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for format options. [default: "%Y-%m-%d_%H-%M-%S"] + #[clap(long)] + time_template: Option, + #[clap(value_name = "PATH")] + /// The mount point to use mountpoint: PathBuf, + + /// Specify directly which path to mount + #[clap(value_name = "SNAPSHOT[:PATH]")] + snap: Option, } impl Runnable for MountCmd { @@ -34,12 +46,28 @@ impl MountCmd { let config = RUSTIC_APP.config(); let repo = open_repository(&config)?.to_indexed()?; - let node = - repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?; + + let path_template = self + .path_template + .clone() + .unwrap_or("[{hostname}]/[{label}]/{time}".to_string()); + let time_template = self + .time_template + .clone() + .unwrap_or("%Y-%m-%d_%H-%M-%S".to_string()); + + let sn_filter = |sn: &_| config.snapshot_filter.matches(sn); + let target_fs = if let Some(snap) = &self.snap { + let node = repo.node_from_snapshot_path(snap, sn_filter)?; + RusticFS::from_node(repo, node)? + } else { + let snapshots = repo.get_matching_snapshots(sn_filter)?; + RusticFS::from_snapshots(repo, snapshots, path_template, time_template)? + }; let options = [OsStr::new("-o"), OsStr::new("fsname=rusticfs")]; - let fs = FuseMT::new(RusticFS::from_node(repo, node)?, 1); + let fs = FuseMT::new(target_fs, 1); mount(fs, &self.mountpoint, &options)?; Ok(()) diff --git a/src/commands/mount/format.rs b/src/commands/mount/format.rs new file mode 100644 index 000000000..7466d4582 --- /dev/null +++ b/src/commands/mount/format.rs @@ -0,0 +1,37 @@ +use std::fmt; + +use runtime_format::{FormatKey, FormatKeyError}; +use rustic_core::repofile::SnapshotFile; + +pub struct FormattedSnapshot<'a>(pub &'a SnapshotFile, pub &'a str); + +impl<'a> FormatKey for FormattedSnapshot<'a> { + fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> { + match key { + "id" => write!(f, "{}", self.0.id), + "long_id" => write!(f, "{:?}", self.0.id), + "time" => write!(f, "{}", self.0.time.format(self.1)), + "username" => write!(f, "{}", self.0.username), + "hostname" => write!(f, "{}", self.0.hostname), + "label" => write!(f, "{}", self.0.label), + "tags" => write!(f, "{}", self.0.tags), + "backup_start" => { + if let Some(summary) = &self.0.summary { + write!(f, "{}", summary.backup_start.format(self.1)) + } else { + write!(f, "no_backup_start") + } + } + "backup_end" => { + if let Some(summary) = &self.0.summary { + write!(f, "{}", summary.backup_end.format(self.1)) + } else { + write!(f, "no_backup_end") + } + } + + _ => return Err(FormatKeyError::UnknownKey), + } + .map_err(FormatKeyError::Fmt) + } +} diff --git a/src/commands/mount/fs.rs b/src/commands/mount/fs.rs index aa502c8ae..e94ac318a 100644 --- a/src/commands/mount/fs.rs +++ b/src/commands/mount/fs.rs @@ -1,222 +1,229 @@ -#[cfg(not(windows))] -use std::os::unix::prelude::OsStrExt; use std::{ collections::BTreeMap, - ffi::{CString, OsStr}, - path::Path, - sync::RwLock, - time::{Duration, SystemTime}, + ffi::{OsStr, OsString}, + path::{Component, Path, PathBuf}, }; -use fuse_mt::{ - CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, ResultData, - ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultXattr, Xattr, -}; -use itertools::Itertools; -use rustic_core::{ - repofile::{Node, NodeType}, - OpenFile, -}; +use anyhow::{anyhow, bail}; +use runtime_format::FormatArgs; +use rustic_core::repofile::{Metadata, Node, NodeType, SnapshotFile}; use rustic_core::{Id, IndexedFull, Repository}; -pub(super) struct RusticFS { - repo: Repository, - root: Id, - open_files: RwLock>, - now: SystemTime, -} - -impl RusticFS { - pub(crate) fn from_node(repo: Repository, node: Node) -> anyhow::Result { - let open_files = RwLock::new(BTreeMap::new()); - - Ok(Self { - repo, - root: node.subtree.unwrap(), - open_files, - now: SystemTime::now(), - }) - } +use super::format::FormattedSnapshot; - fn node_from_path(&self, path: &Path) -> Result { - Ok(self - .repo - .node_from_path(self.root, path) - .map_err(|_| libc::ENOENT)?) - } +pub enum IdenticalSnapshot { + AsLink, + AsDir, } -fn node_to_filetype(node: &Node) -> FileType { - match node.node_type { - NodeType::File => FileType::RegularFile, - NodeType::Dir => FileType::Directory, - NodeType::Symlink { .. } => FileType::Symlink, - NodeType::Chardev { .. } => FileType::CharDevice, - NodeType::Dev { .. } => FileType::BlockDevice, - NodeType::Fifo => FileType::NamedPipe, - NodeType::Socket => FileType::Socket, - } +pub enum Latest { + No, + AsLink, + AsDir, } -fn node_type_to_rdev(tpe: &NodeType) -> u32 { - u32::try_from(match tpe { - NodeType::Dev { device } => *device, - NodeType::Chardev { device } => *device, - _ => 0, - }) - .unwrap() +pub enum FsTree { + RusticTree(Id), + Link(OsString), + VirtualTree(BTreeMap), } -impl FilesystemMT for RusticFS { - fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option) -> ResultEntry { - let node = self.node_from_path(path)?; - Ok(( - Duration::from_secs(1), - FileAttr { - /// Size in bytes - size: node.meta.size, - /// Size in blocks - blocks: 0, - // Time of last access - atime: node.meta.atime.map(SystemTime::from).unwrap_or(self.now), - /// Time of last modification - mtime: node.meta.mtime.map(SystemTime::from).unwrap_or(self.now), - /// Time of last metadata change - ctime: node.meta.ctime.map(SystemTime::from).unwrap_or(self.now), - /// Time of creation (macOS only) - crtime: self.now, - /// Kind of file (directory, file, pipe, etc.) - kind: node_to_filetype(&node), - /// Permissions - perm: node.meta.mode.unwrap_or(0) as u16, - /// Number of hard links - nlink: node.meta.links.try_into().unwrap_or(1), - /// User ID - uid: node.meta.uid.unwrap_or(0), - /// Group ID - gid: node.meta.gid.unwrap_or(0), - /// Device ID (if special file) - rdev: node_type_to_rdev(&node.node_type), - /// Flags (macOS only; see chflags(2)) - flags: 0, - }, - )) +impl FsTree { + pub fn new() -> Self { + Self::VirtualTree(BTreeMap::new()) } - #[cfg(not(windows))] - fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData { - let node = self.node_from_path(path)?; - if node.is_symlink() { - let target = node.node_type.to_link().as_os_str().as_bytes(); - Ok(target.to_vec()) - } else { - Err(libc::ENOSYS) - } - } + pub fn from_snapshots( + mut snapshots: Vec, + path_template: String, + time_template: String, + latest_option: Latest, + id_snap_option: IdenticalSnapshot, + ) -> anyhow::Result { + snapshots.sort_unstable(); + let mut root = FsTree::new(); + + // to handle identical trees + let mut last_parent = None; + let mut last_name = None; + let mut last_tree = Id::default(); + + // to handle "latest" entries + let mut dirs_for_link = BTreeMap::new(); + let mut dirs_for_snap = BTreeMap::new(); + + for snap in snapshots { + let path = FormatArgs::new( + path_template.as_str(), + &FormattedSnapshot(&snap, &time_template), + ) + .to_string(); + let path = Path::new(&path); + let filename = path.file_name().map(OsStr::to_os_string); + let parent_path = path.parent().map(Path::to_path_buf); + + // Save pathes for latest entries, if requested + if matches!(latest_option, Latest::AsLink) { + _ = dirs_for_link.insert(parent_path.clone(), filename.clone()); + } + if matches!(latest_option, Latest::AsDir) { + _ = dirs_for_snap.insert(parent_path.clone(), snap.tree); + } - fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen { - let node = self.node_from_path(path)?; - let open = self.repo.open_file(&node).map_err(|_| libc::ENOSYS)?; - let mut open_files = self.open_files.write().unwrap(); - let fh = open_files.last_key_value().map(|(fh, _)| *fh + 1).unwrap_or(0); - _ = open_files.insert(fh, open); - Ok((fh, 0)) - } + // Create the entry, potentially as symlink if requested + if last_parent != parent_path || last_name != filename { + if matches!(id_snap_option, IdenticalSnapshot::AsLink) + && last_parent == parent_path + && last_tree == snap.tree + { + if let Some(name) = last_name { + root.add_tree(path, FsTree::Link(name))?; + } + } else { + root.add_tree(path, FsTree::RusticTree(snap.tree))?; + } + } + last_parent = parent_path; + last_name = filename; + last_tree = snap.tree; + } - fn release( - &self, - _req: RequestInfo, - _path: &Path, - fh: u64, - _flags: u32, - _lock_owner: u64, - _flush: bool, - ) -> ResultEmpty { - _ = self.open_files.write().unwrap().remove(&fh); - Ok(()) + // Add latest entries if requested + match latest_option { + Latest::No => {} + Latest::AsLink => { + for (path, target) in dirs_for_link { + if let (Some(mut path), Some(target)) = (path, target) { + path.push("latest"); + root.add_tree(&path, FsTree::Link(target))?; + } + } + } + Latest::AsDir => { + for (path, tree) in dirs_for_snap { + if let Some(mut path) = path { + path.push("latest"); + root.add_tree(&path, FsTree::RusticTree(tree))?; + } + } + } + } + Ok(root) } - fn read( - &self, - _req: RequestInfo, - _path: &Path, - fh: u64, - offset: u64, - size: u32, - - callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult, - ) -> CallbackResult { - if let Some(open_file) = self.open_files.read().unwrap().get(&fh) { - if let Ok(data) = - self.repo - .read_file_at(open_file, offset.try_into().unwrap(), size as usize) - { - return callback(Ok(&data)); + pub fn add_tree(&mut self, path: &Path, new_tree: FsTree) -> anyhow::Result<()> { + let mut tree = self; + let mut components = path.components(); + let Component::Normal(last) = components.next_back().unwrap() else { + bail!("only normal paths allowed!"); + }; + + for comp in components { + if let Component::Normal(name) = comp { + match tree { + FsTree::VirtualTree(vtree) => { + tree = vtree + .entry(name.to_os_string()) + .or_insert(FsTree::VirtualTree(BTreeMap::new())); + } + _ => { + bail!("dir exists as non-virtual dir") + } + } } } - callback(Err(libc::ENOSYS)) - } - fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { - Ok((0, 0)) - } + let FsTree::VirtualTree(vtree) = tree else { + bail!("dir exists as non-virtual dir!") + }; - fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir { - let node = self.node_from_path(path)?; - - let tree = self - .repo - .get_tree(&node.subtree.unwrap()) - .map_err(|_| libc::ENOSYS)?; - - let result = tree - .nodes - .into_iter() - .map(|node| DirectoryEntry { - name: node.name(), - kind: node_to_filetype(&node), - }) - .collect(); - Ok(result) + _ = vtree.insert(last.to_os_string(), new_tree); + Ok(()) } - fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty { - Ok(()) + fn get_path(&self, path: &Path) -> anyhow::Result<(&Self, Option)> { + let mut tree = self; + let mut components = path.components(); + loop { + match tree { + Self::RusticTree(_) => { + let path: PathBuf = components.collect(); + return Ok((tree, Some(path))); + } + Self::VirtualTree(vtree) => match components.next() { + Some(std::path::Component::Normal(name)) => { + tree = vtree + .get(name) + .ok_or_else(|| anyhow!("name {:?} doesn't exist", name))?; + } + None => { + return Ok((tree, None)); + } + + _ => {} + }, + Self::Link(_) => return Ok((tree, None)), + } + } } - fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr { - let node = self.node_from_path(path)?; - let xattrs = node - .meta - .extended_attributes - .into_iter() - // convert into null-terminated [u8] - .map(|a| CString::new(a.name).unwrap().into_bytes_with_nul()) - .concat(); - - if size == 0 { - Ok(Xattr::Size(u32::try_from(xattrs.len()).unwrap())) - } else { - Ok(Xattr::Data(xattrs)) + pub fn node_from_path( + &self, + repo: &Repository, + path: &Path, + ) -> anyhow::Result { + let (tree, path) = self.get_path(path)?; + let meta = Metadata::default(); + match tree { + Self::RusticTree(tree_id) => { + return Ok(repo.node_from_path(*tree_id, &path.unwrap())?); + } + Self::VirtualTree(_) => { + return Ok(Node::new(String::new(), NodeType::Dir, meta, None, None)); + } + Self::Link(target) => { + return Ok(Node::new( + String::new(), + NodeType::from_link(Path::new(target)), + meta, + None, + None, + )); + } } } - fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr { - let node = self.node_from_path(path)?; - match node - .meta - .extended_attributes - .into_iter() - .find(|a| name == OsStr::new(&a.name)) - { - None => Err(libc::ENOSYS), - Some(attr) => { - if size == 0 { - Ok(Xattr::Size(u32::try_from(attr.value.len()).unwrap())) + pub fn dir_entries_from_path( + &self, + repo: &Repository, + path: &Path, + ) -> anyhow::Result> { + let (tree, path) = self.get_path(path)?; + + let result = match tree { + Self::RusticTree(tree_id) => { + let node = repo.node_from_path(*tree_id, &path.unwrap())?; + if node.is_dir() { + let tree = repo.get_tree(&node.subtree.unwrap())?; + tree.nodes } else { - Ok(Xattr::Data(attr.value)) + Vec::new() } } - } + Self::VirtualTree(vtree) => vtree + .iter() + .map(|(name, tree)| { + let node_type = match tree { + FsTree::Link(target) => NodeType::from_link(Path::new(target)), + _ => NodeType::Dir, + }; + Node::new_node(name, node_type, Metadata::default()) + }) + .collect(), + Self::Link(_) => { + bail!("no dir entries for symlink!"); + } + }; + Ok(result) } } diff --git a/src/commands/mount/mountfs.rs b/src/commands/mount/mountfs.rs new file mode 100644 index 000000000..f6c88e2f9 --- /dev/null +++ b/src/commands/mount/mountfs.rs @@ -0,0 +1,255 @@ +use super::fs::{FsTree, IdenticalSnapshot, Latest}; + +#[cfg(not(windows))] +use std::os::unix::prelude::OsStrExt; +use std::{ + collections::BTreeMap, + ffi::{CString, OsStr}, + path::Path, + sync::RwLock, + time::{Duration, SystemTime}, +}; + +use fuse_mt::{ + CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, ResultData, + ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultXattr, Xattr, +}; +use itertools::Itertools; +use rustic_core::{ + repofile::{Node, NodeType, SnapshotFile}, + IndexedFull, OpenFile, Repository, +}; + +pub(super) struct RusticFS { + repo: Repository, + root: FsTree, + open_files: RwLock>, + now: SystemTime, +} + +impl RusticFS { + pub(crate) fn from_node(repo: Repository, node: Node) -> anyhow::Result { + Self::new(repo, FsTree::RusticTree(node.subtree.unwrap())) + } + + pub(crate) fn from_snapshots( + repo: Repository, + snapshots: Vec, + path_template: String, + time_template: String, + ) -> anyhow::Result { + let root = FsTree::from_snapshots( + snapshots, + path_template, + time_template, + Latest::AsLink, + IdenticalSnapshot::AsLink, + )?; + Self::new(repo, root) + } + + pub(crate) fn new(repo: Repository, root: FsTree) -> anyhow::Result { + let open_files = RwLock::new(BTreeMap::new()); + + Ok(Self { + repo, + root, + open_files, + now: SystemTime::now(), + }) + } + + fn node_from_path(&self, path: &Path) -> Result { + Ok(self + .root + .node_from_path(&self.repo, path) + .map_err(|_| libc::ENOENT)?) + } + + fn dir_entries_from_path(&self, path: &Path) -> Result, i32> { + Ok(self + .root + .dir_entries_from_path(&self.repo, path) + .map_err(|_| libc::ENOENT)?) + } +} + +fn node_to_filetype(node: &Node) -> FileType { + match node.node_type { + NodeType::File => FileType::RegularFile, + NodeType::Dir => FileType::Directory, + NodeType::Symlink { .. } => FileType::Symlink, + NodeType::Chardev { .. } => FileType::CharDevice, + NodeType::Dev { .. } => FileType::BlockDevice, + NodeType::Fifo => FileType::NamedPipe, + NodeType::Socket => FileType::Socket, + } +} + +fn node_type_to_rdev(tpe: &NodeType) -> u32 { + u32::try_from(match tpe { + NodeType::Dev { device } => *device, + NodeType::Chardev { device } => *device, + _ => 0, + }) + .unwrap() +} + +fn node_to_linktarget(node: &Node) -> Option<&OsStr> { + if node.is_symlink() { + Some(node.node_type.to_link().as_os_str()) + } else { + None + } +} + +fn node_to_file_attr(node: &Node, now: SystemTime) -> FileAttr { + FileAttr { + /// Size in bytes + size: node.meta.size, + /// Size in blocks + blocks: 0, + // Time of last access + atime: node.meta.atime.map(SystemTime::from).unwrap_or(now), + /// Time of last modification + mtime: node.meta.mtime.map(SystemTime::from).unwrap_or(now), + /// Time of last metadata change + ctime: node.meta.ctime.map(SystemTime::from).unwrap_or(now), + /// Time of creation (macOS only) + crtime: now, + /// Kind of file (directory, file, pipe, etc.) + kind: node_to_filetype(node), + /// Permissions + perm: node.meta.mode.unwrap_or(0o755) as u16, + /// Number of hard links + nlink: node.meta.links.try_into().unwrap_or(1), + /// User ID + uid: node.meta.uid.unwrap_or(0), + /// Group ID + gid: node.meta.gid.unwrap_or(0), + /// Device ID (if special file) + rdev: node_type_to_rdev(&node.node_type), + /// Flags (macOS only; see chflags(2)) + flags: 0, + } +} + +impl FilesystemMT for RusticFS { + fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option) -> ResultEntry { + let node = self.node_from_path(path)?; + Ok((Duration::from_secs(1), node_to_file_attr(&node, self.now))) + } + + #[cfg(not(windows))] + fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData { + let target = node_to_linktarget(&self.node_from_path(path)?) + .ok_or(libc::ENOSYS)? + .as_bytes() + .to_vec(); + + Ok(target) + } + + fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen { + let node = self.node_from_path(path)?; + let open = self.repo.open_file(&node).map_err(|_| libc::ENOSYS)?; + let mut open_files = self.open_files.write().unwrap(); + let fh = open_files + .last_key_value() + .map(|(fh, _)| *fh + 1) + .unwrap_or(0); + _ = open_files.insert(fh, open); + Ok((fh, 0)) + } + + fn release( + &self, + _req: RequestInfo, + _path: &Path, + fh: u64, + _flags: u32, + _lock_owner: u64, + _flush: bool, + ) -> ResultEmpty { + _ = self.open_files.write().unwrap().remove(&fh); + Ok(()) + } + + fn read( + &self, + _req: RequestInfo, + _path: &Path, + fh: u64, + offset: u64, + size: u32, + + callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult, + ) -> CallbackResult { + if let Some(open_file) = self.open_files.read().unwrap().get(&fh) { + if let Ok(data) = + self.repo + .read_file_at(open_file, offset.try_into().unwrap(), size as usize) + { + return callback(Ok(&data)); + } + } + callback(Err(libc::ENOSYS)) + } + + fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { + Ok((0, 0)) + } + + fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir { + let nodes = self.dir_entries_from_path(path)?; + + let result = nodes + .into_iter() + .map(|node| DirectoryEntry { + name: node.name(), + kind: node_to_filetype(&node), + }) + .collect(); + Ok(result) + } + + fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty { + Ok(()) + } + + fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr { + let node = self.node_from_path(path)?; + let xattrs = node + .meta + .extended_attributes + .into_iter() + // convert into null-terminated [u8] + .map(|a| CString::new(a.name).unwrap().into_bytes_with_nul()) + .concat(); + + if size == 0 { + Ok(Xattr::Size(u32::try_from(xattrs.len()).unwrap())) + } else { + Ok(Xattr::Data(xattrs)) + } + } + + fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr { + let node = self.node_from_path(path)?; + match node + .meta + .extended_attributes + .into_iter() + .find(|a| name == OsStr::new(&a.name)) + { + None => Err(libc::ENOSYS), + Some(attr) => { + if size == 0 { + Ok(Xattr::Size(u32::try_from(attr.value.len()).unwrap())) + } else { + Ok(Xattr::Data(attr.value)) + } + } + } + } +}