ntfs_mac_core/error.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 kodephp contributors
3
4//! Typed errors + POSIX-style exit codes used by the CLI/GUI layer.
5
6use std::path::PathBuf;
7use std::time::Duration;
8
9use thiserror::Error;
10
11/// Exit code surface for the CLI. Kept small and stable so scripts can
12/// branch on it without parsing stderr.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum ExitCode {
16 /// Command succeeded.
17 Ok = 0,
18 /// Generic failure (I/O, unexpected stderr).
19 Failure = 1,
20 /// Missing or unsatisfiable dependency (`ntfs-3g`, macFUSE/FUSE-T).
21 MissingDependency = 2,
22 /// User declined an interactive prompt (Ctrl-C, "no" to confirm).
23 UserCancelled = 3,
24 /// Confirmation phrase did not match.
25 ConfirmationMismatch = 4,
26 /// Input/argument validation error (unknown device, invalid label).
27 InvalidArgument = 5,
28}
29
30impl From<ExitCode> for u8 {
31 fn from(e: ExitCode) -> Self {
32 e as u8
33 }
34}
35
36/// Library error type. Every variant carries enough context that a
37/// user-facing message can be derived without re-running the failing
38/// command.
39#[derive(Debug, Error)]
40pub enum Error {
41 /// A subprocess exited non-zero. `stderr` is preserved verbatim
42 /// so the caller can show the underlying tool's diagnosis.
43 #[error("command `{cmd}` exited with status {status}: {stderr}")]
44 CommandFailed {
45 cmd: String,
46 status: i32,
47 stderr: String,
48 #[source]
49 io: Option<std::io::Error>,
50 },
51
52 /// The subprocess exceeded its configured timeout and had to be
53 /// terminated. Distinct from [`Error::CommandFailed`] because the
54 /// tool produced no diagnosis of its own: `fsck_ntfs` running out
55 /// of time is a different failure mode than `fsck_ntfs` reporting
56 /// corruption, and the fix for each is different (raise the limit
57 /// or fix the volume).
58 #[error("command `{cmd}` timed out after {timeout:?}")]
59 CommandTimedOut { cmd: String, timeout: Duration },
60
61 /// The target binary is not on PATH. `hint` is a copy-pastable
62 /// fix instruction.
63 #[error("missing dependency `{binary}`: {detail}")]
64 MissingDependency {
65 binary: String,
66 detail: String,
67 hint: Option<String>,
68 #[source]
69 io: Option<std::io::Error>,
70 },
71
72 /// Config file could not be read or written.
73 #[error("config error: {0}")]
74 Config(#[from] ConfigError),
75
76 /// The parsed `diskutil` output did not contain an NTFS volume
77 /// matching the user's request.
78 #[error("no matching NTFS volume for `{pattern}`")]
79 NoMatch { pattern: String },
80
81 /// Confirmation phrase did not match.
82 #[error("confirmation mismatch: expected `{expected}`, got `{actual}`")]
83 ConfirmationMismatch { expected: String, actual: String },
84
85 /// User pressed Ctrl-C or answered "no" to a confirmation prompt.
86 #[error("user cancelled")]
87 Cancelled,
88
89 /// Argument validation failure (empty label, unknown device, ...).
90 #[error("invalid argument: {0}")]
91 InvalidArgument(String),
92
93 /// Underlying I/O error that does not fit into a more specific
94 /// variant. Kept narrow so `?` from `std::fs::read_to_string` and
95 /// the like still works.
96 #[error("i/o error: {0}")]
97 Io(#[from] std::io::Error),
98
99 /// Serialization / parsing error (plist, toml, json).
100 #[error("serialization error: {0}")]
101 Serde(String),
102}
103
104#[derive(Debug, Error)]
105pub enum ConfigError {
106 #[error("failed to read config at {path}: {reason}")]
107 Read { path: PathBuf, reason: String },
108 #[error("failed to write config to {path}: {reason}")]
109 Write { path: PathBuf, reason: String },
110 #[error("config parse error at {path}: {reason}")]
111 Parse { path: PathBuf, reason: String },
112}
113
114impl From<ConfigError> for std::io::Error {
115 fn from(e: ConfigError) -> Self {
116 std::io::Error::other(e.to_string())
117 }
118}
119
120/// Library result alias.
121pub type Result<T> = std::result::Result<T, Error>;