Skip to main content

latexsnipper_runtime_plugin_api/
descriptor.rs

1//! Installed runtime plugin descriptor schema.
2
3use std::path::{Component, Path};
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::{plugin_runtime_error, PluginRuntimeResult};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct RuntimePluginDescriptor {
12    pub schema_version: u32,
13    pub runtime_id: String,
14    pub plugin_version: String,
15    /// Package-relative dynamic-library path. It is never resolved relative to
16    /// a model directory.
17    pub library: String,
18    pub sha256: String,
19}
20
21impl RuntimePluginDescriptor {
22    pub fn validate(&self) -> PluginRuntimeResult<()> {
23        if self.schema_version != 1 {
24            return Err(plugin_runtime_error(format!(
25                "unsupported runtime plugin descriptor schema version {}",
26                self.schema_version
27            )));
28        }
29        validate_runtime_id(&self.runtime_id)?;
30        if self.plugin_version.trim().is_empty() || self.plugin_version.len() > 128 {
31            return Err(plugin_runtime_error(
32                "runtime plugin version must contain 1 to 128 characters",
33            ));
34        }
35        let library = Path::new(&self.library);
36        if self.library.trim().is_empty()
37            || library.is_absolute()
38            || library.components().any(|component| {
39                matches!(
40                    component,
41                    Component::ParentDir | Component::RootDir | Component::Prefix(_)
42                )
43            })
44        {
45            return Err(plugin_runtime_error(
46                "runtime plugin library must be a package-relative path without '..'",
47            ));
48        }
49        validate_sha256(&self.sha256)
50    }
51}
52
53pub(crate) fn validate_runtime_id(runtime_id: &str) -> PluginRuntimeResult<()> {
54    if runtime_id.is_empty()
55        || runtime_id.len() > 128
56        || runtime_id.starts_with("custom:")
57        || !runtime_id
58            .bytes()
59            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
60    {
61        return Err(plugin_runtime_error(
62            "custom runtime id must contain 1 to 128 ASCII letters, digits, '.', '_', or '-' and omit the 'custom:' prefix",
63        ));
64    }
65    if matches!(
66        runtime_id,
67        "onnx-runtime"
68            | "onnxruntime"
69            | "onnx"
70            | "paddle-inference"
71            | "paddle"
72            | "executorch"
73            | "tensorrt"
74            | "tensorrt-rtx"
75            | "coreml"
76            | "core-ml"
77    ) {
78        return Err(plugin_runtime_error(format!(
79            "runtime id '{runtime_id}' is reserved by a built-in runtime"
80        )));
81    }
82    Ok(())
83}
84
85pub(crate) fn validate_sha256(value: &str) -> PluginRuntimeResult<()> {
86    if value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
87        Ok(())
88    } else {
89        Err(plugin_runtime_error(
90            "runtime plugin SHA-256 must contain exactly 64 hexadecimal characters",
91        ))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn descriptor() -> RuntimePluginDescriptor {
100        RuntimePluginDescriptor {
101            schema_version: 1,
102            runtime_id: "vendor.npu".to_owned(),
103            plugin_version: "1.0.0".to_owned(),
104            library: "bin/vendor_npu.dll".to_owned(),
105            sha256: "a".repeat(64),
106        }
107    }
108
109    #[test]
110    fn accepts_a_scoped_relative_library() {
111        assert!(descriptor().validate().is_ok());
112    }
113
114    #[test]
115    fn rejects_builtin_ids_and_path_traversal() {
116        let mut value = descriptor();
117        value.runtime_id = "onnx-runtime".to_owned();
118        assert!(value.validate().is_err());
119        value.runtime_id = "vendor.npu".to_owned();
120        value.library = "../plugin.dll".to_owned();
121        assert!(value.validate().is_err());
122    }
123}