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 =
143 super::collect_non_whitespace_unescaped(&mut s, p.text, p.escape_byte).value;
144 match self {
145 Self::AtLeastOneArgument { .. } => {
146 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;