1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Error types produced by parsing.
use thiserror::Error;
/// Errors returned from [`crate::parse`] and [`crate::parse_fragment`].
///
/// Every variant carries a 1-based `line` number pointing at the offending
/// position in the source document. The error's `Display` impl produces a
/// human-readable message that includes the line number.
#[derive(Debug, Clone, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
/// An opening tag had no matching closing tag.
#[error("line {line}: <{tag}> has no matching </{tag}>")]
UnclosedTag {
/// Tag name that was never closed.
tag: String,
/// 1-based line of the opening tag.
line: u32,
},
/// A closing tag did not match the most recent open tag.
#[error("line {line}: </{found}> does not match the most recent open tag <{expected}>")]
MismatchedClose {
/// Tag name that was found in the closing position.
found: String,
/// Tag name expected based on the most recent open tag.
expected: String,
/// 1-based line of the closing tag.
line: u32,
},
/// A closing tag appeared with no corresponding open tag.
#[error("line {line}: </{tag}> has no matching open tag")]
StrayClose {
/// Tag name in the closing position.
tag: String,
/// 1-based line of the closing tag.
line: u32,
},
/// A tag was malformed — for example, an unterminated `<` at end of input
/// or an unquoted attribute value.
#[error("line {line}: malformed tag — {reason}")]
MalformedTag {
/// Short description of what went wrong.
reason: String,
/// 1-based line of the malformed tag.
line: u32,
},
/// An element's attribute value was malformed (e.g. missing closing quote).
#[error("line {line}: malformed attribute on <{tag}> — {reason}")]
MalformedAttribute {
/// Tag name carrying the attribute.
tag: String,
/// Short description of what went wrong.
reason: String,
/// 1-based line of the offending attribute.
line: u32,
},
/// Two sibling elements with the same tag carried the same `id` attribute.
#[error("line {line}: duplicate id=\"{id}\" on <{tag}>")]
DuplicateId {
/// Tag name where the duplicate was detected.
tag: String,
/// The repeated `id` value.
id: String,
/// 1-based line of the duplicate.
line: u32,
},
/// Element nesting exceeded the configured maximum depth.
///
/// Acts as a guard against pathological inputs (recursive walks could
/// otherwise stack-overflow on adversarial documents).
#[error("line {line}: <{tag}> exceeds maximum nesting depth of {max}")]
MaxDepthExceeded {
/// Tag that pushed past the limit.
tag: String,
/// Configured maximum depth.
max: u32,
/// 1-based line of the offending opening tag.
line: u32,
},
/// Two attributes with the same name appeared on a single element.
///
/// XML requires attribute names to be unique per element; lenient parsing
/// would yield first/last/middle ambiguity in downstream consumers, so
/// duplicates are rejected up front.
#[error("line {line}: duplicate attribute {attr} on <{tag}>")]
DuplicateAttr {
/// Tag carrying the duplicate.
tag: String,
/// Repeated attribute name.
attr: String,
/// 1-based line of the duplicate.
line: u32,
},
/// Input exceeded the maximum byte length the parser can address.
///
/// Source byte offsets are stored as `u32`, so inputs larger than
/// `u32::MAX` bytes (4 GiB - 1) cannot have their spans tracked accurately
/// and are refused at parse time.
#[error("input is {size} bytes — exceeds maximum of {max} bytes")]
InputTooLarge {
/// Byte length of the offending input.
size: u64,
/// Configured maximum byte length.
max: u64,
},
}
impl ParseError {
/// 1-based line number where the error was detected, when one is
/// available.
///
/// Returns `None` for [`ParseError::InputTooLarge`] — that variant is
/// raised before the input is scanned, so no source position exists.
#[must_use]
pub fn line(&self) -> Option<u32> {
match self {
Self::UnclosedTag { line, .. }
| Self::MismatchedClose { line, .. }
| Self::StrayClose { line, .. }
| Self::MalformedTag { line, .. }
| Self::MalformedAttribute { line, .. }
| Self::DuplicateId { line, .. }
| Self::MaxDepthExceeded { line, .. }
| Self::DuplicateAttr { line, .. } => Some(*line),
Self::InputTooLarge { .. } => None,
}
}
}