From 4ce885ecbb834ff65a77267f2e3730bbeb69b90b Mon Sep 17 00:00:00 2001 From: Jonathon Belotti Date: Sun, 7 May 2023 02:43:04 +0000 Subject: [PATCH 1/3] fix: parse anything that comes after @ in FROM instruction --- src/dockerfile_parser.pest | 2 +- tests/parsing.rs | 520 +++++++++++++++++++++---------------- 2 files changed, 294 insertions(+), 228 deletions(-) diff --git a/src/dockerfile_parser.pest b/src/dockerfile_parser.pest index 0e19d94..39da77e 100644 --- a/src/dockerfile_parser.pest +++ b/src/dockerfile_parser.pest @@ -104,7 +104,7 @@ string_array = _{ from_flag_name = @{ ASCII_ALPHA+ } from_flag_value = @{ any_whitespace } from_flag = { "--" ~ from_flag_name ~ "=" ~ from_flag_value } -from_image = @{ (ASCII_ALPHANUMERIC | "_" | "-" | "." | ":" | "/" | "$" | "{" | "}")+ } +from_image = @{ (ASCII_ALPHANUMERIC | "_" | "-" | "." | ":" | "/" | "$" | "{" | "}" | "@")+ } from_alias = { identifier_whitespace } from_alias_outer = _{ arg_ws ~ ^"as" ~ arg_ws ~ from_alias } from = { ^"from" ~ (arg_ws ~ from_flag)* ~ arg_ws ~ from_image ~ from_alias_outer? } diff --git a/tests/parsing.rs b/tests/parsing.rs index f7e2626..c9ff7e4 100644 --- a/tests/parsing.rs +++ b/tests/parsing.rs @@ -8,109 +8,129 @@ use pretty_assertions::assert_eq; #[test] fn parse_basic() -> Result<(), dockerfile_parser::Error> { - let dockerfile = Dockerfile::parse(r#" + let dockerfile = Dockerfile::parse( + r#" FROM alpine:3.10 RUN apk add --no-cache curl - "#)?; - - assert_eq!(dockerfile.instructions.len(), 2); - - assert_eq!( - dockerfile.instructions[0], - Instruction::From(FromInstruction { - span: Span { start: 5, end: 21 }, - image: SpannedString { - span: Span { start: 10, end: 21 }, - content: "alpine:3.10".into(), - }, - image_parsed: ImageRef { - registry: None, - image: "alpine".into(), - tag: Some("3.10".into()), - hash: None - }, - index: 0, - alias: None, - flags: vec![], - }) - ); - - assert_eq!( - &dockerfile.instructions[1] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "apk add --no-cache curl" - ); - - Ok(()) + "#, + )?; + + assert_eq!(dockerfile.instructions.len(), 2); + + assert_eq!( + dockerfile.instructions[0], + Instruction::From(FromInstruction { + span: Span { start: 5, end: 21 }, + image: SpannedString { + span: Span { start: 10, end: 21 }, + content: "alpine:3.10".into(), + }, + image_parsed: ImageRef { + registry: None, + image: "alpine".into(), + tag: Some("3.10".into()), + hash: None + }, + index: 0, + alias: None, + flags: vec![], + }) + ); + + assert_eq!( + &dockerfile.instructions[1] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "apk add --no-cache curl" + ); + + Ok(()) } #[test] fn parse_multiline_shell() -> Result<(), dockerfile_parser::Error> { - let dockerfile = Dockerfile::parse(indoc!(r#" + let dockerfile = Dockerfile::parse(indoc!( + r#" RUN apk add --no-cache \ curl RUN foo - "#))?; - - assert_eq!(dockerfile.instructions.len(), 2); - - // note: 9 spaces due to 1 before the \ + 8 for indent - assert_eq!( - &dockerfile.instructions[0] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "apk add --no-cache curl" - ); - - assert_eq!( - &dockerfile.instructions[1] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "foo" - ); - - Ok(()) + "# + ))?; + + assert_eq!(dockerfile.instructions.len(), 2); + + // note: 9 spaces due to 1 before the \ + 8 for indent + assert_eq!( + &dockerfile.instructions[0] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "apk add --no-cache curl" + ); + + assert_eq!( + &dockerfile.instructions[1] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "foo" + ); + + Ok(()) } #[test] fn parse_multiline_exec() -> Result<(), dockerfile_parser::Error> { - let dockerfile = Dockerfile::parse(r#" + let dockerfile = Dockerfile::parse( + r#" RUN ["apk", \ "add", \ "--no-cache", \ "curl"] RUN foo - "#)?; - - assert_eq!(dockerfile.instructions.len(), 2); - - // note: 9 spaces due to 1 before the \ + 8 for indent - assert_eq!( - dockerfile.instructions[0].as_run().unwrap().as_exec().unwrap().as_str_vec(), - &["apk", "add", "--no-cache", "curl"] - ); - - assert_eq!( - &dockerfile.instructions[1] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "foo" - ); - - Ok(()) + "#, + )?; + + assert_eq!(dockerfile.instructions.len(), 2); + + // note: 9 spaces due to 1 before the \ + 8 for indent + assert_eq!( + dockerfile.instructions[0] + .as_run() + .unwrap() + .as_exec() + .unwrap() + .as_str_vec(), + &["apk", "add", "--no-cache", "curl"] + ); + + assert_eq!( + &dockerfile.instructions[1] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "foo" + ); + + Ok(()) } #[test] fn parse_label() -> Result<(), dockerfile_parser::Error> { - let dockerfile = Dockerfile::parse(r#" + let dockerfile = Dockerfile::parse( + r#" LABEL foo=bar LABEL "foo"="bar" @@ -121,113 +141,111 @@ fn parse_label() -> Result<(), dockerfile_parser::Error> { baz" RUN foo - "#)?; - - assert_eq!(dockerfile.instructions.len(), 5); - - assert_eq!( - dockerfile.instructions[0] - .as_label().unwrap(), - &LabelInstruction { - span: Span::new(5, 18), - labels: vec![ - Label::new( - Span::new(11, 18), - SpannedString { - span: Span::new(11, 14), - content: "foo".to_string(), - }, - SpannedString { - span: Span::new(15, 18), - content: "bar".to_string(), - }, - ) - ] - } - ); - - assert_eq!( - dockerfile.instructions[1], - Instruction::Label(LabelInstruction { - span: Span::new(24, 41), - labels: vec![ - Label::new( - Span::new(30, 41), - SpannedString { - span: Span::new(30, 35), - content: "foo".to_string(), - }, - SpannedString { - span: Span::new(36, 41), - content: "bar".to_string(), - }, - ) - ] - }) - ); - - assert_eq!( - dockerfile.instructions[2], - Instruction::Label(LabelInstruction { - span: Span::new(47, 66), - labels: vec![ - Label::new( - Span::new(53, 66), - SpannedString { - span: Span::new(53, 62), - content: "foo=bar".to_string(), - }, - SpannedString { - span: Span::new(63, 66), - content: "bar".to_string(), - }, - ) - ] - }) - ); - - assert_eq!( - dockerfile.instructions[3], - Instruction::Label(LabelInstruction { - span: Span::new(72, 102), - labels: vec![ - Label::new( - Span::new(78, 102), - SpannedString { - span: Span::new(78, 81), - content: "foo".to_string(), - }, - SpannedString { - span: Span::new(82, 102), - content: "bar baz".to_string(), - }, - ) - ] - }) - ); - - assert_eq!( - &dockerfile.instructions[4] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "foo" - ); - - // ambiguous line continuation is an error - assert!(Dockerfile::parse(r#" + "#, + )?; + + assert_eq!(dockerfile.instructions.len(), 5); + + assert_eq!( + dockerfile.instructions[0].as_label().unwrap(), + &LabelInstruction { + span: Span::new(5, 18), + labels: vec![Label::new( + Span::new(11, 18), + SpannedString { + span: Span::new(11, 14), + content: "foo".to_string(), + }, + SpannedString { + span: Span::new(15, 18), + content: "bar".to_string(), + }, + )] + } + ); + + assert_eq!( + dockerfile.instructions[1], + Instruction::Label(LabelInstruction { + span: Span::new(24, 41), + labels: vec![Label::new( + Span::new(30, 41), + SpannedString { + span: Span::new(30, 35), + content: "foo".to_string(), + }, + SpannedString { + span: Span::new(36, 41), + content: "bar".to_string(), + }, + )] + }) + ); + + assert_eq!( + dockerfile.instructions[2], + Instruction::Label(LabelInstruction { + span: Span::new(47, 66), + labels: vec![Label::new( + Span::new(53, 66), + SpannedString { + span: Span::new(53, 62), + content: "foo=bar".to_string(), + }, + SpannedString { + span: Span::new(63, 66), + content: "bar".to_string(), + }, + )] + }) + ); + + assert_eq!( + dockerfile.instructions[3], + Instruction::Label(LabelInstruction { + span: Span::new(72, 102), + labels: vec![Label::new( + Span::new(78, 102), + SpannedString { + span: Span::new(78, 81), + content: "foo".to_string(), + }, + SpannedString { + span: Span::new(82, 102), + content: "bar baz".to_string(), + }, + )] + }) + ); + + assert_eq!( + &dockerfile.instructions[4] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "foo" + ); + + // ambiguous line continuation is an error + assert!(Dockerfile::parse( + r#" LABEL foo="bar\ baz"\ RUN foo - "#).is_err()); + "# + ) + .is_err()); - Ok(()) + Ok(()) } #[test] fn parse_comment() -> Result<(), dockerfile_parser::Error> { - let dockerfile = Dockerfile::parse(r#" + let dockerfile = Dockerfile::parse( + r#" # lorem ipsum LABEL foo=bar #dolor sit amet @@ -262,55 +280,103 @@ fn parse_comment() -> Result<(), dockerfile_parser::Error> { ] run echo 'hello # world' - "#)?; - - assert_eq!(dockerfile.instructions.len(), 8); - - assert_eq!( - &dockerfile.instructions[4] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "foo" - ); - - assert_eq!( - dockerfile.instructions[5].as_env().unwrap().vars, - vec![ - EnvVar::new( - Span::new(396, 401), - SpannedString { - span: Span::new(396, 399), - content: "foo".to_string(), - }, - ((400, 401), "a") - ), - EnvVar::new( - Span::new(433, 438), - SpannedString { - span: Span::new(433, 436), - content: "bar".to_string(), - }, - ((437, 438), "b") - ), - ] - ); - - assert_eq!( - dockerfile.instructions[6] - .as_run().unwrap() - .as_exec().unwrap() - .as_str_vec(), - vec!["echo", "hello", "world"] - ); - - assert_eq!( - dockerfile.instructions[7] - .as_run().unwrap() - .as_shell().unwrap() - .to_string(), - "echo 'hello # world'" - ); - - Ok(()) + "#, + )?; + + assert_eq!(dockerfile.instructions.len(), 8); + + assert_eq!( + &dockerfile.instructions[4] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "foo" + ); + + assert_eq!( + dockerfile.instructions[5].as_env().unwrap().vars, + vec![ + EnvVar::new( + Span::new(396, 401), + SpannedString { + span: Span::new(396, 399), + content: "foo".to_string(), + }, + ((400, 401), "a") + ), + EnvVar::new( + Span::new(433, 438), + SpannedString { + span: Span::new(433, 436), + content: "bar".to_string(), + }, + ((437, 438), "b") + ), + ] + ); + + assert_eq!( + dockerfile.instructions[6] + .as_run() + .unwrap() + .as_exec() + .unwrap() + .as_str_vec(), + vec!["echo", "hello", "world"] + ); + + assert_eq!( + dockerfile.instructions[7] + .as_run() + .unwrap() + .as_shell() + .unwrap() + .to_string(), + "echo 'hello # world'" + ); + + Ok(()) +} + +#[test] +fn parse_from_sha256_digest() -> Result<(), dockerfile_parser::Error> { + let dockerfile = Dockerfile::parse( + r#" + FROM alpine@sha256:074d3636ebda6dd446d0d00304c4454f468237fdacf08fb0eeac90bdbfa1bac7 as foo + "#, + )?; + + assert_eq!(dockerfile.instructions.len(), 1); + + assert_eq!( + dockerfile.instructions[0].as_from(), + Some(&FromInstruction { + index: 0, + span: (5, 95).into(), + image: SpannedString { + span: Span { start: 10, end: 88 }, + content: + "alpine@sha256:074d3636ebda6dd446d0d00304c4454f468237fdacf08fb0eeac90bdbfa1bac7" + .into(), + }, + image_parsed: ImageRef { + registry: None, + image: "alpine".into(), + tag: None, + hash: Some( + "sha256:074d3636ebda6dd446d0d00304c4454f468237fdacf08fb0eeac90bdbfa1bac7" + .into() + ), + }, + alias: Some(SpannedString { + span: Span { start: 92, end: 95 }, + content: "foo".into(), + }), + flags: vec![], + }) + ); + + Ok(()) } From fa81b4000fc66eec81e4d9c005043a904b2e2be7 Mon Sep 17 00:00:00 2001 From: Jonathon Belotti Date: Sun, 7 May 2023 21:17:59 +0000 Subject: [PATCH 2/3] feat: reject bad DIGEST part in FROM image --- src/instructions/from.rs | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/instructions/from.rs b/src/instructions/from.rs index 6694ead..8118bc5 100644 --- a/src/instructions/from.rs +++ b/src/instructions/from.rs @@ -10,6 +10,9 @@ use crate::SpannedString; use crate::splicer::*; use crate::error::*; +use lazy_static::lazy_static; +use regex::Regex; + /// A key/value pair passed to a `FROM` instruction as a flag. /// /// Examples include: `FROM --platform=linux/amd64 node:lts-alpine` @@ -68,6 +71,11 @@ pub struct FromInstruction { impl FromInstruction { pub(crate) fn from_record(record: Pair, index: usize) -> Result { + lazy_static! { + static ref HEX: Regex = + Regex::new(r"[0-9a-fA-F]+").unwrap(); + } + let span = Span::from_pair(&record); let mut image_field = None; let mut alias_field = None; @@ -93,6 +101,17 @@ impl FromInstruction { let image_parsed = ImageRef::parse(&image.as_ref()); + if let Some(hash) = &image_parsed.hash { + let parts: Vec<&str> = hash.split(":").collect(); + if let ["sha256", hexdata] = parts[..] { + if !HEX.is_match(hexdata) || hexdata.len() != 64 { + return Err(Error::GenericParseError { message: "image reference digest is invalid".into() }); + } + } else { + return Err(Error::GenericParseError { message: "image reference digest is invalid".into() }); + } + } + let alias = if let Some(alias_field) = alias_field { Some(parse_string(&alias_field)?) } else { @@ -129,12 +148,37 @@ impl<'a> TryFrom<&'a Instruction> for &'a FromInstruction { #[cfg(test)] mod tests { - use indoc::indoc; + use core::panic; + +use indoc::indoc; use pretty_assertions::assert_eq; use super::*; use crate::test_util::*; + #[test] + fn from_bad_digest() { + let cases = vec![ + "from alpine@sha256:ca5a2eb9b7917e542663152b04c0", + "from alpine@sha257:ca5a2eb9b7917e542663152b04c0ad0572e0522fcf80ff080156377fc08ea8f8", + "from alpine@ca5a2eb9b7917e542663152b04c0ad0572e0522fcf80ff080156377fc08ea8f8", + ]; + + for case in cases { + let result = parse_direct( + case, + Rule::from, + |p| FromInstruction::from_record(p, 0) + ); + + match result { + Ok(_) => panic!("Expected parse error."), + Err(Error::GenericParseError { message: _}) => {}, + Err(_) => panic!("Expected GenericParseError"), + }; + } + } + #[test] fn from_no_alias() -> Result<()> { // pulling the FromInstruction out of the enum is messy, so just parse From baf77b58219402817d471e940813f3531ab059fb Mon Sep 17 00:00:00 2001 From: Brian Shih Date: Tue, 5 Mar 2024 17:45:29 +0000 Subject: [PATCH 3/3] make substitute public --- src/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image.rs b/src/image.rs index 03e1c93..f58ef54 100644 --- a/src/image.rs +++ b/src/image.rs @@ -52,7 +52,7 @@ fn is_registry(token: &str) -> bool { /// 16. /// If None is returned, substitution was impossible, either because a /// referenced variable did not exist, or recursion depth was exceeded. -fn substitute<'a, 'b>( +pub fn substitute<'a, 'b>( s: &'a str, vars: &'b HashMap<&'b str, &'b str>, used_vars: &mut HashSet,