hara-native 0.1.13

HAL-free native host runtime and package launcher for Hara
Documentation
use super::super::halc::{HalcModule, HalcOrigin};
use super::super::halc_trace::{
    HalcArtifactInspection, HalcArtifactTrace, HalcTraceEvent, HalcTraceEvidence, HalcTraceStatus,
    HalcTraceValue, HALC_TRACE_SCHEMA,
};
use super::super::{Form, SchemaType};
use sha2::{Digest, Sha256};
use std::collections::HashMap;

pub const DEFAULT_MAX_FORMS: usize = 32;
pub const DEFAULT_MAX_SCHEMA_ENTRIES: usize = 64;
pub const DEFAULT_MAX_TEXT_BYTES: usize = 512;
pub const DEFAULT_ARTIFACT_PREVIEW_BYTES: usize = 64;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HalcSourceTraceLimits {
    pub max_forms: usize,
    pub max_schema_entries: usize,
    pub max_text_bytes: usize,
    pub max_artifact_preview_bytes: usize,
}

impl Default for HalcSourceTraceLimits {
    fn default() -> Self {
        Self {
            max_forms: DEFAULT_MAX_FORMS,
            max_schema_entries: DEFAULT_MAX_SCHEMA_ENTRIES,
            max_text_bytes: DEFAULT_MAX_TEXT_BYTES,
            max_artifact_preview_bytes: DEFAULT_ARTIFACT_PREVIEW_BYTES,
        }
    }
}

pub(super) fn string(value: impl Into<String>) -> HalcTraceValue {
    HalcTraceValue::String(value.into())
}

pub(super) fn integer(value: usize) -> HalcTraceValue {
    HalcTraceValue::Integer(value as u64)
}

pub(super) fn boolean(value: bool) -> HalcTraceValue {
    HalcTraceValue::Boolean(value)
}

fn strings(values: Vec<String>) -> HalcTraceValue {
    HalcTraceValue::Strings(values)
}

pub(super) fn event(
    id: u64,
    stage: &'static str,
    status: HalcTraceStatus,
    evidence: HalcTraceEvidence,
    error: Option<String>,
) -> HalcTraceEvent {
    HalcTraceEvent {
        id,
        sequence: id,
        stage,
        status,
        evidence,
        error,
    }
}

pub(super) fn next_event(
    events: &mut Vec<HalcTraceEvent>,
    stage: &'static str,
    status: HalcTraceStatus,
    evidence: HalcTraceEvidence,
    error: Option<String>,
) {
    let id = events.len() as u64 + 1;
    events.push(event(id, stage, status, evidence, error));
}

pub(super) fn normalize_id(id: impl Into<String>) -> String {
    let id = id.into();
    if id.is_empty() {
        "halc-source".to_owned()
    } else {
        id
    }
}

fn hex(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        use std::fmt::Write;
        let _ = write!(&mut output, "{byte:02x}");
    }
    output
}

pub(super) fn sha256(bytes: &[u8]) -> String {
    hex(&Sha256::digest(bytes))
}

fn bounded_text(value: &str, limit: usize) -> (String, bool) {
    if value.len() <= limit {
        return (value.to_owned(), false);
    }
    let mut end = limit.min(value.len());
    while end > 0 && !value.is_char_boundary(end) {
        end -= 1;
    }
    (value[..end].to_owned(), true)
}

fn bounded_strings(
    values: Vec<String>,
    max_items: usize,
    max_text_bytes: usize,
) -> (Vec<String>, bool) {
    let mut truncated = values.len() > max_items;
    let mut output = Vec::with_capacity(values.len().min(max_items));
    for value in values.into_iter().take(max_items) {
        let (value, text_truncated) = bounded_text(&value, max_text_bytes);
        truncated |= text_truncated;
        output.push(value);
    }
    (output, truncated)
}

pub(super) fn structural_forms(forms: &[Form]) -> Vec<String> {
    forms
        .iter()
        .map(|form| format!("{form:?}"))
        .collect::<Vec<_>>()
}

pub(super) fn form_evidence(forms: &[Form], limits: HalcSourceTraceLimits) -> HalcTraceEvidence {
    let readable = forms.iter().map(ToString::to_string).collect::<Vec<_>>();
    let structural = structural_forms(forms);
    let (readable, readable_truncated) =
        bounded_strings(readable, limits.max_forms, limits.max_text_bytes);
    let (structural, structural_truncated) =
        bounded_strings(structural, limits.max_forms, limits.max_text_bytes);
    let mut evidence = HalcTraceEvidence::new();
    evidence.insert("form/count".into(), integer(forms.len()));
    evidence.insert("forms/readable".into(), strings(readable));
    evidence.insert("forms/structural".into(), strings(structural));
    evidence.insert(
        "forms/truncated".into(),
        boolean(readable_truncated || structural_truncated),
    );
    evidence
}

fn sorted_names<T>(values: &HashMap<String, T>) -> Vec<String> {
    let mut names = values.keys().cloned().collect::<Vec<_>>();
    names.sort();
    names
}

fn type_values(values: &HashMap<String, SchemaType>) -> Vec<String> {
    let mut output = values
        .iter()
        .map(|(name, schema)| format!("{name}={schema:?}"))
        .collect::<Vec<_>>();
    output.sort();
    output
}

pub(super) fn schema_evidence(
    module: &HalcModule,
    limits: HalcSourceTraceLimits,
) -> HalcTraceEvidence {
    let definitions = sorted_names(&module.schemas.definitions);
    let functions = sorted_names(&module.schemas.functions);
    let definition_types = type_values(&module.schemas.definition_types);
    let function_types = type_values(&module.schemas.function_types);
    let mut resolved_functions = functions
        .iter()
        .filter_map(|name| {
            module
                .schemas
                .resolved_function_type(name)
                .map(|schema| format!("{name}={schema:?}"))
        })
        .collect::<Vec<_>>();
    resolved_functions.sort();

    let (definitions, definitions_truncated) = bounded_strings(
        definitions,
        limits.max_schema_entries,
        limits.max_text_bytes,
    );
    let (functions, functions_truncated) =
        bounded_strings(functions, limits.max_schema_entries, limits.max_text_bytes);
    let (definition_types, definition_types_truncated) = bounded_strings(
        definition_types,
        limits.max_schema_entries,
        limits.max_text_bytes,
    );
    let (function_types, function_types_truncated) = bounded_strings(
        function_types,
        limits.max_schema_entries,
        limits.max_text_bytes,
    );
    let (resolved_functions, resolved_functions_truncated) = bounded_strings(
        resolved_functions,
        limits.max_schema_entries,
        limits.max_text_bytes,
    );

    let mut evidence = HalcTraceEvidence::new();
    evidence.insert("schema/definitions".into(), strings(definitions));
    evidence.insert("schema/functions".into(), strings(functions));
    evidence.insert("schema/definition-types".into(), strings(definition_types));
    evidence.insert("schema/function-types".into(), strings(function_types));
    evidence.insert(
        "schema/resolved-functions".into(),
        strings(resolved_functions),
    );
    evidence.insert(
        "schema/truncated".into(),
        boolean(
            definitions_truncated
                || functions_truncated
                || definition_types_truncated
                || function_types_truncated
                || resolved_functions_truncated,
        ),
    );
    evidence
}

fn origin_name(origin: HalcOrigin) -> &'static str {
    match origin {
        HalcOrigin::Halc => "halc",
        HalcOrigin::LegacyHir => "legacy-hir",
    }
}

pub(super) fn module_identity_evidence(
    namespace: &str,
    resource: &str,
    source_hash: &str,
) -> HalcTraceEvidence {
    let mut evidence = HalcTraceEvidence::new();
    evidence.insert("module/namespace".into(), string(namespace));
    evidence.insert("module/resource".into(), string(resource));
    evidence.insert("source/hash".into(), string(source_hash));
    evidence
}

pub(super) fn payload_evidence(
    inspection: &HalcArtifactInspection,
    artifact: &[u8],
) -> HalcTraceEvidence {
    let mut evidence = HalcTraceEvidence::new();
    evidence.insert(
        "payload/bytes".into(),
        HalcTraceValue::Integer(inspection.payload_length as u64),
    );
    evidence.insert(
        "payload/checksum".into(),
        string(&inspection.payload_checksum),
    );
    evidence.insert("artifact/bytes".into(), integer(artifact.len()));
    evidence.insert("artifact/digest".into(), string(sha256(artifact)));
    evidence
}

pub(super) fn envelope_evidence(
    inspection: &HalcArtifactInspection,
    artifact: &[u8],
    limits: HalcSourceTraceLimits,
) -> HalcTraceEvidence {
    let preview_bytes = artifact.len().min(limits.max_artifact_preview_bytes);
    let mut evidence = payload_evidence(inspection, artifact);
    evidence.insert(
        "artifact/origin".into(),
        string(origin_name(inspection.origin)),
    );
    evidence.insert(
        "format/version".into(),
        HalcTraceValue::Integer(inspection.format_version as u64),
    );
    evidence.insert(
        "format/flags".into(),
        HalcTraceValue::Integer(inspection.flags as u64),
    );
    evidence.insert(
        "artifact/preview-hex".into(),
        string(hex(&artifact[..preview_bytes])),
    );
    evidence.insert("artifact/preview-bytes".into(), integer(preview_bytes));
    evidence.insert(
        "artifact/preview-truncated".into(),
        boolean(preview_bytes < artifact.len()),
    );
    evidence
}

pub(super) fn diagnostic_evidence(
    stage: &'static str,
    category: &'static str,
    message: &str,
) -> HalcTraceEvidence {
    let mut evidence = HalcTraceEvidence::new();
    evidence.insert("diagnostic/stage".into(), string(stage));
    evidence.insert("diagnostic/category".into(), string(category));
    evidence.insert("diagnostic/message".into(), string(message));
    evidence
}

pub(super) fn failed_trace(
    id: String,
    events: Vec<HalcTraceEvent>,
    error: impl Into<String>,
) -> HalcArtifactTrace {
    let error = error.into();
    HalcArtifactTrace {
        schema: HALC_TRACE_SCHEMA,
        id,
        status: HalcTraceStatus::Error,
        events,
        result: None,
        error: Some(error),
    }
}

pub(super) fn encode_failure(error: &str) -> (&'static str, &'static str) {
    let lower = error.to_ascii_lowercase();
    if lower.contains("schema") || lower.contains("reference") {
        ("schema/index", "schema/invalid")
    } else if lower.contains("canonical") {
        ("forms/canonicalize", "forms/canonicalization")
    } else {
        ("payload/encode", "artifact/encode")
    }
}

pub(super) fn module_projection_equal(left: &HalcModule, right: &HalcModule) -> bool {
    left.namespace == right.namespace
        && left.resource == right.resource
        && left.source_hash == right.source_hash
        && structural_forms(&left.forms) == structural_forms(&right.forms)
        && left.schemas == right.schemas
        && left.origin == right.origin
}