mod support;
use support::wb_empty;
use sheets_diff::{compare_bytes, compare_paths, compare_readers};
const SENSITIVE_DIR: &str = "clients_confidential_do_not_leak";
fn unique_root(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"sheets-diff-privacy-{tag}-{}-{}",
std::process::id(),
tag
))
}
#[test]
fn parent_directory_does_not_survive_into_the_result() {
let root = unique_root("dir");
let dir = root.join(SENSITIVE_DIR);
std::fs::create_dir_all(&dir).unwrap();
let old_path = dir.join("old.xlsx");
let new_path = dir.join("new.xlsx");
std::fs::write(&old_path, wb_empty()).unwrap();
std::fs::write(&new_path, wb_empty()).unwrap();
let diff = compare_paths(&old_path, &new_path).unwrap();
assert_eq!(diff.old.source.display_name.as_deref(), Some("old.xlsx"));
assert_eq!(diff.new.source.display_name.as_deref(), Some("new.xlsx"));
let debug_repr = format!("{diff:?}");
let summary = sheets_diff::output::text::render_summary(&diff);
let unified = sheets_diff::output::text::render_unified(&diff);
for (label, rendered) in [
("Debug", debug_repr.as_str()),
("render_summary", summary.as_str()),
("render_unified", unified.as_str()),
] {
assert!(
!rendered.contains(SENSITIVE_DIR),
"{label} output leaked the parent directory: {rendered}"
);
assert!(
!rendered.contains(root.to_string_lossy().as_ref()),
"{label} output leaked the full temp root: {rendered}"
);
}
assert!(summary.contains("old.xlsx") && summary.contains("new.xlsx"));
assert!(unified.contains("old.xlsx") && unified.contains("new.xlsx"));
std::fs::remove_dir_all(&root).ok();
}
#[cfg(unix)]
#[test]
fn non_utf8_file_name_yields_none_display_name_without_panic() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let root = unique_root("utf8");
std::fs::create_dir_all(&root).unwrap();
let mut raw = b"bad-".to_vec();
raw.push(0xFF);
raw.extend_from_slice(b".xlsx");
let file_name = OsString::from_vec(raw);
assert!(
file_name.to_str().is_none(),
"test construction bug: the chosen byte sequence is valid UTF-8 \
after all, so this test would not exercise the non-UTF-8 path"
);
let old_path = root.join(&file_name);
let new_path = root.join("new.xlsx");
std::fs::write(&old_path, wb_empty())
.unwrap_or_else(|e| panic!("filesystem rejected a non-UTF-8 file name ({e}) -- this test cannot run on this filesystem; do not silently skip, report it"));
std::fs::write(&new_path, wb_empty()).unwrap();
let diff = compare_paths(&old_path, &new_path).unwrap();
assert_eq!(
diff.old.source.display_name, None,
"non-UTF-8 file name must yield None, not a lossy or truncated string"
);
assert_eq!(diff.new.source.display_name.as_deref(), Some("new.xlsx"));
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn open_error_on_a_nested_path_does_not_leak_the_directory() {
let root = unique_root("err");
let dir = root.join(SENSITIVE_DIR);
std::fs::create_dir_all(&dir).unwrap();
let missing = dir.join("missing.xlsx");
let new_path = dir.join("new.xlsx");
std::fs::write(&new_path, wb_empty()).unwrap();
let err = compare_paths(&missing, &new_path).unwrap_err();
let rendered = err.to_string();
assert!(
!rendered.contains(SENSITIVE_DIR),
"error message leaked the parent directory: {rendered}"
);
assert!(
!rendered.contains(root.to_string_lossy().as_ref()),
"error message leaked the full temp root: {rendered}"
);
assert!(
rendered.contains("missing.xlsx") || rendered.contains("<unknown>"),
"expected the file name or the '<unknown>' fallback, got: {rendered}"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn byte_inputs_carry_no_display_name() {
let old = wb_empty();
let new = wb_empty();
let diff = compare_bytes(&old, &new).unwrap();
assert_eq!(diff.old.source.display_name, None);
assert_eq!(diff.new.source.display_name, None);
}
#[test]
fn reader_inputs_carry_no_display_name() {
let old = std::io::Cursor::new(wb_empty());
let new = std::io::Cursor::new(wb_empty());
let diff = compare_readers(old, new).unwrap();
assert_eq!(diff.old.source.display_name, None);
assert_eq!(diff.new.source.display_name, None);
}