safety_parser/
configuration.rs1use 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
71fn default_types() -> Box<[TagType]> {
73 Box::new([TagType::Precond])
74}
75
76#[derive(Clone, Copy, Debug, Deserialize, Default)]
77pub struct GenDocOption {
78 #[serde(default)]
80 pub heading_safety_title: bool,
81 #[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
97pub const ENV_SP_FILE: &str = "SP_FILE";
99pub const ENV_SP_DIR: &str = "SP_DIR";
101pub const LOCAL_SP_FILE: &str = "safety-tags.toml";
103pub const LOCAL_SP_DIR: &str = "safety-tags";
105
106pub 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
115pub 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
151pub 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
167pub const ANY: &str = "any";
169
170#[derive(Debug)]
172struct Key {
173 tag: Tag,
175 #[allow(dead_code)]
178 src: Str,
179}
180
181#[derive(Default)]
182struct Cache {
183 map: IndexMap<Str, Key>,
185 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}