use std::collections::BTreeSet;
use uuid::Uuid;
use crate::{Form, critical, form, gzip, identity, json, signed};
pub const MAX_SIGNED_LAYERS: usize = 64;
pub const MAX_JSON_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Limits {
pub inflated: u64,
}
impl Limits {
#[must_use]
pub const fn new(inflated: u64) -> Limits {
Limits { inflated }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Inspection {
pub form: Form,
pub gzip: bool,
pub uuid: Uuid,
}
pub fn inspect(
body: &[u8],
content_type: Option<&str>,
limits: &Limits,
supported: &BTreeSet<String>,
) -> Result<Inspection, Rejection> {
let (inflated, gzip) = gzip::inflate(body, content_type, limits.inflated)?;
let mut budget = limits.inflated;
if gzip {
budget -= inflated.len() as u64;
}
let object = json::parse_object(&inflated)?;
let form = form::form_of(&object)?;
let uuid = match form {
Form::Unsigned => {
let uuid = identity::unsigned_identity(&object)?;
critical::check_critical(&object, supported)?;
uuid
}
Form::Signed => signed::signed_identity(&object, &mut budget, supported)?,
Form::Encrypted => identity::encrypted_identity(&object)?,
};
Ok(Inspection { form, gzip, uuid })
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Rejection {
TooLarge,
TooDeep,
BadGzip,
NotJson,
DuplicateKey,
AmbiguousForm,
NoForm,
MissingUuid,
BadUuid,
MissingCreatedAt,
BadPayload,
UuidMismatch,
BadCritical,
UnsupportedCritical(Vec<String>),
}
#[cfg(test)]
mod tests {
use std::io::Write;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use flate2::Compression;
use flate2::write::GzEncoder;
use serde_json::{Value, json};
use super::*;
const ID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
const LIMITS: Limits = Limits::new(1 << 20);
fn gz(bytes: &[u8]) -> Vec<u8> {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(bytes).expect("compress");
e.finish().expect("finish")
}
fn b64(bytes: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(bytes)
}
fn unsigned() -> Value {
json!({"uuid": ID, "created_at": "2026-10-23T00:00:00Z", "parties": []})
}
fn run(body: &[u8]) -> Result<(Form, bool, Uuid), Rejection> {
inspect(body, None, &LIMITS, &BTreeSet::new()).map(|i| (i.form, i.gzip, i.uuid))
}
fn refused_as(rejection: &Rejection) -> Vec<u8> {
match rejection {
Rejection::TooLarge => gz(&vec![b' '; (LIMITS.inflated + 1) as usize]),
Rejection::TooDeep => format!(
"{}{}",
"[".repeat(MAX_JSON_DEPTH + 1),
"]".repeat(MAX_JSON_DEPTH + 1)
)
.into_bytes(),
Rejection::BadGzip => vec![0x1f, 0x8b, 0],
Rejection::NotJson => b"[]".to_vec(),
Rejection::DuplicateKey => br#"{"uuid": "a", "uuid": "b"}"#.to_vec(),
Rejection::AmbiguousForm => {
br#"{"payload": "", "signatures": [], "ciphertext": "", "recipients": []}"#.to_vec()
}
Rejection::NoForm => b"{}".to_vec(),
Rejection::MissingUuid => br#"{"parties": []}"#.to_vec(),
Rejection::BadUuid => br#"{"uuid": "x"}"#.to_vec(),
Rejection::MissingCreatedAt => format!(r#"{{"uuid": "{ID}"}}"#).into_bytes(),
Rejection::BadPayload => br#"{"payload": "%", "signatures": []}"#.to_vec(),
Rejection::UuidMismatch => json!({
"payload": b64(unsigned().to_string().as_bytes()),
"signatures": [{"header": {"uuid": "01a07da8-c2bb-83e5-b9a2-279e0d16bc47"}}]
})
.to_string()
.into_bytes(),
Rejection::BadCritical => {
format!(r#"{{"uuid": "{ID}", "created_at": "x", "critical": 1}}"#).into_bytes()
}
Rejection::UnsupportedCritical(names) => {
json!({"uuid": ID, "created_at": "x", "critical": names})
.to_string()
.into_bytes()
}
}
}
#[test]
fn every_rejection_is_reachable() {
let every = [
Rejection::TooLarge,
Rejection::TooDeep,
Rejection::BadGzip,
Rejection::NotJson,
Rejection::DuplicateKey,
Rejection::AmbiguousForm,
Rejection::NoForm,
Rejection::MissingUuid,
Rejection::BadUuid,
Rejection::MissingCreatedAt,
Rejection::BadPayload,
Rejection::UuidMismatch,
Rejection::BadCritical,
Rejection::UnsupportedCritical(vec!["x".to_owned()]),
];
for rejection in every {
assert_eq!(
run(&refused_as(&rejection)),
Err(rejection.clone()),
"refused_as gives a body that only this rejection fits"
);
}
}
}