Skip to main content

abi_audit_core/
config.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result, bail};
6use serde::{Deserialize, Serialize};
7
8use crate::model::HeaderSyncTool;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct AbiAuditConfig {
12    #[serde(default = "default_version")]
13    pub version: u32,
14    #[serde(default = "default_snapshot_path")]
15    pub snapshot: PathBuf,
16    #[serde(default)]
17    pub baseline: BaselineConfig,
18    #[serde(default)]
19    pub rules: BTreeMap<String, RuleConfig>,
20    #[serde(default)]
21    pub targets: Vec<TargetConfig>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(untagged)]
26pub enum BaselineConfig {
27    Path(PathBuf),
28    Source(BaselineSourceConfig),
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub struct BaselineSourceConfig {
33    #[serde(default)]
34    pub kind: BaselineSourceKind,
35    pub path: PathBuf,
36    #[serde(default = "default_baseline_artifact_snapshot")]
37    pub snapshot: PathBuf,
38}
39
40#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
41#[serde(rename_all = "snake_case")]
42pub enum BaselineSourceKind {
43    #[default]
44    Snapshot,
45    ArtifactDir,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct TargetConfig {
50    pub package: String,
51    #[serde(default)]
52    pub headers: Vec<PathBuf>,
53    #[serde(default)]
54    pub header_sync: Option<HeaderSyncConfig>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct HeaderSyncConfig {
59    #[serde(default)]
60    pub tool: HeaderSyncTool,
61    pub output: PathBuf,
62    #[serde(default)]
63    pub config: Option<PathBuf>,
64    #[serde(default)]
65    pub crate_dir: Option<PathBuf>,
66    #[serde(default = "default_verify_freshness")]
67    pub verify_freshness: bool,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
71pub struct RuleConfig {
72    #[serde(default)]
73    pub severity: Option<RuleSeverity>,
74}
75
76#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "snake_case")]
78pub enum RuleSeverity {
79    Off,
80    Warning,
81    Error,
82}
83
84#[derive(Debug, Clone)]
85pub struct InitOptions {
86    pub path: PathBuf,
87    pub force: bool,
88}
89
90impl Default for AbiAuditConfig {
91    fn default() -> Self {
92        Self {
93            version: default_version(),
94            snapshot: default_snapshot_path(),
95            baseline: BaselineConfig::default(),
96            rules: BTreeMap::new(),
97            targets: Vec::new(),
98        }
99    }
100}
101
102impl Default for BaselineConfig {
103    fn default() -> Self {
104        Self::Path(default_baseline_path())
105    }
106}
107
108pub fn load_config(workspace_root: &Path, explicit_path: Option<&Path>) -> Result<AbiAuditConfig> {
109    let path = explicit_path
110        .map(PathBuf::from)
111        .unwrap_or_else(|| workspace_root.join("abi-audit.toml"));
112    if !path.exists() {
113        return Ok(AbiAuditConfig::default());
114    }
115
116    let text = fs::read_to_string(&path)
117        .with_context(|| format!("failed to read config at {}", path.display()))?;
118    let config: AbiAuditConfig = toml::from_str(&text)
119        .with_context(|| format!("failed to parse config at {}", path.display()))?;
120    if config.version != 1 {
121        bail!(
122            "unsupported config version {} in {}",
123            config.version,
124            path.display()
125        );
126    }
127    Ok(config)
128}
129
130pub fn write_starter_config(options: &InitOptions) -> Result<PathBuf> {
131    if options.path.exists() && !options.force {
132        bail!(
133            "refusing to overwrite existing config at {} (use --force to replace it)",
134            options.path.display()
135        );
136    }
137
138    if let Some(parent) = options.path.parent() {
139        fs::create_dir_all(parent).with_context(|| {
140            format!(
141                "failed to create parent directory for {}",
142                options.path.display()
143            )
144        })?;
145    }
146
147    let template = r#"# Leave `headers` empty to auto-discover `include/**/*.h` in the target package.
148# `header_sync` is optional and records an explicit cbindgen workflow for freshness checks.
149version = 1
150snapshot = "abi-audit/snapshot.json"
151baseline = "abi-audit/baseline.json"
152
153[[targets]]
154package = "your-ffi-crate"
155headers = ["include/your_ffi_crate.h"]
156
157[targets.header_sync]
158tool = "cbindgen"
159output = "include/your_ffi_crate.h"
160config = "cbindgen.toml"
161verify_freshness = true
162
163[rules.baseline-drift]
164severity = "error"
165"#;
166    fs::write(&options.path, template)
167        .with_context(|| format!("failed to write {}", options.path.display()))?;
168    Ok(options.path.clone())
169}
170
171const fn default_version() -> u32 {
172    1
173}
174
175fn default_snapshot_path() -> PathBuf {
176    PathBuf::from("abi-audit/snapshot.json")
177}
178
179fn default_baseline_path() -> PathBuf {
180    PathBuf::from("abi-audit/baseline.json")
181}
182
183fn default_baseline_artifact_snapshot() -> PathBuf {
184    PathBuf::from("snapshot.json")
185}
186
187const fn default_verify_freshness() -> bool {
188    true
189}