use std::collections::BTreeSet;
use serde_json::Value;
use crate::dispatcher::downcast_payload;
use crate::document::TrustTask;
use crate::error::RejectReason;
use crate::payload::Payload;
use crate::type_uri::TypeUri;
const MAX_CHAIN: usize = 256;
const MAX_DEPTH: usize = 128;
pub fn extends_for(type_uri: &str) -> Option<&'static str> {
#[cfg(test)]
if let Some(answer) = test_index::lookup(type_uri) {
return answer;
}
crate::extends_index::declared_extends(type_uri).or_else(|| application_extends(type_uri))
}
static APPLICATION_EXTENDS: std::sync::RwLock<Vec<&'static [(&'static str, &'static str)]>> =
std::sync::RwLock::new(Vec::new());
pub fn declare_extends(declarations: &'static [(&'static str, &'static str)]) {
let mut tables = APPLICATION_EXTENDS
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !tables.iter().any(|t| std::ptr::eq(*t, declarations)) {
tables.push(declarations);
}
}
fn application_extends(type_uri: &str) -> Option<&'static str> {
let tables = APPLICATION_EXTENDS
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
tables
.iter()
.flat_map(|t| t.iter())
.find(|(uri, _)| *uri == type_uri)
.map(|(_, target)| *target)
}
pub(crate) fn chain(received: &TypeUri) -> Vec<TypeUri> {
let mut out = Vec::new();
let mut current = received.clone();
while out.len() < MAX_CHAIN {
let key = current.bare().to_string();
let Some(target) = extends_for(&key) else {
break;
};
let Some((major, minor)) = parse_major_minor(target) else {
break;
};
if major != current.major() || minor >= current.minor() {
break;
}
current = current.with_version(major, minor);
out.push(current.clone());
}
out
}
fn parse_major_minor(v: &str) -> Option<(u32, u32)> {
let (major, minor) = v.split_once('.')?;
Some((major.parse().ok()?, minor.parse().ok()?))
}
pub fn extension_reaches(received: &TypeUri, implemented: &TypeUri) -> bool {
let target = implemented.for_routing();
received.specification_key() == implemented.specification_key()
&& chain(&received.for_routing()).contains(&target)
}
pub fn read_as<P: Payload>(doc: TrustTask<Value>) -> Result<TrustTask<P>, RejectReason> {
let implemented = P::type_uri();
let received = doc.type_uri.for_routing();
if received == implemented.for_routing() {
return downcast_payload::<P>(doc);
}
if extension_reaches(&doc.type_uri, &implemented) {
return forward::<P>(doc, P::PAYLOAD_SCHEMA);
}
let type_uri = received.to_string();
if doc.type_uri.specification_key() == implemented.specification_key() {
Err(RejectReason::UnsupportedVersion { type_uri })
} else {
Err(RejectReason::UnsupportedType { type_uri })
}
}
pub(crate) fn forward<P: Payload>(
mut doc: TrustTask<Value>,
schema: Option<&str>,
) -> Result<TrustTask<P>, RejectReason> {
if let Some(schema) = schema {
prune_payload(&mut doc.payload, schema);
}
downcast_payload::<P>(doc)
}
pub fn prune_payload(payload: &mut Value, schema_json: &str) {
let Ok(root) = serde_json::from_str::<Value>(schema_json) else {
return;
};
prune(payload, &root, &root, 0);
}
fn resolve<'a>(reference: &str, root: &'a Value) -> Option<&'a Value> {
let pointer = reference.strip_prefix('#')?;
if pointer.is_empty() {
return Some(root);
}
root.pointer(pointer)
}
fn deref<'a>(mut schema: &'a Value, root: &'a Value) -> &'a Value {
for _ in 0..MAX_DEPTH {
match schema.get("$ref").and_then(Value::as_str) {
Some(r) => match resolve(r, root) {
Some(target) => schema = target,
None => break,
},
None => break,
}
}
schema
}
fn prune(value: &mut Value, schema: &Value, root: &Value, depth: usize) {
if depth > MAX_DEPTH {
return;
}
let Some(s) = schema.as_object() else {
return;
};
if let Some(target) = s
.get("$ref")
.and_then(Value::as_str)
.and_then(|r| resolve(r, root))
{
prune(value, target, root, depth + 1);
}
if let Some(branches) = s.get("allOf").and_then(Value::as_array) {
for branch in branches {
prune(value, branch, root, depth + 1);
}
}
let chosen = ["oneOf", "anyOf"]
.iter()
.filter_map(|k| s.get(*k).and_then(Value::as_array))
.map(|branches| sole_admitting_branch(value, branches, root))
.collect::<Vec<_>>();
for branch in chosen.iter().flatten() {
prune(value, branch, root, depth + 1);
}
match value {
Value::Object(members) => {
let properties = s.get("properties").and_then(Value::as_object);
let opaque = s.contains_key("patternProperties");
if !opaque && s.get("additionalProperties") == Some(&Value::Bool(false)) {
members.retain(|k, _| properties.is_some_and(|p| p.contains_key(k)));
} else if !opaque
&& s.get("unevaluatedProperties") == Some(&Value::Bool(false))
&& !["if", "then", "else", "dependentSchemas"]
.iter()
.any(|k| s.contains_key(*k))
&& chosen.iter().all(Option::is_some)
{
let mut known = BTreeSet::new();
evaluated_properties(schema, root, &mut known, 0);
for branch in chosen.iter().flatten() {
evaluated_properties(branch, root, &mut known, 0);
}
members.retain(|k, _| known.contains(k.as_str()));
}
let additional = s.get("additionalProperties").filter(|a| a.is_object());
for (name, member) in members.iter_mut() {
match properties.and_then(|p| p.get(name)) {
Some(sub) => prune(member, sub, root, depth + 1),
None => {
if let Some(additional) = additional {
prune(member, additional, root, depth + 1);
}
}
}
}
}
Value::Array(elements) => {
let prefix = s
.get("prefixItems")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let items = s.get("items").filter(|i| i.is_object());
for (i, element) in elements.iter_mut().enumerate() {
if let Some(sub) = prefix.get(i).or(items) {
prune(element, sub, root, depth + 1);
}
}
}
_ => {}
}
}
fn evaluated_properties<'a>(
schema: &'a Value,
root: &'a Value,
out: &mut BTreeSet<&'a str>,
depth: usize,
) {
if depth > MAX_DEPTH {
return;
}
let Some(s) = schema.as_object() else {
return;
};
if let Some(p) = s.get("properties").and_then(Value::as_object) {
out.extend(p.keys().map(String::as_str));
}
if let Some(target) = s
.get("$ref")
.and_then(Value::as_str)
.and_then(|r| resolve(r, root))
{
evaluated_properties(target, root, out, depth + 1);
}
if let Some(branches) = s.get("allOf").and_then(Value::as_array) {
for branch in branches {
evaluated_properties(branch, root, out, depth + 1);
}
}
}
fn sole_admitting_branch<'a>(
value: &Value,
branches: &'a [Value],
root: &'a Value,
) -> Option<&'a Value> {
let mut admitting = branches.iter().filter(|b| admits(value, b, root, 0));
let first = admitting.next()?;
match admitting.next() {
Some(_) => None,
None => Some(first),
}
}
fn admits(value: &Value, schema: &Value, root: &Value, depth: usize) -> bool {
if depth > 8 {
return true;
}
let schema = deref(schema, root);
let Some(s) = schema.as_object() else {
return schema != &Value::Bool(false);
};
if let Some(t) = s.get("type") {
let names: Vec<&str> = match t {
Value::String(one) => vec![one.as_str()],
Value::Array(many) => many.iter().filter_map(Value::as_str).collect(),
_ => vec![],
};
if !names.is_empty() && !names.iter().any(|n| has_type(value, n)) {
return false;
}
}
if let Some(c) = s.get("const") {
if c != value {
return false;
}
}
if let Some(e) = s.get("enum").and_then(Value::as_array) {
if !e.contains(value) {
return false;
}
}
if let Value::Object(members) = value {
if let Some(required) = s.get("required").and_then(Value::as_array) {
if !required
.iter()
.filter_map(Value::as_str)
.all(|r| members.contains_key(r))
{
return false;
}
}
if let Some(properties) = s.get("properties").and_then(Value::as_object) {
for (name, sub) in properties {
let Some(member) = members.get(name) else {
continue;
};
let sub = deref(sub, root);
let discriminates = sub.get("const").is_some() || sub.get("enum").is_some();
if discriminates && !admits(member, sub, root, depth + 1) {
return false;
}
}
}
}
if let Some(branches) = s.get("allOf").and_then(Value::as_array) {
if !branches.iter().all(|b| admits(value, b, root, depth + 1)) {
return false;
}
}
true
}
fn has_type(value: &Value, name: &str) -> bool {
match name {
"object" => value.is_object(),
"array" => value.is_array(),
"string" => value.is_string(),
"boolean" => value.is_boolean(),
"null" => value.is_null(),
"number" => value.is_number(),
"integer" => {
value.is_i64() || value.is_u64() || value.as_f64().is_some_and(|f| f.fract() == 0.0)
}
_ => true,
}
}
#[cfg(test)]
pub(crate) mod test_index {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static OVERLAY: RefCell<HashMap<String, Option<&'static str>>> =
RefCell::new(HashMap::new());
}
pub(crate) fn declare(type_uri: &str, target: Option<&'static str>) {
OVERLAY.with(|o| o.borrow_mut().insert(type_uri.to_string(), target));
}
pub(super) fn lookup(type_uri: &str) -> Option<Option<&'static str>> {
OVERLAY.with(|o| o.borrow().get(type_uri).copied())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn additional_properties_false_drops_unknown_members_through_refs() {
let schema = json!({
"type": "object",
"additionalProperties": false,
"properties": {
"entry": { "$ref": "#/$defs/Entry" },
"list": { "type": "array", "items": { "$ref": "#/$defs/Entry" } },
"open": { "type": "object" }
},
"$defs": {
"Entry": {
"type": "object",
"additionalProperties": false,
"properties": { "subject": { "type": "string" } }
}
}
})
.to_string();
let mut payload = json!({
"entry": { "subject": "a", "added": 1 },
"list": [{ "subject": "b", "added": 2 }],
"open": { "kept": true },
"newTop": "x"
});
prune_payload(&mut payload, &schema);
assert_eq!(
payload,
json!({
"entry": { "subject": "a" },
"list": [{ "subject": "b" }],
"open": { "kept": true }
})
);
}
#[test]
fn a_discriminated_one_of_prunes_against_its_branch_and_an_ambiguous_one_is_left() {
let schema = json!({
"oneOf": [
{ "type": "object", "additionalProperties": false, "required": ["kind"],
"properties": { "kind": { "const": "a" }, "x": {} } },
{ "type": "object", "additionalProperties": false, "required": ["kind"],
"properties": { "kind": { "const": "b" }, "y": {} } }
]
})
.to_string();
let mut payload = json!({ "kind": "b", "y": 1, "z": 2 });
prune_payload(&mut payload, &schema);
assert_eq!(payload, json!({ "kind": "b", "y": 1 }));
let mut unknown = json!({ "kind": "c", "z": 2 });
prune_payload(&mut unknown, &schema);
assert_eq!(unknown, json!({ "kind": "c", "z": 2 }));
}
#[test]
fn unevaluated_properties_false_keeps_what_all_of_evaluates() {
let schema = json!({
"allOf": [{ "$ref": "#/$defs/Base" }],
"properties": { "own": {} },
"unevaluatedProperties": false,
"$defs": { "Base": { "properties": { "base": {} } } }
})
.to_string();
let mut payload = json!({ "base": 1, "own": 2, "added": 3 });
prune_payload(&mut payload, &schema);
assert_eq!(payload, json!({ "base": 1, "own": 2 }));
}
#[test]
fn pattern_properties_make_an_object_opaque() {
let schema = json!({
"additionalProperties": false,
"patternProperties": { "^x-": {} },
"properties": { "a": {} }
})
.to_string();
let mut payload = json!({ "a": 1, "x-b": 2, "c": 3 });
prune_payload(&mut payload, &schema);
assert_eq!(payload, json!({ "a": 1, "x-b": 2, "c": 3 }));
}
#[test]
fn chain_walks_to_each_earlier_version_and_stops_at_a_bad_link() {
let base = "https://trusttasks.org/spec/test-ext/chain";
test_index::declare(&format!("{base}/0.3"), Some("0.2"));
test_index::declare(&format!("{base}/0.2"), Some("0.1"));
test_index::declare(&format!("{base}/0.1"), Some("0.5"));
let received: TypeUri = format!("{base}/0.3#request").parse().unwrap();
let links: Vec<String> = chain(&received.for_routing())
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(links, vec![format!("{base}/0.2"), format!("{base}/0.1")]);
let v01: TypeUri = format!("{base}/0.1").parse().unwrap();
let v03: TypeUri = format!("{base}/0.3").parse().unwrap();
assert!(extension_reaches(&received, &v01));
assert!(!extension_reaches(&v01, &v03), "never backwards");
assert!(
!extension_reaches(&v03, &v03),
"the version itself is not an extension"
);
assert!(extension_reaches(
&received.with_response(),
&v01.with_response()
));
assert!(!extension_reaches(&received.with_response(), &v01));
}
#[test]
fn application_declarations_are_walked() {
static DECLARED: &[(&str, &str)] = &[
("https://trusttasks.org/spec/test-app-ext/thing/0.3", "0.2"),
("https://trusttasks.org/spec/test-app-ext/thing/0.2", "0.1"),
];
let base = "https://trusttasks.org/spec/test-app-ext/thing";
assert_eq!(extends_for(&format!("{base}/0.3")), None);
declare_extends(DECLARED);
declare_extends(DECLARED);
assert_eq!(extends_for(&format!("{base}/0.3")), Some("0.2"));
assert_eq!(
APPLICATION_EXTENDS
.read()
.unwrap()
.iter()
.filter(|t| std::ptr::eq(**t, DECLARED))
.count(),
1
);
let received: TypeUri = format!("{base}/0.3").parse().unwrap();
let v01: TypeUri = format!("{base}/0.1").parse().unwrap();
assert!(extension_reaches(&received, &v01));
}
}