Skip to main content

khive_db/
file_identity.rs

1//! Physical identity of a database file, including SQLite's opened Windows handle.
2
3use std::io;
4use std::path::Path;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct DatabaseFileIdentity {
8    #[cfg(unix)]
9    device: u64,
10    #[cfg(unix)]
11    inode: u64,
12    #[cfg(windows)]
13    volume_serial: u64,
14    #[cfg(windows)]
15    file_id: [u8; 16],
16}
17
18#[cfg(unix)]
19impl DatabaseFileIdentity {
20    pub(crate) fn unix_parts(self) -> (u64, u64) {
21        (self.device, self.inode)
22    }
23}
24
25#[cfg(unix)]
26pub fn database_file_identity(path: &Path) -> io::Result<DatabaseFileIdentity> {
27    use std::os::unix::fs::MetadataExt as _;
28
29    let metadata = std::fs::metadata(path)?;
30    Ok(DatabaseFileIdentity {
31        device: metadata.dev(),
32        inode: metadata.ino(),
33    })
34}
35
36/// Identity of a file held open by an external owner, without resolving its path.
37#[cfg(unix)]
38pub fn database_file_identity_from_file(file: &std::fs::File) -> io::Result<DatabaseFileIdentity> {
39    use std::os::unix::fs::MetadataExt as _;
40
41    let metadata = file.metadata()?;
42    Ok(DatabaseFileIdentity {
43        device: metadata.dev(),
44        inode: metadata.ino(),
45    })
46}
47
48/// Number of directory entries naming the file held open by `file`, read from
49/// the descriptor itself rather than from a path.
50#[cfg(unix)]
51pub fn file_link_count(file: &std::fs::File) -> io::Result<u64> {
52    use std::os::unix::fs::MetadataExt as _;
53
54    Ok(file.metadata()?.nlink())
55}
56
57#[cfg(windows)]
58pub fn database_file_identity(path: &Path) -> io::Result<DatabaseFileIdentity> {
59    use std::os::windows::fs::OpenOptionsExt as _;
60    use std::os::windows::io::AsRawHandle as _;
61    use windows_sys::Win32::Storage::FileSystem::{
62        FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
63    };
64
65    let file = std::fs::OpenOptions::new()
66        .read(true)
67        .access_mode(FILE_READ_ATTRIBUTES)
68        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
69        .open(path)?;
70    identity_from_handle(file.as_raw_handle())
71}
72
73/// Identity of a file held open by an external owner, without resolving its path.
74#[cfg(windows)]
75pub fn database_file_identity_from_file(file: &std::fs::File) -> io::Result<DatabaseFileIdentity> {
76    use std::os::windows::io::AsRawHandle as _;
77
78    identity_from_handle(file.as_raw_handle())
79}
80
81/// Number of directory entries naming the file held open by `file`, read from
82/// the handle itself rather than from a path.
83#[cfg(windows)]
84pub fn file_link_count(file: &std::fs::File) -> io::Result<u64> {
85    use std::os::windows::io::AsRawHandle as _;
86    use windows_sys::Win32::Storage::FileSystem::{
87        GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
88    };
89
90    let mut info = BY_HANDLE_FILE_INFORMATION::default();
91    // SAFETY: the caller retains a live handle; `info` is the correctly sized
92    // output buffer. No ownership is transferred.
93    if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &raw mut info) } == 0 {
94        return Err(io::Error::last_os_error());
95    }
96    Ok(u64::from(info.nNumberOfLinks))
97}
98
99#[cfg(windows)]
100fn identity_from_handle(
101    handle: std::os::windows::io::RawHandle,
102) -> io::Result<DatabaseFileIdentity> {
103    use windows_sys::Win32::Storage::FileSystem::{
104        FileIdInfo, GetFileInformationByHandleEx, FILE_ID_INFO,
105    };
106
107    let mut info = FILE_ID_INFO::default();
108    // SAFETY: the caller retains a live handle; `info` is a writable buffer
109    // of the exact size required by FileIdInfo. No ownership is transferred.
110    let ok = unsafe {
111        GetFileInformationByHandleEx(
112            handle,
113            FileIdInfo,
114            (&raw mut info).cast(),
115            std::mem::size_of::<FILE_ID_INFO>() as u32,
116        )
117    };
118    if ok == 0 {
119        return Err(io::Error::last_os_error());
120    }
121    Ok(DatabaseFileIdentity {
122        volume_serial: info.VolumeSerialNumber,
123        file_id: info.FileId.Identifier,
124    })
125}
126
127#[cfg(windows)]
128pub fn sqlite_opened_file_identity(
129    conn: &rusqlite::Connection,
130) -> Result<DatabaseFileIdentity, crate::error::SqliteError> {
131    use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
132
133    let mut handle: HANDLE = std::ptr::null_mut();
134    // SAFETY: the connection remains live for this call and the writable
135    // out-parameter has the native HANDLE representation SQLite expects.
136    let result = unsafe {
137        rusqlite::ffi::sqlite3_file_control(
138            conn.handle(),
139            c"main".as_ptr(),
140            rusqlite::ffi::SQLITE_FCNTL_WIN32_GET_HANDLE,
141            (&raw mut handle).cast(),
142        )
143    };
144    if result != rusqlite::ffi::SQLITE_OK || handle.is_null() || handle == INVALID_HANDLE_VALUE {
145        return Err(crate::error::SqliteError::InvalidData(format!(
146            "cannot inspect opened SQLite database handle (file control {result})"
147        )));
148    }
149    Ok(identity_from_handle(handle)?)
150}