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!(
312 out,
313 r#" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0.xsd""#
314 )?;
315 writeln!(out, r#" version="1.1.0">"#)?;
316 }
317
318 writeln!(out, r#" <cvList count="2">"#)?;
319 writeln!(
320 out,
321 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"/>"#
322 )?;
323 writeln!(
324 out,
325 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"/>"#
326 )?;
327 writeln!(out, r#" </cvList>"#)?;
328
329 writeln!(out, r#" <fileDescription>"#)?;
330 writeln!(out, r#" <fileContent>"#)?;
331 writeln!(
332 out,
333 r#" <cvParam cvRef="MS" accession="MS:1000579" name="MS1 spectrum" value=""/>"#
334 )?;
335 writeln!(
336 out,
337 r#" <cvParam cvRef="MS" accession="MS:1000580" name="MSn spectrum" value=""/>"#
338 )?;
339 writeln!(out, r#" </fileContent>"#)?;
340 writeln!(out, r#" <sourceFileList count="1">"#)?;
341 writeln!(
342 out,
343 r#" <sourceFile id="sf1" name="{}" location="">"#,
344 escape(&meta.source_file_name)
345 )?;
346 write_cv(out, " ", &meta.source_file_format)?;
347 write_cv(out, " ", &meta.native_id_format)?;
348 writeln!(out, r#" </sourceFile>"#)?;
349 writeln!(out, r#" </sourceFileList>"#)?;
350 writeln!(out, r#" </fileDescription>"#)?;
351
352 writeln!(out, r#" <softwareList count="1">"#)?;
353 writeln!(
354 out,
355 r#" <software id="{}" version="{}">"#,
356 escape(&meta.software_name),
357 escape(&meta.software_version)
358 )?;
359 writeln!(
360 out,
361 r#" <cvParam cvRef="MS" accession="MS:1000799" name="custom unreleased software tool" value="{}"/>"#,
362 escape(&meta.software_name)
363 )?;
364 writeln!(out, r#" </software>"#)?;
365 writeln!(out, r#" </softwareList>"#)?;
366
367 writeln!(out, r#" <instrumentConfigurationList count="1">"#)?;
368 writeln!(out, r#" <instrumentConfiguration id="IC1">"#)?;
369 write_cv(out, " ", &meta.instrument)?;
370 writeln!(out, r#" </instrumentConfiguration>"#)?;
371 writeln!(out, r#" </instrumentConfigurationList>"#)?;
372
373 writeln!(out, r#" <dataProcessingList count="1">"#)?;
374 writeln!(out, r#" <dataProcessing id="dp1">"#)?;
375 writeln!(
376 out,
377 r#" <processingMethod order="0" softwareRef="{}">"#,
378 escape(&meta.software_name)
379 )?;
380 writeln!(
381 out,
382 r#" <cvParam cvRef="MS" accession="MS:1000544" name="Conversion to mzML" value=""/>"#
383 )?;
384 writeln!(out, r#" </processingMethod>"#)?;
385 writeln!(out, r#" </dataProcessing>"#)?;
386 writeln!(out, r#" </dataProcessingList>"#)?;
387
388 writeln!(
389 out,
390 r#" <run id="{}" defaultInstrumentConfigurationRef="IC1" defaultSourceFileRef="sf1">"#,
391 escape(&meta.source_file_name)
392 )?;
393 writeln!(
394 out,
395 r#" <spectrumList count="{}" defaultDataProcessingRef="dp1">"#,
396 n_spectra
397 )?;
398 Ok(())
399}
400
401fn write_cv<W: Write>(out: &mut W, indent: &str, cv: &CvTerm) -> Result<()> {
402 writeln!(
403 out,
404 r#"{indent}<cvParam cvRef="MS" accession="{}" name="{}" value=""/>"#,
405 cv.accession,
406 escape(&cv.name)
407 )
408}
409
410fn write_spectrum<W: Write>(
411 out: &mut W,
412 rec: &SpectrumRecord,
413 mobility_kind: Option<MobilityArrayKind>,
414) -> Result<()> {
415 let spectrum_type = if rec.ms_level <= 1 {
416 ("MS:1000579", "MS1 spectrum")
417 } else {
418 ("MS:1000580", "MSn spectrum")
419 };
420 let n_peaks = rec.mz.len();
421
422 writeln!(
423 out,
424 r#" <spectrum id="{id}" index="{idx}" defaultArrayLength="{n}">"#,
425 id = escape(&rec.native_id),
426 idx = rec.index,
427 n = n_peaks
428 )?;
429 writeln!(
430 out,
431 r#" <cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="{}"/>"#,
432 rec.ms_level
433 )?;
434 writeln!(
435 out,
436 r#" <cvParam cvRef="MS" accession="{}" name="{}" value=""/>"#,
437 spectrum_type.0, spectrum_type.1
438 )?;
439
440 match rec.scan_mode {
441 Some(ScanMode::Centroid) => writeln!(
442 out,
443 r#" <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum" value=""/>"#
444 )?,
445 _ => writeln!(
446 out,
447 r#" <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum" value=""/>"#
448 )?,
449 }
450
451 match rec.polarity {
452 Some(Polarity::Positive) => writeln!(
453 out,
454 r#" <cvParam cvRef="MS" accession="MS:1000130" name="positive scan" value=""/>"#
455 )?,
456 Some(Polarity::Negative) => writeln!(
457 out,
458 r#" <cvParam cvRef="MS" accession="MS:1000129" name="negative scan" value=""/>"#
459 )?,
460 None => {}
461 }
462
463 let tic = rec.effective_tic();
464 let (bp_mz, bp_int) = rec.effective_base_peak().unwrap_or((0.0, 0.0));
465 let (lo_mz, hi_mz) = rec.effective_mz_range().unwrap_or((0.0, 0.0));
466
467 writeln!(
468 out,
469 r#" <cvParam cvRef="MS" accession="MS:1000285" name="total ion current" value="{:.6}"/>"#,
470 tic
471 )?;
472 writeln!(
473 out,
474 r#" <cvParam cvRef="MS" accession="MS:1000504" name="base peak m/z" value="{:.6}"/>"#,
475 bp_mz
476 )?;
477 writeln!(
478 out,
479 r#" <cvParam cvRef="MS" accession="MS:1000505" name="base peak intensity" value="{:.6}"/>"#,
480 bp_int
481 )?;
482 writeln!(
483 out,
484 r#" <cvParam cvRef="MS" accession="MS:1000528" name="lowest observed m/z" value="{:.6}"/>"#,
485 lo_mz
486 )?;
487 writeln!(
488 out,
489 r#" <cvParam cvRef="MS" accession="MS:1000527" name="highest observed m/z" value="{:.6}"/>"#,
490 hi_mz
491 )?;
492
493 writeln!(out, r#" <scanList count="1">"#)?;
494 writeln!(
495 out,
496 r#" <cvParam cvRef="MS" accession="MS:1000795" name="no combination" value=""/>"#
497 )?;
498 writeln!(out, r#" <scan>"#)?;
499
500 if let Some(f) = rec.filter.as_deref() {
501 if !f.is_empty() {
502 writeln!(
503 out,
504 r#" <cvParam cvRef="MS" accession="MS:1000512" name="filter string" value="{}"/>"#,
505 escape(f)
506 )?;
507 }
508 }
509
510 let rt_min = rec.retention_time_sec / 60.0;
512 writeln!(
513 out,
514 r#" <cvParam cvRef="MS" accession="MS:1000016" name="scan start time" value="{:.6}" unitCvRef="UO" unitAccession="UO:0000031" unitName="minute"/>"#,
515 rt_min
516 )?;
517
518 if let Some(it) = rec.ion_injection_time_ms {
519 writeln!(
520 out,
521 r#" <cvParam cvRef="MS" accession="MS:1000927" name="ion injection time" value="{:.6}" unitCvRef="UO" unitAccession="UO:0000028" unitName="millisecond"/>"#,
522 it
523 )?;
524 }
525
526 if let Some(mob) = rec.inv_mobility {
527 writeln!(
528 out,
529 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"/>"#,
530 mob
531 )?;
532 }
533
534 writeln!(out, r#" <scanWindowList count="1">"#)?;
535 writeln!(out, r#" <scanWindow>"#)?;
536 writeln!(
537 out,
538 r#" <cvParam cvRef="MS" accession="MS:1000501" name="scan window lower limit" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
539 lo_mz
540 )?;
541 writeln!(
542 out,
543 r#" <cvParam cvRef="MS" accession="MS:1000500" name="scan window upper limit" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
544 hi_mz
545 )?;
546 writeln!(out, r#" </scanWindow>"#)?;
547 writeln!(out, r#" </scanWindowList>"#)?;
548 writeln!(out, r#" </scan>"#)?;
549 writeln!(out, r#" </scanList>"#)?;
550
551 if let Some(pre) = rec.precursor.as_ref() {
552 writeln!(out, r#" <precursorList count="1">"#)?;
553 if let Some(ref nid) = pre.precursor_native_id {
554 writeln!(
555 out,
556 r#" <precursor spectrumRef="{}">"#,
557 escape(nid)
558 )?;
559 } else {
560 writeln!(out, r#" <precursor>"#)?;
561 }
562
563 if pre.target_mz.is_some() || pre.isolation_width.is_some() {
564 writeln!(out, r#" <isolationWindow>"#)?;
565 if let Some(mz) = pre.target_mz {
566 writeln!(
567 out,
568 r#" <cvParam cvRef="MS" accession="MS:1000827" name="isolation window target m/z" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
569 mz
570 )?;
571 }
572 if let Some(w) = pre.isolation_width {
573 let half = w / 2.0;
574 writeln!(
575 out,
576 r#" <cvParam cvRef="MS" accession="MS:1000828" name="isolation window lower offset" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
577 half
578 )?;
579 writeln!(
580 out,
581 r#" <cvParam cvRef="MS" accession="MS:1000829" name="isolation window upper offset" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
582 half
583 )?;
584 }
585 writeln!(out, r#" </isolationWindow>"#)?;
586 }
587
588 if let Some(mz) = pre.selected_mz {
589 writeln!(out, r#" <selectedIonList count="1">"#)?;
590 writeln!(out, r#" <selectedIon>"#)?;
591 writeln!(
592 out,
593 r#" <cvParam cvRef="MS" accession="MS:1000744" name="selected ion m/z" value="{:.6}" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>"#,
594 mz
595 )?;
596 if let Some(z) = pre.charge {
597 writeln!(
598 out,
599 r#" <cvParam cvRef="MS" accession="MS:1000041" name="charge state" value="{z}"/>"#
600 )?;
601 }
602 if let Some(i) = pre.intensity {
603 writeln!(
604 out,
605 r#" <cvParam cvRef="MS" accession="MS:1000042" name="peak intensity" value="{:.6}"/>"#,
606 i
607 )?;
608 }
609 writeln!(out, r#" </selectedIon>"#)?;
610 writeln!(out, r#" </selectedIonList>"#)?;
611 }
612
613 writeln!(out, r#" <activation>"#)?;
614 if let Some(act) = pre.activation {
615 let (acc, name) = activation_cv(act, pre.analyzer);
616 writeln!(
617 out,
618 r#" <cvParam cvRef="MS" accession="{acc}" name="{name}" value=""/>"#
619 )?;
620 } else {
621 writeln!(
622 out,
623 r#" <cvParam cvRef="MS" accession="MS:1000133" name="collision-induced dissociation" value=""/>"#
624 )?;
625 }
626 if let Some(e) = pre.collision_energy {
627 if pre.ce_is_nce {
628 writeln!(
629 out,
630 r#" <cvParam cvRef="MS" accession="MS:1002013" name="normalized collision energy" value="{:.2}"/>"#,
631 e
632 )?;
633 } else {
634 writeln!(
635 out,
636 r#" <cvParam cvRef="MS" accession="MS:1000045" name="collision energy" value="{:.2}" unitCvRef="UO" unitAccession="UO:0000266" unitName="electronvolt"/>"#,
637 e
638 )?;
639 }
640 }
641 writeln!(out, r#" </activation>"#)?;
642 writeln!(out, r#" </precursor>"#)?;
643 writeln!(out, r#" </precursorList>"#)?;
644 }
645
646 if n_peaks > 0 {
647 let mz_b64 = encode_f64_array(&rec.mz);
648 let int_b64 = encode_f32_array(&rec.intensity);
649 let mobility_b64_opt = rec
650 .inv_mobility_per_peak
651 .as_ref()
652 .filter(|v| v.len() == n_peaks)
653 .map(|v| encode_f32_array(v));
654 let array_count = 2 + usize::from(mobility_b64_opt.is_some());
655
656 writeln!(
657 out,
658 r#" <binaryDataArrayList count="{array_count}">"#
659 )?;
660
661 writeln!(
662 out,
663 r#" <binaryDataArray encodedLength="{}">"#,
664 mz_b64.len()
665 )?;
666 writeln!(
667 out,
668 r#" <cvParam cvRef="MS" accession="MS:1000514" name="m/z array" value=""/>"#
669 )?;
670 writeln!(
671 out,
672 r#" <cvParam cvRef="MS" accession="MS:1000523" name="64-bit float" value=""/>"#
673 )?;
674 writeln!(
675 out,
676 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
677 )?;
678 writeln!(out, r#" <binary>{mz_b64}</binary>"#)?;
679 writeln!(out, r#" </binaryDataArray>"#)?;
680
681 writeln!(
682 out,
683 r#" <binaryDataArray encodedLength="{}">"#,
684 int_b64.len()
685 )?;
686 writeln!(
687 out,
688 r#" <cvParam cvRef="MS" accession="MS:1000515" name="intensity array" value=""/>"#
689 )?;
690 writeln!(
691 out,
692 r#" <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float" value=""/>"#
693 )?;
694 writeln!(
695 out,
696 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
697 )?;
698 writeln!(out, r#" <binary>{int_b64}</binary>"#)?;
699 writeln!(out, r#" </binaryDataArray>"#)?;
700
701 if let Some(mobility_b64) = mobility_b64_opt {
702 let (cv_acc, cv_name, unit_acc, unit_ref, unit_name) = match mobility_kind {
703 Some(MobilityArrayKind::DriftTimeMilliseconds) => (
704 "MS:1003007",
705 "raw ion mobility array",
706 "UO:0000028",
707 "UO",
708 "millisecond",
709 ),
710 Some(MobilityArrayKind::InverseReducedVsPerCm2) | None => (
711 "MS:1003008",
712 "raw inverse reduced ion mobility array",
713 "MS:1002814",
714 "MS",
715 "volt-second per square centimeter",
716 ),
717 };
718 writeln!(
719 out,
720 r#" <binaryDataArray encodedLength="{}">"#,
721 mobility_b64.len()
722 )?;
723 writeln!(
724 out,
725 r#" <cvParam cvRef="MS" accession="{cv_acc}" name="{cv_name}" value="" unitCvRef="{unit_ref}" unitAccession="{unit_acc}" unitName="{unit_name}"/>"#
726 )?;
727 writeln!(
728 out,
729 r#" <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float" value=""/>"#
730 )?;
731 writeln!(
732 out,
733 r#" <cvParam cvRef="MS" accession="MS:1000576" name="no compression" value=""/>"#
734 )?;
735 writeln!(out, r#" <binary>{mobility_b64}</binary>"#)?;
736 writeln!(out, r#" </binaryDataArray>"#)?;
737 }
738
739 writeln!(out, r#" </binaryDataArrayList>"#)?;
740 }
741
742 writeln!(out, r#" </spectrum>"#)?;
743 Ok(())
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn base64_rfc_vectors() {
752 assert_eq!(base64_encode(b""), "");
753 assert_eq!(base64_encode(b"f"), "Zg==");
754 assert_eq!(base64_encode(b"fo"), "Zm8=");
755 assert_eq!(base64_encode(b"foo"), "Zm9v");
756 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
757 assert_eq!(base64_encode(b"Man"), "TWFu");
758 }
759}