1use std::{
2 ffi::{c_void, OsStr},
3 slice::from_raw_parts,
4};
5
6const INVALID_HANDLE_VALUE: *mut c_void = -1isize as *mut c_void;
7
8#[link(name = "user32")]
9extern "system" {
10 fn FileTimeToSystemTime(lpFileTime: *const FileTime, lpSystemTime: *mut SystemTime) -> bool;
11 fn FindClose(hFindFile: *mut c_void) -> bool;
12 fn GetLogicalDrives() -> u32;
13 fn FindFirstFileW(lpFileName: *const u16, lpFindFileData: *mut FindDataW) -> *mut c_void;
14 fn FindNextFileW(hFindFile: *mut c_void, lpFindFileData: *mut FindDataW) -> bool;
15
16}
17
18#[repr(C)]
19#[derive(Copy, Clone, Debug)]
20struct FindDataW {
21 pub file_attributes: u32,
22 pub creation_time: FileTime,
23 pub last_access_time: FileTime,
24 pub last_write_time: FileTime,
25 pub file_size_high: u32,
26 pub file_size_low: u32,
27 pub reserved0: u32,
28 pub reserved1: u32,
29 pub file_name: [u16; 260],
30 pub alternate_file_name: [u16; 14],
31}
32
33#[repr(C)]
34#[derive(Copy, Clone, Debug, Default, PartialEq)]
35struct FileTime {
36 pub dw_low_date_time: u32,
37 pub dw_high_date_time: u32,
38}
39
40#[derive(Debug)]
41pub enum Error {
42 InvalidSearch(String),
43 InvalidSystemTime,
44}
45
46impl TryInto<SystemTime> for FileTime {
47 type Error = Error;
48
49 fn try_into(self) -> Result<SystemTime, Self::Error> {
50 unsafe {
51 let mut system_time = SystemTime::default();
52 if FileTimeToSystemTime(&self, &mut system_time) {
53 Ok(system_time)
54 } else {
55 Err(Error::InvalidSystemTime)
56 }
57 }
58 }
59}
60
61#[repr(C)]
62#[derive(Copy, Clone, Debug, Default, PartialEq)]
63pub struct SystemTime {
64 pub year: u16,
65 pub month: u16,
66 pub day_of_week: u16,
67 pub day: u16,
68 pub hour: u16,
69 pub minute: u16,
70 pub second: u16,
71 pub milliseconds: u16,
72}
73
74impl SystemTime {
75 pub fn dmyhm(&self) -> String {
77 format!(
78 "{:02}/{:02}/{:04} {:02}:{:02}",
79 self.day, self.month, self.year, self.hour, self.minute,
80 )
81 }
82}
83
84pub mod attributes {
88 pub const READONLY: u32 = 0x00000001;
89 pub const HIDDEN: u32 = 0x00000002;
90 pub const SYSTEM: u32 = 0x00000004;
91 pub const DIRECTORY: u32 = 0x00000010;
92 pub const ARCHIVE: u32 = 0x00000020;
93 pub const DEVICE: u32 = 0x00000040;
94 pub const NORMAL: u32 = 0x00000080;
95 pub const TEMPORARY: u32 = 0x00000100;
96 pub const SPARSE_FILE: u32 = 0x00000200;
97 pub const REPARSE_POINT: u32 = 0x00000400;
98 pub const COMPRESSED: u32 = 0x00000800;
99 pub const OFFLINE: u32 = 0x00001000;
100 pub const NOT_CONTENT_INDEXED: u32 = 0x00002000;
101 pub const ENCRYPTED: u32 = 0x00004000;
102 pub const INTEGRITY_STREAM: u32 = 0x00008000;
103 pub const VIRTUAL: u32 = 0x00010000;
104 pub const NO_SCRUB_DATA: u32 = 0x00020000;
105 pub const EA: u32 = 0x00040000;
106 pub const PINNED: u32 = 0x00080000;
107 pub const UNPINNED: u32 = 0x00100000;
108 pub const RECALL_ON_OPEN: u32 = 0x00400000;
109 pub const RECALL_ON_DATA_ACCESS: u32 = 0x00400000;
110}
111
112#[derive(Debug, Clone, PartialEq, Default)]
113pub struct DirEntry {
114 pub name: String,
115 pub path: String,
116 pub date_created: SystemTime,
117 pub last_access: SystemTime,
118 pub last_write: SystemTime,
119 pub attributes: u32,
121 pub size: u64,
123 pub is_folder: bool,
124}
125
126impl DirEntry {
127 pub fn extension(&self) -> Option<&'_ OsStr> {
128 let mut iter = self.name.as_bytes().rsplitn(2, |b| *b == b'.');
129 let after = iter.next();
130 let before = iter.next();
131 if before == Some(b"") {
132 None
133 } else {
134 unsafe { after.map(|s| &*(s as *const [u8] as *const OsStr)) }
135 }
136 }
137}
138
139pub fn walkdir<S: AsRef<str>>(path: S, depth: usize) -> Vec<Result<DirEntry, Error>> {
155 unsafe {
156 let path = path.as_ref();
157 let mut fd: FindDataW = core::mem::zeroed();
158 let mut files = Vec::new();
159
160 let path_utf16: Vec<u16> = path.encode_utf16().collect();
161 let search_pattern = [path_utf16.as_slice(), &[b'\\' as u16, b'*' as u16, 0]].concat();
162 let search_handle = FindFirstFileW(search_pattern.as_ptr() as *mut u16, &mut fd);
163
164 if !search_handle.is_null() && search_handle != INVALID_HANDLE_VALUE {
165 loop {
166 let end = fd
168 .file_name
169 .iter()
170 .position(|&c| c == b'\0' as u16)
171 .unwrap_or(fd.file_name.len());
172 let slice = from_raw_parts(fd.file_name.as_ptr() as *const u16, end);
173 let name = String::from_utf16(slice).unwrap();
174 let path = [path, name.as_str()].join("\\");
175
176 if name == ".." || name == "." {
178 fd = core::mem::zeroed();
179 if !FindNextFileW(search_handle, &mut fd) {
180 break;
181 }
182 continue;
183 }
184
185 let is_folder = (fd.file_attributes & attributes::DIRECTORY) != 0;
186
187 let date_created = fd.creation_time.try_into().unwrap();
189 let last_access = fd.last_access_time.try_into().unwrap();
190 let last_write = fd.last_write_time.try_into().unwrap();
191
192 let size =
193 (fd.file_size_high as u64 * (u32::MAX as u64 + 1)) + fd.file_size_low as u64;
194
195 if is_folder {
196 if depth == 0 {
197 files.extend(walkdir(&path, 0));
198 } else if depth - 1 != 0 {
199 files.extend(walkdir(&path, depth - 1));
200 }
201 }
202
203 files.push(Ok(DirEntry {
204 name,
205 path,
206 date_created,
207 last_access,
208 last_write,
209 attributes: fd.file_attributes,
210 size,
211 is_folder,
212 }));
213
214 fd = core::mem::zeroed();
215
216 if !FindNextFileW(search_handle, &mut fd) {
217 break;
218 }
219 }
220
221 FindClose(search_handle);
222 } else {
223 files.push(Err(Error::InvalidSearch(path.to_owned())));
224 }
225
226 files
227 }
228}
229
230pub fn drives() -> [Option<char>; 26] {
232 let logical_drives = unsafe { GetLogicalDrives() };
233 let mut drives = [None; 26];
234 let mut mask = 1;
235
236 for (i, letter) in (b'A'..=b'Z').enumerate() {
237 if (logical_drives & mask) != 0 {
238 drives[i] = Some(letter as char);
239 }
240 mask <<= 1;
241 }
242
243 drives
244}