parse_dockerfile/
error.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3use core::{fmt, marker::PhantomData, ops::Range, str};
4use std::borrow::Cow;
5
6use super::ParseIter;
7
8pub(crate) type Result<T, E = Error> = core::result::Result<T, E>;
9
10/// An error that occurred during parsing the dockerfile.
11// Boxing ErrorInner to keep error type small for performance.
12// Using PhantomData to make error type !UnwindSafe & !RefUnwindSafe for forward compatibility.
13pub struct Error(Box<ErrorInner>, PhantomData<Box<dyn Send + Sync>>);
14
15impl Error {
16    /// Returns the line number at which the error was detected.
17    #[must_use]
18    pub fn line(&self) -> usize {
19        self.0.line
20    }
21    /// Returns the column number at which the error was detected.
22    #[must_use]
23    pub fn column(&self) -> usize {
24        self.0.column
25    }
26}
27
28#[derive(Debug)]
29struct ErrorInner {
30    msg: Cow<'static, str>,
31    line: usize,
32    column: usize,
33}
34
35#[cfg_attr(test, derive(Debug))]
36pub(crate) enum ErrorKind {
37    Expected(&'static str, /* pos */ usize),
38    ExpectedOwned(String, /* pos */ usize),
39    AtLeastOneArgument { instruction_start: usize },
40    AtLeastTwoArguments { instruction_start: usize },
41    ExactlyOneArgument { instruction_start: usize },
42    UnknownInstruction { instruction_start: usize },
43    InvalidEscape { escape_start: usize },
44    DuplicateName { first: Range<usize>, second: Range<usize> },
45    NoStages,
46    Json { arguments_start: usize },
47}
48
49impl ErrorKind {
50    #[cold]
51    #[inline(never)]
52    pub(crate) fn into_error(self, p: &ParseIter<'_>) -> Error {
53        let msg = match self {
54            Self::Expected(msg, ..) => format!("expected {msg}").into(),
55            Self::ExpectedOwned(ref msg, ..) => format!("expected {msg}").into(),
56            Self::AtLeastOneArgument { instruction_start: pos }
57            | Self::AtLeastTwoArguments { instruction_start: pos }
58            | Self::ExactlyOneArgument { instruction_start: pos }
59            | Self::UnknownInstruction { instruction_start: pos }
60            | Self::DuplicateName { first: Range { start: pos, .. }, .. } => {
61                let mut s = &p.text.as_bytes()[pos..];
62                let word =
63                    super::collect_non_whitespace_unescaped(&mut s, p.text, p.escape_byte).value;
64                match self {
65                    Self::AtLeastOneArgument { .. } => {
66                        format!("{word} instruction requires at least one argument").into()
67                    }
68                    Self::AtLeastTwoArguments { .. } => {
69                        format!("{word} instruction requires at least two arguments").into()
70                    }
71                    Self::ExactlyOneArgument { .. } => {
72                        format!("{word} instruction requires exactly one argument").into()
73                    }
74                    Self::UnknownInstruction { .. } => {
75                        format!("unknown instruction '{word}'").into()
76                    }
77                    Self::DuplicateName { .. } => format!("duplicate name '{word}'").into(),
78                    _ => unreachable!(),
79                }
80            }
81            Self::NoStages => "expected at least one FROM instruction".into(),
82            Self::Json { .. } => "invalid JSON".into(),
83            Self::InvalidEscape { escape_start } => {
84                let mut s = &p.text.as_bytes()[escape_start..];
85                super::skip_non_whitespace_no_escape(&mut s);
86                let escape = &p.text[escape_start..p.text.len() - s.len()];
87                format!("invalid escape '{escape}'").into()
88            }
89        };
90        let (line, column) = match self {
91            Self::Expected(_, pos)
92            | Self::ExpectedOwned(_, pos)
93            | Self::AtLeastOneArgument { instruction_start: pos }
94            | Self::AtLeastTwoArguments { instruction_start: pos }
95            | Self::ExactlyOneArgument { instruction_start: pos }
96            | Self::UnknownInstruction { instruction_start: pos, .. }
97            | Self::InvalidEscape { escape_start: pos }
98            | Self::DuplicateName { second: Range { start: pos, .. }, .. }
99            | Self::Json { arguments_start: pos } => find_location_from_pos(pos, p.text.as_bytes()),
100            Self::NoStages => (0, 0),
101        };
102        Error(Box::new(ErrorInner { msg, line, column }), PhantomData)
103    }
104}
105
106impl fmt::Debug for Error {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        fmt::Debug::fmt(&self.0, f)
109    }
110}
111
112impl fmt::Display for Error {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        if self.0.line == 0 || f.alternate() {
115            fmt::Display::fmt(&self.0.msg, f)
116        } else {
117            write!(f, "{} at line {} column {}", self.0.msg, self.0.line, self.0.column)
118        }
119    }
120}
121
122impl std::error::Error for Error {}
123
124#[cold]
125fn find_location_from_pos(pos: usize, text: &[u8]) -> (usize, usize) {
126    let line = find_line_from_pos(pos, text);
127    let column = memrchr(b'\n', text.get(..pos).unwrap_or_default()).unwrap_or(pos) + 1;
128    (line, column)
129}
130
131#[cold]
132fn find_line_from_pos(pos: usize, text: &[u8]) -> usize {
133    bytecount(b'\n', text.get(..pos).unwrap_or_default()) + 1
134}
135
136#[inline]
137const fn memrchr_naive(needle: u8, mut s: &[u8]) -> Option<usize> {
138    let start = s;
139    while let Some((&b, s_next)) = s.split_last() {
140        if b == needle {
141            return Some(start.len() - s.len());
142        }
143        s = s_next;
144    }
145    None
146}
147use self::memrchr_naive as memrchr;
148
149#[inline]
150const fn bytecount_naive(needle: u8, mut s: &[u8]) -> usize {
151    let mut n = 0;
152    while let Some((&b, s_next)) = s.split_first() {
153        n += (b == needle) as usize;
154        s = s_next;
155    }
156    n
157}
158use self::bytecount_naive as bytecount;