#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NackReason {
BadMagic = 0x01,
VersionMismatch = 0x02,
BadHeaderCrc = 0x03,
PayloadTooLarge = 0x04,
BadPayloadCrc = 0x05,
InternalError = 0x06,
EmptyPayload = 0x07,
UnknownMessage = 0x08,
ReservedFlagsSet = 0x09,
BadBatchFraming = 0x0B,
}
impl NackReason {
#[must_use]
pub fn as_metric_label(&self) -> &'static str {
match self {
NackReason::BadMagic => "bad_magic",
NackReason::VersionMismatch => "version_mismatch",
NackReason::BadHeaderCrc => "bad_header_crc",
NackReason::PayloadTooLarge => "payload_too_large",
NackReason::BadPayloadCrc => "bad_payload_crc",
NackReason::InternalError => "internal_error",
NackReason::EmptyPayload => "empty_payload",
NackReason::UnknownMessage => "unknown_message",
NackReason::ReservedFlagsSet => "reserved_flags_set",
NackReason::BadBatchFraming => "bad_batch_framing",
}
}
#[must_use]
pub fn from_metric_label(s: &str) -> Option<NackReason> {
match s {
"bad_magic" => Some(NackReason::BadMagic),
"version_mismatch" => Some(NackReason::VersionMismatch),
"bad_header_crc" => Some(NackReason::BadHeaderCrc),
"payload_too_large" => Some(NackReason::PayloadTooLarge),
"bad_payload_crc" => Some(NackReason::BadPayloadCrc),
"internal_error" => Some(NackReason::InternalError),
"empty_payload" => Some(NackReason::EmptyPayload),
"unknown_message" => Some(NackReason::UnknownMessage),
"reserved_flags_set" => Some(NackReason::ReservedFlagsSet),
"bad_batch_framing" => Some(NackReason::BadBatchFraming),
_ => None,
}
}
}
impl From<NackReason> for u8 {
fn from(reason: NackReason) -> u8 {
reason as u8
}
}
impl std::fmt::Display for NackReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
NackReason::BadMagic => "bad frame magic",
NackReason::VersionMismatch => "wire protocol version mismatch",
NackReason::BadHeaderCrc => "header CRC mismatch",
NackReason::PayloadTooLarge => "payload exceeds the daemon's cap",
NackReason::BadPayloadCrc => "payload CRC mismatch",
NackReason::InternalError => "daemon internal error (transient)",
NackReason::EmptyPayload => "empty payload (not representable)",
NackReason::UnknownMessage => "unknown message type or durability byte",
NackReason::ReservedFlagsSet => "reserved flags byte was non-zero",
NackReason::BadBatchFraming => "batch body could not be parsed",
};
write!(f, "{msg}")
}
}
impl std::error::Error for NackReason {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownNackReason(pub u8);
impl std::fmt::Display for UnknownNackReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown nack reason byte: {:#04x}", self.0)
}
}
impl std::error::Error for UnknownNackReason {}
impl TryFrom<u8> for NackReason {
type Error = UnknownNackReason;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x01 => Ok(NackReason::BadMagic),
0x02 => Ok(NackReason::VersionMismatch),
0x03 => Ok(NackReason::BadHeaderCrc),
0x04 => Ok(NackReason::PayloadTooLarge),
0x05 => Ok(NackReason::BadPayloadCrc),
0x06 => Ok(NackReason::InternalError),
0x07 => Ok(NackReason::EmptyPayload),
0x08 => Ok(NackReason::UnknownMessage),
0x09 => Ok(NackReason::ReservedFlagsSet),
0x0B => Ok(NackReason::BadBatchFraming),
v => Err(UnknownNackReason(v)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::version::WIRE_VERSION;
#[test]
fn try_from_accepts_all_known_reasons() {
assert_eq!(NackReason::try_from(0x01).unwrap(), NackReason::BadMagic);
assert_eq!(
NackReason::try_from(0x02).unwrap(),
NackReason::VersionMismatch
);
assert_eq!(
NackReason::try_from(0x03).unwrap(),
NackReason::BadHeaderCrc
);
assert_eq!(
NackReason::try_from(0x04).unwrap(),
NackReason::PayloadTooLarge
);
assert_eq!(
NackReason::try_from(0x05).unwrap(),
NackReason::BadPayloadCrc
);
assert_eq!(
NackReason::try_from(0x06).unwrap(),
NackReason::InternalError
);
assert_eq!(
NackReason::try_from(0x07).unwrap(),
NackReason::EmptyPayload
);
assert_eq!(
NackReason::try_from(0x08).unwrap(),
NackReason::UnknownMessage
);
assert_eq!(
NackReason::try_from(0x09).unwrap(),
NackReason::ReservedFlagsSet
);
}
#[test]
fn from_for_u8_is_inverse_of_try_from_and_display_is_nonempty() {
let mut seen = 0;
for byte in 0x00u8..=0xFF {
let Ok(r) = NackReason::try_from(byte) else {
continue;
};
seen += 1;
assert_eq!(u8::from(r), byte);
assert!(!r.to_string().is_empty(), "{r:?} has an empty Display");
}
assert!(seen >= 10, "the sweep decoded only {seen} reasons");
let _: &dyn std::error::Error = &NackReason::BadMagic;
}
#[test]
fn try_from_returns_unknown_for_unrecognised_byte() {
let err = NackReason::try_from(0x0A).unwrap_err();
assert_eq!(err.0, 0x0A);
let err = NackReason::try_from(0x00).unwrap_err();
assert_eq!(err.0, 0x00);
let err = NackReason::try_from(0xff).unwrap_err();
assert_eq!(err.0, 0xff);
}
#[test]
fn repr_values_match_wire() {
assert_eq!(NackReason::BadMagic as u8, 0x01);
assert_eq!(NackReason::VersionMismatch as u8, 0x02);
assert_eq!(NackReason::BadHeaderCrc as u8, 0x03);
assert_eq!(NackReason::PayloadTooLarge as u8, 0x04);
assert_eq!(NackReason::BadPayloadCrc as u8, 0x05);
assert_eq!(NackReason::InternalError as u8, 0x06);
assert_eq!(NackReason::EmptyPayload as u8, 0x07);
assert_eq!(NackReason::UnknownMessage as u8, 0x08);
assert_eq!(NackReason::ReservedFlagsSet as u8, 0x09);
}
#[test]
fn as_metric_label_pins_every_variant() {
assert_eq!(NackReason::BadMagic.as_metric_label(), "bad_magic");
assert_eq!(
NackReason::VersionMismatch.as_metric_label(),
"version_mismatch"
);
assert_eq!(NackReason::BadHeaderCrc.as_metric_label(), "bad_header_crc");
assert_eq!(
NackReason::PayloadTooLarge.as_metric_label(),
"payload_too_large"
);
assert_eq!(
NackReason::BadPayloadCrc.as_metric_label(),
"bad_payload_crc"
);
assert_eq!(
NackReason::InternalError.as_metric_label(),
"internal_error"
);
assert_eq!(NackReason::EmptyPayload.as_metric_label(), "empty_payload");
assert_eq!(
NackReason::UnknownMessage.as_metric_label(),
"unknown_message"
);
assert_eq!(
NackReason::ReservedFlagsSet.as_metric_label(),
"reserved_flags_set"
);
assert_eq!(
NackReason::BadBatchFraming.as_metric_label(),
"bad_batch_framing"
);
}
#[test]
fn metric_label_round_trips_for_every_variant() {
let mut seen = 0;
for byte in 0x00u8..=0xFF {
let Ok(r) = NackReason::try_from(byte) else {
continue;
};
seen += 1;
assert_eq!(
NackReason::from_metric_label(r.as_metric_label()),
Some(r),
"metric label for {r:?} ({byte:#04x}) did not round-trip"
);
assert!(
!r.as_metric_label().is_empty(),
"{r:?} ({byte:#04x}) has no metric label"
);
}
assert!(
seen >= 10,
"the sweep decoded only {seen} reasons; it is no longer reaching the \
assigned set"
);
assert!(
NackReason::try_from(0x0A).is_err(),
"0x0A must stay unassigned — the frozen vector nack_reserved_reason \
pins it as the worked example of an unknown reason byte"
);
assert_eq!(NackReason::from_metric_label("not_a_reason"), None);
assert_eq!(NackReason::from_metric_label(""), None);
}
#[test]
fn version_mismatch_nack_payload_encodes_daemon_version() {
let payload = [NackReason::VersionMismatch as u8, WIRE_VERSION];
assert_eq!(payload, [0x02, 0x01]);
}
}