aboutsummaryrefslogtreecommitdiff
path: root/crates/detect-core/src/manifest_update.rs
blob: 1bea8a12dd04b482bc513d7594970c9b2a9c0e03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! 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<PathBuf> {
    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<Self, String> {
        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::<u64>()
                .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<D>(deserializer: D) -> Result<Self, D::Error>
    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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    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::<u64>().unwrap_or(0);
                    let right = right.parse::<u64>().unwrap_or(0);
                    match left.cmp(&right) {
                        Ordering::Equal => {}
                        ordering => return ordering,
                    }
                }
                (Some(left), None) => {
                    let left = left.parse::<u64>().unwrap_or(0);
                    if left == 0 {
                        continue;
                    }
                    return Ordering::Greater;
                }
                (None, Some(right)) => {
                    let right = right.parse::<u64>().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<std::cmp::Ordering> {
        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 {}