#![doc(test(
no_crate_inject,
attr(
deny(warnings, rust_2018_idioms, single_use_lifetimes),
allow(dead_code, unused_variables)
)
))]
#![forbid(unsafe_code)]
#![warn(
missing_debug_implementations,
missing_docs,
clippy::alloc_instead_of_core,
clippy::exhaustive_enums,
clippy::exhaustive_structs,
clippy::impl_trait_in_params,
clippy::std_instead_of_core,
)]
#![allow(clippy::inline_always)]
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
const _README: () = ();
#[cfg(test)]
#[path = "gen/tests/assert_impl.rs"]
mod assert_impl;
mod error;
use std::{borrow::Cow, collections::HashMap, mem, ops::Range, str};
use smallvec::SmallVec;
pub use self::error::Error;
use self::error::{ErrorKind, Result};
#[allow(clippy::missing_panics_doc)]
pub fn parse(text: &str) -> Result<Dockerfile<'_>> {
let mut p = ParseIter::new(text)?;
let mut s = p.s;
let mut instructions = Vec::with_capacity(p.text.len() / 60);
let mut stages = Vec::with_capacity(1);
let mut named_stages = 0;
let mut current_stage = None;
while let Some((&b, s_next)) = s.split_first() {
let instruction =
parse_instruction(&mut p, &mut s, b, s_next).map_err(|e| e.into_error(&p))?;
match instruction {
Instruction::From(from) => {
named_stages += from.as_.is_some() as usize;
let new_stage = instructions.len();
if let Some(prev_stage) = current_stage.replace(new_stage) {
stages.push(prev_stage..new_stage);
}
instructions.push(Instruction::From(from));
}
arg @ Instruction::Arg(..) => instructions.push(arg),
instruction => {
if current_stage.is_none() {
return Err(ErrorKind::Expected("FROM", instruction.instruction_span().start)
.into_error(&p));
}
instructions.push(instruction);
}
}
skip_comments_and_whitespaces(&mut s, p.escape_byte);
}
if let Some(current_stage) = current_stage {
stages.push(current_stage..instructions.len());
}
if stages.is_empty() {
return Err(ErrorKind::NoStages.into_error(&p));
}
let mut stages_by_name = HashMap::with_capacity(named_stages);
for (i, stage) in stages.iter().enumerate() {
let Instruction::From(from) = &instructions[stage.start] else { unreachable!() };
if let Some((_as, name)) = &from.as_ {
if let Some(first_occurrence) = stages_by_name.insert(name.value.clone(), i) {
let Instruction::From(from) = &instructions[stages[first_occurrence].start] else {
unreachable!()
};
let first = from.as_.as_ref().unwrap().1.span.clone();
let second = name.span.clone();
return Err(ErrorKind::DuplicateName { first, second }.into_error(&p));
}
}
}
Ok(Dockerfile { parser_directives: p.parser_directives, instructions, stages, stages_by_name })
}
pub fn parse_iter(text: &str) -> Result<ParseIter<'_>> {
ParseIter::new(text)
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct Dockerfile<'a> {
pub parser_directives: ParserDirectives<'a>,
pub instructions: Vec<Instruction<'a>>,
#[cfg_attr(feature = "serde", serde(skip))]
stages: Vec<Range<usize>>,
#[cfg_attr(feature = "serde", serde(skip))]
stages_by_name: HashMap<Cow<'a, str>, usize>,
}
impl<'a> Dockerfile<'a> {
#[allow(clippy::missing_panics_doc)] #[must_use]
pub fn global_args<'b>(&'b self) -> impl ExactSizeIterator<Item = &'b ArgInstruction<'a>> {
self.instructions[..self.stages.first().unwrap().start].iter().map(|arg| {
let Instruction::Arg(arg) = arg else { unreachable!() };
arg
})
}
#[must_use]
pub fn stage<'b>(&'b self, name: &str) -> Option<Stage<'a, 'b>> {
let i = *self.stages_by_name.get(name)?;
let stage = &self.stages[i];
let Instruction::From(from) = &self.instructions[stage.start] else { unreachable!() };
Some(Stage { from, instructions: &self.instructions[stage.start + 1..stage.end] })
}
#[must_use]
pub fn stages<'b>(&'b self) -> impl ExactSizeIterator<Item = Stage<'a, 'b>> {
self.stages.iter().map(move |stage| {
let Instruction::From(from) = &self.instructions[stage.start] else { unreachable!() };
Stage { from, instructions: &self.instructions[stage.start + 1..stage.end] }
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Stage<'a, 'b> {
pub from: &'b FromInstruction<'a>,
pub instructions: &'b [Instruction<'a>],
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct ParserDirectives<'a> {
pub syntax: Option<ParserDirective<&'a str>>,
pub escape: Option<ParserDirective<char>>,
pub check: Option<ParserDirective<&'a str>>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct ParserDirective<T> {
start: usize,
pub value: Spanned<T>,
}
impl<T> ParserDirective<T> {
#[must_use]
pub fn span(&self) -> Span {
self.start..self.value.span.end
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind"))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
#[non_exhaustive]
pub enum Instruction<'a> {
Add(AddInstruction<'a>),
Arg(ArgInstruction<'a>),
Cmd(CmdInstruction<'a>),
Copy(CopyInstruction<'a>),
Entrypoint(EntrypointInstruction<'a>),
Env(EnvInstruction<'a>),
Expose(ExposeInstruction<'a>),
From(FromInstruction<'a>),
Healthcheck(HealthcheckInstruction<'a>),
Label(LabelInstruction<'a>),
Maintainer(MaintainerInstruction<'a>),
Onbuild(OnbuildInstruction<'a>),
Run(RunInstruction<'a>),
Shell(ShellInstruction<'a>),
Stopsignal(StopsignalInstruction<'a>),
User(UserInstruction<'a>),
Volume(VolumeInstruction<'a>),
Workdir(WorkdirInstruction<'a>),
}
impl Instruction<'_> {
fn instruction_span(&self) -> Span {
match self {
Instruction::Add(instruction) => instruction.add.span.clone(),
Instruction::Arg(instruction) => instruction.arg.span.clone(),
Instruction::Cmd(instruction) => instruction.cmd.span.clone(),
Instruction::Copy(instruction) => instruction.copy.span.clone(),
Instruction::Entrypoint(instruction) => instruction.entrypoint.span.clone(),
Instruction::Env(instruction) => instruction.env.span.clone(),
Instruction::Expose(instruction) => instruction.expose.span.clone(),
Instruction::From(instruction) => instruction.from.span.clone(),
Instruction::Healthcheck(instruction) => instruction.healthcheck.span.clone(),
Instruction::Label(instruction) => instruction.label.span.clone(),
Instruction::Maintainer(instruction) => instruction.maintainer.span.clone(),
Instruction::Onbuild(instruction) => instruction.onbuild.span.clone(),
Instruction::Run(instruction) => instruction.run.span.clone(),
Instruction::Shell(instruction) => instruction.shell.span.clone(),
Instruction::Stopsignal(instruction) => instruction.stopsignal.span.clone(),
Instruction::User(instruction) => instruction.user.span.clone(),
Instruction::Volume(instruction) => instruction.volume.span.clone(),
Instruction::Workdir(instruction) => instruction.workdir.span.clone(),
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct AddInstruction<'a> {
pub add: Keyword,
pub options: SmallVec<[Flag<'a>; 1]>,
pub src: SmallVec<[Source<'a>; 1]>,
pub dest: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct ArgInstruction<'a> {
pub arg: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct CmdInstruction<'a> {
pub cmd: Keyword,
pub arguments: Command<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct CopyInstruction<'a> {
pub copy: Keyword,
pub options: SmallVec<[Flag<'a>; 1]>,
pub src: SmallVec<[Source<'a>; 1]>,
pub dest: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Source<'a> {
Path(UnescapedString<'a>),
HereDoc(HereDoc<'a>),
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct EntrypointInstruction<'a> {
pub entrypoint: Keyword,
pub arguments: Command<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct EnvInstruction<'a> {
pub env: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct ExposeInstruction<'a> {
pub expose: Keyword,
pub arguments: SmallVec<[UnescapedString<'a>; 1]>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct FromInstruction<'a> {
pub from: Keyword,
pub options: Vec<Flag<'a>>,
pub image: UnescapedString<'a>,
pub as_: Option<(Keyword, UnescapedString<'a>)>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct HealthcheckInstruction<'a> {
pub healthcheck: Keyword,
pub options: Vec<Flag<'a>>,
pub arguments: HealthcheckArguments<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind"))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
#[non_exhaustive]
pub enum HealthcheckArguments<'a> {
#[non_exhaustive]
Cmd {
cmd: Keyword,
arguments: Command<'a>,
},
#[non_exhaustive]
None {
none: Keyword,
},
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct LabelInstruction<'a> {
pub label: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct MaintainerInstruction<'a> {
pub maintainer: Keyword,
pub name: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct OnbuildInstruction<'a> {
pub onbuild: Keyword,
pub instruction: Box<Instruction<'a>>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct RunInstruction<'a> {
pub run: Keyword,
pub options: SmallVec<[Flag<'a>; 1]>,
pub arguments: Command<'a>,
pub here_docs: Vec<HereDoc<'a>>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct ShellInstruction<'a> {
pub shell: Keyword,
pub arguments: SmallVec<[UnescapedString<'a>; 4]>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct StopsignalInstruction<'a> {
pub stopsignal: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct UserInstruction<'a> {
pub user: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct VolumeInstruction<'a> {
pub volume: Keyword,
pub arguments: JsonOrStringArray<'a, 1>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct WorkdirInstruction<'a> {
pub workdir: Keyword,
pub arguments: UnescapedString<'a>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct Keyword {
#[allow(missing_docs)]
pub span: Span,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct Flag<'a> {
flag_start: usize,
pub name: UnescapedString<'a>,
pub value: Option<UnescapedString<'a>>,
}
impl Flag<'_> {
#[must_use]
pub fn flag_span(&self) -> Span {
self.flag_start..self.name.span.end
}
#[must_use]
pub fn span(&self) -> Span {
match &self.value {
Some(v) => self.flag_start..v.span.end,
None => self.flag_span(),
}
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct UnescapedString<'a> {
#[allow(missing_docs)]
pub span: Span,
#[allow(missing_docs)]
pub value: Cow<'a, str>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Command<'a> {
Exec(Spanned<SmallVec<[UnescapedString<'a>; 1]>>),
Shell(Spanned<&'a str>),
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::exhaustive_enums)]
pub enum JsonOrStringArray<'a, const N: usize> {
Json(Spanned<SmallVec<[UnescapedString<'a>; N]>>),
String(SmallVec<[UnescapedString<'a>; N]>),
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub struct HereDoc<'a> {
#[allow(missing_docs)]
pub span: Span,
pub expand: bool,
#[allow(missing_docs)]
pub value: Cow<'a, str>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::exhaustive_structs)]
pub struct Spanned<T> {
#[allow(missing_docs)]
pub span: Span,
#[allow(missing_docs)]
pub value: T,
}
#[allow(missing_docs)]
pub type Span = Range<usize>;
#[allow(missing_debug_implementations)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct ParseIter<'a> {
text: &'a str,
s: &'a [u8],
escape_byte: u8,
has_stage: bool,
in_onbuild: bool,
parser_directives: ParserDirectives<'a>,
}
impl<'a> ParseIter<'a> {
fn new(mut text: &'a str) -> Result<Self> {
if text.as_bytes().starts_with(UTF8_BOM) {
text = &text[UTF8_BOM.len()..];
}
let mut p = Self {
text,
s: text.as_bytes(),
escape_byte: DEFAULT_ESCAPE_BYTE,
has_stage: false,
in_onbuild: false,
parser_directives: ParserDirectives {
syntax: None,
escape: None,
check: None,
},
};
parse_parser_directives(&mut p).map_err(|e| e.into_error(&p))?;
skip_comments_and_whitespaces(&mut p.s, p.escape_byte);
Ok(p)
}
}
impl<'a> Iterator for ParseIter<'a> {
type Item = Result<Instruction<'a>>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let p = self;
let mut s = p.s;
if let Some((&b, s_next)) = s.split_first() {
let instruction = match parse_instruction(p, &mut s, b, s_next) {
Ok(i) => i,
Err(e) => return Some(Err(e.into_error(p))),
};
match &instruction {
Instruction::From(..) => {
p.has_stage = true;
}
Instruction::Arg(..) => {}
instruction => {
if !p.has_stage {
return Some(Err(ErrorKind::Expected(
"FROM",
instruction.instruction_span().start,
)
.into_error(p)));
}
}
}
skip_comments_and_whitespaces(&mut s, p.escape_byte);
p.s = s;
return Some(Ok(instruction));
}
if !p.has_stage {
return Some(Err(ErrorKind::NoStages.into_error(p)));
}
None
}
}
const DEFAULT_ESCAPE_BYTE: u8 = b'\\';
fn parse_parser_directives(p: &mut ParseIter<'_>) -> Result<(), ErrorKind> {
while let Some((&b'#', s_next)) = p.s.split_first() {
p.s = s_next;
skip_spaces_no_escape(&mut p.s);
let directive_start = p.text.len() - p.s.len();
if token(&mut p.s, b"SYNTAX") {
skip_spaces_no_escape(&mut p.s);
if let Some((&b'=', s_next)) = p.s.split_first() {
p.s = s_next;
if p.parser_directives.syntax.is_some() {
p.parser_directives.syntax = None;
p.parser_directives.escape = None;
p.parser_directives.check = None;
p.escape_byte = DEFAULT_ESCAPE_BYTE;
skip_this_line_no_escape(&mut p.s);
break;
}
skip_spaces_no_escape(&mut p.s);
let value_start = p.text.len() - p.s.len();
skip_non_whitespace_no_escape(&mut p.s);
let end = p.text.len() - p.s.len();
let value = p.text[value_start..end].trim_ascii_end();
p.parser_directives.syntax = Some(ParserDirective {
start: directive_start,
value: Spanned { span: value_start..value_start + value.len(), value },
});
skip_this_line_no_escape(&mut p.s);
continue;
}
} else if token(&mut p.s, b"CHECK") {
skip_spaces_no_escape(&mut p.s);
if let Some((&b'=', s_next)) = p.s.split_first() {
p.s = s_next;
if p.parser_directives.check.is_some() {
p.parser_directives.syntax = None;
p.parser_directives.escape = None;
p.parser_directives.check = None;
p.escape_byte = DEFAULT_ESCAPE_BYTE;
skip_this_line_no_escape(&mut p.s);
break;
}
skip_spaces_no_escape(&mut p.s);
let value_start = p.text.len() - p.s.len();
skip_non_whitespace_no_escape(&mut p.s);
let end = p.text.len() - p.s.len();
let value = p.text[value_start..end].trim_ascii_end();
p.parser_directives.check = Some(ParserDirective {
start: directive_start,
value: Spanned { span: value_start..value_start + value.len(), value },
});
skip_this_line_no_escape(&mut p.s);
continue;
}
} else if token(&mut p.s, b"ESCAPE") {
skip_spaces_no_escape(&mut p.s);
if let Some((&b'=', s_next)) = p.s.split_first() {
p.s = s_next;
if p.parser_directives.escape.is_some() {
p.parser_directives.syntax = None;
p.parser_directives.escape = None;
p.parser_directives.check = None;
p.escape_byte = DEFAULT_ESCAPE_BYTE;
skip_this_line_no_escape(&mut p.s);
break;
}
skip_spaces_no_escape(&mut p.s);
let value_start = p.text.len() - p.s.len();
skip_non_whitespace_no_escape(&mut p.s);
let end = p.text.len() - p.s.len();
let value = p.text[value_start..end].trim_ascii_end();
match value {
"`" => p.escape_byte = b'`',
"\\" => {}
_ => return Err(ErrorKind::InvalidEscape { escape_start: value_start }),
}
p.parser_directives.escape = Some(ParserDirective {
start: directive_start,
value: Spanned {
span: value_start..value_start + value.len(),
value: p.escape_byte as char,
},
});
skip_this_line_no_escape(&mut p.s);
continue;
}
}
skip_this_line_no_escape(&mut p.s);
break;
}
Ok(())
}
#[inline]
fn parse_instruction<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
b: u8,
s_next: &'a [u8],
) -> Result<Instruction<'a>, ErrorKind> {
let instruction_start = p.text.len() - s.len();
*s = s_next;
match b & TO_UPPER8 {
b'A' => {
if token(s, &b"ARG"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_arg(p, s, Keyword { span: instruction_span });
}
} else if token(s, &b"ADD"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
let add = Keyword { span: instruction_span };
let (options, src, dest) = parse_add_or_copy(p, s, &add)?;
return Ok(Instruction::Add(AddInstruction { add, options, src, dest }));
}
} else if token_slow(s, &b"ARG"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_arg(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"ADD"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
let add = Keyword { span: instruction_span };
let (options, src, dest) = parse_add_or_copy(p, s, &add)?;
return Ok(Instruction::Add(AddInstruction { add, options, src, dest }));
}
}
}
b'C' => {
if token(s, &b"COPY"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
let copy = Keyword { span: instruction_span };
let (options, src, dest) = parse_add_or_copy(p, s, ©)?;
return Ok(Instruction::Copy(CopyInstruction { copy, options, src, dest }));
}
} else if token(s, &b"CMD"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_cmd(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"COPY"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
let copy = Keyword { span: instruction_span };
let (options, src, dest) = parse_add_or_copy(p, s, ©)?;
return Ok(Instruction::Copy(CopyInstruction { copy, options, src, dest }));
}
} else if token_slow(s, &b"CMD"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_cmd(p, s, Keyword { span: instruction_span });
}
}
}
b'E' => {
if token(s, &b"ENV"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_env(p, s, Keyword { span: instruction_span });
}
} else if token(s, &b"EXPOSE"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_expose(p, s, Keyword { span: instruction_span });
}
} else if token(s, &b"ENTRYPOINT"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_entrypoint(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"ENV"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_env(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"EXPOSE"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_expose(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"ENTRYPOINT"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_entrypoint(p, s, Keyword { span: instruction_span });
}
}
}
b'F' => {
if token(s, &b"FROM"[1..]) || token_slow(s, &b"FROM"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_from(p, s, Keyword { span: instruction_span });
}
}
}
b'H' => {
if token(s, &b"HEALTHCHECK"[1..]) || token_slow(s, &b"HEALTHCHECK"[1..], p.escape_byte)
{
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_healthcheck(p, s, Keyword { span: instruction_span });
}
}
}
b'L' => {
if token(s, &b"LABEL"[1..]) || token_slow(s, &b"LABEL"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_label(p, s, Keyword { span: instruction_span });
}
}
}
b'M' => {
if token(s, &b"MAINTAINER"[1..]) || token_slow(s, &b"MAINTAINER"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_maintainer(p, s, Keyword { span: instruction_span });
}
}
}
b'O' => {
if token(s, &b"ONBUILD"[1..]) || token_slow(s, &b"ONBUILD"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_onbuild(p, s, Keyword { span: instruction_span });
}
}
}
b'R' => {
if token(s, &b"RUN"[1..]) || token_slow(s, &b"RUN"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_run(p, s, Keyword { span: instruction_span });
}
}
}
b'S' => {
if token(s, &b"SHELL"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_shell(p, s, Keyword { span: instruction_span });
}
} else if token(s, &b"STOPSIGNAL"[1..]) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_stopsignal(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"SHELL"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_shell(p, s, Keyword { span: instruction_span });
}
} else if token_slow(s, &b"STOPSIGNAL"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_stopsignal(p, s, Keyword { span: instruction_span });
}
}
}
b'U' => {
if token(s, &b"USER"[1..]) || token_slow(s, &b"USER"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_user(p, s, Keyword { span: instruction_span });
}
}
}
b'V' => {
if token(s, &b"VOLUME"[1..]) || token_slow(s, &b"VOLUME"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_volume(p, s, Keyword { span: instruction_span });
}
}
}
b'W' => {
if token(s, &b"WORKDIR"[1..]) || token_slow(s, &b"WORKDIR"[1..], p.escape_byte) {
let instruction_span = instruction_start..p.text.len() - s.len();
if spaces_or_line_end(s, p.escape_byte) {
return parse_workdir(p, s, Keyword { span: instruction_span });
}
}
}
_ => {}
}
Err(ErrorKind::UnknownInstruction { instruction_start })
}
#[inline]
fn parse_arg<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"ARG",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Arg(ArgInstruction { arg: instruction, arguments }))
}
#[inline]
fn parse_add_or_copy<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: &Keyword,
) -> Result<(SmallVec<[Flag<'a>; 1]>, SmallVec<[Source<'a>; 1]>, UnescapedString<'a>), ErrorKind> {
debug_assert!(
token_slow(&mut p.text[instruction.span.clone()].as_bytes(), b"ADD", p.escape_byte,)
|| token_slow(&mut p.text[instruction.span.clone()].as_bytes(), b"COPY", p.escape_byte,)
);
let options = parse_options(s, p.text, p.escape_byte);
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok(((src, dest), _array_span)) = parse_json_array::<(
SmallVec<[Source<'_>; 1]>,
Option<_>,
)>(&mut tmp, p.text, p.escape_byte)
{
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
if src.is_empty() {
return Err(ErrorKind::AtLeastTwoArguments {
instruction_start: instruction.span.start,
});
}
return Ok((options, src, dest.unwrap()));
}
}
let (mut src, dest) = collect_space_separated_unescaped_consume_line::<(
SmallVec<[Source<'_>; 1]>,
Option<_>,
)>(s, p.text, p.escape_byte);
if src.is_empty() {
return Err(ErrorKind::AtLeastTwoArguments { instruction_start: instruction.span.start });
}
for src in &mut src {
let Source::Path(path) = src else { unreachable!() };
let Some(mut delim) = path.value.as_bytes().strip_prefix(b"<<") else { continue };
if delim.is_empty() {
continue;
}
let mut strip_tab = false;
let mut quote = None;
if let Some((&b'-', delim_next)) = delim.split_first() {
strip_tab = true;
delim = delim_next;
}
if let Some((&b, delim_next)) = delim.split_first() {
if matches!(b, b'"' | b'\'') {
quote = Some(b);
delim = delim_next;
if delim.last() != Some(&b) {
return Err(ErrorKind::ExpectedOwned(
format!(
"quote ({}), but found '{}'",
b as char,
*delim.last().unwrap_or(&0) as char
),
p.text.len() - s.len(),
));
}
delim = &delim[..delim.len() - 1];
}
}
if strip_tab {
let (here_doc, span) = collect_here_doc_strip_tab(s, p.text, p.escape_byte, delim)?;
*src = Source::HereDoc(HereDoc { span, expand: quote.is_none(), value: here_doc });
} else {
let (here_doc, span) = collect_here_doc_no_strip_tab(s, p.text, p.escape_byte, delim)?;
*src =
Source::HereDoc(HereDoc { span, expand: quote.is_none(), value: here_doc.into() });
}
}
Ok((options, src, dest.unwrap()))
}
#[allow(clippy::unnecessary_wraps)]
#[inline]
fn parse_cmd<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"CMD",
p.escape_byte,
));
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok((arguments, array_span)) =
parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
{
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
return Ok(Instruction::Cmd(CmdInstruction {
cmd: instruction,
arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
}));
}
}
let arguments_start = p.text.len() - s.len();
skip_this_line(s, p.escape_byte);
let end = p.text.len() - s.len();
let arguments = p.text[arguments_start..end].trim_ascii_end();
Ok(Instruction::Cmd(CmdInstruction {
cmd: instruction,
arguments: Command::Shell(Spanned {
span: arguments_start..arguments_start + arguments.len(),
value: arguments,
}),
}))
}
#[inline]
fn parse_env<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"ENV",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Env(EnvInstruction { env: instruction, arguments }))
}
#[inline]
fn parse_expose<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"EXPOSE",
p.escape_byte,
));
let arguments: SmallVec<[_; 1]> =
collect_space_separated_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Expose(ExposeInstruction { expose: instruction, arguments }))
}
#[inline]
fn parse_entrypoint<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"ENTRYPOINT",
p.escape_byte,
));
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok((arguments, array_span)) =
parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
{
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument {
instruction_start: instruction.span.start,
});
}
return Ok(Instruction::Entrypoint(EntrypointInstruction {
entrypoint: instruction,
arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
}));
}
}
let arguments_start = p.text.len() - s.len();
skip_this_line(s, p.escape_byte);
let end = p.text.len() - s.len();
let arguments = p.text[arguments_start..end].trim_ascii_end();
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Entrypoint(EntrypointInstruction {
entrypoint: instruction,
arguments: Command::Shell(Spanned {
span: arguments_start..arguments_start + arguments.len(),
value: arguments,
}),
}))
}
#[inline]
fn parse_from<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"FROM",
p.escape_byte,
));
let options = parse_options(s, p.text, p.escape_byte);
let image = collect_non_whitespace_unescaped(s, p.text, p.escape_byte);
if image.value.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
let mut as_ = None;
if skip_spaces(s, p.escape_byte) {
let as_start = p.text.len() - s.len();
if token(s, b"AS") || token_slow(s, b"AS", p.escape_byte) {
let as_span = as_start..p.text.len() - s.len();
if !skip_spaces(s, p.escape_byte) {
return Err(ErrorKind::Expected("AS", as_start));
}
let name = collect_non_whitespace_unescaped(s, p.text, p.escape_byte);
skip_spaces(s, p.escape_byte);
if !is_line_end(s.first()) {
return Err(ErrorKind::Expected("newline or eof", p.text.len() - s.len()));
}
as_ = Some((Keyword { span: as_span }, name));
} else if !is_line_end(s.first()) {
return Err(ErrorKind::Expected("AS", as_start));
}
}
Ok(Instruction::From(FromInstruction { from: instruction, options, image, as_ }))
}
#[inline]
fn parse_healthcheck<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"HEALTHCHECK",
p.escape_byte,
));
let options = parse_options(s, p.text, p.escape_byte);
let Some((&b, s_next)) = s.split_first() else {
return Err(ErrorKind::Expected("CMD or NONE", p.text.len() - s.len()));
};
let cmd_or_none_start = p.text.len() - s.len();
match b & TO_UPPER8 {
b'C' => {
*s = s_next;
if token(s, &b"CMD"[1..]) || token_slow(s, &b"CMD"[1..], p.escape_byte) {
let cmd_span = cmd_or_none_start..p.text.len() - s.len();
let cmd_keyword = Keyword { span: cmd_span };
if spaces_or_line_end(s, p.escape_byte) {
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok((arguments, array_span)) =
parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
{
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
if arguments.is_empty() {
return Err(ErrorKind::Expected(
"at least 1 arguments",
array_span.start,
));
}
return Ok(Instruction::Healthcheck(HealthcheckInstruction {
healthcheck: instruction,
options,
arguments: HealthcheckArguments::Cmd {
cmd: cmd_keyword,
arguments: Command::Exec(Spanned {
span: array_span,
value: arguments,
}),
},
}));
}
}
let arguments_start = p.text.len() - s.len();
skip_this_line(s, p.escape_byte);
let end = p.text.len() - s.len();
let arguments = p.text[arguments_start..end].trim_ascii_end();
return Ok(Instruction::Healthcheck(HealthcheckInstruction {
healthcheck: instruction,
options,
arguments: HealthcheckArguments::Cmd {
cmd: cmd_keyword,
arguments: Command::Shell(Spanned {
span: arguments_start..arguments_start + arguments.len(),
value: arguments,
}),
},
}));
}
}
}
b'N' => {
*s = s_next;
if token(s, &b"NONE"[1..]) || token_slow(s, &b"NONE"[1..], p.escape_byte) {
let none_span = cmd_or_none_start..p.text.len() - s.len();
skip_spaces(s, p.escape_byte);
if !is_line_end(s.first()) {
return Err(ErrorKind::Expected(
"HEALTHCHECK NONE takes no arguments",
p.text.len() - s.len(),
));
}
let none_keyword = Keyword { span: none_span };
return Ok(Instruction::Healthcheck(HealthcheckInstruction {
healthcheck: instruction,
options,
arguments: HealthcheckArguments::None { none: none_keyword },
}));
}
}
_ => {}
}
Err(ErrorKind::Expected("CMD or NONE", p.text.len() - s.len()))
}
#[inline]
fn parse_label<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"LABEL",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Label(LabelInstruction { label: instruction, arguments }))
}
#[cold]
fn parse_maintainer<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"MAINTAINER",
p.escape_byte,
));
let name = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if name.value.is_empty() {
return Err(ErrorKind::ExactlyOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Maintainer(MaintainerInstruction { maintainer: instruction, name }))
}
#[inline]
fn parse_onbuild<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"ONBUILD",
p.escape_byte,
));
if mem::replace(&mut p.in_onbuild, true) {
return Err(ErrorKind::Expected("ONBUILD ONBUILD is not allowed", instruction.span.start));
}
let Some((&b, s_next)) = s.split_first() else {
return Err(ErrorKind::Expected("instruction after ONBUILD", instruction.span.start));
};
let inner_instruction = parse_instruction(p, s, b, s_next)?;
p.in_onbuild = false;
Ok(Instruction::Onbuild(OnbuildInstruction {
onbuild: instruction,
instruction: Box::new(inner_instruction),
}))
}
#[inline]
fn parse_run<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"RUN",
p.escape_byte,
));
let options = parse_options(s, p.text, p.escape_byte);
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok((arguments, array_span)) =
parse_json_array::<SmallVec<[_; 1]>>(&mut tmp, p.text, p.escape_byte)
{
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument {
instruction_start: instruction.span.start,
});
}
return Ok(Instruction::Run(RunInstruction {
run: instruction,
options,
arguments: Command::Exec(Spanned { span: array_span, value: arguments }),
here_docs: vec![],
}));
}
}
let mut strip_tab = false;
let mut quote = None;
let mut pos = 2;
if s.len() >= 5 && s.starts_with(b"<<") && {
if s[pos] == b'-' {
strip_tab = true;
pos += 1;
}
if matches!(s[pos], b'"' | b'\'') {
quote = Some(s[pos]);
pos += 1;
}
s[pos].is_ascii_alphanumeric()
} {
*s = &s[pos..];
let delim_start = p.text.len() - s.len();
while let Some((&b, s_next)) = s.split_first() {
if b.is_ascii_alphanumeric() {
*s = s_next;
continue;
}
break;
}
let delim = &p.text.as_bytes()[delim_start..p.text.len() - s.len()];
if let Some(quote) = quote {
if let Some((&b, s_next)) = s.split_first() {
if b != quote {
return Err(ErrorKind::ExpectedOwned(
format!("quote ({}), but found '{}'", quote as char, b as char),
p.text.len() - s.len(),
));
}
*s = s_next;
} else {
return Err(ErrorKind::ExpectedOwned(
format!("quote ({}), but reached eof", quote as char),
p.text.len() - s.len(),
));
}
}
let arguments_start = p.text.len() - s.len();
skip_this_line(s, p.escape_byte);
let end = p.text.len() - s.len();
let arguments = p.text[arguments_start..end].trim_ascii_end();
let here_doc = if strip_tab {
let (here_doc, span) = collect_here_doc_strip_tab(s, p.text, p.escape_byte, delim)?;
HereDoc { span, expand: quote.is_none(), value: here_doc }
} else {
let (here_doc, span) = collect_here_doc_no_strip_tab(s, p.text, p.escape_byte, delim)?;
HereDoc { span, expand: quote.is_none(), value: here_doc.into() }
};
return Ok(Instruction::Run(RunInstruction {
run: instruction,
options,
arguments: Command::Shell(Spanned {
span: arguments_start..arguments_start + arguments.len(),
value: arguments,
}),
here_docs: vec![here_doc],
}));
}
let arguments_start = p.text.len() - s.len();
skip_this_line(s, p.escape_byte);
let end = p.text.len() - s.len();
let arguments = p.text[arguments_start..end].trim_ascii_end();
Ok(Instruction::Run(RunInstruction {
run: instruction,
options,
arguments: Command::Shell(Spanned {
span: arguments_start..arguments_start + arguments.len(),
value: arguments,
}),
here_docs: vec![],
}))
}
#[inline]
fn parse_shell<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"SHELL",
p.escape_byte,
));
if !is_maybe_json(s) {
return Err(ErrorKind::Expected("JSON array", p.text.len() - s.len()));
}
match parse_json_array::<SmallVec<[_; 4]>>(s, p.text, p.escape_byte) {
Ok((arguments, _array_span)) => {
if !s.is_empty() {
*s = &s[1..];
}
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument {
instruction_start: instruction.span.start,
});
}
Ok(Instruction::Shell(ShellInstruction { shell: instruction, arguments }))
}
Err(array_start) => Err(ErrorKind::Json { arguments_start: array_start }),
}
}
#[inline]
fn parse_stopsignal<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"STOPSIGNAL",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::ExactlyOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Stopsignal(StopsignalInstruction { stopsignal: instruction, arguments }))
}
#[inline]
fn parse_user<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"USER",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::ExactlyOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::User(UserInstruction { user: instruction, arguments }))
}
#[inline]
fn parse_volume<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"VOLUME",
p.escape_byte,
));
if is_maybe_json(s) {
let mut tmp = *s;
if let Ok((arguments, array_span)) = parse_json_array(&mut tmp, p.text, p.escape_byte) {
debug_assert!(is_line_end(tmp.first()));
if tmp.is_empty() {
*s = &[];
} else {
*s = &tmp[1..];
}
return Ok(Instruction::Volume(VolumeInstruction {
volume: instruction,
arguments: JsonOrStringArray::Json(Spanned { span: array_span, value: arguments }),
}));
}
}
let arguments: SmallVec<[_; 1]> =
collect_space_separated_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.is_empty() {
return Err(ErrorKind::AtLeastOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Volume(VolumeInstruction {
volume: instruction,
arguments: JsonOrStringArray::String(arguments),
}))
}
#[inline]
fn parse_workdir<'a>(
p: &mut ParseIter<'a>,
s: &mut &'a [u8],
instruction: Keyword,
) -> Result<Instruction<'a>, ErrorKind> {
debug_assert!(token_slow(
&mut p.text[instruction.span.clone()].as_bytes(),
b"WORKDIR",
p.escape_byte,
));
let arguments = collect_non_line_unescaped_consume_line(s, p.text, p.escape_byte);
if arguments.value.is_empty() {
return Err(ErrorKind::ExactlyOneArgument { instruction_start: instruction.span.start });
}
Ok(Instruction::Workdir(WorkdirInstruction { workdir: instruction, arguments }))
}
const LINE: u8 = 1 << 0;
const SPACE: u8 = 1 << 1;
const WHITESPACE: u8 = 1 << 2;
const COMMENT: u8 = 1 << 3;
const DOUBLE_QUOTE: u8 = 1 << 4;
const POSSIBLE_ESCAPE: u8 = 1 << 5;
const EQ: u8 = 1 << 6;
static TABLE: [u8; 256] = {
let mut table = [0; 256];
let mut i = 0;
loop {
match i {
b' ' | b'\t' => table[i as usize] = WHITESPACE | SPACE,
b'\n' | b'\r' => table[i as usize] = WHITESPACE | LINE,
b'#' => table[i as usize] = COMMENT,
b'"' => table[i as usize] = DOUBLE_QUOTE,
b'\\' | b'`' => table[i as usize] = POSSIBLE_ESCAPE,
b'=' => table[i as usize] = EQ,
_ => {}
}
if i == u8::MAX {
break;
}
i += 1;
}
table
};
const UTF8_BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
trait Store<T>: Sized {
fn new() -> Self;
fn push(&mut self, val: T);
}
impl<T> Store<T> for Vec<T> {
#[inline]
fn new() -> Self {
Self::new()
}
#[inline]
fn push(&mut self, val: T) {
self.push(val);
}
}
impl<T, const N: usize> Store<T> for SmallVec<[T; N]> {
#[inline]
fn new() -> Self {
Self::new()
}
#[inline]
fn push(&mut self, val: T) {
self.push(val);
}
}
impl<'a, const N: usize> Store<UnescapedString<'a>>
for (SmallVec<[Source<'a>; N]>, Option<UnescapedString<'a>>)
{
#[inline]
fn new() -> Self {
(SmallVec::new(), None)
}
#[inline]
fn push(&mut self, val: UnescapedString<'a>) {
if let Some(val) = self.1.replace(val) {
self.0.push(Source::Path(val));
}
}
}
#[inline]
fn parse_options<'a, S: Store<Flag<'a>>>(s: &mut &[u8], start: &'a str, escape_byte: u8) -> S {
let mut options = S::new();
'outer: loop {
let Some((&b'-', mut s_next)) = s.split_first() else {
break;
};
loop {
let Some((&b, s_next_next)) = s_next.split_first() else {
break 'outer;
};
if b == b'-' {
s_next = s_next_next;
break;
}
if skip_line_escape(&mut s_next, b, s_next_next, escape_byte) {
skip_line_escape_followup(&mut s_next, escape_byte);
continue;
}
break 'outer;
}
let flag_start = start.len() - s.len();
*s = s_next;
let name = collect_until_unescaped::<{ WHITESPACE | EQ }>(s, start, escape_byte);
let Some((&b'=', s_next)) = s.split_first() else {
options.push(Flag { flag_start, name, value: None });
skip_spaces(s, escape_byte);
continue;
};
*s = s_next;
let value = collect_non_whitespace_unescaped(s, start, escape_byte);
options.push(Flag { flag_start, name, value: Some(value) });
skip_spaces(s, escape_byte);
}
options
}
fn parse_json_array<'a, S: Store<UnescapedString<'a>>>(
s: &mut &[u8],
start: &'a str,
escape_byte: u8,
) -> Result<(S, Span), usize> {
debug_assert_eq!(s.first(), Some(&b'['));
debug_assert_ne!(s.get(1), Some(&b'['));
let mut res = S::new();
let array_start = start.len() - s.len();
*s = &s[1..];
skip_spaces(s, escape_byte);
let (&b, s_next) = s.split_first().ok_or(array_start)?;
match b {
b'"' => {
*s = s_next;
loop {
let full_word_start = start.len() - s.len();
let mut word_start = full_word_start;
let mut buf = String::new();
loop {
let (&b, s_next) = s.split_first().ok_or(array_start)?;
if TABLE[b as usize] & (LINE | DOUBLE_QUOTE | POSSIBLE_ESCAPE) == 0 {
*s = s_next;
continue;
}
match b {
b'"' => break,
b'\n' | b'\r' => return Err(array_start),
_ => {}
}
let word_end = start.len() - s.len();
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
buf.push_str(&start[word_start..word_end]);
word_start = start.len() - s.len();
continue;
}
if b == b'\\' {
let word_end = start.len() - s.len();
buf.push_str(&start[word_start..word_end]);
*s = s_next;
let (new, new_start) = match *s.first().ok_or(array_start)? {
b @ (b'"' | b'\\' | b'/') => (b as char, 1),
b'b' => ('\x08', 1),
b'f' => ('\x0c', 1),
b'n' => ('\n', 1),
b'r' => ('\r', 1),
b't' => ('\t', 1),
b'u' => (parse_json_hex_escape(s, array_start)?, 5),
_ => return Err(array_start), };
buf.push(new);
*s = &s[new_start..];
word_start = start.len() - s.len();
continue;
}
*s = s_next;
}
let word_end = start.len() - s.len();
let value = if buf.is_empty() {
Cow::Borrowed(&start[word_start..word_end])
} else {
buf.push_str(&start[word_start..word_end]);
Cow::Owned(buf)
};
res.push(UnescapedString { span: full_word_start..word_end, value });
*s = &s[1..]; skip_spaces(s, escape_byte);
let (&b, s_next) = s.split_first().ok_or(array_start)?;
match b {
b',' => {
*s = s_next;
skip_spaces(s, escape_byte);
let (&b, s_next) = s.split_first().ok_or(array_start)?;
if b == b'"' {
*s = s_next;
continue;
}
return Err(array_start);
}
b']' => {
*s = s_next;
break;
}
_ => return Err(array_start),
}
}
}
b']' => *s = s_next,
_ => return Err(array_start),
}
let array_end = start.len() - s.len();
skip_spaces(s, escape_byte);
if !is_line_end(s.first()) {
return Err(array_start);
}
Ok((res, array_start..array_end))
}
#[cold]
fn parse_json_hex_escape(s: &mut &[u8], array_start: usize) -> Result<char, usize> {
fn decode_hex_escape(s: &mut &[u8], array_start: usize) -> Result<u16, usize> {
if s.len() < 4 {
return Err(array_start); }
let mut n = 0;
for _ in 0..4 {
let ch = decode_hex_val(s[0]);
*s = &s[1..];
match ch {
None => return Err(array_start), Some(val) => {
n = (n << 4) + val;
}
}
}
Ok(n)
}
fn decode_hex_val(val: u8) -> Option<u16> {
let n = HEX_DECODE_TABLE[val as usize] as u16;
if n == u8::MAX as u16 {
None
} else {
Some(n)
}
}
let c = match decode_hex_escape(s, array_start)? {
_n @ 0xDC00..=0xDFFF => return Err(array_start), n1 @ 0xD800..=0xDBFF => {
if s.first() == Some(&b'\\') {
*s = &s[1..];
} else {
return Err(array_start); }
if s.first() == Some(&b'u') {
*s = &s[1..];
} else {
return Err(array_start); }
let n2 = decode_hex_escape(s, array_start)?;
if n2 < 0xDC00 || n2 > 0xDFFF {
return Err(array_start); }
let n = ((((n1 - 0xD800) as u32) << 10) | (n2 - 0xDC00) as u32) + 0x1_0000;
match char::from_u32(n) {
Some(c) => c,
None => return Err(array_start), }
}
n => char::from_u32(n as u32).unwrap(),
};
Ok(c)
}
#[allow(clippy::needless_raw_string_hashes)]
#[test]
fn test_parse_json_array() {
let t = r#"[]"#;
let mut s = t.as_bytes();
assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[]);
assert_eq!(s, b"");
let t = r#"[ ]"#;
let mut s = t.as_bytes();
assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[]);
assert_eq!(s, b"");
let t = r#"["abc"]"#;
let mut s = t.as_bytes();
assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[UnescapedString {
span: 2..5,
value: "abc".into()
}]);
assert_eq!(s, b"");
let t = "[\"ab\",\"c\" , \"de\" ] \n";
let mut s = t.as_bytes();
assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[
UnescapedString { span: 2..4, value: "ab".into() },
UnescapedString { span: 7..8, value: "c".into() },
UnescapedString { span: 14..16, value: "de".into() },
]);
assert_eq!(s, b"\n");
let t = "[\"a\\\"\\\\\\/\\b\\f\\n\\r\\tbc\"]";
let mut s = t.as_bytes();
assert_eq!(&*parse_json_array::<Vec<_>>(&mut s, t, b'\\').unwrap().0, &[UnescapedString {
span: 2..21,
value: "a\"\\/\x08\x0c\n\r\tbc".into()
}]);
assert_eq!(s, b"");
let t = r#"['abc']"#;
let mut s = t.as_bytes();
assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
assert_eq!(s, br#"'abc']"#);
let t = r#"["abc",]"#;
let mut s = t.as_bytes();
assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
assert_eq!(s, br#"]"#);
let t = r#"["abc"] c"#;
let mut s = t.as_bytes();
assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
assert_eq!(s, br#"c"#);
let t = "[\"ab\\c\"]";
let mut s = t.as_bytes();
assert_eq!(parse_json_array::<Vec<_>>(&mut s, t, b'\\'), Err(0));
assert_eq!(s, b"c\"]");
}
#[inline]
fn skip_spaces_no_escape(s: &mut &[u8]) -> bool {
let start = *s;
while let Some((&b, s_next)) = s.split_first() {
if TABLE[b as usize] & SPACE != 0 {
*s = s_next;
continue;
}
break;
}
start.len() != s.len()
}
#[inline]
fn skip_spaces(s: &mut &[u8], escape_byte: u8) -> bool {
let mut has_space = false;
while let Some((&b, s_next)) = s.split_first() {
let t = TABLE[b as usize];
if t & (SPACE | POSSIBLE_ESCAPE) != 0 {
if t & SPACE != 0 {
*s = s_next;
has_space = true;
continue;
}
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
continue;
}
}
break;
}
has_space
}
#[inline]
fn spaces_or_line_end(s: &mut &[u8], escape_byte: u8) -> bool {
let mut has_space = false;
loop {
let Some((&b, s_next)) = s.split_first() else { return true };
{
let t = TABLE[b as usize];
if t & (WHITESPACE | POSSIBLE_ESCAPE) != 0 {
if t & SPACE != 0 {
*s = s_next;
has_space = true;
continue;
}
if t & LINE != 0 {
return true;
}
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
continue;
}
}
break;
}
}
has_space
}
#[inline]
fn skip_comments_and_whitespaces(s: &mut &[u8], escape_byte: u8) {
while let Some((&b, s_next)) = s.split_first() {
let t = TABLE[b as usize];
if t & (WHITESPACE | COMMENT | POSSIBLE_ESCAPE) != 0 {
if t & WHITESPACE != 0 {
*s = s_next;
continue;
}
if t & COMMENT != 0 {
*s = s_next;
skip_this_line_no_escape(s);
continue;
}
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
continue;
}
}
break;
}
}
#[inline]
fn is_line_end(b: Option<&u8>) -> bool {
matches!(b, Some(b'\n' | b'\r') | None)
}
#[inline]
fn is_maybe_json(s: &[u8]) -> bool {
s.first() == Some(&b'[') && s.get(1) != Some(&b'[')
}
#[inline]
fn collect_here_doc_no_strip_tab<'a>(
s: &mut &[u8],
start: &'a str,
_escape_byte: u8,
delim: &[u8],
) -> Result<(&'a str, Span), ErrorKind> {
let here_doc_start = start.len() - s.len();
loop {
if s.len() < delim.len() {
return Err(ErrorKind::ExpectedOwned(
str::from_utf8(delim).unwrap().to_owned(),
start.len() - s.len(),
));
}
if s.starts_with(delim) && is_line_end(s.get(delim.len())) {
break;
}
skip_this_line_no_escape(s);
}
let end = start.len() - s.len();
*s = &s[delim.len()..];
if !s.is_empty() {
*s = &s[1..];
}
let span = here_doc_start..end;
Ok((&start[span.clone()], span))
}
#[inline]
fn collect_here_doc_strip_tab<'a>(
s: &mut &[u8],
start: &'a str,
_escape_byte: u8,
delim: &[u8],
) -> Result<(Cow<'a, str>, Span), ErrorKind> {
let here_doc_start = start.len() - s.len();
let mut current_start = here_doc_start;
let mut res = String::new();
loop {
if s.len() < delim.len() {
return Err(ErrorKind::ExpectedOwned(
str::from_utf8(delim).unwrap().to_owned(),
start.len() - s.len(),
));
}
if let Some((&b'\t', s_next)) = s.split_first() {
let end = start.len() - s.len();
res.push_str(&start[current_start..end]);
*s = s_next;
while let Some((&b'\t', s_next)) = s.split_first() {
*s = s_next;
}
current_start = start.len() - s.len();
}
if s.starts_with(delim) && is_line_end(s.get(delim.len())) {
break;
}
skip_this_line_no_escape(s);
}
let end = start.len() - s.len();
*s = &s[delim.len()..];
if !s.is_empty() {
*s = &s[1..];
}
let span = here_doc_start..end;
if here_doc_start == current_start {
Ok((Cow::Borrowed(&start[span.clone()]), span))
} else {
res.push_str(&start[current_start..end]);
Ok((Cow::Owned(res), span))
}
}
#[inline]
fn collect_space_separated_unescaped_consume_line<'a, S: Store<UnescapedString<'a>>>(
s: &mut &[u8],
start: &'a str,
escape_byte: u8,
) -> S {
let mut res = S::new();
loop {
let val = collect_non_whitespace_unescaped(s, start, escape_byte);
if !val.value.is_empty() {
res.push(val);
if skip_spaces(s, escape_byte) {
continue;
}
}
debug_assert!(is_line_end(s.first()));
if !s.is_empty() {
*s = &s[1..];
}
break;
}
res
}
#[inline]
fn collect_non_whitespace_unescaped<'a>(
s: &mut &[u8],
start: &'a str,
escape_byte: u8,
) -> UnescapedString<'a> {
collect_until_unescaped::<WHITESPACE>(s, start, escape_byte)
}
#[inline]
fn collect_non_line_unescaped_consume_line<'a>(
s: &mut &[u8],
start: &'a str,
escape_byte: u8,
) -> UnescapedString<'a> {
let mut val = collect_until_unescaped::<LINE>(s, start, escape_byte);
debug_assert!(is_line_end(s.first()));
if !s.is_empty() {
*s = &s[1..];
}
match &mut val.value {
Cow::Borrowed(v) => {
while let Some(b' ' | b'\t') = v.as_bytes().last() {
*v = &v[..v.len() - 1];
val.span.end -= 1;
}
}
Cow::Owned(v) => {
while let Some(b' ' | b'\t') = v.as_bytes().last() {
v.pop();
val.span.end -= 1;
}
}
}
val
}
#[inline]
fn collect_until_unescaped<'a, const UNTIL_MASK: u8>(
s: &mut &[u8],
start: &'a str,
escape_byte: u8,
) -> UnescapedString<'a> {
let full_word_start = start.len() - s.len();
let mut word_start = full_word_start;
let mut buf = String::new();
while let Some((&b, s_next)) = s.split_first() {
let t = TABLE[b as usize];
if t & (UNTIL_MASK | POSSIBLE_ESCAPE) != 0 {
if t & UNTIL_MASK != 0 {
break;
}
let word_end = start.len() - s.len();
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
buf.push_str(&start[word_start..word_end]);
word_start = start.len() - s.len();
continue;
}
}
*s = s_next;
}
let word_end = start.len() - s.len();
let value = if buf.is_empty() {
Cow::Borrowed(&start[word_start..word_end])
} else {
buf.push_str(&start[word_start..word_end]);
Cow::Owned(buf)
};
UnescapedString { span: full_word_start..word_end, value }
}
#[inline]
fn skip_non_whitespace_no_escape(s: &mut &[u8]) -> bool {
let start = *s;
while let Some((&b, s_next)) = s.split_first() {
if TABLE[b as usize] & WHITESPACE != 0 {
break;
}
*s = s_next;
}
start.len() != s.len()
}
#[inline]
fn skip_line_escape<'a>(s: &mut &'a [u8], b: u8, s_next: &'a [u8], escape_byte: u8) -> bool {
if b == escape_byte {
if let Some((&b, mut s_next)) = s_next.split_first() {
if b == b'\n' {
*s = s_next;
return true;
}
if b == b'\r' {
if s_next.first() == Some(&b'\n') {
*s = &s_next[1..];
} else {
*s = s_next;
}
return true;
}
if TABLE[b as usize] & SPACE != 0 {
skip_spaces_no_escape(&mut s_next);
if let Some((&b, s_next)) = s_next.split_first() {
if b == b'\n' {
*s = s_next;
return true;
}
if b == b'\r' {
if s_next.first() == Some(&b'\n') {
*s = &s_next[1..];
} else {
*s = s_next;
}
return true;
}
}
}
}
}
false
}
#[inline]
fn skip_line_escape_followup(s: &mut &[u8], _escape_byte: u8) {
while let Some((&b, mut s_next)) = s.split_first() {
let t = TABLE[b as usize];
if t & (WHITESPACE | COMMENT) != 0 {
if t & SPACE != 0 {
skip_spaces_no_escape(&mut s_next);
if let Some((&b, s_next)) = s_next.split_first() {
let t = TABLE[b as usize];
if t & (COMMENT | LINE) != 0 {
*s = s_next;
if t & COMMENT != 0 {
skip_this_line_no_escape(s);
}
continue;
}
}
} else {
*s = s_next;
if t & COMMENT != 0 {
skip_this_line_no_escape(s);
}
continue;
}
}
break;
}
}
#[inline]
fn skip_this_line_no_escape(s: &mut &[u8]) {
while let Some((&b, s_next)) = s.split_first() {
*s = s_next;
if TABLE[b as usize] & LINE != 0 {
break;
}
}
}
#[inline]
fn skip_this_line(s: &mut &[u8], escape_byte: u8) {
let mut has_space_only = 0;
while let Some((&b, s_next)) = s.split_first() {
let t = TABLE[b as usize];
if t & (LINE | COMMENT | POSSIBLE_ESCAPE) != 0 {
if t & LINE != 0 {
*s = s_next;
break;
}
if has_space_only != 0 && t & COMMENT != 0 {
*s = s_next;
skip_this_line_no_escape(s);
continue;
}
if skip_line_escape(s, b, s_next, escape_byte) {
skip_line_escape_followup(s, escape_byte);
has_space_only = SPACE;
continue;
}
}
has_space_only &= t;
*s = s_next;
}
}
#[inline(always)]
fn token(s: &mut &[u8], token: &'static [u8]) -> bool {
let matched = starts_with_ignore_ascii_case(s, token);
if matched {
*s = &s[token.len()..];
true
} else {
false
}
}
#[cold]
fn token_slow(s: &mut &[u8], mut token: &'static [u8], escape_byte: u8) -> bool {
debug_assert!(!token.is_empty() && token.iter().all(|&n| n & TO_UPPER8 == n));
if s.len() < token.len() {
return false;
}
let mut tmp = *s;
while let Some((&b, tmp_next)) = tmp.split_first() {
if b & TO_UPPER8 == token[0] {
tmp = tmp_next;
token = &token[1..];
if token.is_empty() {
*s = tmp;
return true;
}
continue;
}
if skip_line_escape(&mut tmp, b, tmp_next, escape_byte) {
skip_line_escape_followup(&mut tmp, escape_byte);
continue;
}
break;
}
false
}
const TO_UPPER8: u8 = 0xDF;
const TO_UPPER64: u64 = 0xDFDFDFDFDFDFDFDF;
#[inline(always)] fn starts_with_ignore_ascii_case(mut s: &[u8], mut needle: &'static [u8]) -> bool {
debug_assert!(!needle.is_empty() && needle.iter().all(|&n| n & TO_UPPER8 == n));
if s.len() < needle.len() {
return false;
}
if needle.len() == 1 {
return needle[0] == s[0] & TO_UPPER8;
}
if needle.len() >= 8 {
loop {
if u64::from_ne_bytes(needle[..8].try_into().unwrap())
!= u64::from_ne_bytes(s[..8].try_into().unwrap()) & TO_UPPER64
{
return false;
}
needle = &needle[8..];
s = &s[8..];
if needle.len() < 8 {
if needle.is_empty() {
return true;
}
break;
}
}
}
let s = {
let mut buf = [0; 8];
buf[..needle.len()].copy_from_slice(&s[..needle.len()]);
u64::from_ne_bytes(buf)
};
let needle = {
let mut buf = [0; 8];
buf[..needle.len()].copy_from_slice(needle);
u64::from_ne_bytes(buf)
};
needle == s & TO_UPPER64
}
#[test]
fn test_starts_with_ignore_ascii_case() {
assert!(starts_with_ignore_ascii_case(b"ABC", b"ABC"));
assert!(starts_with_ignore_ascii_case(b"abc", b"ABC"));
assert!(starts_with_ignore_ascii_case(b"AbC", b"ABC"));
assert!(!starts_with_ignore_ascii_case(b"ABB", b"ABC"));
assert!(starts_with_ignore_ascii_case(b"ABCDEFGH", b"ABCDEFGH"));
assert!(starts_with_ignore_ascii_case(b"abcdefgh", b"ABCDEFGH"));
assert!(starts_with_ignore_ascii_case(b"AbCdEfGh", b"ABCDEFGH"));
assert!(!starts_with_ignore_ascii_case(b"ABCDEFGc", b"ABCDEFGH"));
assert!(starts_with_ignore_ascii_case(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
));
assert!(starts_with_ignore_ascii_case(
b"abcdefghijklmnopqrstuvwxyz",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
));
assert!(starts_with_ignore_ascii_case(
b"aBcDeFgHiJkLmNoPqRsTuVwXyZ",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
));
assert!(!starts_with_ignore_ascii_case(
b"aBcDeFgHiJkLmNoPqRsTuVwXyc",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
));
}
#[rustfmt::skip]
static HEX_DECODE_TABLE: [u8; 256] = {
const __: u8 = u8::MAX;
[
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, __, __, __, __, __, __, __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, ]
};