Skip to main content

rightkit_tauri_shell/
error.rs

1//! Typed error envelope for shell commands that need category handling in the renderer.
2//!
3//! Donor: ViewRight `error.rs` (`{code, message}` with snake_case codes). The wire shape is
4//! `{"code": "broker_denied", "message": "..."}` and a Tauri command returning
5//! `Result<_, ShellError>` rejects its promise with exactly that object. Older shell commands
6//! still reject with plain strings; the bridge normalises both (`ShellError` in `@rightkit/tauri-shell`).
7
8use serde::Serialize;
9
10#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
11#[serde(tag = "code", content = "message", rename_all = "snake_case")]
12pub enum ShellError {
13    /// The path broker (or a stream/site token) denied access.
14    BrokerDenied(String),
15    FileNotFound(String),
16    /// The OS denied access (macOS TCC, folder ACLs, notification permission). User-facing text.
17    PermissionDenied(String),
18    UnsupportedFormat(String),
19    DocumentTooLarge(String),
20    Io(String),
21    Encoding(String),
22    WriteConflict(String),
23    /// A request was malformed (bad argument, bad snapshot, unknown version).
24    Invalid(String),
25    /// The capability does not exist on this platform / build.
26    Unsupported(String),
27    /// An OS call did not answer in time.
28    Timeout(String),
29    Other(String),
30}
31
32impl ShellError {
33    pub fn code(&self) -> &'static str {
34        match self {
35            Self::BrokerDenied(_) => "broker_denied",
36            Self::FileNotFound(_) => "file_not_found",
37            Self::PermissionDenied(_) => "permission_denied",
38            Self::UnsupportedFormat(_) => "unsupported_format",
39            Self::DocumentTooLarge(_) => "document_too_large",
40            Self::Io(_) => "io",
41            Self::Encoding(_) => "encoding",
42            Self::WriteConflict(_) => "write_conflict",
43            Self::Invalid(_) => "invalid",
44            Self::Unsupported(_) => "unsupported",
45            Self::Timeout(_) => "timeout",
46            Self::Other(_) => "other",
47        }
48    }
49    pub fn message(&self) -> &str {
50        match self {
51            Self::BrokerDenied(m)
52            | Self::FileNotFound(m)
53            | Self::PermissionDenied(m)
54            | Self::UnsupportedFormat(m)
55            | Self::DocumentTooLarge(m)
56            | Self::Io(m)
57            | Self::Encoding(m)
58            | Self::WriteConflict(m)
59            | Self::Invalid(m)
60            | Self::Unsupported(m)
61            | Self::Timeout(m)
62            | Self::Other(m) => m,
63        }
64    }
65}
66
67impl std::fmt::Display for ShellError {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        let label = match self {
70            Self::BrokerDenied(_) => "access denied",
71            Self::FileNotFound(_) => "file not found",
72            Self::PermissionDenied(_) => "permission denied",
73            Self::UnsupportedFormat(_) => "unsupported format",
74            Self::DocumentTooLarge(_) => "document too large",
75            Self::Io(_) => "I/O error",
76            Self::Encoding(_) => "encoding error",
77            Self::WriteConflict(_) => "write conflict",
78            Self::Invalid(_) => "invalid request",
79            Self::Unsupported(_) => "unsupported",
80            Self::Timeout(_) => "timed out",
81            Self::Other(_) => return write!(f, "{}", self.message()),
82        };
83        write!(f, "{label}: {}", self.message())
84    }
85}
86
87impl std::error::Error for ShellError {}
88
89impl From<String> for ShellError {
90    fn from(s: String) -> Self {
91        broker_error(s)
92    }
93}
94impl From<&str> for ShellError {
95    fn from(s: &str) -> Self {
96        broker_error(s.to_owned())
97    }
98}
99
100impl From<std::io::Error> for ShellError {
101    fn from(e: std::io::Error) -> Self {
102        use std::io::ErrorKind::*;
103        match e.kind() {
104            NotFound => Self::FileNotFound(e.to_string()),
105            PermissionDenied => Self::PermissionDenied(e.to_string()),
106            _ => Self::Io(e.to_string()),
107        }
108    }
109}
110
111/// Classify a string error from the broker or file helpers, preserving the message text.
112pub fn broker_error(message: String) -> ShellError {
113    let lower = message.to_ascii_lowercase();
114    if lower.contains("not authorized")
115        || lower.contains("not just written")
116        || lower.contains("unknown or revoked")
117        || lower.contains("unknown or already-used")
118    {
119        ShellError::BrokerDenied(message)
120    } else if lower.contains("no such file")
121        || lower.contains("cannot find the path")
122        || lower.contains("cannot find the file")
123        || lower.contains("not a file")
124    {
125        ShellError::FileNotFound(message)
126    } else if lower.contains("permission denied") || lower.contains("operation not permitted") {
127        ShellError::PermissionDenied(message)
128    } else {
129        ShellError::Other(message)
130    }
131}