Skip to main content

diffs_rs/source/
correspondence.rs

1use super::*;
2use crate::storage::SourceRef;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5static NEXT_PAIR: AtomicU64 = AtomicU64::new(1);
6
7/// Published only after both supplied records and every omitted range match.
8#[derive(Debug)]
9pub struct CheckedPair {
10    pub(crate) identity: u64,
11    pub(crate) file: Arc<FileIndex>,
12    old: Option<Arc<Snapshot>>,
13    new: Option<Arc<Snapshot>>,
14    pub(crate) gaps: Box<[Gap]>,
15}
16
17#[derive(Clone, Copy, Debug)]
18pub(crate) struct Gap {
19    pub old: u32,
20    pub new: u32,
21    pub len: u32,
22    pub before: Option<usize>,
23    pub no_newline: bool,
24}
25
26impl CheckedPair {
27    pub fn file(&self) -> &Arc<FileIndex> {
28        &self.file
29    }
30    pub fn old(&self) -> Option<&Arc<Snapshot>> {
31        self.old.as_ref()
32    }
33    pub fn new_source(&self) -> Option<&Arc<Snapshot>> {
34        self.new.as_ref()
35    }
36
37    pub(crate) fn shared_gap_after(
38        &self,
39        old: usize,
40        new: usize,
41    ) -> Option<(Range<usize>, Range<usize>)> {
42        let index = self.gaps.partition_point(|gap| {
43            gap.old as usize + gap.len as usize <= old || gap.new as usize + gap.len as usize <= new
44        });
45        let gap = self.gaps.get(index)?;
46        let offset = old
47            .saturating_sub(gap.old as usize)
48            .max(new.saturating_sub(gap.new as usize));
49        Some((
50            gap.old as usize + offset..gap.old as usize + gap.len as usize,
51            gap.new as usize + offset..gap.new as usize + gap.len as usize,
52        ))
53    }
54
55    pub(crate) fn context_info(&self, old: u32, new: u32) -> Result<crate::storage::LineInfo> {
56        let index = self.gaps.partition_point(|gap| gap.old < old);
57        let gap = index
58            .checked_sub(1)
59            .and_then(|index| self.gaps.get(index))
60            .context("Context line is outside the checked gaps")?;
61        let offset = old - gap.old - 1;
62        ensure!(
63            offset < gap.len && new.checked_sub(gap.new) == Some(offset + 1),
64            "Context line does not match the checked source pair"
65        );
66        Ok(crate::storage::LineInfo {
67            kind: crate::model::Kind::Context,
68            old: Some(old),
69            new: Some(new),
70            no_newline: gap.no_newline && offset + 1 == gap.len,
71        })
72    }
73}
74
75pub fn check_pair(
76    file: Arc<FileIndex>,
77    old: Option<Arc<Snapshot>>,
78    new: Option<Arc<Snapshot>>,
79    check: &impl Fn() -> Result<()>,
80) -> Result<CheckedPair> {
81    ensure!(
82        file.status.as_ref() != "Binary change",
83        "Binary entries do not supply textual context"
84    );
85    let mut old_line = 0;
86    let mut new_line = 0;
87    let mut comparison = ByteComparison::new(check);
88    let mut gaps = Vec::new();
89    for (index, hunk) in file.hunks.iter().enumerate() {
90        check()?;
91        let has_old = hunk.lines.clone().any(|id| file.info(id).old.is_some());
92        let has_new = hunk.lines.clone().any(|id| file.info(id).new.is_some());
93        let old_start = hunk
94            .old_start
95            .checked_sub(u32::from(has_old))
96            .context("Invalid old source range")?;
97        let new_start = hunk
98            .new_start
99            .checked_sub(u32::from(has_new))
100            .context("Invalid new source range")?;
101        gap(
102            old.as_deref(),
103            new.as_deref(),
104            old_line..old_start,
105            new_line..new_start,
106            &mut comparison,
107        )?;
108        if old_start > old_line {
109            gaps.push(Gap {
110                old: old_line,
111                new: new_line,
112                len: old_start - old_line,
113                before: Some(index),
114                no_newline: false,
115            });
116        }
117        old_line = old_start;
118        new_line = new_start;
119        let mut old_cursor = old
120            .as_ref()
121            .filter(|_| has_old)
122            .map(|s| s.line_cursor(old_start))
123            .transpose()?;
124        let mut new_cursor = new
125            .as_ref()
126            .filter(|_| has_new)
127            .map(|s| s.line_cursor(new_start))
128            .transpose()?;
129        for (id, reference) in hunk
130            .lines
131            .clone()
132            .zip(file.source_refs(hunk.lines.clone())?)
133        {
134            check()?;
135            let info = file.info(id);
136            let reference = reference?;
137            if let Some(number) = info.old {
138                line(
139                    &file,
140                    reference,
141                    info.no_newline,
142                    old_cursor.as_mut(),
143                    number,
144                    &mut old_line,
145                    &mut comparison,
146                )?;
147            }
148            if let Some(number) = info.new {
149                line(
150                    &file,
151                    reference,
152                    info.no_newline,
153                    new_cursor.as_mut(),
154                    number,
155                    &mut new_line,
156                    &mut comparison,
157                )?;
158            }
159        }
160    }
161    gap(
162        old.as_deref(),
163        new.as_deref(),
164        old_line..old.as_ref().map_or(0, |s| s.lines()),
165        new_line..new.as_ref().map_or(0, |s| s.lines()),
166        &mut comparison,
167    )?;
168    let end = old.as_ref().map_or(0, |s| s.lines());
169    if !file.hunks.is_empty() && end > old_line {
170        gaps.push(Gap {
171            old: old_line,
172            new: new_line,
173            len: end - old_line,
174            before: None,
175            no_newline: old.as_ref().is_some_and(|s| !s.final_newline()),
176        });
177    }
178    let identity = NEXT_PAIR
179        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
180        .map_err(|_| anyhow::anyhow!("Checked source identity exhausted"))?;
181    Ok(CheckedPair {
182        identity,
183        file,
184        old,
185        new,
186        gaps: gaps.into_boxed_slice(),
187    })
188}
189
190fn line(
191    file: &FileIndex,
192    reference: SourceRef,
193    no_newline: bool,
194    cursor: Option<&mut LineCursor<'_>>,
195    number: u32,
196    next: &mut u32,
197    comparison: &mut ByteComparison<'_>,
198) -> Result<()> {
199    let cursor =
200        cursor.context("Source does not match this diff: absent side has a source line")?;
201    let source = cursor.source;
202    let end = next.checked_add(1).context("Source line overflow")?;
203    ensure!(
204        number == end,
205        "Source does not match this diff: incorrect line number"
206    );
207    ensure!(
208        no_newline == (number == source.lines && !source.final_newline),
209        "Source does not match this diff: final newline differs"
210    );
211    comparison.equal(cursor.next()?, file.spool.source_reader(reference)?)?;
212    *next = end;
213    Ok(())
214}
215
216fn gap(
217    old: Option<&Snapshot>,
218    new: Option<&Snapshot>,
219    a: Range<u32>,
220    b: Range<u32>,
221    comparison: &mut ByteComparison<'_>,
222) -> Result<()> {
223    ensure!(
224        a.start <= a.end && b.start <= b.end && a.end - a.start == b.end - b.start,
225        "Source does not match this diff: omitted line ranges differ"
226    );
227    if a.is_empty() {
228        return Ok(());
229    }
230    let old = old.context("Source does not match this diff: absent old context")?;
231    let new = new.context("Source does not match this diff: absent new context")?;
232    comparison.equal(old.uncached_range(a)?, new.uncached_range(b)?)
233}