1#![forbid(unsafe_code)]
21
22use std::path::{Path, PathBuf};
23
24mod error;
25pub use error::{Error, Result};
26
27pub use openmassspec_core as core;
28
29#[cfg(feature = "arrow")]
30pub use openmassspec_core::arrow;
31
32pub mod vendor {
34 #[cfg(feature = "agilent")]
35 pub use openaraw;
36 #[cfg(feature = "sciex")]
37 pub use opensxraw;
38 #[cfg(feature = "shimadzu")]
39 pub use openszraw;
40 #[cfg(feature = "thermo")]
41 pub use opentfraw;
42 #[cfg(feature = "bruker")]
43 pub use opentimstdf;
44 #[cfg(feature = "waters")]
45 pub use openwraw;
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum VendorFormat {
51 ThermoRaw,
53 BrukerTdf,
56 WatersRaw,
59 AgilentMassHunter,
62 SciexWiff,
64 ShimadzuLabSolutions,
70}
71
72impl VendorFormat {
73 pub fn name(self) -> &'static str {
75 match self {
76 Self::ThermoRaw => "thermo",
77 Self::BrukerTdf => "bruker",
78 Self::WatersRaw => "waters",
79 Self::AgilentMassHunter => "agilent",
80 Self::SciexWiff => "sciex",
81 Self::ShimadzuLabSolutions => "shimadzu",
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct Detected {
89 pub path: PathBuf,
93 pub format: VendorFormat,
95}
96
97pub fn detect_format(path: &Path) -> Option<Detected> {
104 if path.is_dir() {
105 if path.join("analysis.tdf").is_file() && path.join("analysis.tdf_bin").is_file() {
110 return Some(Detected {
111 path: path.to_path_buf(),
112 format: VendorFormat::BrukerTdf,
113 });
114 }
115 if path.join("AcqData").join("MSScan.bin").is_file() {
116 return Some(Detected {
117 path: path.to_path_buf(),
118 format: VendorFormat::AgilentMassHunter,
119 });
120 }
121 if path.join("_HEADER.TXT").is_file() {
122 return Some(Detected {
123 path: path.to_path_buf(),
124 format: VendorFormat::WatersRaw,
125 });
126 }
127 return None;
128 }
129 if path.is_file() {
130 if is_thermo_raw(path) {
131 return Some(Detected {
132 path: path.to_path_buf(),
133 format: VendorFormat::ThermoRaw,
134 });
135 }
136 if is_sciex_wiff(path) {
137 return Some(Detected {
138 path: path.to_path_buf(),
139 format: VendorFormat::SciexWiff,
140 });
141 }
142 if is_shimadzu_labsolutions(path) {
143 return Some(Detected {
144 path: path.to_path_buf(),
145 format: VendorFormat::ShimadzuLabSolutions,
146 });
147 }
148 return None;
149 }
150 None
151}
152
153fn is_sciex_wiff(path: &Path) -> bool {
157 let is_wiff_ext = path
158 .extension()
159 .and_then(|e| e.to_str())
160 .is_some_and(|e| e.eq_ignore_ascii_case("wiff"));
161 if !is_wiff_ext {
162 return false;
163 }
164 let mut scan = path.as_os_str().to_os_string();
165 scan.push(".scan");
166 Path::new(&scan).is_file()
167}
168
169fn is_shimadzu_labsolutions(path: &Path) -> bool {
177 let is_shimadzu_ext = path
178 .extension()
179 .and_then(|e| e.to_str())
180 .is_some_and(|e| e.eq_ignore_ascii_case("qgd") || e.eq_ignore_ascii_case("lcd"));
181 if !is_shimadzu_ext {
182 return false;
183 }
184 use std::fs::File;
185 use std::io::Read;
186 let Ok(mut f) = File::open(path) else {
187 return false;
188 };
189 let mut buf = [0u8; 8];
190 if f.read_exact(&mut buf).is_err() {
191 return false;
192 }
193 const CFBF_MAGIC: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
194 buf == CFBF_MAGIC
195}
196
197fn is_thermo_raw(path: &Path) -> bool {
202 use std::fs::File;
203 use std::io::Read;
204 let Ok(mut f) = File::open(path) else {
205 return false;
206 };
207 let mut buf = [0u8; 18];
208 if f.read_exact(&mut buf).is_err() {
209 return false;
210 }
211 const FINNIGAN_UTF16LE: [u8; 16] = [
213 0x46, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x69, 0x00, 0x67, 0x00, 0x61, 0x00, 0x6e,
214 0x00,
215 ];
216 buf[2..18] == FINNIGAN_UTF16LE
217}
218
219#[allow(clippy::needless_pass_by_value)] pub fn convert_to_mzml(detected: Detected, output: &Path, indexed: bool) -> Result<()> {
224 use std::fs::File;
225 use std::io::BufWriter;
226 let f = File::create(output)?;
227 let mut w = BufWriter::new(f);
228 write_to(detected.format, &detected.path, &mut w, indexed)
229}
230
231#[allow(clippy::needless_pass_by_value)]
235pub fn convert_to_mzml_writer<W: std::io::Write>(
236 detected: Detected,
237 writer: &mut W,
238 indexed: bool,
239) -> Result<()> {
240 write_to(detected.format, &detected.path, writer, indexed)
241}
242
243fn write_to(
244 format: VendorFormat,
245 path: &Path,
246 w: &mut impl std::io::Write,
247 indexed: bool,
248) -> Result<()> {
249 match format {
250 VendorFormat::ThermoRaw => {
251 #[cfg(feature = "thermo")]
252 {
253 thermo_convert(path, w, indexed)
254 }
255 #[cfg(not(feature = "thermo"))]
256 {
257 let _ = (path, w, indexed);
258 Err(Error::FeatureDisabled { vendor: "thermo" })
259 }
260 }
261 VendorFormat::BrukerTdf => {
262 #[cfg(feature = "bruker")]
263 {
264 if indexed {
265 opentimstdf::mzml::write_indexed_mzml(path, w)?;
266 } else {
267 opentimstdf::mzml::write_mzml(path, w)?;
268 }
269 Ok(())
270 }
271 #[cfg(not(feature = "bruker"))]
272 {
273 let _ = (path, w, indexed);
274 Err(Error::FeatureDisabled { vendor: "bruker" })
275 }
276 }
277 VendorFormat::WatersRaw => {
278 #[cfg(feature = "waters")]
279 {
280 if indexed {
281 openwraw::mzml::write_indexed_mzml(path, w)?;
282 } else {
283 openwraw::mzml::write_mzml(path, w)?;
284 }
285 Ok(())
286 }
287 #[cfg(not(feature = "waters"))]
288 {
289 let _ = (path, w, indexed);
290 Err(Error::FeatureDisabled { vendor: "waters" })
291 }
292 }
293 VendorFormat::AgilentMassHunter => {
294 #[cfg(feature = "agilent")]
295 {
296 let mut reader = openaraw::reader::Reader::open(path)?;
300 if indexed {
301 openmassspec_core::write_indexed_mzml(&mut reader, w)?;
302 } else {
303 openmassspec_core::write_mzml(&mut reader, w)?;
304 }
305 Ok(())
306 }
307 #[cfg(not(feature = "agilent"))]
308 {
309 let _ = (path, w, indexed);
310 Err(Error::FeatureDisabled { vendor: "agilent" })
311 }
312 }
313 VendorFormat::SciexWiff => {
314 #[cfg(feature = "sciex")]
315 {
316 let mut reader = opensxraw::reader::Reader::open(path)?;
317 if indexed {
318 openmassspec_core::write_indexed_mzml(&mut reader, w)?;
319 } else {
320 openmassspec_core::write_mzml(&mut reader, w)?;
321 }
322 Ok(())
323 }
324 #[cfg(not(feature = "sciex"))]
325 {
326 let _ = (path, w, indexed);
327 Err(Error::FeatureDisabled { vendor: "sciex" })
328 }
329 }
330 VendorFormat::ShimadzuLabSolutions => {
331 #[cfg(feature = "shimadzu")]
332 {
333 let mut reader = openszraw::reader::Reader::open(path)?;
337 if indexed {
338 openmassspec_core::write_indexed_mzml(&mut reader, w)?;
339 } else {
340 openmassspec_core::write_mzml(&mut reader, w)?;
341 }
342 Ok(())
343 }
344 #[cfg(not(feature = "shimadzu"))]
345 {
346 let _ = (path, w, indexed);
347 Err(Error::FeatureDisabled { vendor: "shimadzu" })
348 }
349 }
350 }
351}
352
353#[cfg(feature = "thermo")]
354fn thermo_convert(path: &Path, out: &mut impl std::io::Write, indexed: bool) -> Result<()> {
355 use std::fs::File;
356 use std::io::BufReader;
357 let raw = opentfraw::RawFileReader::open_path(path)?;
358 let mut source = BufReader::with_capacity(2 << 20, File::open(path)?);
359 let filename = path
360 .file_name()
361 .and_then(|n| n.to_str())
362 .unwrap_or("unknown.raw");
363 if indexed {
364 opentfraw::mzml::write_indexed_mzml(&raw, &mut source, out, filename, false)?;
365 } else {
366 opentfraw::mzml::write_mzml(&raw, &mut source, out, filename, false)?;
367 }
368 Ok(())
369}
370
371#[allow(clippy::needless_pass_by_value)]
375pub fn convert_to_mzml_centroided(
376 detected: Detected,
377 output: &Path,
378 indexed: bool,
379 min_intensity: Option<f32>,
380) -> Result<()> {
381 use std::fs::File;
382 use std::io::BufWriter;
383 let f = File::create(output)?;
384 let mut w = BufWriter::new(f);
385 write_to_centroided(
386 detected.format,
387 &detected.path,
388 &mut w,
389 indexed,
390 min_intensity,
391 )
392}
393
394#[allow(clippy::needless_pass_by_value)]
398pub fn convert_to_mzml_writer_centroided<W: std::io::Write>(
399 detected: Detected,
400 writer: &mut W,
401 indexed: bool,
402 min_intensity: Option<f32>,
403) -> Result<()> {
404 write_to_centroided(
405 detected.format,
406 &detected.path,
407 writer,
408 indexed,
409 min_intensity,
410 )
411}
412
413fn write_to_centroided(
421 format: VendorFormat,
422 path: &Path,
423 w: &mut impl std::io::Write,
424 indexed: bool,
425 min_intensity: Option<f32>,
426) -> Result<()> {
427 #[allow(unused_imports)]
428 use openmassspec_core::SpectrumSource;
429 match format {
430 VendorFormat::ThermoRaw => {
431 #[cfg(feature = "thermo")]
432 {
433 use std::fs::File;
434 use std::io::BufReader;
435 let raw = opentfraw::RawFileReader::open_path(path)?;
436 let mut source = BufReader::with_capacity(2 << 20, File::open(path)?);
437 let filename = path
438 .file_name()
439 .and_then(|n| n.to_str())
440 .unwrap_or("unknown.raw");
441 let src = opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
442 let mut src = with_min_intensity_opt(src, min_intensity);
443 if indexed {
444 openmassspec_core::write_indexed_mzml(&mut src, w)?;
445 } else {
446 openmassspec_core::write_mzml(&mut src, w)?;
447 }
448 Ok(())
449 }
450 #[cfg(not(feature = "thermo"))]
451 {
452 let _ = (path, w, indexed, min_intensity);
453 Err(Error::FeatureDisabled { vendor: "thermo" })
454 }
455 }
456 VendorFormat::BrukerTdf => {
457 #[cfg(feature = "bruker")]
458 {
459 let src = opentimstdf::mzml::TdfSource::open(path)?;
460 let mut src = with_min_intensity_opt(src, min_intensity);
461 if indexed {
462 openmassspec_core::write_indexed_mzml(&mut src, w)?;
463 } else {
464 openmassspec_core::write_mzml(&mut src, w)?;
465 }
466 Ok(())
467 }
468 #[cfg(not(feature = "bruker"))]
469 {
470 let _ = (path, w, indexed, min_intensity);
471 Err(Error::FeatureDisabled { vendor: "bruker" })
472 }
473 }
474 VendorFormat::WatersRaw => {
475 #[cfg(feature = "waters")]
476 {
477 let src = openwraw::mzml::WatersSource::open(path)?;
478 let mut src = with_min_intensity_opt(src, min_intensity);
479 if indexed {
480 openmassspec_core::write_indexed_mzml(&mut src, w)?;
481 } else {
482 openmassspec_core::write_mzml(&mut src, w)?;
483 }
484 Ok(())
485 }
486 #[cfg(not(feature = "waters"))]
487 {
488 let _ = (path, w, indexed, min_intensity);
489 Err(Error::FeatureDisabled { vendor: "waters" })
490 }
491 }
492 VendorFormat::AgilentMassHunter => {
493 #[cfg(feature = "agilent")]
494 {
495 let reader = openaraw::reader::Reader::open(path)?;
496 let mut src = with_min_intensity_opt(reader, min_intensity);
497 if indexed {
498 openmassspec_core::write_indexed_mzml(&mut src, w)?;
499 } else {
500 openmassspec_core::write_mzml(&mut src, w)?;
501 }
502 Ok(())
503 }
504 #[cfg(not(feature = "agilent"))]
505 {
506 let _ = (path, w, indexed, min_intensity);
507 Err(Error::FeatureDisabled { vendor: "agilent" })
508 }
509 }
510 VendorFormat::SciexWiff => {
511 #[cfg(feature = "sciex")]
512 {
513 let reader = opensxraw::reader::Reader::open(path)?;
514 let mut src = with_min_intensity_opt(reader, min_intensity);
515 if indexed {
516 openmassspec_core::write_indexed_mzml(&mut src, w)?;
517 } else {
518 openmassspec_core::write_mzml(&mut src, w)?;
519 }
520 Ok(())
521 }
522 #[cfg(not(feature = "sciex"))]
523 {
524 let _ = (path, w, indexed, min_intensity);
525 Err(Error::FeatureDisabled { vendor: "sciex" })
526 }
527 }
528 VendorFormat::ShimadzuLabSolutions => {
529 #[cfg(feature = "shimadzu")]
530 {
531 let reader = openszraw::reader::Reader::open(path)?;
532 let mut src = with_min_intensity_opt(reader, min_intensity);
533 if indexed {
534 openmassspec_core::write_indexed_mzml(&mut src, w)?;
535 } else {
536 openmassspec_core::write_mzml(&mut src, w)?;
537 }
538 Ok(())
539 }
540 #[cfg(not(feature = "shimadzu"))]
541 {
542 let _ = (path, w, indexed, min_intensity);
543 Err(Error::FeatureDisabled { vendor: "shimadzu" })
544 }
545 }
546 }
547}
548
549#[allow(dead_code)]
555fn with_min_intensity_opt<S: openmassspec_core::SpectrumSource>(
556 src: S,
557 min_intensity: Option<f32>,
558) -> openmassspec_core::Centroided<S> {
559 let centroided = openmassspec_core::Centroided::new(src);
560 match min_intensity {
561 Some(v) => centroided.with_min_intensity(v),
562 None => centroided,
563 }
564}
565
566#[allow(clippy::needless_pass_by_value)]
575pub fn collect(
576 detected: Detected,
577) -> Result<(
578 Vec<openmassspec_core::SpectrumRecord>,
579 openmassspec_core::RunMetadata,
580)> {
581 #[allow(unused_imports)]
582 use openmassspec_core::SpectrumSource;
583 match detected.format {
584 VendorFormat::ThermoRaw => {
585 #[cfg(feature = "thermo")]
586 {
587 use std::fs::File;
588 use std::io::BufReader;
589 let raw = opentfraw::RawFileReader::open_path(&detected.path)?;
590 let mut source = BufReader::with_capacity(2 << 20, File::open(&detected.path)?);
591 let filename = detected
592 .path
593 .file_name()
594 .and_then(|n| n.to_str())
595 .unwrap_or("unknown.raw");
596 let mut src =
597 opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
598 let meta = src.run_metadata();
599 let recs: Vec<_> = src.iter_spectra().collect();
600 Ok((recs, meta))
601 }
602 #[cfg(not(feature = "thermo"))]
603 Err(Error::FeatureDisabled { vendor: "thermo" })
604 }
605 VendorFormat::BrukerTdf => {
606 #[cfg(feature = "bruker")]
607 {
608 let mut src = opentimstdf::mzml::TdfSource::open(&detected.path)?;
609 let meta = src.run_metadata();
610 let recs: Vec<_> = src.iter_spectra().collect();
611 Ok((recs, meta))
612 }
613 #[cfg(not(feature = "bruker"))]
614 Err(Error::FeatureDisabled { vendor: "bruker" })
615 }
616 VendorFormat::WatersRaw => {
617 #[cfg(feature = "waters")]
618 {
619 let mut src = openwraw::mzml::WatersSource::open(&detected.path)?;
620 let meta = src.run_metadata();
621 let recs: Vec<_> = src.iter_spectra().collect();
622 Ok((recs, meta))
623 }
624 #[cfg(not(feature = "waters"))]
625 Err(Error::FeatureDisabled { vendor: "waters" })
626 }
627 VendorFormat::AgilentMassHunter => {
628 #[cfg(feature = "agilent")]
629 {
630 let mut src = openaraw::reader::Reader::open(&detected.path)?;
631 let meta = src.run_metadata();
632 let recs: Vec<_> = src.iter_spectra().collect();
633 Ok((recs, meta))
634 }
635 #[cfg(not(feature = "agilent"))]
636 Err(Error::FeatureDisabled { vendor: "agilent" })
637 }
638 VendorFormat::SciexWiff => {
639 #[cfg(feature = "sciex")]
640 {
641 let mut src = opensxraw::reader::Reader::open(&detected.path)?;
642 let meta = src.run_metadata();
643 let recs: Vec<_> = src.iter_spectra().collect();
644 Ok((recs, meta))
645 }
646 #[cfg(not(feature = "sciex"))]
647 Err(Error::FeatureDisabled { vendor: "sciex" })
648 }
649 VendorFormat::ShimadzuLabSolutions => {
650 #[cfg(feature = "shimadzu")]
651 {
652 let mut src = openszraw::reader::Reader::open(&detected.path)?;
653 let meta = src.run_metadata();
654 let recs: Vec<_> = src.iter_spectra().collect();
655 Ok((recs, meta))
656 }
657 #[cfg(not(feature = "shimadzu"))]
658 Err(Error::FeatureDisabled { vendor: "shimadzu" })
659 }
660 }
661}
662
663#[allow(clippy::needless_pass_by_value, unused_variables)]
668pub fn collect_centroided(
669 detected: Detected,
670 min_intensity: Option<f32>,
671) -> Result<(
672 Vec<openmassspec_core::SpectrumRecord>,
673 openmassspec_core::RunMetadata,
674)> {
675 #[allow(unused_imports)]
676 use openmassspec_core::SpectrumSource;
677 match detected.format {
678 VendorFormat::ThermoRaw => {
679 #[cfg(feature = "thermo")]
680 {
681 use std::fs::File;
682 use std::io::BufReader;
683 let raw = opentfraw::RawFileReader::open_path(&detected.path)?;
684 let mut source = BufReader::with_capacity(2 << 20, File::open(&detected.path)?);
685 let filename = detected
686 .path
687 .file_name()
688 .and_then(|n| n.to_str())
689 .unwrap_or("unknown.raw");
690 let src = opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
691 let mut src = with_min_intensity_opt(src, min_intensity);
692 let meta = src.run_metadata();
693 let recs: Vec<_> = src.iter_spectra().collect();
694 Ok((recs, meta))
695 }
696 #[cfg(not(feature = "thermo"))]
697 Err(Error::FeatureDisabled { vendor: "thermo" })
698 }
699 VendorFormat::BrukerTdf => {
700 #[cfg(feature = "bruker")]
701 {
702 let src = opentimstdf::mzml::TdfSource::open(&detected.path)?;
703 let mut src = with_min_intensity_opt(src, min_intensity);
704 let meta = src.run_metadata();
705 let recs: Vec<_> = src.iter_spectra().collect();
706 Ok((recs, meta))
707 }
708 #[cfg(not(feature = "bruker"))]
709 Err(Error::FeatureDisabled { vendor: "bruker" })
710 }
711 VendorFormat::WatersRaw => {
712 #[cfg(feature = "waters")]
713 {
714 let src = openwraw::mzml::WatersSource::open(&detected.path)?;
715 let mut src = with_min_intensity_opt(src, min_intensity);
716 let meta = src.run_metadata();
717 let recs: Vec<_> = src.iter_spectra().collect();
718 Ok((recs, meta))
719 }
720 #[cfg(not(feature = "waters"))]
721 Err(Error::FeatureDisabled { vendor: "waters" })
722 }
723 VendorFormat::AgilentMassHunter => {
724 #[cfg(feature = "agilent")]
725 {
726 let src = openaraw::reader::Reader::open(&detected.path)?;
727 let mut src = with_min_intensity_opt(src, min_intensity);
728 let meta = src.run_metadata();
729 let recs: Vec<_> = src.iter_spectra().collect();
730 Ok((recs, meta))
731 }
732 #[cfg(not(feature = "agilent"))]
733 Err(Error::FeatureDisabled { vendor: "agilent" })
734 }
735 VendorFormat::SciexWiff => {
736 #[cfg(feature = "sciex")]
737 {
738 let src = opensxraw::reader::Reader::open(&detected.path)?;
739 let mut src = with_min_intensity_opt(src, min_intensity);
740 let meta = src.run_metadata();
741 let recs: Vec<_> = src.iter_spectra().collect();
742 Ok((recs, meta))
743 }
744 #[cfg(not(feature = "sciex"))]
745 Err(Error::FeatureDisabled { vendor: "sciex" })
746 }
747 VendorFormat::ShimadzuLabSolutions => {
748 #[cfg(feature = "shimadzu")]
749 {
750 let src = openszraw::reader::Reader::open(&detected.path)?;
751 let mut src = with_min_intensity_opt(src, min_intensity);
752 let meta = src.run_metadata();
753 let recs: Vec<_> = src.iter_spectra().collect();
754 Ok((recs, meta))
755 }
756 #[cfg(not(feature = "shimadzu"))]
757 Err(Error::FeatureDisabled { vendor: "shimadzu" })
758 }
759 }
760}
761
762#[allow(clippy::needless_pass_by_value)]
773pub fn stream(
774 detected: Detected,
775 mut on_spectrum: impl FnMut(openmassspec_core::SpectrumRecord) -> Result<()>,
776) -> Result<openmassspec_core::RunMetadata> {
777 #[allow(unused_imports)]
778 use openmassspec_core::SpectrumSource;
779 match detected.format {
780 VendorFormat::ThermoRaw => {
781 #[cfg(feature = "thermo")]
782 {
783 use std::fs::File;
784 use std::io::BufReader;
785 let raw = opentfraw::RawFileReader::open_path(&detected.path)?;
786 let mut source = BufReader::with_capacity(2 << 20, File::open(&detected.path)?);
787 let filename = detected
788 .path
789 .file_name()
790 .and_then(|n| n.to_str())
791 .unwrap_or("unknown.raw");
792 let mut src =
793 opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
794 let meta = src.run_metadata();
795 for rec in src.iter_spectra() {
796 on_spectrum(rec)?;
797 }
798 Ok(meta)
799 }
800 #[cfg(not(feature = "thermo"))]
801 Err(Error::FeatureDisabled { vendor: "thermo" })
802 }
803 VendorFormat::BrukerTdf => {
804 #[cfg(feature = "bruker")]
805 {
806 let mut src = opentimstdf::mzml::TdfSource::open(&detected.path)?;
807 let meta = src.run_metadata();
808 for rec in src.iter_spectra() {
809 on_spectrum(rec)?;
810 }
811 Ok(meta)
812 }
813 #[cfg(not(feature = "bruker"))]
814 Err(Error::FeatureDisabled { vendor: "bruker" })
815 }
816 VendorFormat::WatersRaw => {
817 #[cfg(feature = "waters")]
818 {
819 let mut src = openwraw::mzml::WatersSource::open(&detected.path)?;
820 let meta = src.run_metadata();
821 for rec in src.iter_spectra() {
822 on_spectrum(rec)?;
823 }
824 Ok(meta)
825 }
826 #[cfg(not(feature = "waters"))]
827 Err(Error::FeatureDisabled { vendor: "waters" })
828 }
829 VendorFormat::AgilentMassHunter => {
830 #[cfg(feature = "agilent")]
831 {
832 let mut src = openaraw::reader::Reader::open(&detected.path)?;
833 let meta = src.run_metadata();
834 for rec in src.iter_spectra() {
835 on_spectrum(rec)?;
836 }
837 Ok(meta)
838 }
839 #[cfg(not(feature = "agilent"))]
840 Err(Error::FeatureDisabled { vendor: "agilent" })
841 }
842 VendorFormat::SciexWiff => {
843 #[cfg(feature = "sciex")]
844 {
845 let mut src = opensxraw::reader::Reader::open(&detected.path)?;
846 let meta = src.run_metadata();
847 for rec in src.iter_spectra() {
848 on_spectrum(rec)?;
849 }
850 Ok(meta)
851 }
852 #[cfg(not(feature = "sciex"))]
853 Err(Error::FeatureDisabled { vendor: "sciex" })
854 }
855 VendorFormat::ShimadzuLabSolutions => {
856 #[cfg(feature = "shimadzu")]
857 {
858 let mut src = openszraw::reader::Reader::open(&detected.path)?;
859 let meta = src.run_metadata();
860 for rec in src.iter_spectra() {
861 on_spectrum(rec)?;
862 }
863 Ok(meta)
864 }
865 #[cfg(not(feature = "shimadzu"))]
866 Err(Error::FeatureDisabled { vendor: "shimadzu" })
867 }
868 }
869}
870
871#[allow(clippy::needless_pass_by_value, unused_variables)]
876pub fn stream_centroided(
877 detected: Detected,
878 min_intensity: Option<f32>,
879 mut on_spectrum: impl FnMut(openmassspec_core::SpectrumRecord) -> Result<()>,
880) -> Result<openmassspec_core::RunMetadata> {
881 #[allow(unused_imports)]
882 use openmassspec_core::SpectrumSource;
883 match detected.format {
884 VendorFormat::ThermoRaw => {
885 #[cfg(feature = "thermo")]
886 {
887 use std::fs::File;
888 use std::io::BufReader;
889 let raw = opentfraw::RawFileReader::open_path(&detected.path)?;
890 let mut source = BufReader::with_capacity(2 << 20, File::open(&detected.path)?);
891 let filename = detected
892 .path
893 .file_name()
894 .and_then(|n| n.to_str())
895 .unwrap_or("unknown.raw");
896 let src = opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
897 let mut src = with_min_intensity_opt(src, min_intensity);
898 let meta = src.run_metadata();
899 for rec in src.iter_spectra() {
900 on_spectrum(rec)?;
901 }
902 Ok(meta)
903 }
904 #[cfg(not(feature = "thermo"))]
905 Err(Error::FeatureDisabled { vendor: "thermo" })
906 }
907 VendorFormat::BrukerTdf => {
908 #[cfg(feature = "bruker")]
909 {
910 let src = opentimstdf::mzml::TdfSource::open(&detected.path)?;
911 let mut src = with_min_intensity_opt(src, min_intensity);
912 let meta = src.run_metadata();
913 for rec in src.iter_spectra() {
914 on_spectrum(rec)?;
915 }
916 Ok(meta)
917 }
918 #[cfg(not(feature = "bruker"))]
919 Err(Error::FeatureDisabled { vendor: "bruker" })
920 }
921 VendorFormat::WatersRaw => {
922 #[cfg(feature = "waters")]
923 {
924 let src = openwraw::mzml::WatersSource::open(&detected.path)?;
925 let mut src = with_min_intensity_opt(src, min_intensity);
926 let meta = src.run_metadata();
927 for rec in src.iter_spectra() {
928 on_spectrum(rec)?;
929 }
930 Ok(meta)
931 }
932 #[cfg(not(feature = "waters"))]
933 Err(Error::FeatureDisabled { vendor: "waters" })
934 }
935 VendorFormat::AgilentMassHunter => {
936 #[cfg(feature = "agilent")]
937 {
938 let src = openaraw::reader::Reader::open(&detected.path)?;
939 let mut src = with_min_intensity_opt(src, min_intensity);
940 let meta = src.run_metadata();
941 for rec in src.iter_spectra() {
942 on_spectrum(rec)?;
943 }
944 Ok(meta)
945 }
946 #[cfg(not(feature = "agilent"))]
947 Err(Error::FeatureDisabled { vendor: "agilent" })
948 }
949 VendorFormat::SciexWiff => {
950 #[cfg(feature = "sciex")]
951 {
952 let src = opensxraw::reader::Reader::open(&detected.path)?;
953 let mut src = with_min_intensity_opt(src, min_intensity);
954 let meta = src.run_metadata();
955 for rec in src.iter_spectra() {
956 on_spectrum(rec)?;
957 }
958 Ok(meta)
959 }
960 #[cfg(not(feature = "sciex"))]
961 Err(Error::FeatureDisabled { vendor: "sciex" })
962 }
963 VendorFormat::ShimadzuLabSolutions => {
964 #[cfg(feature = "shimadzu")]
965 {
966 let src = openszraw::reader::Reader::open(&detected.path)?;
967 let mut src = with_min_intensity_opt(src, min_intensity);
968 let meta = src.run_metadata();
969 for rec in src.iter_spectra() {
970 on_spectrum(rec)?;
971 }
972 Ok(meta)
973 }
974 #[cfg(not(feature = "shimadzu"))]
975 Err(Error::FeatureDisabled { vendor: "shimadzu" })
976 }
977 }
978}
979
980#[allow(clippy::needless_pass_by_value)]
987pub fn metadata_only(detected: Detected) -> Result<openmassspec_core::RunMetadata> {
988 #[allow(unused_imports)]
989 use openmassspec_core::SpectrumSource;
990 match detected.format {
991 VendorFormat::ThermoRaw => {
992 #[cfg(feature = "thermo")]
993 {
994 use std::fs::File;
995 use std::io::BufReader;
996 let raw = opentfraw::RawFileReader::open_path(&detected.path)?;
997 let mut source = BufReader::with_capacity(2 << 20, File::open(&detected.path)?);
998 let filename = detected
999 .path
1000 .file_name()
1001 .and_then(|n| n.to_str())
1002 .unwrap_or("unknown.raw");
1003 let src = opentfraw::mzml::OpenTfRawSource::new(&raw, &mut source, filename, false);
1004 Ok(src.run_metadata())
1005 }
1006 #[cfg(not(feature = "thermo"))]
1007 Err(Error::FeatureDisabled { vendor: "thermo" })
1008 }
1009 VendorFormat::BrukerTdf => {
1010 #[cfg(feature = "bruker")]
1011 {
1012 let src = opentimstdf::mzml::TdfSource::open(&detected.path)?;
1013 Ok(src.run_metadata())
1014 }
1015 #[cfg(not(feature = "bruker"))]
1016 Err(Error::FeatureDisabled { vendor: "bruker" })
1017 }
1018 VendorFormat::WatersRaw => {
1019 #[cfg(feature = "waters")]
1020 {
1021 let src = openwraw::mzml::WatersSource::open(&detected.path)?;
1022 Ok(src.run_metadata())
1023 }
1024 #[cfg(not(feature = "waters"))]
1025 Err(Error::FeatureDisabled { vendor: "waters" })
1026 }
1027 VendorFormat::AgilentMassHunter => {
1028 #[cfg(feature = "agilent")]
1029 {
1030 let src = openaraw::reader::Reader::open(&detected.path)?;
1031 Ok(src.run_metadata())
1032 }
1033 #[cfg(not(feature = "agilent"))]
1034 Err(Error::FeatureDisabled { vendor: "agilent" })
1035 }
1036 VendorFormat::SciexWiff => {
1037 #[cfg(feature = "sciex")]
1038 {
1039 let src = opensxraw::reader::Reader::open(&detected.path)?;
1040 Ok(src.run_metadata())
1041 }
1042 #[cfg(not(feature = "sciex"))]
1043 Err(Error::FeatureDisabled { vendor: "sciex" })
1044 }
1045 VendorFormat::ShimadzuLabSolutions => {
1046 #[cfg(feature = "shimadzu")]
1047 {
1048 let src = openszraw::reader::Reader::open(&detected.path)?;
1049 Ok(src.run_metadata())
1050 }
1051 #[cfg(not(feature = "shimadzu"))]
1052 Err(Error::FeatureDisabled { vendor: "shimadzu" })
1053 }
1054 }
1055}
1056
1057pub struct VecSource {
1062 pub metadata: openmassspec_core::RunMetadata,
1063 pub records: Vec<openmassspec_core::SpectrumRecord>,
1064}
1065
1066impl VecSource {
1067 pub fn new(
1068 metadata: openmassspec_core::RunMetadata,
1069 records: Vec<openmassspec_core::SpectrumRecord>,
1070 ) -> Self {
1071 Self { metadata, records }
1072 }
1073}
1074
1075impl openmassspec_core::SpectrumSource for VecSource {
1076 fn run_metadata(&self) -> openmassspec_core::RunMetadata {
1077 self.metadata.clone()
1078 }
1079 fn iter_spectra<'s>(
1080 &'s mut self,
1081 ) -> Box<dyn Iterator<Item = openmassspec_core::SpectrumRecord> + 's> {
1082 Box::new(self.records.drain(..))
1083 }
1084 fn spectrum_count_hint(&self) -> Option<usize> {
1085 Some(self.records.len())
1086 }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use std::io::Write;
1093
1094 #[test]
1095 fn detect_returns_none_for_garbage_file() {
1096 let tmp = tempfile_path();
1097 std::fs::write(&tmp, b"hello").unwrap();
1098 assert!(detect_format(&tmp).is_none());
1099 let _ = std::fs::remove_file(&tmp);
1100 }
1101
1102 #[test]
1103 fn detect_returns_thermo_for_finnigan_magic() {
1104 let tmp = tempfile_path();
1105 let mut f = std::fs::File::create(&tmp).unwrap();
1106 f.write_all(&[
1108 0x01, 0xa1, 0x46, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x69, 0x00, 0x67, 0x00,
1109 0x61, 0x00, 0x6e, 0x00, 0xff, 0xff,
1110 ])
1111 .unwrap();
1112 let det = detect_format(&tmp).expect("detect");
1113 assert_eq!(det.format, VendorFormat::ThermoRaw);
1114 let _ = std::fs::remove_file(&tmp);
1115 }
1116
1117 #[test]
1118 fn detect_returns_bruker_for_tdf_layout() {
1119 let tmp = tempfile_dir();
1120 std::fs::write(tmp.join("analysis.tdf"), b"").unwrap();
1121 std::fs::write(tmp.join("analysis.tdf_bin"), b"").unwrap();
1122 let det = detect_format(&tmp).expect("detect");
1123 assert_eq!(det.format, VendorFormat::BrukerTdf);
1124 let _ = std::fs::remove_dir_all(&tmp);
1125 }
1126
1127 #[test]
1128 fn detect_returns_waters_for_header_layout() {
1129 let tmp = tempfile_dir();
1130 std::fs::write(tmp.join("_HEADER.TXT"), b"$$ FAKE\n").unwrap();
1131 let det = detect_format(&tmp).expect("detect");
1132 assert_eq!(det.format, VendorFormat::WatersRaw);
1133 let _ = std::fs::remove_dir_all(&tmp);
1134 }
1135
1136 #[test]
1137 fn detect_returns_agilent_for_acqdata_layout() {
1138 let tmp = tempfile_dir();
1139 let acq = tmp.join("AcqData");
1140 std::fs::create_dir_all(&acq).unwrap();
1141 std::fs::write(acq.join("MSScan.bin"), b"").unwrap();
1142 let det = detect_format(&tmp).expect("detect");
1143 assert_eq!(det.format, VendorFormat::AgilentMassHunter);
1144 let _ = std::fs::remove_dir_all(&tmp);
1145 }
1146
1147 #[test]
1148 fn detect_returns_sciex_for_wiff_with_scan_sibling() {
1149 let dir = tempfile_dir();
1150 let wiff = dir.join("run.wiff");
1151 std::fs::write(&wiff, b"\xd0\xcf\x11\xe0").unwrap();
1152 std::fs::write(dir.join("run.wiff.scan"), b"").unwrap();
1153 let det = detect_format(&wiff).expect("detect");
1154 assert_eq!(det.format, VendorFormat::SciexWiff);
1155 let _ = std::fs::remove_dir_all(&dir);
1156 }
1157
1158 #[test]
1159 fn detect_returns_none_for_wiff_without_scan_sibling() {
1160 let dir = tempfile_dir();
1161 let wiff = dir.join("lonely.wiff");
1162 std::fs::write(&wiff, b"\xd0\xcf\x11\xe0").unwrap();
1163 assert!(detect_format(&wiff).is_none());
1165 let _ = std::fs::remove_dir_all(&dir);
1166 }
1167
1168 const CFBF_MAGIC_8: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
1169
1170 #[test]
1171 fn detect_returns_shimadzu_for_lcd_with_cfbf_magic() {
1172 let dir = tempfile_dir();
1173 let lcd = dir.join("run.lcd");
1174 std::fs::write(&lcd, CFBF_MAGIC_8).unwrap();
1175 let det = detect_format(&lcd).expect("detect");
1176 assert_eq!(det.format, VendorFormat::ShimadzuLabSolutions);
1177 let _ = std::fs::remove_dir_all(&dir);
1178 }
1179
1180 #[test]
1181 fn detect_returns_shimadzu_for_qgd_with_cfbf_magic() {
1182 let dir = tempfile_dir();
1183 let qgd = dir.join("run.qgd");
1184 std::fs::write(&qgd, CFBF_MAGIC_8).unwrap();
1185 let det = detect_format(&qgd).expect("detect");
1186 assert_eq!(det.format, VendorFormat::ShimadzuLabSolutions);
1187 let _ = std::fs::remove_dir_all(&dir);
1188 }
1189
1190 #[test]
1191 fn detect_returns_none_for_lcd_without_cfbf_magic() {
1192 let dir = tempfile_dir();
1193 let lcd = dir.join("not_really.lcd");
1194 std::fs::write(&lcd, b"not a real container").unwrap();
1195 assert!(detect_format(&lcd).is_none());
1196 let _ = std::fs::remove_dir_all(&dir);
1197 }
1198
1199 #[test]
1200 fn detect_returns_none_for_unrelated_extension_with_cfbf_magic() {
1201 let dir = tempfile_dir();
1204 let other = dir.join("run.xyz");
1205 std::fs::write(&other, CFBF_MAGIC_8).unwrap();
1206 assert!(detect_format(&other).is_none());
1207 let _ = std::fs::remove_dir_all(&dir);
1208 }
1209
1210 fn tempfile_path() -> PathBuf {
1211 let pid = std::process::id();
1212 let mut p = std::env::temp_dir();
1213 p.push(format!("msio-test-{pid}-{:p}", &pid));
1214 p
1215 }
1216
1217 fn tempfile_dir() -> PathBuf {
1218 let p = tempfile_path();
1219 let _ = std::fs::create_dir_all(&p);
1220 p
1221 }
1222
1223 #[test]
1224 fn convert_unsupported_format_returns_typed_error() {
1225 let e: Error = std::io::Error::other("boom").into();
1230 assert!(matches!(e, Error::Io(_)));
1231 let e = Error::FeatureDisabled { vendor: "thermo" };
1232 assert_eq!(
1233 e.to_string(),
1234 "openmassspec-io was built without the 'thermo' feature"
1235 );
1236 let e = Error::UnsupportedFormat(PathBuf::from("/tmp/nope"));
1237 assert!(matches!(e, Error::UnsupportedFormat(_)));
1238 }
1239}