tor_netdoc/parse2/lines.rs
1//! Version of `std::str::Lines` that tracks line numbers and has `remainder()`
2
3use extend::ext;
4
5/// Version of `std::str::Lines` that tracks line numbers and has `remainder()`
6///
7/// Implements `Iterator`, returning one `str` for each line, with the `'\n'` removed.
8///
9/// Missing final newline is silently tolerated.
10#[derive(Debug, Clone)]
11pub struct Lines<'s> {
12 /// Line number at the start of `rest`
13 lno: usize,
14 /// The remaining part of the document
15 rest: &'s str,
16}
17
18/// Extension trait adding a method to `str`
19#[ext(name = StrExt)]
20pub impl str {
21 /// Remove `count` bytes from the end of `self`
22 ///
23 /// # Panics
24 ///
25 /// Panics if `count > self.len()`.
26 #[allow(clippy::string_slice)] // TODO
27 fn strip_end_counted(&self, count: usize) -> &str {
28 &self[0..self.len().checked_sub(count).expect("stripping too much")]
29 }
30}
31
32/// Information about the next line we have peeked
33///
34/// To get the line as an actual string, pass this to `peeked_line`.
35///
36/// # Correctness
37///
38/// Each `Peeked` is only valid in conjunction with the `Lines` that returned it,
39/// and becomes invalidated if the `Lines` is modified
40/// (ie, it can be invalidated by calls that take `&mut Lines`).
41///
42/// Cloning a `Peeked` is hazrdous since using it twice would be wrong.
43///
44/// None of this is checked at compile- or run-time.
45// We could perhaps use lifetimes somehow to enforce this,
46// but `ItemStream` wants `Peeked` to be `'static` and `Clone`.
47#[derive(Debug, Clone, amplify::Getters)]
48pub struct Peeked {
49 /// The length of the next line
50 //
51 // # Invariant
52 //
53 // `rest[line_len]` is a newline, or `line_len` is `rest.len()`.
54 #[getter(as_copy)]
55 line_len: usize,
56}
57
58impl<'s> Lines<'s> {
59 /// Start reading lines from a document as a string
60 pub fn new(s: &'s str) -> Self {
61 Lines { lno: 1, rest: s }
62 }
63
64 /// Line number of the next line we'll read
65 pub fn peek_lno(&self) -> usize {
66 self.lno
67 }
68
69 /// Peek the next line
70 pub fn peek(&self) -> Option<Peeked> {
71 if self.rest.is_empty() {
72 None
73 } else if let Some(newline) = self.rest.find('\n') {
74 Some(Peeked { line_len: newline })
75 } else {
76 Some(Peeked {
77 line_len: self.rest.len(),
78 })
79 }
80 }
81
82 /// The rest of the file as a `str`
83 pub fn remaining(&self) -> &'s str {
84 self.rest
85 }
86
87 /// After `peek`, advance to the next line, consuming the one that was peeked
88 ///
89 /// # Correctness
90 ///
91 /// See [`Peeked`].
92 #[allow(clippy::needless_pass_by_value)] // Yes, we want to consume Peeked
93 #[allow(clippy::string_slice)] // TODO
94 pub fn consume_peeked(&mut self, peeked: Peeked) -> &'s str {
95 let line = self.peeked_line(&peeked);
96 self.rest = &self.rest[peeked.line_len..];
97 if !self.rest.is_empty() {
98 debug_assert!(self.rest.starts_with('\n'));
99 self.rest = &self.rest[1..];
100 }
101 self.lno += 1;
102 line
103 }
104
105 /// After `peek`, obtain the actual peeked line as a `str`
106 ///
107 /// As with [`<Lines as Iterator>::next`](Lines::next), does not include the newline.
108 // Rustdoc doesn't support linking` fully qualified syntax.
109 // https://github.com/rust-lang/rust/issues/74563
110 ///
111 /// # Correctness
112 ///
113 /// See [`Peeked`].
114 #[allow(clippy::string_slice)] // TODO
115 pub fn peeked_line(&self, peeked: &Peeked) -> &'s str {
116 &self.rest[0..peeked.line_len()]
117 }
118}
119
120impl<'s> Iterator for Lines<'s> {
121 type Item = &'s str;
122
123 fn next(&mut self) -> Option<&'s str> {
124 let peeked = self.peek()?;
125 let line = self.consume_peeked(peeked);
126 Some(line)
127 }
128}