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#[cfg(windows)]
49pub fn database_file_identity(path: &Path) -> io::Result<DatabaseFileIdentity> {
50    use std::os::windows::fs::OpenOptionsExt as _;
51    use std::os::windows::io::AsRawHandle as _;
52    use windows_sys::Win32::Storage::FileSystem::{
53        FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
54    };
55
56    let file = std::fs::OpenOptions::new()
57        .read(true)
58        .access_mode(FILE_READ_ATTRIBUTES)
59        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
60        .open(path)?;
61    identity_from_handle(file.as_raw_handle())
62}
63
64#[cfg(windows)]
65fn identity_from_handle(
66    handle: std::os::windows::io::RawHandle,
67) -> io::Result<DatabaseFileIdentity> {
68    use windows_sys::Win32::Storage::FileSystem::{
69        FileIdInfo, GetFileInformationByHandleEx, FILE_ID_INFO,
70    };
71
72    let mut info = FILE_ID_INFO::default();
73    // SAFETY: the caller retains a live handle; `info` is a writable buffer
74    // of the exact size required by FileIdInfo. No ownership is transferred.
75    let ok = unsafe {
76        GetFileInformationByHandleEx(
77            handle,
78            FileIdInfo,
79            (&raw mut info).cast(),
80            std::mem::size_of::<FILE_ID_INFO>() as u32,
81        )
82    };
83    if ok == 0 {
84        return Err(io::Error::last_os_error());
85    }
86    Ok(DatabaseFileIdentity {
87        volume_serial: info.VolumeSerialNumber,
88        file_id: info.FileId.Identifier,
89    })
90}
91
92#[cfg(windows)]
93pub fn sqlite_opened_file_identity(
94    conn: &rusqlite::Connection,
95) -> Result<DatabaseFileIdentity, crate::error::SqliteError> {
96    use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
97
98    let mut handle: HANDLE = std::ptr::null_mut();
99    // SAFETY: the connection remains live for this call and the writable
100    // out-parameter has the native HANDLE representation SQLite expects.
101    let result = unsafe {
102        rusqlite::ffi::sqlite3_file_control(
103            conn.handle(),
104            c"main".as_ptr(),
105            rusqlite::ffi::SQLITE_FCNTL_WIN32_GET_HANDLE,
106            (&raw mut handle).cast(),
107        )
108    };
109    if result != rusqlite::ffi::SQLITE_OK || handle.is_null() || handle == INVALID_HANDLE_VALUE {
110        return Err(crate::error::SqliteError::InvalidData(format!(
111            "cannot inspect opened SQLite database handle (file control {result})"
112        )));
113    }
114    Ok(identity_from_handle(handle)?)
115}