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
53mod glob;
54
55/// Decode deb822 paragraph markers in a multi-line field value.
56///
57/// According to Debian policy, blank lines in multi-line field values are
58/// represented as lines containing only "." (a single period). The deb822
59/// parser already strips the leading indentation whitespace from continuation lines,
60/// so we only need to decode the period markers back to blank lines.
61///
62/// # Arguments
63///
64/// * `text` - The raw field value text from deb822 parser with indentation already stripped
65///
66/// # Returns
67///
68/// The decoded text with blank lines restored
69fn decode_field_text(text: &str) -> String {
70 text.lines()
71 .map(|line| {
72 if line == "." {
73 // Paragraph marker representing a blank line
74 ""
75 } else {
76 line
77 }
78 })
79 .collect::<Vec<_>>()
80 .join("\n")
81}
82
83/// Encode blank lines in a field value to deb822 paragraph markers.
84///
85/// According to Debian policy, blank lines in multi-line field values must be
86/// represented as lines containing only "." (a single period).
87///
88/// # Arguments
89///
90/// * `text` - The decoded text with normal blank lines
91///
92/// # Returns
93///
94/// The encoded text with blank lines replaced by "."
95fn encode_field_text(text: &str) -> String {
96 text.lines()
97 .map(|line| {
98 if line.is_empty() {
99 // Blank line must be encoded as period marker
100 "."
101 } else {
102 line
103 }
104 })
105 .collect::<Vec<_>>()
106 .join("\n")
107}
108
109/// A license, which can be just a name, a text or a named license.
110#[derive(Clone, PartialEq, Eq, Debug)]
111pub enum License {
112 /// A license with just a name.
113 Name(String),
114
115 /// A license with just a text.
116 Text(String),
117
118 /// A license with a name and a text.
119 Named(String, String),
120}
121
122impl License {
123 /// Returns the name of the license, if any.
124 pub fn name(&self) -> Option<&str> {
125 match self {
126 License::Name(name) => Some(name),
127 License::Text(_) => None,
128 License::Named(name, _) => Some(name),
129 }
130 }
131
132 /// Returns the text of the license, if any.
133 pub fn text(&self) -> Option<&str> {
134 match self {
135 License::Name(_) => None,
136 License::Text(text) => Some(text),
137 License::Named(_, text) => Some(text),
138 }
139 }
140}
141
142impl std::str::FromStr for License {
143 type Err = String;
144
145 fn from_str(text: &str) -> Result<Self, Self::Err> {
146 if let Some((name, rest)) = text.split_once('\n') {
147 let decoded_text = decode_field_text(rest);
148 if name.is_empty() {
149 Ok(License::Text(decoded_text))
150 } else {
151 Ok(License::Named(name.to_string(), decoded_text))
152 }
153 } else {
154 Ok(License::Name(text.to_string()))
155 }
156 }
157}
158
159impl std::fmt::Display for License {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match self {
162 License::Name(name) => f.write_str(name),
163 License::Text(text) => write!(f, "\n{}", encode_field_text(text)),
164 License::Named(name, text) => write!(f, "{}\n{}", name, encode_field_text(text)),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_decode_field_text() {
175 // Test basic decoding of period markers
176 let input = "line 1\n.\nline 3";
177 let output = decode_field_text(input);
178 assert_eq!(output, "line 1\n\nline 3");
179 }
180
181 #[test]
182 fn test_decode_field_text_no_markers() {
183 // Test text without markers remains unchanged
184 let input = "line 1\nline 2\nline 3";
185 let output = decode_field_text(input);
186 assert_eq!(output, input);
187 }
188
189 #[test]
190 fn test_license_from_str_with_paragraph_markers() {
191 // Test that License::from_str decodes paragraph markers
192 let input = "GPL-3+\nThis is line 1\n.\nThis is line 3";
193 let license: License = input.parse().unwrap();
194
195 match license {
196 License::Named(name, text) => {
197 assert_eq!(name, "GPL-3+");
198 assert_eq!(text, "This is line 1\n\nThis is line 3");
199 assert!(!text.contains("\n.\n"));
200 }
201 _ => panic!("Expected Named license"),
202 }
203 }
204
205 #[test]
206 fn test_encode_field_text() {
207 // Test basic encoding of blank lines
208 let input = "line 1\n\nline 3";
209 let output = encode_field_text(input);
210 assert_eq!(output, "line 1\n.\nline 3");
211 }
212
213 #[test]
214 fn test_encode_decode_round_trip() {
215 // Test that encoding and decoding are inverse operations
216 let original = "First paragraph\n\nSecond paragraph\n\nThird paragraph";
217 let encoded = encode_field_text(original);
218 let decoded = decode_field_text(&encoded);
219 assert_eq!(
220 decoded, original,
221 "Round-trip encoding/decoding should preserve text"
222 );
223 }
224
225 #[test]
226 fn test_license_display_encodes_blank_lines() {
227 // Test that License::Display encodes blank lines
228 let license = License::Named("MIT".to_string(), "Line 1\n\nLine 2".to_string());
229 let displayed = license.to_string();
230 assert_eq!(displayed, "MIT\nLine 1\n.\nLine 2");
231 assert!(displayed.contains("\n.\n"), "Should contain period marker");
232 assert_eq!(
233 displayed.matches("\n\n").count(),
234 0,
235 "Should not contain literal blank lines"
236 );
237 }
238}