Skip to main content

diskann_platform/win/
file_handle.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5use std::{ffi::CString, io, ptr};
6
7use windows_sys::Win32::{
8    Foundation::{
9        CloseHandle, GetLastError, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
10    },
11    Storage::FileSystem::{
12        CreateFileA, FILE_FLAG_NO_BUFFERING, FILE_FLAG_OVERLAPPED, FILE_FLAG_RANDOM_ACCESS,
13        FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
14    },
15};
16
17use super::DWORD;
18
19pub const FILE_ATTRIBUTE_READONLY: DWORD = 0x00000001;
20
21/// `AccessMode` determines how a file can be accessed.
22/// These modes are used when creating or opening a file to decide what operations are allowed
23/// to be performed on the file.
24///
25/// # Variants
26///
27/// - `Read`: The file is opened in read-only mode.
28///
29/// - `Write`: The file is opened in write-only mode.
30///
31/// - `ReadWrite`: The file is opened for both reading and writing.
32pub enum AccessMode {
33    Read,
34    Write,
35    ReadWrite,
36}
37
38/// `ShareMode` determines how a file can be shared.
39///
40/// These modes are used when creating or opening a file to decide what operations other
41/// opening instances of the file can perform on it.
42/// # Variants
43/// - `None`: Prevents other processes from opening a file if they request delete,
44///   read, or write access.
45///
46/// - `Read`: Allows subsequent open operations on the same file to request read access.
47///
48/// - `Write`: Allows subsequent open operations on the same file file to request write access.
49///
50/// - `Delete`: Allows subsequent open operations on the same file file to request delete access.
51pub enum ShareMode {
52    None,
53    Read,
54    Write,
55    Delete,
56}
57
58/// # Windows File Handle Wrapper
59///
60/// Introduces a Rust-friendly wrapper around the native Windows `HANDLE` object, `FileHandle`.
61/// `FileHandle` provides safe creation and automatic cleanup of Windows file handles, leveraging Rust's ownership model.
62///
63/// `FileHandle` struct that wraps a native Windows `HANDLE` object
64pub struct FileHandle {
65    pub handle: HANDLE,
66}
67// SAFETY: THIS IS NOT ENTIRELY SAFE! PLEASE READ!
68//
69// The Windows API functions `ReadFile` and `GetQueuedCompletionStatus` are safe to call
70// from multiple threads when using the OVERLAPPED API.
71// ReadFile Function - https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
72// Synchronous and Asynchronous I/O - https://learn.microsoft.com/en-us/windows/win32/FileIO/synchronous-and-asynchronous-i-o
73//
74// However, `GetQueuedCompletionStatus` will not behave as expected as it will return when
75// **any** request completes, not just the request associated with a particular thread.
76//
77// Our code uses `GetQueuedCompletionStatus` and therefore functions on the `FileHandle`
78// may only be reliably called when a thread has exclusive access to that handle.
79//
80// This is done through the `WindowsAlignedFileReader` but is not captured in the type system.
81//
82// The correct long-term solution is to remove these implementations to make `FileHandle`
83// not sharable between threads and embed it in a higher-level wrapper that is acquired early
84// in search to guarantee exclusive access.
85unsafe impl Send for FileHandle {}
86unsafe impl Sync for FileHandle {}
87
88impl FileHandle {
89    /// Creates a new `FileHandle` by opening an existing file with the given access and shared mode.
90    ///
91    /// This function is marked unsafe because it creates a raw pointer to the filename and try to create
92    /// a Windows `HANDLE` object without checking if you have sufficient permissions.
93    ///
94    /// # Safety
95    ///
96    /// Ensure that the file specified by `file_name` is valid and the calling process has
97    /// sufficient permissions to perform the specified `access_mode` and `share_mode` operations.
98    ///
99    /// # Parameters
100    ///
101    /// - `file_name`: The name of the file.
102    /// - `access_mode`: The access mode to be used for the file.
103    /// - `share_mode`: The share mode to be used for the file
104    ///
105    /// # Errors
106    /// This function will return an error if the `file_name` is invalid or if the file cannot
107    /// be opened with the specified `access_mode` and `share_mode`.
108    pub unsafe fn new(
109        file_name: &str,
110        access_mode: AccessMode,
111        share_mode: ShareMode,
112    ) -> io::Result<Self> {
113        let file_name_c = CString::new(file_name).map_err(|_| {
114            io::Error::new(
115                io::ErrorKind::InvalidData,
116                format!("Invalid file name. {}", file_name),
117            )
118        })?;
119
120        let dw_desired_access = match access_mode {
121            AccessMode::Read => GENERIC_READ,
122            AccessMode::Write => GENERIC_WRITE,
123            AccessMode::ReadWrite => GENERIC_READ | GENERIC_WRITE,
124        };
125
126        let dw_share_mode = match share_mode {
127            ShareMode::None => 0,
128            ShareMode::Read => FILE_SHARE_READ,
129            ShareMode::Write => FILE_SHARE_WRITE,
130            ShareMode::Delete => FILE_SHARE_DELETE,
131        };
132
133        let dw_flags_and_attributes = FILE_ATTRIBUTE_READONLY
134            | FILE_FLAG_NO_BUFFERING
135            | FILE_FLAG_OVERLAPPED
136            | FILE_FLAG_RANDOM_ACCESS;
137
138        let handle = unsafe {
139            CreateFileA(
140                Self::as_windows_pcstr(&file_name_c),
141                dw_desired_access,
142                dw_share_mode,
143                ptr::null_mut(),
144                OPEN_EXISTING,
145                dw_flags_and_attributes,
146                std::ptr::null_mut(),
147            )
148        };
149
150        if handle == INVALID_HANDLE_VALUE {
151            let error_code = unsafe { GetLastError() };
152            Err(io::Error::from_raw_os_error(error_code as i32))
153        } else {
154            Ok(Self { handle })
155        }
156    }
157
158    fn as_windows_pcstr(str: &CString) -> ::windows_sys::core::PCSTR {
159        str.as_ptr() as ::windows_sys::core::PCSTR
160    }
161}
162
163impl Drop for FileHandle {
164    fn drop(&mut self) {
165        let result = unsafe { CloseHandle(self.handle) };
166        if result == 0 {
167            let error_code = unsafe { GetLastError() };
168            let error = io::Error::from_raw_os_error(error_code as i32);
169            tracing::warn!("Error when dropping FileHandle: {:?}", error);
170        }
171    }
172}
173
174/// Returns a `FileHandle` with an `INVALID_HANDLE_VALUE`.
175impl Default for FileHandle {
176    fn default() -> Self {
177        FileHandle {
178            handle: INVALID_HANDLE_VALUE,
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use std::{fs::File, path::Path};
186
187    use super::*;
188
189    #[test]
190    fn test_create_file() {
191        // Create a dummy file
192        let dummy_file_path = "dummy_file.txt";
193        {
194            let _file = File::create(dummy_file_path).expect("Failed to create dummy file.");
195        }
196
197        let path = Path::new(dummy_file_path);
198        {
199            let file_handle = unsafe {
200                FileHandle::new(path.to_str().unwrap(), AccessMode::Read, ShareMode::Read)
201            };
202
203            // Check that the file handle is valid
204            assert!(file_handle.is_ok());
205        }
206
207        // Try to delete the file. If the handle was correctly dropped, this should succeed.
208        match std::fs::remove_file(dummy_file_path) {
209            Ok(()) => (), // File was deleted successfully, which means the handle was closed.
210            Err(e) => panic!("Failed to delete file: {}", e), // Failed to delete the file, likely because the handle is still open.
211        }
212    }
213
214    #[test]
215    fn test_file_not_found() {
216        let path = Path::new("non_existent_file.txt");
217        let file_handle =
218            unsafe { FileHandle::new(path.to_str().unwrap(), AccessMode::Read, ShareMode::Read) };
219
220        // Check that opening a non-existent file returns an error
221        assert!(file_handle.is_err());
222    }
223}