From 0172da869920b9ad8bbab1b392c9116caed3e861 Mon Sep 17 00:00:00 2001 From: lmtlssss <261705307+lmtlssss@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:27:13 -0400 Subject: [PATCH 1/2] fix(backup): report unreadable xattrs instead of silently discarding them --- crates/core/src/backend/ignore/mapper.rs | 56 +++++++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/core/src/backend/ignore/mapper.rs b/crates/core/src/backend/ignore/mapper.rs index 6bb5dcdec..9e0dd5aa7 100644 --- a/crates/core/src/backend/ignore/mapper.rs +++ b/crates/core/src/backend/ignore/mapper.rs @@ -6,7 +6,6 @@ use std::{ffi::OsStr, path::Path}; use derive_setters::Setters; use ignore::DirEntry; use jiff::Timestamp; -use log::warn; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -105,12 +104,10 @@ impl LocalSourceSaveOptions { .set_devid .unwrap_or_default() .map_or_else(|| Self::device_id(&m), Self::hardlink(&m)); - let xattr = || { - Self::xattrs(entry.path()) - .inspect_err(|err| warn!("ignoring error obtaining xargs: {err}")) - .unwrap_or_default() - }; - let extended_attributes = self.set_xattrs.unwrap_or_default().map_or_else(xattr); + let extended_attributes = Self::collect_xattrs( + self.set_xattrs.unwrap_or_default(), + || Self::xattrs(entry.path()), + )?; let (mode, inode, links) = Self::nix_infos(&m); let meta = Metadata { @@ -135,6 +132,19 @@ impl LocalSourceSaveOptions { Ok(ReadSourceEntry { path, node, open }) } + // Distinguish deliberately excluded metadata from metadata we could not + // read. Treat failed xattr reads like other failed source metadata reads, + // rather than silently producing a snapshot with missing attributes. + fn collect_xattrs( + option: XattrOption, + read: impl FnOnce() -> IgnoreResult>, + ) -> IgnoreResult> { + match option { + XattrOption::Yes => read(), + XattrOption::No => Ok(Vec::new()), + } + } + fn to_node( self, entry: &DirEntry, @@ -275,3 +285,35 @@ impl LocalSourceSaveOptions { Node::new_node(name, NodeType::File, meta) } } + +#[cfg(test)] +mod xattr_error_tests { + use super::*; + + #[test] + fn unreadable_xattrs_are_reported_instead_of_omitted() { + let result = LocalSourceSaveOptions::collect_xattrs(XattrOption::Yes, || { + Err(IgnoreErrorKind::ErrorXattr { + path: Path::new("unreadable-metadata").to_path_buf(), + source: std::io::Error::from(std::io::ErrorKind::PermissionDenied), + }) + }); + assert!(matches!(result, Err(IgnoreErrorKind::ErrorXattr { .. }))); + } + + #[test] + fn deliberate_xattr_exclusion_does_not_read_attributes() { + let result = LocalSourceSaveOptions::collect_xattrs(XattrOption::No, || { + panic!("attribute reader must not run when xattrs are disabled") + }); + assert!(result.unwrap().is_empty()); + } + + #[test] + fn readable_empty_xattrs_remain_valid() { + let result = LocalSourceSaveOptions::collect_xattrs(XattrOption::Yes, || { + Ok(Vec::new()) + }); + assert!(result.unwrap().is_empty()); + } +} From 80552e7887ef7efc7d8dd6344823d44c548ad8bc Mon Sep 17 00:00:00 2001 From: lmtlssss <261705307+lmtlssss@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:29:50 -0400 Subject: [PATCH 2/2] style: apply rustfmt to xattr error propagation tests --- crates/core/src/backend/ignore/mapper.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/core/src/backend/ignore/mapper.rs b/crates/core/src/backend/ignore/mapper.rs index 9e0dd5aa7..069e5d8ba 100644 --- a/crates/core/src/backend/ignore/mapper.rs +++ b/crates/core/src/backend/ignore/mapper.rs @@ -104,10 +104,10 @@ impl LocalSourceSaveOptions { .set_devid .unwrap_or_default() .map_or_else(|| Self::device_id(&m), Self::hardlink(&m)); - let extended_attributes = Self::collect_xattrs( - self.set_xattrs.unwrap_or_default(), - || Self::xattrs(entry.path()), - )?; + let extended_attributes = + Self::collect_xattrs(self.set_xattrs.unwrap_or_default(), || { + Self::xattrs(entry.path()) + })?; let (mode, inode, links) = Self::nix_infos(&m); let meta = Metadata { @@ -311,9 +311,7 @@ mod xattr_error_tests { #[test] fn readable_empty_xattrs_remain_valid() { - let result = LocalSourceSaveOptions::collect_xattrs(XattrOption::Yes, || { - Ok(Vec::new()) - }); + let result = LocalSourceSaveOptions::collect_xattrs(XattrOption::Yes, || Ok(Vec::new())); assert!(result.unwrap().is_empty()); } }