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