oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! Load a `.cyix` extension archive.
//!
//! A `.cyix` file is a ZIP containing:
//! - `extension.yaml`  (required) — the manifest
//! - `extension.wasm`  (required) — the compiled extension
//! - `config.default.yaml` (optional) — default config values

use std::io::Read;
use std::path::Path;

use anyhow::Context as _;

use crate::prelude::*;

use super::manifest::{ExtensionManifest, parse_manifest};
use crate::log_matcher::{self, CompiledMatcher};

/// Schema packaged inside a `.cyix` archive.
pub(crate) struct CyixSchema {
    pub id: String,
    pub name: Option<String>,
    pub patterns: Vec<String>,
    pub schema_json: String,
}

/// Contents extracted from a `.cyix` archive.
pub(crate) struct CyixPackage {
    pub manifest: ExtensionManifest,
    pub wasm_bytes: Vec<u8>,
    /// Raw YAML text from `config.default.yaml`, if present.
    pub default_config: Option<String>,
    /// Packaged schema files declared in the manifest (id + JSON content).
    pub schemas: Vec<CyixSchema>,
    /// Compiled log matchers from this extension's manifest.
    pub log_matchers: Vec<CompiledMatcher>,
}

pub(crate) fn load_cyix(path: &Path) -> Result<CyixPackage> {
    let file = std::fs::File::open(path).with_context(|| format!("opening {:?}", path))?;
    let mut archive = zip::ZipArchive::new(file).with_context(|| format!("reading zip {:?}", path))?;

    // First, read the manifest.
    let mut manifest_yaml: Option<String> = None;
    for i in 0..archive.len() {
        let mut entry = archive.by_index(i)?;
        if entry.name() == "extension.yaml" {
            let mut s = String::new();
            entry.read_to_string(&mut s)?;
            manifest_yaml = Some(s);
            break;
        }
    }

    let yaml = manifest_yaml.ok_or_else(|| anyhow!("missing extension.yaml in {:?}", path))?;
    let manifest = parse_manifest(&yaml).with_context(|| format!("parsing extension.yaml in {:?}", path))?;

    // Prepare structures for discovered files.
    let mut wasm_bytes: Option<Vec<u8>> = None;
    let mut default_config: Option<String> = None;
    let mut schemas: Vec<CyixSchema> = Vec::new();

    // Build a quick lookup of declared schema files. manifest.schemas may be absent;
    // assign an id for each entry derived from the schema filename stem.
    let declared_schemas: Vec<(String, super::manifest::SchemaManifestEntry)> = match &manifest.schemas {
        Some(list) => list
            .iter()
            .map(|e| {
                let id = std::path::Path::new(&e.schema)
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .unwrap_or("schema")
                    .to_string();
                (id, e.clone())
            })
            .collect(),
        None => Vec::new(),
    };

    // Iterate entries once and extract wasm/config/schema files as we find them.
    for i in 0..archive.len() {
        let mut entry = archive.by_index(i)?;
        let name = entry.name().to_owned();

        if name == "config.default.yaml" {
            if default_config.is_none() {
                let mut s = String::new();
                entry.read_to_string(&mut s)?;
                default_config = Some(s);
            }
            continue;
        }

        // wasm: prefer explicit name from manifest.wasm, otherwise any .wasm.
        if wasm_bytes.is_none() && (name == manifest.wasm || name.ends_with(".wasm")) {
            let mut bytes = Vec::new();
            entry.read_to_end(&mut bytes)?;
            wasm_bytes = Some(bytes);
            continue;
        }

        // Check if this entry matches any declared schema file.
        for (id, mentry) in declared_schemas.iter() {
            if name == mentry.schema || name.ends_with(&mentry.schema) {
                // read schema file using the already-opened `entry`
                let mut s = String::new();
                entry.read_to_string(&mut s)?;
                let patterns = mentry.patterns.clone();
                // patterns may be empty; leave as-is
                schemas.push(CyixSchema {
                    id: id.clone(),
                    name: None,
                    patterns,
                    schema_json: s,
                });
                break;
            }
        }
    }

    // Warn for declared but missing schema files.
    for (id, mentry) in declared_schemas.iter() {
        let found = schemas.iter().any(|s| &s.id == id);
        if !found {
            log::warn!(
                "schema file {:?} declared in manifest for extension {:?} not found in archive {:?}",
                mentry.schema,                manifest.name,
                path
            );
        }
    }

    let wasm_bytes = wasm_bytes.ok_or_else(|| anyhow!("missing .wasm in {:?}", path))?;

    // Compile log matchers declared in the manifest (graceful degradation on error).
    let compiled_log_matchers = {
        let defs = manifest.log_matchers.clone();
        let opts = log_matcher::CompileOptions {
            source_file: Some("extension.yaml".to_string()),
            ..Default::default()
        };
        match log_matcher::compile_matchers(defs, opts) {
            Ok(result) => {
                for msg in &result.messages {
                    log::info!("[{}] log_matcher compile: {}", manifest.name, msg);
                }
                result.matchers
            }
            Err(msgs) => {
                for msg in &msgs {
                    log::warn!("[{}] log_matcher compile error: {}", manifest.name, msg);
                }
                vec![]
            }
        }
    };

    Ok(CyixPackage {
        manifest,
        wasm_bytes,
        default_config,
        schemas,
        log_matchers: compiled_log_matchers,
    })
}