1use crate::{License, CURRENT_FORMAT, KNOWN_FORMATS};
39use deb822_lossless::IndentPattern;
40use deb822_lossless::{Deb822, Paragraph};
41use std::path::Path;
42
43fn decode_field_text(text: &str) -> String {
58 text.lines()
59 .map(|line| {
60 if line == "." {
61 ""
63 } else {
64 line
65 }
66 })
67 .collect::<Vec<_>>()
68 .join("\n")
69}
70
71fn encode_field_text(text: &str) -> String {
85 text.lines()
86 .map(|line| {
87 if line.is_empty() {
88 "."
90 } else {
91 line
92 }
93 })
94 .collect::<Vec<_>>()
95 .join("\n")
96}
97
98const HEADER_FIELD_ORDER: &[&str] = &[
100 "Format",
101 "Upstream-Name",
102 "Upstream-Contact",
103 "Source",
104 "Disclaimer",
105 "Comment",
106 "License",
107 "Copyright",
108];
109
110const FILES_FIELD_ORDER: &[&str] = &["Files", "Copyright", "License", "Comment"];
112
113const LICENSE_FIELD_ORDER: &[&str] = &["License", "Comment"];
115
116const FILES_SEPARATOR: &str = " ";
118
119#[derive(Debug, Clone, PartialEq)]
121pub struct Copyright(Deb822);
122
123impl Copyright {
124 pub fn new() -> Self {
126 let mut deb822 = Deb822::new();
127 let mut header = deb822.add_paragraph();
128 header.set("Format", CURRENT_FORMAT);
129 Copyright(deb822)
130 }
131
132 pub fn empty() -> Self {
136 Self(Deb822::new())
137 }
138
139 pub fn header(&self) -> Option<Header> {
141 self.0.paragraphs().next().map(Header)
142 }
143
144 pub fn iter_files(&self) -> impl Iterator<Item = FilesParagraph> {
146 self.0
147 .paragraphs()
148 .filter(|x| x.contains_key("Files"))
149 .map(FilesParagraph)
150 }
151
152 pub fn iter_licenses(&self) -> impl Iterator<Item = LicenseParagraph> {
154 self.0
155 .paragraphs()
156 .filter(|x| {
157 !x.contains_key("Files") && !x.contains_key("Format") && x.contains_key("License")
158 })
159 .map(LicenseParagraph)
160 }
161
162 pub fn find_files(&self, filename: &Path) -> Option<FilesParagraph> {
167 self.iter_files().filter(|p| p.matches(filename)).last()
168 }
169
170 pub fn find_license_by_name(&self, name: &str) -> Option<License> {
174 self.iter_licenses()
175 .find(|p| p.name().as_deref() == Some(name))
176 .map(|x| x.into())
177 }
178
179 pub fn find_license_for_file(&self, filename: &Path) -> Option<License> {
181 let files = self.find_files(filename)?;
182 let license = files.license()?;
183 if license.text().is_some() {
184 return Some(license);
185 }
186 self.find_license_by_name(license.name()?)
187 }
188
189 pub fn from_str_relaxed(s: &str) -> Result<(Self, Vec<String>), Error> {
191 if !s.starts_with("Format:") {
192 return Err(Error::NotMachineReadable);
193 }
194
195 let (deb822, errors) = Deb822::from_str_relaxed(s);
196 Ok((Self(deb822), errors))
197 }
198
199 pub fn from_file_relaxed<P: AsRef<Path>>(path: P) -> Result<(Self, Vec<String>), Error> {
201 let text = std::fs::read_to_string(path)?;
202 Self::from_str_relaxed(&text)
203 }
204
205 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
207 let text = std::fs::read_to_string(path)?;
208 use std::str::FromStr;
209 Self::from_str(&text)
210 }
211
212 pub fn add_files(
216 &mut self,
217 files: &[&str],
218 copyright: &[&str],
219 license: &License,
220 ) -> FilesParagraph {
221 let mut para = self.0.add_paragraph();
222 para.set_with_field_order("Files", &files.join(FILES_SEPARATOR), FILES_FIELD_ORDER);
223 para.set_with_field_order("Copyright", ©right.join("\n"), FILES_FIELD_ORDER);
224 let license_text = match license {
225 License::Name(name) => name.to_string(),
226 License::Named(name, text) => format!("{}\n{}", name, text),
227 License::Text(text) => text.to_string(),
228 };
229 para.set_with_forced_indent(
230 "License",
231 &license_text,
232 &IndentPattern::Fixed(1),
233 Some(FILES_FIELD_ORDER),
234 );
235 FilesParagraph(para)
236 }
237
238 pub fn add_license(&mut self, license: &License) -> LicenseParagraph {
242 let mut para = self.0.add_paragraph();
243 let license_text = match license {
244 License::Name(name) => name.to_string(),
245 License::Named(name, text) => format!("{}\n{}", name, encode_field_text(text)),
246 License::Text(text) => encode_field_text(text),
247 };
248 para.set_with_indent_pattern(
250 "License",
251 &license_text,
252 Some(&IndentPattern::Fixed(1)),
253 Some(LICENSE_FIELD_ORDER),
254 );
255 LicenseParagraph(para)
256 }
257
258 pub fn remove_license_by_name(&mut self, name: &str) -> bool {
263 let mut index = None;
265 for (i, para) in self.0.paragraphs().enumerate() {
266 if !para.contains_key("Files")
267 && !para.contains_key("Format")
268 && para.contains_key("License")
269 {
270 let license_para = LicenseParagraph(para);
271 if license_para.name().as_deref() == Some(name) {
272 index = Some(i);
273 break;
274 }
275 }
276 }
277
278 if let Some(i) = index {
279 self.0.remove_paragraph(i);
280 true
281 } else {
282 false
283 }
284 }
285
286 pub fn remove_files_by_pattern(&mut self, pattern: &str) -> bool {
291 let mut index = None;
293 for (i, para) in self.0.paragraphs().enumerate() {
294 if para.contains_key("Files") {
295 let files_para = FilesParagraph(para);
296 if files_para.files().iter().any(|f| f == pattern) {
297 index = Some(i);
298 break;
299 }
300 }
301 }
302
303 if let Some(i) = index {
304 self.0.remove_paragraph(i);
305 true
306 } else {
307 false
308 }
309 }
310}
311
312#[derive(Debug)]
314pub enum Error {
315 ParseError(deb822_lossless::ParseError),
317
318 IoError(std::io::Error),
320
321 InvalidValue(String),
323
324 NotMachineReadable,
326}
327
328impl From<deb822_lossless::Error> for Error {
329 fn from(e: deb822_lossless::Error) -> Self {
330 match e {
331 deb822_lossless::Error::ParseError(e) => Error::ParseError(e),
332 deb822_lossless::Error::IoError(e) => Error::IoError(e),
333 deb822_lossless::Error::InvalidValue(msg) => Error::InvalidValue(msg),
334 }
335 }
336}
337
338impl From<std::io::Error> for Error {
339 fn from(e: std::io::Error) -> Self {
340 Error::IoError(e)
341 }
342}
343
344impl From<deb822_lossless::ParseError> for Error {
345 fn from(e: deb822_lossless::ParseError) -> Self {
346 Error::ParseError(e)
347 }
348}
349
350impl std::fmt::Display for Error {
351 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
352 match &self {
353 Error::ParseError(e) => write!(f, "parse error: {}", e),
354 Error::NotMachineReadable => write!(f, "not machine readable"),
355 Error::IoError(e) => write!(f, "io error: {}", e),
356 Error::InvalidValue(msg) => write!(f, "invalid value: {}", msg),
357 }
358 }
359}
360
361impl std::error::Error for Error {}
362
363impl Default for Copyright {
364 fn default() -> Self {
365 Copyright(Deb822::new())
366 }
367}
368
369impl std::str::FromStr for Copyright {
370 type Err = Error;
371
372 fn from_str(s: &str) -> Result<Self, Self::Err> {
373 if !s.starts_with("Format:") {
374 return Err(Error::NotMachineReadable);
375 }
376 Ok(Self(Deb822::from_str(s)?))
377 }
378}
379
380impl std::fmt::Display for Copyright {
381 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
382 f.write_str(&self.0.to_string())
383 }
384}
385
386pub struct Header(Paragraph);
388
389impl Header {
390 pub fn format_string(&self) -> Option<String> {
392 self.0
393 .get("Format")
394 .or_else(|| self.0.get("Format-Specification"))
395 }
396
397 pub fn as_deb822(&self) -> &Paragraph {
399 &self.0
400 }
401
402 #[deprecated = "Use as_deb822 instead"]
404 pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
405 &mut self.0
406 }
407
408 pub fn upstream_name(&self) -> Option<String> {
410 self.0.get("Upstream-Name")
411 }
412
413 pub fn set_upstream_name(&mut self, name: &str) {
415 self.0
416 .set_with_field_order("Upstream-Name", name, HEADER_FIELD_ORDER);
417 }
418
419 pub fn upstream_contact(&self) -> Option<String> {
421 self.0.get("Upstream-Contact")
422 }
423
424 pub fn set_upstream_contact(&mut self, contact: &str) {
426 self.0
427 .set_with_field_order("Upstream-Contact", contact, HEADER_FIELD_ORDER);
428 }
429
430 pub fn source(&self) -> Option<String> {
432 self.0.get("Source")
433 }
434
435 pub fn set_source(&mut self, source: &str) {
437 self.0
438 .set_with_field_order("Source", source, HEADER_FIELD_ORDER);
439 }
440
441 pub fn files_excluded(&self) -> Option<Vec<String>> {
443 self.0
444 .get("Files-Excluded")
445 .map(|x| x.split('\n').map(|x| x.to_string()).collect::<Vec<_>>())
446 }
447
448 pub fn set_files_excluded(&mut self, files: &[&str]) {
450 self.0
451 .set_with_field_order("Files-Excluded", &files.join("\n"), HEADER_FIELD_ORDER);
452 }
453
454 pub fn fix(&mut self) {
459 if self.0.contains_key("Format-Specification") {
460 self.0.rename("Format-Specification", "Format");
461 }
462
463 if let Some(mut format) = self.0.get("Format") {
464 if !format.ends_with('/') {
465 format.push('/');
466 }
467
468 if let Some(rest) = format.strip_prefix("http:") {
469 format = format!("https:{}", rest);
470 }
471
472 if KNOWN_FORMATS.contains(&format.as_str()) {
473 format = CURRENT_FORMAT.to_string();
474 }
475
476 self.0.set("Format", format.as_str());
477 }
478 }
479}
480
481pub struct FilesParagraph(Paragraph);
483
484impl FilesParagraph {
485 pub fn as_deb822(&self) -> &Paragraph {
487 &self.0
488 }
489
490 pub fn files(&self) -> Vec<String> {
492 self.0
493 .get("Files")
494 .unwrap()
495 .split_whitespace()
496 .map(|v| v.to_string())
497 .collect::<Vec<_>>()
498 }
499
500 pub fn set_files(&mut self, files: &[&str]) {
502 self.0
503 .set_with_field_order("Files", &files.join(FILES_SEPARATOR), FILES_FIELD_ORDER);
504 }
505
506 pub fn add_file(&mut self, pattern: &str) {
510 let mut files = self.files();
511 if !files.contains(&pattern.to_string()) {
512 files.push(pattern.to_string());
513 self.0
514 .set_with_field_order("Files", &files.join(FILES_SEPARATOR), FILES_FIELD_ORDER);
515 }
516 }
517
518 pub fn remove_file(&mut self, pattern: &str) -> bool {
522 let mut files = self.files();
523 if let Some(pos) = files.iter().position(|f| f == pattern) {
524 files.remove(pos);
525 self.0
526 .set_with_field_order("Files", &files.join(FILES_SEPARATOR), FILES_FIELD_ORDER);
527 true
528 } else {
529 false
530 }
531 }
532
533 pub fn matches(&self, filename: &std::path::Path) -> bool {
535 self.files()
536 .iter()
537 .any(|f| crate::glob::glob_to_regex(f).is_match(filename.to_str().unwrap()))
538 }
539
540 pub fn copyright(&self) -> Vec<String> {
542 self.0
543 .get("Copyright")
544 .unwrap_or_default()
545 .split('\n')
546 .map(|x| x.to_string())
547 .collect::<Vec<_>>()
548 }
549
550 pub fn set_copyright(&mut self, authors: &[&str]) {
552 self.0
553 .set_with_field_order("Copyright", &authors.join("\n"), FILES_FIELD_ORDER);
554 }
555
556 pub fn comment(&self) -> Option<String> {
558 self.0.get("Comment")
559 }
560
561 pub fn set_comment(&mut self, comment: &str) {
563 self.0
564 .set_with_field_order("Comment", comment, FILES_FIELD_ORDER);
565 }
566
567 pub fn license(&self) -> Option<License> {
569 self.0.get_multiline("License").map(|x| {
570 x.split_once('\n').map_or_else(
571 || License::Name(x.to_string()),
572 |(name, text)| {
573 if name.is_empty() {
574 License::Text(text.to_string())
575 } else {
576 License::Named(name.to_string(), text.to_string())
577 }
578 },
579 )
580 })
581 }
582
583 pub fn set_license(&mut self, license: &License) {
585 let text = match license {
586 License::Name(name) => name.to_string(),
587 License::Named(name, text) => format!("{}\n{}", name, encode_field_text(text)),
588 License::Text(text) => encode_field_text(text),
589 };
590 let indent_pattern = deb822_lossless::IndentPattern::Fixed(1);
592 self.0
593 .set_with_forced_indent("License", &text, &indent_pattern, Some(FILES_FIELD_ORDER));
594 }
595}
596
597pub struct LicenseParagraph(Paragraph);
599
600impl From<LicenseParagraph> for License {
601 fn from(p: LicenseParagraph) -> Self {
602 let x = p.0.get_multiline("License").unwrap();
603 x.split_once('\n').map_or_else(
604 || License::Name(x.to_string()),
605 |(name, text)| {
606 if name.is_empty() {
607 License::Text(text.to_string())
608 } else {
609 License::Named(name.to_string(), text.to_string())
610 }
611 },
612 )
613 }
614}
615
616impl LicenseParagraph {
617 pub fn as_deb822(&self) -> &Paragraph {
619 &self.0
620 }
621
622 pub fn comment(&self) -> Option<String> {
624 self.0.get("Comment")
625 }
626
627 pub fn set_comment(&mut self, comment: &str) {
629 self.0
630 .set_with_field_order("Comment", comment, LICENSE_FIELD_ORDER);
631 }
632
633 pub fn name(&self) -> Option<String> {
635 self.0
636 .get_multiline("License")
637 .and_then(|x| x.split_once('\n').map(|(name, _)| name.to_string()))
638 }
639
640 pub fn text(&self) -> Option<String> {
642 self.0
643 .get_multiline("License")
644 .and_then(|x| x.split_once('\n').map(|(_, text)| decode_field_text(text)))
645 }
646
647 pub fn license(&self) -> License {
649 let x = self.0.get_multiline("License").unwrap();
650 x.split_once('\n').map_or_else(
651 || License::Name(x.to_string()),
652 |(name, text)| {
653 let decoded_text = decode_field_text(text);
654 if name.is_empty() {
655 License::Text(decoded_text)
656 } else {
657 License::Named(name.to_string(), decoded_text)
658 }
659 },
660 )
661 }
662
663 pub fn set_license(&mut self, license: &License) {
665 let text = match license {
666 License::Name(name) => name.to_string(),
667 License::Named(name, text) => format!("{}\n{}", name, encode_field_text(text)),
668 License::Text(text) => encode_field_text(text),
669 };
670 let indent_pattern = deb822_lossless::IndentPattern::Fixed(1);
672 self.0
673 .set_with_forced_indent("License", &text, &indent_pattern, Some(LICENSE_FIELD_ORDER));
674 }
675
676 pub fn set_name(&mut self, name: &str) {
681 let current = self.license();
682 let new_license = match current {
683 License::Named(_, text) | License::Text(text) => License::Named(name.to_string(), text),
684 License::Name(_) => License::Name(name.to_string()),
685 };
686 self.set_license(&new_license);
687 }
688
689 pub fn set_text(&mut self, text: Option<&str>) {
695 let current = self.license();
696 let new_license = match (current, text) {
697 (License::Named(name, _), Some(new_text)) | (License::Name(name), Some(new_text)) => {
698 License::Named(name, new_text.to_string())
699 }
700 (License::Named(name, _), None) | (License::Name(name), None) => License::Name(name),
701 (License::Text(_), Some(new_text)) => License::Text(new_text.to_string()),
702 (License::Text(_), None) => {
703 License::Name(String::new())
705 }
706 };
707 self.set_license(&new_license);
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 #[test]
714 fn test_not_machine_readable() {
715 let s = r#"
716This copyright file is not machine readable.
717"#;
718 let ret = s.parse::<super::Copyright>();
719 assert!(ret.is_err());
720 assert!(matches!(ret.unwrap_err(), super::Error::NotMachineReadable));
721 }
722
723 #[test]
724 fn test_new() {
725 let n = super::Copyright::new();
726 assert_eq!(
727 n.to_string().as_str(),
728 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n"
729 );
730 }
731
732 #[test]
733 fn test_parse() {
734 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
735Upstream-Name: foo
736Upstream-Contact: Joe Bloggs <joe@example.com>
737Source: https://example.com/foo
738
739Files: *
740Copyright:
741 2020 Joe Bloggs <joe@example.com>
742License: GPL-3+
743
744Files: debian/*
745Comment: Debian packaging is licensed under the GPL-3+.
746Copyright: 2023 Jelmer Vernooij
747License: GPL-3+
748
749License: GPL-3+
750 This program is free software: you can redistribute it and/or modify
751 it under the terms of the GNU General Public License as published by
752 the Free Software Foundation, either version 3 of the License, or
753 (at your option) any later version.
754"#;
755 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
756
757 assert_eq!(
758 "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
759 copyright.header().unwrap().format_string().unwrap()
760 );
761 assert_eq!("foo", copyright.header().unwrap().upstream_name().unwrap());
762 assert_eq!(
763 "Joe Bloggs <joe@example.com>",
764 copyright.header().unwrap().upstream_contact().unwrap()
765 );
766 assert_eq!(
767 "https://example.com/foo",
768 copyright.header().unwrap().source().unwrap()
769 );
770
771 let files = copyright.iter_files().collect::<Vec<_>>();
772 assert_eq!(2, files.len());
773 assert_eq!("*", files[0].files().join(" "));
774 assert_eq!("debian/*", files[1].files().join(" "));
775 assert_eq!(
776 "Debian packaging is licensed under the GPL-3+.",
777 files[1].comment().unwrap()
778 );
779 assert_eq!(
780 vec!["2023 Jelmer Vernooij".to_string()],
781 files[1].copyright()
782 );
783 assert_eq!("GPL-3+", files[1].license().unwrap().name().unwrap());
784 assert_eq!(files[1].license().unwrap().text(), None);
785
786 let licenses = copyright.iter_licenses().collect::<Vec<_>>();
787 assert_eq!(1, licenses.len());
788 assert_eq!("GPL-3+", licenses[0].name().unwrap());
789 assert_eq!(
790 "This program is free software: you can redistribute it and/or modify
791it under the terms of the GNU General Public License as published by
792the Free Software Foundation, either version 3 of the License, or
793(at your option) any later version.",
794 licenses[0].text().unwrap()
795 );
796
797 let upstream_files = copyright.find_files(std::path::Path::new("foo.c")).unwrap();
798 assert_eq!(vec!["*"], upstream_files.files());
799
800 let debian_files = copyright
801 .find_files(std::path::Path::new("debian/foo.c"))
802 .unwrap();
803 assert_eq!(vec!["debian/*"], debian_files.files());
804
805 let gpl = copyright.find_license_by_name("GPL-3+");
806 assert!(gpl.is_some());
807
808 let gpl = copyright.find_license_for_file(std::path::Path::new("debian/foo.c"));
809 assert_eq!(gpl.unwrap().name().unwrap(), "GPL-3+");
810 }
811
812 #[test]
813 fn test_from_str_relaxed() {
814 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
815Upstream-Name: foo
816Source: https://example.com/foo
817
818Files: *
819Copyright: 2020 Joe Bloggs <joe@example.com>
820License: GPL-3+
821"#;
822 let (copyright, errors) = super::Copyright::from_str_relaxed(s).unwrap();
823 assert!(errors.is_empty());
824 assert_eq!("foo", copyright.header().unwrap().upstream_name().unwrap());
825 }
826
827 #[test]
828 fn test_from_file_relaxed() {
829 let tmpfile = std::env::temp_dir().join("test_copyright.txt");
830 std::fs::write(
831 &tmpfile,
832 r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
833Upstream-Name: foo
834Source: https://example.com/foo
835
836Files: *
837Copyright: 2020 Joe Bloggs <joe@example.com>
838License: GPL-3+
839"#,
840 )
841 .unwrap();
842 let (copyright, errors) = super::Copyright::from_file_relaxed(&tmpfile).unwrap();
843 assert!(errors.is_empty());
844 assert_eq!("foo", copyright.header().unwrap().upstream_name().unwrap());
845 std::fs::remove_file(&tmpfile).unwrap();
846 }
847
848 #[test]
849 fn test_header_set_upstream_contact() {
850 let copyright = super::Copyright::new();
851 let mut header = copyright.header().unwrap();
852 header.set_upstream_contact("Test Person <test@example.com>");
853 assert_eq!(
854 header.upstream_contact().unwrap(),
855 "Test Person <test@example.com>"
856 );
857 }
858
859 #[test]
860 fn test_header_set_source() {
861 let copyright = super::Copyright::new();
862 let mut header = copyright.header().unwrap();
863 header.set_source("https://example.com/source");
864 assert_eq!(header.source().unwrap(), "https://example.com/source");
865 }
866
867 #[test]
868 fn test_license_paragraph_set_comment() {
869 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
870
871License: GPL-3+
872 This is the license text.
873"#;
874 let copyright = s.parse::<super::Copyright>().unwrap();
875 let mut license = copyright.iter_licenses().next().unwrap();
876 license.set_comment("This is a test comment");
877 assert_eq!(license.comment().unwrap(), "This is a test comment");
878 }
879
880 #[test]
881 fn test_license_paragraph_set_license() {
882 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
883
884License: GPL-3+
885 Old license text.
886"#;
887 let copyright = s.parse::<super::Copyright>().unwrap();
888 let mut license = copyright.iter_licenses().next().unwrap();
889
890 let new_license = crate::License::Named(
891 "MIT".to_string(),
892 "Permission is hereby granted...".to_string(),
893 );
894 license.set_license(&new_license);
895
896 assert_eq!(license.name().unwrap(), "MIT");
897 assert_eq!(license.text().unwrap(), "Permission is hereby granted...");
898 }
899
900 #[test]
901 fn test_iter_licenses_excludes_header() {
902 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
904Upstream-Name: foo
905License: GPL-3+
906
907Files: *
908Copyright: 2020 Joe Bloggs
909License: MIT
910
911License: GPL-3+
912 This is the GPL-3+ license text.
913"#;
914 let copyright = s.parse::<super::Copyright>().unwrap();
915 let licenses: Vec<_> = copyright.iter_licenses().collect();
916
917 assert_eq!(1, licenses.len());
919 assert_eq!("GPL-3+", licenses[0].name().unwrap());
920 assert_eq!(
921 "This is the GPL-3+ license text.",
922 licenses[0].text().unwrap()
923 );
924 }
925
926 #[test]
927 fn test_add_files() {
928 let mut copyright = super::Copyright::new();
929 let license = crate::License::Name("GPL-3+".to_string());
930 copyright.add_files(
931 &["src/*", "*.rs"],
932 &["2024 John Doe", "2024 Jane Doe"],
933 &license,
934 );
935
936 let files: Vec<_> = copyright.iter_files().collect();
937 assert_eq!(1, files.len());
938 assert_eq!(vec!["src/*", "*.rs"], files[0].files());
939 assert_eq!(vec!["2024 John Doe", "2024 Jane Doe"], files[0].copyright());
940 assert_eq!("GPL-3+", files[0].license().unwrap().name().unwrap());
941
942 assert_eq!(
944 copyright.to_string(),
945 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\n\
946 Files: src/* *.rs\n\
947 Copyright: 2024 John Doe\n 2024 Jane Doe\n\
948 License: GPL-3+\n"
949 );
950 }
951
952 #[test]
953 fn test_add_files_with_license_text() {
954 let mut copyright = super::Copyright::new();
955 let license = crate::License::Named(
956 "MIT".to_string(),
957 "Permission is hereby granted...".to_string(),
958 );
959 copyright.add_files(&["*"], &["2024 Test Author"], &license);
960
961 let files: Vec<_> = copyright.iter_files().collect();
962 assert_eq!(1, files.len());
963 assert_eq!("MIT", files[0].license().unwrap().name().unwrap());
964 assert_eq!(
965 "Permission is hereby granted...",
966 files[0].license().unwrap().text().unwrap()
967 );
968
969 assert_eq!(
971 copyright.to_string(),
972 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\n\
973 Files: *\n\
974 Copyright: 2024 Test Author\n\
975 License: MIT\n Permission is hereby granted...\n"
976 );
977 }
978
979 #[test]
980 fn test_add_license() {
981 let mut copyright = super::Copyright::new();
982 let license = crate::License::Named(
983 "GPL-3+".to_string(),
984 "This is the GPL-3+ license text.".to_string(),
985 );
986 copyright.add_license(&license);
987
988 let licenses: Vec<_> = copyright.iter_licenses().collect();
989 assert_eq!(1, licenses.len());
990 assert_eq!("GPL-3+", licenses[0].name().unwrap());
991 assert_eq!(
992 "This is the GPL-3+ license text.",
993 licenses[0].text().unwrap()
994 );
995
996 assert_eq!(
998 copyright.to_string(),
999 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\n\
1000 License: GPL-3+\n This is the GPL-3+ license text.\n"
1001 );
1002 }
1003
1004 #[test]
1005 fn test_add_multiple_paragraphs() {
1006 let mut copyright = super::Copyright::new();
1007
1008 let license1 = crate::License::Name("MIT".to_string());
1010 copyright.add_files(&["src/*"], &["2024 Author One"], &license1);
1011
1012 let license2 = crate::License::Name("GPL-3+".to_string());
1014 copyright.add_files(&["debian/*"], &["2024 Author Two"], &license2);
1015
1016 let license3 =
1018 crate::License::Named("GPL-3+".to_string(), "Full GPL-3+ text here.".to_string());
1019 copyright.add_license(&license3);
1020
1021 assert_eq!(2, copyright.iter_files().count());
1023 assert_eq!(1, copyright.iter_licenses().count());
1024
1025 let files: Vec<_> = copyright.iter_files().collect();
1026 assert_eq!(vec!["src/*"], files[0].files());
1027 assert_eq!(vec!["debian/*"], files[1].files());
1028
1029 let licenses: Vec<_> = copyright.iter_licenses().collect();
1030 assert_eq!("GPL-3+", licenses[0].name().unwrap());
1031 assert_eq!("Full GPL-3+ text here.", licenses[0].text().unwrap());
1032
1033 assert_eq!(
1035 copyright.to_string(),
1036 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\n\
1037 Files: src/*\n\
1038 Copyright: 2024 Author One\n\
1039 License: MIT\n\n\
1040 Files: debian/*\n\
1041 Copyright: 2024 Author Two\n\
1042 License: GPL-3+\n\n\
1043 License: GPL-3+\n Full GPL-3+ text here.\n"
1044 );
1045 }
1046
1047 #[test]
1048 fn test_remove_license_by_name() {
1049 let mut copyright = super::Copyright::new();
1050
1051 let license1 = crate::License::Named("MIT".to_string(), "MIT license text.".to_string());
1053 copyright.add_license(&license1);
1054
1055 let license2 =
1056 crate::License::Named("GPL-3+".to_string(), "GPL-3+ license text.".to_string());
1057 copyright.add_license(&license2);
1058
1059 let license3 =
1060 crate::License::Named("Apache-2.0".to_string(), "Apache license text.".to_string());
1061 copyright.add_license(&license3);
1062
1063 assert_eq!(3, copyright.iter_licenses().count());
1065
1066 let removed = copyright.remove_license_by_name("GPL-3+");
1068 assert!(removed);
1069
1070 assert_eq!(2, copyright.iter_licenses().count());
1072
1073 let licenses: Vec<_> = copyright.iter_licenses().collect();
1075 assert_eq!("MIT", licenses[0].name().unwrap());
1076 assert_eq!("Apache-2.0", licenses[1].name().unwrap());
1077
1078 let removed = copyright.remove_license_by_name("BSD-3-Clause");
1080 assert!(!removed);
1081 assert_eq!(2, copyright.iter_licenses().count());
1082 }
1083
1084 #[test]
1085 fn test_remove_files_by_pattern() {
1086 let mut copyright = super::Copyright::new();
1087
1088 let license1 = crate::License::Name("MIT".to_string());
1090 copyright.add_files(&["src/*"], &["2024 Author One"], &license1);
1091
1092 let license2 = crate::License::Name("GPL-3+".to_string());
1093 copyright.add_files(&["debian/*"], &["2024 Author Two"], &license2);
1094
1095 let license3 = crate::License::Name("Apache-2.0".to_string());
1096 copyright.add_files(&["docs/*"], &["2024 Author Three"], &license3);
1097
1098 assert_eq!(3, copyright.iter_files().count());
1100
1101 let removed = copyright.remove_files_by_pattern("debian/*");
1103 assert!(removed);
1104
1105 assert_eq!(2, copyright.iter_files().count());
1107
1108 let files: Vec<_> = copyright.iter_files().collect();
1110 assert_eq!(vec!["src/*"], files[0].files());
1111 assert_eq!(vec!["docs/*"], files[1].files());
1112
1113 let removed = copyright.remove_files_by_pattern("tests/*");
1115 assert!(!removed);
1116 assert_eq!(2, copyright.iter_files().count());
1117 }
1118
1119 #[test]
1120 fn test_remove_files_by_pattern_with_multiple_patterns() {
1121 let mut copyright = super::Copyright::new();
1122
1123 let license = crate::License::Name("MIT".to_string());
1125 copyright.add_files(&["src/*", "*.rs"], &["2024 Author"], &license);
1126
1127 assert_eq!(1, copyright.iter_files().count());
1129
1130 let removed = copyright.remove_files_by_pattern("*.rs");
1132 assert!(removed);
1133
1134 assert_eq!(0, copyright.iter_files().count());
1136 }
1137
1138 #[test]
1139 fn test_license_paragraph_set_name() {
1140 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1141
1142License: GPL-3+
1143 This is the GPL-3+ license text.
1144"#;
1145 let copyright = s.parse::<super::Copyright>().unwrap();
1146 let mut license = copyright.iter_licenses().next().unwrap();
1147
1148 license.set_name("Apache-2.0");
1150
1151 assert_eq!(license.name().unwrap(), "Apache-2.0");
1152 assert_eq!(license.text().unwrap(), "This is the GPL-3+ license text.");
1153 }
1154
1155 #[test]
1156 fn test_license_paragraph_set_name_no_text() {
1157 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1158
1159License: GPL-3+
1160"#;
1161 let copyright = s.parse::<super::Copyright>().unwrap();
1162 let mut license = copyright.iter_licenses().next().unwrap();
1163
1164 license.set_name("MIT");
1166
1167 assert_eq!(license.license(), crate::License::Name("MIT".to_string()));
1168 assert_eq!(license.text(), None);
1169 }
1170
1171 #[test]
1172 fn test_license_paragraph_set_text() {
1173 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1174
1175License: GPL-3+
1176 Old license text.
1177"#;
1178 let copyright = s.parse::<super::Copyright>().unwrap();
1179 let mut license = copyright.iter_licenses().next().unwrap();
1180
1181 license.set_text(Some("New license text."));
1183
1184 assert_eq!(license.name().unwrap(), "GPL-3+");
1185 assert_eq!(license.text().unwrap(), "New license text.");
1186 }
1187
1188 #[test]
1189 fn test_license_paragraph_set_text_remove() {
1190 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1191
1192License: GPL-3+
1193 Old license text.
1194"#;
1195 let copyright = s.parse::<super::Copyright>().unwrap();
1196 let mut license = copyright.iter_licenses().next().unwrap();
1197
1198 license.set_text(None);
1200
1201 assert_eq!(
1202 license.license(),
1203 crate::License::Name("GPL-3+".to_string())
1204 );
1205 assert_eq!(license.text(), None);
1206 }
1207
1208 #[test]
1209 fn test_license_paragraph_set_text_add() {
1210 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1211
1212License: GPL-3+
1213"#;
1214 let copyright = s.parse::<super::Copyright>().unwrap();
1215 let mut license = copyright.iter_licenses().next().unwrap();
1216
1217 license.set_text(Some("This is the full GPL-3+ license text."));
1219
1220 assert_eq!(license.name().unwrap(), "GPL-3+");
1221 assert_eq!(
1222 license.text().unwrap(),
1223 "This is the full GPL-3+ license text."
1224 );
1225 }
1226
1227 #[test]
1228 fn test_files_paragraph_set_files() {
1229 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1230
1231Files: *
1232Copyright: 2024 Test Author
1233License: MIT
1234"#;
1235 let copyright = s.parse::<super::Copyright>().unwrap();
1236 let mut files = copyright.iter_files().next().unwrap();
1237
1238 files.set_files(&["src/*", "*.rs", "tests/*"]);
1240
1241 assert_eq!(vec!["src/*", "*.rs", "tests/*"], files.files());
1243 }
1244
1245 #[test]
1246 fn test_files_paragraph_add_file() {
1247 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1248
1249Files: src/*
1250Copyright: 2024 Test Author
1251License: MIT
1252"#;
1253 let copyright = s.parse::<super::Copyright>().unwrap();
1254 let mut files = copyright.iter_files().next().unwrap();
1255
1256 files.add_file("*.rs");
1258 assert_eq!(vec!["src/*", "*.rs"], files.files());
1259
1260 files.add_file("tests/*");
1262 assert_eq!(vec!["src/*", "*.rs", "tests/*"], files.files());
1263
1264 files.add_file("*.rs");
1266 assert_eq!(vec!["src/*", "*.rs", "tests/*"], files.files());
1267 }
1268
1269 #[test]
1270 fn test_files_paragraph_remove_file() {
1271 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1272
1273Files: src/* *.rs tests/*
1274Copyright: 2024 Test Author
1275License: MIT
1276"#;
1277 let copyright = s.parse::<super::Copyright>().unwrap();
1278 let mut files = copyright.iter_files().next().unwrap();
1279
1280 let removed = files.remove_file("*.rs");
1282 assert!(removed);
1283 assert_eq!(vec!["src/*", "tests/*"], files.files());
1284
1285 let removed = files.remove_file("tests/*");
1287 assert!(removed);
1288 assert_eq!(vec!["src/*"], files.files());
1289
1290 let removed = files.remove_file("debian/*");
1292 assert!(!removed);
1293 assert_eq!(vec!["src/*"], files.files());
1294 }
1295
1296 #[test]
1297 fn test_field_order_with_comment() {
1298 let mut copyright = super::Copyright::new();
1300
1301 let files = vec!["*"];
1302 let copyrights = vec!["Unknown"];
1303 let license = crate::License::Name("GPL-2+".to_string());
1304
1305 let mut para = copyright.add_files(&files, ©rights, &license);
1306 para.set_comment("Test comment");
1307
1308 let output = copyright.to_string();
1309
1310 let expected =
1312 "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\n\
1313 Files: *\n\
1314 Copyright: Unknown\n\
1315 License: GPL-2+\n\
1316 Comment: Test comment\n";
1317
1318 assert_eq!(
1319 output, expected,
1320 "Fields should be in DEP-5 order (Files, Copyright, License, Comment), but got:\n{}",
1321 output
1322 );
1323 }
1324
1325 #[test]
1326 fn test_license_text_decoding_paragraph_markers() {
1327 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1329
1330License: MIT
1331 Permission is hereby granted, free of charge, to any person obtaining a copy
1332 of this software and associated documentation files.
1333 .
1334 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
1335"#;
1336 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1337 let license_para = copyright
1338 .iter_licenses()
1339 .next()
1340 .expect("no license paragraph");
1341 let text = license_para.text().expect("no license text");
1342
1343 assert!(
1345 text.contains("\n\n"),
1346 "Expected blank line in decoded text, got: {:?}",
1347 text
1348 );
1349 assert!(
1350 !text.contains("\n.\n"),
1351 "Period marker should be decoded, not present in output"
1352 );
1353
1354 let expected = "Permission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND.";
1356 assert_eq!(text, expected);
1357 }
1358
1359 #[test]
1360 fn test_license_enum_decoding() {
1361 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1363
1364License: GPL-3+
1365 This program is free software.
1366 .
1367 You can redistribute it.
1368"#;
1369 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1370 let license_para = copyright
1371 .iter_licenses()
1372 .next()
1373 .expect("no license paragraph");
1374 let license = license_para.license();
1375
1376 match license {
1377 crate::License::Named(name, text) => {
1378 assert_eq!(name, "GPL-3+");
1379 assert!(text.contains("\n\n"), "Expected blank line in decoded text");
1380 assert!(!text.contains("\n.\n"), "Period marker should be decoded");
1381 assert_eq!(
1382 text,
1383 "This program is free software.\n\nYou can redistribute it."
1384 );
1385 }
1386 _ => panic!("Expected Named license"),
1387 }
1388 }
1389
1390 #[test]
1391 fn test_encode_field_text() {
1392 let input = "line 1\n\nline 3";
1394 let output = super::encode_field_text(input);
1395 assert_eq!(output, "line 1\n.\nline 3");
1396 }
1397
1398 #[test]
1399 fn test_encode_decode_round_trip() {
1400 let original = "First paragraph\n\nSecond paragraph\n\nThird paragraph";
1402 let encoded = super::encode_field_text(original);
1403 let decoded = super::decode_field_text(&encoded);
1404 assert_eq!(
1405 decoded, original,
1406 "Round-trip encoding/decoding should preserve text"
1407 );
1408 }
1409
1410 #[test]
1411 fn test_set_license_with_blank_lines() {
1412 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1414
1415License: GPL-3+
1416 Original text
1417"#;
1418 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1419 let mut license_para = copyright
1420 .iter_licenses()
1421 .next()
1422 .expect("no license paragraph");
1423
1424 let new_license = crate::License::Named(
1426 "GPL-3+".to_string(),
1427 "First paragraph.\n\nSecond paragraph.".to_string(),
1428 );
1429 license_para.set_license(&new_license);
1430
1431 let raw_text = copyright.to_string();
1433 let expected_output = "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\nLicense: GPL-3+\n First paragraph.\n .\n Second paragraph.\n";
1434 assert_eq!(raw_text, expected_output);
1435
1436 let retrieved = license_para.text().expect("no text");
1438 assert_eq!(retrieved, "First paragraph.\n\nSecond paragraph.");
1439 }
1440
1441 #[test]
1442 fn test_set_text_with_blank_lines() {
1443 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1445
1446License: MIT
1447 Original text
1448"#;
1449 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1450 let mut license_para = copyright
1451 .iter_licenses()
1452 .next()
1453 .expect("no license paragraph");
1454
1455 license_para.set_text(Some("Line 1\n\nLine 2"));
1457
1458 let raw_text = copyright.to_string();
1460 let expected_output = "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\nLicense: MIT\n Line 1\n .\n Line 2\n";
1461 assert_eq!(raw_text, expected_output);
1462
1463 let retrieved = license_para.text().expect("no text");
1465 assert_eq!(retrieved, "Line 1\n\nLine 2");
1466 }
1467
1468 #[test]
1469 fn test_set_license_uses_single_space_indent_for_new_multiline() {
1470 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1473
1474License: Apache-2.0
1475"#;
1476 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1477 let mut license_para = copyright
1478 .iter_licenses()
1479 .next()
1480 .expect("no license paragraph");
1481
1482 let new_license = crate::License::Named(
1484 "Apache-2.0".to_string(),
1485 "Licensed under the Apache License, Version 2.0".to_string(),
1486 );
1487 license_para.set_license(&new_license);
1488
1489 let result = copyright.to_string();
1491 let expected = "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\nLicense: Apache-2.0\n Licensed under the Apache License, Version 2.0\n";
1492 assert_eq!(result, expected);
1493 }
1494
1495 #[test]
1496 fn test_header_as_deb822() {
1497 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1498Upstream-Name: foo
1499"#;
1500 let copyright = s.parse::<super::Copyright>().unwrap();
1501 let header = copyright.header().unwrap();
1502 let para = header.as_deb822();
1503 assert_eq!(para.get("Upstream-Name"), Some("foo".to_string()));
1504 }
1505
1506 #[test]
1507 fn test_files_paragraph_as_deb822() {
1508 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1509
1510Files: *
1511Copyright: 2024 Test
1512License: MIT
1513"#;
1514 let copyright = s.parse::<super::Copyright>().unwrap();
1515 let files = copyright.iter_files().next().unwrap();
1516 let para = files.as_deb822();
1517 assert_eq!(para.get("Files"), Some("*".to_string()));
1518 }
1519
1520 #[test]
1521 fn test_license_paragraph_as_deb822() {
1522 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1523
1524License: GPL-3+
1525 License text
1526"#;
1527 let copyright = s.parse::<super::Copyright>().unwrap();
1528 let license = copyright.iter_licenses().next().unwrap();
1529 let para = license.as_deb822();
1530 assert!(para.get("License").unwrap().starts_with("GPL-3+"));
1531 }
1532
1533 #[test]
1534 fn test_set_license_normalizes_unusual_indentation() {
1535 let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
1538
1539License: Apache-2.0
1540 Apache License
1541 Version 2.0, January 2004
1542 http://www.apache.org/licenses/
1543 .
1544 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1545"#;
1546 let copyright = s.parse::<super::Copyright>().expect("failed to parse");
1547 let mut license_para = copyright
1548 .iter_licenses()
1549 .next()
1550 .expect("no license paragraph");
1551
1552 let new_text = "Licensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0";
1554 let new_license = crate::License::Named("Apache-2.0".to_string(), new_text.to_string());
1555 license_para.set_license(&new_license);
1556
1557 let result = copyright.to_string();
1559
1560 let expected = "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\nLicense: Apache-2.0\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n .\n http://www.apache.org/licenses/LICENSE-2.0\n";
1562
1563 assert_eq!(result, expected);
1564 }
1565}