cross_path/platform/
windows.rs

1//! Windows-specific path handling implementation
2//!
3//! This module provides Windows-specific implementations for path operations,
4//! including UTF-16 conversion, drive letter handling, and Windows API integration.
5//!
6//! It uses the `windows` crate to interact with the Windows API.
7
8use crate::PathError;
9use crate::platform::{DiskInfo, FileAttributes, PathExt, PlatformPath};
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13use core::iter::Iterator;
14use core::option::Option;
15use std::ffi::OsString;
16use std::os::windows::ffi::OsStringExt;
17use std::os::windows::fs::MetadataExt;
18use std::path::{Path, PathBuf};
19use windows::Win32::Foundation::GetLastError;
20use windows::Win32::Storage::FileSystem::{
21    FILE_ATTRIBUTE_HIDDEN, GetDiskFreeSpaceExW, GetFileAttributesW, GetVolumeInformationW,
22};
23use windows::core::PCWSTR;
24
25/// Windows platform path extension
26pub struct WindowsPathExt {
27    path: PathBuf,
28}
29
30impl WindowsPathExt {
31    /// Create new WindowsPathExt
32    pub fn new<P: AsRef<Path>>(path: P) -> Self {
33        Self {
34            path: path.as_ref().to_path_buf(),
35        }
36    }
37}
38
39impl PlatformPath for WindowsPathExt {
40    fn separator(&self) -> char {
41        '\\'
42    }
43
44    fn is_absolute(&self) -> bool {
45        self.path.is_absolute()
46    }
47
48    fn to_platform_specific(&self) -> String {
49        self.path.to_string_lossy().into_owned()
50    }
51}
52
53impl PathExt for WindowsPathExt {
54    fn get_attributes(&self) -> Option<FileAttributes> {
55        let metadata = std::fs::metadata(&self.path).ok()?;
56
57        let size = metadata.len();
58        let is_directory = metadata.is_dir();
59        let is_readonly = metadata.permissions().readonly();
60
61        // Get hidden attribute using Windows metadata
62        let attrs = metadata.file_attributes();
63        let is_hidden = (attrs & FILE_ATTRIBUTE_HIDDEN.0) != 0;
64
65        let creation_time = metadata
66            .created()
67            .ok()
68            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
69            .map(|d| d.as_secs());
70
71        let modification_time = metadata
72            .modified()
73            .ok()
74            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
75            .map(|d| d.as_secs());
76
77        Some(FileAttributes {
78            size,
79            is_directory,
80            is_hidden,
81            is_readonly,
82            creation_time,
83            modification_time,
84        })
85    }
86
87    fn is_accessible(&self) -> bool {
88        self.path.exists()
89    }
90
91    fn get_disk_info(&self) -> Option<DiskInfo> {
92        // Find root path (e.g., "C:\" or "\\Server\Share\")
93        let root = self.path.components().next().and_then(|c| match c {
94            std::path::Component::Prefix(prefix) => {
95                let mut s = prefix.as_os_str().to_os_string();
96                s.push("\\");
97                Some(s)
98            }
99            std::path::Component::RootDir => Some(std::path::PathBuf::from("\\").into_os_string()),
100            _ => None,
101        })?;
102
103        let root_str = root.to_string_lossy();
104        let wide_root = to_windows_path(&root_str).ok()?;
105
106        let mut total_bytes = 0u64;
107        let mut free_bytes_caller = 0u64;
108        let mut total_free_bytes = 0u64;
109
110        unsafe {
111            let result = GetDiskFreeSpaceExW(
112                PCWSTR(wide_root.as_ptr()),
113                Some(&mut free_bytes_caller),
114                Some(&mut total_bytes),
115                Some(&mut total_free_bytes),
116            );
117
118            if result.is_err() {
119                return None;
120            }
121        }
122
123        // Get Filesystem Name
124        let mut fs_name_buf = [0u16; 256];
125        let fs_type = unsafe {
126            let res = GetVolumeInformationW(
127                PCWSTR(wide_root.as_ptr()),
128                None,
129                None,
130                None,
131                None,
132                Some(&mut fs_name_buf),
133            );
134
135            if res.is_ok() {
136                let len = fs_name_buf
137                    .iter()
138                    .position(|&c| c == 0)
139                    .unwrap_or(fs_name_buf.len());
140                String::from_utf16_lossy(&fs_name_buf[..len])
141            } else {
142                "Unknown".to_string()
143            }
144        };
145
146        Some(DiskInfo {
147            total_space: total_bytes,
148            free_space: free_bytes_caller,
149            filesystem_type: fs_type,
150        })
151    }
152}
153
154/// Convert string to Windows UTF-16 path
155pub fn to_windows_path(path: &str) -> Result<Vec<u16>, PathError> {
156    let mut wide: Vec<u16> = path.encode_utf16().collect();
157    wide.push(0); // Add null terminator
158
159    // Convert separators
160    for ch in &mut wide {
161        if *ch == b'/' as u16 {
162            *ch = b'\\' as u16;
163        }
164    }
165
166    Ok(wide)
167}
168
169/// Convert Windows UTF-16 path to string
170pub fn from_windows_path(wide: &[u16]) -> Result<String, PathError> {
171    // Find null terminator
172    let null_pos = wide.iter().position(|&c| c == 0).unwrap_or(wide.len());
173    let slice = &wide[..null_pos];
174
175    // Convert separators
176    let mut result = OsString::from_wide(slice)
177        .into_string()
178        .map_err(|e| PathError::encoding_error(e.to_string_lossy().into_owned()))?;
179
180    // Unify separator display
181    result = result.replace('\\', "/");
182
183    Ok(result)
184}
185
186/// Check if string is a valid Windows path
187pub fn is_valid_windows_path(path: &str) -> bool {
188    // Check drive letter format
189    if path.len() >= 2 {
190        let first_char = path.chars().next().unwrap();
191        let second_char = path.chars().nth(1).unwrap();
192
193        if first_char.is_ascii_alphabetic() && second_char == ':' {
194            return true;
195        }
196    }
197
198    // Check UNC path
199    if path.starts_with(r"\\") {
200        return true;
201    }
202
203    false
204}
205
206/// Extract drive letter from Windows path
207pub fn get_drive_letter(path: &str) -> Option<char> {
208    if path.len() >= 2 {
209        let first_char = path.chars().next().unwrap();
210        let second_char = path.chars().nth(1).unwrap();
211
212        if first_char.is_ascii_alphabetic() && second_char == ':' {
213            return Some(first_char.to_ascii_uppercase());
214        }
215    }
216
217    None
218}
219
220/// Get Windows file attributes using Windows API
221pub fn get_windows_file_attributes(path: &str) -> Result<u32, PathError> {
222    let wide_path = to_windows_path(path)?;
223
224    unsafe {
225        let attrs = GetFileAttributesW(PCWSTR(wide_path.as_ptr()));
226
227        if attrs == 0xFFFFFFFF {
228            let error = GetLastError();
229            return Err(PathError::platform_error(format!(
230                "Failed to get file attributes: {:?}",
231                error
232            )));
233        }
234
235        Ok(attrs)
236    }
237}
238
239/// Check if path exists on Windows
240pub fn windows_path_exists(path: &str) -> Result<bool, PathError> {
241    let attrs = get_windows_file_attributes(path)?;
242    Ok(attrs != 0xFFFFFFFF)
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_is_valid_windows_path() {
251        assert!(is_valid_windows_path(r"C:\Windows"));
252        assert!(is_valid_windows_path(r"D:\Data\file.txt"));
253        assert!(is_valid_windows_path(r"\\Server\Share"));
254        assert!(!is_valid_windows_path(r"/usr/bin"));
255        assert!(!is_valid_windows_path(r"relative\path"));
256    }
257
258    #[test]
259    fn test_get_drive_letter() {
260        assert_eq!(get_drive_letter(r"C:\Windows"), Some('C'));
261        assert_eq!(get_drive_letter(r"d:\data"), Some('D'));
262        assert_eq!(get_drive_letter(r"\\Server\Share"), None);
263        assert_eq!(get_drive_letter(r"/usr/bin"), None);
264    }
265
266    #[test]
267    fn test_to_windows_path() {
268        let path = "C:/Windows/System32";
269        let wide = to_windows_path(path).unwrap();
270
271        // Check null terminator
272        assert_eq!(*wide.last().unwrap(), 0);
273
274        // Check separator conversion
275        let backslash = b'\\' as u16;
276        assert!(wide.contains(&backslash));
277    }
278
279    #[test]
280    fn test_from_windows_path() {
281        let wide = vec![
282            'C' as u16,
283            ':' as u16,
284            '\\' as u16,
285            'T' as u16,
286            'e' as u16,
287            's' as u16,
288            't' as u16,
289            0,
290        ];
291        let path = from_windows_path(&wide).unwrap();
292
293        // Should be normalized to forward slashes by default in this lib
294        assert_eq!(path, "C:/Test");
295    }
296}