safety_parser/
configuration.rs

1//! Property definition through config file.
2use indexmap::IndexMap;
3use serde::Deserialize;
4use std::{
5    env::{self, var},
6    fs,
7    path::Path,
8    sync::LazyLock,
9};
10
11pub type Str = Box<str>;
12pub type OptStr = Option<Box<str>>;
13
14#[derive(Debug, Deserialize)]
15pub struct Configuration {
16    pub package: Option<Package>,
17    pub tag: IndexMap<Str, Tag>,
18    #[serde(default)]
19    pub doc: GenDocOption,
20}
21
22impl Configuration {
23    pub fn read_toml(path: &str) -> Self {
24        if !fs::exists(path).unwrap() {
25            panic!("{path:?} doesn't exist.")
26        }
27        let text =
28            &fs::read_to_string(path).unwrap_or_else(|e| panic!("Failed to read {path}:\n{e}"));
29        toml::from_str(text).unwrap_or_else(|e| panic!("Failed to parse {path}:\n{e}"))
30    }
31}
32
33#[derive(Debug, Deserialize)]
34pub struct Package {
35    pub name: Str,
36    pub version: OptStr,
37    pub crate_name: OptStr,
38}
39
40#[derive(Debug, Deserialize)]
41pub struct Tag {
42    #[serde(default)]
43    pub args: Box<[Str]>,
44    pub desc: OptStr,
45    pub expr: OptStr,
46    #[serde(default = "default_types")]
47    pub types: Box<[TagType]>,
48    pub url: OptStr,
49}
50
51#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)]
52#[serde(rename_all = "lowercase")]
53pub enum TagType {
54    #[default]
55    Precond,
56    Hazard,
57    Option,
58}
59
60impl TagType {
61    pub fn new(s: &str) -> Self {
62        match s {
63            "precond" => Self::Precond,
64            "hazard" => Self::Hazard,
65            "option" => Self::Option,
66            _ => panic!("Only support: precond, hazard, and option."),
67        }
68    }
69}
70
71/// If types field doesn't exist, default to Precond.
72fn default_types() -> Box<[TagType]> {
73    Box::new([TagType::Precond])
74}
75
76#[derive(Clone, Copy, Debug, Deserialize, Default)]
77pub struct GenDocOption {
78    /// Generate `/// Safety` at the beginning.
79    #[serde(default)]
80    pub heading_safety_title: bool,
81    /// Generate `Tag:` before `desc`.
82    #[serde(default)]
83    pub heading_tag: bool,
84}
85
86impl GenDocOption {
87    fn merge(&mut self, other: &Self) {
88        if other.heading_safety_title {
89            self.heading_safety_title = true;
90        }
91        if other.heading_tag {
92            self.heading_tag = true;
93        }
94    }
95}
96
97/// Single toml config file path.
98pub const ENV_SP_FILE: &str = "SP_FILE";
99/// Folder where all toml files are searched.
100pub const ENV_SP_DIR: &str = "SP_DIR";
101/// SP file to crate being compiled.
102pub const LOCAL_SP_FILE: &str = "safety-tags.toml";
103/// SP folder to crate being compiled.
104pub const LOCAL_SP_DIR: &str = "safety-tags";
105
106/// If ENV_SP_DIR or ENV_SP_DIR is provided, check tag and emit `#[doc]` for each tag.
107/// If neither is provided, do nothing.
108pub fn config_exists() -> bool {
109    static EMIT: LazyLock<bool> = LazyLock::new(|| {
110        crate_sp_paths().is_some() || var(ENV_SP_FILE).is_ok() || var(ENV_SP_DIR).is_ok()
111    });
112    *EMIT
113}
114
115/// Paths to toml config.
116///
117/// First, search `safety-tags.toml` or `safety-tags` folder
118/// in the crate being compiled:
119/// * `CARGO_MANIFEST_DIR/safety-tags.toml`
120/// * `CARGO_MANIFEST_DIR/safety-tags/`
121/// * if both exist, only respect safety-tags.toml
122///
123/// If no toml found, pass one of these env vars:
124/// * if `SP_FILE` is specified, use that toml path
125/// * if `SP_DIR` is specified, use that path to find toml files
126/// * if both are given, only respect `SP_FILE`
127pub fn toml_file_paths() -> Vec<String> {
128    if let Some(paths) = crate_sp_paths() {
129        paths
130    } else if let Ok(file) = env::var(ENV_SP_FILE) {
131        vec![file]
132    } else if let Ok(dir) = env::var(ENV_SP_DIR) {
133        list_toml_files(&dir)
134    } else {
135        panic!("Environment variable `SP_FILE` or `SP_DIR` should be specified.");
136    }
137}
138
139fn list_toml_files(dir: &str) -> Vec<String> {
140    let mut files = Vec::new();
141    for entry in fs::read_dir(dir).unwrap_or_else(|e| panic!("Failed to read {dir} folder:\n{e}")) {
142        let entry = entry.unwrap();
143        let path = entry.path();
144        if path.extension().map(|ext| ext == "toml").unwrap_or(false) {
145            files.push(path.into_os_string().into_string().unwrap());
146        }
147    }
148    files
149}
150
151/// Search in the crate being compiled, i.e. `CARGO_MANIFEST_DIR/safety-tags.toml`
152/// or `CARGO_MANIFEST_DIR/safety-tags/`.
153pub fn crate_sp_paths() -> Option<Vec<String>> {
154    if let Ok(dir) = env::var("CARGO_MANIFEST_DIR") {
155        let dir = Path::new(&*dir);
156        let sp_file = dir.join(LOCAL_SP_FILE);
157        let sp_dir = dir.join(LOCAL_SP_DIR);
158        if sp_file.exists() {
159            return Some(vec![sp_file.to_str()?.to_owned()]);
160        } else if sp_dir.exists() {
161            return Some(list_toml_files(sp_dir.to_str()?));
162        }
163    }
164    None
165}
166
167/// `any` tag is denied in user's spec, and special in doc generation.
168pub const ANY: &str = "any";
169
170/// Data shared in `#[safety]` proc macro.
171#[derive(Debug)]
172struct Key {
173    /// Tag defined in config file.
174    tag: Tag,
175    /// File path where the tag is defined: we must be sure each tag only
176    /// derives from single file path.
177    #[allow(dead_code)]
178    src: Str,
179}
180
181#[derive(Default)]
182struct Cache {
183    /// Defined tags.
184    map: IndexMap<Str, Key>,
185    /// Merged doc generation options: if any is true, set true.
186    doc: GenDocOption,
187}
188
189static CACHE: LazyLock<Cache> = LazyLock::new(|| {
190    let mut cache = Cache::default();
191
192    let configs: Vec<_> = toml_file_paths()
193        .into_iter()
194        .map(|f| (Configuration::read_toml(&f), f.into_boxed_str()))
195        .collect();
196    let cap = configs.iter().map(|c| c.0.tag.len()).sum();
197    cache.map.reserve(cap);
198
199    for (config, path) in configs {
200        for (name, tag) in config.tag {
201            if &*name == ANY {
202                panic!("`any` is a builtin tag. Please remove it from spec.");
203            }
204            if let Some(old) = cache.map.get(&name) {
205                panic!("Tag {name:?} has been defined: {old:?}");
206            }
207            _ = cache.map.insert(name, Key { tag, src: path.clone() });
208        }
209        cache.doc.merge(&config.doc);
210    }
211
212    cache.map.sort_unstable_keys();
213    eprintln!("Got {} tags.", cache.map.len());
214    cache
215});
216
217pub fn get_tag(name: &str) -> &'static Tag {
218    &CACHE.map.get(name).unwrap_or_else(|| panic!("Tag {name:?} is not defined")).tag
219}
220
221pub fn doc_option() -> GenDocOption {
222    CACHE.doc
223}