Skip to main content

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