use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, DesktopError>;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum WebsiteDataError {
#[error("persistent website data requires an app_id; help: set LocalServerAppBuilder::app_id to the packaged application's bundle identifier")]
MissingIdentity,
#[error("invalid app_id for persistent website data; help: use 1–255 ASCII letters, digits, dots, underscores or hyphens, with no trailing dot")]
InvalidIdentity,
#[error("persistent website data requires a packaged macOS .app executable; help: launch the app from its packaged Contents/MacOS executable")]
Unpackaged,
#[error("app_id does not match the running macOS bundle identifier; help: use the exact CFBundleIdentifier of the packaged .app")]
IdentityMismatch,
#[error("persistent website data is unsupported on this platform; help: use a packaged macOS app or a Windows app-owned WebView2 profile")]
Unsupported,
}
#[derive(Debug, Error)]
pub enum DesktopError {
#[error(transparent)]
WebsiteData(#[from] WebsiteDataError),
#[cfg(feature = "application-ipc")]
#[error("desktop application IPC failed")]
Ipc(#[from] crate::ipc::IpcError),
#[error("packaged desktop resources were not found")]
PackagedResourcesNotFound,
#[error("desktop backend failed")]
Backend {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("I/O error while {context}")]
Io {
context: String,
#[source]
source: std::io::Error,
},
#[cfg(feature = "source")]
#[error("desktop WebUI build failed")]
Build(#[from] webui::WebUIError),
#[error("desktop render failed")]
Render(#[from] webui_handler::HandlerError),
#[error("desktop protocol error")]
Protocol(#[from] webui_protocol::ProtocolError),
#[error("failed to parse desktop state JSON from {}", path.display())]
StateJson {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("failed while {context}")]
Serialization {
context: String,
#[source]
source: serde_json::Error,
},
#[error("failed while {context}")]
Token {
context: String,
#[source]
source: webui_tokens::TokenError,
},
#[error("invalid desktop asset path: {path}")]
InvalidAssetPath {
path: String,
},
#[error("invalid desktop route pattern: {pattern}")]
InvalidRoutePattern {
pattern: String,
help: String,
},
#[error("desktop route provider failed for {path}: {message}")]
RouteProvider {
path: String,
message: String,
},
#[error(
"desktop asset is too large: {} ({} bytes, max {} bytes)",
path.display(),
size,
max_bytes
)]
AssetTooLarge {
path: PathBuf,
size: u64,
max_bytes: u64,
},
#[error("desktop bundle asset collision: {path}")]
BundleAssetCollision {
path: String,
},
#[error(
"desktop output path {} overlaps {input_label} path {}",
output.display(),
input.display()
)]
OutputPathOverlap {
output: PathBuf,
input: PathBuf,
input_label: &'static str,
},
#[error("invalid precompiled desktop package: {message}; help: {help}")]
PackageValidation {
message: String,
help: &'static str,
},
#[error("failed to serialize desktop bundle manifest")]
ManifestSerialization(#[source] serde_json::Error),
#[error("failed to parse desktop bundle manifest at {}", path.display())]
ManifestDeserialization {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("unsupported desktop runtime: {message}")]
UnsupportedRuntime {
message: String,
help: String,
},
}
impl DesktopError {
#[must_use]
pub fn hint(&self) -> Option<&str> {
match self {
DesktopError::PackagedResourcesNotFound => Some(
"Run the app from its packaged bundle, or enable the source feature for development with cargo run --features source",
),
DesktopError::InvalidAssetPath { .. } => {
Some("Use a relative asset path without '.', '..', backslashes, NUL bytes, or drive prefixes")
}
DesktopError::AssetTooLarge { .. } => {
Some("Reduce the asset size or raise the desktop max asset size for a trusted app")
}
DesktopError::InvalidRoutePattern { help, .. } => Some(help.as_str()),
DesktopError::RouteProvider { .. } => {
Some("Fix the Rust route provider or return a valid route-scoped state object")
}
DesktopError::BundleAssetCollision { .. } => {
Some("Rename the static asset or generated CSS file so each bundle path is unique")
}
DesktopError::OutputPathOverlap { .. } => {
Some("Choose an output directory outside the app, bundle, state, asset, and runner paths")
}
DesktopError::PackageValidation { help, .. } => Some(help),
DesktopError::UnsupportedRuntime { help, .. } => Some(help.as_str()),
_ => None,
}
}
#[must_use]
pub fn chain_message(&self) -> String {
let mut message = self.to_string();
let mut source = std::error::Error::source(self);
while let Some(err) = source {
message.push_str(": ");
message.push_str(&err.to_string());
source = err.source();
}
message
}
}
#[cfg(all(test, feature = "application-ipc"))]
mod tests {
use super::DesktopError;
use crate::ipc::{IpcError, IpcErrorCode};
#[test]
fn ipc_errors_preserve_their_typed_source_and_actionable_chain() {
let error = DesktopError::from(IpcError::new(
IpcErrorCode::PayloadTooLarge,
"frame exceeds the configured limit",
"send a smaller message",
));
assert!(matches!(
&error,
DesktopError::Ipc(source) if source.code == IpcErrorCode::PayloadTooLarge
));
assert!(std::error::Error::source(&error).is_some());
assert_eq!(
error.chain_message(),
"desktop application IPC failed: frame exceeds the configured limit; help: send a smaller message"
);
}
}