diffy_imara/patch/
mod.rs

1mod format;
2mod parse;
3
4pub use format::PatchFormatter;
5pub use parse::ParsePatchError;
6
7use std::{borrow::Cow, fmt, ops};
8
9const NO_NEWLINE_AT_EOF: &str = "\\ No newline at end of file";
10
11/// Representation of all the differences between two files
12#[derive(PartialEq, Eq)]
13pub struct Patch<'a, T: ToOwned + ?Sized> {
14    // TODO GNU patch is able to parse patches without filename headers.
15    // This should be changed to an `Option` type to reflect this instead of setting this to ""
16    // when they're missing
17    original: Option<Filename<'a, T>>,
18    modified: Option<Filename<'a, T>>,
19    hunks: Vec<Hunk<'a, T>>,
20}
21
22impl<'a, T: ToOwned + ?Sized> Patch<'a, T> {
23    pub(crate) fn new<O, M>(
24        original: Option<O>,
25        modified: Option<M>,
26        hunks: Vec<Hunk<'a, T>>,
27    ) -> Self
28    where
29        O: Into<Cow<'a, T>>,
30        M: Into<Cow<'a, T>>,
31    {
32        let original = original.map(|o| Filename(o.into()));
33        let modified = modified.map(|m| Filename(m.into()));
34        Self {
35            original,
36            modified,
37            hunks,
38        }
39    }
40
41    /// Return the name of the old file
42    pub fn original(&self) -> Option<&T> {
43        self.original.as_ref().map(AsRef::as_ref)
44    }
45
46    /// Return the name of the new file
47    pub fn modified(&self) -> Option<&T> {
48        self.modified.as_ref().map(AsRef::as_ref)
49    }
50
51    /// Returns the hunks in the patch
52    pub fn hunks(&self) -> &[Hunk<'_, T>] {
53        &self.hunks
54    }
55
56    pub fn reverse(&self) -> Patch<'_, T> {
57        let hunks = self.hunks.iter().map(Hunk::reverse).collect();
58        Patch {
59            original: self.modified.clone(),
60            modified: self.original.clone(),
61            hunks,
62        }
63    }
64}
65
66impl<T: AsRef<[u8]> + ToOwned + ?Sized> Patch<'_, T> {
67    /// Convert a `Patch` into bytes
68    ///
69    /// This is the equivalent of the `to_string` function but for
70    /// potentially non-utf8 patches.
71    pub fn to_bytes(&self) -> Vec<u8> {
72        let mut bytes = Vec::new();
73        PatchFormatter::new()
74            .write_patch_into(self, &mut bytes)
75            .unwrap();
76        bytes
77    }
78}
79
80impl<'a> Patch<'a, str> {
81    /// Parse a `Patch` from a string
82    ///
83    /// ```
84    /// use diffy_imara::Patch;
85    ///
86    /// let s = "\
87    /// --- a/ideals
88    /// +++ b/ideals
89    /// @@ -1,4 +1,6 @@
90    ///  First:
91    ///      Life before death,
92    ///      strength before weakness,
93    ///      journey before destination.
94    /// +Second:
95    /// +    I will protect those who cannot protect themselves.
96    /// ";
97    ///
98    /// let patch = Patch::from_str(s).unwrap();
99    /// ```
100    #[allow(clippy::should_implement_trait)]
101    pub fn from_str(s: &'a str) -> Result<Patch<'a, str>, ParsePatchError> {
102        parse::parse(s)
103    }
104}
105
106impl<'a> Patch<'a, [u8]> {
107    /// Parse a `Patch` from bytes
108    pub fn from_bytes(s: &'a [u8]) -> Result<Patch<'a, [u8]>, ParsePatchError> {
109        parse::parse_bytes(s)
110    }
111}
112
113impl<T: ToOwned + ?Sized> Clone for Patch<'_, T> {
114    fn clone(&self) -> Self {
115        Self {
116            original: self.original.clone(),
117            modified: self.modified.clone(),
118            hunks: self.hunks.clone(),
119        }
120    }
121}
122
123impl fmt::Display for Patch<'_, str> {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}", PatchFormatter::new().fmt_patch(self))
126    }
127}
128
129impl<T: ?Sized, O> fmt::Debug for Patch<'_, T>
130where
131    T: ToOwned<Owned = O> + fmt::Debug,
132    O: std::borrow::Borrow<T> + fmt::Debug,
133{
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.debug_struct("Patch")
136            .field("original", &self.original)
137            .field("modified", &self.modified)
138            .field("hunks", &self.hunks)
139            .finish()
140    }
141}
142
143#[derive(PartialEq, Eq)]
144struct Filename<'a, T: ToOwned + ?Sized>(Cow<'a, T>);
145
146const ESCAPED_CHARS: &[char] = &['\n', '\t', '\0', '\r', '\"', '\\'];
147#[allow(clippy::byte_char_slices)]
148const ESCAPED_CHARS_BYTES: &[u8] = &[b'\n', b'\t', b'\0', b'\r', b'\"', b'\\'];
149
150impl Filename<'_, str> {
151    fn needs_to_be_escaped(&self) -> bool {
152        self.0.contains(ESCAPED_CHARS)
153    }
154}
155
156impl<T: ToOwned + AsRef<[u8]> + ?Sized> Filename<'_, T> {
157    fn needs_to_be_escaped_bytes(&self) -> bool {
158        self.0
159            .as_ref()
160            .as_ref()
161            .iter()
162            .any(|b| ESCAPED_CHARS_BYTES.contains(b))
163    }
164
165    fn write_into<W: std::io::Write>(&self, mut w: W) -> std::io::Result<()> {
166        if self.needs_to_be_escaped_bytes() {
167            w.write_all(b"\"")?;
168            for b in self.0.as_ref().as_ref() {
169                if ESCAPED_CHARS_BYTES.contains(b) {
170                    w.write_all(b"\\")?;
171                }
172                w.write_all(&[*b])?;
173            }
174            w.write_all(b"\"")?;
175        } else {
176            w.write_all(self.0.as_ref().as_ref())?;
177        }
178
179        Ok(())
180    }
181}
182
183impl<T: ToOwned + ?Sized> AsRef<T> for Filename<'_, T> {
184    fn as_ref(&self) -> &T {
185        &self.0
186    }
187}
188
189impl<T: ToOwned + ?Sized> ops::Deref for Filename<'_, T> {
190    type Target = T;
191
192    fn deref(&self) -> &Self::Target {
193        &self.0
194    }
195}
196
197impl<T: ToOwned + ?Sized> Clone for Filename<'_, T> {
198    fn clone(&self) -> Self {
199        Self(self.0.clone())
200    }
201}
202
203impl fmt::Display for Filename<'_, str> {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        use std::fmt::Write;
206        if self.needs_to_be_escaped() {
207            f.write_char('\"')?;
208            for c in self.0.chars() {
209                if ESCAPED_CHARS.contains(&c) {
210                    f.write_char('\\')?;
211                }
212                f.write_char(c)?;
213            }
214            f.write_char('\"')?;
215        } else {
216            f.write_str(&self.0)?;
217        }
218
219        Ok(())
220    }
221}
222
223impl<T: ?Sized, O> fmt::Debug for Filename<'_, T>
224where
225    T: ToOwned<Owned = O> + fmt::Debug,
226    O: std::borrow::Borrow<T> + fmt::Debug,
227{
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_tuple("Filename").field(&self.0).finish()
230    }
231}
232
233/// Represents a group of differing lines between two files
234#[derive(Debug, PartialEq, Eq)]
235pub struct Hunk<'a, T: ?Sized> {
236    old_range: HunkRange,
237    new_range: HunkRange,
238
239    function_context: Option<&'a T>,
240
241    lines: Vec<Line<'a, T>>,
242}
243
244fn hunk_lines_count<T: ?Sized>(lines: &[Line<'_, T>]) -> (usize, usize) {
245    lines.iter().fold((0, 0), |count, line| match line {
246        Line::Context(_) => (count.0 + 1, count.1 + 1),
247        Line::Delete(_) => (count.0 + 1, count.1),
248        Line::Insert(_) => (count.0, count.1 + 1),
249    })
250}
251
252impl<'a, T: ?Sized> Hunk<'a, T> {
253    pub(crate) fn new(
254        old_range: HunkRange,
255        new_range: HunkRange,
256        function_context: Option<&'a T>,
257        lines: Vec<Line<'a, T>>,
258    ) -> Self {
259        let (old_count, new_count) = hunk_lines_count(&lines);
260
261        assert_eq!(old_range.len, old_count);
262        assert_eq!(new_range.len, new_count);
263
264        Self {
265            old_range,
266            new_range,
267            function_context,
268            lines,
269        }
270    }
271
272    /// Returns the corresponding range for the old file in the hunk
273    pub fn old_range(&self) -> HunkRange {
274        self.old_range
275    }
276
277    /// Returns the corresponding range for the new file in the hunk
278    pub fn new_range(&self) -> HunkRange {
279        self.new_range
280    }
281
282    /// Returns the function context (if any) for the hunk
283    pub fn function_context(&self) -> Option<&T> {
284        self.function_context
285    }
286
287    /// Returns the lines in the hunk
288    pub fn lines(&self) -> &[Line<'a, T>] {
289        &self.lines
290    }
291
292    /// Creates a reverse patch for the hunk.  This is equivalent to what
293    /// XDL_PATCH_REVERSE would apply in libxdiff.
294    pub fn reverse(&self) -> Self {
295        let lines = self.lines.iter().map(Line::reverse).collect();
296        Self {
297            old_range: self.new_range,
298            new_range: self.old_range,
299            function_context: self.function_context,
300            lines,
301        }
302    }
303}
304
305impl<T: ?Sized> Clone for Hunk<'_, T> {
306    fn clone(&self) -> Self {
307        Self {
308            old_range: self.old_range,
309            new_range: self.new_range,
310            function_context: self.function_context,
311            lines: self.lines.clone(),
312        }
313    }
314}
315
316/// The range of lines in a file for a particular `Hunk`.
317#[derive(Copy, Clone, Debug, PartialEq, Eq)]
318pub struct HunkRange {
319    /// The starting line number of a hunk
320    start: usize,
321    /// The hunk size (number of lines)
322    len: usize,
323}
324
325impl HunkRange {
326    pub(crate) fn new(start: usize, len: usize) -> Self {
327        Self { start, len }
328    }
329
330    /// Returns the range as a `ops::Range`
331    pub fn range(&self) -> ops::Range<usize> {
332        self.start..self.end()
333    }
334
335    /// Returns the starting line number of the range (inclusive)
336    pub fn start(&self) -> usize {
337        self.start
338    }
339
340    /// Returns the ending line number of the range (exclusive)
341    pub fn end(&self) -> usize {
342        self.start + self.len
343    }
344
345    /// Returns the number of lines in the range
346    pub fn len(&self) -> usize {
347        self.len
348    }
349
350    /// Returns `true` if the range is empty (has a length of `0`)
351    pub fn is_empty(&self) -> bool {
352        self.len == 0
353    }
354}
355
356impl fmt::Display for HunkRange {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(f, "{}", self.start)?;
359        if self.len != 1 {
360            write!(f, ",{}", self.len)?;
361        }
362        Ok(())
363    }
364}
365
366/// A line in either the old file, new file, or both.
367///
368/// A `Line` contains the terminating newline character `\n` unless it is the final
369/// line in the file and the file does not end with a newline character.
370#[derive(Debug, PartialEq, Eq)]
371pub enum Line<'a, T: ?Sized> {
372    /// A line providing context in the diff which is present in both the old and new file
373    Context(&'a T),
374    /// A line deleted from the old file
375    Delete(&'a T),
376    /// A line inserted to the new file
377    Insert(&'a T),
378}
379
380impl<T: ?Sized> Copy for Line<'_, T> {}
381
382impl<T: ?Sized> Clone for Line<'_, T> {
383    fn clone(&self) -> Self {
384        *self
385    }
386}
387
388impl<T: ?Sized> Line<'_, T> {
389    pub fn reverse(&self) -> Self {
390        match self {
391            Line::Context(s) => Line::Context(s),
392            Line::Delete(s) => Line::Insert(s),
393            Line::Insert(s) => Line::Delete(s),
394        }
395    }
396}