1use super::*;
2use crate::storage::SourceRef;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5static NEXT_PAIR: AtomicU64 = AtomicU64::new(1);
6
7#[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}