Skip to main content

debian_copyright/
lossy.rs

1//! A library for parsing and manipulating debian/copyright files that
2//! use the DEP-5 format.
3//!
4//! # Examples
5//!
6//! ```rust
7//!
8//! use debian_copyright::Copyright;
9//! use std::path::Path;
10//!
11//! let text = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
12//! Upstream-Author: John Doe <john@example>
13//! Upstream-Name: example
14//! Source: https://example.com/example
15//!
16//! Files: *
17//! License: GPL-3+
18//! Copyright: 2019 John Doe
19//!
20//! Files: debian/*
21//! License: GPL-3+
22//! Copyright: 2019 Jane Packager
23//!
24//! License: GPL-3+
25//!  This program is free software: you can redistribute it and/or modify
26//!  it under the terms of the GNU General Public License as published by
27//!  the Free Software Foundation, either version 3 of the License, or
28//!  (at your option) any later version.
29//! "#;
30//!
31//! let c = text.parse::<Copyright>().unwrap();
32//! let license = c.find_license_for_file(Path::new("debian/foo")).unwrap();
33//! assert_eq!(license.name(), Some("GPL-3+"));
34//! ```
35
36use crate::License;
37use crate::CURRENT_FORMAT;
38use deb822_fast::{Deb822, FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph};
39use std::path::Path;
40
41fn deserialize_file_list(text: &str) -> Result<Vec<String>, String> {
42    Ok(text.split('\n').map(|x| x.to_string()).collect())
43}
44
45fn serialize_file_list(files: &[String]) -> String {
46    files.join("\n")
47}
48
49/// A header paragraph.
50#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)]
51pub struct Header {
52    #[deb822(field = "Format")]
53    /// The format of the file.
54    format: String,
55
56    #[deb822(field = "Files-Excluded", deserialize_with = deserialize_file_list, serialize_with = serialize_file_list)]
57    /// Files that are excluded from the copyright information, and should be excluded from the package.
58    files_excluded: Option<Vec<String>>,
59
60    #[deb822(field = "Source")]
61    /// The source of the package.
62    source: Option<String>,
63
64    #[deb822(field = "Upstream-Contact")]
65    /// Contact information for the upstream author.
66    upstream_contact: Option<String>,
67}
68
69impl Default for Header {
70    fn default() -> Self {
71        Header {
72            format: CURRENT_FORMAT.to_string(),
73            files_excluded: None,
74            source: None,
75            upstream_contact: None,
76        }
77    }
78}
79
80impl std::fmt::Display for Header {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        let para: deb822_fast::Paragraph = self.to_paragraph();
83        write!(f, "{}", para)?;
84        Ok(())
85    }
86}
87
88/// A copyright file.
89#[derive(Clone, PartialEq, Eq, Debug)]
90pub struct Copyright {
91    /// The header paragraph.
92    pub header: Header,
93
94    /// Files paragraphs.
95    pub files: Vec<FilesParagraph>,
96
97    /// License paragraphs.
98    pub licenses: Vec<LicenseParagraph>,
99}
100
101impl std::str::FromStr for Copyright {
102    type Err = String;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        if !s.starts_with("Format:") {
106            return Err("Not machine readable".to_string());
107        }
108
109        let deb822: Deb822 = s.parse().map_err(|e: deb822_fast::Error| e.to_string())?;
110
111        let mut paragraphs = deb822.iter();
112
113        let first_para = if let Some(para) = paragraphs.next() {
114            para
115        } else {
116            return Err("No paragraphs".to_string());
117        };
118
119        let header: Header = Header::from_paragraph(first_para)?;
120
121        let mut files_paras = vec![];
122        let mut license_paras = vec![];
123
124        for para in paragraphs {
125            if para.get("Files").is_some() {
126                files_paras.push(FilesParagraph::from_paragraph(para)?);
127            } else if para.get("License").is_some() {
128                license_paras.push(LicenseParagraph::from_paragraph(para)?);
129            } else {
130                return Err("Paragraph is neither License nor Files".to_string());
131            }
132        }
133
134        Ok(Copyright {
135            header,
136            files: files_paras,
137            licenses: license_paras,
138        })
139    }
140}
141
142/// A paragraph describing a license.
143#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)]
144pub struct LicenseParagraph {
145    /// The license text.
146    #[deb822(field = "License")]
147    license: License,
148
149    /// A comment.
150    #[deb822(field = "Comment")]
151    comment: Option<String>,
152}
153
154impl std::fmt::Display for LicenseParagraph {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        let para: deb822_fast::Paragraph = self.to_paragraph();
157        f.write_str(&para.to_string())
158    }
159}
160
161fn deserialize_copyrights(text: &str) -> Result<Vec<String>, String> {
162    Ok(text.split('\n').map(ToString::to_string).collect())
163}
164
165fn serialize_copyrights(copyrights: &[String]) -> String {
166    copyrights.join("\n")
167}
168
169/// A paragraph describing a set of files.
170#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)]
171pub struct FilesParagraph {
172    #[deb822(field="Files", deserialize_with = deserialize_file_list, serialize_with = serialize_file_list)]
173    files: Vec<String>,
174    #[deb822(field = "License")]
175    license: License,
176    #[deb822(field="Copyright", deserialize_with = deserialize_copyrights, serialize_with = serialize_copyrights)]
177    copyright: Vec<String>,
178    #[deb822(field = "Comment")]
179    comment: Option<String>,
180}
181
182impl FilesParagraph {
183    /// Check if the given filename matches one of the file patterns in this paragraph.
184    pub fn matches(&self, filename: &std::path::Path) -> bool {
185        self.files
186            .iter()
187            .any(|f| crate::glob::glob_to_regex(f).is_match(filename.to_str().unwrap()))
188    }
189}
190
191impl std::fmt::Display for FilesParagraph {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        let para: deb822_fast::Paragraph = self.to_paragraph();
194        f.write_str(&para.to_string())?;
195        Ok(())
196    }
197}
198
199impl Default for Copyright {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl Copyright {
206    /// Create a new empty `Copyright` object.
207    pub fn new() -> Self {
208        Self {
209            header: Header::default(),
210            licenses: Vec::new(),
211            files: Vec::new(),
212        }
213    }
214
215    /// Find the files paragraph that matches the given path.
216    ///
217    /// Returns `None` if no matching files paragraph is found.
218    ///
219    /// # Arguments
220    /// * `path` - The path to the file to find the license for.
221    pub fn find_files(&self, path: &std::path::Path) -> Option<&FilesParagraph> {
222        self.files.iter().filter(|f| f.matches(path)).next_back()
223    }
224
225    /// Returns the license for the given file.
226    pub fn find_license_for_file(&self, filename: &Path) -> Option<&License> {
227        let files = self.find_files(filename)?;
228        if files.license.text().is_some() {
229            return Some(&files.license);
230        }
231        self.find_license_by_name(files.license.name().unwrap())
232    }
233
234    /// Find a license by name.
235    ///
236    /// Returns `None` if no license with the given name is found.
237    ///
238    /// # Arguments
239    /// * `name` - The name of the license to find.
240    pub fn find_license_by_name(&self, name: &str) -> Option<&License> {
241        self.licenses
242            .iter()
243            .find(|p| p.license.name() == Some(name))
244            .map(|p| &p.license)
245    }
246}
247
248impl std::fmt::Display for Copyright {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        write!(f, "{}", self.header)?;
251        for files in &self.files {
252            writeln!(f)?;
253            write!(f, "{}", files)?;
254        }
255        for license in &self.licenses {
256            writeln!(f)?;
257            write!(f, "{}", license)?;
258        }
259        Ok(())
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    #[test]
266    fn test_not_machine_readable() {
267        let s = r#"
268This copyright file is not machine readable.
269"#;
270        let ret = s.parse::<super::Copyright>();
271        assert!(ret.is_err());
272        assert_eq!(ret.unwrap_err(), "Not machine readable".to_string());
273    }
274
275    #[test]
276    fn test_new() {
277        let n = super::Copyright::new();
278        assert_eq!(
279            n.to_string().as_str(),
280            "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n"
281        );
282    }
283
284    #[test]
285    fn test_parse() {
286        let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
287Upstream-Name: foo
288Upstream-Contact: Joe Bloggs <joe@example.com>
289Source: https://example.com/foo
290
291Files: *
292Copyright:
293  2020 Joe Bloggs <joe@example.com>
294License: GPL-3+
295
296Files: debian/*
297Comment: Debian packaging is licensed under the GPL-3+.
298Copyright: 2023 Jelmer Vernooij
299License: GPL-3+
300
301License: GPL-3+
302 This program is free software: you can redistribute it and/or modify
303 it under the terms of the GNU General Public License as published by
304 the Free Software Foundation, either version 3 of the License, or
305 (at your option) any later version.
306"#;
307        let copyright = s.parse::<super::Copyright>().expect("failed to parse");
308
309        assert_eq!(
310            "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
311            copyright.header.format
312        );
313        assert_eq!(
314            "Joe Bloggs <joe@example.com>",
315            copyright.header.upstream_contact.as_ref().unwrap()
316        );
317        assert_eq!(
318            "https://example.com/foo",
319            copyright.header.source.as_ref().unwrap()
320        );
321
322        let files = &copyright.files;
323        assert_eq!(2, files.len());
324        assert_eq!("*", files[0].files.join(" "));
325        assert_eq!("debian/*", files[1].files.join(" "));
326        assert_eq!(
327            "Debian packaging is licensed under the GPL-3+.",
328            files[1].comment.as_ref().unwrap()
329        );
330        assert_eq!(vec!["2023 Jelmer Vernooij".to_string()], files[1].copyright);
331        assert_eq!("GPL-3+", files[1].license.name().unwrap());
332        assert_eq!(files[1].license.text(), None);
333
334        let licenses = &copyright.licenses;
335        assert_eq!(1, licenses.len());
336        assert_eq!("GPL-3+", licenses[0].license.name().unwrap());
337        assert_eq!(
338            "This program is free software: you can redistribute it and/or modify
339it under the terms of the GNU General Public License as published by
340the Free Software Foundation, either version 3 of the License, or
341(at your option) any later version.",
342            licenses[0].license.text().unwrap()
343        );
344
345        let upstream_files = copyright.find_files(std::path::Path::new("foo.c")).unwrap();
346        assert_eq!(vec!["*"], upstream_files.files);
347
348        let debian_files = copyright
349            .find_files(std::path::Path::new("debian/foo.c"))
350            .unwrap();
351        assert_eq!(vec!["debian/*"], debian_files.files);
352
353        let gpl = copyright.find_license_by_name("GPL-3+");
354        assert!(gpl.is_some());
355
356        let gpl = copyright.find_license_for_file(std::path::Path::new("debian/foo.c"));
357        assert_eq!(gpl.unwrap().name().unwrap(), "GPL-3+");
358    }
359}