use serde::Serialize;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "code", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Error {
Identity { detail: String },
PortInUse { port: u16 },
Listen { port: u16, detail: String },
DiscoveryUnavailable { detail: String },
TrustStore { detail: String },
InvalidFingerprint,
NotListed,
AllowNotDefault,
DeviceNearby,
DeviceNotNearby,
DeviceBlocked,
NoUsableAddress,
InvalidIdentity { peer: String },
PeerPlaintext { peer: String },
PeerIdentityMismatch { peer: String },
PeerUnreachable { peer: String, detail: String },
PeerLost { peer: String, detail: String },
PeerAnswer { peer: String, status: u16 },
PeerReply { peer: String },
PeerNeedsPin { peer: String },
PeerBusy { peer: String },
PeerTooLarge { peer: String },
PeerRateLimited { peer: String },
FileRefused { peer: String, file: String },
FileDamaged { peer: String, file: String },
FileStopped { file: String, detail: String },
TooManyFiles { max: usize },
NothingToSend,
NotFileOrFolder { path: String },
Damaged { file: String },
Internal { detail: String },
Unreadable { path: String, detail: String },
SenderStopped,
Unverified { max_files: usize, max_bytes: u64 },
SaveFailed { file: String, detail: String },
Interrupted { file: String, detail: String },
Mark { file: String, detail: String },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Identity { detail } => write!(f, "identity unusable: {detail}"),
Error::PortInUse { port } => write!(f, "port {port} is already in use"),
Error::Listen { port, detail } => write!(f, "cannot listen on port {port}: {detail}"),
Error::DiscoveryUnavailable { detail } => write!(f, "discovery unavailable: {detail}"),
Error::TrustStore { detail } => write!(f, "cannot save the device list: {detail}"),
Error::InvalidFingerprint => write!(f, "fingerprint is not 64 hex characters"),
Error::NotListed => write!(f, "device is not in the list"),
Error::AllowNotDefault => write!(f, "the default state cannot be allow"),
Error::DeviceNearby => write!(f, "device is on the network now"),
Error::DeviceNotNearby => write!(f, "device is not on the network"),
Error::DeviceBlocked => write!(f, "device is set to deny"),
Error::NoUsableAddress => write!(f, "device has no usable address"),
Error::InvalidIdentity { peer } => write!(f, "{peer} presented no valid identity"),
Error::PeerPlaintext { peer } => write!(f, "{peer} has TLS turned off"),
Error::PeerIdentityMismatch { peer } => {
write!(f, "{peer}: certificate does not match its fingerprint")
}
Error::PeerUnreachable { peer, detail } => write!(f, "cannot reach {peer}: {detail}"),
Error::PeerLost { peer, detail } => {
write!(f, "lost the connection to {peer}: {detail}")
}
Error::PeerAnswer { peer, status } => write!(f, "{peer} answered {status}"),
Error::PeerReply { peer } => write!(f, "{peer} sent an answer that cannot be read"),
Error::PeerNeedsPin { peer } => write!(f, "{peer} requires a PIN"),
Error::PeerBusy { peer } => write!(f, "{peer} is busy with another transfer"),
Error::PeerTooLarge { peer } => {
write!(f, "{peer} refused: the request is over the unproven limit")
}
Error::PeerRateLimited { peer } => write!(f, "{peer} is rate limiting requests"),
Error::FileRefused { peer, file } => write!(f, "{peer} refused {file}"),
Error::FileDamaged { peer, file } => write!(f, "{peer} reports {file} arrived damaged"),
Error::FileStopped { file, detail } => write!(f, "sending {file} stopped: {detail}"),
Error::TooManyFiles { max } => write!(f, "more than {max} files"),
Error::NothingToSend => write!(f, "nothing to send"),
Error::NotFileOrFolder { path } => write!(f, "{path} is not a file or folder"),
Error::Unreadable { path, detail } => write!(f, "cannot read {path}: {detail}"),
Error::SenderStopped => write!(f, "the sender stopped responding"),
Error::Unverified {
max_files,
max_bytes,
} => write!(
f,
"a sender that cannot prove its identity may send at most {max_files} files and {max_bytes} bytes at once"
),
Error::SaveFailed { file, detail } => write!(f, "cannot save {file}: {detail}"),
Error::Interrupted { file, detail } => {
write!(f, "transfer of {file} interrupted: {detail}")
}
Error::Mark { file, detail } => write!(f, "cannot mark {file} as downloaded: {detail}"),
Error::Damaged { file } => write!(f, "{file} arrived damaged"),
Error::Internal { detail } => write!(f, "internal error: {detail}"),
}
}
}
pub(crate) fn peer_io(
peer: &str,
error: &std::io::Error,
otherwise: impl FnOnce(String, String) -> Error,
) -> Error {
if crate::net::is_identity_mismatch(error) {
Error::PeerIdentityMismatch {
peer: peer.to_string(),
}
} else {
otherwise(peer.to_string(), error.to_string())
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn errors_carry_stable_codes() {
let error = Error::PeerIdentityMismatch {
peer: "Phone".into(),
};
let value = serde_json::to_value(&error).unwrap();
assert_eq!(value["code"], "peer_identity_mismatch");
assert_eq!(value["peer"], "Phone");
let unit = serde_json::to_value(&Error::InvalidFingerprint).unwrap();
assert_eq!(unit["code"], "invalid_fingerprint");
assert_eq!(
Error::PortInUse { port: 53317 }.to_string(),
"port 53317 is already in use"
);
}
#[test]
fn a_mismatch_is_distinct_from_other_connection_failures() {
let refused = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let classified = peer_io("Phone", &refused, |peer, detail| Error::PeerUnreachable {
peer,
detail,
});
assert!(matches!(classified, Error::PeerUnreachable { .. }));
}
}