Skip to main content

kellnr_settings/
filter.rs

1// The field names are the settings keys. The prefix leaves room for other filters.
2#![allow(clippy::struct_field_names)]
3
4use std::fmt::Display;
5
6use clap::ValueEnum;
7use provcfg::{ClapArgs, Configurable};
8use serde::{Deserialize, Deserializer, Serialize};
9
10/// Filters for downloads through the crates.io proxy.
11///
12/// The fields are the same as in [`RegistryFilter`]. Both are separate structs
13/// because the CLI flag prefix is set per struct.
14#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
15#[serde(default)]
16#[configurable(clap_prefix = "cratesio-filter")]
17pub struct CratesioFilter {
18    /// Deny downloads of crates whose license is not allowed
19    pub license_enabled: bool,
20
21    /// Allowed SPDX license identifiers (comma-separated), e.g. "MIT,Apache-2.0"
22    #[configurable(env_list)]
23    #[arg(value_delimiter = ',')]
24    pub license_allowed: Vec<String>,
25
26    /// Decision for crates without a known or valid license: "deny" or "allow"
27    #[arg(value_enum)]
28    pub license_unknown: LicenseUnknown,
29
30    /// Crates (comma-separated) that are allowed regardless of their license
31    #[configurable(env_list)]
32    #[arg(value_delimiter = ',')]
33    pub license_exempt_crates: Vec<String>,
34}
35
36/// Filters for downloads of crates hosted in Kellnr.
37///
38/// The fields are the same as in [`CratesioFilter`].
39#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
40#[serde(default)]
41#[configurable(clap_prefix = "registry-filter")]
42pub struct RegistryFilter {
43    /// Deny downloads of crates whose license is not allowed
44    pub license_enabled: bool,
45
46    /// Allowed SPDX license identifiers (comma-separated), e.g. "MIT,Apache-2.0"
47    #[configurable(env_list)]
48    #[arg(value_delimiter = ',')]
49    pub license_allowed: Vec<String>,
50
51    /// Decision for crates without a known or valid license: "deny" or "allow"
52    #[arg(value_enum)]
53    pub license_unknown: LicenseUnknown,
54
55    /// Crates (comma-separated) that are allowed regardless of their license
56    #[configurable(env_list)]
57    #[arg(value_delimiter = ',')]
58    pub license_exempt_crates: Vec<String>,
59}
60
61/// What the license filter does with a crate whose license is unknown: none
62/// is declared, the expression does not parse, or it could not be fetched.
63#[derive(Debug, PartialEq, Eq, Clone, Copy, ValueEnum, Default)]
64pub enum LicenseUnknown {
65    #[default]
66    Deny,
67    Allow,
68}
69
70impl<'de> Deserialize<'de> for LicenseUnknown {
71    fn deserialize<D>(de: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        match String::deserialize(de)?.to_lowercase().as_str() {
76            "deny" => Ok(Self::Deny),
77            "allow" => Ok(Self::Allow),
78            other => Err(serde::de::Error::custom(format!(
79                "unknown license_unknown value {other:?}; expected \"deny\" or \"allow\""
80            ))),
81        }
82    }
83}
84
85impl Serialize for LicenseUnknown {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: serde::Serializer,
89    {
90        serializer.serialize_str(&self.to_string())
91    }
92}
93
94impl Display for LicenseUnknown {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            LicenseUnknown::Deny => write!(f, "deny"),
98            LicenseUnknown::Allow => write!(f, "allow"),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use provcfg::Config;
106
107    use crate::settings::SettingsProv;
108    use crate::{CratesioFilter, LicenseUnknown, RegistryFilter, Settings};
109
110    fn from_toml(toml: &str) -> Result<Settings, provcfg::Error> {
111        Config::new()
112            .add_toml_str("test.toml", toml)
113            .build::<SettingsProv>()
114            .map(|prov| Settings::from(&prov))
115    }
116
117    #[test]
118    fn filters_are_disabled_by_default() {
119        let settings = Settings::default();
120
121        assert_eq!(CratesioFilter::default(), settings.cratesio_filter);
122        assert_eq!(RegistryFilter::default(), settings.registry_filter);
123        assert!(!settings.cratesio_filter.license_enabled);
124        assert!(settings.cratesio_filter.license_allowed.is_empty());
125        assert_eq!(
126            LicenseUnknown::Deny,
127            settings.cratesio_filter.license_unknown
128        );
129        assert!(settings.cratesio_filter.license_exempt_crates.is_empty());
130    }
131
132    #[test]
133    fn both_sections_read_from_toml() {
134        let settings = from_toml(
135            r#"
136            [cratesio_filter]
137            license_enabled = true
138            license_allowed = ["MIT", "Apache-2.0"]
139            license_unknown = "allow"
140            license_exempt_crates = ["ring"]
141
142            [registry_filter]
143            license_enabled = true
144            license_allowed = ["MIT"]
145            "#,
146        )
147        .unwrap();
148
149        assert_eq!(
150            CratesioFilter {
151                license_enabled: true,
152                license_allowed: vec!["MIT".to_string(), "Apache-2.0".to_string()],
153                license_unknown: LicenseUnknown::Allow,
154                license_exempt_crates: vec!["ring".to_string()],
155            },
156            settings.cratesio_filter
157        );
158        assert_eq!(
159            RegistryFilter {
160                license_enabled: true,
161                license_allowed: vec!["MIT".to_string()],
162                license_unknown: LicenseUnknown::Deny,
163                license_exempt_crates: vec![],
164            },
165            settings.registry_filter
166        );
167    }
168
169    #[test]
170    fn invalid_license_unknown_fails() {
171        let result = from_toml("[cratesio_filter]\nlicense_unknown = \"deneay\"\n");
172
173        assert!(result.is_err());
174    }
175
176    #[test]
177    fn list_settings_are_env_list_keys() {
178        let keys = crate::settings::env_list_keys();
179
180        for key in [
181            "cratesio_filter.license_allowed",
182            "cratesio_filter.license_exempt_crates",
183            "registry_filter.license_allowed",
184            "registry_filter.license_exempt_crates",
185        ] {
186            assert!(keys.contains(&key), "{key} must be an env list key");
187        }
188    }
189}