Skip to main content

kellnr_common/
crate_data.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6use crate::index_metadata::IndexDep;
7use crate::publish_metadata::RegistryDep;
8
9/// Index URL that cargo sets as the registry of crates.io dependencies
10pub const CRATESIO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
11
12#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
13pub struct CrateData {
14    pub name: String,
15    // additional information from kellnr about the crate
16    pub owners: Vec<String>,
17    pub max_version: String,
18    pub total_downloads: i64,
19    pub last_updated: String,
20    // metadata information from the publishing
21    pub homepage: Option<String>,
22    pub description: Option<String>,
23    pub repository: Option<String>,
24    pub categories: Vec<String>,
25    pub keywords: Vec<String>,
26    pub authors: Vec<String>,
27    pub versions: Vec<CrateVersionData>,
28}
29
30#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
31pub struct CrateVersionData {
32    pub version: String,
33    // additional information about the crate version from kellnr
34    pub created: String,
35    pub downloads: i64,
36    // metadata information from the publishing
37    pub readme: Option<String>,
38    pub license: Option<String>,
39    pub license_file: Option<String>,
40    pub documentation: Option<String>,
41    pub dependencies: Vec<CrateRegistryDep>,
42    pub checksum: String,
43    pub features: BTreeMap<String, Vec<String>>,
44    pub yanked: bool,
45    pub links: Option<String>,
46    pub v: i32,
47    // Size of the .crate file in bytes, 0 if unknown
48    pub size: i64,
49    // Minimal supported Rust version
50    pub rust_version: Option<String>,
51    // User who published the version, None if unknown
52    pub publisher: Option<String>,
53    // Name of the auth token used to publish, None if unknown
54    pub publisher_token: Option<String>,
55}
56
57#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
58pub struct CrateRegistryDep {
59    // Name of the dependency.
60    // If the dependency is renamed from the original package name,
61    // this is the original name. The new package name is stored in
62    // the `explicit_name_in_toml` field.
63    pub name: String,
64    pub description: Option<String>,
65    // The semver requirement for this dependency.
66    pub version_req: String,
67    // Array of features (as strings) enabled for this dependency.
68    pub features: Option<Vec<String>>,
69    // Boolean of whether or not this is an optional dependency.
70    pub optional: bool,
71    // Boolean of whether or not default features are enabled.
72    pub default_features: bool,
73    // The target platform for the dependency.
74    // null if not a target dependency.
75    // Otherwise, a string such as "cfg(windows)".
76    pub target: Option<String>,
77    // The dependency kind.
78    // "dev", "build", or "normal".
79    pub kind: Option<String>,
80    // The URL of the index of the registry where this dependency is
81    // from as a string. If not specified or null, it is assumed the
82    // dependency is in the current registry.
83    pub registry: Option<String>,
84    // If the dependency is renamed, this is a string of the new
85    // package name. If not specified or null, this dependency is not
86    // renamed.
87    pub explicit_name_in_toml: Option<String>,
88    // Latest version of the dependency that is not yanked, if Kellnr knows
89    // it. crates.io dependencies are only known if they are in the proxy cache.
90    pub latest_version: Option<String>,
91    // Whether the latest version is newer than the version requirement allows.
92    pub outdated: bool,
93}
94
95impl From<RegistryDep> for CrateRegistryDep {
96    fn from(dep: RegistryDep) -> Self {
97        CrateRegistryDep {
98            name: dep.name,
99            description: None,
100            version_req: dep.version_req,
101            features: dep.features,
102            optional: dep.optional,
103            default_features: dep.default_features,
104            target: dep.target,
105            kind: dep.kind,
106            registry: dep.registry,
107            explicit_name_in_toml: dep.explicit_name_in_toml,
108            latest_version: None,
109            outdated: false,
110        }
111    }
112}
113
114impl CrateRegistryDep {
115    pub fn from_index(desc: Option<String>, latest_version: Option<String>, dep: IndexDep) -> Self {
116        let outdated = latest_version
117            .as_deref()
118            .is_some_and(|latest| is_outdated(&dep.req, latest));
119        CrateRegistryDep {
120            name: match dep.package {
121                Some(ref package) => package.clone(),
122                None => dep.name.clone(),
123            },
124            description: desc,
125            version_req: dep.req,
126            features: Some(dep.features),
127            optional: dep.optional,
128            default_features: dep.default_features,
129            target: dep.target,
130            kind: dep.kind.map(|k| k.to_string()),
131            registry: dep.registry,
132            explicit_name_in_toml: match dep.package {
133                Some(_) => Some(dep.name),
134                None => None,
135            },
136            latest_version,
137            outdated,
138        }
139    }
140}
141
142/// Whether `latest` is newer than the version requirement `req` allows, e.g.
143/// `^2.0` with the latest version `3.1`. A requirement that is newer than the
144/// latest version, or that cannot be parsed, is not outdated.
145pub fn is_outdated(req: &str, latest: &str) -> bool {
146    let (Ok(req), Ok(latest)) = (
147        semver::VersionReq::parse(req),
148        semver::Version::parse(latest),
149    ) else {
150        return false;
151    };
152    if req.matches(&latest) {
153        return false;
154    }
155    // Not outdated if the latest version is below a lower bound of the
156    // requirement. Upper bounds like `<2` are what make a version outdated.
157    let below_lower_bound = req.comparators.iter().any(|c| {
158        let bound = semver::Version::new(c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0));
159        match c.op {
160            semver::Op::Less | semver::Op::LessEq => false,
161            semver::Op::Greater => latest <= bound,
162            _ => latest < bound,
163        }
164    });
165    !below_lower_bound
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn latest_version_outside_the_requirement_is_outdated() {
174        assert!(is_outdated("^2.0", "3.1.0"));
175        assert!(is_outdated("2.0", "3.1.0"));
176        assert!(is_outdated("~1.2", "1.3.0"));
177        assert!(is_outdated("=1.0.0", "1.0.1"));
178        assert!(is_outdated(">=1, <2", "2.0.0"));
179        assert!(is_outdated("^0.3", "0.4.0"));
180        assert!(is_outdated("<2", "3.0.0"));
181    }
182
183    #[test]
184    fn latest_version_inside_the_requirement_is_not_outdated() {
185        assert!(!is_outdated("^2.0", "2.4.0"));
186        assert!(!is_outdated("*", "9.0.0"));
187        assert!(!is_outdated(">=1", "3.0.0"));
188    }
189
190    #[test]
191    fn requirement_newer_than_the_latest_version_is_not_outdated() {
192        assert!(!is_outdated("^5", "4.0.0"));
193        assert!(!is_outdated(">=2.1", "2.0.0"));
194        assert!(!is_outdated(">2.0.0", "2.0.0"));
195        assert!(!is_outdated("=2.0.0", "1.0.0"));
196    }
197
198    #[test]
199    fn unparsable_versions_are_not_outdated() {
200        assert!(!is_outdated("not a req", "1.0.0"));
201        assert!(!is_outdated("^1", "latest"));
202    }
203}