dockerfile_parser/
util.rs

1// (C) Copyright 2019 Hewlett Packard Enterprise Development LP
2
3use std::fmt;
4
5use crate::error::*;
6use crate::parser::*;
7use crate::splicer::Span;
8
9use enquote::unquote;
10use snafu::ResultExt;
11
12/// Given a node ostensibly containing a string array, returns an unescaped
13/// array of strings
14pub(crate) fn parse_string_array(array: Pair) -> Result<StringArray> {
15  let span = Span::from_pair(&array);
16  let mut elements = Vec::new();
17
18  for field in array.into_inner() {
19    match field.as_rule() {
20      Rule::string => {
21        elements.push(parse_string(&field)?);
22      },
23      Rule::comment => continue,
24      _ => return Err(unexpected_token(field))
25    }
26  }
27
28  Ok(StringArray {
29    span,
30    elements,
31  })
32}
33
34pub(crate) fn parse_string(field: &Pair) -> Result<SpannedString> {
35  let str_span = Span::from_pair(field);
36  let field_str = field.as_str();
37  let content = if matches!(field_str.chars().next(), Some('"' | '\'' | '`')) {
38    unquote(field_str).context(UnescapeError)?
39  } else {
40    field_str.to_string()
41  };
42
43  Ok(SpannedString {
44    span: str_span,
45    content,
46  })
47}
48
49/// Removes escaped line breaks (\\\n) from a string
50///
51/// This should be used to clean any input from the any_breakable rule
52pub(crate) fn clean_escaped_breaks(s: &str) -> String {
53  s.replace("\\\n", "")
54}
55
56/// A string that may be broken across many lines or an array of strings.
57#[derive(Debug, PartialEq, Eq, Clone)]
58pub enum ShellOrExecExpr {
59  Shell(BreakableString),
60  Exec(StringArray),
61}
62
63impl ShellOrExecExpr {
64  /// Unpacks this expression into its inner value if it is a Shell-form
65  /// instruction, otherwise returns None.
66  pub fn into_shell(self) -> Option<BreakableString> {
67    if let ShellOrExecExpr::Shell(s) = self {
68      Some(s)
69    } else {
70      None
71    }
72  }
73
74  /// Unpacks this expression into its inner value if it is a Shell-form
75  /// instruction, otherwise returns None.
76  pub fn as_shell(&self) -> Option<&BreakableString> {
77    if let ShellOrExecExpr::Shell(s) = self {
78      Some(s)
79    } else {
80      None
81    }
82  }
83
84  /// Unpacks this expression into its inner value if it is an Exec-form
85  /// instruction, otherwise returns None.
86  pub fn into_exec(self) -> Option<StringArray> {
87    if let ShellOrExecExpr::Exec(s) = self {
88      Some(s)
89    } else {
90      None
91    }
92  }
93
94  /// Unpacks this expression into its inner value if it is an Exec-form
95  /// instruction, otherwise returns None.
96  pub fn as_exec(&self) -> Option<&StringArray> {
97    if let ShellOrExecExpr::Exec(s) = self {
98      Some(s)
99    } else {
100      None
101    }
102  }
103}
104
105/// A string array (ex. ["executable", "param1", "param2"])
106#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
107pub struct StringArray {
108  pub span: Span,
109  pub elements: Vec<SpannedString>,
110}
111
112impl StringArray {
113  pub fn as_str_vec(&self) -> Vec<&str> {
114    self.elements.iter().map(|c| c.as_ref()).collect()
115  }
116}
117
118/// A comment with a character span.
119#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
120pub struct SpannedComment {
121  pub span: Span,
122  pub content: String,
123}
124
125/// A string with a character span.
126#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
127pub struct SpannedString {
128  pub span: Span,
129  pub content: String,
130}
131
132impl AsRef<str> for SpannedString {
133  fn as_ref(&self) -> &str {
134    &self.content
135  }
136}
137
138impl fmt::Display for SpannedString {
139  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140    self.content.fmt(f)
141  }
142}
143
144/// A component of a breakable string.
145#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
146pub enum BreakableStringComponent {
147  String(SpannedString),
148  Comment(SpannedComment),
149}
150
151impl From<SpannedString> for BreakableStringComponent {
152  fn from(s: SpannedString) -> Self {
153    BreakableStringComponent::String(s)
154  }
155}
156
157impl From<((usize, usize), &str)> for BreakableStringComponent {
158  fn from(s: ((usize, usize), &str)) -> Self {
159    let ((start, end), content) = s;
160
161    BreakableStringComponent::String(SpannedString {
162      span: (start, end).into(),
163      content: content.to_string(),
164    })
165  }
166}
167
168impl From<SpannedComment> for BreakableStringComponent {
169  fn from(c: SpannedComment) -> Self {
170    BreakableStringComponent::Comment(c)
171  }
172}
173
174/// A Docker string that may be broken across several lines, separated by line
175/// continuations (`\\\n`), and possibly intermixed with comments.
176///
177/// These strings have several potentially valid interpretations. As these line
178/// continuations match those natively supported by bash, a given multiline
179/// `RUN` block can be pasted into a bash shell unaltered and with line
180/// continuations included. However, at "runtime" line continuations and
181/// comments (*) are stripped from the string handed to the shell.
182///
183/// To ensure output is correct in all cases, `BreakableString` preserves the
184/// user's original AST, including comments, and implements Docker's
185/// continuation-stripping behavior in the `Display` implementation.
186#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone)]
187pub struct BreakableString {
188  pub span: Span,
189  pub components: Vec<BreakableStringComponent>,
190}
191
192/// Formats this breakable string as it will be interpreted by the underlying
193/// Docker engine, i.e. on a single line with line continuations removed
194impl fmt::Display for BreakableString {
195  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196    for component in &self.components {
197      if let BreakableStringComponent::String(s) = &component {
198        write!(f, "{}", s.content)?;
199      }
200    }
201
202    Ok(())
203  }
204}
205
206impl BreakableString {
207  pub fn new(span: impl Into<Span>) -> Self {
208    BreakableString {
209      span: span.into(),
210      components: Vec::new(),
211    }
212  }
213
214  pub fn add(mut self, c: impl Into<BreakableStringComponent>) -> Self {
215    self.components.push(c.into());
216
217    self
218  }
219
220  pub fn add_string(mut self, s: impl Into<Span>, c: impl Into<String>) -> Self {
221    self.components.push(SpannedString {
222      span: s.into(),
223      content: c.into(),
224    }.into());
225
226    self
227  }
228
229  pub fn add_comment(mut self, s: impl Into<Span>, c: impl Into<String>) -> Self {
230    self.components.push(SpannedComment {
231      span: s.into(),
232      content: c.into(),
233    }.into());
234
235    self
236  }
237
238  pub fn iter_components(&self) -> impl Iterator<Item = &BreakableStringComponent> {
239    self.components.iter()
240  }
241}
242
243impl From<((usize, usize), &str)> for BreakableString {
244  fn from(s: ((usize, usize), &str)) -> Self {
245    let ((start, end), content) = s;
246
247    BreakableString::new((start, end))
248      .add_string((start, end), content)
249  }
250}
251
252fn parse_any_breakable_inner(pair: Pair) -> Result<Vec<BreakableStringComponent>> {
253  let mut components = Vec::new();
254
255  for field in pair.into_inner() {
256    match field.as_rule() {
257      Rule::any_breakable => components.extend(parse_any_breakable_inner(field)?),
258      Rule::comment => components.push(SpannedComment {
259        span: (&field).into(),
260        content: field.as_str().to_string(),
261      }.into()),
262      Rule::any_content => components.push(SpannedString {
263        span: (&field).into(),
264        content: field.as_str().to_string(),
265      }.into()),
266      _ => return Err(unexpected_token(field))
267    }
268  }
269
270  Ok(components)
271}
272
273pub(crate) fn parse_any_breakable(pair: Pair) -> Result<BreakableString> {
274  Ok(BreakableString {
275    span: (&pair).into(),
276    components: parse_any_breakable_inner(pair)?,
277  })
278}