use super::Version;
use crate::{SessionError, StreamError};
pub const INTERNAL_ERROR: u32 = 0x0;
pub const CANCELLED: u32 = 0x1;
const DELIVERY_TIMEOUT: u32 = 0x2;
const SESSION_CLOSED: u32 = 0x3;
const GOING_AWAY: u32 = 0x4;
const TOO_FAR_BEHIND: u32 = 0x5;
const MALFORMED_TRACK: u32 = 0x12;
fn has_going_away(version: Version) -> bool {
matches!(
version,
Version::Draft18 | Version::Draft19 | Version::Draft20 | Version::Draft21 | Version::Draft22
)
}
fn has_too_far_behind(version: Version) -> bool {
!matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16)
}
fn has_malformed_track(version: Version) -> bool {
!matches!(version, Version::Draft14 | Version::Draft15)
}
pub fn to_stream_code(err: &StreamError, version: Version) -> u32 {
match err {
StreamError::Internal => INTERNAL_ERROR,
StreamError::Cancel => CANCELLED,
StreamError::DeliveryTimeout => DELIVERY_TIMEOUT,
StreamError::Session(_) => SESSION_CLOSED,
StreamError::GoingAway if has_going_away(version) => GOING_AWAY,
StreamError::TooFarBehind if has_too_far_behind(version) => TOO_FAR_BEHIND,
StreamError::MalformedTrack if has_malformed_track(version) => MALFORMED_TRACK,
_ => INTERNAL_ERROR,
}
}
pub fn from_stream_code(code: u32, version: Version) -> StreamError {
match code {
INTERNAL_ERROR => StreamError::Internal,
CANCELLED => StreamError::Cancel,
DELIVERY_TIMEOUT => StreamError::DeliveryTimeout,
SESSION_CLOSED => StreamError::Session(SessionError::Internal),
GOING_AWAY if has_going_away(version) => StreamError::GoingAway,
TOO_FAR_BEHIND if has_too_far_behind(version) => StreamError::TooFarBehind,
MALFORMED_TRACK if has_malformed_track(version) => StreamError::MalformedTrack,
code => StreamError::Unknown(code),
}
}
pub(crate) mod request {
use super::Version;
use crate::{Error, SessionError, StreamError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Kind {
Subscribe,
Fetch,
Publish,
PublishNamespace,
SubscribeNamespace,
}
const INTERNAL_ERROR: u64 = 0x0;
const UNAUTHORIZED: u64 = 0x1;
const TIMEOUT: u64 = 0x2;
const NOT_SUPPORTED: u64 = 0x3;
const GOING_AWAY: u64 = 0x6;
const DOES_NOT_EXIST_14: u64 = 0x4;
const DOES_NOT_EXIST: u64 = 0x10;
const UNINTERESTED_14: u64 = 0x4;
const UNINTERESTED: u64 = 0x20;
const MALFORMED_TRACK_14: u64 = 0x9;
const MALFORMED_TRACK: u64 = 0x12;
fn does_not_exist(kind: Kind, version: Version) -> Option<u64> {
match version {
Version::Draft14 => match kind {
Kind::Subscribe | Kind::Fetch => Some(DOES_NOT_EXIST_14),
_ => None,
},
_ => Some(DOES_NOT_EXIST),
}
}
fn uninterested(kind: Kind, version: Version) -> Option<u64> {
match version {
Version::Draft14 => match kind {
Kind::Publish | Kind::PublishNamespace => Some(UNINTERESTED_14),
_ => None,
},
_ => Some(UNINTERESTED),
}
}
fn malformed_track(kind: Kind, version: Version) -> Option<u64> {
match version {
Version::Draft14 => match kind {
Kind::Fetch => Some(MALFORMED_TRACK_14),
_ => None,
},
_ => Some(MALFORMED_TRACK),
}
}
fn going_away(version: Version) -> Option<u64> {
match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => None,
_ => Some(GOING_AWAY),
}
}
pub(crate) fn to_code(err: &Error, kind: Kind, version: Version) -> u64 {
let registered = match err {
Error::Unauthorized | Error::Session(SessionError::Unauthorized) => return UNAUTHORIZED,
Error::Timeout | Error::Stream(StreamError::DeliveryTimeout) | Error::Session(SessionError::Timeout) => {
return TIMEOUT;
}
Error::Unsupported | Error::Version | Error::Session(SessionError::Version) => return NOT_SUPPORTED,
Error::NotFound | Error::Stream(StreamError::NotFound) => does_not_exist(kind, version),
Error::Unroutable | Error::Stream(StreamError::Unroutable) => match kind {
Kind::Subscribe | Kind::Fetch => does_not_exist(kind, version),
Kind::Publish | Kind::PublishNamespace | Kind::SubscribeNamespace => uninterested(kind, version),
},
Error::Decode(_)
| Error::BoundsExceeded(_)
| Error::MalformedTrack
| Error::Stream(StreamError::MalformedTrack) => malformed_track(kind, version),
Error::GoingAway | Error::Stream(StreamError::GoingAway) => going_away(version),
_ => return INTERNAL_ERROR,
};
registered.unwrap_or(INTERNAL_ERROR)
}
pub(crate) fn from_code(code: u64, kind: Kind, version: Version) -> Error {
match code {
UNAUTHORIZED => Error::Unauthorized,
TIMEOUT => Error::Timeout,
NOT_SUPPORTED => Error::Unsupported,
code if Some(code) == does_not_exist(kind, version) => Error::NotFound,
code if Some(code) == uninterested(kind, version) => Error::Unroutable,
code if Some(code) == malformed_track(kind, version) => Error::MalformedTrack,
code if Some(code) == going_away(version) => Error::GoingAway,
code => Error::Remote(u32::try_from(code).unwrap_or(u32::MAX)),
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL: [Version; 9] = [
Version::Draft14,
Version::Draft15,
Version::Draft16,
Version::Draft17,
Version::Draft18,
Version::Draft19,
Version::Draft20,
Version::Draft21,
Version::Draft22,
];
const KINDS: [Kind; 5] = [
Kind::Subscribe,
Kind::Fetch,
Kind::Publish,
Kind::PublishNamespace,
Kind::SubscribeNamespace,
];
const EVERY_ERROR: [Error; 9] = [
Error::Duplicate,
Error::Unauthorized,
Error::Timeout,
Error::Unsupported,
Error::NotFound,
Error::Unroutable,
Error::MalformedTrack,
Error::GoingAway,
Error::Remote(0x30),
];
#[test]
fn a_missing_broadcast_uses_the_draft_s_own_number() {
for kind in [Kind::Subscribe, Kind::Fetch] {
assert_eq!(to_code(&Error::NotFound, kind, Version::Draft14), 0x4);
assert!(matches!(from_code(0x4, kind, Version::Draft14), Error::NotFound));
for version in ALL.into_iter().skip(1) {
assert_eq!(to_code(&Error::NotFound, kind, version), 0x10);
assert!(matches!(from_code(0x10, kind, version), Error::NotFound));
assert!(matches!(from_code(0x4, kind, version), Error::Remote(0x4)));
}
}
assert_eq!(to_code(&Error::Unroutable, Kind::Subscribe, Version::Draft20), 0x10);
}
#[test]
fn draft_14_reads_0x4_per_request() {
for kind in [Kind::Publish, Kind::PublishNamespace] {
assert_eq!(to_code(&Error::Unroutable, kind, Version::Draft14), 0x4);
assert!(matches!(from_code(0x4, kind, Version::Draft14), Error::Unroutable));
assert_eq!(to_code(&Error::NotFound, kind, Version::Draft14), INTERNAL_ERROR);
}
assert!(matches!(
from_code(0x4, Kind::SubscribeNamespace, Version::Draft14),
Error::Remote(0x4)
));
}
#[test]
fn going_away_only_exists_from_draft_17() {
for kind in KINDS {
for version in [Version::Draft14, Version::Draft15, Version::Draft16] {
assert_eq!(to_code(&Error::GoingAway, kind, version), INTERNAL_ERROR);
assert!(matches!(from_code(GOING_AWAY, kind, version), Error::Remote(0x6)));
}
for version in [
Version::Draft17,
Version::Draft18,
Version::Draft19,
Version::Draft20,
Version::Draft21,
Version::Draft22,
] {
assert_eq!(to_code(&Error::GoingAway, kind, version), GOING_AWAY);
assert!(matches!(from_code(GOING_AWAY, kind, version), Error::GoingAway));
}
}
}
#[test]
fn every_emitted_code_round_trips() {
for version in ALL {
for kind in KINDS {
for err in &EVERY_ERROR {
let code = to_code(err, kind, version);
let decoded = from_code(code, kind, version);
assert_eq!(
to_code(&decoded, kind, version),
code,
"{err:?} on {version:?}/{kind:?} did not survive a round trip"
);
}
}
}
}
#[test]
fn only_registered_codes_reach_the_wire() {
fn registered(kind: Kind, version: Version) -> &'static [u64] {
match (version, kind) {
(Version::Draft14, Kind::Subscribe) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x12],
(Version::Draft14, Kind::Fetch) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10, 0x12],
(Version::Draft14, Kind::Publish) => &[0x0, 0x1, 0x2, 0x3, 0x4],
(Version::Draft14, Kind::PublishNamespace) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x10, 0x12],
(Version::Draft14, Kind::SubscribeNamespace) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x12],
(Version::Draft15, _) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x11, 0x12, 0x20, 0x30, 0x32, 0x33],
(Version::Draft16, _) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x32],
(Version::Draft17, _) => &[
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x9, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x31, 0x32,
],
_ => &[
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x9, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36,
],
}
}
for version in ALL {
for kind in KINDS {
for err in &EVERY_ERROR {
let code = to_code(err, kind, version);
assert!(
registered(kind, version).contains(&code),
"{err:?} sends {code:#x}, which {version} does not register for {kind:?}"
);
}
}
}
}
#[test]
fn an_unregistered_error_is_internal() {
for err in [Error::Duplicate, Error::Cancel, Error::ProtocolViolation, Error::Closed] {
for kind in KINDS {
assert_eq!(
to_code(&err, kind, Version::Draft20),
INTERNAL_ERROR,
"{err} is not internal on {kind:?}"
);
}
}
assert!(matches!(
from_code(INTERNAL_ERROR, Kind::Subscribe, Version::Draft20),
Error::Remote(0)
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error;
const ALL: [Version; 9] = [
Version::Draft14,
Version::Draft15,
Version::Draft16,
Version::Draft17,
Version::Draft18,
Version::Draft19,
Version::Draft20,
Version::Draft21,
Version::Draft22,
];
#[test]
fn a_cancellation_is_not_an_internal_error() {
for version in ALL {
assert_eq!(to_stream_code(&StreamError::Cancel, version), CANCELLED);
assert_eq!(from_stream_code(CANCELLED, version), StreamError::Cancel);
assert!(matches!(
Error::from(from_stream_code(CANCELLED, version)),
Error::Stream(StreamError::Cancel)
));
}
assert_ne!(
CANCELLED,
SessionError::Cancel.to_code(),
"the two spaces disagree about 0"
);
assert_eq!(SessionError::Cancel.to_code(), INTERNAL_ERROR);
}
#[test]
fn every_emitted_code_round_trips() {
let errors = [
StreamError::Internal,
StreamError::Cancel,
StreamError::DeliveryTimeout,
StreamError::ControlTimeout,
StreamError::GoingAway,
StreamError::TooFarBehind,
StreamError::MalformedTrack,
StreamError::NotFound,
StreamError::Old,
StreamError::Evicted,
StreamError::App(7),
];
for version in ALL {
for err in &errors {
let code = to_stream_code(err, version);
let decoded = from_stream_code(code, version);
assert_eq!(
to_stream_code(&decoded, version),
code,
"{err:?} on {version:?} did not survive a round trip"
);
}
let code = to_stream_code(&StreamError::Session(SessionError::Unauthorized), version);
assert_eq!(code, SESSION_CLOSED);
assert_eq!(
from_stream_code(code, version),
StreamError::Session(SessionError::Internal)
);
}
}
#[test]
fn going_away_only_exists_from_draft_18() {
for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] {
assert_eq!(to_stream_code(&StreamError::GoingAway, version), INTERNAL_ERROR);
assert_eq!(from_stream_code(GOING_AWAY, version), StreamError::Unknown(GOING_AWAY));
}
for version in [
Version::Draft18,
Version::Draft19,
Version::Draft20,
Version::Draft21,
Version::Draft22,
] {
assert_eq!(to_stream_code(&StreamError::GoingAway, version), GOING_AWAY);
assert_eq!(from_stream_code(GOING_AWAY, version), StreamError::GoingAway);
}
}
#[test]
fn later_codes_are_not_sent_to_earlier_drafts() {
for version in ALL {
let too_far_behind = to_stream_code(&StreamError::TooFarBehind, version);
let malformed = to_stream_code(&StreamError::MalformedTrack, version);
assert_eq!(
too_far_behind,
match has_too_far_behind(version) {
true => TOO_FAR_BEHIND,
false => INTERNAL_ERROR,
},
"{version:?} disagrees about TOO_FAR_BEHIND"
);
assert_eq!(
malformed,
match has_malformed_track(version) {
true => MALFORMED_TRACK,
false => INTERNAL_ERROR,
},
"{version:?} disagrees about MALFORMED_TRACK"
);
}
assert_eq!(
from_stream_code(TOO_FAR_BEHIND, Version::Draft16),
StreamError::Unknown(TOO_FAR_BEHIND)
);
assert_eq!(
from_stream_code(MALFORMED_TRACK, Version::Draft15),
StreamError::Unknown(MALFORMED_TRACK)
);
}
#[test]
fn unregistered_conditions_are_internal() {
for err in [
StreamError::NotFound,
StreamError::Unroutable,
StreamError::Old,
StreamError::Evicted,
StreamError::WrongSize,
StreamError::FrameTooLarge,
StreamError::GroupTooLarge,
StreamError::TimestampMismatch,
StreamError::ControlTimeout,
StreamError::App(7),
StreamError::Unknown(0x1234),
] {
assert_eq!(
to_stream_code(&err, Version::Draft20),
INTERNAL_ERROR,
"{err:?} has no value in this registry"
);
}
for code in [
0x6,
0x7,
0x9,
0x20,
0x22,
0x33,
0x34,
0x35,
0x36,
0x37,
0x38,
0x39,
64 + 7,
] {
assert_eq!(from_stream_code(code, Version::Draft20), StreamError::Unknown(code));
assert!(matches!(
Error::from(from_stream_code(code, Version::Draft20)),
Error::Stream(StreamError::Unknown(remote)) if remote == code
));
}
}
const EVERY_ERROR: [StreamError; 18] = [
StreamError::Session(SessionError::Cancel),
StreamError::Internal,
StreamError::Cancel,
StreamError::DeliveryTimeout,
StreamError::ControlTimeout,
StreamError::GoingAway,
StreamError::TooFarBehind,
StreamError::MalformedTrack,
StreamError::NotFound,
StreamError::Unroutable,
StreamError::Old,
StreamError::Evicted,
StreamError::WrongSize,
StreamError::FrameTooLarge,
StreamError::GroupTooLarge,
StreamError::TimestampMismatch,
StreamError::App(7),
StreamError::Unknown(0x22),
];
#[test]
fn only_registered_codes_reach_the_wire() {
fn registered(version: Version) -> &'static [u32] {
match version {
Version::Draft14 | Version::Draft15 => &[0x0, 0x1, 0x2, 0x3],
Version::Draft16 => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x12],
Version::Draft17 => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x9, 0x12],
_ => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9, 0x12],
}
}
for version in ALL {
for err in EVERY_ERROR {
let code = to_stream_code(&err, version);
assert!(
registered(version).contains(&code),
"{err:?} sends {code:#x}, which {version} does not register"
);
}
}
}
#[test]
fn relaying_a_code_does_not_change_its_meaning() {
for version in ALL {
for code in [INTERNAL_ERROR, CANCELLED, DELIVERY_TIMEOUT, SESSION_CLOSED] {
let relayed = StreamError::from(&Error::from(from_stream_code(code, version)));
assert_eq!(
to_stream_code(&relayed, version),
code,
"{code:#x} changed across a relay on {version:?}"
);
}
}
}
}