1use rowan::ast::AstNode;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Changes(deb822_lossless::Paragraph);
8
9#[derive(Debug)]
11pub enum ParseError {
12 Deb822(deb822_lossless::Error),
14
15 NoParagraphs,
17
18 MultipleParagraphs,
20}
21
22impl std::fmt::Display for ParseError {
23 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
24 match self {
25 Self::Deb822(e) => write!(f, "{}", e),
26 Self::NoParagraphs => write!(f, "no paragraphs found"),
27 Self::MultipleParagraphs => write!(f, "multiple paragraphs found"),
28 }
29 }
30}
31
32impl std::error::Error for ParseError {}
33
34impl From<deb822_lossless::Error> for ParseError {
35 fn from(e: deb822_lossless::Error) -> Self {
36 Self::Deb822(e)
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
42pub struct File {
43 pub md5sum: String,
45 pub size: usize,
47 pub section: String,
49 pub priority: crate::Priority,
51 pub filename: String,
53}
54
55impl std::fmt::Display for File {
56 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
57 write!(
58 f,
59 "{} {} {} {} {}",
60 self.md5sum, self.size, self.section, self.priority, self.filename
61 )
62 }
63}
64
65impl std::str::FromStr for File {
66 type Err = ();
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 let mut parts = s.split_whitespace();
70 let md5sum = parts.next().ok_or(())?;
71 let size = parts.next().ok_or(())?.parse().map_err(|_| ())?;
72 let section = parts.next().ok_or(())?.to_string();
73 let priority = parts.next().ok_or(())?.parse().map_err(|_| ())?;
74 let filename = parts.next().ok_or(())?.to_string();
75 Ok(Self {
76 md5sum: md5sum.to_string(),
77 size,
78 section,
79 priority,
80 filename,
81 })
82 }
83}
84
85impl Changes {
86 pub fn parse(text: &str) -> deb822_lossless::Parse<Changes> {
90 let deb822_parse = deb822_lossless::Deb822::parse(text);
91 let green = deb822_parse.green().clone();
92 let mut errors = deb822_parse.errors().to_vec();
93
94 if errors.is_empty() {
96 let deb822 = deb822_parse.tree();
97 let paragraph_count = deb822.paragraphs().count();
98 if paragraph_count == 0 {
99 errors.push("No paragraphs found".to_string());
100 } else if paragraph_count > 1 {
101 errors.push("Multiple paragraphs found, expected one".to_string());
102 }
103 }
104
105 deb822_lossless::Parse::new(green, errors)
106 }
107
108 pub fn format(&self) -> Option<String> {
110 self.0.get("Format").map(|s| s.to_string())
111 }
112
113 pub fn set_format(&mut self, value: &str) {
115 self.0.set("Format", value);
116 }
117
118 pub fn source(&self) -> Option<String> {
120 self.0.get("Source").map(|s| s.to_string())
121 }
122
123 pub fn binary(&self) -> Option<Vec<String>> {
125 self.0
126 .get("Binary")
127 .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
128 }
129
130 pub fn architecture(&self) -> Option<Vec<String>> {
132 self.0
133 .get("Architecture")
134 .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
135 }
136
137 pub fn version(&self) -> Option<debversion::Version> {
139 self.0.get("Version").map(|s| s.parse().unwrap())
140 }
141
142 pub fn distribution(&self) -> Option<String> {
144 self.0.get("Distribution").map(|s| s.to_string())
145 }
146
147 pub fn urgency(&self) -> Option<crate::fields::Urgency> {
149 self.0.get("Urgency").map(|s| s.parse().unwrap())
150 }
151
152 pub fn maintainer(&self) -> Option<String> {
154 self.0.get("Maintainer").map(|s| s.to_string())
155 }
156
157 pub fn changed_by(&self) -> Option<String> {
159 self.0.get("Changed-By").map(|s| s.to_string())
160 }
161
162 pub fn description(&self) -> Option<String> {
164 self.0.get("Description").map(|s| s.to_string())
165 }
166
167 pub fn checksums_sha1(&self) -> Option<Vec<crate::fields::Sha1Checksum>> {
169 self.0
170 .get("Checksums-Sha1")
171 .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
172 }
173
174 pub fn checksums_sha256(&self) -> Option<Vec<crate::fields::Sha256Checksum>> {
176 self.0
177 .get("Checksums-Sha256")
178 .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
179 }
180
181 pub fn files(&self) -> Option<Vec<File>> {
183 self.0
184 .get("Files")
185 .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
186 }
187
188 pub fn get_pool_path(&self) -> Option<String> {
190 let files = self.files()?;
191
192 let section = &files.first().unwrap().section;
193
194 let section = if let Some((section, _subsection)) = section.split_once('/') {
195 section
196 } else {
197 "main"
198 };
199
200 let source = self.source()?;
201
202 let subdir = if source.starts_with("lib") {
203 "lib".to_string()
204 } else {
205 source[..1].to_lowercase()
206 };
207
208 Some(format!("pool/{}/{}/{}", section, subdir, source))
209 }
210
211 pub fn new() -> Self {
213 let mut slf = Self(deb822_lossless::Paragraph::new());
214 slf.set_format("1.8");
215 slf
216 }
217
218 pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ParseError> {
220 let deb822 = deb822_lossless::Deb822::from_file(path)?;
221 let mut paras = deb822.paragraphs();
222 let para = match paras.next() {
223 Some(para) => para,
224 None => return Err(ParseError::NoParagraphs),
225 };
226 if paras.next().is_some() {
227 return Err(ParseError::MultipleParagraphs);
228 }
229 Ok(Self(para))
230 }
231
232 pub fn from_file_relaxed<P: AsRef<std::path::Path>>(
234 path: P,
235 ) -> Result<(Self, Vec<String>), std::io::Error> {
236 let (mut deb822, mut errors) = deb822_lossless::Deb822::from_file_relaxed(path)?;
237 let mut paras = deb822.paragraphs();
238 let para = match paras.next() {
239 Some(para) => para,
240 None => deb822.add_paragraph(),
241 };
242 if paras.next().is_some() {
243 errors.push("multiple paragraphs found".to_string());
244 }
245 Ok((Self(para), errors))
246 }
247
248 pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, ParseError> {
250 let deb822 = deb822_lossless::Deb822::read(&mut r)?;
251 let mut paras = deb822.paragraphs();
252 let para = match paras.next() {
253 Some(para) => para,
254 None => return Err(ParseError::NoParagraphs),
255 };
256 if paras.next().is_some() {
257 return Err(ParseError::MultipleParagraphs);
258 }
259 Ok(Self(para))
260 }
261
262 pub fn read_relaxed<R: std::io::Read>(
264 mut r: R,
265 ) -> Result<(Self, Vec<String>), deb822_lossless::Error> {
266 let (mut deb822, mut errors) = deb822_lossless::Deb822::read_relaxed(&mut r)?;
267 let mut paras = deb822.paragraphs();
268 let para = match paras.next() {
269 Some(para) => para,
270 None => deb822.add_paragraph(),
271 };
272 if paras.next().is_some() {
273 errors.push("multiple paragraphs found".to_string());
274 }
275 Ok((Self(para), errors))
276 }
277}
278
279impl Default for Changes {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285#[cfg(feature = "python-debian")]
286impl<'py> pyo3::IntoPyObject<'py> for Changes {
287 type Target = pyo3::PyAny;
288 type Output = pyo3::Bound<'py, Self::Target>;
289 type Error = pyo3::PyErr;
290
291 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
292 self.0.into_pyobject(py)
293 }
294}
295
296#[cfg(feature = "python-debian")]
297impl<'a, 'py> pyo3::IntoPyObject<'py> for &'a Changes {
298 type Target = pyo3::PyAny;
299 type Output = pyo3::Bound<'py, Self::Target>;
300 type Error = pyo3::PyErr;
301
302 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
303 (&self.0).into_pyobject(py)
304 }
305}
306
307#[cfg(feature = "python-debian")]
308impl<'py> pyo3::FromPyObject<'_, 'py> for Changes {
309 type Error = pyo3::PyErr;
310
311 fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
312 Ok(Changes(ob.extract()?))
313 }
314}
315
316impl AstNode for Changes {
317 type Language = deb822_lossless::Lang;
318
319 fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
320 deb822_lossless::Paragraph::can_cast(kind) || deb822_lossless::Deb822::can_cast(kind)
321 }
322
323 fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
324 if let Some(para) = deb822_lossless::Paragraph::cast(syntax.clone()) {
325 Some(Changes(para))
326 } else if let Some(deb822) = deb822_lossless::Deb822::cast(syntax) {
327 deb822.paragraphs().next().map(Changes)
328 } else {
329 None
330 }
331 }
332
333 fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
334 self.0.syntax()
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 #[test]
341 fn test_new() {
342 let changes = super::Changes::new();
343 assert_eq!(changes.format(), Some("1.8".to_string()));
344 }
345
346 #[test]
347 fn test_parse() {
348 let changes = r#"Format: 1.8
349Date: Fri, 08 Sep 2023 18:23:59 +0100
350Source: buildlog-consultant
351Binary: python3-buildlog-consultant
352Architecture: all
353Version: 0.0.34-1
354Distribution: unstable
355Urgency: medium
356Maintainer: Jelmer Vernooij <jelmer@debian.org>
357Changed-By: Jelmer Vernooij <jelmer@debian.org>
358Description:
359 python3-buildlog-consultant - build log parser and analyser
360Changes:
361 buildlog-consultant (0.0.34-1) UNRELEASED; urgency=medium
362 .
363 * New upstream release.
364 * Update standards version to 4.6.2, no changes needed.
365Checksums-Sha1:
366 f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
367 b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
368Checksums-Sha256:
369 342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
370 7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
371Files:
372 aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo
373 a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb
374"#;
375 let changes = super::Changes::read(changes.as_bytes()).unwrap();
376 assert_eq!(changes.format(), Some("1.8".to_string()));
377 assert_eq!(changes.source(), Some("buildlog-consultant".to_string()));
378 assert_eq!(
379 changes.binary(),
380 Some(vec!["python3-buildlog-consultant".to_string()])
381 );
382 assert_eq!(changes.architecture(), Some(vec!["all".to_string()]));
383 assert_eq!(changes.version(), Some("0.0.34-1".parse().unwrap()));
384 assert_eq!(changes.distribution(), Some("unstable".to_string()));
385 assert_eq!(changes.urgency(), Some(crate::fields::Urgency::Medium));
386 assert_eq!(
387 changes.maintainer(),
388 Some("Jelmer Vernooij <jelmer@debian.org>".to_string())
389 );
390 assert_eq!(
391 changes.changed_by(),
392 Some("Jelmer Vernooij <jelmer@debian.org>".to_string())
393 );
394 assert_eq!(
395 changes.description(),
396 Some("python3-buildlog-consultant - build log parser and analyser".to_string())
397 );
398 assert_eq!(
399 changes.checksums_sha1(),
400 Some(vec![
401 "f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
402 "b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
403 ])
404 );
405 assert_eq!(
406 changes.checksums_sha256(),
407 Some(vec![
408 "342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo"
409 .parse()
410 .unwrap(),
411 "7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb"
412 .parse()
413 .unwrap()
414 ])
415 );
416 assert_eq!(
417 changes.files(),
418 Some(vec![
419 "aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
420 "a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
421 ])
422 );
423
424 assert_eq!(
425 changes.get_pool_path(),
426 Some("pool/main/b/buildlog-consultant".to_string())
427 );
428 }
429}