sampler_config/
utils.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_diagnostics::{Selector, StringSelector, TreeNames};
6use selectors::FastError;
7use serde::{de, Deserialize, Deserializer};
8use std::fmt;
9use std::marker::PhantomData;
10use std::sync::LazyLock;
11use thiserror::Error;
12
13pub fn greater_than_zero<'de, D>(deserializer: D) -> Result<i64, D::Error>
14where
15    D: Deserializer<'de>,
16{
17    let value = i64::deserialize(deserializer)?;
18    if value <= 0 {
19        return Err(de::Error::custom(format!("i64 out of range: {value}")));
20    }
21    Ok(value)
22}
23
24pub fn one_or_many_selectors<'de, D>(deserializer: D) -> Result<Vec<Selector>, D::Error>
25where
26    D: Deserializer<'de>,
27{
28    deserializer.deserialize_any(OneOrMany(PhantomData::<Selector>))
29}
30
31pub(crate) struct OneOrMany<T>(pub PhantomData<T>);
32
33trait ParseString {
34    fn parse_string(value: &str) -> Result<Self, Error>
35    where
36        Self: Sized;
37}
38
39impl ParseString for String {
40    fn parse_string(value: &str) -> Result<Self, Error> {
41        Ok(value.into())
42    }
43}
44
45impl ParseString for Selector {
46    fn parse_string(value: &str) -> Result<Self, Error> {
47        parse_selector(value)
48    }
49}
50
51impl<'de, T> de::Visitor<'de> for OneOrMany<T>
52where
53    T: ParseString,
54{
55    type Value = Vec<T>;
56
57    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.write_str("either a single string or an array of strings")
59    }
60
61    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
62    where
63        E: de::Error,
64    {
65        let result = T::parse_string(value).map_err(E::custom)?;
66        Ok(vec![result])
67    }
68
69    fn visit_seq<S>(self, mut visitor: S) -> Result<Self::Value, S::Error>
70    where
71        S: de::SeqAccess<'de>,
72    {
73        let mut out = vec![];
74        while let Some(s) = visitor.next_element::<String>()? {
75            use serde::de::Error;
76            let selector = T::parse_string(&s).map_err(S::Error::custom)?;
77            out.push(selector);
78        }
79        if out.is_empty() {
80            Err(de::Error::invalid_length(0, &"expected at least one string"))
81        } else {
82            Ok(out)
83        }
84    }
85}
86
87pub fn parse_selector(selector_str: &str) -> Result<Selector, Error> {
88    let selector = selectors::parse_selector::<FastError>(selector_str)?;
89    verify_wildcard_restrictions(&selector, selector_str)?;
90    Ok(selector)
91}
92
93struct WildcardRestriction {
94    segments: Vec<StringSelector>,
95    must_have_tree_name: bool,
96}
97
98static WILDCARD_RESTRICTIONS: LazyLock<Vec<WildcardRestriction>> = LazyLock::new(|| {
99    vec![
100        WildcardRestriction {
101            segments: vec![
102                StringSelector::ExactMatch("core".into()),
103                StringSelector::ExactMatch("bluetooth-core".into()),
104                StringSelector::StringPattern("bt-host-collection:bt-host_*".into()),
105            ],
106            must_have_tree_name: false,
107        },
108        WildcardRestriction {
109            segments: vec![
110                StringSelector::ExactMatch("bootstrap".into()),
111                StringSelector::StringPattern("*-drivers:*".into()),
112            ],
113            must_have_tree_name: true,
114        },
115    ]
116});
117
118// `selector` must be validated.
119fn verify_wildcard_restrictions(selector: &Selector, raw_selector: &str) -> Result<(), Error> {
120    // Safety: assuming that the selector was parsed by selectors::parse_selectors, it has
121    // been validated, and these unwraps are safe
122    let actual_segments =
123        selector.component_selector.as_ref().unwrap().moniker_segments.as_ref().unwrap();
124    if !actual_segments.iter().any(|segment| matches!(segment, StringSelector::StringPattern(_))) {
125        return Ok(());
126    }
127    for restriction in &*WILDCARD_RESTRICTIONS {
128        if restriction.segments.len() != actual_segments.len() {
129            continue;
130        }
131        if restriction
132            .segments
133            .iter()
134            .zip(actual_segments.iter())
135            .any(|(expected_segment, actual_segment)| expected_segment != actual_segment)
136        {
137            continue;
138        }
139        if restriction.must_have_tree_name {
140            let Some(TreeNames::Some(_)) = selector.tree_names else {
141                return Err(Error::InvalidWildcardedSelector(raw_selector.to_string()));
142            };
143        }
144        return Ok(());
145    }
146    Err(Error::InvalidWildcardedSelector(raw_selector.to_string()))
147}
148
149#[derive(Debug, Error)]
150pub enum Error {
151    #[error("wildcarded component not allowlisted: '{0}'")]
152    InvalidWildcardedSelector(String),
153
154    #[error(transparent)]
155    ParseSelector(#[from] selectors::Error),
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use assert_matches::assert_matches;
162
163    #[derive(Debug, Deserialize, PartialEq)]
164    struct Test(#[serde(deserialize_with = "super::one_or_many_selectors")] Vec<Selector>);
165
166    #[derive(Debug, Deserialize, Eq, PartialEq)]
167    struct TestInt(#[serde(deserialize_with = "super::greater_than_zero")] i64);
168
169    #[fuchsia::test]
170    fn parse_valid_single_selector() {
171        let json = "\"core/bar:root/twig:leaf\"";
172        let data: Test = serde_json5::from_str(json).expect("deserialize");
173        assert_eq!(
174            data,
175            Test(vec![selectors::parse_selector::<FastError>("core/bar:root/twig:leaf").unwrap()])
176        );
177    }
178
179    #[fuchsia::test]
180    fn parse_valid_multiple_selectors() {
181        let json = "[ \"core/foo:some/branch:leaf\", \"core/bar:root/twig:leaf\"]";
182        let data: Test = serde_json5::from_str(json).expect("deserialize");
183        assert_eq!(
184            data,
185            Test(vec![
186                selectors::parse_selector::<FastError>("core/foo:some/branch:leaf").unwrap(),
187                selectors::parse_selector::<FastError>("core/bar:root/twig:leaf").unwrap()
188            ])
189        );
190    }
191
192    #[fuchsia::test]
193    fn refuse_invalid_selectors() {
194        let not_string = "42";
195        let bad_list = "[ 42, \"core/bar:not:a:selector:root/twig:leaf\"]";
196        serde_json5::from_str::<Test>(not_string).expect_err("should fail");
197        serde_json5::from_str::<Test>(bad_list).expect_err("should fail");
198    }
199
200    #[fuchsia::test]
201    fn test_greater_than_zero() {
202        let data: TestInt = serde_json5::from_str("1").unwrap();
203        assert_eq!(data, TestInt(1));
204        serde_json5::from_str::<Test>("0").expect_err("0 is not greater than 0");
205        serde_json5::from_str::<Test>("-1").expect_err("-1 is not greater than 0");
206    }
207
208    #[fuchsia::test]
209    fn wild_card_selectors() {
210        let good_selector = r#"["bootstrap/*-drivers\\:*:[name=fvm]root:field"]"#;
211        assert_matches!(serde_json5::from_str::<Test>(good_selector), Ok(_));
212
213        let good_selector = r#"["core/bluetooth-core/bt-host-collection\\:bt-host_*:root:field"]"#;
214        assert_matches!(serde_json5::from_str::<Test>(good_selector), Ok(_));
215
216        let bad_selector = r#"["not_bootstrap/*-drivers\\:*:[name=fvm]root:field"]"#;
217        assert_matches!(serde_json5::from_str::<Test>(bad_selector), Err(_));
218
219        let not_exact_collection_match = r#"["bootstrap/*-drivers*:[name=fvm]root:field"]"#;
220        assert_matches!(serde_json5::from_str::<Test>(not_exact_collection_match), Err(_));
221
222        let missing_filter = r#"["not_bootstrap/*-drivers\\:*:root:field"]"#;
223        assert_matches!(serde_json5::from_str::<Test>(missing_filter), Err(_));
224    }
225}