@@ -322,6 +322,83 @@ impl<'a> GenomeIndexView<'a> {
322322 }
323323}
324324
325+ /// A borrowed, zero-copy view over the persisted 2-bit forward reference
326+ /// (`Reference2bit`). Decodes bases on demand from the mmap — no full-reference
327+ /// allocation — so consumers (the aligner DP window in B4b, variants' `ref_base`
328+ /// in B4c) read the reference from the `.idx` alone. Uses **global** (concatenated)
329+ /// coordinates; `(contig, pos)` mapping stays with `ContigSet`. The little-endian
330+ /// host requirement is the same as `FmIndexView` (checked in `new`).
331+ #[ derive( Debug ) ]
332+ pub struct ReferenceView < ' a > {
333+ len : usize ,
334+ /// 2-bit packed bases, 32 per `u64` word (A/C/G/T = 0/1/2/3).
335+ data : & ' a [ u64 ] ,
336+ /// Ambiguity bits, one per base (a set bit marks `N`).
337+ amb : & ' a [ u64 ] ,
338+ }
339+
340+ impl < ' a > ReferenceView < ' a > {
341+ /// Parse + validate the `Reference2bit` section into a borrowed view.
342+ pub ( crate ) fn new ( bytes : & ' a [ u8 ] , sections : & [ SectionEntry ] ) -> Result < Self , IndexIoError > {
343+ if cfg ! ( target_endian = "big" ) {
344+ return Err ( IndexIoError :: Invalid (
345+ "zero-copy reference view requires a little-endian host" . to_string ( ) ,
346+ ) ) ;
347+ }
348+ let section = section_bytes ( bytes, sections, SectionKind :: Reference2bit ) ?;
349+ let mut o = 0usize ;
350+ let len = read_u64 ( section, & mut o) ? as usize ;
351+ let data_words = read_u64 ( section, & mut o) ? as usize ;
352+ let amb_words = read_u64 ( section, & mut o) ? as usize ;
353+ let data = as_u64_slice ( slice_exact ( section, & mut o, data_words. saturating_mul ( 8 ) ) ?) ?;
354+ let amb = as_u64_slice ( slice_exact ( section, & mut o, amb_words. saturating_mul ( 8 ) ) ?) ?;
355+ // The slices must cover `len` bases so `base_at` cannot index out of bounds.
356+ if data. len ( ) . saturating_mul ( 32 ) < len || amb. len ( ) . saturating_mul ( 64 ) < len {
357+ return Err ( IndexIoError :: Invalid (
358+ "Reference2bit section too small for the declared length" . to_string ( ) ,
359+ ) ) ;
360+ }
361+ Ok ( Self { len, data, amb } )
362+ }
363+
364+ /// Number of reference bases.
365+ pub fn len ( & self ) -> usize {
366+ self . len
367+ }
368+
369+ /// Whether the reference is empty.
370+ pub fn is_empty ( & self ) -> bool {
371+ self . len == 0
372+ }
373+
374+ /// The ASCII base at global position `global`. Mirrors `CompressedDNA::base_at`:
375+ /// a set ambiguity bit decodes to `N`, otherwise the 2-bit code maps to A/C/G/T.
376+ pub fn base_at ( & self , global : usize ) -> u8 {
377+ debug_assert ! ( global < self . len, "reference index out of range" ) ;
378+ if self . amb [ global / 64 ] & ( 1u64 << ( global % 64 ) ) != 0 {
379+ return b'N' ;
380+ }
381+ let code = ( ( self . data [ global / 32 ] >> ( ( global % 32 ) * 2 ) ) & 0b11 ) as u8 ;
382+ match code {
383+ 0 => b'A' ,
384+ 1 => b'C' ,
385+ 2 => b'G' ,
386+ _ => b'T' ,
387+ }
388+ }
389+
390+ /// Decode `[start, end.min(len))` into `out` (cleared first). `end` is clamped
391+ /// to `len`, so an over-range request never panics; `start <= end` is the
392+ /// caller's contract. Bounded by the window size — never the whole genome.
393+ pub fn decode_window ( & self , start : usize , end : usize , out : & mut Vec < u8 > ) {
394+ out. clear ( ) ;
395+ let end = end. min ( self . len ) ;
396+ for i in start..end {
397+ out. push ( self . base_at ( i) ) ;
398+ }
399+ }
400+ }
401+
325402/// Validate that every block record (at the directory's offsets) lies fully
326403/// within the `Blocks` section, using checked arithmetic so a corrupt word-count
327404/// can never overflow into an in-bounds-but-wrong slice. Lets `block()` use
0 commit comments