1pub use brep_kernel::feature_pipeline::extension::PluginPin;
3use brep_kernel::feature_pipeline::{self as kernel, FeatureContext, FeatureResult};
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use sha2::{Digest, Sha256};
7use std::{
8 collections::{BTreeMap, BTreeSet},
9 rc::Rc,
10};
11mod annotations;
12mod invocation;
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct PackageManifest {
16 pub id: String,
17 pub version: String,
18 pub api_version: u32,
19 pub name: String,
20 pub entry: String,
21}
22#[derive(Clone, Debug, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24pub struct PackageBundle {
25 pub manifest: PackageManifest,
26 pub modules: BTreeMap<String, String>,
27 #[serde(default)]
28 pub assets: BTreeMap<String, Vec<u8>>,
29}
30fn local_path(s: &str) -> bool {
31 !s.is_empty()
32 && !s.contains('\\')
33 && !s.contains(':')
34 && s.split('/').all(|p| !p.is_empty() && p != "." && p != "..")
35}
36impl PackageBundle {
37 pub fn pin(&self) -> Result<PluginPin, String> {
38 let m = &self.manifest;
39 if m.api_version != 1
40 || m.id.is_empty()
41 || m.id.contains('/')
42 || !m
43 .id
44 .chars()
45 .all(|c| c.is_ascii_alphanumeric() || ".-_".contains(c))
46 || m.version.is_empty()
47 || m.name.is_empty()
48 {
49 return Err(format!(
50 "{}: invalid manifest or unsupported API version",
51 m.id
52 ));
53 }
54 if !local_path(&m.entry)
55 || !self.modules.contains_key(&m.entry)
56 || self.modules.len() > 128
57 || self
58 .modules
59 .keys()
60 .chain(self.assets.keys())
61 .any(|p| !local_path(p))
62 {
63 return Err(format!(
64 "{}: invalid package-local paths or module count",
65 m.id
66 ));
67 }
68 if self.modules.values().map(String::len).sum::<usize>()
69 + self.assets.values().map(Vec::len).sum::<usize>()
70 > 8 * 1024 * 1024
71 {
72 return Err("package exceeds 8 MiB".into());
73 }
74 let digest = format!(
75 "{:x}",
76 Sha256::digest(serde_json::to_vec(self).map_err(|e| e.to_string())?)
77 );
78 Ok(PluginPin {
79 id: m.id.clone(),
80 version: m.version.clone(),
81 api_version: m.api_version,
82 digest,
83 })
84 }
85}
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct FeatureRegistration {
89 pub id: String,
90 pub label: String,
91 pub glyph: String,
92 pub ribbon_path: String,
93 pub command_size: String,
94 pub short_name: String,
95 pub input_params_schema: Value,
96 pub package: PluginPin,
97}
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ActionRegistration {
101 pub id: String,
102 pub label: String,
103 pub glyph: String,
104 pub ribbon_path: String,
105 pub command_size: String,
106 pub input_params_schema: Value,
107 pub package: PluginPin,
108}
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct WorkbenchRegistration {
112 pub id: String,
113 pub label: String,
114 pub feature_types: Vec<String>,
115 pub actions: Vec<String>,
116 pub panels: Vec<String>,
117 pub package: PluginPin,
118}
119#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct AnnotationRegistration {
122 pub id: String,
123 pub label: String,
124 pub input_params_schema: Value,
125 pub package: PluginPin,
126}
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct PanelRegistration {
130 pub id: String,
131 pub label: String,
132 pub controls: Vec<PanelControl>,
133 pub package: PluginPin,
134}
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
136#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
137pub enum PanelControl {
138 Text {
139 text: String,
140 },
141 Table {
142 columns: Vec<String>,
143 rows: Vec<Vec<String>>,
144 },
145 Action {
146 id: String,
147 label: String,
148 action: String,
149 #[serde(default = "empty_object")]
150 params: Value,
151 },
152 Form {
153 id: String,
154 label: String,
155 action: String,
156 #[serde(default = "empty_object")]
157 params: Value,
158 },
159}
160fn empty_object() -> Value {
161 json!({})
162}
163#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
164pub struct Registry {
165 features: Vec<FeatureRegistration>,
166 actions: Vec<ActionRegistration>,
167 workbenches: Vec<WorkbenchRegistration>,
168 #[serde(default)]
169 panels: Vec<PanelRegistration>,
170 #[serde(default)]
171 annotations: Vec<AnnotationRegistration>,
172 packages: Vec<PluginPin>,
173 revision: String,
174}
175impl Registry {
176 pub fn panels(&self) -> &[PanelRegistration] {
177 &self.panels
178 }
179 pub fn annotations(&self) -> &[AnnotationRegistration] {
180 &self.annotations
181 }
182 pub fn features(&self) -> &[FeatureRegistration] {
183 &self.features
184 }
185 pub fn actions(&self) -> &[ActionRegistration] {
186 &self.actions
187 }
188 pub fn workbenches(&self) -> &[WorkbenchRegistration] {
189 &self.workbenches
190 }
191 pub fn revision(&self) -> &str {
192 &self.revision
193 }
194 pub fn install(packages: &[PackageBundle]) -> Result<Self, String> {
195 let mut result = Self::default();
196 let mut seen = BTreeSet::new();
197 for package in packages {
198 let pin = package.pin()?;
199 if !seen.insert((pin.id.clone(), pin.digest.clone())) {
200 return Err(format!("duplicate package {}", pin.id));
201 }
202 let r = invocation::invoke(package, None, None, None, None)?.0;
203 result.packages.push(pin);
204 result.features.extend(r.features);
205 result.actions.extend(r.actions);
206 result.workbenches.extend(r.workbenches);
207 result.panels.extend(r.panels);
208 result.annotations.extend(r.annotations);
209 }
210 result.revision = format!("{:x}", Sha256::digest(serde_json::to_vec(&result).unwrap()));
211 Ok(result)
212 }
213 pub fn for_document(&self, pins: &[PluginPin]) -> Result<Self, String> {
214 let mut ids = BTreeSet::new();
215 for pin in pins {
216 if !ids.insert(&pin.id) {
217 return Err(format!("duplicate document pin {}", pin.id));
218 }
219 if !self.packages.contains(pin) {
220 return Err(format!("missing or disabled pinned package {}", pin.id));
221 }
222 }
223 let mut r = self.clone();
224 r.packages = pins.to_vec();
225 r.features.retain(|x| pins.contains(&x.package));
226 r.actions.retain(|x| pins.contains(&x.package));
227 r.workbenches.retain(|x| pins.contains(&x.package));
228 r.panels.retain(|x| pins.contains(&x.package));
229 r.annotations.retain(|x| pins.contains(&x.package));
230 r.revision = format!("{:x}", Sha256::digest(serde_json::to_vec(&r).unwrap()));
231 Ok(r)
232 }
233}
234#[derive(Clone, Debug, Serialize, Deserialize)]
235#[serde(tag = "op", rename_all = "camelCase")]
236pub enum DocumentCommand {
237 AddFeature {
238 #[serde(rename = "featureType")]
239 feature_type: String,
240 #[serde(rename = "inputParams")]
241 input_params: Value,
242 #[serde(rename = "persistentData")]
243 persistent_data: Value,
244 },
245 UpdateFeature {
246 id: String,
247 #[serde(rename = "inputParams")]
248 input_params: Value,
249 },
250 DeleteFeature {
251 id: String,
252 },
253}
254#[derive(Clone, Debug, Default, Serialize, Deserialize)]
255pub struct ActionPlan {
256 pub commands: Vec<DocumentCommand>,
257 pub notifications: Vec<String>,
258}
259
260pub fn execute_script(source: &str, input: &Value) -> Result<ActionPlan, String> {
264 let package = PackageBundle {
265 manifest: PackageManifest {
266 id: "cad.editor".into(), version: "1".into(), api_version: 1,
267 name: "JavaScript editor".into(), entry: "editor.js".into(),
268 },
269 modules: BTreeMap::from([("editor.js".into(), format!(r#"
270export default function install(app) {{
271 app.registerAction({{id: 'cad.editor/run', label: 'Run script', run(cad) {{
272 const ui = Object.freeze({{notify: cad.notify}});
273 const result = (function(cad, ui) {{
274{source}
275 }})(cad, ui);
276 if (result && typeof result.then === 'function') throw Error('Promise results are unsupported');
277 if (result !== undefined) console.log('Result:', JSON.stringify(result));
278 }} }});
279}}
280"#))]),
281 assets: BTreeMap::new(),
282 };
283 let (_, value, _) = invocation::invoke(
284 &package, Some(("action", "cad.editor/run")), Some(input), None, None,
285 )?;
286 serde_json::from_value(value).map_err(|e| e.to_string())
287}
288#[derive(Clone)]
289pub struct Runtime {
290 packages: Vec<PackageBundle>,
291 registry: Registry,
292}
293impl Runtime {
294 pub fn new(packages: Vec<PackageBundle>) -> Result<Self, String> {
295 let registry = Registry::install(&packages)?;
296 Ok(Self { packages, registry })
297 }
298 pub fn from_validated(
301 packages: Vec<PackageBundle>,
302 registry: Registry,
303 ) -> Result<Self, String> {
304 let pins = packages
305 .iter()
306 .map(PackageBundle::pin)
307 .collect::<Result<Vec<_>, _>>()?;
308 if pins != registry.packages {
309 return Err("validated registry package pins mismatch".into());
310 }
311 for (id, pin) in registry
312 .features
313 .iter()
314 .map(|r| (&r.id, &r.package))
315 .chain(registry.actions.iter().map(|r| (&r.id, &r.package)))
316 .chain(registry.workbenches.iter().map(|r| (&r.id, &r.package)))
317 .chain(registry.panels.iter().map(|r| (&r.id, &r.package)))
318 .chain(registry.annotations.iter().map(|r| (&r.id, &r.package)))
319 {
320 if !pins.contains(pin) || !id.starts_with(&format!("{}/", pin.id)) {
321 return Err("validated registry ownership mismatch".into());
322 }
323 }
324 Ok(Self { packages, registry })
325 }
326 pub fn registry(&self) -> &Registry {
327 &self.registry
328 }
329 pub fn with_provider<R>(&self, f: impl FnOnce() -> R) -> R {
330 kernel::extension::with_provider(Rc::new(self.clone()), f)
331 }
332 pub fn execute_history(&self, request: &kernel::HistoryRequest) -> kernel::HistoryResult {
333 self.with_provider(|| kernel::execute_history(request))
334 }
335 fn package(&self, pins: &[PluginPin], id: &str) -> Result<&PackageBundle, String> {
336 let owner = id.split('/').next().unwrap_or("");
337 let matches: Vec<_> = pins.iter().filter(|p| p.id == owner).collect();
338 if matches.len() != 1 {
339 return Err(format!("{} requires exactly one document plugin pin", id));
340 }
341 self.packages
342 .iter()
343 .find(|p| p.pin().as_ref() == Ok(matches[0]))
344 .ok_or_else(|| format!("missing or disabled pinned package for {id}"))
345 }
346 pub fn execute_action(
347 &self,
348 pins: &[PluginPin],
349 id: &str,
350 input: &Value,
351 ) -> Result<ActionPlan, String> {
352 let p = self.package(pins, id)?;
353 let (r, v, _) = invocation::invoke(
354 p,
355 Some(("action", id)),
356 Some(input),
357 None,
358 Some(&self.registry.for_document(&[p.pin()?])?),
359 )?;
360 self.verify(p, &r)?;
361 serde_json::from_value(v).map_err(|e| e.to_string())
362 }
363 fn verify(&self, p: &PackageBundle, r: &Registry) -> Result<(), String> {
364 let expected = self.registry.for_document(&[p.pin()?])?;
365 if expected.features != r.features
366 || expected.actions != r.actions
367 || expected.workbenches != r.workbenches
368 || expected.panels != r.panels
369 || expected.annotations != r.annotations
370 {
371 return Err("registration metadata changed during invocation".into());
372 }
373 Ok(())
374 }
375}
376impl kernel::extension::ExtensionProvider for Runtime {
377 fn execute_annotation(
378 &self,
379 pins: &[PluginPin],
380 annotation: &kernel::pmi::PmiAnnotation,
381 context: &kernel::pmi::PmiContext<'_>,
382 ) -> Result<kernel::pmi::PluginAnnotationOutput, String> {
383 Runtime::execute_annotation(self, pins, annotation, context)
384 }
385 fn identity(&self, pins: &[PluginPin], kind: &str) -> Result<String, String> {
386 let p = self.package(pins, kind)?;
387 if !self
388 .registry
389 .features
390 .iter()
391 .any(|r| r.id == kind && r.package == p.pin().unwrap())
392 {
393 return Err(format!("unregistered plugin feature {kind}"));
394 }
395 Ok(format!("{}:{}:1", p.pin()?.digest, kind))
396 }
397 fn execute(&self, pins: &[PluginPin], ctx: &FeatureContext<'_>) -> FeatureResult {
398 let run = || -> Result<FeatureResult, String> {
399 let p = self.package(pins, &ctx.feature_type)?;
400 let reg = self
401 .registry
402 .features
403 .iter()
404 .find(|r| r.id == ctx.feature_type && r.package == p.pin().unwrap())
405 .ok_or("feature registration unavailable")?;
406 let mut params = ctx.params.clone();
407 validate_params(®.input_params_schema, &mut params, Some(ctx))?;
408 let input = json!({"params":params,"persistentData":ctx.persistent,"featureId":ctx.id,"units":"mm"});
409 let (r, _, result) = invocation::invoke(
410 p,
411 Some(("feature", &ctx.feature_type)),
412 Some(&input),
413 Some(ctx),
414 Some(&self.registry.for_document(&[p.pin()?])?),
415 )?;
416 self.verify(p, &r)?;
417 result.ok_or_else(|| "missing feature result".into())
419 };
420 match run() {
421 Ok(r) => r,
422 Err(e) => ctx.fail(e),
423 }
424 }
425}
426fn validate_params(
427 schema: &Value,
428 params: &mut Value,
429 ctx: Option<&FeatureContext<'_>>,
430) -> Result<(), String> {
431 let obj = params
432 .as_object_mut()
433 .ok_or("parameters must be an object")?;
434 let fields = schema.as_object().ok_or("schema must be object")?;
435 for key in obj.keys() {
436 if key != "id" && !fields.contains_key(key) {
437 return Err(format!("unknown parameter {key}"));
438 }
439 }
440 if obj.get("id").is_some_and(|v| !v.is_string()) {
441 return Err("feature id must be string".into());
442 }
443 for (key, s) in schema.as_object().ok_or("schema must be object")? {
444 if !obj.contains_key(key) {
445 if let Some(v) = s.get("default_value") {
446 obj.insert(key.clone(), v.clone());
447 }
448 }
449 let v = obj
450 .get_mut(key)
451 .ok_or_else(|| format!("missing parameter {key}"))?;
452 match s["type"].as_str().unwrap_or("") {
453 "number" => {
454 let n = if let Some(n) = v.as_f64() {
455 n
456 } else if let (Some(c), Some(text)) = (ctx, v.as_str()) {
457 c.env.eval(text)?
458 } else {
459 return Err(format!("{key} must be numeric"));
460 };
461 if !n.is_finite()
462 || s["min"].as_f64().is_some_and(|m| n < m)
463 || s["max"].as_f64().is_some_and(|m| n > m)
464 {
465 return Err(format!("{key} outside finite numeric bounds"));
466 }
467 *v = json!(n);
468 }
469 "reference_selection" => {
470 fn reference(v: &Value) -> bool {
471 v.is_string()
472 || v.as_object()
473 .is_some_and(|o| o.get("name").is_some_and(Value::is_string))
474 }
475 if !reference(v) && !v.as_array().is_some_and(|a| a.iter().all(reference)) {
476 return Err(format!("{key} must contain reference names"));
477 }
478 }
479 "string" if !v.is_string() => return Err(format!("{key} must be string")),
480 "boolean" if !v.is_boolean() => return Err(format!("{key} must be boolean")),
481 _ => {}
482 }
483 }
484 Ok(())
485}