1use 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
10pub struct Error(Box<ErrorInner>, PhantomData<Box<dyn Send + Sync>>);
14
15impl Error {
16 #[must_use]
18 pub fn line(&self) -> usize {
19 self.0.line
20 }
21 #[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 = super::collect_non_whitespace(&mut s, p.text, p.escape_byte).value;
143 match self {
144 Self::AtLeastOneArgument { .. } => {
145 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 Self::DuplicateName { .. } => format!("duplicate name '{word}'").into(),
161 _ => unreachable!(),
162 }
163 }
164 Self::NoStage => "expected at least one FROM instruction".into(),
165 Self::Json { .. } => "invalid JSON".into(),
166 Self::InvalidEscape { escape_start } => {
167 let mut s = &p.text.as_bytes()[escape_start..];
168 super::consume_until_whitespaces_or_line_no_line_continuation(&mut s);
169 let escape = &p.text[escape_start..p.text.len() - s.len()];
170 format!("invalid escape '{escape}'").into()
171 }
172 };
173 let (line, column) = match self {
174 Self::Other { pos, .. }
175 | Self::Expected { pos, .. }
176 | Self::ExpectedHereDocEnd { pos, .. }
177 | Self::ExpectedQuote { pos, .. }
178 | Self::AtLeastOneArgument { instruction_start: pos }
179 | Self::AtLeastTwoArguments { instruction_start: pos }
180 | Self::ExactlyOneArgument { instruction_start: pos }
181 | Self::UnknownInstruction { instruction_start: pos, .. }
182 | Self::InvalidEscape { escape_start: pos }
183 | Self::DuplicateName { second_start: pos, .. }
184 | Self::Json { arguments_start: pos } => find_location_from_pos(pos, p.text.as_bytes()),
185 Self::NoStage => (0, 0),
186 };
187 Error(Box::new(ErrorInner { msg, line, column }), PhantomData)
188 }
189}
190
191impl fmt::Debug for Error {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 fmt::Debug::fmt(&self.0, f)
194 }
195}
196
197impl fmt::Display for Error {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 if self.0.line == 0 || f.alternate() {
200 fmt::Display::fmt(&self.0.msg, f)
201 } else {
202 write!(f, "{} at line {} column {}", self.0.msg, self.0.line, self.0.column)
203 }
204 }
205}
206
207impl std::error::Error for Error {}
208
209#[cold]
210fn find_location_from_pos(pos: usize, text: &[u8]) -> (usize, usize) {
211 let line = find_line_from_pos(pos, text);
212 let column = memrchr(b'\n', text.get(..pos).unwrap_or_default()).unwrap_or(pos) + 1;
213 (line, column)
214}
215
216#[cold]
217fn find_line_from_pos(pos: usize, text: &[u8]) -> usize {
218 bytecount(b'\n', text.get(..pos).unwrap_or_default()) + 1
219}
220
221#[inline]
222const fn memrchr_naive(needle: u8, mut s: &[u8]) -> Option<usize> {
223 let start = s;
224 while let Some((&b, s_next)) = s.split_last() {
225 if b == needle {
226 return Some(start.len() - s.len());
227 }
228 s = s_next;
229 }
230 None
231}
232use self::memrchr_naive as memrchr;
233
234#[inline]
235const fn bytecount_naive(needle: u8, mut s: &[u8]) -> usize {
236 let mut n = 0;
237 while let Some((&b, s_next)) = s.split_first() {
238 n += (b == needle) as usize;
239 s = s_next;
240 }
241 n
242}
243use self::bytecount_naive as bytecount;