#![expect(
clippy::significant_drop_tightening,
reason = "fixture serialization and directory ownership must last through output/filesystem assertions"
)]
use super::*;
use crate::test_support::{Fixture, LIMITS};
use ic_host_process::tool::{ExecutionFailure, OutputStream};
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
use std::{ffi::OsString, fs};
#[test]
fn normalization_preserves_the_canic_line_contract_including_blank_lines() {
for (input, expected) in [
("", ""),
("service : {}", "service : {}\n"),
(
"// \r\nservice : { \r\n query : () -> ();\t\r\n}",
"//\nservice : {\n query : () -> ();\n}\n",
),
("service : {}\n\n", "service : {}\n\n"),
(" indented\u{2003}\n\t\n", " indented\n\n"),
("standalone\rcarriage", "standalone\rcarriage\n"),
] {
assert_eq!(normalize(input.as_bytes(), 1024).unwrap(), expected);
}
}
#[test]
fn normalization_bounds_input_and_added_newlines_without_lossy_utf8() {
assert!(matches!(
normalize(&[0xff], 10),
Err(NormalizationError::Utf8(_))
));
assert!(matches!(
normalize(b"abc", 2),
Err(NormalizationError::InputLimit {
actual: 3,
limit: 2
})
));
assert!(matches!(
normalize(b"abc", 3),
Err(NormalizationError::OutputLimit { limit: 3 })
));
assert_eq!(normalize(b"abc", 4).unwrap(), "abc\n");
assert_eq!(normalize(b"abc\n", 4).unwrap(), "abc\n");
assert_eq!(normalize(b"", 0).unwrap(), "");
}
#[test]
fn extraction_retains_source_tool_and_original_output_evidence() {
let fixture = Fixture::new();
let source = fixture.source();
let original = fs::read(&source).unwrap();
let tool = fixture.tool();
let candid = extract(&tool, &source, &fixture.context(&[]), 8, LIMITS).unwrap();
assert_eq!(
candid.text,
"//\nservice : {\n query : () -> () query;\n}\n"
);
assert_eq!(candid.source_identity, hash_file(&source, 8).unwrap());
assert_eq!(candid.tool_identity, tool.identity());
assert!(candid.evidence.stdout.contains(&b'\r'));
assert!(
candid
.evidence
.status
.is_some_and(|status| status.success())
);
assert_eq!(fs::read(source).unwrap(), original);
}
#[test]
fn extraction_rejects_missing_relative_and_oversized_sources_before_invocation() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let marker = fixture.root.join("invocations");
let environment = [(
OsString::from("FIXTURE_MARKER"),
marker.as_os_str().to_owned(),
)];
let context = fixture.context(&environment);
assert!(matches!(
extract(&tool, Path::new("source.wasm"), &context, 8, LIMITS),
Err(ExtractionError::SourcePath)
));
assert!(matches!(
extract(&tool, &source, &context, 7, LIMITS),
Err(ExtractionError::Input(ArtifactError::LimitExceeded {
limit: 7
}))
));
assert!(matches!(
extract(&tool, &fixture.root.join("missing"), &context, 8, LIMITS),
Err(ExtractionError::Input(ArtifactError::Io(_)))
));
assert!(!marker.exists());
}
#[test]
fn extractor_failure_invalid_utf8_and_output_overflow_keep_bounded_evidence() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("fail"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::Tool(ToolError::Execution(error))) if matches!(error.failure, ExecutionFailure::ExitStatus) && error.evidence.stderr == b"extract failed")
);
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("invalid-utf8"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::Normalize { source: NormalizationError::Utf8(_), evidence }) if evidence.stdout == [0xff])
);
let error = extract(
&tool,
&source,
&fixture.context(&[]),
8,
OutputLimits {
stdout_bytes: 10,
..LIMITS
},
);
assert!(
matches!(error, Err(ExtractionError::Tool(ToolError::Execution(error))) if matches!(error.failure, ExecutionFailure::OutputLimit { stream: OutputStream::Stdout }) && error.evidence.stdout.len() == 10)
);
assert_eq!(fs::read(source).unwrap(), b"\0asm\x01\0\0\0");
}
#[test]
fn normalized_output_overflow_retains_successful_raw_extractor_bytes() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("unterminated"),
)];
let error = extract(
&tool,
&source,
&fixture.context(&environment),
8,
OutputLimits {
stdout_bytes: b"service : {}".len(),
..LIMITS
},
);
assert!(
matches!(error, Err(ExtractionError::Normalize { source: NormalizationError::OutputLimit { .. }, evidence }) if evidence.stdout == b"service : {}" && evidence.status.is_some_and(|status| status.success()))
);
}
#[test]
fn source_changes_or_failed_reinspection_reject_successful_output() {
let fixture = Fixture::new();
let tool = fixture.tool();
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("mutate"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 100, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceChanged { before, after, evidence }) if before != after && evidence.stdout == b"service : {}\n")
);
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("grow"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceInspection { source: ArtifactError::LimitExceeded { limit: 8 }, evidence }) if evidence.stdout == b"service : {}\n")
);
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("remove"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceInspection { source: ArtifactError::Io(_), evidence }) if evidence.stdout == b"service : {}\n")
);
}
#[test]
fn grammar_and_expected_service_policy_remain_with_the_consumer() {
let fixture = Fixture::new();
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("empty"),
)];
let candid = extract(
&fixture.tool(),
&source,
&fixture.context(&environment),
8,
LIMITS,
)
.unwrap();
assert_eq!(candid.text, "");
}
#[test]
fn source_paths_with_spaces_shell_characters_and_non_utf8_are_literal_arguments() {
let mut name = b"source $(must-not-execute) space ".to_vec();
name.push(0xff);
name.extend_from_slice(b".wasm");
for name in [
OsString::from("source $(must-not-execute) space ☃.wasm"),
OsString::from_vec(name),
] {
let fixture = Fixture::new();
let tool = fixture.tool();
let non_utf8 = name.to_str().is_none();
let source = fixture.root.join(name);
let marker = fixture.root.join("invocations");
let environment = [
(
OsString::from("FIXTURE_MARKER"),
marker.as_os_str().to_owned(),
),
(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("source-path"),
),
];
let context = fixture.context(&environment);
if let Err(error) = fs::write(&source, b"\0asm\x01\0\0\0") {
assert!(non_utf8);
let illegal_sequence = rustix::io::Errno::ILSEQ.raw_os_error();
assert_eq!(error.raw_os_error(), Some(illegal_sequence));
let lookup = fs::metadata(&source).expect_err("rejected source must not exist");
assert!(
lookup.kind() == std::io::ErrorKind::NotFound
|| lookup.raw_os_error() == Some(illegal_sequence)
);
let extraction = extract(&tool, &source, &context, 8, LIMITS)
.expect_err("rejected source must fail before dispatch");
assert!(
matches!(
&extraction,
ExtractionError::Input(ArtifactError::Io(error))
if error.kind() == lookup.kind()
&& error.raw_os_error() == lookup.raw_os_error()
),
"{extraction:?}"
);
assert!(!marker.exists());
continue;
}
let candid = extract(&tool, &source, &context, 8, LIMITS).unwrap();
assert_eq!(candid.evidence.stderr, source.as_os_str().as_bytes());
assert_eq!(candid.source_identity.bytes, 8);
assert_eq!(fs::read(&source).unwrap(), b"\0asm\x01\0\0\0");
assert_eq!(fs::read(marker).unwrap(), b"invoked\n");
}
}