1use std::path::Path;
12use std::time::Duration;
13
14use camel_api::body::DEFAULT_MATERIALIZE_LIMIT;
15
16const DEFAULT_TIMEOUT_SECS: u64 = 30;
18
19const DEFAULT_MAX_MEMORY_BYTES: u64 = 50 * 1024 * 1024;
21
22const DEFAULT_MAX_CONCURRENT_CALLS: usize = 4;
24
25const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 10 * 1024 * 1024;
27
28pub(crate) const DEFAULT_MAX_STREAM_BYTES: u64 = DEFAULT_MATERIALIZE_LIMIT as u64;
30
31const DEFAULT_MAX_INSTANCES: usize = 10_000;
33
34const DEFAULT_MAX_TABLES: usize = 10_000;
36
37pub(crate) const DEFAULT_MAX_KV_ENTRIES: usize = 256;
39
40pub(crate) const DEFAULT_MAX_KEY_BYTES: usize = 1024;
42
43pub(crate) const DEFAULT_MAX_VALUE_BYTES: usize = 65_536;
45
46const EPOCH_INTERVAL_MILLIS: u64 = 10;
48
49#[derive(Debug, Clone)]
54pub struct WasmConfig {
55 pub timeout_secs: u64,
57
58 pub max_memory_bytes: u64,
61
62 pub max_concurrent_calls: usize,
64
65 pub max_wasm_size_bytes: u64,
69
70 pub allow_call_schemes: String,
74
75 pub max_stream_bytes: u64,
77
78 pub max_instances: usize,
80
81 pub max_tables: usize,
83
84 pub max_table_elements: Option<usize>,
86
87 pub max_kv_entries: usize,
91
92 pub max_key_bytes: usize,
94
95 pub max_value_bytes: usize,
97}
98
99impl Default for WasmConfig {
100 fn default() -> Self {
101 Self {
102 timeout_secs: DEFAULT_TIMEOUT_SECS,
103 max_memory_bytes: DEFAULT_MAX_MEMORY_BYTES,
104 max_concurrent_calls: DEFAULT_MAX_CONCURRENT_CALLS,
105 max_wasm_size_bytes: DEFAULT_MAX_WASM_SIZE_BYTES,
106 allow_call_schemes: String::new(),
107 max_stream_bytes: DEFAULT_MAX_STREAM_BYTES,
108 max_instances: DEFAULT_MAX_INSTANCES,
109 max_tables: DEFAULT_MAX_TABLES,
110 max_table_elements: None,
111 max_kv_entries: DEFAULT_MAX_KV_ENTRIES,
112 max_key_bytes: DEFAULT_MAX_KEY_BYTES,
113 max_value_bytes: DEFAULT_MAX_VALUE_BYTES,
114 }
115 }
116}
117
118impl WasmConfig {
119 pub fn from_limits(limits: &camel_config::WasmLimitsConfig) -> WasmConfig {
125 WasmConfig {
126 timeout_secs: limits.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS),
127 max_memory_bytes: limits.max_memory.unwrap_or(DEFAULT_MAX_MEMORY_BYTES),
128 max_concurrent_calls: limits
129 .max_concurrent_calls
130 .unwrap_or(DEFAULT_MAX_CONCURRENT_CALLS),
131 max_wasm_size_bytes: limits.max_wasm_size.unwrap_or(DEFAULT_MAX_WASM_SIZE_BYTES),
132 allow_call_schemes: limits.allow_call_schemes.clone().unwrap_or_default(),
133 max_stream_bytes: limits.max_stream_bytes.unwrap_or(DEFAULT_MAX_STREAM_BYTES),
134 max_instances: limits.max_instances.unwrap_or(DEFAULT_MAX_INSTANCES),
135 max_tables: limits.max_tables.unwrap_or(DEFAULT_MAX_TABLES),
136 max_table_elements: limits.max_table_elements,
137 max_kv_entries: limits.max_kv_entries.unwrap_or(DEFAULT_MAX_KV_ENTRIES),
138 max_key_bytes: limits.max_key_bytes.unwrap_or(DEFAULT_MAX_KEY_BYTES),
139 max_value_bytes: limits.max_value_bytes.unwrap_or(DEFAULT_MAX_VALUE_BYTES),
140 }
141 }
142
143 pub fn from_uri(uri_without_scheme: &str) -> (String, WasmConfig) {
150 let (path, query) = match uri_without_scheme.find('?') {
151 Some(i) => (&uri_without_scheme[..i], Some(&uri_without_scheme[i + 1..])),
152 None => (uri_without_scheme, None),
153 };
154
155 let mut config = WasmConfig::default();
156
157 if let Some(q) = query {
158 for pair in q.split('&') {
159 if let Some((key, value)) = pair.split_once('=') {
160 match key {
161 "timeout" => {
162 if let Ok(secs) = value.parse::<u64>()
163 && secs > 0
164 {
165 config.timeout_secs = secs;
166 }
167 }
168 "max-memory" => {
169 if let Ok(bytes) = value.parse::<u64>()
170 && bytes > 0
171 {
172 config.max_memory_bytes = bytes;
173 }
174 }
175 "max-concurrent-calls" => {
176 if let Ok(max) = value.parse::<usize>()
177 && max > 0
178 {
179 config.max_concurrent_calls = max;
180 }
181 }
182 "max-wasm-size" => {
183 if let Ok(bytes) = value.parse::<u64>()
184 && bytes > 0
185 {
186 config.max_wasm_size_bytes = bytes;
187 }
188 }
189 "allow-call" => {
190 config.allow_call_schemes = value.to_string();
191 }
192 "max-stream-bytes" => {
193 if let Ok(bytes) = value.parse::<u64>()
194 && bytes > 0
195 {
196 config.max_stream_bytes = bytes;
197 }
198 }
199 "max-instances" => {
200 if let Ok(n) = value.parse::<usize>()
201 && n > 0
202 {
203 config.max_instances = n;
204 }
205 }
206 "max-tables" => {
207 if let Ok(n) = value.parse::<usize>()
208 && n > 0
209 {
210 config.max_tables = n;
211 }
212 }
213 "max-table-elements" => {
214 if let Ok(n) = value.parse::<usize>()
215 && n > 0
216 {
217 config.max_table_elements = Some(n);
218 }
219 }
220 "max-kv-entries" => {
221 if let Ok(n) = value.parse::<usize>()
222 && n > 0
223 {
224 config.max_kv_entries = n;
225 }
226 }
227 "max-key-bytes" => {
228 if let Ok(n) = value.parse::<usize>()
229 && n > 0
230 {
231 config.max_key_bytes = n;
232 }
233 }
234 "max-value-bytes" => {
235 if let Ok(n) = value.parse::<usize>()
236 && n > 0
237 {
238 config.max_value_bytes = n;
239 }
240 }
241 _ => {} }
243 }
244 }
245 }
246
247 (path.to_string(), config)
248 }
249
250 pub fn epoch_deadline(&self) -> u64 {
254 self.timeout_secs * (1000 / EPOCH_INTERVAL_MILLIS)
255 }
256
257 pub fn epoch_interval(&self) -> Duration {
259 Duration::from_millis(EPOCH_INTERVAL_MILLIS)
260 }
261
262 pub fn epoch_interval_millis(&self) -> u64 {
264 EPOCH_INTERVAL_MILLIS
265 }
266
267 pub fn classify_error(
268 &self,
269 plugin_path: &Path,
270 e: wasmtime::Error,
271 ) -> crate::error::WasmError {
272 classify_error(self, plugin_path, e)
273 }
274}
275
276pub fn classify_error(
282 config: &WasmConfig,
283 plugin_path: &Path,
284 e: wasmtime::Error,
285) -> crate::error::WasmError {
286 use crate::error::{TrapReason, WasmError};
287 let name = plugin_path.display().to_string();
288 if let Some(trap) = e.downcast_ref::<wasmtime::Trap>() {
289 match WasmError::classify_trap(trap) {
290 TrapReason::Timeout => WasmError::Timeout {
291 plugin: name,
292 timeout_secs: config.timeout_secs,
293 },
294 TrapReason::OutOfMemory => WasmError::OutOfMemory {
295 plugin: name,
296 max_memory_bytes: config.max_memory_bytes,
297 },
298 other => WasmError::Trap {
299 plugin: name,
300 reason: other,
301 },
302 }
303 } else {
304 WasmError::GuestPanic(e.to_string())
305 }
306}
307
308pub fn validate_wasm_size(path: &std::path::Path, max_bytes: u64) -> Result<(), String> {
309 let metadata = std::fs::metadata(path)
310 .map_err(|e| format!("cannot stat wasm module {}: {}", path.display(), e))?;
311 let size = metadata.len();
312 if size > max_bytes {
313 return Err(format!(
314 "wasm module {} is {} bytes ({} KiB), exceeds cap of {} bytes ({} KiB)",
315 path.display(),
316 size,
317 size / 1024,
318 max_bytes,
319 max_bytes / 1024,
320 ));
321 }
322 Ok(())
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_default_config() {
331 let config = WasmConfig::default();
332 assert_eq!(config.timeout_secs, 30);
333 assert_eq!(config.max_memory_bytes, 50 * 1024 * 1024);
334 assert_eq!(config.max_concurrent_calls, 4);
335 }
336
337 #[test]
338 fn test_from_uri_no_params() {
339 let (path, config) = WasmConfig::from_uri("plugins/test.wasm");
340 assert_eq!(path, "plugins/test.wasm");
341 assert_eq!(config.timeout_secs, 30);
342 assert_eq!(config.max_memory_bytes, 50 * 1024 * 1024);
343 assert_eq!(config.max_concurrent_calls, 4);
344 }
345
346 #[test]
347 fn test_from_uri_with_timeout() {
348 let (path, config) = WasmConfig::from_uri("plugins/test.wasm?timeout=10");
349 assert_eq!(path, "plugins/test.wasm");
350 assert_eq!(config.timeout_secs, 10);
351 assert_eq!(config.max_memory_bytes, 50 * 1024 * 1024);
352 assert_eq!(config.max_concurrent_calls, 4);
353 }
354
355 #[test]
356 fn test_from_uri_with_max_memory() {
357 let (path, config) = WasmConfig::from_uri("plugins/test.wasm?max-memory=10485760");
358 assert_eq!(path, "plugins/test.wasm");
359 assert_eq!(config.timeout_secs, 30);
360 assert_eq!(config.max_memory_bytes, 10_485_760);
361 assert_eq!(config.max_concurrent_calls, 4);
362 }
363
364 #[test]
365 fn test_from_uri_with_both_params() {
366 let (path, config) = WasmConfig::from_uri("plugins/test.wasm?timeout=5&max-memory=1048576");
367 assert_eq!(path, "plugins/test.wasm");
368 assert_eq!(config.timeout_secs, 5);
369 assert_eq!(config.max_memory_bytes, 1_048_576);
370 assert_eq!(config.max_concurrent_calls, 4);
371 }
372
373 #[test]
374 fn test_from_uri_with_max_concurrent_calls() {
375 let (path, config) = WasmConfig::from_uri("plugins/test.wasm?max-concurrent-calls=8");
376 assert_eq!(path, "plugins/test.wasm");
377 assert_eq!(config.max_concurrent_calls, 8);
378 }
379
380 #[test]
381 fn test_from_uri_ignores_unknown_params() {
382 let (path, config) = WasmConfig::from_uri("plugins/test.wasm?foo=bar&timeout=60");
383 assert_eq!(path, "plugins/test.wasm");
384 assert_eq!(config.timeout_secs, 60);
385 }
386
387 #[test]
388 fn test_from_uri_ignores_invalid_values() {
389 let (_path, config) = WasmConfig::from_uri("plugins/test.wasm?timeout=abc");
390 assert_eq!(config.timeout_secs, 30); }
392
393 #[test]
394 fn test_from_uri_ignores_zero_values() {
395 let (_path, config) = WasmConfig::from_uri("plugins/test.wasm?timeout=0&max-memory=0");
396 assert_eq!(config.timeout_secs, 30); assert_eq!(config.max_memory_bytes, 50 * 1024 * 1024); assert_eq!(config.max_concurrent_calls, 4);
399 }
400
401 #[test]
402 fn test_epoch_deadline() {
403 let config = WasmConfig {
404 timeout_secs: 30,
405 max_memory_bytes: 0,
406 max_concurrent_calls: 4,
407 ..WasmConfig::default()
408 };
409 assert_eq!(config.epoch_deadline(), 3000); }
411
412 #[test]
413 fn test_epoch_deadline_custom_timeout() {
414 let config = WasmConfig {
415 timeout_secs: 5,
416 max_memory_bytes: 0,
417 max_concurrent_calls: 4,
418 ..WasmConfig::default()
419 };
420 assert_eq!(config.epoch_deadline(), 500);
421 }
422
423 #[test]
424 fn test_epoch_interval() {
425 let config = WasmConfig::default();
426 assert_eq!(config.epoch_interval(), Duration::from_millis(10));
427 }
428
429 #[test]
430 fn from_limits_applies_provided_values() {
431 let limits = camel_config::WasmLimitsConfig {
432 timeout_secs: Some(90),
433 max_memory: Some(128 * 1024 * 1024),
434 max_concurrent_calls: Some(2),
435 ..camel_config::WasmLimitsConfig::default()
436 };
437
438 let config = WasmConfig::from_limits(&limits);
439
440 assert_eq!(config.timeout_secs, 90);
441 assert_eq!(config.max_memory_bytes, 128 * 1024 * 1024);
442 assert_eq!(config.max_concurrent_calls, 2);
443 }
444
445 #[test]
446 fn from_limits_falls_back_to_runtime_defaults_when_none() {
447 let limits = camel_config::WasmLimitsConfig::default();
448
449 let config = WasmConfig::from_limits(&limits);
450
451 assert_eq!(config.timeout_secs, DEFAULT_TIMEOUT_SECS);
452 assert_eq!(config.max_memory_bytes, DEFAULT_MAX_MEMORY_BYTES);
453 assert_eq!(config.max_concurrent_calls, 4);
454 }
455
456 #[test]
457 fn from_limits_mixed_some_and_none() {
458 let limits = camel_config::WasmLimitsConfig {
459 timeout_secs: Some(15),
460 max_memory: None,
461 max_concurrent_calls: Some(1),
462 ..camel_config::WasmLimitsConfig::default()
463 };
464
465 let config = WasmConfig::from_limits(&limits);
466
467 assert_eq!(config.timeout_secs, 15);
468 assert_eq!(config.max_memory_bytes, DEFAULT_MAX_MEMORY_BYTES);
469 assert_eq!(config.max_concurrent_calls, 1);
470 }
471
472 #[test]
473 fn test_validate_wasm_size_rejects_oversized() {
474 let dir = tempfile::tempdir().unwrap();
475 let path = dir.path().join("big.wasm");
476 std::fs::write(&path, vec![0u8; 100]).unwrap();
477 let err = validate_wasm_size(&path, 50).unwrap_err();
478 assert!(err.contains("exceeds cap"), "got: {err}");
479 }
480
481 #[test]
482 fn test_validate_wasm_size_allows_within_cap() {
483 let dir = tempfile::tempdir().unwrap();
484 let path = dir.path().join("ok.wasm");
485 std::fs::write(&path, vec![0u8; 100]).unwrap();
486 validate_wasm_size(&path, 200).expect("100 bytes within 200 cap");
487 }
488
489 #[test]
490 fn test_default_max_wasm_size_bytes() {
491 let config = WasmConfig::default();
492 assert_eq!(config.max_wasm_size_bytes, 10 * 1024 * 1024);
493 }
494
495 #[test]
496 fn test_from_uri_max_wasm_size() {
497 let (_path, config) = WasmConfig::from_uri("p.wasm?max-wasm-size=1048576");
498 assert_eq!(config.max_wasm_size_bytes, 1_048_576);
499 }
500
501 #[test]
502 fn test_validate_wasm_size_errors_on_missing_file() {
503 let err = validate_wasm_size(std::path::Path::new("/nonexistent.wasm"), 1000).unwrap_err();
504 assert!(err.contains("cannot stat"));
505 }
506
507 #[test]
508 fn wasm_config_default_max_stream_bytes() {
509 let cfg = WasmConfig::default();
510 assert_eq!(cfg.max_stream_bytes, DEFAULT_MATERIALIZE_LIMIT as u64);
511 }
512
513 #[test]
514 fn wasm_config_from_uri_max_stream_bytes() {
515 let (_, cfg) = WasmConfig::from_uri("test.wasm?max-stream-bytes=52428800");
516 assert_eq!(cfg.max_stream_bytes, 52_428_800);
517 }
518
519 #[test]
520 fn wasm_config_from_uri_max_stream_bytes_ignores_zero() {
521 let (_, cfg) = WasmConfig::from_uri("test.wasm?max-stream-bytes=0");
522 assert_eq!(cfg.max_stream_bytes, DEFAULT_MAX_STREAM_BYTES);
523 }
524
525 #[test]
526 fn wasm_config_from_limits_max_stream_bytes() {
527 let limits = camel_config::WasmLimitsConfig {
528 max_stream_bytes: Some(52_428_800),
529 ..camel_config::WasmLimitsConfig::default()
530 };
531 let cfg = WasmConfig::from_limits(&limits);
532 assert_eq!(cfg.max_stream_bytes, 52_428_800);
533 }
534
535 #[test]
536 fn wasm_config_from_limits_max_stream_bytes_default() {
537 let limits = camel_config::WasmLimitsConfig::default();
538 let cfg = WasmConfig::from_limits(&limits);
539 assert_eq!(cfg.max_stream_bytes, DEFAULT_MAX_STREAM_BYTES);
540 }
541
542 #[test]
543 fn wasm_config_default_instances_tables_table_elements() {
544 let cfg = WasmConfig::default();
545 assert_eq!(cfg.max_instances, 10_000);
546 assert_eq!(cfg.max_tables, 10_000);
547 assert_eq!(cfg.max_table_elements, None);
548 }
549
550 #[test]
551 fn wasm_config_from_limits_instances_tables_table_elements() {
552 let limits = camel_config::WasmLimitsConfig {
553 max_instances: Some(100),
554 max_tables: Some(50),
555 max_table_elements: Some(200),
556 ..camel_config::WasmLimitsConfig::default()
557 };
558 let cfg = WasmConfig::from_limits(&limits);
559 assert_eq!(cfg.max_instances, 100);
560 assert_eq!(cfg.max_tables, 50);
561 assert_eq!(cfg.max_table_elements, Some(200));
562 }
563
564 #[test]
565 fn wasm_config_from_limits_instances_tables_table_elements_defaults() {
566 let limits = camel_config::WasmLimitsConfig::default();
567 let cfg = WasmConfig::from_limits(&limits);
568 assert_eq!(cfg.max_instances, 10_000);
569 assert_eq!(cfg.max_tables, 10_000);
570 assert_eq!(cfg.max_table_elements, None);
571 }
572
573 #[test]
574 fn wasm_config_from_uri_max_instances() {
575 let (_, cfg) = WasmConfig::from_uri("test.wasm?max-instances=100");
576 assert_eq!(cfg.max_instances, 100);
577 }
578
579 #[test]
580 fn wasm_config_from_uri_max_tables() {
581 let (_, cfg) = WasmConfig::from_uri("test.wasm?max-tables=50");
582 assert_eq!(cfg.max_tables, 50);
583 }
584
585 #[test]
586 fn wasm_config_from_uri_max_table_elements() {
587 let (_, cfg) = WasmConfig::from_uri("test.wasm?max-table-elements=200");
588 assert_eq!(cfg.max_table_elements, Some(200));
589 }
590}