Skip to main content

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>;