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}