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    /// The file this reads, so a caller raising its own error can name it
53    /// rather than thread the path alongside the cursor.
54    pub fn path(&self) -> &'a str {
55        self.path
56    }
57
58    pub fn remaining(&self) -> usize {
59        self.buf.len() - self.pos
60    }
61
62    pub fn is_empty(&self) -> bool {
63        self.remaining() == 0
64    }
65
66    pub fn seek(&mut self, pos: usize) -> Result<()> {
67        if pos > self.buf.len() {
68            return Err(self.short(pos - self.buf.len(), "seek"));
69        }
70        self.pos = pos;
71        Ok(())
72    }
73
74    pub fn skip(&mut self, n: usize) -> Result<()> {
75        // Checked, as `take` is. Every caller today passes a count derived from
76        // a `u32`, so on 64-bit this cannot wrap — but a primitive that is safe
77        // only because of what its callers happen to be is a primitive waiting
78        // for a new caller.
79        let pos = self
80            .pos
81            .checked_add(n)
82            .ok_or_else(|| self.short(n, "skip"))?;
83        self.seek(pos)
84    }
85
86    pub fn take(&mut self, n: usize) -> Result<&'a [u8]> {
87        let end = self
88            .pos
89            .checked_add(n)
90            .ok_or_else(|| self.short(n, "take"))?;
91        if end > self.buf.len() {
92            return Err(self.short(n, "take"));
93        }
94        let out = &self.buf[self.pos..end];
95        self.pos = end;
96        Ok(out)
97    }
98
99    /// A NUL-terminated string, cursor left after the NUL. BAM read names and
100    /// SAM header text are stored this way.
101    pub fn take_cstr(&mut self) -> Result<&'a str> {
102        let rest = &self.buf[self.pos..];
103        let nul = memchr::memchr(0, rest).ok_or_else(|| self.corrupt("unterminated string"))?;
104        let out = std::str::from_utf8(&rest[..nul])
105            .map_err(|_| self.corrupt("string is not valid UTF-8"))?;
106        self.pos += nul + 1;
107        Ok(out)
108    }
109
110    /// A fixed-width, NUL-padded string, as the bbi chromosome B+ tree stores
111    /// its keys. The whole field is consumed whatever the string's length.
112    pub fn take_padded_str(&mut self, width: usize) -> Result<&'a str> {
113        let field = self.take(width)?;
114        let end = memchr::memchr(0, field).unwrap_or(width);
115        std::str::from_utf8(&field[..end]).map_err(|_| self.corrupt("string is not valid UTF-8"))
116    }
117
118    fn short(&self, wanted: usize, what: &str) -> Error {
119        Error::corrupt(
120            self.path,
121            self.file_offset(),
122            format!(
123                "{what} of {wanted} bytes with {} left in a {}-byte buffer",
124                self.remaining(),
125                self.buf.len()
126            ),
127        )
128    }
129
130    fn corrupt(&self, what: &str) -> Error {
131        Error::corrupt(self.path, self.file_offset(), what)
132    }
133}
134
135/// Generates `read_u32`, `read_i64`, `read_f32`, … on [`LeCursor`], plus a
136/// `peek_` form that does not advance.
137macro_rules! le_readers {
138    ($($name:ident, $peek:ident => $t:ty),* $(,)?) => {
139        impl LeCursor<'_> {
140            $(
141                #[inline]
142                pub fn $name(&mut self) -> Result<$t> {
143                    const N: usize = std::mem::size_of::<$t>();
144                    let bytes = self.take(N)?;
145                    let mut arr = [0u8; N];
146                    arr.copy_from_slice(bytes);
147                    Ok(<$t>::from_le_bytes(arr))
148                }
149
150                #[inline]
151                pub fn $peek(&self) -> Result<$t> {
152                    const N: usize = std::mem::size_of::<$t>();
153                    if self.remaining() < N {
154                        return Err(self.short(N, stringify!($peek)));
155                    }
156                    let mut arr = [0u8; N];
157                    arr.copy_from_slice(&self.buf[self.pos..self.pos + N]);
158                    Ok(<$t>::from_le_bytes(arr))
159                }
160            )*
161        }
162    };
163}
164
165le_readers! {
166    read_u8,  peek_u8  => u8,
167    read_i8,  peek_i8  => i8,
168    read_u16, peek_u16 => u16,
169    read_i16, peek_i16 => i16,
170    read_u32, peek_u32 => u32,
171    read_i32, peek_i32 => i32,
172    read_u64, peek_u64 => u64,
173    read_i64, peek_i64 => i64,
174    read_f32, peek_f32 => f32,
175    read_f64, peek_f64 => f64,
176}
177
178/// The write half: an append-only buffer with the same field vocabulary, plus
179/// the patch-back-in-place the bbi writer needs for counts and offsets whose
180/// values are only known once what follows them has been built.
181#[derive(Default)]
182pub struct LeBuf {
183    buf: Vec<u8>,
184}
185
186impl LeBuf {
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    pub fn with_capacity(n: usize) -> Self {
192        Self {
193            buf: Vec::with_capacity(n),
194        }
195    }
196
197    pub fn len(&self) -> usize {
198        self.buf.len()
199    }
200
201    pub fn is_empty(&self) -> bool {
202        self.buf.is_empty()
203    }
204
205    pub fn clear(&mut self) {
206        self.buf.clear();
207    }
208
209    pub fn as_slice(&self) -> &[u8] {
210        &self.buf
211    }
212
213    pub fn into_bytes(self) -> Bytes {
214        Bytes::from(self.buf)
215    }
216
217    pub fn put_bytes(&mut self, data: &[u8]) {
218        self.buf.extend_from_slice(data);
219    }
220
221    pub fn put_zeros(&mut self, n: usize) {
222        self.buf.resize(self.buf.len() + n, 0);
223    }
224
225    /// Zero-padded to a fixed width, as the chromosome B+ tree stores keys.
226    /// A name longer than `width` is an error rather than a truncation: a
227    /// silently truncated key breaks chromosome resolution in a way that only
228    /// surfaces at read time.
229    pub fn put_padded_str(&mut self, s: &str, width: usize) -> Result<()> {
230        if s.len() > width {
231            return Err(Error::invalid(format!(
232                "chromosome name {s:?} is longer than the {width}-byte key field"
233            )));
234        }
235        self.buf.extend_from_slice(s.as_bytes());
236        self.put_zeros(width - s.len());
237        Ok(())
238    }
239}
240
241macro_rules! le_writers {
242    ($($name:ident, $set:ident => $t:ty),* $(,)?) => {
243        impl LeBuf {
244            $(
245                #[inline]
246                pub fn $name(&mut self, value: $t) {
247                    self.buf.extend_from_slice(&value.to_le_bytes());
248                }
249
250                /// Overwrite a field already placed. Panics if it does not fit,
251                /// which would be a bug in the writer rather than bad input.
252                #[inline]
253                pub fn $set(&mut self, offset: usize, value: $t) {
254                    let bytes = value.to_le_bytes();
255                    self.buf[offset..offset + bytes.len()].copy_from_slice(&bytes);
256                }
257            )*
258        }
259    };
260}
261
262le_writers! {
263    put_u8,  set_u8  => u8,
264    put_u16, set_u16 => u16,
265    put_u32, set_u32 => u32,
266    put_i32, set_i32 => i32,
267    put_u64, set_u64 => u64,
268    put_i64, set_i64 => i64,
269    put_f32, set_f32 => f32,
270    put_f64, set_f64 => f64,
271}