plugin_host/detector.rs
1//! Wasm-бекенд поверхні `detector` (`wit/detector.wit`, world
2//! `detector-plugin`) — друга реалізація [`harness::pipeline::DetectFn`]
3//! поруч з in-process-замиканням, яке `harness::registry::PluginRegistry`
4//! уже приймає (пункт 16а). [`WasmDetector::into_detect_fn`] — точка
5//! підключення: `PluginRegistry::new`/`replace_detector` беруть її
6//! результат так само, як `Arc::new(|| ...)`.
7
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
12use wasmtime::{Engine, Store};
13
14use harness::pipeline::{DetectFn, Violation};
15
16use crate::engine::new_engine;
17use crate::host_state::HostState;
18use crate::wasi_ctx::read_only_repo_ctx;
19
20// `wasmtime::component::bindgen!` — host-сторона генератора WIT-байндінгів
21// живе В САМОМУ `wasmtime` під фічею `component-model` (задача, розділ
22// "Залежності `plugin-host`"): окремого `wit-bindgen` у `Cargo.toml` не
23// треба. `path` — відносно `CARGO_MANIFEST_DIR` цього крейта
24// (`crates/plugin-host`), тобто корінь `wit/` репозиторію напряму: жодного
25// дубльованого `wit/` усередині `plugin-host` — контракт лишається одним
26// джерелом правди (пункт 16а, doc-коментар `wit/types.wit`).
27//
28// Кожна з чотирьох поверхонь (`detector`/`verify`/`ast_facts`/`t0`) робить
29// СВІЙ `bindgen!` у СВОЄМУ приватному `mod bindings` — одна макро-
30// інвокація на файл, а не одна спільна на весь world-набір: `bindgen!`
31// генерує одну структуру світу на виклик (тут — `DetectorPlugin`), і
32// чотири світи цього пакета експортують РІЗНІ інтерфейси (`detector` проти
33// `verify` проти...), тож спільний виклик однаково довелось би повторити
34// чотири рази з різним `world:`.
35mod bindings {
36 wasmtime::component::bindgen!({
37 path: "wit",
38 world: "detector-plugin",
39 });
40}
41
42/// Дзеркало `record violation` (`wit/types.wit`) → [`Violation`] —
43/// РЕАЛЬНИЙ тип, який `bindgen!` вивів із `.wit`-файлів (на відміну від
44/// ручного дзеркала `crates/harness/src/registry.rs::wit_conformance`,
45/// яке лише перевіряє ФОРМУ; тут — фактичний конвертер типу, який
46/// `wasmtime` згенерував сам). Якщо `record violation` і
47/// `harness::pipeline::Violation` розійдуться полями, це впаде тут
48/// компіляцією, не тестом.
49impl From<bindings::exports::n7n::fix_deps::detector::Violation> for Violation {
50 fn from(v: bindings::exports::n7n::fix_deps::detector::Violation) -> Self {
51 Violation {
52 file: PathBuf::from(v.file),
53 line: v.line.map(|l| l as usize),
54 message: v.message,
55 }
56 }
57}
58
59/// Реалізація вужчого `host-fs`-імпорту (`wit/host.wit`) для поверхні
60/// `detector` — читання ЗАВЖДИ, запису немає в самій сигнатурі
61/// (`host-fs.read-file` не має аналога "write"). Кожен bindgen!-модуль
62/// генерує СВІЙ окремий трейт `Host` (чотири номінально різних типи з
63/// однаковою формою — bindgen! не дедуплікує їх між макро-інвокаціями),
64/// тож ця імплементація повторюється в кожному з чотирьох файлів
65/// (`verify.rs`/`ast_facts.rs`/`t0.rs`) з тим самим однорядковим тілом.
66impl bindings::n7n::fix_deps::host_fs::Host for HostState {
67 fn read_file(&mut self, path: String) -> Result<String, String> {
68 std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
69 }
70}
71
72/// Wasm-бекенд одного `detector`-компонента: скомпільований
73/// [`Component`]/[`Engine`]/[`Linker`] — кешовані (доккоментар crate-level
74/// "інстанс на спробу, без кешу" — кешується лише СКОМПІЛЬОВАНИЙ
75/// компонент, не `Instance`), плюс корінь репозиторію для read-only
76/// preopen-у.
77pub struct WasmDetector {
78 engine: Engine,
79 component: Component,
80 linker: Linker<HostState>,
81 repo_root: PathBuf,
82}
83
84impl WasmDetector {
85 /// Завантажує й компілює `.wasm`-компонент, лінкує WASI p2 (§3.9: guest
86 /// на `wasm32-wasip2` фактично потребує `wasi:cli`/`wasi:io`/
87 /// `wasi:random`/`wasi:clocks` навіть якщо власний `wit/detector.wit`
88 /// їх не імпортує напряму — рантайм `std` для цієї цілі лінкує їх
89 /// безумовно) і власний вужчий `host-fs`.
90 pub fn from_file(
91 repo_root: impl Into<PathBuf>,
92 wasm_path: impl AsRef<Path>,
93 ) -> wasmtime::Result<Self> {
94 let engine = new_engine()?;
95 let component = Component::from_file(&engine, wasm_path.as_ref())?;
96 let mut linker = Linker::<HostState>::new(&engine);
97 wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
98 bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
99 &mut linker,
100 |s| s,
101 )?;
102 Ok(Self {
103 engine,
104 component,
105 linker,
106 repo_root: repo_root.into(),
107 })
108 }
109
110 /// Один прогін: свіжий `Store`+`WasiCtx`+`Instance` — жодного поля
111 /// цього типу не переживає виклик, тож наступний виклик фізично не
112 /// може успадкувати стан попереднього (доккоментар crate-level).
113 fn call(&self) -> wasmtime::Result<Result<Vec<Violation>, String>> {
114 let wasi = read_only_repo_ctx(&self.repo_root)?;
115 let mut store = Store::new(
116 &self.engine,
117 HostState {
118 wasi,
119 table: ResourceTable::new(),
120 repo_root: self.repo_root.clone(),
121 },
122 );
123 let instance =
124 bindings::DetectorPlugin::instantiate(&mut store, &self.component, &self.linker)?;
125 let result = instance.n7n_fix_deps_detector().call_detect(&mut store)?;
126 Ok(result.map(|violations| violations.into_iter().map(Violation::from).collect()))
127 }
128
129 /// Перетворює на [`DetectFn`] — та сама інʼєкція, яку
130 /// `PluginRegistry::new`/`replace_detector` приймають від in-process
131 /// бекенда сьогодні (`crates/harness/src/registry.rs`).
132 ///
133 /// Помилка САМОГО виклику (інстанціювання впало, трап тощо) мапиться в
134 /// `Err(String)` — той самий канал, яким `detector.wit` уже описує
135 /// "інфраструктурний збій самого виклику" (доккоментар
136 /// `wit/detector.wit`), а не мовчазний "чисто".
137 #[must_use]
138 pub fn into_detect_fn(self) -> DetectFn {
139 let this = Arc::new(self);
140 Arc::new(move || {
141 let this = Arc::clone(&this);
142 Box::pin(async move {
143 this.call()
144 .unwrap_or_else(|err| Err(format!("wasm-компонент detector: {err}")))
145 })
146 })
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 //! Компіляційний доказ, що `From<bindings::...::Violation> for
153 //! Violation` — РЕАЛЬНИЙ конвертер `bindgen!`-типу (доккоментар
154 //! `From` вище) — узгоджений із `wit/detector.wit` без потреби в
155 //! `.wasm`-компоненті (доккоментар crate-level "Тестовий guest",
156 //! сценарій 2: "тести обмеж тим, що можливо без `.wasm`"). Наскрізний
157 //! доказ (реальний guest, реальний виклик) — `tests/wasmtime_backend.rs`.
158 use super::*;
159
160 #[test]
161 fn bindgen_violation_converts_to_harness_violation_field_by_field() {
162 let wit_violation = bindings::exports::n7n::fix_deps::detector::Violation {
163 file: "src/a.rs".to_string(),
164 line: Some(12),
165 message: "порушення".to_string(),
166 };
167
168 let violation = Violation::from(wit_violation);
169
170 assert_eq!(violation.file, PathBuf::from("src/a.rs"));
171 assert_eq!(violation.line, Some(12));
172 assert_eq!(violation.message, "порушення");
173 }
174
175 #[test]
176 fn bindgen_violation_without_a_line_converts_to_none() {
177 let wit_violation = bindings::exports::n7n::fix_deps::detector::Violation {
178 file: "src/b.rs".to_string(),
179 line: None,
180 message: "без рядка".to_string(),
181 };
182
183 assert_eq!(Violation::from(wit_violation).line, None);
184 }
185}