Skip to main content

nginx_lint_plugin/
lib.rs

1//! Plugin SDK for building nginx-lint WASM plugins
2//!
3//! This crate provides everything needed to create custom lint rules as WASM plugins
4//! for [nginx-lint](https://github.com/walf443/nginx-lint).
5//!
6//! # Getting Started
7//!
8//! 1. Create a library crate with `crate-type = ["cdylib", "rlib"]`
9//! 2. Implement the [`Plugin`] trait
10//! 3. Register with [`export_plugin!`]
11//! 4. Build with `cargo build --target wasm32-unknown-unknown --release`
12//!
13//! # Modules
14//!
15//! - [`types`] - Core types: [`Plugin`], [`PluginSpec`], [`LintError`], [`Fix`],
16//!   [`ConfigExt`], [`DirectiveExt`]
17//! - [`helpers`] - Utility functions for common checks (domain names, URLs, etc.)
18//! - [`testing`] - Test runner and builder: [`testing::PluginTestRunner`], [`testing::TestCase`]
19//! - [`native`] - [`native::NativePluginRule`] adapter for running plugins without WASM
20//! - [`prelude`] - Convenient re-exports for `use nginx_lint_plugin::prelude::*`
21//!
22//! # API Versioning
23//!
24//! Plugins declare the API version they use via [`PluginSpec::api_version`].
25//! This allows the host to support multiple output formats for backward compatibility.
26//! [`PluginSpec::new()`] automatically sets the current API version ([`API_VERSION`]).
27//!
28//! # Example
29//!
30//! ```
31//! use nginx_lint_plugin::prelude::*;
32//!
33//! #[derive(Default)]
34//! struct MyRule;
35//!
36//! impl Plugin for MyRule {
37//!     fn spec(&self) -> PluginSpec {
38//!         PluginSpec::new("my-custom-rule", "custom", "My custom lint rule")
39//!             .with_severity("warning")
40//!             .with_why("Explain why this rule matters.")
41//!             .with_bad_example("server {\n    dangerous_directive on;\n}")
42//!             .with_good_example("server {\n    # dangerous_directive removed\n}")
43//!     }
44//!
45//!     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
46//!         let mut errors = Vec::new();
47//!         let err = self.spec().error_builder();
48//!
49//!         for ctx in config.all_directives_with_context() {
50//!             if ctx.directive.is("dangerous_directive") {
51//!                 errors.push(
52//!                     err.warning_at("Avoid using dangerous_directive", ctx.directive)
53//!                 );
54//!             }
55//!         }
56//!         errors
57//!     }
58//! }
59//!
60//! // export_plugin!(MyRule);  // Required for WASM build
61//!
62//! // Verify it works
63//! let plugin = MyRule;
64//! let config = nginx_lint_plugin::parse_string("dangerous_directive on;").unwrap();
65//! let errors = plugin.check(&config, "test.conf");
66//! assert_eq!(errors.len(), 1);
67//! ```
68
69pub mod helpers;
70pub mod native;
71pub mod testing;
72mod types;
73
74#[cfg(feature = "container-testing")]
75pub mod container_testing;
76
77pub use types::*;
78
79// Re-export common types from nginx-lint-common
80pub use nginx_lint_common::parse_string;
81pub use nginx_lint_common::parser;
82
83/// Prelude module for convenient imports.
84///
85/// Importing everything from this module is the recommended way to use the SDK:
86///
87/// ```
88/// use nginx_lint_plugin::prelude::*;
89///
90/// // All core types are now available
91/// let spec = PluginSpec::new("example", "test", "Example rule");
92/// assert_eq!(spec.name, "example");
93/// ```
94///
95/// This re-exports all core types ([`Plugin`], [`PluginSpec`], [`LintError`], [`Fix`],
96/// [`Config`], [`Directive`], etc.), extension traits ([`ConfigExt`], [`DirectiveExt`]),
97/// the [`helpers`] module, and the [`export_plugin!`] macro.
98pub mod prelude {
99    pub use super::export_plugin;
100    pub use super::helpers;
101    pub use super::types::API_VERSION;
102    pub use super::types::*;
103}
104
105/// Macro to export a plugin implementation
106///
107/// This macro generates all the required WASM exports for your plugin.
108///
109/// # Example
110///
111/// ```
112/// use nginx_lint_plugin::prelude::*;
113///
114/// #[derive(Default)]
115/// struct MyPlugin;
116///
117/// impl Plugin for MyPlugin {
118///     fn spec(&self) -> PluginSpec {
119///         PluginSpec::new("my-plugin", "custom", "My plugin")
120///     }
121///
122///     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
123///         Vec::new()
124///     }
125/// }
126///
127/// export_plugin!(MyPlugin);
128/// ```
129#[macro_export]
130macro_rules! export_plugin {
131    ($plugin_type:ty) => {
132        #[cfg(all(target_arch = "wasm32", feature = "wasm-export"))]
133        const _: () = {
134            static PLUGIN: std::sync::OnceLock<$plugin_type> = std::sync::OnceLock::new();
135            static PLUGIN_SPEC_CACHE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
136            static CHECK_RESULT_CACHE: std::sync::Mutex<String> =
137                std::sync::Mutex::new(String::new());
138
139            fn get_plugin() -> &'static $plugin_type {
140                PLUGIN.get_or_init(|| <$plugin_type>::default())
141            }
142
143            /// Allocate memory for the host to write data
144            #[unsafe(no_mangle)]
145            pub extern "C" fn alloc(size: u32) -> *mut u8 {
146                let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
147                unsafe { std::alloc::alloc(layout) }
148            }
149
150            /// Deallocate memory
151            #[unsafe(no_mangle)]
152            pub extern "C" fn dealloc(ptr: *mut u8, size: u32) {
153                let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
154                unsafe { std::alloc::dealloc(ptr, layout) }
155            }
156
157            /// Get the length of the plugin spec JSON
158            #[unsafe(no_mangle)]
159            pub extern "C" fn plugin_spec_len() -> u32 {
160                let info = PLUGIN_SPEC_CACHE.get_or_init(|| {
161                    let plugin = get_plugin();
162                    let info = $crate::Plugin::spec(plugin);
163                    serde_json::to_string(&info).unwrap_or_default()
164                });
165                info.len() as u32
166            }
167
168            /// Get the plugin spec JSON pointer
169            #[unsafe(no_mangle)]
170            pub extern "C" fn plugin_spec() -> *const u8 {
171                let info = PLUGIN_SPEC_CACHE.get_or_init(|| {
172                    let plugin = get_plugin();
173                    let info = $crate::Plugin::spec(plugin);
174                    serde_json::to_string(&info).unwrap_or_default()
175                });
176                info.as_ptr()
177            }
178
179            /// Run the lint check
180            #[unsafe(no_mangle)]
181            pub extern "C" fn check(
182                config_ptr: *const u8,
183                config_len: u32,
184                path_ptr: *const u8,
185                path_len: u32,
186            ) -> *const u8 {
187                // Read config JSON from memory
188                let config_json = unsafe {
189                    let slice = std::slice::from_raw_parts(config_ptr, config_len as usize);
190                    std::str::from_utf8_unchecked(slice)
191                };
192
193                // Read path from memory
194                let path = unsafe {
195                    let slice = std::slice::from_raw_parts(path_ptr, path_len as usize);
196                    std::str::from_utf8_unchecked(slice)
197                };
198
199                // Parse config
200                let config: $crate::Config = match serde_json::from_str(config_json) {
201                    Ok(c) => c,
202                    Err(e) => {
203                        let errors = vec![$crate::LintError::error(
204                            "plugin-error",
205                            "plugin",
206                            &format!("Failed to parse config: {}", e),
207                            0,
208                            0,
209                        )];
210                        let result = serde_json::to_string(&errors).unwrap_or_default();
211                        let mut cache = CHECK_RESULT_CACHE.lock().unwrap();
212                        *cache = result;
213                        return cache.as_ptr();
214                    }
215                };
216
217                // Run the check
218                let plugin = get_plugin();
219                let errors = $crate::Plugin::check(plugin, &config, path);
220
221                // Serialize result
222                let result = serde_json::to_string(&errors).unwrap_or_else(|_| "[]".to_string());
223                let mut cache = CHECK_RESULT_CACHE.lock().unwrap();
224                *cache = result;
225                cache.as_ptr()
226            }
227
228            /// Get the length of the check result
229            #[unsafe(no_mangle)]
230            pub extern "C" fn check_result_len() -> u32 {
231                let cache = CHECK_RESULT_CACHE.lock().unwrap();
232                cache.len() as u32
233            }
234        };
235    };
236}