Skip to main content

gwseq_io/cram/
reader.rs

1//! The CRAM reader.
2//!
3//! Four read paths over the same machinery, matching the BAM reader's: the
4//! index turns a locus into slices, a slice decodes into BAM records, and the
5//! filter decides which of them the caller asked for.
6//!
7//! # Why the unit of work is a slice
8//!
9//! A CRAM slice cannot be entered partway. `AP` is delta-coded from the
10//! slice's own start, the entropy coders run the length of a block, and `NF`
11//! reaches from one record to another within the slice — so ten thousand
12//! records come back or none do. Unlike a BGZF chunk there is nothing smaller
13//! to seek to, which makes the slice the unit of caching as well as of work:
14//! two loci landing in one slice decode it once.
15//!
16//! # Two reads per query, and what is memoised
17//!
18//! §12: a slice cannot be decoded without its container's compression header,
19//! and the index gives a slice's offset *relative to the end of its container
20//! header* — so reaching a slice means reading its container header first.
21//! Both the container header and the compression header parsed out of it are
22//! memoised per container, because every slice in a container wants the same
23//! ones and parsing a compression header is two maps of encodings.
24
25use std::sync::Arc;
26
27use bytes::Bytes;
28use parking_lot::Mutex;
29
30use crate::bam::header::{HeaderLine, SamHeader};
31use crate::bam::record::{decode_block, BamRecord, EntryFilter};
32use crate::bam::EntriesRequest;
33use crate::error::{Error, Result};
34use crate::genomic::{ChrMap, Locs};
35use crate::parallel::Executor;
36use crate::progress::ProgressTracker;
37use crate::source::ByteSource;
38
39use super::compression::CompressionHeader;
40use super::container::{
41    Block, BlockContentType, ContainerHeader, FileDefinition, EOF_CONTAINER, FILE_DEFINITION_SIZE,
42};
43use super::crai::{CramIndex, IndexEntry};
44use super::record::{decode_slice, ReferenceBases, References};
45use super::reference::{resolve, Reference, ReferenceSource};
46use super::slice::Slice;
47
48/// Container headers kept parsed. A container is a few hundred kilobytes, so
49/// this is a handful of megabytes of reach for a few kilobytes of state.
50const CONTAINER_CACHE: usize = 8;
51
52/// Decoded slices kept, in bytes of BAM records. A slice of ten thousand
53/// hundred-base reads is roughly 2 MB, so this holds a few dozen.
54const SLICE_CACHE_BYTES: usize = 64 << 20;
55
56#[derive(Debug)]
57struct Inner {
58    source: Arc<dyn ByteSource>,
59    executor: Executor,
60    /// The `.crai`, when one was found beside the file.
61    index: Option<Arc<CramIndex>>,
62    /// One built by walking container headers, when there was no `.crai`.
63    /// Built on first use rather than at open, so opening a file for its header
64    /// costs one read.
65    built: Mutex<Option<Arc<CramIndex>>>,
66    reference: Option<ReferenceSource>,
67    caches: Mutex<Caches>,
68    /// Woken whenever a slice decode finishes, so a worker that found one
69    /// already in flight can take the result instead of decoding it again.
70    slice_ready: parking_lot::Condvar,
71}
72
73/// What a reader holds between calls.
74#[derive(Debug, Default)]
75struct Caches {
76    /// `(container offset, header, compression header)`, most recent last.
77    containers: Vec<(u64, Arc<ContainerHeader>, Arc<CompressionHeader>)>,
78    /// `(container offset, landmark, records)`, most recent last.
79    slices: Vec<(u64, u64, Bytes)>,
80    slice_bytes: usize,
81    /// Slices being decoded right now, by `(container offset, landmark)`.
82    ///
83    /// Without this, `parallel` workers that miss the same slice at the same
84    /// moment all decode it and all insert a copy — each one counted against
85    /// the cache's byte budget, so the duplicates then evict what the next
86    /// locus needs. Adjacent loci share slices routinely, so this is the
87    /// common case rather than a race worth ignoring.
88    in_flight: Vec<(u64, u64)>,
89}
90
91pub struct CramReader {
92    inner: Option<Inner>,
93    path: String,
94    index_path: String,
95    header: SamHeader,
96    chr_map: ChrMap,
97    /// Reference names by index, shared with every record.
98    chr_names: Arc<Vec<String>>,
99    /// `@RG` identifiers by their position in the header, for rebuilding the
100    /// `RG` tag the encoder was free to drop.
101    read_groups: Vec<String>,
102    version: (u8, u8),
103    index_error: String,
104    indexed: bool,
105    reference: Reference,
106    reference_error: String,
107}
108
109impl std::fmt::Debug for CramReader {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("CramReader")
112            .field("path", &self.path)
113            .field("version", &self.version)
114            .field("references", &self.chr_map.len())
115            .field("reference", &self.reference.path())
116            .field("closed", &self.is_closed())
117            .finish()
118    }
119}
120
121impl CramReader {
122    pub fn open(
123        path: &str,
124        index_path: Option<&str>,
125        reference: Option<&str>,
126        parallel: i64,
127        block_size: Option<u64>,
128        max_blocks: Option<usize>,
129    ) -> Result<Self> {
130        let source = crate::source::open(path, block_size, max_blocks)?;
131        Self::from_source(
132            source, path, index_path, reference, parallel, block_size, max_blocks,
133        )
134    }
135
136    pub(crate) fn from_source(
137        source: Arc<dyn ByteSource>,
138        path: &str,
139        index_path: Option<&str>,
140        reference: Option<&str>,
141        parallel: i64,
142        block_size: Option<u64>,
143        max_blocks: Option<usize>,
144    ) -> Result<Self> {
145        let definition = FileDefinition::parse(&source.read_at(0, FILE_DEFINITION_SIZE)?, path)?;
146        let (header, chr_map, read_groups) = read_header(source.as_ref(), path)?;
147
148        let mut names = vec![String::new(); chr_map.iter().map(|e| e.index + 1).max().unwrap_or(0)];
149        for entry in chr_map.iter() {
150            names[entry.index] = entry.id.clone();
151        }
152
153        // The index, as the BAM reader treats its own: absent is no error, and
154        // unreadable is a different failure worth reporting separately.
155        let index_path_given = index_path;
156        let index_path = index_path
157            .map(str::to_string)
158            .unwrap_or_else(|| format!("{path}.crai"));
159        // A named index that is not there is the caller's mistake and is said
160        // so; the default `<file>.crai` simply not existing is not, and falls
161        // through to building one. The BAM reader draws the same line.
162        let named = index_path_given.is_some();
163        let (index, index_error) =
164            if !crate::source::is_url(&index_path) && !std::path::Path::new(&index_path).exists() {
165                (
166                    None,
167                    if named {
168                        format!("{index_path} is not there")
169                    } else {
170                        String::new()
171                    },
172                )
173            } else {
174                match crate::source::open(&index_path, block_size, max_blocks)
175                    .and_then(|s| s.read_to_end(0))
176                    .and_then(|data| CramIndex::parse(&data, &index_path))
177                {
178                    Ok(index) => (Some(Arc::new(index)), String::new()),
179                    Err(e) => (None, e.to_string()),
180                }
181            };
182
183        // §9's EOF container is the only thing that says a file is whole.
184        // Without it the file was cut, and the symptom is a query that finds
185        // nothing where it should have found the missing tail — reported here
186        // as information, the way `samtools` warns "EOF marker is absent",
187        // rather than as a refusal, because everything before the cut still
188        // reads.
189        let truncated = match source.len() {
190            Ok(length) if length >= EOF_CONTAINER.len() as u64 => {
191                let at = length - EOF_CONTAINER.len() as u64;
192                !matches!(source.read_at(at, EOF_CONTAINER.len()), Ok(tail) if tail[..] == EOF_CONTAINER[..])
193            }
194            _ => true,
195        };
196
197        // The reference. A file that cannot find one still opens and still
198        // answers everything but `sequence`, which is what `reference_error`
199        // is for — the same shape `index_error` already has.
200        // One reference window per worker, at least: `parallel` workers on
201        // different loci otherwise evict each other's windows on every locus.
202        let slots = if parallel <= 0 {
203            std::thread::available_parallelism().map_or(4, |n| n.get())
204        } else {
205            parallel as usize
206        };
207        let sequences = sequence_details(&header);
208        let mut resolved = resolve(reference, &sequences);
209        let (mut reference_source, mut reference_error) = match &resolved {
210            // The cache is a directory of bare sequences named by checksum,
211            // not a FASTA, so it needs the `@SQ` lines to know which file is
212            // which sequence.
213            Reference::FromCache(root) => (
214                Some(ReferenceSource::open_cache(root, &sequences).with_slots(slots)),
215                String::new(),
216            ),
217            Reference::Given(path) | Reference::FromHeader(path) => {
218                match ReferenceSource::open(path) {
219                    Ok(source) => (Some(source.with_slots(slots)), String::new()),
220                    Err(e) => (None, e.to_string()),
221                }
222            }
223            Reference::None(why) => (None, why.clone()),
224        };
225
226        // A reference that opened is not yet a reference that *fits*. Without
227        // this check, pointing at the wrong FASTA gives a populated
228        // `reference_path`, an empty `reference_error` and every base `N` —
229        // which is exactly what a read with no substitutions looks like, so a
230        // caller has no way to learn the reference was never used. `samtools`
231        // fails the read outright.
232        if let Some(source) = &reference_source {
233            if let Some(complaint) = reference_complaint(source, &chr_map, &sequences) {
234                reference_source = None;
235                // `reference_path` has to go with it: a path *and* an error is
236                // a contradiction, and a caller reading only the path would
237                // conclude a reference was used.
238                resolved = Reference::None(complaint.clone());
239                reference_error = complaint;
240            }
241        }
242
243        // A `.crai` beside the file means indexed; so does a local file, whose
244        // index can be built from its container headers on demand. A remote
245        // file with no `.crai` cannot, because building means a request per
246        // container.
247        let indexed = index.is_some() || !crate::source::is_url(path);
248        // A local file whose `.crai` would not parse still has an index — the
249        // one `build` makes from its container headers — so the parse failure
250        // is information rather than a refusal. Reported and then set aside;
251        // before this it was returned from every read, which made a corrupt
252        // index worse than no index at all.
253        let index_error = if truncated && index_error.is_empty() {
254            format!(
255                "{path} has no EOF container, so it was cut short; anything past the cut is \
256                 missing and a locus there will find nothing"
257            )
258        } else if index.is_none() && !index_error.is_empty() && !crate::source::is_url(path) {
259            format!("{index_error} — reading it from the file's containers instead")
260        } else if indexed || !index_error.is_empty() {
261            index_error
262        } else {
263            format!(
264                "{index_path} is not there, and this file is remote, so an index cannot be \
265                 built by walking its containers"
266            )
267        };
268
269        Ok(Self {
270            inner: Some(Inner {
271                source,
272                executor: Executor::new(parallel)?,
273                index,
274                built: Mutex::new(None),
275                reference: reference_source,
276                caches: Mutex::new(Caches::default()),
277                slice_ready: parking_lot::Condvar::new(),
278            }),
279            path: path.to_string(),
280            index_path,
281            header,
282            chr_map,
283            chr_names: Arc::new(names),
284            read_groups,
285            version: (definition.major, definition.minor),
286            index_error,
287            indexed,
288            reference: resolved,
289            reference_error,
290        })
291    }
292
293    pub fn header(&self) -> &SamHeader {
294        &self.header
295    }
296    pub fn chr_sizes(&self) -> &ChrMap {
297        &self.chr_map
298    }
299    pub fn index_error(&self) -> &str {
300        &self.index_error
301    }
302    pub fn is_indexed(&self) -> bool {
303        self.indexed
304    }
305    pub fn is_closed(&self) -> bool {
306        self.inner.is_none()
307    }
308    pub fn path(&self) -> &str {
309        &self.path
310    }
311    pub fn parallel(&self) -> usize {
312        self.inner.as_ref().map_or(0, |i| i.executor.parallel())
313    }
314    /// The CRAM version, as `(major, minor)`.
315    pub fn version(&self) -> (u8, u8) {
316        self.version
317    }
318    /// The reference in use, or `None` when none resolved.
319    pub fn reference_path(&self) -> Option<&str> {
320        self.reference.path()
321    }
322    /// Why there is no reference, when there is none. Empty when one resolved.
323    pub fn reference_error(&self) -> &str {
324        &self.reference_error
325    }
326    pub fn close(&mut self) {
327        if let Some(inner) = self.inner.take() {
328            inner.source.close();
329        }
330    }
331
332    fn inner(&self) -> Result<&Inner> {
333        self.inner.as_ref().ok_or_else(|| Error::Closed {
334            path: self.path.clone(),
335        })
336    }
337
338    /// The index: the `.crai`, or one built from the container headers.
339    fn index(&self, inner: &Inner) -> Result<Arc<CramIndex>> {
340        if let Some(index) = &inner.index {
341            return Ok(index.clone());
342        }
343        // Only a remote file has nothing to fall back on: building an index
344        // means a request per container. A local file with an unreadable
345        // `.crai` builds one, and `index_error` says that is what happened.
346        if !self.index_error.is_empty() && crate::source::is_url(&self.path) {
347            return Err(Error::invalid(format!(
348                "cram index {} could not be read: {}",
349                self.index_path, self.index_error
350            )));
351        }
352        let mut built = inner.built.lock();
353        if let Some(index) = built.as_ref() {
354            return Ok(index.clone());
355        }
356        let index = Arc::new(CramIndex::build(inner.source.as_ref())?);
357        *built = Some(index.clone());
358        Ok(index)
359    }
360
361    /// One list per locus, in the order the loci were given.
362    pub fn read_entries(&self, req: &EntriesRequest) -> Result<Vec<Vec<BamRecord>>> {
363        let inner = self.inner()?;
364        let resolved = self.resolve(&req.locs)?;
365        let coverage = resolved.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
366        let tracker = ProgressTracker::with_callback(coverage, req.progress.clone());
367        let index = self.index(inner)?;
368
369        // Loci are spread over the workers in runs rather than round-robin, so
370        // each worker walks the request forward and the slices it decoded for
371        // one locus serve the ones after it.
372        let workers = inner.executor.parallel().min(resolved.len().max(1)).max(1);
373        let per_worker = resolved.len().div_ceil(workers).max(1);
374        let batches: Vec<(usize, usize)> = (0..workers)
375            .map(|w| (w * per_worker, ((w + 1) * per_worker).min(resolved.len())))
376            .filter(|(from, to)| from < to)
377            .collect();
378
379        let lists = inner.executor.map_batches(&batches, |_, (from, to)| {
380            let mut out = Vec::with_capacity(to - from);
381            for locus in &resolved[*from..*to] {
382                out.push(self.read_locus(inner, &index, *locus, req)?);
383                tracker.add((locus.2 - locus.1).max(0) as u64);
384            }
385            Ok(out)
386        })?;
387        tracker.done_report();
388        Ok(lists.into_iter().flatten().collect())
389    }
390
391    /// Every alignment on the named references, in reference then coordinate
392    /// order.
393    pub fn read_all_entries(&self, req: &EntriesRequest) -> Result<Vec<BamRecord>> {
394        let locs = Locs::whole_chromosomes(&self.chr_map, &req.locs.chr_ids)?;
395        let whole = EntriesRequest {
396            locs,
397            ..req.clone()
398        };
399        Ok(self.read_entries(&whole)?.into_iter().flatten().collect())
400    }
401
402    /// Per-locus, lazily.
403    pub fn iter_entries(&self, req: &EntriesRequest) -> Result<LocusEntries<'_>> {
404        LocusEntries::plan(self, req)
405    }
406
407    /// Successive windows over whole references.
408    pub fn iter_all_entries(&self, req: &EntriesRequest, window: i64) -> Result<WindowEntries<'_>> {
409        WindowEntries::plan(self, req, window)
410    }
411
412    fn resolve(&self, locs: &Locs) -> Result<Vec<(usize, i64, i64)>> {
413        (0..locs.len())
414            .map(|i| {
415                let entry = self.chr_map.resolve(&locs.chr_ids[i])?;
416                Ok((entry.index, locs.starts[i], locs.ends[i]))
417            })
418            .collect()
419    }
420
421    /// The alignments of one locus.
422    fn read_locus(
423        &self,
424        inner: &Inner,
425        index: &CramIndex,
426        locus: (usize, i64, i64),
427        req: &EntriesRequest,
428    ) -> Result<Vec<BamRecord>> {
429        let (chr, start, end) = locus;
430        let filter = EntryFilter {
431            chr_index: Some(chr as i32),
432            start,
433            end: Some(end),
434            standard_flags: req.filter.enabled,
435        };
436        let mut out = Vec::new();
437        for entry in index.slices(chr as i32, start, end) {
438            let records = self.slice_records(inner, &entry)?;
439            out.extend(decode_block(
440                &records,
441                req.parse_tags,
442                &filter,
443                &self.chr_names,
444                &self.path,
445            )?);
446        }
447        Ok(out)
448    }
449
450    /// One slice's records as BAM bytes, from the cache or from the file.
451    fn slice_records(&self, inner: &Inner, entry: &IndexEntry) -> Result<Bytes> {
452        let key = (entry.container_offset, entry.landmark);
453        {
454            let mut caches = inner.caches.lock();
455            loop {
456                if let Some(position) = caches.slices.iter().position(|(c, l, _)| (*c, *l) == key) {
457                    // Move to the back, so the cache evicts what has gone
458                    // longest unused rather than what was read longest ago.
459                    let hit = caches.slices.remove(position);
460                    let records = hit.2.clone();
461                    caches.slices.push(hit);
462                    return Ok(records);
463                }
464                if !caches.in_flight.contains(&key) {
465                    caches.in_flight.push(key);
466                    break;
467                }
468                // Another worker is decoding this one. Wait for it rather than
469                // doing the same work: a slice decode is milliseconds and the
470                // whole point of the cache.
471                inner.slice_ready.wait(&mut caches);
472            }
473        }
474        // From here the marker is ours and must come off whichever way this
475        // goes, or every later reader of this slice waits forever.
476        let outcome = self.decode_one_slice(inner, entry);
477        let mut caches = inner.caches.lock();
478        caches.in_flight.retain(|held| *held != key);
479        if let Ok(records) = &outcome {
480            // Never twice: a key already present would be counted against the
481            // byte budget twice over.
482            if !caches.slices.iter().any(|(c, l, _)| (*c, *l) == key) {
483                caches.slice_bytes += records.len();
484                caches.slices.push((key.0, key.1, records.clone()));
485                while caches.slice_bytes > SLICE_CACHE_BYTES && caches.slices.len() > 1 {
486                    let (_, _, dropped) = caches.slices.remove(0);
487                    caches.slice_bytes -= dropped.len();
488                }
489            }
490        }
491        drop(caches);
492        inner.slice_ready.notify_all();
493        outcome
494    }
495
496    /// Read and decode one slice, with no cache involved.
497    fn decode_one_slice(&self, inner: &Inner, entry: &IndexEntry) -> Result<Bytes> {
498        let (container, compression) = self.container(inner, entry.container_offset)?;
499        let offset = container.blocks_offset() + entry.landmark;
500        let slice = Slice::read(inner.source.as_ref(), offset, entry.size)?;
501
502        // A slice on one reference gets its window fetched once, up front. A
503        // multi-reference slice gets the source itself, and looks a reference
504        // up per record — see `References`.
505        let window = self.reference_window(inner, &slice)?;
506        let references = match (&window, &inner.reference) {
507            (Some((bases, start)), _) => References::Fixed(ReferenceBases {
508                bases,
509                // 1-based, as the record decoder works in.
510                start: start + 1,
511            }),
512            (None, Some(source)) if slice.header.is_multi_ref() => References::ByRefId {
513                source,
514                names: &self.chr_names,
515            },
516            _ => References::None,
517        };
518        decode_slice(
519            &slice,
520            &compression,
521            references,
522            &self.read_groups,
523            &self.path,
524        )
525    }
526
527    /// The container header at `offset` and its compression header, memoised.
528    fn container(
529        &self,
530        inner: &Inner,
531        offset: u64,
532    ) -> Result<(Arc<ContainerHeader>, Arc<CompressionHeader>)> {
533        {
534            let mut caches = inner.caches.lock();
535            if let Some(position) = caches.containers.iter().position(|(at, ..)| *at == offset) {
536                let hit = caches.containers.remove(position);
537                let out = (hit.1.clone(), hit.2.clone());
538                caches.containers.push(hit);
539                return Ok(out);
540            }
541        }
542
543        let header = ContainerHeader::read(inner.source.as_ref(), offset)?;
544        // §5: the first block of a container is its compression header.
545        let first = header
546            .landmarks
547            .first()
548            .copied()
549            .map(|landmark| landmark.max(0) as usize)
550            .unwrap_or(header.length.max(0) as usize);
551        let data = inner
552            .source
553            .read_exact_at(header.blocks_offset(), first.max(1))?;
554        let block = Block::parse(&data, header.blocks_offset(), &self.path)?;
555        if block.content_type != BlockContentType::CompressionHeader {
556            return Err(Error::corrupt(
557                &self.path,
558                header.blocks_offset(),
559                format!(
560                    "the first block of a container is {:?}, not its compression header",
561                    block.content_type
562                ),
563            ));
564        }
565        let compression = Arc::new(CompressionHeader::parse(&block.data, &self.path)?);
566        let header = Arc::new(header);
567
568        let mut caches = inner.caches.lock();
569        caches
570            .containers
571            .push((offset, header.clone(), compression.clone()));
572        while caches.containers.len() > CONTAINER_CACHE {
573            caches.containers.remove(0);
574        }
575        Ok((header, compression))
576    }
577
578    /// The one window a single-reference slice needs, fetched before the
579    /// decode.
580    ///
581    /// `None` means there is no single window to fetch — the slice is unmapped,
582    /// or it is multi-reference and its records are resolved one at a time.
583    fn reference_window(
584        &self,
585        inner: &Inner,
586        slice: &Slice,
587    ) -> Result<Option<(Arc<Vec<u8>>, i64)>> {
588        // A slice carrying its own reference needs nothing from outside.
589        if let Some(embedded) = &slice.embedded_reference {
590            let start = slice.header.range().map(|(from, _)| from).unwrap_or(0);
591            return Ok(Some((Arc::new(embedded.to_vec()), start)));
592        }
593        let Some(reference) = &inner.reference else {
594            return Ok(None);
595        };
596        let Some((start, end)) = slice.header.range() else {
597            return Ok(None);
598        };
599        let Some(name) = self.chr_names.get(slice.header.ref_id.max(0) as usize) else {
600            return Ok(None);
601        };
602        if !reference.has(name) {
603            return Ok(None);
604        }
605        Ok(Some(reference.window(name, start, end)?))
606    }
607}
608
609/// Read the CRAM header container: a SAM text header, and the references and
610/// read groups read out of it.
611///
612/// Unlike BAM, there is no binary reference list — the `@SQ` lines are the
613/// only statement of what reference a record's id means.
614fn read_header(source: &dyn ByteSource, path: &str) -> Result<(SamHeader, ChrMap, Vec<String>)> {
615    let container = ContainerHeader::read(source, FILE_DEFINITION_SIZE as u64)?;
616    let data = source.read_exact_at(container.blocks_offset(), container.length.max(0) as usize)?;
617    let block = Block::parse(&data, container.blocks_offset(), path)?;
618    if block.content_type != BlockContentType::FileHeader {
619        return Err(Error::corrupt(
620            path,
621            container.blocks_offset(),
622            format!(
623                "the first container holds a {:?} block where its header should be",
624                block.content_type
625            ),
626        ));
627    }
628    // §8.3: the block opens with a 32-bit length and then the SAM text.
629    if block.data.len() < 4 {
630        return Err(Error::corrupt(
631            path,
632            container.blocks_offset(),
633            "a header block too short to hold its own length",
634        ));
635    }
636    let length =
637        i32::from_le_bytes(block.data[..4].try_into().expect("four bytes")).max(0) as usize;
638    let text = &block.data[4..(4 + length).min(block.data.len())];
639    let header = SamHeader::parse(&String::from_utf8_lossy(text));
640
641    let mut entries = Vec::new();
642    let mut read_groups = Vec::new();
643    for line in &header.lines {
644        match line.kind.as_str() {
645            "SQ" => {
646                // Reference ids are positional over the `@SQ` lines, so
647                // skipping one renumbers every reference after it and every
648                // record then names the wrong chromosome. `LN` is mandatory in
649                // SAM, so a line without it is a broken header rather than a
650                // line to step over quietly.
651                let name = field(line, "SN");
652                let size = field(line, "LN").and_then(|v| v.parse::<i64>().ok());
653                match (name, size) {
654                    (Some(name), Some(size)) => entries.push((name.to_string(), size)),
655                    (name, _) => {
656                        return Err(Error::format(
657                            path,
658                            format!(
659                                "an @SQ line for {} has no usable LN, and reference ids are \
660                                 counted over these lines, so every later one would shift",
661                                name.unwrap_or("an unnamed sequence")
662                            ),
663                        ))
664                    }
665                }
666            }
667            "RG" => {
668                if let Some(id) = field(line, "ID") {
669                    read_groups.push(id.to_string());
670                }
671            }
672            _ => {}
673        }
674    }
675    let chr_map = ChrMap::from_entries(entries);
676    Ok((header, chr_map, read_groups))
677}
678
679/// `(name, UR, M5)` for each `@SQ` line, which is what a reference is resolved
680/// from.
681/// Why this reference does not fit this file, or `None` if it does.
682///
683/// Two questions, and the second is the cheap half of the `M5` check §11 asks
684/// for and this reader deliberately skips — hashing whole chromosomes at open
685/// is not worth it, but comparing their lengths costs nothing and catches the
686/// wrong assembly as well as the wrong names.
687///
688/// A partial match is not an error: a CRAM aligned against a full assembly may
689/// name scaffolds a trimmed FASTA leaves out, and every read on a sequence the
690/// FASTA does have is still perfectly readable. What is an error is *none* of
691/// them matching, which means the wrong file, and a length disagreement, which
692/// means the wrong version of the right file.
693fn reference_complaint(
694    source: &ReferenceSource,
695    chr_map: &ChrMap,
696    sequences: &[(String, Option<String>, Option<String>)],
697) -> Option<String> {
698    if sequences.is_empty() {
699        return None;
700    }
701    let mut found = 0usize;
702    for (name, ..) in sequences {
703        let Some(length) = source.length(name) else {
704            continue;
705        };
706        found += 1;
707        let declared = chr_map.get(name).map(|entry| entry.size);
708        if let Some(declared) = declared {
709            if declared != length {
710                return Some(format!(
711                    "this reference has {name} at {length} bases where the file's header says                      {declared}, so it is not the assembly these reads were aligned to"
712                ));
713            }
714        }
715    }
716    if found == 0 {
717        let named = sequences
718            .iter()
719            .take(3)
720            .map(|(name, ..)| name.as_str())
721            .collect::<Vec<_>>()
722            .join(", ");
723        return Some(format!(
724            "this reference holds none of the {} sequences this file names ({named}...), so              every sequence would read as N",
725            sequences.len()
726        ));
727    }
728    None
729}
730
731fn sequence_details(header: &SamHeader) -> Vec<(String, Option<String>, Option<String>)> {
732    header
733        .lines
734        .iter()
735        .filter(|line| line.kind == "SQ")
736        .filter_map(|line| {
737            Some((
738                field(line, "SN")?.to_string(),
739                field(line, "UR").map(str::to_string),
740                field(line, "M5").map(str::to_string),
741            ))
742        })
743        .collect()
744}
745
746fn field<'a>(line: &'a HeaderLine, tag: &str) -> Option<&'a str> {
747    line.fields
748        .iter()
749        .find(|f| f.tag == tag)
750        .map(|f| f.value.as_str())
751}
752
753/// What a walk holds: the loci, and where it is among them.
754struct WalkPlan {
755    loci: Vec<(usize, i64, i64)>,
756    order: Vec<usize>,
757    /// For a windowed walk, the start each window reports from — so an
758    /// alignment reaching over a boundary belongs to one window only.
759    min_starts: Vec<Option<i64>>,
760    request: EntriesRequest,
761    coverage: u64,
762}
763
764/// A walk over loci, one step per locus.
765pub struct LocusWalk {
766    plan: Arc<WalkPlan>,
767    next: usize,
768    tracker: Arc<ProgressTracker>,
769}
770
771impl std::fmt::Debug for LocusWalk {
772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773        f.debug_struct("LocusWalk")
774            .field("loci", &self.plan.loci.len())
775            .field("next", &self.next)
776            .finish()
777    }
778}
779
780impl LocusWalk {
781    pub fn plan(reader: &CramReader, req: &EntriesRequest) -> Result<Self> {
782        Self::plan_with(reader, req, false)
783    }
784
785    fn plan_with(
786        reader: &CramReader,
787        req: &EntriesRequest,
788        from_locus_start: bool,
789    ) -> Result<Self> {
790        let inner = reader.inner()?;
791        // Planned up front, so a request that cannot be served says so before
792        // the first step rather than partway through the walk.
793        reader.index(inner)?;
794        let resolved = reader.resolve(&req.locs)?;
795        let mut order: Vec<usize> = (0..resolved.len()).collect();
796        if req.sort_locations {
797            order.sort_by_key(|i| resolved[*i]);
798        }
799        let loci: Vec<(usize, i64, i64)> = order.iter().map(|i| resolved[*i]).collect();
800        let coverage = loci.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
801        let min_starts = if from_locus_start {
802            loci.iter().map(|(_, start, _)| Some(*start)).collect()
803        } else {
804            vec![None; loci.len()]
805        };
806        Ok(Self {
807            tracker: Arc::new(ProgressTracker::with_callback(
808                coverage,
809                req.progress.clone(),
810            )),
811            plan: Arc::new(WalkPlan {
812                min_starts,
813                loci,
814                order,
815                request: req.clone(),
816                coverage,
817            }),
818            next: 0,
819        })
820    }
821
822    /// The same walk, back at its first locus. Shares the plan.
823    pub fn restarted(&self) -> Self {
824        Self {
825            plan: self.plan.clone(),
826            next: 0,
827            tracker: Arc::new(ProgressTracker::with_callback(
828                self.plan.coverage,
829                self.plan.request.progress.clone(),
830            )),
831        }
832    }
833
834    pub fn plan_windows(reader: &CramReader, req: &EntriesRequest, span: i64) -> Result<Self> {
835        if span < 1 {
836            return Err(Error::invalid(format!(
837                "span must be positive (got {span})"
838            )));
839        }
840        let locs = crate::bam::window_locs(&reader.chr_map, &req.locs.chr_ids, span)?;
841        let windowed = EntriesRequest {
842            locs,
843            sort_locations: false,
844            ..req.clone()
845        };
846        Self::plan_with(reader, &windowed, true)
847    }
848
849    pub fn len(&self) -> usize {
850        self.plan.loci.len()
851    }
852    pub fn is_empty(&self) -> bool {
853        self.plan.loci.is_empty()
854    }
855    pub fn order(&self) -> &[usize] {
856        &self.plan.order
857    }
858
859    pub fn next_window(&mut self, reader: &CramReader) -> Option<Result<Vec<BamRecord>>> {
860        if self.next >= self.plan.loci.len() {
861            self.tracker.done_report();
862            return None;
863        }
864        let index = self.next;
865        let locus = self.plan.loci[index];
866        let outcome = reader.inner().and_then(|inner| {
867            let cram_index = reader.index(inner)?;
868            reader.read_locus(inner, &cram_index, locus, &self.plan.request)
869        });
870        match outcome {
871            // A failed step is not a step: the walk stays where it was, so the
872            // call after it raises the same error rather than reading past a
873            // locus nothing was read for.
874            Err(e) => Some(Err(e)),
875            Ok(mut records) => {
876                self.next += 1;
877                if let Some(min_start) = self.plan.min_starts[index] {
878                    records.retain(|r| r.start() >= min_start);
879                }
880                let (_, start, end) = locus;
881                self.tracker.add((end - start).max(0) as u64);
882                Some(Ok(records))
883            }
884        }
885    }
886}
887
888/// [`LocusWalk`] as a plain [`Iterator`], for Rust callers.
889#[derive(Debug)]
890pub struct LocusEntries<'a> {
891    reader: &'a CramReader,
892    walk: LocusWalk,
893}
894
895impl<'a> LocusEntries<'a> {
896    fn plan(reader: &'a CramReader, req: &EntriesRequest) -> Result<Self> {
897        Ok(Self {
898            reader,
899            walk: LocusWalk::plan(reader, req)?,
900        })
901    }
902    pub fn len(&self) -> usize {
903        self.walk.len()
904    }
905    pub fn is_empty(&self) -> bool {
906        self.walk.is_empty()
907    }
908    pub fn order(&self) -> &[usize] {
909        self.walk.order()
910    }
911}
912
913impl Iterator for LocusEntries<'_> {
914    type Item = Result<Vec<BamRecord>>;
915    fn next(&mut self) -> Option<Self::Item> {
916        self.walk.next_window(self.reader)
917    }
918}
919
920/// [`LocusWalk`] over windows tiling whole references.
921#[derive(Debug)]
922pub struct WindowEntries<'a> {
923    reader: &'a CramReader,
924    walk: LocusWalk,
925}
926
927impl<'a> WindowEntries<'a> {
928    fn plan(reader: &'a CramReader, req: &EntriesRequest, span: i64) -> Result<Self> {
929        Ok(Self {
930            reader,
931            walk: LocusWalk::plan_windows(reader, req, span)?,
932        })
933    }
934    pub fn len(&self) -> usize {
935        self.walk.len()
936    }
937    pub fn is_empty(&self) -> bool {
938        self.walk.is_empty()
939    }
940}
941
942impl Iterator for WindowEntries<'_> {
943    type Item = Result<Vec<BamRecord>>;
944    fn next(&mut self) -> Option<Self::Item> {
945        self.walk.next_window(self.reader)
946    }
947}