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