use std::{
collections::HashMap,
error::Error,
fmt, fs,
path::{Path, PathBuf},
};
use lazy_static::lazy_static;
use path_clean::clean;
use crate::{
cache::{Cache, CdiOption},
spec::{read_spec, Spec},
utils::is_cdi_spec,
};
const DEFAULT_STATIC_DIR: &str = "/etc/cdi";
const DEFAULT_DYNAMIC_DIR: &str = "/var/run/cdi";
lazy_static! {
pub static ref DEFAULT_SPEC_DIRS: &'static [&'static str] = &[
DEFAULT_STATIC_DIR,
DEFAULT_DYNAMIC_DIR,
];
}
#[derive(Debug)]
pub struct SpecError {
message: String,
}
impl SpecError {
pub fn new(info: &str) -> Self {
Self {
message: info.to_owned(),
}
}
}
impl fmt::Display for SpecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "spec error message {}", self.message)
}
}
impl Error for SpecError {}
pub fn convert_errors(
spec_errors: &HashMap<String, Vec<Box<dyn Error>>>,
) -> HashMap<String, Vec<Box<dyn Error + Send + Sync + 'static>>> {
spec_errors
.iter()
.map(|(key, value)| {
(
key.clone(),
value
.iter()
.map(|error| {
Box::new(SpecError::new(&error.to_string()))
as Box<dyn Error + Send + Sync + 'static>
})
.collect(),
)
})
.collect()
}
pub fn with_spec_dirs(dirs: &[&str]) -> CdiOption {
let cleaned_dirs: Vec<String> = dirs
.iter()
.map(|dir| {
clean(PathBuf::from(*dir))
.into_os_string()
.into_string()
.unwrap()
})
.collect();
Box::new(move |cache: &mut Cache| {
cache.spec_dirs.clone_from(&cleaned_dirs);
})
}
#[allow(dead_code)]
fn traverse_dir<F>(dir_path: &Path, traverse_fn: &mut F) -> Result<(), Box<dyn Error>>
where
F: FnMut(&Path) -> Result<(), Box<dyn Error>>,
{
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
traverse_dir(&path, traverse_fn)?;
} else {
traverse_fn(&path)?;
}
}
}
Ok(())
}
#[allow(dead_code)]
pub(crate) fn scan_spec_dirs<P: AsRef<Path>>(dirs: &[P]) -> Result<Vec<Spec>, Box<dyn Error>> {
let mut scaned_specs = Vec::new();
for (priority, dir) in dirs.iter().enumerate() {
let dir_path = dir.as_ref();
if !dir_path.is_dir() {
continue;
}
let mut operation = |path: &Path| -> Result<(), Box<dyn Error>> {
if !path.is_dir() && is_cdi_spec(path) {
let spec = match read_spec(&path.to_path_buf(), priority as i32) {
Ok(spec) => spec,
Err(err) => {
return Err(Box::new(SpecError::new(&err.to_string())));
}
};
scaned_specs.push(spec);
}
Ok(())
};
if let Err(e) = traverse_dir(dir_path, &mut operation) {
return Err(Box::new(SpecError::new(&e.to_string())));
}
}
Ok(scaned_specs)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::fs;
const SPEC_YAML: &str = r#"cdiVersion: "0.6.0"
kind: "vendor.com/device"
devices:
- name: "gpu0"
containerEdits:
env:
- "VENDOR=1"
"#;
const SPEC_JSON: &str = r#"{
"cdiVersion": "0.6.0",
"kind": "vendor2.com/device",
"devices": [
{ "name": "d0", "containerEdits": { "env": ["OTHER=1"] } }
]
}"#;
#[test]
fn with_spec_dirs_cleans_paths() {
let mut cache = Cache::default();
with_spec_dirs(&["/etc/cdi/../cdi", "/var/run/cdi/"])(&mut cache);
assert_eq!(cache.spec_dirs, vec!["/etc/cdi", "/var/run/cdi"]);
}
#[test]
fn spec_error_display_and_conversion() {
let mut errors: HashMap<String, Vec<Box<dyn Error>>> = HashMap::new();
errors.insert(
"/etc/cdi/broken.yaml".to_string(),
vec![Box::new(SpecError::new("bad spec"))],
);
let converted = convert_errors(&errors);
let msgs = &converted["/etc/cdi/broken.yaml"];
assert_eq!(msgs.len(), 1);
assert!(msgs[0].to_string().contains("bad spec"));
}
#[test]
fn scan_finds_specs_recursively_with_dir_index_as_priority() {
let low = tempfile::tempdir().unwrap();
let high = tempfile::tempdir().unwrap();
fs::write(low.path().join("vendor.yaml"), SPEC_YAML).unwrap();
let nested = high.path().join("nested");
fs::create_dir(&nested).unwrap();
fs::write(nested.join("vendor2.json"), SPEC_JSON).unwrap();
fs::write(high.path().join("README.txt"), "not a spec").unwrap();
let specs = scan_spec_dirs(&[low.path(), high.path()]).unwrap();
assert_eq!(specs.len(), 2);
let by_vendor: HashMap<_, _> = specs
.iter()
.map(|s| (s.get_vendor().to_string(), s.get_priority()))
.collect();
assert_eq!(by_vendor["vendor.com"], 0);
assert_eq!(by_vendor["vendor2.com"], 1);
}
#[test]
fn scan_skips_missing_dirs() {
let specs = scan_spec_dirs(&["/nonexistent/cdi/dir"]).unwrap();
assert!(specs.is_empty());
}
#[test]
fn scan_fails_on_invalid_spec() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("broken.yaml"), "cdiVersion: [not a spec").unwrap();
assert!(scan_spec_dirs(&[dir.path()]).is_err());
}
}