termivar_scanner/
config.rs1use serde::{Deserialize, Serialize};
15
16use crate::lua_config::LuaEngineConfig;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum ScanIntensity {
21 Light,
22 Normal,
23 Aggressive,
24 Stealth,
25}
26
27impl ScanIntensity {
28 #[must_use]
29 pub fn as_str(&self) -> &str {
30 match self {
31 Self::Light => "light",
32 Self::Normal => "normal",
33 Self::Aggressive => "aggressive",
34 Self::Stealth => "stealth",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ScanConfig {
42 pub intensity: ScanIntensity,
43 pub timeout_secs: u64,
44 pub max_concurrency: u32,
45 #[serde(with = "positive_f32")]
46 pub rate_limit: f32,
47 pub max_payload_size: u64,
48 pub phases: Vec<u8>,
50 pub lua_engine: LuaEngineConfig,
51}
52
53mod positive_f32 {
54 use serde::{Deserialize, Deserializer, Serializer};
55
56 pub fn serialize<S>(value: &f32, serializer: S) -> Result<S::Ok, S::Error>
57 where
58 S: Serializer,
59 {
60 if value.is_finite() && *value > 0.0 {
61 serializer.serialize_f32(*value)
62 } else {
63 Err(serde::ser::Error::custom(
64 "rate limit must be finite and greater than zero",
65 ))
66 }
67 }
68
69 pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
70 where
71 D: Deserializer<'de>,
72 {
73 let value = f32::deserialize(deserializer)?;
74 if value.is_finite() && value > 0.0 {
75 Ok(value)
76 } else {
77 Err(serde::de::Error::custom(
78 "rate limit must be finite and greater than zero",
79 ))
80 }
81 }
82}
83
84impl ScanConfig {
85 pub fn validate(&self) -> Result<(), String> {
87 if self.timeout_secs == 0 {
88 return Err("Timeout must be > 0".to_string());
89 }
90 if self.max_concurrency == 0 {
91 return Err("Max concurrency must be > 0".to_string());
92 }
93 if !self.rate_limit.is_finite() || self.rate_limit <= 0.0 {
94 return Err("Rate limit must be finite and > 0".to_string());
95 }
96 if self.max_payload_size == 0 {
97 return Err("Max payload size must be > 0".to_string());
98 }
99 if self.phases.is_empty() {
100 return Err("At least one phase identifier must be supplied".to_string());
101 }
102 for phase in &self.phases {
103 if !(1..=9).contains(phase) {
104 return Err(format!("Invalid phase number: {phase}"));
105 }
106 }
107 self.lua_engine
108 .validate()
109 .map_err(|error| format!("Invalid Lua engine configuration: {error}"))?;
110 Ok(())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn valid_record() -> ScanConfig {
119 ScanConfig {
120 intensity: ScanIntensity::Normal,
121 timeout_secs: 5,
122 max_concurrency: 4,
123 rate_limit: 10.0,
124 max_payload_size: 1024,
125 phases: vec![1, 2],
126 lua_engine: LuaEngineConfig::minimal(),
127 }
128 }
129
130 #[test]
131 fn intensity_is_only_a_stable_label() {
132 assert_eq!(ScanIntensity::Light.as_str(), "light");
133 assert_eq!(ScanIntensity::Normal.as_str(), "normal");
134 assert_eq!(ScanIntensity::Aggressive.as_str(), "aggressive");
135 assert_eq!(ScanIntensity::Stealth.as_str(), "stealth");
136 }
137
138 #[test]
139 fn valid_host_record_passes_envelope_validation() {
140 assert!(valid_record().validate().is_ok());
141 }
142
143 #[test]
144 fn invalid_limits_and_phase_ids_fail_closed() {
145 let mut config = valid_record();
146 config.timeout_secs = 0;
147 assert!(config.validate().is_err());
148
149 let mut config = valid_record();
150 config.phases = vec![0];
151 assert!(config.validate().is_err());
152
153 let mut config = valid_record();
154 config.phases.clear();
155 assert_eq!(
156 config.validate().unwrap_err(),
157 "At least one phase identifier must be supplied"
158 );
159
160 for invalid_rate in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, -1.0] {
161 let mut config = valid_record();
162 config.rate_limit = invalid_rate;
163 assert_eq!(
164 config.validate().unwrap_err(),
165 "Rate limit must be finite and > 0"
166 );
167 }
168 }
169
170 #[test]
171 fn nested_lua_limits_are_validated() {
172 let mut config = valid_record();
173 config.lua_engine.default_timeout_ms = 0;
174 assert_eq!(
175 config.validate().unwrap_err(),
176 "Invalid Lua engine configuration: default_timeout_ms must be nonzero"
177 );
178 }
179
180 #[test]
181 fn wire_rejects_invalid_rate_and_uses_fixed_width_limits() {
182 let mut invalid = valid_record();
183 invalid.rate_limit = f32::NAN;
184 assert!(serde_json::to_string(&invalid).is_err());
185
186 let record = valid_record();
187 let encoded = serde_json::to_string(&record).expect("valid record serializes");
188 let decoded: ScanConfig = serde_json::from_str(&encoded).expect("record round-trips");
189 assert_eq!(decoded.max_concurrency, 4_u32);
190 assert_eq!(decoded.max_payload_size, 1_024_u64);
191
192 let mut invalid_wire = serde_json::to_value(record).unwrap();
193 invalid_wire["rate_limit"] = serde_json::json!(-1.0);
194 assert!(serde_json::from_value::<ScanConfig>(invalid_wire).is_err());
195 }
196}