1use std::io::Cursor;
2
3use murmur_artifact::payload_shape::{select_root_wasm_in_archive, SKILL_MD_ENTRY};
4use murmur_artifact::zip_guard;
5use murmur_artifact::{BUILD_INFO_FILE, PACKED_MANIFEST_ENTRY};
6use zip::ZipArchive;
7
8use crate::errors::RuntimeError;
9
10pub fn extract_root_wasm(
19 artifact_name: &str,
20 artifact_version: &str,
21 artifact_bytes: &[u8],
22) -> Result<Vec<u8>, RuntimeError> {
23 extract_root_wasm_capped(
24 artifact_name,
25 artifact_version,
26 artifact_bytes,
27 zip_guard::max_artifact_decompressed_bytes(),
28 )
29}
30
31pub fn check_root_wasm(
34 artifact_name: &str,
35 artifact_version: &str,
36 artifact_bytes: &[u8],
37) -> Result<(), RuntimeError> {
38 locate_root_wasm(artifact_name, artifact_version, artifact_bytes).map(|_| ())
39}
40
41type BorrowedArchive<'a> = ZipArchive<Cursor<&'a [u8]>>;
42
43fn locate_root_wasm<'a>(
45 artifact_name: &str,
46 artifact_version: &str,
47 artifact_bytes: &'a [u8],
48) -> Result<(BorrowedArchive<'a>, String), RuntimeError> {
49 let mut archive = ZipArchive::new(Cursor::new(artifact_bytes)).map_err(|err| {
50 RuntimeError::ArtifactArchive {
51 name: artifact_name.to_string(),
52 version: artifact_version.to_string(),
53 message: err.to_string(),
54 }
55 })?;
56
57 let selected_name =
60 select_root_wasm_in_archive(&mut archive).map_err(|err| RuntimeError::ArtifactArchive {
61 name: artifact_name.to_string(),
62 version: artifact_version.to_string(),
63 message: err.to_string(),
64 })?;
65
66 Ok((archive, selected_name))
67}
68
69fn extract_root_wasm_capped(
70 artifact_name: &str,
71 artifact_version: &str,
72 artifact_bytes: &[u8],
73 max_bytes: u64,
74) -> Result<Vec<u8>, RuntimeError> {
75 let (mut archive, selected_name) =
76 locate_root_wasm(artifact_name, artifact_version, artifact_bytes)?;
77
78 zip_guard::read_zip_entry_capped(&mut archive, &selected_name, max_bytes).map_err(|err| {
79 RuntimeError::ArtifactArchive {
80 name: artifact_name.to_string(),
81 version: artifact_version.to_string(),
82 message: err.to_string(),
83 }
84 })
85}
86
87pub type SkillFiles = Vec<(String, Vec<u8>)>;
90
91pub fn extract_skill_files(
103 artifact_name: &str,
104 artifact_version: &str,
105 artifact_bytes: &[u8],
106) -> Result<SkillFiles, RuntimeError> {
107 extract_skill_files_capped(
108 artifact_name,
109 artifact_version,
110 artifact_bytes,
111 zip_guard::max_artifact_decompressed_bytes(),
112 )
113}
114
115fn extract_skill_files_capped(
116 artifact_name: &str,
117 artifact_version: &str,
118 artifact_bytes: &[u8],
119 max_bytes: u64,
120) -> Result<SkillFiles, RuntimeError> {
121 let archive_error = |message: String| RuntimeError::ArtifactArchive {
122 name: artifact_name.to_string(),
123 version: artifact_version.to_string(),
124 message,
125 };
126 let cursor = std::io::Cursor::new(artifact_bytes);
127 let mut archive = ZipArchive::new(cursor).map_err(|err| archive_error(err.to_string()))?;
128
129 let skill_md = zip_guard::read_zip_entry_capped(&mut archive, SKILL_MD_ENTRY, max_bytes)
130 .map_err(|err| {
131 archive_error(format!(
132 "{SKILL_MD_ENTRY} not found or unreadable at archive root (ensure \
133 {SKILL_MD_ENTRY} is at the root of the .mur.zip): {err}"
134 ))
135 })?;
136 let mut total = skill_md.len() as u64;
137 let mut files = vec![(SKILL_MD_ENTRY.to_string(), skill_md)];
138
139 let mut supporting = Vec::new();
140 for index in 0..archive.len() {
141 let entry = archive
142 .by_index_raw(index)
143 .map_err(|err| archive_error(err.to_string()))?;
144 if entry.is_dir() {
145 continue;
146 }
147 let raw_name = entry
148 .name()
149 .map_err(|err| archive_error(err.to_string()))?
150 .into_owned();
151 let relative = zip_guard::normalized_entry_name(&raw_name)
152 .map_err(|err| archive_error(err.to_string()))?;
153 if relative.is_empty()
154 || [SKILL_MD_ENTRY, PACKED_MANIFEST_ENTRY, BUILD_INFO_FILE].contains(&relative.as_str())
155 {
156 continue;
157 }
158 supporting.push((raw_name, relative));
159 }
160
161 for (raw_name, relative) in supporting {
162 let remaining = max_bytes.saturating_sub(total);
163 let bytes = zip_guard::read_zip_entry_capped(&mut archive, &raw_name, remaining)
164 .map_err(|err| archive_error(format!("failed to read '{raw_name}': {err}")))?;
165 total += bytes.len() as u64;
166 files.push((relative, bytes));
167 }
168 Ok(files)
169}
170
171pub fn extract_manifest_yaml(
172 artifact_name: &str,
173 artifact_version: &str,
174 artifact_bytes: &[u8],
175) -> Result<String, RuntimeError> {
176 extract_manifest_yaml_capped(
177 artifact_name,
178 artifact_version,
179 artifact_bytes,
180 zip_guard::max_artifact_decompressed_bytes(),
181 )
182}
183
184fn extract_manifest_yaml_capped(
185 artifact_name: &str,
186 artifact_version: &str,
187 artifact_bytes: &[u8],
188 max_bytes: u64,
189) -> Result<String, RuntimeError> {
190 let cursor = std::io::Cursor::new(artifact_bytes);
191 let mut archive = ZipArchive::new(cursor).map_err(|err| RuntimeError::ArtifactArchive {
192 name: artifact_name.to_string(),
193 version: artifact_version.to_string(),
194 message: err.to_string(),
195 })?;
196
197 zip_guard::read_zip_entry_to_string_capped(&mut archive, PACKED_MANIFEST_ENTRY, max_bytes)
198 .map_err(|err| RuntimeError::ArtifactArchive {
199 name: artifact_name.to_string(),
200 version: artifact_version.to_string(),
201 message: format!(
202 "missing or unreadable {PACKED_MANIFEST_ENTRY} in artifact archive: {err}"
203 ),
204 })
205}
206
207pub fn extract_declared_env_allow(
219 artifact_name: &str,
220 artifact_version: &str,
221 manifest_yaml: &str,
222) -> Result<Vec<String>, RuntimeError> {
223 let declared: DeclaredAllows =
224 serde_norway::from_str(manifest_yaml).map_err(|err| RuntimeError::ArtifactArchive {
225 name: artifact_name.to_string(),
226 version: artifact_version.to_string(),
227 message: format!(
228 "cannot read capabilities.env.allow from {PACKED_MANIFEST_ENTRY}: {err}"
229 ),
230 })?;
231 Ok(declared
232 .capabilities
233 .and_then(|capabilities| capabilities.env)
234 .map(|env| env.allow)
235 .unwrap_or_default())
236}
237
238pub fn extract_declared_spawn_allow(
248 artifact_name: &str,
249 artifact_version: &str,
250 manifest_yaml: &str,
251) -> Result<Vec<String>, RuntimeError> {
252 let declared: DeclaredAllows =
253 serde_norway::from_str(manifest_yaml).map_err(|err| RuntimeError::ArtifactArchive {
254 name: artifact_name.to_string(),
255 version: artifact_version.to_string(),
256 message: format!(
257 "cannot read capabilities.spawn.allow from {PACKED_MANIFEST_ENTRY}: {err}"
258 ),
259 })?;
260 Ok(declared
261 .capabilities
262 .and_then(|capabilities| capabilities.spawn)
263 .map(|spawn| spawn.allow)
264 .unwrap_or_default())
265}
266
267pub fn extract_declared_workdir_max_bytes(
276 artifact_name: &str,
277 artifact_version: &str,
278 manifest_yaml: &str,
279) -> Result<Option<u64>, RuntimeError> {
280 let declared: DeclaredResourcesManifest =
281 serde_norway::from_str(manifest_yaml).map_err(|err| RuntimeError::ArtifactArchive {
282 name: artifact_name.to_string(),
283 version: artifact_version.to_string(),
284 message: format!(
285 "cannot read capabilities.resources.workdir_max_bytes from \
286 {PACKED_MANIFEST_ENTRY}: {err}"
287 ),
288 })?;
289 Ok(declared
290 .capabilities
291 .and_then(|capabilities| capabilities.resources)
292 .and_then(|resources| resources.workdir_max_bytes))
293}
294
295#[derive(serde::Deserialize)]
299struct DeclaredAllows {
300 #[serde(default)]
301 capabilities: Option<DeclaredCapabilities>,
302}
303
304#[derive(serde::Deserialize)]
305struct DeclaredCapabilities {
306 #[serde(default)]
307 env: Option<DeclaredAllowList>,
308 #[serde(default)]
309 spawn: Option<DeclaredAllowList>,
310}
311
312#[derive(serde::Deserialize)]
315struct DeclaredResourcesManifest {
316 #[serde(default)]
317 capabilities: Option<DeclaredResourcesCapabilities>,
318}
319
320#[derive(serde::Deserialize)]
321struct DeclaredResourcesCapabilities {
322 #[serde(default)]
323 resources: Option<DeclaredResources>,
324}
325
326#[derive(serde::Deserialize)]
327struct DeclaredResources {
328 #[serde(default)]
329 workdir_max_bytes: Option<u64>,
330}
331
332#[derive(serde::Deserialize)]
333struct DeclaredAllowList {
334 #[serde(default)]
335 allow: Vec<String>,
336}
337
338#[cfg(test)]
339pub(crate) mod test_support {
340 use std::io::Write;
341
342 use zip::write::SimpleFileOptions;
343
344 pub(crate) fn archive_with_files(files: &[(&str, &[u8])]) -> Vec<u8> {
346 let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
347 {
348 let mut zip = zip::ZipWriter::new(&mut cursor);
349 let options = SimpleFileOptions::default();
350 for (name, bytes) in files {
351 zip.start_file(*name, options).unwrap();
352 zip.write_all(bytes).unwrap();
353 }
354 zip.finish().unwrap();
355 }
356 cursor.into_inner()
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::test_support::archive_with_files;
363 use super::*;
364
365 #[test]
366 fn prefers_capsule_wasm_when_present() {
367 let archive = archive_with_files(&[
368 ("tool.wasm", b"tool"),
369 ("capsule.wasm", b"capsule"),
370 (PACKED_MANIFEST_ENTRY, b"name: demo"),
371 ]);
372
373 let wasm = extract_root_wasm("demo", "0.0.1", &archive).unwrap();
374 assert_eq!(wasm, b"capsule");
375 }
376
377 #[test]
378 fn extracts_manifest_yaml_from_archive() {
379 let archive = archive_with_files(&[
380 (PACKED_MANIFEST_ENTRY, b"name: demo\nversion: 0.1.0\n"),
381 ("tool.wasm", b"tool"),
382 ]);
383
384 let manifest = extract_manifest_yaml("demo", "0.1.0", &archive).unwrap();
385 assert!(manifest.contains("name: demo"));
386 assert!(manifest.contains("version: 0.1.0"));
387 }
388
389 #[test]
390 fn reads_the_declared_env_allow_out_of_a_manifest() {
391 let manifest = "name: worker\nversion: 0.1.0\ncapabilities:\n network:\n \
392 allow: [127.0.0.1]\n env:\n allow: [OPENAI_API_KEY, HTTPS_PROXY]\n";
393
394 assert_eq!(
395 extract_declared_env_allow("worker", "0.1.0", manifest).unwrap(),
396 vec!["OPENAI_API_KEY".to_string(), "HTTPS_PROXY".to_string()]
397 );
398 }
399
400 #[test]
404 fn an_unresolvable_inference_key_does_not_stop_the_read() {
405 let manifest = "name: worker\nversion: 0.1.0\ninference:\n transport: http\n \
406 api_key: ${MURMUR_A_VARIABLE_NOBODY_EXPORTED}\ncapabilities:\n env:\n \
407 allow: [MURMUR_A_VARIABLE_NOBODY_EXPORTED]\n";
408
409 assert_eq!(
410 extract_declared_env_allow("worker", "0.1.0", manifest).unwrap(),
411 vec!["MURMUR_A_VARIABLE_NOBODY_EXPORTED".to_string()]
412 );
413 }
414
415 #[test]
416 fn a_manifest_declaring_no_env_allow_declares_nothing() {
417 for manifest in [
418 "name: worker\nversion: 0.1.0\n",
419 "name: worker\nversion: 0.1.0\ncapabilities:\n shell:\n allow: [bash]\n",
420 "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n allow: []\n",
421 ] {
422 assert_eq!(
423 extract_declared_env_allow("worker", "0.1.0", manifest).unwrap(),
424 Vec::<String>::new(),
425 "{manifest}"
426 );
427 }
428 }
429
430 #[test]
431 fn an_unreadable_env_allow_is_an_error_naming_the_artifact() {
432 for manifest in [
433 "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n allow: OPENAI_API_KEY\n",
434 "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n allow: [[OPENAI_API_KEY]]\n",
435 "name: worker\nversion: 0.1.0\ncapabilities:\n\tenv: broken\n",
436 ] {
437 let err = extract_declared_env_allow("worker", "0.1.0", manifest).unwrap_err();
438 match err {
439 RuntimeError::ArtifactArchive {
440 name,
441 version,
442 message,
443 } => {
444 assert_eq!(name, "worker");
445 assert_eq!(version, "0.1.0");
446 assert!(message.contains("capabilities.env.allow"), "{message}");
447 }
448 other => panic!("expected ArtifactArchive error, got {other:?}"),
449 }
450 }
451 }
452
453 #[test]
454 fn reads_the_declared_spawn_allow_out_of_a_manifest() {
455 let manifest = "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n \
456 allow: [OPENAI_API_KEY]\n spawn:\n allow: [deep-worker, sibling]\n";
457
458 assert_eq!(
459 extract_declared_spawn_allow("worker", "0.1.0", manifest).unwrap(),
460 vec!["deep-worker".to_string(), "sibling".to_string()]
461 );
462 assert_eq!(
463 extract_declared_env_allow("worker", "0.1.0", manifest).unwrap(),
464 vec!["OPENAI_API_KEY".to_string()]
465 );
466 }
467
468 #[test]
469 fn a_manifest_declaring_no_spawn_allow_declares_nothing() {
470 for manifest in [
471 "name: worker\nversion: 0.1.0\n",
472 "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n allow: [A_KEY]\n",
473 "name: worker\nversion: 0.1.0\ncapabilities:\n spawn:\n allow: []\n",
474 ] {
475 assert_eq!(
476 extract_declared_spawn_allow("worker", "0.1.0", manifest).unwrap(),
477 Vec::<String>::new(),
478 "{manifest}"
479 );
480 }
481 }
482
483 #[test]
484 fn an_unreadable_spawn_allow_is_an_error_naming_the_artifact() {
485 for manifest in [
486 "name: worker\nversion: 0.1.0\ncapabilities:\n spawn:\n allow: deep-worker\n",
487 "name: worker\nversion: 0.1.0\ncapabilities:\n spawn:\n allow: [[deep]]\n",
488 "name: worker\nversion: 0.1.0\ncapabilities:\n\tspawn: broken\n",
489 ] {
490 match extract_declared_spawn_allow("worker", "0.1.0", manifest).unwrap_err() {
491 RuntimeError::ArtifactArchive {
492 name,
493 version,
494 message,
495 } => {
496 assert_eq!(name, "worker");
497 assert_eq!(version, "0.1.0");
498 assert!(message.contains("capabilities.spawn.allow"), "{message}");
499 }
500 other => panic!("expected ArtifactArchive error, got {other:?}"),
501 }
502 }
503 }
504
505 #[test]
506 fn reads_the_declared_workdir_max_bytes_out_of_a_manifest() {
507 let declared = "name: worker\nversion: 0.1.0\ncapabilities:\n resources:\n \
508 workdir_max_bytes: 1024\n cgroup_pids_max: 64\n";
509 assert_eq!(
510 extract_declared_workdir_max_bytes("worker", "0.1.0", declared).unwrap(),
511 Some(1024)
512 );
513 for manifest in [
514 "name: worker\nversion: 0.1.0\n",
515 "name: worker\nversion: 0.1.0\ncapabilities:\n env:\n allow: [A_KEY]\n",
516 "name: worker\nversion: 0.1.0\ncapabilities:\n resources:\n cgroup_pids_max: 8\n",
517 ] {
518 assert_eq!(
519 extract_declared_workdir_max_bytes("worker", "0.1.0", manifest).unwrap(),
520 None,
521 "{manifest}"
522 );
523 }
524 let malformed = "name: worker\nversion: 0.1.0\ncapabilities:\n resources:\n \
525 workdir_max_bytes: lots\n env:\n allow: [A_KEY]\n";
526 match extract_declared_workdir_max_bytes("worker", "0.1.0", malformed).unwrap_err() {
527 RuntimeError::ArtifactArchive { name, message, .. } => {
528 assert_eq!(name, "worker");
529 assert!(
530 message.contains("capabilities.resources.workdir_max_bytes"),
531 "{message}"
532 );
533 }
534 other => panic!("expected ArtifactArchive error, got {other:?}"),
535 }
536 assert_eq!(
537 extract_declared_env_allow("worker", "0.1.0", malformed).unwrap(),
538 vec!["A_KEY".to_string()],
539 "a malformed resources block does not stop env.allow being read"
540 );
541 }
542
543 #[test]
544 fn rejects_path_traversal_entry_as_root_wasm_candidate() {
545 let archive = archive_with_files(&[
546 ("../../evil.wasm", b"evil"),
547 (PACKED_MANIFEST_ENTRY, b"name: demo"),
548 ]);
549
550 let err = extract_root_wasm("demo", "0.0.1", &archive).unwrap_err();
551 match err {
552 RuntimeError::ArtifactArchive { message, .. } => {
553 assert!(message.contains("missing root .wasm file"));
554 }
555 other => panic!("expected ArtifactArchive error, got {other:?}"),
556 }
557 }
558
559 #[test]
560 fn rejects_leading_slash_entry_as_root_wasm_candidate() {
561 let archive = archive_with_files(&[
562 ("/capsule.wasm", b"evil"),
563 (PACKED_MANIFEST_ENTRY, b"name: demo"),
564 ]);
565
566 let err = extract_root_wasm("demo", "0.0.1", &archive).unwrap_err();
567 match err {
568 RuntimeError::ArtifactArchive { message, .. } => {
569 assert!(message.contains("missing root .wasm file"));
570 }
571 other => panic!("expected ArtifactArchive error, got {other:?}"),
572 }
573 }
574
575 #[test]
576 fn extract_manifest_yaml_rejects_entry_past_ceiling() {
577 let big = vec![b'a'; 1024];
578 let archive = archive_with_files(&[(PACKED_MANIFEST_ENTRY, &big)]);
579
580 let err = extract_manifest_yaml_capped("demo", "0.0.1", &archive, 16).unwrap_err();
581 match err {
582 RuntimeError::ArtifactArchive { message, .. } => {
583 assert!(message.contains("decompression ceiling"));
584 }
585 other => panic!("expected ArtifactArchive error, got {other:?}"),
586 }
587 }
588
589 #[test]
590 fn extract_root_wasm_rejects_entry_past_ceiling() {
591 let big = vec![b'a'; 1024];
592 let archive = archive_with_files(&[("capsule.wasm", &big)]);
593
594 let err = extract_root_wasm_capped("demo", "0.0.1", &archive, 16).unwrap_err();
595 match err {
596 RuntimeError::ArtifactArchive { message, .. } => {
597 assert!(message.contains("decompression ceiling"));
598 }
599 other => panic!("expected ArtifactArchive error, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn extract_skill_files_rejects_entry_past_ceiling() {
605 let big = vec![b'a'; 1024];
606 let archive = archive_with_files(&[("skill.md", &big)]);
607
608 let err = extract_skill_files_capped("demo", "0.0.1", &archive, 16).unwrap_err();
609 match err {
610 RuntimeError::ArtifactArchive { message, .. } => {
611 assert!(message.contains("decompression ceiling"));
612 }
613 other => panic!("expected ArtifactArchive error, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn extract_skill_files_rejects_a_total_past_ceiling() {
619 let archive =
620 archive_with_files(&[("skill.md", &[b'a'; 10]), ("references/a.md", &[b'b'; 10])]);
621
622 let err = extract_skill_files_capped("demo", "0.0.1", &archive, 16).unwrap_err();
623 match err {
624 RuntimeError::ArtifactArchive { message, .. } => {
625 assert!(message.contains("references/a.md"), "{message}");
626 assert!(message.contains("decompression ceiling"), "{message}");
627 }
628 other => panic!("expected ArtifactArchive error, got {other:?}"),
629 }
630 }
631
632 #[test]
633 fn extract_skill_files_returns_supporting_files_without_manifest_or_build_record() {
634 let archive = archive_with_files(&[
635 (PACKED_MANIFEST_ENTRY, b"name: demo"),
636 ("references/forms.md", b"# Forms\n"),
637 ("skill.md", b"# Demo\n"),
638 (BUILD_INFO_FILE, b"{}"),
639 ("scripts/fill.py", b"print(1)\n"),
640 ]);
641
642 let files = extract_skill_files("demo", "0.0.1", &archive).unwrap();
643 assert_eq!(
644 files,
645 vec![
646 ("skill.md".to_string(), b"# Demo\n".to_vec()),
647 ("references/forms.md".to_string(), b"# Forms\n".to_vec()),
648 ("scripts/fill.py".to_string(), b"print(1)\n".to_vec()),
649 ]
650 );
651 }
652
653 #[test]
654 fn extract_skill_files_requires_skill_md() {
655 let archive = archive_with_files(&[(PACKED_MANIFEST_ENTRY, b"name: demo")]);
656
657 let err = extract_skill_files("demo", "0.0.1", &archive).unwrap_err();
658 match err {
659 RuntimeError::ArtifactArchive { message, .. } => {
660 assert!(
661 message.contains("skill.md not found or unreadable at archive root"),
662 "{message}"
663 );
664 }
665 other => panic!("expected ArtifactArchive error, got {other:?}"),
666 }
667 }
668
669 #[test]
670 fn extract_skill_files_refuses_an_escaping_entry() {
671 let archive = archive_with_files(&[("skill.md", b"# s"), ("../escape.md", b"x")]);
672
673 let err = extract_skill_files("demo", "0.0.1", &archive).unwrap_err();
674 match err {
675 RuntimeError::ArtifactArchive { message, .. } => {
676 assert!(message.contains("../escape.md"), "{message}");
677 }
678 other => panic!("expected ArtifactArchive error, got {other:?}"),
679 }
680 }
681
682 #[test]
683 fn check_root_wasm_refuses_exactly_as_extract_root_wasm() {
684 let no_root_wasm = archive_with_files(&[(PACKED_MANIFEST_ENTRY, b"name: demo")]);
685 let two_root_wasms = archive_with_files(&[
686 ("alpha.wasm", b"a"),
687 ("zeta.wasm", b"z"),
688 (PACKED_MANIFEST_ENTRY, b"name: demo"),
689 ]);
690 for payload in [b"not a zip".to_vec(), no_root_wasm, two_root_wasms] {
691 let checked = check_root_wasm("demo", "0.0.1", &payload).unwrap_err();
692 let extracted = extract_root_wasm("demo", "0.0.1", &payload).unwrap_err();
693 assert_eq!(checked.to_string(), extracted.to_string());
694 }
695
696 let valid = archive_with_files(&[
697 (PACKED_MANIFEST_ENTRY, b"name: demo"),
698 ("tool.wasm", b"tool"),
699 ]);
700 check_root_wasm("demo", "0.0.1", &valid).unwrap();
701 extract_root_wasm("demo", "0.0.1", &valid).unwrap();
702 }
703
704 #[test]
705 fn check_root_wasm_does_not_inflate() {
706 let big = vec![b'a'; 1024];
707 let archive = archive_with_files(&[("tool.wasm", &big)]);
708
709 let err = extract_root_wasm_capped("demo", "0.0.1", &archive, 16).unwrap_err();
710 assert!(
711 err.to_string()
712 .contains("exceeds the 16-byte decompression ceiling"),
713 "{err}"
714 );
715 check_root_wasm("demo", "0.0.1", &archive).unwrap();
716 }
717
718 #[test]
719 fn default_ceiling_does_not_reject_small_fixtures() {
720 let archive = archive_with_files(&[
721 (PACKED_MANIFEST_ENTRY, b"name: demo\nversion: 0.1.0\n"),
722 ("capsule.wasm", b"small wasm bytes"),
723 ]);
724
725 extract_manifest_yaml("demo", "0.1.0", &archive).unwrap();
726 extract_root_wasm("demo", "0.1.0", &archive).unwrap();
727 }
728}