1use std::io::{Result, Write};
15
16use crate::enums::{Activation, Analyzer, MobilityArrayKind, Polarity, ScanMode};
17use crate::source::SpectrumSource;
18use crate::types::{CvTerm, RunMetadata, SpectrumRecord};
19
20struct CountingWriter<'a, W: Write> {
23 inner: &'a mut W,
24 pos: u64,
25 sha1: Sha1,
26 hashing: bool,
27}
28
29impl<'a, W: Write> CountingWriter<'a, W> {
30 fn new(inner: &'a mut W) -> Self {
31 Self {
32 inner,
33 pos: 0,
34 sha1: Sha1::new(),
35 hashing: true,
36 }
37 }
38}
39
40impl<W: Write> Write for CountingWriter<'_, W> {
41 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
42 let n = self.inner.write(buf)?;
43 self.pos += n as u64;
44 if self.hashing {
45 self.sha1.update(&buf[..n]);
46 }
47 Ok(n)
48 }
49 fn flush(&mut self) -> std::io::Result<()> {
50 self.inner.flush()
51 }
52}
53
54struct Sha1 {
57 state: [u32; 5],
58 count: u64,
59 buf: [u8; 64],
60 buf_len: usize,
61}
62
63impl Sha1 {
64 fn new() -> Self {
65 Self {
66 state: [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0],
67 count: 0,
68 buf: [0u8; 64],
69 buf_len: 0,
70 }
71 }
72
73 fn update(&mut self, data: &[u8]) {
74 let mut off = 0;
75 while off < data.len() {
76 let space = 64 - self.buf_len;
77 let take = space.min(data.len() - off);
78 self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&data[off..off + take]);
79 self.buf_len += take;
80 self.count += take as u64;
81 off += take;
82 if self.buf_len == 64 {
83 self.compress();
84 self.buf_len = 0;
85 }
86 }
87 }
88
89 fn compress(&mut self) {
90 let mut w = [0u32; 80];
91 for (i, word) in w.iter_mut().enumerate().take(16) {
92 *word = u32::from_be_bytes(self.buf[i * 4..i * 4 + 4].try_into().unwrap());
93 }
94 for i in 16..80 {
95 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
96 }
97 let [mut a, mut b, mut c, mut d, mut e] = self.state;
98 for (i, &wi) in w.iter().enumerate() {
99 let (f, k) = match i {
100 0..=19 => ((b & c) | (!b & d), 0x5A827999u32),
101 20..=39 => (b ^ c ^ d, 0x6ED9EBA1),
102 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC),
103 _ => (b ^ c ^ d, 0xCA62C1D6),
104 };
105 let temp = a
106 .rotate_left(5)
107 .wrapping_add(f)
108 .wrapping_add(e)
109 .wrapping_add(k)
110 .wrapping_add(wi);
111 e = d;
112 d = c;
113 c = b.rotate_left(30);
114 b = a;
115 a = temp;
116 }
117 self.state[0] = self.state[0].wrapping_add(a);
118 self.state[1] = self.state[1].wrapping_add(b);
119 self.state[2] = self.state[2].wrapping_add(c);
120 self.state[3] = self.state[3].wrapping_add(d);
121 self.state[4] = self.state[4].wrapping_add(e);
122 }
123
124 fn finalize(mut self) -> [u8; 20] {
125 let bit_count = self.count * 8;
126 self.update(&[0x80]);
127 while self.buf_len != 56 {
128 self.update(&[0u8]);
129 }
130 self.update(&bit_count.to_be_bytes());
131 let mut digest = [0u8; 20];
132 for (i, &word) in self.state.iter().enumerate() {
133 digest[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
134 }
135 digest
136 }
137}
138
139const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
142
143fn base64_encode(data: &[u8]) -> String {
144 let n = data.len();
145 let mut out = Vec::with_capacity(n.div_ceil(3) * 4);
146 let mut i = 0;
147 while i + 2 < n {
148 let b = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8) | (data[i + 2] as u32);
149 out.push(B64[((b >> 18) & 0x3f) as usize]);
150 out.push(B64[((b >> 12) & 0x3f) as usize]);
151 out.push(B64[((b >> 6) & 0x3f) as usize]);
152 out.push(B64[(b & 0x3f) as usize]);
153 i += 3;
154 }
155 if n - i == 2 {
156 let b = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8);
157 out.push(B64[((b >> 18) & 0x3f) as usize]);
158 out.push(B64[((b >> 12) & 0x3f) as usize]);
159 out.push(B64[((b >> 6) & 0x3f) as usize]);
160 out.push(b'=');
161 } else if n - i == 1 {
162 let b = (data[i] as u32) << 16;
163 out.push(B64[((b >> 18) & 0x3f) as usize]);
164 out.push(B64[((b >> 12) & 0x3f) as usize]);
165 out.push(b'=');
166 out.push(b'=');
167 }
168 String::from_utf8(out).expect("base64 output is ASCII")
169}
170
171fn encode_f64_array(vals: &[f64]) -> String {
172 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
173 base64_encode(&bytes)
174}
175
176fn encode_f32_array(vals: &[f32]) -> String {
177 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
178 base64_encode(&bytes)
179}
180
181fn escape(s: &str) -> String {
182 s.replace('&', "&")
183 .replace('<', "<")
184 .replace('>', ">")
185 .replace('"', """)
186 .replace('\'', "'")
187}
188
189fn activation_cv(act: Activation, analyzer: Option<Analyzer>) -> (&'static str, &'static str) {
190 match act {
191 Activation::HCD => ("MS:1000422", "beam-type collision-induced dissociation"),
192 Activation::ETD | Activation::EThcD => ("MS:1000598", "electron transfer dissociation"),
193 Activation::CID => match analyzer {
194 Some(Analyzer::FTMS) => ("MS:1000422", "beam-type collision-induced dissociation"),
195 _ => ("MS:1000133", "collision-induced dissociation"),
196 },
197 Activation::MPID => (
198 "MS:1002481",
199 "supplemental beam-type collision-induced dissociation",
200 ),
201 Activation::ECD => ("MS:1000250", "electron capture dissociation"),
202 Activation::IRMPD => ("MS:1000262", "infrared multiphoton dissociation"),
203 Activation::PD => ("MS:1001880", "in-source collision-induced dissociation"),
204 Activation::PQD => ("MS:1000599", "pulsed q dissociation"),
205 Activation::UVPD => ("MS:1003246", "ultraviolet photodissociation"),
206 Activation::SID => ("MS:1000422", "beam-type collision-induced dissociation"),
207 }
208}
209
210pub fn write_mzml<S: SpectrumSource + ?Sized, W: Write>(src: &mut S, out: &mut W) -> Result<()> {
214 let meta = src.run_metadata();
215 let count = src.spectrum_count_hint().unwrap_or(0);
216 let mobility_kind = meta.mobility_array_kind;
217
218 write_prologue(out, &meta, count, false)?;
219 for rec in src.iter_spectra() {
220 write_spectrum(out, &rec, mobility_kind)?;
221 }
222 writeln!(out, r#" </spectrumList>"#)?;
223 writeln!(out, r#" </run>"#)?;
224 writeln!(out, r#"</mzML>"#)?;
225 Ok(())
226}
227
228pub fn write_indexed_mzml<S: SpectrumSource + ?Sized, W: Write>(
231 src: &mut S,
232 out: &mut W,
233) -> Result<()> {
234 let meta = src.run_metadata();
235 let count = src.spectrum_count_hint().unwrap_or(0);
236 let mobility_kind = meta.mobility_array_kind;
237
238 let mut cw = CountingWriter::new(out);
239 write_prologue(&mut cw, &meta, count, true)?;
240
241 let mut offsets: Vec<(String, u64)> = Vec::with_capacity(count);
242 for rec in src.iter_spectra() {
243 offsets.push((rec.native_id.clone(), cw.pos));
244 write_spectrum(&mut cw, &rec, mobility_kind)?;
245 }
246
247 writeln!(cw, r#" </spectrumList>"#)?;
248 writeln!(cw, r#" </run>"#)?;
249 writeln!(cw, r#" </mzML>"#)?;
250
251 let index_list_offset = cw.pos;
252 writeln!(cw, r#" <indexList count="1">"#)?;
253 writeln!(cw, r#" <index name="spectrum">"#)?;
254 for (id, offset) in &offsets {
255 writeln!(
256 cw,
257 r#" <offset idRef="{}">{}</offset>"#,
258 escape(id),
259 offset
260 )?;
261 }
262 writeln!(cw, r#" </index>"#)?;
263 writeln!(cw, r#" </indexList>"#)?;
264
265 cw.hashing = false;
266 let digest = std::mem::replace(&mut cw.sha1, Sha1::new()).finalize();
267 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
268
269 writeln!(
270 cw,
271 r#" <indexListOffset>{}</indexListOffset>"#,
272 index_list_offset
273 )?;
274 writeln!(cw, r#" <fileChecksum>{}</fileChecksum>"#, hex)?;
275 writeln!(cw, r#"</indexedmzML>"#)?;
276 Ok(())
277}
278
279fn write_prologue<W: Write>(
282 out: &mut W,
283 meta: &RunMetadata,
284 n_spectra: usize,
285 indexed: bool,
286) -> Result<()> {
287 writeln!(out, r#"<?xml version="1.0" encoding="utf-8"?>"#)?;
288 if indexed {
289 writeln!(out, r#"<indexedmzML xmlns="http://psi.hupo.org/ms/mzml""#)?;
290 writeln!(
291 out,
292 r#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
293 )?;
294 writeln!(
295 out,
296 r#" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.2_idx.xsd">"#
297 )?;
298 writeln!(
299 out,
300 r#" <mzML xmlns="http://psi.hupo.org/ms/mzml" version="1.1.0">"#
301 )?;
302 } else {
303 writeln!(out, r#"<mzML xmlns="http://psi.hupo.org/ms/mzml""#)?;
304 writeln!(
305 out,
306 r#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
307 )?;
308 writeln!(
309 out,
310 r#" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.2_idx.xsd""#
311 )?;
312 writeln!(out, r#" version="1.1.0">"#)?;
313 }
314
315 writeln!(out, r#" <cvList count="2">"#)?;
316 writeln!(
317 out,
318 r#" <cv id="MS" fullName="Proteomics Standards Initiative Mass Spectrometry Ontology" version="4.1.100" URI="https://raw.githubusercontent.com/HUPO-PSI/psi-ms-CV/master/psi-ms.obo"/>"#
319 )?;
320 writeln!(
321 out,
322 r#" <cv id="UO" fullName="Unit Ontology" version="09:04:2014" URI="https://raw.githubusercontent.com/bio-ontology-research-group/unit-ontology/master/unit.obo"/>"#
323 )?;
324 writeln!(out, r#" </cvList>"#)?;
325
326 writeln!(out, r#" <fileDescription>"#)?;
327 writeln!(out, r#" <fileContent>"#)?;
328 writeln!(
329 out,
330 r#" <cvParam cvRef="MS" accession="MS:1000579" name="MS1 spectrum" value=""/>"#
331 )?;
332 writeln!(
333 out,
334 r#" <cvParam cvRef="MS" accession="MS:1000580" name="MSn spectrum" value=""/>"#
335 )?;
336 writeln!(out, r#" </fileContent>"#)?;
337 writeln!(out, r#" <sourceFileList count="1">"#)?;
338 writeln!(
339 out,
340 r#" <sourceFile id="sf1" name="{}" location="">"#,
341 escape(&meta.source_file_name)
342 )?;
343 write_cv(out, " ", &meta.source_file_format)?;
344 write_cv(out, " ", &meta.native_id_format)?;
345 writeln!(out, r#" </sourceFile>"#)?;
346 writeln!(out, r#" </sourceFileList>"#)?;
347 writeln!(out, r#" </fileDescription>"#)?;
348
349 writeln!(out, r#" <softwareList count="1">"#)?;
350 writeln!(
351 out,
352 r#" <software id="{}" version="{}">"#,
353 escape(&meta.software_name),
354 escape(&meta.software_version)
355 )?;
356 writeln!(
357 out,
358 r#" <cvParam cvRef="MS" accession="MS:1000799" name="custom unreleased software tool" value="{}"/>"#,
359 escape(&meta.software_name)
360 )?;
361 writeln!(out, r#" </software>"#)?;
362 writeln!(out, r#" </softwareList>"#)?;
363
364 writeln!(out, r#" <instrumentConfigurationList count="1">"#)?;
365 writeln!(out, r#" <instrumentConfiguration id="IC1">"#)?;
366 write_cv(out, " ", &meta.instrument)?;
367 writeln!(out, r#" </instrumentConfiguration>"#)?;
368 writeln!(out, r#" </instrumentConfigurationList>"#)?;
369
370 writeln!(out, r#" <dataProcessingList count="1">"#)?;
371 writeln!(out, r#" <dataProcessing id="dp1">"#)?;
372 writeln!(
373 out,
374 r#" <processingMethod order="0" softwareRef="{}">"#,
375 escape(&meta.software_name)
376 )?;
377 writeln!(
378 out,
379 r#" <cvParam cvRef="MS" accession="MS:1000544" name="Conversion to mzML" value=""/>"#
380 )?;
381 writeln!(out, r#" </processingMethod>"#)?;
382 writeln!(out, r#" </dataProcessing>"#)?;
383 writeln!(out, r#" </dataProcessingList>"#)?;
384
385 writeln!(
386 out,
387 r#" <run id="{}" defaultInstrumentConfigurationRef="IC1" defaultSourceFileRef="sf1">"#,
388 escape(&meta.source_file_name)
389 )?;
390 writeln!(
391 out,
392 r#" <spectrumList count="{}" defaultDataProcessingRef="dp1">"#,
393 n_spectra
394 )?;
395 Ok(())
396}
397
398fn write_cv<W: Write>(out: &mut W, indent: &str, cv: &CvTerm) -> Result<()> {
399 writeln!(
400 out,
401 r#"{indent}<cvParam cvRef="MS" accession="{}" name="{}" value=""/>"#,
402 cv.accession,
403 escape(&cv.name)
404 )
405}
406
407fn write_spectrum<W: Write>(
408 out: &mut W,
409 rec: &SpectrumRecord,
410 mobility_kind: Option<MobilityArrayKind>,
411) -> Result<()> {
412 let spectrum_type = if rec.ms_level <= 1 {
413 ("MS:1000579", "MS1 spectrum")
414 } else {
415 ("MS:1000580", "MSn spectrum")
416 };
417 let n_peaks = rec.mz.len();
418
419 writeln!(
420 out,
421 r#" <spectrum id="{id}" index="{idx}" defaultArrayLength="{n}">"#,
422 id = escape(&rec.native_id),
423 idx = rec.index,
424 n = n_peaks
425 )?;
426 writeln!(
427 out,
428 r#" <cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="{}"/>"#,
429 rec.ms_level
430 )?;
431 writeln!(
432 out,
433 r#" <cvParam cvRef="MS" accession="{}" name="{}" value=""/>"#,
434 spectrum_type.0, spectrum_type.1
435 )?;
436
437 match rec.scan_mode {
438 Some(ScanMode::Centroid) => writeln!(
439 out,
440 r#" <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum" value=""/>"#
441 )?,
442 _ => writeln!(
443 out,
444 r#" <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum" value=""/>"#
445 )?,
446 }
447
448 match rec.polarity {
449 Some(Polarity::Positive) => writeln!(
450 out,
451 r#" <cvParam cvRef="MS" accession="MS:1000130" name="positive scan" value=""/>"#
452 )?,
453 Some(Polarity::Negative) => writeln!(
454 out,
455 r#" <cvParam cvRef="MS" accession="MS:1000129" name="negative scan" value=""/>"#
456 )?,
457 None => {}
458 }
459
460 let tic = rec.effective_tic();
461 let (bp_mz, bp_int) = rec.effective_base_peak().unwrap_or((0.0, 0.0));
462 let (lo_mz, hi_mz) = rec.effective_mz_range().unwrap_or((0.0, 0.0));
463
464 writeln!(
465 out,
466 r#" <cvParam cvRef="MS" accession="MS:1000285" name="total ion current" value="{:.6}"/>"#,
467 tic
468 )?;
469 writeln!(
470 out,
471 r#" <cvParam cvRef="MS" accession="MS:1000504" name="base peak m/z" value="{:.6}"/>"#,
472 bp_mz
473 )?;
474 writeln!(
475 out,
476 r#" <cvParam cvRef="MS" accession="MS:1000505" name="base peak intensity" value="{:.6}"/>"#,
477 bp_int
478 )?;
479 writeln!(
480 out,
481 r#" <cvParam cvRef="MS" accession="MS:1000528" name="lowest observed m/z" value="{:.6}"/>"#,
482 lo_mz
483 )?;
484 writeln!(
485 out,
486 r#" <cvParam cvRef="MS" accession="MS:1000527" name="highest observed m/z" value="{:.6}"/>"#,
487 hi_mz
488 )?;
489
490 writeln!(out, r#" <scanList count="1">"#)?;
491 writeln!(
492 out,
493 r#" <cvParam cvRef="MS" accession="MS:1000795" name="no combination" value=""/>"#
494 )?;
495 writeln!(out, r#" <scan>"#)?;
496
497 if let Some(f) = rec.filter.as_deref() {
498 if !f.is_empty() {
499 writeln!(
500 out,
501 r#" <cvParam cvRef="MS" accession="MS:1000512" name="filter string" value="{}"/>"#,
502 escape(f)
503 )?;
504 }
505 }
506
507 let rt_min = rec.retention_time_sec / 60.0;
509 writeln!(
510 out,
511 r#" <cvParam cvRef="MS" accession="MS:1000016" name="scan start time" value="{:.6}" unitCvRef="UO" unitAccession="UO:0000031" unitName="minute"/>"#,
512 rt_min
513 )?;
514
515 if let Some(it) = rec.ion_injection_time_ms {
516 writeln!(
517 out,
518 r#" <cvParam cvRef="MS" accession="MS:1000927" name="ion injection time" value="{:.6}" unitCvRef="UO" unitAccession="UO:0000028" unitName="millisecond"/>"#,
519 it
520 )?;
521 }
522
523 if let Some(mob) = rec.inv_mobility {
524 writeln!(
525 out,
526 r#" <cvParam cvRef="MS" accession="MS:1002815" name="inverse reduced ion mobility" value="{:.6}" unitCvRef="MS" unitAccession="MS:1002814" unitName="volt-second per square centimeter"/>"#,
527 mob
528 )?;
529 }
530
531 writeln!(out, r#" <scanWindowList count="1">"#)?;
532 writeln!(out, r#" <scanWindow>"#)?;
533 writeln!(
534 out,
535 r#" <cvParam cvRef="MS" accession="MS:1000501" name="scan window lower limit" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
536 lo_mz
537 )?;
538 writeln!(
539 out,
540 r#" <cvParam cvRef="MS" accession="MS:1000500" name="scan window upper limit" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
541 hi_mz
542 )?;
543 writeln!(out, r#" </scanWindow>"#)?;
544 writeln!(out, r#" </scanWindowList>"#)?;
545 writeln!(out, r#" </scan>"#)?;
546 writeln!(out, r#" </scanList>"#)?;
547
548 if let Some(pre) = rec.precursor.as_ref() {
549 writeln!(out, r#" <precursorList count="1">"#)?;
550 if let Some(ref nid) = pre.precursor_native_id {
551 writeln!(
552 out,
553 r#" <precursor spectrumRef="{}">"#,
554 escape(nid)
555 )?;
556 } else {
557 writeln!(out, r#" <precursor>"#)?;
558 }
559
560 if pre.target_mz.is_some() || pre.isolation_width.is_some() {
561 writeln!(out, r#" <isolationWindow>"#)?;
562 if let Some(mz) = pre.target_mz {
563 writeln!(
564 out,
565 r#" <cvParam cvRef="MS" accession="MS:1000827" name="isolation window target m/z" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
566 mz
567 )?;
568 }
569 if let Some(w) = pre.isolation_width {
570 let half = w / 2.0;
571 writeln!(
572 out,
573 r#" <cvParam cvRef="MS" accession="MS:1000828" name="isolation window lower offset" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
574 half
575 )?;
576 writeln!(
577 out,
578 r#" <cvParam cvRef="MS" accession="MS:1000829" name="isolation window upper offset" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
579 half
580 )?;
581 }
582 writeln!(out, r#" </isolationWindow>"#)?;
583 }
584
585 if let Some(mz) = pre.selected_mz {
586 writeln!(out, r#" <selectedIonList count="1">"#)?;
587 writeln!(out, r#" <selectedIon>"#)?;
588 writeln!(
589 out,
590 r#" <cvParam cvRef="MS" accession="MS:1000744" name="selected ion m/z" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
591 mz
592 )?;
593 if let Some(z) = pre.charge {
594 writeln!(
595 out,
596 r#" <cvParam cvRef="MS" accession="MS:1000041" name="charge state" value="{z}"/>"#
597 )?;
598 }
599 if let Some(i) = pre.intensity {
600 writeln!(
601 out,
602 r#" <cvParam cvRef="MS" accession="MS:1000042" name="peak intensity" value="{:.6}"/>"#,
603 i
604 )?;
605 }
606 writeln!(out, r#" </selectedIon>"#)?;
607 writeln!(out, r#" </selectedIonList>"#)?;
608 }
609
610 writeln!(out, r#" <activation>"#)?;
611 if let Some(act) = pre.activation {
612 let (acc, name) = activation_cv(act, pre.analyzer);
613 writeln!(
614 out,
615 r#" <cvParam cvRef="MS" accession="{acc}" name="{name}" value=""/>"#
616 )?;
617 } else {
618 writeln!(
619 out,
620 r#" <cvParam cvRef="MS" accession="MS:1000133" name="collision-induced dissociation" value=""/>"#
621 )?;
622 }
623 if let Some(e) = pre.collision_energy {
624 if pre.ce_is_nce {
625 writeln!(
626 out,
627 r#" <cvParam cvRef="MS" accession="MS:1002013" name="normalized collision energy" value="{:.2}"/>"#,
628 e
629 )?;
630 } else {
631 writeln!(
632 out,
633 r#" <cvParam cvRef="MS" accession="MS:1000045" name="collision energy" value="{:.2}" unitCvRef="UO" unitAccession="UO:0000266" unitName="electronvolt"/>"#,
634 e
635 )?;
636 }
637 }
638 writeln!(out, r#" </activation>"#)?;
639 writeln!(out, r#" </precursor>"#)?;
640 writeln!(out, r#" </precursorList>"#)?;
641 }
642
643 if n_peaks > 0 {
644 let mz_b64 = encode_f64_array(&rec.mz);
645 let int_b64 = encode_f32_array(&rec.intensity);
646 let mobility_b64_opt = rec
647 .inv_mobility_per_peak
648 .as_ref()
649 .filter(|v| v.len() == n_peaks)
650 .map(|v| encode_f32_array(v));
651 let array_count = 2 + usize::from(mobility_b64_opt.is_some());
652
653 writeln!(
654 out,
655 r#" <binaryDataArrayList count="{array_count}">"#
656 )?;
657
658 writeln!(
659 out,
660 r#" <binaryDataArray encodedLength="{}">"#,
661 mz_b64.len()
662 )?;
663 writeln!(
664 out,
665 r#" <cvParam cvRef="MS" accession="MS:1000514" name="m/z array" value=""/>"#
666 )?;
667 writeln!(
668 out,
669 r#" <cvParam cvRef="MS" accession="MS:1000523" name="64-bit float" value=""/>"#
670 )?;
671 writeln!(
672 out,
673 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
674 )?;
675 writeln!(out, r#" <binary>{mz_b64}</binary>"#)?;
676 writeln!(out, r#" </binaryDataArray>"#)?;
677
678 writeln!(
679 out,
680 r#" <binaryDataArray encodedLength="{}">"#,
681 int_b64.len()
682 )?;
683 writeln!(
684 out,
685 r#" <cvParam cvRef="MS" accession="MS:1000515" name="intensity array" value=""/>"#
686 )?;
687 writeln!(
688 out,
689 r#" <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float" value=""/>"#
690 )?;
691 writeln!(
692 out,
693 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
694 )?;
695 writeln!(out, r#" <binary>{int_b64}</binary>"#)?;
696 writeln!(out, r#" </binaryDataArray>"#)?;
697
698 if let Some(mobility_b64) = mobility_b64_opt {
699 let (cv_acc, cv_name, unit_acc, unit_ref, unit_name) = match mobility_kind {
700 Some(MobilityArrayKind::DriftTimeMilliseconds) => (
701 "MS:1003007",
702 "raw ion mobility array",
703 "UO:0000028",
704 "UO",
705 "millisecond",
706 ),
707 Some(MobilityArrayKind::InverseReducedVsPerCm2) | None => (
708 "MS:1003008",
709 "raw inverse reduced ion mobility array",
710 "MS:1002814",
711 "MS",
712 "volt-second per square centimeter",
713 ),
714 };
715 writeln!(
716 out,
717 r#" <binaryDataArray encodedLength="{}">"#,
718 mobility_b64.len()
719 )?;
720 writeln!(
721 out,
722 r#" <cvParam cvRef="MS" accession="{cv_acc}" name="{cv_name}" value="" unitCvRef="{unit_ref}" unitAccession="{unit_acc}" unitName="{unit_name}"/>"#
723 )?;
724 writeln!(
725 out,
726 r#" <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float" value=""/>"#
727 )?;
728 writeln!(
729 out,
730 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
731 )?;
732 writeln!(out, r#" <binary>{mobility_b64}</binary>"#)?;
733 writeln!(out, r#" </binaryDataArray>"#)?;
734 }
735
736 writeln!(out, r#" </binaryDataArrayList>"#)?;
737 }
738
739 writeln!(out, r#" </spectrum>"#)?;
740 Ok(())
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746
747 #[test]
748 fn base64_rfc_vectors() {
749 assert_eq!(base64_encode(b""), "");
750 assert_eq!(base64_encode(b"f"), "Zg==");
751 assert_eq!(base64_encode(b"fo"), "Zm8=");
752 assert_eq!(base64_encode(b"foo"), "Zm9v");
753 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
754 assert_eq!(base64_encode(b"Man"), "TWFu");
755 }
756}