debian_control/lossless/
changes.rs

1//! Changes files
2
3/// Changes file
4pub struct Changes(deb822_lossless::Paragraph);
5
6/// Errors that can occur when parsing a Changes file.
7#[derive(Debug)]
8pub enum ParseError {
9    /// An error occurred while parsing a Deb822 file.
10    Deb822(deb822_lossless::Error),
11
12    /// No paragraphs were found in the file.
13    NoParagraphs,
14
15    /// Multiple paragraphs were found in the file.
16    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/// A file in a source package.
38#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct File {
40    /// MD5 checksum of the file.
41    pub md5sum: String,
42    /// Size of the file in bytes.
43    pub size: usize,
44    /// Section the file belongs to.
45    pub section: String,
46    /// Priority of the file.
47    pub priority: crate::Priority,
48    /// Filename of the file.
49    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    /// Returns the format of the Changes file.
84    pub fn format(&self) -> Option<String> {
85        self.0.get("Format").map(|s| s.to_string())
86    }
87
88    /// Set the format of the Changes file.
89    pub fn set_format(&mut self, value: &str) {
90        self.0.set("Format", value);
91    }
92
93    /// Returns the name of the source package.
94    pub fn source(&self) -> Option<String> {
95        self.0.get("Source").map(|s| s.to_string())
96    }
97
98    /// Returns the list of binary packages generated by the source package.
99    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    /// Returns the architecture the source package is intended for.
106    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    /// Returns the version of the source package.
113    pub fn version(&self) -> Option<debversion::Version> {
114        self.0.get("Version").map(|s| s.parse().unwrap())
115    }
116
117    /// Returns the distribution the source package is intended for.
118    pub fn distribution(&self) -> Option<String> {
119        self.0.get("Distribution").map(|s| s.to_string())
120    }
121
122    /// Returns the urgency of the source package.
123    pub fn urgency(&self) -> Option<crate::fields::Urgency> {
124        self.0.get("Urgency").map(|s| s.parse().unwrap())
125    }
126
127    /// Returns the name and email address of the person who maintains the package.
128    pub fn maintainer(&self) -> Option<String> {
129        self.0.get("Maintainer").map(|s| s.to_string())
130    }
131
132    /// Returns the name and email address of the person who uploaded the package.
133    pub fn changed_by(&self) -> Option<String> {
134        self.0.get("Changed-By").map(|s| s.to_string())
135    }
136
137    /// Returns the description of the source package.
138    pub fn description(&self) -> Option<String> {
139        self.0.get("Description").map(|s| s.to_string())
140    }
141
142    /// Returns the SHA-1 checksums of the files in the source package.
143    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    /// Returns the SHA-256 checksums of the files in the source package.
150    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    /// Returns the list of files in the source package.
157    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    /// Returns the path to the pool directory for the source package.
164    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    /// Create a new Changes file.
187    pub fn new() -> Self {
188        let mut slf = Self(deb822_lossless::Paragraph::new());
189        slf.set_format("1.8");
190        slf
191    }
192
193    /// Read a Changes file from a file.
194    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    /// Read a Changes file from a file, allowing syntax errors.
208    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    /// Read a Changes file from a reader.
224    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    /// Read a Changes file from a reader, allowing syntax errors.
238    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}