use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardLinkInfo {
pub target_index: usize,
}
#[derive(Debug, Default)]
pub struct HardLinkTracker {
seen_files: HashMap<FileId, usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct FileId {
device: u64,
inode: u64,
}
impl HardLinkTracker {
pub fn new() -> Self {
Self::default()
}
#[cfg(unix)]
pub fn check_file(
&mut self,
path: impl AsRef<Path>,
entry_index: usize,
) -> std::io::Result<Option<usize>> {
use std::os::unix::fs::MetadataExt;
let path = path.as_ref();
let metadata = std::fs::metadata(path)?;
if metadata.nlink() <= 1 {
return Ok(None);
}
let file_id = FileId {
device: metadata.dev(),
inode: metadata.ino(),
};
if let Some(&target_index) = self.seen_files.get(&file_id) {
Ok(Some(target_index))
} else {
self.seen_files.insert(file_id, entry_index);
Ok(None)
}
}
#[cfg(windows)]
pub fn check_file(
&mut self,
path: impl AsRef<Path>,
entry_index: usize,
) -> std::io::Result<Option<usize>> {
use std::fs::File;
use std::os::windows::io::AsRawHandle;
#[repr(C)]
#[allow(non_snake_case)]
struct BY_HANDLE_FILE_INFORMATION {
dwFileAttributes: u32,
ftCreationTime: [u32; 2],
ftLastAccessTime: [u32; 2],
ftLastWriteTime: [u32; 2],
dwVolumeSerialNumber: u32,
nFileSizeHigh: u32,
nFileSizeLow: u32,
nNumberOfLinks: u32,
nFileIndexHigh: u32,
nFileIndexLow: u32,
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetFileInformationByHandle(
hFile: *mut std::ffi::c_void,
lpFileInformation: *mut BY_HANDLE_FILE_INFORMATION,
) -> i32;
}
let path = path.as_ref();
let file = File::open(path)?;
let handle = file.as_raw_handle();
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let result = unsafe { GetFileInformationByHandle(handle as *mut _, &mut info) };
if result == 0 {
return Err(std::io::Error::last_os_error());
}
let file_index = ((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64);
let volume_serial = info.dwVolumeSerialNumber;
let file_id = FileId {
device: volume_serial as u64,
inode: file_index,
};
if file_index != 0 {
if let Some(&target_index) = self.seen_files.get(&file_id) {
return Ok(Some(target_index));
}
self.seen_files.insert(file_id, entry_index);
}
Ok(None)
}
#[cfg(not(any(unix, windows)))]
pub fn check_file(
&mut self,
_path: impl AsRef<Path>,
_entry_index: usize,
) -> std::io::Result<Option<usize>> {
Ok(None)
}
pub fn tracked_count(&self) -> usize {
self.seen_files.len()
}
pub fn clear(&mut self) {
self.seen_files.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_tracker_new() {
let tracker = HardLinkTracker::new();
assert_eq!(tracker.tracked_count(), 0);
}
#[test]
fn test_tracker_regular_file() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("regular.txt");
File::create(&file_path).unwrap();
let mut tracker = HardLinkTracker::new();
let result = tracker.check_file(&file_path, 0).unwrap();
assert!(result.is_none());
}
#[test]
#[cfg(unix)]
fn test_tracker_hard_link_detection() {
let dir = TempDir::new().unwrap();
let original = dir.path().join("original.txt");
let link = dir.path().join("link.txt");
File::create(&original).unwrap();
std::fs::hard_link(&original, &link).unwrap();
let mut tracker = HardLinkTracker::new();
let result1 = tracker.check_file(&original, 0).unwrap();
assert!(result1.is_none());
assert_eq!(tracker.tracked_count(), 1);
let result2 = tracker.check_file(&link, 1).unwrap();
assert_eq!(result2, Some(0));
}
#[test]
fn test_tracker_clear() {
let mut tracker = HardLinkTracker::new();
tracker.seen_files.insert(
FileId {
device: 1,
inode: 100,
},
0,
);
tracker.seen_files.insert(
FileId {
device: 1,
inode: 200,
},
1,
);
assert_eq!(tracker.tracked_count(), 2);
tracker.clear();
assert_eq!(tracker.tracked_count(), 0);
}
}