| 1 | //! Pure roster facts tied to existing Native entry receipts. No new profile, |
| 2 | //! session, filesystem root or writable catalog authority. |
| 3 | use super::{PluginRegistry, activation::PluginActivationCapability}; |
| 4 | use crate::extension_host::composition_scope::NativePresetRef; |
| 5 | use crate::extension_host::protocol::EntryRef; |
| 6 | use serde::Deserialize; |
| 7 | use std::collections::{BTreeMap, BTreeSet}; |
| 8 | use std::io::Read; |
| 9 | use std::path::Path; |
| 10 | |
| 11 | const MARKER: &str = "// codewhale-native-preset-v1 "; |
| 12 | const MAX_ENTRY: u64 = 16 * 1024; |
| 13 | |
| 14 | #[derive(Debug, Clone, Deserialize)] |
| 15 | #[serde(deny_unknown_fields)] |
| 16 | pub(crate) struct NativePresetMetadata { |
| 17 | pub id: String, |
| 18 | pub trust: String, |
| 19 | pub name: Option<String>, |
| 20 | pub description: Option<String>, |
| 21 | pub order: Option<f64>, |
| 22 | pub is_default: bool, |
| 23 | pub broken: Option<String>, |
| 24 | } |
| 25 | |
| 26 | pub(crate) fn metadata_from_bytes(bytes: &[u8]) -> Option<NativePresetMetadata> { |
| 27 | let source = std::str::from_utf8(bytes).ok()?; |
| 28 | let first = source.lines().next()?.strip_prefix(MARKER)?; |
| 29 | let data: NativePresetMetadata = serde_json::from_str(first).ok()?; |
| 30 | if data.id.is_empty() |
| 31 | || data.id.len() > 64 |
| 32 | || !data.id.as_bytes()[0].is_ascii_alphanumeric() |
| 33 | || !data |
| 34 | .id |
| 35 | .bytes() |
| 36 | .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-') |
| 37 | || !matches!(data.trust.as_str(), "system" | "user") |
| 38 | || data.name.as_ref().is_some_and(|v| v.len() > 4096) |
| 39 | || data |
| 40 | .description |
| 41 | .as_ref() |
| 42 | .is_some_and(|v| v.len() > 16 * 1024) |
| 43 | || data.order.is_some_and(|v| !v.is_finite()) |
| 44 | || data.broken.is_some() |
| 45 | { |
| 46 | return None; |
| 47 | } |
| 48 | Some(data) |
| 49 | } |
| 50 | |
| 51 | pub(crate) fn metadata( |
| 52 | plugins: &PluginRegistry, |
| 53 | preset: &NativePresetRef, |
| 54 | ) -> Option<NativePresetMetadata> { |
| 55 | let plugin = plugins.get(&preset.plugin_id)?; |
| 56 | if plugin.content_hash != preset.content_hash |
| 57 | || !plugin.component_active(PluginActivationCapability::Native) |
| 58 | { |
| 59 | return None; |
| 60 | } |
| 61 | let root = plugin.staged_root.as_deref()?; |
| 62 | let path = Path::new(&preset.entry.path); |
| 63 | // Reuse the sole discovery-to-stage translator: it resolves both the |
| 64 | // root and entry, proves confinement and requires an actual Native entry. |
| 65 | if !plugin.components.native.iter().any(|source| { |
| 66 | super::runtime::staged_component_path(&plugin.canonical_root, root, source) |
| 67 | .is_ok_and(|admitted| admitted == path) |
| 68 | }) { |
| 69 | return None; |
| 70 | } |
| 71 | let mut file = crate::plugins::manifest::open_bundle_file(path).ok()?; |
| 72 | let mut bytes = Vec::new(); |
| 73 | file.by_ref() |
| 74 | .take(MAX_ENTRY + 1) |
| 75 | .read_to_end(&mut bytes) |
| 76 | .ok()?; |
| 77 | if bytes.len() > MAX_ENTRY as usize || crate::hashing::sha256_hex(&bytes) != preset.entry.sha256 |
| 78 | { |
| 79 | return None; |
| 80 | } |
| 81 | metadata_from_bytes(&bytes) |
| 82 | } |
| 83 | |
| 84 | fn reviewed_entry( |
| 85 | source: &super::runtime::PluginComponentSource, |
| 86 | checked: &mut BTreeMap<String, Option<super::manifest::ValidatedManifest>>, |
| 87 | ) -> Option<NativePresetRef> { |
| 88 | let id = source.authority.plugin_id.to_string(); |
| 89 | let receipt = checked.entry(id.clone()).or_insert_with(|| { |
| 90 | let validated = |
| 91 | super::manifest::PluginManifest::validate_from_path(&source.authority.staged_manifest) |
| 92 | .ok()?; |
| 93 | (validated.content_hash == source.authority.content_hash |
| 94 | && validated.capability_hash == source.authority.capability_hash) |
| 95 | .then_some(validated) |
| 96 | }); |
| 97 | let receipt = receipt.as_ref()?; |
| 98 | // Component paths and the validated receipt share canonical identity; |
| 99 | // the selected manifest's lexical alias is not a containment root. |
| 100 | let relative = source.path.strip_prefix(&receipt.canonical_root).ok()?; |
| 101 | Some(NativePresetRef { |
| 102 | plugin_id: id, |
| 103 | content_hash: source.authority.content_hash.clone(), |
| 104 | entry: EntryRef { |
| 105 | path: source.path.to_string_lossy().into_owned(), |
| 106 | sha256: receipt.native_entry_hashes.get(relative)?.clone(), |
| 107 | }, |
| 108 | }) |
| 109 | } |
| 110 | |
| 111 | /// New roster choices come from a whole admitted bundle validation, not a |
| 112 | /// fresh hash of an unmounted mutable file mistaken for a reviewed receipt. |
| 113 | pub(crate) fn admitted_entries( |
| 114 | plugins: &PluginRegistry, |
| 115 | ) -> Vec<( |
| 116 | NativePresetRef, |
| 117 | NativePresetMetadata, |
| 118 | super::types::PluginAuthority, |
| 119 | )> { |
| 120 | let (sources, _) = |
| 121 | super::runtime::active_component_sources(plugins, PluginActivationCapability::Native); |
| 122 | let mut checked = BTreeMap::new(); |
| 123 | let mut result = Vec::new(); |
| 124 | for source in sources { |
| 125 | // Ordinary Native modules have no catalog metadata; do not rehash |
| 126 | // their whole bundles just to rediscover their existing roster view. |
| 127 | // active_component_sources already validated the authority and used |
| 128 | // the shared translator to prove this canonical entry is confined. |
| 129 | let Ok(mut file) = super::manifest::open_bundle_file(&source.path) else { |
| 130 | continue; |
| 131 | }; |
| 132 | let mut header = Vec::new(); |
| 133 | if file |
| 134 | .by_ref() |
| 135 | .take(MAX_ENTRY + 1) |
| 136 | .read_to_end(&mut header) |
| 137 | .is_err() |
| 138 | || header.len() > MAX_ENTRY as usize |
| 139 | || metadata_from_bytes(&header).is_none() |
| 140 | { |
| 141 | continue; |
| 142 | } |
| 143 | let Some(preset) = reviewed_entry(&source, &mut checked) else { |
| 144 | continue; |
| 145 | }; |
| 146 | if let Some(data) = metadata(plugins, &preset) { |
| 147 | result.push((preset, data, source.authority)); |
| 148 | } |
| 149 | } |
| 150 | result |
| 151 | } |
| 152 | |
| 153 | /// One caller snapshot: each raw catalog chooses one default; ordinary Native |
| 154 | /// plugins keep their existing complete entry set. The manager may own a union |
| 155 | /// across callers but no caller receives a union of that catalog's presets. |
| 156 | pub(crate) fn default_selection( |
| 157 | plugins: &PluginRegistry, |
| 158 | ) -> (Vec<NativePresetRef>, BTreeSet<String>) { |
| 159 | let (sources, _) = |
| 160 | super::runtime::active_component_sources(plugins, PluginActivationCapability::Native); |
| 161 | let admitted = admitted_entries(plugins); |
| 162 | if admitted.is_empty() { |
| 163 | return (Vec::new(), BTreeSet::new()); |
| 164 | } |
| 165 | let mut normal = Vec::new(); |
| 166 | let mut catalogs: BTreeMap<String, Vec<(NativePresetRef, NativePresetMetadata)>> = |
| 167 | BTreeMap::new(); |
| 168 | for (preset, data, _) in admitted { |
| 169 | catalogs |
| 170 | .entry(preset.plugin_id.clone()) |
| 171 | .or_default() |
| 172 | .push((preset, data)); |
| 173 | } |
| 174 | let mut checked = BTreeMap::new(); |
| 175 | for source in sources { |
| 176 | if catalogs.contains_key(source.authority.plugin_id.as_str()) { |
| 177 | continue; |
| 178 | } |
| 179 | if let Some(entry) = reviewed_entry(&source, &mut checked) { |
| 180 | normal.push(entry); |
| 181 | } |
| 182 | } |
| 183 | if catalogs.is_empty() { |
| 184 | return (Vec::new(), BTreeSet::new()); |
| 185 | } |
| 186 | let mut unselected = BTreeSet::new(); |
| 187 | for (id, rows) in catalogs { |
| 188 | if let Some((preset, _)) = rows.into_iter().find(|(_, data)| data.is_default) { |
| 189 | normal.push(preset); |
| 190 | } else { |
| 191 | // Absence is upstream data, never permission to mount the first row. |
| 192 | unselected.insert(id); |
| 193 | } |
| 194 | } |
| 195 | (normal, unselected) |
| 196 | } |
| 197 | |
| 198 | #[cfg(test)] |
| 199 | mod tests { |
| 200 | use super::*; |
| 201 | #[test] |
| 202 | fn native_preset_metadata_is_data_and_cannot_mint_trust_or_an_entry() { |
| 203 | let good = br#"// codewhale-native-preset-v1 {"id":"reviewer","trust":"user","name":"Repo reviewer","description":"bounded","is_default":true} |
| 204 | export function apply() {}"#; |
| 205 | assert_eq!(metadata_from_bytes(good).unwrap().id, "reviewer"); |
| 206 | for source in [ |
| 207 | r#"{"id":"../escape","trust":"user","is_default":true}"#, |
| 208 | r#"{"id":"ok","trust":"admin","is_default":true}"#, |
| 209 | r#"{"id":"ok","trust":"user","is_default":true,"entry":{"path":"outside"}}"#, |
| 210 | r#"{"id":"ok","trust":"user","is_default":true,"broken":"missing"}"#, |
| 211 | ] { |
| 212 | assert!(metadata_from_bytes(format!("{MARKER}{source}\n").as_bytes()).is_none()); |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 |