use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::path::Path;
use cargo_metadata::semver::{BuildMetadata, Comparator, Op, Prerelease, Version, VersionReq};
use cargo_metadata::{Dependency, Target};
use crate::diff::DiffItem;
use crate::error::Error;
use crate::format::{make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target};
use crate::krate::Crate;
use crate::metadata::Metadata;
use crate::utils::file_mode;
use super::common::dependency_partial_eq;
pub struct CrateComparator<'a> {
old: &'a Crate,
new: &'a Crate,
}
impl<'a> CrateComparator<'a> {
pub const fn new(old: &'a Crate, new: &'a Crate) -> Self {
Self { old, new }
}
pub fn compare(&self) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_metadata = &self.old.metadata;
let new_metadata = &self.new.metadata;
let old_contents = self.old.file_contents()?;
let new_contents = self.new.file_contents()?;
items.extend(compare_metadata(old_metadata, new_metadata));
for file in &new_contents.files {
if !old_contents.files.contains(file) {
items.push(DiffItem::FileAdded {
path: file.to_string_lossy().to_string(),
});
continue;
}
items.extend(self.compare_contents(&file, &file)?);
items.extend(self.compare_modes(&file, &file)?);
}
for file in &old_contents.files {
if !new_contents.files.contains(file) {
items.push(DiffItem::FileRemoved {
path: file.to_string_lossy().to_string(),
});
}
}
Ok(items)
}
fn compare_contents<P: AsRef<Path>>(&self, old_path: P, new_path: P) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_file_content = self.old.read_entry_to_bytes(&old_path)?;
let new_file_content = self.new.read_entry_to_bytes(&new_path)?;
if old_file_content != new_file_content {
let old_fc_lf: Vec<_> = old_file_content.iter().filter(|c| **c != b'\r').collect();
let new_fc_lf: Vec<_> = new_file_content.iter().filter(|c| **c != b'\r').collect();
if old_fc_lf == new_fc_lf {
items.push(DiffItem::LineEndingsChange {
path: old_path.as_ref().to_string_lossy().to_string(),
});
} else {
let diff = if let (Ok(old_file_utf8), Ok(new_file_utf8)) = (
self.old.read_entry_to_string(&old_path),
self.new.read_entry_to_string(&new_path),
) {
Some(make_diff(
&old_file_utf8,
&new_file_utf8,
Some((
&format!("old/{}", old_path.as_ref().to_string_lossy()),
&format!("new/{}", new_path.as_ref().to_string_lossy()),
)),
))
} else {
None
};
items.push(DiffItem::FileChanged {
path: old_path.as_ref().to_string_lossy().to_string(),
diff,
});
}
}
Ok(items)
}
fn compare_modes<P: AsRef<Path>>(&self, old_path: P, new_path: P) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_file_mode = file_mode(self.old.root.join(&old_path))?;
let new_file_mode = file_mode(self.new.root.join(&new_path))?;
if old_file_mode != new_file_mode {
items.push(DiffItem::PermissionChange {
path: old_path.as_ref().to_string_lossy().to_string(),
old: old_file_mode,
new: new_file_mode,
});
}
Ok(items)
}
}
#[allow(clippy::too_many_lines)]
fn compare_metadata(old_metadata: &Metadata, new_metadata: &Metadata) -> Vec<DiffItem> {
let mut items = Vec::new();
let old_md = &old_metadata.inner;
let new_md = &new_metadata.inner;
if old_md.name != new_md.name {
items.push(DiffItem::NameChange {
old: old_md.name.to_string(),
new: new_md.name.to_string(),
});
}
if old_md.version != new_md.version {
items.push(DiffItem::VersionChange {
old: old_md.version.to_string(),
new: new_md.version.to_string(),
});
}
if old_md.edition != new_md.edition {
items.push(DiffItem::EditionChange {
old: old_md.edition.to_string(),
new: new_md.edition.to_string(),
});
}
if old_md.rust_version != new_md.rust_version {
items.push(DiffItem::RustVersionChange {
old: old_md.rust_version.as_ref().map(ToString::to_string),
new: new_md.rust_version.as_ref().map(ToString::to_string),
});
}
if old_md.authors != new_md.authors {
let (added, removed) = compare_str_list(&old_md.authors, &new_md.authors);
items.push(DiffItem::AuthorsChange { added, removed });
}
if old_md.description != new_md.description {
items.push(DiffItem::DescriptionChange {
old: old_md.description.clone(),
new: new_md.description.clone(),
});
}
if old_md.license != new_md.license {
items.push(DiffItem::LicenseChange {
old: old_md.license.clone(),
new: new_md.license.clone(),
});
}
if old_md.license_file != new_md.license_file {
items.push(DiffItem::LicenseFileChange {
old: old_md.license_file.as_ref().map(ToString::to_string),
new: new_md.license_file.as_ref().map(ToString::to_string),
});
}
if old_md.readme != new_md.readme {
items.push(DiffItem::ReadmeChange {
old: old_md.readme.as_ref().map(ToString::to_string),
new: new_md.readme.as_ref().map(ToString::to_string),
});
}
if old_md.categories != new_md.categories {
let (added, removed) = compare_str_list(&old_md.categories, &new_md.categories);
items.push(DiffItem::CategoriesChange { added, removed });
}
if old_md.keywords != new_md.keywords {
let (added, removed) = compare_str_list(&old_md.keywords, &new_md.keywords);
items.push(DiffItem::KeywordsChange { added, removed });
}
if old_md.repository != new_md.repository {
items.push(DiffItem::RepositoryChange {
old: old_md.repository.clone(),
new: new_md.repository.clone(),
});
}
if old_md.homepage != new_md.homepage {
items.push(DiffItem::HomepageChange {
old: old_md.homepage.clone(),
new: new_md.homepage.clone(),
});
}
if old_md.documentation != new_md.documentation {
items.push(DiffItem::DocumentationChange {
old: old_md.documentation.clone(),
new: new_md.documentation.clone(),
});
}
if old_md.links != new_md.links {
items.push(DiffItem::LinksChange {
old: old_md.links.clone(),
new: new_md.links.clone(),
});
}
if old_md.default_run != new_md.default_run {
items.push(DiffItem::DefaultRunChange {
old: old_md.default_run.clone(),
new: new_md.default_run.clone(),
});
}
if old_md.metadata != new_md.metadata {
items.push(DiffItem::MetadataChange {
old: old_md.metadata.clone(),
new: new_md.metadata.clone(),
});
}
items.extend(compare_dependencies(&old_md.dependencies, &new_md.dependencies));
items.extend(compare_targets(&old_md.targets, &new_md.targets));
items.extend(compare_features(&old_md.features, &new_md.features));
items
}
fn compare_dependencies(old_deps: &[Dependency], new_deps: &[Dependency]) -> Vec<DiffItem> {
if old_deps.len() == new_deps.len() && old_deps.iter().zip(new_deps).all(|(k, u)| dependency_partial_eq(k, u)) {
return Vec::new();
}
let mut items = Vec::new();
let old_deps_map: BTreeMap<String, &Dependency> = old_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();
let new_deps_map: BTreeMap<String, &Dependency> = new_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();
for (path, old_dep) in &old_deps_map {
if let Some(new_dep) = new_deps_map.get(path) {
let (lower_bound, upper_bound) = compare_version_req(&old_dep.req, &new_dep.req);
if lower_bound == Ordering::Greater || upper_bound == Ordering::Greater {
items.push(DiffItem::DependencyUpgraded {
path: path_from_dep(new_dep),
old: old_dep.req.to_string(),
new: new_dep.req.to_string(),
});
}
if lower_bound == Ordering::Less || upper_bound == Ordering::Less {
items.push(DiffItem::DependencyDowngraded {
path: path_from_dep(new_dep),
old: old_dep.req.to_string(),
new: new_dep.req.to_string(),
});
}
let (added_features, removed_features) = compare_str_list(&old_dep.features, &new_dep.features);
if !added_features.is_empty() || !removed_features.is_empty() {
items.push(DiffItem::DependencyFeatures {
path: path_from_dep(new_dep),
added: added_features,
removed: removed_features,
});
}
if old_dep.optional != new_dep.optional {
items.push(DiffItem::DependencyOptionality {
path: path_from_dep(new_dep),
old: old_dep.optional,
new: new_dep.optional,
});
}
} else {
items.push(DiffItem::DependencyRemoved {
path: path_from_dep(old_dep),
value: value_from_dep(old_dep),
});
}
}
for (path, new_dep) in &new_deps_map {
if !old_deps_map.contains_key(path) {
items.push(DiffItem::DependencyAdded {
path: path_from_dep(new_dep),
value: value_from_dep(new_dep),
});
}
}
items
}
fn compare_targets(old_targets: &[Target], new_targets: &[Target]) -> Vec<DiffItem> {
if old_targets == new_targets {
return Vec::new();
}
let mut items = Vec::new();
for old_target in old_targets {
if new_targets.contains(old_target) {
continue;
}
let path = path_from_target(old_target);
items.push(DiffItem::TargetRemoved {
path,
target: value_from_target(old_target),
});
}
for new_target in new_targets {
if old_targets.contains(new_target) {
continue;
}
let path = path_from_target(new_target);
items.push(DiffItem::TargetAdded {
path,
target: value_from_target(new_target),
});
}
items
}
type Features = BTreeMap<String, Vec<String>>;
fn compare_features(old_features: &Features, new_features: &Features) -> Vec<DiffItem> {
if old_features == new_features {
return Vec::new();
}
let mut items = Vec::new();
for (old_key, old_values) in old_features {
match new_features.get(old_key) {
Some(new_values) => {
if old_values == new_values {
continue;
}
let (added, removed) = compare_str_list(old_values, new_values);
items.push(DiffItem::FeatureChanged {
name: String::from(old_key),
added,
removed,
});
},
None => {
items.push(DiffItem::FeatureRemoved {
name: String::from(old_key),
});
},
}
}
for new_key in new_features.keys() {
if old_features.get(new_key).is_none() {
items.push(DiffItem::FeatureAdded {
name: String::from(new_key),
});
}
}
items
}
fn compare_str_list(old: &[String], new: &[String]) -> (Vec<String>, Vec<String>) {
let mut added = Vec::new();
let mut removed = Vec::new();
for new_str in new {
if !old.contains(new_str) {
added.push(new_str.clone());
}
}
for old_str in old {
if !new.contains(old_str) {
removed.push(old_str.clone());
}
}
(added, removed)
}
fn compare_version_req(old: &VersionReq, new: &VersionReq) -> (Ordering, Ordering) {
const fn version(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Version {
Version {
major,
minor,
patch,
pre,
build: BuildMetadata::EMPTY,
}
}
fn lower_bound(c: &Comparator) -> Option<Version> {
match c.op {
Op::Exact | Op::GreaterEq => Some(version(
c.major,
c.minor.unwrap_or(0),
c.patch.unwrap_or(0),
c.pre.clone(),
)),
Op::Less => None,
_ => unreachable!(),
}
}
fn upper_bound(c: &Comparator) -> Option<Version> {
match c.op {
Op::Exact | Op::Less => Some(version(
c.major,
c.minor.unwrap_or(0),
c.patch.unwrap_or(0),
c.pre.clone(),
)),
Op::GreaterEq => None,
_ => unreachable!(),
}
}
if old == new {
return (Ordering::Equal, Ordering::Equal);
}
let old_comps: Vec<_> = old.comparators.iter().flat_map(normalize_version_req).collect();
let new_comps: Vec<_> = new.comparators.iter().flat_map(normalize_version_req).collect();
let old_lower_bound = old_comps.iter().filter_map(lower_bound).max();
let new_lower_bound = new_comps.iter().filter_map(lower_bound).max();
let old_upper_bound = new_comps.iter().filter_map(upper_bound).min();
let new_upper_bound = old_comps.iter().filter_map(upper_bound).min();
let lower_bound_cmp = match (old_lower_bound, new_lower_bound) {
(None, None) => Ordering::Equal,
(Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
};
let upper_bound_cmp = match (old_upper_bound, new_upper_bound) {
(None, None) => Ordering::Equal,
(Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
};
(lower_bound_cmp, upper_bound_cmp)
}
#[allow(clippy::too_many_lines)]
fn normalize_version_req(comp: &Comparator) -> Vec<Comparator> {
const fn comparator(op: Op, major: u64, minor: Option<u64>, patch: Option<u64>, pre: Prerelease) -> Comparator {
Comparator {
op,
major,
minor,
patch,
pre,
}
}
let mut result = Vec::with_capacity(2);
match comp.op {
Op::Exact => match (comp.minor, comp.patch) {
(None, None) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(0),
Some(0),
Prerelease::EMPTY,
));
result.push(comparator(
Op::Less,
comp.major + 1,
Some(0),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), None) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor),
Some(0),
Prerelease::EMPTY,
));
result.push(comparator(
Op::Less,
comp.major,
Some(minor + 1),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::Exact,
comp.major,
Some(minor),
Some(patch),
comp.pre.clone(),
));
},
(None, Some(_)) => unreachable!(),
},
Op::Greater => match (comp.minor, comp.patch) {
(None, None) => {
result.push(comparator(
Op::GreaterEq,
comp.major + 1,
Some(0),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), None) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor + 1),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor),
Some(patch + 1),
Prerelease::EMPTY,
));
},
(None, Some(_)) => unreachable!(),
},
Op::GreaterEq => match (comp.minor, comp.patch) {
(None, None) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(0),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), None) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor),
Some(patch),
comp.pre.clone(),
));
},
(None, Some(_)) => unreachable!(),
},
Op::Less => match (comp.minor, comp.patch) {
(None, None) => {
result.push(comparator(Op::Less, comp.major, Some(0), Some(0), Prerelease::EMPTY));
},
(Some(minor), None) => {
result.push(comparator(
Op::Less,
comp.major,
Some(minor),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::Less,
comp.major,
Some(minor),
Some(patch),
comp.pre.clone(),
));
},
(None, Some(_)) => unreachable!(),
},
Op::LessEq => match (comp.minor, comp.patch) {
(None, None) => {
result.push(comparator(
Op::Less,
comp.major + 1,
Some(0),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), None) => {
result.push(comparator(
Op::Less,
comp.major,
Some(minor + 1),
Some(0),
Prerelease::EMPTY,
));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::Less,
comp.major,
Some(minor),
Some(patch + 1),
Prerelease::EMPTY,
));
},
(None, Some(_)) => unreachable!(),
},
Op::Tilde => match (comp.minor, comp.patch) {
(None, None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
comp.major,
None,
None,
Prerelease::EMPTY,
)));
},
(Some(minor), None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
comp.major,
Some(minor),
None,
Prerelease::EMPTY,
)));
},
(Some(minor), Some(patch)) => {
result.push(comparator(
Op::GreaterEq,
comp.major,
Some(minor),
Some(patch),
comp.pre.clone(),
));
result.push(comparator(
Op::Less,
comp.major,
Some(minor + 1),
Some(0),
Prerelease::EMPTY,
));
},
(None, Some(_)) => unreachable!(),
},
Op::Caret => match (comp.major, comp.minor, comp.patch) {
(major, None, None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
major,
None,
None,
Prerelease::EMPTY,
)));
},
(0, Some(0), None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
0,
Some(0),
None,
Prerelease::EMPTY,
)));
},
(major, Some(minor), None) => {
result.extend(normalize_version_req(&comparator(
Op::Caret,
major,
Some(minor),
Some(0),
Prerelease::EMPTY,
)));
},
(0, Some(0), Some(patch)) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
0,
Some(0),
Some(patch),
comp.pre.clone(),
)));
},
(0, Some(minor), Some(patch)) => {
result.push(comparator(Op::GreaterEq, 0, Some(minor), Some(patch), comp.pre.clone()));
result.push(comparator(Op::Less, 0, Some(minor + 1), Some(0), Prerelease::EMPTY));
},
(major, Some(minor), Some(patch)) => {
result.push(comparator(
Op::GreaterEq,
major,
Some(minor),
Some(patch),
comp.pre.clone(),
));
result.push(comparator(Op::Less, major + 1, Some(0), Some(0), Prerelease::EMPTY));
},
(_, None, Some(_)) => unreachable!(),
},
Op::Wildcard => match (comp.minor, comp.patch) {
(None, None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
comp.major,
None,
None,
Prerelease::EMPTY,
)));
},
(Some(minor), None) => {
result.extend(normalize_version_req(&comparator(
Op::Exact,
comp.major,
Some(minor),
None,
Prerelease::EMPTY,
)));
},
(Some(_) | None, Some(_)) => unreachable!(),
},
_ => unimplemented!(),
}
result
}