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