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 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 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 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}