use std::path::Path;
use std::fs::FileType;
use tokio::fs::DirEntry;
pub async fn list_dir_entries(path: &Path) -> Vec<DirEntry> {
let mut entries = match tokio::fs::read_dir(path).await {
Ok(rd) => rd,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
while let Ok(Some(entry)) = entries.next_entry().await {
out.push(entry);
}
out
}
pub async fn entry_is_dir(entry: &DirEntry) -> bool {
tokio::fs::metadata(entry.path())
.await
.map(|m| m.is_dir())
.unwrap_or(false)
}
pub async fn entry_file_type(entry: &DirEntry) -> Option<FileType> {
entry.file_type().await.ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn list_dir_entries_empty_dir() {
let tmp = tempfile::tempdir().unwrap();
let entries = list_dir_entries(tmp.path()).await;
assert!(entries.is_empty());
}
#[tokio::test]
async fn list_dir_entries_missing_path_returns_empty() {
let tmp = tempfile::tempdir().unwrap();
let entries = list_dir_entries(&tmp.path().join("does-not-exist")).await;
assert!(entries.is_empty());
}
#[tokio::test]
async fn list_dir_entries_returns_children() {
let tmp = tempfile::tempdir().unwrap();
tokio::fs::create_dir(tmp.path().join("a")).await.unwrap();
tokio::fs::create_dir(tmp.path().join("b")).await.unwrap();
tokio::fs::write(tmp.path().join("c.txt"), b"")
.await
.unwrap();
let mut names: Vec<String> = list_dir_entries(tmp.path())
.await
.into_iter()
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
names.sort();
assert_eq!(names, vec!["a", "b", "c.txt"]);
}
#[tokio::test]
async fn entry_is_dir_distinguishes_dir_and_file() {
let tmp = tempfile::tempdir().unwrap();
tokio::fs::create_dir(tmp.path().join("d")).await.unwrap();
tokio::fs::write(tmp.path().join("f"), b"x").await.unwrap();
let entries = list_dir_entries(tmp.path()).await;
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = entry_is_dir(&entry).await;
match name.as_str() {
"d" => assert!(is_dir),
"f" => assert!(!is_dir),
other => panic!("unexpected entry: {other}"),
}
}
}
#[cfg(unix)]
#[tokio::test]
async fn entry_is_dir_follows_symlink_to_dir() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real_dir");
tokio::fs::create_dir(&target).await.unwrap();
tokio::fs::symlink(&target, tmp.path().join("link_to_dir"))
.await
.unwrap();
let entries = list_dir_entries(tmp.path()).await;
let link = entries
.into_iter()
.find(|e| e.file_name().to_string_lossy() == "link_to_dir")
.expect("symlink entry present");
assert!(
entry_is_dir(&link).await,
"symlink pointing at a directory must resolve to is_dir = true"
);
}
#[cfg(unix)]
#[tokio::test]
async fn entry_is_dir_symlink_to_file_and_broken_link() {
let tmp = tempfile::tempdir().unwrap();
let file_target = tmp.path().join("real_file");
tokio::fs::write(&file_target, b"x").await.unwrap();
tokio::fs::symlink(&file_target, tmp.path().join("link_to_file"))
.await
.unwrap();
tokio::fs::symlink(
tmp.path().join("missing_target"),
tmp.path().join("dangling"),
)
.await
.unwrap();
for entry in list_dir_entries(tmp.path()).await {
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = entry_is_dir(&entry).await;
match name.as_str() {
"real_file" | "link_to_file" | "dangling" => {
assert!(!is_dir, "{name} should not be a dir");
}
other => panic!("unexpected entry: {other}"),
}
}
}
#[cfg(unix)]
#[tokio::test]
async fn entry_file_type_does_not_follow_symlinks() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real_dir");
tokio::fs::create_dir(&target).await.unwrap();
tokio::fs::symlink(&target, tmp.path().join("link_to_dir"))
.await
.unwrap();
let entries = list_dir_entries(tmp.path()).await;
let link = entries
.into_iter()
.find(|e| e.file_name().to_string_lossy() == "link_to_dir")
.expect("symlink entry present");
let ft = entry_file_type(&link).await.expect("file_type available");
assert!(
ft.is_symlink(),
"entry_file_type must surface the link kind"
);
assert!(!ft.is_dir(), "entry_file_type must not resolve the target");
}
}