dockerfile_parser/
splicer.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryInto;
4use std::fmt;
5
6use crate::parser::Pair;
7use crate::dockerfile_parser::Dockerfile;
8
9/// An offset used to adjust proceeding Spans after content has been spliced
10#[derive(Debug)]
11struct SpliceOffset {
12  position: usize,
13  offset: isize
14}
15
16/// A byte-index tuple representing a span of characters in a string
17#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Copy)]
18pub struct Span {
19  pub start: usize,
20  pub end: usize
21}
22
23impl Span {
24  pub fn new(start: usize, end: usize) -> Span {
25    Span { start, end }
26  }
27
28  pub(crate) fn from_pair(record: &Pair) -> Span {
29    let pest_span = record.as_span();
30
31    Span {
32      start: pest_span.start(),
33      end: pest_span.end()
34    }
35  }
36
37  fn adjust_offsets(&self, offsets: &[SpliceOffset]) -> Span {
38    let mut start = self.start as isize;
39    let mut end = self.end as isize;
40
41    for splice in offsets {
42      if splice.position < start as usize {
43        start += splice.offset;
44        end += splice.offset;
45      } else if splice.position < end as usize {
46        end += splice.offset;
47      }
48    }
49
50    Span {
51      start: start.try_into().ok().unwrap_or(0),
52      end: end.try_into().ok().unwrap_or(0)
53    }
54  }
55
56  /// Determines the 0-indexed line number and line-relative position of this
57  /// span.
58  ///
59  /// A reference to the Dockerfile is necessary to examine the original input
60  /// string. Note that if the original span crosses a newline boundary, the
61  /// relative span's `end` field will be larger than the line length.
62  pub fn relative_span(&self, dockerfile: &Dockerfile) -> (usize, Span) {
63    let mut line_start_offset = 0;
64    let mut lines = 0;
65    for (i, c) in dockerfile.content.as_bytes().iter().enumerate() {
66      if i == self.start {
67        break;
68      }
69
70      if *c == b'\n' {
71        lines += 1;
72        line_start_offset = i + 1;
73      }
74    }
75
76    let start = self.start - line_start_offset;
77    let end = start + (self.end - self.start);
78
79    (lines, Span { start, end })
80  }
81}
82
83impl From<(usize, usize)> for Span {
84  fn from(tup: (usize, usize)) -> Span {
85    Span::new(tup.0, tup.1)
86  }
87}
88
89impl From<&Pair<'_>> for Span {
90  fn from(pair: &Pair<'_>) -> Self {
91    Span::from_pair(&pair)
92  }
93}
94
95impl fmt::Debug for Span {
96  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97    f.debug_tuple("")
98      .field(&self.start)
99      .field(&self.end)
100      .finish()
101  }
102}
103
104/// A utility to repeatedly replace spans of text within a larger document.
105///
106/// Each subsequent call to `Splicer::splice(...)` rewrites the `content` buffer
107/// and appends to the list of internal offsets. `Splicer::splice(...)` then
108/// adjusts span bounds at call-time to ensures repeated calls to `splice(...)`
109/// continue to work even if one or both of the span bounds have shifted.
110///
111/// # Example
112/// ```
113/// use dockerfile_parser::*;
114///
115/// let dockerfile: Dockerfile = r#"
116///   FROM alpine:3.10
117/// "#.parse()?;
118///
119/// let from = match &dockerfile.instructions[0] {
120///   Instruction::From(f) => f,
121///   _ => panic!("invalid")
122/// };
123///
124/// let mut splicer = dockerfile.splicer();
125/// splicer.splice(&from.image.span, "alpine:3.11");
126///
127/// assert_eq!(splicer.content, r#"
128///   FROM alpine:3.11
129/// "#);
130/// # Ok::<(), dockerfile_parser::Error>(())
131/// ```
132pub struct Splicer {
133  /// The current content of the splice buffer.
134  pub content: String,
135
136  splice_offsets: Vec<SpliceOffset>
137}
138
139impl Splicer {
140  /// Creates a new Splicer from the given Dockerfile.
141  pub(crate) fn from(dockerfile: &Dockerfile) -> Splicer {
142    Splicer {
143      content: dockerfile.content.clone(),
144      splice_offsets: Vec::new()
145    }
146  }
147
148  pub(crate) fn from_str(s: &str) -> Splicer {
149    Splicer {
150      content: s.to_string(),
151      splice_offsets: Vec::new()
152    }
153  }
154
155  /// Replaces a Span with the given replacement string, mutating the `content`
156  /// string.
157  ///
158  /// Sections may be deleted by replacing them with an empty string (`""`).
159  ///
160  /// Note that spans are always relative to the *original input document*.
161  /// Span offsets are recalculated at call-time to account for previous calls
162  /// to `splice(...)` that may have shifted one or both of the span bounds.
163  pub fn splice(&mut self, span: &Span, replacement: &str) {
164    let span = span.adjust_offsets(&self.splice_offsets);
165
166    // determine the splice offset (only used on subsequent splices)
167    let prev_len = span.end - span.start;
168    let new_len = replacement.len();
169    let offset = new_len as isize - prev_len as isize;
170    self.splice_offsets.push(
171      SpliceOffset { position: span.start, offset }
172    );
173
174    // split and rebuild the content with the replacement instead
175    let (beginning, rest) = self.content.split_at(span.start);
176    let (_, end) = rest.split_at(span.end - span.start);
177    self.content = format!("{}{}{}", beginning, replacement, end);
178  }
179}
180
181#[cfg(test)]
182mod tests {
183  use std::convert::TryInto;
184  use indoc::indoc;
185  use crate::*;
186
187  #[test]
188  fn test_relative_span() {
189    let d = Dockerfile::parse(indoc!(r#"
190      FROM alpine:3.10 as build
191      FROM alpine:3.10
192
193      RUN echo "hello world"
194
195      COPY --from=build /foo /bar
196    "#)).unwrap();
197
198    let first_from = TryInto::<&FromInstruction>::try_into(&d.instructions[0]).unwrap();
199    assert_eq!(
200      first_from.alias.as_ref().unwrap().span.relative_span(&d),
201      (0, (20, 25).into())
202    );
203
204    let copy = TryInto::<&CopyInstruction>::try_into(&d.instructions[3]).unwrap();
205
206    let len = copy.span.end - copy.span.start;
207    let content = &d.content[copy.span.start .. copy.span.end];
208
209    let (rel_line_index, rel_span) = copy.span.relative_span(&d);
210    let rel_len = rel_span.end - rel_span.start;
211    assert_eq!(len, rel_len);
212
213    let rel_line = d.content.lines().collect::<Vec<&str>>()[rel_line_index];
214    let rel_content = &rel_line[rel_span.start .. rel_span.end];
215    assert_eq!(rel_line, "COPY --from=build /foo /bar");
216    assert_eq!(content, rel_content);
217
218    // COPY --from=build /foo /bar
219    assert_eq!(
220      copy.span.relative_span(&d),
221      (5, (0, 27).into())
222    );
223
224    // --from=build
225    assert_eq!(
226      copy.flags[0].span.relative_span(&d),
227      (5, (5, 17).into())
228    );
229
230    // build
231    assert_eq!(
232      copy.flags[0].value.span.relative_span(&d),
233      (5, (12, 17).into())
234    );
235  }
236}