fix(sync): S-03 — symlink detection and cycle protection
This commit is contained in:
+12
@@ -1481,6 +1481,12 @@ fn handle_sync(
|
|||||||
" • Bereits aktuell: {} Dateien (übersprungen)",
|
" • Bereits aktuell: {} Dateien (übersprungen)",
|
||||||
stats.files_skipped
|
stats.files_skipped
|
||||||
);
|
);
|
||||||
|
if stats.files_skipped_symlinks > 0 {
|
||||||
|
println!(
|
||||||
|
" • Symlinks: {} übersprungen",
|
||||||
|
stats.files_skipped_symlinks
|
||||||
|
);
|
||||||
|
}
|
||||||
if delete {
|
if delete {
|
||||||
println!(
|
println!(
|
||||||
" • Zu löschen: {} Dateien/Ordner",
|
" • Zu löschen: {} Dateien/Ordner",
|
||||||
@@ -1508,6 +1514,12 @@ fn handle_sync(
|
|||||||
" • Übersprungen: {} Dateien (bereits aktuell)",
|
" • Übersprungen: {} Dateien (bereits aktuell)",
|
||||||
stats.files_skipped
|
stats.files_skipped
|
||||||
);
|
);
|
||||||
|
if stats.files_skipped_symlinks > 0 {
|
||||||
|
println!(
|
||||||
|
" • Symlinks: {} übersprungen",
|
||||||
|
stats.files_skipped_symlinks
|
||||||
|
);
|
||||||
|
}
|
||||||
if delete {
|
if delete {
|
||||||
println!(
|
println!(
|
||||||
" • Gelöscht: {} verwaiste Dateien/Ordner",
|
" • Gelöscht: {} verwaiste Dateien/Ordner",
|
||||||
|
|||||||
+39
-2
@@ -105,6 +105,7 @@ pub struct SyncStats {
|
|||||||
pub files_scanned: usize,
|
pub files_scanned: usize,
|
||||||
pub files_transferred: usize,
|
pub files_transferred: usize,
|
||||||
pub files_skipped: usize,
|
pub files_skipped: usize,
|
||||||
|
pub files_skipped_symlinks: usize,
|
||||||
pub files_deleted: usize,
|
pub files_deleted: usize,
|
||||||
pub bytes_transferred: u64,
|
pub bytes_transferred: u64,
|
||||||
pub elapsed: Duration,
|
pub elapsed: Duration,
|
||||||
@@ -485,6 +486,21 @@ fn sync_push(
|
|||||||
target_str
|
target_str
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sym_meta = fs::symlink_metadata(local_source)?;
|
||||||
|
if sym_meta.file_type().is_symlink() {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
stats.files_skipped_symlinks += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(
|
||||||
|
" {} Symlink übersprungen: {}",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
source_str
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if local_source.is_file() {
|
if local_source.is_file() {
|
||||||
let file_name = local_source
|
let file_name = local_source
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -615,6 +631,22 @@ fn collect_and_push_dir(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S-03: Symlinks standardmäßig überspringen (Schutz vor Zyklen und Rekursion)
|
||||||
|
let sym_meta = match fs::symlink_metadata(&path) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if sym_meta.file_type().is_symlink() {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
stats.files_skipped_symlinks += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Symlink übersprungen: {}", ui::yellow("[!]"), rel_path);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
local_relative_paths.insert(rel_path.clone());
|
local_relative_paths.insert(rel_path.clone());
|
||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
@@ -1071,9 +1103,14 @@ fn delete_orphans_on_host(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_symlink = match fs::symlink_metadata(&path) {
|
||||||
|
Ok(m) => m.file_type().is_symlink(),
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
if !vault_relative_paths.contains(&rel_path) {
|
if !vault_relative_paths.contains(&rel_path) {
|
||||||
stats.files_deleted += 1;
|
stats.files_deleted += 1;
|
||||||
if path.is_dir() {
|
if path.is_dir() && !is_symlink {
|
||||||
if dry_run {
|
if dry_run {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
println!(
|
println!(
|
||||||
@@ -1104,7 +1141,7 @@ fn delete_orphans_on_host(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if path.is_dir() {
|
} else if path.is_dir() && !is_symlink {
|
||||||
delete_orphans_on_host(
|
delete_orphans_on_host(
|
||||||
base_dir,
|
base_dir,
|
||||||
&path,
|
&path,
|
||||||
|
|||||||
@@ -562,3 +562,84 @@ fn test_s02_platform_independent_path_construction_and_traversal_rejection() {
|
|||||||
// Aufräumen
|
// Aufräumen
|
||||||
let _ = fs::remove_dir_all(&temp_root);
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_s03_symlink_skipping_and_cycle_protection() {
|
||||||
|
let temp_root =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_s03_test_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let sub_dir = source_dir.join("sub");
|
||||||
|
|
||||||
|
fs::create_dir_all(&sub_dir).unwrap();
|
||||||
|
fs::write(source_dir.join("regular.txt"), b"Regular content").unwrap();
|
||||||
|
|
||||||
|
// Erzeuge eine rekursive Verknüpfung (Junction unter Windows / Symlink unter Unix)
|
||||||
|
let loop_path = sub_dir.join("cycle");
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let status = std::process::Command::new("powershell")
|
||||||
|
.args([
|
||||||
|
"-Command",
|
||||||
|
&format!(
|
||||||
|
"New-Item -ItemType Junction -Path '{}' -Target '{}'",
|
||||||
|
loop_path.display(),
|
||||||
|
source_dir.display()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
if status.is_err() || !status.unwrap().success() {
|
||||||
|
eprintln!("Junction creation skipped (not supported in current environment)");
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
if let Err(e) = std::os::unix::fs::symlink(&source_dir, &loop_path) {
|
||||||
|
eprintln!("Symlink creation skipped: {e}");
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
// Ohne S-03 Fix würde der Sync endlos in die Schleife laufen und mit Stack-Overflow abstürzen.
|
||||||
|
// Mit Fix wird der Symlink übersprungen und gezählt.
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/SafePush",
|
||||||
|
&opts,
|
||||||
|
)
|
||||||
|
.expect("Sync push must succeed despite symlink cycle");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stats.files_transferred, 1,
|
||||||
|
"Nur reguläre Datei darf übertragen werden"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
stats.files_skipped_symlinks >= 1,
|
||||||
|
"Symlink/Junction muss erkannt und in files_skipped_symlinks gezählt werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aufräumen: Unter Windows Junction vor dem remove_dir_all entfernen
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("powershell")
|
||||||
|
.args([
|
||||||
|
"-Command",
|
||||||
|
&format!("[System.IO.Directory]::Delete('{}')", loop_path.display()),
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user