clankerdiff_core/
content.rs1use crate::{DiffSide, Fingerprint, RepoPath, SourceSequenceId};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::sync::Arc;
6
7pub const MAX_SOURCE_FILE_BYTES: u64 = 8 * 1024 * 1024;
9pub const MAX_SOURCE_FILE_LINES: usize = 1_000_000;
11
12pub type SourceResult = Result<Arc<SourceDocument>, SourceUnavailable>;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct SourceLineRef {
18 pub side: DiffSide,
19 pub line_number: usize,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct SourceLocation {
25 pub path: RepoPath,
26 pub side: DiffSide,
27 pub line_number: usize,
28}
29
30#[derive(Debug, Clone)]
35pub struct SourceDocument {
36 text: Arc<str>,
37 line_starts: Arc<[usize]>,
38 content_id: Fingerprint,
39 sequence_id: SourceSequenceId,
40 byte_len: u64,
41 trailing_newline: bool,
42}
43
44impl PartialEq for SourceDocument {
45 fn eq(&self, other: &Self) -> bool {
46 self.content_id == other.content_id
47 }
48}
49
50impl Eq for SourceDocument {}
51
52impl Serialize for SourceDocument {
53 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54 serializer.serialize_str(&self.text)
55 }
56}
57
58impl<'de> Deserialize<'de> for SourceDocument {
59 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
60 let text = String::deserialize(deserializer)?;
61 Self::try_from_text(&text).map_err(serde::de::Error::custom)
62 }
63}
64
65impl SourceDocument {
66 pub fn new(text: impl AsRef<str>) -> Result<Self, SourceUnavailable> {
72 Self::try_from_text(text.as_ref())
73 }
74
75 pub fn try_from_text(text: &str) -> Result<Self, SourceUnavailable> {
81 let byte_len = u64::try_from(text.len()).unwrap_or(u64::MAX);
82 if byte_len > MAX_SOURCE_FILE_BYTES {
83 return Err(SourceUnavailable::TooLarge { bytes: byte_len });
84 }
85 let trailing_newline = text.ends_with('\n');
86 let mut line_starts = vec![0];
87 for (index, byte) in text.bytes().enumerate() {
88 if byte == b'\n' {
89 line_starts.push(index + 1);
90 }
91 }
92
93 if trailing_newline {
94 line_starts.pop();
95 }
96 if text.is_empty() {
97 line_starts.clear();
98 }
99
100 let line_count = line_starts.len();
101 if line_count > MAX_SOURCE_FILE_LINES {
102 return Err(SourceUnavailable::TooManyLines { lines: line_count });
103 }
104 let content_id = Fingerprint::of([b"diff-source-document-v1".as_slice(), text.as_bytes()]);
105 let text: Arc<str> = Arc::from(text);
106 let sequence_id = SourceSequenceId::from_lines((0..line_count).map(|line| {
107 let start = line_starts[line];
108 let end = text[start..]
109 .find('\n')
110 .map_or(text.len(), |offset| start + offset);
111 text[start..end]
112 .strip_suffix('\r')
113 .unwrap_or(&text[start..end])
114 }));
115 Ok(Self {
116 text,
117 line_starts: line_starts.into(),
118 content_id,
119 sequence_id,
120 byte_len,
121 trailing_newline,
122 })
123 }
124
125 #[must_use]
127 pub fn line(&self, number: usize) -> Option<&str> {
128 let span = self.line_span(number)?;
129 Some(&self.text[span])
130 }
131
132 #[must_use]
134 pub fn line_span(&self, number: usize) -> Option<std::ops::Range<usize>> {
135 let index = number.checked_sub(1)?;
136 let start = *self.line_starts.get(index)?;
137 let end = self
138 .line_starts
139 .get(index + 1)
140 .copied()
141 .unwrap_or(self.text.len());
142 let end = if end > start && self.text.as_bytes()[end - 1] == b'\n' {
143 end - 1
144 } else {
145 end
146 };
147 let end = if end > start && self.text.as_bytes()[end - 1] == b'\r' {
148 end - 1
149 } else {
150 end
151 };
152 Some(start..end)
153 }
154
155 #[must_use]
156 pub fn text(&self) -> &str {
157 &self.text
158 }
159
160 #[must_use]
161 pub fn line_starts(&self) -> &[usize] {
162 &self.line_starts
163 }
164
165 #[must_use]
166 pub const fn identity(&self) -> Fingerprint {
167 self.content_id
168 }
169
170 #[must_use]
171 pub fn line_count(&self) -> usize {
172 self.line_starts.len()
173 }
174
175 #[must_use]
176 pub const fn content_id(&self) -> Fingerprint {
177 self.content_id
178 }
179
180 #[must_use]
181 pub const fn sequence_id(&self) -> SourceSequenceId {
182 self.sequence_id
183 }
184
185 #[must_use]
186 pub const fn byte_len(&self) -> u64 {
187 self.byte_len
188 }
189
190 #[must_use]
191 pub const fn trailing_newline(&self) -> bool {
192 self.trailing_newline
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
198pub enum SourceUnavailable {
199 #[error("source version is absent")]
200 Absent,
201 #[error("source version was not captured")]
202 NotCaptured,
203 #[error("source version is binary or not valid UTF-8")]
204 Binary,
205 #[error("source version is too large ({bytes} bytes)")]
206 TooLarge { bytes: u64 },
207 #[error("source version has too many lines ({lines})")]
208 TooManyLines { lines: usize },
209 #[error("source snapshot budget was exceeded")]
210 SnapshotBudgetExceeded,
211 #[error("repository changed while capturing the snapshot")]
212 UnstableSnapshot,
213 #[error("{0}")]
214 Error(String),
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn exact_identity_and_normalized_lines_are_independent() {
223 let lf = SourceDocument::try_from_text("a\nb\n").unwrap();
224 let crlf = SourceDocument::try_from_text("a\r\nb\r\n").unwrap();
225 assert_eq!(lf.line(1), crlf.line(1));
226 assert_eq!(lf.line(2), crlf.line(2));
227 assert_eq!(lf.sequence_id(), crlf.sequence_id());
228 assert_ne!(lf.content_id(), crlf.content_id());
229 assert_eq!(lf.line(1), Some("a"));
230 assert_eq!(lf.line(2), Some("b"));
231 assert_eq!(lf.line(3), None);
232 assert!(lf.trailing_newline());
233 }
234
235 #[test]
236 fn contiguous_documents_provide_stable_byte_coordinates() {
237 let document = SourceDocument::new("α\r\nb\n").unwrap();
238 assert_eq!(document.text(), "α\r\nb\n");
239 assert_eq!(document.line_starts(), &[0, 4]);
240 assert_eq!(document.line_span(1), Some(0..2));
241 assert_eq!(document.line_span(2), Some(4..5));
242 assert_eq!(document.line(1), Some("α"));
243 assert_eq!(document.identity(), document.content_id());
244 }
245
246 #[test]
247 fn equality_and_serialization_follow_the_text() {
248 let document = SourceDocument::new("a\r\nb").unwrap();
249 let json = serde_json::to_string(&document).unwrap();
250 assert_eq!(json, "\"a\\r\\nb\"");
251 let decoded = serde_json::from_str::<SourceDocument>(&json).unwrap();
252 assert_eq!(decoded, document);
253 assert_eq!(decoded.line_starts(), document.line_starts());
254 assert!(!decoded.trailing_newline());
255 assert_ne!(SourceDocument::new("a\nb").unwrap(), document);
256 }
257
258 #[test]
259 fn empty_and_blank_files_have_distinct_line_models() {
260 assert_eq!(SourceDocument::try_from_text("").unwrap().line_count(), 0);
261 let blank = SourceDocument::try_from_text("\n").unwrap();
262 assert_eq!(blank.line_count(), 1);
263 assert_eq!(blank.line(1), Some(""));
264 }
265}