use std::collections::{BTreeMap, BTreeSet};
use std::iter::FromIterator;
use std::num::NonZeroU64;
use crate::freshness::FreshnessPolicy;
use crate::payload::Payload;
use crate::specs::trust_task_discovery::v0_3 as wire_v0_3;
use crate::type_uri::TypeUri;
mod superseded;
const DEFAULT_FRAMEWORK_VERSION: &str = "0.6.0";
fn framework_version_parts(v: &str) -> Option<(u64, u64, u64)> {
let mut parts = v.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = match parts.next() {
Some(p) => p.parse().ok()?,
None => 0,
};
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
pub fn match_slug(pattern: &str, slug: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix("/*") {
if prefix.is_empty() {
return false;
}
let mut full_prefix = String::with_capacity(prefix.len() + 1);
full_prefix.push_str(prefix);
full_prefix.push('/');
return slug.starts_with(&full_prefix);
}
pattern == slug
}
pub fn query_matches<S: AsRef<str>>(patterns: &[S], slug: &str) -> bool {
if patterns.is_empty() {
return true;
}
patterns.iter().any(|p| match_slug(p.as_ref(), slug))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AcceptanceWindow {
max_age_seconds: NonZeroU64,
clock_skew_seconds: u64,
}
impl AcceptanceWindow {
fn of_policy(policy: &FreshnessPolicy) -> Option<Self> {
let max_age_seconds = u64::try_from(policy.max_age?.num_seconds()).ok()?;
Some(Self {
max_age_seconds: NonZeroU64::new(max_age_seconds)?,
clock_skew_seconds: u64::try_from(policy.skew.num_seconds()).unwrap_or(0),
})
}
fn to_wire(self) -> wire_v0_3::AcceptanceWindow {
wire_v0_3::AcceptanceWindow {
max_age_seconds: self.max_age_seconds,
clock_skew_seconds: self.clock_skew_seconds,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DiscoveryRegistry {
type_uris: BTreeSet<String>,
required_ext: BTreeMap<String, BTreeSet<String>>,
framework_version: Option<String>,
acceptance_window: Option<AcceptanceWindow>,
entry_acceptance_windows: BTreeMap<String, AcceptanceWindow>,
}
impl DiscoveryRegistry {
pub fn new() -> Self {
Self {
framework_version: Some(DEFAULT_FRAMEWORK_VERSION.to_string()),
..Self::default()
}
}
pub fn framework_version(mut self, version: impl Into<String>) -> Self {
self.framework_version = Some(version.into());
self
}
pub fn no_framework_version(mut self) -> Self {
self.framework_version = None;
self
}
pub fn acceptance_window(
mut self,
max_age_seconds: NonZeroU64,
clock_skew_seconds: u64,
) -> Self {
self.acceptance_window = Some(AcceptanceWindow {
max_age_seconds,
clock_skew_seconds,
});
self
}
pub fn acceptance_window_from(mut self, policy: &FreshnessPolicy) -> Self {
if let Some(window) = AcceptanceWindow::of_policy(policy) {
self.acceptance_window = Some(window);
}
self
}
pub fn no_acceptance_window(mut self) -> Self {
self.acceptance_window = None;
self
}
pub fn with_acceptance_window_for(
mut self,
uri: TypeUri,
max_age_seconds: NonZeroU64,
clock_skew_seconds: u64,
) -> Self {
let bare = uri.bare().to_string();
self.type_uris.insert(bare.clone());
self.entry_acceptance_windows.insert(
bare,
AcceptanceWindow {
max_age_seconds,
clock_skew_seconds,
},
);
self
}
pub fn with_required_ext<I, S>(mut self, uri: TypeUri, namespaces: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let bare = uri.bare().to_string();
self.type_uris.insert(bare.clone());
let entry = self.required_ext.entry(bare).or_default();
entry.extend(namespaces.into_iter().map(Into::into));
self
}
pub fn require_ext<I, S>(&mut self, uri: TypeUri, namespaces: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let bare = uri.bare().to_string();
self.type_uris.insert(bare.clone());
let entry = self.required_ext.entry(bare).or_default();
entry.extend(namespaces.into_iter().map(Into::into));
}
pub fn with<P: Payload>(self) -> Self {
let uri = P::type_uri();
self.with_type_uri(uri)
}
pub fn with_type_uri(mut self, uri: TypeUri) -> Self {
self.type_uris.insert(uri.bare().to_string());
self
}
pub fn register(&mut self, uri: TypeUri) {
self.type_uris.insert(uri.bare().to_string());
}
pub fn register_payload<P: Payload>(&mut self) {
self.register(P::type_uri());
}
pub fn register_str(&mut self, uri: impl Into<String>) {
self.type_uris.insert(uri.into());
}
pub fn with_str(mut self, uri: impl Into<String>) -> Self {
self.register_str(uri);
self
}
pub fn supported_types(&self) -> Vec<&str> {
self.type_uris.iter().map(String::as_str).collect()
}
pub fn respond_to_v0_3(&self, query: &wire_v0_3::Payload) -> wire_v0_3::Response {
let patterns: Vec<&str> = query.patterns.iter().map(|p| p.as_str()).collect();
let supported_types: Vec<wire_v0_3::ResponseSupportedTypesItem> = self
.matching(&patterns)
.map(|uri| {
let required_ext = self
.required_ext_for(uri)
.map(|namespaces| {
namespaces
.filter_map(|ns| ns.parse().ok())
.collect::<Vec<_>>()
})
.filter(|namespaces| !namespaces.is_empty());
let acceptance_window = self
.entry_acceptance_windows
.get(uri)
.map(|window| window.to_wire());
if required_ext.is_none() && acceptance_window.is_none() {
wire_v0_3::ResponseSupportedTypesItem::Uri(uri.to_string())
} else {
wire_v0_3::ResponseSupportedTypesItem::Object {
type_: uri.to_string(),
required_ext,
acceptance_window,
}
}
})
.collect();
wire_v0_3::Response {
supported_types,
framework_version: self
.three_part_framework_version()
.and_then(|v| v.parse().ok()),
acceptance_window: self.acceptance_window.map(AcceptanceWindow::to_wire),
}
}
fn three_part_framework_version(&self) -> Option<String> {
self.framework_version
.as_deref()
.and_then(framework_version_parts)
.map(|(major, minor, patch)| format!("{major}.{minor}.{patch}"))
}
fn matching<'a>(&'a self, patterns: &'a [&'a str]) -> impl Iterator<Item = &'a String> + 'a {
self.type_uris
.iter()
.filter(move |uri| match parse_slug(uri) {
Some(slug) => query_matches(patterns, &slug),
None => false,
})
}
fn required_ext_for(&self, uri: &str) -> Option<impl Iterator<Item = &String>> {
self.required_ext
.get(uri)
.filter(|namespaces| !namespaces.is_empty())
.map(|namespaces| namespaces.iter())
}
}
impl<S: Into<String>> FromIterator<S> for DiscoveryRegistry {
fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
let mut registry = Self::new();
for uri in iter {
registry.register_str(uri);
}
registry
}
}
fn parse_slug(uri: &str) -> Option<String> {
if let Ok(t) = uri.parse::<crate::TypeUri>() {
return Some(t.slug().to_string());
}
let after_spec = uri.split_once("/spec/")?.1;
let last_slash = after_spec.rfind('/')?;
Some(after_spec[..last_slash].to_string())
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
fn nz(n: u64) -> NonZeroU64 {
NonZeroU64::new(n).unwrap()
}
fn everything() -> wire_v0_3::Payload {
wire_v0_3::Payload { patterns: vec![] }
}
fn uri_of(entry: &wire_v0_3::ResponseSupportedTypesItem) -> &str {
match entry {
wire_v0_3::ResponseSupportedTypesItem::Uri(s) => s.as_str(),
wire_v0_3::ResponseSupportedTypesItem::Object { type_, .. } => type_.as_str(),
}
}
fn uris_in(response: &wire_v0_3::Response) -> Vec<&str> {
response.supported_types.iter().map(uri_of).collect()
}
#[test]
fn star_matches_anything() {
assert!(match_slug("*", "acl/grant"));
assert!(match_slug("*", "trust-task-discovery"));
}
#[test]
fn prefix_wildcard_matches_descendants() {
assert!(match_slug("acl/*", "acl/grant"));
assert!(match_slug("acl/*", "acl/revoke"));
assert!(match_slug("acl/*", "acl/grant/sub"));
}
#[test]
fn prefix_wildcard_does_not_match_bare_prefix() {
assert!(!match_slug("acl/*", "acl"));
assert!(!match_slug("acl/*", "aclx"));
assert!(!match_slug("acl/*", "trust-task-discovery"));
}
#[test]
fn exact_pattern_matches_exact_slug_only() {
assert!(match_slug("trust-task-discovery", "trust-task-discovery"));
assert!(!match_slug(
"trust-task-discovery",
"trust-task-discovery/v2"
));
assert!(!match_slug("trust-task-discovery", "trust-task"));
}
#[test]
fn interior_wildcards_are_treated_literally() {
assert!(!match_slug("acl/*/grant", "acl/grant"));
assert!(!match_slug("a*b", "ab"));
}
#[test]
fn empty_patterns_match_everything() {
let patterns: &[&str] = &[];
assert!(query_matches(patterns, "acl/grant"));
assert!(query_matches(patterns, "anything"));
}
#[test]
fn or_semantics_across_patterns() {
let patterns = ["acl/*", "trust-task-discovery"];
assert!(query_matches(&patterns, "acl/grant"));
assert!(query_matches(&patterns, "trust-task-discovery"));
assert!(!query_matches(&patterns, "consent/give"));
}
#[test]
fn registry_dedupes_and_sorts() {
let registry = DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/revoke", 0, 2).unwrap())
.with_type_uri(TypeUri::canonical("acl/grant", 0, 2).unwrap())
.with_type_uri(TypeUri::canonical("acl/grant", 0, 2).unwrap());
let types = registry.supported_types();
assert_eq!(
types,
vec![
"https://trusttasks.org/spec/acl/grant/0.2",
"https://trusttasks.org/spec/acl/revoke/0.2",
]
);
}
#[cfg(feature = "acl")] #[test]
fn registry_responds_to_query_with_filtered_subset() {
use crate::specs::acl::{change_role, grant, list, revoke, show};
let registry = DiscoveryRegistry::new()
.with::<grant::v0_2::Payload>()
.with::<revoke::v0_2::Payload>()
.with::<show::v0_2::Payload>()
.with::<list::v0_3::Payload>()
.with::<change_role::v0_2::Payload>()
.with::<wire_v0_3::Payload>();
let acl_only = wire_v0_3::Payload {
patterns: vec!["acl/*".parse().unwrap()],
};
let response = registry.respond_to_v0_3(&acl_only);
assert_eq!(response.supported_types.len(), 5);
let only_grant = wire_v0_3::Payload {
patterns: vec!["acl/grant".parse().unwrap()],
};
let response = registry.respond_to_v0_3(&only_grant);
assert_eq!(
uris_in(&response),
vec!["https://trusttasks.org/spec/acl/grant/0.2"]
);
let response = registry.respond_to_v0_3(&everything());
assert_eq!(response.supported_types.len(), 6);
let nothing = wire_v0_3::Payload {
patterns: vec!["does-not-exist/*".parse().unwrap()],
};
let response = registry.respond_to_v0_3(¬hing);
assert!(response.supported_types.is_empty());
let response = registry.respond_to_v0_3(&everything());
assert_eq!(
response.framework_version.as_ref().map(|v| v.to_string()),
Some("0.6.0".to_string())
);
assert!(response.acceptance_window.is_none());
}
#[test]
fn no_framework_version_suppresses_field() {
let registry = DiscoveryRegistry::new().no_framework_version();
let response = registry.respond_to_v0_3(&everything());
assert!(response.framework_version.is_none());
}
#[test]
fn a_patch_release_is_written_verbatim() {
let registry = DiscoveryRegistry::new().framework_version("0.6.1");
let response = registry.respond_to_v0_3(&everything());
assert_eq!(
response.framework_version.map(|v| v.to_string()),
Some("0.6.1".into())
);
}
#[test]
fn a_two_part_override_is_written_as_its_dot_zero() {
let registry = DiscoveryRegistry::new().framework_version("0.5");
let response = registry.respond_to_v0_3(&everything());
assert_eq!(
response.framework_version.map(|v| v.to_string()),
Some("0.5.0".into())
);
}
#[test]
fn malformed_framework_version_override_is_omitted_not_panicked() {
let registry = DiscoveryRegistry::new().framework_version("not-a-version");
assert!(registry
.respond_to_v0_3(&everything())
.framework_version
.is_none());
}
#[test]
fn with_required_ext_advertises_namespace_policy_in_expanded_form() {
let grant_uri = TypeUri::canonical("acl/grant", 0, 2).unwrap();
let registry = DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/revoke", 0, 2).unwrap())
.with_required_ext(grant_uri, ["vnd.affinidi.webvh"]);
let response = registry.respond_to_v0_3(&everything());
match &response.supported_types[0] {
wire_v0_3::ResponseSupportedTypesItem::Object {
type_,
required_ext,
acceptance_window,
} => {
assert_eq!(type_, "https://trusttasks.org/spec/acl/grant/0.2");
let namespaces: Vec<String> = required_ext
.as_ref()
.expect("requiredExt populated")
.iter()
.map(|n| n.to_string())
.collect();
assert_eq!(namespaces, vec!["vnd.affinidi.webvh".to_string()]);
assert!(acceptance_window.is_none());
}
other => panic!("expected expanded Object form, got {other:?}"),
}
assert!(matches!(
&response.supported_types[1],
wire_v0_3::ResponseSupportedTypesItem::Uri(uri)
if uri == "https://trusttasks.org/spec/acl/revoke/0.2"
));
}
#[test]
fn a_required_ext_with_no_valid_namespace_is_left_out() {
let registry = DiscoveryRegistry::new().with_required_ext(
TypeUri::canonical("acl/grant", 0, 2).unwrap(),
["Not A Namespace"],
);
let response = registry.respond_to_v0_3(&everything());
assert!(matches!(
&response.supported_types[0],
wire_v0_3::ResponseSupportedTypesItem::Uri(_)
));
}
#[test]
fn the_response_level_acceptance_window_is_carried() {
let registry = DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/list", 0, 3).unwrap())
.acceptance_window(nz(600), 60);
let response = registry.respond_to_v0_3(&everything());
let window = response.acceptance_window.expect("window stated");
assert_eq!(window.max_age_seconds.get(), 600);
assert_eq!(window.clock_skew_seconds, 60);
assert!(matches!(
&response.supported_types[0],
wire_v0_3::ResponseSupportedTypesItem::Uri(_)
));
let cleared = registry.no_acceptance_window();
assert!(cleared
.respond_to_v0_3(&everything())
.acceptance_window
.is_none());
}
#[test]
fn a_per_entry_window_is_carried_in_expanded_form_beside_the_response_level_one() {
let registry = DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/list", 0, 3).unwrap())
.acceptance_window(nz(600), 60)
.with_acceptance_window_for(
TypeUri::canonical("acl/grant", 0, 2).unwrap(),
nz(120),
30,
);
let response = registry.respond_to_v0_3(&everything());
let window = response.acceptance_window.as_ref().unwrap();
assert_eq!(
(window.max_age_seconds.get(), window.clock_skew_seconds),
(600, 60)
);
match &response.supported_types[0] {
wire_v0_3::ResponseSupportedTypesItem::Object {
type_,
acceptance_window: Some(window),
required_ext: None,
} => {
assert_eq!(type_, "https://trusttasks.org/spec/acl/grant/0.2");
assert_eq!(
(window.max_age_seconds.get(), window.clock_skew_seconds),
(120, 30)
);
}
other => panic!("expected an expanded entry with a window, got {other:?}"),
}
assert_eq!(
uris_in(&response),
vec![
"https://trusttasks.org/spec/acl/grant/0.2",
"https://trusttasks.org/spec/acl/list/0.3",
]
);
}
#[test]
fn the_window_is_read_off_the_freshness_policy_never_wider() {
let registry =
DiscoveryRegistry::new().acceptance_window_from(&FreshnessPolicy::consequential());
let window = registry
.respond_to_v0_3(&everything())
.acceptance_window
.expect("a bounded policy has a window");
assert_eq!(
(window.max_age_seconds.get(), window.clock_skew_seconds),
(300, 60)
);
let policy = FreshnessPolicy::consequential()
.with_max_age(chrono::TimeDelta::milliseconds(90_900))
.with_skew(chrono::TimeDelta::milliseconds(1_999));
let window = DiscoveryRegistry::new()
.acceptance_window_from(&policy)
.respond_to_v0_3(&everything())
.acceptance_window
.unwrap();
assert_eq!(
(window.max_age_seconds.get(), window.clock_skew_seconds),
(90, 1)
);
}
#[test]
fn a_policy_with_no_age_bound_states_no_window() {
let registry = DiscoveryRegistry::new().acceptance_window_from(&FreshnessPolicy::default());
assert!(registry
.respond_to_v0_3(&everything())
.acceptance_window
.is_none());
let policy = FreshnessPolicy::default().with_max_age(chrono::TimeDelta::milliseconds(500));
assert!(DiscoveryRegistry::new()
.acceptance_window_from(&policy)
.respond_to_v0_3(&everything())
.acceptance_window
.is_none());
}
#[cfg(feature = "validate")]
#[test]
fn every_0_3_response_shape_validates_against_the_schema() {
use crate::validate::ValidatedPayload;
let registries = [
DiscoveryRegistry::new(),
DiscoveryRegistry::new().no_framework_version(),
DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/list", 0, 3).unwrap())
.with_required_ext(
TypeUri::canonical("acl/grant", 0, 2).unwrap(),
["vnd.affinidi.webvh"],
)
.acceptance_window_from(&FreshnessPolicy::consequential())
.with_acceptance_window_for(
TypeUri::canonical("acl/show", 0, 2).unwrap(),
nz(120),
0,
),
];
for registry in registries {
let response = registry.respond_to_v0_3(&everything());
let value = serde_json::to_value(&response).unwrap();
wire_v0_3::Response::validate_value(&value)
.unwrap_or_else(|e| panic!("{value} does not validate: {:?}", e.messages()));
}
}
}