1use 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
11pub struct Error(Box<ErrorInner>, PhantomData<Box<dyn Send + Sync>>);
15
16impl Error {
17 #[must_use]
19 pub fn line(&self) -> usize {
20 self.0.line
21 }
22 #[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()) )
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 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}