Skip to main content

debian_copyright/
lib.rs

1#![deny(missing_docs)]
2//! A library for parsing and manipulating debian/copyright files that
3//! use the DEP-5 format.
4//!
5//! # Examples
6//!
7//! ```rust
8//!
9//! use debian_copyright::Copyright;
10//! use std::path::Path;
11//!
12//! let text = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
13//! Upstream-Author: John Doe <john@example>
14//! Upstream-Name: example
15//! Source: https://example.com/example
16//!
17//! Files: *
18//! License: GPL-3+
19//! Copyright: 2019 John Doe
20//!
21//! Files: debian/*
22//! License: GPL-3+
23//! Copyright: 2019 Jane Packager
24//!
25//! License: GPL-3+
26//!  This program is free software: you can redistribute it and/or modify
27//!  it under the terms of the GNU General Public License as published by
28//!  the Free Software Foundation, either version 3 of the License, or
29//!  (at your option) any later version.
30//! "#;
31//!
32//! let c = text.parse::<Copyright>().unwrap();
33//! let license = c.find_license_for_file(Path::new("debian/foo")).unwrap();
34//! assert_eq!(license.name(), Some("GPL-3+"));
35//! ```
36//!
37//! See the ``lossless`` module (behind the ``lossless`` feature) for a more forgiving parser that
38//! allows partial parsing, parsing files with errors and unknown fields and editing while
39//! preserving formatting.
40
41#[cfg(feature = "lossless")]
42pub mod lossless;
43pub mod lossy;
44pub use lossy::Copyright;
45
46/// The current version of the DEP-5 format.
47pub const CURRENT_FORMAT: &str =
48    "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/";
49
50/// The known versions of the DEP-5 format.
51pub const KNOWN_FORMATS: &[&str] = &[CURRENT_FORMAT];
52
53pub mod expression;
54mod glob;
55pub use expression::LicenseExpr;
56
57/// Decode deb822 paragraph markers in a multi-line field value.
58///
59/// According to Debian policy, blank lines in multi-line field values are
60/// represented as lines containing only "." (a single period). The deb822
61/// parser already strips the leading indentation whitespace from continuation lines,
62/// so we only need to decode the period markers back to blank lines.
63///
64/// # Arguments
65///
66/// * `text` - The raw field value text from deb822 parser with indentation already stripped
67///
68/// # Returns
69///
70/// The decoded text with blank lines restored
71fn decode_field_text(text: &str) -> String {
72    text.lines()
73        .map(|line| {
74            if line == "." {
75                // Paragraph marker representing a blank line
76                ""
77            } else {
78                line
79            }
80        })
81        .collect::<Vec<_>>()
82        .join("\n")
83}
84
85/// Encode blank lines in a field value to deb822 paragraph markers.
86///
87/// According to Debian policy, blank lines in multi-line field values must be
88/// represented as lines containing only "." (a single period).
89///
90/// # Arguments
91///
92/// * `text` - The decoded text with normal blank lines
93///
94/// # Returns
95///
96/// The encoded text with blank lines replaced by "."
97fn encode_field_text(text: &str) -> String {
98    text.lines()
99        .map(|line| {
100            if line.is_empty() {
101                // Blank line must be encoded as period marker
102                "."
103            } else {
104                line
105            }
106        })
107        .collect::<Vec<_>>()
108        .join("\n")
109}
110
111/// A license, which can be just a name, a text or a named license.
112#[derive(Clone, PartialEq, Eq, Debug)]
113pub enum License {
114    /// A license with just a name.
115    Name(String),
116
117    /// A license with just a text.
118    Text(String),
119
120    /// A license with a name and a text.
121    Named(String, String),
122}
123
124impl License {
125    /// Returns the name of the license, if any.
126    ///
127    /// Note that this may be a license expression containing multiple licenses
128    /// combined with `or`, `and`, or `with`. Use [`License::expr`] to parse
129    /// the expression into a structured [`LicenseExpr`].
130    pub fn name(&self) -> Option<&str> {
131        match self {
132            License::Name(name) => Some(name),
133            License::Text(_) => None,
134            License::Named(name, _) => Some(name),
135        }
136    }
137
138    /// Returns the text of the license, if any.
139    pub fn text(&self) -> Option<&str> {
140        match self {
141            License::Name(_) => None,
142            License::Text(text) => Some(text),
143            License::Named(_, text) => Some(text),
144        }
145    }
146
147    /// Parse the license name as a structured expression.
148    ///
149    /// Returns `None` if the license has no name (i.e. is a `Text` variant).
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use debian_copyright::{License, LicenseExpr};
155    ///
156    /// let license = License::Name("GPL-2+ or MIT".to_string());
157    /// assert_eq!(
158    ///     license.expr(),
159    ///     Some(LicenseExpr::Or(vec![
160    ///         LicenseExpr::Name("GPL-2+".to_string()),
161    ///         LicenseExpr::Name("MIT".to_string()),
162    ///     ])),
163    /// );
164    /// ```
165    pub fn expr(&self) -> Option<LicenseExpr> {
166        self.name().map(LicenseExpr::parse)
167    }
168}
169
170impl std::str::FromStr for License {
171    type Err = String;
172
173    fn from_str(text: &str) -> Result<Self, Self::Err> {
174        if let Some((name, rest)) = text.split_once('\n') {
175            let decoded_text = decode_field_text(rest);
176            if name.is_empty() {
177                Ok(License::Text(decoded_text))
178            } else {
179                Ok(License::Named(name.to_string(), decoded_text))
180            }
181        } else {
182            Ok(License::Name(text.to_string()))
183        }
184    }
185}
186
187impl std::fmt::Display for License {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            License::Name(name) => f.write_str(name),
191            License::Text(text) => write!(f, "\n{}", encode_field_text(text)),
192            License::Named(name, text) => write!(f, "{}\n{}", name, encode_field_text(text)),
193        }
194    }
195}
196
197/// Calculate the depth of a Files pattern by counting '/' characters.
198pub fn pattern_depth(pattern: &str) -> usize {
199    pattern.matches('/').count()
200}
201
202/// Check if a pattern is a debian/* pattern (should be sorted last by convention).
203pub fn is_debian_pattern(pattern: &str) -> bool {
204    let trimmed = pattern.trim();
205    trimmed.starts_with("debian/") || trimmed == "debian/*"
206}
207
208/// Calculate a sort key for a Files pattern.
209///
210/// Returns `(priority, depth)` where:
211/// - priority 0: `*` (always first)
212/// - priority 1: normal patterns (sorted by depth)
213/// - priority 2: debian/* patterns (always last, then by depth)
214///
215/// This follows the Debian convention that the `*` wildcard should be first,
216/// and `debian/*` patterns should be last in debian/copyright Files paragraphs.
217pub fn pattern_sort_key(pattern: &str, depth: usize) -> (u8, usize) {
218    let trimmed = pattern.trim();
219
220    if trimmed == "*" {
221        (0, 0)
222    } else if is_debian_pattern(pattern) {
223        (2, depth)
224    } else {
225        (1, depth)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_decode_field_text() {
235        // Test basic decoding of period markers
236        let input = "line 1\n.\nline 3";
237        let output = decode_field_text(input);
238        assert_eq!(output, "line 1\n\nline 3");
239    }
240
241    #[test]
242    fn test_decode_field_text_no_markers() {
243        // Test text without markers remains unchanged
244        let input = "line 1\nline 2\nline 3";
245        let output = decode_field_text(input);
246        assert_eq!(output, input);
247    }
248
249    #[test]
250    fn test_license_from_str_with_paragraph_markers() {
251        // Test that License::from_str decodes paragraph markers
252        let input = "GPL-3+\nThis is line 1\n.\nThis is line 3";
253        let license: License = input.parse().unwrap();
254
255        match license {
256            License::Named(name, text) => {
257                assert_eq!(name, "GPL-3+");
258                assert_eq!(text, "This is line 1\n\nThis is line 3");
259                assert!(!text.contains("\n.\n"));
260            }
261            _ => panic!("Expected Named license"),
262        }
263    }
264
265    #[test]
266    fn test_encode_field_text() {
267        // Test basic encoding of blank lines
268        let input = "line 1\n\nline 3";
269        let output = encode_field_text(input);
270        assert_eq!(output, "line 1\n.\nline 3");
271    }
272
273    #[test]
274    fn test_encode_decode_round_trip() {
275        // Test that encoding and decoding are inverse operations
276        let original = "First paragraph\n\nSecond paragraph\n\nThird paragraph";
277        let encoded = encode_field_text(original);
278        let decoded = decode_field_text(&encoded);
279        assert_eq!(
280            decoded, original,
281            "Round-trip encoding/decoding should preserve text"
282        );
283    }
284
285    #[test]
286    fn test_license_display_encodes_blank_lines() {
287        // Test that License::Display encodes blank lines
288        let license = License::Named("MIT".to_string(), "Line 1\n\nLine 2".to_string());
289        let displayed = license.to_string();
290        assert_eq!(displayed, "MIT\nLine 1\n.\nLine 2");
291        assert!(displayed.contains("\n.\n"), "Should contain period marker");
292        assert_eq!(
293            displayed.matches("\n\n").count(),
294            0,
295            "Should not contain literal blank lines"
296        );
297    }
298
299    #[test]
300    fn test_pattern_depth() {
301        assert_eq!(pattern_depth("*"), 0);
302        assert_eq!(pattern_depth("src/*"), 1);
303        assert_eq!(pattern_depth("src/foo/*"), 2);
304        assert_eq!(pattern_depth("a/b/c/d/*"), 4);
305        assert_eq!(pattern_depth("debian/*"), 1);
306    }
307
308    #[test]
309    fn test_is_debian_pattern() {
310        assert!(is_debian_pattern("debian/*"));
311        assert!(is_debian_pattern("debian/patches/*"));
312        assert!(is_debian_pattern(" debian/* "));
313        assert!(!is_debian_pattern("*"));
314        assert!(!is_debian_pattern("src/*"));
315        assert!(!is_debian_pattern("src/debian/*"));
316    }
317
318    #[test]
319    fn test_pattern_sort_key() {
320        // Test wildcard pattern (priority 0)
321        assert_eq!(pattern_sort_key("*", 0), (0, 0));
322        assert_eq!(pattern_sort_key(" * ", 0), (0, 0));
323
324        // Test normal patterns (priority 1)
325        assert_eq!(pattern_sort_key("src/*", 1), (1, 1));
326        assert_eq!(pattern_sort_key("src/foo/*", 2), (1, 2));
327        assert_eq!(pattern_sort_key("tests/*", 1), (1, 1));
328
329        // Test debian patterns (priority 2)
330        assert_eq!(pattern_sort_key("debian/*", 1), (2, 1));
331        assert_eq!(pattern_sort_key("debian/patches/*", 2), (2, 2));
332    }
333
334    #[test]
335    fn test_pattern_sort_key_ordering() {
336        // Wildcard comes first
337        assert!(pattern_sort_key("*", 0) < pattern_sort_key("src/*", 1));
338        assert!(pattern_sort_key("*", 0) < pattern_sort_key("debian/*", 1));
339
340        // Normal patterns come before debian patterns
341        assert!(pattern_sort_key("src/*", 1) < pattern_sort_key("debian/*", 1));
342        assert!(pattern_sort_key("tests/*", 1) < pattern_sort_key("debian/*", 1));
343
344        // Within same priority, shallower comes before deeper
345        assert!(pattern_sort_key("src/*", 1) < pattern_sort_key("src/foo/*", 2));
346        assert!(pattern_sort_key("debian/*", 1) < pattern_sort_key("debian/patches/*", 2));
347
348        // Debian patterns come last even with same depth as normal patterns
349        assert!(pattern_sort_key("src/*", 1) < pattern_sort_key("debian/*", 1));
350    }
351}