latexsnipper_runtime_plugin_api/
descriptor.rs1use 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 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}