1use crate::clap::{
4 CLAP_EXT_AUDIO_PORTS, CLAP_EXT_PARAMS, CLAP_VERSION, ClapAudioPortInfo, ClapHost,
5 ClapParamInfo, ClapPluginAudioPorts, ClapPluginEntry, ClapPluginFactory, ClapPluginParams,
6};
7use serde::{Deserialize, Serialize};
8use std::ffi::{CStr, CString, c_char, c_void};
9use std::path::{Path, PathBuf};
10use std::ptr;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ParamMetadata {
15 pub id: u32,
16 pub name: String,
17 pub module: String,
18 pub min_value: f64,
19 pub max_value: f64,
20 pub default_value: f64,
21 pub flags: u32,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AudioPortMetadata {
27 pub id: u32,
28 pub name: String,
29 pub channel_count: u32,
30 pub flags: u32,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PluginMetadata {
36 pub id: String,
37 pub name: String,
38 pub vendor: String,
39 pub version: String,
40 pub description: String,
41 pub params: Vec<ParamMetadata>,
42 pub audio_inputs: Vec<AudioPortMetadata>,
43 pub audio_outputs: Vec<AudioPortMetadata>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ScanResult {
49 pub format: String,
50 pub path: String,
51 pub plugins: Vec<PluginMetadata>,
52 pub error: Option<String>,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ClapPluginInfo {
59 pub name: String,
60 pub path: String,
61 pub capabilities: Option<ClapPluginCapabilities>,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ClapPluginCapabilities {
66 pub has_gui: bool,
67 pub gui_apis: Vec<String>,
68 pub supports_embedded: bool,
69 pub supports_floating: bool,
70 pub has_params: bool,
71 pub has_state: bool,
72 pub audio_inputs: usize,
73 pub audio_outputs: usize,
74 pub midi_inputs: usize,
75 pub midi_outputs: usize,
76}
77
78unsafe extern "C" fn dummy_get_extension(_: *const ClapHost, _: *const c_char) -> *const c_void {
79 ptr::null()
80}
81unsafe extern "C" fn dummy_request_restart(_: *const ClapHost) {}
82unsafe extern "C" fn dummy_request_process(_: *const ClapHost) {}
83unsafe extern "C" fn dummy_request_callback(_: *const ClapHost) {}
84
85fn cstr_to_string(ptr: *const c_char) -> String {
86 if ptr.is_null() {
87 return String::new();
88 }
89 unsafe { CStr::from_ptr(ptr) }
90 .to_string_lossy()
91 .into_owned()
92}
93
94fn clap_version_is_compatible(plugin_version: &crate::clap::ClapVersion) -> bool {
96 plugin_version.major == crate::clap::CLAP_VERSION.major
97 && plugin_version.minor <= crate::clap::CLAP_VERSION.minor
98}
99
100pub fn scan_clap_plugin(plugin_path: &str) -> ScanResult {
102 let path = Path::new(plugin_path);
103 if !path.exists() {
104 return ScanResult {
105 format: "clap".to_string(),
106 path: plugin_path.to_string(),
107 plugins: Vec::new(),
108 error: Some(format!("path does not exist: {plugin_path}")),
109 };
110 }
111
112 let library = match unsafe { libloading::Library::new(path) } {
113 Ok(lib) => lib,
114 Err(e) => {
115 return ScanResult {
116 format: "clap".to_string(),
117 path: plugin_path.to_string(),
118 plugins: Vec::new(),
119 error: Some(format!("failed to load library: {e}")),
120 };
121 }
122 };
123
124 let entry: libloading::Symbol<*const ClapPluginEntry> =
125 match unsafe { library.get(b"clap_entry\0") } {
126 Ok(sym) => sym,
127 Err(e) => {
128 return ScanResult {
129 format: "clap".to_string(),
130 path: plugin_path.to_string(),
131 plugins: Vec::new(),
132 error: Some(format!("clap_entry not found: {e}")),
133 };
134 }
135 };
136
137 let entry = unsafe { &**entry };
138
139 if let Some(init) = entry.init {
140 let path_c = match CString::new(plugin_path) {
141 Ok(s) => s,
142 Err(_) => {
143 return ScanResult {
144 format: "clap".to_string(),
145 path: plugin_path.to_string(),
146 plugins: Vec::new(),
147 error: Some("plugin path contains null bytes".to_string()),
148 };
149 }
150 };
151 if !unsafe { init(path_c.as_ptr()) } {
152 return ScanResult {
153 format: "clap".to_string(),
154 path: plugin_path.to_string(),
155 plugins: Vec::new(),
156 error: Some("clap_entry.init() failed".to_string()),
157 };
158 }
159 }
160
161 let factory = if let Some(get_factory) = entry.get_factory {
162 let factory_id = CString::new("clap.plugin-factory").unwrap();
163 let factory_ptr = unsafe { get_factory(factory_id.as_ptr()) };
164 if factory_ptr.is_null() {
165 return ScanResult {
166 format: "clap".to_string(),
167 path: plugin_path.to_string(),
168 plugins: Vec::new(),
169 error: Some("clap.plugin-factory not found".to_string()),
170 };
171 }
172 unsafe { &*(factory_ptr as *const ClapPluginFactory) }
173 } else {
174 return ScanResult {
175 format: "clap".to_string(),
176 path: plugin_path.to_string(),
177 plugins: Vec::new(),
178 error: Some("clap_entry.get_factory is null".to_string()),
179 };
180 };
181
182 let count = factory
183 .get_plugin_count
184 .map(|f| unsafe { f(factory) })
185 .unwrap_or(0);
186
187 let mut host = ClapHost {
188 clap_version: CLAP_VERSION,
189 host_data: ptr::null_mut(),
190 name: c"maolan-plugin-host".as_ptr(),
191 vendor: c"Maolan".as_ptr(),
192 url: c"https://maolan.github.io".as_ptr(),
193 version: c"0.1.0".as_ptr(),
194 get_extension: Some(dummy_get_extension),
195 request_restart: Some(dummy_request_restart),
196 request_process: Some(dummy_request_process),
197 request_callback: Some(dummy_request_callback),
198 };
199 host.host_data = (&mut host as *mut ClapHost).cast::<c_void>();
200
201 let mut plugins = Vec::with_capacity(count as usize);
202
203 for i in 0..count {
204 let desc = factory
205 .get_plugin_descriptor
206 .map(|f| unsafe { f(factory, i) })
207 .unwrap_or(ptr::null());
208 if desc.is_null() {
209 continue;
210 }
211 let desc = unsafe { &*desc };
212
213 if !clap_version_is_compatible(&desc.clap_version) {
214 continue;
215 }
216
217 let plugin_id = cstr_to_string(desc.id);
218 let plugin_id_c = match CString::new(&*plugin_id) {
219 Ok(s) => s,
220 Err(_) => continue,
221 };
222
223 let plugin = factory
224 .create_plugin
225 .map(|f| unsafe { f(factory, &host, plugin_id_c.as_ptr()) })
226 .unwrap_or(ptr::null());
227 if plugin.is_null() {
228 continue;
229 }
230
231 let init_ok = unsafe { (*plugin).init }
232 .map(|f| unsafe { f(plugin) })
233 .unwrap_or(false);
234 if !init_ok {
235 unsafe {
236 if let Some(destroy) = (*plugin).destroy {
237 destroy(plugin);
238 }
239 }
240 continue;
241 }
242
243 let mut params = Vec::new();
244 let mut audio_inputs = Vec::new();
245 let mut audio_outputs = Vec::new();
246
247 unsafe {
249 let ext = (*plugin)
250 .get_extension
251 .map(|f| f(plugin, CLAP_EXT_PARAMS.as_ptr()));
252 if let Some(ptr) = ext
253 && !ptr.is_null()
254 {
255 let p = &*(ptr as *const ClapPluginParams);
256 let count = p.count.map(|f| f(plugin)).unwrap_or(0);
257 for pi in 0..count {
258 let mut info = ClapParamInfo {
259 id: 0,
260 flags: 0,
261 cookie: ptr::null_mut(),
262 name: [0; 256],
263 module: [0; 1024],
264 min_value: 0.0,
265 max_value: 0.0,
266 default_value: 0.0,
267 };
268 if p.get_info
269 .map(|f| f(plugin, pi, &mut info))
270 .unwrap_or(false)
271 {
272 let name = CStr::from_ptr(info.name.as_ptr())
273 .to_string_lossy()
274 .into_owned();
275 let module = CStr::from_ptr(info.module.as_ptr())
276 .to_string_lossy()
277 .into_owned();
278 params.push(ParamMetadata {
279 id: info.id,
280 name,
281 module,
282 min_value: info.min_value,
283 max_value: info.max_value,
284 default_value: info.default_value,
285 flags: info.flags,
286 });
287 }
288 }
289 }
290 }
291
292 unsafe {
294 let ext = (*plugin)
295 .get_extension
296 .map(|f| f(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()));
297 if let Some(ptr) = ext
298 && !ptr.is_null()
299 {
300 let ap = &*(ptr as *const ClapPluginAudioPorts);
301 let in_count = ap.count.map(|f| f(plugin, true)).unwrap_or(0);
302 let out_count = ap.count.map(|f| f(plugin, false)).unwrap_or(0);
303 for pi in 0..in_count {
304 let mut info = ClapAudioPortInfo {
305 id: 0,
306 name: [0; 256],
307 flags: 0,
308 channel_count: 0,
309 port_type: ptr::null(),
310 in_place_pair: 0,
311 };
312 if ap
313 .get
314 .map(|f| f(plugin, pi, true, &mut info))
315 .unwrap_or(false)
316 {
317 let name = CStr::from_ptr(info.name.as_ptr())
318 .to_string_lossy()
319 .into_owned();
320 audio_inputs.push(AudioPortMetadata {
321 id: info.id,
322 name,
323 channel_count: info.channel_count,
324 flags: info.flags,
325 });
326 }
327 }
328 for pi in 0..out_count {
329 let mut info = ClapAudioPortInfo {
330 id: 0,
331 name: [0; 256],
332 flags: 0,
333 channel_count: 0,
334 port_type: ptr::null(),
335 in_place_pair: 0,
336 };
337 if ap
338 .get
339 .map(|f| f(plugin, pi, false, &mut info))
340 .unwrap_or(false)
341 {
342 let name = CStr::from_ptr(info.name.as_ptr())
343 .to_string_lossy()
344 .into_owned();
345 audio_outputs.push(AudioPortMetadata {
346 id: info.id,
347 name,
348 channel_count: info.channel_count,
349 flags: info.flags,
350 });
351 }
352 }
353 }
354 }
355
356 plugins.push(PluginMetadata {
357 id: plugin_id,
358 name: cstr_to_string(desc.name),
359 vendor: cstr_to_string(desc.vendor),
360 version: cstr_to_string(desc.version),
361 description: cstr_to_string(desc.description),
362 params,
363 audio_inputs,
364 audio_outputs,
365 });
366
367 unsafe {
368 if let Some(destroy) = (*plugin).destroy {
369 destroy(plugin);
370 }
371 }
372 }
373
374 if let Some(deinit) = entry.deinit {
375 unsafe { deinit() };
376 }
377
378 ScanResult {
379 format: "clap".to_string(),
380 path: plugin_path.to_string(),
381 plugins,
382 error: None,
383 }
384}
385
386#[cfg(any(
389 target_os = "macos",
390 target_os = "linux",
391 target_os = "freebsd",
392 target_os = "openbsd"
393))]
394fn default_clap_search_roots() -> Vec<PathBuf> {
395 let mut roots = Vec::new();
396 #[cfg(target_os = "macos")]
397 {
398 crate::paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
399 }
400 #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
401 {
402 crate::paths::push_unix_plugin_roots(&mut roots, "clap");
403 }
404 roots
405}
406
407#[cfg(not(any(
408 target_os = "macos",
409 target_os = "linux",
410 target_os = "freebsd",
411 target_os = "openbsd"
412)))]
413fn default_clap_search_roots() -> Vec<PathBuf> {
414 Vec::new()
415}
416
417fn is_supported_clap_binary(path: &Path) -> bool {
418 path.extension()
419 .is_some_and(|ext| ext.eq_ignore_ascii_case("clap"))
420}
421
422fn scan_clap_bundle(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
424 use crate::clap::{
425 ClapPluginAudioPorts, ClapPluginEntry, ClapPluginFactory, ClapPluginGui,
426 ClapPluginNotePorts, ClapPluginParams,
427 };
428
429 let path_str = path.to_string_lossy().to_string();
430 let factory_id = c"clap.plugin-factory";
431 let mut host = ClapHost {
432 clap_version: CLAP_VERSION,
433 host_data: ptr::null_mut(),
434 name: c"Maolan".as_ptr(),
435 vendor: c"Maolan".as_ptr(),
436 url: c"https://example.invalid".as_ptr(),
437 version: c"0.0.1".as_ptr(),
438 get_extension: Some(dummy_get_extension),
439 request_restart: Some(dummy_request_restart),
440 request_process: Some(dummy_request_process),
441 request_callback: Some(dummy_request_callback),
442 };
443 host.host_data = (&mut host as *mut ClapHost).cast::<c_void>();
444
445 let lib = match unsafe { libloading::Library::new(path) } {
446 Ok(l) => l,
447 Err(_) => {
448 return vec![ClapPluginInfo {
449 name: path
450 .file_stem()
451 .map(|s| s.to_string_lossy().to_string())
452 .unwrap_or_else(|| path_str.clone()),
453 path: path_str,
454 capabilities: None,
455 }];
456 }
457 };
458
459 let entry: libloading::Symbol<*const ClapPluginEntry> =
460 match unsafe { lib.get(b"clap_entry\0") } {
461 Ok(sym) => sym,
462 Err(_) => {
463 return vec![ClapPluginInfo {
464 name: path
465 .file_stem()
466 .map(|s| s.to_string_lossy().to_string())
467 .unwrap_or_else(|| path_str.clone()),
468 path: path_str,
469 capabilities: None,
470 }];
471 }
472 };
473 let entry = unsafe { &**entry };
474
475 if let Some(init) = entry.init {
476 let path_c = match CString::new(&*path_str) {
477 Ok(s) => s,
478 Err(_) => {
479 return vec![ClapPluginInfo {
480 name: path_str.clone(),
481 path: path_str,
482 capabilities: None,
483 }];
484 }
485 };
486 if !unsafe { init(path_c.as_ptr()) } {
487 return vec![ClapPluginInfo {
488 name: path_str.clone(),
489 path: path_str,
490 capabilities: None,
491 }];
492 }
493 }
494
495 let factory = if let Some(get_factory) = entry.get_factory {
496 let factory_ptr = unsafe { get_factory(factory_id.as_ptr()) };
497 if factory_ptr.is_null() {
498 return vec![ClapPluginInfo {
499 name: path_str.clone(),
500 path: path_str,
501 capabilities: None,
502 }];
503 }
504 unsafe { &*(factory_ptr as *const ClapPluginFactory) }
505 } else {
506 return vec![ClapPluginInfo {
507 name: path_str.clone(),
508 path: path_str,
509 capabilities: None,
510 }];
511 };
512
513 let count = factory
514 .get_plugin_count
515 .map(|f| unsafe { f(factory) })
516 .unwrap_or(0);
517
518 let mut out = Vec::with_capacity(count as usize);
519
520 for i in 0..count {
521 let desc = factory
522 .get_plugin_descriptor
523 .map(|f| unsafe { f(factory, i) })
524 .unwrap_or(ptr::null());
525 if desc.is_null() {
526 continue;
527 }
528 let desc = unsafe { &*desc };
529 if !clap_version_is_compatible(&desc.clap_version) {
530 continue;
531 }
532 let name = cstr_to_string(desc.name);
533 let plugin_id = cstr_to_string(desc.id);
534 let plugin_id_c = match CString::new(&*plugin_id) {
535 Ok(s) => s,
536 Err(_) => continue,
537 };
538
539 let plugin = factory
540 .create_plugin
541 .map(|f| unsafe { f(factory, &host, plugin_id_c.as_ptr()) })
542 .unwrap_or(ptr::null());
543 if plugin.is_null() {
544 continue;
545 }
546 let init_ok = unsafe { (*plugin).init }
547 .map(|f| unsafe { f(plugin) })
548 .unwrap_or(false);
549 if !init_ok {
550 unsafe {
551 if let Some(destroy) = (*plugin).destroy {
552 destroy(plugin);
553 }
554 }
555 continue;
556 }
557
558 let mut capabilities = None;
559 if scan_capabilities {
560 let mut caps = ClapPluginCapabilities {
561 has_gui: false,
562 gui_apis: Vec::new(),
563 supports_embedded: false,
564 supports_floating: false,
565 has_params: false,
566 has_state: false,
567 audio_inputs: 0,
568 audio_outputs: 0,
569 midi_inputs: 0,
570 midi_outputs: 0,
571 };
572
573 unsafe {
574 let ext = (*plugin)
575 .get_extension
576 .map(|f| f(plugin, c"clap.gui".as_ptr()));
577 if let Some(ptr) = ext
578 && !ptr.is_null()
579 {
580 let gui = &*(ptr as *const ClapPluginGui);
581 caps.has_gui = gui
582 .is_api_supported
583 .map(|f| f(plugin, c"x11".as_ptr(), true))
584 .unwrap_or(false)
585 || gui
586 .is_api_supported
587 .map(|f| f(plugin, c"win32".as_ptr(), true))
588 .unwrap_or(false)
589 || gui
590 .is_api_supported
591 .map(|f| f(plugin, c"cocoa".as_ptr(), true))
592 .unwrap_or(false);
593 if caps.has_gui {
594 caps.gui_apis = vec!["x11".to_string()];
595 caps.supports_embedded = true;
596 caps.supports_floating = gui
597 .is_api_supported
598 .map(|f| f(plugin, ptr::null(), false))
599 .unwrap_or(false);
600 }
601 }
602 }
603
604 unsafe {
605 let ext = (*plugin)
606 .get_extension
607 .map(|f| f(plugin, CLAP_EXT_PARAMS.as_ptr()));
608 if let Some(ptr) = ext
609 && !ptr.is_null()
610 {
611 let p = &*(ptr as *const ClapPluginParams);
612 caps.has_params = p.count.map(|f| f(plugin)).unwrap_or(0) > 0;
613 }
614 }
615
616 unsafe {
617 let ext = (*plugin)
618 .get_extension
619 .map(|f| f(plugin, c"clap.state".as_ptr()));
620 if let Some(ptr) = ext
621 && !ptr.is_null()
622 {
623 caps.has_state = true;
624 }
625 }
626
627 unsafe {
628 let ext = (*plugin)
629 .get_extension
630 .map(|f| f(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()));
631 if let Some(ptr) = ext
632 && !ptr.is_null()
633 {
634 let ap = &*(ptr as *const ClapPluginAudioPorts);
635 caps.audio_inputs = ap.count.map(|f| f(plugin, true)).unwrap_or(0) as usize;
636 caps.audio_outputs = ap.count.map(|f| f(plugin, false)).unwrap_or(0) as usize;
637 }
638 }
639
640 unsafe {
641 let ext = (*plugin)
642 .get_extension
643 .map(|f| f(plugin, c"clap.note-ports".as_ptr()));
644 if let Some(ptr) = ext
645 && !ptr.is_null()
646 {
647 let np = &*(ptr as *const ClapPluginNotePorts);
648 caps.midi_inputs = np.count.map(|f| f(plugin, true)).unwrap_or(0) as usize;
649 caps.midi_outputs = np.count.map(|f| f(plugin, false)).unwrap_or(0) as usize;
650 }
651 }
652
653 capabilities = Some(caps);
654 }
655
656 unsafe {
657 if let Some(destroy) = (*plugin).destroy {
658 destroy(plugin);
659 }
660 }
661
662 out.push(ClapPluginInfo {
663 name,
664 path: format!("{}::{}", path_str, plugin_id),
665 capabilities,
666 });
667 }
668
669 if let Some(deinit) = entry.deinit {
670 unsafe { deinit() };
671 }
672
673 if out.is_empty() {
674 out.push(ClapPluginInfo {
675 name: path
676 .file_stem()
677 .map(|s| s.to_string_lossy().to_string())
678 .unwrap_or_else(|| path_str.clone()),
679 path: path_str,
680 capabilities: None,
681 });
682 }
683
684 out
685}
686
687fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
688 let Ok(entries) = std::fs::read_dir(root) else {
689 return;
690 };
691 for entry in entries.flatten() {
692 let path = entry.path();
693 let Ok(ft) = entry.file_type() else {
694 continue;
695 };
696 if ft.is_dir() {
697 if path
698 .file_name()
699 .and_then(|name| name.to_str())
700 .is_some_and(|name| {
701 matches!(
702 name,
703 "deps" | "build" | "incremental" | ".fingerprint" | "examples"
704 )
705 })
706 {
707 continue;
708 }
709 collect_clap_plugins(&path, out, scan_capabilities);
710 continue;
711 }
712
713 if is_supported_clap_binary(&path) {
714 let infos = scan_clap_bundle(&path, scan_capabilities);
715 if infos.is_empty() {
716 let name = path
717 .file_stem()
718 .map(|s| s.to_string_lossy().to_string())
719 .unwrap_or_else(|| path.to_string_lossy().to_string());
720 out.push(ClapPluginInfo {
721 name,
722 path: path.to_string_lossy().to_string(),
723 capabilities: None,
724 });
725 } else {
726 out.extend(infos);
727 }
728 }
729 }
730}
731
732pub fn scan_clap_plugins(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
734 let mut roots = default_clap_search_roots();
735
736 if let Ok(extra) = std::env::var("CLAP_PATH") {
737 for p in std::env::split_paths(&extra) {
738 if !p.as_os_str().is_empty() {
739 roots.push(p);
740 }
741 }
742 }
743
744 let mut out = Vec::new();
745 for root in roots {
746 collect_clap_plugins(&root, &mut out, scan_capabilities);
747 }
748
749 out.sort_by_key(|a| a.name.to_lowercase());
750 out.dedup_by(|a, b| {
751 a.name.eq_ignore_ascii_case(&b.name) && a.path.eq_ignore_ascii_case(&b.path)
752 });
753 out
754}
755
756pub fn scan_vst3_plugins() -> Vec<crate::vst3::Vst3PluginInfo> {
759 crate::vst3::host::Vst3Host::new().list_plugins()
760}
761
762#[cfg(unix)]
765pub fn scan_lv2_plugins() -> Vec<crate::lv2::Lv2PluginInfo> {
766 crate::lv2::Lv2Host::new(48_000.0).list_plugins()
767}
768
769pub fn run_scan(format: &str, plugin_path: &str, output_path: Option<&str>) -> i32 {
777 let json = match format {
778 "clap" => {
779 if plugin_path == "--system" {
780 match serde_json::to_string_pretty(&scan_clap_plugins(true)) {
781 Ok(j) => j,
782 Err(e) => {
783 eprintln!("Failed to serialize scan result: {e}");
784 return 1;
785 }
786 }
787 } else {
788 match serde_json::to_string_pretty(&scan_clap_plugin(plugin_path)) {
789 Ok(j) => j,
790 Err(e) => {
791 eprintln!("Failed to serialize scan result: {e}");
792 return 1;
793 }
794 }
795 }
796 }
797 "vst3" => {
798 if plugin_path != "--system" {
799 eprintln!("VST3 single-file scan not yet supported; use --system");
800 return 1;
801 }
802 match serde_json::to_string_pretty(&scan_vst3_plugins()) {
803 Ok(j) => j,
804 Err(e) => {
805 eprintln!("Failed to serialize scan result: {e}");
806 return 1;
807 }
808 }
809 }
810 #[cfg(unix)]
811 "lv2" => {
812 if plugin_path != "--system" {
813 eprintln!("LV2 single-file scan not yet supported; use --system");
814 return 1;
815 }
816 match serde_json::to_string_pretty(&scan_lv2_plugins()) {
817 Ok(j) => j,
818 Err(e) => {
819 eprintln!("Failed to serialize scan result: {e}");
820 return 1;
821 }
822 }
823 }
824 _ => {
825 eprintln!("Scan format '{}' not supported", format);
826 return 1;
827 }
828 };
829
830 if let Some(path) = output_path {
831 match std::fs::write(path, &json) {
832 Ok(()) => {
833 println!("{path}");
834 0
835 }
836 Err(e) => {
837 eprintln!("Failed to write {path}: {e}");
838 1
839 }
840 }
841 } else {
842 println!("{json}");
843 0
844 }
845}