1use anyhow::{Error, bail};
5use glob::glob;
6use regex::Regex;
7use serde::Serialize;
8use serde_derive::Deserialize;
9use std::borrow::Borrow;
10use std::collections::HashMap;
11use std::fmt::Display;
12use std::ops::Deref;
13use std::sync::LazyLock;
14
15pub type Config = HashMap<ServiceName, HashMap<Tag, TagConfig>>;
17
18#[derive(Deserialize, Default, Debug, PartialEq)]
20#[cfg_attr(test, derive(Clone))]
21#[serde(deny_unknown_fields)]
22struct TaggedPersist {
23 tag: String,
27 service_name: String,
30 #[serde(flatten)]
31 tag_config: TagConfig,
32}
33
34#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
38pub struct TagConfig {
39 #[serde(with = "selectors_ext::inspect")]
41 pub selectors: Vec<fidl_fuchsia_diagnostics::Selector>,
42 pub max_bytes: usize,
45 pub min_seconds_between_fetch: i64,
47 #[serde(default)]
49 pub persist_across_boot: bool,
50}
51
52#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
57pub struct Tag(String);
58
59impl From<&Tag> for Tag {
61 fn from(value: &Self) -> Self {
62 value.clone()
63 }
64}
65
66#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
71pub struct ServiceName(String);
72
73impl From<&ServiceName> for ServiceName {
75 fn from(value: &Self) -> Self {
76 value.clone()
77 }
78}
79
80const NAME_PATTERN: &str = r"^[a-z][a-z-]*$";
82
83static NAME_VALIDATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
84
85impl Tag {
86 pub fn new(tag: impl Into<String>) -> Result<Self, Error> {
87 let tag = tag.into();
88 if !NAME_VALIDATOR.is_match(&tag) {
89 bail!("Invalid tag {} must match [a-z][a-z-]*", tag);
90 }
91 Ok(Self(tag))
92 }
93
94 pub fn as_str(&self) -> &str {
95 self.0.as_ref()
96 }
97}
98
99impl ServiceName {
100 pub fn new(name: String) -> Result<Self, Error> {
101 if !NAME_VALIDATOR.is_match(&name) {
102 bail!("Invalid service name {} must match [a-z][a-z-]*", name);
103 }
104 Ok(Self(name))
105 }
106}
107
108impl Deref for Tag {
110 type Target = str;
111
112 fn deref(&self) -> &Self::Target {
113 self.as_str()
114 }
115}
116
117impl Deref for ServiceName {
119 type Target = str;
120
121 fn deref(&self) -> &Self::Target {
122 let Self(tag) = self;
123 tag
124 }
125}
126
127impl Borrow<str> for Tag {
129 fn borrow(&self) -> &str {
130 self
131 }
132}
133
134impl Borrow<str> for ServiceName {
137 fn borrow(&self) -> &str {
138 self
139 }
140}
141
142impl Display for Tag {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 let Self(name) = self;
145 name.fmt(f)
146 }
147}
148
149impl PartialEq<str> for Tag {
150 fn eq(&self, other: &str) -> bool {
151 self.0 == other
152 }
153}
154
155impl Display for ServiceName {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 let Self(name) = self;
158 name.fmt(f)
159 }
160}
161
162impl From<ServiceName> for String {
163 fn from(ServiceName(value): ServiceName) -> Self {
164 value
165 }
166}
167
168const CONFIG_GLOB: &str = "/config/data/*.persist";
169
170fn try_insert_items(config: &mut Config, config_text: &str) -> Result<(), Error> {
171 let items: Vec<TaggedPersist> = serde_json5::from_str(config_text)?;
172 for item in items {
173 let TaggedPersist { tag, service_name, mut tag_config } = item;
174 let tag = Tag::new(tag)?;
175 let name = ServiceName::new(service_name)?;
176 let mut name_filter = SameTreeNameFilter::default();
177 tag_config.selectors.retain(|s| name_filter.check(s));
178 if let Some(existing) = config.entry(name.clone()).or_default().insert(tag, tag_config) {
179 bail!("Duplicate TagConfig found: {:?}", existing);
180 }
181 }
182 Ok(())
183}
184
185#[derive(Default)]
187struct SameTreeNameFilter {
188 tree_names: Option<Option<fidl_fuchsia_diagnostics::TreeNames>>,
189}
190
191impl SameTreeNameFilter {
192 fn check(&mut self, s: &fidl_fuchsia_diagnostics::Selector) -> bool {
193 let tree_names = match &self.tree_names {
194 Some(names) => names,
195 None => {
196 self.tree_names = Some(s.tree_names.clone());
197 return true;
198 }
199 };
200 match (tree_names, &s.tree_names) {
201 (None, None) => true,
202 (
203 Some(fidl_fuchsia_diagnostics::TreeNames::All(_)),
204 Some(fidl_fuchsia_diagnostics::TreeNames::All(_)),
205 ) => true,
206 (
207 Some(fidl_fuchsia_diagnostics::TreeNames::Some(a)),
208 Some(fidl_fuchsia_diagnostics::TreeNames::Some(b)),
209 ) if a == b => true,
210 _ => {
211 log::warn!(
212 "Only selectors targeting the same tree are allowed: \"{}\"",
213 selectors::selector_to_string(s, selectors::SelectorDisplayOptions::default())
214 .unwrap_or_else(|e| format!("<INVALID: {e}>"))
215 );
216 false
217 }
218 }
219 }
220}
221
222pub fn load_configuration_files() -> Result<Config, Error> {
223 load_configuration_files_from(CONFIG_GLOB)
224}
225
226pub fn load_configuration_files_from(path: &str) -> Result<Config, Error> {
227 let mut config = HashMap::new();
228 for file_path in glob(path)? {
229 try_insert_items(&mut config, &std::fs::read_to_string(file_path?)?)?;
230 }
231 Ok(config)
232}
233
234#[cfg(test)]
235mod test {
236 use assert_matches::assert_matches;
237
238 use super::*;
239 use test_case::test_case;
240
241 #[fuchsia::test]
242 fn verify_insert_logic() {
243 let mut config = HashMap::new();
244 let taga_servab = r#"[
245 {
246 service_name: 'serv-a',
247 tag: 'tag-a',
248 max_bytes: 10,
249 min_seconds_between_fetch: 31,
250 selectors: ['INSPECT:a:b', 'INSPECT:b:c']
251 },
252 {
253 service_name: 'serv-b',
254 tag: 'tag-a',
255 max_bytes: 20,
256 min_seconds_between_fetch: 32,
257 selectors: ['INSPECT:c:d'],
258 persist_across_boot: true
259 }
260 ]"#;
261
262 let tagb_servb = r#"[
263 {
264 service_name: 'serv-b',
265 tag: 'tag-b',
266 max_bytes: 30,
267 min_seconds_between_fetch: 33,
268 selectors: ['INSPECT:d:e']
269 }
270 ]"#;
271
272 assert_matches!(try_insert_items(&mut config, taga_servab), Ok(()));
273 assert_matches!(try_insert_items(&mut config, tagb_servb), Ok(()));
274
275 assert_eq!(
276 config,
277 HashMap::from([
278 (
279 ServiceName("serv-a".to_string()),
280 HashMap::from([(
281 Tag("tag-a".to_string()),
282 TagConfig {
283 max_bytes: 10,
284 min_seconds_between_fetch: 31,
285 selectors: vec![
286 selectors::parse_verbose("a:b").unwrap(),
287 selectors::parse_verbose("b:c").unwrap(),
288 ],
289 persist_across_boot: false,
290 }
291 )])
292 ),
293 (
294 ServiceName("serv-b".to_string()),
295 HashMap::from([
296 (
297 Tag("tag-a".to_string()),
298 TagConfig {
299 max_bytes: 20,
300 min_seconds_between_fetch: 32,
301 selectors: vec![selectors::parse_verbose("c:d").unwrap()],
302 persist_across_boot: true,
303 }
304 ),
305 (
306 Tag("tag-b".to_string()),
307 TagConfig {
308 max_bytes: 30,
309 min_seconds_between_fetch: 33,
310 selectors: vec![selectors::parse_verbose("d:e").unwrap()],
311 persist_across_boot: false,
312 }
313 )
314 ])
315 )
316 ])
317 );
318
319 assert_matches!(try_insert_items(&mut config, tagb_servb), Err(_));
321 }
322
323 #[fuchsia::test]
324 fn test_tag_equals_str() {
325 assert_eq!(&Tag::new("foo").unwrap(), "foo");
326 }
327
328 #[test_case(
329 r#"[{
330 tag: 'tag',
331 service_name: 'bad-service-1',
332 max_bytes: 10,
333 min_seconds_between_fetch: 10,
334 selectors: ['INSPECT:a:b']
335 }]"#
336 ; "numbers_in_name"
337 )]
338 #[test_case(
339 r#"[{
340 tag: 'tag',
341 service_name: 'bad_service',
342 max_bytes: 10,
343 min_seconds_between_fetch: 10,
344 selectors: ['INSPECT:a:b']
345 }]"#
346 ; "underscores_in_name"
347 )]
348 #[test_case(
349 r#"[{
350 tag: 'tag',
351 service_name: 'service',
352 max_bytes: 10,
353 min_seconds_between_fetch: 10,
354 selectors: ['a:b']
355 }]"#
356 ; "selector_source_not_specified"
357 )]
358 #[test_case(
359 r#"[{
360 tag: 'tag',
361 service_name: 'service',
362 max_bytes: 10,
363 min_seconds_between_fetch: 10,
364 selectors: [
365 'INSPECT:a:b'
366 'INSPECT:a:[name=custom_tree]c'
367 ]
368 }]"#
369 ; "different_tree_names"
370 )]
371 #[fuchsia::test]
372 fn rejects_invalid_config(config_text: &str) {
373 let mut config = HashMap::new();
374 assert_matches!(try_insert_items(&mut config, config_text), Err(_));
375 }
376
377 #[test_case(
378 r#"[{
379 tag: 'tag',
380 service_name: 'service',
381 max_bytes: 10,
382 min_seconds_between_fetch: 10,
383 selectors: [
384 'INSPECT:a:[name=custom_tree]b',
385 'INSPECT:a:[name=custom_tree]c'
386 ]
387 }]"#
388 ; "same_custom_tree_names"
389 )]
390 #[fuchsia::test]
391 fn valid_config(config_text: &str) {
392 let mut config = HashMap::new();
393 assert_matches!(try_insert_items(&mut config, config_text), Ok(()));
394 }
395}