diff --git a/Cargo.lock b/Cargo.lock index ab614e14..b3de5101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1120,6 +1120,7 @@ dependencies = [ "rustic_backend", "rustic_core", "simplelog", + "typed-path", ] [[package]] @@ -3351,6 +3352,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "toml", + "typed-path", "walkdir", "xattr", "zstd", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index cf4dfea9..de0dd3d9 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -99,11 +99,13 @@ enum-map = { workspace = true } enum-map-derive = "0.17.0" enumset = { version = "1.1.5", features = ["serde"] } gethostname = "1.0.1" +globset = "0.4.16" humantime = "2.2.0" itertools = "0.14.0" quick_cache = "0.6.13" shell-words = "1.1.0" strum = { version = "0.27.1", features = ["derive"] } +typed-path = "0.10.0" zstd = "0.13.3" [target.'cfg(not(windows))'.dependencies] @@ -122,7 +124,6 @@ xattr = "1" anyhow = { workspace = true } expect-test = "1.5.1" flate2 = "1.1.1" -globset = "0.4.16" insta = { version = "1.42.2", features = ["redactions", "ron"] } mockall = "0.13" pretty_assertions = "1.4.1" diff --git a/crates/core/src/archiver.rs b/crates/core/src/archiver.rs index 33e0b1e3..b1e1d55c 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::{IteratorExt, scope}; +use typed_path::UnixPathBuf; use crate::{ Progress, @@ -126,8 +125,7 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { pub fn archive( mut self, src: &R, - backup_path: &Path, - as_path: Option<&PathBuf>, + as_path: Option<&UnixPathBuf>, skip_identical_parent: bool, no_scan: bool, p: &impl Progress, @@ -157,9 +155,7 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { } Ok(ReadSourceEntry { path, node, open }) => { let snapshot_path = if let Some(as_path) = as_path { - as_path - .clone() - .join(path.strip_prefix(backup_path).unwrap()) + as_path.clone().join(path) } else { path }; diff --git a/crates/core/src/archiver/parent.rs b/crates/core/src/archiver/parent.rs index e6d2d44e..99857722 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 a5edbc72..17132492 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 ef38b587..ca6bd855 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 acca3a89..c1267aaa 100644 --- a/crates/core/src/backend/ignore.rs +++ b/crates/core/src/backend/ignore.rs @@ -20,6 +20,7 @@ use log::warn; #[cfg(not(windows))] use nix::unistd::{Gid, Group, Uid, User}; use serde_with::{DisplayFromStr, serde_as}; +use typed_path::UnixPath; #[cfg(not(windows))] use crate::backend::node::ExtendedAttribute; @@ -30,6 +31,7 @@ use crate::{ node::{Metadata, Node, NodeType}, }, error::{ErrorKind, RusticError, RusticResult}, + util::path_to_unix_path, }; /// [`IgnoreErrorKind`] describes the errors that can be returned by a Ignore action in Backends @@ -58,6 +60,8 @@ pub enum IgnoreErrorKind { #[cfg(not(windows))] /// Error acquiring metadata for `{name}`: `{source:?}` AcquiringMetadataFailed { name: String, source: ignore::Error }, + /// Non-UTF8 filename is not allowed: `{0:?}` + Utf8Error(#[from] std::str::Utf8Error), } pub(crate) type IgnoreResult = Result; @@ -65,6 +69,7 @@ pub(crate) type IgnoreResult = Result; /// A [`LocalSource`] is a source from local paths which is used to be read from (i.e. to backup it). #[derive(Debug)] pub struct LocalSource { + base_path: PathBuf, /// The walk builder. builder: WalkBuilder, /// The save options to use. @@ -309,7 +314,17 @@ impl LocalSource { let builder = walk_builder; - Ok(Self { builder, save_opts }) + let base_path = if backup_paths.len() == 1 { + backup_paths[0].as_ref().to_path_buf() + } else { + PathBuf::new() + }; + + Ok(Self { + base_path, + builder, + save_opts, + }) } } @@ -374,6 +389,7 @@ impl ReadSource for LocalSource { /// An iterator over the entries of the local source. fn entries(&self) -> Self::Iter { LocalSourceWalker { + base_path: self.base_path.clone(), walker: self.builder.build(), save_opts: self.save_opts, } @@ -383,6 +399,7 @@ impl ReadSource for LocalSource { // Walk doesn't implement Debug #[allow(missing_debug_implementations)] pub struct LocalSourceWalker { + base_path: PathBuf, /// The walk iterator. walker: Walk, /// The save options to use. @@ -410,6 +427,7 @@ impl Iterator for LocalSourceWalker { ) .ask_report() })?, + &self.base_path, self.save_opts.with_atime, self.save_opts.ignore_devid, ) @@ -441,10 +459,11 @@ impl Iterator for LocalSourceWalker { #[allow(clippy::similar_names)] fn map_entry( entry: DirEntry, + base_path: &Path, with_atime: bool, _ignore_devid: bool, ) -> IgnoreResult> { - let name = entry.file_name(); + let name = entry.file_name().as_encoded_bytes(); let m = entry .metadata() .map_err(|err| IgnoreErrorKind::FailedToGetMetadata { source: err })?; @@ -502,7 +521,8 @@ fn map_entry( path: path.to_path_buf(), source: err, })?; - let node_type = NodeType::from_link(&target); + let target = target.as_os_str().as_encoded_bytes(); + let node_type = NodeType::from_link(&UnixPath::new(target).to_typed_path()); Node::new_node(name, node_type, meta) } else { Node::new_node(name, NodeType::File, meta) @@ -510,6 +530,8 @@ fn map_entry( let path = entry.into_path(); let open = Some(OpenFile(path.clone())); + let path = path.strip_prefix(base_path).unwrap(); + let path = path_to_unix_path(path)?.to_path_buf(); Ok(ReadSourceEntry { path, node, open }) } @@ -607,14 +629,15 @@ fn list_extended_attributes(path: &Path) -> IgnoreResult> #[allow(clippy::similar_names)] fn map_entry( entry: DirEntry, + base_path: &Path, with_atime: bool, ignore_devid: bool, ) -> IgnoreResult> { - let name = entry.file_name(); + let name = entry.file_name().as_encoded_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, })?; @@ -688,7 +711,8 @@ fn map_entry( path: path.to_path_buf(), source: err, })?; - let node_type = NodeType::from_link(&target); + let target = target.as_os_str().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 +729,8 @@ fn map_entry( }; let path = entry.into_path(); let open = Some(OpenFile(path.clone())); + let path = path.strip_prefix(base_path).unwrap(); + let path = path_to_unix_path(path)?.to_path_buf(); Ok(ReadSourceEntry { path, node, open }) } diff --git a/crates/core/src/backend/local_destination.rs b/crates/core/src/backend/local_destination.rs index d87a6d96..86db2106 100644 --- a/crates/core/src/backend/local_destination.rs +++ b/crates/core/src/backend/local_destination.rs @@ -23,14 +23,16 @@ use nix::{ fcntl::AtFlags, unistd::{Gid, Group, Uid, User, fchownat}, }; +use typed_path::UnixPath; #[cfg(not(windows))] use crate::backend::ignore::mapper::map_mode_from_go; #[cfg(not(windows))] -use crate::backend::node::NodeType; +use crate::{backend::node::NodeType, util::typed_path_to_path}; use crate::{ backend::node::{ExtendedAttribute, Metadata, Node}, error::{ErrorKind, RusticError, RusticResult}, + util::unix_path_to_path, }; /// [`LocalDestinationErrorKind`] describes the errors that can be returned by an action on the filesystem in Backends @@ -102,6 +104,8 @@ pub enum LocalDestinationErrorKind { filename: PathBuf, source: std::io::Error, }, + /// Non-UTF8 filename is not allowed: `{0:?}` + Utf8Error(#[from] std::str::Utf8Error), } pub(crate) type LocalDestinationResult = Result; @@ -189,12 +193,13 @@ 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()); } + let item = unix_path_to_path(item.as_ref())?; + Ok(self.path.join(item)) } /// Remove the given directory (relative to the base path) @@ -249,8 +254,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 +272,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( @@ -299,7 +304,7 @@ impl LocalDestination { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub(crate) fn set_user_group( &self, - _item: impl AsRef, + _item: impl AsRef, _meta: &Metadata, ) -> LocalDestinationResult<()> { // https://learn.microsoft.com/en-us/windows/win32/fileio/file-security-and-access-rights @@ -322,10 +327,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) @@ -355,7 +360,7 @@ impl LocalDestination { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub(crate) fn set_uid_gid( &self, - _item: impl AsRef, + _item: impl AsRef, _meta: &Metadata, ) -> LocalDestinationResult<()> { Ok(()) @@ -375,10 +380,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); @@ -403,7 +408,7 @@ impl LocalDestination { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub(crate) fn set_permission( &self, - _item: impl AsRef, + _item: impl AsRef, _node: &Node, ) -> LocalDestinationResult<()> { Ok(()) @@ -423,14 +428,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 +461,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 +490,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 +502,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 { @@ -564,10 +571,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()))?; @@ -603,7 +610,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(()) @@ -624,15 +631,17 @@ 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 = typed_path_to_path(&linktarget) + .map_err(LocalDestinationErrorKind::Utf8Error)?; + symlink(&linktarget, &filename).map_err(|err| { LocalDestinationErrorKind::SymlinkingFailed { linktarget: linktarget.to_path_buf(), filename, @@ -721,11 +730,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 @@ -757,8 +766,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| { @@ -790,11 +799,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 39178c9f..4db078ff 100644 --- a/crates/core/src/backend/node.rs +++ b/crates/core/src/backend/node.rs @@ -1,17 +1,7 @@ -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; @@ -23,16 +13,15 @@ use serde_with::{ formats::Padded, serde_as, skip_serializing_none, }; +use typed_path::TypedPath; use crate::blob::{DataId, tree::TreeId}; -#[cfg(not(windows))] /// [`NodeErrorKind`] describes the errors that can be returned by an action utilizing a node in Backends #[derive(thiserror::Error, Debug, displaydoc::Display)] #[non_exhaustive] pub enum NodeErrorKind<'a> { /// Unexpected EOF while parsing filename: `{file_name}` - #[cfg(not(windows))] UnexpectedEOF { /// The filename file_name: String, @@ -40,7 +29,6 @@ pub enum NodeErrorKind<'a> { chars: std::str::Chars<'a>, }, /// Invalid unicode - #[cfg(not(windows))] InvalidUnicode { /// The filename file_name: String, @@ -50,7 +38,6 @@ pub enum NodeErrorKind<'a> { chars: std::str::Chars<'a>, }, /// Unrecognized Escape while parsing filename: `{file_name}` - #[cfg(not(windows))] UnrecognizedEscape { /// The filename file_name: String, @@ -58,7 +45,6 @@ pub enum NodeErrorKind<'a> { chars: std::str::Chars<'a>, }, /// Parsing hex chars {chars:?} failed for `{hex}` in filename: `{file_name}` : `{source}` - #[cfg(not(windows))] ParsingHexFailed { /// The filename file_name: String, @@ -70,7 +56,6 @@ pub enum NodeErrorKind<'a> { source: ParseIntError, }, /// Parsing unicode chars {chars:?} failed for `{target}` in filename: `{file_name}` : `{source}` - #[cfg(not(windows))] ParsingUnicodeFailed { /// The filename file_name: String, @@ -83,7 +68,6 @@ pub enum NodeErrorKind<'a> { }, } -#[cfg(not(windows))] pub(crate) type NodeResult<'a, T> = Result>; #[derive( @@ -171,15 +155,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), @@ -190,57 +173,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)), - ), + } => linktarget_raw + .as_ref() + .map_or_else(|| linktarget.as_bytes(), |t| 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), - _ => panic!("called method to_link on non-symlink!"), - } + }) } } @@ -314,7 +263,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, @@ -360,8 +309,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) } } @@ -380,25 +329,6 @@ pub fn last_modified_node(n1: &Node, n2: &Node) -> Ordering { n1.meta.mtime.cmp(&n2.meta.mtime) } -// TODO: Should be probably called `_lossy` -// TODO(Windows): This is not able to handle non-unicode filenames and -// doesn't treat filenames which need and escape (like `\`, `"`, ...) correctly -#[cfg(windows)] -fn escape_filename(name: &OsStr) -> String { - name.to_string_lossy().to_string() -} - -/// Unescape a filename -/// -/// # Arguments -/// -/// * `s` - The escaped filename -#[cfg(windows)] -fn unescape_filename(s: &str) -> Result { - OsString::from_str(s) -} - -#[cfg(not(windows))] /// Escape a filename /// /// # Arguments @@ -408,8 +338,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| { @@ -456,14 +386,13 @@ fn escape_filename(name: &OsStr) -> String { s } -#[cfg(not(windows))] /// Unescape a filename /// /// # Arguments /// /// * `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 { @@ -560,10 +489,9 @@ fn unescape_filename(s: &str) -> NodeResult<'_, OsString> { } } - Ok(OsStr::from_bytes(&u).to_os_string()) + Ok(u) } -#[cfg(not(windows))] #[inline] // Iterator#take cannot be used because it consumes the iterator fn take>(iterator: &mut I, n: usize) -> String { @@ -574,7 +502,6 @@ fn take>(iterator: &mut I, n: usize) -> String { s } -#[cfg(not(windows))] #[cfg(test)] mod tests { use super::*; @@ -584,9 +511,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, } @@ -610,8 +536,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)); } @@ -635,14 +560,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 3027bfe9..407d320b 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 3e090509..9050b4c2 100644 --- a/crates/core/src/blob/tree.rs +++ b/crates/core/src/blob/tree.rs @@ -1,9 +1,8 @@ use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet, BinaryHeap}, - ffi::{OsStr, OsString}, mem, - path::{Component, Path, PathBuf, Prefix}, + path::PathBuf, str::{self, Utf8Error}, }; @@ -13,6 +12,7 @@ use ignore::Match; use ignore::overrides::{Override, OverrideBuilder}; use serde::{Deserialize, Deserializer}; use serde_derive::Serialize; +use typed_path::{Component, UnixPath, UnixPathBuf}; use crate::{ backend::{ @@ -52,8 +52,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 +170,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 +200,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 +232,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 +253,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 +286,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 +312,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 +330,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 +350,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,43 +385,16 @@ 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`]. -/// -/// # Arguments -/// -/// * `p` - The component to convert. -/// -/// # Errors -/// -/// * 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) -} - impl IntoIterator for Tree { type Item = Node; type IntoIter = std::vec::IntoIter; @@ -513,7 +464,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 +525,7 @@ where Ok(Self { inner, open_iterators: Vec::new(), - path: PathBuf::new(), + path: UnixPathBuf::new(), be, index, overrides, @@ -726,7 +677,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 +717,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 +782,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 +817,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 +873,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 db4096d1..ad1dd614 100644 --- a/crates/core/src/commands/backup.rs +++ b/crates/core/src/commands/backup.rs @@ -1,10 +1,10 @@ //! `backup` subcommand use derive_setters::Setters; use log::info; +use typed_path::{UnixPath, UnixPathBuf}; use std::path::PathBuf; -use path_dedot::ParseDot; use serde_derive::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, serde_as}; @@ -221,22 +221,12 @@ pub(crate) fn backup( source.paths() }; - let as_path = opts - .as_path - .as_ref() - .map(|p| -> RusticResult<_> { - Ok(p.parse_dot() - .map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Failed to parse dotted path `{path}`", - err, - ) - .attach_context("path", p.display().to_string()) - })? - .to_path_buf()) - }) - .transpose()?; + let as_path = match &opts.as_path { + Some(p) => Some(p), + None if !backup_stdin && backup_path.len() == 1 => Some(&backup_path[0]), + None => None, + } + .map(|p| UnixPath::new(p.as_os_str().as_encoded_bytes()).normalize()); match &as_path { Some(p) => snap.paths.set_paths(&[p.clone()]).map_err(|err| { @@ -247,21 +237,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 +282,10 @@ 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)?; let res = archiver.archive( &src, - path, as_path.as_ref(), opts.parent_opts.skip_if_unchanged, opts.no_scan, @@ -295,10 +294,10 @@ pub(crate) fn backup( src.finish()?; res } else { - let src = StdinSource::new(path.clone()); + let path: UnixPathBuf = path.clone().try_into().unwrap(); // TODO: error handling + let src = StdinSource::new(path); archiver.archive( &src, - path, as_path.as_ref(), opts.parent_opts.skip_if_unchanged, opts.no_scan, @@ -313,7 +312,6 @@ pub(crate) fn backup( )?; archiver.archive( &src, - &backup_path[0], 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 fdf78142..975690b6 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::{FileType, FindInBackend, decrypt::DecryptReadBackend}, @@ -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 5533f968..81c0379c 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 9276f70c..669aa981 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(); @@ -215,7 +216,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 { @@ -296,7 +297,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(); @@ -349,7 +358,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<()> { @@ -396,9 +405,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.display()); dest.create_special(path, node) .unwrap_or_else(|_| warn!("restore {}: creating special file failed.", path.display())); @@ -675,7 +685,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/lib.rs b/crates/core/src/lib.rs index 0112a191..fe8c37fb 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -117,6 +117,8 @@ pub(crate) mod progress; /// Structs which are saved in JSON or binary format in the repository pub mod repofile; pub(crate) mod repository; +/// Utils for using unix paths +pub mod util; /// Virtual File System support - allows to act on the repository like on a file system pub mod vfs; @@ -158,3 +160,6 @@ pub use crate::{ command_input::{CommandInput, CommandInputErrorKind}, }, }; + +// re-exports +pub use typed_path; diff --git a/crates/core/src/repofile/snapshotfile.rs b/crates/core/src/repofile/snapshotfile.rs index e6acca98..a7cef566 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::{DisplayFromStr, serde_as, skip_serializing_none}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ Id, @@ -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:?}` @@ -1203,7 +1204,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| { @@ -1307,6 +1308,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 b912cad9..5ceda7ac 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::{DisplayFromStr, serde_as}; +use typed_path::{UnixPath, UnixPathBuf}; use crate::{ RepositoryBackends, RusticError, @@ -1678,8 +1679,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 @@ -1695,7 +1696,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) } @@ -1713,7 +1714,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) } @@ -1756,7 +1757,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` @@ -1776,7 +1777,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 /// @@ -1821,7 +1822,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) } @@ -1841,7 +1842,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) @@ -2023,7 +2024,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 fd51c842..b3acf762 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/util.rs b/crates/core/src/util.rs new file mode 100644 index 00000000..acacfa2c --- /dev/null +++ b/crates/core/src/util.rs @@ -0,0 +1,221 @@ +/// Utilities for handling paths on ``rustic_core`` +/// +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + str::Utf8Error, +}; + +use serde::{Serialize, Serializer}; +use typed_path::{ + Component, TypedPath, UnixComponent, UnixPath, UnixPathBuf, WindowsComponent, WindowsPath, + WindowsPrefix, +}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(transparent)] +/// Like `UnixPathBuf` , but implements `Serialize` +pub struct SerializablePath(#[serde(serialize_with = "serialize_unix_path")] pub UnixPathBuf); + +fn serialize_unix_path(path: &UnixPath, serializer: S) -> Result +where + S: Serializer, +{ + let s = format!("{}", path.display()); + serializer.serialize_str(&s) +} + +/// Converts a [`Path`] to a [`WindowsPath`]. +/// +/// # Arguments +/// +/// * `path` - The path to convert. +/// +/// # Errors +/// +/// * If the path is non-unicode +pub fn path_to_windows_path(path: &Path) -> Result<&WindowsPath, Utf8Error> { + let str = std::str::from_utf8(path.as_os_str().as_encoded_bytes())?; + Ok(WindowsPath::new(str)) +} + +/// Converts a [`Path`] to a [`Cow`]. +/// +/// Note: On windows, this converts prefixes into unix paths, e.g. "C:\dir" into "/c/dir" +/// +/// # Arguments +/// +/// * `path` - The path to convert. +/// +/// # Errors +/// +/// * If the path is non-unicode and we are using windows +pub fn path_to_unix_path(path: &Path) -> Result, Utf8Error> { + #[cfg(not(windows))] + { + let path = UnixPath::new(path.as_os_str().as_encoded_bytes()); + Ok(Cow::Borrowed(path)) + } + #[cfg(windows)] + { + let path = windows_path_to_unix_path(path_to_windows_path(path)?); + Ok(Cow::Owned(path)) + } +} + +/// Converts a [`TypedPath`] to a [`Cow`]. +/// +/// Note: On unix, this converts windows prefixes into unix paths, e.g. "C:\dir" into "/c/dir" +/// +/// # Arguments +/// +/// * `path` - The path to convert. +/// +/// # Errors +/// +/// * If the path is non-unicode and we are using windows +pub fn typed_path_to_path<'a>(path: &'a TypedPath<'a>) -> Result, Utf8Error> { + #[cfg(not(windows))] + { + let path = match typed_path_to_unix_path(path) { + Cow::Borrowed(path) => Cow::Borrowed(unix_path_to_path(path)?), + Cow::Owned(path) => Cow::Owned(unix_path_to_path(&path)?.to_path_buf()), + }; + Ok(path) + } + #[cfg(windows)] + { + // only utf8 items are allowed on windows + let str = std::str::from_utf8(path.as_bytes())?; + Ok(Cow::Borrowed(Path::new(str))) + } +} + +/// Converts a [`UnixPath`] to a [`Path`]. +/// +/// # Arguments +/// +/// * `path` - The path to convert. +/// +/// # Errors +/// +/// * If the path is non-unicode and we are using windows +pub fn unix_path_to_path(path: &UnixPath) -> Result<&Path, Utf8Error> { + #[cfg(not(windows))] + { + use std::ffi::OsStr; + let osstr: &OsStr = path.as_ref(); + Ok(Path::new(osstr)) + } + #[cfg(windows)] + { + // only utf8 items are allowed on windows + let str = std::str::from_utf8(path.as_bytes())?; + Ok(Path::new(str)) + } +} + +/// Converts a [`[u8]`] to a [`PathBuf`]. +// This is a hacky implementation, espeically for windows where we convert lossily +// into an utf8 string and match on the windows path given by that string. +// Note: `GlobMatcher` internally converts into a `&[u8]` to perform the matching +// TODO: Use https://github.com/BurntSushi/ripgrep/pull/2955 once it is available. +#[cfg(not(windows))] +pub fn u8_to_path(path: impl AsRef<[u8]>) -> PathBuf { + use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; + Path::new(OsStr::from_bytes(path.as_ref())).to_path_buf() +} +#[cfg(windows)] +pub fn u8_to_path(path: impl AsRef<[u8]>) -> PathBuf { + use std::ffi::OsStr; + let string: &str = &String::from_utf8_lossy(path.as_ref()); + Path::new(OsStr::new(string)).to_path_buf() +} + +/// Converts a [`TypedPath`] to a [`Cow`]. +/// +/// # Arguments +/// +/// * `path` - The path to convert. +#[must_use] +pub fn typed_path_to_unix_path<'a>(path: &'a TypedPath<'_>) -> Cow<'a, UnixPath> { + match path { + TypedPath::Unix(p) => Cow::Borrowed(p), + TypedPath::Windows(p) => Cow::Owned(windows_path_to_unix_path(p)), + } +} + +/// Converts a [`WindowsPath`] to a [`UnixPathBuf`]. +/// +/// Note: This converts windows prefixes into unix paths, e.g. "C:\dir" into "/c/dir" +/// +/// # Arguments +/// +/// * `path` - The path to convert. +#[must_use] +pub fn windows_path_to_unix_path(path: &WindowsPath) -> UnixPathBuf { + let mut unix_path = UnixPathBuf::new(); + let mut components = path.components(); + if let Some(c) = components.next() { + match c { + WindowsComponent::Prefix(p) => { + unix_path.push(UnixComponent::RootDir); + match p.kind() { + WindowsPrefix::Verbatim(p) | WindowsPrefix::DeviceNS(p) => { + unix_path.push(p.to_ascii_lowercase()); + } + WindowsPrefix::VerbatimUNC(_, q) | WindowsPrefix::UNC(_, q) => { + unix_path.push(q.to_ascii_lowercase()); + } + WindowsPrefix::VerbatimDisk(p) | WindowsPrefix::Disk(p) => { + let c = vec![p.to_ascii_lowercase()]; + unix_path.push(&c); + } + } + // remove RootDir from iterator + _ = components.next(); + } + WindowsComponent::RootDir => { + unix_path.push(UnixComponent::RootDir); + } + c => { + unix_path.push(c.as_bytes()); + } + } + } + for c in components { + match c { + WindowsComponent::RootDir => { + unix_path.push(UnixComponent::RootDir); + } + c => { + unix_path.push(c); + } + } + } + unix_path +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + #[rstest] + #[case("/", "/")] + #[case(r#"\"#, "/")] + #[case("/test/test2", "/test/test2")] + #[case(r#"\test\test2"#, "/test/test2")] + #[case(r#"C:\"#, "/c")] + #[case(r#"C:\dir"#, "/c/dir")] + #[case(r#"d:\"#, "/d")] + #[case(r#"a\b\"#, "a/b")] + #[case(r#"a\b\c"#, "a/b/c")] + fn test_windows_path_to_unix_path(#[case] windows_path: &str, #[case] unix_path: &str) { + assert_eq!( + windows_path_to_unix_path(WindowsPath::new(windows_path)) + .to_str() + .unwrap(), + UnixPath::new(unix_path).to_str().unwrap() + ); + } +} diff --git a/crates/core/src/vfs.rs b/crates/core/src/vfs.rs index 7ab4607c..41af5fb1 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::{BlobId, DataId, tree::TreeId}, @@ -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 e2d87bb2..1d20364c 100644 --- a/crates/core/tests/integration.rs +++ b/crates/core/tests/integration.rs @@ -168,6 +168,12 @@ fn assert_with_win(test: &str, snap: T) { assert_ron_snapshot!(format!("{test}-nix"), snap); } +// helper function +// TODO: Use macro directly (need to refactor snapshot location) +fn assert_ron(test: &str, snap: T) { + assert_ron_snapshot!(test, snap); +} + #[test] fn repo_with_commands() -> Result<()> { let be = InMemoryBackend::new(); diff --git a/crates/core/tests/integration/backup.rs b/crates/core/tests/integration/backup.rs index 8c19be0d..03a4633b 100644 --- a/crates/core/tests/integration/backup.rs +++ b/crates/core/tests/integration/backup.rs @@ -1,7 +1,4 @@ -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{path::PathBuf, str::FromStr}; use anyhow::Result; use insta::Settings; @@ -13,6 +10,7 @@ use rustic_core::{ StringList, repofile::{PackId, SnapshotFile}, }; +use typed_path::UnixPath; use super::{ RepoOpen, TestSource, assert_with_win, insta_node_redaction, insta_snapshotfile_redaction, @@ -52,7 +50,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(|| { @@ -184,7 +182,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(|| { diff --git a/crates/core/tests/integration/find.rs b/crates/core/tests/integration/find.rs index 787abca1..d7b44f81 100644 --- a/crates/core/tests/integration/find.rs +++ b/crates/core/tests/integration/find.rs @@ -1,7 +1,7 @@ -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +// don't warn about try_from paths conversion on unix +#![allow(clippy::unnecessary_fallible_conversions)] + +use std::{path::PathBuf, str::FromStr}; use anyhow::Result; use globset::Glob; @@ -10,9 +10,11 @@ use rstest::rstest; use rustic_core::{ BackupOptions, FindMatches, FindNode, repofile::{Node, SnapshotFile}, + util::{SerializablePath, u8_to_path}, }; +use typed_path::UnixPath; -use super::{RepoOpen, TestSource, assert_with_win, set_up_repo, tar_gz_testdata}; +use super::{RepoOpen, TestSource, assert_ron, set_up_repo, tar_gz_testdata}; #[rstest] fn test_find(tar_gz_testdata: Result, set_up_repo: Result) -> Result<()> { @@ -29,33 +31,45 @@ 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"))?; - assert_with_win("find-nodes-not-found", not_found); + let not_found = + repo.find_nodes_from_path(vec![snapshot.tree], UnixPath::new("not_existing"))?; + assert_ron("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 FindMatches { paths, matches, .. } = repo + .find_matching_nodes(vec![snapshot.tree], &|path, _| { + glob.is_match(u8_to_path(path)) + })?; + assert!(paths.is_empty()); + assert_eq!(matches, [[]]); // test existing path let FindNode { matches, .. } = - repo.find_nodes_from_path(vec![snapshot.tree], Path::new("test/0/tests/testfile"))?; - assert_with_win("find-nodes-existing", matches); + repo.find_nodes_from_path(vec![snapshot.tree], UnixPath::new("test/0/tests/testfile"))?; + assert_ron("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(u8_to_path(path)) + || path + .file_name() + .is_some_and(|f| glob.is_match(u8_to_path(f))) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-existing", (paths, matches)); + let paths: Vec<_> = paths.into_iter().map(SerializablePath).collect(); + assert_ron("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(u8_to_path(path)) + || path + .file_name() + .is_some_and(|f| glob.is_match(u8_to_path(f))) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; - assert_with_win("find-matching-wildcard-existing", (paths, matches)); + let paths: Vec<_> = paths.into_iter().map(SerializablePath).collect(); + assert_ron("find-matching-wildcard-existing", (paths, matches)); Ok(()) } diff --git a/crates/core/tests/integration/ls.rs b/crates/core/tests/integration/ls.rs index 00452258..2b2c2160 100644 --- a/crates/core/tests/integration/ls.rs +++ b/crates/core/tests/integration/ls.rs @@ -1,13 +1,15 @@ -use std::{collections::BTreeMap, ffi::OsStr}; +use std::collections::BTreeMap; use std::{path::PathBuf, str::FromStr}; use anyhow::Result; use insta::Settings; +use itertools::Itertools; use rstest::rstest; use rustic_core::{ BackupOptions, LsOptions, RusticResult, repofile::{Metadata, Node, SnapshotFile}, + util::SerializablePath, }; use super::{ @@ -31,7 +33,7 @@ fn test_ls( // test non-existing entries let mut node = Node::new_node( - OsStr::new(""), + &[], rustic_core::repofile::NodeType::Dir, Metadata::default(), ); @@ -42,6 +44,7 @@ fn test_ls( let entries: BTreeMap<_, _> = repo .ls(&node, &LsOptions::default())? + .map_ok(|(path, node)| (SerializablePath(path), node)) .collect::>()?; insta_node_redaction.bind(|| { diff --git a/crates/core/tests/integration/vfs.rs b/crates/core/tests/integration/vfs.rs index a2fd819a..da0b88e2 100644 --- a/crates/core/tests/integration/vfs.rs +++ b/crates/core/tests/integration/vfs.rs @@ -6,7 +6,11 @@ use insta::Settings; use pretty_assertions::assert_eq; use rstest::rstest; -use rustic_core::{BackupOptions, repofile::SnapshotFile, vfs::Vfs}; +use rustic_core::{ + BackupOptions, SnapshotOptions, + vfs::{IdenticalSnapshot, Latest, Vfs}, +}; +use typed_path::UnixPath; use super::{ RepoOpen, TestSource, assert_with_win, insta_node_redaction, set_up_repo, tar_gz_testdata, @@ -25,7 +29,8 @@ fn test_vfs( // we use as_path to not depend on the actual tempdir let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?); // backup test-data - let snapshot = repo.backup(&opts, paths, SnapshotFile::default())?; + let snap_opts = SnapshotOptions::default().label("label".to_string()); + let snapshot = repo.backup(&opts, paths, snap_opts.to_snapshot()?)?; // re-read index let repo = repo.to_indexed()?; @@ -34,15 +39,13 @@ fn test_vfs( let vfs = Vfs::from_dir_node(&node); // test reading a directory using vfs - let path: PathBuf = ["test", "0", "tests"].iter().collect(); - let entries = vfs.dir_entries_from_path(&repo, &path)?; + let entries = vfs.dir_entries_from_path(&repo, UnixPath::new("test/0/tests"))?; insta_node_redaction.bind(|| { assert_with_win("vfs", &entries); }); // test reading a file from the repository - let path: PathBuf = ["test", "0", "tests", "testfile"].iter().collect(); - let node = vfs.node_from_path(&repo, &path)?; + let node = vfs.node_from_path(&repo, UnixPath::new("test/0/tests/testfile"))?; let file = repo.open_file(&node)?; let data = repo.read_file_at(&file, 0, 21)?; // read full content @@ -52,9 +55,20 @@ fn test_vfs( 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 node = vfs.node_from_path(&repo, &path)?; + let node = vfs.node_from_path(&repo, UnixPath::new("test/0/tests/empty-file"))?; let file = repo.open_file(&node)?; assert_eq!(Bytes::new(), repo.read_file_at(&file, 0, 0)?); // empty files + + // create Vfs from snapshots + let vfs = Vfs::from_snapshots( + vec![snapshot], + "{label}/{time}", + "%Y", + Latest::AsDir, + IdenticalSnapshot::AsLink, + )?; + let entries_from_snapshots = + vfs.dir_entries_from_path(&repo, UnixPath::new("label/latest/test/0/tests"))?; + assert_eq!(entries_from_snapshots, entries); Ok(()) } diff --git a/crates/core/tests/snapshots/integration__find-matching-existing-windows.snap b/crates/core/tests/snapshots/integration__find-matching-existing-windows.snap deleted file mode 100644 index 174a9544..00000000 --- a/crates/core/tests/snapshots/integration__find-matching-existing-windows.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -([ - "test\\0\\tests\\testfile", -], [ - [ - (0, 0), - ], -]) diff --git a/crates/core/tests/snapshots/integration__find-matching-existing-nix.snap b/crates/core/tests/snapshots/integration__find-matching-existing.snap similarity index 100% rename from crates/core/tests/snapshots/integration__find-matching-existing-nix.snap rename to crates/core/tests/snapshots/integration__find-matching-existing.snap diff --git a/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-nix.snap b/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-nix.snap deleted file mode 100644 index 9badfa81..00000000 --- a/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-nix.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -FindMatches( - paths: [], - nodes: [], - matches: [ - [], - ], -) diff --git a/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-windows.snap b/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-windows.snap deleted file mode 100644 index 9badfa81..00000000 --- a/crates/core/tests/snapshots/integration__find-matching-nodes-not-found-windows.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -FindMatches( - paths: [], - nodes: [], - matches: [ - [], - ], -) diff --git a/crates/core/tests/snapshots/integration__find-matching-wildcard-existing-windows.snap b/crates/core/tests/snapshots/integration__find-matching-wildcard-existing-windows.snap deleted file mode 100644 index 5f367bb1..00000000 --- a/crates/core/tests/snapshots/integration__find-matching-wildcard-existing-windows.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -([ - "test\\0\\tests\\testfile", - "test\\0\\tests\\testfile-hardlink", - "test\\0\\tests\\testfile-symlink", -], [ - [ - (0, 0), - (1, 1), - (2, 2), - ], -]) diff --git a/crates/core/tests/snapshots/integration__find-matching-wildcard-existing-nix.snap b/crates/core/tests/snapshots/integration__find-matching-wildcard-existing.snap similarity index 100% rename from crates/core/tests/snapshots/integration__find-matching-wildcard-existing-nix.snap rename to crates/core/tests/snapshots/integration__find-matching-wildcard-existing.snap diff --git a/crates/core/tests/snapshots/integration__find-nodes-existing-windows.snap b/crates/core/tests/snapshots/integration__find-nodes-existing-windows.snap deleted file mode 100644 index 90f957a2..00000000 --- a/crates/core/tests/snapshots/integration__find-nodes-existing-windows.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -[ - Some(0), -] diff --git a/crates/core/tests/snapshots/integration__find-nodes-existing-nix.snap b/crates/core/tests/snapshots/integration__find-nodes-existing.snap similarity index 100% rename from crates/core/tests/snapshots/integration__find-nodes-existing-nix.snap rename to crates/core/tests/snapshots/integration__find-nodes-existing.snap diff --git a/crates/core/tests/snapshots/integration__find-nodes-not-found-windows.snap b/crates/core/tests/snapshots/integration__find-nodes-not-found-windows.snap deleted file mode 100644 index 56f60279..00000000 --- a/crates/core/tests/snapshots/integration__find-nodes-not-found-windows.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -FindNode( - nodes: [], - matches: [ - None, - ], -) diff --git a/crates/core/tests/snapshots/integration__find-nodes-not-found-nix.snap b/crates/core/tests/snapshots/integration__find-nodes-not-found.snap similarity index 100% rename from crates/core/tests/snapshots/integration__find-nodes-not-found-nix.snap rename to crates/core/tests/snapshots/integration__find-nodes-not-found.snap diff --git a/crates/core/tests/snapshots/integration__ls-windows.snap b/crates/core/tests/snapshots/integration__ls-windows.snap index 714b0ecb..871f8bab 100644 --- a/crates/core/tests/snapshots/integration__ls-windows.snap +++ b/crates/core/tests/snapshots/integration__ls-windows.snap @@ -10,7 +10,7 @@ expression: snap "content": None, "subtree": "[some]", }, - "test\\0": { + "test/0": { "name": "0", "type": "dir", "mtime": "[some]", @@ -19,7 +19,7 @@ expression: snap "content": None, "subtree": "[some]", }, - "test\\0\\0": { + "test/0/0": { "name": "0", "type": "dir", "mtime": "[some]", @@ -28,7 +28,7 @@ expression: snap "content": None, "subtree": "[some]", }, - "test\\0\\0\\9": { + "test/0/0/9": { "name": "9", "type": "dir", "mtime": "[some]", @@ -37,7 +37,7 @@ expression: snap "content": None, "subtree": "[some]", }, - "test\\0\\0\\9\\0": { + "test/0/0/9/0": { "name": "0", "type": "file", "mtime": "[some]", @@ -48,7 +48,7 @@ expression: snap Id("aa6da76dc4b3bd8d8cd9d42c5ed6db2e9c3e43cecef7e7ee925a3accab96ad79"), ]), }, - "test\\0\\0\\9\\1": { + "test/0/0/9/1": { "name": "1", "type": "file", "mtime": "[some]", @@ -59,7 +59,7 @@ expression: snap Id("bd15c2d9117aa305a0cf99c9cc30359b66ee454b00b897fb1a925c56025f4556"), ]), }, - "test\\0\\0\\9\\10": { + "test/0/0/9/10": { "name": "10", "type": "file", "mtime": "[some]", @@ -70,7 +70,7 @@ expression: snap Id("753d116d34c772059a9a4a2ca33f6ea9ac618470acc13687d08fff7eb5ad3c72"), ]), }, - "test\\0\\0\\9\\11": { + "test/0/0/9/11": { "name": "11", "type": "file", "mtime": "[some]", @@ -81,7 +81,7 @@ expression: snap Id("efe42dd90fd602df1c5b3b7675cd845e23befba069eac0222a9997fea2a9852f"), ]), }, - "test\\0\\0\\9\\12": { + "test/0/0/9/12": { "name": "12", "type": "file", "mtime": "[some]", @@ -92,7 +92,7 @@ expression: snap Id("032ad465747269b41dbdfc87accad1fd49e8dc574ae4bfaf49f036150ec92e57"), ]), }, - "test\\0\\0\\9\\13": { + "test/0/0/9/13": { "name": "13", "type": "file", "mtime": "[some]", @@ -103,7 +103,7 @@ expression: snap Id("b0b5afc29b6391722bd60a9a74c386c856cb130a219954f34944a4f19bbecc41"), ]), }, - "test\\0\\0\\9\\14": { + "test/0/0/9/14": { "name": "14", "type": "file", "mtime": "[some]", @@ -114,7 +114,7 @@ expression: snap Id("b19a49a2e711d5547e1fb247170c58d2da17f9b0bbf75eb84507097a301f1c6d"), ]), }, - "test\\0\\0\\9\\15": { + "test/0/0/9/15": { "name": "15", "type": "file", "mtime": "[some]", @@ -125,7 +125,7 @@ expression: snap Id("f11e72e0290dc75261ee48c39fd42597bdcd80e110ffd0651f71d54ea22030e5"), ]), }, - "test\\0\\0\\9\\16": { + "test/0/0/9/16": { "name": "16", "type": "file", "mtime": "[some]", @@ -136,7 +136,7 @@ expression: snap Id("71935a6f85198c65584ed5c4ff5e34cf93cb79756023e82ecc85153663be2b00"), ]), }, - "test\\0\\0\\9\\17": { + "test/0/0/9/17": { "name": "17", "type": "file", "mtime": "[some]", @@ -147,7 +147,7 @@ expression: snap Id("b81f6dd6ad54fbed57ffac74bcecf9f1a56ce8948edc92153ee34ed415ed68d1"), ]), }, - "test\\0\\0\\9\\18": { + "test/0/0/9/18": { "name": "18", "type": "file", "mtime": "[some]", @@ -158,7 +158,7 @@ expression: snap Id("1fad45c4bf21695fae9e100626f42d35d8c2c48cb90f6a6256973c262608879d"), ]), }, - "test\\0\\0\\9\\19": { + "test/0/0/9/19": { "name": "19", "type": "file", "mtime": "[some]", @@ -169,7 +169,7 @@ expression: snap Id("92c4aa4fa67ede99b9ab1b694bc32b4f3d127f406a7e6583ecbe55b1bb919928"), ]), }, - "test\\0\\0\\9\\2": { + "test/0/0/9/2": { "name": "2", "type": "file", "mtime": "[some]", @@ -180,7 +180,7 @@ expression: snap Id("f5a3517234ef2f213afe617cdab9d25364e1c8c9e61198bf387b157542ee7dbb"), ]), }, - "test\\0\\0\\9\\20": { + "test/0/0/9/20": { "name": "20", "type": "file", "mtime": "[some]", @@ -191,7 +191,7 @@ expression: snap Id("737c19313675671c589bfc613b0b736b95a58218486d09d3a7a649f324d81ef7"), ]), }, - "test\\0\\0\\9\\21": { + "test/0/0/9/21": { "name": "21", "type": "file", "mtime": "[some]", @@ -202,7 +202,7 @@ expression: snap Id("ee25130a4e74b1306087799914d8b92aed2c385b06256705b18266ff088524f4"), ]), }, - "test\\0\\0\\9\\22": { + "test/0/0/9/22": { "name": "22", "type": "file", "mtime": "[some]", @@ -213,7 +213,7 @@ expression: snap Id("3492c28f8ddbb9186e483ef9eb1cc7042c5adf871a4d15d2edf8ee7b2b04069b"), ]), }, - "test\\0\\0\\9\\23": { + "test/0/0/9/23": { "name": "23", "type": "file", "mtime": "[some]", @@ -224,7 +224,7 @@ expression: snap Id("b321dedc16124312ed08cb7f9c67006bd6a4f946ca04bc178323e6147b081943"), ]), }, - "test\\0\\0\\9\\24": { + "test/0/0/9/24": { "name": "24", "type": "file", "mtime": "[some]", @@ -235,7 +235,7 @@ expression: snap Id("7020db85ffe6a3fd5f34971ff37a20fa5d32947181692bfbcca62cfc4a9221d0"), ]), }, - "test\\0\\0\\9\\25": { + "test/0/0/9/25": { "name": "25", "type": "file", "mtime": "[some]", @@ -246,7 +246,7 @@ expression: snap Id("75e7274ef54e91c2857ba0de4c869388df3f9c657d776a3e90eaf3f3f619536d"), ]), }, - "test\\0\\0\\9\\26": { + "test/0/0/9/26": { "name": "26", "type": "file", "mtime": "[some]", @@ -257,7 +257,7 @@ expression: snap Id("418b691988d9bc7357b99c5baf31e83436315d1bcc887482dd83a8537183af17"), ]), }, - "test\\0\\0\\9\\27": { + "test/0/0/9/27": { "name": "27", "type": "file", "mtime": "[some]", @@ -268,7 +268,7 @@ expression: snap Id("06a5749af5715af19fde59d1caaa542df00c56aa0058bf99b67dec5216c27df6"), ]), }, - "test\\0\\0\\9\\28": { + "test/0/0/9/28": { "name": "28", "type": "file", "mtime": "[some]", @@ -279,7 +279,7 @@ expression: snap Id("61e8ffda5a11d743d7dc4bcfd1e18e8b6179a53f8b28b25adf55d3acb0b10eff"), ]), }, - "test\\0\\0\\9\\29": { + "test/0/0/9/29": { "name": "29", "type": "file", "mtime": "[some]", @@ -290,7 +290,7 @@ expression: snap Id("60c1c2f3a0b8e5b1c4e2b7f1453661170395c5c26f2e2d241e81eb929cfc930f"), ]), }, - "test\\0\\0\\9\\3": { + "test/0/0/9/3": { "name": "3", "type": "file", "mtime": "[some]", @@ -301,7 +301,7 @@ expression: snap Id("f0ad279385b5b5c862e1e2fdd599616115729e0b8085833d7b31de68c8326b85"), ]), }, - "test\\0\\0\\9\\30": { + "test/0/0/9/30": { "name": "30", "type": "file", "mtime": "[some]", @@ -312,7 +312,7 @@ expression: snap Id("790c34f1d4d95a6067f2406a78aa0dcca2fc2aa1db932fbd865ce9cafd7baa23"), ]), }, - "test\\0\\0\\9\\31": { + "test/0/0/9/31": { "name": "31", "type": "file", "mtime": "[some]", @@ -323,7 +323,7 @@ expression: snap Id("8e53f45db9249373828c588774471d34c9c1e436b83b1603a49d675de810db7e"), ]), }, - "test\\0\\0\\9\\32": { + "test/0/0/9/32": { "name": "32", "type": "file", "mtime": "[some]", @@ -334,7 +334,7 @@ expression: snap Id("c4eccf95ba2409d21283981297bf9919ec15c2cc0dadef84aafaad49a0c79130"), ]), }, - "test\\0\\0\\9\\33": { + "test/0/0/9/33": { "name": "33", "type": "file", "mtime": "[some]", @@ -345,7 +345,7 @@ expression: snap Id("da8b8b2d79bba6a3d4784233e4421293cea0cf27fd03521e11ce0c7c941f6dbb"), ]), }, - "test\\0\\0\\9\\34": { + "test/0/0/9/34": { "name": "34", "type": "file", "mtime": "[some]", @@ -356,7 +356,7 @@ expression: snap Id("a1413ef1018e7a4f3df7f19aad942a611cbda5bdd45d4269cb54d5f9fa58cd01"), ]), }, - "test\\0\\0\\9\\35": { + "test/0/0/9/35": { "name": "35", "type": "file", "mtime": "[some]", @@ -367,7 +367,7 @@ expression: snap Id("f534a7fe839d440a0fdd1d45254921601b2a90ff5b61edcdc631b722f84a0dbb"), ]), }, - "test\\0\\0\\9\\36": { + "test/0/0/9/36": { "name": "36", "type": "file", "mtime": "[some]", @@ -378,7 +378,7 @@ expression: snap Id("d56e2a8c47f4ae36886784c852916f36036f3daffa547a5922dda5814ecc5a86"), ]), }, - "test\\0\\0\\9\\37": { + "test/0/0/9/37": { "name": "37", "type": "file", "mtime": "[some]", @@ -389,7 +389,7 @@ expression: snap Id("5800df73dcfdbd261800f2474c8631fe0727fbc1788cfd70bd206ab2f50b0707"), ]), }, - "test\\0\\0\\9\\38": { + "test/0/0/9/38": { "name": "38", "type": "file", "mtime": "[some]", @@ -400,7 +400,7 @@ expression: snap Id("e7c987bd8d9257bea1e44f882bc075c84ee7f103d68eec8677e37b49d2854d90"), ]), }, - "test\\0\\0\\9\\39": { + "test/0/0/9/39": { "name": "39", "type": "file", "mtime": "[some]", @@ -411,7 +411,7 @@ expression: snap Id("03a1411035add5b10ed183b2ca7d8db8deee2402f45ee445f54de50896c3e26f"), ]), }, - "test\\0\\0\\9\\4": { + "test/0/0/9/4": { "name": "4", "type": "file", "mtime": "[some]", @@ -422,7 +422,7 @@ expression: snap Id("917aae16764fc905abf0514e965133ed80e2b66c426fb09be24eabd452d4438d"), ]), }, - "test\\0\\0\\9\\40": { + "test/0/0/9/40": { "name": "40", "type": "file", "mtime": "[some]", @@ -433,7 +433,7 @@ expression: snap Id("1adcd19cf770f7d8ab3bb4b3996fb6187f59c468953848ec4f119c9cef484f28"), ]), }, - "test\\0\\0\\9\\41": { + "test/0/0/9/41": { "name": "41", "type": "file", "mtime": "[some]", @@ -444,7 +444,7 @@ expression: snap Id("b5debda793f22adb93204dc81f107d03636b7431192fc8497f5c9a340e43460c"), ]), }, - "test\\0\\0\\9\\42": { + "test/0/0/9/42": { "name": "42", "type": "file", "mtime": "[some]", @@ -455,7 +455,7 @@ expression: snap Id("e6e9b35f3ca77afbd0043bc42085b814ceea8cfb93a0bcba9696dee5034ffead"), ]), }, - "test\\0\\0\\9\\43": { + "test/0/0/9/43": { "name": "43", "type": "file", "mtime": "[some]", @@ -466,7 +466,7 @@ expression: snap Id("31d87fd77a88ca00e9c5612f578c0b3243347ba328a6daa09742fdc8c7cec6f4"), ]), }, - "test\\0\\0\\9\\44": { + "test/0/0/9/44": { "name": "44", "type": "file", "mtime": "[some]", @@ -477,7 +477,7 @@ expression: snap Id("76aec0f2d829d01eff332099ebd5628f61adc72ff44e7d6b82beabc28fa44f1f"), ]), }, - "test\\0\\0\\9\\45": { + "test/0/0/9/45": { "name": "45", "type": "file", "mtime": "[some]", @@ -488,7 +488,7 @@ expression: snap Id("121242341ff94a05074a31cb2a0f566093e5c052e64dcde4c0c9b686e58daf87"), ]), }, - "test\\0\\0\\9\\46": { + "test/0/0/9/46": { "name": "46", "type": "file", "mtime": "[some]", @@ -499,7 +499,7 @@ expression: snap Id("a69efe0ab923672b5aa1a2113f54e80684a7c126c1114af4fac3953be18bea74"), ]), }, - "test\\0\\0\\9\\47": { + "test/0/0/9/47": { "name": "47", "type": "file", "mtime": "[some]", @@ -510,7 +510,7 @@ expression: snap Id("a50a7ab010d0d349b213576d1e2d2d2b345334be1be6f62798d250fc1e5cc51f"), ]), }, - "test\\0\\0\\9\\48": { + "test/0/0/9/48": { "name": "48", "type": "file", "mtime": "[some]", @@ -521,7 +521,7 @@ expression: snap Id("32715f839edd796c5b6f2d48c88748857ed439867c2dc4b2b65c5f96c6d9ef73"), ]), }, - "test\\0\\0\\9\\49": { + "test/0/0/9/49": { "name": "49", "type": "file", "mtime": "[some]", @@ -532,7 +532,7 @@ expression: snap Id("d3fdbaa72299c1abfe47bc26bdf201cc5cb66efc031e517111d413c38bfea882"), ]), }, - "test\\0\\0\\9\\5": { + "test/0/0/9/5": { "name": "5", "type": "file", "mtime": "[some]", @@ -543,7 +543,7 @@ expression: snap Id("de68adab4fe82b31fe5b14abe5d17faecf4f7c8d93d74374cbf3793701290ede"), ]), }, - "test\\0\\0\\9\\50": { + "test/0/0/9/50": { "name": "50", "type": "file", "mtime": "[some]", @@ -554,7 +554,7 @@ expression: snap Id("939e25879149a8b87ecb6b44ded96f6b029d5463d38fe5525005984d6d42c34a"), ]), }, - "test\\0\\0\\9\\51": { + "test/0/0/9/51": { "name": "51", "type": "file", "mtime": "[some]", @@ -565,7 +565,7 @@ expression: snap Id("958a554f813d199f9f897c9f5c4d90571dfe7f67d8a3733a64598906a8e7208d"), ]), }, - "test\\0\\0\\9\\52": { + "test/0/0/9/52": { "name": "52", "type": "file", "mtime": "[some]", @@ -576,7 +576,7 @@ expression: snap Id("2e8799330876d03e95428975593175ea5dd75ed3d5f3d83b4d9115dd2c6d3b71"), ]), }, - "test\\0\\0\\9\\53": { + "test/0/0/9/53": { "name": "53", "type": "file", "mtime": "[some]", @@ -587,7 +587,7 @@ expression: snap Id("f476577492f325d8e617814bde3ecc83eb9e8662023d2e057c6c958ce77511e5"), ]), }, - "test\\0\\0\\9\\54": { + "test/0/0/9/54": { "name": "54", "type": "file", "mtime": "[some]", @@ -598,7 +598,7 @@ expression: snap Id("b03ca92f97233541fcc95c22455cadf63c5a25089117af4b433ffc1ed9a8991f"), ]), }, - "test\\0\\0\\9\\55": { + "test/0/0/9/55": { "name": "55", "type": "file", "mtime": "[some]", @@ -609,7 +609,7 @@ expression: snap Id("63d9f467b844e7d5a48120b236408e381b160c9c8355b0e3113cd407ad694ab4"), ]), }, - "test\\0\\0\\9\\56": { + "test/0/0/9/56": { "name": "56", "type": "file", "mtime": "[some]", @@ -620,7 +620,7 @@ expression: snap Id("c93bcbbab1c234a01574f913bc49cc2857ccc17e4d5d8c9dc9b9b720729ed3f7"), ]), }, - "test\\0\\0\\9\\57": { + "test/0/0/9/57": { "name": "57", "type": "file", "mtime": "[some]", @@ -631,7 +631,7 @@ expression: snap Id("86f85e1d9af7cac33a60fae72c05f0f410ec0c9dfd917ad18a7e2308a272e1fb"), ]), }, - "test\\0\\0\\9\\58": { + "test/0/0/9/58": { "name": "58", "type": "file", "mtime": "[some]", @@ -642,7 +642,7 @@ expression: snap Id("858f7dc0f2b2e7532fec3b2a2daa551855ebaf27e29b60a90807e3f770d60815"), ]), }, - "test\\0\\0\\9\\59": { + "test/0/0/9/59": { "name": "59", "type": "file", "mtime": "[some]", @@ -653,7 +653,7 @@ expression: snap Id("6047339cfc27b694b82bfc1621888997bf0e954bc5459a7ed51c424d7bd92583"), ]), }, - "test\\0\\0\\9\\6": { + "test/0/0/9/6": { "name": "6", "type": "file", "mtime": "[some]", @@ -664,7 +664,7 @@ expression: snap Id("f6eca86a06925045b3b3a09f38999144ace49570b7e3a203ca7f24644fe35f20"), ]), }, - "test\\0\\0\\9\\60": { + "test/0/0/9/60": { "name": "60", "type": "file", "mtime": "[some]", @@ -675,7 +675,7 @@ expression: snap Id("fcd9ec0c99f7992c184666e3040831b919f3375157bd563a2b65cde1c6789847"), ]), }, - "test\\0\\0\\9\\61": { + "test/0/0/9/61": { "name": "61", "type": "file", "mtime": "[some]", @@ -686,7 +686,7 @@ expression: snap Id("f5738472e3ab1372588f9048c3198bb9e05338acc94c33aca3c34277c675116c"), ]), }, - "test\\0\\0\\9\\62": { + "test/0/0/9/62": { "name": "62", "type": "file", "mtime": "[some]", @@ -697,7 +697,7 @@ expression: snap Id("7d07ba1cacc97dc5ae31b7e49a8926bec040102f1211fda9e98f0ea70a6aecf9"), ]), }, - "test\\0\\0\\9\\63": { + "test/0/0/9/63": { "name": "63", "type": "file", "mtime": "[some]", @@ -708,7 +708,7 @@ expression: snap Id("c8d553f609367281f390160dd77113ca952dbb7a7fd073daa2ac4620a63148b4"), ]), }, - "test\\0\\0\\9\\64": { + "test/0/0/9/64": { "name": "64", "type": "file", "mtime": "[some]", @@ -719,7 +719,7 @@ expression: snap Id("5d2c26fc0563f43d161b5abc982af18696d7178ad1f7aeeae940c595415cc6df"), ]), }, - "test\\0\\0\\9\\65": { + "test/0/0/9/65": { "name": "65", "type": "file", "mtime": "[some]", @@ -730,7 +730,7 @@ expression: snap Id("2888d521727e1ba03471bcea1d48927818b54d42331caef8115fac5be94406b6"), ]), }, - "test\\0\\0\\9\\66": { + "test/0/0/9/66": { "name": "66", "type": "file", "mtime": "[some]", @@ -741,7 +741,7 @@ expression: snap Id("a9ba60dcf9b908c3488d03d2f3ecf9aff2fa1b419f30f3d938b2eb3c319566cc"), ]), }, - "test\\0\\0\\9\\67": { + "test/0/0/9/67": { "name": "67", "type": "file", "mtime": "[some]", @@ -752,7 +752,7 @@ expression: snap Id("7246d1d67000b94ac3e1928fcef3804263156e72aeb886b00b3f59ec1090c427"), ]), }, - "test\\0\\0\\9\\68": { + "test/0/0/9/68": { "name": "68", "type": "file", "mtime": "[some]", @@ -763,7 +763,7 @@ expression: snap Id("a8fafe2f1d514d1efc327d76fcc33916c20acd6838f524848a259de752862c41"), ]), }, - "test\\0\\0\\9\\7": { + "test/0/0/9/7": { "name": "7", "type": "file", "mtime": "[some]", @@ -774,7 +774,7 @@ expression: snap Id("6a1da85d5dccfdefb75bb607e1f3ded8f6068085a7359ceb41a1b9930d9442f3"), ]), }, - "test\\0\\0\\9\\8": { + "test/0/0/9/8": { "name": "8", "type": "file", "mtime": "[some]", @@ -785,7 +785,7 @@ expression: snap Id("105068aefc2d42cce9acc224bb369002f53f0036da394632f4623170f3b0277e"), ]), }, - "test\\0\\0\\9\\9": { + "test/0/0/9/9": { "name": "9", "type": "file", "mtime": "[some]", @@ -796,7 +796,7 @@ expression: snap Id("656039f259149cf97af18fb11ebee3194cfaa059ff72d6bc9a2b91b0889d65cc"), ]), }, - "test\\0\\tests": { + "test/0/tests": { "name": "tests", "type": "dir", "mtime": "[some]", @@ -805,7 +805,7 @@ expression: snap "content": None, "subtree": "[some]", }, - "test\\0\\tests\\empty-file": { + "test/0/tests/empty-file": { "name": "empty-file", "type": "file", "mtime": "[some]", @@ -813,7 +813,7 @@ expression: snap "ctime": "[some]", "content": Some([]), }, - "test\\0\\tests\\testfile": { + "test/0/tests/testfile": { "name": "testfile", "type": "file", "mtime": "[some]", @@ -824,7 +824,7 @@ expression: snap Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), ]), }, - "test\\0\\tests\\testfile-hardlink": { + "test/0/tests/testfile-hardlink": { "name": "testfile-hardlink", "type": "file", "mtime": "[some]", @@ -835,7 +835,7 @@ expression: snap Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), ]), }, - "test\\0\\tests\\testfile-symlink": { + "test/0/tests/testfile-symlink": { "name": "testfile-symlink", "type": "symlink", "linktarget": "testfile", diff --git a/crates/core/tests/snapshots/integration__windows.snap b/crates/core/tests/snapshots/integration__windows.snap deleted file mode 100644 index 2e93393b..00000000 --- a/crates/core/tests/snapshots/integration__windows.snap +++ /dev/null @@ -1,847 +0,0 @@ ---- -source: crates/core/tests/integration.rs -expression: snap ---- -{ - "test": { - "name": "test", - "type": "dir", - "content": None, - "subtree": "[some]", - }, - "test\\0": { - "name": "0", - "type": "dir", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "content": None, - "subtree": "[some]", - }, - "test\\0\\0": { - "name": "0", - "type": "dir", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "content": None, - "subtree": "[some]", - }, - "test\\0\\0\\9": { - "name": "9", - "type": "dir", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "content": None, - "subtree": "[some]", - }, - "test\\0\\0\\9\\0": { - "name": "0", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("aa6da76dc4b3bd8d8cd9d42c5ed6db2e9c3e43cecef7e7ee925a3accab96ad79"), - ]), - }, - "test\\0\\0\\9\\1": { - "name": "1", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("bd15c2d9117aa305a0cf99c9cc30359b66ee454b00b897fb1a925c56025f4556"), - ]), - }, - "test\\0\\0\\9\\10": { - "name": "10", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("753d116d34c772059a9a4a2ca33f6ea9ac618470acc13687d08fff7eb5ad3c72"), - ]), - }, - "test\\0\\0\\9\\11": { - "name": "11", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("efe42dd90fd602df1c5b3b7675cd845e23befba069eac0222a9997fea2a9852f"), - ]), - }, - "test\\0\\0\\9\\12": { - "name": "12", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("032ad465747269b41dbdfc87accad1fd49e8dc574ae4bfaf49f036150ec92e57"), - ]), - }, - "test\\0\\0\\9\\13": { - "name": "13", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b0b5afc29b6391722bd60a9a74c386c856cb130a219954f34944a4f19bbecc41"), - ]), - }, - "test\\0\\0\\9\\14": { - "name": "14", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b19a49a2e711d5547e1fb247170c58d2da17f9b0bbf75eb84507097a301f1c6d"), - ]), - }, - "test\\0\\0\\9\\15": { - "name": "15", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f11e72e0290dc75261ee48c39fd42597bdcd80e110ffd0651f71d54ea22030e5"), - ]), - }, - "test\\0\\0\\9\\16": { - "name": "16", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("71935a6f85198c65584ed5c4ff5e34cf93cb79756023e82ecc85153663be2b00"), - ]), - }, - "test\\0\\0\\9\\17": { - "name": "17", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b81f6dd6ad54fbed57ffac74bcecf9f1a56ce8948edc92153ee34ed415ed68d1"), - ]), - }, - "test\\0\\0\\9\\18": { - "name": "18", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("1fad45c4bf21695fae9e100626f42d35d8c2c48cb90f6a6256973c262608879d"), - ]), - }, - "test\\0\\0\\9\\19": { - "name": "19", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("92c4aa4fa67ede99b9ab1b694bc32b4f3d127f406a7e6583ecbe55b1bb919928"), - ]), - }, - "test\\0\\0\\9\\2": { - "name": "2", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f5a3517234ef2f213afe617cdab9d25364e1c8c9e61198bf387b157542ee7dbb"), - ]), - }, - "test\\0\\0\\9\\20": { - "name": "20", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("737c19313675671c589bfc613b0b736b95a58218486d09d3a7a649f324d81ef7"), - ]), - }, - "test\\0\\0\\9\\21": { - "name": "21", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("ee25130a4e74b1306087799914d8b92aed2c385b06256705b18266ff088524f4"), - ]), - }, - "test\\0\\0\\9\\22": { - "name": "22", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("3492c28f8ddbb9186e483ef9eb1cc7042c5adf871a4d15d2edf8ee7b2b04069b"), - ]), - }, - "test\\0\\0\\9\\23": { - "name": "23", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b321dedc16124312ed08cb7f9c67006bd6a4f946ca04bc178323e6147b081943"), - ]), - }, - "test\\0\\0\\9\\24": { - "name": "24", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("7020db85ffe6a3fd5f34971ff37a20fa5d32947181692bfbcca62cfc4a9221d0"), - ]), - }, - "test\\0\\0\\9\\25": { - "name": "25", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("75e7274ef54e91c2857ba0de4c869388df3f9c657d776a3e90eaf3f3f619536d"), - ]), - }, - "test\\0\\0\\9\\26": { - "name": "26", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("418b691988d9bc7357b99c5baf31e83436315d1bcc887482dd83a8537183af17"), - ]), - }, - "test\\0\\0\\9\\27": { - "name": "27", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("06a5749af5715af19fde59d1caaa542df00c56aa0058bf99b67dec5216c27df6"), - ]), - }, - "test\\0\\0\\9\\28": { - "name": "28", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("61e8ffda5a11d743d7dc4bcfd1e18e8b6179a53f8b28b25adf55d3acb0b10eff"), - ]), - }, - "test\\0\\0\\9\\29": { - "name": "29", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("60c1c2f3a0b8e5b1c4e2b7f1453661170395c5c26f2e2d241e81eb929cfc930f"), - ]), - }, - "test\\0\\0\\9\\3": { - "name": "3", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f0ad279385b5b5c862e1e2fdd599616115729e0b8085833d7b31de68c8326b85"), - ]), - }, - "test\\0\\0\\9\\30": { - "name": "30", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("790c34f1d4d95a6067f2406a78aa0dcca2fc2aa1db932fbd865ce9cafd7baa23"), - ]), - }, - "test\\0\\0\\9\\31": { - "name": "31", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("8e53f45db9249373828c588774471d34c9c1e436b83b1603a49d675de810db7e"), - ]), - }, - "test\\0\\0\\9\\32": { - "name": "32", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("c4eccf95ba2409d21283981297bf9919ec15c2cc0dadef84aafaad49a0c79130"), - ]), - }, - "test\\0\\0\\9\\33": { - "name": "33", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("da8b8b2d79bba6a3d4784233e4421293cea0cf27fd03521e11ce0c7c941f6dbb"), - ]), - }, - "test\\0\\0\\9\\34": { - "name": "34", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("a1413ef1018e7a4f3df7f19aad942a611cbda5bdd45d4269cb54d5f9fa58cd01"), - ]), - }, - "test\\0\\0\\9\\35": { - "name": "35", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f534a7fe839d440a0fdd1d45254921601b2a90ff5b61edcdc631b722f84a0dbb"), - ]), - }, - "test\\0\\0\\9\\36": { - "name": "36", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("d56e2a8c47f4ae36886784c852916f36036f3daffa547a5922dda5814ecc5a86"), - ]), - }, - "test\\0\\0\\9\\37": { - "name": "37", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("5800df73dcfdbd261800f2474c8631fe0727fbc1788cfd70bd206ab2f50b0707"), - ]), - }, - "test\\0\\0\\9\\38": { - "name": "38", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("e7c987bd8d9257bea1e44f882bc075c84ee7f103d68eec8677e37b49d2854d90"), - ]), - }, - "test\\0\\0\\9\\39": { - "name": "39", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("03a1411035add5b10ed183b2ca7d8db8deee2402f45ee445f54de50896c3e26f"), - ]), - }, - "test\\0\\0\\9\\4": { - "name": "4", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("917aae16764fc905abf0514e965133ed80e2b66c426fb09be24eabd452d4438d"), - ]), - }, - "test\\0\\0\\9\\40": { - "name": "40", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("1adcd19cf770f7d8ab3bb4b3996fb6187f59c468953848ec4f119c9cef484f28"), - ]), - }, - "test\\0\\0\\9\\41": { - "name": "41", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b5debda793f22adb93204dc81f107d03636b7431192fc8497f5c9a340e43460c"), - ]), - }, - "test\\0\\0\\9\\42": { - "name": "42", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("e6e9b35f3ca77afbd0043bc42085b814ceea8cfb93a0bcba9696dee5034ffead"), - ]), - }, - "test\\0\\0\\9\\43": { - "name": "43", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("31d87fd77a88ca00e9c5612f578c0b3243347ba328a6daa09742fdc8c7cec6f4"), - ]), - }, - "test\\0\\0\\9\\44": { - "name": "44", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("76aec0f2d829d01eff332099ebd5628f61adc72ff44e7d6b82beabc28fa44f1f"), - ]), - }, - "test\\0\\0\\9\\45": { - "name": "45", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("121242341ff94a05074a31cb2a0f566093e5c052e64dcde4c0c9b686e58daf87"), - ]), - }, - "test\\0\\0\\9\\46": { - "name": "46", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("a69efe0ab923672b5aa1a2113f54e80684a7c126c1114af4fac3953be18bea74"), - ]), - }, - "test\\0\\0\\9\\47": { - "name": "47", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("a50a7ab010d0d349b213576d1e2d2d2b345334be1be6f62798d250fc1e5cc51f"), - ]), - }, - "test\\0\\0\\9\\48": { - "name": "48", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("32715f839edd796c5b6f2d48c88748857ed439867c2dc4b2b65c5f96c6d9ef73"), - ]), - }, - "test\\0\\0\\9\\49": { - "name": "49", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("d3fdbaa72299c1abfe47bc26bdf201cc5cb66efc031e517111d413c38bfea882"), - ]), - }, - "test\\0\\0\\9\\5": { - "name": "5", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("de68adab4fe82b31fe5b14abe5d17faecf4f7c8d93d74374cbf3793701290ede"), - ]), - }, - "test\\0\\0\\9\\50": { - "name": "50", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("939e25879149a8b87ecb6b44ded96f6b029d5463d38fe5525005984d6d42c34a"), - ]), - }, - "test\\0\\0\\9\\51": { - "name": "51", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("958a554f813d199f9f897c9f5c4d90571dfe7f67d8a3733a64598906a8e7208d"), - ]), - }, - "test\\0\\0\\9\\52": { - "name": "52", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("2e8799330876d03e95428975593175ea5dd75ed3d5f3d83b4d9115dd2c6d3b71"), - ]), - }, - "test\\0\\0\\9\\53": { - "name": "53", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f476577492f325d8e617814bde3ecc83eb9e8662023d2e057c6c958ce77511e5"), - ]), - }, - "test\\0\\0\\9\\54": { - "name": "54", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("b03ca92f97233541fcc95c22455cadf63c5a25089117af4b433ffc1ed9a8991f"), - ]), - }, - "test\\0\\0\\9\\55": { - "name": "55", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("63d9f467b844e7d5a48120b236408e381b160c9c8355b0e3113cd407ad694ab4"), - ]), - }, - "test\\0\\0\\9\\56": { - "name": "56", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("c93bcbbab1c234a01574f913bc49cc2857ccc17e4d5d8c9dc9b9b720729ed3f7"), - ]), - }, - "test\\0\\0\\9\\57": { - "name": "57", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("86f85e1d9af7cac33a60fae72c05f0f410ec0c9dfd917ad18a7e2308a272e1fb"), - ]), - }, - "test\\0\\0\\9\\58": { - "name": "58", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("858f7dc0f2b2e7532fec3b2a2daa551855ebaf27e29b60a90807e3f770d60815"), - ]), - }, - "test\\0\\0\\9\\59": { - "name": "59", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("6047339cfc27b694b82bfc1621888997bf0e954bc5459a7ed51c424d7bd92583"), - ]), - }, - "test\\0\\0\\9\\6": { - "name": "6", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f6eca86a06925045b3b3a09f38999144ace49570b7e3a203ca7f24644fe35f20"), - ]), - }, - "test\\0\\0\\9\\60": { - "name": "60", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("fcd9ec0c99f7992c184666e3040831b919f3375157bd563a2b65cde1c6789847"), - ]), - }, - "test\\0\\0\\9\\61": { - "name": "61", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("f5738472e3ab1372588f9048c3198bb9e05338acc94c33aca3c34277c675116c"), - ]), - }, - "test\\0\\0\\9\\62": { - "name": "62", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("7d07ba1cacc97dc5ae31b7e49a8926bec040102f1211fda9e98f0ea70a6aecf9"), - ]), - }, - "test\\0\\0\\9\\63": { - "name": "63", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("c8d553f609367281f390160dd77113ca952dbb7a7fd073daa2ac4620a63148b4"), - ]), - }, - "test\\0\\0\\9\\64": { - "name": "64", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("5d2c26fc0563f43d161b5abc982af18696d7178ad1f7aeeae940c595415cc6df"), - ]), - }, - "test\\0\\0\\9\\65": { - "name": "65", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("2888d521727e1ba03471bcea1d48927818b54d42331caef8115fac5be94406b6"), - ]), - }, - "test\\0\\0\\9\\66": { - "name": "66", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("a9ba60dcf9b908c3488d03d2f3ecf9aff2fa1b419f30f3d938b2eb3c319566cc"), - ]), - }, - "test\\0\\0\\9\\67": { - "name": "67", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("7246d1d67000b94ac3e1928fcef3804263156e72aeb886b00b3f59ec1090c427"), - ]), - }, - "test\\0\\0\\9\\68": { - "name": "68", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 11520, - "content": Some([ - Id("a8fafe2f1d514d1efc327d76fcc33916c20acd6838f524848a259de752862c41"), - ]), - }, - "test\\0\\0\\9\\7": { - "name": "7", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("6a1da85d5dccfdefb75bb607e1f3ded8f6068085a7359ceb41a1b9930d9442f3"), - ]), - }, - "test\\0\\0\\9\\8": { - "name": "8", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("105068aefc2d42cce9acc224bb369002f53f0036da394632f4623170f3b0277e"), - ]), - }, - "test\\0\\0\\9\\9": { - "name": "9", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 16384, - "content": Some([ - Id("656039f259149cf97af18fb11ebee3194cfaa059ff72d6bc9a2b91b0889d65cc"), - ]), - }, - "test\\0\\tests": { - "name": "tests", - "type": "dir", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "content": None, - "subtree": "[some]", - }, - "test\\0\\tests\\empty-file": { - "name": "empty-file", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "content": Some([]), - }, - "test\\0\\tests\\testfile": { - "name": "testfile", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 21, - "content": Some([ - Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), - ]), - }, - "test\\0\\tests\\testfile-hardlink": { - "name": "testfile-hardlink", - "type": "file", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 21, - "content": Some([ - Id("649b8b471e7d7bc175eec758a7006ac693c434c8297c07db15286788c837154a"), - ]), - }, - "test\\0\\tests\\testfile-symlink": { - "name": "testfile-symlink", - "type": "symlink", - "linktarget": "testfile", - "mtime": "[some]", - "atime": "[some]", - "ctime": "[some]", - "size": 8, - "content": None, - }, -} diff --git a/examples/find/Cargo.toml b/examples/find/Cargo.toml index adee4527..35b806ae 100644 --- a/examples/find/Cargo.toml +++ b/examples/find/Cargo.toml @@ -12,6 +12,7 @@ globset = "0.4.16" 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..060847b8 100644 --- a/examples/find/examples/find.rs +++ b/examples/find/examples/find.rs @@ -1,10 +1,12 @@ //! `ls` example use globset::Glob; use rustic_backend::BackendOptions; -use rustic_core::{FindMatches, Repository, RepositoryOptions}; +use rustic_core::{util::u8_to_path, FindMatches, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; +// don't warn about try_from paths conversion on unix +#[allow(clippy::unnecessary_fallible_conversions)] fn main() -> Result<(), Box> { // Display info logs let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); @@ -30,7 +32,7 @@ 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(u8_to_path(path)))?; for (snap, matches) in snapshots.iter().zip(matches) { println!("results in {snap:?}"); for (path_idx, node_idx) in matches {