1use flate2::read::GzDecoder;
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use std::fs;
13use std::io::Read;
14use std::path::Path;
15use std::sync::LazyLock;
16
17pub(crate) const XREF_SUPPORTED_EXTENSIONS: &[&str] = &[
19 ".als", ".rpp", ".rpp-bak", ".bwproject", ".song", ".dawproject", ".flp", ".logicx", ".cpr",
20 ".npr", ".ptx", ".ptf", ".reason",
21];
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24pub struct PluginRef {
25 pub name: String,
26 #[serde(rename = "normalizedName")]
27 pub normalized_name: String,
28 pub manufacturer: String,
29 #[serde(rename = "pluginType")]
30 pub plugin_type: String, }
32
33static ARCH_SUFFIX_RE: LazyLock<Regex> = LazyLock::new(|| {
35 Regex::new(r"(?i)\s*[\(\[](x64|x86_64|x86|arm64|aarch64|64[- ]?bit|32[- ]?bit|intel|apple silicon|universal|stereo|mono|vst3?|au|aax)[\)\]]$").unwrap()
36});
37
38static ALS_VST2_BLOCK_RE: LazyLock<Regex> =
41 LazyLock::new(|| Regex::new(r#"<VstPluginInfo[^>]*>[\s\S]*?</VstPluginInfo>"#).unwrap());
42static ALS_VST2_NAME_RE: LazyLock<Regex> =
43 LazyLock::new(|| Regex::new(r#"<PlugName\s+Value="([^"]+)""#).unwrap());
44static ALS_VST2_MFG_RE: LazyLock<Regex> =
45 LazyLock::new(|| Regex::new(r#"<Manufacturer\s+Value="([^"]+)""#).unwrap());
46static ALS_VST3_BLOCK_RE: LazyLock<Regex> =
47 LazyLock::new(|| Regex::new(r#"<Vst3PluginInfo[^>]*>[\s\S]*?</Vst3PluginInfo>"#).unwrap());
48static ALS_VST3_NAME_RE: LazyLock<Regex> =
49 LazyLock::new(|| Regex::new(r#"<Name\s+Value="([^"]+)""#).unwrap());
50static ALS_VST3_MFG_RE: LazyLock<Regex> =
51 LazyLock::new(|| Regex::new(r#"<DeviceCreator\s+Value="([^"]+)""#).unwrap());
52static ALS_AU_BLOCK_RE: LazyLock<Regex> =
53 LazyLock::new(|| Regex::new(r#"<AuPluginInfo[^>]*>[\s\S]*?</AuPluginInfo>"#).unwrap());
54static ALS_AU_NAME_RE: LazyLock<Regex> =
55 LazyLock::new(|| Regex::new(r#"<Name\s+Value="([^"]+)""#).unwrap());
56static ALS_AU_MFG_RE: LazyLock<Regex> =
57 LazyLock::new(|| Regex::new(r#"<Manufacturer\s+Value="([^"]+)""#).unwrap());
58
59static RPP_PLUGIN_RE: LazyLock<Regex> = LazyLock::new(|| {
62 Regex::new(r#"<(?:VST|AU|CLAP)\s+"(VST3?|AU|CLAP):\s*(.+?)\s*(?:\(([^)]+)\))?\s*""#).unwrap()
63});
64
65static XML_PLUG_NAME_RE: LazyLock<Regex> =
68 LazyLock::new(|| Regex::new(r#"plugName="([^"]+)""#).unwrap());
69static XML_DEVICE_NAME_RE: LazyLock<Regex> =
70 LazyLock::new(|| Regex::new(r#"deviceName="([^"]+)""#).unwrap());
71static XML_PLUGIN_NAME_RE: LazyLock<Regex> =
72 LazyLock::new(|| Regex::new(r#"<Plugin\s+name="([^"]+)""#).unwrap());
73
74pub fn normalize_plugin_name(name: &str) -> String {
77 let mut s = name.trim().to_string();
78 loop {
80 let before = s.len();
81 s = ARCH_SUFFIX_RE.replace(&s, "").to_string();
82 if s.len() == before {
83 break;
84 }
85 }
86 static BARE_SUFFIX_RE: LazyLock<Regex> =
88 LazyLock::new(|| Regex::new(r"(?i)\s+(x64|x86_64|x86|64bit|32bit)$").unwrap());
89 s = BARE_SUFFIX_RE.replace(&s, "").to_string();
90 let result = s
92 .split_whitespace()
93 .collect::<Vec<_>>()
94 .join(" ")
95 .to_lowercase();
96 if result.is_empty() {
98 name.trim().to_lowercase()
99 } else {
100 result
101 }
102}
103
104pub fn extract_plugins(project_path: &str) -> Vec<PluginRef> {
107 let path = Path::new(project_path);
108 let ext = path
109 .extension()
110 .or_else(|| {
111 let name = path.file_name()?.to_str()?;
113 if name.ends_with(".rpp-bak") {
114 Some(std::ffi::OsStr::new("rpp-bak"))
115 } else {
116 None
117 }
118 })
119 .and_then(|e| e.to_str())
120 .unwrap_or("")
121 .to_lowercase();
122
123 let mut plugins = match ext.as_str() {
124 "als" => parse_ableton(path),
125 "rpp" | "rpp-bak" => parse_reaper(path),
126 "bwproject" => parse_bitwig(path),
127 "song" => parse_studio_one(path),
128 "dawproject" => parse_dawproject(path),
129 "flp" => parse_flp(path),
130 "logicx" => parse_logic(path),
131 "cpr" | "npr" => parse_cubase(path),
132 "ptx" | "ptf" => parse_protools(path),
133 "reason" => parse_reason(path),
134 _ => vec![],
135 };
136
137 let mut seen = HashSet::new();
139 plugins.retain(|p| seen.insert((p.normalized_name.clone(), p.plugin_type.clone())));
140 plugins.sort_by(|a, b| a.normalized_name.cmp(&b.normalized_name));
141 plugins
142}
143
144fn parse_ableton(path: &Path) -> Vec<PluginRef> {
151 let data = match fs::read(path) {
152 Ok(d) => d,
153 Err(_) => return vec![],
154 };
155
156 let mut decoder = GzDecoder::new(&data[..]);
157 let mut xml = String::new();
158 if decoder.read_to_string(&mut xml).is_err() {
159 return vec![];
160 }
161
162 let mut plugins = Vec::new();
163
164 for block in ALS_VST2_BLOCK_RE.find_iter(&xml) {
166 let text = block.as_str();
167 let name = ALS_VST2_NAME_RE
168 .captures(text)
169 .and_then(|c| c.get(1))
170 .map(|m| m.as_str().to_string())
171 .unwrap_or_default();
172 let mfg = ALS_VST2_MFG_RE
173 .captures(text)
174 .and_then(|c| c.get(1))
175 .map(|m| m.as_str().to_string())
176 .unwrap_or_default();
177 if !name.is_empty() {
178 let normalized_name = normalize_plugin_name(&name);
179 plugins.push(PluginRef {
180 name,
181 normalized_name,
182 manufacturer: mfg,
183 plugin_type: "VST2".into(),
184 });
185 }
186 }
187
188 for block in ALS_VST3_BLOCK_RE.find_iter(&xml) {
190 let text = block.as_str();
191 let name = ALS_VST3_NAME_RE
192 .captures(text)
193 .and_then(|c| c.get(1))
194 .map(|m| m.as_str().to_string())
195 .unwrap_or_default();
196 let mfg = ALS_VST3_MFG_RE
197 .captures(text)
198 .and_then(|c| c.get(1))
199 .map(|m| m.as_str().to_string())
200 .unwrap_or_default();
201 if !name.is_empty() {
202 let normalized_name = normalize_plugin_name(&name);
203 plugins.push(PluginRef {
204 name,
205 normalized_name,
206 manufacturer: mfg,
207 plugin_type: "VST3".into(),
208 });
209 }
210 }
211
212 for block in ALS_AU_BLOCK_RE.find_iter(&xml) {
214 let text = block.as_str();
215 let name = ALS_AU_NAME_RE
216 .captures(text)
217 .and_then(|c| c.get(1))
218 .map(|m| m.as_str().to_string())
219 .unwrap_or_default();
220 let mfg = ALS_AU_MFG_RE
221 .captures(text)
222 .and_then(|c| c.get(1))
223 .map(|m| m.as_str().to_string())
224 .unwrap_or_default();
225 if !name.is_empty() {
226 let normalized_name = normalize_plugin_name(&name);
227 plugins.push(PluginRef {
228 name,
229 normalized_name,
230 manufacturer: mfg,
231 plugin_type: "AU".into(),
232 });
233 }
234 }
235
236 plugins
237}
238
239fn parse_reaper(path: &Path) -> Vec<PluginRef> {
247 let text = match fs::read_to_string(path) {
248 Ok(t) => t,
249 Err(_) => return vec![],
250 };
251
252 let mut plugins = Vec::new();
253
254 for cap in RPP_PLUGIN_RE.captures_iter(&text) {
256 let ptype = cap.get(1).map(|m| m.as_str()).unwrap_or("VST2");
257 let name = cap
258 .get(2)
259 .map(|m| m.as_str().trim().to_string())
260 .unwrap_or_default();
261 let mfg = cap
262 .get(3)
263 .map(|m| m.as_str().trim().to_string())
264 .unwrap_or_default();
265
266 if !name.is_empty() {
267 let plugin_type = match ptype {
268 "VST" => "VST2",
269 "VST3" => "VST3",
270 "AU" => "AU",
271 "CLAP" => "CLAP",
272 _ => "VST2",
273 }
274 .to_string();
275
276 let normalized_name = normalize_plugin_name(&name);
277 plugins.push(PluginRef {
278 name,
279 normalized_name,
280 manufacturer: mfg,
281 plugin_type,
282 });
283 }
284 }
285
286 plugins
287}
288
289fn parse_studio_one(path: &Path) -> Vec<PluginRef> {
296 let file = match fs::File::open(path) {
297 Ok(f) => f,
298 Err(_) => return vec![],
299 };
300 let mut archive = match zip::ZipArchive::new(file) {
301 Ok(a) => a,
302 Err(_) => return vec![],
303 };
304 let mut all_xml = String::new();
305 let names: Vec<String> = (0..archive.len())
307 .filter_map(|i| archive.by_index(i).ok().map(|e| e.name().to_string()))
308 .filter(|n| n.ends_with(".xml"))
309 .collect();
310 for name in &names {
311 if let Ok(mut entry) = archive.by_name(name) {
312 let mut s = String::new();
313 if entry.read_to_string(&mut s).is_ok() {
314 all_xml.push_str(&s);
315 all_xml.push('\n');
316 }
317 }
318 }
319 if all_xml.is_empty() {
320 return vec![];
321 }
322 extract_plugins_from_xml(&all_xml, &[(&XML_PLUG_NAME_RE, "", "VST")])
326}
327
328fn parse_dawproject(path: &Path) -> Vec<PluginRef> {
330 let file = match fs::File::open(path) {
331 Ok(f) => f,
332 Err(_) => return vec![],
333 };
334 let mut archive = match zip::ZipArchive::new(file) {
335 Ok(a) => a,
336 Err(_) => return vec![],
337 };
338 let xml = match archive.by_name("project.xml") {
339 Ok(mut entry) => {
340 let mut s = String::new();
341 entry.read_to_string(&mut s).ok();
342 s
343 }
344 Err(_) => return vec![],
345 };
346 extract_plugins_from_xml(
347 &xml,
348 &[
349 (&XML_PLUGIN_NAME_RE, "", "VST"),
350 (&XML_DEVICE_NAME_RE, "", "VST"),
351 ],
352 )
353}
354
355fn parse_flp(path: &Path) -> Vec<PluginRef> {
358 let data = match fs::read(path) {
359 Ok(d) => d,
360 Err(_) => return vec![],
361 };
362 let mut plugins = extract_plugins_from_binary(&data);
363 plugins.extend(extract_plugins_utf16le(&data));
365 plugins
366}
367
368fn extract_plugins_utf16le(data: &[u8]) -> Vec<PluginRef> {
371 let mut plugins = Vec::new();
372 if data.len() < 2 {
373 return plugins;
374 }
375 let mut start = 0;
377 while start + 1 < data.len() {
378 let lo = data[start];
379 let hi = data[start + 1];
380 if hi == 0 && (0x20..=0x7E).contains(&lo) {
382 let run_start = start;
383 let mut end = start;
384 while end + 1 < data.len() && data[end + 1] == 0 && (0x20..=0x7E).contains(&data[end]) {
385 end += 2;
386 }
387 let char_count = (end - run_start) / 2;
388 if char_count >= 6 {
389 let u16s: Vec<u16> = data[run_start..end]
390 .chunks(2)
391 .map(|c| u16::from_le_bytes([c[0], c.get(1).copied().unwrap_or(0)]))
392 .collect();
393 let s = String::from_utf16_lossy(&u16s);
394 if let Some(p) = extract_plugin_from_string(&s) {
395 plugins.push(p);
396 }
397 }
398 start = end;
399 } else {
400 start += 1;
401 }
402 }
403 plugins
404}
405
406fn parse_logic(path: &Path) -> Vec<PluginRef> {
408 let candidates = [
409 path.join("Alternatives/000/ProjectData"),
410 path.join("ProjectData"),
411 ];
412 let mut all_plugins = Vec::new();
413
414 for plist_path in &candidates {
415 if let Ok(data) = fs::read(plist_path) {
416 if let Ok(val) = plist::from_bytes::<plist::Value>(&data) {
418 extract_plugins_from_plist(&val, &mut all_plugins);
419 }
420 all_plugins.extend(extract_plugins_from_binary(&data));
422 all_plugins.extend(extract_au_identifiers(&data));
424 all_plugins.extend(extract_logic_plugin_names(&data));
426 }
427 }
428
429 if all_plugins.is_empty() {
430 all_plugins = extract_plugins_from_dir(path);
431 }
432
433 all_plugins
434}
435
436fn extract_logic_plugin_names(data: &[u8]) -> Vec<PluginRef> {
439 let mut plugins = Vec::new();
440 let stock_effects = [
442 "Channel EQ",
443 "Compressor",
444 "Adaptive Limiter",
445 "Multipressor",
446 "Space Designer",
447 "Tape Delay",
448 "Stereo Delay",
449 "ChromaVerb",
450 "Exciter",
451 "Overdrive",
452 "AutoFilter",
453 "Direction Mixer",
454 "Gain",
455 "Stereo Spread",
456 "Limiter",
457 "Noise Gate",
458 "DeEsser",
459 "Tremolo",
460 "Phaser",
461 "Flanger",
462 "Chorus",
463 "Ringshifter",
464 "Pitch Correction",
465 "Pitch Shifter",
466 "Vocal Transformer",
467 ];
468 let mut current = Vec::new();
470 let mut found_names = std::collections::HashSet::new();
471 for &byte in data {
472 if (0x20..=0x7E).contains(&byte) {
473 current.push(byte);
474 } else {
475 if current.len() >= 3 && current.len() <= 64 {
476 let s = String::from_utf8_lossy(¤t).to_string();
477 if !s.contains('/')
479 && !s.contains('\\')
480 && !s.starts_with("com.")
481 && !s.starts_with("kD")
482 && !s.starts_with("0x")
483 && !s.starts_with("Aco")
484 && !s.starts_with("Output ")
485 && !s.starts_with("Input ")
486 && !s.starts_with("Automatic-")
487 && !s.contains("KeyLab")
488 && !s.ends_with(".pst")
489 && !s.ends_with(".aif")
490 && !s.ends_with(".wav")
491 && !s.ends_with(".cst")
492 && !s.ends_with(".exs")
493 && !found_names.contains(&s)
494 {
495 let is_stock = stock_effects.contains(&s.as_str());
496 let known_third_party = [
497 "Sylenth1",
498 "Spire",
499 "Serum",
500 "Massive",
501 "Kontakt",
502 "Omnisphere",
503 "Nexus",
504 "Diva",
505 "Hive",
506 "Vital",
507 "Phase Plant",
508 "Pro-Q",
509 "Pro-L",
510 "Pro-R",
511 "Pro-C",
512 "Pro-G",
513 "Pro-MB",
514 "Ozone",
515 "Neutron",
516 "Trash",
517 "VocalSynth",
518 "Iris",
519 "Valhalla",
520 "FabFilter",
521 "iZotope",
522 "Waves",
523 "Soundtoys",
524 "LFOTool",
525 "CamelCrusher",
526 "OTT",
527 "Sausage Fattener",
528 "Saturn",
529 "Volcano",
530 "Timeless",
531 "Decapitator",
532 "EchoBoy",
533 "Radiator",
534 "Devil-Loc",
535 "PanMan",
536 "FilterFreak",
537 "PhaseMistress",
538 "RC-20",
539 "Kickstart",
540 "Cableguys",
541 "Portal",
542 "Output",
543 "Arturia",
544 "u-he",
545 "Xfer",
546 "Native Instruments",
547 "Spectrasonics",
548 "Alchemy",
549 "ES2",
550 "EXS24",
551 "Retro Synth",
552 "Drum Kit Designer",
553 ];
554 let is_known = known_third_party
555 .iter()
556 .any(|&kp| s.starts_with(kp) || s == kp);
557
558 if is_stock || is_known {
559 let s = s
561 .trim_end_matches(|c: char| {
562 !c.is_alphanumeric() && c != ')' && c != ']'
563 })
564 .to_string();
565 if s.len() < 2 {
566 current.clear();
567 continue;
568 }
569 found_names.insert(s.clone());
570 let normalized = normalize_plugin_name(&s);
571 if !normalized.is_empty() {
572 plugins.push(PluginRef {
573 name: s,
574 normalized_name: normalized,
575 manufacturer: String::new(),
576 plugin_type: if is_stock {
577 "AU (Stock)".into()
578 } else {
579 "AU".into()
580 },
581 });
582 }
583 }
584 }
585 }
586 current.clear();
587 }
588 }
589 plugins
590}
591
592fn extract_au_identifiers(data: &[u8]) -> Vec<PluginRef> {
595 let mut plugins = Vec::new();
596 let mut current = Vec::new();
597 for &byte in data {
599 if (0x20..=0x7E).contains(&byte) {
600 current.push(byte);
601 } else {
602 if current.len() >= 4 {
603 let s = String::from_utf8_lossy(¤t).to_string();
604 if !s.contains('/')
607 && !s.contains('\\')
608 && !s.contains("com.apple")
609 && s.len() >= 4
610 && s.len() <= 64
611 && (s.ends_with(".component")
612 || s.contains("AUPlugin")
613 || s.contains("AudioUnit"))
614 {
615 let name = s.trim_end_matches(".component").trim();
616 if name.len() >= 3 {
617 let normalized = normalize_plugin_name(name);
618 if !normalized.is_empty() {
619 plugins.push(PluginRef {
620 name: name.to_string(),
621 normalized_name: normalized,
622 manufacturer: String::new(),
623 plugin_type: "AU".into(),
624 });
625 }
626 }
627 }
628 }
629 current.clear();
630 }
631 }
632 plugins
633}
634
635fn parse_cubase(path: &Path) -> Vec<PluginRef> {
637 let data = match fs::read(path) {
638 Ok(d) => d,
639 Err(_) => return vec![],
640 };
641 let mut plugins = extract_plugins_from_binary(&data);
642 plugins.extend(extract_named_plugins(&data, b"Plugin Name"));
644 plugins
645}
646
647fn parse_protools(path: &Path) -> Vec<PluginRef> {
652 let data = match fs::read(path) {
653 Ok(d) => d,
654 Err(_) => return vec![],
655 };
656 let mut plugins = extract_plugins_from_binary(&data);
657 plugins.extend(extract_named_plugins(&data, b"PlugIn Name"));
659 plugins.extend(extract_named_plugins(&data, b"Insert Name"));
660 plugins
661}
662
663fn parse_reason(path: &Path) -> Vec<PluginRef> {
665 let data = match fs::read(path) {
666 Ok(d) => d,
667 Err(_) => return vec![],
668 };
669 extract_plugins_from_binary(&data)
670}
671
672fn extract_plugins_from_xml(xml: &str, patterns: &[(&Regex, &str, &str)]) -> Vec<PluginRef> {
676 let mut plugins = Vec::new();
677 for &(re, manufacturer_default, type_default) in patterns {
678 for cap in re.captures_iter(xml) {
679 if let Some(name) = cap.get(1) {
680 let n = name.as_str().trim();
681 if n.is_empty() || n.len() < 2 {
682 continue;
683 }
684 let normalized = normalize_plugin_name(n);
685 if normalized.is_empty() {
686 continue;
687 }
688 plugins.push(PluginRef {
689 name: n.to_string(),
690 normalized_name: normalized,
691 manufacturer: manufacturer_default.to_string(),
692 plugin_type: type_default.to_string(),
693 });
694 }
695 }
696 }
697 plugins
698}
699
700fn extract_plugins_from_binary(data: &[u8]) -> Vec<PluginRef> {
703 let mut plugins = Vec::new();
704 let mut current = Vec::new();
705 for &byte in data {
706 if (0x20..=0x7E).contains(&byte) {
707 current.push(byte);
708 } else {
709 if current.len() >= 6 {
710 let s = String::from_utf8_lossy(¤t).to_string();
711 if let Some(p) = extract_plugin_from_string(&s) {
712 plugins.push(p);
713 }
714 }
715 current.clear();
716 }
717 }
718 if current.len() >= 6 {
719 let s = String::from_utf8_lossy(¤t).to_string();
720 if let Some(p) = extract_plugin_from_string(&s) {
721 plugins.push(p);
722 }
723 }
724 plugins
725}
726
727fn extract_plugins_from_dir(dir: &Path) -> Vec<PluginRef> {
729 let mut plugins = Vec::new();
730 if let Ok(entries) = fs::read_dir(dir) {
731 for entry in entries.flatten() {
732 let p = entry.path();
733 if p.is_file() {
734 if let Ok(data) = fs::read(&p) {
735 plugins.extend(extract_plugins_from_binary(&data));
736 }
737 } else if p.is_dir() && plugins.len() < 500 {
738 plugins.extend(extract_plugins_from_dir(&p));
739 }
740 }
741 }
742 plugins
743}
744
745fn extract_plugins_from_plist(val: &plist::Value, plugins: &mut Vec<PluginRef>) {
747 match val {
748 plist::Value::Dictionary(dict) => {
749 for key in ["pluginName", "PluginName", "name", "Name", "plugName"] {
751 if let Some(plist::Value::String(name)) = dict.get(key) {
752 let n = name.trim();
753 if n.len() >= 2 {
754 let normalized = normalize_plugin_name(n);
755 if !normalized.is_empty() {
756 plugins.push(PluginRef {
757 name: n.to_string(),
758 normalized_name: normalized,
759 manufacturer: dict
760 .get("manufacturer")
761 .and_then(|v| v.as_string())
762 .unwrap_or("")
763 .to_string(),
764 plugin_type: dict
765 .get("pluginType")
766 .and_then(|v| v.as_string())
767 .map(|s| s.to_string())
768 .unwrap_or_else(|| "AU".into()),
769 });
770 }
771 }
772 }
773 }
774 for (_, v) in dict.iter() {
775 extract_plugins_from_plist(v, plugins);
776 }
777 }
778 plist::Value::Array(arr) => {
779 for v in arr {
780 extract_plugins_from_plist(v, plugins);
781 }
782 }
783 _ => {}
784 }
785}
786
787fn extract_plugin_from_string(s: &str) -> Option<PluginRef> {
790 let exts = [
791 (".dll", "VST2"),
792 (".vst3", "VST3"),
793 (".component", "AU"),
794 (".clap", "CLAP"),
795 (".aaxplugin", "AAX"),
796 ];
797 for (ext, ptype) in &exts {
798 if let Some(pos) = s.find(ext) {
800 let path_part = &s[..pos + ext.len()];
802 let name = path_part
803 .rsplit(['\\', '/'])
804 .next()?
805 .trim_end_matches(ext)
806 .trim();
807 if name.is_empty() || name.len() < 2 {
808 continue;
809 }
810 if name.contains("VstPlugins")
811 || name.contains("Program Files")
812 || name.contains("CommonFiles")
813 {
814 continue;
815 }
816 let normalized = normalize_plugin_name(name);
817 if normalized.is_empty() {
818 continue;
819 }
820 return Some(PluginRef {
821 name: name.to_string(),
822 normalized_name: normalized,
823 manufacturer: String::new(),
824 plugin_type: ptype.to_string(),
825 });
826 }
827 }
828 None
829}
830
831fn extract_named_plugins(data: &[u8], marker: &[u8]) -> Vec<PluginRef> {
834 let mut plugins = Vec::new();
835 let builtin = [
836 "Standard Panner",
837 "Stereo Combined Panner",
838 "Mono",
839 "Stereo",
840 "No Bus",
841 ];
842 let mut pos = 0;
843 while pos + marker.len() < data.len() {
844 if let Some(idx) = data[pos..].windows(marker.len()).position(|w| w == marker) {
845 let after = pos + idx + marker.len();
846 let mut start = after;
848 while start < data.len() && (data[start] < 0x20 || data[start] > 0x7E) {
849 start += 1;
850 }
851 if start < data.len() {
852 let mut end = start;
853 while end < data.len() && data[end] >= 0x20 && data[end] <= 0x7E {
854 end += 1;
855 }
856 if end - start >= 3 && end - start <= 100 {
857 let name = String::from_utf8_lossy(&data[start..end]).to_string();
858 if !builtin.contains(&name.as_str())
859 && !name.starts_with("VST")
860 && !name.contains("Plugin")
861 {
862 let normalized = normalize_plugin_name(&name);
863 if !normalized.is_empty() {
864 plugins.push(PluginRef {
865 name: name.clone(),
866 normalized_name: normalized,
867 manufacturer: String::new(),
868 plugin_type: "VST".into(),
869 });
870 }
871 }
872 }
873 }
874 pos = after + 1;
875 } else {
876 break;
877 }
878 }
879 plugins
880}
881
882fn parse_bitwig(path: &Path) -> Vec<PluginRef> {
884 let data = match fs::read(path) {
885 Ok(d) => d,
886 Err(_) => return vec![],
887 };
888 extract_plugins_from_binary(&data)
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use flate2::write::GzEncoder;
895 use flate2::Compression;
896 use std::io::Write;
897
898 #[test]
899 fn test_normalize_plugin_name_whitespace_only() {
900 assert_eq!(normalize_plugin_name(" "), "");
901 }
902
903 #[test]
904 fn test_plugin_ref_json_roundtrip() {
905 let p = PluginRef {
906 name: "Serum".into(),
907 normalized_name: "serum".into(),
908 manufacturer: "Xfer Records".into(),
909 plugin_type: "VST3".into(),
910 };
911 let json = serde_json::to_string(&p).unwrap();
912 let back: PluginRef = serde_json::from_str(&json).unwrap();
913 assert_eq!(p, back);
914 }
915
916 #[test]
917 fn test_extract_empty_for_unsupported() {
918 let result = extract_plugins("/some/file.flp");
919 assert!(result.is_empty());
920 }
921
922 #[test]
923 fn test_extract_nonexistent_file() {
924 let result = extract_plugins("/nonexistent/project.als");
925 assert!(result.is_empty());
926 }
927
928 #[test]
929 fn test_parse_ableton_vst2() {
930 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
931<Ableton>
932 <LiveSet>
933 <Tracks>
934 <MidiTrack>
935 <DeviceChain>
936 <Devices>
937 <PluginDevice>
938 <PluginDesc>
939 <VstPluginInfo>
940 <PlugName Value="Serum" />
941 <Manufacturer Value="Xfer Records" />
942 </VstPluginInfo>
943 </PluginDesc>
944 </PluginDevice>
945 </Devices>
946 </DeviceChain>
947 </MidiTrack>
948 </Tracks>
949 </LiveSet>
950</Ableton>"#;
951
952 let tmp = std::env::temp_dir().join("test_xref_als_vst2.als");
953 let f = fs::File::create(&tmp).unwrap();
954 let mut enc = GzEncoder::new(f, Compression::default());
955 enc.write_all(xml.as_bytes()).unwrap();
956 enc.finish().unwrap();
957
958 let result = extract_plugins(tmp.to_str().unwrap());
959 assert_eq!(result.len(), 1);
960 assert_eq!(result[0].name, "Serum");
961 assert_eq!(result[0].manufacturer, "Xfer Records");
962 assert_eq!(result[0].plugin_type, "VST2");
963
964 let _ = fs::remove_file(&tmp);
965 }
966
967 #[test]
968 fn test_parse_ableton_vst3() {
969 let xml = r#"<Ableton>
970 <Vst3PluginInfo>
971 <Name Value="Pro-Q 3" />
972 <DeviceCreator Value="FabFilter" />
973 </Vst3PluginInfo>
974</Ableton>"#;
975
976 let tmp = std::env::temp_dir().join("test_xref_als_vst3.als");
977 let f = fs::File::create(&tmp).unwrap();
978 let mut enc = GzEncoder::new(f, Compression::default());
979 enc.write_all(xml.as_bytes()).unwrap();
980 enc.finish().unwrap();
981
982 let result = extract_plugins(tmp.to_str().unwrap());
983 assert_eq!(result.len(), 1);
984 assert_eq!(result[0].name, "Pro-Q 3");
985 assert_eq!(result[0].manufacturer, "FabFilter");
986 assert_eq!(result[0].plugin_type, "VST3");
987
988 let _ = fs::remove_file(&tmp);
989 }
990
991 #[test]
992 fn test_parse_ableton_au() {
993 let xml = r#"<Ableton>
994 <AuPluginInfo>
995 <Name Value="AUReverb2" />
996 <Manufacturer Value="Apple" />
997 </AuPluginInfo>
998</Ableton>"#;
999
1000 let tmp = std::env::temp_dir().join("test_xref_als_au.als");
1001 let f = fs::File::create(&tmp).unwrap();
1002 let mut enc = GzEncoder::new(f, Compression::default());
1003 enc.write_all(xml.as_bytes()).unwrap();
1004 enc.finish().unwrap();
1005
1006 let result = extract_plugins(tmp.to_str().unwrap());
1007 assert_eq!(result.len(), 1);
1008 assert_eq!(result[0].name, "AUReverb2");
1009 assert_eq!(result[0].plugin_type, "AU");
1010
1011 let _ = fs::remove_file(&tmp);
1012 }
1013
1014 #[test]
1015 fn test_parse_ableton_multiple_deduped() {
1016 let xml = r#"<Ableton>
1017 <VstPluginInfo><PlugName Value="Serum" /><Manufacturer Value="Xfer" /></VstPluginInfo>
1018 <VstPluginInfo><PlugName Value="Serum" /><Manufacturer Value="Xfer" /></VstPluginInfo>
1019 <Vst3PluginInfo><Name Value="Pro-Q 3" /><DeviceCreator Value="FabFilter" /></Vst3PluginInfo>
1020</Ableton>"#;
1021
1022 let tmp = std::env::temp_dir().join("test_xref_als_multi.als");
1023 let f = fs::File::create(&tmp).unwrap();
1024 let mut enc = GzEncoder::new(f, Compression::default());
1025 enc.write_all(xml.as_bytes()).unwrap();
1026 enc.finish().unwrap();
1027
1028 let result = extract_plugins(tmp.to_str().unwrap());
1029 assert_eq!(result.len(), 2); assert!(result.iter().any(|p| p.name == "Serum"));
1031 assert!(result.iter().any(|p| p.name == "Pro-Q 3"));
1032
1033 let _ = fs::remove_file(&tmp);
1034 }
1035
1036 #[test]
1037 fn test_parse_reaper_vst2() {
1038 let rpp = r#"<REAPER_PROJECT 0.1 "7.0"
1039 <TRACK
1040 <FXCHAIN
1041 <VST "VST: Serum (Xfer Records)" Serum_x64.dll 0 "" 1397572658
1042 >
1043 >
1044 >
1045>"#;
1046 let tmp = std::env::temp_dir().join("test_xref_rpp_vst2.rpp");
1047 fs::write(&tmp, rpp).unwrap();
1048
1049 let result = extract_plugins(tmp.to_str().unwrap());
1050 assert_eq!(result.len(), 1);
1051 assert_eq!(result[0].name, "Serum");
1052 assert_eq!(result[0].manufacturer, "Xfer Records");
1053 assert_eq!(result[0].plugin_type, "VST2");
1054
1055 let _ = fs::remove_file(&tmp);
1056 }
1057
1058 #[test]
1059 fn test_parse_reaper_vst3() {
1060 let rpp = r#"<REAPER_PROJECT
1061 <TRACK
1062 <FXCHAIN
1063 <VST "VST3: Pro-Q 3 (FabFilter)" "{ABCDEF}" 0
1064 >
1065 >
1066 >
1067>"#;
1068 let tmp = std::env::temp_dir().join("test_xref_rpp_vst3.rpp");
1069 fs::write(&tmp, rpp).unwrap();
1070
1071 let result = extract_plugins(tmp.to_str().unwrap());
1072 assert_eq!(result.len(), 1);
1073 assert_eq!(result[0].name, "Pro-Q 3");
1074 assert_eq!(result[0].manufacturer, "FabFilter");
1075 assert_eq!(result[0].plugin_type, "VST3");
1076
1077 let _ = fs::remove_file(&tmp);
1078 }
1079
1080 #[test]
1081 fn test_parse_reaper_mixed() {
1082 let rpp = r#"<REAPER_PROJECT
1083 <TRACK
1084 <FXCHAIN
1085 <VST "VST: Serum (Xfer Records)" Serum.dll 0 "" 123
1086 >
1087 <VST "VST3: Ozone 11 (iZotope, Inc.)" Ozone.vst3 0 "" 456
1088 >
1089 <AU "AU: AUHighShelfFilter (Apple)" "" 0 "" 789
1090 >
1091 >
1092 >
1093>"#;
1094 let tmp = std::env::temp_dir().join("test_xref_rpp_mixed.rpp");
1095 fs::write(&tmp, rpp).unwrap();
1096
1097 let result = extract_plugins(tmp.to_str().unwrap());
1098 assert_eq!(result.len(), 3);
1099 assert!(result
1100 .iter()
1101 .any(|p| p.name == "Serum" && p.plugin_type == "VST2"));
1102 assert!(result
1103 .iter()
1104 .any(|p| p.name == "Ozone 11" && p.plugin_type == "VST3"));
1105 assert!(result
1106 .iter()
1107 .any(|p| p.name == "AUHighShelfFilter" && p.plugin_type == "AU"));
1108
1109 let _ = fs::remove_file(&tmp);
1110 }
1111
1112 #[test]
1113 fn test_parse_reaper_no_manufacturer() {
1114 let rpp = r#"<REAPER_PROJECT
1115 <TRACK
1116 <FXCHAIN
1117 <VST "VST: ReaComp" reacomp.dll 0
1118 >
1119 >
1120 >
1121>"#;
1122 let tmp = std::env::temp_dir().join("test_xref_rpp_nomfg.rpp");
1123 fs::write(&tmp, rpp).unwrap();
1124
1125 let result = extract_plugins(tmp.to_str().unwrap());
1126 assert_eq!(result.len(), 1);
1127 assert_eq!(result[0].name, "ReaComp");
1128 assert_eq!(result[0].manufacturer, "");
1129
1130 let _ = fs::remove_file(&tmp);
1131 }
1132
1133 #[test]
1134 fn test_parse_reaper_deduplicates() {
1135 let rpp = r#"<REAPER_PROJECT
1136 <TRACK
1137 <FXCHAIN
1138 <VST "VST: Serum (Xfer Records)" Serum.dll 0 "" 123
1139 >
1140 >
1141 >
1142 <TRACK
1143 <FXCHAIN
1144 <VST "VST: Serum (Xfer Records)" Serum.dll 0 "" 123
1145 >
1146 >
1147 >
1148>"#;
1149 let tmp = std::env::temp_dir().join("test_xref_rpp_dedup.rpp");
1150 fs::write(&tmp, rpp).unwrap();
1151
1152 let result = extract_plugins(tmp.to_str().unwrap());
1153 assert_eq!(result.len(), 1);
1154
1155 let _ = fs::remove_file(&tmp);
1156 }
1157
1158 #[test]
1159 fn test_parse_reaper_empty_fx_chain() {
1160 let rpp = r#"<REAPER_PROJECT
1161 <TRACK
1162 <FXCHAIN
1163 >
1164 >
1165>"#;
1166 let tmp = std::env::temp_dir().join("test_xref_rpp_empty.rpp");
1167 fs::write(&tmp, rpp).unwrap();
1168
1169 let result = extract_plugins(tmp.to_str().unwrap());
1170 assert!(result.is_empty());
1171
1172 let _ = fs::remove_file(&tmp);
1173 }
1174
1175 #[test]
1176 fn test_parse_ableton_not_gzip() {
1177 let tmp = std::env::temp_dir().join("test_xref_als_bad.als");
1178 fs::write(&tmp, b"not gzip data").unwrap();
1179
1180 let result = extract_plugins(tmp.to_str().unwrap());
1181 assert!(result.is_empty());
1182
1183 let _ = fs::remove_file(&tmp);
1184 }
1185
1186 #[test]
1187 fn test_parse_ableton_empty_xml() {
1188 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1190<Ableton>
1191 <LiveSet>
1192 <Tracks>
1193 <AudioTrack>
1194 <DeviceChain>
1195 <Devices />
1196 </DeviceChain>
1197 </AudioTrack>
1198 </Tracks>
1199 </LiveSet>
1200</Ableton>"#;
1201
1202 let tmp = std::env::temp_dir().join("test_xref_als_empty_xml.als");
1203 let f = fs::File::create(&tmp).unwrap();
1204 let mut enc = GzEncoder::new(f, Compression::default());
1205 enc.write_all(xml.as_bytes()).unwrap();
1206 enc.finish().unwrap();
1207
1208 let result = extract_plugins(tmp.to_str().unwrap());
1209 assert!(
1210 result.is_empty(),
1211 "No plugin blocks should yield empty result"
1212 );
1213
1214 let _ = fs::remove_file(&tmp);
1215 }
1216
1217 #[test]
1218 fn test_parse_reaper_clap() {
1219 let rpp = r#"<REAPER_PROJECT
1220 <TRACK
1221 <FXCHAIN
1222 <CLAP "CLAP: Surge XT (Surge Synth Team)" com.surge-synth-team.surge-xt 0
1223 >
1224 >
1225 >
1226>"#;
1227 let tmp = std::env::temp_dir().join("test_xref_rpp_clap.rpp");
1228 fs::write(&tmp, rpp).unwrap();
1229
1230 let result = extract_plugins(tmp.to_str().unwrap());
1231 assert_eq!(result.len(), 1);
1232 assert_eq!(result[0].name, "Surge XT");
1233 assert_eq!(result[0].manufacturer, "Surge Synth Team");
1234 assert_eq!(result[0].plugin_type, "CLAP");
1235
1236 let _ = fs::remove_file(&tmp);
1237 }
1238
1239 #[test]
1240 fn test_extract_rpp_bak_extension() {
1241 let rpp = r#"<REAPER_PROJECT
1242 <TRACK
1243 <FXCHAIN
1244 <VST "VST: Vital (Matt Tytel)" Vital.dll 0 "" 999
1245 >
1246 >
1247 >
1248>"#;
1249 let tmp = std::env::temp_dir().join("test_xref.rpp-bak");
1250 fs::write(&tmp, rpp).unwrap();
1251
1252 let result = extract_plugins(tmp.to_str().unwrap());
1253 assert_eq!(result.len(), 1, ".rpp-bak should be treated as REAPER");
1254 assert_eq!(result[0].name, "Vital");
1255 assert_eq!(result[0].plugin_type, "VST2");
1256
1257 let _ = fs::remove_file(&tmp);
1258 }
1259
1260 #[test]
1261 fn test_normalize_plugin_name_basic() {
1262 assert_eq!(normalize_plugin_name("Serum"), "serum");
1263 assert_eq!(normalize_plugin_name("Pro-Q 3"), "pro-q 3");
1264 assert_eq!(normalize_plugin_name(" Diva "), "diva");
1265 }
1266
1267 #[test]
1268 fn test_normalize_plugin_name_mixed_case_collapses_to_lowercase() {
1269 assert_eq!(
1270 normalize_plugin_name("FabFilter Pro-Q 3"),
1271 "fabfilter pro-q 3"
1272 );
1273 }
1274
1275 #[test]
1276 fn test_normalize_strips_arch_suffixes() {
1277 assert_eq!(normalize_plugin_name("Serum (x64)"), "serum");
1278 assert_eq!(normalize_plugin_name("Kontakt (x86_64)"), "kontakt");
1279 assert_eq!(normalize_plugin_name("Massive (64-bit)"), "massive");
1280 assert_eq!(normalize_plugin_name("Sylenth1 (32-bit)"), "sylenth1");
1281 assert_eq!(normalize_plugin_name("Reaktor (ARM64)"), "reaktor");
1282 assert_eq!(
1283 normalize_plugin_name("Omnisphere (Universal)"),
1284 "omnisphere"
1285 );
1286 assert_eq!(normalize_plugin_name("Pigments [x64]"), "pigments");
1287 assert_eq!(normalize_plugin_name("Vital (Stereo)"), "vital");
1288 }
1289
1290 #[test]
1291 fn test_normalize_strips_bare_arch_suffix() {
1292 assert_eq!(normalize_plugin_name("Serum x64"), "serum");
1293 assert_eq!(normalize_plugin_name("Kontakt x86_64"), "kontakt");
1294 assert_eq!(normalize_plugin_name("Massive x86"), "massive");
1295 }
1296
1297 #[test]
1298 fn test_normalize_strips_multiple_suffixes() {
1299 assert_eq!(normalize_plugin_name("Serum (x64) (VST3)"), "serum");
1300 assert_eq!(normalize_plugin_name("Kontakt (Stereo) (x64)"), "kontakt");
1301 }
1302
1303 #[test]
1304 fn test_normalize_preserves_inner_parens() {
1305 assert_eq!(normalize_plugin_name("EQ (3-band)"), "eq (3-band)");
1306 assert_eq!(
1307 normalize_plugin_name("Compressor (Legacy)"),
1308 "compressor (legacy)"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_normalize_collapses_whitespace() {
1314 assert_eq!(normalize_plugin_name("Pro Q 3"), "pro q 3");
1315 }
1316
1317 #[test]
1318 fn test_normalize_all_suffix_fallback() {
1319 assert_eq!(normalize_plugin_name("(x64)"), "(x64)");
1321 assert_eq!(normalize_plugin_name("(x64) (VST3)"), "(x64) (vst3)");
1322 assert_eq!(normalize_plugin_name(""), "");
1324 assert_eq!(normalize_plugin_name(" "), "");
1325 }
1326
1327 #[test]
1328 fn test_normalize_strips_au_vst_aax_brackets() {
1329 assert_eq!(normalize_plugin_name("Pro-Q 3 (AU)"), "pro-q 3");
1330 assert_eq!(normalize_plugin_name("Serum (VST3)"), "serum");
1331 assert_eq!(normalize_plugin_name("Tune (AAX)"), "tune");
1332 }
1333
1334 #[test]
1335 fn test_normalize_intel_bracket() {
1336 assert_eq!(normalize_plugin_name("Legacy (Intel)"), "legacy");
1337 }
1338
1339 #[test]
1340 fn test_normalize_strips_apple_silicon_bracket() {
1341 assert_eq!(
1342 normalize_plugin_name("Melodyne (Apple Silicon)"),
1343 "melodyne"
1344 );
1345 }
1346
1347 #[test]
1348 fn test_normalize_equivalent_after_strip() {
1349 assert_eq!(
1350 normalize_plugin_name("Massive (x64)"),
1351 normalize_plugin_name("Massive x64")
1352 );
1353 }
1354
1355 #[test]
1356 fn test_extract_plugin_from_string_embedded_windows_path() {
1357 let s = r"C:\VSTPlugins\Xfer\Serum.vst3";
1358 let p = extract_plugin_from_string(s).expect("vst3 in path");
1359 assert_eq!(p.name, "Serum");
1360 assert_eq!(p.plugin_type, "VST3");
1361 assert_eq!(p.normalized_name, "serum");
1362 }
1363
1364 #[test]
1365 fn test_extract_plugin_from_string_skips_vstplugins_substring_in_stem() {
1366 let s = r"C:\Plugins\SerumVstPluginsBundle.vst3";
1368 assert!(extract_plugin_from_string(s).is_none());
1369 }
1370
1371 #[test]
1372 fn test_extract_plugin_from_string_short_stem_skipped() {
1373 let s = r"C:\p\X.vst3";
1374 assert!(extract_plugin_from_string(s).is_none());
1375 }
1376
1377 #[test]
1378 fn test_extract_plugin_from_string_clap_suffix() {
1379 let s = "/lib/CLAP/Pigments.clap";
1380 let p = extract_plugin_from_string(s).unwrap();
1381 assert_eq!(p.name, "Pigments");
1382 assert_eq!(p.plugin_type, "CLAP");
1383 }
1384
1385 #[test]
1386 fn test_extract_au_identifiers_component_bundle_name() {
1387 let mut data = b"MyDelay.component".to_vec();
1388 data.push(0);
1389 let refs = extract_au_identifiers(&data);
1390 assert_eq!(refs.len(), 1);
1391 assert_eq!(refs[0].name, "MyDelay");
1392 assert_eq!(refs[0].plugin_type, "AU");
1393 }
1394
1395 #[test]
1396 fn test_extract_plugins_from_xml_regex_capture() {
1397 let xml = r#"<Plugin name="Serum" />"#;
1399 let re = Regex::new(r#"name="([^"]+)""#).unwrap();
1400 let patterns: &[(&Regex, &str, &str)] = &[(&re, "Xfer Records", "VST3")];
1401 let refs = extract_plugins_from_xml(xml, patterns);
1402 assert_eq!(refs.len(), 1);
1403 assert_eq!(refs[0].name, "Serum");
1404 assert_eq!(refs[0].manufacturer, "Xfer Records");
1405 assert_eq!(refs[0].plugin_type, "VST3");
1406 }
1407
1408 #[test]
1409 fn test_extract_plugins_from_xml_skips_single_char_name() {
1410 let xml = r#"<x name="X" />"#;
1411 let re = Regex::new(r#"name="([^"]+)""#).unwrap();
1412 let patterns: &[(&Regex, &str, &str)] = &[(&re, "Co", "VST3")];
1413 assert!(
1414 extract_plugins_from_xml(xml, patterns).is_empty(),
1415 "stem length < 2 must be skipped"
1416 );
1417 }
1418
1419 #[test]
1420 fn test_dedup_case_insensitive() {
1421 let rpp = r#"<REAPER_PROJECT
1422 <TRACK
1423 <FXCHAIN
1424 <VST "VST: Serum (Xfer Records)" Serum.dll 0 "" 123
1425 >
1426 <VST "VST: SERUM (Xfer Records)" Serum.dll 0 "" 456
1427 >
1428 <VST "VST: serum (Xfer)" Serum.dll 0 "" 789
1429 >
1430 >
1431 >
1432>"#;
1433 let tmp = std::env::temp_dir().join("test_xref_rpp_case_dedup.rpp");
1434 fs::write(&tmp, rpp).unwrap();
1435
1436 let result = extract_plugins(tmp.to_str().unwrap());
1437 assert_eq!(result.len(), 1, "case variants should dedup to one");
1438 assert_eq!(result[0].normalized_name, "serum");
1439
1440 let _ = fs::remove_file(&tmp);
1441 }
1442
1443 #[test]
1444 fn test_dedup_arch_suffix_variants() {
1445 let rpp = r#"<REAPER_PROJECT
1446 <TRACK
1447 <FXCHAIN
1448 <VST "VST: Serum (Xfer)" Serum.dll 0 "" 1
1449 >
1450 <VST "VST: Serum x64 (Xfer)" Serum_x64.dll 0 "" 2
1451 >
1452 >
1453 >
1454>"#;
1455 let tmp = std::env::temp_dir().join("test_xref_rpp_arch_dedup.rpp");
1456 fs::write(&tmp, rpp).unwrap();
1457
1458 let result = extract_plugins(tmp.to_str().unwrap());
1459 assert_eq!(result.len(), 1, "arch suffix variants should dedup");
1460 assert_eq!(result[0].normalized_name, "serum");
1461
1462 let _ = fs::remove_file(&tmp);
1463 }
1464
1465 #[test]
1466 fn test_results_sorted_by_name() {
1467 let rpp = r#"<REAPER_PROJECT
1468 <TRACK
1469 <FXCHAIN
1470 <VST "VST: Zebra2 (u-he)" z.dll 0 "" 1
1471 >
1472 <VST "VST: Diva (u-he)" d.dll 0 "" 2
1473 >
1474 <VST "VST: Ace (u-he)" a.dll 0 "" 3
1475 >
1476 >
1477 >
1478>"#;
1479 let tmp = std::env::temp_dir().join("test_xref_rpp_sorted.rpp");
1480 fs::write(&tmp, rpp).unwrap();
1481
1482 let result = extract_plugins(tmp.to_str().unwrap());
1483 assert_eq!(result.len(), 3);
1484 assert_eq!(result[0].name, "Ace");
1485 assert_eq!(result[1].name, "Diva");
1486 assert_eq!(result[2].name, "Zebra2");
1487
1488 let _ = fs::remove_file(&tmp);
1489 }
1490
1491 #[test]
1492 fn test_extract_bitwig_plugin_dll() {
1493 let p = extract_plugin_from_string(r"C:\Program Files\Steinberg\VstPlugins\Serum.dll");
1494 assert!(p.is_some());
1495 let p = p.unwrap();
1496 assert_eq!(p.name, "Serum");
1497 assert_eq!(p.plugin_type, "VST2");
1498 }
1499
1500 #[test]
1501 fn test_extract_bitwig_plugin_vst3() {
1502 let p = extract_plugin_from_string("/Library/Audio/Plug-Ins/VST3/FabFilter Pro-Q 3.vst3");
1503 assert!(p.is_some());
1504 let p = p.unwrap();
1505 assert_eq!(p.name, "FabFilter Pro-Q 3");
1506 assert_eq!(p.plugin_type, "VST3");
1507 }
1508
1509 #[test]
1510 fn test_extract_bitwig_plugin_au() {
1511 let p = extract_plugin_from_string("/Library/Audio/Plug-Ins/Components/Massive.component");
1512 assert!(p.is_some());
1513 let p = p.unwrap();
1514 assert_eq!(p.name, "Massive");
1515 assert_eq!(p.plugin_type, "AU");
1516 }
1517
1518 #[test]
1519 fn test_extract_bitwig_plugin_clap() {
1520 let p = extract_plugin_from_string("/Library/Audio/Plug-Ins/CLAP/Vital.clap");
1521 assert!(p.is_some());
1522 let p = p.unwrap();
1523 assert_eq!(p.name, "Vital");
1524 assert_eq!(p.plugin_type, "CLAP");
1525 }
1526
1527 #[test]
1528 fn test_extract_bitwig_plugin_rejects_dir() {
1529 assert!(extract_plugin_from_string(r"C:\Program Files\Steinberg\VstPlugins").is_none());
1531 }
1532
1533 #[test]
1534 fn test_extract_bitwig_plugin_strips_path() {
1535 let p = extract_plugin_from_string(r"MeldaProduction\Modulation\MFlanger.dll");
1536 assert!(p.is_some());
1537 assert_eq!(p.unwrap().name, "MFlanger");
1538 }
1539
1540 #[test]
1541 fn test_parse_bitwig_synthetic() {
1542 let tmp = std::env::temp_dir().join("test_bitwig.bwproject");
1544 let mut data = b"BtWg0003".to_vec();
1545 data.extend_from_slice(&[0u8; 100]); data.extend_from_slice(b"C:\\VstPlugins\\Serum.dll");
1547 data.extend_from_slice(&[0u8; 20]);
1548 data.extend_from_slice(b"/Library/Audio/Plug-Ins/VST3/Pro-Q 3.vst3");
1549 data.extend_from_slice(&[0u8; 20]);
1550 data.extend_from_slice(b"/Library/Audio/Plug-Ins/Components/Kontakt.component");
1551 data.extend_from_slice(&[0u8; 20]);
1552 fs::write(&tmp, &data).unwrap();
1553
1554 let result = extract_plugins(tmp.to_str().unwrap());
1555 assert!(
1556 result.len() >= 3,
1557 "should find at least 3 plugins, got {}",
1558 result.len()
1559 );
1560 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1561 assert!(
1562 names.contains(&"Serum"),
1563 "should find Serum, got {:?}",
1564 names
1565 );
1566 assert!(
1567 names.contains(&"Pro-Q 3"),
1568 "should find Pro-Q 3, got {:?}",
1569 names
1570 );
1571 assert!(
1572 names.contains(&"Kontakt"),
1573 "should find Kontakt, got {:?}",
1574 names
1575 );
1576
1577 let _ = fs::remove_file(&tmp);
1578 }
1579
1580 #[test]
1583 fn test_binary_extraction_all_plugin_types() {
1584 let mut data = vec![0u8; 100];
1585 for (path, expected_type) in [
1586 ("C:\\VSTPlugins\\Massive.dll", "VST2"),
1587 ("/Library/Audio/Plug-Ins/VST3/Serum.vst3", "VST3"),
1588 ("/Library/Audio/Plug-Ins/Components/Kontakt.component", "AU"),
1589 ("/Library/Audio/Plug-Ins/CLAP/Vital.clap", "CLAP"),
1590 ("C:\\AAX\\Pro-Q 3.aaxplugin", "AAX"),
1591 ] {
1592 data.extend_from_slice(path.as_bytes());
1593 data.extend_from_slice(&[0; 50]);
1594 let _ = expected_type;
1595 }
1596 let result = extract_plugins_from_binary(&data);
1597 let types: Vec<&str> = result.iter().map(|p| p.plugin_type.as_str()).collect();
1598 assert!(types.contains(&"VST2"), "missing VST2");
1599 assert!(types.contains(&"VST3"), "missing VST3");
1600 assert!(types.contains(&"AU"), "missing AU");
1601 assert!(types.contains(&"CLAP"), "missing CLAP");
1602 assert!(types.contains(&"AAX"), "missing AAX");
1603 }
1604
1605 #[test]
1606 fn test_extract_plugin_with_trailing_junk() {
1607 let p = extract_plugin_from_string("F:\\VSTPlugins\\Serum_x64.dll8");
1609 assert!(p.is_some(), "should extract despite trailing '8'");
1610 assert_eq!(p.unwrap().name, "Serum_x64");
1611 }
1612
1613 #[test]
1614 fn test_extract_plugin_embedded_in_longer_string() {
1615 let p = extract_plugin_from_string("some_prefix/Sylenth1.dll/some_suffix");
1616 assert!(p.is_some());
1617 assert_eq!(p.unwrap().name, "Sylenth1");
1618 }
1619
1620 #[test]
1623 fn test_flp_ascii_extraction() {
1624 let mut data = vec![0u8; 100];
1625 data.extend_from_slice(b"C:\\Program Files\\VSTPlugins\\Sylenth1.dll");
1626 data.extend_from_slice(&[0; 50]);
1627 data.extend_from_slice(b"C:\\VST3\\FabFilter Pro-Q 3.vst3");
1628 data.extend_from_slice(&[0; 50]);
1629 let result = extract_plugins_from_binary(&data);
1630 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1631 assert!(names.contains(&"Sylenth1"), "missing Sylenth1: {:?}", names);
1632 assert!(
1633 names.contains(&"FabFilter Pro-Q 3"),
1634 "missing Pro-Q 3: {:?}",
1635 names
1636 );
1637 }
1638
1639 #[test]
1640 fn test_flp_utf16le_extraction() {
1641 let mut data = vec![0u8; 50];
1642 for c in "Serum_x64.dll".chars() {
1644 data.push(c as u8);
1645 data.push(0);
1646 }
1647 data.extend_from_slice(&[0; 50]);
1648 let result = extract_plugins_utf16le(&data);
1649 assert!(!result.is_empty(), "UTF-16LE extraction failed");
1650 assert_eq!(result[0].name, "Serum_x64");
1651 }
1652
1653 #[test]
1654 fn test_flp_combined_ascii_and_utf16() {
1655 let tmp = std::env::temp_dir().join("test_xref_flp_combined.flp");
1656 let mut data = vec![0u8; 100];
1657 data.extend_from_slice(b"C:\\Plugins\\OTT.dll");
1658 data.extend_from_slice(&[0; 30]);
1659 for c in "F:\\VSTPlugins\\Massive.dll".chars() {
1660 data.push(c as u8);
1661 data.push(0);
1662 }
1663 data.extend_from_slice(&[0; 50]);
1664 fs::write(&tmp, &data).unwrap();
1665 let result = parse_flp(&tmp);
1666 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1667 assert!(
1668 names.iter().any(|n| n.contains("OTT")),
1669 "missing OTT: {:?}",
1670 names
1671 );
1672 assert!(
1673 names.iter().any(|n| n.contains("Massive")),
1674 "missing Massive: {:?}",
1675 names
1676 );
1677 let _ = fs::remove_file(&tmp);
1678 }
1679
1680 #[test]
1683 fn test_cubase_plugin_name_markers() {
1684 let mut data = vec![0u8; 50];
1685 data.extend_from_slice(b"Plugin Name");
1686 data.push(0);
1687 data.extend_from_slice(b"Spire-1.5");
1688 data.extend_from_slice(&[0; 30]);
1689 data.extend_from_slice(b"Plugin Name");
1690 data.push(0);
1691 data.extend_from_slice(b"LFOTool");
1692 data.extend_from_slice(&[0; 30]);
1693 data.extend_from_slice(b"Plugin Name");
1695 data.push(0);
1696 data.extend_from_slice(b"Standard Panner");
1697 data.extend_from_slice(&[0; 30]);
1698 let result = extract_named_plugins(&data, b"Plugin Name");
1699 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1700 assert!(names.contains(&"Spire-1.5"), "missing Spire: {:?}", names);
1701 assert!(names.contains(&"LFOTool"), "missing LFOTool: {:?}", names);
1702 assert!(
1703 !names.contains(&"Standard Panner"),
1704 "should filter Standard Panner"
1705 );
1706 }
1707
1708 #[test]
1709 fn test_cubase_binary_paths_plus_markers() {
1710 let mut data = vec![0u8; 50];
1711 data.extend_from_slice(b"C:\\VST3\\Serum.vst3");
1712 data.extend_from_slice(&[0; 30]);
1713 data.extend_from_slice(b"Plugin Name");
1714 data.push(0);
1715 data.extend_from_slice(b"LFOTool");
1716 data.extend_from_slice(&[0; 30]);
1717 let tmp = std::env::temp_dir().join("test_xref_cubase.cpr");
1718 fs::write(&tmp, &data).unwrap();
1719 let result = extract_plugins(tmp.to_str().unwrap());
1720 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1721 assert!(
1722 names.contains(&"Serum"),
1723 "missing Serum from binary: {:?}",
1724 names
1725 );
1726 assert!(
1727 names.contains(&"LFOTool"),
1728 "missing LFOTool from marker: {:?}",
1729 names
1730 );
1731 let _ = fs::remove_file(&tmp);
1732 }
1733
1734 #[test]
1737 fn test_logic_known_plugins_extraction() {
1738 let mut data = vec![0u8; 50];
1739 for name in ["Sylenth1", "Channel EQ", "Compressor", "Alchemy", "Hive"] {
1741 data.extend_from_slice(name.as_bytes());
1742 data.extend_from_slice(&[0; 10]);
1743 }
1744 let result = extract_logic_plugin_names(&data);
1745 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1746 assert!(names.contains(&"Sylenth1"), "missing Sylenth1: {:?}", names);
1747 assert!(
1748 names.contains(&"Channel EQ"),
1749 "missing Channel EQ: {:?}",
1750 names
1751 );
1752 assert!(
1753 names.contains(&"Compressor"),
1754 "missing Compressor: {:?}",
1755 names
1756 );
1757 assert!(names.contains(&"Alchemy"), "missing Alchemy: {:?}", names);
1758 assert!(names.contains(&"Hive"), "missing Hive: {:?}", names);
1759 }
1760
1761 #[test]
1762 fn test_logic_filters_false_positives() {
1763 let mut data = vec![0u8; 50];
1764 for name in [
1765 "Output 1",
1766 "Output 5-6H",
1767 "Automatic-Generic Audio 12",
1768 "com.apple.foo",
1769 ] {
1770 data.extend_from_slice(name.as_bytes());
1771 data.extend_from_slice(&[0; 10]);
1772 }
1773 let result = extract_logic_plugin_names(&data);
1774 assert!(
1775 result.is_empty(),
1776 "should filter all false positives, got: {:?}",
1777 result.iter().map(|p| &p.name).collect::<Vec<_>>()
1778 );
1779 }
1780
1781 #[test]
1782 fn test_logic_stock_vs_thirdparty_type() {
1783 let mut data = vec![0u8; 50];
1784 data.extend_from_slice(b"Channel EQ");
1785 data.extend_from_slice(&[0; 10]);
1786 data.extend_from_slice(b"Sylenth1");
1787 data.extend_from_slice(&[0; 10]);
1788 let result = extract_logic_plugin_names(&data);
1789 let stock = result.iter().find(|p| p.name == "Channel EQ").unwrap();
1790 let third = result.iter().find(|p| p.name == "Sylenth1").unwrap();
1791 assert_eq!(stock.plugin_type, "AU (Stock)");
1792 assert_eq!(third.plugin_type, "AU");
1793 }
1794
1795 #[test]
1796 fn test_logic_component_path_extraction() {
1797 let mut data = vec![0u8; 50];
1798 data.extend_from_slice(b"/Library/Audio/Plug-Ins/Components/FabFilter Pro-Q 3.component");
1799 data.extend_from_slice(&[0; 50]);
1800 let result = extract_plugins_from_binary(&data);
1801 assert!(!result.is_empty());
1802 assert_eq!(result[0].name, "FabFilter Pro-Q 3");
1803 assert_eq!(result[0].plugin_type, "AU");
1804 }
1805
1806 #[test]
1809 fn test_studio_one_zip_xml() {
1810 use std::io::Write;
1811 let tmp = std::env::temp_dir().join("test_xref_s1.song");
1812 let file = fs::File::create(&tmp).unwrap();
1813 let mut zip = zip::ZipWriter::new(file);
1814 zip.start_file::<_, ()>("Song/song.xml", Default::default())
1815 .unwrap();
1816 zip.write_all(b"<Song><MediaTrack name=\"Bass\"/></Song>")
1817 .unwrap();
1818 zip.start_file::<_, ()>("Devices/audiomixer.xml", Default::default())
1819 .unwrap();
1820 zip.write_all(
1821 b"<AudioMixer><Insert plugName=\"Pro-Q 3\" deviceName=\"FabFilter\"/></AudioMixer>",
1822 )
1823 .unwrap();
1824 zip.finish().unwrap();
1825 let result = extract_plugins(tmp.to_str().unwrap());
1826 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1827 assert!(names.contains(&"Pro-Q 3"), "missing Pro-Q 3: {:?}", names);
1828 assert!(
1829 !names.iter().any(|n| *n == "FabFilter"),
1830 "deviceName is vendor/model, not a second plugin: {:?}",
1831 names
1832 );
1833 let _ = fs::remove_file(&tmp);
1834 }
1835
1836 #[test]
1837 fn test_studio_one_ignores_generic_label_attributes() {
1838 use std::io::Write;
1839 let tmp = std::env::temp_dir().join("test_xref_s1_labels.song");
1840 let file = fs::File::create(&tmp).unwrap();
1841 let mut zip = zip::ZipWriter::new(file);
1842 zip.start_file::<_, ()>("Song/song.xml", Default::default())
1843 .unwrap();
1844 zip.write_all(
1845 b"<Song><MediaTrack label=\"Lead Vocals\"/><Channel label=\"Main\"/>\
1846 <Insert plugName=\"True-Plugin\"/></Song>",
1847 )
1848 .unwrap();
1849 zip.finish().unwrap();
1850 let result = extract_plugins(tmp.to_str().unwrap());
1851 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1852 assert_eq!(names, vec!["True-Plugin"]);
1853 let _ = fs::remove_file(&tmp);
1854 }
1855
1856 #[test]
1859 fn test_dawproject_zip_xml() {
1860 use std::io::Write;
1861 let tmp = std::env::temp_dir().join("test_xref.dawproject");
1862 let file = fs::File::create(&tmp).unwrap();
1863 let mut zip = zip::ZipWriter::new(file);
1864 zip.start_file::<_, ()>("project.xml", Default::default())
1865 .unwrap();
1866 zip.write_all(b"<Project><Plugin name=\"Serum\" deviceName=\"Xfer Records\"/><Plugin name=\"Diva\" deviceName=\"u-he\"/></Project>").unwrap();
1867 zip.finish().unwrap();
1868 let result = extract_plugins(tmp.to_str().unwrap());
1869 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1870 assert!(names.contains(&"Serum"), "missing Serum: {:?}", names);
1871 assert!(names.contains(&"Diva"), "missing Diva: {:?}", names);
1872 let _ = fs::remove_file(&tmp);
1873 }
1874
1875 #[test]
1878 fn test_protools_binary_extraction() {
1879 let mut data = vec![0u8; 100];
1881 data.extend_from_slice(
1882 b"/Library/Application Support/Avid/Audio/Plug-Ins/EQ III.aaxplugin",
1883 );
1884 data.extend_from_slice(&[0; 50]);
1885 let result = extract_plugins_from_binary(&data);
1886 assert!(!result.is_empty(), "should find AAX plugin");
1887 assert_eq!(result[0].name, "EQ III");
1888 assert_eq!(result[0].plugin_type, "AAX");
1889 }
1890
1891 #[test]
1892 fn test_protools_named_markers() {
1893 let mut data = vec![0u8; 50];
1894 data.extend_from_slice(b"PlugIn Name");
1895 data.push(0);
1896 data.extend_from_slice(b"Channel Strip");
1897 data.extend_from_slice(&[0; 30]);
1898 data.extend_from_slice(b"Insert Name");
1899 data.push(0);
1900 data.extend_from_slice(b"D-Verb");
1901 data.extend_from_slice(&[0; 30]);
1902 let result = extract_named_plugins(&data, b"PlugIn Name");
1903 let result2 = extract_named_plugins(&data, b"Insert Name");
1904 assert!(
1905 !result.is_empty() || !result2.is_empty(),
1906 "should find named plugins"
1907 );
1908 }
1909
1910 #[test]
1913 fn test_reason_binary_extraction() {
1914 let mut data = vec![0u8; 100];
1915 data.extend_from_slice(b"C:\\VST\\Massive.dll");
1916 data.extend_from_slice(&[0; 50]);
1917 data.extend_from_slice(b"/Library/Audio/Plug-Ins/VST3/Serum.vst3");
1918 data.extend_from_slice(&[0; 50]);
1919 let tmp = std::env::temp_dir().join("test_xref.reason");
1920 fs::write(&tmp, &data).unwrap();
1921 let result = extract_plugins(tmp.to_str().unwrap());
1922 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
1923 assert!(names.contains(&"Massive"), "missing Massive: {:?}", names);
1924 assert!(names.contains(&"Serum"), "missing Serum: {:?}", names);
1925 let _ = fs::remove_file(&tmp);
1926 }
1927
1928 #[test]
1931 fn test_bitwig_all_plugin_types() {
1932 let tmp = std::env::temp_dir().join("test_xref_bw_types.bwproject");
1933 let mut data = vec![0u8; 100];
1934 data.extend_from_slice(b"/VST3/Serum.vst3");
1935 data.extend_from_slice(&[0; 20]);
1936 data.extend_from_slice(b"C:\\Plugins\\Massive.dll");
1937 data.extend_from_slice(&[0; 20]);
1938 data.extend_from_slice(b"/Components/Kontakt.component");
1939 data.extend_from_slice(&[0; 20]);
1940 data.extend_from_slice(b"/CLAP/Vital.clap");
1941 data.extend_from_slice(&[0; 20]);
1942 fs::write(&tmp, &data).unwrap();
1943 let result = extract_plugins(tmp.to_str().unwrap());
1944 assert!(
1945 result.len() >= 4,
1946 "should find 4+ plugins, got {}",
1947 result.len()
1948 );
1949 let _ = fs::remove_file(&tmp);
1950 }
1951
1952 #[test]
1955 fn test_dedup_across_extraction_methods() {
1956 let mut data = vec![0u8; 50];
1958 data.extend_from_slice(b"C:\\Plugins\\Serum.dll");
1959 data.extend_from_slice(&[0; 30]);
1960 for c in "C:\\Plugins\\Serum.dll".chars() {
1961 data.push(c as u8);
1962 data.push(0);
1963 }
1964 data.extend_from_slice(&[0; 50]);
1965 let tmp = std::env::temp_dir().join("test_xref_dedup.flp");
1966 fs::write(&tmp, &data).unwrap();
1967 let result = extract_plugins(tmp.to_str().unwrap());
1968 let serum_count = result
1969 .iter()
1970 .filter(|p| p.normalized_name == "serum")
1971 .count();
1972 assert_eq!(
1973 serum_count, 1,
1974 "duplicate Serum should be deduped, got {}",
1975 serum_count
1976 );
1977 let _ = fs::remove_file(&tmp);
1978 }
1979
1980 #[test]
1982 fn test_extract_plugins_reaper_backup_extension() {
1983 let tmp = std::env::temp_dir().join("test_xref_reaper_bak.rpp-bak");
1984 let content = r#"<VST "VST3: Serum (Xfer)" C:\VST3\Serum.vst3"#;
1985 fs::write(&tmp, content).unwrap();
1986 let result = extract_plugins(tmp.to_str().unwrap());
1987 assert!(
1988 result.iter().any(|p| p.name == "Serum"),
1989 "expected Serum from REAPER backup path, got {:?}",
1990 result.iter().map(|p| &p.name).collect::<Vec<_>>()
1991 );
1992 let _ = fs::remove_file(&tmp);
1993 }
1994
1995 #[test]
1996 fn test_extract_plugins_no_extension_returns_empty() {
1997 assert!(extract_plugins("/tmp/no-extension").is_empty());
1998 }
1999
2000 #[test]
2001 fn test_normalize_plugin_name_idempotent_on_clean_name() {
2002 let once = normalize_plugin_name("FabFilter Pro-Q 3");
2003 let twice = normalize_plugin_name(&once);
2004 assert_eq!(once, twice);
2005 }
2006
2007 #[test]
2010 fn test_extract_plugins_count_matrix_all_daw_extensions() {
2011 let td = std::env::temp_dir();
2012
2013 let als_path = td.join("xref_matrix_count.als");
2015 {
2016 let xml = r#"<Ableton><Vst3PluginInfo><Name Value="MatrixALS" /><DeviceCreator Value="M" /></Vst3PluginInfo></Ableton>"#;
2017 let f = fs::File::create(&als_path).unwrap();
2018 let mut enc = GzEncoder::new(f, Compression::default());
2019 enc.write_all(xml.as_bytes()).unwrap();
2020 enc.finish().unwrap();
2021 }
2022 assert_eq!(extract_plugins(als_path.to_str().unwrap()).len(), 1);
2023 let _ = fs::remove_file(&als_path);
2024
2025 let rpp_body = r#"<REAPER_PROJECT
2027 <TRACK
2028 <FXCHAIN
2029 <VST "VST3: MatrixRPP (X)" "{ABCDEF}" 0
2030 >
2031 >
2032 >
2033>"#;
2034 let rpp_path = td.join("xref_matrix_count.rpp");
2035 fs::write(&rpp_path, rpp_body).unwrap();
2036 assert_eq!(extract_plugins(rpp_path.to_str().unwrap()).len(), 1);
2037 let _ = fs::remove_file(&rpp_path);
2038
2039 let rpp_bak_path = td.join("xref_matrix_count.rpp-bak");
2040 fs::write(&rpp_bak_path, rpp_body).unwrap();
2041 assert_eq!(extract_plugins(rpp_bak_path.to_str().unwrap()).len(), 1);
2042 let _ = fs::remove_file(&rpp_bak_path);
2043
2044 let bw_path = td.join("xref_matrix_count.bwproject");
2046 {
2047 let mut data = b"BtWg0003".to_vec();
2048 data.extend_from_slice(&[0u8; 64]);
2049 data.extend_from_slice(b"C:\\VSTPlugins\\MatrixBW.dll");
2050 fs::write(&bw_path, &data).unwrap();
2051 }
2052 assert_eq!(extract_plugins(bw_path.to_str().unwrap()).len(), 1);
2053 let _ = fs::remove_file(&bw_path);
2054
2055 let song_path = td.join("xref_matrix_count.song");
2057 {
2058 use std::io::Write as _;
2059 let file = fs::File::create(&song_path).unwrap();
2060 let mut zip = zip::ZipWriter::new(file);
2061 zip.start_file::<_, ()>("Song/song.xml", Default::default())
2062 .unwrap();
2063 zip.write_all(
2064 b"<Song><Insert plugName=\"MatrixS1A\"/><Insert plugName=\"MatrixS1B\"/></Song>",
2065 )
2066 .unwrap();
2067 zip.finish().unwrap();
2068 }
2069 assert_eq!(extract_plugins(song_path.to_str().unwrap()).len(), 2);
2070 let _ = fs::remove_file(&song_path);
2071
2072 let dawproj_path = td.join("xref_matrix_count.dawproject");
2074 {
2075 use std::io::Write as _;
2076 let file = fs::File::create(&dawproj_path).unwrap();
2077 let mut zip = zip::ZipWriter::new(file);
2078 zip.start_file::<_, ()>("project.xml", Default::default())
2079 .unwrap();
2080 zip.write_all(
2081 br#"<Project><Plugin name="MatrixDJP1" deviceName="A"/><Plugin name="MatrixDJP2" deviceName="B"/></Project>"#,
2082 )
2083 .unwrap();
2084 zip.finish().unwrap();
2085 }
2086 assert_eq!(extract_plugins(dawproj_path.to_str().unwrap()).len(), 2);
2087 let _ = fs::remove_file(&dawproj_path);
2088
2089 let flp_path = td.join("xref_matrix_count.flp");
2091 {
2092 let mut data = vec![0u8; 80];
2093 data.extend_from_slice(b"/Library/Audio/Plug-Ins/VST3/MatrixFLP.vst3");
2094 data.resize(4000, 0u8);
2095 fs::write(&flp_path, &data).unwrap();
2096 }
2097 assert_eq!(extract_plugins(flp_path.to_str().unwrap()).len(), 1);
2098 let _ = fs::remove_file(&flp_path);
2099
2100 let logic_path = td.join("xref_matrix_count.logicx");
2102 {
2103 let _ = fs::remove_dir_all(&logic_path);
2104 fs::create_dir_all(&logic_path).unwrap();
2105 let mut pd = vec![0u8; 16];
2106 pd.extend_from_slice(
2107 b"/Library/Audio/Plug-Ins/Components/MatrixLogicAU.component",
2108 );
2109 fs::write(logic_path.join("ProjectData"), &pd).unwrap();
2110 }
2111 assert_eq!(extract_plugins(logic_path.to_str().unwrap()).len(), 1);
2112 let _ = fs::remove_dir_all(&logic_path);
2113
2114 let mut cpr_data = vec![0u8; 8];
2116 cpr_data.extend_from_slice(b"Plugin Name");
2117 cpr_data.push(0);
2118 cpr_data.extend_from_slice(b"MatrixCPR");
2119 let cpr_path = td.join("xref_matrix_count.cpr");
2120 fs::write(&cpr_path, &cpr_data).unwrap();
2121 assert_eq!(extract_plugins(cpr_path.to_str().unwrap()).len(), 1);
2122 let _ = fs::remove_file(&cpr_path);
2123
2124 let npr_path = td.join("xref_matrix_count.npr");
2125 fs::write(&npr_path, &cpr_data).unwrap();
2126 assert_eq!(extract_plugins(npr_path.to_str().unwrap()).len(), 1);
2127 let _ = fs::remove_file(&npr_path);
2128
2129 let mut pt_data = vec![0u8; 16];
2131 pt_data.extend_from_slice(
2132 b"/Library/Application Support/Avid/Audio/Plug-Ins/MatrixPT.aaxplugin",
2133 );
2134 let ptx_path = td.join("xref_matrix_count.ptx");
2135 fs::write(&ptx_path, &pt_data).unwrap();
2136 assert_eq!(extract_plugins(ptx_path.to_str().unwrap()).len(), 1);
2137 let _ = fs::remove_file(&ptx_path);
2138
2139 let ptf_path = td.join("xref_matrix_count.ptf");
2140 fs::write(&ptf_path, &pt_data).unwrap();
2141 assert_eq!(extract_plugins(ptf_path.to_str().unwrap()).len(), 1);
2142 let _ = fs::remove_file(&ptf_path);
2143
2144 let reason_path = td.join("xref_matrix_count.reason");
2146 {
2147 let mut data = vec![0u8; 12];
2148 data.extend_from_slice(b"C:\\VST\\MatrixReasonA.dll");
2149 data.extend_from_slice(&[0u8; 40]);
2150 data.extend_from_slice(b"/Library/Audio/Plug-Ins/VST3/MatrixReasonB.vst3");
2151 fs::write(&reason_path, &data).unwrap();
2152 }
2153 assert_eq!(extract_plugins(reason_path.to_str().unwrap()).len(), 2);
2154 let _ = fs::remove_file(&reason_path);
2155 }
2156
2157 #[test]
2160 #[ignore]
2161 fn test_real_flp() {
2162 let path = "/Users/wizard/mnt/production/MusicProduction/Samples/Producer loops/2021/prototypesamples_RAGE - PROJECT/RAGE PROJECT/_RAGE.flp";
2163 if !std::path::Path::new(path).exists() {
2164 return;
2165 }
2166 let result = extract_plugins(path);
2167 println!("FLP: {} plugins", result.len());
2168 for p in &result {
2169 println!(" {} ({})", p.name, p.plugin_type);
2170 }
2171 assert!(result.len() >= 5, "Real FLP should have 5+ plugins");
2172 }
2173
2174 #[test]
2175 #[ignore]
2176 fn test_real_cubase() {
2177 let path = "/Users/wizard/mnt/production/MusicProduction/Samples/Producer loops/2021/OST Audio - Trance Collection/Collection/Powerful Trance For Spire/Templates/Cubase/0_1 By OST_Audio/0_1 By OST_Audio.cpr";
2178 if !std::path::Path::new(path).exists() {
2179 return;
2180 }
2181 let result = extract_plugins(path);
2182 println!("Cubase: {} plugins", result.len());
2183 for p in &result {
2184 println!(" {} ({})", p.name, p.plugin_type);
2185 }
2186 assert!(result.len() >= 2, "Real Cubase should have 2+ plugins");
2187 }
2188
2189 #[test]
2190 #[ignore]
2191 fn test_real_logic() {
2192 let path = "/Users/wizard/mnt/production/MusicProduction/Samples/mettaglyde/Alex Di Stefano Logic Pro Tech-Trance Template Vol One/Alex Di Stefano Logic Pro Tech-Trance Template Vol One.logicx";
2193 if !std::path::Path::new(path).exists() {
2194 return;
2195 }
2196 let result = extract_plugins(path);
2197 println!("Logic: {} plugins", result.len());
2198 for p in &result {
2199 println!(" {} ({})", p.name, p.plugin_type);
2200 }
2201 assert!(result.len() >= 5, "Real Logic should have 5+ plugins");
2202 }
2203}