Skip to main content

path_rs/
inspect.rs

1//! Directory existence and inspection helpers.
2//!
3//! These APIs intentionally document symlink and metadata semantics so callers
4//! do not rely on ambiguous convenience methods alone.
5
6use crate::error::PathError;
7use crate::internal::validation::reject_nul_path;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12/// Summary of filesystem metadata without exposing full `std::fs::Metadata`.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct MetadataSummary {
15    /// File size in bytes (for files).
16    pub len: u64,
17    /// Whether the entry is read-only according to permissions.
18    pub readonly: bool,
19    /// Modification time when available.
20    pub modified: Option<SystemTime>,
21}
22
23/// Result of inspecting a path as a potential directory.
24///
25/// # Symlink semantics
26///
27/// - [`Path::is_dir`] follows symlinks.
28/// - `symlink_metadata` inspects the link itself.
29/// - `metadata` follows symlinks.
30/// - Broken symlinks are not directories.
31/// - Permission errors may surface as `exists: false` only in convenience
32///   booleans; prefer [`inspect_directory`] / [`require_directory`] for errors.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DirectoryInspection {
35    /// Path that was inspected.
36    pub path: PathBuf,
37    /// Whether the path exists (symlink target may not, if inspecting a link).
38    pub exists: bool,
39    /// Whether the path is a directory (symlink-to-dir counts when following).
40    pub is_directory: bool,
41    /// Whether the path itself is a symbolic link.
42    pub is_symlink: bool,
43    /// Best-effort readability probe (`None` if not checked / not applicable).
44    pub is_readable: Option<bool>,
45    /// Optional metadata summary.
46    pub metadata: Option<MetadataSummary>,
47}
48
49/// Returns `true` if `path` exists and is a directory.
50///
51/// Uses `std::fs::metadata` (follows symlinks). Permission errors and missing
52/// paths yield `false` — this is a convenience wrapper, not a strict probe.
53///
54/// Equivalent in intent to:
55///
56/// ```ignore
57/// path.is_dir()
58///     || fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
59/// ```
60///
61/// # Filesystem access
62///
63/// **Yes.** Follows symlinks. Does not populate a cache.
64///
65/// Prefer [`inspect_directory`] when you need error context.
66pub fn directory_exists(path: impl AsRef<Path>) -> bool {
67    let path = path.as_ref();
68    if path.is_dir() {
69        return true;
70    }
71    fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
72}
73
74/// Alias for [`directory_exists`].
75#[inline]
76pub fn is_existing_directory(path: impl AsRef<Path>) -> bool {
77    directory_exists(path)
78}
79
80/// Inspect a path without panicking on I/O errors.
81///
82/// Uses `symlink_metadata` for the link itself and, when the path is a symlink,
83/// also checks whether the target is a directory via `metadata`.
84///
85/// # Filesystem access
86///
87/// **Yes.** Does not follow symlinks for `is_symlink`; `is_directory` is true
88/// for symlink-to-directory when the target is reachable.
89pub fn inspect_directory(path: impl AsRef<Path>) -> Result<DirectoryInspection, PathError> {
90    let path = path.as_ref();
91    reject_nul_path(path)?;
92    let path_buf = path.to_path_buf();
93
94    let symlink_meta = match fs::symlink_metadata(path) {
95        Ok(m) => m,
96        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
97            return Ok(DirectoryInspection {
98                path: path_buf,
99                exists: false,
100                is_directory: false,
101                is_symlink: false,
102                is_readable: None,
103                metadata: None,
104            });
105        }
106        Err(e) => return Err(PathError::filesystem(path, e)),
107    };
108
109    let is_symlink = symlink_meta.file_type().is_symlink();
110    let is_directory = if is_symlink {
111        fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
112    } else {
113        symlink_meta.is_dir()
114    };
115
116    let is_readable = if is_directory {
117        Some(fs::read_dir(path).is_ok())
118    } else {
119        None
120    };
121
122    let metadata = Some(MetadataSummary {
123        len: symlink_meta.len(),
124        readonly: symlink_meta.permissions().readonly(),
125        modified: symlink_meta.modified().ok(),
126    });
127
128    Ok(DirectoryInspection {
129        path: path_buf,
130        exists: true,
131        is_directory,
132        is_symlink,
133        is_readable,
134        metadata,
135    })
136}
137
138/// Require that `path` exists and is a directory; return its path on success.
139///
140/// # Filesystem access
141///
142/// **Yes.** Follows symlinks for the directory check.
143///
144/// # Errors
145///
146/// Returns a contextual filesystem or invalid-path error when the path is
147/// missing, not a directory, or inaccessible.
148pub fn require_directory(path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
149    let path = path.as_ref();
150    reject_nul_path(path)?;
151    let inspection = inspect_directory(path)?;
152    if !inspection.exists {
153        return Err(PathError::invalid(format!(
154            "path does not exist: {}",
155            path.to_string_lossy()
156        )));
157    }
158    if !inspection.is_directory {
159        return Err(PathError::invalid(format!(
160            "path is not a directory: {}",
161            path.to_string_lossy()
162        )));
163    }
164    Ok(path.to_path_buf())
165}