auditable-serde-0.6.0/.cargo_vcs_info.json0000644000000001550000000000100140720ustar { "git": { "sha1": "47121426a859ef14a91ba9be0bce19d4d0019faf" }, "path_in_vcs": "auditable-serde" }auditable-serde-0.6.0/CHANGELOG.md000064400000000000000000000016031046102023000144720ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.6.0] - 2023-04-27 ### Changed - `toml` feature: upgraded to `cargo-lock` crate v9.x ### Fixed - Fixed changelog formatting ## [0.5.2] - 2022-10-24 ### Changed - `toml` feature: Versions are no longer roundtripped through `&str`, resulting in faster conversion. - `toml` feature: `cargo_lock::Dependency.source` field is now populated when when converting into `cargo-lock` crate format. ### Added - This changelog file ## [0.5.1] - 2022-10-02 ### Added - JSON schema (thanks to @tofay) - A mention of the `auditable-info` crate in the crate documentation ## [0.5.0] - 2022-08-08 ### Changed - This is the first feature-complete release auditable-serde-0.6.0/Cargo.lock0000644000000205170000000000100120510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "auditable-serde" version = "0.6.0" dependencies = [ "cargo-lock", "cargo_metadata", "schemars", "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "camino" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ "serde", ] [[package]] name = "cargo-lock" version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" dependencies = [ "semver", "serde", "toml", "url", ] [[package]] name = "cargo-platform" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3abb7553d5b9b8421c6de7cb02606ff15e0c6eea7d8eadd75ef013fd636bec36" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", ] [[package]] name = "dyn-clone" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "form_urlencoded" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ "percent-encoding", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "idna" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "itoa" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "proc-macro2" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "ryu" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "schemars" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed" dependencies = [ "dyn-clone", "schemars_derive", "serde", "serde_json", ] [[package]] name = "schemars_derive" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "semver" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_derive_internals" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" dependencies = [ "serde", ] [[package]] name = "syn" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "unicode-bidi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "winnow" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] auditable-serde-0.6.0/Cargo.toml0000644000000026370000000000100120770ustar # 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 = "2018" name = "auditable-serde" version = "0.6.0" authors = ["Sergey \"Shnatsel\" Davidoff "] description = "Serialize/deserialize data encoded by `cargo auditable`" categories = ["encoding"] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" [package.metadata.docs.rs] all-features = true [[example]] name = "json-to-toml" required-features = ["toml"] [dependencies.cargo-lock] version = "9" optional = true default-features = false [dependencies.cargo_metadata] version = "0.15" optional = true [dependencies.schemars] version = "0.8.10" optional = true [dependencies.semver] version = "1.0" features = ["serde"] [dependencies.serde] version = "1" features = ["serde_derive"] [dependencies.serde_json] version = "1.0.57" [dependencies.topological-sort] version = "0.2.2" [features] default = [] from_metadata = ["cargo_metadata"] schema = ["schemars"] toml = ["cargo-lock"] auditable-serde-0.6.0/Cargo.toml.orig0000644000000015770000000000100130400ustar [package] name = "auditable-serde" version = "0.6.0" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "Serialize/deserialize data encoded by `cargo auditable`" categories = ["encoding"] edition = "2018" [package.metadata.docs.rs] all-features = true [features] default = [] from_metadata = ["cargo_metadata"] toml = ["cargo-lock"] schema = ["schemars"] [dependencies] serde = { version = "1", features = ["serde_derive"] } serde_json = "1.0.57" semver = { version = "1.0", features = ["serde"] } cargo_metadata = { version = "0.15", optional = true } cargo-lock = { version = "9", default-features = false, optional = true } topological-sort = "0.2.2" schemars = {version = "0.8.10", optional = true } [[example]] name = "json-to-toml" required-features = ["toml"] auditable-serde-0.6.0/Cargo.toml.orig000064400000000000000000000015771046102023000155620ustar 00000000000000[package] name = "auditable-serde" version = "0.6.0" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "Serialize/deserialize data encoded by `cargo auditable`" categories = ["encoding"] edition = "2018" [package.metadata.docs.rs] all-features = true [features] default = [] from_metadata = ["cargo_metadata"] toml = ["cargo-lock"] schema = ["schemars"] [dependencies] serde = { version = "1", features = ["serde_derive"] } serde_json = "1.0.57" semver = { version = "1.0", features = ["serde"] } cargo_metadata = { version = "0.15", optional = true } cargo-lock = { version = "9", default-features = false, optional = true } topological-sort = "0.2.2" schemars = {version = "0.8.10", optional = true } [[example]] name = "json-to-toml" required-features = ["toml"] auditable-serde-0.6.0/src/lib.rs000064400000000000000000000544331046102023000145750ustar 00000000000000#![forbid(unsafe_code)] #![allow(clippy::redundant_field_names)] //! Parses and serializes the JSON dependency tree embedded in executables by the //! [`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable). //! //! 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. //! //! ## Basic usage //! //! **Note:** this is a low-level crate that only implements JSON parsing. It rarely should be used directly. //! You probably want the higher-level [`auditable-info`](https://docs.rs/auditable-info) crate instead. //! //! The following snippet demonstrates full extraction pipeline, including //! platform-specific executable handling via //! [`auditable-extract`](http://docs.rs/auditable-serde/) and decompression //! using the safe-Rust [`miniz_oxide`](http://docs.rs/miniz_oxide/): //! //! ```rust,ignore //! use std::io::{Read, BufReader}; //! use std::{error::Error, fs::File, str::FromStr}; //! //! fn main() -> Result<(), Box> { //! // Read the input //! let f = File::open("target/release/hello-world")?; //! let mut f = BufReader::new(f); //! let mut input_binary = Vec::new(); //! f.read_to_end(&mut input_binary)?; //! // Extract the compressed audit data //! let compressed_audit_data = auditable_extract::raw_auditable_data(&input_binary)?; //! // Decompress it with your Zlib implementation of choice. We recommend miniz_oxide //! use miniz_oxide::inflate::decompress_to_vec_zlib; //! let decompressed_data = decompress_to_vec_zlib(&compressed_audit_data) //! .map_err(|_| "Failed to decompress audit data")?; //! let decompressed_data = String::from_utf8(decompressed_data)?; //! println!("{}", decompressed_data); //! // Parse the audit data to Rust data structures //! let dependency_tree = auditable_serde::VersionInfo::from_str(&decompressed_data); //! Ok(()) //! } //! ``` mod validation; use validation::RawVersionInfo; use serde::{Deserialize, Serialize}; #[cfg(feature = "toml")] use cargo_lock; #[cfg(any(feature = "from_metadata", feature = "toml"))] use std::convert::TryFrom; #[cfg(feature = "toml")] use std::convert::TryInto; use std::str::FromStr; #[cfg(feature = "from_metadata")] #[cfg(feature = "from_metadata")] use std::{cmp::min, cmp::Ordering::*, collections::HashMap, error::Error, fmt::Display}; /// Dependency tree embedded in the binary. /// /// Implements `Serialize` and `Deserialize` traits from `serde`, so you can use /// [all the usual methods from serde-json](https://docs.rs/serde_json/1.0.57/serde_json/#functions) /// to read and write it. /// /// `from_str()` that parses JSON is also implemented for your convenience: /// ```rust /// use auditable_serde::VersionInfo; /// use std::str::FromStr; /// let json_str = r#"{"packages":[{ /// "name":"adler", /// "version":"0.2.3", /// "source":"registry" /// }]}"#; /// let info = VersionInfo::from_str(json_str).unwrap(); /// assert_eq!(&info.packages[0].name, "adler"); /// ``` /// /// 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))] pub struct VersionInfo { pub packages: Vec, } /// A single package in the dependency tree #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Package { /// Crate name specified in the `name` field in Cargo.toml file. Examples: "libc", "rand" pub name: String, /// The package's version in the [semantic version](https://semver.org) format. #[cfg_attr(feature = "schema", schemars(with = "String"))] pub version: semver::Version, /// Currently "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc. pub source: Source, /// "build" or "runtime". May be omitted if set to "runtime". /// If it's both a build and a runtime dependency, "runtime" is recorded. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub kind: DependencyKind, /// Packages are stored in an ordered array both in the `VersionInfo` struct and in JSON. /// Here we refer to each package by its index in the array. /// May be omitted if the list is empty. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub dependencies: Vec, /// Whether this is the root package in the dependency tree. /// There should only be one root package. /// May be omitted if set to `false`. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub root: bool, } /// Serializes to "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc. #[non_exhaustive] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[serde(from = "&str")] #[serde(into = "String")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Source { CratesIo, Git, Local, Registry, Other(String), } impl From<&str> for Source { fn from(s: &str) -> Self { match s { "crates.io" => Self::CratesIo, "git" => Self::Git, "local" => Self::Local, "registry" => Self::Registry, other_str => Self::Other(other_str.to_string()), } } } impl From for String { fn from(s: Source) -> String { match s { Source::CratesIo => "crates.io".to_owned(), Source::Git => "git".to_owned(), Source::Local => "local".to_owned(), Source::Registry => "registry".to_owned(), Source::Other(string) => 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 { // The values are ordered from weakest to strongest so that casting to integer would make sense #[serde(rename = "build")] Build, #[default] #[serde(rename = "runtime")] 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 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(value: &T) -> bool { let default_value = T::default(); value == &default_value } impl FromStr for VersionInfo { type Err = serde_json::Error; fn from_str(s: &str) -> Result { serde_json::from_str(s) } } #[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 { 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 = 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.dependencies.iter() { // omit package if it is a development-only dependency let dep_id = dep.repr.as_str(); if id_to_dep_kind[dep_id] != 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(feature = "toml")] impl TryFrom<&Package> for cargo_lock::Dependency { type Error = cargo_lock::Error; fn try_from(input: &Package) -> Result { Ok(cargo_lock::Dependency { name: cargo_lock::package::Name::from_str(&input.name)?, version: input.version.clone(), source: (&input.source).into(), }) } } #[cfg(feature = "toml")] impl From<&Source> for Option { fn from(source: &Source) -> Self { match source { Source::CratesIo => Some( cargo_lock::package::SourceId::from_url( "registry+https://github.com/rust-lang/crates.io-index", ) .unwrap(), ), _ => None, // we don't store enough info about other sources to reconstruct the URL } } } #[cfg(feature = "toml")] impl TryFrom<&VersionInfo> for cargo_lock::Lockfile { type Error = cargo_lock::Error; fn try_from(input: &VersionInfo) -> Result { let mut root_package: Option = None; let mut packages: Vec = Vec::new(); for pkg in input.packages.iter() { let lock_pkg = cargo_lock::package::Package { name: cargo_lock::package::Name::from_str(&pkg.name)?, version: pkg.version.clone(), checksum: Option::None, dependencies: { let result: Result, _> = pkg.dependencies .iter() .map(|i| { input.packages.get(*i).ok_or(cargo_lock::Error::Parse( format!("There is no dependency with index {} in the input JSON", i)) )?.try_into() }) .collect(); result? }, replace: None, source: (&pkg.source).into(), }; if pkg.root { if root_package.is_some() { return Err(cargo_lock::Error::Parse( "More than one root package specified in JSON!".to_string(), )); } root_package = Some(lock_pkg.clone()); } packages.push(lock_pkg); } Ok(cargo_lock::Lockfile { version: cargo_lock::ResolveVersion::V2, packages: packages, root: root_package, metadata: std::collections::BTreeMap::new(), patch: cargo_lock::Patch { unused: Vec::new() }, }) } } #[cfg(test)] mod tests { #![allow(unused_imports)] // otherwise conditional compilation emits warnings use super::*; use std::fs; use std::{convert::TryInto, path::PathBuf}; #[cfg(feature = "toml")] #[cfg(feature = "from_metadata")] fn load_own_metadata() -> cargo_metadata::Metadata { let mut cmd = cargo_metadata::MetadataCommand::new(); let cargo_toml_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml"); cmd.manifest_path(cargo_toml_path); cmd.exec().unwrap() } #[test] #[cfg(feature = "toml")] #[cfg(feature = "from_metadata")] fn to_toml() { let metadata = load_own_metadata(); let version_info_struct: VersionInfo = (&metadata).try_into().unwrap(); let _lockfile_struct: cargo_lock::Lockfile = (&version_info_struct).try_into().unwrap(); } #[cfg(feature = "schema")] /// Generate a JsonSchema for VersionInfo fn generate_schema() -> schemars::schema::RootSchema { let mut schema = schemars::schema_for!(VersionInfo); let mut metadata = *schema.schema.metadata.clone().unwrap(); let title = "cargo-auditable schema".to_string(); metadata.title = Some(title); metadata.id = Some("https://rustsec.org/schemas/cargo-auditable.json".to_string()); metadata.examples = [].to_vec(); metadata.description = Some( "Describes the `VersionInfo` JSON data structure that cargo-auditable embeds into Rust binaries." .to_string(), ); schema.schema.metadata = Some(Box::new(metadata)); schema } #[test] #[cfg(feature = "schema")] fn verify_schema() { use schemars::schema::RootSchema; let expected = generate_schema(); // Printing here makes it easier to update the schema when required println!( "expected schema:\n{}", serde_json::to_string_pretty(&expected).unwrap() ); let contents = fs::read_to_string( // `CARGO_MANIFEST_DIR` env is path to dir containing auditable-serde's Cargo.toml PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("cargo-auditable.schema.json"), ) .expect("error reading existing schema"); let actual: RootSchema = serde_json::from_str(&contents).expect("error deserializing existing schema"); assert_eq!(expected, actual); } } auditable-serde-0.6.0/src/validation.rs000064400000000000000000000064751046102023000161640ustar 00000000000000use crate::{Package, VersionInfo}; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt::Display}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct RawVersionInfo { pub packages: Vec, } pub enum ValidationError { MultipleRoots, CyclicDependency, } impl Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ValidationError::MultipleRoots => { write!(f, "Multiple root packages specified in the input JSON") } ValidationError::CyclicDependency => { write!(f, "The input JSON specifies a cyclic dependency graph") } } } } impl TryFrom for VersionInfo { type Error = ValidationError; fn try_from(v: RawVersionInfo) -> Result { if has_multiple_root_packages(&v) { Err(ValidationError::MultipleRoots) } else if has_cylic_dependencies(&v) { Err(ValidationError::CyclicDependency) } else { Ok(VersionInfo { packages: v.packages, }) } } } fn has_multiple_root_packages(v: &RawVersionInfo) -> bool { let mut seen_a_root = false; for package in &v.packages { if package.root { if seen_a_root { return true; } else { seen_a_root = true; } } } false } fn has_cylic_dependencies(v: &RawVersionInfo) -> bool { // I've reviewed the `topological_sort` crate and it appears to be high-quality, // so I'm not concerned about having it exposed to untrusted input. // It's better than my hand-rolled version would have been. // populate the topological sorting map let mut ts = topological_sort::TopologicalSort::::new(); for (index, package) in v.packages.iter().enumerate() { for dep in &package.dependencies { ts.add_dependency(*dep, index); } } // drain all elements that are not part of a cycle while ts.pop().is_some() {} // if the set isn't empty, the graph has cycles !ts.is_empty() } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; use crate::*; fn dummy_package(pkg_counter: u32, root: bool, deps: Vec) -> Package { Package { name: format!("test_{pkg_counter}"), version: semver::Version::from_str("0.0.0").unwrap(), source: Source::Local, kind: DependencyKind::Build, dependencies: deps, root: root, } } // these tests are very basic because `topological_sort` crate is already tested extensively #[test] fn cyclic_dependencies() { let pkg0 = dummy_package(0, true, vec![1]); let pkg1 = dummy_package(1, false, vec![0]); let raw = RawVersionInfo { packages: vec![pkg0, pkg1], }; assert!(VersionInfo::try_from(raw).is_err()); } #[test] fn no_cyclic_dependencies() { let pkg0 = dummy_package(0, true, vec![1]); let pkg1 = dummy_package(1, false, vec![]); let raw = RawVersionInfo { packages: vec![pkg0, pkg1], }; assert!(VersionInfo::try_from(raw).is_ok()); } }