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}