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#[derive(PartialEq, Eq)]
13pub struct Patch<'a, T: ToOwned + ?Sized> {
14 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 pub fn original(&self) -> Option<&T> {
43 self.original.as_ref().map(AsRef::as_ref)
44 }
45
46 pub fn modified(&self) -> Option<&T> {
48 self.modified.as_ref().map(AsRef::as_ref)
49 }
50
51 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 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 #[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 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#[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 pub fn old_range(&self) -> HunkRange {
274 self.old_range
275 }
276
277 pub fn new_range(&self) -> HunkRange {
279 self.new_range
280 }
281
282 pub fn function_context(&self) -> Option<&T> {
284 self.function_context
285 }
286
287 pub fn lines(&self) -> &[Line<'a, T>] {
289 &self.lines
290 }
291
292 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
318pub struct HunkRange {
319 start: usize,
321 len: usize,
323}
324
325impl HunkRange {
326 pub(crate) fn new(start: usize, len: usize) -> Self {
327 Self { start, len }
328 }
329
330 pub fn range(&self) -> ops::Range<usize> {
332 self.start..self.end()
333 }
334
335 pub fn start(&self) -> usize {
337 self.start
338 }
339
340 pub fn end(&self) -> usize {
342 self.start + self.len
343 }
344
345 pub fn len(&self) -> usize {
347 self.len
348 }
349
350 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#[derive(Debug, PartialEq, Eq)]
371pub enum Line<'a, T: ?Sized> {
372 Context(&'a T),
374 Delete(&'a T),
376 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}