Skip to main content

parse_dockerfile/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3/*!
4Dockerfile parser, written in Rust.
5
6### Usage
7
8<!-- Note: Document from sync-markdown-to-rustdoc:start through sync-markdown-to-rustdoc:end
9     is synchronized from README.md. Any changes to that range are not preserved. -->
10<!-- tidy:sync-markdown-to-rustdoc:start -->
11
12To use this crate as a library, add this to your `Cargo.toml`:
13
14```toml
15[dependencies]
16parse-dockerfile = { version = "0.1", default-features = false }
17```
18
19<div class="rustdoc-alert rustdoc-alert-note">
20
21> **ⓘ Note**
22>
23> We recommend disabling default features because they enable CLI-related
24> dependencies which the library part does not use.
25
26</div>
27
28<!-- omit in toc -->
29### Examples
30
31```
32use parse_dockerfile::{parse, Instruction};
33
34let text = "
35ARG UBUNTU_VERSION=latest
36
37FROM ubuntu:${UBUNTU_VERSION}
38RUN echo
39";
40
41let dockerfile = parse(text).unwrap();
42
43// Iterate over all instructions.
44let mut instructions = dockerfile.instructions.iter();
45assert!(matches!(instructions.next(), Some(Instruction::Arg(..))));
46assert!(matches!(instructions.next(), Some(Instruction::From(..))));
47assert!(matches!(instructions.next(), Some(Instruction::Run(..))));
48assert!(instructions.next().is_none());
49
50// Iterate over global args.
51let mut global_args = dockerfile.global_args();
52let global_arg1 = global_args.next().unwrap();
53assert_eq!(global_arg1.arguments.value, "UBUNTU_VERSION=latest");
54assert!(global_args.next().is_none());
55
56// Iterate over stages.
57let mut stages = dockerfile.stages();
58let stage1 = stages.next().unwrap();
59assert_eq!(stage1.from.image.value, "ubuntu:${UBUNTU_VERSION}");
60let mut stage1_instructions = stage1.instructions.iter();
61assert!(matches!(stage1_instructions.next(), Some(Instruction::Run(..))));
62assert!(stage1_instructions.next().is_none());
63assert!(stages.next().is_none());
64```
65
66<!-- omit in toc -->
67### Optional features
68
69- **`serde`** - Implements [`serde::Serialize`] trait for parse-dockerfile types.
70
71[`serde::Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
72
73<!-- tidy:sync-markdown-to-rustdoc:end -->
74*/
75
76#![no_std]
77#![doc(test(
78    no_crate_inject,
79    attr(allow(
80        dead_code,
81        unused_variables,
82        clippy::undocumented_unsafe_blocks,
83        clippy::unused_trait_names,
84    ))
85))]
86#![forbid(unsafe_code)]
87#![warn(
88    // Lints that may help when writing public library.
89    missing_debug_implementations,
90    missing_docs,
91    clippy::alloc_instead_of_core,
92    clippy::exhaustive_enums,
93    clippy::exhaustive_structs,
94    clippy::impl_trait_in_params,
95    clippy::std_instead_of_alloc,
96    clippy::std_instead_of_core,
97    // clippy::missing_inline_in_public_items,
98)]
99#![allow(clippy::inline_always)]
100
101extern crate alloc;
102extern crate std;
103
104#[cfg(test)]
105#[path = "gen/tests/assert_impl.rs"]
106mod assert_impl;
107#[cfg(test)]
108#[path = "gen/tests/track_size.rs"]
109mod track_size;
110
111mod error;
112
113use alloc::{borrow::Cow, boxed::Box, string::String, vec, vec::Vec};
114use core::{mem, ops::Range, str};
115use std::collections::HashMap;
116
117use smallvec::SmallVec;
118
119pub use self::error::Error;
120use self::error::{ErrorKind, InternalResult, Result};
121
122/// Parses dockerfile from the given `text`.
123#[allow(clippy::missing_panics_doc)]
124pub fn parse(text: &str) -> Result<Dockerfile<'_>> {
125    #[cold]
126    fn error(
127        p: &ParseIter<'_>,
128        e: ErrorKind<'_>,
129        instructions: &mut Vec<Instruction<'_>>,
130        stages: &mut Vec<Range<usize>>,
131    ) -> Error {
132        // reduce memory usage before allocation in into_error
133        *instructions = vec![];
134        *stages = vec![];
135        e.into_error(p)
136    }
137
138    let mut p = ParseIter::new(text)?;
139    let mut s = p.s;
140
141    let mut instructions = Vec::with_capacity((p.text.len() / 60).min(1024));
142    let mut stages = Vec::with_capacity(1);
143    let mut named_stages = 0;
144    let mut current_stage = None;
145    while let Some((&b, s_next)) = s.split_first() {
146        let instruction = parse_instruction(&mut p, &mut s, b, s_next)
147            .map_err(|e| error(&p, e, &mut instructions, &mut stages))?;
148        match instruction {
149            Instruction::From(from) => {
150                named_stages += from.as_.is_some() as usize;
151                let new_stage = instructions.len();
152                if let Some(prev_stage) = current_stage.replace(new_stage) {
153                    stages.push(prev_stage..new_stage);
154                }
155                instructions.push(Instruction::From(from));
156            }
157            arg @ Instruction::Arg(..) => instructions.push(arg),
158            instruction => {
159                if current_stage.is_none() {
160                    return Err(error(
161                        &p,
162                        error::expected("FROM", instruction.instruction_span().start),
163                        &mut instructions,
164                        &mut stages,
165                    ));
166                }
167                instructions.push(instruction);
168            }
169        }
170        consume_comments_and_whitespaces(&mut s, p.escape_byte);
171    }
172    if let Some(current_stage) = current_stage {
173        stages.push(current_stage..instructions.len());
174    }
175
176    if stages.is_empty() {
177        // https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/dockerfile2llb/convert.go#L278
178        return Err(error(&p, error::no_stage(), &mut instructions, &mut stages));
179    }
180    // TODO: https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/dockerfile2llb/convert.go#L413
181    // > base name (%s) should not be blank
182
183    let mut stages_by_name = HashMap::<Cow<'_, str>, usize>::with_capacity(named_stages);
184    for (i, stage) in stages.iter().enumerate() {
185        let Instruction::From(from) = &instructions[stage.start] else { unreachable!() };
186        if let Some((_as, name)) = &from.as_ {
187            if let Some(&first_occurrence) = stages_by_name.get(&name.value) {
188                drop(stages_by_name);
189                let second_start = name.span.start;
190                let Instruction::From(from) = &mut instructions[stages[first_occurrence].start]
191                else {
192                    unreachable!()
193                };
194                let name = mem::take(&mut from.as_.as_mut().unwrap().1.value);
195                return Err(error(
196                    &p,
197                    error::duplicate_name(name, second_start),
198                    &mut instructions,
199                    &mut stages,
200                ));
201            }
202            stages_by_name.insert(name.value.clone(), i);
203        }
204    }
205
206    Ok(Dockerfile { parser_directives: p.parser_directives, instructions, stages, stages_by_name })
207}
208
209/// Returns an iterator over instructions in the given `text`.
210///
211/// Unlike [`parse`] function, the returned iterator doesn't error on
212/// duplicate stage names.
213///
214/// # Errors
215///
216/// When `.next()` on the returned iterator has returned an `Some(Err(..))`
217/// once, the subsequent `.next()` calls return `None`.
218pub fn parse_iter(text: &str) -> Result<ParseIter<'_>> {
219    ParseIter::new(text)
220}
221
222/// A dockerfile.
223#[derive(Debug)]
224#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
225#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
226pub struct Dockerfile<'a> {
227    /// Parser directives.
228    pub parser_directives: ParserDirectives<'a>,
229    /// Instructions.
230    pub instructions: Vec<Instruction<'a>>,
231    #[cfg_attr(feature = "serde", serde(skip))]
232    stages: Vec<Range<usize>>,
233    #[cfg_attr(feature = "serde", serde(skip))]
234    stages_by_name: HashMap<Cow<'a, str>, usize>,
235}
236impl<'a> Dockerfile<'a> {
237    /// Returns an iterator over global args.
238    #[allow(clippy::missing_panics_doc)] // self.stages is not empty
239    #[must_use]
240    pub fn global_args<'b>(&'b self) -> impl ExactSizeIterator<Item = &'b ArgInstruction<'a>> {
241        self.instructions[..self.stages.first().unwrap().start].iter().map(|arg| {
242            let Instruction::Arg(arg) = arg else { unreachable!() };
243            arg
244        })
245    }
246    /// Gets a stage by name.
247    #[must_use]
248    pub fn stage<'b>(&'b self, name: &str) -> Option<Stage<'a, 'b>> {
249        let i = *self.stages_by_name.get(name)?;
250        let stage = &self.stages[i];
251        let Instruction::From(from) = &self.instructions[stage.start] else { unreachable!() };
252        Some(Stage { from, instructions: &self.instructions[stage.start + 1..stage.end] })
253    }
254    /// Returns an iterator over stages.
255    #[must_use]
256    pub fn stages<'b>(&'b self) -> impl ExactSizeIterator<Item = Stage<'a, 'b>> {
257        self.stages.iter().map(move |stage| {
258            let Instruction::From(from) = &self.instructions[stage.start] else { unreachable!() };
259            Stage { from, instructions: &self.instructions[stage.start + 1..stage.end] }
260        })
261    }
262}
263/// A stage.
264#[derive(Debug)]
265#[non_exhaustive]
266pub struct Stage<'a, 'b> {
267    /// The `FROM` instruction.
268    pub from: &'b FromInstruction<'a>,
269    /// The remaining instructions.
270    pub instructions: &'b [Instruction<'a>],
271}
272
273/// Parser directives.
274///
275/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#parser-directives)
276#[derive(Debug)]
277#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
278#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
279#[non_exhaustive]
280pub struct ParserDirectives<'a> {
281    /// `syntax` parser directive.
282    ///
283    /// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#syntax)
284    pub syntax: Option<ParserDirective<&'a str>>,
285    /// `escape` parser directive.
286    ///
287    /// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#escape)
288    pub escape: Option<ParserDirective<char>>,
289    /// `check` parser directive.
290    ///
291    /// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#check)
292    pub check: Option<ParserDirective<&'a str>>,
293}
294/// A parser directive.
295#[derive(Debug)]
296#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
297#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
298pub struct ParserDirective<T> {
299    /// ```text
300    /// syntax=value
301    /// ^
302    /// ```
303    start: usize,
304    /// ```text
305    /// syntax=value
306    ///        ^^^^^
307    /// ```
308    pub value: Spanned<T>,
309}
310impl<T> ParserDirective<T> {
311    /// ```text
312    /// syntax=value
313    /// ^^^^^^^^^^^^
314    /// ```
315    #[must_use]
316    pub fn span(&self) -> Span {
317        self.start..self.value.span.end
318    }
319}
320
321/// An instruction.
322#[derive(Debug)]
323#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
324#[cfg_attr(feature = "serde", serde(tag = "kind"))]
325#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
326#[non_exhaustive]
327// NB: When adding new variants, update ALL_INST in tests/test.rs.
328pub enum Instruction<'a> {
329    /// `ADD` instruction.
330    Add(AddInstruction<'a>),
331    /// `ARG` instruction.
332    Arg(ArgInstruction<'a>),
333    /// `CMD` instruction.
334    Cmd(CmdInstruction<'a>),
335    /// `COPY` instruction.
336    Copy(CopyInstruction<'a>),
337    /// `ENTRYPOINT` instruction.
338    Entrypoint(EntrypointInstruction<'a>),
339    /// `ENV` instruction.
340    Env(EnvInstruction<'a>),
341    /// `EXPOSE` instruction.
342    Expose(ExposeInstruction<'a>),
343    /// `FROM` instruction.
344    From(FromInstruction<'a>),
345    /// `HEALTHCHECK` instruction.
346    Healthcheck(HealthcheckInstruction<'a>),
347    /// `LABEL` instruction.
348    Label(LabelInstruction<'a>),
349    /// `MAINTAINER` instruction (deprecated).
350    Maintainer(MaintainerInstruction<'a>),
351    /// `ONBUILD` instruction.
352    Onbuild(OnbuildInstruction<'a>),
353    /// `RUN` instruction.
354    Run(RunInstruction<'a>),
355    /// `SHELL` instruction.
356    Shell(ShellInstruction<'a>),
357    /// `STOPSIGNAL` instruction.
358    Stopsignal(StopsignalInstruction<'a>),
359    /// `USER` instruction.
360    User(UserInstruction<'a>),
361    /// `VOLUME` instruction.
362    Volume(VolumeInstruction<'a>),
363    /// `WORKDIR` instruction.
364    Workdir(WorkdirInstruction<'a>),
365}
366impl Instruction<'_> {
367    fn instruction_span(&self) -> Span {
368        match self {
369            Instruction::Add(instruction) => instruction.add.span.clone(),
370            Instruction::Arg(instruction) => instruction.arg.span.clone(),
371            Instruction::Cmd(instruction) => instruction.cmd.span.clone(),
372            Instruction::Copy(instruction) => instruction.copy.span.clone(),
373            Instruction::Entrypoint(instruction) => instruction.entrypoint.span.clone(),
374            Instruction::Env(instruction) => instruction.env.span.clone(),
375            Instruction::Expose(instruction) => instruction.expose.span.clone(),
376            Instruction::From(instruction) => instruction.from.span.clone(),
377            Instruction::Healthcheck(instruction) => instruction.healthcheck.span.clone(),
378            Instruction::Label(instruction) => instruction.label.span.clone(),
379            Instruction::Maintainer(instruction) => instruction.maintainer.span.clone(),
380            Instruction::Onbuild(instruction) => instruction.onbuild.span.clone(),
381            Instruction::Run(instruction) => instruction.run.span.clone(),
382            Instruction::Shell(instruction) => instruction.shell.span.clone(),
383            Instruction::Stopsignal(instruction) => instruction.stopsignal.span.clone(),
384            Instruction::User(instruction) => instruction.user.span.clone(),
385            Instruction::Volume(instruction) => instruction.volume.span.clone(),
386            Instruction::Workdir(instruction) => instruction.workdir.span.clone(),
387        }
388    }
389}
390/// An `ADD` instruction.
391///
392/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#add)
393#[derive(Debug)]
394#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
395#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
396#[non_exhaustive]
397pub struct AddInstruction<'a> {
398    /// ```text
399    /// ADD [options] <src> ... <dest>
400    /// ^^^
401    /// ```
402    pub add: Keyword,
403    /// ```text
404    /// ADD [options] <src> ... <dest>
405    ///     ^^^^^^^^^
406    /// ```
407    pub options: SmallVec<[Flag<'a>; 1]>,
408    /// ```text
409    /// ADD [options] <src> ... <dest>
410    ///               ^^^^^^^^^
411    /// ```
412    // At least 1
413    pub src: SmallVec<[Source<'a>; 1]>,
414    /// ```text
415    /// ADD [options] <src> ... <dest>
416    ///                         ^^^^^^
417    /// ```
418    pub dest: UnescapedString<'a>,
419}
420/// An `ARG` instruction.
421///
422/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#arg)
423#[derive(Debug)]
424#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
425#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
426#[non_exhaustive]
427pub struct ArgInstruction<'a> {
428    /// ```text
429    /// ARG <name>[=<default value>] [<name>[=<default value>]...]
430    /// ^^^
431    /// ```
432    pub arg: Keyword,
433    /// ```text
434    /// ARG <name>[=<default value>] [<name>[=<default value>]...]
435    ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
436    /// ```
437    // TODO: SmallVec<[NameOptValue<'a>; 1]>
438    pub arguments: UnescapedString<'a>,
439}
440/// A `CMD` instruction.
441///
442/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#cmd)
443#[derive(Debug)]
444#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
445#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
446#[non_exhaustive]
447pub struct CmdInstruction<'a> {
448    /// ```text
449    /// CMD ["executable", "param"]
450    /// ^^^
451    /// ```
452    pub cmd: Keyword,
453    /// ```text
454    /// CMD ["executable", "param"]
455    ///     ^^^^^^^^^^^^^^^^^^^^^^^
456    /// ```
457    pub arguments: Command<'a>,
458}
459/// A `COPY` instruction.
460///
461/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#copy)
462#[derive(Debug)]
463#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
464#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
465#[non_exhaustive]
466pub struct CopyInstruction<'a> {
467    /// ```text
468    /// COPY [options] <src> ... <dest>
469    /// ^^^^
470    /// ```
471    pub copy: Keyword,
472    /// ```text
473    /// COPY [options] <src> ... <dest>
474    ///      ^^^^^^^^^
475    /// ```
476    pub options: SmallVec<[Flag<'a>; 1]>,
477    /// ```text
478    /// COPY [options] <src> ... <dest>
479    ///                ^^^^^^^^^
480    /// ```
481    // At least 1
482    pub src: SmallVec<[Source<'a>; 1]>,
483    /// ```text
484    /// COPY [options] <src> ... <dest>
485    ///                          ^^^^^^
486    /// ```
487    pub dest: UnescapedString<'a>,
488}
489/// A enum that represents source value of [`ARG` instruction](ArgInstruction) and
490/// [`COPY` instruction](CopyInstruction).
491#[derive(Debug)]
492#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
493#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
494#[non_exhaustive]
495pub enum Source<'a> {
496    /// Path or URL.
497    Path(UnescapedString<'a>),
498    /// Here-document.
499    HereDoc(HereDoc<'a>),
500}
501/// An `ENTRYPOINT` instruction.
502///
503/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#entrypoint)
504#[derive(Debug)]
505#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
506#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
507#[non_exhaustive]
508pub struct EntrypointInstruction<'a> {
509    /// ```text
510    /// ENTRYPOINT ["executable", "param"]
511    /// ^^^^^^^^^^
512    /// ```
513    pub entrypoint: Keyword,
514    /// ```text
515    /// ENTRYPOINT ["executable", "param"]
516    ///            ^^^^^^^^^^^^^^^^^^^^^^^
517    /// ```
518    pub arguments: Command<'a>,
519}
520/// An `ENV` instruction.
521///
522/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#env)
523#[derive(Debug)]
524#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
525#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
526#[non_exhaustive]
527pub struct EnvInstruction<'a> {
528    /// ```text
529    /// ENV <key>=<value> [<key>=<value>...]
530    /// ^^^
531    /// ```
532    pub env: Keyword,
533    /// ```text
534    /// ENV <key>=<value> [<key>=<value>...]
535    ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
536    /// ```
537    // TODO: SmallVec<[NameValue<'a>; 1]>
538    pub arguments: UnescapedString<'a>,
539}
540/// An `EXPOSE` instruction.
541///
542/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#expose)
543#[derive(Debug)]
544#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
545#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
546#[non_exhaustive]
547pub struct ExposeInstruction<'a> {
548    /// ```text
549    /// EXPOSE <port>[/<protocol>] [<port>[/<protocol>]...]
550    /// ^^^^^^
551    /// ```
552    pub expose: Keyword,
553    /// ```text
554    /// EXPOSE <port>[/<protocol>] [<port>[/<protocol>]...]
555    ///        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
556    /// ```
557    pub arguments: SmallVec<[UnescapedString<'a>; 1]>,
558}
559/// A `FROM` instruction.
560///
561/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#from)
562#[derive(Debug)]
563#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
564#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
565#[non_exhaustive]
566pub struct FromInstruction<'a> {
567    /// ```text
568    /// FROM [--platform=<platform>] <image> [AS <name>]
569    /// ^^^^
570    /// ```
571    pub from: Keyword,
572    /// ```text
573    /// FROM [--platform=<platform>] <image> [AS <name>]
574    ///      ^^^^^^^^^^^^^^^^^^^^^^^
575    /// ```
576    pub options: Vec<Flag<'a>>,
577    /// ```text
578    /// FROM [--platform=<platform>] <image> [AS <name>]
579    ///                              ^^^^^^^
580    /// ```
581    pub image: UnescapedString<'a>,
582    /// ```text
583    /// FROM [--platform=<platform>] <image> [AS <name>]
584    ///                                      ^^^^^^^^^^^
585    /// ```
586    pub as_: Option<(Keyword, UnescapedString<'a>)>,
587}
588/// A `HEALTHCHECK` instruction.
589///
590/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#healthcheck)
591#[derive(Debug)]
592#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
593#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
594#[non_exhaustive]
595pub struct HealthcheckInstruction<'a> {
596    /// ```text
597    /// HEALTHCHECK [options] CMD command
598    /// ^^^^^^^^^^^
599    /// ```
600    pub healthcheck: Keyword,
601    /// ```text
602    /// HEALTHCHECK [options] CMD command
603    ///             ^^^^^^^^^
604    /// ```
605    pub options: Vec<Flag<'a>>,
606    /// ```text
607    /// HEALTHCHECK [options] CMD command
608    ///                       ^^^^^^^^^^^
609    /// ```
610    pub arguments: HealthcheckArguments<'a>,
611}
612/// Arguments of the [`HEALTHCHECK` instruction](HealthcheckInstruction).
613#[derive(Debug)]
614#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
615#[cfg_attr(feature = "serde", serde(tag = "kind"))]
616#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
617#[non_exhaustive]
618pub enum HealthcheckArguments<'a> {
619    /// `HEALTHCHECK [options] CMD ...`
620    #[non_exhaustive]
621    Cmd {
622        /// ```text
623        /// HEALTHCHECK [options] CMD command
624        ///                       ^^^
625        /// ```
626        cmd: Keyword,
627        /// ```text
628        /// HEALTHCHECK [options] CMD command
629        ///                           ^^^^^^^
630        /// ```
631        arguments: Command<'a>,
632    },
633    /// `HEALTHCHECK [options] NONE`
634    #[non_exhaustive]
635    None {
636        /// ```text
637        /// HEALTHCHECK [options] NONE
638        ///                       ^^^^
639        /// ```
640        none: Keyword,
641    },
642}
643/// A `LABEL` instruction.
644///
645/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#label)
646#[derive(Debug)]
647#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
648#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
649#[non_exhaustive]
650pub struct LabelInstruction<'a> {
651    /// ```text
652    /// LABEL <key>=<value> [<key>=<value>...]
653    /// ^^^^^
654    /// ```
655    pub label: Keyword,
656    /// ```text
657    /// LABEL <key>=<value> [<key>=<value>...]
658    ///       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
659    /// ```
660    // TODO: SmallVec<[NameValue<'a>; 1]>
661    pub arguments: UnescapedString<'a>,
662}
663/// A `MAINTAINER` instruction (deprecated).
664///
665/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#maintainer-deprecated)
666#[derive(Debug)]
667#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
668#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
669#[non_exhaustive]
670pub struct MaintainerInstruction<'a> {
671    /// ```text
672    /// MAINTAINER <name>
673    /// ^^^^^^^^^^
674    /// ```
675    pub maintainer: Keyword,
676    /// ```text
677    /// MAINTAINER <name>
678    ///            ^^^^^^
679    /// ```
680    pub name: UnescapedString<'a>,
681}
682/// A `ONBUILD` instruction.
683///
684/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#onbuild)
685#[derive(Debug)]
686#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
687#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
688#[non_exhaustive]
689pub struct OnbuildInstruction<'a> {
690    /// ```text
691    /// ONBUILD <INSTRUCTION>
692    /// ^^^^^^^
693    /// ```
694    pub onbuild: Keyword,
695    /// ```text
696    /// ONBUILD <INSTRUCTION>
697    ///         ^^^^^^^^^^^^^
698    /// ```
699    pub instruction: Box<Instruction<'a>>,
700}
701/// A `RUN` instruction.
702///
703/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#run)
704#[derive(Debug)]
705#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
706#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
707#[non_exhaustive]
708pub struct RunInstruction<'a> {
709    /// ```text
710    /// RUN [options] <command> ...
711    /// ^^^
712    /// ```
713    pub run: Keyword,
714    /// ```text
715    /// RUN [options] <command> ...
716    ///     ^^^^^^^^^
717    /// ```
718    pub options: SmallVec<[Flag<'a>; 1]>,
719    /// ```text
720    /// RUN [options] <command> ...
721    ///               ^^^^^^^^^^^^^
722    /// ```
723    pub arguments: Command<'a>,
724    /// ```text
725    ///   RUN [options] <<EOF
726    /// /               ^^^^^
727    /// | ...
728    /// | EOF
729    /// |_^^^
730    /// ```
731    pub here_docs: Vec<HereDoc<'a>>,
732}
733/// A `SHELL` instruction.
734///
735/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#shell)
736#[derive(Debug)]
737#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
738#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
739#[non_exhaustive]
740pub struct ShellInstruction<'a> {
741    /// ```text
742    /// SHELL ["executable", "param"]
743    /// ^^^^^
744    /// ```
745    pub shell: Keyword,
746    /// ```text
747    /// SHELL ["executable", "param"]
748    ///       ^^^^^^^^^^^^^^^^^^^^^^^
749    /// ```
750    // Usually at least 2, e.g., ["/bin/sh", "-c"]
751    // Common cases are 4, e.g., ["/bin/bash", "-o", "pipefail", "-c"]
752    pub arguments: SmallVec<[UnescapedString<'a>; 4]>,
753}
754/// A `STOPSIGNAL` instruction.
755///
756/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#stopsignal)
757#[derive(Debug)]
758#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
759#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
760#[non_exhaustive]
761pub struct StopsignalInstruction<'a> {
762    /// ```text
763    /// STOPSIGNAL signal
764    /// ^^^^^^^^^^
765    /// ```
766    pub stopsignal: Keyword,
767    /// ```text
768    /// STOPSIGNAL signal
769    ///            ^^^^^^
770    /// ```
771    pub arguments: UnescapedString<'a>,
772}
773/// A `USER` instruction.
774///
775/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#user)
776#[derive(Debug)]
777#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
778#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
779#[non_exhaustive]
780pub struct UserInstruction<'a> {
781    /// ```text
782    /// USER <user>[:<group>]
783    /// ^^^^
784    /// ```
785    pub user: Keyword,
786    /// ```text
787    /// USER <user>[:<group>]
788    ///      ^^^^^^^^^^^^^^^^
789    /// ```
790    pub arguments: UnescapedString<'a>,
791}
792/// A `VOLUME` instruction.
793///
794/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#volume)
795#[derive(Debug)]
796#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
797#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
798#[non_exhaustive]
799pub struct VolumeInstruction<'a> {
800    /// ```text
801    /// VOLUME ["/data"]
802    /// ^^^^^^
803    /// ```
804    pub volume: Keyword,
805    /// ```text
806    /// VOLUME ["/data"]
807    ///        ^^^^^^^^^
808    /// ```
809    pub arguments: JsonOrStringArray<'a, 1>,
810}
811/// A `WORKDIR` instruction.
812///
813/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#workdir)
814#[derive(Debug)]
815#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
816#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
817#[non_exhaustive]
818pub struct WorkdirInstruction<'a> {
819    /// ```text
820    /// WORKDIR /path/to/workdir
821    /// ^^^^^^^
822    /// ```
823    pub workdir: Keyword,
824    /// ```text
825    /// WORKDIR /path/to/workdir
826    ///         ^^^^^^^^^^^^^^^^
827    /// ```
828    pub arguments: UnescapedString<'a>,
829}
830
831/// A keyword.
832#[derive(Debug)]
833#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
834#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
835#[non_exhaustive]
836pub struct Keyword {
837    #[allow(missing_docs)]
838    pub span: Span,
839}
840
841/// An option flag.
842#[derive(Debug)]
843#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
844#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
845pub struct Flag<'a> {
846    /// ```text
847    /// --platform=linux/amd64
848    /// ^
849    /// ```
850    flag_start: usize,
851    /// ```text
852    /// --platform=linux/amd64
853    ///   ^^^^^^^^
854    /// ```
855    pub name: UnescapedString<'a>,
856    /// ```text
857    /// --platform=linux/amd64
858    ///            ^^^^^^^^^^^
859    /// ```
860    pub value: Option<UnescapedString<'a>>,
861}
862impl Flag<'_> {
863    /// ```text
864    /// --platform=linux/amd64
865    /// ^^^^^^^^^^
866    /// ```
867    #[must_use]
868    pub fn flag_span(&self) -> Span {
869        self.flag_start..self.name.span.end
870    }
871    /// ```text
872    /// --platform=linux/amd64
873    /// ^^^^^^^^^^^^^^^^^^^^^^
874    /// ```
875    #[must_use]
876    pub fn span(&self) -> Span {
877        match &self.value {
878            Some(v) => self.flag_start..v.span.end,
879            None => self.flag_span(),
880        }
881    }
882}
883
884/// An unescaped string.
885#[derive(Debug, PartialEq)]
886#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
887#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
888#[non_exhaustive]
889pub struct UnescapedString<'a> {
890    #[allow(missing_docs)]
891    pub span: Span,
892    #[allow(missing_docs)]
893    pub value: Cow<'a, str>,
894}
895impl UnescapedString<'_> {
896    #[inline]
897    fn trim_end(&mut self) {
898        // trim trailing spaces of the value
899        match &mut self.value {
900            Cow::Borrowed(v) => {
901                while let Some(&b) = v.as_bytes().last() {
902                    if TABLE[b as usize] & (WHITESPACE | POSSIBLE_LINE) == 0 {
903                        break;
904                    }
905                    *v = &v[..v.len() - 1];
906                    self.span.end -= 1;
907                }
908            }
909            Cow::Owned(v) => {
910                while let Some(&b) = v.as_bytes().last() {
911                    if TABLE[b as usize] & (WHITESPACE | POSSIBLE_LINE) == 0 {
912                        break;
913                    }
914                    v.pop();
915                    self.span.end -= 1;
916                }
917            }
918        }
919    }
920}
921
922/// A command.
923///
924/// This is used in the [`RUN`](RunInstruction), [`CMD`](CmdInstruction), and
925/// [`ENTRYPOINT`](EntrypointInstruction) instructions.
926///
927/// [Dockerfile reference](https://docs.docker.com/reference/dockerfile/#shell-and-exec-form)
928#[derive(Debug)]
929#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
930#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
931#[non_exhaustive]
932pub enum Command<'a> {
933    /// Exec-form (JSON array)
934    // At least 1
935    Exec(Spanned<SmallVec<[UnescapedString<'a>; 1]>>),
936    /// Shell-form (space-separated string or here-documents), escape preserved
937    Shell(Spanned<&'a str>),
938}
939
940// TODO: merge two? it reduce size, but make confusing when array modified.
941/// A JSON array or space-separated string.
942///
943/// This is used in the [`VOLUME` instruction](VolumeInstruction).
944#[derive(Debug)]
945#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
946#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
947#[allow(clippy::exhaustive_enums)]
948pub enum JsonOrStringArray<'a, const N: usize> {
949    /// JSON array.
950    Json(Spanned<SmallVec<[UnescapedString<'a>; N]>>),
951    /// Space-separated string.
952    String(SmallVec<[UnescapedString<'a>; N]>),
953}
954
955/// A here-document.
956#[derive(Debug)]
957#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
958#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
959#[non_exhaustive]
960pub struct HereDoc<'a> {
961    #[allow(missing_docs)]
962    pub span: Span,
963    /// `false` if delimiter is quoted.
964    pub expand: bool,
965    #[allow(missing_docs)]
966    pub value: Cow<'a, str>,
967}
968
969/// A spanned value.
970#[derive(Debug)]
971#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
972#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
973#[allow(clippy::exhaustive_structs)]
974pub struct Spanned<T> {
975    #[allow(missing_docs)]
976    pub span: Span,
977    #[allow(missing_docs)]
978    pub value: T,
979}
980
981#[allow(missing_docs)]
982pub type Span = Range<usize>;
983
984// -----------------------------------------------------------------------------
985// Parsing
986
987/// An iterator over instructions.
988///
989/// This type is returned by [`parse_iter`] function.
990#[allow(missing_debug_implementations)]
991#[must_use = "iterators are lazy and do nothing unless consumed"]
992pub struct ParseIter<'a> {
993    text: &'a str,
994    s: &'a [u8],
995    escape_byte: u8,
996    has_stage: bool,
997    in_onbuild: bool,
998    parser_directives: ParserDirectives<'a>,
999}
1000impl<'a> ParseIter<'a> {
1001    fn new(mut text: &'a str) -> Result<Self> {
1002        // https://github.com/moby/moby/pull/23234
1003        if text.as_bytes().starts_with(UTF8_BOM) {
1004            text = &text[UTF8_BOM.len()..];
1005        }
1006        let mut p = Self {
1007            text,
1008            s: text.as_bytes(),
1009            escape_byte: DEFAULT_ESCAPE_BYTE,
1010            has_stage: false,
1011            in_onbuild: false,
1012            parser_directives: ParserDirectives {
1013                // https://docs.docker.com/reference/dockerfile/#parser-directives
1014                syntax: None,
1015                escape: None,
1016                check: None,
1017            },
1018        };
1019
1020        parse_parser_directives(&mut p).map_err(|e| e.into_error(&p))?;
1021
1022        // https://docs.docker.com/reference/dockerfile/#format
1023        // > For backward compatibility, leading whitespace before comments (#) and
1024        // > instructions (such as RUN) are ignored, but discouraged.
1025        consume_comments_and_whitespaces(&mut p.s, p.escape_byte);
1026        Ok(p)
1027    }
1028}
1029impl<'a> Iterator for ParseIter<'a> {
1030    type Item = Result<Instruction<'a>>;
1031    #[inline]
1032    fn next(&mut self) -> Option<Self::Item> {
1033        #[cold]
1034        fn error(p: &mut ParseIter<'_>, e: ErrorKind<'_>) -> Error {
1035            let e = e.into_error(p);
1036            // avoid error loop
1037            p.s = &[];
1038            p.has_stage = true;
1039            e
1040        }
1041
1042        let p = self;
1043        let mut s = p.s;
1044        if let Some((&b, s_next)) = s.split_first() {
1045            let instruction = match parse_instruction(p, &mut s, b, s_next) {
1046                Ok(i) => i,
1047                Err(e) => return Some(Err(error(p, e))),
1048            };
1049            match &instruction {
1050                Instruction::From(..) => {
1051                    p.has_stage = true;
1052                }
1053                Instruction::Arg(..) => {}
1054                instruction => {
1055                    if !p.has_stage {
1056                        return Some(Err(error(
1057                            p,
1058                            error::expected("FROM", instruction.instruction_span().start),
1059                        )));
1060                    }
1061                }
1062            }
1063            consume_comments_and_whitespaces(&mut s, p.escape_byte);
1064            p.s = s;
1065            return Some(Ok(instruction));
1066        }
1067        if !p.has_stage {
1068            // https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/dockerfile2llb/convert.go#L278
1069            return Some(Err(error(p, error::no_stage())));
1070        }
1071        None
1072    }
1073}
1074
1075impl core::iter::FusedIterator for ParseIter<'_> {}
1076
1077const DEFAULT_ESCAPE_BYTE: u8 = b'\\';
1078
1079fn parse_parser_directives(p: &mut ParseIter<'_>) -> InternalResult<'static, ()> {
1080    // TODO: https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/parser/parser.go#L540
1081    while let Some((&b'#', s_next)) = p.s.split_first() {
1082        p.s = s_next;
1083        consume_whitespaces_no_line_continuation(&mut p.s);
1084        let directive_start = p.text.len() - p.s.len();
1085        if token(&mut p.s, b"SYNTAX") {
1086            consume_whitespaces_no_line_continuation(&mut p.s);
1087            if let Some((&b'=', s_next)) = p.s.split_first() {
1088                p.s = s_next;
1089                if p.parser_directives.syntax.is_some() {
1090                    // > Invalid due to appearing twice
1091                    p.parser_directives.syntax = None;
1092                    p.parser_directives.escape = None;
1093                    p.parser_directives.check = None;
1094                    p.escape_byte = DEFAULT_ESCAPE_BYTE;
1095                    consume_current_line_no_line_continuation(&mut p.s);
1096                    break;
1097                }
1098                consume_whitespaces_no_line_continuation(&mut p.s);
1099                let value_start = p.text.len() - p.s.len();
1100                consume_until_whitespaces_or_line_no_line_continuation(&mut p.s);
1101                let end = p.text.len() - p.s.len();
1102                let value = trim_end(p.text, value_start, end);
1103                p.parser_directives.syntax = Some(ParserDirective {
1104                    start: directive_start,
1105                    value: Spanned { span: value_start..value_start + value.len(), value },
1106                });
1107                consume_current_line_no_line_continuation(&mut p.s);
1108                continue;
1109            }
1110        } else if token(&mut p.s, b"CHECK") {
1111            consume_whitespaces_no_line_continuation(&mut p.s);
1112            if let Some((&b'=', s_next)) = p.s.split_first() {
1113                p.s = s_next;
1114                if p.parser_directives.check.is_some() {
1115                    // > Invalid due to appearing twice
1116                    p.parser_directives.syntax = None;
1117                    p.parser_directives.escape = None;
1118                    p.parser_directives.check = None;
1119                    p.escape_byte = DEFAULT_ESCAPE_BYTE;
1120                    consume_current_line_no_line_continuation(&mut p.s);
1121                    break;
1122                }
1123                consume_whitespaces_no_line_continuation(&mut p.s);
1124                let value_start = p.text.len() - p.s.len();
1125                consume_until_whitespaces_or_line_no_line_continuation(&mut p.s);
1126                let end = p.text.len() - p.s.len();
1127                let value = trim_end(p.text, value_start, end);
1128                p.parser_directives.check = Some(ParserDirective {
1129                    start: directive_start,
1130                    value: Spanned { span: value_start..value_start + value.len(), value },
1131                });
1132                consume_current_line_no_line_continuation(&mut p.s);
1133                continue;
1134            }
1135        } else if token(&mut p.s, b"ESCAPE") {
1136            consume_whitespaces_no_line_continuation(&mut p.s);
1137            if let Some((&b'=', s_next)) = p.s.split_first() {
1138                p.s = s_next;
1139                if p.parser_directives.escape.is_some() {
1140                    // > Invalid due to appearing twice
1141                    p.parser_directives.syntax = None;
1142                    p.parser_directives.escape = None;
1143                    p.parser_directives.check = None;
1144                    p.escape_byte = DEFAULT_ESCAPE_BYTE;
1145                    consume_current_line_no_line_continuation(&mut p.s);
1146                    break;
1147                }
1148                consume_whitespaces_no_line_continuation(&mut p.s);
1149                let value_start = p.text.len() - p.s.len();
1150                consume_until_whitespaces_or_line_no_line_continuation(&mut p.s);
1151                let end = p.text.len() - p.s.len();
1152                let value = trim_end(p.text, value_start, end);
1153                match value {
1154                    "`" => p.escape_byte = b'`',
1155                    "\\" => {}
1156                    _ => return Err(error::invalid_escape(value_start)),
1157                }
1158                p.parser_directives.escape = Some(ParserDirective {
1159                    start: directive_start,
1160                    value: Spanned {
1161                        span: value_start..value_start + value.len(),
1162                        value: p.escape_byte as char,
1163                    },
1164                });
1165                consume_current_line_no_line_continuation(&mut p.s);
1166                continue;
1167            }
1168        }
1169        consume_current_line_no_line_continuation(&mut p.s);
1170        break;
1171    }
1172    Ok(())
1173}
1174
1175#[inline]
1176fn parse_instruction<'a>(
1177    p: &mut ParseIter<'a>,
1178    s: &mut &'a [u8],
1179    b: u8,
1180    s_next: &'a [u8],
1181) -> InternalResult<'a, Instruction<'a>> {
1182    let instruction_start = p.text.len() - s.len();
1183    *s = s_next;
1184    // NB: `token_slow` must be called after all `token` calls.
1185    match b & TO_UPPER8 {
1186        b'A' => {
1187            if token(s, &b"ARG"[1..]) {
1188                let instruction_span = instruction_start..p.text.len() - s.len();
1189                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1190                    return parse_arg(p, s, Keyword { span: instruction_span });
1191                }
1192            } else if token(s, &b"ADD"[1..]) {
1193                let instruction_span = instruction_start..p.text.len() - s.len();
1194                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1195                    let add = Keyword { span: instruction_span };
1196                    let (options, src, dest) = parse_add_or_copy(p, s, &add)?;
1197                    return Ok(Instruction::Add(AddInstruction { add, options, src, dest }));
1198                }
1199            } else if token_slow(s, &b"ARG"[1..], p.escape_byte) {
1200                let instruction_span = instruction_start..p.text.len() - s.len();
1201                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1202                    return parse_arg(p, s, Keyword { span: instruction_span });
1203                }
1204            } else if token_slow(s, &b"ADD"[1..], p.escape_byte) {
1205                let instruction_span = instruction_start..p.text.len() - s.len();
1206                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1207                    let add = Keyword { span: instruction_span };
1208                    let (options, src, dest) = parse_add_or_copy(p, s, &add)?;
1209                    return Ok(Instruction::Add(AddInstruction { add, options, src, dest }));
1210                }
1211            }
1212        }
1213        b'C' => {
1214            if token(s, &b"COPY"[1..]) {
1215                let instruction_span = instruction_start..p.text.len() - s.len();
1216                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1217                    let copy = Keyword { span: instruction_span };
1218                    let (options, src, dest) = parse_add_or_copy(p, s, &copy)?;
1219                    return Ok(Instruction::Copy(CopyInstruction { copy, options, src, dest }));
1220                }
1221            } else if token(s, &b"CMD"[1..]) {
1222                let instruction_span = instruction_start..p.text.len() - s.len();
1223                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1224                    return parse_cmd(p, s, Keyword { span: instruction_span });
1225                }
1226            } else if token_slow(s, &b"COPY"[1..], p.escape_byte) {
1227                let instruction_span = instruction_start..p.text.len() - s.len();
1228                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1229                    let copy = Keyword { span: instruction_span };
1230                    let (options, src, dest) = parse_add_or_copy(p, s, &copy)?;
1231                    return Ok(Instruction::Copy(CopyInstruction { copy, options, src, dest }));
1232                }
1233            } else if token_slow(s, &b"CMD"[1..], p.escape_byte) {
1234                let instruction_span = instruction_start..p.text.len() - s.len();
1235                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1236                    return parse_cmd(p, s, Keyword { span: instruction_span });
1237                }
1238            }
1239        }
1240        b'E' => {
1241            if token(s, &b"ENV"[1..]) {
1242                let instruction_span = instruction_start..p.text.len() - s.len();
1243                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1244                    return parse_env(p, s, Keyword { span: instruction_span });
1245                }
1246            } else if token(s, &b"EXPOSE"[1..]) {
1247                let instruction_span = instruction_start..p.text.len() - s.len();
1248                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1249                    return parse_expose(p, s, Keyword { span: instruction_span });
1250                }
1251            } else if token(s, &b"ENTRYPOINT"[1..]) {
1252                let instruction_span = instruction_start..p.text.len() - s.len();
1253                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1254                    return parse_entrypoint(p, s, Keyword { span: instruction_span });
1255                }
1256            } else if token_slow(s, &b"ENV"[1..], p.escape_byte) {
1257                let instruction_span = instruction_start..p.text.len() - s.len();
1258                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1259                    return parse_env(p, s, Keyword { span: instruction_span });
1260                }
1261            } else if token_slow(s, &b"EXPOSE"[1..], p.escape_byte) {
1262                let instruction_span = instruction_start..p.text.len() - s.len();
1263                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1264                    return parse_expose(p, s, Keyword { span: instruction_span });
1265                }
1266            } else if token_slow(s, &b"ENTRYPOINT"[1..], p.escape_byte) {
1267                let instruction_span = instruction_start..p.text.len() - s.len();
1268                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1269                    return parse_entrypoint(p, s, Keyword { span: instruction_span });
1270                }
1271            }
1272        }
1273        b'F' => {
1274            cold_path();
1275            if token(s, &b"FROM"[1..]) || token_slow(s, &b"FROM"[1..], p.escape_byte) {
1276                let instruction_span = instruction_start..p.text.len() - s.len();
1277                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1278                    return parse_from(p, s, Keyword { span: instruction_span });
1279                }
1280            }
1281        }
1282        b'H' => {
1283            cold_path();
1284            if token(s, &b"HEALTHCHECK"[1..]) || token_slow(s, &b"HEALTHCHECK"[1..], p.escape_byte)
1285            {
1286                let instruction_span = instruction_start..p.text.len() - s.len();
1287                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1288                    return parse_healthcheck(p, s, Keyword { span: instruction_span });
1289                }
1290            }
1291        }
1292        b'L' => {
1293            cold_path();
1294            if token(s, &b"LABEL"[1..]) || token_slow(s, &b"LABEL"[1..], p.escape_byte) {
1295                let instruction_span = instruction_start..p.text.len() - s.len();
1296                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1297                    return parse_label(p, s, Keyword { span: instruction_span });
1298                }
1299            }
1300        }
1301        b'M' => {
1302            cold_path();
1303            if token(s, &b"MAINTAINER"[1..]) || token_slow(s, &b"MAINTAINER"[1..], p.escape_byte) {
1304                let instruction_span = instruction_start..p.text.len() - s.len();
1305                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1306                    return parse_maintainer(p, s, Keyword { span: instruction_span });
1307                }
1308            }
1309        }
1310        b'O' => {
1311            cold_path();
1312            if token(s, &b"ONBUILD"[1..]) || token_slow(s, &b"ONBUILD"[1..], p.escape_byte) {
1313                let instruction_span = instruction_start..p.text.len() - s.len();
1314                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1315                    return parse_onbuild(p, s, Keyword { span: instruction_span });
1316                }
1317            }
1318        }
1319        b'R' => {
1320            if token(s, &b"RUN"[1..]) || token_slow(s, &b"RUN"[1..], p.escape_byte) {
1321                let instruction_span = instruction_start..p.text.len() - s.len();
1322                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1323                    return parse_run(p, s, Keyword { span: instruction_span });
1324                }
1325            }
1326        }
1327        b'S' => {
1328            cold_path();
1329            if token(s, &b"SHELL"[1..]) {
1330                let instruction_span = instruction_start..p.text.len() - s.len();
1331                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1332                    return parse_shell(p, s, Keyword { span: instruction_span });
1333                }
1334            } else if token(s, &b"STOPSIGNAL"[1..]) {
1335                let instruction_span = instruction_start..p.text.len() - s.len();
1336                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1337                    return parse_stopsignal(p, s, Keyword { span: instruction_span });
1338                }
1339            } else if token_slow(s, &b"SHELL"[1..], p.escape_byte) {
1340                let instruction_span = instruction_start..p.text.len() - s.len();
1341                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1342                    return parse_shell(p, s, Keyword { span: instruction_span });
1343                }
1344            } else if token_slow(s, &b"STOPSIGNAL"[1..], p.escape_byte) {
1345                let instruction_span = instruction_start..p.text.len() - s.len();
1346                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1347                    return parse_stopsignal(p, s, Keyword { span: instruction_span });
1348                }
1349            }
1350        }
1351        b'U' => {
1352            cold_path();
1353            if token(s, &b"USER"[1..]) || token_slow(s, &b"USER"[1..], p.escape_byte) {
1354                let instruction_span = instruction_start..p.text.len() - s.len();
1355                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1356                    return parse_user(p, s, Keyword { span: instruction_span });
1357                }
1358            }
1359        }
1360        b'V' => {
1361            cold_path();
1362            if token(s, &b"VOLUME"[1..]) || token_slow(s, &b"VOLUME"[1..], p.escape_byte) {
1363                let instruction_span = instruction_start..p.text.len() - s.len();
1364                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1365                    return parse_volume(p, s, Keyword { span: instruction_span });
1366                }
1367            }
1368        }
1369        b'W' => {
1370            cold_path();
1371            if token(s, &b"WORKDIR"[1..]) || token_slow(s, &b"WORKDIR"[1..], p.escape_byte) {
1372                let instruction_span = instruction_start..p.text.len() - s.len();
1373                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1374                    return parse_workdir(p, s, Keyword { span: instruction_span });
1375                }
1376            }
1377        }
1378        _ => {}
1379    }
1380    Err(error::unknown_instruction(instruction_start))
1381}
1382
1383#[inline]
1384fn parse_arg<'a>(
1385    p: &mut ParseIter<'a>,
1386    s: &mut &'a [u8],
1387    instruction: Keyword,
1388) -> InternalResult<'static, Instruction<'a>> {
1389    debug_assert!(token_slow(
1390        &mut p.text[instruction.span.clone()].as_bytes(),
1391        b"ARG",
1392        p.escape_byte,
1393    ));
1394    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1395    arguments.trim_end();
1396    if arguments.value.is_empty() {
1397        return Err(error::at_least_one_argument(instruction.span.start));
1398    }
1399    Ok(Instruction::Arg(ArgInstruction { arg: instruction, arguments }))
1400}
1401
1402#[inline]
1403fn parse_add_or_copy<'a>(
1404    p: &mut ParseIter<'a>,
1405    s: &mut &'a [u8],
1406    instruction: &Keyword,
1407) -> InternalResult<'a, (SmallVec<[Flag<'a>; 1]>, SmallVec<[Source<'a>; 1]>, UnescapedString<'a>)> {
1408    debug_assert!(
1409        token_slow(&mut p.text[instruction.span.clone()].as_bytes(), b"ADD", p.escape_byte,)
1410            || token_slow(&mut p.text[instruction.span.clone()].as_bytes(), b"COPY", p.escape_byte,)
1411    );
1412    let options = parse_options(s, p.text, p.escape_byte);
1413    if is_maybe_json(s) {
1414        let mut tmp = *s;
1415        if let Ok(((src, dest), _array_span)) = parse_json_array::<(
1416            SmallVec<[Source<'_>; 1]>,
1417            Option<_>,
1418        )>(&mut tmp, p.text, p.escape_byte)
1419        {
1420            if let Some((&b, s_next)) = tmp.split_first() {
1421                let consumed = consume_newline(b, s, s_next);
1422                debug_assert!(consumed);
1423            } else {
1424                *s = &[];
1425            }
1426            if src.is_empty() {
1427                return Err(error::at_least_two_arguments(instruction.span.start));
1428            }
1429            return Ok((options, src, dest.unwrap()));
1430        }
1431    }
1432    let (mut src, dest) = collect_space_separated_consume_line::<(
1433        SmallVec<[Source<'_>; 1]>,
1434        Option<_>,
1435    )>(s, p.text, p.escape_byte);
1436    if src.is_empty() {
1437        return Err(error::at_least_two_arguments(instruction.span.start));
1438    }
1439    for src in &mut src {
1440        let Source::Path(path) = src else { unreachable!() };
1441        let full = mem::take(&mut path.value);
1442        let mut tmp = full.as_bytes();
1443        if let Some(tmp_next) = tmp.strip_prefix(b"<<") {
1444            if let Some((delim, strip_tab, expand)) =
1445                collect_here_doc_delim(&mut tmp, tmp_next, &full)?
1446            {
1447                let delim_start = 2 + usize::from(strip_tab);
1448                // because of collect_space_separated_unescaped_consume_line
1449                debug_assert!(
1450                    tmp.is_empty()
1451                        && (matches!(delim, Cow::Owned(..))
1452                            || full.as_bytes()[delim_start..] == *delim)
1453                );
1454                let delim = match delim {
1455                    Cow::Borrowed(_) => match full {
1456                        Cow::Borrowed(v) => Cow::Borrowed(&v.as_bytes()[delim_start..]),
1457                        Cow::Owned(v) => {
1458                            let mut v = v.into_bytes();
1459                            drop(v.drain(..delim_start));
1460                            Cow::Owned(v)
1461                        }
1462                    },
1463                    Cow::Owned(v) => Cow::Owned(v),
1464                };
1465                let (here_doc, span) = collect_here_doc(s, p.text, delim, strip_tab)?;
1466                *src = Source::HereDoc(HereDoc { span, expand, value: here_doc });
1467                continue;
1468            }
1469        }
1470        path.value = full;
1471    }
1472    Ok((options, src, dest.unwrap()))
1473}
1474
1475#[allow(clippy::unnecessary_wraps)]
1476#[inline]
1477fn parse_cmd<'a>(
1478    p: &mut ParseIter<'a>,
1479    s: &mut &'a [u8],
1480    instruction: Keyword,
1481) -> InternalResult<'static, Instruction<'a>> {
1482    debug_assert!(token_slow(
1483        &mut p.text[instruction.span.clone()].as_bytes(),
1484        b"CMD",
1485        p.escape_byte,
1486    ));
1487    if is_maybe_json(s) {
1488        let mut tmp = *s;
1489        if let Ok((arguments, array_span)) =
1490            parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
1491        {
1492            if let Some((&b, s_next)) = tmp.split_first() {
1493                let consumed = consume_newline(b, s, s_next);
1494                debug_assert!(consumed);
1495            } else {
1496                *s = &[];
1497            }
1498            // "CMD []" seems to be okay?
1499            // https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/parser/testfiles/brimstone-docker-consul/Dockerfile#L3
1500            return Ok(Instruction::Cmd(CmdInstruction {
1501                cmd: instruction,
1502                arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
1503            }));
1504        }
1505    }
1506    let arguments_start = p.text.len() - s.len();
1507    consume_current_line(s, p.escape_byte);
1508    let end = p.text.len() - s.len();
1509    let arguments = trim_end(p.text, arguments_start, end);
1510    Ok(Instruction::Cmd(CmdInstruction {
1511        cmd: instruction,
1512        arguments: Command::Shell(Spanned {
1513            span: arguments_start..arguments_start + arguments.len(),
1514            value: arguments,
1515        }),
1516    }))
1517}
1518
1519#[inline]
1520fn parse_env<'a>(
1521    p: &mut ParseIter<'a>,
1522    s: &mut &'a [u8],
1523    instruction: Keyword,
1524) -> InternalResult<'static, Instruction<'a>> {
1525    debug_assert!(token_slow(
1526        &mut p.text[instruction.span.clone()].as_bytes(),
1527        b"ENV",
1528        p.escape_byte,
1529    ));
1530    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1531    arguments.trim_end();
1532    if arguments.value.is_empty() {
1533        return Err(error::at_least_one_argument(instruction.span.start));
1534    }
1535    Ok(Instruction::Env(EnvInstruction { env: instruction, arguments }))
1536}
1537
1538#[inline]
1539fn parse_expose<'a>(
1540    p: &mut ParseIter<'a>,
1541    s: &mut &'a [u8],
1542    instruction: Keyword,
1543) -> InternalResult<'static, Instruction<'a>> {
1544    debug_assert!(token_slow(
1545        &mut p.text[instruction.span.clone()].as_bytes(),
1546        b"EXPOSE",
1547        p.escape_byte,
1548    ));
1549    let arguments: SmallVec<[_; 1]> =
1550        collect_space_separated_consume_line(s, p.text, p.escape_byte);
1551    if arguments.is_empty() {
1552        return Err(error::at_least_one_argument(instruction.span.start));
1553    }
1554    Ok(Instruction::Expose(ExposeInstruction { expose: instruction, arguments }))
1555}
1556
1557#[inline]
1558fn parse_entrypoint<'a>(
1559    p: &mut ParseIter<'a>,
1560    s: &mut &'a [u8],
1561    instruction: Keyword,
1562) -> InternalResult<'static, Instruction<'a>> {
1563    debug_assert!(token_slow(
1564        &mut p.text[instruction.span.clone()].as_bytes(),
1565        b"ENTRYPOINT",
1566        p.escape_byte,
1567    ));
1568    if is_maybe_json(s) {
1569        let mut tmp = *s;
1570        if let Ok((arguments, array_span)) =
1571            parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
1572        {
1573            if let Some((&b, s_next)) = tmp.split_first() {
1574                let consumed = consume_newline(b, s, s_next);
1575                debug_assert!(consumed);
1576            } else {
1577                *s = &[];
1578            }
1579            if arguments.is_empty() {
1580                return Err(error::at_least_one_argument(instruction.span.start));
1581            }
1582            return Ok(Instruction::Entrypoint(EntrypointInstruction {
1583                entrypoint: instruction,
1584                arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
1585            }));
1586        }
1587    }
1588    let arguments_start = p.text.len() - s.len();
1589    consume_current_line(s, p.escape_byte);
1590    let end = p.text.len() - s.len();
1591    let arguments = trim_end(p.text, arguments_start, end);
1592    if arguments.is_empty() {
1593        return Err(error::at_least_one_argument(instruction.span.start));
1594    }
1595    Ok(Instruction::Entrypoint(EntrypointInstruction {
1596        entrypoint: instruction,
1597        arguments: Command::Shell(Spanned {
1598            span: arguments_start..arguments_start + arguments.len(),
1599            value: arguments,
1600        }),
1601    }))
1602}
1603
1604#[inline]
1605fn parse_from<'a>(
1606    p: &mut ParseIter<'a>,
1607    s: &mut &'a [u8],
1608    instruction: Keyword,
1609) -> InternalResult<'static, Instruction<'a>> {
1610    debug_assert!(token_slow(
1611        &mut p.text[instruction.span.clone()].as_bytes(),
1612        b"FROM",
1613        p.escape_byte,
1614    ));
1615    let options = parse_options(s, p.text, p.escape_byte);
1616    // TODO: https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/dockerfile2llb/convert.go#L413
1617    // > base name (%s) should not be blank
1618    let image = collect_non_whitespace(s, p.text, p.escape_byte);
1619    if image.value.is_empty() {
1620        return Err(error::at_least_one_argument(instruction.span.start));
1621    }
1622    let mut as_ = None;
1623    if consume_whitespaces(s, p.escape_byte) {
1624        let as_start = p.text.len() - s.len();
1625        if token(s, b"AS") || token_slow(s, b"AS", p.escape_byte) {
1626            let as_span = as_start..p.text.len() - s.len();
1627            if !consume_whitespaces(s, p.escape_byte) {
1628                return Err(error::expected("AS", as_start));
1629            }
1630            let name = collect_non_whitespace(s, p.text, p.escape_byte);
1631            consume_whitespaces(s, p.escape_byte);
1632            if !is_line_end(s.first()) {
1633                return Err(error::expected("newline or eof", p.text.len() - s.len()));
1634            }
1635            as_ = Some((Keyword { span: as_span }, name));
1636        } else if !is_line_end(s.first()) {
1637            return Err(error::expected("AS", as_start));
1638        }
1639    }
1640    Ok(Instruction::From(FromInstruction { from: instruction, options, image, as_ }))
1641}
1642
1643#[inline]
1644fn parse_healthcheck<'a>(
1645    p: &mut ParseIter<'a>,
1646    s: &mut &'a [u8],
1647    instruction: Keyword,
1648) -> InternalResult<'static, Instruction<'a>> {
1649    debug_assert!(token_slow(
1650        &mut p.text[instruction.span.clone()].as_bytes(),
1651        b"HEALTHCHECK",
1652        p.escape_byte,
1653    ));
1654    let options = parse_options(s, p.text, p.escape_byte);
1655    let Some((&b, s_next)) = s.split_first() else {
1656        return Err(error::expected("CMD or NONE", p.text.len() - s.len()));
1657    };
1658    let cmd_or_none_start = p.text.len() - s.len();
1659    match b & TO_UPPER8 {
1660        b'C' => {
1661            *s = s_next;
1662            if token(s, &b"CMD"[1..]) || token_slow(s, &b"CMD"[1..], p.escape_byte) {
1663                let cmd_span = cmd_or_none_start..p.text.len() - s.len();
1664                let cmd_keyword = Keyword { span: cmd_span };
1665                if consume_whitespaces_or_is_empty_line(s, p.escape_byte) {
1666                    if is_maybe_json(s) {
1667                        let mut tmp = *s;
1668                        if let Ok((arguments, array_span)) =
1669                            parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
1670                        {
1671                            if let Some((&b, s_next)) = tmp.split_first() {
1672                                let consumed = consume_newline(b, s, s_next);
1673                                debug_assert!(consumed);
1674                            } else {
1675                                *s = &[];
1676                            }
1677                            if arguments.is_empty() {
1678                                return Err(error::at_least_one_argument(instruction.span.start));
1679                            }
1680                            return Ok(Instruction::Healthcheck(HealthcheckInstruction {
1681                                healthcheck: instruction,
1682                                options,
1683                                arguments: HealthcheckArguments::Cmd {
1684                                    cmd: cmd_keyword,
1685                                    arguments: Command::Exec(Spanned {
1686                                        span: array_span,
1687                                        value: arguments,
1688                                    }),
1689                                },
1690                            }));
1691                        }
1692                    }
1693                    let arguments_start = p.text.len() - s.len();
1694                    consume_current_line(s, p.escape_byte);
1695                    let end = p.text.len() - s.len();
1696                    let arguments = trim_end(p.text, arguments_start, end);
1697                    return Ok(Instruction::Healthcheck(HealthcheckInstruction {
1698                        healthcheck: instruction,
1699                        options,
1700                        arguments: HealthcheckArguments::Cmd {
1701                            cmd: cmd_keyword,
1702                            arguments: Command::Shell(Spanned {
1703                                span: arguments_start..arguments_start + arguments.len(),
1704                                value: arguments,
1705                            }),
1706                        },
1707                    }));
1708                }
1709            }
1710        }
1711        b'N' => {
1712            *s = s_next;
1713            if token(s, &b"NONE"[1..]) || token_slow(s, &b"NONE"[1..], p.escape_byte) {
1714                let none_span = cmd_or_none_start..p.text.len() - s.len();
1715                consume_whitespaces(s, p.escape_byte);
1716                if !is_line_end(s.first()) {
1717                    return Err(error::other(
1718                        "HEALTHCHECK NONE does not accept arguments",
1719                        p.text.len() - s.len(),
1720                    ));
1721                }
1722                // TODO: HEALTHCHECK NONE doesn't support options
1723                let none_keyword = Keyword { span: none_span };
1724                return Ok(Instruction::Healthcheck(HealthcheckInstruction {
1725                    healthcheck: instruction,
1726                    options,
1727                    arguments: HealthcheckArguments::None { none: none_keyword },
1728                }));
1729            }
1730        }
1731        _ => {}
1732    }
1733    Err(error::expected("CMD or NONE", p.text.len() - s.len()))
1734}
1735
1736#[inline]
1737fn parse_label<'a>(
1738    p: &mut ParseIter<'a>,
1739    s: &mut &'a [u8],
1740    instruction: Keyword,
1741) -> InternalResult<'static, Instruction<'a>> {
1742    debug_assert!(token_slow(
1743        &mut p.text[instruction.span.clone()].as_bytes(),
1744        b"LABEL",
1745        p.escape_byte,
1746    ));
1747    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1748    arguments.trim_end();
1749    if arguments.value.is_empty() {
1750        return Err(error::at_least_one_argument(instruction.span.start));
1751    }
1752    Ok(Instruction::Label(LabelInstruction { label: instruction, arguments }))
1753}
1754
1755#[cold]
1756fn parse_maintainer<'a>(
1757    p: &mut ParseIter<'a>,
1758    s: &mut &'a [u8],
1759    instruction: Keyword,
1760) -> InternalResult<'static, Instruction<'a>> {
1761    debug_assert!(token_slow(
1762        &mut p.text[instruction.span.clone()].as_bytes(),
1763        b"MAINTAINER",
1764        p.escape_byte,
1765    ));
1766    let mut name = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1767    name.trim_end();
1768    if name.value.is_empty() {
1769        return Err(error::exactly_one_argument(instruction.span.start));
1770    }
1771    Ok(Instruction::Maintainer(MaintainerInstruction { maintainer: instruction, name }))
1772}
1773
1774#[inline]
1775fn parse_onbuild<'a>(
1776    p: &mut ParseIter<'a>,
1777    s: &mut &'a [u8],
1778    instruction: Keyword,
1779) -> InternalResult<'a, Instruction<'a>> {
1780    debug_assert!(token_slow(
1781        &mut p.text[instruction.span.clone()].as_bytes(),
1782        b"ONBUILD",
1783        p.escape_byte,
1784    ));
1785    // https://docs.docker.com/reference/dockerfile/#onbuild-limitations
1786    if p.in_onbuild {
1787        return Err(error::other("ONBUILD ONBUILD is not allowed", instruction.span.start));
1788    }
1789    p.in_onbuild = true;
1790    let Some((&b, s_next)) = s.split_first() else {
1791        return Err(error::expected("instruction after ONBUILD", instruction.span.start));
1792    };
1793    // TODO: https://docs.docker.com/reference/dockerfile/#onbuild-limitations
1794    // match b & TO_UPPER8 {
1795    //     b'F' => {
1796    //         if token(s, b"FROM") || token_slow(s, b"FROM", p.escape_byte) {
1797    //             return Err(error::other(p,
1798    //                 "ONBUILD FROM is not allowed",
1799    //                 instruction.span.start,
1800    //             ));
1801    //         }
1802    //     }
1803    //     b'M' => {
1804    //         if token(s, b"MAINTAINER")
1805    //             || token_slow(s, b"MAINTAINER", p.escape_byte)
1806    //         {
1807    //             return Err(error::other(p,
1808    //                 "ONBUILD MAINTAINER is not allowed",
1809    //                 instruction.span.start,
1810    //             ));
1811    //         }
1812    //     }
1813    //     _ => {}
1814    // }
1815    let inner_instruction = parse_instruction(p, s, b, s_next)?;
1816    p.in_onbuild = false;
1817    Ok(Instruction::Onbuild(OnbuildInstruction {
1818        onbuild: instruction,
1819        instruction: Box::new(inner_instruction),
1820    }))
1821}
1822
1823#[inline]
1824fn parse_run<'a>(
1825    p: &mut ParseIter<'a>,
1826    s: &mut &'a [u8],
1827    instruction: Keyword,
1828) -> InternalResult<'a, Instruction<'a>> {
1829    debug_assert!(token_slow(
1830        &mut p.text[instruction.span.clone()].as_bytes(),
1831        b"RUN",
1832        p.escape_byte,
1833    ));
1834    let options = parse_options(s, p.text, p.escape_byte);
1835    if is_maybe_json(s) {
1836        let mut tmp = *s;
1837        if let Ok((arguments, array_span)) =
1838            parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
1839        {
1840            if let Some((&b, s_next)) = tmp.split_first() {
1841                let consumed = consume_newline(b, s, s_next);
1842                debug_assert!(consumed);
1843            } else {
1844                *s = &[];
1845            }
1846            if arguments.is_empty() {
1847                return Err(error::at_least_one_argument(instruction.span.start));
1848            }
1849            return Ok(Instruction::Run(RunInstruction {
1850                run: instruction,
1851                options,
1852                arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
1853                // TODO: https://github.com/moby/buildkit/issues/2207
1854                here_docs: vec![],
1855            }));
1856        }
1857    }
1858
1859    // https://docs.docker.com/reference/dockerfile/#here-documents
1860    // At least 5, <<E\nE
1861    if s.len() >= 5 {
1862        // TODO: here-doc is not always start from first.
1863        if let Some(s_next) = s.strip_prefix(b"<<") {
1864            if let Some((delim, strip_tab, expand)) = collect_here_doc_delim(s, s_next, p.text)? {
1865                // TODO: skip space
1866                let arguments_start = p.text.len() - s.len();
1867                consume_current_line(s, p.escape_byte);
1868                let end = p.text.len() - s.len();
1869                let arguments = trim_end(p.text, arguments_start, end);
1870                let (here_doc, span) = collect_here_doc(s, p.text, delim, strip_tab)?;
1871                let here_doc = HereDoc { span, expand, value: here_doc };
1872                return Ok(Instruction::Run(RunInstruction {
1873                    run: instruction,
1874                    options,
1875                    arguments: Command::Shell(Spanned {
1876                        span: arguments_start..arguments_start + arguments.len(),
1877                        value: arguments,
1878                    }),
1879                    // TODO: multiple here-docs
1880                    here_docs: vec![here_doc],
1881                }));
1882            }
1883        }
1884    }
1885
1886    let arguments_start = p.text.len() - s.len();
1887    consume_current_line(s, p.escape_byte);
1888    let end = p.text.len() - s.len();
1889    let arguments = trim_end(p.text, arguments_start, end);
1890    Ok(Instruction::Run(RunInstruction {
1891        run: instruction,
1892        options,
1893        arguments: Command::Shell(Spanned {
1894            span: arguments_start..arguments_start + arguments.len(),
1895            value: arguments,
1896        }),
1897        here_docs: vec![],
1898    }))
1899}
1900
1901#[inline]
1902fn parse_shell<'a>(
1903    p: &mut ParseIter<'a>,
1904    s: &mut &'a [u8],
1905    instruction: Keyword,
1906) -> InternalResult<'static, Instruction<'a>> {
1907    debug_assert!(token_slow(
1908        &mut p.text[instruction.span.clone()].as_bytes(),
1909        b"SHELL",
1910        p.escape_byte,
1911    ));
1912    if !is_maybe_json(s) {
1913        return Err(error::expected("JSON array", p.text.len() - s.len()));
1914    }
1915    let (arguments, _array_span) =
1916        parse_json_array::<SmallVec<[_; 4]>>(s, p.text, p.escape_byte).map_err(error::json)?;
1917    if let Some((&b, s_next)) = s.split_first() {
1918        let consumed = consume_newline(b, s, s_next);
1919        debug_assert!(consumed);
1920    }
1921    if arguments.is_empty() {
1922        return Err(error::at_least_one_argument(instruction.span.start));
1923    }
1924    Ok(Instruction::Shell(ShellInstruction { shell: instruction, arguments }))
1925}
1926
1927#[inline]
1928fn parse_stopsignal<'a>(
1929    p: &mut ParseIter<'a>,
1930    s: &mut &'a [u8],
1931    instruction: Keyword,
1932) -> InternalResult<'static, Instruction<'a>> {
1933    debug_assert!(token_slow(
1934        &mut p.text[instruction.span.clone()].as_bytes(),
1935        b"STOPSIGNAL",
1936        p.escape_byte,
1937    ));
1938    // TODO: space is disallowed?
1939    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1940    arguments.trim_end();
1941    if arguments.value.is_empty() {
1942        return Err(error::exactly_one_argument(instruction.span.start));
1943    }
1944    Ok(Instruction::Stopsignal(StopsignalInstruction { stopsignal: instruction, arguments }))
1945}
1946
1947#[inline]
1948fn parse_user<'a>(
1949    p: &mut ParseIter<'a>,
1950    s: &mut &'a [u8],
1951    instruction: Keyword,
1952) -> InternalResult<'static, Instruction<'a>> {
1953    debug_assert!(token_slow(
1954        &mut p.text[instruction.span.clone()].as_bytes(),
1955        b"USER",
1956        p.escape_byte,
1957    ));
1958    // TODO: space is disallowed?
1959    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
1960    arguments.trim_end();
1961    if arguments.value.is_empty() {
1962        return Err(error::exactly_one_argument(instruction.span.start));
1963    }
1964    Ok(Instruction::User(UserInstruction { user: instruction, arguments }))
1965}
1966
1967#[inline]
1968fn parse_volume<'a>(
1969    p: &mut ParseIter<'a>,
1970    s: &mut &'a [u8],
1971    instruction: Keyword,
1972) -> InternalResult<'static, Instruction<'a>> {
1973    debug_assert!(token_slow(
1974        &mut p.text[instruction.span.clone()].as_bytes(),
1975        b"VOLUME",
1976        p.escape_byte,
1977    ));
1978    if is_maybe_json(s) {
1979        let mut tmp = *s;
1980        if let Ok((arguments, array_span)) = parse_json_array(&mut tmp, p.text, p.escape_byte) {
1981            if let Some((&b, s_next)) = tmp.split_first() {
1982                let consumed = consume_newline(b, s, s_next);
1983                debug_assert!(consumed);
1984            } else {
1985                *s = &[];
1986            }
1987            // "VOLUME []" seems to be okay?
1988            return Ok(Instruction::Volume(VolumeInstruction {
1989                volume: instruction,
1990                arguments: JsonOrStringArray::Json(Spanned { span: array_span, value: arguments }),
1991            }));
1992        }
1993    }
1994    let arguments: SmallVec<[_; 1]> =
1995        collect_space_separated_consume_line(s, p.text, p.escape_byte);
1996    if arguments.is_empty() {
1997        // TODO: "VOLUME" too?
1998        return Err(error::at_least_one_argument(instruction.span.start));
1999    }
2000    Ok(Instruction::Volume(VolumeInstruction {
2001        volume: instruction,
2002        arguments: JsonOrStringArray::String(arguments),
2003    }))
2004}
2005
2006#[inline]
2007fn parse_workdir<'a>(
2008    p: &mut ParseIter<'a>,
2009    s: &mut &'a [u8],
2010    instruction: Keyword,
2011) -> InternalResult<'static, Instruction<'a>> {
2012    debug_assert!(token_slow(
2013        &mut p.text[instruction.span.clone()].as_bytes(),
2014        b"WORKDIR",
2015        p.escape_byte,
2016    ));
2017    // TODO: space is disallowed if not escaped/quoted?
2018    let mut arguments = collect_until_line_consume_newline(s, p.text, p.escape_byte);
2019    arguments.trim_end();
2020    if arguments.value.is_empty() {
2021        return Err(error::exactly_one_argument(instruction.span.start));
2022    }
2023    Ok(Instruction::Workdir(WorkdirInstruction { workdir: instruction, arguments }))
2024}
2025
2026// -----------------------------------------------------------------------------
2027// Parsing Helpers
2028
2029// [\r\n]
2030const POSSIBLE_LINE: u8 = 1 << 0;
2031// [ \t]
2032const SPACE: u8 = 1 << 1;
2033// [ \r\t\v\f]
2034// \v = \x0B, \f = \x0C
2035// https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/parser/parser.go#L120
2036const WHITESPACE: u8 = 1 << 2;
2037// [#]
2038const COMMENT: u8 = 1 << 3;
2039// ["]
2040const DOUBLE_QUOTE: u8 = 1 << 4;
2041// [\`]
2042const POSSIBLE_ESCAPE: u8 = 1 << 5;
2043// [=]
2044const EQ: u8 = 1 << 6;
2045// 0x00..0x20
2046const CONTROL: u8 = 1 << 7;
2047
2048static TABLE: [u8; 256] = {
2049    let mut table = [0; 256];
2050    let mut i = 0;
2051    loop {
2052        let mut v = 0;
2053        if i < 0x20 {
2054            v |= CONTROL;
2055        }
2056        match i {
2057            b' ' | b'\t' => v |= WHITESPACE | SPACE,
2058            b'\x0B' | b'\x0C' => v |= WHITESPACE,
2059            b'\r' => v |= WHITESPACE | POSSIBLE_LINE,
2060            b'\n' => v |= POSSIBLE_LINE,
2061            b'#' => v |= COMMENT,
2062            b'"' => v |= DOUBLE_QUOTE,
2063            b'\\' | b'`' => v |= POSSIBLE_ESCAPE,
2064            b'=' => v |= EQ,
2065            _ => {}
2066        }
2067        table[i as usize] = v;
2068        if i == u8::MAX {
2069            break;
2070        }
2071        i += 1;
2072    }
2073    table
2074};
2075
2076// Lookup table for ascii to hex decoding.
2077#[rustfmt::skip]
2078static HEX_DECODE_TABLE: [u8; 256] = {
2079    const __: u8 = u8::MAX;
2080    [
2081        //  _1  _2  _3  _4  _5  _6  _7  _8  _9  _A  _B  _C  _D  _E  _F
2082        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0_
2083        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 1_
2084        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2_
2085         0,  1,  2,  3,  4,  5,  6,  7,  8,  9, __, __, __, __, __, __, // 3_
2086        __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 4_
2087        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 5_
2088        __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 6_
2089        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7_
2090        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8_
2091        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9_
2092        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A_
2093        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B_
2094        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C_
2095        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D_
2096        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E_
2097        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F_
2098    ]
2099};
2100
2101const UTF8_BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
2102
2103trait Store<T>: Sized {
2104    fn new() -> Self;
2105    fn push(&mut self, val: T);
2106}
2107impl<T> Store<T> for Vec<T> {
2108    #[inline]
2109    fn new() -> Self {
2110        Self::new()
2111    }
2112    #[inline]
2113    fn push(&mut self, val: T) {
2114        self.push(val);
2115    }
2116}
2117impl<T, const N: usize> Store<T> for SmallVec<[T; N]> {
2118    #[inline]
2119    fn new() -> Self {
2120        Self::new()
2121    }
2122    #[inline]
2123    fn push(&mut self, val: T) {
2124        self.push(val);
2125    }
2126}
2127impl<'a, const N: usize> Store<UnescapedString<'a>>
2128    for (SmallVec<[Source<'a>; N]>, Option<UnescapedString<'a>>)
2129{
2130    #[inline]
2131    fn new() -> Self {
2132        (SmallVec::new(), None)
2133    }
2134    #[inline]
2135    fn push(&mut self, val: UnescapedString<'a>) {
2136        if let Some(val) = self.1.replace(val) {
2137            self.0.push(Source::Path(val));
2138        }
2139    }
2140}
2141
2142// Equivalent to core::hint::cold_path, but compatible with pre-1.95 rustc.
2143#[inline(always)]
2144#[cold]
2145fn cold_path() {}
2146
2147// Note: must be called after consume_whitespaces
2148#[inline]
2149fn is_line_end(b: Option<&u8>) -> bool {
2150    matches!(b, Some(b'\n') | None)
2151}
2152
2153#[inline]
2154fn parse_options<'a, S: Store<Flag<'a>>>(s: &mut &[u8], start: &'a str, escape_byte: u8) -> S {
2155    let mut options = S::new();
2156    'outer: while let Some((&b'-', mut s_next)) = s.split_first() {
2157        loop {
2158            let Some((&b, s_next_next)) = s_next.split_first() else {
2159                break 'outer;
2160            };
2161            if b == b'-' {
2162                s_next = s_next_next;
2163                break;
2164            }
2165            if consume_line_continuation(&mut s_next, b, s_next_next, escape_byte) {
2166                continue;
2167            }
2168            break 'outer;
2169        }
2170        let flag_start = start.len() - s.len();
2171        *s = s_next;
2172        let name = collect_until::<{ WHITESPACE | POSSIBLE_LINE | EQ }>(s, start, escape_byte);
2173        let Some((&b'=', s_next)) = s.split_first() else {
2174            options.push(Flag { flag_start, name, value: None });
2175            consume_whitespaces(s, escape_byte);
2176            continue;
2177        };
2178        *s = s_next;
2179        let value = collect_non_whitespace(s, start, escape_byte);
2180        options.push(Flag { flag_start, name, value: Some(value) });
2181        consume_whitespaces(s, escape_byte);
2182    }
2183    options
2184}
2185
2186#[inline]
2187fn is_maybe_json(s: &[u8]) -> bool {
2188    // ADD/COPY: checking [[ to handle escape of [ https://docs.docker.com/reference/dockerfile/#add
2189    // Others: TODO: checking [[ to handle [[ -e .. ], but not enough to check [ -e .. ]
2190    // TODO: full json check without allocation?
2191    s.first() == Some(&b'[') && s.get(1) != Some(&b'[')
2192}
2193fn parse_json_array<'a, S: Store<UnescapedString<'a>>>(
2194    s: &mut &[u8],
2195    start: &'a str,
2196    escape_byte: u8,
2197) -> Result<(S, Span), usize> {
2198    debug_assert!(is_maybe_json(s));
2199    let mut res = S::new();
2200    let array_start = start.len() - s.len();
2201    *s = &s[1..];
2202    consume_whitespaces(s, escape_byte);
2203    let (&b, s_next) = s.split_first().ok_or(array_start)?;
2204    match b {
2205        b'"' => {
2206            *s = s_next;
2207            loop {
2208                let full_word_start = start.len() - s.len();
2209                let mut word_start = full_word_start;
2210                let mut buf = String::new();
2211                loop {
2212                    let (&b, s_next) = s.split_first().ok_or(array_start)?;
2213                    if TABLE[b as usize] & (DOUBLE_QUOTE | POSSIBLE_ESCAPE | CONTROL) == 0 {
2214                        *s = s_next;
2215                        continue;
2216                    }
2217                    match b {
2218                        b'"' => break,
2219                        _ if b < 0x20 => return Err(array_start),
2220                        _ => {}
2221                    }
2222                    let word_end = start.len() - s.len();
2223                    if consume_line_continuation(s, b, s_next, escape_byte) {
2224                        // dockerfile escape
2225                        buf.push_str(&start[word_start..word_end]);
2226                        word_start = start.len() - s.len();
2227                        continue;
2228                    }
2229                    if b == b'\\' {
2230                        // JSON escape
2231                        let word_end = start.len() - s.len();
2232                        buf.push_str(&start[word_start..word_end]);
2233                        *s = s_next;
2234                        if let Some((&b, s_next)) = s.split_first() {
2235                            consume_line_continuation(s, b, s_next, escape_byte);
2236                        }
2237                        let (&b, s_next) = s.split_first().ok_or(array_start)?;
2238                        *s = s_next;
2239                        let new = match b {
2240                            b'"' | b'\\' | b'/' => b as char,
2241                            b'b' => '\x08',
2242                            b'f' => '\x0c',
2243                            b'n' => '\n',
2244                            b'r' => '\r',
2245                            b't' => '\t',
2246                            b'u' => parse_json_hex_escape(s, escape_byte, array_start)?,
2247                            _ => return Err(array_start), // invalid escape
2248                        };
2249                        buf.push(new);
2250                        word_start = start.len() - s.len();
2251                        continue;
2252                    }
2253                    *s = s_next;
2254                }
2255                let word_end = start.len() - s.len();
2256                let value = if full_word_start == word_start {
2257                    // no escape
2258                    Cow::Borrowed(&start[word_start..word_end])
2259                } else {
2260                    buf.push_str(&start[word_start..word_end]);
2261                    Cow::Owned(buf)
2262                };
2263                res.push(UnescapedString { span: full_word_start..word_end, value });
2264                *s = &s[1..]; // drop "
2265                consume_whitespaces(s, escape_byte);
2266                let (&b, s_next) = s.split_first().ok_or(array_start)?;
2267                match b {
2268                    b',' => {
2269                        *s = s_next;
2270                        consume_whitespaces(s, escape_byte);
2271                        let (&b, s_next) = s.split_first().ok_or(array_start)?;
2272                        if b == b'"' {
2273                            *s = s_next;
2274                            continue;
2275                        }
2276                        return Err(array_start);
2277                    }
2278                    b']' => {
2279                        *s = s_next;
2280                        break;
2281                    }
2282                    _ => return Err(array_start),
2283                }
2284            }
2285        }
2286        b']' => *s = s_next,
2287        _ => return Err(array_start),
2288    }
2289    let array_end = start.len() - s.len();
2290    consume_whitespaces(s, escape_byte);
2291    if !is_line_end(s.first()) {
2292        return Err(array_start);
2293    }
2294    Ok((res, array_start..array_end))
2295}
2296// Adapted from https://github.com/serde-rs/json/blob/3f1c6de4af28b1f6c5100da323f2bffaf7c2083f/src/read.rs
2297#[cold]
2298fn parse_json_hex_escape(
2299    s: &mut &[u8],
2300    escape_byte: u8,
2301    array_start: usize,
2302) -> Result<char, usize> {
2303    fn decode_hex_escape(s: &mut &[u8], escape_byte: u8, array_start: usize) -> Result<u16, usize> {
2304        if s.len() < 4 {
2305            return Err(array_start); // EofWhileParsingString
2306        }
2307
2308        let mut n = 0;
2309        for _ in 0..4 {
2310            if let Some((&b, s_next)) = s.split_first() {
2311                consume_line_continuation(s, b, s_next, escape_byte);
2312            }
2313            let (&b, s_next) = s.split_first().ok_or(array_start)?;
2314            *s = s_next;
2315            match decode_hex_val(b) {
2316                None => return Err(array_start), // InvalidEscape
2317                Some(val) => {
2318                    n = (n << 4) + val;
2319                }
2320            }
2321        }
2322        Ok(n)
2323    }
2324
2325    fn decode_hex_val(val: u8) -> Option<u16> {
2326        let n = HEX_DECODE_TABLE[val as usize] as u16;
2327        if n == u8::MAX as u16 { None } else { Some(n) }
2328    }
2329
2330    let c = match decode_hex_escape(s, escape_byte, array_start)? {
2331        _n @ 0xDC00..=0xDFFF => return Err(array_start), // ErrorCode::LoneLeadingSurrogateInHexEscape)
2332
2333        // Non-BMP characters are encoded as a sequence of two hex
2334        // escapes, representing UTF-16 surrogates. If deserializing a
2335        // utf-8 string the surrogates are required to be paired,
2336        // whereas deserializing a byte string accepts lone surrogates.
2337        n1 @ 0xD800..=0xDBFF => {
2338            if let Some((&b, s_next)) = s.split_first() {
2339                consume_line_continuation(s, b, s_next, escape_byte);
2340            }
2341            let Some((&b'\\', s_next)) = s.split_first() else {
2342                return Err(array_start); // UnexpectedEndOfHexEscape
2343            };
2344            *s = s_next;
2345
2346            if let Some((&b, s_next)) = s.split_first() {
2347                consume_line_continuation(s, b, s_next, escape_byte);
2348            }
2349            let Some((&b'u', s_next)) = s.split_first() else {
2350                return Err(array_start); // UnexpectedEndOfHexEscape
2351            };
2352            *s = s_next;
2353
2354            let n2 = decode_hex_escape(s, escape_byte, array_start)?;
2355
2356            if n2 < 0xDC00 || n2 > 0xDFFF {
2357                return Err(array_start); // LoneLeadingSurrogateInHexEscape
2358            }
2359
2360            let n = ((((n1 - 0xD800) as u32) << 10) | (n2 - 0xDC00) as u32) + 0x1_0000;
2361
2362            match char::from_u32(n) {
2363                Some(c) => c,
2364                None => return Err(array_start), // InvalidUnicodeCodePoint
2365            }
2366        }
2367
2368        // Every u16 outside of the surrogate ranges above is guaranteed
2369        // to be a legal char.
2370        n => char::from_u32(n as u32).unwrap(),
2371    };
2372    Ok(c)
2373}
2374#[allow(clippy::needless_raw_string_hashes)]
2375#[test]
2376fn test_parse_json_array() {
2377    // empty
2378    let t = r#"[]"#;
2379    let mut s = t.as_bytes();
2380    assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[]);
2381    assert_eq!(s, b"");
2382    let t = r#"[ ]"#;
2383    let mut s = t.as_bytes();
2384    assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[]);
2385    assert_eq!(s, b"");
2386    // one value
2387    let t = r#"["abc"]"#;
2388    let mut s = t.as_bytes();
2389    assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[UnescapedString {
2390        span: 2..5,
2391        value: "abc".into()
2392    }]);
2393    assert_eq!(s, b"");
2394    // multi values
2395    let t = "[\"ab\",\"c\" ,  \"de\" ] \n";
2396    let mut s = t.as_bytes();
2397    assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[
2398        UnescapedString { span: 2..4, value: "ab".into() },
2399        UnescapedString { span: 7..8, value: "c".into() },
2400        UnescapedString { span: 14..16, value: "de".into() },
2401    ]);
2402    assert_eq!(s, b"\n");
2403    // escape
2404    let t = "[\"a\\\"\\\\\\/\\b\\f\\n\\r\\tbc\\u12ab\\uAB12\\uD83C\\uDF95\\\n\\\\\nu\\\nD\\\n8\\\n3\\\nC\\\n\\\\\nu\\\nD\\\nF\\\n9\\\n5\\\n\"]";
2405    let mut s = t.as_bytes();
2406    assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[UnescapedString {
2407        span: 2..83,
2408        value: "a\"\\/\x08\x0c\n\r\tbc\u{12ab}\u{AB12}\u{1F395}\u{1F395}".into()
2409    }]);
2410    assert_eq!(s, b"");
2411
2412    // fail (no ending)
2413    let t = r#"["]"#;
2414    let mut s = t.as_bytes();
2415    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2416    assert_eq!(s, br#""#);
2417    let t = r#"["a]"#;
2418    let mut s = t.as_bytes();
2419    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2420    assert_eq!(s, br#""#);
2421    // fail (single quote)
2422    let t = r#"['abc']"#;
2423    let mut s = t.as_bytes();
2424    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2425    assert_eq!(s, br#"'abc']"#);
2426    // fail (extra comma)
2427    let t = r#"["abc",]"#;
2428    let mut s = t.as_bytes();
2429    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2430    assert_eq!(s, br#"]"#);
2431    // fail (extra char after string)
2432    let t = r#"["abc"d]"#;
2433    let mut s = t.as_bytes();
2434    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2435    assert_eq!(s, br#"d]"#);
2436    // fail (extra char after array)
2437    let t = r#"["abc"] c"#;
2438    let mut s = t.as_bytes();
2439    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2440    assert_eq!(s, br#"c"#);
2441    // fail (invalid escape)
2442    let t = "[\"ab\\c\"]";
2443    let mut s = t.as_bytes();
2444    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2445    assert_eq!(s, b"\"]");
2446    // fail (invalid escape)
2447    let t = "[\"\\uD83C\\uFFFF\"]";
2448    let mut s = t.as_bytes();
2449    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2450    assert_eq!(s, b"\"]");
2451    // fail (control)
2452    let t = "[\"a\nb\"]";
2453    let mut s = t.as_bytes();
2454    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2455    assert_eq!(s, b"\nb\"]");
2456    // fail (control)
2457    let t = "[\"a\x1Fb\"]";
2458    let mut s = t.as_bytes();
2459    assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
2460    assert_eq!(s, b"\x1Fb\"]");
2461    // TODO: more from https://github.com/serde-rs/json/blob/3f1c6de4af28b1f6c5100da323f2bffaf7c2083f/tests/test.rs#L1079
2462}
2463
2464#[inline]
2465fn collect_here_doc_delim<'a>(
2466    s: &mut &'a [u8],
2467    mut s_next: &'a [u8],
2468    start: &'a str,
2469) -> InternalResult<'static, Option<(Cow<'a, [u8]>, bool, bool)>> {
2470    let strip_tab = if let Some((&b'-', s_next_next)) = s_next.split_first() {
2471        s_next = s_next_next;
2472        true
2473    } else {
2474        false
2475    };
2476    let delim_start = start.len() - s_next.len();
2477    let mut current_start = delim_start;
2478    let mut expand = true;
2479    let mut quote = None;
2480    let mut buf = vec![];
2481    while let Some((&b, s_next_next)) = s_next.split_first() {
2482        match b {
2483            b'"' | b'\'' => {
2484                if let Some(q) = quote {
2485                    if b == q {
2486                        quote = None;
2487                        let end = start.len() - s_next.len();
2488                        buf.extend_from_slice(&start.as_bytes()[current_start..end]);
2489                        current_start = start.len() - s_next_next.len();
2490                    }
2491                } else {
2492                    quote = Some(b);
2493                    expand = false;
2494                    let end = start.len() - s_next.len();
2495                    buf.extend_from_slice(&start.as_bytes()[current_start..end]);
2496                    current_start = start.len() - s_next_next.len();
2497                }
2498            }
2499            b'\\' => {
2500                // here-doc escape is always \ https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/parser/parser.go#L482
2501                let end = start.len() - s_next.len();
2502                buf.extend_from_slice(&start.as_bytes()[current_start..end]);
2503                current_start = start.len() - s_next_next.len();
2504                let Some((_, s_next_next)) = s_next_next.split_first() else {
2505                    return Err(error::other("unterminated escape", start.len() - s_next.len()));
2506                };
2507                s_next = s_next_next;
2508                continue;
2509            }
2510            _ if quote.is_none() && TABLE[b as usize] & (WHITESPACE | POSSIBLE_LINE) != 0 => break,
2511            _ => {}
2512        }
2513        s_next = s_next_next;
2514    }
2515    if let Some(quote) = quote {
2516        return Err(error::expected_quote(quote, None, start.len() - s_next.len()));
2517    }
2518    let end = start.len() - s_next.len();
2519    let delim = if delim_start == current_start {
2520        Cow::Borrowed(&start.as_bytes()[delim_start..end])
2521    } else {
2522        buf.extend_from_slice(&start.as_bytes()[current_start..end]);
2523        Cow::Owned(buf)
2524    };
2525    if delim.is_empty() {
2526        return Ok(None);
2527    }
2528    *s = s_next;
2529    Ok(Some((delim, strip_tab, expand)))
2530}
2531#[inline]
2532fn collect_here_doc<'a>(
2533    s: &mut &[u8],
2534    start: &'a str,
2535    delim_cow: Cow<'a, [u8]>,
2536    strip_tab: bool,
2537) -> InternalResult<'a, (Cow<'a, str>, Span)> {
2538    let delim: &[u8] = &delim_cow;
2539    let here_doc_start = start.len() - s.len();
2540    let mut current_start = here_doc_start;
2541    let mut buf = String::new();
2542    let mut end;
2543    loop {
2544        if strip_tab {
2545            // trim leading tabs
2546            if let Some((&b'\t', mut s_next)) = s.split_first() {
2547                let end = start.len() - s.len();
2548                buf.push_str(&start[current_start..end]);
2549                while let Some((&b'\t', s_next_next)) = s_next.split_first() {
2550                    s_next = s_next_next;
2551                }
2552                *s = s_next;
2553                current_start = start.len() - s.len();
2554            }
2555        }
2556        if s.len() < delim.len() {
2557            return Err(error::expected_here_doc_end(delim_cow, start.len() - s.len()));
2558        }
2559        if s.starts_with(delim) {
2560            let s_next = &s[delim.len()..];
2561            end = start.len() - s.len();
2562            if let Some((&b, s_next)) = s_next.split_first() {
2563                if consume_newline(b, s, s_next) {
2564                    break;
2565                }
2566            } else {
2567                *s = s_next;
2568                break;
2569            }
2570        }
2571        consume_current_line_no_line_continuation(s);
2572    }
2573    let span = here_doc_start..end;
2574    if here_doc_start == current_start {
2575        Ok((Cow::Borrowed(&start[span.clone()]), span))
2576    } else {
2577        buf.push_str(&start[current_start..end]);
2578        Ok((Cow::Owned(buf), span))
2579    }
2580}
2581
2582// TODO: escaped/quoted space?
2583#[inline]
2584fn collect_space_separated_consume_line<'a, S: Store<UnescapedString<'a>>>(
2585    s: &mut &'a [u8],
2586    start: &'a str,
2587    escape_byte: u8,
2588) -> S {
2589    let mut res = S::new();
2590    loop {
2591        let val = collect_non_whitespace(s, start, escape_byte);
2592        if !val.value.is_empty() {
2593            res.push(val);
2594            if consume_whitespaces(s, escape_byte) {
2595                continue;
2596            }
2597        }
2598        if let Some((&b, s_next)) = s.split_first() {
2599            let consumed = consume_newline(b, s, s_next);
2600            debug_assert!(consumed);
2601        }
2602        break;
2603    }
2604    res
2605}
2606#[inline]
2607fn collect_non_whitespace<'a>(
2608    s: &mut &[u8],
2609    start: &'a str,
2610    escape_byte: u8,
2611) -> UnescapedString<'a> {
2612    collect_until::<{ WHITESPACE | POSSIBLE_LINE }>(s, start, escape_byte)
2613}
2614#[inline]
2615fn collect_until<'a, const UNTIL_MASK: u8>(
2616    s: &mut &[u8],
2617    start: &'a str,
2618    escape_byte: u8,
2619) -> UnescapedString<'a> {
2620    let full_word_start = start.len() - s.len();
2621    let mut word_start = full_word_start;
2622    let mut buf = String::new();
2623    while let Some((&b, s_next)) = s.split_first() {
2624        let t = TABLE[b as usize];
2625        if t & (UNTIL_MASK | POSSIBLE_ESCAPE) != 0 {
2626            if t & UNTIL_MASK != 0 {
2627                break;
2628            }
2629            let word_end = start.len() - s.len();
2630            if consume_line_continuation(s, b, s_next, escape_byte) {
2631                buf.push_str(&start[word_start..word_end]);
2632                word_start = start.len() - s.len();
2633                continue;
2634            }
2635        }
2636        *s = s_next;
2637    }
2638    let word_end = start.len() - s.len();
2639    let value = if full_word_start == word_start {
2640        // no escape
2641        Cow::Borrowed(&start[word_start..word_end])
2642    } else {
2643        buf.push_str(&start[word_start..word_end]);
2644        Cow::Owned(buf)
2645    };
2646    UnescapedString { span: full_word_start..word_end, value }
2647}
2648#[inline]
2649fn collect_until_line_consume_newline<'a>(
2650    s: &mut &[u8],
2651    start: &'a str,
2652    escape_byte: u8,
2653) -> UnescapedString<'a> {
2654    let full_word_start = start.len() - s.len();
2655    let mut word_start = full_word_start;
2656    let mut buf = String::new();
2657    let word_end;
2658    loop {
2659        let Some((&b, s_next)) = s.split_first() else {
2660            word_end = start.len() - s.len();
2661            break;
2662        };
2663        let t = TABLE[b as usize];
2664        if t & (POSSIBLE_LINE | POSSIBLE_ESCAPE) != 0 {
2665            match b {
2666                b'\n' => {
2667                    word_end = start.len() - s.len();
2668                    *s = s_next;
2669                    break;
2670                }
2671                b'\r' => {
2672                    if s_next.first() == Some(&b'\n') {
2673                        word_end = start.len() - s.len();
2674                        *s = &s_next[1..];
2675                        break;
2676                    }
2677                }
2678                _ => {
2679                    let word_end = start.len() - s.len();
2680                    if consume_line_continuation(s, b, s_next, escape_byte) {
2681                        buf.push_str(&start[word_start..word_end]);
2682                        word_start = start.len() - s.len();
2683                        continue;
2684                    }
2685                }
2686            }
2687        }
2688        *s = s_next;
2689    }
2690    let value = if full_word_start == word_start {
2691        // no escape
2692        Cow::Borrowed(&start[word_start..word_end])
2693    } else {
2694        buf.push_str(&start[word_start..word_end]);
2695        Cow::Owned(buf)
2696    };
2697    UnescapedString { span: full_word_start..word_end, value }
2698}
2699
2700/// Consumes a newline if present and returns `true` if consumed.
2701/// (not consumes non-newline characters)
2702#[inline(always)]
2703fn consume_newline<'a>(b: u8, s: &mut &'a [u8], s_next: &'a [u8]) -> bool {
2704    match b {
2705        b'\n' => {
2706            *s = s_next;
2707            return true;
2708        }
2709        b'\r' => {
2710            if s_next.first() == Some(&b'\n') {
2711                *s = &s_next[1..];
2712                return true;
2713            }
2714        }
2715        _ => {}
2716    }
2717    false
2718}
2719
2720/// Consumes a line continuation if present and returns `true` if consumed.
2721#[inline]
2722fn consume_line_continuation<'a>(
2723    s: &mut &'a [u8],
2724    b: u8,
2725    s_next: &'a [u8],
2726    escape_byte: u8,
2727) -> bool {
2728    #[inline]
2729    fn followup(s: &mut &[u8], _escape_byte: u8) {
2730        while let Some((&b, mut s_next)) = s.split_first() {
2731            let t = TABLE[b as usize];
2732            if t & (WHITESPACE | POSSIBLE_LINE | COMMENT) == 0 {
2733                break;
2734            }
2735            let mut b = b;
2736            if t & WHITESPACE != 0 {
2737                // TODO: escape after spaces is handled in consume_whitespaces_no_line_continuation
2738                consume_whitespaces_no_line_continuation(&mut s_next);
2739                let Some((&b_, s_next_next)) = s_next.split_first() else { break };
2740                let t = TABLE[b_ as usize];
2741                if t & (COMMENT | POSSIBLE_LINE) == 0 {
2742                    break;
2743                }
2744                b = b_;
2745                s_next = s_next_next;
2746            }
2747            *s = s_next;
2748            // comment or empty continuation line
2749            // b == \r case never happen because handled in the above consume_whitespaces_no_line_continuation
2750            if b != b'\n' {
2751                consume_current_line_no_line_continuation(s);
2752            }
2753        }
2754    }
2755
2756    if b == escape_byte {
2757        cold_path();
2758        if let Some((&b, mut s_next)) = s_next.split_first() {
2759            if consume_newline(b, s, s_next) {
2760                followup(s, escape_byte);
2761                return true;
2762            }
2763            // "\\[ \t]\n" is also accepted.
2764            // https://github.com/moby/buildkit/blob/v0.30/frontend/dockerfile/parser/parser.go#L168
2765            if TABLE[b as usize] & SPACE != 0 {
2766                cold_path();
2767                consume_whitespaces_no_line_continuation(&mut s_next);
2768                if let Some((&b, s_next)) = s_next.split_first() {
2769                    if consume_newline(b, s, s_next) {
2770                        followup(s, escape_byte);
2771                        return true;
2772                    }
2773                }
2774            }
2775        }
2776    }
2777    false
2778}
2779
2780/// Consumes until whitespace/line character found without line continuation handling,
2781/// and returns `true` if one or more non-whitespace characters are present.
2782/// (not consumes whitespace/line character)
2783#[inline]
2784fn consume_until_whitespaces_or_line_no_line_continuation(s: &mut &[u8]) -> bool {
2785    let start = *s;
2786    while let Some((&b, s_next)) = s.split_first() {
2787        if TABLE[b as usize] & (WHITESPACE | POSSIBLE_LINE) != 0 {
2788            break;
2789        }
2790        *s = s_next;
2791    }
2792    start.len() != s.len()
2793}
2794
2795/// Consumes the current line without line continuation handling.
2796/// (consumes newline characters of the current line)
2797#[inline]
2798fn consume_current_line_no_line_continuation(s: &mut &[u8]) {
2799    while let Some((&b, s_next)) = s.split_first() {
2800        if consume_newline(b, s, s_next) {
2801            break;
2802        }
2803        *s = s_next;
2804    }
2805}
2806/// Consumes the current line.
2807/// (consumes newline characters of the current line)
2808#[inline]
2809fn consume_current_line(s: &mut &[u8], escape_byte: u8) {
2810    let mut has_whitespace_only = 0;
2811    while let Some((&b, s_next)) = s.split_first() {
2812        let t = TABLE[b as usize];
2813        if t & (POSSIBLE_LINE | COMMENT | POSSIBLE_ESCAPE) != 0 {
2814            if consume_newline(b, s, s_next) {
2815                break;
2816            }
2817            if has_whitespace_only != 0 && t & COMMENT != 0 {
2818                *s = s_next;
2819                consume_current_line_no_line_continuation(s);
2820                continue;
2821            }
2822            if consume_line_continuation(s, b, s_next, escape_byte) {
2823                has_whitespace_only = WHITESPACE;
2824                continue;
2825            }
2826        }
2827        has_whitespace_only &= t;
2828        *s = s_next;
2829    }
2830}
2831
2832/// Consumes whitespaces without line continuation handling, and returns `true`
2833/// if one or more whitespaces ware consumed.
2834/// (not consumes non-whitespace characters)
2835#[inline]
2836fn consume_whitespaces_no_line_continuation(s: &mut &[u8]) -> bool {
2837    let start = *s;
2838    while let Some((&b, s_next)) = s.split_first() {
2839        if TABLE[b as usize] & WHITESPACE != 0 {
2840            *s = s_next;
2841            continue;
2842        }
2843        break;
2844    }
2845    start.len() != s.len()
2846}
2847/// Consumes whitespaces, and returns `true`
2848/// if one or more whitespaces ware consumed.
2849/// (not consumes non-whitespace characters)
2850#[inline]
2851fn consume_whitespaces(s: &mut &[u8], escape_byte: u8) -> bool {
2852    let mut has_space = false;
2853    while let Some((&b, s_next)) = s.split_first() {
2854        let t = TABLE[b as usize];
2855        if t & (WHITESPACE | POSSIBLE_ESCAPE) != 0 {
2856            if t & WHITESPACE != 0 {
2857                *s = s_next;
2858                has_space = true;
2859                continue;
2860            }
2861            if consume_line_continuation(s, b, s_next, escape_byte) {
2862                continue;
2863            }
2864        }
2865        break;
2866    }
2867    has_space
2868}
2869/// Consumes whitespaces, and returns `true`
2870/// if one or more whitespaces ware consumed or reached line end.
2871/// (not consumes non-whitespace characters)
2872#[inline]
2873fn consume_whitespaces_or_is_empty_line(s: &mut &[u8], escape_byte: u8) -> bool {
2874    let mut has_space = false;
2875    loop {
2876        let Some((&b, s_next)) = s.split_first() else { return true };
2877        {
2878            let t = TABLE[b as usize];
2879            if t & (WHITESPACE | POSSIBLE_ESCAPE | POSSIBLE_LINE) != 0 {
2880                if t & WHITESPACE != 0 {
2881                    *s = s_next;
2882                    has_space = true;
2883                    continue;
2884                }
2885                if b == b'\n' {
2886                    return true;
2887                }
2888                if consume_line_continuation(s, b, s_next, escape_byte) {
2889                    continue;
2890                }
2891            }
2892            break;
2893        }
2894    }
2895    has_space
2896}
2897/// Consumes whitespaces, whitespace/empty lines, and comment lines.
2898#[inline]
2899fn consume_comments_and_whitespaces(s: &mut &[u8], escape_byte: u8) {
2900    while let Some((&b, s_next)) = s.split_first() {
2901        let t = TABLE[b as usize];
2902        if t & (WHITESPACE | POSSIBLE_LINE | COMMENT | POSSIBLE_ESCAPE) != 0 {
2903            if t & (WHITESPACE | POSSIBLE_LINE) != 0 {
2904                *s = s_next;
2905                continue;
2906            }
2907            if t & COMMENT != 0 {
2908                *s = s_next;
2909                consume_current_line_no_line_continuation(s);
2910                continue;
2911            }
2912            if consume_line_continuation(s, b, s_next, escape_byte) {
2913                continue;
2914            }
2915        }
2916        break;
2917    }
2918}
2919
2920#[inline]
2921#[track_caller]
2922fn trim_end(text: &str, start: usize, mut end: usize) -> &str {
2923    while start < end {
2924        let next_end = end - 1;
2925        if let Some(&b) = text.as_bytes().get(next_end) {
2926            if TABLE[b as usize] & (WHITESPACE | POSSIBLE_LINE) != 0 {
2927                end = next_end;
2928                continue;
2929            }
2930        }
2931        break;
2932    }
2933    &text[start..end]
2934}
2935
2936#[inline(always)]
2937fn token(s: &mut &[u8], token: &'static [u8]) -> bool {
2938    let matched = starts_with_ignore_ascii_case(s, token);
2939    if matched {
2940        *s = &s[token.len()..];
2941        true
2942    } else {
2943        false
2944    }
2945}
2946#[cold]
2947fn token_slow(s: &mut &[u8], mut token: &'static [u8], escape_byte: u8) -> bool {
2948    debug_assert!(!token.is_empty() && token.iter().all(|&n| n & TO_UPPER8 == n));
2949    if s.len() < token.len() {
2950        return false;
2951    }
2952    let mut tmp = *s;
2953    while let Some((&b, tmp_next)) = tmp.split_first() {
2954        if b & TO_UPPER8 == token[0] {
2955            tmp = tmp_next;
2956            token = &token[1..];
2957            if token.is_empty() {
2958                *s = tmp;
2959                return true;
2960            }
2961            continue;
2962        }
2963        if consume_line_continuation(&mut tmp, b, tmp_next, escape_byte) {
2964            continue;
2965        }
2966        break;
2967    }
2968    false
2969}
2970
2971const TO_UPPER8: u8 = 0xDF;
2972const TO_UPPER64: u64 = 0xDFDF_DFDF_DFDF_DFDF;
2973
2974#[inline(always)] // Ensure the code getting the length of the needle is inlined.
2975fn starts_with_ignore_ascii_case(mut s: &[u8], mut needle: &'static [u8]) -> bool {
2976    debug_assert!(!needle.is_empty() && needle.iter().all(|&n| n & TO_UPPER8 == n));
2977    if s.len() < needle.len() {
2978        return false;
2979    }
2980    if needle.len() == 1 {
2981        return needle[0] == s[0] & TO_UPPER8;
2982    }
2983    if needle.len() >= 8 {
2984        loop {
2985            if u64::from_ne_bytes(needle[..8].try_into().unwrap())
2986                != u64::from_ne_bytes(s[..8].try_into().unwrap()) & TO_UPPER64
2987            {
2988                return false;
2989            }
2990            needle = &needle[8..];
2991            s = &s[8..];
2992            if needle.len() < 8 {
2993                if needle.is_empty() {
2994                    return true;
2995                }
2996                break;
2997            }
2998        }
2999    }
3000    let s = {
3001        let mut buf = [0; 8];
3002        buf[..needle.len()].copy_from_slice(&s[..needle.len()]);
3003        u64::from_ne_bytes(buf)
3004    };
3005    let needle = {
3006        let mut buf = [0; 8];
3007        buf[..needle.len()].copy_from_slice(needle);
3008        u64::from_ne_bytes(buf)
3009    };
3010    needle == s & TO_UPPER64
3011}
3012#[test]
3013fn test_starts_with_ignore_ascii_case() {
3014    assert!(starts_with_ignore_ascii_case(b"ABC", b"ABC"));
3015    assert!(starts_with_ignore_ascii_case(b"abc", b"ABC"));
3016    assert!(starts_with_ignore_ascii_case(b"AbC", b"ABC"));
3017    assert!(!starts_with_ignore_ascii_case(b"ABB", b"ABC"));
3018    assert!(starts_with_ignore_ascii_case(b"ABCDEFGH", b"ABCDEFGH"));
3019    assert!(starts_with_ignore_ascii_case(b"abcdefgh", b"ABCDEFGH"));
3020    assert!(starts_with_ignore_ascii_case(b"AbCdEfGh", b"ABCDEFGH"));
3021    assert!(!starts_with_ignore_ascii_case(b"ABCDEFGc", b"ABCDEFGH"));
3022    assert!(starts_with_ignore_ascii_case(
3023        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
3024        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
3025    ));
3026    assert!(starts_with_ignore_ascii_case(
3027        b"abcdefghijklmnopqrstuvwxyz",
3028        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
3029    ));
3030    assert!(starts_with_ignore_ascii_case(
3031        b"aBcDeFgHiJkLmNoPqRsTuVwXyZ",
3032        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
3033    ));
3034    assert!(!starts_with_ignore_ascii_case(
3035        b"aBcDeFgHiJkLmNoPqRsTuVwXyc",
3036        b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
3037    ));
3038}