lisensor/
config.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025-2026 Pistonite
3
4use std::collections::BTreeMap;
5use std::ops::Deref;
6use std::path::Path;
7use std::sync::Arc;
8
9use cu::pre::*;
10
11/// Try finding the default config files according to the order
12/// specified in the documentation (see repo README)
13pub fn try_find_default_config_file() -> Option<&'static str> {
14    ["Lisensor.toml", "lisensor.toml"]
15        .into_iter()
16        .find(|x| Path::new(x).exists())
17}
18
19/// Config object
20#[derive(Debug, Default, Clone, PartialEq, Eq)]
21pub struct Config {
22    // glob -> (holder, license)
23    globs: BTreeMap<String, (Arc<String>, Arc<String>)>,
24}
25
26/// Raw config read from a toml config file.
27///
28/// The format is holder -> glob -> license
29#[derive(Deserialize)]
30struct TomlConfig(BTreeMap<String, BTreeMap<String, String>>);
31
32impl Config {
33    /// Create a config object from a single holder and license,
34    /// with multiple glob patterns.
35    pub fn new(holder: String, license: String, glob_list: Vec<String>) -> Self {
36        let holder = Arc::new(holder);
37        let license = Arc::new(license);
38        let mut globs = BTreeMap::new();
39        for glob in glob_list {
40            use std::collections::btree_map::Entry;
41            match globs.entry(glob) {
42                Entry::Vacant(entry) => {
43                    entry.insert((Arc::clone(&holder), Arc::clone(&license)));
44                }
45                Entry::Occupied(entry) => {
46                    let glob = entry.key();
47                    cu::warn!("glob '{glob}' is specfied multiple times!");
48                }
49            }
50        }
51        Self { globs }
52    }
53
54    /// Build the config by reading the file specified, error if conflicts are detected
55    ///
56    /// The globs specified in the config file are relative to the parent directory
57    /// of `path`.
58    pub fn build(path: &str) -> cu::Result<Self> {
59        let raw = toml::parse::<TomlConfig>(&cu::fs::read_string(path)?)?;
60        let parent = Path::new(path)
61            .parent()
62            .context("failed to get parent path for config")?;
63        let mut globs = BTreeMap::new();
64        for (holder, table) in raw.0 {
65            let holder = Arc::new(holder);
66            for (glob, license) in table {
67                // globs in config files are resolved relative
68                // to the directory where the config file is in
69                let glob = parent.join(glob).into_utf8()?;
70                use std::collections::btree_map::Entry;
71                match globs.entry(glob) {
72                    Entry::Vacant(entry) => {
73                        entry.insert((Arc::clone(&holder), Arc::new(license)));
74                    }
75                    Entry::Occupied(entry) => {
76                        let glob = entry.key();
77                        let (curr_holder, curr_license) = entry.get();
78                        if *curr_holder == holder && curr_license.deref() == license.as_str() {
79                            cu::warn!("glob '{glob}' specified multiple times in '{path}'!");
80                            continue;
81                        }
82                        cu::error!("conflicting config specified for glob '{glob}':");
83                        cu::error!(
84                            "- in one config, it has holder '{holder}' and license '{license}'"
85                        );
86                        cu::error!(
87                            "- in another, it has holder '{curr_holder}' and license '{curr_license}'"
88                        );
89                        cu::bail!("conflicting config detected!");
90                    }
91                }
92            }
93        }
94        Ok(Self { globs })
95    }
96
97    /// Merge another config into self, error if conflicts are detected
98    pub fn absorb(&mut self, other: Self) -> cu::Result<()> {
99        for (glob, (holder, license)) in other.globs {
100            use std::collections::btree_map::Entry;
101            match self.globs.entry(glob) {
102                Entry::Vacant(entry) => {
103                    entry.insert((holder, license));
104                }
105                Entry::Occupied(entry) => {
106                    let glob = entry.key();
107                    let (curr_holder, curr_license) = entry.get();
108                    if *curr_holder == holder && curr_license.deref() == license.deref() {
109                        cu::warn!("glob '{glob}' specified multiple times in multiple configs!");
110                        continue;
111                    }
112                    cu::error!(
113                        "conflicting config specified for glob '{glob}' in multiple configs:"
114                    );
115                    cu::error!("- in one config, it has holder '{holder}' and license '{license}'");
116                    cu::error!(
117                        "- in another, it has holder '{curr_holder}' and license '{curr_license}'"
118                    );
119                    cu::bail!("conflicting config detected!");
120                }
121            }
122        }
123        Ok(())
124    }
125}
126
127impl Config {
128    /// Iterate the resolve paths as (path, holder, license)
129    #[allow(clippy::should_implement_trait)]
130    pub fn into_iter(self) -> impl Iterator<Item = (String, Arc<String>, Arc<String>)> {
131        // we can't implement the IntoIterator trait because
132        // the map object has an unnamed function type
133        self.globs
134            .into_iter()
135            .map(|(path, (holder, license))| (path, holder, license))
136    }
137}