winwalk/
lib.rs

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    /// Returns the date in day/month/year hour:minute format.
76    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
84/// File attributes are metadata values stored by the file system on disk.
85///
86/// [File Attribute Constants - MSDN](https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants)
87pub 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    /// Bitflag for file [attributes].
120    pub attributes: u32,
121    /// Size in bytes.
122    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
139/// Traverse the requested directory.
140///
141/// A depth of `0` will set no limit.
142///
143/// ```
144/// for file in winwalk::walkdir("D:\\Desktop", 1).into_iter().flatten() {
145///     println!("Name: {}", file.name);
146///     println!("Path: {}", file.path);
147///     println!("Size: {}", file.size);
148///     println!("Folder?: {}", file.is_folder);
149///     println!("Last Write: {:?}", file.last_write);
150///     println!("Last Access: {:?}", file.last_access);
151///     println!("Attributes: {:?}", file.attributes);
152/// }
153/// ```
154pub 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                //Create the full path.
167                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                //Skip these results.
177                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                //TODO: I think these dates are wrong.
188                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
230/// Get the current system drives. `A-Z` `0-25`
231pub 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}