Skip to main content

gwseq_io/
bytes.rs

1//! Little-endian field reads and writes over a byte buffer.
2//!
3//! Buffer ownership is [`bytes::Bytes`] — refcounted, cheaply sliced, which
4//! is what lets a BAM record borrow its decompressed BGZF block and a bbi item
5//! its decompressed data block without a copy. What is left is the typed-field
6//! half, and that is this file.
7//!
8//! Every format here is little-endian and its readers refuse a byte-swapped
9//! file rather than swapping it, so there is no big-endian path. Reads copy
10//! rather than transmute: a record offset in a binary format is under no
11//! obligation to be aligned, and `from_le_bytes` over a copied array is both
12//! correct and, at these widths, free.
13//!
14//! Bounds checking is not optional. Every read is checked and returns
15//! [`Error::Corrupt`] naming the offset, which is the same check the language
16//! emits anyway.
17
18use bytes::Bytes;
19
20use crate::error::{Error, Result};
21
22/// A forward cursor over a buffer, reading little-endian fields.
23///
24/// Carries the path it came from so that a short read names the file rather
25/// than reporting a bare offset the caller has to attribute.
26pub struct LeCursor<'a> {
27    buf: &'a [u8],
28    pos: usize,
29    /// Where `buf[0]` sits in the file, so errors report file offsets.
30    base: u64,
31    path: &'a str,
32}
33
34impl<'a> LeCursor<'a> {
35    pub fn new(buf: &'a [u8], base: u64, path: &'a str) -> Self {
36        Self {
37            buf,
38            pos: 0,
39            base,
40            path,
41        }
42    }
43
44    pub fn position(&self) -> usize {
45        self.pos
46    }
47
48    pub fn file_offset(&self) -> u64 {
49        self.base + self.pos as u64
50    }
51
52    pub fn remaining(&self) -> usize {
53        self.buf.len() - self.pos
54    }
55
56    pub fn is_empty(&self) -> bool {
57        self.remaining() == 0
58    }
59
60    pub fn seek(&mut self, pos: usize) -> Result<()> {
61        if pos > self.buf.len() {
62            return Err(self.short(pos - self.buf.len(), "seek"));
63        }
64        self.pos = pos;
65        Ok(())
66    }
67
68    pub fn skip(&mut self, n: usize) -> Result<()> {
69        // Checked, as `take` is. Every caller today passes a count derived from
70        // a `u32`, so on 64-bit this cannot wrap — but a primitive that is safe
71        // only because of what its callers happen to be is a primitive waiting
72        // for a new caller.
73        let pos = self
74            .pos
75            .checked_add(n)
76            .ok_or_else(|| self.short(n, "skip"))?;
77        self.seek(pos)
78    }
79
80    pub fn take(&mut self, n: usize) -> Result<&'a [u8]> {
81        let end = self
82            .pos
83            .checked_add(n)
84            .ok_or_else(|| self.short(n, "take"))?;
85        if end > self.buf.len() {
86            return Err(self.short(n, "take"));
87        }
88        let out = &self.buf[self.pos..end];
89        self.pos = end;
90        Ok(out)
91    }
92
93    /// A NUL-terminated string, cursor left after the NUL. BAM read names and
94    /// SAM header text are stored this way.
95    pub fn take_cstr(&mut self) -> Result<&'a str> {
96        let rest = &self.buf[self.pos..];
97        let nul = memchr::memchr(0, rest).ok_or_else(|| self.corrupt("unterminated string"))?;
98        let out = std::str::from_utf8(&rest[..nul])
99            .map_err(|_| self.corrupt("string is not valid UTF-8"))?;
100        self.pos += nul + 1;
101        Ok(out)
102    }
103
104    /// A fixed-width, NUL-padded string, as the bbi chromosome B+ tree stores
105    /// its keys. The whole field is consumed whatever the string's length.
106    pub fn take_padded_str(&mut self, width: usize) -> Result<&'a str> {
107        let field = self.take(width)?;
108        let end = memchr::memchr(0, field).unwrap_or(width);
109        std::str::from_utf8(&field[..end]).map_err(|_| self.corrupt("string is not valid UTF-8"))
110    }
111
112    fn short(&self, wanted: usize, what: &str) -> Error {
113        Error::corrupt(
114            self.path,
115            self.file_offset(),
116            format!(
117                "{what} of {wanted} bytes with {} left in a {}-byte buffer",
118                self.remaining(),
119                self.buf.len()
120            ),
121        )
122    }
123
124    fn corrupt(&self, what: &str) -> Error {
125        Error::corrupt(self.path, self.file_offset(), what)
126    }
127}
128
129/// Generates `read_u32`, `read_i64`, `read_f32`, … on [`LeCursor`], plus a
130/// `peek_` form that does not advance.
131macro_rules! le_readers {
132    ($($name:ident, $peek:ident => $t:ty),* $(,)?) => {
133        impl LeCursor<'_> {
134            $(
135                #[inline]
136                pub fn $name(&mut self) -> Result<$t> {
137                    const N: usize = std::mem::size_of::<$t>();
138                    let bytes = self.take(N)?;
139                    let mut arr = [0u8; N];
140                    arr.copy_from_slice(bytes);
141                    Ok(<$t>::from_le_bytes(arr))
142                }
143
144                #[inline]
145                pub fn $peek(&self) -> Result<$t> {
146                    const N: usize = std::mem::size_of::<$t>();
147                    if self.remaining() < N {
148                        return Err(self.short(N, stringify!($peek)));
149                    }
150                    let mut arr = [0u8; N];
151                    arr.copy_from_slice(&self.buf[self.pos..self.pos + N]);
152                    Ok(<$t>::from_le_bytes(arr))
153                }
154            )*
155        }
156    };
157}
158
159le_readers! {
160    read_u8,  peek_u8  => u8,
161    read_i8,  peek_i8  => i8,
162    read_u16, peek_u16 => u16,
163    read_i16, peek_i16 => i16,
164    read_u32, peek_u32 => u32,
165    read_i32, peek_i32 => i32,
166    read_u64, peek_u64 => u64,
167    read_i64, peek_i64 => i64,
168    read_f32, peek_f32 => f32,
169    read_f64, peek_f64 => f64,
170}
171
172/// The write half: an append-only buffer with the same field vocabulary, plus
173/// the patch-back-in-place the bbi writer needs for counts and offsets whose
174/// values are only known once what follows them has been built.
175#[derive(Default)]
176pub struct LeBuf {
177    buf: Vec<u8>,
178}
179
180impl LeBuf {
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    pub fn with_capacity(n: usize) -> Self {
186        Self {
187            buf: Vec::with_capacity(n),
188        }
189    }
190
191    pub fn len(&self) -> usize {
192        self.buf.len()
193    }
194
195    pub fn is_empty(&self) -> bool {
196        self.buf.is_empty()
197    }
198
199    pub fn clear(&mut self) {
200        self.buf.clear();
201    }
202
203    pub fn as_slice(&self) -> &[u8] {
204        &self.buf
205    }
206
207    pub fn into_bytes(self) -> Bytes {
208        Bytes::from(self.buf)
209    }
210
211    pub fn put_bytes(&mut self, data: &[u8]) {
212        self.buf.extend_from_slice(data);
213    }
214
215    pub fn put_zeros(&mut self, n: usize) {
216        self.buf.resize(self.buf.len() + n, 0);
217    }
218
219    /// Zero-padded to a fixed width, as the chromosome B+ tree stores keys.
220    /// A name longer than `width` is an error rather than a truncation: a
221    /// silently truncated key breaks chromosome resolution in a way that only
222    /// surfaces at read time.
223    pub fn put_padded_str(&mut self, s: &str, width: usize) -> Result<()> {
224        if s.len() > width {
225            return Err(Error::invalid(format!(
226                "chromosome name {s:?} is longer than the {width}-byte key field"
227            )));
228        }
229        self.buf.extend_from_slice(s.as_bytes());
230        self.put_zeros(width - s.len());
231        Ok(())
232    }
233}
234
235macro_rules! le_writers {
236    ($($name:ident, $set:ident => $t:ty),* $(,)?) => {
237        impl LeBuf {
238            $(
239                #[inline]
240                pub fn $name(&mut self, value: $t) {
241                    self.buf.extend_from_slice(&value.to_le_bytes());
242                }
243
244                /// Overwrite a field already placed. Panics if it does not fit,
245                /// which would be a bug in the writer rather than bad input.
246                #[inline]
247                pub fn $set(&mut self, offset: usize, value: $t) {
248                    let bytes = value.to_le_bytes();
249                    self.buf[offset..offset + bytes.len()].copy_from_slice(&bytes);
250                }
251            )*
252        }
253    };
254}
255
256le_writers! {
257    put_u8,  set_u8  => u8,
258    put_u16, set_u16 => u16,
259    put_u32, set_u32 => u32,
260    put_i32, set_i32 => i32,
261    put_u64, set_u64 => u64,
262    put_i64, set_i64 => i64,
263    put_f32, set_f32 => f32,
264    put_f64, set_f64 => f64,
265}