From 87b126497a6aad1a559e9f3e1016d0762354712c Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Mon, 12 Jun 2023 20:35:11 +0200 Subject: [PATCH] Store file id in an enum --- CHANGELOG.md | 12 ++ file-id/Cargo.toml | 10 +- file-id/bin/file_id.rs | 27 ++++ file-id/src/lib.rs | 187 +++++++++++++++++++++++---- notify-debouncer-full/src/testing.rs | 6 +- 5 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 file-id/bin/file_id.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a3897a..7c4af042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ v5 maintenance branch is on `v5_maintenance` after `5.2.0` v4 commits split out to branch `v4_maintenance` starting with `4.0.16` +## file-id 0.2.0 + +- CHANGE: switch from winapi to windows-sys [#494] +- CHANGE: turn FileId struct into an enum [#494] +- FEATURE: support for high resolution file ids on Windows using GetFileInformationByHandleEx [#494] + +[#494]: https://github.com/notify-rs/notify/pull/494 + ## notify 6.0.1 (2023-06-16) - DOCS: fix swapped debouncer-full / -mini links in the readme/crates.io [4be6bde] @@ -36,6 +44,10 @@ Newly introduced alternative debouncer with more features. [#480] - FEATURE: don't emit duplicate create events - FEATURE: don't emit `Modify` events after a `Create` event +## file-id 0.1.0 (2023-05-17) + +Utility for reading inode numbers (Linux, MacOS) and file IDs (Windows). [#480] + [#480]: https://github.com/notify-rs/notify/pull/480 ## notify 5.2.0 (2023-05-17) diff --git a/file-id/Cargo.toml b/file-id/Cargo.toml index 7aa4247d..8aa92fa6 100644 --- a/file-id/Cargo.toml +++ b/file-id/Cargo.toml @@ -2,7 +2,7 @@ name = "file-id" version = "0.1.0" rust-version = "1.60" -description = "Platform independent file id library" +description = "Utility for reading inode numbers (Linux, MacOS) and file IDs (Windows)" documentation = "https://docs.rs/notify" homepage = "https://github.com/notify-rs/notify" repository = "https://github.com/notify-rs/notify.git" @@ -14,11 +14,15 @@ authors = ["Daniel Faust "] edition = "2021" +[[bin]] +name = "file-id" +path = "bin/file_id.rs" + [dependencies] serde = { version = "1.0.89", features = ["derive"], optional = true } -[target.'cfg(windows)'.dependencies.winapi-util] -version = "0.1.5" +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.48.0", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } [dev-dependencies] tempfile = "3.2.0" diff --git a/file-id/bin/file_id.rs b/file-id/bin/file_id.rs new file mode 100644 index 00000000..9f9c900d --- /dev/null +++ b/file-id/bin/file_id.rs @@ -0,0 +1,27 @@ +use std::io; + +use file_id::FileId; + +fn main() { + let path = std::env::args().nth(1).expect("no path given"); + + print_file_id(&path); +} + +#[cfg(target_family = "unix")] +fn print_file_id(path: &str) { + print_result(file_id::get_file_id(path)); +} + +#[cfg(target_family = "windows")] +fn print_file_id(path: &str) { + print_result(file_id::get_low_res_file_id(path)); + print_result(file_id::get_high_res_file_id(path)); +} + +fn print_result(result: io::Result) { + match result { + Ok(file_id) => println!("{file_id:?}"), + Err(error) => println!("Error: {error}"), + } +} diff --git a/file-id/src/lib.rs b/file-id/src/lib.rs index e4bb18b9..5a6703fc 100644 --- a/file-id/src/lib.rs +++ b/file-id/src/lib.rs @@ -1,65 +1,198 @@ -//! A utility to read file IDs that are unique on a given device. +//! Utility for reading inode numbers (Linux, MacOS) and file ids (Windows) that uniquely identify a file on a single computer. //! -//! Modern file systems assign a unique ID to each file. On Linux and MacOS it is called an `inode number`, on Windows it is called a `file index`. -//! Together with the `device id`, a file can be identified uniquely on a device at a given time. +//! Modern file systems assign a unique ID to each file. On Linux and MacOS it is called an `inode number`, +//! on Windows it is called a `file id` or `file index`. +//! Together with the `device id` (Linux, MacOS) or the `volume serial number` (Windows), +//! a file or directory can be uniquely identified on a single computer at a given time. //! //! Keep in mind though, that IDs may be re-used at some point. //! //! ## Example //! -//! ```rust +//! ``` //! let file = tempfile::NamedTempFile::new().unwrap(); //! //! let file_id = file_id::get_file_id(file.path()).unwrap(); +//! println!("{file_id:?}"); +//! ``` +//! +//! ## Example (Windows Only) +//! +//! ```ignore +//! let file = tempfile::NamedTempFile::new().unwrap(); +//! +//! let file_id = file_id::get_low_res_file_id(file.path()).unwrap(); +//! println!("{file_id:?}"); //! +//! let file_id = file_id::get_high_res_file_id(file.path()).unwrap(); //! println!("{file_id:?}"); //! ``` -use std::{io, path::Path}; +use std::{fs, io, path::Path}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// Unique identifier of a file -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct FileId { - /// Device ID or volume serial number - pub device: u64, +pub enum FileId { + /// Inode number, available on Linux and MacOS. + #[cfg_attr(feature = "serde", serde(rename = "inode"))] + Inode { + /// Device ID + #[cfg_attr(feature = "serde", serde(rename = "device"))] + device_id: u64, + + /// Inode number + #[cfg_attr(feature = "serde", serde(rename = "inode"))] + inode_number: u64, + }, + + /// Low resolution file ID, available on Windows XP and above. + /// + /// Compared to the high resolution variant, only the lower parts of the IDs are stored. + /// + /// On Windows, the low resolution variant can be requested explicitly with the `get_low_res_file_id` function. + /// + /// Details: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle. + #[cfg_attr(feature = "serde", serde(rename = "lowres"))] + LowRes { + /// Volume serial number + #[cfg_attr(feature = "serde", serde(rename = "volume"))] + volume_serial_number: u32, - /// Inode number or file index - pub file: u64, + /// File index + #[cfg_attr(feature = "serde", serde(rename = "index"))] + file_index: u64, + }, + + /// High resolution file ID, available on Windows Vista and above. + /// + /// On Windows, the high resolution variant can be requested explicitly with the `get_high_res_file_id` function. + /// + /// Details: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getfileinformationbyhandleex. + #[cfg_attr(feature = "serde", serde(rename = "highres"))] + HighRes { + /// Volume serial number + #[cfg_attr(feature = "serde", serde(rename = "volume"))] + volume_serial_number: u64, + + /// File ID + #[cfg_attr(feature = "serde", serde(rename = "file"))] + file_id: u128, + }, } impl FileId { - pub fn new(device: u64, file: u64) -> Self { - Self { device, file } + pub fn new_inode(device_id: u64, inode_number: u64) -> Self { + FileId::Inode { + device_id, + inode_number, + } + } + + pub fn new_low_res(volume_serial_number: u32, file_index: u64) -> Self { + FileId::LowRes { + volume_serial_number, + file_index, + } + } + + pub fn new_high_res(volume_serial_number: u64, file_id: u128) -> Self { + FileId::HighRes { + volume_serial_number, + file_id, + } } } -/// Get the `FileId` for the file at `path` +/// Get the `FileId` for the file or directory at `path` #[cfg(target_family = "unix")] pub fn get_file_id(path: impl AsRef) -> io::Result { - use std::fs; use std::os::unix::fs::MetadataExt; let metadata = fs::metadata(path.as_ref())?; - Ok(FileId { - device: metadata.dev(), - file: metadata.ino(), - }) + Ok(FileId::new_inode(metadata.dev(), metadata.ino())) } -/// Get the `FileId` for the file at `path` +/// Get the `FileId` for the file or directory at `path` #[cfg(target_family = "windows")] pub fn get_file_id(path: impl AsRef) -> io::Result { - use winapi_util::{file::information, Handle}; + let file = open_file(path)?; + + unsafe { get_file_info_ex(&file).or_else(|_| get_file_info(&file)) } +} + +/// Get the `FileId` with the low resolution variant for the file or directory at `path` +#[cfg(target_family = "windows")] +pub fn get_low_res_file_id(path: impl AsRef) -> io::Result { + let file = open_file(path)?; + + unsafe { get_file_info_ex(&file) } +} + +/// Get the `FileId` with the high resolution variant for the file or directory at `path` +#[cfg(target_family = "windows")] +pub fn get_high_res_file_id(path: impl AsRef) -> io::Result { + let file = open_file(path)?; + + unsafe { get_file_info(&file) } +} + +#[cfg(target_family = "windows")] +unsafe fn get_file_info_ex(file: &fs::File) -> Result { + use std::{mem, os::windows::prelude::*}; + use windows_sys::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{FileIdInfo, GetFileInformationByHandleEx, FILE_ID_INFO}, + }; + + let mut info: FILE_ID_INFO = mem::zeroed(); + let ret = GetFileInformationByHandleEx( + file.as_raw_handle() as HANDLE, + FileIdInfo, + &mut info as *mut FILE_ID_INFO as _, + mem::size_of::() as u32, + ); - let handle = Handle::from_path_any(path.as_ref())?; - let info = information(&handle)?; + if ret == 0 { + return Err(io::Error::last_os_error()); + }; + + Ok(FileId::new_high_res( + info.VolumeSerialNumber, + u128::from_le_bytes(info.FileId.Identifier), + )) +} + +#[cfg(target_family = "windows")] +unsafe fn get_file_info(file: &fs::File) -> Result { + use std::{mem, os::windows::prelude::*}; + use windows_sys::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION}, + }; + + let mut info: BY_HANDLE_FILE_INFORMATION = mem::zeroed(); + let ret = GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info); + if ret == 0 { + return Err(io::Error::last_os_error()); + }; + + Ok(FileId::new_low_res( + info.dwVolumeSerialNumber, + ((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64), + )) +} + +#[cfg(target_family = "windows")] +fn open_file>(path: P) -> io::Result { + use std::{fs::OpenOptions, os::windows::fs::OpenOptionsExt}; + use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; - Ok(FileId { - device: info.volume_serial_number(), - file: info.file_index(), - }) + OpenOptions::new() + .read(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) } diff --git a/notify-debouncer-full/src/testing.rs b/notify-debouncer-full/src/testing.rs index 9aee394f..45ea44e5 100644 --- a/notify-debouncer-full/src/testing.rs +++ b/notify-debouncer-full/src/testing.rs @@ -239,7 +239,7 @@ impl schema::State { .into_iter() .map(|(path, id)| { let path = PathBuf::from(path); - let id = FileId::new(id, id); + let id = FileId::new_inode(id, id); (path, id) }) .collect::>(); @@ -249,7 +249,7 @@ impl schema::State { .into_iter() .map(|(path, id)| { let path = PathBuf::from(path); - let id = FileId::new(id, id); + let id = FileId::new_inode(id, id); (path, id) }) .collect::>(); @@ -257,7 +257,7 @@ impl schema::State { let cache = TestCache::new(cache, file_system); let rename_event = self.rename_event.map(|e| { - let file_id = e.file_id.map(|id| FileId::new(id, id)); + let file_id = e.file_id.map(|id| FileId::new_inode(id, id)); let event = e.into_debounced_event(time, None); (event, file_id) });