1use serde::Deserialize;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
5pub struct FileConfig {
6 pub format: Option<String>,
7 pub baseline: Option<PathBuf>,
8 pub md_out: Option<PathBuf>,
9 pub agent_pack: Option<PathBuf>,
10 pub agent_pack_format: Option<String>,
11 pub profile: Option<String>,
12 pub fail_on: Option<String>,
13 pub timings: Option<String>,
14 pub include: Option<Vec<String>>,
15 pub exclude: Option<Vec<String>>,
16}
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct CliOverrides {
20 pub format: Option<String>,
21 pub baseline: Option<PathBuf>,
22 pub md_out: Option<PathBuf>,
23 pub agent_pack: Option<PathBuf>,
24 pub agent_pack_format: Option<String>,
25 pub profile: Option<String>,
26 pub fail_on: Option<String>,
27 pub timings: Option<String>,
28 pub include: Vec<String>,
29 pub exclude: Vec<String>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RuntimeConfig {
34 pub format: String,
35 pub baseline: Option<PathBuf>,
36 pub md_out: Option<PathBuf>,
37 pub agent_pack: Option<PathBuf>,
38 pub agent_pack_format: String,
39 pub profile: String,
40 pub fail_on: String,
41 pub timings: String,
42 pub include: Vec<String>,
43 pub exclude: Vec<String>,
44}
45
46pub fn load_file_config(config_path: Option<&Path>) -> Result<FileConfig, miette::Report> {
47 let Some(path) = resolve_config_path(config_path) else {
48 return Ok(FileConfig::default());
49 };
50
51 let raw = std::fs::read_to_string(&path)
52 .map_err(|err| miette::miette!("Failed to read config file {}: {}", path.display(), err))?;
53 toml::from_str(&raw)
54 .map_err(|err| miette::miette!("Failed to parse config file {}: {}", path.display(), err))
55}
56
57pub fn resolve_runtime_config(file: FileConfig, cli: CliOverrides) -> RuntimeConfig {
58 RuntimeConfig {
59 format: cli
60 .format
61 .or(file.format)
62 .unwrap_or_else(|| "table".to_string()),
63 baseline: cli.baseline.or(file.baseline),
64 md_out: cli.md_out.or(file.md_out),
65 agent_pack: cli.agent_pack.or(file.agent_pack),
66 agent_pack_format: cli
67 .agent_pack_format
68 .or(file.agent_pack_format)
69 .unwrap_or_else(|| "json".to_string()),
70 profile: cli
71 .profile
72 .or(file.profile)
73 .unwrap_or_else(|| "full".to_string()),
74 fail_on: cli
75 .fail_on
76 .or(file.fail_on)
77 .unwrap_or_else(|| "error".to_string()),
78 timings: cli
79 .timings
80 .or(file.timings)
81 .unwrap_or_else(|| "off".to_string()),
82 include: if cli.include.is_empty() {
83 file.include.unwrap_or_default()
84 } else {
85 cli.include
86 },
87 exclude: if cli.exclude.is_empty() {
88 file.exclude.unwrap_or_default()
89 } else {
90 cli.exclude
91 },
92 }
93}
94
95fn resolve_config_path(config_path: Option<&Path>) -> Option<PathBuf> {
96 if let Some(path) = config_path {
97 return Some(path.to_path_buf());
98 }
99
100 let default = PathBuf::from("verifyos.toml");
101 if default.exists() {
102 Some(default)
103 } else {
104 None
105 }
106}