ic-host-artifacts 0.31.2

Bounded host artifact streams, identities, gzip and Wasm tooling
Documentation
use super::*;
#[cfg(feature = "wasm")]
use crate::wasm::{InspectionLimits, inspect};

fn check(bytes: &[u8]) {
    for payload in wasmparser::Parser::new(0).parse_all(bytes) {
        payload.unwrap();
    }
    #[cfg(feature = "wasm")]
    inspect(
        bytes,
        InspectionLimits {
            module_bytes: bytes.len(),
            sections: 8,
            exports: 10,
            custom_sections: 0,
        },
    )
    .unwrap();
    #[cfg(feature = "wasm-validation")]
    crate::wasm::validate(bytes, bytes.len()).unwrap();
}

#[test]
fn preserves_imports_calls_exports_and_binary_data_across_pages() {
    let imports = [
        FunctionImport {
            module: "host",
            name: "append",
            parameters: 2,
        },
        FunctionImport {
            module: "host",
            name: "finish",
            parameters: 0,
        },
    ];
    let payload = vec![0xff; 65_537];
    let functions = [
        ExportedFunction {
            name: "read\"\\é",
            calls: vec![
                Call {
                    import: 0,
                    arguments: vec![0, 65_537],
                },
                Call {
                    import: 1,
                    arguments: vec![],
                },
            ],
        },
        ExportedFunction {
            name: "write",
            calls: vec![
                Call {
                    import: 0,
                    arguments: vec![65_537, 4],
                },
                Call {
                    import: 1,
                    arguments: vec![],
                },
            ],
        },
    ];
    let data = [
        DataSegment {
            offset: 0,
            bytes: &payload,
        },
        DataSegment {
            offset: 65_537,
            bytes: b"raw\0",
        },
    ];
    let module = CallModule {
        imports: &imports,
        functions: &functions,
        memory: Memory32 {
            pages: 2,
            export: "memory",
        },
        data: &data,
    };
    let bytes = encode(&module, 100_000).unwrap();
    check(&bytes);
    #[cfg(feature = "wasm")]
    {
        let facts = inspect(
            &bytes,
            InspectionLimits {
                module_bytes: bytes.len(),
                sections: 8,
                exports: 3,
                custom_sections: 0,
            },
        )
        .unwrap();
        assert_eq!(facts.imported_functions, 2);
        assert_eq!(facts.defined_functions, 2);
        assert_eq!(facts.data_segments, 2);
        assert_eq!(facts.exports.len(), 3);
    }
    assert!(bytes.windows(payload.len()).any(|window| window == payload));
    assert!(bytes.windows(4).any(|window| window == b"raw\0"));
    let mut calls = Vec::new();
    for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
        if let wasmparser::Payload::CodeSectionEntry(body) = payload.unwrap() {
            for operator in body.get_operators_reader().unwrap() {
                if let wasmparser::Operator::Call { function_index } = operator.unwrap() {
                    calls.push(function_index);
                }
            }
        }
    }
    assert_eq!(calls, [0, 1, 0, 1]);
}

#[test]
fn empty_catalog_and_empty_function_obey_exact_output_allowance() {
    let empty_functions = [ExportedFunction {
        name: "",
        calls: vec![],
    }];
    for functions in [&[][..], &empty_functions[..]] {
        let module = CallModule {
            imports: &[],
            functions,
            memory: Memory32 {
                pages: 0,
                export: "memory",
            },
            data: &[],
        };
        let bytes = encode(&module, 1024).unwrap();
        check(&bytes);
        assert_eq!(encode(&module, bytes.len()).unwrap(), bytes);
        for limit in [0, 7, bytes.len() - 1] {
            assert_eq!(
                encode(&module, limit),
                Err(EncodingError::LimitExceeded { limit })
            );
        }
    }
}

#[test]
fn refuses_duplicate_exports_and_invalid_import_calls() {
    let imports = [FunctionImport {
        module: "host",
        name: "consume",
        parameters: 1,
    }];
    for call in [
        Call {
            import: 1,
            arguments: vec![0],
        },
        Call {
            import: 0,
            arguments: vec![],
        },
        Call {
            import: 0,
            arguments: vec![0, 1],
        },
    ] {
        let functions = [ExportedFunction {
            name: "entry",
            calls: vec![call],
        }];
        let module = CallModule {
            imports: &imports,
            functions: &functions,
            memory: Memory32 {
                pages: 1,
                export: "memory",
            },
            data: &[],
        };
        assert_eq!(
            encode(&module, 1024),
            Err(EncodingError::InvalidCall {
                function: 0,
                call: 0
            })
        );
    }
    for names in [["memory", "other"], ["same", "same"]] {
        let functions = names.map(|name| ExportedFunction {
            name,
            calls: vec![],
        });
        let module = CallModule {
            imports: &[],
            functions: &functions,
            memory: Memory32 {
                pages: 1,
                export: "memory",
            },
            data: &[],
        };
        assert_eq!(encode(&module, 1024), Err(EncodingError::DuplicateExport));
    }
}

#[test]
fn admits_data_extents_and_preserves_unsigned_wasm32_addresses() {
    let mut module = CallModule {
        imports: &[],
        functions: &[],
        memory: Memory32 {
            pages: 1,
            export: "memory",
        },
        data: &[DataSegment {
            offset: 65_535,
            bytes: b"ab",
        }],
    };
    assert_eq!(
        encode(&module, 1024),
        Err(EncodingError::DataOutOfBounds { segment: 0 })
    );
    module.data = &[DataSegment {
        offset: 65_536,
        bytes: &[],
    }];
    check(&encode(&module, 1024).unwrap());
    module.memory.pages = 65_536;
    module.data = &[DataSegment {
        offset: 0x8000_0000,
        bytes: b"a",
    }];
    let bytes = encode(&module, 1024).unwrap();
    check(&bytes);
    let mut observed = false;
    for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
        if let wasmparser::Payload::DataSection(section) = payload.unwrap() {
            for segment in section {
                let segment = segment.unwrap();
                let wasmparser::DataKind::Active { offset_expr, .. } = segment.kind else {
                    panic!("expected active data")
                };
                assert!(matches!(
                    offset_expr.get_operators_reader().read().unwrap(),
                    wasmparser::Operator::I32Const { value: i32::MIN }
                ));
                assert_eq!(segment.data, b"a");
                observed = true;
            }
        }
    }
    assert!(observed);
}

#[test]
fn refuses_invalid_memory_and_unrepresentable_lengths() {
    let module = CallModule {
        imports: &[],
        functions: &[],
        memory: Memory32 {
            pages: 65_537,
            export: "memory",
        },
        data: &[],
    };
    assert_eq!(encode(&module, 1024), Err(EncodingError::InvalidMemory));
    #[cfg(target_pointer_width = "64")]
    assert_eq!(length(usize::MAX), Err(EncodingError::LengthOverflow));
}