Skip to main content

tor_netdoc/
parse2.rs

1//! New netdoc parsing arrangements, with `derive`
2//!
3//! # Parsing principles
4//!
5//! A parseable network document is a type implementing [`NetdocParseable`].
6//! usually via the
7//! [`NetdocParseable` derive=deftly macro`](crate::derive_deftly_template_NetdocParseable).
8//!
9//! A document type is responsible for recognising its own heading item.
10//! Its parser will also be told other of structural items that it should not consume.
11//! The structural lines can then be used to pass control to the appropriate parser.
12//!
13//! A "structural item" is a netdoc item that is defines the structure of the document.
14//! This includes the intro items for whole documents,
15//! the items that introduce document sections
16//! (which we model by treating the section as a sub-document)
17//! and signature items (which introduce the signatures at the end of the document,
18//! and after which no non-signature items may appear).
19//!
20//! # Ordering
21//!
22//! We don't always parse things into a sorted order.
23//! Sorting will be done when assembling documents, before outputting.
24//!
25//! # Types, and signature handling
26//!
27//! Most top-level network documents are signed somehow.
28//! In this case there are three types:
29//!
30//!   * **`FooUnverified`**: a signed `Foo`, with its signatures, not yet verified.
31//!     Implements [`NetdocParseableUnverified`],
32//!     typically by invoking the
33//!     [`NetdocUParseablenverified` derive macro](crate::derive_deftly_template_NetdocParseableUnverified)
34//!     on `Foo`.
35//!
36//!     Type-specific methods are provided for verification,
37//!     to obtain a `Foo`.
38//!
39//!   * **`Foo`**: the body data for the document.
40//!     This doesn't contain any signatures.
41//!     Having one of these to play with means signatures have already been validated.
42//!     Can be parsed as part of the signed document,
43//!     via the `NetdocParseable` implementation on `FooUnverified`,
44//!     and then obtained via `.verify_...` method(s) on `FooUnverified`,
45//!
46//!   * **`FooSignatures`**: the signatures for a `Foo`.
47//!     Implements `NetdocParseableSignatures`, via
48//!     [derive](crate::derive_deftly_template_NetdocParseableSignatures),
49//!     with `#[deftly(netdoc(signatures))]`.
50//!
51//! # Relationship to tor_netdoc::parse
52//!
53//! This is a completely new parsing approach, based on different principles.
54//! The key principle is the recognition of "structural keywords",
55//! recursively within a parsing stack, via the p`NetdocParseable`] trait.
56//!
57//! This allows the parser to be derived.  We have type-driven parsing
58//! of whole Documents, Items, and their Arguments and Objects,
59//! including of their multiplicity.
60//!
61//! The different keyword handling means we can't use most of the existing lexer,
62//! and need new item parsing API:
63//!
64//!  * [`NetdocParseable`] trait.
65//!  * [`KeywordRef`] type.
66//!  * [`ItemStream`], [`UnparsedItem`], [`ArgumentStream`], [`UnparsedObject`].
67//!
68//! The different error handling means we have our own error types.
69//! (The crate's existing parse errors have information that we don't track,
70//! and is also a portmanteau error for parsing, writing, and other functions.)
71//!
72//! Document signing is handled in a more abstract way.
73//!
74//! Some old netdoc constructs are not supported.
75//! For example, the obsolete `opt` prefix on safe-to-ignore Items.
76//! The parser may make different decisions about netdocs with anomalous item ordering.
77
78#[doc(hidden)]
79#[macro_use]
80pub mod internal_prelude;
81
82#[macro_use]
83mod structural;
84
85#[macro_use]
86mod derive;
87
88mod error;
89mod impls;
90pub mod keyword;
91mod lex;
92mod lines;
93pub mod multiplicity;
94mod signatures;
95mod traits;
96
97use internal_prelude::*;
98
99pub use error::{ArgumentError, ErrorProblem, ParseError, UnexpectedArgument, VerifyFailed};
100pub use impls::times::NdaSystemTimeDeprecatedSyntax;
101pub use keyword::KeywordRef;
102pub use lex::{ArgumentStream, ItemStream, NoFurtherArguments, UnparsedItem, UnparsedObject};
103pub use lines::{Lines, Peeked, StrExt};
104pub use signatures::{
105    HasUnverifiedParsedBody, NetdocParseableSignatures, NetdocParseableUnverified,
106    SignatureHashInputs, SignatureHashesAccumulator, SignatureItemParseable, SignaturesData,
107    sig_hashes,
108};
109#[allow(deprecated)]
110#[deprecated]
111pub use signatures::{check_validity_time, check_validity_time_tolerance};
112pub use structural::{StopAt, StopPredicate};
113pub use traits::{
114    IsStructural, ItemArgumentParseable, ItemObjectParseable, ItemValueParseable, NetdocParseable,
115    NetdocParseableFields,
116};
117
118#[doc(hidden)]
119pub use derive::netdoc_parseable_derive_debug;
120
121pub(crate) use internal_prelude::EP;
122
123//---------- input ----------
124
125/// Options for parsing
126///
127/// Specific document and type parsing methods may use these parameters
128/// to control their parsing behaviour at run-time.
129#[derive(educe::Educe, Debug, Clone)]
130#[allow(clippy::manual_non_exhaustive)]
131#[educe(Default)]
132pub struct ParseOptions {
133    /// Retain unknown values?
134    ///
135    /// Some field types, especially for flags fields, have the capability to retain
136    /// unknown flags.  But, whereas known flags can be represented as single bits,
137    /// representing unknown flags involves allocating and copying strings.
138    /// Unless the document is to be reproduced, this is a waste of effort.
139    ///
140    /// Each document field type affected by this option should store the unknowns
141    /// as `Unknown<HashSet<String>>` or similar.
142    ///
143    /// This feature should only be used where performance is important.
144    /// For example, it is useful for types that appear in md consensus routerdescs,
145    /// but less useful for types that appear only in a netstatus preamble.
146    ///
147    /// This is currently used for router flags.
148    #[educe(Default(expression = "Unknown::new_discard()"))]
149    pub retain_unknown_values: Unknown<()>,
150
151    // Like `#[non_exhaustive]`, but doesn't prevent use of struct display syntax with `..`
152    #[doc(hidden)]
153    _private_non_exhaustive: (),
154}
155
156/// Input to a network document top-level parsing operation
157#[derive(Debug, Clone, amplify::Getters)]
158pub struct ParseInput<'s> {
159    /// The actual document text
160    #[getter(as_copy)]
161    input: &'s str,
162
163    /// Filename (for error reporting)
164    #[getter(as_copy)]
165    file: &'s str,
166
167    /// Parsing options
168    #[getter(as_ref, as_mut)]
169    options: ParseOptions,
170}
171
172impl<'s> ParseInput<'s> {
173    /// Prepare to parse an input string
174    pub fn new(input: &'s str, file: &'s str) -> Self {
175        ParseInput {
176            input,
177            file,
178            options: ParseOptions::default(),
179        }
180    }
181
182    /// Enable retention of unknown values during parsing
183    ///
184    /// Convenience method to set
185    /// [`.options_mut().retain_unknown_values`](ParseOptions::retain_unknown_values)
186    /// to [`Unknown::Retained`].
187    #[cfg(feature = "retain-unknown")]
188    pub fn retain_unknown_values(&mut self) {
189        self.options_mut().retain_unknown_values = Unknown::Retained(());
190    }
191}
192
193//---------- parser ----------
194
195/// Common code for `parse_netdoc` and `parse_netdoc_multiple`
196///
197/// Creates the `ItemStream`, calls `parse_completely`, and handles errors.
198fn parse_internal<T, D: NetdocParseable>(
199    input: &ParseInput<'_>,
200    parse_completely: impl FnOnce(&mut ItemStream) -> Result<T, ErrorProblem>,
201) -> Result<T, ParseError> {
202    let mut items = ItemStream::new(input);
203    parse_completely(&mut items).map_err(error_handler::<D>(input, &items))
204}
205
206/// Return a function for converting `ErrorProblem` to `ParseError`
207///
208/// For use in `.map_err()`.
209//
210// Returning a closure means the usual kind of call site doesn't need to name `problem`.
211fn error_handler<D: NetdocParseable>(
212    input: &ParseInput<'_>,
213    items: &ItemStream<'_>,
214) -> impl Fn(ErrorProblem) -> ParseError {
215    |problem| ParseError {
216        problem,
217        doctype: D::doctype_for_error(),
218        file: input.file.to_owned(),
219        lno: items.lno_for_error(),
220        column: problem.column(),
221    }
222}
223
224/// Parse a network document - **toplevel entrypoint**
225pub fn parse_netdoc<D: NetdocParseable>(input: &ParseInput<'_>) -> Result<D, ParseError> {
226    parse_internal::<_, D>(input, |items| {
227        let doc = D::from_items(items, StopAt(false))?;
228        if let Some(_kw) = items.peek_keyword()? {
229            return Err(EP::MultipleDocuments);
230        }
231        Ok(doc)
232    })
233}
234
235/// Parse multiple concatenated network documents - **toplevel entrypoint**
236pub fn parse_netdoc_multiple<D: NetdocParseable>(
237    input: &ParseInput<'_>,
238) -> Result<Vec<D>, ParseError> {
239    parse_internal::<_, D>(input, |items| {
240        let mut docs = vec![];
241        while items.peek_keyword()?.is_some() {
242            let doc = D::from_items(items, StopAt(false))?;
243            docs.push(doc);
244        }
245        Ok(docs)
246    })
247}
248
249/// Error from `multi_push_doc`
250#[derive(Debug, Error)]
251#[error("out of bounds bug")]
252struct OutOfBoundsBug;
253
254/// Add `(doc, start_pos, end_pos)` to `docs`, checking bounds
255///
256/// Helper function for use by `parse_netdoc_ multiple_*` functions that return offsets.
257fn multi_push_doc<T>(
258    docs: &mut Vec<(T, usize, usize)>,
259    input: &ParseInput<'_>,
260    doc: T,
261    start_pos: usize,
262    end_pos: usize,
263) -> Result<(), OutOfBoundsBug> {
264    // Check start_pos and end_pos are in range.
265    if input.input.get(start_pos..end_pos).is_none() {
266        return Err(OutOfBoundsBug);
267    }
268
269    docs.push((doc, start_pos, end_pos));
270    Ok(())
271}
272
273/// Parse multiple network documents, also returning their offsets  - **toplevel entrypoint**
274///
275/// Each returned document is accompanied by the byte offsets of its start and end.
276///
277/// (The netdoc metaformat does not allow anything in between subsequent documents in a file,
278/// so the end of one document is the start of the next.)
279///
280/// This returns byte offsets rather than string slices,
281/// because the caller can always convert the offsets into string slices,
282/// but it is not straightforward to convert string slices borrowed from some input string
283/// into offsets, in a way that is obviously correct without nightly `str::substr_range`.
284///
285/// Interfacing code can assume that slicing the input string with the returned
286/// [`usize`] values will not cause an out-of-bounds error, meaning runtime
287/// checks are not necessary there.
288pub fn parse_netdoc_multiple_with_offsets<D: NetdocParseable>(
289    input: &ParseInput<'_>,
290) -> Result<Vec<(D, usize, usize)>, ParseError> {
291    parse_internal::<_, D>(input, |items| {
292        let mut docs = vec![];
293        while items.peek_keyword()?.is_some() {
294            let start_pos = items.byte_position();
295            let doc = D::from_items(items, StopAt(false))?;
296            let end_pos = items.byte_position();
297
298            multi_push_doc(&mut docs, input, doc, start_pos, end_pos)
299                .map_err(|OutOfBoundsBug| ErrorProblem::Internal("out-of-bounds bug?"))?;
300        }
301        Ok(docs)
302    })
303}
304
305/// Parse multiple network documents, with error recovery  - **toplevel entrypoint**
306///
307/// Parses multiple documents.  If an error is encountered, it is returned,
308/// and parsing continues with the next document (if possible).
309///
310/// Each document or error is accompanied by the applicable byte offsets in the input document,
311/// as with [`parse_netdoc_multiple_with_offsets`].
312#[allow(clippy::type_complexity)] // Yes, the return type is complicated
313pub fn parse_netdoc_multiple_sophisticated<D: NetdocParseable>(
314    input: &ParseInput<'_>,
315) -> Result<Vec<(Result<D, ParseError>, usize, usize)>, Bug> {
316    // Largely separate from parse_netdoc_multiple_with_offsets because the differences
317    // are control flow; attempts at unifying these led to very confusing code.
318
319    let mut items = ItemStream::new(input);
320    let mut docs = vec![];
321    let mut push_doc = |doc, start, end| {
322        multi_push_doc(&mut docs, input, doc, start, end)
323            .map_err(into_internal!("while parsing netdoc sequence"))
324    };
325
326    'docs: loop {
327        let start_pos = items.byte_position();
328
329        // Insisting on a KeywordRef prevents mistaken omission of code in match arms.
330        let _intro_kw: KeywordRef = match items.peek_keyword() {
331            Ok(Some(kw)) => kw,
332            Ok(None) => break 'docs,
333            Err(e) => {
334                push_doc(
335                    Err(error_handler::<D>(input, &items)(e)),
336                    start_pos,
337                    items.whole_input().len(),
338                )?;
339                // can't continue
340                break 'docs;
341            }
342        };
343
344        let doc = D::from_items(&mut items, StopAt(false)) //
345            .map_err(error_handler::<D>(input, &items));
346
347        let is_err = doc.is_err();
348        let end_pos = items.byte_position();
349        push_doc(doc, start_pos, end_pos)?;
350
351        if is_err {
352            // Skip the rest of the erroneous document until we find the next intro item.
353            //
354            // If we get lexical errors during error recovery, we don't *also* report them
355            // and instead, just stop processing the input; hence the `.unwrap_or(None)`.
356            // (And this is why we insist on `break 'docs`, rather than just break.)
357            //
358            // Insisting on KeywordRef and UnparsedItem helps prevent mistakes.
359            let _next_intro_kw: KeywordRef = 'skip: loop {
360                let _discard_item: UnparsedItem = match items.peek_keyword().unwrap_or(None) {
361                    None => break 'docs,
362                    Some(kw) if D::is_intro_item_keyword(kw) => break 'skip kw,
363                    Some(_other_kw) => match items.next().transpose().unwrap_or(None) {
364                        Some(item) => item,
365                        None => break 'docs,
366                    },
367                };
368            };
369        }
370    }
371    Ok(docs)
372}