use std::collections::HashSet;
use std::fs::Metadata;
#[cfg(windows)]
use std::path::Path;
#[derive(Debug, Default)]
pub struct HardlinkTracker {
seen: HashSet<InodeKey>,
}
impl HardlinkTracker {
#[must_use]
pub fn new() -> Self {
Self {
seen: HashSet::new(),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
seen: HashSet::with_capacity(capacity),
}
}
pub fn is_hardlink(&mut self, metadata: &Metadata) -> bool {
if let Some(key) = InodeKey::from_metadata(metadata) {
if self.seen.contains(&key) {
return true;
}
self.seen.insert(key);
}
false
}
#[must_use]
pub fn check_hardlink(&self, metadata: &Metadata) -> Option<bool> {
InodeKey::from_metadata(metadata).map(|key| self.seen.contains(&key))
}
pub fn record(&mut self, metadata: &Metadata) -> bool {
if let Some(key) = InodeKey::from_metadata(metadata) {
return self.seen.insert(key);
}
false
}
#[must_use]
pub fn seen_count(&self) -> usize {
self.seen.len()
}
pub fn clear(&mut self) {
self.seen.clear();
}
#[must_use]
pub const fn is_supported() -> bool {
cfg!(unix)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct InodeKey {
#[cfg(unix)]
dev: u64,
#[cfg(unix)]
ino: u64,
#[cfg(windows)]
volume_serial: u32,
#[cfg(windows)]
file_index: u64,
#[cfg(not(any(unix, windows)))]
_phantom: (),
}
impl InodeKey {
#[cfg(unix)]
fn from_metadata(metadata: &Metadata) -> Option<Self> {
use std::os::unix::fs::MetadataExt;
Some(Self {
dev: metadata.dev(),
ino: metadata.ino(),
})
}
#[cfg(windows)]
fn from_metadata(_metadata: &Metadata) -> Option<Self> {
None
}
#[cfg(not(any(unix, windows)))]
fn from_metadata(_metadata: &Metadata) -> Option<Self> {
None
}
}
#[cfg(windows)]
#[allow(dead_code)]
fn get_inode_key_from_path(path: &Path) -> Option<InodeKey> {
let _ = path;
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let path = dir.path().join(name);
let mut file = File::create(&path).unwrap();
writeln!(file, "{}", content).unwrap();
path
}
#[test]
fn test_tracker_new() {
let tracker = HardlinkTracker::new();
assert_eq!(tracker.seen_count(), 0);
}
#[test]
fn test_tracker_with_capacity() {
let tracker = HardlinkTracker::with_capacity(100);
assert_eq!(tracker.seen_count(), 0);
}
#[test]
fn test_tracker_clear() {
let dir = TempDir::new().unwrap();
let path = create_test_file(&dir, "test.txt", "content");
let metadata = std::fs::metadata(&path).unwrap();
let mut tracker = HardlinkTracker::new();
tracker.is_hardlink(&metadata);
if HardlinkTracker::is_supported() {
assert!(tracker.seen_count() > 0);
} else {
assert_eq!(tracker.seen_count(), 0);
}
tracker.clear();
assert_eq!(tracker.seen_count(), 0);
}
#[test]
fn test_same_file_not_hardlink() {
let dir = TempDir::new().unwrap();
let path = create_test_file(&dir, "test.txt", "content");
let metadata = std::fs::metadata(&path).unwrap();
let mut tracker = HardlinkTracker::new();
assert!(!tracker.is_hardlink(&metadata));
}
#[test]
fn test_different_files_not_hardlinks() {
let dir = TempDir::new().unwrap();
let path1 = create_test_file(&dir, "file1.txt", "content1");
let path2 = create_test_file(&dir, "file2.txt", "content2");
let meta1 = std::fs::metadata(&path1).unwrap();
let meta2 = std::fs::metadata(&path2).unwrap();
let mut tracker = HardlinkTracker::new();
assert!(!tracker.is_hardlink(&meta1));
assert!(!tracker.is_hardlink(&meta2));
}
#[test]
#[cfg(unix)]
fn test_hardlink_detected() {
use std::fs::hard_link;
let dir = TempDir::new().unwrap();
let original = create_test_file(&dir, "original.txt", "content");
let link_path = dir.path().join("hardlink.txt");
hard_link(&original, &link_path).unwrap();
let meta_original = std::fs::metadata(&original).unwrap();
let meta_link = std::fs::metadata(&link_path).unwrap();
let mut tracker = HardlinkTracker::new();
assert!(!tracker.is_hardlink(&meta_original));
assert!(tracker.is_hardlink(&meta_link));
}
#[test]
fn test_check_hardlink_readonly() {
let dir = TempDir::new().unwrap();
let path = create_test_file(&dir, "test.txt", "content");
let metadata = std::fs::metadata(&path).unwrap();
let mut tracker = HardlinkTracker::new();
if !HardlinkTracker::is_supported() {
assert_eq!(tracker.check_hardlink(&metadata), None);
return;
}
assert_eq!(tracker.check_hardlink(&metadata), Some(false));
tracker.is_hardlink(&metadata);
assert_eq!(tracker.check_hardlink(&metadata), Some(true));
}
#[test]
fn test_record_without_check() {
let dir = TempDir::new().unwrap();
let path = create_test_file(&dir, "test.txt", "content");
let metadata = std::fs::metadata(&path).unwrap();
let mut tracker = HardlinkTracker::new();
if HardlinkTracker::is_supported() {
assert!(tracker.record(&metadata));
assert!(!tracker.record(&metadata));
} else {
assert!(!tracker.record(&metadata));
}
}
#[test]
fn test_is_supported() {
let supported = HardlinkTracker::is_supported();
#[cfg(unix)]
assert!(supported);
println!("Hardlink detection supported: {}", supported);
}
#[test]
#[cfg(unix)]
fn test_seen_count_increases() {
let dir = TempDir::new().unwrap();
let path1 = create_test_file(&dir, "file1.txt", "content1");
let path2 = create_test_file(&dir, "file2.txt", "content2");
let meta1 = std::fs::metadata(&path1).unwrap();
let meta2 = std::fs::metadata(&path2).unwrap();
let mut tracker = HardlinkTracker::new();
assert_eq!(tracker.seen_count(), 0);
tracker.is_hardlink(&meta1);
assert_eq!(tracker.seen_count(), 1);
tracker.is_hardlink(&meta2);
assert_eq!(tracker.seen_count(), 2);
tracker.is_hardlink(&meta1);
assert_eq!(tracker.seen_count(), 2);
}
#[test]
#[cfg(unix)]
fn test_multiple_hardlinks_same_inode() {
use std::fs::hard_link;
let dir = TempDir::new().unwrap();
let original = create_test_file(&dir, "original.txt", "content");
let link1 = dir.path().join("link1.txt");
let link2 = dir.path().join("link2.txt");
hard_link(&original, &link1).unwrap();
hard_link(&original, &link2).unwrap();
let meta_original = std::fs::metadata(&original).unwrap();
let meta_link1 = std::fs::metadata(&link1).unwrap();
let meta_link2 = std::fs::metadata(&link2).unwrap();
let mut tracker = HardlinkTracker::new();
assert!(!tracker.is_hardlink(&meta_original));
assert!(tracker.is_hardlink(&meta_link1));
assert!(tracker.is_hardlink(&meta_link2));
assert_eq!(tracker.seen_count(), 1);
}
}