use std::fmt;
use crate::constants::MAX_URI_LENGTH;
const MAX_ERROR_INPUT_LENGTH: usize = MAX_URI_LENGTH;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub input: String,
pub kind: ParseErrorKind,
}
impl ParseError {
pub(crate) fn new(input: &str, kind: ParseErrorKind) -> Self {
Self {
input: truncate_input(input),
kind,
}
}
}
const TRUNCATION_MARKER: char = '…';
fn truncate_input(input: &str) -> String {
let mut chars = input.chars();
let mut kept: String = chars.by_ref().take(MAX_ERROR_INPUT_LENGTH).collect();
if chars.next().is_some() {
kept.push(TRUNCATION_MARKER);
}
kept
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseErrorKind {
Empty,
TooLong {
max: usize,
actual: usize,
},
InvalidScheme {
found: Option<String>,
},
InvalidTrustRoot(TrustRootError),
InvalidCapabilityPath(CapabilityPathError),
InvalidAgentId(AgentIdError),
InvalidQuery(QueryError),
InvalidFragment(FragmentError),
MissingComponent {
component: &'static str,
},
UnexpectedChar {
char: char,
position: usize,
},
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse agent URI '{}': ", self.input)?;
match &self.kind {
ParseErrorKind::Empty => write!(f, "input is empty"),
ParseErrorKind::TooLong { max, actual } => {
write!(
f,
"URI length {actual} exceeds maximum {max}; consider shorter capability path or trust root"
)
}
ParseErrorKind::InvalidScheme { found } => match found {
Some(s) => write!(f, "expected scheme 'agent://', found '{s}'"),
None => write!(f, "missing scheme; URI must start with 'agent://'"),
},
ParseErrorKind::InvalidTrustRoot(e) => write!(f, "invalid trust root: {e}"),
ParseErrorKind::InvalidCapabilityPath(e) => write!(f, "invalid capability path: {e}"),
ParseErrorKind::InvalidAgentId(e) => write!(f, "invalid agent ID: {e}"),
ParseErrorKind::InvalidQuery(e) => write!(f, "invalid query string: {e}"),
ParseErrorKind::InvalidFragment(e) => write!(f, "invalid fragment: {e}"),
ParseErrorKind::MissingComponent { component } => {
write!(f, "missing required component: {component}")
}
ParseErrorKind::UnexpectedChar { char, position } => {
write!(f, "unexpected character '{char}' at position {position}")
}
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TrustRootError {
Empty,
TooLong {
max: usize,
actual: usize,
},
InvalidDomain {
domain: String,
reason: &'static str,
},
InvalidIpAddress {
value: String,
reason: &'static str,
},
InvalidPort {
value: String,
reason: &'static str,
},
LabelTooLong {
label: String,
max: usize,
actual: usize,
},
InvalidChar {
char: char,
position: usize,
},
}
impl fmt::Display for TrustRootError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "trust root cannot be empty"),
Self::TooLong { max, actual } => {
write!(f, "trust root length {actual} exceeds maximum {max}")
}
Self::InvalidDomain { domain, reason } => {
write!(f, "invalid domain '{domain}': {reason}")
}
Self::InvalidIpAddress { value, reason } => {
write!(f, "invalid IP address '{value}': {reason}")
}
Self::InvalidPort { value, reason } => {
write!(f, "invalid port '{value}': {reason}")
}
Self::LabelTooLong { label, max, actual } => {
write!(f, "DNS label '{label}' is {actual} chars, max is {max}")
}
Self::InvalidChar { char, position } => {
write!(f, "invalid character '{char}' at position {position}")
}
}
}
}
impl std::error::Error for TrustRootError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CapabilityPathError {
Empty,
TooLong {
max: usize,
actual: usize,
},
TooManySegments {
max: usize,
actual: usize,
},
InvalidSegment {
segment: String,
index: usize,
reason: PathSegmentError,
},
}
impl fmt::Display for CapabilityPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(
f,
"capability path cannot be empty; at least one segment required"
),
Self::TooLong { max, actual } => {
write!(f, "capability path length {actual} exceeds maximum {max}")
}
Self::TooManySegments { max, actual } => {
write!(f, "path has {actual} segments, maximum is {max}")
}
Self::InvalidSegment {
segment,
index,
reason,
} => {
write!(f, "invalid segment '{segment}' at index {index}: {reason}")
}
}
}
}
impl std::error::Error for CapabilityPathError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathSegmentError {
Empty,
TooLong {
max: usize,
actual: usize,
},
InvalidChar {
char: char,
position: usize,
},
}
impl fmt::Display for PathSegmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "segment cannot be empty"),
Self::TooLong { max, actual } => {
write!(f, "segment length {actual} exceeds maximum {max}")
}
Self::InvalidChar { char, position } => {
write!(
f,
"invalid character '{char}' at position {position}; only lowercase letters, digits, and hyphens allowed"
)
}
}
}
}
impl std::error::Error for PathSegmentError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AgentIdError {
Empty,
TooLong {
max: usize,
actual: usize,
},
InvalidPrefix(AgentPrefixError),
InvalidSuffix {
value: String,
reason: &'static str,
},
MissingSeparator,
TypeIdError(String),
}
impl fmt::Display for AgentIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "agent ID cannot be empty"),
Self::TooLong { max, actual } => {
write!(f, "agent ID length {actual} exceeds maximum {max}")
}
Self::InvalidPrefix(e) => write!(f, "invalid prefix: {e}"),
Self::InvalidSuffix { value, reason } => {
write!(f, "invalid suffix '{value}': {reason}")
}
Self::MissingSeparator => {
write!(f, "missing underscore separator between prefix and suffix")
}
Self::TypeIdError(msg) => write!(f, "`TypeID` error: {msg}"),
}
}
}
impl std::error::Error for AgentIdError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AgentPrefixError {
Empty,
TooLong {
max: usize,
actual: usize,
},
InvalidTypeClass {
class: String,
reason: TypeClassError,
},
InvalidChar {
char: char,
position: usize,
},
MustStartWithLetter {
found: char,
},
MustEndWithLetter {
found: char,
},
ContainsDigit {
position: usize,
},
}
impl fmt::Display for AgentPrefixError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "prefix cannot be empty"),
Self::TooLong { max, actual } => {
write!(f, "prefix length {actual} exceeds maximum {max}")
}
Self::InvalidTypeClass { class, reason } => {
write!(f, "invalid type class '{class}': {reason}")
}
Self::InvalidChar { char, position } => {
write!(
f,
"invalid character '{char}' at position {position}; only lowercase letters and underscores allowed"
)
}
Self::MustStartWithLetter { found } => {
write!(f, "prefix must start with a letter, found '{found}'")
}
Self::MustEndWithLetter { found } => {
write!(f, "prefix must end with a letter, found '{found}'")
}
Self::ContainsDigit { position } => {
write!(
f,
"prefix cannot contain digits (found at position {position})"
)
}
}
}
}
impl std::error::Error for AgentPrefixError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidTypeClass { reason, .. } => Some(reason),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum QueryError {
TooLong {
max: usize,
actual: usize,
},
InvalidParamName {
name: String,
reason: &'static str,
},
InvalidParamValue {
name: String,
value: String,
reason: &'static str,
},
DuplicateParam {
name: String,
},
InvalidPercentEncoding {
value: String,
},
InvalidUtf8 {
value: String,
},
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLong { max, actual } => {
write!(f, "query string length {actual} exceeds maximum {max}")
}
Self::InvalidParamName { name, reason } => {
write!(f, "invalid parameter name '{name}': {reason}")
}
Self::InvalidParamValue {
name,
value,
reason,
} => {
write!(
f,
"invalid value '{value}' for parameter '{name}': {reason}"
)
}
Self::DuplicateParam { name } => {
write!(f, "duplicate parameter '{name}'")
}
Self::InvalidPercentEncoding { value } => {
write!(f, "invalid percent encoding in '{value}'")
}
Self::InvalidUtf8 { value } => {
write!(f, "percent-encoded value '{value}' is not valid UTF-8")
}
}
}
}
impl std::error::Error for QueryError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FragmentError {
TooLong {
max: usize,
actual: usize,
},
InvalidChar {
char: char,
position: usize,
},
Empty,
}
impl fmt::Display for FragmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLong { max, actual } => {
write!(f, "fragment length {actual} exceeds maximum {max}")
}
Self::InvalidChar { char, position } => {
write!(
f,
"invalid character '{char}' at position {position}; allowed: alphanumeric, hyphen, underscore, dot, slash"
)
}
Self::Empty => write!(f, "fragment must not be empty; omit the '#' instead"),
}
}
}
impl std::error::Error for FragmentError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BuilderError {
UriTooLong {
max: usize,
actual: usize,
},
InvalidUri(ParseError),
}
impl fmt::Display for BuilderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UriTooLong { max, actual } => {
write!(
f,
"resulting URI length {actual} exceeds maximum {max}; \
consider shorter component values"
)
}
Self::InvalidUri(e) => write!(f, "cannot build agent URI: {e}"),
}
}
}
impl std::error::Error for BuilderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidUri(e) => Some(e),
Self::UriTooLong { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TypeClassError {
Empty,
InvalidChar {
char: char,
position: usize,
},
}
impl fmt::Display for TypeClassError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "type class name cannot be empty"),
Self::InvalidChar { char, position } => {
write!(
f,
"invalid character '{char}' at position {position}; \
a type class name is lowercase letters only"
)
}
}
}
}
impl std::error::Error for TypeClassError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_input_a_uri_could_have_been_is_kept_whole() {
let input = format!("agent://{}", "a".repeat(MAX_ERROR_INPUT_LENGTH - 8));
assert_eq!(input.chars().count(), MAX_ERROR_INPUT_LENGTH);
let error = ParseError::new(&input, ParseErrorKind::Empty);
assert_eq!(error.input, input);
assert!(!error.input.ends_with(TRUNCATION_MARKER));
}
#[test]
fn a_hostile_input_is_not_copied_into_the_error() {
let input = "a".repeat(4 * 1024 * 1024);
let error = ParseError::new(
&input,
ParseErrorKind::TooLong {
max: MAX_URI_LENGTH,
actual: input.len(),
},
);
assert_eq!(error.input.chars().count(), MAX_ERROR_INPUT_LENGTH + 1);
assert!(error.input.ends_with(TRUNCATION_MARKER));
let ParseErrorKind::TooLong { actual, .. } = error.kind else {
panic!("wrong kind");
};
assert_eq!(actual, 4 * 1024 * 1024);
}
#[test]
fn truncation_counts_characters_and_not_bytes() {
let input = "\u{1f300}".repeat(MAX_ERROR_INPUT_LENGTH * 2);
assert!(input.len() > MAX_ERROR_INPUT_LENGTH * 4);
let error = ParseError::new(&input, ParseErrorKind::Empty);
assert_eq!(error.input.chars().count(), MAX_ERROR_INPUT_LENGTH + 1);
assert!(
error
.input
.chars()
.take(MAX_ERROR_INPUT_LENGTH)
.all(|c| c == '\u{1f300}'),
"a character was cut in half"
);
}
#[test]
fn an_input_one_character_over_the_limit_is_marked() {
let input = "a".repeat(MAX_ERROR_INPUT_LENGTH + 1);
let error = ParseError::new(&input, ParseErrorKind::Empty);
assert!(error.input.ends_with(TRUNCATION_MARKER));
assert_eq!(
error.input.chars().filter(|c| *c == 'a').count(),
MAX_ERROR_INPUT_LENGTH
);
}
#[test]
fn a_truncated_input_still_renders() {
let error = ParseError::new(
&"\u{1f300}".repeat(MAX_ERROR_INPUT_LENGTH + 4),
ParseErrorKind::Empty,
);
assert!(error.to_string().contains(TRUNCATION_MARKER));
}
}