Skip to main content

bcp_types/
diff.rs

1use crate::error::TypeError;
2use crate::fields::{
3    decode_bytes_value, decode_field_header, decode_varint_value, encode_bytes_field,
4    encode_nested_field, encode_varint_field, skip_field,
5};
6
7/// DIFF block — represents code changes for a single file.
8///
9/// Used to compactly represent modifications (e.g. from a git diff or
10/// an edit operation). Each hunk captures a contiguous range of changes
11/// in unified diff format.
12///
13/// Field layout within body:
14///
15/// ```text
16/// ┌──────────┬───────────┬───────┬───────────────────────────────┐
17/// │ Field ID │ Wire Type │ Name  │ Description                   │
18/// ├──────────┼───────────┼───────┼───────────────────────────────┤
19/// │ 1        │ Bytes     │ path  │ File path                     │
20/// │ 2        │ Nested    │ hunks │ Repeated DiffHunk             │
21/// └──────────┴───────────┴───────┴───────────────────────────────┘
22/// ```
23///
24/// Multiple hunks produce multiple field-2 occurrences (repeated field
25/// pattern, same as `FileEntry` in FILE_TREE).
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct DiffBlock {
28    pub path: String,
29    pub hunks: Vec<DiffHunk>,
30}
31
32/// A single contiguous range of changes within a diff.
33///
34/// Nested fields within a `DiffHunk`:
35///
36/// ```text
37/// ┌──────────┬───────────┬───────────┬────────────────────────────┐
38/// │ Field ID │ Wire Type │ Name      │ Description                │
39/// ├──────────┼───────────┼───────────┼────────────────────────────┤
40/// │ 1        │ Varint    │ old_start │ Start line in old file     │
41/// │ 2        │ Varint    │ new_start │ Start line in new file     │
42/// │ 3        │ Bytes     │ lines     │ Hunk content (unified fmt) │
43/// └──────────┴───────────┴───────────┴────────────────────────────┘
44/// ```
45///
46/// The `lines` field contains the hunk body in unified diff format:
47/// lines prefixed with `+` (added), `-` (removed), or ` ` (context).
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct DiffHunk {
50    pub old_start: u32,
51    pub new_start: u32,
52    pub lines: Vec<u8>,
53}
54
55impl DiffHunk {
56    /// Encode this hunk into TLV bytes (used as nested field payload).
57    fn encode(&self) -> Vec<u8> {
58        let mut buf = Vec::new();
59        encode_varint_field(&mut buf, 1, u64::from(self.old_start));
60        encode_varint_field(&mut buf, 2, u64::from(self.new_start));
61        encode_bytes_field(&mut buf, 3, &self.lines);
62        buf
63    }
64
65    /// Decode a `DiffHunk` from nested TLV bytes.
66    fn decode(mut buf: &[u8]) -> Result<Self, TypeError> {
67        let mut old_start: Option<u32> = None;
68        let mut new_start: Option<u32> = None;
69        let mut lines: Option<Vec<u8>> = None;
70
71        while !buf.is_empty() {
72            let (header, n) = decode_field_header(buf)?;
73            buf = &buf[n..];
74
75            match header.field_id {
76                1 => {
77                    let (v, n) = decode_varint_value(buf)?;
78                    buf = &buf[n..];
79                    old_start = Some(v as u32);
80                }
81                2 => {
82                    let (v, n) = decode_varint_value(buf)?;
83                    buf = &buf[n..];
84                    new_start = Some(v as u32);
85                }
86                3 => {
87                    let (data, n) = decode_bytes_value(buf)?;
88                    buf = &buf[n..];
89                    lines = Some(data.to_vec());
90                }
91                _ => {
92                    let n = skip_field(buf, header.wire_type)?;
93                    buf = &buf[n..];
94                }
95            }
96        }
97
98        Ok(Self {
99            old_start: old_start.ok_or(TypeError::MissingRequiredField { field: "old_start" })?,
100            new_start: new_start.ok_or(TypeError::MissingRequiredField { field: "new_start" })?,
101            lines: lines.ok_or(TypeError::MissingRequiredField { field: "lines" })?,
102        })
103    }
104}
105
106impl DiffBlock {
107    /// Serialize this block's fields into a TLV-encoded body.
108    pub fn encode_body(&self) -> Vec<u8> {
109        let mut buf = Vec::new();
110        encode_bytes_field(&mut buf, 1, self.path.as_bytes());
111        for hunk in &self.hunks {
112            encode_nested_field(&mut buf, 2, &hunk.encode());
113        }
114        buf
115    }
116
117    /// Deserialize a DIFF block from a TLV-encoded body.
118    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
119        let mut path: Option<String> = None;
120        let mut hunks = Vec::new();
121
122        while !buf.is_empty() {
123            let (header, n) = decode_field_header(buf)?;
124            buf = &buf[n..];
125
126            match header.field_id {
127                1 => {
128                    let (data, n) = decode_bytes_value(buf)?;
129                    buf = &buf[n..];
130                    path = Some(String::from_utf8_lossy(data).into_owned());
131                }
132                2 => {
133                    let (data, n) = decode_bytes_value(buf)?;
134                    buf = &buf[n..];
135                    hunks.push(DiffHunk::decode(data)?);
136                }
137                _ => {
138                    let n = skip_field(buf, header.wire_type)?;
139                    buf = &buf[n..];
140                }
141            }
142        }
143
144        Ok(Self {
145            path: path.ok_or(TypeError::MissingRequiredField { field: "path" })?,
146            hunks,
147        })
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn roundtrip_single_hunk() {
157        let block = DiffBlock {
158            path: "src/main.rs".to_string(),
159            hunks: vec![DiffHunk {
160                old_start: 10,
161                new_start: 10,
162                lines: b" fn main() {\n-    println!(\"old\");\n+    println!(\"new\");\n }\n"
163                    .to_vec(),
164            }],
165        };
166        let body = block.encode_body();
167        let decoded = DiffBlock::decode_body(&body).unwrap();
168        assert_eq!(decoded, block);
169    }
170
171    #[test]
172    fn roundtrip_multiple_hunks() {
173        let block = DiffBlock {
174            path: "lib.rs".to_string(),
175            hunks: vec![
176                DiffHunk {
177                    old_start: 1,
178                    new_start: 1,
179                    lines: b"+use std::io;\n".to_vec(),
180                },
181                DiffHunk {
182                    old_start: 50,
183                    new_start: 51,
184                    lines: b"-    old_call();\n+    new_call();\n".to_vec(),
185                },
186            ],
187        };
188        let body = block.encode_body();
189        let decoded = DiffBlock::decode_body(&body).unwrap();
190        assert_eq!(decoded, block);
191    }
192
193    #[test]
194    fn empty_hunks() {
195        let block = DiffBlock {
196            path: "empty.rs".to_string(),
197            hunks: vec![],
198        };
199        let body = block.encode_body();
200        let decoded = DiffBlock::decode_body(&body).unwrap();
201        assert_eq!(decoded, block);
202    }
203}