Skip to main content

differential_engine/
paths.rs

1//! Path extraction from `diff --git a/X b/Y` headers.
2//!
3//! The prototype used a greedy regex, which breaks on paths containing spaces.
4//! Under `--no-renames` the two sides are always the same path, so the header is
5//! split symmetrically: find the split point where the `a/` half equals the `b/`
6//! half. C-quoted paths (git quotes bytes outside the printable range even with
7//! core.quotepath=false) are unquoted first.
8
9use memchr::memmem;
10
11/// Parse the remainder of a `diff --git ` line into the (single) path.
12/// `rest` is everything after `"diff --git "`. Returns `None` if the line cannot
13/// be understood — callers treat that as a parse error, never a skip.
14pub fn parse_diff_git_path(rest: &[u8]) -> Option<Vec<u8>> {
15    // Quoted form: "a/pa th" "b/pa th" (either or both sides may be quoted).
16    if rest.first() == Some(&b'"') {
17        let (a, after) = unquote_c(rest)?;
18        let after = after.strip_prefix(b" ")?;
19        let b = if after.first() == Some(&b'"') {
20            unquote_c(after)?.0
21        } else {
22            after.to_vec()
23        };
24        return strip_ab(&a, &b);
25    }
26    // Second side quoted only.
27    if let Some(pos) = memmem::find(rest, b" \"b/") {
28        let a = &rest[..pos];
29        let (b, _) = unquote_c(&rest[pos + 1..])?;
30        return strip_ab(a, &b);
31    }
32    // Unquoted: try every ` b/` boundary until both halves agree.
33    let mut idx = 0;
34    while let Some(off) = memmem::find(&rest[idx..], b" b/") {
35        let pos = idx + off;
36        if let Some(p) = strip_ab(&rest[..pos], &rest[pos + 1..]) {
37            return Some(p);
38        }
39        idx = pos + 1;
40    }
41    None
42}
43
44fn strip_ab(a: &[u8], b: &[u8]) -> Option<Vec<u8>> {
45    let a = a.strip_prefix(b"a/")?;
46    let b = b.strip_prefix(b"b/")?;
47    (a == b).then(|| a.to_vec())
48}
49
50/// Decode one git C-quoted string starting at `s[0] == '"'`.
51/// Returns the decoded bytes and the remainder after the closing quote.
52pub fn unquote_c(s: &[u8]) -> Option<(Vec<u8>, &[u8])> {
53    if s.first() != Some(&b'"') {
54        return None;
55    }
56    let mut out = Vec::new();
57    let mut i = 1;
58    while i < s.len() {
59        match s[i] {
60            b'"' => return Some((out, &s[i + 1..])),
61            b'\\' => {
62                i += 1;
63                let c = *s.get(i)?;
64                match c {
65                    b'n' => out.push(b'\n'),
66                    b't' => out.push(b'\t'),
67                    b'r' => out.push(b'\r'),
68                    b'a' => out.push(0x07),
69                    b'b' => out.push(0x08),
70                    b'f' => out.push(0x0c),
71                    b'v' => out.push(0x0b),
72                    b'\\' => out.push(b'\\'),
73                    b'"' => out.push(b'"'),
74                    b'0'..=b'7' => {
75                        // Up to three octal digits.
76                        let mut val = 0u32;
77                        let mut n = 0;
78                        while n < 3 {
79                            match s.get(i) {
80                                Some(&d @ b'0'..=b'7') => {
81                                    val = val * 8 + u32::from(d - b'0');
82                                    i += 1;
83                                    n += 1;
84                                }
85                                _ => break,
86                            }
87                        }
88                        i -= 1; // loop tail advances
89                        out.push(val as u8);
90                    }
91                    _ => return None,
92                }
93                i += 1;
94            }
95            c => {
96                out.push(c);
97                i += 1;
98            }
99        }
100    }
101    None // unterminated
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn plain_path() {
110        assert_eq!(
111            parse_diff_git_path(b"a/src/main.rs b/src/main.rs").unwrap(),
112            b"src/main.rs"
113        );
114    }
115
116    #[test]
117    fn path_with_spaces() {
118        assert_eq!(
119            parse_diff_git_path(b"a/docs/my file.md b/docs/my file.md").unwrap(),
120            b"docs/my file.md"
121        );
122    }
123
124    #[test]
125    fn adversarial_space_b_slash() {
126        // A path containing " b/" itself: symmetric matching still finds the
127        // unique split where both halves agree.
128        assert_eq!(parse_diff_git_path(b"a/x b/y b/x b/y").unwrap(), b"x b/y");
129    }
130
131    #[test]
132    fn quoted_path() {
133        assert_eq!(
134            parse_diff_git_path(br#""a/t\tab.txt" "b/t\tab.txt""#).unwrap(),
135            b"t\tab.txt"
136        );
137    }
138
139    #[test]
140    fn quoted_octal() {
141        let (v, rest) = unquote_c(br#""\303\251.txt" tail"#).unwrap();
142        assert_eq!(v, "é.txt".as_bytes());
143        assert_eq!(rest, b" tail");
144    }
145
146    #[test]
147    fn mismatched_halves_rejected() {
148        assert!(parse_diff_git_path(b"a/one b/two").is_none());
149    }
150}