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