tor_netdoc/err.rs
1//! Error type from parsing a document, and the position where it occurred
2use thiserror::Error;
3
4use crate::types::policy::PolicyError;
5use std::{borrow::Cow, fmt, sync::Arc};
6
7/// A position within a directory object. Used to tell where an error
8/// occurred.
9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10#[non_exhaustive]
11pub enum Pos {
12 /// The error did not occur at any particular position.
13 ///
14 /// This can happen when the error is something like a missing entry:
15 /// the entry is supposed to go _somewhere_, but we can't say where.
16 None,
17 /// The error occurred at an unknown position.
18 ///
19 /// We should avoid using this case.
20 Unknown,
21 /// The error occurred at an invalid offset within the string, or
22 /// outside the string entirely.
23 ///
24 /// This can only occur because of an internal error of some kind.
25 Invalid(usize),
26 /// The error occurred at a particular byte within the string.
27 ///
28 /// We try to convert these to a Pos before displaying them to the user.
29 Byte {
30 /// Byte offset within a string.
31 off: usize,
32 },
33 /// The error occurred at a particular line (and possibly at a
34 /// particular byte within the line.)
35 PosInLine {
36 /// Line offset within a string.
37 line: usize,
38 /// Byte offset within the line.
39 byte: usize,
40 },
41 /// The error occurred at a position in memory. This shouldn't be
42 /// exposed to the user, but rather should be mapped to a position
43 /// in the string.
44 Raw {
45 /// A raw pointer to the position where the error occurred.
46 ptr: *const u8,
47 },
48}
49
50// It's okay to send a Pos to another thread, even though its Raw
51// variant contains a pointer. That's because we never dereference the
52// pointer: we only compare it to another pointer representing a
53// string.
54//
55// TODO: Find a better way to have Pos work.
56unsafe impl Send for Pos {}
57unsafe impl Sync for Pos {}
58
59impl Pos {
60 /// Construct a Pos from an offset within a &str slice.
61 pub fn from_offset(s: &str, off: usize) -> Self {
62 if off > s.len() || !s.is_char_boundary(off) {
63 Pos::Invalid(off)
64 } else {
65 let s = &s[..off];
66 let last_nl = s.rfind('\n');
67 match last_nl {
68 Some(pos) => {
69 let newlines = s.bytes().filter(|b| *b == b'\n').count();
70 Pos::PosInLine {
71 line: newlines + 1,
72 byte: off - pos,
73 }
74 }
75 None => Pos::PosInLine {
76 line: 1,
77 byte: off + 1,
78 },
79 }
80 }
81 }
82 /// Construct a Pos from a slice of some other string. This
83 /// Pos won't be terribly helpful, but it may be converted
84 /// into a useful Pos with `within`.
85 pub fn at(s: &str) -> Self {
86 let ptr = s.as_ptr();
87 Pos::Raw { ptr }
88 }
89 /// Construct Pos from the end of some other string.
90 pub fn at_end_of(s: &str) -> Self {
91 let ending = &s[s.len()..];
92 Pos::at(ending)
93 }
94 /// Construct a position from a byte offset.
95 pub fn from_byte(off: usize) -> Self {
96 Pos::Byte { off }
97 }
98 /// Construct a position from a line and a byte offset within that line.
99 pub fn from_line(line: usize, byte: usize) -> Self {
100 Pos::PosInLine { line, byte }
101 }
102 /// If this position appears within `s`, and has not yet been mapped to
103 /// a line-and-byte position, return its offset.
104 pub(crate) fn offset_within(&self, s: &str) -> Option<usize> {
105 match self {
106 Pos::Byte { off } => Some(*off),
107 Pos::Raw { ptr } => offset_in(*ptr, s),
108 _ => None,
109 }
110 }
111 /// Given a position, if it was at a byte offset, convert it to a
112 /// line-and-byte position within `s`.
113 ///
114 /// Requires that this position was actually generated from `s`.
115 /// If it was not, the results here may be nonsensical.
116 ///
117 /// TODO: I wish I knew an efficient safe way to do this that
118 /// guaranteed that we we always talking about the right string.
119 #[must_use]
120 pub fn within(self, s: &str) -> Self {
121 match self {
122 Pos::Byte { off } => Self::from_offset(s, off),
123 Pos::Raw { ptr } => {
124 if let Some(off) = offset_in(ptr, s) {
125 Self::from_offset(s, off)
126 } else {
127 self
128 }
129 }
130 _ => self,
131 }
132 }
133}
134
135/// If `ptr` is within `s`, return its byte offset.
136fn offset_in(ptr: *const u8, s: &str) -> Option<usize> {
137 // We need to confirm that 'ptr' falls within 's' in order
138 // to subtract it meaningfully and find its offset.
139 // Otherwise, we'll get a bogus result.
140 //
141 // Fortunately, we _only_ get a bogus result: we don't
142 // hit unsafe behavior.
143 let ptr_u = ptr as usize;
144 let start_u = s.as_ptr() as usize;
145 let end_u = (s.as_ptr() as usize) + s.len();
146 if start_u <= ptr_u && ptr_u < end_u {
147 Some(ptr_u - start_u)
148 } else {
149 None
150 }
151}
152
153impl fmt::Display for Pos {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 use Pos::*;
156 match self {
157 None => write!(f, ""),
158 Unknown => write!(f, " at unknown position"),
159 Invalid(off) => write!(f, " at invalid offset at index {}", off),
160 Byte { off } => write!(f, " at byte {}", off),
161 PosInLine { line, byte } => write!(f, " on line {}, byte {}", line, byte),
162 Raw { ptr } => write!(f, " at {:?}", ptr),
163 }
164 }
165}
166
167/// A variety of parsing error.
168#[derive(Copy, Clone, Debug, derive_more::Display, PartialEq, Eq)]
169#[non_exhaustive]
170pub enum NetdocErrorKind {
171 /// An internal error in the parser: these should never happen.
172 #[display("internal error")]
173 Internal,
174 /// Invoked an API in an incorrect manner.
175 #[display("bad API usage")]
176 BadApiUsage,
177 /// An entry was found with no keyword.
178 #[display("no keyword for entry")]
179 MissingKeyword,
180 /// An entry was found with no newline at the end.
181 #[display("line truncated before newline")]
182 TruncatedLine,
183 /// A bad string was found in the keyword position.
184 #[display("invalid keyword")]
185 BadKeyword,
186 /// We found an ill-formed "BEGIN FOO" tag.
187 #[display("invalid PEM BEGIN tag")]
188 BadObjectBeginTag,
189 /// We found an ill-formed "END FOO" tag.
190 #[display("invalid PEM END tag")]
191 BadObjectEndTag,
192 /// We found a "BEGIN FOO" tag with an "END FOO" tag that didn't match.
193 #[display("mismatched PEM tags")]
194 BadObjectMismatchedTag,
195 /// We found a base64 object with an invalid base64 encoding.
196 #[display("invalid base64 in object")]
197 BadObjectBase64,
198 /// The document is not supposed to contain more than one of some
199 /// kind of entry, but we found one anyway.
200 #[display("duplicate entry")]
201 DuplicateToken,
202 /// The document is not supposed to contain any of some particular kind
203 /// of entry, but we found one anyway.
204 #[display("unexpected entry")]
205 UnexpectedToken,
206 /// The document is supposed to contain any of some particular kind
207 /// of entry, but we didn't find one one anyway.
208 #[display("didn't find required entry")]
209 MissingToken,
210 /// The document was supposed to have one of these, but not where we
211 /// found it.
212 #[display("entry out of place")]
213 MisplacedToken,
214 /// We found more arguments on an entry than it is allowed to have.
215 #[display("too many arguments")]
216 TooManyArguments,
217 /// We didn't fine enough arguments for some entry.
218 #[display("too few arguments")]
219 TooFewArguments,
220 /// We found an object attached to an entry that isn't supposed to
221 /// have one.
222 #[display("unexpected object")]
223 UnexpectedObject,
224 /// An entry was supposed to have an object, but it didn't.
225 #[display("missing object")]
226 MissingObject,
227 /// We found an object on an entry, but the type was wrong.
228 #[display("wrong object type")]
229 WrongObject,
230 /// We tried to find an argument that we were sure would be there,
231 /// but it wasn't!
232 ///
233 /// This error should never occur in correct code; it should be
234 /// caught earlier by TooFewArguments.
235 #[display("missing argument")]
236 MissingArgument,
237 /// We found an argument that couldn't be parsed.
238 #[display("bad argument for entry")]
239 BadArgument,
240 /// We found an object that couldn't be parsed after it was decoded.
241 #[display("bad object for entry")]
242 BadObjectVal,
243 /// There was some signature that we couldn't validate.
244 #[display("couldn't validate signature")]
245 BadSignature, // TODO(nickm): say which kind of signature.
246 /// The object is not valid at the required time.
247 #[display("couldn't validate time bound")]
248 BadTimeBound,
249 /// There was a tor version we couldn't parse.
250 #[display("couldn't parse Tor version")]
251 BadTorVersion,
252 /// There was an ipv4 or ipv6 policy entry that we couldn't parse.
253 #[display("invalid policy entry")]
254 BadPolicy,
255 /// An underlying byte sequence couldn't be decoded.
256 #[display("decoding error")]
257 Undecodable,
258 /// Versioned document with an unrecognized version.
259 #[display("unrecognized document version")]
260 BadDocumentVersion,
261 /// Unexpected document type
262 #[display("unexpected document type")]
263 BadDocumentType,
264 /// We expected a kind of entry that we didn't find
265 #[display("missing entry")]
266 MissingEntry,
267 /// Document or section started with wrong token
268 #[display("Wrong starting token")]
269 WrongStartingToken,
270 /// Document or section ended with wrong token
271 #[display("Wrong ending token")]
272 WrongEndingToken,
273 /// Items not sorted as expected
274 #[display("Incorrect sort order")]
275 WrongSortOrder,
276 /// A consensus lifetime was ill-formed.
277 #[display("Invalid consensus lifetime")]
278 InvalidLifetime,
279 /// Found an empty line in the middle of a document
280 #[display("Empty line")]
281 EmptyLine,
282 /// The document began with a deprecated unicode BOM marker.
283 #[display("unexpected byte-order marker")]
284 BomMarkerFound,
285 /// The document contained an internal NUL byte
286 #[display("unexpected NUL")]
287 NulFound,
288 /// An item contained extra spaces at a place where they are not allowed.
289 #[display("Extraneous spaces")]
290 ExtraneousSpace,
291}
292
293/// The underlying source for an [`Error`](struct@Error).
294#[derive(Clone, Debug, Error)]
295#[non_exhaustive]
296pub(crate) enum NetdocErrorSource {
297 /// An error when parsing a binary object.
298 #[error("Error parsing binary object")]
299 Bytes(#[from] tor_bytes::Error),
300 /// An error when parsing an exit policy.
301 #[error("Error parsing policy")]
302 Policy(#[from] PolicyError),
303 /// An error when parsing an integer.
304 #[error("Couldn't parse integer")]
305 Int(#[from] std::num::ParseIntError),
306 /// An error when parsing an IP or socket address.
307 #[error("Couldn't parse address")]
308 Address(#[from] std::net::AddrParseError),
309 /// An error when validating a signature.
310 #[error("Invalid signature")]
311 Signature(#[source] Arc<signature::Error>),
312 /// An error when validating a signature on an embedded binary certificate.
313 #[error("Invalid certificate")]
314 CertSignature(#[from] tor_cert::CertError),
315 /// An error caused by an expired or not-yet-valid descriptor.
316 #[error("Descriptor expired or not yet valid")]
317 UntimelyDescriptor(#[from] tor_checkable::TimeValidityError),
318 /// Invalid protocol versions.
319 #[error("Protocol versions")]
320 Protovers(#[from] tor_protover::ParseError),
321 /// A bug in our programming, or somebody else's.
322 #[error("Internal error or bug")]
323 Bug(#[from] tor_error::Bug),
324}
325
326impl NetdocErrorKind {
327 /// Construct a new Error with this kind.
328 #[must_use]
329 pub(crate) fn err(self) -> Error {
330 Error {
331 kind: self,
332 msg: None,
333 pos: Pos::Unknown,
334 source: None,
335 }
336 }
337
338 /// Construct a new error with this kind at a given position.
339 #[must_use]
340 pub(crate) fn at_pos(self, pos: Pos) -> Error {
341 self.err().at_pos(pos)
342 }
343
344 /// Construct a new error with this kind and a given message.
345 #[must_use]
346 pub(crate) fn with_msg<T>(self, msg: T) -> Error
347 where
348 T: Into<Cow<'static, str>>,
349 {
350 self.err().with_msg(msg)
351 }
352}
353
354impl From<signature::Error> for NetdocErrorSource {
355 fn from(err: signature::Error) -> Self {
356 NetdocErrorSource::Signature(Arc::new(err))
357 }
358}
359
360/// An error that occurred while parsing a directory object of some kind.
361#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct Error {
364 /// What kind of error occurred?
365 pub(crate) kind: NetdocErrorKind,
366 /// Do we have more information about the error?>
367 msg: Option<Cow<'static, str>>,
368 /// Where did the error occur?
369 pos: Pos,
370 /// Was this caused by another error?
371 source: Option<NetdocErrorSource>,
372}
373
374impl PartialEq for Error {
375 fn eq(&self, other: &Self) -> bool {
376 self.kind == other.kind && self.msg == other.msg && self.pos == other.pos
377 }
378}
379
380impl Error {
381 /// Helper: return this error's position.
382 pub(crate) fn pos(&self) -> Pos {
383 self.pos
384 }
385
386 /// Return a new error based on this one, with any byte-based
387 /// position mapped to some line within a string.
388 #[must_use]
389 pub fn within(mut self, s: &str) -> Error {
390 self.pos = self.pos.within(s);
391 self
392 }
393
394 /// Return a new error based on this one, with the position (if
395 /// any) replaced by 'p'.
396 #[must_use]
397 pub fn at_pos(mut self, p: Pos) -> Error {
398 self.pos = p;
399 self
400 }
401
402 /// Return a new error based on this one, with the position (if
403 /// replaced by 'p' if it had no position before.
404 #[must_use]
405 pub fn or_at_pos(mut self, p: Pos) -> Error {
406 match self.pos {
407 Pos::None | Pos::Unknown => {
408 self.pos = p;
409 }
410 _ => (),
411 }
412 self
413 }
414
415 /// Return a new error based on this one, with the message
416 /// value set to a provided static string.
417 #[must_use]
418 pub(crate) fn with_msg<T>(mut self, message: T) -> Error
419 where
420 T: Into<Cow<'static, str>>,
421 {
422 self.msg = Some(message.into());
423 self
424 }
425
426 /// Return a new error based on this one, with the source-error
427 /// value set to the provided error.
428 #[must_use]
429 pub(crate) fn with_source<T>(mut self, source: T) -> Error
430 where
431 T: Into<NetdocErrorSource>,
432 {
433 self.source = Some(source.into());
434 self
435 }
436
437 /// Return the [`NetdocErrorKind`] of this error.
438 pub fn netdoc_error_kind(&self) -> NetdocErrorKind {
439 self.kind
440 }
441}
442
443impl fmt::Display for Error {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 write!(f, "{}{}", self.kind, self.pos)?;
446 if let Some(msg) = &self.msg {
447 write!(f, ": {}", msg)?;
448 }
449 Ok(())
450 }
451}
452
453impl std::error::Error for Error {
454 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
455 self.source.as_ref().map(|s| s as _)
456 }
457}
458
459/// Helper: declare an Into<> implementation to automatically convert a $source
460/// into an Error with kind $kind.
461macro_rules! declare_into {
462 {$source:ty => $kind:ident} => {
463 impl From<$source> for Error {
464 fn from(source: $source) -> Error {
465 Error {
466 kind: NetdocErrorKind::$kind,
467 msg: None,
468 pos: Pos::Unknown,
469 source: Some(source.into())
470 }
471 }
472 }
473 }
474}
475
476declare_into! { signature::Error => BadSignature }
477declare_into! { tor_checkable::TimeValidityError => BadTimeBound }
478declare_into! { tor_bytes::Error => Undecodable }
479declare_into! { std::num::ParseIntError => BadArgument }
480declare_into! { std::net::AddrParseError => BadArgument }
481declare_into! { PolicyError => BadPolicy }
482
483impl From<tor_error::Bug> for Error {
484 fn from(err: tor_error::Bug) -> Self {
485 use tor_error::HasKind;
486 let kind = match err.kind() {
487 tor_error::ErrorKind::BadApiUsage => NetdocErrorKind::BadApiUsage,
488 _ => NetdocErrorKind::Internal,
489 };
490
491 Error {
492 kind,
493 msg: None,
494 pos: Pos::Unknown,
495 source: Some(err.into()),
496 }
497 }
498}
499
500/// An error that occurs while trying to construct a network document.
501#[derive(Clone, Debug, Error)]
502#[non_exhaustive]
503pub enum BuildError {
504 /// We were unable to build the document, probably due to an invalid
505 /// argument of some kind.
506 #[error("cannot build document: {0}")]
507 CannotBuild(&'static str),
508
509 /// An argument that was given as a string turned out to be unparsable.
510 #[error("unable to parse argument")]
511 Parse(#[from] crate::err::Error),
512}