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}