use core::fmt;
use core::num::ParseIntError;
use core::str::FromStr;
use thiserror::Error;
use crate::domain::session::SessionId;
pub const VISIBLE_ASCII_RANGE: core::ops::RangeInclusive<u8> = 0x21..=0x7E;
const DIGITS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct McpSessionId(SessionId);
impl McpSessionId {
#[must_use]
pub const fn new(session: SessionId) -> Self {
Self(session)
}
#[must_use]
pub const fn session(self) -> SessionId {
self.0
}
#[must_use]
pub fn as_bytes(self, buffer: &mut [u8; DIGITS]) -> &[u8; DIGITS] {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut remaining = self.0.get();
for slot in buffer.iter_mut().rev() {
let index = usize::from(u8::try_from(remaining & 0xF).unwrap_or(0));
*slot = HEX.get(index).copied().unwrap_or(b'0');
remaining >>= 4;
}
buffer
}
}
impl fmt::Display for McpSessionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer = [0u8; DIGITS];
formatter.write_str(core::str::from_utf8(self.as_bytes(&mut buffer)).unwrap_or_default())
}
}
impl From<SessionId> for McpSessionId {
fn from(session: SessionId) -> Self {
Self(session)
}
}
impl From<McpSessionId> for SessionId {
fn from(id: McpSessionId) -> Self {
id.0
}
}
impl FromStr for McpSessionId {
type Err = McpSessionIdError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let value = McpSessionIdError::validate(text)?;
let digits = u64::from_str_radix(value, 16)
.map_err(|source| McpSessionIdError::Unparsable { source })?;
Ok(Self(SessionId::new(digits)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum McpSessionIdError {
#[error("Mcp-Session-Id is empty once surrounding whitespace is trimmed")]
Empty,
#[error(
"Mcp-Session-Id contains {byte:#04X} at byte {index}, outside the \
visible ASCII range {low:#04X}-{high:#04X} required of an HTTP field-value"
)]
NotVisibleAscii {
index: usize,
byte: u8,
low: u8,
high: u8,
},
#[error("Mcp-Session-Id must be {expected} hexadecimal digits, found {found}")]
WrongLength {
expected: usize,
found: usize,
},
#[error(
"Mcp-Session-Id contains {character:?} at byte {index}, which is not a hexadecimal digit"
)]
NotHex {
index: usize,
character: char,
},
#[error("Mcp-Session-Id is not a valid hexadecimal number: {source}")]
Unparsable {
source: ParseIntError,
},
}
impl McpSessionIdError {
pub fn validate_opaque(value: &str) -> Result<&str, Self> {
let trimmed = value.trim_matches([' ', '\t']);
if trimmed.is_empty() {
return Err(Self::Empty);
}
for (index, character) in trimmed.char_indices() {
let byte = u8::try_from(character).unwrap_or(u8::MAX);
if !VISIBLE_ASCII_RANGE.contains(&byte) {
return Err(Self::NotVisibleAscii {
index,
byte,
low: *VISIBLE_ASCII_RANGE.start(),
high: *VISIBLE_ASCII_RANGE.end(),
});
}
}
Ok(trimmed)
}
pub fn validate(value: &str) -> Result<&str, Self> {
let trimmed = Self::validate_opaque(value)?;
if trimmed.len() != DIGITS {
return Err(Self::WrongLength {
expected: DIGITS,
found: trimmed.len(),
});
}
for (index, character) in trimmed.char_indices() {
if !character.is_ascii_hexdigit() {
return Err(Self::NotHex { index, character });
}
}
Ok(trimmed)
}
}
#[cfg(test)]
mod tests {
use super::{DIGITS, McpSessionId, McpSessionIdError, VISIBLE_ASCII_RANGE};
use crate::domain::session::SessionId;
use core::str::FromStr;
#[derive(Debug)]
enum Failure {
Session(McpSessionIdError),
Display,
}
impl From<McpSessionIdError> for Failure {
fn from(error: McpSessionIdError) -> Self {
Self::Session(error)
}
}
#[test]
fn a_conversion_keeps_the_refusal_it_was_given() -> Result<(), Failure> {
let failure: Failure = McpSessionIdError::Empty.into();
let Failure::Session(reason) = failure else {
return Err(Failure::Display);
};
if reason.to_string().is_empty() {
return Err(Failure::Display);
}
Ok(())
}
#[test]
fn rendering_is_fixed_width_visible_ascii() -> Result<(), Failure> {
for value in [0, 1, 9, 10, 15, 16, 255, 256, u32::MAX.into(), u64::MAX] {
let id = McpSessionId::new(SessionId::new(value));
let rendered = id.to_string();
if rendered.len() != DIGITS {
return Err(Failure::Display);
}
if !rendered.bytes().all(|b| VISIBLE_ASCII_RANGE.contains(&b)) {
return Err(Failure::Display);
}
}
Ok(())
}
#[test]
fn renders_the_documented_spelling() -> Result<(), Failure> {
for (session, expected) in [
(0, "0000000000000000"),
(1, "0000000000000001"),
(255, "00000000000000ff"),
] {
if McpSessionId::new(SessionId::new(session)).to_string() != expected {
return Err(Failure::Display);
}
}
Ok(())
}
#[test]
fn round_trips_through_the_header_form() -> Result<(), Failure> {
for value in [0, 1, 7, 123_456_789, u64::MAX] {
let original = McpSessionId::new(SessionId::new(value));
let parsed = McpSessionId::from_str(&original.to_string())?;
if parsed != original {
return Err(Failure::Display);
}
if parsed.session() != SessionId::new(value) {
return Err(Failure::Display);
}
}
Ok(())
}
#[test]
fn accepts_lowercase_and_agrees_with_uppercase() -> Result<(), Failure> {
if McpSessionId::from_str("00000000000000ff")?
!= McpSessionId::from_str("00000000000000FF")?
{
return Err(Failure::Display);
}
Ok(())
}
#[test]
fn trims_surrounding_whitespace() -> Result<(), Failure> {
if McpSessionId::from_str(" 0000000000000001\t")?.session() != SessionId::new(1) {
return Err(Failure::Display);
}
Ok(())
}
#[test]
fn refuses_non_visible_ascii_naming_the_byte() {
let refused = McpSessionId::from_str("000000000000000\n");
assert_eq!(
refused,
Err(McpSessionIdError::NotVisibleAscii {
index: 15,
byte: b'\n',
low: *VISIBLE_ASCII_RANGE.start(),
high: *VISIBLE_ASCII_RANGE.end(),
})
);
}
#[test]
fn refuses_obs_text() {
assert_eq!(
McpSessionId::from_str("00000000000000\u{00ff}"),
Err(McpSessionIdError::NotVisibleAscii {
index: 14,
byte: 0xff,
low: *VISIBLE_ASCII_RANGE.start(),
high: *VISIBLE_ASCII_RANGE.end(),
})
);
}
#[test]
fn distinguishes_empty_wrong_length_and_non_hex() {
assert_eq!(McpSessionId::from_str(" "), Err(McpSessionIdError::Empty));
assert_eq!(McpSessionId::from_str(""), Err(McpSessionIdError::Empty));
assert_eq!(
McpSessionId::from_str("0001"),
Err(McpSessionIdError::WrongLength {
expected: DIGITS,
found: 4,
})
);
assert_eq!(
McpSessionId::from_str("zzzzzzzzzzzzzzzz"),
Err(McpSessionIdError::NotHex {
index: 0,
character: 'z',
})
);
}
#[test]
fn range_check_runs_before_the_digit_check() {
assert_eq!(
McpSessionId::from_str(
"\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}\u{00e9}"
),
Err(McpSessionIdError::NotVisibleAscii {
index: 0,
byte: 0xe9,
low: *VISIBLE_ASCII_RANGE.start(),
high: *VISIBLE_ASCII_RANGE.end(),
})
);
}
#[test]
fn converts_to_and_from_the_internal_id() -> Result<(), Failure> {
let internal = SessionId::new(42);
let wire: McpSessionId = internal.into();
if wire.session() != internal {
return Err(Failure::Display);
}
let back: SessionId = wire.into();
if back != internal {
return Err(Failure::Display);
}
Ok(())
}
#[test]
fn bytes_and_display_agree() -> Result<(), Failure> {
let id = McpSessionId::new(SessionId::new(0xdead_beef));
let mut buffer = [0u8; DIGITS];
if id.as_bytes(&mut buffer).as_slice() != id.to_string().into_bytes() {
return Err(Failure::Display);
}
Ok(())
}
}