use super::super::directory_scan::{DirectoryScan, Query};
use super::{FileKind, WIDE_UNIT_BYTES, kind};
use std::{
ffi::OsString,
fs::File,
io, mem,
os::windows::{ffi::OsStringExt, io::AsRawHandle},
slice,
};
use windows_sys::Win32::{
Foundation::ERROR_NO_MORE_FILES,
Storage::FileSystem::{
FILE_ID_BOTH_DIR_INFO, FileIdBothDirectoryInfo, FileIdBothDirectoryRestartInfo,
GetFileInformationByHandleEx,
},
};
const DIRECTORY_BUFFER_WORDS: usize = 8 * 1024;
const DIRECTORY_RECORD_ALIGNMENT: usize = 8;
pub fn read_dir(directory: &File) -> io::Result<impl Iterator<Item = io::Result<DirEntry>> + '_> {
Ok(ReadDir::new(move |query, buffer: &mut [u64]| {
query_directory(directory, query, buffer)
}))
}
fn query_directory(directory: &File, query: Query, buffer: &mut [u64]) -> io::Result<()> {
let class = match query {
Query::Restart => FileIdBothDirectoryRestartInfo,
Query::Continue => FileIdBothDirectoryInfo,
};
let ok = unsafe {
GetFileInformationByHandleEx(
directory.as_raw_handle(),
class,
buffer.as_mut_ptr().cast(),
mem::size_of_val(buffer) as u32,
)
};
if ok == 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
pub struct DirEntry {
name: OsString,
kind: FileKind,
}
impl DirEntry {
pub fn file_name(&self) -> OsString {
self.name.clone()
}
pub fn file_type(&self) -> io::Result<FileKind> {
Ok(self.kind)
}
}
struct ReadDir<Q> {
query: Q,
buffer: Vec<u64>,
next: Option<usize>,
scan: DirectoryScan,
}
impl<Q> Iterator for ReadDir<Q>
where
Q: FnMut(Query, &mut [u64]) -> io::Result<()>,
{
type Item = io::Result<DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(offset) = self.next {
match self.record(offset) {
Ok(Some(entry)) => return Some(Ok(entry)),
Ok(None) => continue,
Err(error) => return Some(Err(error)),
}
}
let query = self.scan.query()?;
if let Err(error) = (self.query)(query, &mut self.buffer) {
self.scan.finish();
if error.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) {
return None;
}
return Some(Err(error));
}
self.next = Some(0);
}
}
}
impl<Q> ReadDir<Q> {
fn new(query: Q) -> Self {
Self {
query,
buffer: vec![0; DIRECTORY_BUFFER_WORDS],
next: None,
scan: DirectoryScan::First,
}
}
fn record(&mut self, offset: usize) -> io::Result<Option<DirEntry>> {
let size = mem::size_of_val(self.buffer.as_slice());
let header = mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
if !offset.is_multiple_of(DIRECTORY_RECORD_ALIGNMENT) || offset + header > size {
self.scan.finish();
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let info = unsafe {
self.buffer
.as_ptr()
.cast::<u8>()
.add(offset)
.cast::<FILE_ID_BOTH_DIR_INFO>()
};
let (next, attributes, tag, length) = unsafe {
(
(*info).NextEntryOffset as usize,
(*info).FileAttributes,
(*info).EaSize,
(*info).FileNameLength as usize,
)
};
if !length.is_multiple_of(WIDE_UNIT_BYTES) || offset + header + length > size {
self.scan.finish();
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let name = unsafe {
slice::from_raw_parts(
(&raw const (*info).FileName).cast::<u16>(),
length / WIDE_UNIT_BYTES,
)
};
self.next = (next != 0).then_some(offset + next);
let dot = u16::from(b'.');
if name == [dot] || name == [dot, dot] {
return Ok(None);
}
Ok(Some(DirEntry {
name: OsString::from_wide(name),
kind: kind(attributes, tag),
}))
}
}
#[cfg(all(test, windows))]
mod tests {
use super::super::open_directory;
use super::*;
use std::ptr;
#[test]
fn native_directory_iteration_continues_after_a_full_record_page() {
const ENTRIES: usize = 600;
let directory = crate::test_os::tempdir();
let expected: std::collections::BTreeSet<_> = (0..ENTRIES)
.map(|index| format!("entry-{index:04}.txt"))
.collect();
for name in &expected {
std::fs::write(directory.path().join(name), "").unwrap();
}
let handle = open_directory(directory.path()).unwrap();
let mut entries = read_dir(&handle).unwrap();
let observed: Vec<_> = entries
.by_ref()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect();
assert_eq!(observed.len(), ENTRIES);
assert_eq!(
observed
.into_iter()
.collect::<std::collections::BTreeSet<_>>(),
expected
);
assert!(entries.next().is_none());
assert!(entries.next().is_none());
}
fn page(names: &[&str]) -> Vec<u64> {
let mut buffer = vec![0; DIRECTORY_BUFFER_WORDS];
let header = mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
let mut offset = 0;
for (index, name) in names.iter().enumerate() {
let name: Vec<_> = name.encode_utf16().collect();
let record_bytes = (header + mem::size_of_val(name.as_slice()))
.next_multiple_of(DIRECTORY_RECORD_ALIGNMENT);
assert!(offset + record_bytes <= mem::size_of_val(buffer.as_slice()));
unsafe {
let info = buffer
.as_mut_ptr()
.cast::<u8>()
.add(offset)
.cast::<FILE_ID_BOTH_DIR_INFO>();
(*info).NextEntryOffset = if index + 1 == names.len() {
0
} else {
record_bytes as u32
};
(*info).FileNameLength = mem::size_of_val(name.as_slice()) as u32;
ptr::copy_nonoverlapping(
name.as_ptr(),
(&raw mut (*info).FileName).cast::<u16>(),
name.len(),
);
}
offset += record_bytes;
}
buffer
}
#[test]
fn injected_native_pages_drain_then_continue_without_restarting_or_requerying_eof() {
let mut replies = std::collections::VecDeque::from([
(Query::Restart, Some(page(&[".", "..", "a", "b"]))),
(Query::Continue, Some(page(&["c"]))),
(Query::Continue, None),
]);
let mut entries = ReadDir::new(move |query, buffer: &mut [u64]| {
let (expected, response) = replies.pop_front().expect("query after end-of-directory");
assert_eq!(query, expected, "native query restart/continuation request");
match response {
Some(page) => {
buffer.copy_from_slice(&page);
Ok(())
}
None => Err(io::Error::from_raw_os_error(ERROR_NO_MORE_FILES as i32)),
}
});
let observed: Vec<_> = entries
.by_ref()
.map(|entry| entry.unwrap().file_name())
.collect();
assert_eq!(
observed,
[
OsString::from("a"),
OsString::from("b"),
OsString::from("c")
]
);
assert!(entries.next().is_none());
assert!(entries.next().is_none());
}
#[test]
fn injected_native_query_errors_and_malformed_records_end_iteration() {
let mut query_error = ReadDir::new(|_, _: &mut [u64]| {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"query refused",
))
});
assert_eq!(
query_error.next().unwrap().err().unwrap().kind(),
io::ErrorKind::PermissionDenied
);
assert!(query_error.next().is_none());
assert!(query_error.next().is_none());
for length in [3, mem::size_of::<u64>() * DIRECTORY_BUFFER_WORDS + 2] {
let mut response = page(&["a"]);
unsafe {
(*response.as_mut_ptr().cast::<FILE_ID_BOTH_DIR_INFO>()).FileNameLength =
length as u32;
}
let mut response = Some(response);
let mut entries = ReadDir::new(move |_, buffer: &mut [u64]| {
buffer.copy_from_slice(&response.take().expect("query after malformed record"));
Ok(())
});
assert_eq!(
entries.next().unwrap().err().unwrap().to_string(),
"malformed directory listing"
);
assert!(entries.next().is_none());
assert!(entries.next().is_none());
}
let mut response = page(&["a"]);
unsafe {
(*response.as_mut_ptr().cast::<FILE_ID_BOTH_DIR_INFO>()).NextEntryOffset = 1;
}
let mut response = Some(response);
let mut entries = ReadDir::new(move |_, buffer: &mut [u64]| {
buffer.copy_from_slice(&response.take().expect("query after malformed offset"));
Ok(())
});
assert_eq!(entries.next().unwrap().unwrap().file_name(), "a");
assert_eq!(
entries.next().unwrap().err().unwrap().to_string(),
"malformed directory listing"
);
assert!(entries.next().is_none());
assert!(entries.next().is_none());
}
}