1use std::io::Read;
4
5use crate::error::{Error, Result};
6use crate::genomic::ChrMap;
7use crate::source::ByteSource;
8
9#[derive(Debug, Clone)]
10pub struct HeaderField {
11 pub tag: String,
12 pub value: String,
13}
14
15#[derive(Debug, Clone)]
17pub struct HeaderLine {
18 pub kind: String,
19 pub fields: Vec<HeaderField>,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct SamHeader {
24 pub lines: Vec<HeaderLine>,
25}
26
27impl SamHeader {
28 pub fn parse(text: &str) -> Self {
34 let lines = text
35 .lines()
36 .filter_map(|line| {
37 let line = line.strip_prefix('@')?;
38 let mut parts = line.split('\t');
39 let kind = parts.next()?.to_string();
40 let fields = parts
41 .filter(|f| !f.is_empty())
42 .map(|field| match field.split_once(':') {
43 Some((tag, value)) => HeaderField {
44 tag: tag.to_string(),
45 value: value.to_string(),
46 },
47 None => HeaderField {
48 tag: String::new(),
49 value: field.to_string(),
50 },
51 })
52 .collect();
53 Some(HeaderLine { kind, fields })
54 })
55 .collect();
56 Self { lines }
57 }
58}
59
60pub fn read(source: &dyn ByteSource) -> Result<(SamHeader, ChrMap)> {
65 let path = source.path();
66 let mut reader = super::bgzf::header_reader(source);
67
68 let mut magic = [0u8; 4];
69 read_exact(&mut reader, &mut magic, path)?;
70 if &magic != b"BAM\x01" {
71 return Err(Error::format(path, "invalid bam magic string"));
72 }
73
74 let l_text = read_u32(&mut reader, path)? as usize;
75 let text = read_sized(&mut reader, l_text, MAX_TEXT, "header text", path)?;
76 let header = SamHeader::parse(&String::from_utf8_lossy(&text));
77
78 let n_ref = read_u32(&mut reader, path)? as usize;
79 let mut entries = Vec::with_capacity(n_ref.min(1 << 16));
80 for index in 0..n_ref {
81 let l_name = read_u32(&mut reader, path)? as usize;
82 if l_name == 0 {
83 return Err(Error::corrupt(path, 0, "a bam reference has an empty name"));
84 }
85 let name = read_sized(&mut reader, l_name, MAX_NAME, "reference name", path)?;
86 let name = String::from_utf8_lossy(name.strip_suffix(&[0]).unwrap_or(&name)).into_owned();
88 let size = read_u32(&mut reader, path)? as i64;
89 entries.push((name, size, index));
90 }
91 Ok((header, ChrMap::from_indexed_entries(entries)))
92}
93
94const MAX_TEXT: usize = 256 << 20;
98
99const MAX_NAME: usize = 64 << 10;
103
104fn read_sized(
112 reader: &mut impl Read,
113 len: usize,
114 max: usize,
115 what: &str,
116 path: &str,
117) -> Result<Vec<u8>> {
118 if len > max {
119 return Err(Error::corrupt(
120 path,
121 0,
122 format!("bam {what} declares {len} bytes, more than the {max} this reader allows"),
123 ));
124 }
125 let mut buf = Vec::new();
126 reader
127 .take(len as u64)
128 .read_to_end(&mut buf)
129 .map_err(|e| Error::corrupt(path, 0, format!("bam header ended early: {e}")))?;
130 if buf.len() != len {
131 return Err(Error::corrupt(
132 path,
133 0,
134 format!(
135 "bam {what} declares {len} bytes and the file holds {}",
136 buf.len()
137 ),
138 ));
139 }
140 Ok(buf)
141}
142
143fn read_exact(reader: &mut impl Read, buf: &mut [u8], path: &str) -> Result<()> {
144 reader
145 .read_exact(buf)
146 .map_err(|e| Error::corrupt(path, 0, format!("bam header ended early: {e}")))
147}
148
149fn read_u32(reader: &mut impl Read, path: &str) -> Result<u32> {
150 let mut buf = [0u8; 4];
151 read_exact(reader, &mut buf, path)?;
152 Ok(u32::from_le_bytes(buf))
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn header_lines_split_into_typed_fields() {
161 let header = SamHeader::parse("@HD\tVN:1.6\tSO:coordinate\n@SQ\tSN:chr1\tLN:1000\n");
162 assert_eq!(header.lines.len(), 2);
163 assert_eq!(header.lines[0].kind, "HD");
164 assert_eq!(header.lines[0].fields[1].tag, "SO");
165 assert_eq!(header.lines[0].fields[1].value, "coordinate");
166 assert_eq!(header.lines[1].fields[0].value, "chr1");
167 }
168
169 #[test]
170 fn a_value_carrying_colons_keeps_all_but_the_first() {
171 let header = SamHeader::parse("@PG\tID:x\tCL:bwa mem -R @RG\\tID:1 ref.fa\n");
173 assert_eq!(header.lines[0].fields[1].tag, "CL");
174 assert!(header.lines[0].fields[1].value.contains("ID:1"));
175 }
176
177 #[test]
178 fn a_field_with_no_colon_is_kept_rather_than_dropped() {
179 let header = SamHeader::parse("@CO\tsome free text\n");
180 assert_eq!(header.lines[0].kind, "CO");
181 assert_eq!(header.lines[0].fields[0].tag, "");
182 assert_eq!(header.lines[0].fields[0].value, "some free text");
183 }
184
185 #[test]
186 fn lines_that_are_not_header_lines_are_skipped() {
187 let header = SamHeader::parse("@HD\tVN:1.6\nnot a header line\n\n@SQ\tSN:chr1\n");
188 assert_eq!(header.lines.len(), 2);
189 assert_eq!(header.lines[1].kind, "SQ");
190 }
191
192 #[test]
193 fn an_empty_header_parses_to_nothing() {
194 assert!(SamHeader::parse("").lines.is_empty());
195 }
196}