Skip to main content

sipx_sip/
error.rs

1//! Error types.
2//!
3//! Every rejection names what was wrong. A single opaque `Invalid` variant is not good
4//! enough: the transaction layer chooses between 400, 413 and 505 based on which fault this
5//! was, and an operator reading a log needs to know which byte offended.
6
7use thiserror::Error;
8
9/// A message that could not be built.
10///
11/// Every one of these means a caller tried to put something into a message that would have
12/// changed its structure — the header-injection family. They are errors rather than silent
13/// escaping because a caller that supplies a CRLF in a display name has a bug, and hiding it
14/// helps nobody.
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
16#[non_exhaustive]
17pub enum BuildError {
18    /// A response cannot be routed or correlated because the request omitted a header every
19    /// response must copy (RFC 3261 §8.2.6.1 and §8.2.6.2).
20    #[error("request is missing required response header {header}")]
21    MissingRequiredResponseHeader {
22        /// The missing header's canonical name.
23        header: &'static str,
24    },
25    /// A character that would end a line or terminate a string, in a field that must not
26    /// contain one.
27    #[error("illegal byte {byte:#04x} at offset {offset} in {field}")]
28    IllegalCharacter {
29        /// Which field.
30        field: &'static str,
31        /// Where in the value.
32        offset: usize,
33        /// The offending byte.
34        byte: u8,
35    },
36    /// A field that must be a single token is not one.
37    #[error("{field} is not a token")]
38    NotAToken {
39        /// Which field.
40        field: &'static str,
41    },
42}
43
44/// A message that could not be framed or whose structure is malformed.
45///
46/// Structural only: a message whose *headers* are bad still parses (see [`HeaderError`]).
47/// The transaction layer maps these onto a response status, which is why each variant says
48/// what went wrong rather than merely that something did.
49#[derive(Debug, Clone, PartialEq, Eq, Error)]
50#[non_exhaustive]
51pub enum ParseError {
52    /// The request or status line is malformed. Answer 400.
53    #[error("malformed start line: {0}")]
54    StartLine(#[from] StartLineError),
55    /// A header field line is malformed. Answer 400.
56    #[error("malformed header field on line {line}: {kind}")]
57    HeaderSyntax {
58        /// Which line of the header section, counting the start line as line 1.
59        line: usize,
60        /// What was wrong with it.
61        kind: HeaderSyntaxError,
62    },
63    /// The message body cannot be delimited. Answer 400; on a stream transport the framing
64    /// is unrecoverable and the connection must be closed.
65    #[error("cannot frame message body: {0}")]
66    Framing(#[from] FramingError),
67    /// A configured limit was exceeded. Answer 413 for body limits.
68    #[error("{limit} limit exceeded ({value})")]
69    Limit {
70        /// Which limit.
71        limit: LimitKind,
72        /// The value that exceeded it.
73        value: usize,
74    },
75    /// Not enough bytes yet. Only ever returned internally by the stream parser; callers see
76    /// it as "no message completed".
77    #[error("incomplete message")]
78    Incomplete,
79}
80
81/// What was wrong with a start line.
82#[derive(Debug, Clone, PartialEq, Eq, Error)]
83#[non_exhaustive]
84pub enum StartLineError {
85    /// The message is empty, or the start line is.
86    #[error("empty")]
87    Empty,
88    /// A request line did not have exactly three space-separated elements. Covers multiple
89    /// spaces between elements and a trailing space (RFC 4475 §3.1.2.9, §3.1.2.10).
90    #[error("a request line must have exactly three space-separated elements")]
91    RequestLineShape,
92    /// The method is not a token.
93    #[error("method is not a token")]
94    Method,
95    /// The Request-URI did not parse — including when it is wrapped in `<>`
96    /// (RFC 4475 §3.1.2.7) or contains whitespace (§3.1.2.8).
97    #[error("bad Request-URI: {0}")]
98    Uri(#[from] UriError),
99    /// A status line had no status code.
100    #[error("status line has no status code")]
101    MissingStatusCode,
102    /// The status code is not exactly three digits in `100..=699` (RFC 4475 §3.1.2.19).
103    #[error("status code is not three digits in 100..=699")]
104    StatusCode,
105}
106
107/// What was wrong with a header field line.
108#[derive(Debug, Clone, PartialEq, Eq, Error)]
109#[non_exhaustive]
110pub enum HeaderSyntaxError {
111    /// No colon separating name from value.
112    #[error("no colon")]
113    MissingColon,
114    /// The field name is empty.
115    #[error("empty field name")]
116    EmptyName,
117    /// The field name contains a character outside the `token` set.
118    #[error("field name is not a token")]
119    NameNotToken,
120    /// A bare CR or LF where a CRLF was required.
121    ///
122    /// sipx never accepts a bare LF as a line terminator: two elements disagreeing about
123    /// where a message ends is how a body becomes a second request.
124    #[error("bare CR or LF")]
125    BareNewline,
126    /// The first line of the header section begins with whitespace, so it continues a header
127    /// that does not exist.
128    #[error("header section begins with a continuation line")]
129    LeadingFold,
130}
131
132/// Why a body could not be delimited.
133#[derive(Debug, Clone, PartialEq, Eq, Error)]
134#[non_exhaustive]
135pub enum FramingError {
136    /// No blank line terminating the header section.
137    #[error("no blank line after headers")]
138    NoHeaderTerminator,
139    /// More than one `Content-Length`. Rejected even when the values agree: two elements
140    /// having computed the same length is not worth a second code path (RFC 4475 §3.3.9).
141    #[error("repeated Content-Length")]
142    ContentLengthRepeated,
143    /// `Content-Length` is empty, signed, or not a decimal number. Never converted to a
144    /// number that could be negative, and never used as a length (RFC 4475 §3.1.2.3).
145    #[error("Content-Length is not a decimal number")]
146    ContentLengthMalformed,
147    /// `Content-Length` is larger than the octets actually present (RFC 4475 §3.1.2.2).
148    #[error("Content-Length exceeds the octets present")]
149    BodyTruncated,
150    /// A stream transport requires `Content-Length`; without it the stream cannot be cut into
151    /// messages (RFC 3261 §20.14).
152    #[error("Content-Length is required on stream transports")]
153    ContentLengthRequired,
154}
155
156/// Which limit was exceeded.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum LimitKind {
159    /// Total message size.
160    MessageBytes,
161    /// Declared or actual body size.
162    BodyBytes,
163    /// Number of header fields.
164    Headers,
165    /// Size of a single header field.
166    HeaderBytes,
167    /// Number of continuation lines in one header field.
168    FoldingLines,
169}
170
171impl std::fmt::Display for LimitKind {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        let name = match self {
174            Self::MessageBytes => "message size",
175            Self::BodyBytes => "body size",
176            Self::Headers => "header count",
177            Self::HeaderBytes => "header size",
178            Self::FoldingLines => "folding line count",
179        };
180        f.write_str(name)
181    }
182}
183
184/// A URI that could not be parsed.
185#[derive(Debug, Clone, PartialEq, Eq, Error)]
186#[non_exhaustive]
187pub enum UriError {
188    /// No scheme, or a scheme that is not a token followed by `:`.
189    #[error("missing or malformed URI scheme")]
190    Scheme,
191    /// A `sip:` or `sips:` URI with no host.
192    #[error("URI has no host")]
193    EmptyHost,
194    /// The host contains a character no host may contain.
195    #[error("invalid character in host")]
196    Host,
197    /// The port is not one to five digits, or exceeds 65535.
198    #[error("invalid port")]
199    Port,
200    /// An IPv6 reference missing its closing bracket.
201    #[error("unterminated IPv6 reference")]
202    Ipv6Reference,
203    /// A character illegal anywhere in a URI: whitespace, a control character, or one of
204    /// `<`, `>`, `"`.
205    #[error("illegal character in URI")]
206    IllegalCharacter,
207    /// A `%` not followed by two hex digits.
208    #[error("malformed percent escape")]
209    PercentEscape,
210    /// A SIP user part is empty or contains a byte outside RFC 3261's `user` production.
211    #[error("invalid SIP URI user part")]
212    User,
213    /// A parsed or replacement `tel:` subscriber is empty or falls outside RFC 3966's global and
214    /// local telephone-subscriber productions.
215    #[error("invalid tel URI telephone-subscriber")]
216    TelephoneSubscriber,
217    /// A parsed message's retained URI span no longer points inside its retained wire bytes.
218    #[error("retained URI span is inconsistent with its wire representation")]
219    RetainedSpan,
220    /// A parameter or header with an empty name.
221    #[error("empty parameter name")]
222    EmptyParameterName,
223    /// A uri-parameter name that appears more than once. RFC 3261 §19.1.1: "any given
224    /// parameter-name MUST NOT appear more than once." URI headers may repeat; only the
225    /// `;` list is policed.
226    #[error("repeated uri-parameter name")]
227    DuplicateParameterName,
228}
229
230/// A header whose value could not be parsed, or whose value is out of range.
231///
232/// Distinct from a parse error: the message framed correctly and this one header is bad. A
233/// proxy may still forward such a message; only a party that needs to *read* the header has
234/// a problem.
235#[derive(Debug, Clone, PartialEq, Eq, Error)]
236#[non_exhaustive]
237pub enum HeaderError {
238    /// The value does not match the header's grammar.
239    #[error("malformed {header} header")]
240    Syntax {
241        /// The header that failed to parse.
242        header: &'static str,
243    },
244    /// The value parses but falls outside the range the RFC permits — a `CSeq` above
245    /// 2^31-1, a `Max-Forwards` above 255, a status code outside 100..=699.
246    #[error("{header} value out of range")]
247    OutOfRange {
248        /// The header whose value was out of range.
249        header: &'static str,
250    },
251    /// A URI inside the header did not parse.
252    #[error("invalid URI in {header} header: {source}")]
253    Uri {
254        /// The header carrying the URI.
255        header: &'static str,
256        /// Why the URI was rejected.
257        #[source]
258        source: UriError,
259    },
260    /// A quoted string with no closing quote.
261    #[error("unterminated quoted string in {header} header")]
262    UnterminatedQuotedString {
263        /// The header carrying the unterminated string.
264        header: &'static str,
265    },
266}
267
268/// Why a parser-owned address value could not be edited losslessly.
269#[derive(Debug, Clone, PartialEq, Eq, Error)]
270#[non_exhaustive]
271pub enum AddressEditError {
272    /// The field does not use one of the address grammars exposed by this operation.
273    #[error("header does not have a supported address grammar")]
274    UnsupportedHeader,
275    /// At least one row did not match the field's shared address grammar.
276    #[error("malformed address field: {0}")]
277    Malformed(#[source] HeaderError),
278    /// The flattened, zero-based value index does not exist.
279    #[error("address value index {index} is out of range")]
280    IndexOutOfRange {
281        /// The requested flattened value index.
282        index: usize,
283    },
284    /// The replacement URI did not survive serialization as a valid URI.
285    #[error("invalid replacement URI: {0}")]
286    InvalidUri(#[source] UriError),
287    /// A replacement display name contains a header-delimiting control byte.
288    #[error("replacement display name contains a control byte")]
289    InvalidDisplayName,
290}
291
292/// Why a parser-owned Warning agent could not be edited losslessly.
293#[derive(Debug, Clone, PartialEq, Eq, Error)]
294#[non_exhaustive]
295pub enum WarningEditError {
296    /// At least one row did not match the complete Warning field grammar.
297    #[error("malformed Warning field: {0}")]
298    Malformed(#[source] HeaderError),
299    /// The flattened, zero-based Warning value index does not exist.
300    #[error("Warning value index {index} is out of range")]
301    IndexOutOfRange {
302        /// The requested flattened value index.
303        index: usize,
304    },
305    /// The replacement is empty or is not one RFC 3261 token.
306    #[error("replacement Warning pseudonym is not a token")]
307    InvalidPseudonym,
308}