debian-copyright-0.1.13/.cargo_vcs_info.json0000644000000001560000000000100143500ustar { "git": { "sha1": "de8ee9189fd3aaeda9b4d3f41219c2425a79c6e4" }, "path_in_vcs": "debian-copyright" }debian-copyright-0.1.13/Cargo.lock0000644000000124630000000000100123270ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "deb822-lossless" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9086252eef3fadf8ba2090c0dff8da6ff7bbfd04baf15b094f55cc12f1c9fc5" dependencies = [ "regex", "rowan", "serde", ] [[package]] name = "debian-copyright" version = "0.1.13" dependencies = [ "deb822-lossless", "debversion", "regex", ] [[package]] name = "debversion" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b792f1c685a4959fb027386b96e27733a65d5c36bea1f880541a917223074b30" dependencies = [ "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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" 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 = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rowan" version = "0.15.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" dependencies = [ "countme", "hashbrown", "memoffset", "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.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" 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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" debian-copyright-0.1.13/Cargo.toml0000644000000016500000000000100123460ustar # 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.13" authors = ["Jelmer Vernooij "] description = "A parser for Debian copyright files" homepage = "https://github.com/jelmer/deb822-lossless" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/jelmer/deb822-lossless" [dependencies.deb822-lossless] version = ">=0.1.10" [dependencies.debversion] version = ">=0.3" [dependencies.regex] version = ">=1.10" debian-copyright-0.1.13/Cargo.toml.orig000064400000000000000000000006011046102023000160220ustar 00000000000000[package] name = "debian-copyright" authors = ["Jelmer Vernooij "] version = { workspace = true } edition = "2021" license = "Apache-2.0" description = "A parser for Debian copyright files" repository = { workspace = true } homepage = { workspace = true } [dependencies] debversion = ">=0.3" regex = ">=1.10" deb822-lossless = { version = ">=0.1.10", path = ".." } debian-copyright-0.1.13/README.md000064400000000000000000000043701046102023000144210ustar 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()); ``` debian-copyright-0.1.13/examples/license-for-file.rs000064400000000000000000000014701046102023000204470ustar 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.13/src/lib.rs000064400000000000000000000327001046102023000150430ustar 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::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 deb822_lossless::{Deb822, Paragraph}; use std::path::Path; pub const CURRENT_FORMAT: &str = "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"; pub const KNOWN_FORMATS: &[&str] = &[CURRENT_FORMAT]; pub enum License { Name(String), Text(String), Named(String, String), } impl License { pub fn name(&self) -> Option<&str> { match self { License::Name(name) => Some(name), License::Text(_) => None, License::Named(name, _) => Some(name), } } pub fn text(&self) -> Option<&str> { match self { License::Name(_) => None, License::Text(text) => Some(text), License::Named(_, text) => Some(text), } } } #[derive(Debug)] pub struct Copyright(Deb822); impl Copyright { pub fn new() -> Self { let mut deb822 = Deb822::new(); let mut header = deb822.add_paragraph(); header.insert("Format", CURRENT_FORMAT); Copyright(deb822) } pub fn empty() -> Self { Self(Deb822::new()) } pub fn header(&self) -> Option
{ self.0.paragraphs().next().map(Header) } pub fn iter_files(&self) -> impl Iterator { self.0 .paragraphs() .filter(|x| x.contains_key("Files")) .map(FilesParagraph) } 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() } 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()?) } 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)) } pub fn from_file_relaxed>(path: P) -> Result<(Self, Vec), Error> { let text = std::fs::read_to_string(path)?; Self::from_str_relaxed(&text) } pub fn from_file>(path: P) -> Result { let text = std::fs::read_to_string(path)?; use std::str::FromStr; Self::from_str(&text) } } #[derive(Debug)] pub enum Error { ParseError(deb822_lossless::ParseError), IoError(std::io::Error), 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()) } } 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")) } pub fn get(&self, key: &str) -> Option { self.0.get(key) } pub fn upstream_name(&self) -> Option { self.0.get("Upstream-Name") } pub fn upstream_contact(&self) -> Option { self.0.get("Upstream-Contact") } pub fn source(&self) -> Option { self.0.get("Source") } pub fn files_excluded(&self) -> Option> { self.0 .get("Files-Excluded") .map(|x| x.split('\n').map(|x| x.to_string()).collect::>()) } 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.insert("Format", format.as_str()); } } } pub struct FilesParagraph(Paragraph); impl FilesParagraph { pub fn files(&self) -> Vec { self.0 .get("Files") .unwrap() .split_whitespace() .map(|v| v.to_string()) .collect::>() } pub fn matches(&self, filename: &std::path::Path) -> bool { self.files() .iter() .any(|f| glob_to_regex(f).is_match(filename.to_str().unwrap())) } pub fn copyright(&self) -> Vec { self.0 .get("Copyright") .unwrap_or_default() .split('\n') .map(|x| x.to_string()) .collect::>() } pub fn comment(&self) -> Option { self.0.get("Comment") } 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()) } }, ) }) } } 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 { pub fn comment(&self) -> Option { self.0.get("Comment") } pub fn name(&self) -> Option { self.0 .get("License") .and_then(|x| x.split_once('\n').map(|(name, _)| name.to_string())) } pub fn text(&self) -> Option { self.0 .get("License") .and_then(|x| x.split_once('\n').map(|(_, text)| text.to_string())) } } fn glob_to_regex(glob: &str) -> regex::Regex { let mut it = glob.chars(); let mut r = String::new(); while let Some(c) = it.next() { r.push_str( match c { '*' => ".*".to_string(), '?' => ".".to_string(), '\\' => match it.next().unwrap() { '?' | '*' | '\\' => regex::escape(c.to_string().as_str()), x => { panic!("invalid escape sequence: \\{}", x); } }, c => regex::escape(c.to_string().as_str()), } .as_str(), ) } regex::Regex::new(r.as_str()).unwrap() } #[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+"); } }