Skip to main content

osv_db/types/
ecosystem.rs

1use std::{fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Deserializer, de};
4use strum::{Display, EnumString};
5
6/// Ecosystem name, optionally with a suffix (e.g. `"Debian:10"`).
7#[derive(Debug, Clone)]
8pub struct EcosystemWithSuffix(Ecosystem, Option<String>);
9
10/// Represents an OSV ecosystem, as defined by the OSV schema.
11/// See <https://github.com/ossf/osv-schema/blob/main/validation/schema.json>
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
13pub enum Ecosystem {
14    #[strum(to_string = "AlmaLinux")]
15    AlmaLinux,
16    #[strum(to_string = "Alpaquita")]
17    Alpaquita,
18    #[strum(to_string = "Alpine")]
19    Alpine,
20    #[strum(to_string = "Android")]
21    Android,
22    #[strum(to_string = "Azure Linux")]
23    AzureLinux,
24    #[strum(to_string = "BellSoft Hardened Containers")]
25    BellSoftHardenedContainers,
26    #[strum(to_string = "Bioconductor")]
27    Bioconductor,
28    #[strum(to_string = "Bitnami")]
29    Bitnami,
30    #[strum(to_string = "Chainguard")]
31    Chainguard,
32    #[strum(to_string = "CleanStart")]
33    CleanStart,
34    #[strum(to_string = "ConanCenter")]
35    ConanCenter,
36    #[strum(to_string = "CRAN")]
37    Cran,
38    #[strum(to_string = "crates.io")]
39    CratesIo,
40    #[strum(to_string = "Debian")]
41    Debian,
42    #[strum(to_string = "Docker Hardened Images")]
43    DockerHardenedImages,
44    #[strum(to_string = "Echo")]
45    Echo,
46    #[strum(to_string = "FreeBSD")]
47    FreeBSD,
48    #[strum(to_string = "GHC")]
49    Ghc,
50    #[strum(to_string = "GitHub Actions")]
51    GitHubActions,
52    #[strum(to_string = "Go")]
53    Go,
54    #[strum(to_string = "Hackage")]
55    Hackage,
56    #[strum(to_string = "Hex")]
57    Hex,
58    #[strum(to_string = "Julia")]
59    Julia,
60    #[strum(to_string = "Kubernetes")]
61    Kubernetes,
62    #[strum(to_string = "Linux")]
63    Linux,
64    #[strum(to_string = "Mageia")]
65    Mageia,
66    #[strum(to_string = "Maven")]
67    Maven,
68    #[strum(to_string = "MinimOS")]
69    MinimOS,
70    #[strum(to_string = "npm")]
71    Npm,
72    #[strum(to_string = "NuGet")]
73    NuGet,
74    #[strum(to_string = "opam")]
75    Opam,
76    #[strum(to_string = "openEuler")]
77    OpenEuler,
78    #[strum(to_string = "openSUSE")]
79    OpenSUSE,
80    #[strum(to_string = "OSS-Fuzz")]
81    OssFuzz,
82    #[strum(to_string = "Packagist")]
83    Packagist,
84    #[strum(to_string = "Photon OS")]
85    PhotonOS,
86    #[strum(to_string = "Pub")]
87    Pub,
88    #[strum(to_string = "PyPI")]
89    PyPI,
90    #[strum(to_string = "Red Hat")]
91    RedHat,
92    #[strum(to_string = "Rocky Linux")]
93    RockyLinux,
94    #[strum(to_string = "Root")]
95    Root,
96    #[strum(to_string = "RubyGems")]
97    RubyGems,
98    #[strum(to_string = "SUSE")]
99    Suse,
100    #[strum(to_string = "SwiftURL")]
101    SwiftURL,
102    #[strum(to_string = "Ubuntu")]
103    Ubuntu,
104    #[strum(to_string = "VSCode")]
105    VSCode,
106    #[strum(to_string = "Wolfi")]
107    Wolfi,
108    #[strum(to_string = "GIT")]
109    Git,
110}
111
112impl EcosystemWithSuffix {
113    /// Returns the [`Ecosystem`] variant, without the suffix.
114    #[must_use]
115    pub fn ecosystem(&self) -> Ecosystem {
116        self.0
117    }
118
119    /// Returns the optional suffix component (e.g. `"10"` for `"Debian:10"`),
120    /// or `None` if no suffix is present.
121    #[must_use]
122    pub fn suffix(&self) -> Option<&str> {
123        self.1.as_deref()
124    }
125}
126
127impl Display for EcosystemWithSuffix {
128    fn fmt(
129        &self,
130        f: &mut std::fmt::Formatter<'_>,
131    ) -> std::fmt::Result {
132        write!(f, "{}", self.0)?;
133        if let Some(suffix) = &self.1 {
134            write!(f, ":{suffix}")?;
135        }
136        Ok(())
137    }
138}
139
140impl FromStr for EcosystemWithSuffix {
141    type Err = <Ecosystem as FromStr>::Err;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        if let Some((s, suffix)) = s.split_once(':') {
145            Ok(Self(s.parse()?, Some(suffix.to_string())))
146        } else {
147            Ok(Self(s.parse()?, None))
148        }
149    }
150}
151
152impl<'de> Deserialize<'de> for EcosystemWithSuffix {
153    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
154        let s = String::deserialize(deserializer)?;
155        s.parse().map_err(|_| de::Error::unknown_variant(&s, &[]))
156    }
157}
158
159impl<'de> Deserialize<'de> for Ecosystem {
160    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
161        let s = String::deserialize(deserializer)?;
162        s.parse().map_err(|_| de::Error::unknown_variant(&s, &[]))
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use serde_json::json;
169    use test_case::test_case;
170
171    use super::*;
172
173    // Roundtrip: Display produces the canonical OSV string, parse recovers the variant.
174    #[test_case("AlmaLinux", Ecosystem::AlmaLinux)]
175    #[test_case("Alpaquita", Ecosystem::Alpaquita)]
176    #[test_case("Alpine", Ecosystem::Alpine)]
177    #[test_case("Android", Ecosystem::Android)]
178    #[test_case("Azure Linux", Ecosystem::AzureLinux)]
179    #[test_case("BellSoft Hardened Containers", Ecosystem::BellSoftHardenedContainers)]
180    #[test_case("Bioconductor", Ecosystem::Bioconductor)]
181    #[test_case("Bitnami", Ecosystem::Bitnami)]
182    #[test_case("Chainguard", Ecosystem::Chainguard)]
183    #[test_case("CleanStart", Ecosystem::CleanStart)]
184    #[test_case("ConanCenter", Ecosystem::ConanCenter)]
185    #[test_case("CRAN", Ecosystem::Cran)]
186    #[test_case("crates.io", Ecosystem::CratesIo)]
187    #[test_case("Debian", Ecosystem::Debian)]
188    #[test_case("Docker Hardened Images", Ecosystem::DockerHardenedImages)]
189    #[test_case("Echo", Ecosystem::Echo)]
190    #[test_case("FreeBSD", Ecosystem::FreeBSD)]
191    #[test_case("GHC", Ecosystem::Ghc)]
192    #[test_case("GitHub Actions", Ecosystem::GitHubActions)]
193    #[test_case("Go", Ecosystem::Go)]
194    #[test_case("Hackage", Ecosystem::Hackage)]
195    #[test_case("Hex", Ecosystem::Hex)]
196    #[test_case("Julia", Ecosystem::Julia)]
197    #[test_case("Kubernetes", Ecosystem::Kubernetes)]
198    #[test_case("Linux", Ecosystem::Linux)]
199    #[test_case("Mageia", Ecosystem::Mageia)]
200    #[test_case("Maven", Ecosystem::Maven)]
201    #[test_case("MinimOS", Ecosystem::MinimOS)]
202    #[test_case("npm", Ecosystem::Npm)]
203    #[test_case("NuGet", Ecosystem::NuGet)]
204    #[test_case("opam", Ecosystem::Opam)]
205    #[test_case("openEuler", Ecosystem::OpenEuler)]
206    #[test_case("openSUSE", Ecosystem::OpenSUSE)]
207    #[test_case("OSS-Fuzz", Ecosystem::OssFuzz)]
208    #[test_case("Packagist", Ecosystem::Packagist)]
209    #[test_case("Photon OS", Ecosystem::PhotonOS)]
210    #[test_case("Pub", Ecosystem::Pub)]
211    #[test_case("PyPI", Ecosystem::PyPI)]
212    #[test_case("Red Hat", Ecosystem::RedHat)]
213    #[test_case("Rocky Linux", Ecosystem::RockyLinux)]
214    #[test_case("Root", Ecosystem::Root)]
215    #[test_case("RubyGems", Ecosystem::RubyGems)]
216    #[test_case("SUSE", Ecosystem::Suse)]
217    #[test_case("SwiftURL", Ecosystem::SwiftURL)]
218    #[test_case("Ubuntu", Ecosystem::Ubuntu)]
219    #[test_case("VSCode", Ecosystem::VSCode)]
220    #[test_case("Wolfi", Ecosystem::Wolfi)]
221    #[test_case("GIT", Ecosystem::Git)]
222    #[allow(clippy::needless_pass_by_value)]
223    fn display_and_parse_roundtrip(
224        osv_string: &str,
225        expected: Ecosystem,
226    ) {
227        let eco_from_json: Ecosystem = serde_json::from_value(json!(osv_string)).unwrap();
228        let eco_from_str: Ecosystem = osv_string.parse().unwrap();
229
230        assert_eq!(expected, eco_from_str);
231        assert_eq!(expected, eco_from_json);
232        assert_eq!(expected.to_string(), osv_string);
233
234        let ews_from_str: EcosystemWithSuffix = osv_string.parse().unwrap();
235        let ews_from_json: EcosystemWithSuffix = serde_json::from_value(json!(osv_string)).unwrap();
236        assert_eq!(ews_from_str.to_string(), osv_string);
237        assert_eq!(ews_from_json.to_string(), osv_string);
238    }
239}