//! Compatibility shim for code extracted from herdr's `src/detect/`. //! //! herdr's `manifest_update.rs` handled remote catalog fetching, version //! comparison, and on-disk manifest storage. dingger keeps the version type, //! the engine version contract, and the local storage paths; remote fetching //! is an explicit CLI action that populates the remote store directory. use std::path::PathBuf; use crate::{agent_label, Agent}; /// Engine contract version declared by the manifest schema in use. Remote /// manifests with a higher `min_engine_version` are rejected. pub(crate) const MANIFEST_ENGINE_VERSION: u32 = 3; pub(crate) fn config_dir() -> PathBuf { if let Some(dir) = std::env::var_os("DINGGER_CONFIG_DIR") { return PathBuf::from(dir); } if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME") { return PathBuf::from(dir).join("dingger"); } dirs_home() .map(|home| home.join(".config").join("dingger")) .unwrap_or_else(|| PathBuf::from(".dingger")) } pub(crate) fn state_dir() -> PathBuf { if let Some(dir) = std::env::var_os("DINGGER_STATE_DIR") { return PathBuf::from(dir); } if let Some(dir) = std::env::var_os("XDG_STATE_HOME") { return PathBuf::from(dir).join("dingger"); } dirs_home() .map(|home| home.join(".local").join("state").join("dingger")) .unwrap_or_else(|| PathBuf::from(".dingger")) } fn dirs_home() -> Option { std::env::var_os("HOME").map(PathBuf::from) } /// Directory where remotely refreshed manifests are stored by /// `dingger manifests update`. pub fn remote_store_dir() -> PathBuf { state_dir().join("agent-detection") } pub fn remote_manifest_path(agent: Agent) -> PathBuf { remote_store_dir().join(format!("{}.toml", agent_label(agent))) } /// Directory for local override manifests that shadow bundled and remote ones. pub(crate) fn override_dir() -> PathBuf { config_dir().join("agent-detection") } #[derive(Debug, Clone)] pub(crate) struct ManifestVersion(String); impl ManifestVersion { pub(crate) fn parse(value: &str) -> Result { let trimmed = value.trim(); if trimmed.is_empty() { return Err("version must not be empty".to_string()); } for segment in trimmed.split('.') { if segment.is_empty() { return Err(format!("version {trimmed:?} contains an empty segment")); } if !segment.chars().all(|ch| ch.is_ascii_digit()) { return Err(format!("version {trimmed:?} must be dotted numeric")); } segment .parse::() .map_err(|_| format!("version {trimmed:?} contains an oversized segment"))?; } Ok(Self(trimmed.to_string())) } } impl std::fmt::Display for ManifestVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } impl<'de> serde::Deserialize<'de> for ManifestVersion { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?; Self::parse(&value).map_err(serde::de::Error::custom) } } impl serde::Serialize for ManifestVersion { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.0) } } impl Ord for ManifestVersion { fn cmp(&self, other: &Self) -> std::cmp::Ordering { use std::cmp::Ordering; let mut left = self.0.split('.'); let mut right = other.0.split('.'); loop { match (left.next(), right.next()) { (Some(left), Some(right)) => { let left = left.parse::().unwrap_or(0); let right = right.parse::().unwrap_or(0); match left.cmp(&right) { Ordering::Equal => {} ordering => return ordering, } } (Some(left), None) => { let left = left.parse::().unwrap_or(0); if left == 0 { continue; } return Ordering::Greater; } (None, Some(right)) => { let right = right.parse::().unwrap_or(0); if right == 0 { continue; } return Ordering::Less; } (None, None) => return Ordering::Equal, } } } } impl PartialOrd for ManifestVersion { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for ManifestVersion { fn eq(&self, other: &Self) -> bool { self.cmp(other) == std::cmp::Ordering::Equal } } impl Eq for ManifestVersion {}