use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use cargo_metadata::camino::Utf8PathBuf;
use cargo_metadata::{Dependency, Target};
use tracing::{debug, trace};
use crate::error::Error;
use crate::format::{format_list, make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target};
use crate::krate::Crate;
use crate::metadata::Metadata;
use crate::report::ReportItem;
use crate::upstream::Repository;
use crate::utils::file_mode;
use super::common::dependency_partial_eq;
pub struct RepoComparator<'a> {
krate: &'a Crate,
urepo: &'a Repository<'a>,
}
impl<'a> RepoComparator<'a> {
pub const fn new(krate: &'a Crate, urepo: &'a Repository) -> Self {
Self { krate, urepo }
}
fn path_in_krate(&self, path: &str) -> String {
let name = self.krate.metadata.inner.name.as_str();
let version = self.krate.metadata.inner.version.to_string();
format!("{name}-{version}/{path}")
}
fn path_in_urepo(&self, path: &str) -> String {
let shortref = &self.urepo.id[0..7];
let path_in_vcs = &self.urepo.path_in_vcs;
format!("git#{shortref}/{path_in_vcs}/{path}")
}
pub fn compare(&self) -> Result<Vec<ReportItem>, Error> {
let mut items = Vec::new();
let krate_cargo_toml = std::fs::read_to_string(self.krate.cargo_toml_orig())?;
let urepo_cargo_toml = std::fs::read_to_string(self.urepo.cargo_toml())?;
let krate_metadata = &self.krate.metadata;
let urepo_metadata = &self.urepo.metadata;
let krate_contents = self.krate.file_contents()?;
let urepo_contents = self.urepo.file_contents()?;
debug!("Comparing crate contents with repository contents");
items.extend(compare_metadata(krate_metadata, urepo_metadata, &krate_contents.files));
if krate_cargo_toml != urepo_cargo_toml {
let kct_lf: Vec<_> = krate_cargo_toml.bytes().filter(|c| *c != b'\r').collect();
let rct_lf: Vec<_> = urepo_cargo_toml.bytes().filter(|c| *c != b'\r').collect();
if kct_lf == rct_lf {
items.push(ReportItem::LineEndings {
path: String::from("Cargo.toml.orig"),
});
} else {
let diff = Some(make_diff(
&krate_cargo_toml,
&urepo_cargo_toml,
Some((
&self.path_in_krate("Cargo.toml.orig"),
&self.path_in_urepo("Cargo.toml"),
)),
));
items.push(ReportItem::ContentMismatch {
path: String::from("Cargo.toml.orig"),
diff,
});
}
}
for file in &krate_contents.broken_links {
items.push(ReportItem::BrokenSymlinkInCrate {
path: file.to_string_lossy().to_string(),
});
}
for file in &urepo_contents.broken_links {
items.push(ReportItem::BrokenSymlinkInRepo {
path: file.to_string_lossy().to_string(),
});
}
for file in &krate_contents.outside_base {
items.push(ReportItem::BrokenSymlinkInCrate {
path: file.to_string_lossy().to_string(),
});
}
for file in &urepo_contents.outside_base {
items.push(ReportItem::BrokenSymlinkInRepo {
path: file.to_string_lossy().to_string(),
});
}
for file in &krate_contents.files {
if !urepo_contents.files.contains(file) {
items.push(ReportItem::MissingFile {
path: file.to_string_lossy().to_string(),
});
continue;
}
trace!("Comparing files at path: {}", file.to_string_lossy());
items.extend(self.compare_contents(&file, &file)?);
items.extend(self.compare_modes(&file, &file)?);
}
Ok(items)
}
fn compare_contents<P: AsRef<Path>>(&self, krate_path: P, urepo_path: P) -> Result<Vec<ReportItem>, Error> {
let mut items = Vec::new();
let krate_file_content = self.krate.read_entry_to_bytes(&krate_path)?;
let urepo_file_content = self.urepo.read_entry_to_bytes(&urepo_path)?;
if krate_file_content != urepo_file_content {
let kfc_lf: Vec<_> = krate_file_content.iter().filter(|c| **c != b'\r').collect();
let rfc_lf: Vec<_> = urepo_file_content.iter().filter(|c| **c != b'\r').collect();
if kfc_lf == rfc_lf {
items.push(ReportItem::LineEndings {
path: krate_path.as_ref().to_string_lossy().to_string(),
});
} else {
let diff = if let (Ok(ktc), Ok(utc)) = (
self.krate.read_entry_to_string(&krate_path),
self.urepo.read_entry_to_string(&urepo_path),
) {
Some(make_diff(
&ktc,
&utc,
Some((
&self.path_in_krate(&krate_path.as_ref().to_string_lossy()),
&self.path_in_urepo(&urepo_path.as_ref().to_string_lossy()),
)),
))
} else {
None
};
items.push(ReportItem::ContentMismatch {
path: krate_path.as_ref().to_string_lossy().to_string(),
diff,
});
}
}
Ok(items)
}
fn compare_modes<P: AsRef<Path>>(&self, krate_path: P, urepo_path: P) -> Result<Vec<ReportItem>, Error> {
let mut items = Vec::new();
let krate_file_mode = file_mode(self.krate.root.join(&krate_path))?;
let urepo_file_mode = file_mode(self.urepo.root.join(&self.urepo.path_in_vcs).join(&urepo_path))?;
if krate_file_mode != urepo_file_mode {
items.push(ReportItem::Permissions {
path: krate_path.as_ref().to_string_lossy().to_string(),
krate: krate_file_mode,
urepo: urepo_file_mode,
});
}
Ok(items)
}
}
fn compare_metadata(krate_metadata: &Metadata, urepo_metadata: &Metadata, krate_files: &[PathBuf]) -> Vec<ReportItem> {
let mut items = Vec::new();
let kmd = &krate_metadata.inner;
let umd = &urepo_metadata.inner;
items.extend(compare_displayable("package.name", &kmd.name, &umd.name));
items.extend(compare_displayable("package.version", &kmd.version, &umd.version));
items.extend(compare_displayable_list("package.authors", &kmd.authors, &umd.authors));
items.extend(compare_displayable_option(
"package.description",
kmd.description.as_ref(),
umd.description.as_ref(),
));
items.extend(compare_dependencies(&kmd.dependencies, &umd.dependencies));
items.extend(compare_displayable_option(
"package.license",
kmd.license.as_ref(),
umd.license.as_ref(),
));
items.extend(compare_path_option(
"package.license-file",
kmd.license_file.as_ref(),
umd.license_file.as_ref(),
));
items.extend(compare_targets(&kmd.targets, &umd.targets, krate_files));
items.extend(compare_features(&kmd.features, &umd.features));
items.extend(compare_displayable_list(
"package.categories",
&kmd.categories,
&umd.categories,
));
items.extend(compare_displayable_list(
"package.keywords",
&kmd.keywords,
&umd.keywords,
));
items.extend(compare_path_option(
"package.readme",
kmd.readme.as_ref(),
umd.readme.as_ref(),
));
items.extend(compare_displayable_option(
"package.repository",
kmd.repository.as_ref(),
umd.repository.as_ref(),
));
items.extend(compare_displayable_option(
"package.homepage",
kmd.homepage.as_ref(),
umd.homepage.as_ref(),
));
items.extend(compare_displayable_option(
"package.documentation",
kmd.documentation.as_ref(),
umd.documentation.as_ref(),
));
items.extend(compare_displayable("package.edition", &kmd.edition, &umd.edition));
items.extend(compare_displayable("package.metadata", &kmd.metadata, &umd.metadata));
items.extend(compare_displayable_option(
"package.links",
kmd.links.as_ref(),
umd.links.as_ref(),
));
items.extend(compare_displayable_list_option(
"package.publish",
kmd.publish.as_deref(),
umd.publish.as_deref(),
));
items.extend(compare_displayable_option(
"package.default-run",
kmd.default_run.as_ref(),
umd.default_run.as_ref(),
));
items.extend(compare_displayable_option(
"package.rust-version",
kmd.rust_version.as_ref(),
umd.rust_version.as_ref(),
));
items
}
fn compare_dependencies(kdeps: &[Dependency], udeps: &[Dependency]) -> Vec<ReportItem> {
if kdeps.len() == udeps.len() && kdeps.iter().zip(udeps).all(|(k, u)| dependency_partial_eq(k, u)) {
return Vec::new();
}
let mut items = Vec::new();
for kdep in kdeps {
if udeps.iter().any(|udep| dependency_partial_eq(kdep, udep)) {
continue;
}
let path = path_from_dep(kdep);
items.push(ReportItem::metadata_mismatch(path, Some(value_from_dep(kdep)), None));
}
for udep in udeps {
if kdeps.iter().any(|kdep| dependency_partial_eq(kdep, udep)) {
continue;
}
if let Some(source) = &udep.source
&& !source.is_crates_io()
{
debug!("Skipping non-crates.io / git dependency: {}", value_from_dep(udep));
continue;
}
if udep.path.is_some() {
debug!("Skipping non-crates.io / path dependency: {}", value_from_dep(udep));
continue;
}
let path = path_from_dep(udep);
items.push(ReportItem::metadata_mismatch(path, None, Some(value_from_dep(udep))));
}
items
}
fn compare_targets(ktargets: &[Target], utargets: &[Target], krate_files: &[PathBuf]) -> Vec<ReportItem> {
if ktargets == utargets {
return Vec::new();
}
let mut items = Vec::new();
for ktarget in ktargets {
if utargets.contains(ktarget) {
continue;
}
let path = path_from_target(ktarget);
items.push(ReportItem::metadata_mismatch(
path,
Some(value_from_target(ktarget)),
None,
));
}
for utarget in utargets {
if ktargets.contains(utarget) {
continue;
}
if !krate_files.contains(&PathBuf::from(&utarget.src_path)) {
continue;
}
let path = path_from_target(utarget);
items.push(ReportItem::metadata_mismatch(
path,
None,
Some(value_from_target(utarget)),
));
}
items
}
type Features = BTreeMap<String, Vec<String>>;
fn compare_features(kfeatures: &Features, ufeatures: &Features) -> Vec<ReportItem> {
if kfeatures == ufeatures {
return Vec::new();
}
let mut items = Vec::new();
for (kkey, kvalues) in kfeatures {
match ufeatures.get(kkey) {
Some(uvalues) => {
if kvalues == uvalues {
continue;
}
items.push(ReportItem::metadata_mismatch(
format!("features.{kkey}"),
Some(format_list(kvalues)),
Some(format_list(uvalues)),
));
},
None => {
items.push(ReportItem::metadata_mismatch(
format!("features.{kkey}"),
Some(format_list(kvalues)),
None,
));
},
}
}
for (ukey, uvalues) in ufeatures {
match kfeatures.get(ukey) {
Some(kvalues) => {
if kvalues == uvalues {
continue;
}
items.push(ReportItem::metadata_mismatch(
format!("features.{ukey}"),
Some(format_list(kvalues)),
Some(format_list(uvalues)),
));
},
None => {
items.push(ReportItem::metadata_mismatch(
format!("features.{ukey}"),
None,
Some(format_list(uvalues)),
));
},
}
}
items
}
fn compare_displayable<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
field: S,
kitem: &D,
uitem: &D,
) -> Option<ReportItem> {
if kitem == uitem {
None
} else {
Some(ReportItem::metadata_mismatch(
field,
Some(kitem.to_string()),
Some(uitem.to_string()),
))
}
}
fn compare_path_option<S: Into<Cow<'static, str>>>(
field: S,
kitem: Option<&Utf8PathBuf>,
uitem: Option<&Utf8PathBuf>,
) -> Option<ReportItem> {
if kitem == uitem {
None
} else {
Some(ReportItem::metadata_mismatch(
field,
kitem.map(ToString::to_string),
uitem.map(ToString::to_string),
))
}
}
fn compare_displayable_option<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
field: S,
kitem: Option<&D>,
uitem: Option<&D>,
) -> Option<ReportItem> {
if kitem == uitem {
None
} else {
Some(ReportItem::metadata_mismatch(
field,
kitem.map(ToString::to_string),
uitem.map(ToString::to_string),
))
}
}
fn compare_displayable_list<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
field: S,
kitems: &[D],
uitems: &[D],
) -> Option<ReportItem> {
if kitems == uitems {
None
} else {
Some(ReportItem::metadata_mismatch(
field,
Some(format_list(kitems)),
Some(format_list(uitems)),
))
}
}
fn compare_displayable_list_option<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
field: S,
kitems: Option<&[D]>,
uitems: Option<&[D]>,
) -> Option<ReportItem> {
if kitems == uitems {
None
} else {
Some(ReportItem::metadata_mismatch(
field,
kitems.map(format_list),
uitems.map(format_list),
))
}
}