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.path().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
50/// }
51/// }
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum AppPathError {
55 /// Failed to determine the current executable path.
56 ///
57 /// This error occurs when [`std::env::current_exe()`] fails, which is rare
58 /// but can happen in some embedded or heavily sandboxed environments.
59 ExecutableNotFound(String),
60
61 /// Executable path is empty or invalid.
62 ///
63 /// This error occurs when the system returns an empty executable path,
64 /// which is extremely rare and indicates a corrupted or broken system.
65 InvalidExecutablePath(String),
66
67 /// An I/O operation failed.
68 ///
69 /// This error occurs when filesystem operations fail, such as:
70 /// - Creating directories fails due to permissions
71 /// - Disk space is insufficient
72 /// - Path contains invalid characters for the filesystem
73 /// - Network filesystem issues
74 IoError(String),
75}
76
77impl std::fmt::Display for AppPathError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 AppPathError::ExecutableNotFound(msg) => {
81 write!(f, "Failed to determine executable location: {msg}")
82 }
83 AppPathError::InvalidExecutablePath(msg) => {
84 write!(f, "Invalid executable path: {msg}")
85 }
86 AppPathError::IoError(msg) => {
87 write!(f, "I/O operation failed: {msg}")
88 }
89 }
90 }
91}
92
93impl std::error::Error for AppPathError {}
94
95impl From<std::io::Error> for AppPathError {
96 fn from(err: std::io::Error) -> Self {
97 AppPathError::IoError(err.to_string())
98 }
99}
100
101/// Creates an IoError with path context for better debugging.
102///
103/// This implementation adds the file path to I/O error messages, making it easier
104/// to identify which path caused the failure in complex directory operations.
105///
106/// # Examples
107///
108/// ```rust
109/// use app_path::AppPathError;
110/// use std::path::PathBuf;
111///
112/// let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
113/// let path = PathBuf::from("/some/restricted/path");
114/// let app_error = AppPathError::from((io_error, &path));
115///
116/// // Error message includes both the original error and the path
117/// assert!(app_error.to_string().contains("access denied"));
118/// assert!(app_error.to_string().contains("/some/restricted/path"));
119/// ```
120impl From<(std::io::Error, &PathBuf)> for AppPathError {
121 fn from((err, path): (std::io::Error, &PathBuf)) -> Self {
122 AppPathError::IoError(format!("{err} (path: {})", path.display()))
123 }
124}
125
126/// Try to determine the executable directory (fallible version).
127///
128/// This is the internal fallible initialization function that both the fallible
129/// and infallible APIs use. It handles all the edge cases properly without
130/// exposing them as errors to API users.
131pub(crate) fn try_exe_dir_init() -> Result<PathBuf, AppPathError> {
132 let exe = current_exe().map_err(|e| {
133 AppPathError::ExecutableNotFound(format!(
134 "std::env::current_exe() failed: {e} (environment: {})",
135 std::env::var("OS").unwrap_or_else(|_| "unknown OS".to_string())
136 ))
137 })?;
138
139 if exe.as_os_str().is_empty() {
140 return Err(AppPathError::InvalidExecutablePath(format!(
141 "Executable path is empty - unsupported environment (process id: {})",
142 std::process::id()
143 )));
144 }
145
146 // Handle edge case: executable at filesystem root (jailed environments, etc.)
147 // This is NOT an error - it's a valid case that should be handled internally
148 let dir = match exe.parent() {
149 Some(parent) => parent.to_path_buf(),
150 None => {
151 // If exe has no parent (e.g., running as "/init" or "C:\myapp.exe"),
152 // use the root directory itself
153 exe.ancestors().last().unwrap_or(&exe).to_path_buf()
154 }
155 };
156
157 Ok(dir)
158}