use std::{
ffi::{c_void, OsStr},
slice::from_raw_parts,
};
const INVALID_HANDLE_VALUE: *mut c_void = -1isize as *mut c_void;
#[link(name = "user32")]
extern "system" {
fn FileTimeToSystemTime(lpFileTime: *const FileTime, lpSystemTime: *mut SystemTime) -> bool;
fn FindClose(hFindFile: *mut c_void) -> bool;
fn GetLogicalDrives() -> u32;
fn FindFirstFileW(lpFileName: *const u16, lpFindFileData: *mut FindDataW) -> *mut c_void;
fn FindNextFileW(hFindFile: *mut c_void, lpFindFileData: *mut FindDataW) -> bool;
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct FindDataW {
pub file_attributes: u32,
pub creation_time: FileTime,
pub last_access_time: FileTime,
pub last_write_time: FileTime,
pub file_size_high: u32,
pub file_size_low: u32,
pub reserved0: u32,
pub reserved1: u32,
pub file_name: [u16; 260],
pub alternate_file_name: [u16; 14],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
struct FileTime {
pub dw_low_date_time: u32,
pub dw_high_date_time: u32,
}
#[derive(Debug)]
pub enum Error {
InvalidSearch(String),
InvalidSystemTime,
}
impl TryInto<SystemTime> for FileTime {
type Error = Error;
fn try_into(self) -> Result<SystemTime, Self::Error> {
unsafe {
let mut system_time = SystemTime::default();
if FileTimeToSystemTime(&self, &mut system_time) {
Ok(system_time)
} else {
Err(Error::InvalidSystemTime)
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct SystemTime {
pub year: u16,
pub month: u16,
pub day_of_week: u16,
pub day: u16,
pub hour: u16,
pub minute: u16,
pub second: u16,
pub milliseconds: u16,
}
impl SystemTime {
pub fn dmyhm(&self) -> String {
format!(
"{:02}/{:02}/{:04} {:02}:{:02}",
self.day, self.month, self.year, self.hour, self.minute,
)
}
}
pub mod attributes {
pub const READONLY: u32 = 0x00000001;
pub const HIDDEN: u32 = 0x00000002;
pub const SYSTEM: u32 = 0x00000004;
pub const DIRECTORY: u32 = 0x00000010;
pub const ARCHIVE: u32 = 0x00000020;
pub const DEVICE: u32 = 0x00000040;
pub const NORMAL: u32 = 0x00000080;
pub const TEMPORARY: u32 = 0x00000100;
pub const SPARSE_FILE: u32 = 0x00000200;
pub const REPARSE_POINT: u32 = 0x00000400;
pub const COMPRESSED: u32 = 0x00000800;
pub const OFFLINE: u32 = 0x00001000;
pub const NOT_CONTENT_INDEXED: u32 = 0x00002000;
pub const ENCRYPTED: u32 = 0x00004000;
pub const INTEGRITY_STREAM: u32 = 0x00008000;
pub const VIRTUAL: u32 = 0x00010000;
pub const NO_SCRUB_DATA: u32 = 0x00020000;
pub const EA: u32 = 0x00040000;
pub const PINNED: u32 = 0x00080000;
pub const UNPINNED: u32 = 0x00100000;
pub const RECALL_ON_OPEN: u32 = 0x00400000;
pub const RECALL_ON_DATA_ACCESS: u32 = 0x00400000;
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct DirEntry {
pub name: String,
pub path: String,
pub date_created: SystemTime,
pub last_access: SystemTime,
pub last_write: SystemTime,
pub attributes: u32,
pub size: u64,
pub is_folder: bool,
}
impl DirEntry {
pub fn extension(&self) -> Option<&'_ OsStr> {
let mut iter = self.name.as_bytes().rsplitn(2, |b| *b == b'.');
let after = iter.next();
let before = iter.next();
if before == Some(b"") {
None
} else {
unsafe { after.map(|s| &*(s as *const [u8] as *const OsStr)) }
}
}
}
pub fn walkdir<S: AsRef<str>>(path: S, depth: usize) -> Vec<Result<DirEntry, Error>> {
unsafe {
let path = path.as_ref();
let mut fd: FindDataW = core::mem::zeroed();
let mut files = Vec::new();
let path_utf16: Vec<u16> = path.encode_utf16().collect();
let search_pattern = [path_utf16.as_slice(), &[b'\\' as u16, b'*' as u16, 0]].concat();
let search_handle = FindFirstFileW(search_pattern.as_ptr() as *mut u16, &mut fd);
if !search_handle.is_null() && search_handle != INVALID_HANDLE_VALUE {
loop {
let end = fd
.file_name
.iter()
.position(|&c| c == b'\0' as u16)
.unwrap_or(fd.file_name.len());
let slice = from_raw_parts(fd.file_name.as_ptr() as *const u16, end);
let name = String::from_utf16(slice).unwrap();
let path = [path, name.as_str()].join("\\");
if name == ".." || name == "." {
fd = core::mem::zeroed();
if !FindNextFileW(search_handle, &mut fd) {
break;
}
continue;
}
let is_folder = (fd.file_attributes & attributes::DIRECTORY) != 0;
let date_created = fd.creation_time.try_into().unwrap();
let last_access = fd.last_access_time.try_into().unwrap();
let last_write = fd.last_write_time.try_into().unwrap();
let size =
(fd.file_size_high as u64 * (u32::MAX as u64 + 1)) + fd.file_size_low as u64;
if is_folder {
if depth == 0 {
files.extend(walkdir(&path, 0));
} else if depth - 1 != 0 {
files.extend(walkdir(&path, depth - 1));
}
}
files.push(Ok(DirEntry {
name,
path,
date_created,
last_access,
last_write,
attributes: fd.file_attributes,
size,
is_folder,
}));
fd = core::mem::zeroed();
if !FindNextFileW(search_handle, &mut fd) {
break;
}
}
FindClose(search_handle);
} else {
files.push(Err(Error::InvalidSearch(path.to_owned())));
}
files
}
}
pub fn drives() -> [Option<char>; 26] {
let logical_drives = unsafe { GetLogicalDrives() };
let mut drives = [None; 26];
let mut mask = 1;
for (i, letter) in (b'A'..=b'Z').enumerate() {
if (logical_drives & mask) != 0 {
drives[i] = Some(letter as char);
}
mask <<= 1;
}
drives
}