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    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 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
326/// Error parsing what was supposed to be a fixed, constant, string
327#[derive(Clone, Debug, Error, Eq, PartialEq)]
328#[error("expected fixed string {expected:?}, got {got:?}")]
329// This is unlikely to change - and our macro expansion wants to be able to construct i.
330// Making it exhaustive seems better than the bureaucracy of a constructor.
331#[allow(clippy::exhaustive_structs)]
332pub struct ExpectedConstantString {
333    /// The fixed, constant string we expected
334    pub expected: &'static str,
335
336    /// The actual value we encountered
337    pub got: String,
338}
339
340impl NetdocErrorKind {
341    /// Construct a new Error with this kind.
342    #[must_use]
343    pub(crate) fn err(self) -> Error {
344        Error {
345            kind: self,
346            msg: None,
347            pos: Pos::Unknown,
348            source: None,
349        }
350    }
351
352    /// Construct a new error with this kind at a given position.
353    #[must_use]
354    pub(crate) fn at_pos(self, pos: Pos) -> Error {
355        self.err().at_pos(pos)
356    }
357
358    /// Construct a new error with this kind and a given message.
359    #[must_use]
360    pub(crate) fn with_msg<T>(self, msg: T) -> Error
361    where
362        T: Into<Cow<'static, str>>,
363    {
364        self.err().with_msg(msg)
365    }
366}
367
368impl From<signature::Error> for NetdocErrorSource {
369    fn from(err: signature::Error) -> Self {
370        NetdocErrorSource::Signature(Arc::new(err))
371    }
372}
373
374/// An error that occurred while parsing a directory object of some kind.
375#[derive(Debug, Clone)]
376#[non_exhaustive]
377pub struct Error {
378    /// What kind of error occurred?
379    pub(crate) kind: NetdocErrorKind,
380    /// Do we have more information about the error?>
381    msg: Option<Cow<'static, str>>,
382    /// Where did the error occur?
383    pos: Pos,
384    /// Was this caused by another error?
385    source: Option<NetdocErrorSource>,
386}
387
388impl PartialEq for Error {
389    fn eq(&self, other: &Self) -> bool {
390        self.kind == other.kind && self.msg == other.msg && self.pos == other.pos
391    }
392}
393
394impl Error {
395    /// Helper: return this error's position.
396    pub(crate) fn pos(&self) -> Pos {
397        self.pos
398    }
399
400    /// Return a new error based on this one, with any byte-based
401    /// position mapped to some line within a string.
402    #[must_use]
403    pub fn within(mut self, s: &str) -> Error {
404        self.pos = self.pos.within(s);
405        self
406    }
407
408    /// Return a new error based on this one, with the position (if
409    /// any) replaced by 'p'.
410    #[must_use]
411    pub fn at_pos(mut self, p: Pos) -> Error {
412        self.pos = p;
413        self
414    }
415
416    /// Return a new error based on this one, with the position (if
417    /// replaced by 'p' if it had no position before.
418    #[must_use]
419    pub fn or_at_pos(mut self, p: Pos) -> Error {
420        match self.pos {
421            Pos::None | Pos::Unknown => {
422                self.pos = p;
423            }
424            _ => (),
425        }
426        self
427    }
428
429    /// Return a new error based on this one, with the message
430    /// value set to a provided static string.
431    #[must_use]
432    pub(crate) fn with_msg<T>(mut self, message: T) -> Error
433    where
434        T: Into<Cow<'static, str>>,
435    {
436        self.msg = Some(message.into());
437        self
438    }
439
440    /// Return a new error based on this one, with the source-error
441    /// value set to the provided error.
442    #[must_use]
443    pub(crate) fn with_source<T>(mut self, source: T) -> Error
444    where
445        T: Into<NetdocErrorSource>,
446    {
447        self.source = Some(source.into());
448        self
449    }
450
451    /// Return the [`NetdocErrorKind`] of this error.
452    pub fn netdoc_error_kind(&self) -> NetdocErrorKind {
453        self.kind
454    }
455}
456
457impl fmt::Display for Error {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        write!(f, "{}{}", self.kind, self.pos)?;
460        if let Some(msg) = &self.msg {
461            write!(f, ": {}", msg)?;
462        }
463        Ok(())
464    }
465}
466
467impl std::error::Error for Error {
468    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
469        self.source.as_ref().map(|s| s as _)
470    }
471}
472
473/// Helper: declare an Into<> implementation to automatically convert a $source
474/// into an Error with kind $kind.
475macro_rules! declare_into  {
476    {$source:ty => $kind:ident} => {
477        impl From<$source> for Error {
478            fn from(source: $source) -> Error {
479                Error {
480                    kind: NetdocErrorKind::$kind,
481                    msg: None,
482                    pos: Pos::Unknown,
483                    source: Some(source.into())
484                }
485            }
486        }
487    }
488}
489
490declare_into! { signature::Error => BadSignature }
491declare_into! { tor_checkable::TimeValidityError => BadTimeBound }
492declare_into! { tor_bytes::Error => Undecodable }
493declare_into! { std::num::ParseIntError => BadArgument }
494declare_into! { std::net::AddrParseError => BadArgument }
495declare_into! { PolicyError => BadPolicy }
496
497impl From<tor_error::Bug> for Error {
498    fn from(err: tor_error::Bug) -> Self {
499        use tor_error::HasKind;
500        let kind = match err.kind() {
501            tor_error::ErrorKind::BadApiUsage => NetdocErrorKind::BadApiUsage,
502            _ => NetdocErrorKind::Internal,
503        };
504
505        Error {
506            kind,
507            msg: None,
508            pos: Pos::Unknown,
509            source: Some(err.into()),
510        }
511    }
512}
513
514/// An error that occurs while trying to construct a network document.
515#[derive(Clone, Debug, Error)]
516#[non_exhaustive]
517pub enum BuildError {
518    /// We were unable to build the document, probably due to an invalid
519    /// argument of some kind.
520    #[error("cannot build document: {0}")]
521    CannotBuild(&'static str),
522
523    /// An argument that was given as a string turned out to be unparsable.
524    #[error("unable to parse argument")]
525    Parse(#[from] crate::err::Error),
526}