ic-host-tools 0.1.13

Shared Internet Computer host artifact tooling
Documentation
//! Public consumer projections: policy remains with the artifact owner.

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());
    // The consumer's publisher owns this step; shared copying never performs it.
    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]); // Type () -> ().
    bytes.extend([3, 3, 2, 0, 0]); // Two defined functions of that type.
    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)
    );
    // Different owners may accept or reject identical facts without a library default.
    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 { .. })
    ));
}