use cargo::core::manifest;
use chrono::{DateTime, Datelike};
use git2::Repository;
use regex;
use tempfile;
use textwrap::fill;
use walkdir;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::fs;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use crate::errors::Result;
const DEB_COPYRIGHT_FORMAT: &str = "\
https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/";
macro_rules! format_para {
($fmt: expr, $text:expr) => {{
for line in $text.lines() {
let line = line.trim_end();
if line.is_empty() {
writeln!($fmt, " .")?;
} else {
writeln!($fmt, " {}", line)?;
}
}
write!($fmt, "")
}};
}
struct UpstreamInfo {
name: String,
contacts: Vec<String>,
source: String,
}
#[derive(Clone)]
pub struct Files {
files: String,
copyright: Vec<String>,
license: String,
comment: String,
}
#[derive(Clone)]
struct License {
name: String,
text: String,
}
pub struct DebCopyright {
format: String,
upstream: UpstreamInfo,
files: Vec<Files>,
licenses: Vec<License>,
exclusions: Vec<String>,
}
impl fmt::Display for DebCopyright {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Format: {}", self.format)?;
write!(f, "{}", self.upstream)?;
if !self.exclusions.is_empty() {
write!(f, "Files-Excluded:")?;
for excluded in &self.exclusions {
write!(f, "\n {excluded}")?;
}
writeln!(f)?;
}
for file in &self.files {
write!(f, "\n{file}")?;
}
for license in &self.licenses {
write!(f, "\n{license}")?;
}
Ok(())
}
}
impl DebCopyright {
fn new(u: UpstreamInfo, f: &[Files], l: &[License], e: &[String]) -> DebCopyright {
DebCopyright {
format: DEB_COPYRIGHT_FORMAT.to_string(),
upstream: u,
files: f.to_vec(),
licenses: l.to_vec(),
exclusions: e.to_vec(),
}
}
}
impl fmt::Display for UpstreamInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Upstream-Name: {}", self.name)?;
write!(f, "Upstream-Contact:")?;
if self.contacts.len() > 1 {
writeln!(f)?;
}
for contact in &self.contacts {
writeln!(f, " {contact}")?;
}
if !self.source.is_empty() {
writeln!(f, "Source: {}", self.source)?;
}
Ok(())
}
}
impl UpstreamInfo {
fn new(name: String, authors: &[String], repo: &str) -> UpstreamInfo {
assert!(!authors.is_empty());
UpstreamInfo {
name,
contacts: authors.to_vec(),
source: repo.to_string(),
}
}
}
impl fmt::Display for Files {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Files: {}", self.files)?;
write!(f, "Copyright:")?;
if self.copyright.len() > 1 {
writeln!(f)?;
}
for copyright in &self.copyright {
writeln!(f, " {copyright}")?;
}
writeln!(f, "License: {}", self.license)?;
if !self.comment.is_empty() {
writeln!(f, "Comment:")?;
format_para!(f, &self.comment)?;
}
Ok(())
}
}
impl Files {
pub fn new<T: ToString>(name: &str, notice: &[T], license: &str, comment: &str) -> Files {
assert!(!notice.is_empty());
Files {
files: name.to_string(),
copyright: notice.iter().map(ToString::to_string).collect(),
license: license.to_string(),
comment: comment.to_string(),
}
}
pub fn files(&self) -> &str {
&self.files
}
pub fn license(&self) -> &str {
&self.license
}
}
impl fmt::Display for License {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "License: {}", self.name)?;
format_para!(f, &self.text)?;
Ok(())
}
}
impl License {
fn new(name: String, text: String) -> License {
License { name, text }
}
}
macro_rules! default_files {
($file:expr, $notice:expr) => {{
let comment = concat!(
"FIXME (overlay): These notices are extracted from files. Please ",
"review them before uploading to the archive."
);
Files::new(
$file,
$notice,
"UNKNOWN-LICENSE; FIXME (overlay)",
&fill(comment, 79),
)
}};
}
fn gen_files(debsrcdir: &Path) -> Result<Vec<Files>> {
let mut copyright_notices = BTreeMap::new();
let copyright_notice_re =
regex::Regex::new(r"(?:[Cc]opyright|©)(?:\s|[©:,()Cc<])*\b(\d{4}\b.*)$")?;
let current_dir = env::current_dir()?;
env::set_current_dir(debsrcdir)?;
for entry in walkdir::WalkDir::new(".").sort_by(|a, b| a.file_name().cmp(b.file_name())) {
let entry = entry?;
if entry.file_type().is_file() {
let copyright_file = entry
.path()
.strip_prefix("./")?
.to_str()
.unwrap()
.to_string();
let file = fs::File::open(entry.path())?;
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(line) = line {
if let Some(m) = copyright_notice_re.captures(&line) {
let m = m.get(1).unwrap();
let start = m.start();
let end = m.end();
let notice = line[start..end]
.trim_end()
.trim_end_matches(". See the COPYRIGHT")
.to_string();
if !copyright_notices.contains_key(©right_file) {
copyright_notices.insert(copyright_file.clone(), vec![]);
}
copyright_notices
.get_mut(©right_file)
.unwrap()
.push(notice);
}
} else {
break;
}
}
}
}
env::set_current_dir(current_dir.as_path())?;
let mut notices: Vec<Files> = Vec::new();
for (filename, notice) in ©right_notices {
notices.push(default_files!(filename, notice));
}
Ok(notices)
}
fn get_licenses(license: &str) -> Result<Vec<License>> {
let mut licenses = BTreeMap::new();
let sep = regex::Regex::new(r"(?i:(\s(or|and)\s|/))")?;
let known_licenses = vec![
("agpl-3.0", include_str!("licenses/AGPL-3.0")),
("apache-2.0", include_str!("licenses/Apache-2.0")),
("bsd-2-clause", include_str!("licenses/BSD-2-Clause")),
("bsd-3-clause", include_str!("licenses/BSD-3-Clause")),
("cc0-1.0", include_str!("licenses/CC0-1.0")),
("gpl-2.0", include_str!("licenses/GPL-2.0")),
("gpl-3.0", include_str!("licenses/GPL-3.0")),
("isc", include_str!("licenses/ISC")),
("lgpl-2.0", include_str!("licenses/LGPL-2.0")),
("lgpl-2.1", include_str!("licenses/LGPL-2.1")),
("lgpl-3.0", include_str!("licenses/LGPL-3.0")),
("mit", include_str!("licenses/MIT")),
("mitnfa", include_str!("licenses/MITNFA")),
("mpl-1.1", include_str!("licenses/MPL-1.1")),
("mpl-2.0", include_str!("licenses/MPL-2.0")),
("unlicense", include_str!("licenses/Unlicense")),
("zlib", include_str!("licenses/Zlib")),
]
.into_iter()
.collect::<BTreeMap<_, _>>();
let lses: Vec<&str> = sep.split(license).filter(|s| !s.is_empty()).collect();
for ls in lses {
let lname = ls
.trim()
.to_lowercase()
.trim_end_matches('+')
.trim_end_matches("-or-later")
.to_string();
let text = match known_licenses.get(lname.as_str()) {
Some(s) => s,
None => {
"FIXME (overlay): Unrecognized crate license, please find the \
full license text in the rest of the crate source code and \
copy-paste it here"
}
};
licenses.insert(ls.trim().to_string(), text.to_string());
}
let mut lblocks: Vec<License> = Vec::new();
if !licenses.is_empty() {
for (l, t) in licenses {
lblocks.push(License::new(l, t));
}
}
Ok(lblocks)
}
fn copyright_fromgit(repo_url: &str) -> Result<String> {
let tempdir = tempfile::Builder::new()
.prefix("debcargo")
.tempdir_in(".")?;
let repo = Repository::clone(repo_url, tempdir.path())?;
let mut revwalker = repo.revwalk()?;
revwalker.push_head()?;
let latest_id = revwalker.next().unwrap()?;
let first_id = revwalker.last().unwrap()?;
let first_commit = repo.find_commit(first_id)?;
let latest_commit = repo.find_commit(latest_id)?;
let first_year = &DateTime::from_timestamp(first_commit.time().seconds(), 0)
.ok_or(anyhow::Error::msg("lo"))?
.year();
let latest_year = &DateTime::from_timestamp(latest_commit.time().seconds(), 0)
.ok_or(anyhow::Error::msg("lo"))?
.year();
let notice = match first_year.cmp(latest_year) {
Ordering::Equal => format!("{first_year}"),
_ => format!("{first_year}-{latest_year},"),
};
Ok(notice)
}
#[expect(clippy::too_many_arguments)]
pub fn debian_copyright(
srcdir: &Path,
manifest: &manifest::Manifest,
manifest_path: &Path,
maintainer: &str,
uploaders: &[&str],
year_range: (i32, i32),
guess_harder: bool,
exclusions: &[String],
) -> Result<DebCopyright> {
let meta = manifest.metadata().clone();
let repository = meta.repository.unwrap_or_default();
let unknown_authors = vec!["FIXME (overlay) UNKNOWN-AUTHORS".to_string()];
let authors = if meta.authors.is_empty() {
&unknown_authors
} else {
&meta.authors
};
let upstream = UpstreamInfo::new(manifest.name().to_string(), authors, &repository);
let mut licenses: Vec<License> = Vec::new();
let mut crate_license: String = String::new();
if let Some(ref license_file_name) = meta.license_file {
let license_file = manifest_path.with_file_name(license_file_name);
let mut text = Vec::new();
licenses.reserve(1);
let stext = match fs::File::open(license_file).and_then(|mut f| f.read_to_end(&mut text)) {
Ok(_) => String::from_utf8(text)?,
Err(e) => {
format!("Failed to read license from {license_file_name} - {e:?}")
}
};
licenses.push(License::new(
"UNKNOWN-LICENSE; FIXME (overlay)".to_string(),
stext,
));
} else if let Some(ref license) = meta.license {
licenses = get_licenses(license)?;
crate_license = license
.trim()
.replace('/', " or ")
.replace(" OR ", " or ")
.replace(" AND ", " and ");
} else {
debcargo_bail!("Crate has no license or license_file");
}
let mut files = gen_files(srcdir)?;
let (y0, y1) = year_range;
let years = if y0 == y1 {
format!("{y0}")
} else {
format!("{y0}-{y1}")
};
let mut deb_notice = vec![format!("{} {}", years, maintainer)];
deb_notice.extend(uploaders.iter().map(|s| format!("{years} {s}")));
files.push(Files::new("debian/*", &deb_notice, &crate_license, ""));
let years = if guess_harder && !repository.is_empty() {
match copyright_fromgit(&repository) {
Ok(x) => x,
Err(e) => {
debcargo_warn!(
"Failed to generate d/copyright from git repository {}: {}\n",
repository,
e
);
"FIXME (overlay) UNKNOWN-YEARS".to_string()
}
}
} else {
"FIXME (overlay) UNKNOWN-YEARS".to_string()
};
let notice = match meta.authors.len() {
0 => vec![format!("FIXME (overlay) UNKNOWN-AUTHORS {}", years)],
1 => vec![format!("{} {}", years, &meta.authors[0])],
_ => meta
.authors
.iter()
.map(|s| format!("{years} {s}"))
.collect(),
};
let comment = concat!(
"FIXME (overlay): Since upstream copyright years are not available ",
"in Cargo.toml, they were extracted from the upstream Git ",
"repository. This may not be correct information so you should ",
"review and fix this before uploading to the archive."
);
files.insert(
0,
Files::new("*", notice.as_slice(), &crate_license, &fill(comment, 79)),
);
Ok(DebCopyright::new(upstream, &files, &licenses, exclusions))
}
#[cfg(test)]
mod tests;