Skip to main content

objdiff_core/config/
mod.rs

1pub mod path;
2
3use alloc::{
4    borrow::Cow,
5    collections::BTreeMap,
6    string::{String, ToString},
7    vec::Vec,
8};
9
10use anyhow::{Context, Result, anyhow};
11use globset::{Glob, GlobSet, GlobSetBuilder};
12use path::unix_path_serde_option;
13use typed_path::Utf8UnixPathBuf;
14
15#[derive(Default, Clone)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
17pub struct ProjectConfig {
18    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
19    pub min_version: Option<String>,
20    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
21    pub custom_make: Option<String>,
22    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
23    pub custom_args: Option<Vec<String>>,
24    #[cfg_attr(
25        feature = "serde",
26        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
27    )]
28    pub target_dir: Option<Utf8UnixPathBuf>,
29    #[cfg_attr(
30        feature = "serde",
31        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
32    )]
33    pub base_dir: Option<Utf8UnixPathBuf>,
34    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
35    pub build_base: Option<bool>,
36    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
37    pub build_target: Option<bool>,
38    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
39    pub watch_patterns: Option<Vec<String>>,
40    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
41    pub ignore_patterns: Option<Vec<String>>,
42    #[cfg_attr(
43        feature = "serde",
44        serde(alias = "objects", skip_serializing_if = "Option::is_none")
45    )]
46    pub units: Option<Vec<ProjectObject>>,
47    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
48    pub progress_categories: Option<Vec<ProjectProgressCategory>>,
49    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
50    pub options: Option<ProjectOptions>,
51}
52
53impl ProjectConfig {
54    #[inline]
55    pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
56
57    #[inline]
58    pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
59        self.progress_categories.as_deref().unwrap_or_default()
60    }
61
62    #[inline]
63    pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
64        self.progress_categories.get_or_insert_with(Vec::new)
65    }
66
67    pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
68        Ok(if let Some(watch_patterns) = &self.watch_patterns {
69            watch_patterns
70                .iter()
71                .map(|s| Glob::new(s))
72                .collect::<Result<Vec<Glob>, globset::Error>>()?
73        } else {
74            default_watch_patterns()
75        })
76    }
77
78    pub fn build_ignore_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
79        Ok(if let Some(ignore_patterns) = &self.ignore_patterns {
80            ignore_patterns
81                .iter()
82                .map(|s| Glob::new(s))
83                .collect::<Result<Vec<Glob>, globset::Error>>()?
84        } else {
85            default_ignore_patterns()
86        })
87    }
88}
89
90#[derive(Default, Clone)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
92pub struct ProjectObject {
93    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
94    pub name: Option<String>,
95    #[cfg_attr(
96        feature = "serde",
97        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
98    )]
99    pub path: Option<Utf8UnixPathBuf>,
100    #[cfg_attr(
101        feature = "serde",
102        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
103    )]
104    pub target_path: Option<Utf8UnixPathBuf>,
105    #[cfg_attr(
106        feature = "serde",
107        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
108    )]
109    pub base_path: Option<Utf8UnixPathBuf>,
110    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
111    #[deprecated(note = "Use metadata.reverse_fn_order")]
112    pub reverse_fn_order: Option<bool>,
113    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
114    #[deprecated(note = "Use metadata.complete")]
115    pub complete: Option<bool>,
116    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
117    pub scratch: Option<ScratchConfig>,
118    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
119    pub metadata: Option<ProjectObjectMetadata>,
120    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
121    pub symbol_mappings: Option<BTreeMap<String, String>>,
122    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
123    pub options: Option<ProjectOptions>,
124}
125
126#[derive(Default, Clone)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
128pub struct ProjectObjectMetadata {
129    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
130    pub complete: Option<bool>,
131    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
132    pub reverse_fn_order: Option<bool>,
133    #[cfg_attr(
134        feature = "serde",
135        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
136    )]
137    pub source_path: Option<Utf8UnixPathBuf>,
138    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
139    pub progress_categories: Option<Vec<String>>,
140    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
141    pub auto_generated: Option<bool>,
142}
143
144#[derive(Default, Clone)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
146pub struct ProjectProgressCategory {
147    pub id: String,
148    pub name: String,
149}
150
151pub type ProjectOptions = BTreeMap<String, ProjectOptionValue>;
152
153#[derive(Clone, Debug)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
155pub enum ProjectOptionValue {
156    Bool(bool),
157    String(String),
158}
159
160impl ProjectObject {
161    pub fn name(&self) -> &str {
162        if let Some(name) = &self.name {
163            name
164        } else if let Some(path) = &self.path {
165            path.as_str()
166        } else {
167            "[unknown]"
168        }
169    }
170
171    pub fn complete(&self) -> Option<bool> {
172        #[expect(deprecated)]
173        self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
174    }
175
176    pub fn reverse_fn_order(&self) -> Option<bool> {
177        #[expect(deprecated)]
178        self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
179    }
180
181    pub fn hidden(&self) -> bool {
182        self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
183    }
184
185    pub fn source_path(&self) -> Option<&Utf8UnixPathBuf> {
186        self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
187    }
188
189    pub fn progress_categories(&self) -> &[String] {
190        self.metadata.as_ref().and_then(|m| m.progress_categories.as_deref()).unwrap_or_default()
191    }
192
193    pub fn auto_generated(&self) -> Option<bool> {
194        self.metadata.as_ref().and_then(|m| m.auto_generated)
195    }
196
197    pub fn options(&self) -> Option<&ProjectOptions> { self.options.as_ref() }
198}
199
200#[derive(Default, Clone, Eq, PartialEq)]
201#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
202pub struct ScratchConfig {
203    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
204    pub platform: Option<String>,
205    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
206    pub compiler: Option<String>,
207    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
208    pub c_flags: Option<String>,
209    #[cfg_attr(
210        feature = "serde",
211        serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
212    )]
213    pub ctx_path: Option<Utf8UnixPathBuf>,
214    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
215    pub build_ctx: Option<bool>,
216    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
217    pub preset_id: Option<u32>,
218}
219
220pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
221
222pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
223    "*.c", "*.cc", "*.cp", "*.cpp", "*.cxx", "*.c++", "*.h", "*.hh", "*.hp", "*.hpp", "*.hxx",
224    "*.h++", "*.pch", "*.pch++", "*.inc", "*.s", "*.S", "*.asm", "*.py", "*.yml", "*.txt",
225    "*.json",
226];
227
228pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"];
229
230pub fn default_watch_patterns() -> Vec<Glob> {
231    DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
232}
233
234pub fn default_ignore_patterns() -> Vec<Glob> {
235    DEFAULT_IGNORE_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
236}
237
238#[cfg(feature = "std")]
239#[derive(Clone, Eq, PartialEq)]
240pub struct ProjectConfigInfo {
241    pub path: std::path::PathBuf,
242    pub timestamp: Option<filetime::FileTime>,
243}
244
245#[cfg(feature = "std")]
246pub fn try_project_config(
247    dir: &std::path::Path,
248) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
249    for filename in CONFIG_FILENAMES.iter() {
250        let config_path = dir.join(filename);
251        let Ok(file) = std::fs::File::open(&config_path) else {
252            continue;
253        };
254        let metadata = file.metadata();
255        if let Ok(metadata) = metadata {
256            if !metadata.is_file() {
257                continue;
258            }
259            let ts = filetime::FileTime::from_last_modification_time(&metadata);
260            let mut reader = std::io::BufReader::new(file);
261            let mut result = read_json_config(&mut reader);
262            if let Ok(config) = &result {
263                // Validate min_version if present
264                if let Err(e) = validate_min_version(config) {
265                    result = Err(e);
266                }
267            }
268            return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
269        }
270    }
271    None
272}
273
274#[cfg(feature = "std")]
275pub fn save_project_config(
276    config: &ProjectConfig,
277    info: &ProjectConfigInfo,
278) -> Result<ProjectConfigInfo> {
279    if let Some(last_ts) = info.timestamp {
280        // Check if the file has changed since we last read it
281        if let Ok(metadata) = std::fs::metadata(&info.path) {
282            let ts = filetime::FileTime::from_last_modification_time(&metadata);
283            if ts != last_ts {
284                return Err(anyhow!("Config file has changed since last read"));
285            }
286        }
287    }
288    let mut writer = std::io::BufWriter::new(
289        std::fs::File::create(&info.path).context("Failed to create config file")?,
290    );
291    let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
292    match ext {
293        "json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
294        _ => Err(anyhow!("Unknown config file extension: {ext}")),
295    }?;
296    let file = writer.into_inner().context("Failed to flush file")?;
297    let metadata = file.metadata().context("Failed to get file metadata")?;
298    let ts = filetime::FileTime::from_last_modification_time(&metadata);
299    Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
300}
301
302fn validate_min_version(config: &ProjectConfig) -> Result<()> {
303    let Some(min_version) = &config.min_version else { return Ok(()) };
304    let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
305        .map_err(|e| anyhow::Error::msg(e.to_string()))
306        .context("Failed to parse package version")?;
307    let min_version = semver::Version::parse(min_version)
308        .map_err(|e| anyhow::Error::msg(e.to_string()))
309        .context("Failed to parse min_version")?;
310    if version >= min_version {
311        Ok(())
312    } else {
313        Err(anyhow!("Project requires objdiff version {min_version} or higher"))
314    }
315}
316
317#[cfg(feature = "std")]
318fn read_json_config<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
319    Ok(serde_json::from_reader(reader)?)
320}
321
322pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
323    let mut builder = GlobSetBuilder::new();
324    for glob in vec {
325        builder.add(glob.clone());
326    }
327    builder.build()
328}
329
330#[cfg(feature = "any-arch")]
331pub fn apply_project_options(
332    diff_config: &mut crate::diff::DiffObjConfig,
333    options: &ProjectOptions,
334) -> Result<()> {
335    use core::str::FromStr;
336
337    use crate::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind};
338
339    let mut result = Ok(());
340    for (key, value) in options.iter() {
341        let property_id = ConfigPropertyId::from_str(key)
342            .map_err(|()| anyhow!("Invalid configuration property: {key}"))?;
343        let value = match value {
344            ProjectOptionValue::Bool(value) => Cow::Borrowed(if *value { "true" } else { "false" }),
345            ProjectOptionValue::String(value) => Cow::Borrowed(value.as_str()),
346        };
347        if diff_config.set_property_value_str(property_id, &value).is_err() {
348            if result.is_err() {
349                // Already returning an error, skip further errors
350                continue;
351            }
352            let mut expected = String::new();
353            match property_id.kind() {
354                ConfigPropertyKind::Boolean => expected.push_str("true, false"),
355                ConfigPropertyKind::Choice(variants) => {
356                    for (idx, variant) in variants.iter().enumerate() {
357                        if idx > 0 {
358                            expected.push_str(", ");
359                        }
360                        expected.push_str(variant.value);
361                    }
362                }
363            }
364            result = Err(anyhow!(
365                "Invalid value for {}. Expected one of: {}",
366                property_id.name(),
367                expected
368            ));
369        }
370    }
371    result
372}