diff --git a/Cargo.lock b/Cargo.lock index cafee087..4bbe68ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1118,6 +1118,7 @@ dependencies = [ "rustic_backend", "rustic_core", "simplelog", + "typed-path", ] [[package]] @@ -3208,6 +3209,7 @@ dependencies = [ "tempfile", "thiserror 2.0.9", "toml", + "typed-path", "walkdir", "xattr", "zstd", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 880708d8..c6550fd5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -105,6 +105,7 @@ itertools = "0.13.0" quick_cache = "0.6.9" shell-words = "1.1.0" strum = { version = "0.26.3", features = ["derive"] } +typed-path = "0.10.0" zstd = "0.13.2" [target.'cfg(not(windows))'.dependencies] diff --git a/crates/core/src/archiver.rs b/crates/core/src/archiver.rs index fa2f6db6..ec47b43a 100644 --- a/crates/core/src/archiver.rs +++ b/crates/core/src/archiver.rs @@ -3,11 +3,10 @@ pub(crate) mod parent; pub(crate) mod tree; pub(crate) mod tree_archiver; -use std::path::{Path, PathBuf}; - use chrono::Local; use log::warn; use pariter::{scope, IteratorExt}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ archiver::{ @@ -126,8 +125,8 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { pub fn archive( mut self, src: &R, - backup_path: &Path, - as_path: Option<&PathBuf>, + backup_path: &UnixPath, + as_path: Option<&UnixPathBuf>, skip_identical_parent: bool, no_scan: bool, p: &impl Progress, diff --git a/crates/core/src/archiver/parent.rs b/crates/core/src/archiver/parent.rs index f1eb176f..b48295ce 100644 --- a/crates/core/src/archiver/parent.rs +++ b/crates/core/src/archiver/parent.rs @@ -1,7 +1,4 @@ -use std::{ - cmp::Ordering, - ffi::{OsStr, OsString}, -}; +use std::cmp::Ordering; use log::warn; @@ -124,7 +121,7 @@ impl Parent { /// # Returns /// /// The parent node with the given name, or `None` if the parent node is not found. - fn p_node(&mut self, name: &OsStr) -> Option<&Node> { + fn p_node(&mut self, name: &[u8]) -> Option<&Node> { match &self.tree { None => None, Some(tree) => { @@ -132,12 +129,12 @@ impl Parent { loop { match p_nodes.get(self.node_idx) { None => break None, - Some(p_node) => match p_node.name().as_os_str().cmp(name) { - Ordering::Less => self.node_idx += 1, + Some(p_node) => match name.cmp(&p_node.name()) { + Ordering::Greater => self.node_idx += 1, Ordering::Equal => { break Some(p_node); } - Ordering::Greater => { + Ordering::Less => { break None; } }, @@ -161,7 +158,7 @@ impl Parent { /// # Note /// /// TODO: This function does not check whether the given node is a directory. - fn is_parent(&mut self, node: &Node, name: &OsStr) -> ParentResult<&Node> { + fn is_parent(&mut self, node: &Node, name: &[u8]) -> ParentResult<&Node> { // use new variables as the mutable borrow is used later let ignore_ctime = self.ignore_ctime; let ignore_inode = self.ignore_inode; @@ -190,12 +187,7 @@ impl Parent { /// /// * `be` - The backend to read from. /// * `name` - The name of the parent node. - fn set_dir( - &mut self, - be: &impl DecryptReadBackend, - index: &impl ReadGlobalIndex, - name: &OsStr, - ) { + fn set_dir(&mut self, be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, name: &[u8]) { let tree = self.p_node(name).and_then(|p_node| { p_node.subtree.map_or_else( || { @@ -257,7 +249,7 @@ impl Parent { &mut self, be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, - item: TreeType, + item: TreeType>, ) -> Result, TreeStackEmptyError> { let result = match item { TreeType::NewTree((path, node, tree)) => { diff --git a/crates/core/src/archiver/tree.rs b/crates/core/src/archiver/tree.rs index cc5c4297..87cf9254 100644 --- a/crates/core/src/archiver/tree.rs +++ b/crates/core/src/archiver/tree.rs @@ -1,9 +1,6 @@ -use std::{ffi::OsString, path::PathBuf}; +use crate::backend::node::{Metadata, Node, NodeType}; -use crate::{ - backend::node::{Metadata, Node, NodeType}, - blob::tree::comp_to_osstr, -}; +use typed_path::{Component, UnixPathBuf}; /// `TreeIterator` turns an Iterator yielding items with paths and Nodes into an /// Iterator which ensures that all subdirectories are visited and closed. @@ -18,7 +15,7 @@ pub(crate) struct TreeIterator { /// The original Iterator. iter: I, /// The current path. - path: PathBuf, + path: UnixPathBuf, /// The current item. item: Option, } @@ -31,7 +28,7 @@ where let item = iter.next(); Self { iter, - path: PathBuf::new(), + path: UnixPathBuf::new(), item, } } @@ -49,32 +46,25 @@ where #[derive(Debug)] pub(crate) enum TreeType { /// New tree to be inserted. - NewTree((PathBuf, Node, U)), + NewTree((UnixPathBuf, Node, U)), /// A pseudo item which indicates that a tree is finished. EndTree, /// Original item. - Other((PathBuf, Node, T)), + Other((UnixPathBuf, Node, T)), } -impl Iterator for TreeIterator<(PathBuf, Node, O), I> +impl Iterator for TreeIterator<(UnixPathBuf, Node, O), I> where - I: Iterator, + I: Iterator, { - type Item = TreeType; + type Item = TreeType>; fn next(&mut self) -> Option { match &self.item { None => { if self.path.pop() { Some(TreeType::EndTree) } else { - // Check if we still have a path prefix open... - match self.path.components().next() { - Some(std::path::Component::Prefix(..)) => { - self.path = PathBuf::new(); - Some(TreeType::EndTree) - } - _ => None, - } + None } } Some((path, node, _)) => { @@ -84,24 +74,25 @@ where Some(TreeType::EndTree) } Ok(missing_dirs) => { - for comp in missing_dirs.components() { - self.path.push(comp); - // process next normal path component - other components are simply ignored - if let Some(p) = comp_to_osstr(comp).ok().flatten() { - if node.is_dir() && path == &self.path { - let (path, node, _) = self.item.take().unwrap(); - self.item = self.iter.next(); - let name = node.name(); - return Some(TreeType::NewTree((path, node, name))); - } - // Use mode 755 for missing dirs, so they can be accessed - let meta = Metadata { - mode: Some(0o755), - ..Default::default() - }; - let node = Node::new_node(&p, NodeType::Dir, meta); - return Some(TreeType::NewTree((self.path.clone(), node, p))); + if let Some(p) = missing_dirs.components().next() { + self.path.push(p); + if node.is_dir() && path == &self.path { + let (path, node, _) = self.item.take().unwrap(); + self.item = self.iter.next(); + let name = node.name().to_vec(); + return Some(TreeType::NewTree((path, node, name))); } + // Use mode 755 for missing dirs, so they can be accessed + let meta = Metadata { + mode: Some(0o755), + ..Default::default() + }; + let node = Node::new_node(p.as_bytes(), NodeType::Dir, meta); + return Some(TreeType::NewTree(( + self.path.clone(), + node, + p.as_bytes().to_vec(), + ))); } // there wasn't any normal path component to process - return current item let item = self.item.take().unwrap(); diff --git a/crates/core/src/archiver/tree_archiver.rs b/crates/core/src/archiver/tree_archiver.rs index df2ee52a..0e9483a8 100644 --- a/crates/core/src/archiver/tree_archiver.rs +++ b/crates/core/src/archiver/tree_archiver.rs @@ -1,7 +1,6 @@ -use std::path::{Path, PathBuf}; - use bytesize::ByteSize; use log::{debug, trace}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ archiver::{parent::ParentResult, tree::TreeType}, @@ -30,7 +29,7 @@ pub(crate) struct TreeArchiver<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> /// The current tree. tree: Tree, /// The stack of trees. - stack: Vec<(PathBuf, Node, ParentResult, Tree)>, + stack: Vec<(UnixPathBuf, Node, ParentResult, Tree)>, /// The index to read from. index: &'a I, /// The packer to write to. @@ -129,7 +128,7 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> TreeArchiver<'a, BE, I> { /// * `path` - The path of the file. /// * `node` - The node of the file. /// * `parent` - The parent result of the file. - fn add_file(&mut self, path: &Path, node: Node, parent: &ParentResult<()>, size: u64) { + fn add_file(&mut self, path: &UnixPath, node: Node, parent: &ParentResult<()>, size: u64) { let filename = path.join(node.name()); match parent { ParentResult::Matched(()) => { @@ -164,7 +163,11 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> TreeArchiver<'a, BE, I> { /// # Returns /// /// The id of the tree. - fn backup_tree(&mut self, path: &Path, parent: &ParentResult) -> RusticResult { + fn backup_tree( + &mut self, + path: &UnixPath, + parent: &ParentResult, + ) -> RusticResult { let (chunk, id) = self.tree.serialize().map_err(|err| { RusticError::with_source( ErrorKind::Internal, @@ -224,7 +227,7 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> TreeArchiver<'a, BE, I> { parent_tree: Option, ) -> RusticResult<(TreeId, SnapshotSummary)> { let parent = parent_tree.map_or(ParentResult::NotFound, ParentResult::Matched); - let id = self.backup_tree(&PathBuf::new(), &parent)?; + let id = self.backup_tree(UnixPath::new(&[]), &parent)?; let stats = self.tree_packer.finalize()?; stats.apply(&mut self.summary, BlobType::Tree); diff --git a/crates/core/src/backend.rs b/crates/core/src/backend.rs index 2179671b..1adb876b 100644 --- a/crates/core/src/backend.rs +++ b/crates/core/src/backend.rs @@ -20,6 +20,7 @@ use log::trace; use mockall::mock; use serde_derive::{Deserialize, Serialize}; +use typed_path::UnixPathBuf; use crate::{ backend::node::{Metadata, Node, NodeType}, @@ -31,8 +32,8 @@ use crate::{ #[derive(thiserror::Error, Debug, displaydoc::Display)] #[non_exhaustive] pub enum BackendErrorKind { - /// Path is not allowed: `{0:?}` - PathNotAllowed(PathBuf), + /// Path is not allowed: `{0}` + PathNotAllowed(String), } pub(crate) type BackendResult = Result; @@ -416,7 +417,7 @@ impl std::fmt::Debug for dyn WriteBackend { #[derive(Debug, Clone)] pub struct ReadSourceEntry { /// The path of the entry. - pub path: PathBuf, + pub path: UnixPathBuf, /// The node information of the entry. pub node: Node, @@ -426,10 +427,11 @@ pub struct ReadSourceEntry { } impl ReadSourceEntry { - fn from_path(path: PathBuf, open: Option) -> BackendResult { + fn from_path(path: UnixPathBuf, open: Option) -> BackendResult { let node = Node::new_node( - path.file_name() - .ok_or_else(|| BackendErrorKind::PathNotAllowed(path.clone()))?, + path.file_name().ok_or_else(|| { + BackendErrorKind::PathNotAllowed(path.to_string_lossy().to_string()) + })?, NodeType::File, Metadata::default(), ); diff --git a/crates/core/src/backend/childstdout.rs b/crates/core/src/backend/childstdout.rs index 49b95dba..d4fda1c9 100644 --- a/crates/core/src/backend/childstdout.rs +++ b/crates/core/src/backend/childstdout.rs @@ -1,10 +1,11 @@ use std::{ iter::{once, Once}, - path::PathBuf, process::{Child, ChildStdout, Command, Stdio}, sync::Mutex, }; +use typed_path::UnixPathBuf; + use crate::{ backend::{ReadSource, ReadSourceEntry}, error::{ErrorKind, RusticError, RusticResult}, @@ -15,7 +16,7 @@ use crate::{ #[derive(Debug)] pub struct ChildStdoutSource { /// The path of the stdin entry. - path: PathBuf, + path: UnixPathBuf, /// The child process /// /// # Note @@ -30,14 +31,14 @@ pub struct ChildStdoutSource { impl ChildStdoutSource { /// Creates a new `ChildSource`. - pub fn new(cmd: &CommandInput, path: PathBuf) -> RusticResult { + pub fn new(cmd: &CommandInput, path: UnixPathBuf) -> RusticResult { let process = Command::new(cmd.command()) .args(cmd.args()) .stdout(Stdio::piped()) .spawn() .map_err(|err| CommandInputErrorKind::ProcessExecutionFailed { command: cmd.clone(), - path: path.clone(), + path: path.to_string_lossy().to_string(), source: err, }); diff --git a/crates/core/src/backend/ignore.rs b/crates/core/src/backend/ignore.rs index f33b49ce..08e29f84 100644 --- a/crates/core/src/backend/ignore.rs +++ b/crates/core/src/backend/ignore.rs @@ -610,11 +610,15 @@ fn map_entry( with_atime: bool, ignore_devid: bool, ) -> IgnoreResult> { - let name = entry.file_name(); + use std::os::unix::ffi::OsStrExt; + + use typed_path::{UnixPath, UnixPathBuf}; + + let name = entry.file_name().as_bytes(); let m = entry .metadata() .map_err(|err| IgnoreErrorKind::AcquiringMetadataFailed { - name: name.to_string_lossy().to_string(), + name: String::from_utf8_lossy(name).to_string(), source: err, })?; @@ -684,11 +688,14 @@ fn map_entry( Node::new_node(name, NodeType::Dir, meta) } else if m.is_symlink() { let path = entry.path(); - let target = read_link(path).map_err(|err| IgnoreErrorKind::ErrorLink { - path: path.to_path_buf(), - source: err, - })?; - let node_type = NodeType::from_link(&target); + let target = read_link(path) + .map_err(|err| IgnoreErrorKind::ErrorLink { + path: path.to_path_buf(), + source: err, + })? + .into_os_string(); + let target = target.as_encoded_bytes(); + let node_type = NodeType::from_link(&UnixPath::new(target).to_typed_path()); Node::new_node(name, node_type, meta) } else if filetype.is_block_device() { let node_type = NodeType::Dev { device: m.rdev() }; @@ -705,6 +712,7 @@ fn map_entry( }; let path = entry.into_path(); let open = Some(OpenFile(path.clone())); + let path: UnixPathBuf = path.try_into().unwrap(); // TODO: Error handling Ok(ReadSourceEntry { path, node, open }) } diff --git a/crates/core/src/backend/local_destination.rs b/crates/core/src/backend/local_destination.rs index bef3852b..027c712a 100644 --- a/crates/core/src/backend/local_destination.rs +++ b/crates/core/src/backend/local_destination.rs @@ -23,6 +23,7 @@ use nix::{ fcntl::AtFlags, unistd::{fchownat, Gid, Group, Uid, User}, }; +use typed_path::UnixPath; #[cfg(not(windows))] use crate::backend::ignore::mapper::map_mode_from_go; @@ -189,11 +190,20 @@ impl LocalDestination { /// /// * If the destination is a file, this will return the base path. /// * If the destination is a directory, this will return the base path joined with the item. - pub(crate) fn path(&self, item: impl AsRef) -> PathBuf { + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn path(&self, item: impl AsRef) -> LocalDestinationResult { if self.is_file { - self.path.clone() - } else { - self.path.join(item) + return Ok(self.path.clone()); + } + #[cfg(not(windows))] + { + let item = PathBuf::from(item.as_ref()); + Ok(self.path.join(item)) + } + #[cfg(windows)] + { + let item = PathBuf::try_from(item.as_ref())?; + Ok(self.path.join(item)) } } @@ -249,8 +259,8 @@ impl LocalDestination { /// # Notes /// /// This will create the directory structure recursively. - pub(crate) fn create_dir(&self, item: impl AsRef) -> LocalDestinationResult<()> { - let dirname = self.path.join(item); + pub(crate) fn create_dir(&self, item: impl AsRef) -> LocalDestinationResult<()> { + let dirname = self.path(item)?; fs::create_dir_all(dirname).map_err(LocalDestinationErrorKind::DirectoryCreationFailed)?; Ok(()) } @@ -267,10 +277,10 @@ impl LocalDestination { /// * If the times could not be set pub(crate) fn set_times( &self, - item: impl AsRef, + item: impl AsRef, meta: &Metadata, ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; if let Some(mtime) = meta.mtime { let atime = meta.atime.unwrap_or(mtime); set_symlink_file_times( @@ -322,10 +332,10 @@ impl LocalDestination { #[allow(clippy::similar_names)] pub(crate) fn set_user_group( &self, - item: impl AsRef, + item: impl AsRef, meta: &Metadata, ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; let user = meta.user.clone().and_then(uid_from_name); // use uid from user if valid, else from saved uid (if saved) @@ -375,10 +385,10 @@ impl LocalDestination { #[allow(clippy::similar_names)] pub(crate) fn set_uid_gid( &self, - item: impl AsRef, + item: impl AsRef, meta: &Metadata, ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; let uid = meta.uid.map(Uid::from_raw); let gid = meta.gid.map(Gid::from_raw); @@ -423,14 +433,14 @@ impl LocalDestination { #[allow(clippy::similar_names)] pub(crate) fn set_permission( &self, - item: impl AsRef, + item: impl AsRef, node: &Node, ) -> LocalDestinationResult<()> { if node.is_symlink() { return Ok(()); } - let filename = self.path(item); + let filename = self.path(item)?; if let Some(mode) = node.meta.mode { let mode = map_mode_from_go(mode); @@ -456,7 +466,7 @@ impl LocalDestination { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub(crate) fn set_extended_attributes( &self, - _item: impl AsRef, + _item: impl AsRef, _extended_attributes: &[ExtendedAttribute], ) -> LocalDestinationResult<()> { Ok(()) @@ -485,10 +495,10 @@ impl LocalDestination { /// * If the extended attributes could not be set. pub(crate) fn set_extended_attributes( &self, - item: impl AsRef, + item: impl AsRef, extended_attributes: &[ExtendedAttribute], ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; let mut done = vec![false; extended_attributes.len()]; for curr_name in xattr::list(&filename).map_err(|err| { @@ -497,9 +507,11 @@ impl LocalDestination { path: filename.clone(), } })? { - match extended_attributes.iter().enumerate().find( - |(_, ExtendedAttribute { name, .. })| name == curr_name.to_string_lossy().as_ref(), - ) { + match extended_attributes + .iter() + .enumerate() + .find(|(_, ExtendedAttribute { name, .. })| curr_name.to_string_lossy() == *name) + { Some((index, ExtendedAttribute { name, value })) => { let curr_value = xattr::get(&filename, name).map_err(|err| { LocalDestinationErrorKind::GettingXattrFailed { @@ -561,10 +573,10 @@ impl LocalDestination { /// If it doesn't exist, create a new (empty) one with given length. pub(crate) fn set_length( &self, - item: impl AsRef, + item: impl AsRef, size: u64, ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; let dir = filename .parent() .ok_or_else(|| LocalDestinationErrorKind::FileDoesNotHaveParent(filename.clone()))?; @@ -600,7 +612,7 @@ impl LocalDestination { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub(crate) fn create_special( &self, - _item: impl AsRef, + _item: impl AsRef, _node: &Node, ) -> LocalDestinationResult<()> { Ok(()) @@ -621,17 +633,18 @@ impl LocalDestination { /// * If the device could not be created. pub(crate) fn create_special( &self, - item: impl AsRef, + item: impl AsRef, node: &Node, ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; match &node.node_type { NodeType::Symlink { .. } => { - let linktarget = node.node_type.to_link(); - symlink(linktarget, &filename).map_err(|err| { + let linktarget: PathBuf = + node.node_type.to_link().to_path_buf().try_into().unwrap(); // TODO: Error handling + symlink(linktarget.clone(), &filename).map_err(|err| { LocalDestinationErrorKind::SymlinkingFailed { - linktarget: linktarget.to_path_buf(), + linktarget, filename, source: err, } @@ -718,11 +731,11 @@ impl LocalDestination { /// * If the length of the file could not be read. pub(crate) fn read_at( &self, - item: impl AsRef, + item: impl AsRef, offset: u64, length: u64, ) -> LocalDestinationResult { - let filename = self.path(item); + let filename = self.path(item)?; let mut file = File::open(filename).map_err(LocalDestinationErrorKind::OpeningFileFailed)?; _ = file @@ -754,8 +767,8 @@ impl LocalDestination { /// /// If a file exists and size matches, this returns a `File` open for reading. /// In all other cases, returns `None` - pub fn get_matching_file(&self, item: impl AsRef, size: u64) -> Option { - let filename = self.path(item); + pub fn get_matching_file(&self, item: impl AsRef, size: u64) -> Option { + let filename = self.path(item).ok()?; fs::symlink_metadata(&filename).map_or_else( |_| None, |meta| { @@ -787,11 +800,11 @@ impl LocalDestination { /// This will create the file if it doesn't exist. pub(crate) fn write_at( &self, - item: impl AsRef, + item: impl AsRef, offset: u64, data: &[u8], ) -> LocalDestinationResult<()> { - let filename = self.path(item); + let filename = self.path(item)?; let mut file = OpenOptions::new() .create(true) .truncate(false) diff --git a/crates/core/src/backend/node.rs b/crates/core/src/backend/node.rs index 1c6635d5..0c211c33 100644 --- a/crates/core/src/backend/node.rs +++ b/crates/core/src/backend/node.rs @@ -1,17 +1,9 @@ -use std::{ - cmp::Ordering, - ffi::{OsStr, OsString}, - fmt::Debug, - path::Path, - str::FromStr, -}; +use std::{borrow::Cow, cmp::Ordering, fmt::Debug}; #[cfg(not(windows))] use std::fmt::Write; #[cfg(not(windows))] use std::num::ParseIntError; -#[cfg(not(windows))] -use std::os::unix::ffi::OsStrExt; use chrono::{DateTime, Local}; use derive_more::Constructor; @@ -22,6 +14,7 @@ use serde_with::{ formats::Padded, serde_as, skip_serializing_none, DefaultOnNull, }; +use typed_path::TypedPath; use crate::blob::{tree::TreeId, DataId}; @@ -170,15 +163,14 @@ pub enum NodeType { } impl NodeType { - #[cfg(not(windows))] /// Get a [`NodeType`] from a linktarget path #[must_use] - pub fn from_link(target: &Path) -> Self { + pub fn from_link(target: &TypedPath<'_>) -> Self { let (linktarget, linktarget_raw) = target.to_str().map_or_else( || { ( - target.as_os_str().to_string_lossy().to_string(), - Some(target.as_os_str().as_bytes().to_vec()), + target.to_string_lossy().to_string(), + Some(target.as_bytes().to_vec()), ) }, |t| (t.to_string(), None), @@ -189,57 +181,23 @@ impl NodeType { } } - #[cfg(windows)] - // Windows doesn't support non-unicode link targets, so we assume unicode here. - // TODO: Test and check this! - /// Get a [`NodeType`] from a linktarget path - #[must_use] - pub fn from_link(target: &Path) -> Self { - Self::Symlink { - linktarget: target.as_os_str().to_string_lossy().to_string(), - linktarget_raw: None, - } - } - // Must be only called on NodeType::Symlink! /// Get the link path from a `NodeType::Symlink`. /// /// # Panics /// /// * If called on a non-symlink node - #[cfg(not(windows))] #[must_use] - pub fn to_link(&self) -> &Path { - match self { + pub fn to_link(&self) -> TypedPath<'_> { + TypedPath::derive(match self { Self::Symlink { linktarget, linktarget_raw, - } => linktarget_raw.as_ref().map_or_else( - || Path::new(linktarget), - |t| Path::new(OsStr::from_bytes(t)), - ), - _ => panic!("called method to_link on non-symlink!"), - } - } - - /// Convert a `NodeType::Symlink` to a `Path`. - /// - /// # Warning - /// - /// * Must be only called on `NodeType::Symlink`! - /// - /// # Panics - /// - /// * If called on a non-symlink node - /// * If the link target is not valid unicode - // TODO: Implement non-unicode link targets correctly for windows - #[cfg(windows)] - #[must_use] - pub fn to_link(&self) -> &Path { - match self { - Self::Symlink { linktarget, .. } => Path::new(linktarget), + } => linktarget_raw + .as_ref() + .map_or_else(|| linktarget.as_bytes(), |t| t), _ => panic!("called method to_link on non-symlink!"), - } + }) } } @@ -313,7 +271,7 @@ impl Node { /// /// The created [`Node`] #[must_use] - pub fn new_node(name: &OsStr, node_type: NodeType, meta: Metadata) -> Self { + pub fn new_node(name: &[u8], node_type: NodeType, meta: Metadata) -> Self { Self { name: escape_filename(name), node_type, @@ -359,8 +317,8 @@ impl Node { /// # Panics /// /// * If the name is not valid unicode - pub fn name(&self) -> OsString { - unescape_filename(&self.name).unwrap_or_else(|_| OsString::from_str(&self.name).unwrap()) + pub fn name(&self) -> Cow<'_, [u8]> { + unescape_filename(&self.name).map_or(Cow::Borrowed(self.name.as_bytes()), Cow::Owned) } } @@ -407,8 +365,8 @@ fn unescape_filename(s: &str) -> Result { // stconv.Quote, see https://pkg.go.dev/strconv#Quote // However, so far there was no specification what Quote really does, so this // is some kind of try-and-error and maybe does not cover every case. -fn escape_filename(name: &OsStr) -> String { - let mut input = name.as_bytes(); +fn escape_filename(name: &[u8]) -> String { + let mut input = name; let mut s = String::with_capacity(name.len()); let push = |s: &mut String, p: &str| { @@ -462,7 +420,7 @@ fn escape_filename(name: &OsStr) -> String { /// /// * `s` - The escaped filename // inspired by the enquote crate -fn unescape_filename(s: &str) -> NodeResult<'_, OsString> { +fn unescape_filename(s: &str) -> NodeResult<'_, Vec> { let mut chars = s.chars(); let mut u = Vec::new(); loop { @@ -559,7 +517,7 @@ fn unescape_filename(s: &str) -> NodeResult<'_, OsString> { } } - Ok(OsStr::from_bytes(&u).to_os_string()) + Ok(u) } #[cfg(not(windows))] @@ -583,9 +541,8 @@ mod tests { #[quickcheck] #[allow(clippy::needless_pass_by_value)] - fn escape_unescape_is_identity(bytes: Vec) -> bool { - let name = OsStr::from_bytes(&bytes); - name == match unescape_filename(&escape_filename(name)) { + fn escape_unescape_is_identity(name: Vec) -> bool { + name == match unescape_filename(&escape_filename(&name)) { Ok(s) => s, Err(_) => return false, } @@ -609,8 +566,7 @@ mod tests { #[case(b"\xc3\x9f", "\u{00df}")] #[case(b"\xe2\x9d\xa4", "\u{2764}")] #[case(b"\xf0\x9f\x92\xaf", "\u{01f4af}")] - fn escape_cases(#[case] input: &[u8], #[case] expected: &str) { - let name = OsStr::from_bytes(input); + fn escape_cases(#[case] name: &[u8], #[case] expected: &str) { assert_eq!(expected, escape_filename(name)); } @@ -634,14 +590,13 @@ mod tests { #[case(r#"\u2764"#, b"\xe2\x9d\xa4")] #[case(r#"\U0001f4af"#, b"\xf0\x9f\x92\xaf")] fn unescape_cases(#[case] input: &str, #[case] expected: &[u8]) { - let expected = OsStr::from_bytes(expected); assert_eq!(expected, unescape_filename(input).unwrap()); } #[quickcheck] #[allow(clippy::needless_pass_by_value)] fn from_link_to_link_is_identity(bytes: Vec) -> bool { - let path = Path::new(OsStr::from_bytes(&bytes)); - path == NodeType::from_link(path).to_link() + let path = TypedPath::derive(&bytes); + path == NodeType::from_link(&path).to_link() } } diff --git a/crates/core/src/backend/stdin.rs b/crates/core/src/backend/stdin.rs index c79543fc..a421903b 100644 --- a/crates/core/src/backend/stdin.rs +++ b/crates/core/src/backend/stdin.rs @@ -1,9 +1,10 @@ use std::{ io::{stdin, Stdin}, iter::{once, Once}, - path::PathBuf, }; +use typed_path::UnixPathBuf; + use crate::{ backend::{ReadSource, ReadSourceEntry}, error::{ErrorKind, RusticError, RusticResult}, @@ -13,12 +14,12 @@ use crate::{ #[derive(Debug, Clone)] pub struct StdinSource { /// The path of the stdin entry. - path: PathBuf, + path: UnixPathBuf, } impl StdinSource { /// Creates a new `StdinSource`. - pub const fn new(path: PathBuf) -> Self { + pub fn new(path: UnixPathBuf) -> Self { Self { path } } } diff --git a/crates/core/src/blob/tree.rs b/crates/core/src/blob/tree.rs index a96ba3db..0aa02f1e 100644 --- a/crates/core/src/blob/tree.rs +++ b/crates/core/src/blob/tree.rs @@ -1,9 +1,9 @@ use std::{ + borrow::Cow, cmp::Ordering, collections::{BTreeMap, BTreeSet, BinaryHeap}, - ffi::{OsStr, OsString}, mem, - path::{Component, Path, PathBuf, Prefix}, + path::PathBuf, str::{self, Utf8Error}, }; @@ -13,6 +13,7 @@ use ignore::overrides::{Override, OverrideBuilder}; use ignore::Match; use serde::{Deserialize, Deserializer}; use serde_derive::Serialize; +use typed_path::{Component, TypedPath, UnixPath, UnixPathBuf, WindowsComponent, WindowsPrefix}; use crate::{ backend::{ @@ -52,8 +53,8 @@ pub(super) mod constants { pub(super) const MAX_TREE_LOADER: usize = 4; } -pub(crate) type TreeStreamItem = RusticResult<(PathBuf, Tree)>; -type NodeStreamItem = RusticResult<(PathBuf, Node)>; +pub(crate) type TreeStreamItem = RusticResult<(UnixPathBuf, Tree)>; +type NodeStreamItem = RusticResult<(UnixPathBuf, Node)>; impl_blobid!(TreeId, BlobType::Tree); #[derive(Default, Serialize, Deserialize, Clone, Debug)] @@ -170,37 +171,27 @@ impl Tree { be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, id: TreeId, - path: &Path, + path: &UnixPath, ) -> RusticResult { - let mut node = Node::new_node(OsStr::new(""), NodeType::Dir, Metadata::default()); + let mut node = Node::new_node(&[], NodeType::Dir, Metadata::default()); node.subtree = Some(id); for p in path.components() { - if let Some(p) = comp_to_osstr(p).map_err(|err| { - RusticError::with_source( - ErrorKind::Internal, - "Failed to convert Path component `{path}` to OsString.", - err, - ) - .attach_context("path", path.display().to_string()) - .ask_report() - })? { - let id = node.subtree.ok_or_else(|| { - RusticError::new(ErrorKind::Internal, "Node `{node}` is not a directory.") - .attach_context("node", p.to_string_lossy()) + let id = node.subtree.ok_or_else(|| { + RusticError::new(ErrorKind::Internal, "Node `{node}` is not a directory.") + .attach_context("node", String::from_utf8_lossy(p.as_bytes())) + .ask_report() + })?; + let tree = Self::from_backend(be, index, id)?; + node = tree + .nodes + .into_iter() + .find(|node| node.name() == p.as_bytes()) + .ok_or_else(|| { + RusticError::new(ErrorKind::Internal, "Node `{node}` not found in tree.") + .attach_context("node", String::from_utf8_lossy(p.as_bytes())) .ask_report() })?; - let tree = Self::from_backend(be, index, id)?; - node = tree - .nodes - .into_iter() - .find(|node| node.name() == p) - .ok_or_else(|| { - RusticError::new(ErrorKind::Internal, "Node `{node}` not found in tree.") - .attach_context("node", p.to_string_lossy()) - .ask_report() - })?; - } } Ok(node) @@ -210,14 +201,14 @@ impl Tree { be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, ids: impl IntoIterator, - path: &Path, + path: &UnixPath, ) -> RusticResult { // helper function which is recursively called fn find_node_from_component( be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, tree_id: TreeId, - path_comp: &[OsString], + path_comp: &[&[u8]], results_cache: &mut [BTreeMap>], nodes: &mut BTreeMap, idx: usize, @@ -242,7 +233,7 @@ impl Tree { ErrorKind::Internal, "Subtree ID not found for node `{node}`", ) - .attach_context("node", path_comp[idx].to_string_lossy()) + .attach_context("node", String::from_utf8_lossy(path_comp[idx])) .ask_report() })?; @@ -263,19 +254,7 @@ impl Tree { Ok(result) } - let path_comp: Vec<_> = path - .components() - .filter_map(|p| comp_to_osstr(p).transpose()) - .collect::>() - .map_err(|err| { - RusticError::with_source( - ErrorKind::Internal, - "Failed to convert Path component `{path}` to OsString.", - err, - ) - .attach_context("path", path.display().to_string()) - .ask_report() - })?; + let path_comp: Vec<_> = path.components().map(|p| p.as_bytes()).collect(); // caching all results let mut results_cache = vec![BTreeMap::new(); path_comp.len()]; @@ -308,19 +287,19 @@ impl Tree { be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, ids: impl IntoIterator, - matches: &impl Fn(&Path, &Node) -> bool, + matches: &impl Fn(&UnixPath, &Node) -> bool, ) -> RusticResult { // internal state used to save match information in find_matching_nodes #[derive(Default)] struct MatchInternalState { // we cache all results - cache: BTreeMap<(TreeId, PathBuf), Vec<(usize, usize)>>, + cache: BTreeMap<(TreeId, UnixPathBuf), Vec<(usize, usize)>>, nodes: BTreeMap, - paths: BTreeMap, + paths: BTreeMap, } impl MatchInternalState { - fn insert_result(&mut self, path: PathBuf, node: Node) -> (usize, usize) { + fn insert_result(&mut self, path: UnixPathBuf, node: Node) -> (usize, usize) { let new_idx = self.nodes.len(); let node_idx = self.nodes.entry(node).or_insert(new_idx); let new_idx = self.paths.len(); @@ -334,9 +313,9 @@ impl Tree { be: &impl DecryptReadBackend, index: &impl ReadGlobalIndex, tree_id: TreeId, - path: &Path, + path: &UnixPath, state: &mut MatchInternalState, - matches: &impl Fn(&Path, &Node) -> bool, + matches: &impl Fn(&UnixPath, &Node) -> bool, ) -> RusticResult> { let mut result = Vec::new(); if let Some(result) = state.cache.get(&(tree_id, path.to_path_buf())) { @@ -352,7 +331,7 @@ impl Tree { ErrorKind::Internal, "Subtree ID not found for node `{node}`", ) - .attach_context("node", node.name().to_string_lossy()) + .attach_context("node", String::from_utf8_lossy(&node.name())) .ask_report() })?; @@ -372,7 +351,7 @@ impl Tree { let mut state = MatchInternalState::default(); - let initial_path = PathBuf::new(); + let initial_path = UnixPathBuf::new(); let matches: Vec<_> = ids .into_iter() .map(|id| { @@ -407,17 +386,17 @@ pub struct FindNode { } /// Results from `find_matching_nodes` -#[derive(Debug, Serialize)] +#[derive(Debug)] pub struct FindMatches { /// found matching paths - pub paths: Vec, + pub paths: Vec, /// found matching nodes pub nodes: Vec, /// found paths/nodes for all given snapshots. (usize,usize) is the path / node index pub matches: Vec>, } -/// Converts a [`Component`] to an [`OsString`]. +/// Converts a [`TypedPath`] to an [`Cow`]. /// /// # Arguments /// @@ -427,21 +406,33 @@ pub struct FindMatches { /// /// * If the component is a current or parent directory. /// * If the component is not UTF-8 conform. -pub(crate) fn comp_to_osstr(p: Component<'_>) -> TreeResult> { - let s = match p { - Component::RootDir => None, - Component::Prefix(p) => match p.kind() { - Prefix::Verbatim(p) | Prefix::DeviceNS(p) => Some(p.to_os_string()), - Prefix::VerbatimUNC(_, q) | Prefix::UNC(_, q) => Some(q.to_os_string()), - Prefix::VerbatimDisk(p) | Prefix::Disk(p) => Some( - OsStr::new(str::from_utf8(&[p]).map_err(TreeErrorKind::PathIsNotUtf8Conform)?) - .to_os_string(), - ), - }, - Component::Normal(p) => Some(p.to_os_string()), - _ => return Err(TreeErrorKind::ContainsCurrentOrParentDirectory), - }; - Ok(s) +pub(crate) fn typed_path_to_unix_path<'a>(p: &'a TypedPath<'_>) -> Cow<'a, UnixPath> { + match p { + TypedPath::Unix(path) => Cow::Borrowed(path), + TypedPath::Windows(path) => { + let mut unix_path = UnixPathBuf::new(); + for c in path.with_windows_encoding().components() { + match c { + WindowsComponent::Prefix(p) => match p.kind() { + WindowsPrefix::Verbatim(p) | WindowsPrefix::DeviceNS(p) => { + unix_path.push(p); + } + WindowsPrefix::VerbatimUNC(_, q) | WindowsPrefix::UNC(_, q) => { + unix_path.push(q); + } + WindowsPrefix::VerbatimDisk(p) | WindowsPrefix::Disk(p) => { + let c = vec![p]; + unix_path.push(&c); + } + }, + c => { + unix_path.push(c); + } + } + } + Cow::Owned(unix_path) + } + } } impl IntoIterator for Tree { @@ -513,7 +504,7 @@ where /// Inner iterator for the current subtree nodes inner: std::vec::IntoIter, /// The current path - path: PathBuf, + path: UnixPathBuf, /// The backend to read from be: BE, /// index @@ -574,7 +565,7 @@ where Ok(Self { inner, open_iterators: Vec::new(), - path: PathBuf::new(), + path: UnixPathBuf::new(), be, index, overrides, @@ -726,7 +717,18 @@ where } if let Some(overrides) = &self.overrides { - if let Match::Ignore(_) = overrides.matched(&path, false) { + // TODO: use globset directly with UnixPath instead of converting to std::Path, + // see https://github.com/BurntSushi/ripgrep/issues/2954 + #[cfg(windows)] + if let Match::Ignore(_) = overrides.matched( + // using a lossy UTF-8 string from the underlying bytes is a best effort to get "correct" matching results + PathBuf::from(String::from_utf8_lossy(path.as_bytes()).to_string()), + false, + ) { + continue; + } + #[cfg(not(windows))] + if let Match::Ignore(_) = overrides.matched(PathBuf::from(&path), false) { continue; } } @@ -755,9 +757,9 @@ pub struct TreeStreamerOnce

{ /// The visited tree IDs visited: BTreeSet, /// The queue to send tree IDs to - queue_in: Option>, + queue_in: Option>, /// The queue to receive trees from - queue_out: Receiver>, + queue_out: Receiver>, /// The progress indicator p: P, /// The number of trees that are not yet finished @@ -820,7 +822,7 @@ impl TreeStreamerOnce

{ for (count, id) in ids.into_iter().enumerate() { if !streamer - .add_pending(PathBuf::new(), id, count) + .add_pending(UnixPathBuf::new(), id, count) .map_err(|err| { RusticError::with_source( ErrorKind::Internal, @@ -855,7 +857,7 @@ impl TreeStreamerOnce

{ /// # Errors /// /// * If sending the message fails. - fn add_pending(&mut self, path: PathBuf, id: TreeId, count: usize) -> TreeResult { + fn add_pending(&mut self, path: UnixPathBuf, id: TreeId, count: usize) -> TreeResult { if self.visited.insert(id) { self.queue_in .as_ref() @@ -911,7 +913,7 @@ impl Iterator for TreeStreamerOnce

{ "Failed to add tree ID `{tree_id}` to pending queue (`{count}`).", err, ) - .attach_context("path", path.display().to_string()) + .attach_context("path", path.to_string_lossy().to_string()) .attach_context("tree_id", id.to_string()) .attach_context("count", count.to_string()) .ask_report() diff --git a/crates/core/src/commands/backup.rs b/crates/core/src/commands/backup.rs index 502f7b47..37f784db 100644 --- a/crates/core/src/commands/backup.rs +++ b/crates/core/src/commands/backup.rs @@ -1,6 +1,7 @@ //! `backup` subcommand use derive_setters::Setters; use log::info; +use typed_path::UnixPathBuf; use std::path::PathBuf; @@ -221,7 +222,7 @@ pub(crate) fn backup( source.paths() }; - let as_path = opts + let as_path: Option = opts .as_path .as_ref() .map(|p| -> RusticResult<_> { @@ -234,7 +235,9 @@ pub(crate) fn backup( ) .attach_context("path", p.display().to_string()) })? - .to_path_buf()) + .to_path_buf() + .try_into() + .unwrap()) // TODO: error handling }) .transpose()?; @@ -247,21 +250,30 @@ pub(crate) fn backup( ) .attach_context("paths", p.display().to_string()) })?, - None => snap.paths.set_paths(&backup_path).map_err(|err| { - RusticError::with_source( - ErrorKind::Internal, - "Failed to set paths `{paths}` in snapshot.", - err, - ) - .attach_context( - "paths", - backup_path + None => snap + .paths + .set_paths( + &backup_path .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(","), + .map(|p| UnixPathBuf::try_from(p.clone())) + .collect::, _>>() + .unwrap(), ) - })?, + .map_err(|err| { + RusticError::with_source( + ErrorKind::Internal, + "Failed to set paths `{paths}` in snapshot.", + err, + ) + .attach_context( + "paths", + backup_path + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(","), + ) + })?, }; let (parent_id, parent) = opts.parent_opts.get_parent(repo, &snap, backup_stdin); @@ -283,10 +295,11 @@ pub(crate) fn backup( let snap = if backup_stdin { let path = &backup_path[0]; if let Some(command) = &opts.stdin_command { - let src = ChildStdoutSource::new(command, path.clone())?; + let unix_path: UnixPathBuf = path.clone().try_into().unwrap(); // todo: error handling + let src = ChildStdoutSource::new(command, unix_path.clone())?; let res = archiver.archive( &src, - path, + &unix_path, as_path.as_ref(), opts.parent_opts.skip_if_unchanged, opts.no_scan, @@ -295,10 +308,11 @@ pub(crate) fn backup( src.finish()?; res } else { + let path: UnixPathBuf = path.clone().try_into().unwrap(); // TODO: error handling let src = StdinSource::new(path.clone()); archiver.archive( &src, - path, + &path, as_path.as_ref(), opts.parent_opts.skip_if_unchanged, opts.no_scan, @@ -311,9 +325,10 @@ pub(crate) fn backup( &opts.ignore_filter_opts, &backup_path, )?; + let backup_path: UnixPathBuf = backup_path[0].clone().try_into().unwrap(); archiver.archive( &src, - &backup_path[0], + &backup_path, as_path.as_ref(), opts.parent_opts.skip_if_unchanged, opts.no_scan, diff --git a/crates/core/src/commands/cat.rs b/crates/core/src/commands/cat.rs index 55caa248..383c7639 100644 --- a/crates/core/src/commands/cat.rs +++ b/crates/core/src/commands/cat.rs @@ -1,6 +1,5 @@ -use std::path::Path; - use bytes::Bytes; +use typed_path::UnixPath; use crate::{ backend::{decrypt::DecryptReadBackend, FileType, FindInBackend}, @@ -103,7 +102,7 @@ pub(crate) fn cat_tree( sn_filter, &repo.pb.progress_counter("getting snapshot..."), )?; - let node = Tree::node_from_path(repo.dbe(), repo.index(), snap.tree, Path::new(path))?; + let node = Tree::node_from_path(repo.dbe(), repo.index(), snap.tree, UnixPath::new(path))?; let id = node.subtree.ok_or_else(|| { RusticError::new( ErrorKind::InvalidInput, diff --git a/crates/core/src/commands/merge.rs b/crates/core/src/commands/merge.rs index 10d15dd1..729c9d97 100644 --- a/crates/core/src/commands/merge.rs +++ b/crates/core/src/commands/merge.rs @@ -44,7 +44,7 @@ pub(crate) fn merge_snapshots( .collect::() .merge(); - snap.paths.set_paths(&paths.paths()).map_err(|err| { + snap.paths.set_paths(&paths.unix_paths()).map_err(|err| { RusticError::with_source( ErrorKind::Internal, "Failed to set paths `{paths}` in snapshot.", diff --git a/crates/core/src/commands/restore.rs b/crates/core/src/commands/restore.rs index 6d8e029f..ef23f382 100644 --- a/crates/core/src/commands/restore.rs +++ b/crates/core/src/commands/restore.rs @@ -2,14 +2,9 @@ use derive_setters::Setters; use log::{debug, error, info, trace, warn}; +use typed_path::{UnixPath, UnixPathBuf}; -use std::{ - cmp::Ordering, - collections::BTreeMap, - num::NonZeroU32, - path::{Path, PathBuf}, - sync::Mutex, -}; +use std::{cmp::Ordering, collections::BTreeMap, num::NonZeroU32, path::Path, sync::Mutex}; use chrono::{DateTime, Local, Utc}; use ignore::{DirEntry, WalkBuilder}; @@ -38,7 +33,7 @@ pub(crate) mod constants { } type RestoreInfo = BTreeMap<(PackId, BlobLocation), Vec>; -type Filenames = Vec; +type Filenames = Vec; #[allow(clippy::struct_excessive_bools)] #[cfg_attr(feature = "clap", derive(clap::Parser))] @@ -116,7 +111,7 @@ pub(crate) fn restore_repository( file_infos: RestorePlan, repo: &Repository, opts: RestoreOptions, - node_streamer: impl Iterator>, + node_streamer: impl Iterator>, dest: &LocalDestination, ) -> RusticResult<()> { repo.warm_up_wait(file_infos.to_packs().into_iter())?; @@ -151,12 +146,18 @@ pub(crate) fn restore_repository( pub(crate) fn collect_and_prepare( repo: &Repository, opts: RestoreOptions, - mut node_streamer: impl Iterator>, + mut node_streamer: impl Iterator>, dest: &LocalDestination, dry_run: bool, ) -> RusticResult { let p = repo.pb.progress_spinner("collecting file information..."); - let dest_path = dest.path(""); + let dest_path = dest.path("").map_err(|err| { + RusticError::with_source( + ErrorKind::InputOutput, + "Failed to use the destination directory.", + err, + ) + })?; let mut stats = RestoreStats::default(); let mut restore_infos = RestorePlan::default(); @@ -209,7 +210,7 @@ pub(crate) fn collect_and_prepare( Ok(()) }; - let mut process_node = |path: &PathBuf, node: &Node, exists: bool| -> RusticResult<_> { + let mut process_node = |path: &UnixPathBuf, node: &Node, exists: bool| -> RusticResult<_> { match node.node_type { NodeType::Dir => { if exists { @@ -290,7 +291,15 @@ pub(crate) fn collect_and_prepare( next_dst = dst_iter.next(); } (Some(destination), Some((path, node))) => { - match destination.path().cmp(&dest.path(path)) { + let dest_path = dest.path(path).map_err(|err| { + RusticError::with_source( + ErrorKind::InputOutput, + "Failed to create the directory `{path}`. Please check the path and try again.", + err + ) + .attach_context("path", path.display().to_string()) + })?; + match destination.path().cmp(&dest_path) { Ordering::Less => { process_existing(destination)?; next_dst = dst_iter.next(); @@ -343,7 +352,7 @@ pub(crate) fn collect_and_prepare( /// /// * If the restore failed. fn restore_metadata( - mut node_streamer: impl Iterator>, + mut node_streamer: impl Iterator>, opts: RestoreOptions, dest: &LocalDestination, ) -> RusticResult<()> { @@ -390,9 +399,10 @@ fn restore_metadata( pub(crate) fn set_metadata( dest: &LocalDestination, opts: RestoreOptions, - path: &PathBuf, + path: impl AsRef, node: &Node, ) { + let path = path.as_ref(); debug!("setting metadata for {:?}", path); dest.create_special(path, node) .unwrap_or_else(|_| warn!("restore {:?}: creating special file failed.", path)); @@ -664,7 +674,7 @@ impl RestorePlan { &mut self, dest: &LocalDestination, file: &Node, - name: PathBuf, + name: UnixPathBuf, repo: &Repository, ignore_mtime: bool, ) -> RusticResult { diff --git a/crates/core/src/repofile/snapshotfile.rs b/crates/core/src/repofile/snapshotfile.rs index 90f17903..5a0174fa 100644 --- a/crates/core/src/repofile/snapshotfile.rs +++ b/crates/core/src/repofile/snapshotfile.rs @@ -17,6 +17,7 @@ use log::info; use path_dedot::ParseDot; use serde_derive::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ backend::{decrypt::DecryptReadBackend, FileType, FindInBackend}, @@ -33,7 +34,7 @@ use crate::{ #[non_exhaustive] pub enum SnapshotFileErrorKind { /// non-unicode path `{0:?}` - NonUnicodePath(PathBuf), + NonUnicodePath(UnixPathBuf), /// value `{0:?}` not allowed ValueNotAllowed(String), /// datetime out of range: `{0:?}` @@ -1201,7 +1202,7 @@ impl StringList { /// # Errors /// /// * If a path is not valid unicode - pub(crate) fn set_paths>(&mut self, paths: &[T]) -> SnapshotFileResult<()> { + pub(crate) fn set_paths>(&mut self, paths: &[T]) -> SnapshotFileResult<()> { self.0 = paths .iter() .map(|p| { @@ -1305,6 +1306,16 @@ impl PathList { self.0.clone() } + /// Clone the internal `Vec`. + #[must_use] + pub(crate) fn unix_paths(&self) -> Vec { + self.0 + .iter() + .map(|p| p.clone().try_into()) + .collect::>() + .unwrap() + } + /// Sanitize paths: Parse dots, absolutize if needed and merge paths. /// /// # Errors diff --git a/crates/core/src/repository.rs b/crates/core/src/repository.rs index 309810c7..2bdd2189 100644 --- a/crates/core/src/repository.rs +++ b/crates/core/src/repository.rs @@ -5,7 +5,7 @@ use std::{ cmp::Ordering, fs::File, io::{BufRead, BufReader, Write}, - path::{Path, PathBuf}, + path::PathBuf, process::{Command, Stdio}, sync::Arc, }; @@ -14,6 +14,7 @@ use bytes::Bytes; use derive_setters::Setters; use log::{debug, error, info}; use serde_with::{serde_as, DisplayFromStr}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ backend::{ @@ -1680,8 +1681,8 @@ impl Repository { /// * If the path is not a directory. /// * If the path is not found. /// * If the path is not UTF-8 conform. - pub fn node_from_path(&self, root_tree: TreeId, path: &Path) -> RusticResult { - Tree::node_from_path(self.dbe(), self.index(), root_tree, Path::new(path)) + pub fn node_from_path(&self, root_tree: TreeId, path: &UnixPath) -> RusticResult { + Tree::node_from_path(self.dbe(), self.index(), root_tree, path) } /// Get all [`Node`]s from given root trees and a path @@ -1697,7 +1698,7 @@ impl Repository { pub fn find_nodes_from_path( &self, ids: impl IntoIterator, - path: &Path, + path: &UnixPath, ) -> RusticResult { Tree::find_nodes_from_path(self.dbe(), self.index(), ids, path) } @@ -1715,7 +1716,7 @@ impl Repository { pub fn find_matching_nodes( &self, ids: impl IntoIterator, - matches: &impl Fn(&Path, &Node) -> bool, + matches: &impl Fn(&UnixPath, &Node) -> bool, ) -> RusticResult { Tree::find_matching_nodes(self.dbe(), self.index(), ids, matches) } @@ -1758,7 +1759,7 @@ impl Repository { let p = &self.pb.progress_counter("getting snapshot..."); let snap = SnapshotFile::from_str(self.dbe(), id, filter, p)?; - Tree::node_from_path(self.dbe(), self.index(), snap.tree, Path::new(path)) + Tree::node_from_path(self.dbe(), self.index(), snap.tree, UnixPath::new(path)) } /// Get a [`Node`] from a [`SnapshotFile`] and a `path` @@ -1778,7 +1779,7 @@ impl Repository { snap: &SnapshotFile, path: &str, ) -> RusticResult { - Tree::node_from_path(self.dbe(), self.index(), snap.tree, Path::new(path)) + Tree::node_from_path(self.dbe(), self.index(), snap.tree, UnixPath::new(path)) } /// Reads a raw tree from a "SNAP\[:PATH\]" syntax /// @@ -1823,7 +1824,7 @@ impl Repository { &self, node: &Node, ls_opts: &LsOptions, - ) -> RusticResult> + Clone + '_> { + ) -> RusticResult> + Clone + '_> { NodeStreamer::new_with_glob(self.dbe().clone(), self.index(), node, ls_opts) } @@ -1843,7 +1844,7 @@ impl Repository { &self, restore_infos: RestorePlan, opts: &RestoreOptions, - node_streamer: impl Iterator>, + node_streamer: impl Iterator>, dest: &LocalDestination, ) -> RusticResult<()> { restore_repository(restore_infos, self, *opts, node_streamer, dest) @@ -2025,7 +2026,7 @@ impl Repository { pub fn prepare_restore( &self, opts: &RestoreOptions, - node_streamer: impl Iterator>, + node_streamer: impl Iterator>, dest: &LocalDestination, dry_run: bool, ) -> RusticResult { diff --git a/crates/core/src/repository/command_input.rs b/crates/core/src/repository/command_input.rs index 63e897b0..12f2741f 100644 --- a/crates/core/src/repository/command_input.rs +++ b/crates/core/src/repository/command_input.rs @@ -50,7 +50,7 @@ pub enum CommandInputErrorKind { command: CommandInput, /// The path in which the command was tried to be executed - path: std::path::PathBuf, + path: String, /// The source of the error source: std::io::Error, diff --git a/crates/core/src/vfs.rs b/crates/core/src/vfs.rs index 8a94140d..c10ec7ba 100644 --- a/crates/core/src/vfs.rs +++ b/crates/core/src/vfs.rs @@ -1,14 +1,11 @@ mod format; -use std::{ - collections::BTreeMap, - ffi::{OsStr, OsString}, - path::{Component, Path, PathBuf}, -}; +use std::collections::BTreeMap; use bytes::{Bytes, BytesMut}; use runtime_format::FormatArgs; use strum::EnumString; +use typed_path::{TypedPath, UnixComponent, UnixPath, UnixPathBuf}; use crate::{ blob::{tree::TreeId, BlobId, DataId}, @@ -27,7 +24,7 @@ pub enum VfsErrorKind { /// Only normal paths allowed OnlyNormalPathsAreAllowed, /// Name `{0:?}` doesn't exist - NameDoesNotExist(OsString), + NameDoesNotExist(String), } pub(crate) type VfsResult = Result; @@ -56,22 +53,22 @@ pub enum Latest { /// A potentially virtual tree in the [`Vfs`] enum VfsTree { /// A symlink to the given link target - Link(OsString), + Link(Vec), /// A repository tree; id of the tree RusticTree(TreeId), /// A purely virtual tree containing subtrees - VirtualTree(BTreeMap), + VirtualTree(BTreeMap, VfsTree>), } #[derive(Debug)] /// A resolved path within a [`Vfs`] enum VfsPath<'a> { /// Path is the given symlink - Link(&'a OsString), + Link(&'a [u8]), /// Path is within repository, give the tree [`Id`] and remaining path. - RusticPath(&'a TreeId, PathBuf), + RusticPath(&'a TreeId, UnixPathBuf), /// Path is the given virtual tree - VirtualTree(&'a BTreeMap), + VirtualTree(&'a BTreeMap, VfsTree>), } impl VfsTree { @@ -95,19 +92,19 @@ impl VfsTree { /// # Returns /// /// `Ok(())` if the tree was added successfully - fn add_tree(&mut self, path: &Path, new_tree: Self) -> VfsResult<()> { + fn add_tree(&mut self, path: &UnixPath, new_tree: Self) -> VfsResult<()> { let mut tree = self; let mut components = path.components(); - let Some(Component::Normal(last)) = components.next_back() else { + let Some(UnixComponent::Normal(last)) = components.next_back() else { return Err(VfsErrorKind::OnlyNormalPathsAreAllowed); }; for comp in components { - if let Component::Normal(name) = comp { + if let UnixComponent::Normal(name) = comp { match tree { Self::VirtualTree(virtual_tree) => { tree = virtual_tree - .entry(name.to_os_string()) + .entry(name.to_vec()) .or_insert(Self::VirtualTree(BTreeMap::new())); } _ => { @@ -121,7 +118,7 @@ impl VfsTree { return Err(VfsErrorKind::DirectoryExistsAsNonVirtual); }; - _ = virtual_tree.insert(last.to_os_string(), new_tree); + _ = virtual_tree.insert(last.to_vec(), new_tree); Ok(()) } @@ -138,21 +135,23 @@ impl VfsTree { /// # Returns /// /// If the path is within a real repository tree, this returns the [`VfsTree::RusticTree`] and the remaining path - fn get_path(&self, path: &Path) -> VfsResult> { + fn get_path(&self, path: &UnixPath) -> VfsResult> { let mut tree = self; let mut components = path.components(); loop { match tree { Self::RusticTree(id) => { - let path: PathBuf = components.collect(); + let path = components.collect(); return Ok(VfsPath::RusticPath(id, path)); } Self::VirtualTree(virtual_tree) => match components.next() { - Some(Component::Normal(name)) => { + Some(UnixComponent::Normal(name)) => { if let Some(new_tree) = virtual_tree.get(name) { tree = new_tree; } else { - return Err(VfsErrorKind::NameDoesNotExist(name.to_os_string())); + return Err(VfsErrorKind::NameDoesNotExist( + String::from_utf8_lossy(name).to_string(), + )); }; } None => { @@ -246,9 +245,9 @@ impl Vfs { }, ) .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); + let path = UnixPath::new(&path).to_path_buf(); + let filename = path.file_name().map(<[u8]>::to_vec); + let parent_path = path.parent().map(UnixPath::to_path_buf); // Save paths for latest entries, if requested if matches!(latest_option, Latest::AsLink) { @@ -265,27 +264,27 @@ impl Vfs { && last_tree == snap.tree { if let Some(name) = last_name { - tree.add_tree(path, VfsTree::Link(name.clone())) + tree.add_tree(&path, VfsTree::Link(name.clone())) .map_err(|err| { RusticError::with_source( ErrorKind::Vfs, "Failed to add a link `{name}` to root tree at `{path}`", err, ) - .attach_context("path", path.display().to_string()) - .attach_context("name", name.to_string_lossy()) + .attach_context("path", path.to_string_lossy().to_string()) + .attach_context("name", String::from_utf8_lossy(&name)) .ask_report() })?; } } else { - tree.add_tree(path, VfsTree::RusticTree(snap.tree)) + tree.add_tree(&path, VfsTree::RusticTree(snap.tree)) .map_err(|err| { RusticError::with_source( ErrorKind::Vfs, "Failed to add repository tree `{tree_id}` to root tree at `{path}`", err, ) - .attach_context("path", path.display().to_string()) + .attach_context("path", path.to_string_lossy().to_string()) .attach_context("tree_id", snap.tree.to_string()) .ask_report() })?; @@ -310,8 +309,8 @@ impl Vfs { "Failed to link latest `{target}` entry to root tree at `{path}`", err, ) - .attach_context("path", path.display().to_string()) - .attach_context("target", target.to_string_lossy()) + .attach_context("path", path.to_string_lossy().to_string()) + .attach_context("target", String::from_utf8_lossy(&target)) .attach_context("latest", "link") .ask_report() })?; @@ -329,7 +328,7 @@ impl Vfs { "Failed to add latest subtree id `{id}` to root tree at `{path}`", err, ) - .attach_context("path", path.display().to_string()) + .attach_context("path", path.to_string_lossy().to_string()) .attach_context("tree_id", subtree.to_string()) .attach_context("latest", "dir") .ask_report() @@ -360,7 +359,7 @@ impl Vfs { pub fn node_from_path( &self, repo: &Repository, - path: &Path, + path: &UnixPath, ) -> RusticResult { let meta = Metadata::default(); match self.tree.get_path(path).map_err(|err| { @@ -378,7 +377,7 @@ impl Vfs { } VfsPath::Link(target) => Ok(Node::new( String::new(), - NodeType::from_link(Path::new(target)), + NodeType::from_link(&TypedPath::derive(target)), meta, None, None, @@ -409,7 +408,7 @@ impl Vfs { pub fn dir_entries_from_path( &self, repo: &Repository, - path: &Path, + path: &UnixPath, ) -> RusticResult> { let result = match self.tree.get_path(path).map_err(|err| { RusticError::with_source( @@ -433,7 +432,7 @@ impl Vfs { .iter() .map(|(name, tree)| { let node_type = match tree { - VfsTree::Link(target) => NodeType::from_link(Path::new(target)), + VfsTree::Link(target) => NodeType::from_link(&TypedPath::derive(target)), _ => NodeType::Dir, }; Node::new_node(name, node_type, Metadata::default()) @@ -444,7 +443,7 @@ impl Vfs { ErrorKind::Vfs, "No directory entries for symlink `{symlink}` found. Is the path valid unicode?", ) - .attach_context("symlink", str.to_string_lossy().to_string())); + .attach_context("symlink", String::from_utf8_lossy(str).to_string())); } }; Ok(result) diff --git a/crates/core/tests/integration.rs b/crates/core/tests/integration.rs index 2356c47a..49245677 100644 --- a/crates/core/tests/integration.rs +++ b/crates/core/tests/integration.rs @@ -28,7 +28,7 @@ use bytes::Bytes; use flate2::read::GzDecoder; use globset::Glob; use insta::{ - assert_ron_snapshot, + assert_debug_snapshot, assert_ron_snapshot, internals::{Content, ContentPath}, Settings, }; @@ -44,8 +44,9 @@ use rustic_core::{ use serde::Serialize; use rustic_testing::backend::in_memory_backend::InMemoryBackend; +use typed_path::{UnixPath, UnixPathBuf}; -use std::{collections::BTreeMap, ffi::OsStr}; +use std::collections::BTreeMap; use std::{ env, fs::File, @@ -222,7 +223,7 @@ fn test_backup_with_tar_gz_passes( // tree of first backup // re-read index let repo = repo.to_indexed_ids()?; - let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; + let tree = repo.node_from_path(first_snapshot.tree, &UnixPath::new("test/0/tests"))?; let tree: rustic_core::repofile::Tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; insta_node_redaction.bind(|| { @@ -352,7 +353,7 @@ fn test_backup_dry_run_with_tar_gz_passes( // tree of first backup // re-read index let repo = repo.to_indexed_ids()?; - let tree = repo.node_from_path(first_snapshot.tree, Path::new("test/0/tests"))?; + let tree = repo.node_from_path(first_snapshot.tree, UnixPath::new("test/0/tests"))?; let tree = repo.get_tree(&tree.subtree.expect("Sub tree"))?; insta_node_redaction.bind(|| { @@ -431,7 +432,7 @@ fn test_ls_and_read( // test non-existing entries let mut node = Node::new_node( - OsStr::new(""), + &[], rustic_core::repofile::NodeType::Dir, Metadata::default(), ); @@ -445,12 +446,12 @@ fn test_ls_and_read( .collect::>()?; insta_node_redaction.bind(|| { - assert_with_win("ls", &entries); + assert_debug_snapshot!("ls", &entries); }); // test reading a file from the repository let repo = repo.to_indexed()?; - let path: PathBuf = ["test", "0", "tests", "testfile"].iter().collect(); + let path: UnixPathBuf = ["test", "0", "tests", "testfile"].iter().collect(); let node = entries.get(&path).unwrap(); let file = repo.open_file(node)?; @@ -461,7 +462,7 @@ fn test_ls_and_read( assert_eq!(Bytes::from("test"), repo.read_file_at(&file, 10, 4)?); // read partial content // test reading an empty file from the repository - let path: PathBuf = ["test", "0", "tests", "empty-file"].iter().collect(); + let path: UnixPathBuf = ["test", "0", "tests", "empty-file"].iter().collect(); let node = entries.get(&path).unwrap(); let file = repo.open_file(node)?; assert_eq!(Bytes::new(), repo.read_file_at(&file, 0, 0)?); // empty files @@ -483,34 +484,42 @@ fn test_find(tar_gz_testdata: Result, set_up_repo: Result) let repo = repo.to_indexed_ids()?; // test non-existing path - let not_found = repo.find_nodes_from_path(vec![snapshot.tree], Path::new("not_existing"))?; + let not_found = + repo.find_nodes_from_path(vec![snapshot.tree], UnixPath::new("not_existing"))?; assert_with_win("find-nodes-not-found", not_found); // test non-existing match let glob = Glob::new("not_existing")?.compile_matcher(); - let not_found = - repo.find_matching_nodes(vec![snapshot.tree], &|path, _| glob.is_match(path))?; - assert_with_win("find-matching-nodes-not-found", not_found); + let not_found = repo.find_matching_nodes(vec![snapshot.tree], &|path, _| { + glob.is_match(PathBuf::try_from(path).unwrap()) + })?; + assert_debug_snapshot!("find-matching-nodes-not-found", not_found); // test existing path let FindNode { matches, .. } = - repo.find_nodes_from_path(vec![snapshot.tree], Path::new("test/0/tests/testfile"))?; + repo.find_nodes_from_path(vec![snapshot.tree], UnixPath::new("test/0/tests/testfile"))?; assert_with_win("find-nodes-existing", matches); // test existing match let glob = Glob::new("testfile")?.compile_matcher(); - let match_func = |path: &Path, _: &Node| { - glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) + let match_func = |path: &UnixPath, _: &Node| { + glob.is_match(PathBuf::try_from(path).unwrap()) + || path + .file_name() + .is_some_and(|f| glob.is_match(Path::new(&String::from_utf8(f.to_vec()).unwrap()))) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-existing", (paths, matches)); + assert_debug_snapshot!("find-matching-existing", (paths, matches)); // test existing match let glob = Glob::new("testfile*")?.compile_matcher(); - let match_func = |path: &Path, _: &Node| { - glob.is_match(path) || path.file_name().is_some_and(|f| glob.is_match(f)) + let match_func = |path: &UnixPath, _: &Node| { + glob.is_match(PathBuf::try_from(path).unwrap()) + || path + .file_name() + .is_some_and(|f| glob.is_match(Path::new(&String::from_utf8(f.to_vec()).unwrap()))) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-wildcard-existing", (paths, matches)); + assert_debug_snapshot!("find-matching-wildcard-existing", (paths, matches)); Ok(()) } diff --git a/examples/find/Cargo.toml b/examples/find/Cargo.toml index 88025959..43e8240a 100644 --- a/examples/find/Cargo.toml +++ b/examples/find/Cargo.toml @@ -12,6 +12,7 @@ globset = "0.4.15" rustic_backend = { workspace = true } rustic_core = { workspace = true } simplelog = { workspace = true } +typed-path = "0.10.0" [[example]] name = "find" diff --git a/examples/find/examples/find.rs b/examples/find/examples/find.rs index a1f06d1a..e694510a 100644 --- a/examples/find/examples/find.rs +++ b/examples/find/examples/find.rs @@ -3,7 +3,7 @@ use globset::Glob; use rustic_backend::BackendOptions; use rustic_core::{FindMatches, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; -use std::error::Error; +use std::{error::Error, path::PathBuf}; fn main() -> Result<(), Box> { // Display info logs @@ -30,7 +30,9 @@ fn main() -> Result<(), Box> { paths, nodes, matches, - } = repo.find_matching_nodes(tree_ids, &|path, _| glob.is_match(path))?; + } = repo.find_matching_nodes(tree_ids, &|path, _| { + glob.is_match(PathBuf::try_from(path).unwrap()) + })?; for (snap, matches) in snapshots.iter().zip(matches) { println!("results in {snap:?}"); for (path_idx, node_idx) in matches {