sysprims_core/error.rs
1//! Error types for sysprims operations.
2//!
3//! This module defines the error taxonomy per ADR-0008:
4//! - [`SysprimsError`] - Canonical error type for all sysprims operations
5//!
6//! ## Design Principles
7//!
8//! - **Structured**: Errors carry typed context (pid, operation) not just messages
9//! - **FFI-friendly**: Maps cleanly to error codes for C-ABI
10//! - **ABI-aligned**: Uses `u32` for PIDs (unsigned for cross-platform consistency)
11//! - **Secure**: No sensitive information (paths, credentials) in error messages
12//!
13//! See ADR-0008 for the full error handling strategy.
14
15use std::io;
16use thiserror::Error;
17
18// ============================================================================
19// Canonical Error Type (per ADR-0008)
20// ============================================================================
21
22/// Canonical error type for all sysprims operations.
23///
24/// This is the single error type used across the sysprims ecosystem. It maps
25/// cleanly to FFI error codes and provides structured context for programmatic
26/// handling.
27///
28/// ## FFI Error Code Mapping
29///
30/// | Variant | FFI Code |
31/// |---------|----------|
32/// | `InvalidArgument` | `SYSPRIMS_ERR_INVALID_ARGUMENT` (1) |
33/// | `SpawnFailed` | `SYSPRIMS_ERR_SPAWN_FAILED` (2) |
34/// | `Timeout` | `SYSPRIMS_ERR_TIMEOUT` (3) |
35/// | `PermissionDenied` | `SYSPRIMS_ERR_PERMISSION_DENIED` (4) |
36/// | `NotFound` | `SYSPRIMS_ERR_NOT_FOUND` (5) |
37/// | `NotSupported` | `SYSPRIMS_ERR_NOT_SUPPORTED` (6) |
38/// | `GroupCreationFailed` | `SYSPRIMS_ERR_GROUP_CREATION_FAILED` (7) |
39/// | `System` | `SYSPRIMS_ERR_SYSTEM` (8) |
40/// | `Internal` | `SYSPRIMS_ERR_INTERNAL` (99) |
41#[derive(Debug, Error)]
42pub enum SysprimsError {
43 /// Invalid argument provided.
44 ///
45 /// Returned when input validation fails (e.g., pid = 0, empty command).
46 #[error("Invalid argument: {message}")]
47 InvalidArgument {
48 /// Description of what was invalid.
49 message: String,
50 },
51
52 /// Failed to spawn a child process.
53 ///
54 /// Wraps the underlying IO error from process creation.
55 #[error("Failed to spawn process: {source}")]
56 SpawnFailed {
57 /// The underlying IO error.
58 #[source]
59 source: io::Error,
60 },
61
62 /// Operation timed out.
63 ///
64 /// The child process did not complete within the deadline.
65 #[error("Operation timed out")]
66 Timeout,
67
68 /// Permission denied for the operation.
69 ///
70 /// Typically returned when signaling a process owned by another user.
71 #[error("Permission denied for '{operation}' on PID {pid}")]
72 PermissionDenied {
73 /// The process ID we attempted to operate on.
74 pid: u32,
75 /// The operation that was denied (e.g., "terminate", "signal").
76 operation: String,
77 },
78
79 /// Target process not found.
80 ///
81 /// The specified PID does not exist or has already exited.
82 #[error("Process {pid} not found")]
83 NotFound {
84 /// The process ID that was not found.
85 pid: u32,
86 },
87
88 /// Command not found.
89 ///
90 /// The specified command could not be found in PATH.
91 #[error("Command '{command}' not found")]
92 NotFoundCommand {
93 /// The command that was not found.
94 command: String,
95 },
96
97 /// Permission denied for command execution.
98 ///
99 /// The specified command exists but cannot be executed (e.g., not executable).
100 #[error("Permission denied: cannot execute '{command}'")]
101 PermissionDeniedCommand {
102 /// The command that could not be executed.
103 command: String,
104 },
105
106 /// Permission denied for a filesystem path.
107 ///
108 /// Used when opening caller-provided paths such as nohup output targets.
109 #[error("Permission denied for '{operation}' on path '{path}'")]
110 PermissionDeniedPath {
111 /// The path that could not be accessed.
112 path: String,
113 /// The operation that was denied.
114 operation: String,
115 },
116
117 /// Operation not supported on the current platform.
118 ///
119 /// Some operations are platform-specific (e.g., `killpg` on Windows).
120 #[error("Operation '{feature}' not supported on {platform}")]
121 NotSupported {
122 /// The feature that is not supported.
123 feature: String,
124 /// The platform where it's not supported.
125 platform: String,
126 },
127
128 /// Failed to create process group or job object.
129 ///
130 /// On Unix, this means `setpgid()` failed.
131 /// On Windows, this means Job Object creation failed.
132 #[error("Failed to create process group: {message}")]
133 GroupCreationFailed {
134 /// Description of what failed.
135 message: String,
136 },
137
138 /// System-level error with errno/GetLastError context.
139 ///
140 /// Used when a syscall fails with an unexpected error code.
141 #[error("System error: {message} (errno: {errno})")]
142 System {
143 /// Description of the error.
144 message: String,
145 /// The errno value (Unix) or GetLastError (Windows).
146 errno: i32,
147 },
148
149 /// Internal error (should not happen in normal operation).
150 ///
151 /// Indicates a bug in sysprims or unexpected system state.
152 #[error("Internal error: {message}")]
153 Internal {
154 /// Description of the internal error.
155 message: String,
156 },
157}
158
159impl SysprimsError {
160 /// Get the FFI error code for this error.
161 ///
162 /// Maps to `SysprimsErrorCode` enum in C-ABI.
163 pub fn error_code(&self) -> i32 {
164 match self {
165 SysprimsError::InvalidArgument { .. } => 1,
166 SysprimsError::SpawnFailed { .. } => 2,
167 SysprimsError::Timeout => 3,
168 SysprimsError::PermissionDenied { .. } => 4,
169 SysprimsError::PermissionDeniedCommand { .. } => 4,
170 SysprimsError::PermissionDeniedPath { .. } => 4,
171 SysprimsError::NotFound { .. } => 5,
172 SysprimsError::NotFoundCommand { .. } => 5,
173 SysprimsError::NotSupported { .. } => 6,
174 SysprimsError::GroupCreationFailed { .. } => 7,
175 SysprimsError::System { .. } => 8,
176 SysprimsError::Internal { .. } => 99,
177 }
178 }
179}
180
181// ============================================================================
182// Convenience Constructors
183// ============================================================================
184
185impl SysprimsError {
186 /// Create an `InvalidArgument` error.
187 pub fn invalid_argument(message: impl Into<String>) -> Self {
188 SysprimsError::InvalidArgument {
189 message: message.into(),
190 }
191 }
192
193 /// Create a `SpawnFailed` error from an IO error.
194 pub fn spawn_failed_io(source: io::Error) -> Self {
195 SysprimsError::SpawnFailed { source }
196 }
197
198 /// Create a `PermissionDenied` error.
199 pub fn permission_denied(pid: u32, operation: impl Into<String>) -> Self {
200 SysprimsError::PermissionDenied {
201 pid,
202 operation: operation.into(),
203 }
204 }
205
206 /// Create a `NotFound` error.
207 pub fn not_found(pid: u32) -> Self {
208 SysprimsError::NotFound { pid }
209 }
210
211 /// Create a `NotFoundCommand` error.
212 pub fn not_found_command(command: impl Into<String>) -> Self {
213 SysprimsError::NotFoundCommand {
214 command: command.into(),
215 }
216 }
217
218 /// Create a `PermissionDeniedCommand` error.
219 pub fn permission_denied_command(command: impl Into<String>) -> Self {
220 SysprimsError::PermissionDeniedCommand {
221 command: command.into(),
222 }
223 }
224
225 /// Create a path-oriented permission denied error.
226 pub fn permission_denied_path(path: impl Into<String>, operation: impl Into<String>) -> Self {
227 SysprimsError::PermissionDeniedPath {
228 path: path.into(),
229 operation: operation.into(),
230 }
231 }
232
233 /// Create a `SpawnFailed` error with a command and reason.
234 pub fn spawn_failed(command: impl Into<String>, reason: impl Into<String>) -> Self {
235 let msg = format!("{}: {}", command.into(), reason.into());
236 SysprimsError::SpawnFailed {
237 source: io::Error::other(msg),
238 }
239 }
240
241 /// Create a `NotSupported` error.
242 pub fn not_supported(feature: impl Into<String>, platform: impl Into<String>) -> Self {
243 SysprimsError::NotSupported {
244 feature: feature.into(),
245 platform: platform.into(),
246 }
247 }
248
249 /// Create a `GroupCreationFailed` error.
250 pub fn group_creation_failed(message: impl Into<String>) -> Self {
251 SysprimsError::GroupCreationFailed {
252 message: message.into(),
253 }
254 }
255
256 /// Create a `System` error.
257 pub fn system(message: impl Into<String>, errno: i32) -> Self {
258 SysprimsError::System {
259 message: message.into(),
260 errno,
261 }
262 }
263
264 /// Create an `Internal` error.
265 pub fn internal(message: impl Into<String>) -> Self {
266 SysprimsError::Internal {
267 message: message.into(),
268 }
269 }
270}
271
272// ============================================================================
273// Conversions
274// ============================================================================
275
276impl From<io::Error> for SysprimsError {
277 fn from(source: io::Error) -> Self {
278 // Map common IO errors to structured variants
279 match source.kind() {
280 io::ErrorKind::NotFound => SysprimsError::Internal {
281 message: format!("IO not found: {}", source),
282 },
283 io::ErrorKind::PermissionDenied => SysprimsError::Internal {
284 message: format!("IO permission denied: {}", source),
285 },
286 _ => SysprimsError::SpawnFailed { source },
287 }
288 }
289}
290
291// ============================================================================
292// Result Type Alias
293// ============================================================================
294
295/// Result type alias for sysprims operations.
296pub type SysprimsResult<T> = Result<T, SysprimsError>;
297
298// ============================================================================
299// Tests
300// ============================================================================
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_error_display() {
308 let err = SysprimsError::invalid_argument("pid must be > 0");
309 assert_eq!(err.to_string(), "Invalid argument: pid must be > 0");
310
311 let err = SysprimsError::permission_denied(1234, "terminate");
312 assert_eq!(
313 err.to_string(),
314 "Permission denied for 'terminate' on PID 1234"
315 );
316
317 let err = SysprimsError::not_found(5678);
318 assert_eq!(err.to_string(), "Process 5678 not found");
319
320 let err = SysprimsError::not_supported("killpg", "windows");
321 assert_eq!(
322 err.to_string(),
323 "Operation 'killpg' not supported on windows"
324 );
325
326 let err = SysprimsError::Timeout;
327 assert_eq!(err.to_string(), "Operation timed out");
328 }
329
330 #[test]
331 fn test_error_codes() {
332 assert_eq!(SysprimsError::invalid_argument("").error_code(), 1);
333 assert_eq!(
334 SysprimsError::spawn_failed_io(io::Error::other("test")).error_code(),
335 2
336 );
337 assert_eq!(SysprimsError::Timeout.error_code(), 3);
338 assert_eq!(SysprimsError::permission_denied(0, "").error_code(), 4);
339 assert_eq!(SysprimsError::not_found(0).error_code(), 5);
340 assert_eq!(SysprimsError::not_supported("", "").error_code(), 6);
341 assert_eq!(SysprimsError::group_creation_failed("").error_code(), 7);
342 assert_eq!(SysprimsError::system("", 0).error_code(), 8);
343 assert_eq!(SysprimsError::internal("").error_code(), 99);
344 }
345
346 #[test]
347 fn test_spawn_failed_source() {
348 let io_err = io::Error::new(io::ErrorKind::NotFound, "command not found");
349 let err = SysprimsError::spawn_failed_io(io_err);
350
351 // Verify source is accessible
352 match err {
353 SysprimsError::SpawnFailed { ref source } => {
354 assert_eq!(source.kind(), io::ErrorKind::NotFound);
355 }
356 _ => panic!("Expected SpawnFailed"),
357 }
358 }
359
360 #[test]
361 fn test_pid_is_u32() {
362 // Verify PIDs are unsigned (ABI alignment per ADR-0008)
363 let err = SysprimsError::permission_denied(u32::MAX, "signal");
364 match err {
365 SysprimsError::PermissionDenied { pid, .. } => {
366 assert_eq!(pid, u32::MAX);
367 }
368 _ => panic!("Expected PermissionDenied"),
369 }
370
371 let err = SysprimsError::not_found(u32::MAX);
372 match err {
373 SysprimsError::NotFound { pid } => {
374 assert_eq!(pid, u32::MAX);
375 }
376 _ => panic!("Expected NotFound"),
377 }
378 }
379
380 #[test]
381 fn test_io_error_conversion() {
382 let io_err = io::Error::other("test error");
383 let sysprims_err: SysprimsError = io_err.into();
384
385 match sysprims_err {
386 SysprimsError::SpawnFailed { source } => {
387 assert_eq!(source.kind(), io::ErrorKind::Other);
388 }
389 _ => panic!("Expected SpawnFailed from IO error"),
390 }
391 }
392}