use moqtap_codec::version::DraftVersion;
use crate::shape::{ClassRule, MatchKind, Matcher, ShapeProfile};
use crate::types::BypassReason;
use crate::types::DataStreamType;
const RESERVED_SUBGROUP_ID_MODE: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Site {
Control,
Object,
Datagram,
StreamOpen,
StreamHeader,
StreamEnd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionKind {
Pass,
Replace,
ReplacePayload,
Delay,
Hold,
DropElide,
Truncate,
ResetStream,
CloseSession,
Open,
Reject,
ReplaceObject,
OpenAfter,
SerializeAfter,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Support {
Yes,
No(Refusal),
Conditional(Precondition),
NotAttemptable {
why: NotAttemptable,
refusal: Refusal,
},
Unreachable {
refusal: Refusal,
instead: Instead,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Instead {
FramerBypass(BypassReason),
ControlFrameNotDecodable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NotAttemptable {
SiteReturnsStreamAction,
SiteReturnsAction,
NoConstructor,
KindNotDefinedAtThisSite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Precondition {
ReplacementLengthEqualsPayload,
NotFirstObjectOfImplicitSubgroup,
NotAStatusObject,
DatagramPayloadDelimited,
WithinMaxDatagramSize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Refusal {
WrongSite {
site: Site,
action: ActionKind,
},
WrongComposition {
detail: &'static str,
},
ControlStreamResetIllegal,
LengthChanged {
from: u64,
to: u64,
},
WouldRedefineSubgroupId,
WouldDestroyStatusObject,
ReservedHeaderMode {
mode: u8,
},
PayloadNotDelimited {
detail: &'static str,
},
StreamNotFramed {
reason: BypassReason,
},
ControlFrameNotDecodable,
ErrorCodeOutOfRange {
code: u64,
},
SessionAlreadyClosing,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct CapCtx {
pub draft: Option<DraftVersion>,
pub stream_kind: Option<DataStreamType>,
pub is_control_stream: Option<bool>,
pub index_in_stream: Option<u64>,
pub subgroup_id_resolved: Option<bool>,
pub is_status_object: Option<bool>,
pub payload_len: Option<u64>,
pub replacement_len: Option<u64>,
pub payload_delimited: Option<bool>,
pub subgroup_id_mode: Option<u8>,
}
pub fn classify(site: Site, kind: ActionKind, cx: &CapCtx) -> Support {
if let Some(answer) = not_attemptable(site, kind) {
return answer;
}
let framing_bypass = match (site, cx.draft) {
(Site::Object, Some(draft)) => object_framing_bypass(draft, cx.stream_kind),
_ => None,
};
if let Some(reason) = framing_bypass {
return Support::Unreachable {
refusal: Refusal::StreamNotFramed { reason },
instead: Instead::FramerBypass(reason),
};
}
if site == Site::Control && cx.draft.is_some_and(|draft| !draft_is_compiled(draft)) {
return Support::Unreachable {
refusal: Refusal::ControlFrameNotDecodable,
instead: Instead::ControlFrameNotDecodable,
};
}
match site {
Site::Control => classify_control(kind),
Site::Object => classify_object(kind, cx),
Site::Datagram => classify_datagram(kind, cx),
Site::StreamOpen | Site::StreamHeader => classify_stream_decision(site, kind),
Site::StreamEnd => classify_stream_end(kind, cx),
}
}
#[derive(Debug, Clone, Copy)]
pub struct Capabilities {
draft: DraftVersion,
}
impl Capabilities {
#[must_use]
pub fn for_draft(draft: DraftVersion) -> Self {
Self { draft }
}
#[must_use]
pub fn supports(&self, site: Site, kind: ActionKind) -> Support {
classify(site, kind, &CapCtx { draft: Some(self.draft), ..CapCtx::default() })
}
#[must_use]
pub fn supports_on(
&self,
site: Site,
kind: ActionKind,
stream_kind: DataStreamType,
) -> Support {
classify(
site,
kind,
&CapCtx {
draft: Some(self.draft),
stream_kind: Some(stream_kind),
..CapCtx::default()
},
)
}
#[must_use]
pub fn supports_matcher(&self, kind: MatchKind, field: MatcherKey) -> bool {
supports_matcher(self.draft, kind, field)
}
pub fn admit_class(&self, class: &ClassRule) -> Result<(), UnsupportedMatcherKey> {
let aimed = class.matcher.stream_kind;
for key in keys_named(&class.matcher).into_iter().flatten() {
let carried = match aimed {
Some(kind) => supports_matcher(self.draft, kind, key),
None => ANY_KIND.iter().any(|&kind| supports_matcher(self.draft, kind, key)),
};
if !carried {
return Err(UnsupportedMatcherKey {
class: class.name.clone(),
draft: self.draft,
kind: aimed,
key,
});
}
}
Ok(())
}
pub fn admit_profile(&self, profile: &ShapeProfile) -> Result<(), UnsupportedMatcherKey> {
profile.classes().iter().try_for_each(|class| self.admit_class(class))
}
}
const ANY_KIND: [MatchKind; 3] = [MatchKind::Subgroup, MatchKind::Fetch, MatchKind::Datagram];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MatcherKey {
TrackAlias,
GroupId,
SubgroupId,
ObjectId,
Priority,
EveryNth,
}
impl MatcherKey {
pub const ALL: [MatcherKey; 6] = [
MatcherKey::TrackAlias,
MatcherKey::GroupId,
MatcherKey::SubgroupId,
MatcherKey::ObjectId,
MatcherKey::Priority,
MatcherKey::EveryNth,
];
#[must_use]
pub const fn field_name(self) -> &'static str {
match self {
MatcherKey::TrackAlias => "track_alias",
MatcherKey::GroupId => "group_id",
MatcherKey::SubgroupId => "subgroup_id",
MatcherKey::ObjectId => "object_id",
MatcherKey::Priority => "priority",
MatcherKey::EveryNth => "every_nth",
}
}
}
#[must_use]
pub fn supports_matcher(draft: DraftVersion, kind: MatchKind, field: MatcherKey) -> bool {
if !draft_is_compiled(draft) {
return false;
}
if kind == MatchKind::Fetch && field == MatcherKey::TrackAlias {
return false;
}
!(kind == MatchKind::Datagram && field == MatcherKey::SubgroupId)
}
fn keys_named(matcher: &Matcher) -> [Option<MatcherKey>; 6] {
[
matcher.track_alias.as_ref().map(|_| MatcherKey::TrackAlias),
matcher.group_id.as_ref().map(|_| MatcherKey::GroupId),
matcher.subgroup_id.as_ref().map(|_| MatcherKey::SubgroupId),
matcher.object_id.as_ref().map(|_| MatcherKey::ObjectId),
matcher.priority.as_ref().map(|_| MatcherKey::Priority),
matcher.every_nth.map(|_| MatcherKey::EveryNth),
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnsupportedMatcherKey {
pub class: String,
pub draft: DraftVersion,
pub kind: Option<MatchKind>,
pub key: MatcherKey,
}
impl std::fmt::Display for UnsupportedMatcherKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (class, key, draft) = (&self.class, self.key.field_name(), self.draft);
match self.kind {
Some(kind) => {
write!(f, "class {class} keys on {key}, which no {kind:?} unit carries on {draft}")
}
None => {
write!(f, "class {class} keys on {key}, which no framed unit carries on {draft}")
}
}
}
}
impl std::error::Error for UnsupportedMatcherKey {}
const fn site_returns_stream_action(site: Site) -> bool {
matches!(site, Site::StreamOpen | Site::StreamHeader)
}
const fn is_stream_decision(kind: ActionKind) -> bool {
matches!(
kind,
ActionKind::Open | ActionKind::Reject | ActionKind::OpenAfter | ActionKind::SerializeAfter
)
}
fn not_attemptable(site: Site, kind: ActionKind) -> Option<Support> {
if kind == ActionKind::ReplaceObject && site != Site::Object {
return Some(Support::NotAttemptable {
why: NotAttemptable::KindNotDefinedAtThisSite,
refusal: Refusal::WrongSite { site, action: kind },
});
}
let why = match (site_returns_stream_action(site), is_stream_decision(kind)) {
(true, false) => NotAttemptable::SiteReturnsStreamAction,
(false, true) => NotAttemptable::SiteReturnsAction,
_ => return None,
};
Some(Support::NotAttemptable { why, refusal: Refusal::WrongSite { site, action: kind } })
}
fn filtered_earlier(site: Site, kind: ActionKind) -> Support {
not_attemptable(site, kind).unwrap_or(Support::NotAttemptable {
why: NotAttemptable::KindNotDefinedAtThisSite,
refusal: Refusal::WrongSite { site, action: kind },
})
}
const fn object_framing_bypass(
draft: DraftVersion,
stream_kind: Option<DataStreamType>,
) -> Option<BypassReason> {
let _ = stream_kind;
if !draft_is_compiled(draft) {
return Some(BypassReason::DecodeError);
}
None
}
#[must_use]
pub const fn draft_is_compiled(draft: DraftVersion) -> bool {
match draft {
DraftVersion::Draft07 => cfg!(feature = "draft07"),
DraftVersion::Draft08 => cfg!(feature = "draft08"),
DraftVersion::Draft09 => cfg!(feature = "draft09"),
DraftVersion::Draft10 => cfg!(feature = "draft10"),
DraftVersion::Draft11 => cfg!(feature = "draft11"),
DraftVersion::Draft12 => cfg!(feature = "draft12"),
DraftVersion::Draft13 => cfg!(feature = "draft13"),
DraftVersion::Draft14 => cfg!(feature = "draft14"),
DraftVersion::Draft15 => cfg!(feature = "draft15"),
DraftVersion::Draft16 => cfg!(feature = "draft16"),
DraftVersion::Draft17 => cfg!(feature = "draft17"),
DraftVersion::Draft18 => cfg!(feature = "draft18"),
DraftVersion::Draft19 => cfg!(feature = "draft19"),
}
}
const DEFAULT_DRAFT_ORDER: [DraftVersion; 13] = [
DraftVersion::Draft14,
DraftVersion::Draft19,
DraftVersion::Draft18,
DraftVersion::Draft17,
DraftVersion::Draft16,
DraftVersion::Draft15,
DraftVersion::Draft13,
DraftVersion::Draft12,
DraftVersion::Draft11,
DraftVersion::Draft10,
DraftVersion::Draft09,
DraftVersion::Draft08,
DraftVersion::Draft07,
];
pub const DEFAULT_DRAFT: DraftVersion = default_draft();
const fn default_draft() -> DraftVersion {
let mut i = 0;
while i < DEFAULT_DRAFT_ORDER.len() {
if draft_is_compiled(DEFAULT_DRAFT_ORDER[i]) {
return DEFAULT_DRAFT_ORDER[i];
}
i += 1;
}
panic!("this build compiled no draft at all, so there is no default to take")
}
const _: () = assert!(
draft_is_compiled(DEFAULT_DRAFT),
"the default draft is one this build did not compile"
);
pub(crate) const fn fetch_group_order_is_needed(draft: DraftVersion) -> bool {
matches!(draft, DraftVersion::Draft18 | DraftVersion::Draft19)
}
const fn has_implicit_subgroup_id_mode(draft: DraftVersion) -> bool {
matches!(
draft,
DraftVersion::Draft11
| DraftVersion::Draft12
| DraftVersion::Draft13
| DraftVersion::Draft14
| DraftVersion::Draft15
| DraftVersion::Draft16
| DraftVersion::Draft17
| DraftVersion::Draft18
| DraftVersion::Draft19
)
}
const fn subgroup_id_mode_must_be_consulted(draft: DraftVersion) -> bool {
matches!(
draft,
DraftVersion::Draft15
| DraftVersion::Draft16
| DraftVersion::Draft17
| DraftVersion::Draft18
| DraftVersion::Draft19
)
}
fn classify_control(kind: ActionKind) -> Support {
let honoured = Support::Yes;
match kind {
ActionKind::Pass
| ActionKind::Replace
| ActionKind::Delay
| ActionKind::Hold
| ActionKind::DropElide
| ActionKind::CloseSession => honoured,
ActionKind::ReplacePayload => {
Support::No(Refusal::WrongSite { site: Site::Control, action: kind })
}
ActionKind::Truncate | ActionKind::ResetStream => {
Support::No(Refusal::ControlStreamResetIllegal)
}
ActionKind::Open
| ActionKind::Reject
| ActionKind::ReplaceObject
| ActionKind::OpenAfter
| ActionKind::SerializeAfter => filtered_earlier(Site::Control, kind),
}
}
fn classify_object(kind: ActionKind, cx: &CapCtx) -> Support {
match kind {
ActionKind::Pass
| ActionKind::Delay
| ActionKind::Hold
| ActionKind::Truncate
| ActionKind::ResetStream
| ActionKind::CloseSession => Support::Yes,
ActionKind::Replace | ActionKind::ReplaceObject => Support::No(Refusal::WrongSite {
site: Site::Object,
action: ActionKind::ReplaceObject,
}),
ActionKind::ReplacePayload => object_replace_payload(cx),
ActionKind::DropElide => object_drop_elide(cx),
ActionKind::Open
| ActionKind::Reject
| ActionKind::OpenAfter
| ActionKind::SerializeAfter => filtered_earlier(Site::Object, kind),
}
}
fn object_replace_payload(cx: &CapCtx) -> Support {
if cx.is_status_object == Some(true) {
return Support::No(Refusal::WouldDestroyStatusObject);
}
match (cx.payload_len, cx.replacement_len) {
(Some(from), Some(to)) if from != to => Support::No(Refusal::LengthChanged { from, to }),
(Some(_), Some(_)) if cx.is_status_object == Some(false) => Support::Yes,
(Some(_), Some(_)) => Support::Conditional(Precondition::NotAStatusObject),
_ => Support::Conditional(Precondition::ReplacementLengthEqualsPayload),
}
}
fn object_drop_elide(cx: &CapCtx) -> Support {
let subgroup_stream = cx.stream_kind != Some(DataStreamType::Fetch);
let implicit_mode = cx.draft.is_none_or(has_implicit_subgroup_id_mode);
if subgroup_stream && implicit_mode {
match cx.index_in_stream {
Some(index) if index != 0 => {}
Some(_) => {
if cx.draft.is_none_or(subgroup_id_mode_must_be_consulted)
&& cx.subgroup_id_mode == Some(RESERVED_SUBGROUP_ID_MODE)
{
return Support::No(Refusal::ReservedHeaderMode {
mode: RESERVED_SUBGROUP_ID_MODE,
});
}
match cx.subgroup_id_resolved {
Some(false) => return Support::No(Refusal::WouldRedefineSubgroupId),
None => {
return Support::Conditional(Precondition::NotFirstObjectOfImplicitSubgroup)
}
Some(true) => {}
}
}
None => {
return Support::Conditional(Precondition::NotFirstObjectOfImplicitSubgroup);
}
}
}
match cx.is_status_object {
Some(true) => Support::No(Refusal::WouldDestroyStatusObject),
Some(false) => Support::Yes,
None => Support::Conditional(Precondition::NotAStatusObject),
}
}
fn classify_datagram(kind: ActionKind, cx: &CapCtx) -> Support {
match kind {
ActionKind::Pass | ActionKind::DropElide | ActionKind::CloseSession => Support::Yes,
ActionKind::Replace => Support::Conditional(Precondition::WithinMaxDatagramSize),
ActionKind::ReplacePayload => datagram_replace_payload(cx),
ActionKind::Delay | ActionKind::Hold | ActionKind::Truncate | ActionKind::ResetStream => {
Support::No(Refusal::WrongSite { site: Site::Datagram, action: kind })
}
ActionKind::Open
| ActionKind::Reject
| ActionKind::ReplaceObject
| ActionKind::OpenAfter
| ActionKind::SerializeAfter => filtered_earlier(Site::Datagram, kind),
}
}
fn datagram_replace_payload(cx: &CapCtx) -> Support {
match cx.payload_delimited {
Some(true) => Support::Yes,
Some(false) => {
Support::No(Refusal::PayloadNotDelimited { detail: payload_not_delimited_detail(cx) })
}
None => Support::Conditional(Precondition::DatagramPayloadDelimited),
}
}
fn payload_not_delimited_detail(cx: &CapCtx) -> &'static str {
if cx.draft == Some(DraftVersion::Draft14) {
"draft-14 header decode consumes the payload"
} else if cx.is_status_object == Some(true) {
"status datagram has no payload"
} else {
"datagram header did not decode"
}
}
fn classify_stream_decision(site: Site, kind: ActionKind) -> Support {
match kind {
ActionKind::Open | ActionKind::Reject | ActionKind::SerializeAfter => Support::Yes,
ActionKind::OpenAfter => match site {
Site::StreamOpen => Support::Yes,
_ => Support::No(Refusal::WrongSite { site, action: kind }),
},
ActionKind::Pass
| ActionKind::Replace
| ActionKind::ReplacePayload
| ActionKind::Delay
| ActionKind::Hold
| ActionKind::DropElide
| ActionKind::Truncate
| ActionKind::ResetStream
| ActionKind::CloseSession
| ActionKind::ReplaceObject => filtered_earlier(site, kind),
}
}
fn classify_stream_end(kind: ActionKind, cx: &CapCtx) -> Support {
let control = cx.is_control_stream == Some(true);
match kind {
ActionKind::Pass | ActionKind::CloseSession => Support::Yes,
ActionKind::ResetStream => {
if control {
Support::No(Refusal::ControlStreamResetIllegal)
} else {
Support::Yes
}
}
ActionKind::Truncate => {
if control {
Support::No(Refusal::ControlStreamResetIllegal)
} else {
Support::No(Refusal::WrongSite { site: Site::StreamEnd, action: kind })
}
}
ActionKind::Replace
| ActionKind::ReplacePayload
| ActionKind::Delay
| ActionKind::Hold
| ActionKind::DropElide => {
Support::No(Refusal::WrongSite { site: Site::StreamEnd, action: kind })
}
ActionKind::Open
| ActionKind::Reject
| ActionKind::ReplaceObject
| ActionKind::OpenAfter
| ActionKind::SerializeAfter => filtered_earlier(Site::StreamEnd, kind),
}
}
#[cfg(test)]
mod tests {
use super::*;
const DRAFTS: [DraftVersion; 13] = [
DraftVersion::Draft07,
DraftVersion::Draft08,
DraftVersion::Draft09,
DraftVersion::Draft10,
DraftVersion::Draft11,
DraftVersion::Draft12,
DraftVersion::Draft13,
DraftVersion::Draft14,
DraftVersion::Draft15,
DraftVersion::Draft16,
DraftVersion::Draft17,
DraftVersion::Draft18,
DraftVersion::Draft19,
];
const KINDS: [ActionKind; 14] = [
ActionKind::Pass,
ActionKind::Replace,
ActionKind::ReplacePayload,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::DropElide,
ActionKind::Truncate,
ActionKind::ResetStream,
ActionKind::CloseSession,
ActionKind::Open,
ActionKind::Reject,
ActionKind::ReplaceObject,
ActionKind::OpenAfter,
ActionKind::SerializeAfter,
];
const SITES: [Site; 6] = [
Site::Control,
Site::Object,
Site::Datagram,
Site::StreamOpen,
Site::StreamHeader,
Site::StreamEnd,
];
fn wrong_site(site: Site, action: ActionKind) -> Support {
Support::No(Refusal::WrongSite { site, action })
}
fn returns_action(site: Site, action: ActionKind) -> Support {
Support::NotAttemptable {
why: NotAttemptable::SiteReturnsAction,
refusal: Refusal::WrongSite { site, action },
}
}
fn returns_stream_action(site: Site, action: ActionKind) -> Support {
Support::NotAttemptable {
why: NotAttemptable::SiteReturnsStreamAction,
refusal: Refusal::WrongSite { site, action },
}
}
fn kind_not_here(site: Site, action: ActionKind) -> Support {
Support::NotAttemptable {
why: NotAttemptable::KindNotDefinedAtThisSite,
refusal: Refusal::WrongSite { site, action },
}
}
fn unreachable_with(reason: BypassReason) -> Support {
Support::Unreachable {
refusal: Refusal::StreamNotFramed { reason },
instead: Instead::FramerBypass(reason),
}
}
fn unreachable_control() -> Support {
Support::Unreachable {
refusal: Refusal::ControlFrameNotDecodable,
instead: Instead::ControlFrameNotDecodable,
}
}
fn framing_verdict(draft: DraftVersion, stream_kind: DataStreamType) -> Option<Support> {
object_framing_bypass(draft, Some(stream_kind)).map(unreachable_with)
}
#[cfg(any(
feature = "draft07",
feature = "draft08",
feature = "draft09",
feature = "draft10",
feature = "draft11",
feature = "draft12",
feature = "draft13",
feature = "draft14",
feature = "draft15",
feature = "draft16",
feature = "draft17",
feature = "draft18",
feature = "draft19"
))]
fn some_compiled_draft() -> DraftVersion {
DRAFTS
.into_iter()
.find(|d| draft_is_compiled(*d))
.expect("gated on `any(draft07..draft19)`, so the compiled set is non-empty")
}
#[test]
fn a_default_configuration_can_shape_the_draft_it_names() {
let draft = crate::session::ProxySessionConfig::default().draft;
assert!(
supports_matcher(draft, MatchKind::Subgroup, MatcherKey::GroupId),
"a default configuration names {draft:?}, which this build did not compile, so \
every matcher key is refused on it"
);
}
fn compiled_drafts_where(pred: fn(DraftVersion) -> bool) -> Vec<DraftVersion> {
DRAFTS.into_iter().filter(|d| draft_is_compiled(*d) && pred(*d)).collect()
}
fn a_first_object_carrier_exists(draft: DraftVersion) -> bool {
match draft {
DraftVersion::Draft07
| DraftVersion::Draft08
| DraftVersion::Draft09
| DraftVersion::Draft10 => false,
DraftVersion::Draft11
| DraftVersion::Draft12
| DraftVersion::Draft13
| DraftVersion::Draft14
| DraftVersion::Draft15
| DraftVersion::Draft16
| DraftVersion::Draft17
| DraftVersion::Draft18
| DraftVersion::Draft19 => true,
}
}
#[test]
fn object_site_on_subgroup_streams_matches_the_published_table() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let cell = |kind| caps.supports_on(Site::Object, kind, DataStreamType::Subgroup);
let bypassed = framing_verdict(draft, DataStreamType::Subgroup);
for kind in [
ActionKind::Pass,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::Truncate,
ActionKind::ResetStream,
ActionKind::CloseSession,
] {
let want = bypassed.clone().unwrap_or(Support::Yes);
assert_eq!(cell(kind), want, "{draft:?} {kind:?}");
}
let want = bypassed
.clone()
.unwrap_or(Support::Conditional(Precondition::ReplacementLengthEqualsPayload));
assert_eq!(cell(ActionKind::ReplacePayload), want, "{draft:?}: ReplacePayload support");
let elide_headline = if a_first_object_carrier_exists(draft) {
Precondition::NotFirstObjectOfImplicitSubgroup
} else {
Precondition::NotAStatusObject
};
let want = bypassed.clone().unwrap_or(Support::Conditional(elide_headline));
assert_eq!(cell(ActionKind::DropElide), want, "{draft:?} elide");
let whole_object =
bypassed.clone().unwrap_or(wrong_site(Site::Object, ActionKind::ReplaceObject));
assert_eq!(cell(ActionKind::Replace), whole_object, "{draft:?}");
assert_eq!(cell(ActionKind::ReplaceObject), whole_object, "{draft:?}");
for kind in [
ActionKind::Open,
ActionKind::Reject,
ActionKind::OpenAfter,
ActionKind::SerializeAfter,
] {
assert_eq!(cell(kind), returns_action(Site::Object, kind), "{draft:?}");
}
}
}
#[test]
fn object_site_on_fetch_streams_matches_the_published_table() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let cell = |kind| caps.supports_on(Site::Object, kind, DataStreamType::Fetch);
let bypassed = framing_verdict(draft, DataStreamType::Fetch);
if !draft_is_compiled(draft) {
assert_eq!(
bypassed.clone(),
Some(unreachable_with(BypassReason::DecodeError)),
"{draft:?} is not compiled: the header decode is what fails"
);
} else {
assert_eq!(
bypassed.clone(),
None,
"{draft:?} is compiled, so its fetch objects are the per-kind rules' to decide"
);
}
for kind in [
ActionKind::Pass,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::Truncate,
ActionKind::ResetStream,
ActionKind::CloseSession,
] {
let want = bypassed.clone().unwrap_or(Support::Yes);
assert_eq!(cell(kind), want, "{draft:?} {kind:?}");
}
let want = bypassed
.clone()
.unwrap_or(Support::Conditional(Precondition::ReplacementLengthEqualsPayload));
assert_eq!(cell(ActionKind::ReplacePayload), want, "{draft:?}");
let want =
bypassed.clone().unwrap_or(Support::Conditional(Precondition::NotAStatusObject));
assert_eq!(cell(ActionKind::DropElide), want, "{draft:?}");
for kind in [ActionKind::Replace, ActionKind::ReplaceObject] {
let want =
bypassed.clone().unwrap_or(wrong_site(Site::Object, ActionKind::ReplaceObject));
assert_eq!(cell(kind), want, "{draft:?} {kind:?}");
}
for kind in [
ActionKind::Open,
ActionKind::Reject,
ActionKind::OpenAfter,
ActionKind::SerializeAfter,
] {
assert_eq!(cell(kind), returns_action(Site::Object, kind), "{draft:?}");
}
}
}
#[test]
fn control_site_matches_the_published_table() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let cell = |kind| caps.supports(Site::Control, kind);
let unreachable = !draft_is_compiled(draft);
let or_unreachable =
|want: Support| if unreachable { unreachable_control() } else { want };
for kind in [
ActionKind::Pass,
ActionKind::Replace,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::DropElide,
ActionKind::CloseSession,
] {
assert_eq!(cell(kind), or_unreachable(Support::Yes), "{draft:?} {kind:?}");
}
assert_eq!(
cell(ActionKind::ReplacePayload),
or_unreachable(wrong_site(Site::Control, ActionKind::ReplacePayload)),
"{draft:?}"
);
for kind in [ActionKind::Truncate, ActionKind::ResetStream] {
assert_eq!(
cell(kind),
or_unreachable(Support::No(Refusal::ControlStreamResetIllegal)),
"{draft:?} {kind:?}"
);
}
for kind in [ActionKind::Open, ActionKind::Reject] {
assert_eq!(cell(kind), returns_action(Site::Control, kind), "{draft:?}");
}
assert_eq!(
cell(ActionKind::ReplaceObject),
kind_not_here(Site::Control, ActionKind::ReplaceObject),
"{draft:?}"
);
}
}
#[test]
fn datagram_site_matches_the_published_table() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let cell = |kind| caps.supports(Site::Datagram, kind);
for kind in [ActionKind::Pass, ActionKind::DropElide, ActionKind::CloseSession] {
assert_eq!(cell(kind), Support::Yes, "{draft:?} {kind:?}");
}
assert_eq!(
cell(ActionKind::Replace),
Support::Conditional(Precondition::WithinMaxDatagramSize),
"{draft:?}"
);
assert_eq!(
cell(ActionKind::ReplacePayload),
Support::Conditional(Precondition::DatagramPayloadDelimited),
"{draft:?}"
);
for kind in
[ActionKind::Delay, ActionKind::Hold, ActionKind::Truncate, ActionKind::ResetStream]
{
assert_eq!(cell(kind), wrong_site(Site::Datagram, kind), "{draft:?} {kind:?}");
}
}
}
#[test]
fn stream_decision_sites_match_the_published_table() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
for site in [Site::StreamOpen, Site::StreamHeader] {
assert_eq!(caps.supports(site, ActionKind::Open), Support::Yes);
assert_eq!(caps.supports(site, ActionKind::Reject), Support::Yes);
assert_eq!(
caps.supports(site, ActionKind::SerializeAfter),
Support::Yes,
"{draft:?} {site:?}"
);
let open_after = if site == Site::StreamOpen {
Support::Yes
} else {
wrong_site(site, ActionKind::OpenAfter)
};
assert_eq!(
caps.supports(site, ActionKind::OpenAfter),
open_after,
"{draft:?} {site:?}"
);
for kind in [
ActionKind::Pass,
ActionKind::Replace,
ActionKind::ReplacePayload,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::DropElide,
ActionKind::Truncate,
ActionKind::ResetStream,
ActionKind::CloseSession,
] {
assert_eq!(
caps.supports(site, kind),
returns_stream_action(site, kind),
"{draft:?} {site:?} {kind:?}"
);
}
assert_eq!(
caps.supports(site, ActionKind::ReplaceObject),
kind_not_here(site, ActionKind::ReplaceObject)
);
}
}
}
#[test]
fn stream_end_is_answered_for_both_data_and_control_streams() {
for draft in DRAFTS {
for is_control in [false, true] {
let cx = CapCtx {
draft: Some(draft),
is_control_stream: Some(is_control),
..CapCtx::default()
};
let cell = |kind| classify(Site::StreamEnd, kind, &cx);
assert_eq!(cell(ActionKind::Pass), Support::Yes, "{draft:?}");
assert_eq!(cell(ActionKind::CloseSession), Support::Yes, "{draft:?}");
let reset = cell(ActionKind::ResetStream);
if is_control {
assert_eq!(reset, Support::No(Refusal::ControlStreamResetIllegal));
} else {
assert_eq!(reset, Support::Yes);
}
let truncate = cell(ActionKind::Truncate);
if is_control {
assert_eq!(truncate, Support::No(Refusal::ControlStreamResetIllegal));
} else {
assert_eq!(truncate, wrong_site(Site::StreamEnd, ActionKind::Truncate));
}
for kind in [
ActionKind::Replace,
ActionKind::ReplacePayload,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::DropElide,
] {
assert_eq!(
cell(kind),
wrong_site(Site::StreamEnd, kind),
"{draft:?} control={is_control} {kind:?}"
);
}
}
}
}
#[test]
fn replace_object_has_exactly_one_reading() {
for site in SITES {
let verdict = classify(site, ActionKind::ReplaceObject, &CapCtx::default());
if site == Site::Object {
assert_eq!(
verdict,
wrong_site(Site::Object, ActionKind::ReplaceObject),
"the object site really is asked, and really refuses"
);
} else {
assert_eq!(verdict, kind_not_here(site, ActionKind::ReplaceObject), "{site:?}");
}
}
}
#[test]
fn replace_and_replace_object_agree_at_the_object_site() {
for draft in DRAFTS {
for stream_kind in [DataStreamType::Subgroup, DataStreamType::Fetch] {
let caps = Capabilities::for_draft(draft);
assert_eq!(
caps.supports_on(Site::Object, ActionKind::Replace, stream_kind),
caps.supports_on(Site::Object, ActionKind::ReplaceObject, stream_kind),
"{draft:?} {stream_kind:?}"
);
}
}
}
#[test]
fn table_only_refusals_never_appear_as_no() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
for site in SITES {
for kind in KINDS {
for verdict in [
caps.supports(site, kind),
caps.supports_on(site, kind, DataStreamType::Subgroup),
caps.supports_on(site, kind, DataStreamType::Fetch),
] {
let Support::No(refusal) = verdict else {
continue;
};
assert!(
!matches!(
refusal,
Refusal::StreamNotFramed { .. }
),
"{draft:?} {site:?} {kind:?} declares a table-only refusal as No({refusal:?})"
);
}
}
}
}
}
#[test]
fn every_site_kind_pair_is_answered() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
for site in SITES {
for kind in KINDS {
let verdict = caps.supports(site, kind);
if let Support::NotAttemptable {
why: NotAttemptable::KindNotDefinedAtThisSite,
..
} = verdict
{
assert_eq!(
kind,
ActionKind::ReplaceObject,
"{site:?} {kind:?} fell through to the filtered arm"
);
}
}
}
}
}
#[cfg(any(
feature = "draft07",
feature = "draft08",
feature = "draft09",
feature = "draft10",
feature = "draft11",
feature = "draft12",
feature = "draft13",
feature = "draft14",
feature = "draft15",
feature = "draft16",
feature = "draft17",
feature = "draft18",
feature = "draft19"
))]
#[test]
fn replace_payload_length_mismatch_is_length_changed() {
let cx = CapCtx {
draft: Some(some_compiled_draft()),
payload_len: Some(1200),
replacement_len: Some(800),
is_status_object: Some(false),
..CapCtx::default()
};
assert_eq!(
classify(Site::Object, ActionKind::ReplacePayload, &cx),
Support::No(Refusal::LengthChanged { from: 1200, to: 800 })
);
let ok = CapCtx { replacement_len: Some(1200), ..cx };
assert_eq!(classify(Site::Object, ActionKind::ReplacePayload, &ok), Support::Yes);
}
#[cfg(any(
feature = "draft07",
feature = "draft08",
feature = "draft09",
feature = "draft10",
feature = "draft11",
feature = "draft12",
feature = "draft13",
feature = "draft14",
feature = "draft15",
feature = "draft16",
feature = "draft17",
feature = "draft18",
feature = "draft19"
))]
#[test]
fn replace_payload_on_a_status_object_is_refused() {
let cx = CapCtx {
draft: Some(some_compiled_draft()),
payload_len: Some(0),
replacement_len: Some(0),
is_status_object: Some(true),
..CapCtx::default()
};
assert_eq!(
classify(Site::Object, ActionKind::ReplacePayload, &cx),
Support::No(Refusal::WouldDestroyStatusObject)
);
}
#[test]
fn elide_guards_follow_the_execution_order() {
for draft in compiled_drafts_where(subgroup_id_mode_must_be_consulted) {
let base = CapCtx {
draft: Some(draft),
stream_kind: Some(DataStreamType::Subgroup),
index_in_stream: Some(0),
subgroup_id_resolved: Some(false),
is_status_object: Some(false),
..CapCtx::default()
};
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &base),
Support::No(Refusal::WouldRedefineSubgroupId),
"{draft:?}"
);
let reserved = CapCtx { subgroup_id_mode: Some(3), ..base };
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &reserved),
Support::No(Refusal::ReservedHeaderMode { mode: 3 }),
"{draft:?}"
);
let later = CapCtx { index_in_stream: Some(1), ..base };
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &later),
Support::Yes,
"{draft:?}"
);
let status = CapCtx { is_status_object: Some(true), ..later };
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &status),
Support::No(Refusal::WouldDestroyStatusObject),
"{draft:?}"
);
}
}
#[test]
fn a_reserved_mode_is_answered_as_itself_wherever_a_header_can_carry_one() {
for draft in compiled_drafts_where(|_| true) {
let cx = CapCtx {
draft: Some(draft),
stream_kind: Some(DataStreamType::Subgroup),
index_in_stream: Some(0),
subgroup_id_resolved: Some(false),
is_status_object: Some(false),
subgroup_id_mode: Some(RESERVED_SUBGROUP_ID_MODE),
..CapCtx::default()
};
let want = match draft {
DraftVersion::Draft07
| DraftVersion::Draft08
| DraftVersion::Draft09
| DraftVersion::Draft10 => Support::Yes,
DraftVersion::Draft11
| DraftVersion::Draft12
| DraftVersion::Draft13
| DraftVersion::Draft14 => Support::No(Refusal::WouldRedefineSubgroupId),
DraftVersion::Draft15
| DraftVersion::Draft16
| DraftVersion::Draft17
| DraftVersion::Draft18
| DraftVersion::Draft19 => {
Support::No(Refusal::ReservedHeaderMode { mode: RESERVED_SUBGROUP_ID_MODE })
}
};
assert_eq!(classify(Site::Object, ActionKind::DropElide, &cx), want, "{draft:?}");
}
}
#[test]
fn a_resolved_subgroup_id_frees_the_first_object_on_every_draft() {
for draft in compiled_drafts_where(|_| true) {
let cx = CapCtx {
draft: Some(draft),
stream_kind: Some(DataStreamType::Subgroup),
index_in_stream: Some(0),
subgroup_id_resolved: Some(true),
is_status_object: Some(false),
..CapCtx::default()
};
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &cx),
Support::Yes,
"{draft:?}"
);
}
}
#[test]
fn the_first_object_subgroup_guard_turns_on_the_stream_kind() {
for draft in compiled_drafts_where(|_| true) {
let cx = |stream_kind| CapCtx {
draft: Some(draft),
stream_kind: Some(stream_kind),
index_in_stream: Some(0),
subgroup_id_resolved: Some(false),
is_status_object: Some(false),
..CapCtx::default()
};
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &cx(DataStreamType::Fetch)),
Support::Yes,
"{draft:?} fetch"
);
let subgroup =
classify(Site::Object, ActionKind::DropElide, &cx(DataStreamType::Subgroup));
if a_first_object_carrier_exists(draft) {
assert_eq!(
subgroup,
Support::No(Refusal::WouldRedefineSubgroupId),
"{draft:?} subgroup"
);
} else {
assert_eq!(subgroup, Support::Yes, "{draft:?} subgroup");
}
}
}
#[test]
fn eliding_a_fetch_object_turns_only_on_its_status() {
for draft in compiled_drafts_where(|_| true) {
for index in [0u64, 1, 7] {
for status in [Some(false), Some(true), None] {
let cx = CapCtx {
draft: Some(draft),
stream_kind: Some(DataStreamType::Fetch),
index_in_stream: Some(index),
is_status_object: status,
..CapCtx::default()
};
let want = match status {
Some(true) => Support::No(Refusal::WouldDestroyStatusObject),
Some(false) => Support::Yes,
None => Support::Conditional(Precondition::NotAStatusObject),
};
assert_eq!(
classify(Site::Object, ActionKind::DropElide, &cx),
want,
"{draft:?} index {index} status {status:?}"
);
}
}
}
}
#[test]
fn datagram_payload_not_delimited_names_its_case() {
let undelimited = |draft, status| CapCtx {
draft: Some(draft),
payload_delimited: Some(false),
is_status_object: status,
..CapCtx::default()
};
let detail = |cx: CapCtx| match classify(Site::Datagram, ActionKind::ReplacePayload, &cx) {
Support::No(Refusal::PayloadNotDelimited { detail }) => detail,
other => panic!("expected PayloadNotDelimited, got {other:?}"),
};
assert_eq!(
detail(undelimited(DraftVersion::Draft14, Some(false))),
"draft-14 header decode consumes the payload"
);
assert_eq!(
detail(undelimited(DraftVersion::Draft19, Some(true))),
"status datagram has no payload"
);
assert_eq!(
detail(undelimited(DraftVersion::Draft19, None)),
"datagram header did not decode"
);
let delimited = CapCtx {
draft: Some(DraftVersion::Draft19),
payload_delimited: Some(true),
..CapCtx::default()
};
assert_eq!(classify(Site::Datagram, ActionKind::ReplacePayload, &delimited), Support::Yes);
}
#[test]
fn the_control_site_is_honoured_on_every_draft() {
const UNI_CONTROL_PLANE: [DraftVersion; 3] =
[DraftVersion::Draft17, DraftVersion::Draft18, DraftVersion::Draft19];
const HONOURED: [ActionKind; 6] = [
ActionKind::Pass,
ActionKind::Replace,
ActionKind::Delay,
ActionKind::Hold,
ActionKind::DropElide,
ActionKind::CloseSession,
];
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let uni_control_plane = UNI_CONTROL_PLANE.contains(&draft);
if !draft_is_compiled(draft) {
continue;
}
for kind in HONOURED {
let verdict = caps.supports(Site::Control, kind);
assert_eq!(
verdict,
Support::Yes,
"{draft:?} {kind:?} (pair-of-unidirectional control plane: \
{uni_control_plane})"
);
assert!(
!matches!(
verdict,
Support::Unreachable { .. } | Support::NotAttemptable { .. }
),
"{draft:?} {kind:?}: the control site is attemptable on every draft"
);
}
if !draft_is_compiled(draft) {
assert!(
matches!(
caps.supports_on(Site::Object, ActionKind::Pass, DataStreamType::Fetch),
Support::Unreachable { .. }
),
"{draft:?}: the module does have a verdict for 'never invoked'"
);
}
for kind in [ActionKind::Truncate, ActionKind::ResetStream] {
assert_eq!(
caps.supports(Site::Control, kind),
Support::No(Refusal::ControlStreamResetIllegal),
"{draft:?} {kind:?}"
);
}
}
}
#[test]
fn the_control_site_is_unreachable_on_a_draft_this_build_did_not_compile() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
let want = if draft_is_compiled(draft) { Support::Yes } else { unreachable_control() };
assert_eq!(caps.supports(Site::Control, ActionKind::Pass), want, "{draft:?}");
assert_eq!(
caps.supports(Site::Control, ActionKind::ResetStream),
if draft_is_compiled(draft) {
Support::No(Refusal::ControlStreamResetIllegal)
} else {
unreachable_control()
},
"{draft:?}: the reset refusal is published only where a hook could receive it"
);
assert_eq!(
caps.supports(Site::Control, ActionKind::ReplaceObject),
kind_not_here(Site::Control, ActionKind::ReplaceObject),
"{draft:?}: a control frame is not an object on any build"
);
}
}
#[test]
fn the_object_site_is_unreachable_on_a_draft_this_build_did_not_compile() {
for draft in DRAFTS {
let caps = Capabilities::for_draft(draft);
for stream_kind in [DataStreamType::Subgroup, DataStreamType::Fetch] {
let verdict = caps.supports_on(Site::Object, ActionKind::Pass, stream_kind);
if draft_is_compiled(draft) {
assert_eq!(verdict, Support::Yes, "{draft:?} {stream_kind:?}");
} else {
assert_eq!(
verdict,
unreachable_with(BypassReason::DecodeError),
"{draft:?} {stream_kind:?} is not compiled: the stream header decode \
returns UnsupportedDraft, the framer latches DecodeError, and no object \
reaches the hook"
);
}
}
}
}
#[test]
fn the_compiled_draft_set_agrees_with_the_enabled_features() {
let compiled: Vec<DraftVersion> =
DRAFTS.into_iter().filter(|d| draft_is_compiled(*d)).collect();
let build_has_a_draft = cfg!(any(
feature = "draft07",
feature = "draft08",
feature = "draft09",
feature = "draft10",
feature = "draft11",
feature = "draft12",
feature = "draft13",
feature = "draft14",
feature = "draft15",
feature = "draft16",
feature = "draft17",
feature = "draft18",
feature = "draft19"
));
assert_eq!(
!compiled.is_empty(),
build_has_a_draft,
"`draft_is_compiled` reports {compiled:?}, but this build has {} draft feature \
enabled",
if build_has_a_draft { "at least one" } else { "no" }
);
}
#[cfg(feature = "all-drafts")]
#[test]
fn the_default_build_compiles_every_draft() {
for draft in DRAFTS {
assert!(draft_is_compiled(draft), "{draft:?} is missing from `all-drafts`");
}
}
#[test]
fn a_default_context_is_answerable_at_every_cell() {
for site in SITES {
for kind in KINDS {
let _ = classify(site, kind, &CapCtx::default());
}
}
}
}