1use std::fmt;
10
11use crate::constants::MAX_URI_LENGTH;
12
13const MAX_ERROR_INPUT_LENGTH: usize = MAX_URI_LENGTH;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ParseError {
23 pub input: String,
34 pub kind: ParseErrorKind,
36}
37
38impl ParseError {
39 pub(crate) fn new(input: &str, kind: ParseErrorKind) -> Self {
41 Self {
42 input: truncate_input(input),
43 kind,
44 }
45 }
46}
47
48const TRUNCATION_MARKER: char = '…';
50
51fn truncate_input(input: &str) -> String {
58 let mut chars = input.chars();
59 let mut kept: String = chars.by_ref().take(MAX_ERROR_INPUT_LENGTH).collect();
60
61 if chars.next().is_some() {
62 kept.push(TRUNCATION_MARKER);
63 }
64
65 kept
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ParseErrorKind {
72 Empty,
74 TooLong {
76 max: usize,
78 actual: usize,
80 },
81 InvalidScheme {
83 found: Option<String>,
85 },
86 InvalidTrustRoot(TrustRootError),
88 InvalidCapabilityPath(CapabilityPathError),
90 InvalidAgentId(AgentIdError),
92 InvalidQuery(QueryError),
94 InvalidFragment(FragmentError),
96 MissingComponent {
98 component: &'static str,
100 },
101 UnexpectedChar {
103 char: char,
105 position: usize,
107 },
108}
109
110impl fmt::Display for ParseError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "failed to parse agent URI '{}': ", self.input)?;
113 match &self.kind {
114 ParseErrorKind::Empty => write!(f, "input is empty"),
115 ParseErrorKind::TooLong { max, actual } => {
116 write!(
117 f,
118 "URI length {actual} exceeds maximum {max}; consider shorter capability path or trust root"
119 )
120 }
121 ParseErrorKind::InvalidScheme { found } => match found {
122 Some(s) => write!(f, "expected scheme 'agent://', found '{s}'"),
123 None => write!(f, "missing scheme; URI must start with 'agent://'"),
124 },
125 ParseErrorKind::InvalidTrustRoot(e) => write!(f, "invalid trust root: {e}"),
126 ParseErrorKind::InvalidCapabilityPath(e) => write!(f, "invalid capability path: {e}"),
127 ParseErrorKind::InvalidAgentId(e) => write!(f, "invalid agent ID: {e}"),
128 ParseErrorKind::InvalidQuery(e) => write!(f, "invalid query string: {e}"),
129 ParseErrorKind::InvalidFragment(e) => write!(f, "invalid fragment: {e}"),
130 ParseErrorKind::MissingComponent { component } => {
131 write!(f, "missing required component: {component}")
132 }
133 ParseErrorKind::UnexpectedChar { char, position } => {
134 write!(f, "unexpected character '{char}' at position {position}")
135 }
136 }
137 }
138}
139
140impl std::error::Error for ParseError {}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum TrustRootError {
146 Empty,
148 TooLong {
150 max: usize,
152 actual: usize,
154 },
155 InvalidDomain {
157 domain: String,
159 reason: &'static str,
161 },
162 InvalidIpAddress {
164 value: String,
166 reason: &'static str,
168 },
169 InvalidPort {
171 value: String,
173 reason: &'static str,
175 },
176 LabelTooLong {
178 label: String,
180 max: usize,
182 actual: usize,
184 },
185 InvalidChar {
187 char: char,
189 position: usize,
191 },
192}
193
194impl fmt::Display for TrustRootError {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 Self::Empty => write!(f, "trust root cannot be empty"),
198 Self::TooLong { max, actual } => {
199 write!(f, "trust root length {actual} exceeds maximum {max}")
200 }
201 Self::InvalidDomain { domain, reason } => {
202 write!(f, "invalid domain '{domain}': {reason}")
203 }
204 Self::InvalidIpAddress { value, reason } => {
205 write!(f, "invalid IP address '{value}': {reason}")
206 }
207 Self::InvalidPort { value, reason } => {
208 write!(f, "invalid port '{value}': {reason}")
209 }
210 Self::LabelTooLong { label, max, actual } => {
211 write!(f, "DNS label '{label}' is {actual} chars, max is {max}")
212 }
213 Self::InvalidChar { char, position } => {
214 write!(f, "invalid character '{char}' at position {position}")
215 }
216 }
217 }
218}
219
220impl std::error::Error for TrustRootError {}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224#[non_exhaustive]
225pub enum CapabilityPathError {
226 Empty,
228 TooLong {
230 max: usize,
232 actual: usize,
234 },
235 TooManySegments {
237 max: usize,
239 actual: usize,
241 },
242 InvalidSegment {
244 segment: String,
246 index: usize,
248 reason: PathSegmentError,
250 },
251}
252
253impl fmt::Display for CapabilityPathError {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 match self {
256 Self::Empty => write!(
257 f,
258 "capability path cannot be empty; at least one segment required"
259 ),
260 Self::TooLong { max, actual } => {
261 write!(f, "capability path length {actual} exceeds maximum {max}")
262 }
263 Self::TooManySegments { max, actual } => {
264 write!(f, "path has {actual} segments, maximum is {max}")
265 }
266 Self::InvalidSegment {
267 segment,
268 index,
269 reason,
270 } => {
271 write!(f, "invalid segment '{segment}' at index {index}: {reason}")
272 }
273 }
274 }
275}
276
277impl std::error::Error for CapabilityPathError {}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281#[non_exhaustive]
282pub enum PathSegmentError {
283 Empty,
285 TooLong {
287 max: usize,
289 actual: usize,
291 },
292 InvalidChar {
294 char: char,
296 position: usize,
298 },
299}
300
301impl fmt::Display for PathSegmentError {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 match self {
304 Self::Empty => write!(f, "segment cannot be empty"),
305 Self::TooLong { max, actual } => {
306 write!(f, "segment length {actual} exceeds maximum {max}")
307 }
308 Self::InvalidChar { char, position } => {
309 write!(
310 f,
311 "invalid character '{char}' at position {position}; only lowercase letters, digits, and hyphens allowed"
312 )
313 }
314 }
315 }
316}
317
318impl std::error::Error for PathSegmentError {}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum AgentIdError {
324 Empty,
326 TooLong {
328 max: usize,
330 actual: usize,
332 },
333 InvalidPrefix(AgentPrefixError),
335 InvalidSuffix {
337 value: String,
339 reason: &'static str,
341 },
342 MissingSeparator,
344 TypeIdError(String),
346}
347
348impl fmt::Display for AgentIdError {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 match self {
351 Self::Empty => write!(f, "agent ID cannot be empty"),
352 Self::TooLong { max, actual } => {
353 write!(f, "agent ID length {actual} exceeds maximum {max}")
354 }
355 Self::InvalidPrefix(e) => write!(f, "invalid prefix: {e}"),
356 Self::InvalidSuffix { value, reason } => {
357 write!(f, "invalid suffix '{value}': {reason}")
358 }
359 Self::MissingSeparator => {
360 write!(f, "missing underscore separator between prefix and suffix")
361 }
362 Self::TypeIdError(msg) => write!(f, "`TypeID` error: {msg}"),
363 }
364 }
365}
366
367impl std::error::Error for AgentIdError {}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371#[non_exhaustive]
372pub enum AgentPrefixError {
373 Empty,
375 TooLong {
377 max: usize,
379 actual: usize,
381 },
382 InvalidTypeClass {
384 class: String,
386 reason: TypeClassError,
388 },
389 InvalidChar {
391 char: char,
393 position: usize,
395 },
396 MustStartWithLetter {
398 found: char,
400 },
401 MustEndWithLetter {
403 found: char,
405 },
406 ContainsDigit {
408 position: usize,
410 },
411}
412
413impl fmt::Display for AgentPrefixError {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 match self {
416 Self::Empty => write!(f, "prefix cannot be empty"),
417 Self::TooLong { max, actual } => {
418 write!(f, "prefix length {actual} exceeds maximum {max}")
419 }
420 Self::InvalidTypeClass { class, reason } => {
421 write!(f, "invalid type class '{class}': {reason}")
422 }
423 Self::InvalidChar { char, position } => {
424 write!(
425 f,
426 "invalid character '{char}' at position {position}; only lowercase letters and underscores allowed"
427 )
428 }
429 Self::MustStartWithLetter { found } => {
430 write!(f, "prefix must start with a letter, found '{found}'")
431 }
432 Self::MustEndWithLetter { found } => {
433 write!(f, "prefix must end with a letter, found '{found}'")
434 }
435 Self::ContainsDigit { position } => {
436 write!(
437 f,
438 "prefix cannot contain digits (found at position {position})"
439 )
440 }
441 }
442 }
443}
444
445impl std::error::Error for AgentPrefixError {
446 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
447 match self {
448 Self::InvalidTypeClass { reason, .. } => Some(reason),
449 _ => None,
450 }
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
456#[non_exhaustive]
457pub enum QueryError {
458 TooLong {
460 max: usize,
462 actual: usize,
464 },
465 InvalidParamName {
467 name: String,
469 reason: &'static str,
471 },
472 InvalidParamValue {
474 name: String,
476 value: String,
478 reason: &'static str,
480 },
481 DuplicateParam {
483 name: String,
485 },
486 InvalidPercentEncoding {
488 value: String,
490 },
491 InvalidUtf8 {
493 value: String,
495 },
496}
497
498impl fmt::Display for QueryError {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 match self {
501 Self::TooLong { max, actual } => {
502 write!(f, "query string length {actual} exceeds maximum {max}")
503 }
504 Self::InvalidParamName { name, reason } => {
505 write!(f, "invalid parameter name '{name}': {reason}")
506 }
507 Self::InvalidParamValue {
508 name,
509 value,
510 reason,
511 } => {
512 write!(
513 f,
514 "invalid value '{value}' for parameter '{name}': {reason}"
515 )
516 }
517 Self::DuplicateParam { name } => {
518 write!(f, "duplicate parameter '{name}'")
519 }
520 Self::InvalidPercentEncoding { value } => {
521 write!(f, "invalid percent encoding in '{value}'")
522 }
523 Self::InvalidUtf8 { value } => {
524 write!(f, "percent-encoded value '{value}' is not valid UTF-8")
525 }
526 }
527 }
528}
529
530impl std::error::Error for QueryError {}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
534#[non_exhaustive]
535pub enum FragmentError {
536 TooLong {
538 max: usize,
540 actual: usize,
542 },
543 InvalidChar {
545 char: char,
547 position: usize,
549 },
550 Empty,
552}
553
554impl fmt::Display for FragmentError {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 match self {
557 Self::TooLong { max, actual } => {
558 write!(f, "fragment length {actual} exceeds maximum {max}")
559 }
560 Self::InvalidChar { char, position } => {
561 write!(
562 f,
563 "invalid character '{char}' at position {position}; allowed: alphanumeric, hyphen, underscore, dot, slash"
564 )
565 }
566 Self::Empty => write!(f, "fragment must not be empty; omit the '#' instead"),
567 }
568 }
569}
570
571impl std::error::Error for FragmentError {}
572
573#[derive(Debug, Clone, PartialEq, Eq)]
575#[non_exhaustive]
576pub enum BuilderError {
577 UriTooLong {
579 max: usize,
581 actual: usize,
583 },
584 InvalidUri(ParseError),
586}
587
588impl fmt::Display for BuilderError {
589 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590 match self {
591 Self::UriTooLong { max, actual } => {
592 write!(
593 f,
594 "resulting URI length {actual} exceeds maximum {max}; \
595 consider shorter component values"
596 )
597 }
598 Self::InvalidUri(e) => write!(f, "cannot build agent URI: {e}"),
599 }
600 }
601}
602
603impl std::error::Error for BuilderError {
604 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
605 match self {
606 Self::InvalidUri(e) => Some(e),
607 Self::UriTooLong { .. } => None,
608 }
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
621#[non_exhaustive]
622pub enum TypeClassError {
623 Empty,
625 InvalidChar {
627 char: char,
629 position: usize,
631 },
632}
633
634impl fmt::Display for TypeClassError {
635 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
636 match self {
637 Self::Empty => write!(f, "type class name cannot be empty"),
638 Self::InvalidChar { char, position } => {
639 write!(
640 f,
641 "invalid character '{char}' at position {position}; \
642 a type class name is lowercase letters only"
643 )
644 }
645 }
646 }
647}
648
649impl std::error::Error for TypeClassError {}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn an_input_a_uri_could_have_been_is_kept_whole() {
657 let input = format!("agent://{}", "a".repeat(MAX_ERROR_INPUT_LENGTH - 8));
660 assert_eq!(input.chars().count(), MAX_ERROR_INPUT_LENGTH);
661
662 let error = ParseError::new(&input, ParseErrorKind::Empty);
663
664 assert_eq!(error.input, input);
665 assert!(!error.input.ends_with(TRUNCATION_MARKER));
666 }
667
668 #[test]
669 fn a_hostile_input_is_not_copied_into_the_error() {
670 let input = "a".repeat(4 * 1024 * 1024);
672
673 let error = ParseError::new(
674 &input,
675 ParseErrorKind::TooLong {
676 max: MAX_URI_LENGTH,
677 actual: input.len(),
678 },
679 );
680
681 assert_eq!(error.input.chars().count(), MAX_ERROR_INPUT_LENGTH + 1);
682 assert!(error.input.ends_with(TRUNCATION_MARKER));
683
684 let ParseErrorKind::TooLong { actual, .. } = error.kind else {
687 panic!("wrong kind");
688 };
689 assert_eq!(actual, 4 * 1024 * 1024);
690 }
691
692 #[test]
693 fn truncation_counts_characters_and_not_bytes() {
694 let input = "\u{1f300}".repeat(MAX_ERROR_INPUT_LENGTH * 2);
698 assert!(input.len() > MAX_ERROR_INPUT_LENGTH * 4);
699
700 let error = ParseError::new(&input, ParseErrorKind::Empty);
701
702 assert_eq!(error.input.chars().count(), MAX_ERROR_INPUT_LENGTH + 1);
703 assert!(
704 error
705 .input
706 .chars()
707 .take(MAX_ERROR_INPUT_LENGTH)
708 .all(|c| c == '\u{1f300}'),
709 "a character was cut in half"
710 );
711 }
712
713 #[test]
714 fn an_input_one_character_over_the_limit_is_marked() {
715 let input = "a".repeat(MAX_ERROR_INPUT_LENGTH + 1);
716
717 let error = ParseError::new(&input, ParseErrorKind::Empty);
718
719 assert!(error.input.ends_with(TRUNCATION_MARKER));
720 assert_eq!(
721 error.input.chars().filter(|c| *c == 'a').count(),
722 MAX_ERROR_INPUT_LENGTH
723 );
724 }
725
726 #[test]
727 fn a_truncated_input_still_renders() {
728 let error = ParseError::new(
731 &"\u{1f300}".repeat(MAX_ERROR_INPUT_LENGTH + 4),
732 ParseErrorKind::Empty,
733 );
734
735 assert!(error.to_string().contains(TRUNCATION_MARKER));
736 }
737}