1use std::path::PathBuf;
5
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, DesktopError>;
10
11#[derive(Debug, Error, PartialEq, Eq)]
13pub enum WebsiteDataError {
14 #[error("persistent website data requires an app_id; help: set LocalServerAppBuilder::app_id to the packaged application's bundle identifier")]
16 MissingIdentity,
17 #[error("invalid app_id for persistent website data; help: use 1–255 ASCII letters, digits, dots, underscores or hyphens, with no trailing dot")]
19 InvalidIdentity,
20 #[error("persistent website data requires a packaged macOS .app executable; help: launch the app from its packaged Contents/MacOS executable")]
22 Unpackaged,
23 #[error("app_id does not match the running macOS bundle identifier; help: use the exact CFBundleIdentifier of the packaged .app")]
25 IdentityMismatch,
26 #[error("persistent website data is unsupported on this platform; help: use a packaged macOS app or a Windows app-owned WebView2 profile")]
28 Unsupported,
29}
30
31#[derive(Debug, Error)]
33pub enum DesktopError {
34 #[error(transparent)]
36 WebsiteData(#[from] WebsiteDataError),
37 #[cfg(feature = "application-ipc")]
39 #[error("desktop application IPC failed")]
40 Ipc(#[from] crate::ipc::IpcError),
41
42 #[error("packaged desktop resources were not found")]
44 PackagedResourcesNotFound,
45
46 #[error("desktop backend failed")]
48 Backend {
49 #[source]
51 source: Box<dyn std::error::Error + Send + Sync>,
52 },
53
54 #[error("I/O error while {context}")]
56 Io {
57 context: String,
59 #[source]
61 source: std::io::Error,
62 },
63
64 #[cfg(feature = "source")]
66 #[error("desktop WebUI build failed")]
67 Build(#[from] webui::WebUIError),
68
69 #[error("desktop render failed")]
71 Render(#[from] webui_handler::HandlerError),
72
73 #[error("desktop protocol error")]
75 Protocol(#[from] webui_protocol::ProtocolError),
76
77 #[error("failed to parse desktop state JSON from {}", path.display())]
79 StateJson {
80 path: PathBuf,
82 #[source]
84 source: serde_json::Error,
85 },
86
87 #[error("failed while {context}")]
89 Serialization {
90 context: String,
92 #[source]
94 source: serde_json::Error,
95 },
96
97 #[error("failed while {context}")]
99 Token {
100 context: String,
102 #[source]
104 source: webui_tokens::TokenError,
105 },
106
107 #[error("invalid desktop asset path: {path}")]
109 InvalidAssetPath {
110 path: String,
112 },
113
114 #[error("invalid desktop route pattern: {pattern}")]
116 InvalidRoutePattern {
117 pattern: String,
119 help: String,
121 },
122
123 #[error("desktop route provider failed for {path}: {message}")]
125 RouteProvider {
126 path: String,
128 message: String,
130 },
131
132 #[error(
134 "desktop asset is too large: {} ({} bytes, max {} bytes)",
135 path.display(),
136 size,
137 max_bytes
138 )]
139 AssetTooLarge {
140 path: PathBuf,
142 size: u64,
144 max_bytes: u64,
146 },
147
148 #[error("desktop bundle asset collision: {path}")]
150 BundleAssetCollision {
151 path: String,
153 },
154
155 #[error(
157 "desktop output path {} overlaps {input_label} path {}",
158 output.display(),
159 input.display()
160 )]
161 OutputPathOverlap {
162 output: PathBuf,
164 input: PathBuf,
166 input_label: &'static str,
168 },
169
170 #[error("invalid precompiled desktop package: {message}; help: {help}")]
172 PackageValidation {
173 message: String,
175 help: &'static str,
177 },
178
179 #[error("failed to serialize desktop bundle manifest")]
181 ManifestSerialization(#[source] serde_json::Error),
182
183 #[error("failed to parse desktop bundle manifest at {}", path.display())]
185 ManifestDeserialization {
186 path: PathBuf,
188 #[source]
190 source: serde_json::Error,
191 },
192
193 #[error("unsupported desktop runtime: {message}")]
195 UnsupportedRuntime {
196 message: String,
198 help: String,
200 },
201}
202
203impl DesktopError {
204 #[must_use]
206 pub fn hint(&self) -> Option<&str> {
207 match self {
208 DesktopError::PackagedResourcesNotFound => Some(
209 "Run the app from its packaged bundle, or enable the source feature for development with cargo run --features source",
210 ),
211 DesktopError::InvalidAssetPath { .. } => {
212 Some("Use a relative asset path without '.', '..', backslashes, NUL bytes, or drive prefixes")
213 }
214 DesktopError::AssetTooLarge { .. } => {
215 Some("Reduce the asset size or raise the desktop max asset size for a trusted app")
216 }
217 DesktopError::InvalidRoutePattern { help, .. } => Some(help.as_str()),
218 DesktopError::RouteProvider { .. } => {
219 Some("Fix the Rust route provider or return a valid route-scoped state object")
220 }
221 DesktopError::BundleAssetCollision { .. } => {
222 Some("Rename the static asset or generated CSS file so each bundle path is unique")
223 }
224 DesktopError::OutputPathOverlap { .. } => {
225 Some("Choose an output directory outside the app, bundle, state, asset, and runner paths")
226 }
227 DesktopError::PackageValidation { help, .. } => Some(help),
228 DesktopError::UnsupportedRuntime { help, .. } => Some(help.as_str()),
229 _ => None,
230 }
231 }
232
233 #[must_use]
236 pub fn chain_message(&self) -> String {
237 let mut message = self.to_string();
238 let mut source = std::error::Error::source(self);
239 while let Some(err) = source {
240 message.push_str(": ");
241 message.push_str(&err.to_string());
242 source = err.source();
243 }
244 message
245 }
246}
247
248#[cfg(all(test, feature = "application-ipc"))]
249mod tests {
250 use super::DesktopError;
251 use crate::ipc::{IpcError, IpcErrorCode};
252
253 #[test]
254 fn ipc_errors_preserve_their_typed_source_and_actionable_chain() {
255 let error = DesktopError::from(IpcError::new(
256 IpcErrorCode::PayloadTooLarge,
257 "frame exceeds the configured limit",
258 "send a smaller message",
259 ));
260 assert!(matches!(
261 &error,
262 DesktopError::Ipc(source) if source.code == IpcErrorCode::PayloadTooLarge
263 ));
264 assert!(std::error::Error::source(&error).is_some());
265 assert_eq!(
266 error.chain_message(),
267 "desktop application IPC failed: frame exceeds the configured limit; help: send a smaller message"
268 );
269 }
270}