1use 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#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)]
51pub struct Header {
52 #[deb822(field = "Format")]
53 format: String,
55
56 #[deb822(field = "Files-Excluded", deserialize_with = deserialize_file_list, serialize_with = serialize_file_list)]
57 files_excluded: Option<Vec<String>>,
59
60 #[deb822(field = "Source")]
61 source: Option<String>,
63
64 #[deb822(field = "Upstream-Contact")]
65 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#[derive(Clone, PartialEq, Eq, Debug)]
90pub struct Copyright {
91 pub header: Header,
93
94 pub files: Vec<FilesParagraph>,
96
97 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#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)]
144pub struct LicenseParagraph {
145 #[deb822(field = "License")]
147 license: License,
148
149 #[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(¶.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#[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 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(¶.to_string())?;
195 Ok(())
196 }
197}
198
199impl Default for Copyright {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205impl Copyright {
206 pub fn new() -> Self {
208 Self {
209 header: Header::default(),
210 licenses: Vec::new(),
211 files: Vec::new(),
212 }
213 }
214
215 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 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 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 = ©right.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 = ©right.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}