diff --git a/changelog/new.txt b/changelog/new.txt index e7a2c72..9af438a 100644 --- a/changelog/new.txt +++ b/changelog/new.txt @@ -6,6 +6,7 @@ Bugs fixed: - prune did abort when no time was set for a pack-do-delete. This case is now handled correctly. - retrying backend access didn't work for long operations. This has been fixed (and retries are now customizable) - The zstd compression library led to data corruption in very unlikely cases. This has been fixed by a dependency update. +- Non-unicode link targets are now correctly handled on Unix (after this has been added to the restic repo format). New features: - New global configuration paths are available, located at /etc/rustic/*.toml or %PROGRAMDATA%/rustic/config/*.toml, depending on your platform. diff --git a/crates/rustic_core/src/backend/ignore.rs b/crates/rustic_core/src/backend/ignore.rs index 1d37ad7..2b9d6b7 100644 --- a/crates/rustic_core/src/backend/ignore.rs +++ b/crates/rustic_core/src/backend/ignore.rs @@ -313,15 +313,7 @@ fn map_entry( Node::new_node(name, NodeType::Dir, meta) } else if m.is_symlink() { let target = read_link(entry.path()).map_err(IgnoreErrorKind::FromIoError)?; - let node_type = NodeType::Symlink { - linktarget: target - .to_str() - .ok_or(IgnoreErrorKind::TargetIsNotValidUnicode { - file: entry.path().to_path_buf(), - target: target.clone(), - })? - .to_string(), - }; + let node_type = NodeType::from_link(&target); Node::new_node(name, node_type, meta) } else { Node::new_node(name, NodeType::File, meta) @@ -441,15 +433,7 @@ fn map_entry( Node::new_node(name, NodeType::Dir, meta) } else if m.is_symlink() { let target = read_link(entry.path()).map_err(IgnoreErrorKind::FromIoError)?; - let node_type = NodeType::Symlink { - linktarget: target - .to_str() - .ok_or_else(|| IgnoreErrorKind::TargetIsNotValidUnicode { - file: entry.path().to_path_buf(), - target: target.clone(), - })? - .to_string(), - }; + let node_type = NodeType::from_link(&target); Node::new_node(name, node_type, meta) } else if filetype.is_block_device() { let node_type = NodeType::Dev { device: m.rdev() }; diff --git a/crates/rustic_core/src/backend/local.rs b/crates/rustic_core/src/backend/local.rs index 913288f..245b9b2 100644 --- a/crates/rustic_core/src/backend/local.rs +++ b/crates/rustic_core/src/backend/local.rs @@ -488,12 +488,14 @@ impl LocalDestination { let filename = self.path(item); match &node.node_type { - NodeType::Symlink { linktarget } => symlink(linktarget.clone(), filename.clone()) - .map_err(|err| LocalErrorKind::SymlinkingFailed { - linktarget: linktarget.to_string(), + NodeType::Symlink { .. } => { + let linktarget = node.node_type.to_link(); + symlink(linktarget, &filename).map_err(|err| LocalErrorKind::SymlinkingFailed { + linktarget: linktarget.to_path_buf(), filename, source: err, - })?, + })?; + } NodeType::Dev { device } => { #[cfg(not(any( target_os = "macos", diff --git a/crates/rustic_core/src/backend/node.rs b/crates/rustic_core/src/backend/node.rs index e50a5ad..7797580 100644 --- a/crates/rustic_core/src/backend/node.rs +++ b/crates/rustic_core/src/backend/node.rs @@ -2,6 +2,7 @@ use std::{ cmp::Ordering, ffi::{OsStr, OsString}, fmt::Debug, + path::Path, str::FromStr, }; @@ -17,9 +18,11 @@ use chrono::{DateTime, Local}; use derive_more::{Constructor, IsVariant}; use serde::{Deserialize, Deserializer, Serialize}; use serde_aux::prelude::*; -use serde_with::base64::{Base64, Standard}; -use serde_with::formats::Padded; -use serde_with::{DeserializeAs, SerializeAs}; +use serde_with::{ + base64::{Base64, Standard}, + formats::Padded, + serde_as, DeserializeAs, SerializeAs, +}; #[cfg(not(windows))] use crate::error::NodeErrorKind; @@ -39,6 +42,7 @@ pub struct Node { pub subtree: Option, } +#[serde_as] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, IsVariant)] #[serde(tag = "type", rename_all = "lowercase")] pub enum NodeType { @@ -46,6 +50,9 @@ pub enum NodeType { Dir, Symlink { linktarget: String, + #[serde_as(as = "Option")] + #[serde(default, skip_serializing_if = "Option::is_none")] + linktarget_raw: Option>, }, Dev { #[serde(default)] @@ -59,6 +66,60 @@ pub enum NodeType { Socket, } +impl NodeType { + #[cfg(not(windows))] + pub fn from_link(target: &Path) -> 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()), + ) + }, + |t| (t.to_string(), None), + ); + Self::Symlink { + linktarget, + linktarget_raw, + } + } + + #[cfg(windows)] + // Windows doen't support non-unicode link targets, so we assume unicode here. + // TODO: Test and check this! + 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! + #[cfg(not(windows))] + pub fn to_link(&self) -> &Path { + match self { + Self::Symlink { + linktarget, + linktarget_raw, + } => linktarget_raw.as_ref().map_or_else( + || Path::new(linktarget), + |t| Path::new(OsStr::from_bytes(t)), + ), + _ => panic!("called method to_link on non-symlink!"), + } + } + + // Must be only called on NodeType::Symlink! + // TODO: Implement non-unicode link targets correctly for windows + #[cfg(windows)] + pub fn to_link(&self) -> &Path { + match self { + Self::Symlink { linktarget, .. } => Path::new(linktarget), + _ => panic!("called method to_link on non-symlink!"), + } + } +} + impl Default for NodeType { fn default() -> Self { Self::File @@ -136,9 +197,9 @@ impl Node { pub const fn is_special(&self) -> bool { matches!( self.node_type, - NodeType::Symlink { linktarget: _ } - | NodeType::Dev { device: _ } - | NodeType::Chardev { device: _ } + NodeType::Symlink { .. } + | NodeType::Dev { .. } + | NodeType::Chardev { .. } | NodeType::Fifo | NodeType::Socket ) @@ -356,4 +417,10 @@ mod tests { let expected = OsStr::from_bytes(expected); assert_eq!(expected, unescape_filename(input).unwrap()); } + + #[quickcheck] + 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() + } } diff --git a/crates/rustic_core/src/commands/restore.rs b/crates/rustic_core/src/commands/restore.rs index 6d6dd29..0f4cbfd 100644 --- a/crates/rustic_core/src/commands/restore.rs +++ b/crates/rustic_core/src/commands/restore.rs @@ -225,17 +225,7 @@ impl RestoreOpts { // process existing node if (node.is_dir() && !dst.file_type().unwrap().is_dir()) || (node.is_file() && !dst.metadata().unwrap().is_file()) - || { - let this = &node; - matches!( - this.node_type, - NodeType::Symlink { linktarget: _ } - | NodeType::Dev { device: _ } - | NodeType::Chardev { device: _ } - | NodeType::Fifo - | NodeType::Socket - ) - } + || node.is_special() { // if types do not match, first remove the existing file process_existing(dst)?; diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs index 3e6926d..db7b398 100644 --- a/crates/rustic_core/src/error.rs +++ b/crates/rustic_core/src/error.rs @@ -669,7 +669,7 @@ pub enum LocalErrorKind { /// failed to symlink target {linktarget:?} from {filename:?} with {source:?} #[cfg(not(any(windows, target_os = "openbsd")))] SymlinkingFailed { - linktarget: String, + linktarget: PathBuf, filename: PathBuf, #[source] source: std::io::Error, diff --git a/src/commands/diff.rs b/src/commands/diff.rs index 8f1b73e..38edeaf 100644 --- a/src/commands/diff.rs +++ b/src/commands/diff.rs @@ -194,14 +194,9 @@ fn diff( NodeType::File if metadata && node1.meta != node2.meta => { println!("U {path:?}"); } - NodeType::Symlink { linktarget } => { - if let NodeType::Symlink { - linktarget: linktarget2, - } = &node2.node_type - { - if *linktarget != *linktarget2 { - println!("U {path:?}"); - } + NodeType::Symlink { .. } => { + if node1.node_type.to_link() != node1.node_type.to_link() { + println!("U {path:?}"); } } _ => {} // no difference to show diff --git a/src/commands/ls.rs b/src/commands/ls.rs index de26d5e..f847c39 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -137,8 +137,8 @@ fn print_node(node: &Node, path: &Path) { .mtime .map(|t| t.format("%_d %b %H:%M").to_string()) .unwrap_or_else(|| "?".to_string()), - if let NodeType::Symlink { linktarget } = &node.node_type { - ["->", linktarget].join(" ") + if let NodeType::Symlink { .. } = &node.node_type { + ["->", &node.node_type.to_link().to_string_lossy()].join(" ") } else { String::new() }