Skip to content

Commit

Permalink
Merge pull request #173 from rust-secure-code/private-cargo-metadata
Browse files Browse the repository at this point in the history
Make `cargo_metadata` private
  • Loading branch information
Shnatsel authored Nov 11, 2024
2 parents 88a2b2d + 782e673 commit 7af1021
Show file tree
Hide file tree
Showing 19 changed files with 252 additions and 574 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions auditable-serde/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,11 @@ all-features = true

[features]
default = []
from_metadata = ["cargo_metadata"]
schema = ["schemars"]

[dependencies]
serde = { version = "1", features = ["serde_derive"] }
serde_json = "1.0.57"
semver = { version = "1.0", features = ["serde"] }
cargo_metadata = { version = "0.18", optional = true }
topological-sort = "0.2.2"
schemars = {version = "0.8.10", optional = true }

[[example]]
name = "from-metadata"
required-features = ["from_metadata"]
2 changes: 0 additions & 2 deletions auditable-serde/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ Parses and serializes the JSON dependency tree embedded in executables by the

This crate defines the data structures that a serialized to/from JSON
and implements the serialization/deserialization routines via `serde`.
It also provides optional conversions from [`cargo metadata`](https://docs.rs/cargo_metadata/)
and to [`Cargo.lock`](https://docs.rs/cargo-lock) formats.

The [`VersionInfo`] struct is where all the magic happens, see the docs on it for more info.

Expand Down
29 changes: 0 additions & 29 deletions auditable-serde/examples/from-metadata.rs

This file was deleted.

257 changes: 0 additions & 257 deletions auditable-serde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ use validation::RawVersionInfo;

use serde::{Deserialize, Serialize};

#[cfg(feature = "from_metadata")]
use std::convert::TryFrom;
use std::str::FromStr;
#[cfg(feature = "from_metadata")]
use std::{cmp::min, cmp::Ordering::*, collections::HashMap, error::Error, fmt::Display};

/// Dependency tree embedded in the binary.
///
Expand All @@ -35,19 +31,6 @@ use std::{cmp::min, cmp::Ordering::*, collections::HashMap, error::Error, fmt::D
///
/// If deserialization succeeds, it is guaranteed that there is only one root package,
/// and that are no cyclic dependencies.
///
/// ## Optional features
///
/// If the `from_metadata` feature is enabled, a conversion from
/// [`cargo_metadata::Metadata`](https://docs.rs/cargo_metadata/0.11.1/cargo_metadata/struct.Metadata.html)
/// is possible via the `TryFrom` trait. This is the preferred way to construct this structure.
/// An example demonstrating that can be found
/// [here](https://github.com/rust-secure-code/cargo-auditable/blob/master/auditable-serde/examples/from-metadata.rs).
///
/// If the `toml` feature is enabled, a conversion into the [`cargo_lock::Lockfile`](https://docs.rs/cargo-lock/)
/// struct is possible via the `TryFrom` trait. This can be useful if you need to interoperate with tooling
/// that consumes the `Cargo.lock` file format. An example demonstrating it can be found
/// [here](https://github.com/rust-secure-code/cargo-auditable/blob/master/auditable-serde/examples/json-to-toml.rs).
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[serde(try_from = "RawVersionInfo")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
Expand Down Expand Up @@ -123,21 +106,6 @@ impl From<Source> for String {
}
}

#[cfg(feature = "from_metadata")]
impl From<&cargo_metadata::Source> for Source {
fn from(meta_source: &cargo_metadata::Source) -> Self {
match meta_source.repr.as_str() {
"registry+https://github.com/rust-lang/crates.io-index" => Source::CratesIo,
source => Source::from(
source
.split('+')
.next()
.expect("Encoding of source strings in `cargo metadata` has changed!"),
),
}
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum DependencyKind {
Expand All @@ -149,28 +117,6 @@ pub enum DependencyKind {
Runtime,
}

/// The values are ordered from weakest to strongest so that casting to integer would make sense
#[cfg(feature = "from_metadata")]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum PrivateDepKind {
Development,
Build,
Runtime,
}

#[cfg(feature = "from_metadata")]
impl From<PrivateDepKind> for DependencyKind {
fn from(priv_kind: PrivateDepKind) -> Self {
match priv_kind {
PrivateDepKind::Development => {
panic!("Cannot convert development dependency to serializable format")
}
PrivateDepKind::Build => DependencyKind::Build,
PrivateDepKind::Runtime => DependencyKind::Runtime,
}
}
}

fn is_default<T: Default + PartialEq>(value: &T) -> bool {
let default_value = T::default();
value == &default_value
Expand All @@ -183,191 +129,6 @@ impl FromStr for VersionInfo {
}
}

#[cfg(feature = "from_metadata")]
impl From<&cargo_metadata::DependencyKind> for PrivateDepKind {
fn from(kind: &cargo_metadata::DependencyKind) -> Self {
match kind {
cargo_metadata::DependencyKind::Normal => PrivateDepKind::Runtime,
cargo_metadata::DependencyKind::Development => PrivateDepKind::Development,
cargo_metadata::DependencyKind::Build => PrivateDepKind::Build,
_ => panic!("Unknown dependency kind"),
}
}
}

/// Error returned by the conversion from
/// [`cargo_metadata::Metadata`](https://docs.rs/cargo_metadata/0.11.1/cargo_metadata/struct.Metadata.html)
#[cfg(feature = "from_metadata")]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum InsufficientMetadata {
NoDeps,
VirtualWorkspace,
}

#[cfg(feature = "from_metadata")]
impl Display for InsufficientMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InsufficientMetadata::NoDeps => {
write!(f, "Missing dependency information! Please call 'cargo metadata' without '--no-deps' flag.")
}
InsufficientMetadata::VirtualWorkspace => {
write!(f, "Missing root crate! Please call this from a package directory, not workspace root.")
}
}
}
}

#[cfg(feature = "from_metadata")]
impl Error for InsufficientMetadata {}

#[cfg(feature = "from_metadata")]
impl TryFrom<&cargo_metadata::Metadata> for VersionInfo {
type Error = InsufficientMetadata;
fn try_from(metadata: &cargo_metadata::Metadata) -> Result<Self, Self::Error> {
let toplevel_crate_id = metadata
.resolve
.as_ref()
.ok_or(InsufficientMetadata::NoDeps)?
.root
.as_ref()
.ok_or(InsufficientMetadata::VirtualWorkspace)?
.repr
.as_str();

// Walk the dependency tree and resolve dependency kinds for each package.
// We need this because there may be several different paths to the same package
// and we need to aggregate dependency types across all of them.
// Moreover, `cargo metadata` doesn't propagate dependency information:
// A runtime dependency of a build dependency of your package should be recorded
// as *build* dependency, but Cargo flags it as a runtime dependency.
// Hoo boy, here I go hand-rolling BFS again!
let nodes = &metadata.resolve.as_ref().unwrap().nodes;
let id_to_node: HashMap<&str, &cargo_metadata::Node> =
nodes.iter().map(|n| (n.id.repr.as_str(), n)).collect();
let mut id_to_dep_kind: HashMap<&str, PrivateDepKind> = HashMap::new();
id_to_dep_kind.insert(toplevel_crate_id, PrivateDepKind::Runtime);
let mut current_queue: Vec<&cargo_metadata::Node> = vec![id_to_node[toplevel_crate_id]];
let mut next_step_queue: Vec<&cargo_metadata::Node> = Vec::new();
while !current_queue.is_empty() {
for parent in current_queue.drain(..) {
let parent_dep_kind = id_to_dep_kind[parent.id.repr.as_str()];
for child in &parent.deps {
let child_id = child.pkg.repr.as_str();
let dep_kind = strongest_dep_kind(child.dep_kinds.as_slice());
let dep_kind = min(dep_kind, parent_dep_kind);
let dep_kind_on_previous_visit = id_to_dep_kind.get(child_id);
if dep_kind_on_previous_visit.is_none()
|| &dep_kind > dep_kind_on_previous_visit.unwrap()
{
// if we haven't visited this node in dependency graph yet
// or if we've visited it with a weaker dependency type,
// records its new dependency type and add it to the queue to visit its dependencies
id_to_dep_kind.insert(child_id, dep_kind);
next_step_queue.push(id_to_node[child_id]);
}
}
}
std::mem::swap(&mut next_step_queue, &mut current_queue);
}

let metadata_package_dep_kind = |p: &cargo_metadata::Package| {
let package_id = p.id.repr.as_str();
id_to_dep_kind.get(package_id)
};

// Remove dev-only dependencies from the package list and collect them to Vec
let mut packages: Vec<&cargo_metadata::Package> = metadata
.packages
.iter()
.filter(|p| {
let dep_kind = metadata_package_dep_kind(p);
// Dependencies that are present in the workspace but not used by the current root crate
// will not be in the map we've built by traversing the root crate's dependencies.
// In this case they will not be in the map at all. We skip them, along with dev-dependencies.
dep_kind.is_some() && dep_kind.unwrap() != &PrivateDepKind::Development
})
.collect();

// This function is the simplest place to introduce sorting, since
// it contains enough data to distinguish between equal-looking packages
// and provide a stable sorting that might not be possible
// using the data from VersionInfo struct alone.
//
// We use sort_unstable here because there is no point in
// not reordering equal elements, since they're supplied by
// in arbitrary order by cargo-metadata anyway
// and the order even varies between executions.
packages.sort_unstable_by(|a, b| {
// This is a workaround for Package not implementing Ord.
// Deriving it in cargo_metadata might be more reliable?
let names_order = a.name.cmp(&b.name);
if names_order != Equal {
return names_order;
}
let versions_order = a.name.cmp(&b.name);
if versions_order != Equal {
return versions_order;
}
// IDs are unique so comparing them should be sufficient
a.id.repr.cmp(&b.id.repr)
});

// Build a mapping from package ID to the index of that package in the Vec
// because serializable representation doesn't store IDs
let mut id_to_index = HashMap::new();
for (index, package) in packages.iter().enumerate() {
id_to_index.insert(package.id.repr.as_str(), index);
}

// Convert packages from cargo-metadata representation to our representation
let mut packages: Vec<Package> = packages
.into_iter()
.map(|p| Package {
name: p.name.to_owned(),
version: p.version.clone(),
source: p.source.as_ref().map_or(Source::Local, Source::from),
kind: (*metadata_package_dep_kind(p).unwrap()).into(),
dependencies: Vec::new(),
root: p.id.repr == toplevel_crate_id,
})
.collect();

// Fill in dependency info from resolved dependency graph
for node in metadata.resolve.as_ref().unwrap().nodes.iter() {
let package_id = node.id.repr.as_str();
if id_to_index.contains_key(package_id) {
// dev-dependencies are not included
let package: &mut Package = &mut packages[id_to_index[package_id]];
// Dependencies
for dep in node.deps.iter() {
// Omit the graph edge if this is a development dependency
// to fix https://github.com/rustsec/rustsec/issues/1043
// It is possible that something that we depend on normally
// is also a dev-dependency for something,
// and dev-dependencies are allowed to have cycles,
// so we may end up encoding cyclic graph if we don't handle that.
let dep_id = dep.pkg.repr.as_str();
if strongest_dep_kind(&dep.dep_kinds) != PrivateDepKind::Development {
package.dependencies.push(id_to_index[dep_id]);
}
}
// .sort_unstable() is fine because they're all integers
package.dependencies.sort_unstable();
}
}
Ok(VersionInfo { packages })
}
}

#[cfg(feature = "from_metadata")]
fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind {
deps.iter()
.map(|d| PrivateDepKind::from(&d.kind))
.max()
.unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41
}

#[cfg(test)]
mod tests {
#![allow(unused_imports)] // otherwise conditional compilation emits warnings
Expand All @@ -378,24 +139,6 @@ mod tests {
path::{Path, PathBuf},
};

#[cfg(feature = "from_metadata")]
fn load_metadata(cargo_toml_path: &Path) -> cargo_metadata::Metadata {
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.manifest_path(cargo_toml_path);
cmd.exec().unwrap()
}

#[test]
#[cfg(feature = "from_metadata")]
fn dependency_cycle() {
let cargo_toml_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/fixtures/cargo-audit-dep-cycle/Cargo.toml");
let metadata = load_metadata(&cargo_toml_path);
let version_info_struct: VersionInfo = (&metadata).try_into().unwrap();
let json = serde_json::to_string(&version_info_struct).unwrap();
VersionInfo::from_str(&json).unwrap(); // <- the part we care about succeeding
}

#[cfg(feature = "schema")]
/// Generate a JsonSchema for VersionInfo
fn generate_schema() -> schemars::schema::RootSchema {
Expand Down
24 changes: 0 additions & 24 deletions auditable-serde/tests/fixtures/cargo-audit-dep-cycle/Cargo.lock

This file was deleted.

2 changes: 1 addition & 1 deletion cargo-auditable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "../README.md"

[dependencies]
object = {version = "0.30", default-features = false, features = ["write"]}
auditable-serde = {version = "0.7.0", path = "../auditable-serde", features = ["from_metadata"]}
auditable-serde = {version = "0.7.0", path = "../auditable-serde"}
miniz_oxide = {version = "0.8.0"}
serde_json = "1.0.57"
cargo_metadata = "0.18"
Expand Down
Loading

0 comments on commit 7af1021

Please sign in to comment.