dockerfile_parser/instructions/
from.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryFrom;
4
5use crate::dockerfile_parser::Instruction;
6use crate::image::ImageRef;
7use crate::parser::{Pair, Rule};
8use crate::parse_string;
9use crate::SpannedString;
10use crate::splicer::*;
11use crate::error::*;
12
13use lazy_static::lazy_static;
14use regex::Regex;
15
16/// A key/value pair passed to a `FROM` instruction as a flag.
17///
18/// Examples include: `FROM --platform=linux/amd64 node:lts-alpine`
19#[derive(Debug, PartialEq, Eq, Clone)]
20pub struct FromFlag {
21  pub span: Span,
22  pub name: SpannedString,
23  pub value: SpannedString,
24}
25
26impl FromFlag {
27  fn from_record(record: Pair) -> Result<FromFlag> {
28    let span = Span::from_pair(&record);
29    let mut name = None;
30    let mut value = None;
31
32    for field in record.into_inner() {
33      match field.as_rule() {
34        Rule::from_flag_name => name = Some(parse_string(&field)?),
35        Rule::from_flag_value => value = Some(parse_string(&field)?),
36        _ => return Err(unexpected_token(field))
37      }
38    }
39
40    let name = name.ok_or_else(|| Error::GenericParseError {
41      message: "from flags require a key".into(),
42    })?;
43
44    let value = value.ok_or_else(|| Error::GenericParseError {
45      message: "from flags require a value".into()
46    })?;
47
48    Ok(FromFlag {
49      span, name, value
50    })
51  }
52}
53
54
55/// A Dockerfile [`FROM` instruction][from].
56///
57/// Contains spans for the entire instruction, the image, and the alias (if
58/// any).
59///
60/// [from]: https://docs.docker.com/engine/reference/builder/#from
61#[derive(Debug, PartialEq, Eq, Clone)]
62pub struct FromInstruction {
63  pub span: Span,
64  pub flags: Vec<FromFlag>,
65  pub image: SpannedString,
66  pub image_parsed: ImageRef,
67
68  pub index: usize,
69  pub alias: Option<SpannedString>,
70}
71
72impl FromInstruction {
73  pub(crate) fn from_record(record: Pair, index: usize) -> Result<FromInstruction> {
74    lazy_static! {
75      static ref HEX: Regex =
76          Regex::new(r"[0-9a-fA-F]+").unwrap();
77    }
78
79    let span = Span::from_pair(&record);
80    let mut image_field = None;
81    let mut alias_field = None;
82    let mut flags = Vec::new();
83
84    for field in record.into_inner() {
85      match field.as_rule() {
86        Rule::from_flag => flags.push(FromFlag::from_record(field)?),        
87        Rule::from_image => image_field = Some(field),
88        Rule::from_alias => alias_field = Some(field),
89        Rule::comment => continue,
90        _ => return Err(unexpected_token(field))
91      };
92    }
93
94    let image = if let Some(image_field) = image_field {
95      parse_string(&image_field)?
96    } else {
97      return Err(Error::GenericParseError {
98        message: "missing from image".into()
99      });
100    };
101
102    let image_parsed = ImageRef::parse(&image.as_ref());
103
104    if let Some(hash) = &image_parsed.hash {
105      let parts: Vec<&str> = hash.split(":").collect();
106      if let ["sha256", hexdata] = parts[..] {
107        if !HEX.is_match(hexdata) || hexdata.len() != 64 {
108          return Err(Error::GenericParseError { message: "image reference digest is invalid".into() });
109        }
110      } else {
111        return Err(Error::GenericParseError { message: "image reference digest is invalid".into() });
112      }
113    }
114
115    let alias = if let Some(alias_field) = alias_field {
116      Some(parse_string(&alias_field)?)
117    } else {
118      None
119    };
120
121    Ok(FromInstruction {
122      span, index,
123      image, image_parsed,
124      flags, alias,
125    })
126  }
127
128  // TODO: util for converting to an ImageRef while resolving ARG
129  // per the docs, ARG instructions are only honored in FROMs if they occur
130  // before the *first* FROM (but this should be verified)
131  // fn image_ref(&self) -> ImageRef { ... }
132}
133
134impl<'a> TryFrom<&'a Instruction> for &'a FromInstruction {
135  type Error = Error;
136
137  fn try_from(instruction: &'a Instruction) -> std::result::Result<Self, Self::Error> {
138    if let Instruction::From(f) = instruction {
139      Ok(f)
140    } else {
141      Err(Error::ConversionError {
142        from: format!("{:?}", instruction),
143        to: "FromInstruction".into()
144      })
145    }
146  }
147}
148
149#[cfg(test)]
150mod tests {
151  use core::panic;
152
153use indoc::indoc;
154  use pretty_assertions::assert_eq;
155
156  use super::*;
157  use crate::test_util::*;
158
159  #[test]
160  fn from_bad_digest() {
161    let cases = vec![
162      "from alpine@sha256:ca5a2eb9b7917e542663152b04c0",
163      "from alpine@sha257:ca5a2eb9b7917e542663152b04c0ad0572e0522fcf80ff080156377fc08ea8f8",
164      "from alpine@ca5a2eb9b7917e542663152b04c0ad0572e0522fcf80ff080156377fc08ea8f8",
165    ];
166
167    for case in cases {
168      let result = parse_direct(
169        case,
170        Rule::from,
171        |p| FromInstruction::from_record(p, 0)
172      );
173
174      match result {
175        Ok(_) => panic!("Expected parse error."),
176        Err(Error::GenericParseError { message: _}) => {},
177        Err(_) => panic!("Expected GenericParseError"),
178      };
179    }
180  }
181
182  #[test]
183  fn from_no_alias() -> Result<()> {
184    // pulling the FromInstruction out of the enum is messy, so just parse
185    // directly
186    let from = parse_direct(
187      "from alpine:3.10",
188      Rule::from,
189      |p| FromInstruction::from_record(p, 0)
190    )?;
191
192    assert_eq!(from, FromInstruction {
193      span: Span { start: 0, end: 16 },
194      index: 0,
195      image: SpannedString {
196        span: Span { start: 5, end: 16 },
197        content: "alpine:3.10".into(),
198      },
199      image_parsed: ImageRef {
200        registry: None,
201        image: "alpine".into(),
202        tag: Some("3.10".into()),
203        hash: None
204      },
205      alias: None,
206      flags: vec![],
207    });
208
209    Ok(())
210  }
211
212  #[test]
213  fn from_no_newline() -> Result<()> {
214    // unfortunately we can't use a single rule to test these as individual
215    // rules have no ~ EOI requirement to ensure we parse the whole string
216    assert!(parse_single(
217      "from alpine:3.10 from example",
218      Rule::dockerfile,
219    ).is_err());
220
221    Ok(())
222  }
223
224  #[test]
225  fn from_missing_alias() -> Result<()> {
226    assert!(parse_single(
227      "from alpine:3.10 as",
228      Rule::dockerfile,
229    ).is_err());
230
231    Ok(())
232  }
233
234  #[test]
235  fn from_flags() -> Result<()> {
236    assert_eq!(
237      parse_single(
238        "FROM --platform=linux/amd64 alpine:3.10",
239        Rule::from
240      )?,
241      FromInstruction {
242        index: 0,
243        span: Span { start: 0, end: 39 },
244        flags: vec![
245          FromFlag {
246            span: Span { start: 5, end: 27 },
247            name: SpannedString {
248              content: "platform".into(),
249              span: Span { start: 7, end: 15 },
250            },
251            value: SpannedString {
252              content: "linux/amd64".into(),
253              span: Span { start: 16, end: 27 },
254            }
255          }
256        ],
257        image: SpannedString {
258          span: Span { start: 28, end: 39 },
259          content: "alpine:3.10".into(),
260        },
261        image_parsed: ImageRef {
262          registry: None,
263          image: "alpine".into(),
264          tag: Some("3.10".into()),
265          hash: None
266        },
267        alias: None,
268      }.into()
269    );
270
271    Ok(())
272  }
273
274
275  #[test]
276  fn from_multiline() -> Result<()> {
277    let from = parse_direct(
278      indoc!(r#"
279        from \
280          # foo
281          alpine:3.10 \
282
283          # test
284          # comment
285
286          as \
287
288          test
289      "#),
290      Rule::from,
291      |p| FromInstruction::from_record(p, 0)
292    )?;
293
294    assert_eq!(from, FromInstruction {
295      span: Span { start: 0, end: 68 },
296      index: 0,
297      image: SpannedString {
298        span: Span { start: 17, end: 28 },
299        content: "alpine:3.10".into(),
300      },
301      image_parsed: ImageRef {
302        registry: None,
303        image: "alpine".into(),
304        tag: Some("3.10".into()),
305        hash: None
306      },
307      alias: Some(SpannedString {
308        span: (64, 68).into(),
309        content: "test".into(),
310      }),
311      flags: vec![],
312    });
313
314    Ok(())
315  }
316}