dirwalk 1.1.1

Platform-optimized recursive directory walker with metadata
Documentation
use crate::entry::Entry;
use crate::error::Error;
use crate::walk::StorageHint;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::Foundation::FILETIME;
use windows::Win32::Storage::FileSystem::{
    FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_REPARSE_POINT,
    FIND_FIRST_EX_LARGE_FETCH, FindClose, FindExInfoBasic, FindExSearchNameMatch, FindFirstFileExW,
    FindNextFileW, WIN32_FIND_DATAW,
};
use windows::core::PCWSTR;

pub fn scan_dir_platform(
    path: &Path,
    prefix: &str,
    hint: StorageHint,
) -> Result<Vec<Entry>, Error> {
    let mut entries = Vec::with_capacity(32);

    let wide_path: Vec<u16> = path.as_os_str().encode_wide().collect();

    // Long path support: MAX_PATH is 260; reserve 2 for "\\*" suffix → threshold is 258.
    // Use 248 to stay well clear of edge cases with multibyte encodings.
    // Capacity: path + optional 4-unit "\\?\" prefix + 3-unit "\*\0" suffix = +7 max.
    let mut wide_pattern = Vec::with_capacity(wide_path.len() + 7);
    if wide_path.len() > 248 {
        wide_pattern.extend_from_slice(&[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16]);
    }
    wide_pattern.extend_from_slice(&wide_path);
    wide_pattern.extend_from_slice(&[b'\\' as u16, b'*' as u16, 0]);

    unsafe {
        let mut find_data: WIN32_FIND_DATAW = std::mem::zeroed();
        let flags = if hint == StorageHint::Network {
            FIND_FIRST_EX_LARGE_FETCH
        } else {
            Default::default()
        };
        let handle = FindFirstFileExW(
            PCWSTR(wide_pattern.as_ptr()),
            FindExInfoBasic,
            &mut find_data as *mut WIN32_FIND_DATAW as *mut _,
            FindExSearchNameMatch,
            None,
            flags,
        )
        .map_err(|_| Error::Io {
            path: path.to_path_buf(),
            source: std::io::Error::last_os_error(),
        })?;

        loop {
            if !is_dot_or_dotdot(&find_data.cFileName) {
                let attrs = find_data.dwFileAttributes;
                let is_dir = (attrs & FILE_ATTRIBUTE_DIRECTORY.0) != 0;
                let is_symlink = (attrs & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0;
                let is_hidden = (attrs & FILE_ATTRIBUTE_HIDDEN.0) != 0;

                let size = if is_symlink {
                    0
                } else {
                    ((find_data.nFileSizeHigh as u64) << 32) | (find_data.nFileSizeLow as u64)
                };
                let modified = filetime_to_unix(&find_data.ftLastWriteTime);

                let relative_path = wchar_to_string(&find_data.cFileName, prefix);

                entries.push(Entry {
                    relative_path,
                    depth: 0,
                    size,
                    is_dir,
                    is_symlink,
                    is_hidden,
                    modified,
                });
            }

            if FindNextFileW(handle, &mut find_data).is_err() {
                break;
            }
        }

        let _ = FindClose(handle);
    }

    Ok(entries)
}

fn is_dot_or_dotdot(wchars: &[u16]) -> bool {
    (wchars[0] == b'.' as u16 && wchars[1] == 0)
        || (wchars[0] == b'.' as u16 && wchars[1] == b'.' as u16 && wchars[2] == 0)
}

/// Convert a null-terminated wide string to UTF-8, optionally prepending `prefix + MAIN_SEPARATOR`.
/// Builds the result in a single allocation to avoid an intermediate filename-only String.
fn wchar_to_string(wchars: &[u16], prefix: &str) -> String {
    let name_len = wchars.iter().position(|&c| c == 0).unwrap_or(wchars.len());
    let wchars = &wchars[..name_len];

    if prefix.is_empty() {
        return String::from_utf16_lossy(wchars);
    }

    // prefix + separator + name in one allocation.
    // Each UTF-16 code unit expands to at most 3 UTF-8 bytes (4 for surrogates, but
    // filename components rarely contain them — with_capacity is a lower bound anyway).
    let mut s = String::with_capacity(prefix.len() + 1 + name_len);
    s.push_str(prefix);
    s.push(std::path::MAIN_SEPARATOR);
    for c in char::decode_utf16(wchars.iter().copied()) {
        s.push(c.unwrap_or(char::REPLACEMENT_CHARACTER));
    }
    s
}

fn filetime_to_unix(ft: &FILETIME) -> i64 {
    // FILETIME: 100ns intervals since 1601-01-01
    // Unix epoch: 1970-01-01. Difference: 11644473600 seconds.
    const EPOCH_DIFF: i64 = 11644473600;
    const TICKS_PER_SECOND: i64 = 10_000_000;

    let ticks = (((ft.dwHighDateTime as u64) << 32) | ft.dwLowDateTime as u64) as i64;
    (ticks / TICKS_PER_SECOND) - EPOCH_DIFF
}