auditable_serde/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::redundant_field_names)]
3#![doc = include_str!("../README.md")]
4
5mod validation;
6
7use validation::RawVersionInfo;
8
9use serde::{Deserialize, Serialize};
10
11use std::str::FromStr;
12
13/// Dependency tree embedded in the binary.
14///
15/// Implements `Serialize` and `Deserialize` traits from `serde`, so you can use
16/// [all the usual methods from serde-json](https://docs.rs/serde_json/1.0.57/serde_json/#functions)
17/// to read and write it.
18///
19/// `from_str()` that parses JSON is also implemented for your convenience:
20/// ```rust
21/// use auditable_serde::VersionInfo;
22/// use std::str::FromStr;
23/// let json_str = r#"{"packages":[{
24///     "name":"adler",
25///     "version":"0.2.3",
26///     "source":"registry"
27/// }]}"#;
28/// let info = VersionInfo::from_str(json_str).unwrap();
29/// assert_eq!(&info.packages[0].name, "adler");
30/// ```
31///
32/// If deserialization succeeds, it is guaranteed that there is only one root package,
33/// and that are no cyclic dependencies.
34#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
35#[serde(try_from = "RawVersionInfo")]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct VersionInfo {
38    pub packages: Vec<Package>,
39    /// Format revision. Identifies the data source for the audit data.
40    ///
41    /// Format revisions are **backwards compatible.**
42    /// If an unknown format is encountered, it should be treated as the highest known preceding format.
43    /// For example, if formats `0`, `1` and `8` are known, format `4` should be treated as if it's `1`.
44    ///
45    /// # Known formats
46    ///
47    /// ## 0 (or the field is absent)
48    ///
49    /// Generated based on the data provided by [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html).
50    ///
51    /// There are multiple [known](https://github.com/rust-lang/cargo/issues/7754)
52    /// [issues](https://github.com/rust-lang/cargo/issues/10718) with this data source,
53    /// leading to the audit data sometimes including more dependencies than are really used in the build.
54    ///
55    /// However, is the only machine-readable data source available on stable Rust as of v1.88.
56    ///
57    /// Additionally, this format incorrectly includes [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html)
58    /// and their dependencies as runtime dependencies while in reality they are build-time dependencies.
59    ///
60    /// ## 1
61    ///
62    /// Same as 0, but correctly records proc-macros and their dependencies as build-time dependencies.
63    ///
64    /// May still include slightly more dependencies than are actually used, especially in workspaces.
65    ///
66    /// ## 8
67    ///
68    /// Generated using Cargo's [SBOM precursor](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom) as the data source.
69    ///
70    /// This data is highly accurate, but as of Rust v1.88 can only be generated using a nightly build of Cargo.
71    #[serde(default)]
72    #[serde(skip_serializing_if = "is_default")]
73    pub format: u32,
74}
75
76/// A single package in the dependency tree
77#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
78#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
79pub struct Package {
80    /// Crate name specified in the `name` field in Cargo.toml file. Examples: "libc", "rand"
81    pub name: String,
82    /// The package's version in the [semantic version](https://semver.org) format.
83    #[cfg_attr(feature = "schema", schemars(with = "String"))]
84    pub version: semver::Version,
85    /// Currently "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc.
86    pub source: Source,
87    /// "build" or "runtime". May be omitted if set to "runtime".
88    /// If it's both a build and a runtime dependency, "runtime" is recorded.
89    #[serde(default)]
90    #[serde(skip_serializing_if = "is_default")]
91    pub kind: DependencyKind,
92    /// Packages are stored in an ordered array both in the `VersionInfo` struct and in JSON.
93    /// Here we refer to each package by its index in the array.
94    /// May be omitted if the list is empty.
95    #[serde(default)]
96    #[serde(skip_serializing_if = "is_default")]
97    pub dependencies: Vec<usize>,
98    /// Whether this is the root package in the dependency tree.
99    /// There should only be one root package.
100    /// May be omitted if set to `false`.
101    #[serde(default)]
102    #[serde(skip_serializing_if = "is_default")]
103    pub root: bool,
104}
105
106/// Serializes to "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc.
107#[non_exhaustive]
108#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
109#[serde(from = "&str")]
110#[serde(into = "String")]
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112pub enum Source {
113    CratesIo,
114    Git,
115    Local,
116    Registry,
117    Other(String),
118}
119
120impl From<&str> for Source {
121    fn from(s: &str) -> Self {
122        match s {
123            "crates.io" => Self::CratesIo,
124            "git" => Self::Git,
125            "local" => Self::Local,
126            "registry" => Self::Registry,
127            other_str => Self::Other(other_str.to_string()),
128        }
129    }
130}
131
132impl From<Source> for String {
133    fn from(s: Source) -> String {
134        match s {
135            Source::CratesIo => "crates.io".to_owned(),
136            Source::Git => "git".to_owned(),
137            Source::Local => "local".to_owned(),
138            Source::Registry => "registry".to_owned(),
139            Source::Other(string) => string,
140        }
141    }
142}
143
144#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Default)]
145#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
146pub enum DependencyKind {
147    // The values are ordered from weakest to strongest so that casting to integer would make sense
148    #[serde(rename = "build")]
149    Build,
150    #[default]
151    #[serde(rename = "runtime")]
152    Runtime,
153}
154
155pub(crate) fn is_default<T: Default + PartialEq>(value: &T) -> bool {
156    let default_value = T::default();
157    value == &default_value
158}
159
160impl FromStr for VersionInfo {
161    type Err = serde_json::Error;
162    fn from_str(s: &str) -> Result<Self, Self::Err> {
163        serde_json::from_str(s)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    #![allow(unused_imports)] // otherwise conditional compilation emits warnings
170    use super::*;
171    use std::fs;
172    use std::{
173        convert::TryInto,
174        path::{Path, PathBuf},
175    };
176
177    #[cfg(feature = "schema")]
178    /// Generate a JsonSchema for VersionInfo
179    fn generate_schema() -> schemars::schema::RootSchema {
180        let mut schema = schemars::schema_for!(VersionInfo);
181        let mut metadata = *schema.schema.metadata.clone().unwrap();
182
183        let title = "cargo-auditable schema".to_string();
184        metadata.title = Some(title);
185        metadata.id = Some("https://rustsec.org/schemas/cargo-auditable.json".to_string());
186        metadata.examples = [].to_vec();
187        metadata.description = Some(
188            "Describes the `VersionInfo` JSON data structure that cargo-auditable embeds into Rust binaries."
189                .to_string(),
190        );
191        schema.schema.metadata = Some(Box::new(metadata));
192        schema
193    }
194
195    #[test]
196    #[cfg(feature = "schema")]
197    fn verify_schema() {
198        use schemars::schema::RootSchema;
199
200        let expected = generate_schema();
201        // Printing here makes it easier to update the schema when required
202        println!(
203            "expected schema:\n{}",
204            serde_json::to_string_pretty(&expected).unwrap()
205        );
206
207        let contents = fs::read_to_string(
208            // `CARGO_MANIFEST_DIR` env is path to dir containing auditable-serde's Cargo.toml
209            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
210                .parent()
211                .unwrap()
212                .join("cargo-auditable.schema.json"),
213        )
214        .expect("error reading existing schema");
215        let actual: RootSchema =
216            serde_json::from_str(&contents).expect("error deserializing existing schema");
217
218        assert_eq!(expected, actual);
219    }
220}