Skip to main content

parse_dockerfile/
error.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3use alloc::{boxed::Box, format};
4use core::{fmt, marker::PhantomData, str};
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#[cold]
29#[inline]
30pub(crate) fn other(msg: &'static str, pos: usize) -> ErrorKind {
31    ErrorKind::Other { msg, pos }
32}
33#[cold]
34#[inline]
35pub(crate) fn expected(word: &'static str, pos: usize) -> ErrorKind {
36    ErrorKind::Expected { word, pos }
37}
38#[cold]
39#[inline]
40pub(crate) fn expected_here_doc_end(delim: &[u8], pos: usize) -> ErrorKind {
41    ErrorKind::ExpectedHereDocEnd { delim: delim.into(), pos }
42}
43#[cold]
44#[inline]
45pub(crate) fn expected_quote(quote: u8, found: Option<u8>, pos: usize) -> ErrorKind {
46    ErrorKind::ExpectedQuote { quote, found, pos }
47}
48#[cold]
49#[inline]
50pub(crate) fn at_least_one_argument(instruction_start: usize) -> ErrorKind {
51    ErrorKind::AtLeastOneArgument { instruction_start }
52}
53#[cold]
54#[inline]
55pub(crate) fn at_least_two_arguments(instruction_start: usize) -> ErrorKind {
56    ErrorKind::AtLeastTwoArguments { instruction_start }
57}
58#[cold]
59#[inline]
60pub(crate) fn exactly_one_argument(instruction_start: usize) -> ErrorKind {
61    ErrorKind::ExactlyOneArgument { instruction_start }
62}
63#[cold]
64#[inline]
65pub(crate) fn unknown_instruction(instruction_start: usize) -> ErrorKind {
66    ErrorKind::UnknownInstruction { instruction_start }
67}
68#[cold]
69#[inline]
70pub(crate) fn invalid_escape(escape_start: usize) -> ErrorKind {
71    ErrorKind::InvalidEscape { escape_start }
72}
73#[cold]
74#[inline]
75pub(crate) fn duplicate_name(first_start: usize, second_start: usize) -> ErrorKind {
76    ErrorKind::DuplicateName { first_start, second_start }
77}
78#[cold]
79#[inline]
80pub(crate) fn no_stage() -> ErrorKind {
81    ErrorKind::NoStage
82}
83#[cold]
84#[inline]
85pub(crate) fn json(arguments_start: usize) -> ErrorKind {
86    ErrorKind::Json { arguments_start }
87}
88
89#[derive(Debug)]
90struct ErrorInner {
91    msg: Box<str>,
92    line: usize,
93    column: usize,
94}
95
96#[cfg_attr(test, derive(Debug))]
97pub(crate) enum ErrorKind {
98    Other { msg: &'static str, pos: usize },
99    Expected { word: &'static str, pos: usize },
100    ExpectedHereDocEnd { delim: Box<[u8]>, pos: usize },
101    ExpectedQuote { quote: u8, found: Option<u8>, pos: usize },
102    AtLeastOneArgument { instruction_start: usize },
103    AtLeastTwoArguments { instruction_start: usize },
104    ExactlyOneArgument { instruction_start: usize },
105    UnknownInstruction { instruction_start: usize },
106    InvalidEscape { escape_start: usize },
107    DuplicateName { first_start: usize, second_start: usize },
108    NoStage,
109    Json { arguments_start: usize },
110}
111
112impl ErrorKind {
113    #[cold]
114    #[inline(never)]
115    pub(crate) fn into_error(self, p: &ParseIter<'_>) -> Error {
116        let msg = match self {
117            Self::Other { msg, .. } => msg.into(),
118            Self::Expected { word, .. } => format!("expected {word}").into(),
119            Self::ExpectedHereDocEnd { ref delim, .. } => format!(
120                "expected end of here-document ({}), but reached eof",
121                str::from_utf8(delim).unwrap()
122            )
123            .into(),
124            Self::ExpectedQuote { quote, found, .. } => {
125                if let Some(found) = found {
126                    format!(
127                        "expected end of quoted string ({}), but found '{}'",
128                        quote as char, found as char
129                    )
130                    .into()
131                } else {
132                    format!("expected end of quoted string ({}), but reached eof", quote as char)
133                        .into()
134                }
135            }
136            Self::AtLeastOneArgument { instruction_start: pos }
137            | Self::AtLeastTwoArguments { instruction_start: pos }
138            | Self::ExactlyOneArgument { instruction_start: pos }
139            | Self::UnknownInstruction { instruction_start: pos }
140            | Self::DuplicateName { first_start: pos, .. } => {
141                let mut s = &p.text.as_bytes()[pos..];
142                let mut word =
143                    super::collect_non_whitespace_unescaped(&mut s, p.text, p.escape_byte).value;
144                match self {
145                    Self::AtLeastOneArgument { .. } => {
146                        // TODO: handle in collect_non_whitespace_unescaped
147                        if word == "HEALTHCHECK" {
148                            word = "HEALTHCHECK CMD".into();
149                        }
150                        format!("{word} instruction requires at least one argument").into()
151                    }
152                    Self::AtLeastTwoArguments { .. } => {
153                        format!("{word} instruction requires at least two arguments").into()
154                    }
155                    Self::ExactlyOneArgument { .. } => {
156                        format!("{word} instruction requires exactly one argument").into()
157                    }
158                    Self::UnknownInstruction { .. } => {
159                        format!("unknown instruction '{word}'").into()
160                    }
161                    Self::DuplicateName { .. } => format!("duplicate name '{word}'").into(),
162                    _ => unreachable!(),
163                }
164            }
165            Self::NoStage => "expected at least one FROM instruction".into(),
166            Self::Json { .. } => "invalid JSON".into(),
167            Self::InvalidEscape { escape_start } => {
168                let mut s = &p.text.as_bytes()[escape_start..];
169                super::skip_non_whitespace_no_escape(&mut s);
170                let escape = &p.text[escape_start..p.text.len() - s.len()];
171                format!("invalid escape '{escape}'").into()
172            }
173        };
174        let (line, column) = match self {
175            Self::Other { pos, .. }
176            | Self::Expected { pos, .. }
177            | Self::ExpectedHereDocEnd { pos, .. }
178            | Self::ExpectedQuote { pos, .. }
179            | Self::AtLeastOneArgument { instruction_start: pos }
180            | Self::AtLeastTwoArguments { instruction_start: pos }
181            | Self::ExactlyOneArgument { instruction_start: pos }
182            | Self::UnknownInstruction { instruction_start: pos, .. }
183            | Self::InvalidEscape { escape_start: pos }
184            | Self::DuplicateName { second_start: pos, .. }
185            | Self::Json { arguments_start: pos } => find_location_from_pos(pos, p.text.as_bytes()),
186            Self::NoStage => (0, 0),
187        };
188        Error(Box::new(ErrorInner { msg, line, column }), PhantomData)
189    }
190}
191
192impl fmt::Debug for Error {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        fmt::Debug::fmt(&self.0, f)
195    }
196}
197
198impl fmt::Display for Error {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        if self.0.line == 0 || f.alternate() {
201            fmt::Display::fmt(&self.0.msg, f)
202        } else {
203            write!(f, "{} at line {} column {}", self.0.msg, self.0.line, self.0.column)
204        }
205    }
206}
207
208impl std::error::Error for Error {}
209
210#[cold]
211fn find_location_from_pos(pos: usize, text: &[u8]) -> (usize, usize) {
212    let line = find_line_from_pos(pos, text);
213    let column = memrchr(b'\n', text.get(..pos).unwrap_or_default()).unwrap_or(pos) + 1;
214    (line, column)
215}
216
217#[cold]
218fn find_line_from_pos(pos: usize, text: &[u8]) -> usize {
219    bytecount(b'\n', text.get(..pos).unwrap_or_default()) + 1
220}
221
222#[inline]
223const fn memrchr_naive(needle: u8, mut s: &[u8]) -> Option<usize> {
224    let start = s;
225    while let Some((&b, s_next)) = s.split_last() {
226        if b == needle {
227            return Some(start.len() - s.len());
228        }
229        s = s_next;
230    }
231    None
232}
233use self::memrchr_naive as memrchr;
234
235#[inline]
236const fn bytecount_naive(needle: u8, mut s: &[u8]) -> usize {
237    let mut n = 0;
238    while let Some((&b, s_next)) = s.split_first() {
239        n += (b == needle) as usize;
240        s = s_next;
241    }
242    n
243}
244use self::bytecount_naive as bytecount;