coil-wasm 0.1.1

WASM extension runtime and host APIs for the Coil framework.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use super::super::cache::{CacheVisibility, TypedCacheHint};
use super::super::json_ld::{JsonLdNode, JsonLdValue};
use super::super::metadata::TypedMetadata;
use super::cursor::{
    ByteCursor, read_option_string, write_len_u32, write_option_string, write_string, write_u8,
    write_u16, write_u32, write_u64,
};
use super::mapping::{
    extension_point_kind_from_tag, extension_point_kind_tag, robot_from_tag, robot_tag,
};
use super::{ABI_MAGIC, ABI_VERSION, TypedExecutionOutput, TypedResponseBody};
use crate::error::WasmModelError;

pub(super) fn encode_output(output: &TypedExecutionOutput) -> Result<Vec<u8>, WasmModelError> {
    let mut bytes = Vec::new();
    bytes.extend(ABI_MAGIC);
    write_u16(&mut bytes, ABI_VERSION);
    write_u8(&mut bytes, extension_point_kind_tag(output.surface));
    write_u16(&mut bytes, output.status);
    write_body(&mut bytes, &output.body)?;
    write_metadata(&mut bytes, &output.metadata)?;
    write_cache_hint(&mut bytes, output.cache_hint.as_ref())?;
    Ok(bytes)
}

pub(super) fn decode_output(bytes: &[u8]) -> Result<TypedExecutionOutput, WasmModelError> {
    let mut cursor = ByteCursor::new(bytes);
    let magic = cursor.read_array::<4>()?;
    if magic != ABI_MAGIC {
        return Err(WasmModelError::InvalidTypedReturn {
            reason: "typed return payload has an invalid magic header".to_string(),
        });
    }
    let version = cursor.read_u16()?;
    if version != ABI_VERSION {
        return Err(WasmModelError::InvalidTypedReturn {
            reason: format!("typed return payload version `{version}` is not supported"),
        });
    }

    let surface = extension_point_kind_from_tag(cursor.read_u8()?)?;
    let status = cursor.read_u16()?;
    let body = read_body(&mut cursor)?;
    let metadata = read_metadata(&mut cursor)?;
    let cache_hint = read_cache_hint(&mut cursor)?;
    if !cursor.is_empty() {
        return Err(WasmModelError::InvalidTypedReturn {
            reason: "typed return payload has trailing bytes".to_string(),
        });
    }

    TypedExecutionOutput::new(surface, status, body, metadata, cache_hint)
}

fn write_body(bytes: &mut Vec<u8>, body: &TypedResponseBody) -> Result<(), WasmModelError> {
    match body {
        TypedResponseBody::HtmlDocument(html) => {
            write_u8(bytes, 0);
            write_string(bytes, html)?;
        }
        TypedResponseBody::HtmlFragment(html) => {
            write_u8(bytes, 1);
            write_string(bytes, html)?;
        }
        TypedResponseBody::JsonObject(payload) => {
            write_u8(bytes, 2);
            write_u32(
                bytes,
                write_len_u32("json_object_entry_count", payload.len())?,
            );
            for (key, value) in payload {
                write_string(bytes, key)?;
                write_string(bytes, value)?;
            }
        }
    }
    Ok(())
}

fn read_body(cursor: &mut ByteCursor<'_>) -> Result<TypedResponseBody, WasmModelError> {
    Ok(match cursor.read_u8()? {
        0 => TypedResponseBody::HtmlDocument(cursor.read_string()?),
        1 => TypedResponseBody::HtmlFragment(cursor.read_string()?),
        2 => {
            let count = cursor.read_u32()? as usize;
            let mut payload = BTreeMap::new();
            for _ in 0..count {
                let key = cursor.read_string()?;
                let value = cursor.read_string()?;
                if payload.insert(key.clone(), value).is_some() {
                    return Err(WasmModelError::InvalidTypedReturn {
                        reason: format!("duplicate JSON response key `{key}`"),
                    });
                }
            }
            TypedResponseBody::JsonObject(payload)
        }
        tag => {
            return Err(WasmModelError::InvalidTypedReturn {
                reason: format!("typed response body tag `{tag}` is not supported"),
            });
        }
    })
}

fn write_metadata(bytes: &mut Vec<u8>, metadata: &TypedMetadata) -> Result<(), WasmModelError> {
    write_option_string(bytes, metadata.title.as_ref())?;
    write_option_string(bytes, metadata.description.as_ref())?;
    write_option_string(bytes, metadata.canonical_url.as_ref())?;
    write_u32(
        bytes,
        write_len_u32("alternate_url_count", metadata.alternate_urls.len())?,
    );
    for (locale, url) in &metadata.alternate_urls {
        write_string(bytes, locale)?;
        write_string(bytes, url)?;
    }
    write_u32(bytes, write_len_u32("robots_count", metadata.robots.len())?);
    for directive in &metadata.robots {
        write_u8(bytes, robot_tag(*directive));
    }
    write_u32(
        bytes,
        write_len_u32("json_ld_node_count", metadata.json_ld.len())?,
    );
    for node in &metadata.json_ld {
        write_string(bytes, node.schema_type())?;
        write_u32(
            bytes,
            write_len_u32("json_ld_property_count", node.properties().len())?,
        );
        for (property, value) in node.properties() {
            write_string(bytes, property)?;
            write_json_ld_value(bytes, value)?;
        }
    }
    Ok(())
}

fn read_metadata(cursor: &mut ByteCursor<'_>) -> Result<TypedMetadata, WasmModelError> {
    let title = read_option_string(cursor)?;
    let description = read_option_string(cursor)?;
    let canonical_url = read_option_string(cursor)?;
    let alternate_len = cursor.read_u32()? as usize;
    let mut alternate_urls = BTreeMap::new();
    for _ in 0..alternate_len {
        let locale = cursor.read_string()?;
        let url = cursor.read_string()?;
        if alternate_urls.insert(locale.clone(), url).is_some() {
            return Err(WasmModelError::InvalidTypedReturn {
                reason: format!("duplicate alternate URL locale `{locale}`"),
            });
        }
    }
    let robots_len = cursor.read_u32()? as usize;
    let mut robots = BTreeSet::new();
    for _ in 0..robots_len {
        robots.insert(robot_from_tag(cursor.read_u8()?)?);
    }
    let json_ld_len = cursor.read_u32()? as usize;
    let mut json_ld = Vec::with_capacity(json_ld_len);
    for _ in 0..json_ld_len {
        let schema_type = cursor.read_string()?;
        let property_len = cursor.read_u32()? as usize;
        let mut node = JsonLdNode::new(schema_type)?;
        for _ in 0..property_len {
            let property = cursor.read_string()?;
            let value = read_json_ld_value(cursor)?;
            node = node.insert_value(property, value)?;
        }
        json_ld.push(node);
    }

    Ok(TypedMetadata {
        title,
        description,
        canonical_url,
        alternate_urls,
        robots,
        json_ld,
    })
}

fn write_cache_hint(
    bytes: &mut Vec<u8>,
    cache_hint: Option<&TypedCacheHint>,
) -> Result<(), WasmModelError> {
    match cache_hint {
        Some(cache) => {
            write_u8(bytes, 1);
            write_u8(
                bytes,
                match cache.visibility {
                    CacheVisibility::Public => 0,
                    CacheVisibility::Private => 1,
                },
            );
            write_u64(bytes, cache.max_age_seconds);
            match cache.stale_while_revalidate_seconds {
                Some(value) => {
                    write_u8(bytes, 1);
                    write_u64(bytes, value);
                }
                None => write_u8(bytes, 0),
            }
            let mut flags = 0u8;
            if cache.vary_by_locale {
                flags |= 0b001;
            }
            if cache.vary_by_user {
                flags |= 0b010;
            }
            if cache.vary_by_session {
                flags |= 0b100;
            }
            write_u8(bytes, flags);
            write_u32(bytes, write_len_u32("cache_tag_count", cache.tags.len())?);
            for tag in &cache.tags {
                write_string(bytes, tag)?;
            }
        }
        None => write_u8(bytes, 0),
    }
    Ok(())
}

fn read_cache_hint(cursor: &mut ByteCursor<'_>) -> Result<Option<TypedCacheHint>, WasmModelError> {
    if cursor.read_u8()? == 0 {
        return Ok(None);
    }

    let visibility = match cursor.read_u8()? {
        0 => CacheVisibility::Public,
        1 => CacheVisibility::Private,
        other => {
            return Err(WasmModelError::InvalidTypedReturn {
                reason: format!("typed cache visibility tag `{other}` is not supported"),
            });
        }
    };
    let max_age_seconds = cursor.read_u64()?;
    let stale_while_revalidate_seconds = if cursor.read_u8()? == 0 {
        None
    } else {
        Some(cursor.read_u64()?)
    };
    let flags = cursor.read_u8()?;
    let tag_len = cursor.read_u32()? as usize;
    let mut tags = BTreeSet::new();
    for _ in 0..tag_len {
        let tag = cursor.read_string()?;
        if !tags.insert(tag.clone()) {
            return Err(WasmModelError::InvalidTypedReturn {
                reason: format!("duplicate cache tag `{tag}`"),
            });
        }
    }

    Ok(Some(TypedCacheHint {
        visibility,
        max_age_seconds,
        stale_while_revalidate_seconds,
        vary_by_locale: flags & 0b001 != 0,
        vary_by_user: flags & 0b010 != 0,
        vary_by_session: flags & 0b100 != 0,
        tags,
    }))
}

fn write_json_ld_value(bytes: &mut Vec<u8>, value: &JsonLdValue) -> Result<(), WasmModelError> {
    match value {
        JsonLdValue::String(value) => {
            write_u8(bytes, 0);
            write_string(bytes, value)?;
        }
        JsonLdValue::Number(value) => {
            write_u8(bytes, 1);
            write_string(bytes, value)?;
        }
        JsonLdValue::Bool(value) => {
            write_u8(bytes, 2);
            write_u8(bytes, u8::from(*value));
        }
        JsonLdValue::Node(node) => {
            write_u8(bytes, 3);
            write_string(bytes, node.schema_type())?;
            write_u32(
                bytes,
                write_len_u32("json_ld_property_count", node.properties().len())?,
            );
            for (property, property_value) in node.properties() {
                write_string(bytes, property)?;
                write_json_ld_value(bytes, property_value)?;
            }
        }
        JsonLdValue::List(values) => {
            write_u8(bytes, 4);
            write_u32(
                bytes,
                write_len_u32("json_ld_list_item_count", values.len())?,
            );
            for item in values {
                write_json_ld_value(bytes, item)?;
            }
        }
    }
    Ok(())
}

fn read_json_ld_value(cursor: &mut ByteCursor<'_>) -> Result<JsonLdValue, WasmModelError> {
    Ok(match cursor.read_u8()? {
        0 => JsonLdValue::String(cursor.read_string()?),
        1 => JsonLdValue::Number(cursor.read_string()?),
        2 => JsonLdValue::Bool(match cursor.read_u8()? {
            0 => false,
            1 => true,
            other => {
                return Err(WasmModelError::InvalidTypedReturn {
                    reason: format!("typed JSON-LD boolean tag `{other}` is invalid"),
                });
            }
        }),
        3 => {
            let schema_type = cursor.read_string()?;
            let property_len = cursor.read_u32()? as usize;
            let mut node = JsonLdNode::new(schema_type)?;
            for _ in 0..property_len {
                let property = cursor.read_string()?;
                let value = read_json_ld_value(cursor)?;
                node = node.insert_value(property, value)?;
            }
            JsonLdValue::Node(node)
        }
        4 => {
            let count = cursor.read_u32()? as usize;
            let mut values = Vec::with_capacity(count);
            for _ in 0..count {
                values.push(read_json_ld_value(cursor)?);
            }
            JsonLdValue::List(values)
        }
        tag => {
            return Err(WasmModelError::InvalidTypedReturn {
                reason: format!("typed JSON-LD value tag `{tag}` is invalid"),
            });
        }
    })
}