Skip to main content

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}