nautilus_plugin/
manifest.rs1use std::{fmt::Display, slice};
19
20use nautilus_model::types::fixed::FIXED_PRECISION;
21
22use crate::{
23 NAUTILUS_PLUGIN_ABI_VERSION, PLUGIN_BUILD_ID_VERSION, boundary::BorrowedStr, host::HostVTable,
24};
25
26pub type PluginInitFn = unsafe extern "C" fn(host: *const HostVTable) -> *const PluginManifest;
29
30#[repr(C)]
37#[derive(Debug, Clone, Copy)]
38pub struct PluginBuildId {
39 pub schema_version: u32,
42
43 pub nautilus_plugin_version: BorrowedStr<'static>,
45
46 pub rustc_version: BorrowedStr<'static>,
49
50 pub target_triple: BorrowedStr<'static>,
52
53 pub build_profile: BorrowedStr<'static>,
55
56 pub precision_mode: BorrowedStr<'static>,
58
59 pub fixed_precision: u8,
61}
62
63impl PluginBuildId {
64 #[must_use]
66 pub const fn current() -> Self {
67 Self {
68 schema_version: PLUGIN_BUILD_ID_VERSION,
69 nautilus_plugin_version: BorrowedStr::from_str(env!("CARGO_PKG_VERSION")),
70 rustc_version: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_RUSTC_VERSION")),
71 target_triple: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_TARGET")),
72 build_profile: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_PROFILE")),
73 precision_mode: BorrowedStr::from_str(compiled_precision_mode()),
74 fixed_precision: FIXED_PRECISION,
75 }
76 }
77}
78
79#[must_use]
81pub const fn compiled_precision_mode() -> &'static str {
82 if FIXED_PRECISION > 9 {
83 "high-precision"
84 } else {
85 "standard"
86 }
87}
88
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
91pub struct PluginManifestValidationErrors {
92 messages: Vec<String>,
93}
94
95impl PluginManifestValidationErrors {
96 #[must_use]
98 pub fn is_empty(&self) -> bool {
99 self.messages.is_empty()
100 }
101
102 #[must_use]
104 pub fn messages(&self) -> &[String] {
105 &self.messages
106 }
107
108 fn push(&mut self, message: impl Into<String>) {
109 self.messages.push(message.into());
110 }
111}
112
113impl Display for PluginManifestValidationErrors {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 for (index, message) in self.messages.iter().enumerate() {
116 if index > 0 {
117 write!(f, "; ")?;
118 }
119 write!(f, "{message}")?;
120 }
121 Ok(())
122 }
123}
124
125impl std::error::Error for PluginManifestValidationErrors {}
126
127#[repr(C)]
134#[derive(Debug)]
135pub struct PluginManifest {
136 pub abi_version: u32,
139
140 pub plugin_name: BorrowedStr<'static>,
142
143 pub plugin_vendor: BorrowedStr<'static>,
145
146 pub plugin_version: BorrowedStr<'static>,
148
149 pub build_id: PluginBuildId,
151}
152
153impl PluginManifest {
154 #[must_use]
156 pub fn matches_compiled_abi(&self) -> bool {
157 self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
158 }
159
160 pub fn validate(&self) -> Result<(), PluginManifestValidationErrors> {
166 let mut errors = PluginManifestValidationErrors::default();
167
168 validate_required_str("plugin_name", self.plugin_name, &mut errors);
169 validate_optional_str("plugin_vendor", self.plugin_vendor, &mut errors);
170 validate_required_str("plugin_version", self.plugin_version, &mut errors);
171 validate_build_id(&self.build_id, &mut errors);
172
173 if errors.is_empty() {
174 Ok(())
175 } else {
176 Err(errors)
177 }
178 }
179}
180
181fn validate_build_id(build_id: &PluginBuildId, errors: &mut PluginManifestValidationErrors) {
182 if build_id.schema_version != PLUGIN_BUILD_ID_VERSION {
183 errors.push(format!(
184 "build_id.schema_version {} does not match supported schema {}",
185 build_id.schema_version, PLUGIN_BUILD_ID_VERSION
186 ));
187 return;
188 }
189
190 validate_optional_str(
191 "build_id.nautilus_plugin_version",
192 build_id.nautilus_plugin_version,
193 errors,
194 );
195 validate_optional_str("build_id.rustc_version", build_id.rustc_version, errors);
196 validate_optional_str("build_id.target_triple", build_id.target_triple, errors);
197 validate_optional_str("build_id.build_profile", build_id.build_profile, errors);
198 if let Some(precision_mode) =
199 validate_required_str("build_id.precision_mode", build_id.precision_mode, errors)
200 {
201 let expected = compiled_precision_mode();
202 if precision_mode != expected {
203 errors.push(format!(
204 "build_id.precision_mode '{precision_mode}' does not match host precision mode '{expected}'"
205 ));
206 }
207 }
208
209 if build_id.fixed_precision != FIXED_PRECISION {
210 errors.push(format!(
211 "build_id.fixed_precision {} does not match host fixed precision {}",
212 build_id.fixed_precision, FIXED_PRECISION
213 ));
214 }
215}
216
217fn validate_required_str<'a>(
218 field: &str,
219 value: BorrowedStr<'a>,
220 errors: &mut PluginManifestValidationErrors,
221) -> Option<&'a str> {
222 let text = validate_optional_str(field, value, errors)?;
223 if text.is_empty() {
224 errors.push(format!("{field} must not be empty"));
225 }
226 Some(text)
227}
228
229fn validate_optional_str<'a>(
230 field: &str,
231 value: BorrowedStr<'a>,
232 errors: &mut PluginManifestValidationErrors,
233) -> Option<&'a str> {
234 if value.len == 0 {
235 return Some("");
236 }
237
238 if value.ptr.is_null() {
239 errors.push(format!(
240 "{field} has null pointer with non-zero length {}",
241 value.len
242 ));
243 return None;
244 }
245
246 let bytes = unsafe { slice::from_raw_parts(value.ptr, value.len) };
248 match std::str::from_utf8(bytes) {
249 Ok(text) => Some(text),
250 Err(e) => {
251 errors.push(format!("{field} is not valid UTF-8: {e}"));
252 None
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use rstest::rstest;
260
261 use super::*;
262
263 fn valid_manifest() -> PluginManifest {
264 PluginManifest {
265 abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
266 plugin_name: BorrowedStr::from_str("test-plugin"),
267 plugin_vendor: BorrowedStr::from_str("nautech"),
268 plugin_version: BorrowedStr::from_str("1.0.0"),
269 build_id: PluginBuildId::current(),
270 }
271 }
272
273 #[rstest]
274 fn matches_compiled_abi_accepts_compiled_version() {
275 assert!(valid_manifest().matches_compiled_abi());
276 }
277
278 #[rstest]
279 fn matches_compiled_abi_rejects_mismatch() {
280 let manifest = PluginManifest {
281 abi_version: NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1),
282 ..valid_manifest()
283 };
284
285 assert!(!manifest.matches_compiled_abi());
286 }
287
288 #[rstest]
289 fn validate_accepts_valid_manifest() {
290 valid_manifest().validate().unwrap();
291 }
292
293 #[rstest]
294 fn validate_rejects_missing_name() {
295 let manifest = PluginManifest {
296 plugin_name: BorrowedStr::empty(),
297 ..valid_manifest()
298 };
299
300 let errors = manifest.validate().unwrap_err();
301 assert_eq!(errors.messages(), &["plugin_name must not be empty"]);
302 }
303
304 #[rstest]
305 fn validate_rejects_mismatched_build_schema() {
306 let manifest = PluginManifest {
307 build_id: PluginBuildId {
308 schema_version: PLUGIN_BUILD_ID_VERSION.wrapping_add(1),
309 ..PluginBuildId::current()
310 },
311 ..valid_manifest()
312 };
313
314 let errors = manifest.validate().unwrap_err();
315 assert_eq!(
316 errors.messages(),
317 &[format!(
318 "build_id.schema_version {} does not match supported schema {}",
319 PLUGIN_BUILD_ID_VERSION.wrapping_add(1),
320 PLUGIN_BUILD_ID_VERSION
321 )]
322 );
323 }
324}