dockerfile_parser/instructions/
copy.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryFrom;
4
5use snafu::ensure;
6
7use crate::dockerfile_parser::Instruction;
8use crate::parser::{Pair, Rule};
9use crate::{Span, parse_string};
10use crate::SpannedString;
11use crate::error::*;
12
13/// A key/value pair passed to a `COPY` instruction as a flag.
14///
15/// Examples include: `COPY --from=foo /to /from`
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub struct CopyFlag {
18  pub span: Span,
19  pub name: SpannedString,
20  pub value: SpannedString,
21}
22
23impl CopyFlag {
24  fn from_record(record: Pair) -> Result<CopyFlag> {
25    let span = Span::from_pair(&record);
26    let mut name = None;
27    let mut value = None;
28
29    for field in record.into_inner() {
30      match field.as_rule() {
31        Rule::copy_flag_name => name = Some(parse_string(&field)?),
32        Rule::copy_flag_value => value = Some(parse_string(&field)?),
33        _ => return Err(unexpected_token(field))
34      }
35    }
36
37    let name = name.ok_or_else(|| Error::GenericParseError {
38      message: "copy flags require a key".into(),
39    })?;
40
41    let value = value.ok_or_else(|| Error::GenericParseError {
42      message: "copy flags require a value".into()
43    })?;
44
45    Ok(CopyFlag {
46      span, name, value
47    })
48  }
49}
50
51/// A Dockerfile [`COPY` instruction][copy].
52///
53/// [copy]: https://docs.docker.com/engine/reference/builder/#copy
54#[derive(Debug, PartialEq, Eq, Clone)]
55pub struct CopyInstruction {
56  pub span: Span,
57  pub flags: Vec<CopyFlag>,
58  pub sources: Vec<SpannedString>,
59  pub destination: SpannedString
60}
61
62impl CopyInstruction {
63  pub(crate) fn from_record(record: Pair) -> Result<CopyInstruction> {
64    let span = Span::from_pair(&record);
65    let mut flags = Vec::new();
66    let mut paths = Vec::new();
67
68    for field in record.into_inner() {
69      match field.as_rule() {
70        Rule::copy_flag => flags.push(CopyFlag::from_record(field)?),
71        Rule::copy_pathspec => paths.push(parse_string(&field)?),
72        Rule::comment => continue,
73        _ => return Err(unexpected_token(field))
74      }
75    }
76
77    ensure!(
78      paths.len() >= 2,
79      GenericParseError {
80        message: "copy requires at least one source and a destination"
81      }
82    );
83
84    // naughty unwrap, but we know there's something to pop
85    let destination = paths.pop().unwrap();
86
87    Ok(CopyInstruction {
88      span,
89      flags,
90      sources: paths,
91      destination
92    })
93  }
94}
95
96impl<'a> TryFrom<&'a Instruction> for &'a CopyInstruction {
97  type Error = Error;
98
99  fn try_from(instruction: &'a Instruction) -> std::result::Result<Self, Self::Error> {
100    if let Instruction::Copy(c) = instruction {
101      Ok(c)
102    } else {
103      Err(Error::ConversionError {
104        from: format!("{:?}", instruction),
105        to: "CopyInstruction".into()
106      })
107    }
108  }
109}
110
111#[cfg(test)]
112mod tests {
113  use indoc::indoc;
114  use pretty_assertions::assert_eq;
115
116  use super::*;
117  use crate::test_util::*;
118
119  #[test]
120  fn copy_basic() -> Result<()> {
121    assert_eq!(
122      parse_single("copy foo bar", Rule::copy)?,
123      CopyInstruction {
124        span: Span { start: 0, end: 12 },
125        flags: vec![],
126        sources: vec![SpannedString {
127          span: Span::new(5, 8),
128          content: "foo".to_string()
129        }],
130        destination: SpannedString {
131          span: Span::new(9, 12),
132          content: "bar".to_string()
133        },
134      }.into()
135    );
136
137    Ok(())
138  }
139
140  #[test]
141  fn copy_multiple_sources() -> Result<()> {
142    assert_eq!(
143      parse_single("copy foo bar baz qux", Rule::copy)?,
144      CopyInstruction {
145        span: Span { start: 0, end: 20 },
146        flags: vec![],
147        sources: vec![SpannedString {
148          span: Span::new(5, 8),
149          content: "foo".to_string(),
150        }, SpannedString {
151          span: Span::new(9, 12),
152          content: "bar".to_string()
153        }, SpannedString {
154          span: Span::new(13, 16),
155          content: "baz".to_string()
156        }],
157        destination: SpannedString {
158          span: Span::new(17, 20),
159          content: "qux".to_string()
160        },
161      }.into()
162    );
163
164    Ok(())
165  }
166
167  #[test]
168  fn copy_multiline() -> Result<()> {
169    // multiline is okay; whitespace on the next line is optional
170    assert_eq!(
171      parse_single("copy foo \\\nbar", Rule::copy)?,
172      CopyInstruction {
173        span: Span { start: 0, end: 14 },
174        flags: vec![],
175        sources: vec![SpannedString {
176          span: Span::new(5, 8),
177          content: "foo".to_string(),
178        }],
179        destination: SpannedString {
180          span: Span::new(11, 14),
181          content: "bar".to_string(),
182        },
183      }.into()
184    );
185
186    // newlines must be escaped
187    assert_eq!(
188      parse_single("copy foo\nbar", Rule::copy).is_err(),
189      true
190    );
191
192    Ok(())
193  }
194
195  #[test]
196  fn copy_flags() -> Result<()> {
197    assert_eq!(
198      parse_single(
199        "copy --from=alpine:3.10 /usr/lib/libssl.so.1.1 /tmp/",
200        Rule::copy
201      )?,
202      CopyInstruction {
203        span: Span { start: 0, end: 52 },
204        flags: vec![
205          CopyFlag {
206            span: Span { start: 5, end: 23 },
207            name: SpannedString {
208              content: "from".into(),
209              span: Span { start: 7, end: 11 },
210            },
211            value: SpannedString {
212              content: "alpine:3.10".into(),
213              span: Span { start: 12, end: 23 },
214            }
215          }
216        ],
217        sources: vec![SpannedString {
218          span: Span::new(24, 46),
219          content: "/usr/lib/libssl.so.1.1".to_string(),
220        }],
221        destination: SpannedString {
222          span: Span::new(47, 52),
223          content: "/tmp/".into(),
224        }
225      }.into()
226    );
227
228    Ok(())
229  }
230
231  #[test]
232  fn copy_comments() -> Result<()> {
233    assert_eq!(
234      parse_single(
235        indoc!(r#"
236          copy \
237            --from=alpine:3.10 \
238
239            # hello
240
241            /usr/lib/libssl.so.1.1 \
242            # world
243            /tmp/
244        "#),
245        Rule::copy
246      )?.into_copy().unwrap(),
247      CopyInstruction {
248        span: Span { start: 0, end: 86 },
249        flags: vec![
250          CopyFlag {
251            span: Span { start: 9, end: 27 },
252            name: SpannedString {
253              span: Span { start: 11, end: 15 },
254              content: "from".into(),
255            },
256            value: SpannedString {
257              span: Span { start: 16, end: 27 },
258              content: "alpine:3.10".into(),
259            },
260          }
261        ],
262        sources: vec![SpannedString {
263          span: Span::new(44, 66),
264          content: "/usr/lib/libssl.so.1.1".to_string(),
265        }],
266        destination: SpannedString {
267          span: Span::new(81, 86),
268          content: "/tmp/".into(),
269        },
270      }.into()
271    );
272
273    Ok(())
274  }
275}