use thiserror::Error;
use dig_nat::SafeText;
#[derive(Debug, Error)]
pub enum DhtError {
#[error("transport error: {0}")]
Transport(SafeText),
#[error("malformed response: {0}")]
MalformedResponse(SafeText),
#[error("invalid hex identifier: {0}")]
InvalidHex(SafeText),
#[error("no peers to query (routing table + bootstrap set are empty)")]
NoPeers,
#[error("rpc timed out")]
Timeout,
}
impl DhtError {
pub fn transport(text: impl Into<SafeText>) -> Self {
DhtError::Transport(text.into())
}
pub fn transport_from_untrusted(reason: impl std::fmt::Display) -> Self {
DhtError::Transport(SafeText::from_untrusted(reason.to_string()))
}
pub fn malformed_response(error: &serde_json::Error) -> Self {
DhtError::MalformedResponse(SafeText::describing_json_error(error))
}
pub fn invalid_hex(which: &'static str) -> Self {
DhtError::InvalidHex(SafeText::from_static(which))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transport_helper_formats() {
let e = DhtError::transport("connection refused");
assert!(e.to_string().contains("connection refused"));
assert!(matches!(e, DhtError::Transport(_)));
}
#[test]
fn untrusted_transport_text_cannot_forge_a_log_line() {
let e = DhtError::transport_from_untrusted("refused\n2026-07-31 ERROR forged");
let rendered = e.to_string();
assert!(!rendered.contains('\n'), "got: {rendered:?}");
assert!(
rendered.contains("refused") && rendered.contains("forged"),
"escaped, not deleted: {rendered}"
);
}
#[test]
fn a_malformed_response_does_not_quote_the_response() {
let json_err =
serde_json::from_str::<u64>(r#""what-the-peer-sent""#).expect_err("not a u64");
let rendered = DhtError::malformed_response(&json_err).to_string();
assert!(!rendered.contains("what-the-peer-sent"));
assert!(rendered.contains("malformed response"));
assert!(rendered.contains("line 1"), "still locatable: {rendered}");
}
#[test]
fn invalid_hex_names_the_argument_rather_than_echoing_it() {
let rendered = DhtError::invalid_hex("peer_id").to_string();
assert!(rendered.contains("peer_id"));
assert!(rendered.contains("invalid hex"));
}
#[test]
fn error_messages_are_descriptive() {
assert!(DhtError::NoPeers.to_string().contains("no peers"));
assert!(DhtError::Timeout.to_string().contains("timed out"));
assert!(DhtError::MalformedResponse(SafeText::from_static("x"))
.to_string()
.contains("malformed"));
}
}