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}