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 pyo3::ToPyObject for Changes {
262    fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
263        self.0.to_object(py)
264    }
265}
266
267#[cfg(feature = "python-debian")]
268impl pyo3::FromPyObject<'_> for Changes {
269    fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
270        use pyo3::prelude::*;
271        Ok(Changes(ob.extract()?))
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    #[test]
278    fn test_new() {
279        let changes = super::Changes::new();
280        assert_eq!(changes.format(), Some("1.8".to_string()));
281    }
282
283    #[test]
284    fn test_parse() {
285        let changes = r#"Format: 1.8
286Date: Fri, 08 Sep 2023 18:23:59 +0100
287Source: buildlog-consultant
288Binary: python3-buildlog-consultant
289Architecture: all
290Version: 0.0.34-1
291Distribution: unstable
292Urgency: medium
293Maintainer: Jelmer Vernooij <jelmer@debian.org>
294Changed-By: Jelmer Vernooij <jelmer@debian.org>
295Description:
296 python3-buildlog-consultant - build log parser and analyser
297Changes:
298 buildlog-consultant (0.0.34-1) UNRELEASED; urgency=medium
299 .
300   * New upstream release.
301   * Update standards version to 4.6.2, no changes needed.
302Checksums-Sha1:
303 f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
304 b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
305Checksums-Sha256:
306 342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
307 7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
308Files:
309 aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo
310 a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb
311"#;
312        let changes = super::Changes::read(changes.as_bytes()).unwrap();
313        assert_eq!(changes.format(), Some("1.8".to_string()));
314        assert_eq!(changes.source(), Some("buildlog-consultant".to_string()));
315        assert_eq!(
316            changes.binary(),
317            Some(vec!["python3-buildlog-consultant".to_string()])
318        );
319        assert_eq!(changes.architecture(), Some(vec!["all".to_string()]));
320        assert_eq!(changes.version(), Some("0.0.34-1".parse().unwrap()));
321        assert_eq!(changes.distribution(), Some("unstable".to_string()));
322        assert_eq!(changes.urgency(), Some(crate::fields::Urgency::Medium));
323        assert_eq!(
324            changes.maintainer(),
325            Some("Jelmer Vernooij <jelmer@debian.org>".to_string())
326        );
327        assert_eq!(
328            changes.changed_by(),
329            Some("Jelmer Vernooij <jelmer@debian.org>".to_string())
330        );
331        assert_eq!(
332            changes.description(),
333            Some("python3-buildlog-consultant - build log parser and analyser".to_string())
334        );
335        assert_eq!(
336            changes.checksums_sha1(),
337            Some(vec![
338                "f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
339                "b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
340            ])
341        );
342        assert_eq!(
343            changes.checksums_sha256(),
344            Some(vec![
345                "342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo"
346                    .parse()
347                    .unwrap(),
348                "7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb"
349                    .parse()
350                    .unwrap()
351            ])
352        );
353        assert_eq!(
354            changes.files(),
355            Some(vec![
356                "aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
357                "a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
358            ])
359        );
360
361        assert_eq!(
362            changes.get_pool_path(),
363            Some("pool/main/b/buildlog-consultant".to_string())
364        );
365    }
366}