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}
40
41/// A single package in the dependency tree
42#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44pub struct Package {
45    /// Crate name specified in the `name` field in Cargo.toml file. Examples: "libc", "rand"
46    pub name: String,
47    /// The package's version in the [semantic version](https://semver.org) format.
48    #[cfg_attr(feature = "schema", schemars(with = "String"))]
49    pub version: semver::Version,
50    /// Currently "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc.
51    pub source: Source,
52    /// "build" or "runtime". May be omitted if set to "runtime".
53    /// If it's both a build and a runtime dependency, "runtime" is recorded.
54    #[serde(default)]
55    #[serde(skip_serializing_if = "is_default")]
56    pub kind: DependencyKind,
57    /// Packages are stored in an ordered array both in the `VersionInfo` struct and in JSON.
58    /// Here we refer to each package by its index in the array.
59    /// May be omitted if the list is empty.
60    #[serde(default)]
61    #[serde(skip_serializing_if = "is_default")]
62    pub dependencies: Vec<usize>,
63    /// Whether this is the root package in the dependency tree.
64    /// There should only be one root package.
65    /// May be omitted if set to `false`.
66    #[serde(default)]
67    #[serde(skip_serializing_if = "is_default")]
68    pub root: bool,
69}
70
71/// Serializes to "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc.
72#[non_exhaustive]
73#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
74#[serde(from = "&str")]
75#[serde(into = "String")]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77pub enum Source {
78    CratesIo,
79    Git,
80    Local,
81    Registry,
82    Other(String),
83}
84
85impl From<&str> for Source {
86    fn from(s: &str) -> Self {
87        match s {
88            "crates.io" => Self::CratesIo,
89            "git" => Self::Git,
90            "local" => Self::Local,
91            "registry" => Self::Registry,
92            other_str => Self::Other(other_str.to_string()),
93        }
94    }
95}
96
97impl From<Source> for String {
98    fn from(s: Source) -> String {
99        match s {
100            Source::CratesIo => "crates.io".to_owned(),
101            Source::Git => "git".to_owned(),
102            Source::Local => "local".to_owned(),
103            Source::Registry => "registry".to_owned(),
104            Source::Other(string) => string,
105        }
106    }
107}
108
109#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Default)]
110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
111pub enum DependencyKind {
112    // The values are ordered from weakest to strongest so that casting to integer would make sense
113    #[serde(rename = "build")]
114    Build,
115    #[default]
116    #[serde(rename = "runtime")]
117    Runtime,
118}
119
120fn is_default<T: Default + PartialEq>(value: &T) -> bool {
121    let default_value = T::default();
122    value == &default_value
123}
124
125impl FromStr for VersionInfo {
126    type Err = serde_json::Error;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        serde_json::from_str(s)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    #![allow(unused_imports)] // otherwise conditional compilation emits warnings
135    use super::*;
136    use std::fs;
137    use std::{
138        convert::TryInto,
139        path::{Path, PathBuf},
140    };
141
142    #[cfg(feature = "schema")]
143    /// Generate a JsonSchema for VersionInfo
144    fn generate_schema() -> schemars::schema::RootSchema {
145        let mut schema = schemars::schema_for!(VersionInfo);
146        let mut metadata = *schema.schema.metadata.clone().unwrap();
147
148        let title = "cargo-auditable schema".to_string();
149        metadata.title = Some(title);
150        metadata.id = Some("https://rustsec.org/schemas/cargo-auditable.json".to_string());
151        metadata.examples = [].to_vec();
152        metadata.description = Some(
153            "Describes the `VersionInfo` JSON data structure that cargo-auditable embeds into Rust binaries."
154                .to_string(),
155        );
156        schema.schema.metadata = Some(Box::new(metadata));
157        schema
158    }
159
160    #[test]
161    #[cfg(feature = "schema")]
162    fn verify_schema() {
163        use schemars::schema::RootSchema;
164
165        let expected = generate_schema();
166        // Printing here makes it easier to update the schema when required
167        println!(
168            "expected schema:\n{}",
169            serde_json::to_string_pretty(&expected).unwrap()
170        );
171
172        let contents = fs::read_to_string(
173            // `CARGO_MANIFEST_DIR` env is path to dir containing auditable-serde's Cargo.toml
174            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
175                .parent()
176                .unwrap()
177                .join("cargo-auditable.schema.json"),
178        )
179        .expect("error reading existing schema");
180        let actual: RootSchema =
181            serde_json::from_str(&contents).expect("error deserializing existing schema");
182
183        assert_eq!(expected, actual);
184    }
185}