use ic_host_tools::{
artifact::{ArtifactError, BoundedWriter, Sha256Digest, copy_reader, verify_reader},
wasm::{ExportKind, InspectionLimits, WasmFacts, inspect},
};
use std::{collections::BTreeMap, io};
#[test]
fn staged_stream_identity_is_checked_before_consumer_publication() {
let old = b"existing published artifact".to_vec();
let candidate = b"candidate artifact";
let expected = Sha256Digest::compute(candidate);
let mut published = old.clone();
let mut staged = Vec::new();
let observed = copy_reader(b"wrong".as_slice(), &mut staged, 128).unwrap();
assert_ne!(observed.sha256, expected);
assert_eq!(published, old);
assert_eq!(staged, b"wrong");
let mut staged = BoundedWriter::new(Vec::new(), 128);
let observed = copy_reader(candidate.as_slice(), &mut staged, 128).unwrap();
assert_eq!(observed.sha256, expected);
assert_eq!(observed.bytes, staged.bytes_written());
published = staged.into_inner();
assert_eq!(published, candidate);
let mut count = BoundedWriter::new(io::sink(), 128);
let observed = copy_reader(candidate.as_slice(), &mut count, 128).unwrap();
assert_eq!(observed.bytes, count.bytes_written());
assert_eq!(observed.sha256, expected);
}
const LIMITS: InspectionLimits = InspectionLimits {
module_bytes: 4096,
sections: 20,
exports: 20,
custom_sections: 10,
};
fn artifact(export_index: u8, candid: &[u8]) -> Vec<u8> {
let mut bytes = b"\0asm\x01\0\0\0".to_vec();
bytes.extend([1, 4, 1, 0x60, 0, 0]); bytes.extend([3, 3, 2, 0, 0]); let name = b"canister_query status";
let mut export = vec![1, u8::try_from(name.len()).unwrap()];
export.extend(name);
export.extend([0, export_index]);
bytes.extend([7, u8::try_from(export.len()).unwrap()]);
bytes.extend(export);
bytes.extend([10, 7, 2, 2, 0, 0x0b, 2, 0, 0x0b]);
let metadata_name = b"icp:public candid:service";
let mut metadata = vec![u8::try_from(metadata_name.len()).unwrap()];
metadata.extend(metadata_name);
metadata.extend(candid);
bytes.extend([0, u8::try_from(metadata.len()).unwrap()]);
bytes.extend(metadata);
bytes
}
fn export_kinds<'a>(facts: &WasmFacts<'a>) -> BTreeMap<&'a str, ExportKind> {
facts
.exports
.iter()
.map(|(name, export)| (*name, export.kind))
.collect()
}
fn public_candid<'a>(facts: &WasmFacts<'a>) -> Vec<&'a [u8]> {
facts
.custom_sections
.iter()
.filter(|section| section.name == "icp:public candid:service")
.map(|section| section.data)
.collect()
}
#[test]
fn transform_contract_preserves_export_kinds_and_selected_metadata_after_reindexing() {
let compiler = artifact(0, b"service : { status : () -> () query; }\n");
let final_bytes = artifact(1, b"service : { status : () -> () query; }\n");
let changed = artifact(1, b"service : {}\n");
let before = inspect(&compiler, LIMITS).unwrap();
let after = inspect(&final_bytes, LIMITS).unwrap();
assert_eq!(export_kinds(&before), export_kinds(&after));
assert_eq!(public_candid(&before), public_candid(&after));
assert_ne!(before.exports, after.exports);
assert_ne!(
public_candid(&before),
public_candid(&inspect(&changed, LIMITS).unwrap())
);
}
#[test]
fn report_facts_support_consumer_budgets_after_exact_digest_verification() {
let bytes = artifact(0, b"service : {}\n");
let expected = Sha256Digest::compute(&bytes);
let identity = verify_reader(bytes.as_slice(), LIMITS.module_bytes as u64, expected).unwrap();
let report = inspect(&bytes, LIMITS).unwrap();
assert_eq!(identity.bytes, report.raw_bytes as u64);
assert_eq!(
(report.defined_functions, report.code_section_bytes),
(2, 7)
);
let admits = |budget| report.code_section_bytes <= budget;
assert!(admits(7));
assert!(!admits(6));
let application_functions: Vec<_> = report
.exports
.iter()
.filter(|(name, export)| {
export.kind == ExportKind::Function && name.starts_with("canister_query ")
})
.map(|(name, _)| *name)
.collect();
assert_eq!(application_functions, ["canister_query status"]);
assert!(matches!(
verify_reader(
bytes.as_slice(),
LIMITS.module_bytes as u64,
Sha256Digest::compute(b"wrong")
),
Err(ArtifactError::DigestMismatch { .. })
));
}