use std::collections::BTreeMap;
use std::sync::OnceLock;
use serde::Deserialize;
use super::grammar_manifest::GrammarManifest;
static BUNDLED: &str = include_str!("../../../data/grammar_registry.json");
static PARSED_BUNDLED: OnceLock<Vec<GrammarManifest>> = OnceLock::new();
static PARSED_LOCAL: OnceLock<Option<Vec<GrammarManifest>>> = OnceLock::new();
#[derive(Debug, Default, Deserialize)]
struct RegistryFile {
#[serde(default)]
grammars: Vec<GrammarManifest>,
}
fn parse(json: &str) -> Vec<GrammarManifest> {
serde_json::from_str::<RegistryFile>(json)
.map(|r| r.grammars)
.unwrap_or_default()
}
fn bundled() -> &'static [GrammarManifest] {
PARSED_BUNDLED.get_or_init(|| parse(BUNDLED))
}
fn local() -> Option<&'static [GrammarManifest]> {
PARSED_LOCAL
.get_or_init(|| {
let dir = crate::core::data_dir::lean_ctx_data_dir().ok()?;
let path = dir.join("grammar_registry.json");
let content = std::fs::read_to_string(&path).ok()?;
let require_sig = crate::core::config::Config::load().addons.require_signature;
if let super::signing::OverrideVerdict::Reject(reason) =
gate_override_file(&path, &content, require_sig)
{
tracing::warn!("[SECURITY] ignoring user grammar registry override: {reason}");
return None;
}
Some(parse(&content))
})
.as_deref()
}
fn gate_override_file(
path: &std::path::Path,
content: &str,
require_sig: bool,
) -> super::signing::OverrideVerdict {
let sig = std::fs::read_to_string(super::signing::sidecar_path(path))
.ok()
.and_then(|t| super::signing::RegistrySignature::from_json(&t).ok());
super::signing::gate_override(content, sig.as_ref(), require_sig, |pk| {
crate::core::policy::org::trust::is_trusted(pk)
})
}
pub fn all() -> Vec<GrammarManifest> {
let mut by_name: BTreeMap<String, GrammarManifest> = BTreeMap::new();
for m in bundled() {
by_name.insert(m.name.clone(), m.clone());
}
if let Some(local) = local() {
for m in local {
by_name.insert(m.name.clone(), m.clone());
}
}
by_name.into_values().collect()
}
pub fn get(name: &str) -> Option<GrammarManifest> {
let needle = name.trim().to_ascii_lowercase();
all()
.into_iter()
.find(|m| m.name.to_ascii_lowercase() == needle)
}
pub fn find_by_extension(ext: &str) -> Option<GrammarManifest> {
let needle = ext.trim_start_matches('.').to_ascii_lowercase();
all().into_iter().find(|m| {
m.extensions
.iter()
.any(|e| e.to_ascii_lowercase() == needle)
})
}
#[must_use]
pub fn validate_entries(entries: &[GrammarManifest]) -> Vec<String> {
let mut problems = Vec::new();
let mut seen = std::collections::HashSet::new();
for m in entries {
let slug = m.name.to_ascii_lowercase();
if !seen.insert(slug) {
problems.push(format!("duplicate slug `{}`", m.name));
}
if let Err(e) = m.validate() {
problems.push(format!("`{}`: invalid manifest — {e}", m.name));
}
}
problems
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_registry_is_valid() {
let problems = validate_entries(bundled());
assert!(problems.is_empty(), "{problems:?}");
}
#[test]
fn find_by_extension_is_case_insensitive() {
assert!(find_by_extension("LUA").is_none() || find_by_extension("lua").is_some());
}
}