use serde_json::Value;
use crate::context::TraceContext;
#[derive(Clone, Copy, Debug)]
pub(super) enum Declaration {
Declared,
Withheld,
Unknowable,
}
pub(super) fn server_capability(context: &TraceContext<'_>, path: &[&str]) -> Declaration {
capability_in(context.server_capabilities(), path, context)
}
pub(super) fn client_capability(context: &TraceContext<'_>, path: &[&str]) -> Declaration {
capability_in(context.client_capabilities(), path, context)
}
fn capability_in(
capabilities: Option<&Value>,
path: &[&str],
context: &TraceContext<'_>,
) -> Declaration {
if context.initialize().result.is_none() {
return Declaration::Unknowable;
}
let Some(mut current) = capabilities else {
return Declaration::Withheld;
};
for segment in path {
match current.get(segment) {
Some(next) => current = next,
None => return Declaration::Withheld,
}
}
if current.is_null() || matches!(current, Value::Bool(false)) {
Declaration::Withheld
} else {
Declaration::Declared
}
}
pub(super) fn is_base64(text: &str) -> bool {
let bytes = text.as_bytes();
if !bytes.len().is_multiple_of(4) {
return false;
}
let padding = bytes.iter().rev().take_while(|&&b| b == b'=').count();
if padding > 2 {
return false;
}
let content = &bytes[..bytes.len() - padding];
content
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
}
#[cfg(feature = "draft-2026-07-28")]
pub(super) fn decode_base64(text: &str) -> Option<String> {
if !is_base64(text) {
return None;
}
let mut bytes = Vec::with_capacity(text.len() / 4 * 3);
let mut accumulator: u32 = 0;
let mut bits: u32 = 0;
for byte in text.bytes().take_while(|&byte| byte != b'=') {
let sextet = match byte {
b'A'..=b'Z' => u32::from(byte - b'A'),
b'a'..=b'z' => u32::from(byte - b'a') + 26,
b'0'..=b'9' => u32::from(byte - b'0') + 52,
b'+' => 62,
b'/' => 63,
_ => return None,
};
accumulator = (accumulator << 6) + sextet;
bits += 6;
if bits >= 8 {
bits -= 8;
bytes.push(u8::try_from((accumulator >> bits) & 0xff).ok()?);
}
}
String::from_utf8(bytes).ok()
}
pub(super) fn has_rfc3986_scheme(uri: &str) -> bool {
let Some((scheme, _)) = uri.split_once(':') else {
return false;
};
let mut chars = scheme.chars();
let Some(first) = chars.next() else {
return false;
};
first.is_ascii_alphabetic()
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::checks;
use crate::reader::{Limits, parse_trace};
const CAPABILITY_CHECKS: [&str; 7] = [
"tools.capability-declared",
"tools.embedded-resource-capability",
"resources.capability-declared",
"prompts.capability-declared",
"logging.capability-declared",
"completion.capability-declared",
"lifecycle.negotiated-capabilities-only",
];
fn session(handshake: bool) -> String {
let mut lines: Vec<String> = Vec::new();
if handshake {
lines.push(r#"{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}}"#.to_owned());
lines.push(r#"{"seq":1,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{},"resources":{},"prompts":{},"logging":{},"completions":{}},"serverInfo":{"name":"s","version":"0"}}}}"#.to_owned());
}
for line in [
r#"{"seq":2,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":2,"method":"tools/list"}}"#,
r#"{"seq":3,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":2,"result":{"tools":[]}}}"#,
r#"{"seq":4,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"file:///a"}}}"#,
r#"{"seq":5,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":3,"result":{"contents":[]}}}"#,
r#"{"seq":6,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":4,"method":"prompts/get","params":{"name":"p"}}}"#,
r#"{"seq":7,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":4,"result":{"messages":[]}}}"#,
r#"{"seq":8,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":5,"method":"completion/complete","params":{}}}"#,
r#"{"seq":9,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":5,"result":{"completion":{"values":[]}}}}"#,
r#"{"seq":10,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":"x"}}}"#,
r#"{"seq":11,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"t"}}}"#,
r#"{"seq":12,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":6,"result":{"content":[{"type":"resource","resource":{"uri":"file:///a","text":"x"}}]}}}"#,
] {
lines.push(line.to_owned());
}
lines.join("\n")
}
fn subjects_and_findings(check: &str, trace: &str) -> (u32, usize) {
let events = parse_trace(trace, &Limits::default()).unwrap();
let context = TraceContext::new(&events);
let outcome = checks::find(check).unwrap().run(&context);
(outcome.subjects, outcome.findings.len())
}
#[test]
fn no_declaration_surface_means_no_verdict() {
let trace = session(false);
for check in CAPABILITY_CHECKS {
let (subjects, findings) = subjects_and_findings(check, &trace);
assert_eq!(
subjects, 0,
"{check} counted a subject in a session with no initialize result, \
so the clause it backs reports a pass it cannot support"
);
assert_eq!(findings, 0, "{check} judged an unjudgeable session");
}
}
#[test]
fn a_declaration_surface_is_judged() {
let trace = session(true);
for check in CAPABILITY_CHECKS {
let (subjects, findings) = subjects_and_findings(check, &trace);
assert!(subjects > 0, "{check} found nothing to judge");
assert_eq!(
findings, 0,
"{check} faulted a session that declared everything it used"
);
}
}
#[test]
fn a_present_surface_that_withholds_the_capability_is_a_violation() {
let trace = session(true).replace(
r#""capabilities":{"tools":{},"resources":{},"prompts":{},"logging":{},"completions":{}}"#,
r#""capabilities":{}"#,
);
for check in CAPABILITY_CHECKS {
let (subjects, findings) = subjects_and_findings(check, &trace);
assert!(subjects > 0, "{check} found nothing to judge");
assert!(findings > 0, "{check} excused an undeclared capability");
}
}
#[test]
fn base64_validation_is_exact() {
for valid in ["", "aGk=", "aGV5", "aGV5bw==", "AB+/", "QUJDRA=="] {
assert!(is_base64(valid), "{valid:?} should validate");
}
for invalid in [
"aGk", "aGk =", "aGk!", "====", "aG=k", "aGV5bw=", ] {
assert!(!is_base64(invalid), "{invalid:?} should not validate");
}
}
#[cfg(feature = "draft-2026-07-28")]
#[test]
fn base64_decoding_round_trips_the_specification_examples() {
for (encoded, original) in [
("SGVsbG8sIOS4lueVjA==", "Hello, 世界"),
("IHBhZGRlZCA=", " padded "),
("bGluZTEKbGluZTI=", "line1\nline2"),
("PT9iYXNlNjQ/bGl0ZXJhbD89", "=?base64?literal?="),
] {
assert_eq!(
decode_base64(encoded).as_deref(),
Some(original),
"{encoded:?} should decode to {original:?}"
);
}
assert_eq!(decode_base64("").as_deref(), Some(""));
}
#[cfg(feature = "draft-2026-07-28")]
#[test]
fn base64_decoding_covers_the_whole_alphabet_and_every_padding_length() {
assert_eq!(decode_base64("fn5+").as_deref(), Some("~~~"));
assert_eq!(decode_base64("fn4/").as_deref(), Some("~~?"));
assert_eq!(decode_base64("YQ==").as_deref(), Some("a")); assert_eq!(decode_base64("YWI=").as_deref(), Some("ab")); assert_eq!(decode_base64("YWJj").as_deref(), Some("abc")); assert_eq!(decode_base64("YmFj").as_deref(), Some("bac"));
}
#[cfg(feature = "draft-2026-07-28")]
#[test]
fn base64_decoding_refuses_what_it_cannot_represent() {
assert_eq!(decode_base64("aGk"), None);
assert_eq!(decode_base64("aG=k"), None);
assert_eq!(decode_base64("/w=="), None);
}
#[test]
fn rfc3986_scheme_validation_is_exact() {
for valid in ["https://x", "file:///a", "git://r", "a:", "z+ssh.2-x:rest"] {
assert!(has_rfc3986_scheme(valid), "{valid:?} should validate");
}
for invalid in [
"", "no-colon", ":rest", "1https://x", "ht tp://x", "ht_tp://x", ] {
assert!(
!has_rfc3986_scheme(invalid),
"{invalid:?} should not validate"
);
}
}
}