1use serde::{Deserialize, Serialize};
33
34use crate::BufFile;
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct PluginManifest {
39 #[serde(rename = "type")]
41 pub plugin_type: String,
42
43 pub name: String,
45
46 pub version: String,
48
49 #[serde(default)]
51 pub description: String,
52
53 pub js: String,
55
56 pub wasm: String,
58
59 #[serde(default)]
61 pub data_file: Option<String>,
62
63 #[serde(default)]
65 pub canvas_id: Option<String>,
66
67 #[serde(default)]
69 pub entry_point: Option<String>,
70
71 #[serde(default)]
73 pub storage_key: Option<String>,
74
75 #[serde(default)]
77 pub ldt_file: Option<String>,
78
79 #[serde(default)]
81 pub storage_keys: Option<StorageKeys>,
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
86pub struct StorageKeys {
87 #[serde(default)]
89 pub ldt: Option<String>,
90 #[serde(default)]
92 pub config: Option<String>,
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct Plugin {
98 pub manifest: PluginManifest,
100
101 pub plugin_dir: String,
103
104 pub js_content: Vec<u8>,
106
107 pub wasm_content: Vec<u8>,
109
110 pub data_content: Option<Vec<u8>>,
112
113 pub ldt_content: Option<Vec<u8>>,
115}
116
117impl Plugin {
118 pub fn js_as_string(&self) -> Result<String, std::string::FromUtf8Error> {
120 String::from_utf8(self.js_content.clone())
121 }
122
123 pub fn has_data(&self) -> bool {
125 self.data_content.is_some()
126 }
127
128 pub fn data_as_string(&self) -> Option<Result<String, std::string::FromUtf8Error>> {
130 self.data_content
131 .as_ref()
132 .map(|d| String::from_utf8(d.clone()))
133 }
134}
135
136pub struct PluginManager;
138
139impl PluginManager {
140 pub fn discover_plugins(files: &[BufFile]) -> Vec<Plugin> {
145 let mut plugins = Vec::new();
146
147 let manifests: Vec<_> = files
149 .iter()
150 .filter(|f| {
151 f.name
152 .as_ref()
153 .map(|n| n.starts_with("other/viewer/") && n.ends_with("/manifest.json"))
154 .unwrap_or(false)
155 })
156 .collect();
157
158 for manifest_file in manifests {
159 let manifest_path = match &manifest_file.name {
160 Some(p) => p,
161 None => continue,
162 };
163
164 let manifest_content = match &manifest_file.content {
165 Some(c) => c,
166 None => continue,
167 };
168
169 let manifest: PluginManifest = match serde_json::from_slice(manifest_content) {
171 Ok(m) => m,
172 Err(_e) => {
173 #[cfg(debug_assertions)]
174 eprintln!(
175 "[Plugin] Failed to parse manifest {}: {}",
176 manifest_path, _e
177 );
178 continue;
179 }
180 };
181
182 let plugin_dir = manifest_path
184 .strip_suffix("/manifest.json")
185 .unwrap_or(manifest_path)
186 .to_string();
187
188 let js_path = format!("{}/{}", plugin_dir, manifest.js);
190 let js_content = files
191 .iter()
192 .find(|f| f.name.as_ref() == Some(&js_path))
193 .and_then(|f| f.content.clone());
194
195 let wasm_path = format!("{}/{}", plugin_dir, manifest.wasm);
197 let wasm_content = files
198 .iter()
199 .find(|f| f.name.as_ref() == Some(&wasm_path))
200 .and_then(|f| f.content.clone());
201
202 let data_content = manifest.data_file.as_ref().and_then(|data_path| {
204 files
205 .iter()
206 .find(|f| f.name.as_ref() == Some(data_path))
207 .and_then(|f| f.content.clone())
208 });
209
210 let ldt_content = manifest.ldt_file.as_ref().and_then(|ldt_path| {
212 files
213 .iter()
214 .find(|f| f.name.as_ref() == Some(ldt_path))
215 .and_then(|f| f.content.clone())
216 });
217
218 if let (Some(js), Some(wasm)) = (js_content, wasm_content) {
220 plugins.push(Plugin {
221 manifest,
222 plugin_dir,
223 js_content: js,
224 wasm_content: wasm,
225 data_content,
226 ldt_content,
227 });
228 } else {
229 #[cfg(debug_assertions)]
230 eprintln!("[Plugin] {} missing JS or WASM files", manifest.name);
231 }
232 }
233
234 plugins
235 }
236
237 pub fn has_plugins(files: &[BufFile]) -> bool {
239 files.iter().any(|f| {
240 f.name
241 .as_ref()
242 .map(|n| n.starts_with("other/viewer/") && n.ends_with("/manifest.json"))
243 .unwrap_or(false)
244 })
245 }
246
247 pub fn get_plugin_types(files: &[BufFile]) -> Vec<String> {
249 Self::discover_plugins(files)
250 .iter()
251 .map(|p| p.manifest.plugin_type.clone())
252 .collect()
253 }
254
255 pub fn find_plugin(files: &[BufFile], plugin_type: &str) -> Option<Plugin> {
257 Self::discover_plugins(files)
258 .into_iter()
259 .find(|p| p.manifest.plugin_type == plugin_type)
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 fn create_test_files() -> Vec<BufFile> {
268 let manifest = r#"{
269 "type": "test-plugin",
270 "name": "Test Plugin",
271 "version": "1.0.0",
272 "description": "A test plugin",
273 "js": "test.js",
274 "wasm": "test_bg.wasm",
275 "data_file": "other/test_data.json"
276 }"#;
277
278 vec![
279 BufFile {
280 name: Some("other/viewer/test-plugin/manifest.json".to_string()),
281 content: Some(manifest.as_bytes().to_vec()),
282 file_id: None,
283 path: None,
284 },
285 BufFile {
286 name: Some("other/viewer/test-plugin/test.js".to_string()),
287 content: Some(b"// test JS".to_vec()),
288 file_id: None,
289 path: None,
290 },
291 BufFile {
292 name: Some("other/viewer/test-plugin/test_bg.wasm".to_string()),
293 content: Some(b"\0asm\x01\0\0\0".to_vec()), file_id: None,
295 path: None,
296 },
297 BufFile {
298 name: Some("other/test_data.json".to_string()),
299 content: Some(b"{\"test\": true}".to_vec()),
300 file_id: None,
301 path: None,
302 },
303 ]
304 }
305
306 #[test]
307 fn test_discover_plugins() {
308 let files = create_test_files();
309 let plugins = PluginManager::discover_plugins(&files);
310
311 assert_eq!(plugins.len(), 1);
312 let plugin = &plugins[0];
313 assert_eq!(plugin.manifest.plugin_type, "test-plugin");
314 assert_eq!(plugin.manifest.name, "Test Plugin");
315 assert!(plugin.has_data());
316 }
317
318 #[test]
319 fn test_has_plugins() {
320 let files = create_test_files();
321 assert!(PluginManager::has_plugins(&files));
322
323 let empty_files: Vec<BufFile> = vec![];
324 assert!(!PluginManager::has_plugins(&empty_files));
325 }
326
327 #[test]
328 fn test_find_plugin() {
329 let files = create_test_files();
330
331 let found = PluginManager::find_plugin(&files, "test-plugin");
332 assert!(found.is_some());
333
334 let not_found = PluginManager::find_plugin(&files, "nonexistent");
335 assert!(not_found.is_none());
336 }
337}