1use indexmap::IndexMap;
7
8use crate::bam::bgzf::{Chunk, VirtualOffset};
9use crate::bytes::LeCursor;
10use crate::error::{Error, Result};
11use crate::source::ByteSource;
12
13pub const MAGIC_BIN: u32 = 37450;
15
16const BAI_MAX_POSITION: u64 = 1 << 29;
22
23pub const MAX_MERGE_SPAN: u64 = 64 * 1024 * 1024;
31
32#[derive(Debug, Clone, Default)]
33pub struct RefIndex {
34 pub bins: IndexMap<u32, Vec<Chunk>>,
36 pub linear: Vec<VirtualOffset>,
39 pub metadata: Option<RefMetadata>,
41}
42
43#[derive(Debug, Clone, Copy)]
44pub struct RefMetadata {
45 pub ref_start: VirtualOffset,
46 pub ref_end: VirtualOffset,
47 pub mapped: u64,
48 pub unmapped: u64,
49}
50
51#[derive(Debug, Clone, Default)]
52pub struct BamIndex {
53 pub refs: Vec<RefIndex>,
54 pub unplaced_count: Option<u64>,
56}
57
58impl BamIndex {
59 pub fn read(source: &dyn ByteSource) -> Result<Self> {
61 let path = source.path();
62 let all = source.read_to_end(0)?;
63 let mut c = LeCursor::new(&all, 0, path);
64
65 if c.take(4)? != b"BAI\x01" {
66 return Err(Error::format(path, "invalid bam index magic"));
67 }
68 let n_ref = c.read_u32()? as usize;
69 let mut refs = Vec::with_capacity(n_ref.min(1 << 16));
70 for _ in 0..n_ref {
71 let mut index = RefIndex::default();
72 let n_bin = c.read_u32()? as usize;
73 for _ in 0..n_bin {
74 let bin = c.read_u32()?;
75 let n_chunk = c.read_u32()? as usize;
76 if bin == MAGIC_BIN {
77 if n_chunk != 2 {
78 return Err(Error::corrupt(
79 path,
80 c.file_offset(),
81 "invalid metadata pseudo-bin",
82 ));
83 }
84 index.metadata = Some(RefMetadata {
85 ref_start: VirtualOffset(c.read_u64()?),
86 ref_end: VirtualOffset(c.read_u64()?),
87 mapped: c.read_u64()?,
88 unmapped: c.read_u64()?,
89 });
90 continue;
91 }
92 let mut chunks = Vec::with_capacity(n_chunk.min(1 << 16));
93 for _ in 0..n_chunk {
94 chunks.push(Chunk {
95 begin: VirtualOffset(c.read_u64()?),
96 end: VirtualOffset(c.read_u64()?),
97 });
98 }
99 index.bins.insert(bin, chunks);
100 }
101 let n_intv = c.read_u32()? as usize;
102 index.linear = Vec::with_capacity(n_intv.min(1 << 20));
103 for _ in 0..n_intv {
104 index.linear.push(VirtualOffset(c.read_u64()?));
105 }
106 refs.push(index);
107 }
108 let unplaced_count = if c.remaining() >= 8 {
110 Some(c.read_u64()?)
111 } else {
112 None
113 };
114 Ok(Self {
115 refs,
116 unplaced_count,
117 })
118 }
119
120 pub fn chunks(
123 &self,
124 ref_index: usize,
125 start: i64,
126 end: i64,
127 max_merge_span: Option<u64>,
128 ) -> Result<Vec<Chunk>> {
129 let reference = self
130 .refs
131 .get(ref_index)
132 .ok_or_else(|| Error::invalid(format!("ref index {ref_index} out of range")))?;
133
134 if end < start {
139 return Err(Error::invalid(format!(
140 "Locus {start}-{end} ends before it starts"
141 )));
142 }
143 let bounded_start = start.max(0) as u64;
148 let bounded_end = (end.max(0) as u64).min(BAI_MAX_POSITION);
149 if bounded_start >= BAI_MAX_POSITION || bounded_end <= bounded_start {
150 return Ok(Vec::new());
151 }
152
153 let min_offset = if reference.linear.is_empty() {
155 VirtualOffset(0)
156 } else {
157 let window = (bounded_start >> 14) as usize;
158 *reference
162 .linear
163 .get(window)
164 .unwrap_or_else(|| reference.linear.last().expect("checked non-empty"))
165 };
166
167 let mut chunks: Vec<Chunk> = Vec::new();
168 for bin in reg2bins(bounded_start, bounded_end) {
169 let Some(bin_chunks) = reference.bins.get(&bin) else {
170 continue;
171 };
172 chunks.extend(bin_chunks.iter().copied().filter(|c| c.end >= min_offset));
173 }
174 chunks.sort_by_key(|c| c.begin);
175 Ok(merge(chunks, max_merge_span))
176 }
177}
178
179fn merge(chunks: Vec<Chunk>, max_merge_span: Option<u64>) -> Vec<Chunk> {
182 if chunks.len() <= 1 {
183 return chunks;
184 }
185 let mut merged: Vec<Chunk> = Vec::with_capacity(chunks.len());
186 merged.push(chunks[0]);
187 for current in &chunks[1..] {
188 let last = merged.last_mut().expect("pushed one above");
189 if current.begin < last.end {
190 last.end = last.end.max(current.end);
193 continue;
194 }
195 let span = last.end.max(current.end).block_offset() - last.begin.block_offset();
198 let within_budget = max_merge_span.is_none_or(|budget| span <= budget);
199 if current.begin == last.end && within_budget {
200 last.end = last.end.max(current.end);
203 } else {
204 merged.push(*current);
205 }
206 }
207 merged
208}
209
210pub fn reg2bins(start: u64, end: u64) -> Vec<u32> {
217 let mut bins = Vec::new();
218 if end <= start {
219 return bins;
220 }
221 let end = end - 1;
222 bins.push(0);
223 for (offset, shift) in [(1u64, 26u32), (9, 23), (73, 20), (585, 17), (4681, 14)] {
224 for bin in (offset + (start >> shift))..=(offset + (end >> shift)) {
225 bins.push(bin as u32);
226 }
227 }
228 bins
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::source::testing::MemorySource;
235
236 fn vo(block: u64, within: u16) -> VirtualOffset {
237 VirtualOffset::new(block, within)
238 }
239
240 fn chunk(a: u64, b: u64) -> Chunk {
241 Chunk {
242 begin: vo(a, 0),
243 end: vo(b, 0),
244 }
245 }
246
247 fn bai(bins: &[(u32, &[Chunk])], linear: &[VirtualOffset], tail: bool) -> Vec<u8> {
249 let mut b = b"BAI\x01".to_vec();
250 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&(bins.len() as u32).to_le_bytes());
252 for (bin, chunks) in bins {
253 b.extend_from_slice(&bin.to_le_bytes());
254 b.extend_from_slice(&(chunks.len() as u32).to_le_bytes());
255 for c in *chunks {
256 b.extend_from_slice(&c.begin.0.to_le_bytes());
257 b.extend_from_slice(&c.end.0.to_le_bytes());
258 }
259 }
260 b.extend_from_slice(&(linear.len() as u32).to_le_bytes());
261 for offset in linear {
262 b.extend_from_slice(&offset.0.to_le_bytes());
263 }
264 if tail {
265 b.extend_from_slice(&42u64.to_le_bytes());
266 }
267 b
268 }
269
270 #[test]
271 fn reads_bins_the_linear_index_and_the_optional_tail() {
272 let bytes = bai(
273 &[(4681, &[chunk(100, 200)]), (4682, &[chunk(200, 300)])],
274 &[vo(0, 0), vo(100, 0)],
275 true,
276 );
277 let index = BamIndex::read(&MemorySource::new(bytes)).unwrap();
278 assert_eq!(index.refs.len(), 1);
279 assert_eq!(index.refs[0].bins.len(), 2);
280 assert_eq!(index.refs[0].linear.len(), 2);
281 assert_eq!(index.unplaced_count, Some(42));
282 }
283
284 #[test]
285 fn the_optional_tail_really_is_optional() {
286 let bytes = bai(&[(4681, &[chunk(100, 200)])], &[vo(0, 0)], false);
287 let index = BamIndex::read(&MemorySource::new(bytes)).unwrap();
288 assert_eq!(index.unplaced_count, None);
289 }
290
291 #[test]
292 fn the_metadata_pseudo_bin_is_not_a_bin_of_chunks() {
293 let mut b = b"BAI\x01".to_vec();
294 b.extend_from_slice(&1u32.to_le_bytes());
295 b.extend_from_slice(&2u32.to_le_bytes()); b.extend_from_slice(&4681u32.to_le_bytes());
297 b.extend_from_slice(&1u32.to_le_bytes());
298 b.extend_from_slice(&vo(100, 0).0.to_le_bytes());
299 b.extend_from_slice(&vo(200, 0).0.to_le_bytes());
300 b.extend_from_slice(&MAGIC_BIN.to_le_bytes());
301 b.extend_from_slice(&2u32.to_le_bytes());
302 for value in [7u64, 8, 9, 10] {
303 b.extend_from_slice(&value.to_le_bytes());
304 }
305 b.extend_from_slice(&0u32.to_le_bytes()); let index = BamIndex::read(&MemorySource::new(b)).unwrap();
308 assert_eq!(index.refs[0].bins.len(), 1, "the pseudo-bin is not a bin");
309 let meta = index.refs[0].metadata.unwrap();
310 assert_eq!((meta.mapped, meta.unmapped), (9, 10));
311 }
312
313 #[test]
314 fn a_bad_magic_is_refused() {
315 let err = BamIndex::read(&MemorySource::new(b"NOPE".to_vec()))
316 .unwrap_err()
317 .to_string();
318 assert!(err.contains("invalid bam index magic"), "{err}");
319 }
320
321 #[test]
322 fn a_truncated_index_is_corrupt_not_a_panic() {
323 let mut bytes = bai(&[(4681, &[chunk(100, 200)])], &[vo(0, 0)], false);
324 bytes.truncate(bytes.len() - 5);
325 assert!(matches!(
326 BamIndex::read(&MemorySource::new(bytes)),
327 Err(Error::Corrupt { .. })
328 ));
329 }
330
331 #[test]
332 fn reg2bins_covers_every_level_and_refuses_an_empty_region() {
333 assert!(reg2bins(100, 100).is_empty());
334 assert!(reg2bins(100, 50).is_empty());
335 assert_eq!(reg2bins(0, 16384), [0, 1, 9, 73, 585, 4681]);
337 assert_eq!(reg2bins(0, 16385), [0, 1, 9, 73, 585, 4681, 4682]);
339 }
340
341 #[test]
342 fn a_region_past_what_the_index_addresses_asks_for_nothing() {
343 let index = BamIndex::read(&MemorySource::new(bai(
344 &[(4681, &[chunk(100, 200)])],
345 &[vo(0, 0)],
346 false,
347 )))
348 .unwrap();
349 assert!(index
351 .chunks(0, 1_000_000_000_000, 1_000_000_001_000, None)
352 .unwrap()
353 .is_empty());
354 assert!(index
355 .chunks(0, 1 << 30, (1 << 30) + 10, None)
356 .unwrap()
357 .is_empty());
358 }
359
360 #[test]
361 fn the_linear_index_drops_chunks_that_end_before_the_region_can_start() {
362 let index = BamIndex::read(&MemorySource::new(bai(
365 &[(4681, &[chunk(50, 100)])],
366 &[vo(500, 0)],
367 false,
368 )))
369 .unwrap();
370 assert!(index.chunks(0, 0, 1000, None).unwrap().is_empty());
371 }
372
373 #[test]
374 fn overlapping_chunks_merge_whatever_the_budget() {
375 assert_eq!(
376 merge(vec![chunk(0, 200), chunk(100, 300)], Some(0)),
377 [chunk(0, 300)]
378 );
379 }
380
381 #[test]
382 fn adjacent_chunks_merge_only_inside_the_budget() {
383 assert_eq!(
385 merge(vec![chunk(0, 100), chunk(100, 200)], Some(1000)),
386 [chunk(0, 200)]
387 );
388 assert_eq!(
390 merge(vec![chunk(0, 100), chunk(100, 200)], Some(50)),
391 [chunk(0, 100), chunk(100, 200)]
392 );
393 assert_eq!(
395 merge(vec![chunk(0, 100), chunk(100, 200)], None),
396 [chunk(0, 200)]
397 );
398 }
399
400 #[test]
401 fn chunks_with_a_gap_between_them_never_merge() {
402 let apart = vec![chunk(0, 100), chunk(500, 600)];
403 assert_eq!(merge(apart.clone(), None), apart);
404 }
405
406 #[test]
407 fn an_out_of_range_reference_is_refused_by_name() {
408 let index = BamIndex::default();
409 let err = index.chunks(3, 0, 10, None).unwrap_err().to_string();
410 assert!(err.contains("ref index 3 out of range"), "{err}");
411 }
412}