baedeker-core 0.1.0

WebAssembly runtime core — decode, validate, execute (no_std)
Documentation
// Copyright (C) 2026 Industrial Algebra
// SPDX-License-Identifier: Apache-2.0

use std::path::Path;

use baedeker_core::binary::module::Module;
use baedeker_core::error::DecodeErrorKind;
use baedeker_core::validate::error::{ValidationError, ValidationErrorKind};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Expectation {
    Valid,
    DecodeError,
    ValidationError,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct CaseMeta {
    kind: Option<String>,
    offset: Option<u32>,
    context: Option<String>,
    decode_kind: Option<String>,
}

fn read_case_meta(path: &Path) -> Option<CaseMeta> {
    let meta_path = path.with_extension("meta");
    if !meta_path.exists() {
        return None;
    }

    let mut meta = CaseMeta::default();
    let text = std::fs::read_to_string(&meta_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", meta_path.display()));
    for raw_line in text.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let (key, value) = line.split_once('=').unwrap_or_else(|| {
            panic!(
                "{}: expected key=value metadata line, found {line:?}",
                meta_path.display()
            )
        });
        match key.trim() {
            "kind" => meta.kind = Some(value.trim().to_owned()),
            "offset" => {
                meta.offset = Some(value.trim().parse().unwrap_or_else(|e| {
                    panic!(
                        "{}: invalid offset {:?}: {e}",
                        meta_path.display(),
                        value.trim()
                    )
                }))
            }
            "context" => meta.context = Some(value.trim().to_owned()),
            "decode_kind" => meta.decode_kind = Some(value.trim().to_owned()),
            other => panic!("{}: unknown metadata key {other:?}", meta_path.display()),
        }
    }

    Some(meta)
}

fn validation_kind_name(kind: &ValidationErrorKind) -> &'static str {
    match kind {
        ValidationErrorKind::UnknownTypeIdx { .. } => "UnknownTypeIdx",
        ValidationErrorKind::UnknownFuncIdx { .. } => "UnknownFuncIdx",
        ValidationErrorKind::UndeclaredFuncRef { .. } => "UndeclaredFuncRef",
        ValidationErrorKind::UnknownLocalIdx { .. } => "UnknownLocalIdx",
        ValidationErrorKind::UninitializedLocal { .. } => "UninitializedLocal",
        ValidationErrorKind::UnknownGlobalIdx { .. } => "UnknownGlobalIdx",
        ValidationErrorKind::UnknownTableIdx { .. } => "UnknownTableIdx",
        ValidationErrorKind::UnknownMemIdx { .. } => "UnknownMemIdx",
        ValidationErrorKind::UnknownDataIdx { .. } => "UnknownDataIdx",
        ValidationErrorKind::UnknownElemIdx { .. } => "UnknownElemIdx",
        ValidationErrorKind::InvalidMemArgAlign { .. } => "InvalidMemArgAlign",
        ValidationErrorKind::InvalidSimdLaneIdx { .. } => "InvalidSimdLaneIdx",
        ValidationErrorKind::UnknownLabelIdx { .. } => "UnknownLabelIdx",
        ValidationErrorKind::Decode { .. } => "Decode",
        ValidationErrorKind::InvalidGlobalInitExpr => "InvalidGlobalInitExpr",
        ValidationErrorKind::NonConstantGlobalInitExpr => "NonConstantGlobalInitExpr",
        ValidationErrorKind::MutableGlobalInInitExpr { .. } => "MutableGlobalInInitExpr",
        ValidationErrorKind::ImmutableGlobalSet { .. } => "ImmutableGlobalSet",
        ValidationErrorKind::GlobalInitTypeMismatch { .. } => "GlobalInitTypeMismatch",
        ValidationErrorKind::BranchTypeMismatch { .. } => "BranchTypeMismatch",
        ValidationErrorKind::InvalidBrOnNonNullTarget { .. } => "InvalidBrOnNonNullTarget",
        ValidationErrorKind::InconsistentBranchTypes { .. } => "InconsistentBranchTypes",
        ValidationErrorKind::UnexpectedElse => "UnexpectedElse",
        ValidationErrorKind::UnexpectedEnd => "UnexpectedEnd",
        ValidationErrorKind::UnterminatedControlFrames => "UnterminatedControlFrames",
        ValidationErrorKind::ElseOutsideIf => "ElseOutsideIf",
        ValidationErrorKind::MissingElseForResult => "MissingElseForResult",
        ValidationErrorKind::InvalidBlockType { .. } => "InvalidBlockType",
        ValidationErrorKind::ControlResultTypeMismatch { .. } => "ControlResultTypeMismatch",
        ValidationErrorKind::InvalidSelectResultArity { .. } => "InvalidSelectResultArity",
        ValidationErrorKind::SelectOperandTypeMismatch { .. } => "SelectOperandTypeMismatch",
        ValidationErrorKind::StackUnderflow { .. } => "StackUnderflow",
        ValidationErrorKind::TypeMismatch { .. } => "TypeMismatch",
        ValidationErrorKind::FunctionResultTypeMismatch { .. } => "FunctionResultTypeMismatch",
        ValidationErrorKind::ResultTypeMismatch { .. } => "ResultTypeMismatch",
        ValidationErrorKind::InvalidStartFunctionType { .. } => "InvalidStartFunctionType",
        ValidationErrorKind::InvalidElementExpr => "InvalidElementExpr",
        ValidationErrorKind::NonConstantElementExpr => "NonConstantElementExpr",
        ValidationErrorKind::ElementExprTypeMismatch { .. } => "ElementExprTypeMismatch",
        ValidationErrorKind::ElementTableTypeMismatch { .. } => "ElementTableTypeMismatch",
        ValidationErrorKind::InvalidCallIndirectTableType { .. } => "InvalidCallIndirectTableType",
        ValidationErrorKind::MissingDataCountSection { .. } => "MissingDataCountSection",
        ValidationErrorKind::DuplicateExportName { .. } => "DuplicateExportName",
        ValidationErrorKind::MemorySizeOutOfRange => "MemorySizeOutOfRange",
        ValidationErrorKind::MemoryMinExceedsMax => "MemoryMinExceedsMax",
        ValidationErrorKind::TableTypeMismatch => "TableTypeMismatch",
    }
}

fn decode_kind_name(kind: &DecodeErrorKind) -> &'static str {
    match kind {
        DecodeErrorKind::UnexpectedEof => "UnexpectedEof",
        DecodeErrorKind::InvalidMagic => "InvalidMagic",
        DecodeErrorKind::UnsupportedVersion { .. } => "UnsupportedVersion",
        DecodeErrorKind::Leb128TooLong => "Leb128TooLong",
        DecodeErrorKind::TooManyLocals => "TooManyLocals",
        DecodeErrorKind::Leb128Overflow => "Leb128Overflow",
        DecodeErrorKind::UnknownSectionId { .. } => "UnknownSectionId",
        DecodeErrorKind::SectionOverflow => "SectionOverflow",
        DecodeErrorKind::SectionOutOfOrder { .. } => "SectionOutOfOrder",
        DecodeErrorKind::DuplicateSection { .. } => "DuplicateSection",
        DecodeErrorKind::UnknownValType { .. } => "UnknownValType",
        DecodeErrorKind::UnknownRefType { .. } => "UnknownRefType",
        DecodeErrorKind::UnknownImportDesc { .. } => "UnknownImportDesc",
        DecodeErrorKind::UnknownExportDesc { .. } => "UnknownExportDesc",
        DecodeErrorKind::InvalidMutability { .. } => "InvalidMutability",
        DecodeErrorKind::InvalidUtf8 => "InvalidUtf8",
        DecodeErrorKind::FunctionCodeLengthMismatch { .. } => "FunctionCodeLengthMismatch",
        DecodeErrorKind::UnknownOpcode { .. } => "UnknownOpcode",
        DecodeErrorKind::UnknownSimdOpcode { .. } => "UnknownSimdOpcode",
        DecodeErrorKind::UnexpectedByte { .. } => "UnexpectedByte",
        DecodeErrorKind::SectionSizeMismatch { .. } => "SectionSizeMismatch",
    }
}

fn decode_context_name(context: &baedeker_core::error::DecodeContext) -> String {
    match context {
        baedeker_core::error::DecodeContext::Magic => "Magic".to_owned(),
        baedeker_core::error::DecodeContext::Version => "Version".to_owned(),
        baedeker_core::error::DecodeContext::SectionHeader => "SectionHeader".to_owned(),
        baedeker_core::error::DecodeContext::SectionBody { id } => format!("SectionBody({id})"),
        baedeker_core::error::DecodeContext::Leb128 => "Leb128".to_owned(),
        baedeker_core::error::DecodeContext::TypeSection => "TypeSection".to_owned(),
        baedeker_core::error::DecodeContext::ImportSection => "ImportSection".to_owned(),
        baedeker_core::error::DecodeContext::FunctionSection => "FunctionSection".to_owned(),
        baedeker_core::error::DecodeContext::TableSection => "TableSection".to_owned(),
        baedeker_core::error::DecodeContext::GlobalSection => "GlobalSection".to_owned(),
        baedeker_core::error::DecodeContext::MemorySection => "MemorySection".to_owned(),
        baedeker_core::error::DecodeContext::ExportSection => "ExportSection".to_owned(),
        baedeker_core::error::DecodeContext::StartSection => "StartSection".to_owned(),
        baedeker_core::error::DecodeContext::ElementSection => "ElementSection".to_owned(),
        baedeker_core::error::DecodeContext::DataSection => "DataSection".to_owned(),
        baedeker_core::error::DecodeContext::DataCountSection => "DataCountSection".to_owned(),
        baedeker_core::error::DecodeContext::CodeSection => "CodeSection".to_owned(),
    }
}

fn assert_validation_meta(path: &Path, err: &ValidationError, meta: &CaseMeta) {
    if let Some(offset) = meta.offset {
        assert_eq!(
            err.offset.0,
            offset as usize,
            "{}: unexpected validation error offset",
            path.display()
        );
    }
    if let Some(kind) = &meta.kind {
        assert_eq!(
            validation_kind_name(&err.kind),
            kind,
            "{}: unexpected validation error kind",
            path.display()
        );
    }
    if meta.context.is_some() || meta.decode_kind.is_some() {
        let ValidationErrorKind::Decode { context, kind } = &err.kind else {
            panic!(
                "{}: decode metadata requires ValidationErrorKind::Decode, found {}",
                path.display(),
                validation_kind_name(&err.kind)
            );
        };
        if let Some(expected_context) = &meta.context {
            assert_eq!(
                decode_context_name(context),
                *expected_context,
                "{}: unexpected wrapped decode context",
                path.display()
            );
        }
        if let Some(expected_kind) = &meta.decode_kind {
            assert_eq!(
                decode_kind_name(kind),
                expected_kind,
                "{}: unexpected wrapped decode error kind",
                path.display()
            );
        }
    }
}

fn assert_decode_meta(path: &Path, err: &baedeker_core::error::DecodeError, meta: &CaseMeta) {
    if let Some(offset) = meta.offset {
        assert_eq!(
            err.offset.0,
            offset as usize,
            "{}: unexpected decode error offset",
            path.display()
        );
    }
    if let Some(kind) = &meta.kind {
        assert_eq!(
            decode_kind_name(&err.kind),
            kind,
            "{}: unexpected decode error kind",
            path.display()
        );
    }
    if let Some(context) = &meta.context {
        assert_eq!(
            decode_context_name(&err.context),
            *context,
            "{}: unexpected decode error context",
            path.display()
        );
    }
    if let Some(kind) = &meta.decode_kind {
        assert_eq!(
            decode_kind_name(&err.kind),
            kind,
            "{}: unexpected decode error kind",
            path.display()
        );
    }
}

fn assert_invalid_case_meta(path: &Path, expected: Expectation) {
    let meta = read_case_meta(path).unwrap_or_else(|| {
        panic!(
            "{}: invalid cases must provide .meta assertions",
            path.display()
        )
    });

    assert!(
        meta.kind.is_some(),
        "{}: invalid-case metadata must specify kind=...",
        path.display()
    );
    assert!(
        meta.offset.is_some(),
        "{}: invalid-case metadata must specify offset=...",
        path.display()
    );

    match expected {
        Expectation::DecodeError => {
            assert!(
                meta.context.is_some(),
                "{}: invalid-decode metadata must specify context=...",
                path.display()
            );
        }
        Expectation::ValidationError => {
            if meta.kind.as_deref() == Some("Decode") {
                assert!(
                    meta.context.is_some(),
                    "{}: decode-preserving invalid-validate metadata must specify context=...",
                    path.display()
                );
                assert!(
                    meta.decode_kind.is_some(),
                    "{}: decode-preserving invalid-validate metadata must specify decode_kind=...",
                    path.display()
                );
            }
        }
        Expectation::Valid => unreachable!("only invalid cases require metadata assertions"),
    }
}

fn run_case(path: &Path, expected: Expectation) {
    let bytes = baedeker_testdata::spec_case_bytes(path);
    let meta = read_case_meta(path);
    match (Module::decode(&bytes), expected) {
        (Ok(module), Expectation::Valid) => {
            module.validate().unwrap_or_else(|e| {
                panic!(
                    "{}: expected valid module, got validation error: {e}",
                    path.display()
                )
            });
        }
        (Ok(module), Expectation::ValidationError) => {
            let err = module.validate().unwrap_err();
            if let Some(meta) = &meta {
                assert_validation_meta(path, &err, meta);
            }
        }
        (Err(err), Expectation::DecodeError) => {
            if let Some(meta) = &meta {
                assert_decode_meta(path, &err, meta);
            }
        }
        (Ok(_), Expectation::DecodeError) => {
            panic!(
                "{}: expected decode error, module decoded successfully",
                path.display()
            )
        }
        (Err(e), Expectation::Valid) => {
            panic!(
                "{}: expected valid module, got decode error: {e}",
                path.display()
            )
        }
        (Err(e), Expectation::ValidationError) => {
            panic!(
                "{}: expected validation error, got decode error: {e}",
                path.display()
            )
        }
    }
}

#[test]
fn spec_valid_groundwork_cases() {
    for path in baedeker_testdata::spec_cases("valid") {
        run_case(&path, Expectation::Valid);
    }
}

#[test]
fn spec_invalid_decode_groundwork_cases() {
    for path in baedeker_testdata::spec_cases("invalid-decode") {
        assert_invalid_case_meta(&path, Expectation::DecodeError);
        run_case(&path, Expectation::DecodeError);
    }
}

#[test]
fn spec_invalid_validate_groundwork_cases() {
    for path in baedeker_testdata::spec_cases("invalid-validate") {
        assert_invalid_case_meta(&path, Expectation::ValidationError);
        run_case(&path, Expectation::ValidationError);
    }
}