debian_packaging/
debian_source_control.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Debian source control files. */
6
7use {
8    crate::{
9        control::{ControlParagraph, ControlParagraphReader},
10        dependency::{DependencyList, PackageDependencyFields},
11        error::{DebianError, Result},
12        io::ContentDigest,
13        package_version::PackageVersion,
14        repository::release::ChecksumType,
15    },
16    std::{
17        io::BufRead,
18        ops::{Deref, DerefMut},
19        str::FromStr,
20    },
21};
22
23/// A single file as described by a `Files` or `Checksums-*` field in a [DebianSourceControlFile].
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct DebianSourceControlFileEntry<'a> {
26    /// The filename/path.
27    pub filename: &'a str,
28
29    /// The content digest of this file.
30    pub digest: ContentDigest,
31
32    /// The size in bytes of the file.
33    pub size: u64,
34}
35
36impl<'a> DebianSourceControlFileEntry<'a> {
37    /// Convert this instance to a [DebianSourceControlFileFetch].
38    ///
39    /// The path in the fetch is prefixed with the given `directory` value. It usually
40    /// comes from the `Directory` field of the control paragraph from which the entry was
41    /// derived.
42    pub fn as_fetch(&self, directory: &str) -> DebianSourceControlFileFetch {
43        DebianSourceControlFileFetch {
44            path: format!("{}/{}", directory, self.filename),
45            digest: self.digest.clone(),
46            size: self.size,
47        }
48    }
49}
50
51/// Describes a single binary package entry in a `Package-List` field in a [DebianSourceControlFile].
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct DebianSourceControlFilePackage<'a> {
54    /// The name of the binary package.
55    pub name: &'a str,
56    /// The package type.
57    pub package_type: &'a str,
58    /// The section it appears in.
59    pub section: &'a str,
60    /// The package priority.
61    pub priority: &'a str,
62    /// Extra fields.
63    pub extra: Vec<&'a str>,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct DebianSourceControlFileFetch {
68    /// The path relative to the repository root to fetch.
69    pub path: String,
70
71    /// The digest of the file.
72    pub digest: ContentDigest,
73
74    /// The size of the file in bytes.
75    pub size: u64,
76}
77
78/// A Debian source control file/paragraph.
79///
80/// This control file consists of a single paragraph and defines a source package.
81/// This paragraph is typically found in `.dsc` files and in `Sources` files in repositories.
82///
83/// The fields are defined at
84/// <https://www.debian.org/doc/debian-policy/ch-controlfields.html#debian-source-control-files-dsc>.
85#[derive(Default)]
86pub struct DebianSourceControlFile<'a> {
87    paragraph: ControlParagraph<'a>,
88    /// Parsed PGP signatures for this file.
89    signatures: Option<pgp_cleartext::CleartextSignatures>,
90}
91
92impl<'a> Deref for DebianSourceControlFile<'a> {
93    type Target = ControlParagraph<'a>;
94
95    fn deref(&self) -> &Self::Target {
96        &self.paragraph
97    }
98}
99
100impl<'a> DerefMut for DebianSourceControlFile<'a> {
101    fn deref_mut(&mut self) -> &mut Self::Target {
102        &mut self.paragraph
103    }
104}
105
106impl<'a> From<ControlParagraph<'a>> for DebianSourceControlFile<'a> {
107    fn from(paragraph: ControlParagraph<'a>) -> Self {
108        Self {
109            paragraph,
110            signatures: None,
111        }
112    }
113}
114
115impl<'a> From<DebianSourceControlFile<'a>> for ControlParagraph<'a> {
116    fn from(cf: DebianSourceControlFile<'a>) -> Self {
117        cf.paragraph
118    }
119}
120
121impl<'a> DebianSourceControlFile<'a> {
122    /// Construct an instance by reading data from a reader.
123    ///
124    /// The source must be a Debian source control file with exactly 1 paragraph.
125    ///
126    /// The source must not be PGP armored (e.g. beginning with
127    /// `-----BEGIN PGP SIGNED MESSAGE-----`). For PGP armored data, use
128    /// [Self::from_armored_reader()].
129    pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
130        let paragraphs = ControlParagraphReader::new(reader).collect::<Result<Vec<_>>>()?;
131
132        if paragraphs.len() != 1 {
133            return Err(DebianError::DebianSourceControlFileParagraphMismatch(
134                paragraphs.len(),
135            ));
136        }
137
138        let paragraph = paragraphs
139            .into_iter()
140            .next()
141            .expect("validated paragraph count above");
142
143        Ok(Self {
144            paragraph,
145            signatures: None,
146        })
147    }
148
149    /// Construct an instance by reading data from a reader containing a PGP cleartext signature.
150    ///
151    /// This can be used to parse content from a `.dsc` file which begins
152    /// with `-----BEGIN PGP SIGNED MESSAGE-----`.
153    ///
154    /// An error occurs if the PGP cleartext file is not well-formed or if a PGP parsing
155    /// error occurs.
156    ///
157    /// The PGP signature is NOT validated. The file will be parsed despite lack of
158    /// signature verification. This is conceptually insecure. But since Rust has memory
159    /// safety, some risk is prevented.
160    pub fn from_armored_reader<R: BufRead>(reader: R) -> Result<Self> {
161        let reader = pgp_cleartext::CleartextSignatureReader::new(reader);
162        let mut reader = std::io::BufReader::new(reader);
163
164        let mut slf = Self::from_reader(&mut reader)?;
165        slf.signatures = Some(reader.into_inner().finalize());
166
167        Ok(slf)
168    }
169
170    /// Clone without preserving signatures data.
171    #[must_use]
172    pub fn clone_no_signatures(&self) -> Self {
173        Self {
174            paragraph: self.paragraph.clone(),
175            signatures: None,
176        }
177    }
178
179    /// Obtain PGP signatures from this possibly signed file.
180    pub fn signatures(&self) -> Option<&pgp_cleartext::CleartextSignatures> {
181        self.signatures.as_ref()
182    }
183
184    /// The format of the source package.
185    ///
186    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-format>.
187    pub fn format(&self) -> Result<&str> {
188        self.required_field_str("Format")
189    }
190
191    /// The name of the source package.
192    ///
193    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-source>.
194    pub fn source(&self) -> Result<&str> {
195        self.required_field_str("Source")
196    }
197
198    /// The binary packages this source package produces.
199    ///
200    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-binary>.
201    pub fn binary(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
202        self.iter_field_comma_delimited("Binary")
203    }
204
205    /// The architectures this source package will build for.
206    ///
207    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-architecture>.
208    pub fn architecture(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
209        self.iter_field_words("Architecture")
210    }
211
212    /// The version number of the package as a string.
213    ///
214    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version>.
215    pub fn version_str(&self) -> Result<&str> {
216        self.required_field_str("Version")
217    }
218
219    /// The parsed version of the source package.
220    ///
221    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version>.
222    pub fn version(&self) -> Result<PackageVersion> {
223        PackageVersion::parse(self.version_str()?)
224    }
225
226    /// The package maintainer.
227    ///
228    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-maintainer>.
229    pub fn maintainer(&self) -> Result<&str> {
230        self.required_field_str("Maintainer")
231    }
232
233    /// The list of uploaders and co-maintainers.
234    ///
235    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-uploaders>.
236    pub fn uploaders(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
237        self.iter_field_comma_delimited("Uploaders")
238    }
239
240    /// The URL from which the source of this package can be obtained.
241    ///
242    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-homepage>.
243    pub fn homepage(&self) -> Option<&str> {
244        self.field_str("Homepage")
245    }
246
247    /// Test suites.
248    ///
249    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-testsuite>.
250    pub fn testsuite(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
251        self.iter_field_comma_delimited("Testsuite")
252    }
253
254    /// Describes the Git source from which this package came.
255    ///
256    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-dgit>.
257    pub fn dgit(&self) -> Option<&str> {
258        self.field_str("Dgit")
259    }
260
261    /// The most recent version of the standards this package conforms to.
262    ///
263    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-standards-version>.
264    pub fn standards_version(&self) -> Result<&str> {
265        self.required_field_str("Standards-Version")
266    }
267
268    /// The `Depends` field, parsed to a [DependencyList].
269    pub fn depends(&self) -> Option<Result<DependencyList>> {
270        self.field_dependency_list("Depends")
271    }
272
273    /// The `Recommends` field, parsed to a [DependencyList].
274    pub fn recommends(&self) -> Option<Result<DependencyList>> {
275        self.field_dependency_list("Recommends")
276    }
277
278    /// The `Suggests` field, parsed to a [DependencyList].
279    pub fn suggests(&self) -> Option<Result<DependencyList>> {
280        self.field_dependency_list("Suggests")
281    }
282
283    /// The `Enhances` field, parsed to a [DependencyList].
284    pub fn enhances(&self) -> Option<Result<DependencyList>> {
285        self.field_dependency_list("Enhances")
286    }
287
288    /// The `Pre-Depends` field, parsed to a [DependencyList].
289    pub fn pre_depends(&self) -> Option<Result<DependencyList>> {
290        self.field_dependency_list("Pre-Depends")
291    }
292
293    /// Obtain parsed values of all fields defining dependencies.
294    pub fn package_dependency_fields(&self) -> Result<PackageDependencyFields> {
295        PackageDependencyFields::from_paragraph(self)
296    }
297
298    /// Packages that can be built from this source package.
299    ///
300    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-package-list>.
301    pub fn package_list(
302        &self,
303    ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFilePackage<'_>>> + '_)>> {
304        if let Some(iter) = self.iter_field_lines("Package-List") {
305            Some(Box::new(iter.map(move |v| {
306                let mut words = v.split_ascii_whitespace();
307
308                let name = words
309                    .next()
310                    .ok_or(DebianError::ControlPackageListMissingField("name"))?;
311                let package_type = words
312                    .next()
313                    .ok_or(DebianError::ControlPackageListMissingField("type"))?;
314                let section = words
315                    .next()
316                    .ok_or(DebianError::ControlPackageListMissingField("section"))?;
317                let priority = words
318                    .next()
319                    .ok_or(DebianError::ControlPackageListMissingField("priority"))?;
320                let extra = words.collect::<Vec<_>>();
321
322                Ok(DebianSourceControlFilePackage {
323                    name,
324                    package_type,
325                    section,
326                    priority,
327                    extra,
328                })
329            })))
330        } else {
331            None
332        }
333    }
334
335    /// List of associated files with SHA-1 checksums.
336    ///
337    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-checksums>.
338    pub fn checksums_sha1(
339        &self,
340    ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
341        self.iter_files("Checksums-Sha1", ChecksumType::Sha1)
342    }
343
344    /// List of associated files with SHA-256 checksums.
345    ///
346    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-checksums>.
347    pub fn checksums_sha256(
348        &self,
349    ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
350        self.iter_files("Checksums-Sha256", ChecksumType::Sha256)
351    }
352
353    /// List of associated files with MD5 checksums.
354    ///
355    /// See <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-files>.
356    pub fn files(
357        &self,
358    ) -> Result<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
359        self.iter_files("Files", ChecksumType::Md5)
360            .ok_or_else(|| DebianError::ControlRequiredFieldMissing("Files".to_string()))
361    }
362
363    fn iter_files(
364        &self,
365        field: &str,
366        checksum: ChecksumType,
367    ) -> Option<Box<(dyn Iterator<Item = Result<DebianSourceControlFileEntry<'_>>> + '_)>> {
368        if let Some(iter) = self.iter_field_lines(field) {
369            Some(Box::new(iter.map(move |v| {
370                // Values are of form: <digest> <size> <path>
371
372                let mut parts = v.split_ascii_whitespace();
373
374                let digest = parts.next().ok_or(DebianError::ReleaseMissingDigest)?;
375                let size = parts.next().ok_or(DebianError::ReleaseMissingSize)?;
376                let filename = parts.next().ok_or(DebianError::ReleaseMissingPath)?;
377
378                // Are paths with spaces allowed?
379                if parts.next().is_some() {
380                    return Err(DebianError::ReleasePathWithSpaces(v.to_string()));
381                }
382
383                let digest = ContentDigest::from_hex_digest(checksum, digest)?;
384                let size = u64::from_str(size)?;
385
386                Ok(DebianSourceControlFileEntry {
387                    filename,
388                    digest,
389                    size,
390                })
391            })))
392        } else {
393            None
394        }
395    }
396
397    /// Obtain [DebianSourceControlFileFetch] for a given digest variant.
398    ///
399    /// This obtains records that instruct how to fetch the files that compose this
400    /// source package.
401    pub fn file_fetches(
402        &self,
403        checksum: ChecksumType,
404    ) -> Result<Box<(dyn Iterator<Item = Result<DebianSourceControlFileFetch>> + '_)>> {
405        let entries = match checksum {
406            ChecksumType::Md5 => self.files()?,
407            ChecksumType::Sha1 => self.checksums_sha1().ok_or_else(|| {
408                DebianError::ControlRequiredFieldMissing("Checksums-Sha1".to_string())
409            })?,
410            ChecksumType::Sha256 => self.checksums_sha256().ok_or_else(|| {
411                DebianError::ControlRequiredFieldMissing("Checksums-Sha256".to_string())
412            })?,
413        };
414
415        Ok(Box::new(entries.map(move |entry| {
416            let entry = entry?;
417            let directory = self.required_field_str("Directory")?;
418
419            Ok(entry.as_fetch(directory))
420        })))
421    }
422}
423
424#[cfg(test)]
425mod test {
426    use super::*;
427
428    const ZSTD_DSC: &[u8] = include_bytes!("testdata/libzstd_1.4.8+dfsg-3.dsc");
429
430    #[test]
431    fn parse_cleartext_armored() -> Result<()> {
432        let cf = DebianSourceControlFile::from_armored_reader(std::io::Cursor::new(ZSTD_DSC))?;
433
434        cf.signatures()
435            .expect("PGP signatures should have been parsed");
436
437        assert_eq!(cf.format()?, "3.0 (quilt)");
438        assert_eq!(cf.source()?, "libzstd");
439        assert_eq!(
440            cf.binary().unwrap().collect::<Vec<_>>(),
441            vec!["libzstd-dev", "libzstd1", "zstd", "libzstd1-udeb"]
442        );
443        assert_eq!(cf.architecture().unwrap().collect::<Vec<_>>(), vec!["any"]);
444        assert_eq!(cf.version_str()?, "1.4.8+dfsg-3");
445        assert_eq!(
446            cf.maintainer()?,
447            "Debian Med Packaging Team <debian-med-packaging@lists.alioth.debian.org>"
448        );
449        assert_eq!(
450            cf.uploaders().unwrap().collect::<Vec<_>>(),
451            vec![
452                "Kevin Murray <kdmfoss@gmail.com>",
453                "Olivier Sallou <osallou@debian.org>",
454                "Alexandre Mestiashvili <mestia@debian.org>",
455            ]
456        );
457        assert_eq!(cf.homepage(), Some("https://github.com/facebook/zstd"));
458        assert_eq!(cf.standards_version()?, "4.6.0");
459        assert_eq!(
460            cf.testsuite().unwrap().collect::<Vec<_>>(),
461            vec!["autopkgtest"]
462        );
463        assert_eq!(
464            cf.package_list().unwrap().collect::<Result<Vec<_>>>()?,
465            vec![
466                DebianSourceControlFilePackage {
467                    name: "libzstd-dev",
468                    package_type: "deb",
469                    section: "libdevel",
470                    priority: "optional",
471                    extra: vec!["arch=any"]
472                },
473                DebianSourceControlFilePackage {
474                    name: "libzstd1",
475                    package_type: "deb",
476                    section: "libs",
477                    priority: "optional",
478                    extra: vec!["arch=any"]
479                },
480                DebianSourceControlFilePackage {
481                    name: "libzstd1-udeb",
482                    package_type: "udeb",
483                    section: "debian-installer",
484                    priority: "optional",
485                    extra: vec!["arch=any"]
486                },
487                DebianSourceControlFilePackage {
488                    name: "zstd",
489                    package_type: "deb",
490                    section: "utils",
491                    priority: "optional",
492                    extra: vec!["arch=any"]
493                }
494            ]
495        );
496        assert_eq!(
497            cf.checksums_sha1().unwrap().collect::<Result<Vec<_>>>()?,
498            vec![
499                DebianSourceControlFileEntry {
500                    filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
501                    digest: ContentDigest::sha1_hex("a24e4ccf9fc356aeaaa0783316a26bd65817c354")?,
502                    size: 1331996,
503                },
504                DebianSourceControlFileEntry {
505                    filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
506                    digest: ContentDigest::sha1_hex("896a47a2934d0fcf9faa8397d05a12b932697d1f")?,
507                    size: 12184,
508                }
509            ]
510        );
511        assert_eq!(
512            cf.checksums_sha256().unwrap().collect::<Result<Vec<_>>>()?,
513            vec![
514                DebianSourceControlFileEntry {
515                    filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
516                    digest: ContentDigest::sha256_hex(
517                        "1e8ce5c4880a6d5bd8d3186e4186607dd19b64fc98a3877fc13aeefd566d67c5"
518                    )?,
519                    size: 1331996,
520                },
521                DebianSourceControlFileEntry {
522                    filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
523                    digest: ContentDigest::sha256_hex(
524                        "fecd87a469d5a07b6deeeef53ed24b2f1a74ee097ce11528fe3b58540f05c147"
525                    )?,
526                    size: 12184,
527                }
528            ]
529        );
530        assert_eq!(
531            cf.files().unwrap().collect::<Result<Vec<_>>>()?,
532            vec![
533                DebianSourceControlFileEntry {
534                    filename: "libzstd_1.4.8+dfsg.orig.tar.xz",
535                    digest: ContentDigest::md5_hex("943bed8b8d98a50c8d8a101b12693bb4")?,
536                    size: 1331996,
537                },
538                DebianSourceControlFileEntry {
539                    filename: "libzstd_1.4.8+dfsg-3.debian.tar.xz",
540                    digest: ContentDigest::md5_hex("4d2692830e1f481ce769e2dd24cbc9db")?,
541                    size: 12184,
542                }
543            ]
544        );
545
546        Ok(())
547    }
548}