use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FileMeta {
pub size: u64,
pub modified: SystemTime,
pub created: Option<SystemTime>,
pub accessed: Option<SystemTime>,
pub is_dir: bool,
pub is_file: bool,
pub is_symlink: bool,
pub readonly: bool,
pub permissions: Permissions,
}
impl FileMeta {
pub(crate) fn from_metadata(meta: &std::fs::Metadata, path: &Path) -> Self {
let is_symlink = std::fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
FileMeta {
size: meta.len(),
modified: meta.modified().unwrap_or(SystemTime::UNIX_EPOCH),
created: meta.created().ok(),
accessed: meta.accessed().ok(),
is_dir: meta.is_dir(),
is_file: meta.is_file(),
is_symlink,
readonly: meta.permissions().readonly(),
permissions: Permissions::from_metadata(meta),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DirEntry {
pub path: PathBuf,
pub name: OsString,
pub is_dir: bool,
pub is_file: bool,
pub is_symlink: bool,
}
impl DirEntry {
pub(crate) fn from_std(entry: std::fs::DirEntry) -> Self {
let file_type = entry.file_type().ok();
let is_dir = file_type.as_ref().map(|t| t.is_dir()).unwrap_or(false);
let is_file = file_type.as_ref().map(|t| t.is_file()).unwrap_or(false);
let is_symlink = file_type.as_ref().map(|t| t.is_symlink()).unwrap_or(false);
let path = entry.path();
let name = entry.file_name();
DirEntry {
path,
name,
is_dir,
is_file,
is_symlink,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Permissions {
pub readable: bool,
pub writable: bool,
pub executable: bool,
#[cfg(unix)]
pub mode: u32,
}
impl Permissions {
pub(crate) fn from_metadata(meta: &std::fs::Metadata) -> Self {
let perms = meta.permissions();
let readonly = perms.readonly();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = perms.mode();
Permissions {
readable: (mode & 0o400) != 0,
writable: !readonly,
executable: (mode & 0o100) != 0,
mode,
}
}
#[cfg(not(unix))]
{
Permissions {
readable: true, writable: !readonly,
executable: false, }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
struct TempFile(PathBuf);
impl TempFile {
fn new(content: &[u8]) -> Self {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("fsys_meta_test_{}_{}", std::process::id(), n));
let mut f = fs::File::create(&path).expect("create temp file");
f.write_all(content).expect("write temp file");
TempFile(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
#[test]
fn test_file_meta_from_metadata_reports_correct_size() {
let tmp = TempFile::new(b"hello world");
let meta = fs::metadata(tmp.path()).expect("metadata");
let fm = FileMeta::from_metadata(&meta, tmp.path());
assert_eq!(fm.size, 11);
}
#[test]
fn test_file_meta_is_file_true_for_regular_file() {
let tmp = TempFile::new(b"data");
let meta = fs::metadata(tmp.path()).expect("metadata");
let fm = FileMeta::from_metadata(&meta, tmp.path());
assert!(fm.is_file);
assert!(!fm.is_dir);
}
#[test]
fn test_file_meta_is_symlink_false_for_regular_file() {
let tmp = TempFile::new(b"data");
let meta = fs::metadata(tmp.path()).expect("metadata");
let fm = FileMeta::from_metadata(&meta, tmp.path());
assert!(!fm.is_symlink);
}
#[test]
fn test_permissions_writable_on_writable_file() {
let tmp = TempFile::new(b"data");
let meta = fs::metadata(tmp.path()).expect("metadata");
let perms = Permissions::from_metadata(&meta);
assert!(perms.writable);
}
#[test]
fn test_dir_entry_from_std_is_dir_for_directory() {
let dir = std::env::temp_dir();
if let Some(Ok(entry)) = fs::read_dir(&dir).ok().and_then(|mut d| d.next()) {
let de = DirEntry::from_std(entry);
let count = [de.is_dir, de.is_file, de.is_symlink]
.iter()
.filter(|&&b| b)
.count();
assert!(count <= 1, "at most one type flag should be set");
}
}
#[test]
fn test_permissions_executable_false_on_windows() {
#[cfg(windows)]
{
let tmp = TempFile::new(b"test");
let meta = fs::metadata(tmp.path()).expect("metadata");
let perms = Permissions::from_metadata(&meta);
assert!(!perms.executable, "Windows has no executable bit");
}
#[cfg(not(windows))]
{
let tmp = TempFile::new(b"test");
let meta = fs::metadata(tmp.path()).expect("metadata");
let _perms = Permissions::from_metadata(&meta);
}
}
#[test]
fn test_file_meta_modified_time_is_after_epoch() {
let tmp = TempFile::new(b"ts");
let meta = fs::metadata(tmp.path()).expect("metadata");
let fm = FileMeta::from_metadata(&meta, tmp.path());
assert!(fm.modified >= SystemTime::UNIX_EPOCH);
}
}