standard-plugin-cli 0.1.1

standard-plugin: scaffold, build, check and pack Standard Code plugins
Documentation
//! What a built component imports and exports.

use wasmparser::{Parser, Payload};

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ComponentShape {
    /// Top-level instance and function import names
    /// (`standard:plugin/values@2.0.0`, `wasi:...`).
    pub imports: Vec<String>,
    /// Top-level type imports (`event`, `json`): bound to a type the
    /// component defines, they need nothing from the host.
    pub type_imports: Vec<String>,
    /// Top-level export names (`activate`, ...).
    pub exports: Vec<String>,
    /// The instance imports the component takes at least one function
    /// from; an interface imported only for its types (`account` for the
    /// daemon world's `pane`) is not among them.
    pub function_imports: Vec<String>,
}

/// The instance imports that carry a function, by validating the component
/// and reading each import's instance type. A component the validator
/// refuses counts every instance import.
fn function_imports(bytes: &[u8], imports: &[String]) -> Vec<String> {
    use wasmparser::component_types::ComponentEntityType;
    let mut validator = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
    let Ok(types) = validator.validate_all(bytes) else {
        return imports.to_vec();
    };
    imports
        .iter()
        .filter(
            |name| match types.component_item_for_import(name).map(|item| &item.ty) {
                Some(ComponentEntityType::Instance(id)) => types[*id]
                    .exports
                    .values()
                    .any(|item| matches!(item.ty, ComponentEntityType::Func(_))),
                _ => true,
            },
        )
        .cloned()
        .collect()
}

/// Reads a component's top-level imports and exports. An error names why
/// the bytes are not a component.
pub fn inspect(bytes: &[u8]) -> Result<ComponentShape, String> {
    let mut shape = ComponentShape::default();
    let mut depth = 0usize;
    for payload in Parser::new(0).parse_all(bytes) {
        let payload = payload.map_err(|error| format!("not a valid wasm binary: {error}"))?;
        match payload {
            Payload::Version { encoding, .. } if depth == 0 => {
                if encoding != wasmparser::Encoding::Component {
                    return Err(
                        "a core wasm module, not a component: build for wasm32-wasip2".into(),
                    );
                }
            }
            Payload::ModuleSection { .. } | Payload::ComponentSection { .. } => depth += 1,
            Payload::End(_) => depth = depth.saturating_sub(1),
            Payload::ComponentImportSection(reader) if depth == 0 => {
                for import in reader {
                    let import = import.map_err(|error| error.to_string())?;
                    let name = import.name.name.to_string();
                    if matches!(import.ty, wasmparser::ComponentTypeRef::Type(_)) {
                        shape.type_imports.push(name);
                    } else {
                        shape.imports.push(name);
                    }
                }
            }
            Payload::ComponentExportSection(reader) if depth == 0 => {
                for export in reader {
                    let export = export.map_err(|error| error.to_string())?;
                    shape.exports.push(export.name.name.to_string());
                }
            }
            _ => {}
        }
    }
    shape.function_imports = function_imports(bytes, &shape.imports);
    Ok(shape)
}