1use std::fmt;
8
9use thiserror::Error;
10
11use crate::{
12 snapshot::{CpuMetrics, LoadAverage, MemoryMetrics, StatusSnapshot, SwapMetrics},
13 MAX_SAMPLE_INTERVAL_MS, SCHEMA_VERSION_V1,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Error)]
18#[error("{kind}")]
19pub struct ValidationViolation {
20 pub kind: ViolationKind,
22 pub field: String,
24}
25
26impl ValidationViolation {
27 fn new(kind: ViolationKind, field: impl Into<String>) -> Self {
28 Self {
29 kind,
30 field: field.into(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum ViolationKind {
41 UnsupportedSchemaVersion { found: u16 },
43 ZeroNotAllowed,
45 SampleIntervalOutOfRange { max_ms: u64 },
47 PercentageNotFinite,
49 PercentageOutOfRange,
51 LoadValueOutOfRange,
53 UsedExceedsTotal,
55 IowaitCapabilityMismatch,
57}
58
59impl fmt::Display for ViolationKind {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::UnsupportedSchemaVersion { found } => write!(
63 f,
64 "unsupported schema_version {found} (expected {SCHEMA_VERSION_V1})"
65 ),
66 Self::ZeroNotAllowed => f.write_str("value must be positive"),
67 Self::SampleIntervalOutOfRange { max_ms } => {
68 write!(f, "sample interval must be at most {max_ms} ms")
69 }
70 Self::PercentageNotFinite => f.write_str("percentage must be finite"),
71 Self::PercentageOutOfRange => f.write_str("percentage must be in 0.0..=100.0"),
72 Self::LoadValueOutOfRange => {
73 f.write_str("load average must be finite and non-negative")
74 }
75 Self::UsedExceedsTotal => f.write_str("used_bytes exceeds total_bytes"),
76 Self::IowaitCapabilityMismatch => {
77 f.write_str("iowait_pct must be Some(_) iff cpu_iowait capability is true")
78 }
79 }
80 }
81}
82
83pub(crate) fn validate(snap: &StatusSnapshot) -> Result<(), Vec<ValidationViolation>> {
85 let mut violations = Vec::new();
86
87 if snap.schema_version != SCHEMA_VERSION_V1 {
88 violations.push(ValidationViolation::new(
89 ViolationKind::UnsupportedSchemaVersion {
90 found: snap.schema_version,
91 },
92 "schema_version",
93 ));
94 }
95
96 if snap.observed_at_unix_ms == 0 {
97 violations.push(ValidationViolation::new(
98 ViolationKind::ZeroNotAllowed,
99 "observed_at_unix_ms",
100 ));
101 }
102 if snap.sample_interval_ms == 0 {
103 violations.push(ValidationViolation::new(
104 ViolationKind::ZeroNotAllowed,
105 "sample_interval_ms",
106 ));
107 } else if snap.sample_interval_ms > MAX_SAMPLE_INTERVAL_MS {
108 violations.push(ValidationViolation::new(
109 ViolationKind::SampleIntervalOutOfRange {
110 max_ms: MAX_SAMPLE_INTERVAL_MS,
111 },
112 "sample_interval_ms",
113 ));
114 }
115
116 validate_cpu(&snap.cpu, snap.capabilities.cpu_iowait, &mut violations);
117 validate_load(&snap.load, &mut violations);
118 validate_memory(&snap.memory, &mut violations);
119 validate_swap(&snap.swap, &mut violations);
120
121 if violations.is_empty() {
122 Ok(())
123 } else {
124 Err(violations)
125 }
126}
127
128fn validate_cpu(cpu: &CpuMetrics, cpu_iowait: bool, out: &mut Vec<ValidationViolation>) {
129 if cpu.logical_cores == 0 {
130 out.push(ValidationViolation::new(
131 ViolationKind::ZeroNotAllowed,
132 "cpu.logical_cores",
133 ));
134 }
135 check_percentage(cpu.usage_pct, "cpu.usage_pct", out);
136 match cpu.iowait_pct {
137 None => {
138 if cpu_iowait {
139 out.push(ValidationViolation::new(
140 ViolationKind::IowaitCapabilityMismatch,
141 "cpu.iowait_pct",
142 ));
143 }
144 }
145 Some(value) => {
146 if cpu_iowait {
147 check_percentage(value, "cpu.iowait_pct", out);
148 } else {
149 out.push(ValidationViolation::new(
150 ViolationKind::IowaitCapabilityMismatch,
151 "cpu.iowait_pct",
152 ));
153 }
154 }
155 }
156}
157
158fn validate_load(load: &LoadAverage, out: &mut Vec<ValidationViolation>) {
159 check_load(load.one, "load.one", out);
160 check_load(load.five, "load.five", out);
161 check_load(load.fifteen, "load.fifteen", out);
162}
163
164fn check_load(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
165 if !value.is_finite() || value < 0.0 {
166 out.push(ValidationViolation::new(
167 ViolationKind::LoadValueOutOfRange,
168 field,
169 ));
170 }
171}
172
173fn validate_memory(memory: &MemoryMetrics, out: &mut Vec<ValidationViolation>) {
174 check_percentage(memory.usage_pct, "memory.usage_pct", out);
175 if memory.used_bytes > memory.total_bytes {
176 out.push(ValidationViolation::new(
177 ViolationKind::UsedExceedsTotal,
178 "memory.used_bytes",
179 ));
180 }
181 if memory.total_bytes == 0 && memory.usage_pct != 0.0 {
182 out.push(ValidationViolation::new(
183 ViolationKind::PercentageOutOfRange,
184 "memory.usage_pct",
185 ));
186 }
187}
188
189fn validate_swap(swap: &SwapMetrics, out: &mut Vec<ValidationViolation>) {
190 check_percentage(swap.usage_pct, "swap.usage_pct", out);
191 if swap.used_bytes > swap.total_bytes {
192 out.push(ValidationViolation::new(
193 ViolationKind::UsedExceedsTotal,
194 "swap.used_bytes",
195 ));
196 }
197 if swap.total_bytes == 0 && swap.usage_pct != 0.0 {
198 out.push(ValidationViolation::new(
199 ViolationKind::PercentageOutOfRange,
200 "swap.usage_pct",
201 ));
202 }
203}
204
205fn check_percentage(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
206 if !value.is_finite() {
207 out.push(ValidationViolation::new(
208 ViolationKind::PercentageNotFinite,
209 field,
210 ));
211 return;
212 }
213 if !(0.0..=100.0).contains(&value) {
214 out.push(ValidationViolation::new(
215 ViolationKind::PercentageOutOfRange,
216 field,
217 ));
218 }
219}