app_path/error.rs
1use std::env::current_exe;
2use std::path::PathBuf;
3
4/// Error type for AppPath operations.
5///
6/// This enum represents the possible failures that can occur when working with
7/// AppPath instances. These include both system-level failures and I/O errors.
8///
9/// # When These Errors Occur
10///
11/// - **`ExecutableNotFound`**: When [`std::env::current_exe()`] fails
12/// - Very rare, but can happen in some embedded or heavily sandboxed environments
13/// - May occur if the executable has been deleted while running
14/// - Can happen in some containerized environments with unusual configurations
15///
16/// - **`InvalidExecutablePath`**: When the executable path is empty
17/// - Extremely rare, indicates a corrupted or broken system
18/// - May occur with custom or non-standard program loaders
19///
20/// - **`IoError`**: When I/O operations fail
21/// - Directory creation fails due to insufficient permissions
22/// - Disk space issues or filesystem errors
23/// - Invalid path characters for the target filesystem
24/// - Network filesystem problems
25///
26/// System-level errors are typically unrecoverable for portable applications,
27/// while I/O errors may be recoverable depending on the specific cause.
28///
29/// # Examples
30///
31/// ```rust
32/// use app_path::{AppPath, AppPathError};
33///
34/// // Handle errors explicitly
35/// match AppPath::try_with("config.toml") {
36/// Ok(config) => {
37/// println!("Config path: {}", config.display());
38/// }
39/// Err(AppPathError::ExecutableNotFound(msg)) => {
40/// eprintln!("Cannot find executable: {msg}");
41/// // Fallback to alternative configuration
42/// }
43/// Err(AppPathError::InvalidExecutablePath(msg)) => {
44/// eprintln!("Invalid executable path: {msg}");
45/// // Fallback to alternative configuration
46/// }
47/// Err(AppPathError::IoError(io_err)) => {
48/// eprintln!("I/O operation failed: {io_err}");
49/// // Handle specific I/O error types
50/// match io_err.kind() {
51/// std::io::ErrorKind::PermissionDenied => {
52/// eprintln!("Permission denied - check file permissions");
53/// }
54/// std::io::ErrorKind::NotFound => {
55/// eprintln!("File or directory not found");
56/// }
57/// _ => eprintln!("Other I/O error: {io_err}"),
58/// }
59/// }
60/// }
61/// ```
62#[derive(Debug)]
63pub enum AppPathError {
64 /// Failed to determine the current executable path.
65 ///
66 /// This error occurs when [`std::env::current_exe()`] fails, which is rare
67 /// but can happen in some embedded or heavily sandboxed environments.
68 ExecutableNotFound(String),
69
70 /// Executable path is empty or invalid.
71 ///
72 /// This error occurs when the system returns an empty executable path,
73 /// which is extremely rare and indicates a corrupted or broken system.
74 InvalidExecutablePath(String),
75
76 /// An I/O operation failed.
77 ///
78 /// This error occurs when filesystem operations fail, such as:
79 /// - Creating directories fails due to permissions
80 /// - Disk space is insufficient
81 /// - Path contains invalid characters for the filesystem
82 /// - Network filesystem issues
83 ///
84 /// The original `std::io::Error` is preserved, allowing users to:
85 /// - Check specific error kinds (`error.kind()`)
86 /// - Access OS error codes (`error.raw_os_error()`)
87 /// - Handle different I/O errors appropriately
88 IoError(std::io::Error),
89}
90
91impl std::fmt::Display for AppPathError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 AppPathError::ExecutableNotFound(msg) => {
95 write!(f, "Failed to determine executable location: {msg}")
96 }
97 AppPathError::InvalidExecutablePath(msg) => {
98 write!(f, "Invalid executable path: {msg}")
99 }
100 AppPathError::IoError(err) => {
101 write!(f, "I/O operation failed: {err}")
102 }
103 }
104 }
105}
106
107impl std::error::Error for AppPathError {
108 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
109 match self {
110 AppPathError::IoError(err) => Some(err),
111 _ => None,
112 }
113 }
114}
115
116impl From<std::io::Error> for AppPathError {
117 fn from(err: std::io::Error) -> Self {
118 AppPathError::IoError(err)
119 }
120}
121
122/// Creates an IoError with path context for better debugging.
123///
124/// This implementation adds the file path to I/O error messages, making it easier
125/// to identify which path caused the failure in complex directory operations.
126///
127/// # Examples
128///
129/// ```rust
130/// use app_path::AppPathError;
131/// use std::path::PathBuf;
132///
133/// let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
134/// let path = PathBuf::from("/some/restricted/path");
135/// let app_error = AppPathError::from((io_error, &path));
136///
137/// // Error message includes both the original error and the path
138/// assert!(app_error.to_string().contains("access denied"));
139/// assert!(app_error.to_string().contains("/some/restricted/path"));
140/// ```
141impl From<(std::io::Error, &PathBuf)> for AppPathError {
142 fn from((err, path): (std::io::Error, &PathBuf)) -> Self {
143 // Create a new io::Error that includes path context in the message
144 let kind = err.kind();
145 let msg = format!("{err} (path: {})", path.display());
146 AppPathError::IoError(std::io::Error::new(kind, msg))
147 }
148}
149
150/// Try to determine the executable directory (fallible version).
151///
152/// This is the internal fallible initialization function that both the fallible
153/// and infallible APIs use. It handles all the edge cases properly without
154/// exposing them as errors to API users.
155pub(crate) fn try_exe_dir_init() -> Result<PathBuf, AppPathError> {
156 let exe = current_exe().map_err(|e| {
157 AppPathError::ExecutableNotFound(format!(
158 "std::env::current_exe() failed: {e} (environment: {})",
159 std::env::var("OS").unwrap_or_else(|_| "unknown OS".to_string())
160 ))
161 })?;
162
163 if exe.as_os_str().is_empty() {
164 return Err(AppPathError::InvalidExecutablePath(format!(
165 "Executable path is empty - unsupported environment (process id: {})",
166 std::process::id()
167 )));
168 }
169
170 // Handle edge case: executable at filesystem root (jailed environments, etc.)
171 // This is NOT an error - it's a valid case that should be handled internally
172 let dir = match exe.parent() {
173 Some(parent) => parent.to_path_buf(),
174 None => {
175 // If exe has no parent (e.g., running as "/init" or "C:\myapp.exe"),
176 // use the root directory itself
177 exe.ancestors().last().unwrap_or(&exe).to_path_buf()
178 }
179 };
180
181 Ok(dir)
182}