1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6pub const HARD_MAX_HISTORY_ENTRIES: usize = 1_024;
7pub const HARD_MAX_MEMORY_BYTES: usize = 256 * 1_024 * 1_024;
8pub const HARD_MAX_TIMEOUT_MS: u64 = 60_000;
9pub const HARD_MAX_SOURCE_BYTES: usize = 1_024 * 1_024;
10pub const HARD_MAX_TOTAL_SOURCE_BYTES: usize = 64 * 1_024 * 1_024;
11pub const HARD_MAX_CONTEXT_BYTES: usize = 1_024 * 1_024;
12pub const HARD_MAX_TARGET_BYTES: usize = 256 * 1_024;
13pub const HARD_MAX_PAYLOAD_BYTES: usize = 512 * 1_024;
14pub const HARD_MAX_PARAMETERS: usize = 1_024;
15pub const HARD_MAX_PARAMETER_KEY_BYTES: usize = 4 * 1_024;
16pub const HARD_MAX_PARAMETER_VALUE_BYTES: usize = 64 * 1_024;
17pub const HARD_MAX_OUTPUT_BYTES: usize = 1_024 * 1_024;
18pub const HARD_MAX_RETURN_BYTES: usize = 1_024 * 1_024;
19pub const HARD_MAX_INSTRUCTIONS: u64 = 100_000_000;
20pub const HARD_MAX_HOOK_INTERVAL: u32 = 10_000;
21pub const HARD_MAX_SCRIPTS: usize = 4_096;
22pub const HARD_MAX_CONCURRENT_EXECUTIONS: usize = 64;
23pub const HARD_MAX_HISTORY_BYTES_PER_SCRIPT: usize = 8 * 1_024 * 1_024;
24pub const HARD_MAX_HISTORY_BYTES_TOTAL: usize = 64 * 1_024 * 1_024;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct LuaEngineConfig {
32 pub history_size: usize,
34 pub max_history_bytes_per_script: usize,
36 pub max_history_bytes_total: usize,
38 pub max_memory_bytes: usize,
40 pub default_timeout_ms: u64,
42 pub instruction_limit: u64,
44 pub hook_interval: u32,
46 pub max_source_bytes: usize,
48 pub max_total_source_bytes: usize,
50 pub max_context_bytes: usize,
52 pub max_target_bytes: usize,
54 pub max_payload_bytes: usize,
56 pub max_parameters: usize,
58 pub max_parameter_key_bytes: usize,
60 pub max_parameter_value_bytes: usize,
62 pub max_output_bytes: usize,
64 pub max_return_bytes: usize,
66 pub max_scripts: usize,
68 pub max_concurrent_executions: usize,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum LuaConfigViolation {
75 Zero,
76 AboveHardMaximum,
77 Inconsistent,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct LuaConfigError {
83 field: &'static str,
84 violation: LuaConfigViolation,
85}
86
87impl LuaConfigError {
88 #[must_use]
89 pub fn field(&self) -> &'static str {
90 self.field
91 }
92
93 #[must_use]
94 pub fn violation(&self) -> LuaConfigViolation {
95 self.violation
96 }
97}
98
99impl fmt::Display for LuaConfigError {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 let reason = match self.violation {
102 LuaConfigViolation::Zero => "must be nonzero",
103 LuaConfigViolation::AboveHardMaximum => "exceeds its hard maximum",
104 LuaConfigViolation::Inconsistent => "is inconsistent with another limit",
105 };
106 write!(formatter, "{} {reason}", self.field)
107 }
108}
109
110impl std::error::Error for LuaConfigError {}
111
112impl LuaEngineConfig {
113 #[must_use]
115 pub fn minimal() -> Self {
116 Self {
117 history_size: 10,
118 max_history_bytes_per_script: 128 * 1_024,
119 max_history_bytes_total: 512 * 1_024,
120 max_memory_bytes: 10 * 1_024 * 1_024,
121 default_timeout_ms: 1_000,
122 instruction_limit: 1_000_000,
123 hook_interval: 1_000,
124 max_source_bytes: 64 * 1_024,
125 max_total_source_bytes: 256 * 1_024,
126 max_context_bytes: 128 * 1_024,
127 max_target_bytes: 16 * 1_024,
128 max_payload_bytes: 64 * 1_024,
129 max_parameters: 128,
130 max_parameter_key_bytes: 1_024,
131 max_parameter_value_bytes: 16 * 1_024,
132 max_output_bytes: 64 * 1_024,
133 max_return_bytes: 64 * 1_024,
134 max_scripts: 64,
135 max_concurrent_executions: 4,
136 }
137 }
138
139 #[must_use]
141 pub fn extended() -> Self {
142 Self {
143 history_size: 500,
144 max_history_bytes_per_script: 4 * 1_024 * 1_024,
145 max_history_bytes_total: 32 * 1_024 * 1_024,
146 max_memory_bytes: 100 * 1_024 * 1_024,
147 default_timeout_ms: 30_000,
148 instruction_limit: 50_000_000,
149 hook_interval: 10_000,
150 max_source_bytes: HARD_MAX_SOURCE_BYTES,
151 max_total_source_bytes: HARD_MAX_TOTAL_SOURCE_BYTES,
152 max_context_bytes: HARD_MAX_CONTEXT_BYTES,
153 max_target_bytes: HARD_MAX_TARGET_BYTES,
154 max_payload_bytes: HARD_MAX_PAYLOAD_BYTES,
155 max_parameters: HARD_MAX_PARAMETERS,
156 max_parameter_key_bytes: HARD_MAX_PARAMETER_KEY_BYTES,
157 max_parameter_value_bytes: HARD_MAX_PARAMETER_VALUE_BYTES,
158 max_output_bytes: HARD_MAX_OUTPUT_BYTES,
159 max_return_bytes: HARD_MAX_RETURN_BYTES,
160 max_scripts: 2_048,
161 max_concurrent_executions: 32,
162 }
163 }
164
165 pub fn validate(&self) -> Result<(), LuaConfigError> {
167 validate_usize("history_size", self.history_size, HARD_MAX_HISTORY_ENTRIES)?;
168 validate_usize(
169 "max_history_bytes_per_script",
170 self.max_history_bytes_per_script,
171 HARD_MAX_HISTORY_BYTES_PER_SCRIPT,
172 )?;
173 validate_usize(
174 "max_history_bytes_total",
175 self.max_history_bytes_total,
176 HARD_MAX_HISTORY_BYTES_TOTAL,
177 )?;
178 validate_usize(
179 "max_memory_bytes",
180 self.max_memory_bytes,
181 HARD_MAX_MEMORY_BYTES,
182 )?;
183 validate_u64(
184 "default_timeout_ms",
185 self.default_timeout_ms,
186 HARD_MAX_TIMEOUT_MS,
187 )?;
188 validate_u64(
189 "instruction_limit",
190 self.instruction_limit,
191 HARD_MAX_INSTRUCTIONS,
192 )?;
193 validate_u32("hook_interval", self.hook_interval, HARD_MAX_HOOK_INTERVAL)?;
194 validate_usize(
195 "max_source_bytes",
196 self.max_source_bytes,
197 HARD_MAX_SOURCE_BYTES,
198 )?;
199 validate_usize(
200 "max_total_source_bytes",
201 self.max_total_source_bytes,
202 HARD_MAX_TOTAL_SOURCE_BYTES,
203 )?;
204 validate_usize(
205 "max_context_bytes",
206 self.max_context_bytes,
207 HARD_MAX_CONTEXT_BYTES,
208 )?;
209 validate_usize(
210 "max_target_bytes",
211 self.max_target_bytes,
212 HARD_MAX_TARGET_BYTES,
213 )?;
214 validate_usize(
215 "max_payload_bytes",
216 self.max_payload_bytes,
217 HARD_MAX_PAYLOAD_BYTES,
218 )?;
219 validate_usize("max_parameters", self.max_parameters, HARD_MAX_PARAMETERS)?;
220 validate_usize(
221 "max_parameter_key_bytes",
222 self.max_parameter_key_bytes,
223 HARD_MAX_PARAMETER_KEY_BYTES,
224 )?;
225 validate_usize(
226 "max_parameter_value_bytes",
227 self.max_parameter_value_bytes,
228 HARD_MAX_PARAMETER_VALUE_BYTES,
229 )?;
230 validate_usize(
231 "max_output_bytes",
232 self.max_output_bytes,
233 HARD_MAX_OUTPUT_BYTES,
234 )?;
235 validate_usize(
236 "max_return_bytes",
237 self.max_return_bytes,
238 HARD_MAX_RETURN_BYTES,
239 )?;
240 validate_usize("max_scripts", self.max_scripts, HARD_MAX_SCRIPTS)?;
241 validate_usize(
242 "max_concurrent_executions",
243 self.max_concurrent_executions,
244 HARD_MAX_CONCURRENT_EXECUTIONS,
245 )?;
246
247 if u64::from(self.hook_interval) > self.instruction_limit {
248 return Err(LuaConfigError {
249 field: "hook_interval",
250 violation: LuaConfigViolation::Inconsistent,
251 });
252 }
253 if self.max_history_bytes_per_script > self.max_history_bytes_total {
254 return Err(LuaConfigError {
255 field: "max_history_bytes_per_script",
256 violation: LuaConfigViolation::Inconsistent,
257 });
258 }
259 if self.max_source_bytes > self.max_total_source_bytes {
260 return Err(LuaConfigError {
261 field: "max_source_bytes",
262 violation: LuaConfigViolation::Inconsistent,
263 });
264 }
265 for (field, value) in [
266 ("max_target_bytes", self.max_target_bytes),
267 ("max_payload_bytes", self.max_payload_bytes),
268 ("max_parameter_key_bytes", self.max_parameter_key_bytes),
269 ("max_parameter_value_bytes", self.max_parameter_value_bytes),
270 ] {
271 if value > self.max_context_bytes {
272 return Err(LuaConfigError {
273 field,
274 violation: LuaConfigViolation::Inconsistent,
275 });
276 }
277 }
278 Ok(())
279 }
280}
281
282impl Default for LuaEngineConfig {
283 fn default() -> Self {
284 Self {
285 history_size: 100,
286 max_history_bytes_per_script: 1_024 * 1_024,
287 max_history_bytes_total: 8 * 1_024 * 1_024,
288 max_memory_bytes: 50 * 1_024 * 1_024,
289 default_timeout_ms: 5_000,
290 instruction_limit: 10_000_000,
291 hook_interval: 5_000,
292 max_source_bytes: 256 * 1_024,
293 max_total_source_bytes: 16 * 1_024 * 1_024,
294 max_context_bytes: 256 * 1_024,
295 max_target_bytes: 32 * 1_024,
296 max_payload_bytes: 128 * 1_024,
297 max_parameters: 256,
298 max_parameter_key_bytes: 1_024,
299 max_parameter_value_bytes: 32 * 1_024,
300 max_output_bytes: 128 * 1_024,
301 max_return_bytes: 128 * 1_024,
302 max_scripts: 256,
303 max_concurrent_executions: 8,
304 }
305 }
306}
307
308fn validate_usize(field: &'static str, value: usize, maximum: usize) -> Result<(), LuaConfigError> {
309 if value == 0 {
310 return Err(LuaConfigError {
311 field,
312 violation: LuaConfigViolation::Zero,
313 });
314 }
315 if value > maximum {
316 return Err(LuaConfigError {
317 field,
318 violation: LuaConfigViolation::AboveHardMaximum,
319 });
320 }
321 Ok(())
322}
323
324fn validate_u64(field: &'static str, value: u64, maximum: u64) -> Result<(), LuaConfigError> {
325 if value == 0 {
326 return Err(LuaConfigError {
327 field,
328 violation: LuaConfigViolation::Zero,
329 });
330 }
331 if value > maximum {
332 return Err(LuaConfigError {
333 field,
334 violation: LuaConfigViolation::AboveHardMaximum,
335 });
336 }
337 Ok(())
338}
339
340fn validate_u32(field: &'static str, value: u32, maximum: u32) -> Result<(), LuaConfigError> {
341 if value == 0 {
342 return Err(LuaConfigError {
343 field,
344 violation: LuaConfigViolation::Zero,
345 });
346 }
347 if value > maximum {
348 return Err(LuaConfigError {
349 field,
350 violation: LuaConfigViolation::AboveHardMaximum,
351 });
352 }
353 Ok(())
354}
355
356#[derive(Serialize, Deserialize)]
357struct LuaEngineConfigWire {
358 history_size: u64,
359 max_history_bytes_per_script: u64,
360 max_history_bytes_total: u64,
361 max_memory_bytes: u64,
362 default_timeout_ms: u64,
363 instruction_limit: u64,
364 hook_interval: u32,
365 max_source_bytes: u64,
366 max_total_source_bytes: u64,
367 max_context_bytes: u64,
368 max_target_bytes: u64,
369 max_payload_bytes: u64,
370 max_parameters: u64,
371 max_parameter_key_bytes: u64,
372 max_parameter_value_bytes: u64,
373 max_output_bytes: u64,
374 max_return_bytes: u64,
375 max_scripts: u64,
376 max_concurrent_executions: u64,
377}
378
379impl Serialize for LuaEngineConfig {
380 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
381 where
382 S: serde::Serializer,
383 {
384 let to_u64 = |value: usize| {
385 u64::try_from(value)
386 .map_err(|_| serde::ser::Error::custom("Lua limit does not fit u64"))
387 };
388 LuaEngineConfigWire {
389 history_size: to_u64(self.history_size)?,
390 max_history_bytes_per_script: to_u64(self.max_history_bytes_per_script)?,
391 max_history_bytes_total: to_u64(self.max_history_bytes_total)?,
392 max_memory_bytes: to_u64(self.max_memory_bytes)?,
393 default_timeout_ms: self.default_timeout_ms,
394 instruction_limit: self.instruction_limit,
395 hook_interval: self.hook_interval,
396 max_source_bytes: to_u64(self.max_source_bytes)?,
397 max_total_source_bytes: to_u64(self.max_total_source_bytes)?,
398 max_context_bytes: to_u64(self.max_context_bytes)?,
399 max_target_bytes: to_u64(self.max_target_bytes)?,
400 max_payload_bytes: to_u64(self.max_payload_bytes)?,
401 max_parameters: to_u64(self.max_parameters)?,
402 max_parameter_key_bytes: to_u64(self.max_parameter_key_bytes)?,
403 max_parameter_value_bytes: to_u64(self.max_parameter_value_bytes)?,
404 max_output_bytes: to_u64(self.max_output_bytes)?,
405 max_return_bytes: to_u64(self.max_return_bytes)?,
406 max_scripts: to_u64(self.max_scripts)?,
407 max_concurrent_executions: to_u64(self.max_concurrent_executions)?,
408 }
409 .serialize(serializer)
410 }
411}
412
413impl<'de> Deserialize<'de> for LuaEngineConfig {
414 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
415 where
416 D: serde::Deserializer<'de>,
417 {
418 let wire = LuaEngineConfigWire::deserialize(deserializer)?;
419 let to_usize = |field: &'static str, value: u64| {
420 usize::try_from(value)
421 .map_err(|_| serde::de::Error::custom(format!("{field} does not fit usize")))
422 };
423 let config = Self {
424 history_size: to_usize("history_size", wire.history_size)?,
425 max_history_bytes_per_script: to_usize(
426 "max_history_bytes_per_script",
427 wire.max_history_bytes_per_script,
428 )?,
429 max_history_bytes_total: to_usize(
430 "max_history_bytes_total",
431 wire.max_history_bytes_total,
432 )?,
433 max_memory_bytes: to_usize("max_memory_bytes", wire.max_memory_bytes)?,
434 default_timeout_ms: wire.default_timeout_ms,
435 instruction_limit: wire.instruction_limit,
436 hook_interval: wire.hook_interval,
437 max_source_bytes: to_usize("max_source_bytes", wire.max_source_bytes)?,
438 max_total_source_bytes: to_usize(
439 "max_total_source_bytes",
440 wire.max_total_source_bytes,
441 )?,
442 max_context_bytes: to_usize("max_context_bytes", wire.max_context_bytes)?,
443 max_target_bytes: to_usize("max_target_bytes", wire.max_target_bytes)?,
444 max_payload_bytes: to_usize("max_payload_bytes", wire.max_payload_bytes)?,
445 max_parameters: to_usize("max_parameters", wire.max_parameters)?,
446 max_parameter_key_bytes: to_usize(
447 "max_parameter_key_bytes",
448 wire.max_parameter_key_bytes,
449 )?,
450 max_parameter_value_bytes: to_usize(
451 "max_parameter_value_bytes",
452 wire.max_parameter_value_bytes,
453 )?,
454 max_output_bytes: to_usize("max_output_bytes", wire.max_output_bytes)?,
455 max_return_bytes: to_usize("max_return_bytes", wire.max_return_bytes)?,
456 max_scripts: to_usize("max_scripts", wire.max_scripts)?,
457 max_concurrent_executions: to_usize(
458 "max_concurrent_executions",
459 wire.max_concurrent_executions,
460 )?,
461 };
462 config.validate().map_err(serde::de::Error::custom)?;
463 Ok(config)
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn built_in_profiles_validate() {
473 for config in [
474 LuaEngineConfig::minimal(),
475 LuaEngineConfig::default(),
476 LuaEngineConfig::extended(),
477 ] {
478 assert_eq!(config.validate(), Ok(()));
479 }
480 }
481
482 #[test]
483 fn zero_and_huge_values_fail_before_allocation() {
484 let mut zero = LuaEngineConfig::minimal();
485 zero.max_memory_bytes = 0;
486 assert_eq!(
487 zero.validate(),
488 Err(LuaConfigError {
489 field: "max_memory_bytes",
490 violation: LuaConfigViolation::Zero,
491 })
492 );
493
494 let mut huge = LuaEngineConfig::minimal();
495 huge.history_size = HARD_MAX_HISTORY_ENTRIES + 1;
496 assert_eq!(
497 huge.validate(),
498 Err(LuaConfigError {
499 field: "history_size",
500 violation: LuaConfigViolation::AboveHardMaximum,
501 })
502 );
503 }
504
505 #[test]
506 fn inconsistent_limits_fail_closed() {
507 let mut config = LuaEngineConfig::minimal();
508 config.hook_interval = 1_001;
509 config.instruction_limit = 1_000;
510 assert_eq!(
511 config.validate(),
512 Err(LuaConfigError {
513 field: "hook_interval",
514 violation: LuaConfigViolation::Inconsistent,
515 })
516 );
517
518 let mut config = LuaEngineConfig::minimal();
519 config.max_context_bytes = 1;
520 assert_eq!(
521 config.validate(),
522 Err(LuaConfigError {
523 field: "max_target_bytes",
524 violation: LuaConfigViolation::Inconsistent,
525 })
526 );
527
528 let mut config = LuaEngineConfig::minimal();
529 config.max_total_source_bytes = config.max_source_bytes - 1;
530 assert_eq!(
531 config.validate(),
532 Err(LuaConfigError {
533 field: "max_source_bytes",
534 violation: LuaConfigViolation::Inconsistent,
535 })
536 );
537 }
538
539 #[test]
540 fn serde_uses_fixed_width_wire_values_and_revalidates() {
541 let config = LuaEngineConfig::default();
542 let wire = serde_json::to_string(&config).expect("serialize config");
543 let round_trip: LuaEngineConfig = serde_json::from_str(&wire).expect("deserialize config");
544 assert_eq!(round_trip, config);
545
546 let invalid = wire.replace(
547 &format!("\"max_memory_bytes\":{}", config.max_memory_bytes),
548 "\"max_memory_bytes\":0",
549 );
550 let error = serde_json::from_str::<LuaEngineConfig>(&invalid).unwrap_err();
551 assert!(error.to_string().contains("max_memory_bytes"));
552 }
553}