Skip to main content

parse_dockerfile/
error.rs

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