Skip to main content

miette/
source_impls.rs

1/*!
2Default trait implementations for [`SourceCode`].
3*/
4#[cfg(test)]
5use std::str::from_utf8;
6use std::{borrow::Cow, collections::VecDeque, fmt::Debug, sync::Arc};
7
8use crate::{MietteError, MietteSpanContents, SourceCode, SourceSpan};
9
10fn context_info<'a>(
11    input: &'a [u8],
12    span: &SourceSpan,
13    context_lines_before: usize,
14    context_lines_after: usize,
15) -> Result<MietteSpanContents<'a>, MietteError> {
16    let span_offset = span.offset() as usize;
17    let span_len = span.len() as usize;
18    let mut line_count = 0usize;
19    let mut start_line = 0usize;
20    let mut before_lines_starts = VecDeque::new();
21    let mut current_line_start = 0usize;
22    let mut end_lines = 0usize;
23    let mut post_span = false;
24    let mut post_span_got_newline = false;
25
26    // The byte-by-byte loop below only needs to run from just before the
27    // span to the end of the trailing context: bytes strictly before
28    // `span_offset - 1` can only exercise its "before the span" branches,
29    // so that region is scanned in bulk with memchr instead. `cut` is
30    // adjusted so a CRLF pair is never split across the boundary.
31    let mut cut = span_offset.saturating_sub(1).min(input.len());
32    if cut > 0 && input[cut - 1] == b'\r' {
33        cut -= 1;
34    }
35    for pos in memchr::memchr2_iter(b'\r', b'\n', &input[..cut]) {
36        // Skip the `\n` of a CRLF pair already counted at its `\r`.
37        if input[pos] == b'\n' && pos > 0 && input[pos - 1] == b'\r' {
38            continue;
39        }
40        // A CRLF pair counts as a single line break, ending at the `\n`.
41        let line_end = if input[pos] == b'\r' && pos + 1 < cut && input[pos + 1] == b'\n' {
42            pos + 1
43        } else {
44            pos
45        };
46        line_count += 1;
47        before_lines_starts.push_back(current_line_start);
48        if before_lines_starts.len() > context_lines_before {
49            start_line += 1;
50            before_lines_starts.pop_front();
51        }
52        current_line_start = line_end + 1;
53    }
54    // `current_line_start..cut` contains no line breaks.
55    let mut start_column = cut - current_line_start;
56    let mut offset = cut;
57    let mut iter = input[cut..].iter().copied().peekable();
58    while let Some(char) = iter.next() {
59        if matches!(char, b'\r' | b'\n') {
60            line_count += 1;
61            if char == b'\r' && iter.next_if_eq(&b'\n').is_some() {
62                offset += 1;
63            }
64            if offset < span_offset {
65                // We're before the start of the span.
66                start_column = 0;
67                before_lines_starts.push_back(current_line_start);
68                if before_lines_starts.len() > context_lines_before {
69                    start_line += 1;
70                    before_lines_starts.pop_front();
71                }
72            } else if offset >= span_offset + span_len.saturating_sub(1) {
73                // We're after the end of the span, but haven't necessarily
74                // started collecting end lines yet (we might still be
75                // collecting context lines).
76                if post_span {
77                    start_column = 0;
78                    if post_span_got_newline {
79                        end_lines += 1;
80                    } else {
81                        post_span_got_newline = true;
82                    }
83                    if end_lines >= context_lines_after {
84                        offset += 1;
85                        break;
86                    }
87                }
88            }
89            current_line_start = offset + 1;
90        } else if offset < span_offset {
91            start_column += 1;
92        }
93
94        if offset >= (span_offset + span_len).saturating_sub(1) {
95            post_span = true;
96            if end_lines >= context_lines_after {
97                offset += 1;
98                break;
99            }
100        }
101
102        offset += 1;
103    }
104
105    if offset >= (span_offset + span_len).saturating_sub(1) {
106        let starting_offset = before_lines_starts
107            .front()
108            .copied()
109            .unwrap_or(if context_lines_before == 0 { span_offset } else { 0 });
110        // A zero-length span starting just past the end of the input passes
111        // the check above but has no content to slice.
112        if starting_offset > offset {
113            return Err(MietteError::OutOfBounds);
114        }
115        Ok(MietteSpanContents::new(
116            &input[starting_offset..offset],
117            (starting_offset as u32, (offset - starting_offset) as u32).into(),
118            start_line,
119            if context_lines_before == 0 { start_column } else { 0 },
120            line_count,
121        ))
122    } else {
123        Err(MietteError::OutOfBounds)
124    }
125}
126
127impl SourceCode for [u8] {
128    fn read_span<'a>(
129        &'a self,
130        span: &SourceSpan,
131        context_lines_before: usize,
132        context_lines_after: usize,
133    ) -> Result<MietteSpanContents<'a>, MietteError> {
134        let contents = context_info(self, span, context_lines_before, context_lines_after)?;
135        Ok(contents)
136    }
137}
138
139impl SourceCode for &[u8] {
140    fn read_span<'a>(
141        &'a self,
142        span: &SourceSpan,
143        context_lines_before: usize,
144        context_lines_after: usize,
145    ) -> Result<MietteSpanContents<'a>, MietteError> {
146        <[u8] as SourceCode>::read_span(self, span, context_lines_before, context_lines_after)
147    }
148}
149
150impl SourceCode for Vec<u8> {
151    fn read_span<'a>(
152        &'a self,
153        span: &SourceSpan,
154        context_lines_before: usize,
155        context_lines_after: usize,
156    ) -> Result<MietteSpanContents<'a>, MietteError> {
157        <[u8] as SourceCode>::read_span(self, span, context_lines_before, context_lines_after)
158    }
159}
160
161impl SourceCode for str {
162    fn read_span<'a>(
163        &'a self,
164        span: &SourceSpan,
165        context_lines_before: usize,
166        context_lines_after: usize,
167    ) -> Result<MietteSpanContents<'a>, MietteError> {
168        <[u8] as SourceCode>::read_span(
169            self.as_bytes(),
170            span,
171            context_lines_before,
172            context_lines_after,
173        )
174    }
175}
176
177/// Makes `src: &'static str` or `struct S<'a> { src: &'a str }` usable.
178impl SourceCode for &str {
179    fn read_span<'a>(
180        &'a self,
181        span: &SourceSpan,
182        context_lines_before: usize,
183        context_lines_after: usize,
184    ) -> Result<MietteSpanContents<'a>, MietteError> {
185        <str as SourceCode>::read_span(self, span, context_lines_before, context_lines_after)
186    }
187}
188
189impl SourceCode for String {
190    fn read_span<'a>(
191        &'a self,
192        span: &SourceSpan,
193        context_lines_before: usize,
194        context_lines_after: usize,
195    ) -> Result<MietteSpanContents<'a>, MietteError> {
196        <str as SourceCode>::read_span(self, span, context_lines_before, context_lines_after)
197    }
198}
199
200impl<T: ?Sized + SourceCode> SourceCode for Arc<T> {
201    fn read_span<'a>(
202        &'a self,
203        span: &SourceSpan,
204        context_lines_before: usize,
205        context_lines_after: usize,
206    ) -> Result<MietteSpanContents<'a>, MietteError> {
207        self.as_ref().read_span(span, context_lines_before, context_lines_after)
208    }
209
210    fn name(&self) -> Option<&str> {
211        self.as_ref().name()
212    }
213}
214
215impl<T: ?Sized + SourceCode + ToOwned> SourceCode for Cow<'_, T>
216where
217    // The minimal bounds are used here.
218    // `T::Owned` need not be
219    // `SourceCode`, because `&T`
220    // can always be obtained from
221    // `Cow<'_, T>`.
222    T::Owned: Debug + Send + Sync,
223{
224    fn read_span<'a>(
225        &'a self,
226        span: &SourceSpan,
227        context_lines_before: usize,
228        context_lines_after: usize,
229    ) -> Result<MietteSpanContents<'a>, MietteError> {
230        self.as_ref().read_span(span, context_lines_before, context_lines_after)
231    }
232
233    fn name(&self) -> Option<&str> {
234        self.as_ref().name()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::SpanContents;
242
243    #[test]
244    fn basic() -> Result<(), MietteError> {
245        let src = String::from("foo\n");
246        let contents = src.read_span(&(0, 4).into(), 0, 0)?;
247        assert_eq!("foo\n", from_utf8(contents.data()).unwrap());
248        assert_eq!(0, contents.line());
249        assert_eq!(0, contents.column());
250        Ok(())
251    }
252
253    #[test]
254    fn shifted() -> Result<(), MietteError> {
255        let src = String::from("foobar");
256        let contents = src.read_span(&(3, 3).into(), 1, 1)?;
257        assert_eq!("foobar", from_utf8(contents.data()).unwrap());
258        assert_eq!(0, contents.line());
259        assert_eq!(0, contents.column());
260        Ok(())
261    }
262
263    #[test]
264    fn middle() -> Result<(), MietteError> {
265        let src = String::from("foo\nbar\nbaz\n");
266        let contents = src.read_span(&(4, 4).into(), 0, 0)?;
267        assert_eq!("bar\n", from_utf8(contents.data()).unwrap());
268        assert_eq!(1, contents.line());
269        assert_eq!(0, contents.column());
270        Ok(())
271    }
272
273    #[test]
274    fn middle_of_line() -> Result<(), MietteError> {
275        let src = String::from("foo\nbarbar\nbaz\n");
276        let contents = src.read_span(&(7, 4).into(), 0, 0)?;
277        assert_eq!("bar\n", from_utf8(contents.data()).unwrap());
278        assert_eq!(1, contents.line());
279        assert_eq!(3, contents.column());
280        Ok(())
281    }
282
283    #[test]
284    fn with_crlf() -> Result<(), MietteError> {
285        let src = String::from("foo\r\nbar\r\nbaz\r\n");
286        let contents = src.read_span(&(5, 5).into(), 0, 0)?;
287        assert_eq!("bar\r\n", from_utf8(contents.data()).unwrap());
288        assert_eq!(1, contents.line());
289        assert_eq!(0, contents.column());
290        Ok(())
291    }
292
293    #[test]
294    fn with_context() -> Result<(), MietteError> {
295        let src = String::from("xxx\nfoo\nbar\nbaz\n\nyyy\n");
296        let contents = src.read_span(&(8, 3).into(), 1, 1)?;
297        assert_eq!("foo\nbar\nbaz\n", from_utf8(contents.data()).unwrap());
298        assert_eq!(1, contents.line());
299        assert_eq!(0, contents.column());
300        Ok(())
301    }
302
303    #[test]
304    fn multiline_with_context() -> Result<(), MietteError> {
305        let src = String::from("aaa\nxxx\n\nfoo\nbar\nbaz\n\nyyy\nbbb\n");
306        let contents = src.read_span(&(9, 11).into(), 1, 1)?;
307        assert_eq!("\nfoo\nbar\nbaz\n\n", from_utf8(contents.data()).unwrap());
308        assert_eq!(2, contents.line());
309        assert_eq!(0, contents.column());
310        let span: SourceSpan = (8, 14).into();
311        assert_eq!(&span, contents.span());
312        Ok(())
313    }
314
315    #[test]
316    fn zero_length_span_just_past_eof() {
317        // Used to panic with a slice out-of-range instead of returning an
318        // error (found by differential fuzzing).
319        let src = String::from("a");
320        assert!(matches!(src.read_span(&(2, 0).into(), 0, 0), Err(MietteError::OutOfBounds)));
321        let src = String::new();
322        assert!(matches!(src.read_span(&(1, 0).into(), 0, 0), Err(MietteError::OutOfBounds)));
323    }
324
325    #[test]
326    fn multiline_with_context_line_start() -> Result<(), MietteError> {
327        let src = String::from("one\ntwo\n\nthree\nfour\nfive\n\nsix\nseven\n");
328        let contents = src.read_span(&(2, 0).into(), 2, 2)?;
329        assert_eq!("one\ntwo\n\n", from_utf8(contents.data()).unwrap());
330        assert_eq!(0, contents.line());
331        assert_eq!(0, contents.column());
332        let span: SourceSpan = (0, 9).into();
333        assert_eq!(&span, contents.span());
334        Ok(())
335    }
336}