Skip to main content

clankerdiff_core/
patch.rs

1use similar::TextDiff;
2use thiserror::Error;
3
4#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
5pub enum PatchError {
6    #[error("patch path must be nonempty, contain no NUL bytes, and not be /dev/null")]
7    InvalidPath,
8    #[error("text patches cannot contain NUL bytes")]
9    BinaryText,
10}
11
12pub fn git_patch_from_texts(
13    path: &str,
14    old: Option<&str>,
15    new: Option<&str>,
16) -> Result<Option<String>, PatchError> {
17    if path.is_empty() || path.contains('\0') || path == "/dev/null" {
18        return Err(PatchError::InvalidPath);
19    }
20    if old.into_iter().chain(new).any(|text| text.contains('\0')) {
21        return Err(PatchError::BinaryText);
22    }
23    if old == new {
24        return Ok(None);
25    }
26
27    let quoted_path = quote_path(path);
28    let mut output = format!("diff --git {quoted_path} {quoted_path}\n");
29    if old.is_none() {
30        output.push_str("new file mode 100644\n");
31    } else if new.is_none() {
32        output.push_str("deleted file mode 100644\n");
33    }
34
35    let old_path = if old.is_some() {
36        &quoted_path
37    } else {
38        "/dev/null"
39    };
40    let new_path = if new.is_some() {
41        &quoted_path
42    } else {
43        "/dev/null"
44    };
45    output.extend(["--- ", old_path, "\n+++ ", new_path, "\n"]);
46    let diff = TextDiff::from_lines(old.unwrap_or_default(), new.unwrap_or_default());
47    output.push_str(&diff.unified_diff().context_radius(3).to_string());
48    Ok(Some(output))
49}
50
51fn quote_path(path: &str) -> String {
52    let mut quoted = String::from("\"");
53    for byte in path.bytes() {
54        match byte {
55            b'"' => quoted.push_str("\\\""),
56            b'\\' => quoted.push_str("\\\\"),
57            0x20..=0x7e => quoted.push(char::from(byte)),
58            _ => {
59                quoted.push('\\');
60                quoted.push(char::from(b'0' + (byte >> 6)));
61                quoted.push(char::from(b'0' + ((byte >> 3) & 7)));
62                quoted.push(char::from(b'0' + (byte & 7)));
63            }
64        }
65    }
66    quoted.push('"');
67    quoted
68}