Skip to content

Commit e5f33f7

Browse files
authored
feat(file-id): add get_file_id_no_follow() along with low and high-res variants. (#716)
1 parent dbccc7d commit e5f33f7

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

file-id/src/lib.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
//! let file_id = file_id::get_high_res_file_id(file.path()).unwrap();
2828
//! println!("{file_id:?}");
2929
//! ```
30+
//!
31+
//! ## Example (Not Following Symlinks/Reparse Points)
32+
//!
33+
//! ```
34+
//! let file = tempfile::NamedTempFile::new().unwrap();
35+
//!
36+
//! // Get file ID without following symlinks
37+
//! let file_id = file_id::get_file_id_no_follow(file.path()).unwrap();
38+
//! println!("{file_id:?}");
39+
//! ```
3040
use std::{fs, io, path::Path};
3141

3242
#[cfg(feature = "serde")]
@@ -122,6 +132,16 @@ pub fn get_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
122132
Ok(FileId::new_inode(metadata.dev(), metadata.ino()))
123133
}
124134

135+
/// Get the `FileId` for the file or directory at `path` without following symlinks
136+
#[cfg(target_family = "unix")]
137+
pub fn get_file_id_no_follow(path: impl AsRef<Path>) -> io::Result<FileId> {
138+
use std::os::unix::fs::MetadataExt;
139+
140+
let metadata = fs::symlink_metadata(path.as_ref())?;
141+
142+
Ok(FileId::new_inode(metadata.dev(), metadata.ino()))
143+
}
144+
125145
/// Get the `FileId` for the file or directory at `path`
126146
#[cfg(target_family = "windows")]
127147
pub fn get_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
@@ -130,6 +150,14 @@ pub fn get_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
130150
unsafe { get_file_info_ex(&file).or_else(|_| get_file_info(&file)) }
131151
}
132152

153+
/// Get the `FileId` for the file or directory at `path` without following symlinks/reparse points
154+
#[cfg(target_family = "windows")]
155+
pub fn get_file_id_no_follow(path: impl AsRef<Path>) -> io::Result<FileId> {
156+
let file = open_file_no_follow(path)?;
157+
158+
unsafe { get_file_info_ex(&file).or_else(|_| get_file_info(&file)) }
159+
}
160+
133161
/// Get the `FileId` with the low resolution variant for the file or directory at `path`
134162
#[cfg(target_family = "windows")]
135163
pub fn get_low_res_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
@@ -138,6 +166,14 @@ pub fn get_low_res_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
138166
unsafe { get_file_info(&file) }
139167
}
140168

169+
/// Get the `FileId` with the low resolution variant for the file or directory at `path` without following symlinks/reparse points
170+
#[cfg(target_family = "windows")]
171+
pub fn get_low_res_file_id_no_follow(path: impl AsRef<Path>) -> io::Result<FileId> {
172+
let file = open_file_no_follow(path)?;
173+
174+
unsafe { get_file_info(&file) }
175+
}
176+
141177
/// Get the `FileId` with the high resolution variant for the file or directory at `path`
142178
#[cfg(target_family = "windows")]
143179
pub fn get_high_res_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
@@ -146,6 +182,14 @@ pub fn get_high_res_file_id(path: impl AsRef<Path>) -> io::Result<FileId> {
146182
unsafe { get_file_info_ex(&file) }
147183
}
148184

185+
/// Get the `FileId` with the high resolution variant for the file or directory at `path` without following symlinks/reparse points
186+
#[cfg(target_family = "windows")]
187+
pub fn get_high_res_file_id_no_follow(path: impl AsRef<Path>) -> io::Result<FileId> {
188+
let file = open_file_no_follow(path)?;
189+
190+
unsafe { get_file_info_ex(&file) }
191+
}
192+
149193
#[cfg(target_family = "windows")]
150194
unsafe fn get_file_info_ex(file: &fs::File) -> Result<FileId, io::Error> {
151195
use std::{mem, os::windows::prelude::*};
@@ -202,3 +246,16 @@ fn open_file<P: AsRef<Path>>(path: P) -> io::Result<fs::File> {
202246
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
203247
.open(path)
204248
}
249+
250+
#[cfg(target_family = "windows")]
251+
fn open_file_no_follow<P: AsRef<Path>>(path: P) -> io::Result<fs::File> {
252+
use std::{fs::OpenOptions, os::windows::fs::OpenOptionsExt};
253+
use windows_sys::Win32::Storage::FileSystem::{
254+
FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
255+
};
256+
257+
OpenOptions::new()
258+
.access_mode(0)
259+
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
260+
.open(path)
261+
}

file-id/tests/integration.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use file_id::{get_file_id, get_file_id_no_follow};
2+
use std::{fs, io};
3+
use tempfile::TempDir;
4+
5+
#[test]
6+
fn test_get_file_id_vs_no_follow() -> io::Result<()> {
7+
let temp_dir = TempDir::new()?;
8+
let file_path = temp_dir.path().join("test_file.txt");
9+
let symlink_path = temp_dir.path().join("test_symlink");
10+
11+
// Create a test file
12+
fs::write(&file_path, "test content")?;
13+
14+
// Create a symlink to the file
15+
#[cfg(target_family = "unix")]
16+
std::os::unix::fs::symlink(&file_path, &symlink_path)?;
17+
18+
#[cfg(target_family = "windows")]
19+
std::os::windows::fs::symlink_file(&file_path, &symlink_path)?;
20+
21+
// Get file IDs
22+
let original_file_id = get_file_id(&file_path)?;
23+
let symlink_follow_id = get_file_id(&symlink_path)?;
24+
let symlink_no_follow_id = get_file_id_no_follow(&symlink_path)?;
25+
26+
// Following the symlink should give us the same ID as the original file
27+
assert_eq!(original_file_id, symlink_follow_id);
28+
29+
// Not following the symlink should give us a different ID (the symlink's own ID)
30+
assert_ne!(original_file_id, symlink_no_follow_id);
31+
32+
Ok(())
33+
}

0 commit comments

Comments
 (0)