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}