debian-copyright-0.1.27/.cargo_vcs_info.json0000644000000001560000000000100143550ustar { "git": { "sha1": "0708a219e2147bdc0dedd494cf6e7e7f6e0f1f01" }, "path_in_vcs": "debian-copyright" }debian-copyright-0.1.27/Cargo.lock0000644000000134360000000000100123350ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "num-traits", ] [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "deb822-derive" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b6e5cafe61e77421a090e2a33b8a2e4e2ff1b106fd906ebade111307064d981" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "deb822-lossless" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "607abd8f5e3d131f03a1f351f5a5c8426ba4bf820ad10a2c59a1d4fd6d8eb1d9" dependencies = [ "deb822-derive", "regex", "rowan", "serde", ] [[package]] name = "debian-copyright" version = "0.1.27" dependencies = [ "deb822-lossless", "debversion", "regex", ] [[package]] name = "debversion" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b892997e53d52f9ac5c30bdac09cbea6bb1eeb3f93a204b8548774081a44b496" dependencies = [ "chrono", "lazy-regex", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "lazy-regex" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "proc-macro2" version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rowan" version = "0.15.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" dependencies = [ "countme", "hashbrown", "rustc-hash", "text-size", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" debian-copyright-0.1.27/Cargo.toml0000644000000024250000000000100123540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "debian-copyright" version = "0.1.27" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A parser for Debian copyright files" homepage = "https://github.com/jelmer/deb822-lossless" readme = "README.md" keywords = [ "debian", "copyright", "edit", "dep5", ] categories = ["parser-implementations"] license = "Apache-2.0" repository = "https://github.com/jelmer/deb822-lossless" [lib] name = "debian_copyright" path = "src/lib.rs" [[example]] name = "license-for-file" path = "examples/license-for-file.rs" [dependencies.deb822-lossless] version = ">=0.2" features = ["derive"] [dependencies.debversion] version = ">=0.3" [dependencies.regex] version = ">=1.10" debian-copyright-0.1.27/Cargo.toml.orig000064400000000000000000000007441046102023000160370ustar 00000000000000[package] name = "debian-copyright" authors = ["Jelmer Vernooij "] edition = "2021" license = "Apache-2.0" description = "A parser for Debian copyright files" repository = { workspace = true } homepage = { workspace = true } version = "0.1.27" keywords = ["debian", "copyright", "edit", "dep5"] categories = ["parser-implementations"] [dependencies] debversion = ">=0.3" regex = ">=1.10" deb822-lossless = { version = ">=0.2", path = "..", features = ["derive"] } debian-copyright-0.1.27/README.md000064400000000000000000000046741046102023000144350ustar 00000000000000# Lossless parser for Debian Copyright (DEP5) files This crate contains a lossless parser for Debian Copyright files that use the [DEP-5](https://dep-team.pages.debian.net/deps/dep5/) file format. Once parsed, the files can be introspected as well as changed before written back to disk. Example: ```rust let copyright: debian_copyright::Copyright = r#"\ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: lintian-brush Upstream-Contact: Jelmer Vernooij Source: https://salsa.debian.org/jelmer/lintian-brush Files: * Copyright: 2018-2019 Jelmer Vernooij License: GPL-2+ Files: lintian_brush/systemd.py Copyright: 2001, 2002, 2003 Python Software Foundation 2004-2008 Paramjit Oberoi 2007 Tim Lauridsen 2019 Jelmer Vernooij License: MIT License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License: GPL-2+ On Debian systems, the full text of the GNU General Public License is available in /usr/share/common-licenses/GPL-2. "#.parse().unwrap(); let header = copyright.header().unwrap(); assert_eq!( "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/", header.format_string().unwrap()); assert_eq!("MIT", copyright.find_license_for_file("lintian_brush/systemd.py").unwrap().name()); assert_eq!("GPL-2+", copyright.find_license_for_file("lintian_brush/__init__.py").unwrap().name()); ``` debian-copyright-0.1.27/examples/license-for-file.rs000064400000000000000000000014701046102023000204540ustar 00000000000000use debian_copyright::Copyright; use std::path::Path; pub const TEXT: &str = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Author: John Doe Upstream-Name: example Source: https://example.com/example Files: * License: GPL-3+ Copyright: 2019 John Doe Files: debian/* License: GPL-3+ Copyright: 2019 Jane Packager License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. "#; pub fn main() { let c = TEXT.parse::().unwrap(); let license = c.find_license_for_file(Path::new("debian/foo")).unwrap(); println!("{}", license.name().unwrap()); } debian-copyright-0.1.27/src/glob.rs000064400000000000000000000042661046102023000152330ustar 00000000000000pub fn glob_to_regex(glob: &str) -> regex::Regex { let mut it = glob.chars(); let mut r = "^".to_string(); while let Some(c) = it.next() { r.push_str( match c { '*' => ".*".to_string(), '?' => ".".to_string(), '\\' => { let c = it.next(); match c { Some('?') | Some('*') | Some('\\') => regex::escape(c.unwrap().to_string().as_str()), Some(x) => { panic!("invalid escape sequence: \\{}", x); } None => { panic!("invalid escape sequence: \\"); } } }, c => regex::escape(c.to_string().as_str()), } .as_str(), ) } r.push_str("$"); regex::Regex::new(r.as_str()).unwrap() } #[cfg(test)] mod tests { #[test] fn test_simple() { let r = super::glob_to_regex("*.rs"); assert!(r.is_match("foo.rs")); assert!(r.is_match("bar.rs")); assert!(!r.is_match("foo.rs.bak")); assert!(!r.is_match("foo")); } #[test] fn test_single_char() { let r = super::glob_to_regex("?.rs"); assert!(r.is_match("a.rs")); assert!(r.is_match("b.rs")); assert!(!r.is_match("foo.rs")); assert!(!r.is_match("foo")); } #[test] fn test_escape() { let r = super::glob_to_regex(r"\?.rs"); assert!(r.is_match("?.rs")); assert!(!r.is_match("a.rs")); assert!(!r.is_match("b.rs")); let r = super::glob_to_regex(r"\*.rs"); assert!(r.is_match("*.rs")); assert!(!r.is_match("a.rs")); assert!(!r.is_match("b.rs")); let r = super::glob_to_regex(r"\\?.rs"); assert!(r.is_match("\\a.rs")); assert!(r.is_match("\\b.rs")); assert!(!r.is_match("a.rs")); } #[should_panic] #[test] fn test_invalid_escape() { super::glob_to_regex(r"\x.rs"); } #[should_panic] #[test] fn test_invalid_escape2() { super::glob_to_regex(r"\"); } } debian-copyright-0.1.27/src/lib.rs000064400000000000000000000062501046102023000150510ustar 00000000000000#![deny(missing_docs)] //! A library for parsing and manipulating debian/copyright files that //! use the DEP-5 format. //! //! # Examples //! //! ```rust //! //! use debian_copyright::Copyright; //! use std::path::Path; //! //! let text = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ //! Upstream-Author: John Doe //! Upstream-Name: example //! Source: https://example.com/example //! //! Files: * //! License: GPL-3+ //! Copyright: 2019 John Doe //! //! Files: debian/* //! License: GPL-3+ //! Copyright: 2019 Jane Packager //! //! License: GPL-3+ //! This program is free software: you can redistribute it and/or modify //! it under the terms of the GNU General Public License as published by //! the Free Software Foundation, either version 3 of the License, or //! (at your option) any later version. //! "#; //! //! let c = text.parse::().unwrap(); //! let license = c.find_license_for_file(Path::new("debian/foo")).unwrap(); //! assert_eq!(license.name(), Some("GPL-3+")); //! ``` //! //! See the ``lossless`` module (behind the ``lossless`` feature) for a more forgiving parser that //! allows partial parsing, parsing files with errors and unknown fields and editing while //! preserving formatting. pub mod lossless; pub mod lossy; pub use lossy::Copyright; /// The current version of the DEP-5 format. pub const CURRENT_FORMAT: &str = "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"; /// The known versions of the DEP-5 format. pub const KNOWN_FORMATS: &[&str] = &[CURRENT_FORMAT]; mod glob; /// A license, which can be just a name, a text or a named license. #[derive(Clone, PartialEq, Eq, Debug)] pub enum License { /// A license with just a name. Name(String), /// A license with just a text. Text(String), /// A license with a name and a text. Named(String, String), } impl License { /// Returns the name of the license, if any. pub fn name(&self) -> Option<&str> { match self { License::Name(name) => Some(name), License::Text(_) => None, License::Named(name, _) => Some(name), } } /// Returns the text of the license, if any. pub fn text(&self) -> Option<&str> { match self { License::Name(_) => None, License::Text(text) => Some(text), License::Named(_, text) => Some(text), } } } impl std::str::FromStr for License { type Err = String; fn from_str(text: &str) -> Result { if let Some((name, rest)) = text.split_once('\n') { if name.is_empty() { Ok(License::Text(rest.to_string())) } else { Ok(License::Named(name.to_string(), rest.to_string())) } } else { Ok(License::Name(text.to_string())) } } } impl std::fmt::Display for License { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { License::Name(name) => f.write_str(name), License::Text(text) => write!(f, "\n{}", text), License::Named(name, text) => write!(f, "{}\n{}", name, text), } } } debian-copyright-0.1.27/src/lossless.rs000064400000000000000000000357571046102023000161700ustar 00000000000000//! A library for parsing and manipulating debian/copyright files that //! use the DEP-5 format. //! //! This library is intended to be used for manipulating debian/copyright //! //! # Examples //! //! ```rust //! //! use debian_copyright::lossless::Copyright; //! use std::path::Path; //! //! let text = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ //! Upstream-Author: John Doe //! Upstream-Name: example //! Source: https://example.com/example //! //! Files: * //! License: GPL-3+ //! Copyright: 2019 John Doe //! //! Files: debian/* //! License: GPL-3+ //! Copyright: 2019 Jane Packager //! //! License: GPL-3+ //! This program is free software: you can redistribute it and/or modify //! it under the terms of the GNU General Public License as published by //! the Free Software Foundation, either version 3 of the License, or //! (at your option) any later version. //! "#; //! //! let c = text.parse::().unwrap(); //! let license = c.find_license_for_file(Path::new("debian/foo")).unwrap(); //! assert_eq!(license.name(), Some("GPL-3+")); //! ``` use crate::{License, CURRENT_FORMAT, KNOWN_FORMATS}; use deb822_lossless::{Deb822, Paragraph}; use std::path::Path; /// A copyright file #[derive(Debug)] pub struct Copyright(Deb822); impl Copyright { /// Create a new copyright file, with the current format pub fn new() -> Self { let mut deb822 = Deb822::new(); let mut header = deb822.add_paragraph(); header.set("Format", CURRENT_FORMAT); Copyright(deb822) } /// Create a new empty copyright file /// /// The difference with `new` is that this does not add the `Format` field. pub fn empty() -> Self { Self(Deb822::new()) } /// Return the header paragraph pub fn header(&self) -> Option
{ self.0.paragraphs().next().map(Header) } /// Iterate over all files paragraphs pub fn iter_files(&self) -> impl Iterator { self.0 .paragraphs() .filter(|x| x.contains_key("Files")) .map(FilesParagraph) } /// Iter over all license paragraphs pub fn iter_licenses(&self) -> impl Iterator { self.0 .paragraphs() .filter(|x| !x.contains_key("Files") && x.contains_key("License")) .map(LicenseParagraph) } /// Returns the Files paragraph for the given filename. /// /// Consistent with the specification, this returns the last paragraph /// that matches (which should be the most specific) pub fn find_files(&self, filename: &Path) -> Option { self.iter_files().filter(|p| p.matches(filename)).last() } /// Find license by name /// /// This will return the first license paragraph that has the given name. pub fn find_license_by_name(&self, name: &str) -> Option { self.iter_licenses() .find(|p| p.name().as_deref() == Some(name)) .map(|x| x.into()) } /// Returns the license for the given file. pub fn find_license_for_file(&self, filename: &Path) -> Option { let files = self.find_files(filename)?; let license = files.license()?; if license.text().is_some() { return Some(license); } self.find_license_by_name(license.name()?) } /// Read copyright file from a string, allowing syntax errors pub fn from_str_relaxed(s: &str) -> Result<(Self, Vec), Error> { if !s.starts_with("Format:") { return Err(Error::NotMachineReadable); } let (deb822, errors) = Deb822::from_str_relaxed(s); Ok((Self(deb822), errors)) } /// Read copyright file from a file, allowing syntax errors pub fn from_file_relaxed>(path: P) -> Result<(Self, Vec), Error> { let text = std::fs::read_to_string(path)?; Self::from_str_relaxed(&text) } /// Read copyright file from a file pub fn from_file>(path: P) -> Result { let text = std::fs::read_to_string(path)?; use std::str::FromStr; Self::from_str(&text) } } /// Error parsing copyright files #[derive(Debug)] pub enum Error { /// Parse error ParseError(deb822_lossless::ParseError), /// IO error IoError(std::io::Error), /// The file is not machine readable NotMachineReadable, } impl From for Error { fn from(e: deb822_lossless::Error) -> Self { match e { deb822_lossless::Error::ParseError(e) => Error::ParseError(e), deb822_lossless::Error::IoError(e) => Error::IoError(e), } } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::IoError(e) } } impl From for Error { fn from(e: deb822_lossless::ParseError) -> Self { Error::ParseError(e) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self { Error::ParseError(e) => write!(f, "parse error: {}", e), Error::NotMachineReadable => write!(f, "not machine readable"), Error::IoError(e) => write!(f, "io error: {}", e), } } } impl std::error::Error for Error {} impl Default for Copyright { fn default() -> Self { Copyright(Deb822::new()) } } impl std::str::FromStr for Copyright { type Err = Error; fn from_str(s: &str) -> Result { if !s.starts_with("Format:") { return Err(Error::NotMachineReadable); } Ok(Self(Deb822::from_str(s)?)) } } impl std::fmt::Display for Copyright { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str(&self.0.to_string()) } } /// A header paragraph pub struct Header(Paragraph); impl Header { /// Returns the format string for this file. pub fn format_string(&self) -> Option { self.0 .get("Format") .or_else(|| self.0.get("Format-Specification")) } /// Return the underlying Deb822 paragraph pub fn as_deb822(&self) -> &Paragraph { &self.0 } /// Return the underlying Deb822 paragraph, mutably pub fn as_mut_deb822(&mut self) -> &mut Paragraph { &mut self.0 } /// Upstream name pub fn upstream_name(&self) -> Option { self.0.get("Upstream-Name") } /// Set the upstream name pub fn set_upstream_name(&mut self, name: &str) { self.0.set("Upstream-Name", name); } /// Upstream contact pub fn upstream_contact(&self) -> Option { self.0.get("Upstream-Contact") } /// Set the upstream contact pub fn set_upstream_contact(&mut self, contact: &str) { self.0.set("Upstream-Contact", contact); } /// Source pub fn source(&self) -> Option { self.0.get("Source") } /// Set the source pub fn set_source(&mut self, source: &str) { self.0.set("Source", source); } /// List of files excluded from the copyright information, as well as the source package pub fn files_excluded(&self) -> Option> { self.0 .get("Files-Excluded") .map(|x| x.split('\n').map(|x| x.to_string()).collect::>()) } /// Set excluded files pub fn set_files_excluded(&mut self, files: &[&str]) { self.0.set("Files-Excluded", &files.join("\n")); } /// Fix the the header paragraph /// /// Currently this just renames `Format-Specification` to `Format` and replaces older format /// strings with the current format string. pub fn fix(&mut self) { if self.0.contains_key("Format-Specification") { self.0.rename("Format-Specification", "Format"); } if let Some(mut format) = self.0.get("Format") { if !format.ends_with('/') { format.push('/'); } if let Some(rest) = format.strip_prefix("http:") { format = format!("https:{}", rest); } if KNOWN_FORMATS.contains(&format.as_str()) { format = CURRENT_FORMAT.to_string(); } self.0.set("Format", format.as_str()); } } } /// A files paragraph pub struct FilesParagraph(Paragraph); impl FilesParagraph { /// List of file patterns in the paragraph pub fn files(&self) -> Vec { self.0 .get("Files") .unwrap() .split_whitespace() .map(|v| v.to_string()) .collect::>() } /// Check whether the paragraph matches the given filename pub fn matches(&self, filename: &std::path::Path) -> bool { self.files() .iter() .any(|f| crate::glob::glob_to_regex(f).is_match(filename.to_str().unwrap())) } /// Copyright holders in the paragraph pub fn copyright(&self) -> Vec { self.0 .get("Copyright") .unwrap_or_default() .split('\n') .map(|x| x.to_string()) .collect::>() } /// Set the copyright pub fn set_copyright(&mut self, authors: &[&str]) { self.0.set("Copyright", &authors.join("\n")); } /// Comment associated with the files paragraph pub fn comment(&self) -> Option { self.0.get("Comment") } /// Set the comment associated with the files paragraph pub fn set_comment(&mut self, comment: &str) { self.0.set("Comment", comment); } /// License in the paragraph pub fn license(&self) -> Option { self.0.get("License").map(|x| { x.split_once('\n').map_or_else( || License::Name(x.to_string()), |(name, text)| { if name.is_empty() { License::Text(text.to_string()) } else { License::Named(name.to_string(), text.to_string()) } }, ) }) } /// Set the license associated with the files paragraph pub fn set_license(&mut self, license: &License) { let text = match license { License::Name(name) => name.to_string(), License::Named(name, text) => format!("{}\n{}", name, text), License::Text(text) => text.to_string(), }; self.0.set("License", &text); } } /// A paragraph that contains a license pub struct LicenseParagraph(Paragraph); impl From for License { fn from(p: LicenseParagraph) -> Self { let x = p.0.get("License").unwrap(); x.split_once('\n').map_or_else( || License::Name(x.to_string()), |(name, text)| { if name.is_empty() { License::Text(text.to_string()) } else { License::Named(name.to_string(), text.to_string()) } }, ) } } impl LicenseParagraph { /// Comment associated with the license pub fn comment(&self) -> Option { self.0.get("Comment") } /// Name of the license pub fn name(&self) -> Option { self.0 .get("License") .and_then(|x| x.split_once('\n').map(|(name, _)| name.to_string())) } /// Text of the license pub fn text(&self) -> Option { self.0 .get("License") .and_then(|x| x.split_once('\n').map(|(_, text)| text.to_string())) } } #[cfg(test)] mod tests { #[test] fn test_not_machine_readable() { let s = r#" This copyright file is not machine readable. "#; let ret = s.parse::(); assert!(ret.is_err()); assert!(matches!(ret.unwrap_err(), super::Error::NotMachineReadable)); } #[test] fn test_new() { let n = super::Copyright::new(); assert_eq!( n.to_string().as_str(), "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n" ); } #[test] fn test_parse() { let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: foo Upstream-Contact: Joe Bloggs Source: https://example.com/foo Files: * Copyright: 2020 Joe Bloggs License: GPL-3+ Files: debian/* Comment: Debian packaging is licensed under the GPL-3+. Copyright: 2023 Jelmer Vernooij License: GPL-3+ License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. "#; let copyright = s.parse::().expect("failed to parse"); assert_eq!( "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/", copyright.header().unwrap().format_string().unwrap() ); assert_eq!("foo", copyright.header().unwrap().upstream_name().unwrap()); assert_eq!( "Joe Bloggs ", copyright.header().unwrap().upstream_contact().unwrap() ); assert_eq!( "https://example.com/foo", copyright.header().unwrap().source().unwrap() ); let files = copyright.iter_files().collect::>(); assert_eq!(2, files.len()); assert_eq!("*", files[0].files().join(" ")); assert_eq!("debian/*", files[1].files().join(" ")); assert_eq!( "Debian packaging is licensed under the GPL-3+.", files[1].comment().unwrap() ); assert_eq!( vec!["2023 Jelmer Vernooij".to_string()], files[1].copyright() ); assert_eq!("GPL-3+", files[1].license().unwrap().name().unwrap()); assert_eq!(files[1].license().unwrap().text(), None); let licenses = copyright.iter_licenses().collect::>(); assert_eq!(1, licenses.len()); assert_eq!("GPL-3+", licenses[0].name().unwrap()); assert_eq!( "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.", licenses[0].text().unwrap() ); let upstream_files = copyright.find_files(std::path::Path::new("foo.c")).unwrap(); assert_eq!(vec!["*"], upstream_files.files()); let debian_files = copyright .find_files(std::path::Path::new("debian/foo.c")) .unwrap(); assert_eq!(vec!["debian/*"], debian_files.files()); let gpl = copyright.find_license_by_name("GPL-3+"); assert!(gpl.is_some()); let gpl = copyright.find_license_for_file(std::path::Path::new("debian/foo.c")); assert_eq!(gpl.unwrap().name().unwrap(), "GPL-3+"); } } debian-copyright-0.1.27/src/lossy.rs000064400000000000000000000255411046102023000154600ustar 00000000000000//! A library for parsing and manipulating debian/copyright files that //! use the DEP-5 format. //! //! # Examples //! //! ```rust //! //! use debian_copyright::Copyright; //! use std::path::Path; //! //! let text = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ //! Upstream-Author: John Doe //! Upstream-Name: example //! Source: https://example.com/example //! //! Files: * //! License: GPL-3+ //! Copyright: 2019 John Doe //! //! Files: debian/* //! License: GPL-3+ //! Copyright: 2019 Jane Packager //! //! License: GPL-3+ //! This program is free software: you can redistribute it and/or modify //! it under the terms of the GNU General Public License as published by //! the Free Software Foundation, either version 3 of the License, or //! (at your option) any later version. //! "#; //! //! let c = text.parse::().unwrap(); //! let license = c.find_license_for_file(Path::new("debian/foo")).unwrap(); //! assert_eq!(license.name(), Some("GPL-3+")); //! ``` use crate::License; use crate::CURRENT_FORMAT; use deb822_lossless::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph}; use std::path::Path; fn deserialize_file_list(text: &str) -> Result, String> { Ok(text.split('\n').map(|x| x.to_string()).collect()) } fn serialize_file_list(files: &[String]) -> String { files.join("\n") } /// A header paragraph. #[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)] pub struct Header { #[deb822(field = "Format")] /// The format of the file. format: String, #[deb822(field = "Files-Excluded", deserialize_with = deserialize_file_list, serialize_with = serialize_file_list)] /// Files that are excluded from the copyright information, and should be excluded from the package. files_excluded: Option>, #[deb822(field = "Source")] /// The source of the package. source: Option, #[deb822(field = "Upstream-Contact")] /// Contact information for the upstream author. upstream_contact: Option, } impl Default for Header { fn default() -> Self { Header { format: CURRENT_FORMAT.to_string(), files_excluded: None, source: None, upstream_contact: None, } } } impl std::fmt::Display for Header { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let para: deb822_lossless::lossy::Paragraph = self.to_paragraph(); write!(f, "{}", para)?; Ok(()) } } /// A copyright file. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Copyright { /// The header paragraph. pub header: Header, /// Files paragraphs. pub files: Vec, /// License paragraphs. pub licenses: Vec, } impl std::str::FromStr for Copyright { type Err = String; fn from_str(s: &str) -> Result { if !s.starts_with("Format:") { return Err("Not machine readable".to_string()); } let deb822: deb822_lossless::Deb822 = s .parse() .map_err(|e: deb822_lossless::ParseError| e.to_string())?; let mut paragraphs = deb822.paragraphs(); let first_para = if let Some(para) = paragraphs.next() { para } else { return Err("No paragraphs".to_string()); }; let header: Header = Header::from_paragraph(&first_para)?; let mut files_paras = vec![]; let mut license_paras = vec![]; while let Some(para) = paragraphs.next() { if para.get("Files").is_some() { files_paras.push(FilesParagraph::from_paragraph(¶)?); } else if para.get("License").is_some() { license_paras.push(LicenseParagraph::from_paragraph(¶)?); } else { return Err("Paragraph is neither License nor Files".to_string()); } } Ok(Copyright { header, files: files_paras, licenses: license_paras, }) } } /// A paragraph describing a license. #[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)] pub struct LicenseParagraph { /// The license text. #[deb822(field = "License")] license: License, /// A comment. #[deb822(field = "Comment")] comment: Option, } impl std::fmt::Display for LicenseParagraph { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let para: deb822_lossless::lossy::Paragraph = self.to_paragraph(); f.write_str(¶.to_string()) } } fn deserialize_copyrights(text: &str) -> Result, String> { Ok(text.split('\n').map(ToString::to_string).collect()) } fn serialize_copyrights(copyrights: &[String]) -> String { copyrights.join("\n") } /// A paragraph describing a set of files. #[derive(FromDeb822, ToDeb822, Clone, PartialEq, Eq, Debug)] pub struct FilesParagraph { #[deb822(field="Files", deserialize_with = deserialize_file_list, serialize_with = serialize_file_list)] files: Vec, #[deb822(field = "License")] license: License, #[deb822(field="Copyright", deserialize_with = deserialize_copyrights, serialize_with = serialize_copyrights)] copyright: Vec, #[deb822(field = "Comment")] comment: Option, } impl FilesParagraph { /// Check if the given filename matches one of the file patterns in this paragraph. pub fn matches(&self, filename: &std::path::Path) -> bool { self.files .iter() .any(|f| crate::glob::glob_to_regex(f).is_match(filename.to_str().unwrap())) } } impl std::fmt::Display for FilesParagraph { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let para: deb822_lossless::lossy::Paragraph = self.to_paragraph(); f.write_str(¶.to_string())?; Ok(()) } } impl Default for Copyright { fn default() -> Self { Self::new() } } impl Copyright { /// Create a new empty `Copyright` object. pub fn new() -> Self { Self { header: Header::default(), licenses: Vec::new(), files: Vec::new(), } } /// Find the files paragraph that matches the given path. /// /// Returns `None` if no matching files paragraph is found. /// /// # Arguments /// * `path` - The path to the file to find the license for. pub fn find_files(&self, path: &std::path::Path) -> Option<&FilesParagraph> { self.files.iter().filter(|f| f.matches(path)).last() } /// Returns the license for the given file. pub fn find_license_for_file(&self, filename: &Path) -> Option<&License> { let files = self.find_files(filename)?; if files.license.text().is_some() { return Some(&files.license); } self.find_license_by_name(files.license.name().unwrap()) } /// Find a license by name. /// /// Returns `None` if no license with the given name is found. /// /// # Arguments /// * `name` - The name of the license to find. pub fn find_license_by_name(&self, name: &str) -> Option<&License> { self.licenses .iter() .find(|p| p.license.name() == Some(name)) .map(|p| &p.license) } } impl std::fmt::Display for Copyright { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.header)?; for files in &self.files { writeln!(f)?; write!(f, "{}", files)?; } for license in &self.licenses { writeln!(f)?; write!(f, "{}", license)?; } Ok(()) } } #[cfg(test)] mod tests { #[test] fn test_not_machine_readable() { let s = r#" This copyright file is not machine readable. "#; let ret = s.parse::(); assert!(ret.is_err()); assert_eq!(ret.unwrap_err(), "Not machine readable".to_string()); } #[test] fn test_new() { let n = super::Copyright::new(); assert_eq!( n.to_string().as_str(), "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n" ); } #[test] fn test_parse() { let s = r#"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: foo Upstream-Contact: Joe Bloggs Source: https://example.com/foo Files: * Copyright: 2020 Joe Bloggs License: GPL-3+ Files: debian/* Comment: Debian packaging is licensed under the GPL-3+. Copyright: 2023 Jelmer Vernooij License: GPL-3+ License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. "#; let copyright = s.parse::().expect("failed to parse"); assert_eq!( "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/", copyright.header.format ); assert_eq!( "Joe Bloggs ", copyright.header.upstream_contact.as_ref().unwrap() ); assert_eq!( "https://example.com/foo", copyright.header.source.as_ref().unwrap() ); let files = ©right.files; assert_eq!(2, files.len()); assert_eq!("*", files[0].files.join(" ")); assert_eq!("debian/*", files[1].files.join(" ")); assert_eq!( "Debian packaging is licensed under the GPL-3+.", files[1].comment.as_ref().unwrap() ); assert_eq!(vec!["2023 Jelmer Vernooij".to_string()], files[1].copyright); assert_eq!("GPL-3+", files[1].license.name().unwrap()); assert_eq!(files[1].license.text(), None); let licenses = ©right.licenses; assert_eq!(1, licenses.len()); assert_eq!("GPL-3+", licenses[0].license.name().unwrap()); assert_eq!( "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.", licenses[0].license.text().unwrap() ); let upstream_files = copyright.find_files(std::path::Path::new("foo.c")).unwrap(); assert_eq!(vec!["*"], upstream_files.files); let debian_files = copyright .find_files(std::path::Path::new("debian/foo.c")) .unwrap(); assert_eq!(vec!["debian/*"], debian_files.files); let gpl = copyright.find_license_by_name("GPL-3+"); assert!(gpl.is_some()); let gpl = copyright.find_license_for_file(std::path::Path::new("debian/foo.c")); assert_eq!(gpl.unwrap().name().unwrap(), "GPL-3+"); } }