Skip to main content

java_diff_utils_rs/
unified_diff_utils.rs

1//! Utilities for generating and parsing single-file Unified Diffs.
2
3use regex::Regex;
4use std::collections::HashMap;
5
6use crate::diff_utils::DiffUtils;
7use crate::patch::change_delta::ChangeDelta;
8use crate::patch::chunk::Chunk;
9use crate::patch::delta::Delta;
10use crate::patch::Patch;
11
12const NULL_FILE_INDICATOR: &str = "/dev/null";
13
14/// Utility methods for unified diff parsing and formatting.
15pub struct UnifiedDiffUtils;
16
17impl UnifiedDiffUtils {
18    /// Parses a sequence of unified diff lines and returns a `Patch<String>`.
19    pub fn parse_unified_diff(diff: &[String]) -> Patch<String> {
20        let chunk_regex =
21            Regex::new(r"^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@.*$").unwrap();
22
23        let mut in_prelude = true;
24        let mut raw_chunk: Vec<(String, String)> = Vec::new();
25        let mut patch = Patch::new();
26
27        let mut old_ln: usize = 0;
28        let mut new_ln: usize = 0;
29
30        for line in diff {
31            if in_prelude {
32                if line.starts_with("+++") {
33                    in_prelude = false;
34                }
35                continue;
36            }
37
38            if let Some(caps) = chunk_regex.captures(line) {
39                Self::process_lines_in_prev_chunk(&mut raw_chunk, &mut patch, old_ln, new_ln);
40
41                old_ln = caps
42                    .get(1)
43                    .map_or(1, |m| m.as_str().parse::<usize>().unwrap_or(1));
44                new_ln = caps
45                    .get(3)
46                    .map_or(1, |m| m.as_str().parse::<usize>().unwrap_or(1));
47
48                if old_ln == 0 {
49                    old_ln = 1;
50                }
51                if new_ln == 0 {
52                    new_ln = 1;
53                }
54            } else if !line.is_empty() {
55                let tag = &line[0..1];
56                let rest = &line[1..];
57                if tag == " " || tag == "+" || tag == "-" {
58                    raw_chunk.push((tag.to_string(), rest.to_string()));
59                }
60            } else {
61                raw_chunk.push((" ".to_string(), String::new()));
62            }
63        }
64
65        Self::process_lines_in_prev_chunk(&mut raw_chunk, &mut patch, old_ln, new_ln);
66
67        patch
68    }
69
70    fn process_lines_in_prev_chunk(
71        raw_chunk: &mut Vec<(String, String)>,
72        patch: &mut Patch<String>,
73        old_ln: usize,
74        new_ln: usize,
75    ) {
76        if raw_chunk.is_empty() {
77            return;
78        }
79
80        let mut old_chunk_lines = Vec::new();
81        let mut new_chunk_lines = Vec::new();
82        let mut remove_position = Vec::new();
83        let mut add_position = Vec::new();
84
85        let mut remove_num = 0usize;
86        let mut add_num = 0usize;
87
88        for (tag, rest) in raw_chunk.iter() {
89            if tag == " " || tag == "-" {
90                remove_num += 1;
91                old_chunk_lines.push(rest.clone());
92                if tag == "-" {
93                    remove_position.push((old_ln - 1) + remove_num);
94                }
95            }
96            if tag == " " || tag == "+" {
97                add_num += 1;
98                new_chunk_lines.push(rest.clone());
99                if tag == "+" {
100                    add_position.push((new_ln - 1) + add_num);
101                }
102            }
103        }
104
105        let source_chunk = Chunk::new(
106            old_ln.saturating_sub(1),
107            old_chunk_lines,
108            Some(remove_position),
109        );
110        let target_chunk = Chunk::new(
111            new_ln.saturating_sub(1),
112            new_chunk_lines,
113            Some(add_position),
114        );
115
116        let delta = ChangeDelta::new(source_chunk, target_chunk);
117        patch.add_delta(delta);
118        raw_chunk.clear();
119    }
120
121    /// Generates unified diff output string lines from a given `Patch`.
122    pub fn generate_unified_diff(
123        original_file_name: Option<&str>,
124        revised_file_name: Option<&str>,
125        original_lines: &[String],
126        patch: &Patch<String>,
127        context_size: usize,
128    ) -> Vec<String> {
129        let patch_deltas = patch.deltas();
130        if patch_deltas.is_empty() {
131            return Vec::new();
132        }
133
134        let mut ret = Vec::new();
135        ret.push(format!(
136            "--- {}",
137            original_file_name.unwrap_or(NULL_FILE_INDICATOR)
138        ));
139        ret.push(format!(
140            "+++ {}",
141            revised_file_name.unwrap_or(NULL_FILE_INDICATOR)
142        ));
143
144        let mut deltas: Vec<&Delta<String>> = Vec::new();
145        let mut delta = patch_deltas[0].as_ref();
146        deltas.push(delta);
147
148        if patch_deltas.len() > 1 {
149            for next_delta_box in patch_deltas.iter().skip(1) {
150                let position = delta.source().position();
151                let next_delta = next_delta_box;
152
153                if (position + delta.source().size() + context_size)
154                    >= next_delta.source().position().saturating_sub(context_size)
155                {
156                    deltas.push(next_delta);
157                } else {
158                    let cur_block = Self::process_deltas(original_lines, &deltas, context_size);
159                    ret.extend(cur_block);
160                    deltas.clear();
161                    deltas.push(next_delta);
162                }
163                delta = next_delta;
164            }
165        }
166
167        let cur_block = Self::process_deltas(original_lines, &deltas, context_size);
168        ret.extend(cur_block);
169
170        ret
171    }
172
173    fn process_deltas(
174        orig_lines: &[String],
175        deltas: &[&Delta<String>],
176        context_size: usize,
177    ) -> Vec<String> {
178        let mut buffer = Vec::new();
179        let mut orig_total = 0usize;
180        let mut rev_total = 0usize;
181
182        let cur_delta = deltas[0];
183
184        let context_start = cur_delta.source().position().saturating_sub(context_size);
185
186        for line in context_start..cur_delta.source().position().min(orig_lines.len()) {
187            buffer.push(format!(" {}", orig_lines[line]));
188            orig_total += 1;
189            rev_total += 1;
190        }
191
192        buffer.extend(Self::get_delta_text(cur_delta));
193        orig_total += cur_delta.source().lines().len();
194        rev_total += cur_delta.target().lines().len();
195
196        let mut last_delta = cur_delta;
197        for &next_delta in deltas.iter().skip(1) {
198            let intermediate_start =
199                last_delta.source().position() + last_delta.source().lines().len();
200
201            for line in intermediate_start..next_delta.source().position().min(orig_lines.len()) {
202                buffer.push(format!(" {}", orig_lines[line]));
203                orig_total += 1;
204                rev_total += 1;
205            }
206
207            buffer.extend(Self::get_delta_text(next_delta));
208            orig_total += next_delta.source().lines().len();
209            rev_total += next_delta.target().lines().len();
210            last_delta = next_delta;
211        }
212
213        let post_context_start = last_delta.source().position() + last_delta.source().lines().len();
214        let post_context_end = (post_context_start + context_size).min(orig_lines.len());
215
216        for line in post_context_start..post_context_end {
217            buffer.push(format!(" {}", orig_lines[line]));
218            orig_total += 1;
219            rev_total += 1;
220        }
221
222        let orig_start = if orig_total == 0 {
223            0
224        } else {
225            let pos_plus_one = cur_delta.source().position() + 1;
226            if pos_plus_one > context_size {
227                pos_plus_one - context_size
228            } else {
229                1
230            }
231        };
232
233        let rev_start = if rev_total == 0 {
234            0
235        } else {
236            let rev_pos_plus_one = cur_delta.target().position() + 1;
237            if rev_pos_plus_one > context_size {
238                rev_pos_plus_one - context_size
239            } else {
240                1
241            }
242        };
243
244        let header = format!(
245            "@@ -{},{} +{},{} @@",
246            orig_start, orig_total, rev_start, rev_total
247        );
248        buffer.insert(0, header);
249
250        buffer
251    }
252
253    fn get_delta_text(delta: &Delta<String>) -> Vec<String> {
254        let mut buffer = Vec::new();
255        for line in delta.source().lines() {
256            buffer.push(format!("-{}", line));
257        }
258        for line in delta.target().lines() {
259            buffer.push(format!("+{}", line));
260        }
261        buffer
262    }
263
264    /// Merges diff indicators into original file text (useful for visual diff applications).
265    pub fn generate_original_and_diff(
266        original: &[String],
267        revised: &[String],
268        original_file_name: Option<&str>,
269        revised_file_name: Option<&str>,
270    ) -> Vec<String> {
271        let orig_name = original_file_name.unwrap_or("original");
272        let rev_name = revised_file_name.unwrap_or("revised");
273
274        let patch = DiffUtils::diff(original, revised, None);
275        let mut unified_diff =
276            Self::generate_unified_diff(Some(orig_name), Some(rev_name), original, &patch, 0);
277
278        if unified_diff.is_empty() {
279            unified_diff.push(format!("--- {}", orig_name));
280            unified_diff.push(format!("+++ {}", rev_name));
281            unified_diff.push("@@ -0,0 +0,0 @@".to_string());
282        } else if unified_diff.len() >= 3 && !unified_diff[2].contains("@@ -1,") {
283            unified_diff.insert(2, "@@ -0,0 +0,0 @@".to_string());
284        }
285
286        let original_with_prefix: Vec<String> =
287            original.iter().map(|v| format!(" {}", v)).collect();
288        Self::insert_orig(&original_with_prefix, &unified_diff)
289    }
290
291    fn insert_orig(original: &[String], unified_diff: &[String]) -> Vec<String> {
292        let mut result = Vec::new();
293        let mut diff_list: Vec<Vec<String>> = Vec::new();
294        let mut diff = Vec::new();
295
296        for (i, u) in unified_diff.iter().enumerate() {
297            if u.starts_with("@@") && u != "@@ -0,0 +0,0 @@" && !u.contains("@@ -1,") {
298                diff_list.push(diff.clone());
299                diff.clear();
300                diff.push(u.clone());
301                continue;
302            }
303            if i == unified_diff.len() - 1 {
304                diff.push(u.clone());
305                diff_list.push(diff.clone());
306                diff.clear();
307                break;
308            }
309            diff.push(u.clone());
310        }
311
312        Self::insert_orig_chunks(&diff_list, &mut result, original);
313        result
314    }
315
316    fn insert_orig_chunks(
317        diff_list: &[Vec<String>],
318        result: &mut Vec<String>,
319        original: &[String],
320    ) {
321        for (i, diff) in diff_list.iter().enumerate() {
322            let nex_diff = diff_list.get(i + 1);
323            let simb = if i == 0 { &diff[2] } else { &diff[0] };
324            let nex_simb = nex_diff.map(|d| &d[0]);
325
326            result.extend(diff.clone());
327            let map = Self::get_row_map(simb);
328
329            if let Some(n_simb) = nex_simb {
330                let nex_map = Self::get_row_map(n_simb);
331                let mut start = 0usize;
332                if map.get("orgRow").cloned().unwrap_or(0) != 0 {
333                    start = (map["orgRow"] + map["orgDel"]).wrapping_sub(1);
334                }
335                let end = nex_map["revRow"].saturating_sub(2);
336                result.extend(Self::get_orig_list(original, start, end));
337            }
338
339            let mut start = (map["orgRow"] + map["orgDel"]).wrapping_sub(1);
340            if start == usize::MAX {
341                start = 0;
342            }
343
344            if simb.contains("@@ -1,") && nex_simb.is_none() && map["orgDel"] != original.len() {
345                result.extend(Self::get_orig_list(original, start, original.len() - 1));
346            } else if nex_simb.is_none()
347                && (map["orgRow"] + map["orgDel"]).wrapping_sub(1) < original.len()
348            {
349                result.extend(Self::get_orig_list(original, start, original.len() - 1));
350            }
351        }
352    }
353
354    fn get_row_map(str_header: &str) -> HashMap<&'static str, usize> {
355        let mut map = HashMap::new();
356        if str_header.starts_with("@@") {
357            let sp: Vec<&str> = str_header.split(' ').collect();
358            if sp.len() > 1 {
359                let org = sp[1];
360                let org_sp: Vec<&str> = org.split(',').collect();
361                if org_sp.len() >= 2 {
362                    let org_row = org_sp[0][1..].parse::<usize>().unwrap_or(0);
363                    let org_del = org_sp[1].parse::<usize>().unwrap_or(0);
364                    map.insert("orgRow", org_row);
365                    map.insert("orgDel", org_del);
366                    map.insert("revRow", org_row);
367                    map.insert("revAdd", org_del);
368                }
369            }
370        }
371        map
372    }
373
374    fn get_orig_list(original_with_prefix: &[String], start: usize, end: usize) -> Vec<String> {
375        let mut list = Vec::new();
376        if !original_with_prefix.is_empty() && start <= end && end < original_with_prefix.len() {
377            for item in original_with_prefix.iter().take(end + 1).skip(start) {
378                list.push(item.clone());
379            }
380        }
381        list
382    }
383}