use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::Deserialize;
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct WatchConfig {
pub debounce_secs: Option<u64>,
pub output_dir: Option<PathBuf>,
pub db_path: Option<PathBuf>,
pub projects: Vec<ProjectConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ProjectConfig {
pub name: String,
pub sbom: PathBuf,
#[serde(default)]
pub configs: Vec<ConfigEntry>,
#[serde(default)]
pub device_trees: Vec<DtsEntry>,
pub rules: Option<PathBuf>,
pub author: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ConfigEntry {
#[serde(rename = "type")]
pub config_type: String,
pub path: PathBuf,
}
#[derive(Debug, Deserialize, Clone)]
pub struct DtsEntry {
pub path: PathBuf,
}
impl ProjectConfig {
pub fn watched_paths(&self) -> Vec<PathBuf> {
let mut paths = vec![self.sbom.clone()];
for cfg in &self.configs {
paths.push(cfg.path.clone());
}
for dts in &self.device_trees {
paths.push(dts.path.clone());
}
if let Some(ref rules) = self.rules {
paths.push(rules.clone());
}
paths
}
}
pub fn load_watch_config(path: &PathBuf) -> Result<WatchConfig> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read watch config: {}", path.display()))?;
let config: WatchConfig = toml::from_str(&content)
.with_context(|| format!("Failed to parse watch config: {}", path.display()))?;
info!(
"Loaded {} projects from {}",
config.projects.len(),
path.display()
);
for project in &config.projects {
info!(
" Project '{}': {} configs, {} device trees",
project.name,
project.configs.len(),
project.device_trees.len()
);
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_parse_watch_config() {
let mut file = tempfile::NamedTempFile::new().unwrap();
writeln!(
file,
r#"
debounce_secs = 10
output_dir = "./reports"
db_path = "/tmp/test.db"
[[projects]]
name = "Test Project"
sbom = "build/spdx.json"
[[projects.configs]]
type = "kernel"
path = "build/.config"
[[projects.configs]]
type = "uboot"
path = "build/u-boot/.config"
[[projects.device_trees]]
path = "build/board.dts"
[[projects]]
name = "Second Project"
sbom = "build/spdx2.json"
[[projects.configs]]
type = "kernel"
path = "build/.config2"
"#
)
.unwrap();
let config = load_watch_config(&file.path().to_path_buf()).unwrap();
assert_eq!(config.debounce_secs, Some(10));
assert_eq!(config.projects.len(), 2);
assert_eq!(config.projects[0].name, "Test Project");
assert_eq!(config.projects[0].configs.len(), 2);
assert_eq!(config.projects[0].device_trees.len(), 1);
assert_eq!(config.projects[1].name, "Second Project");
}
#[test]
fn test_watched_paths() {
let project = ProjectConfig {
name: "test".into(),
sbom: "sbom.json".into(),
configs: vec![
ConfigEntry {
config_type: "kernel".into(),
path: ".config".into(),
},
ConfigEntry {
config_type: "uboot".into(),
path: "uboot.config".into(),
},
],
device_trees: vec![DtsEntry {
path: "board.dts".into(),
}],
rules: Some("bitvex.toml".into()),
author: None,
};
let paths = project.watched_paths();
assert_eq!(paths.len(), 5); }
}