Skip to main content

quantydb_sqlite/
record.rs

1//! The record format: how a row's values are laid out inside a cell.
2//!
3//! A record is a header followed by a body. The header is a varint holding
4//! its own total length, then one varint per column giving that column's
5//! serial type. The body is the values, back to back, in the same order.
6//! Serial types encode both the type and the width, and two of them encode
7//! the value as well: type 8 is the integer 0 and type 9 is the integer 1,
8//! neither of which occupies a single byte in the body.
9//!
10//! Values come back as the five storage classes SQLite actually has. This
11//! crate deliberately does not map them onto QuantyDB's value type: SQLite
12//! is dynamically typed and QuantyDB is not, so that mapping is a decision
13//! with its own trade-offs and it belongs to the importer, not to a format
14//! reader.
15
16use crate::error::{Result, SqliteError};
17use crate::header::TextEncoding;
18use crate::varint;
19
20/// One of SQLite's five storage classes.
21///
22/// `PartialEq` is the derived one, so `Real` values follow IEEE rules and a
23/// NaN is not equal to itself. That matters because a file can hold any 64
24/// bit pattern in a float field: SQLite's own API turns a NaN into NULL on
25/// the way in, but a corrupted or hand written file is under no such
26/// obligation. Code asking whether two reads produced the same bytes should
27/// compare `to_bits` rather than the values.
28#[derive(Debug, Clone, PartialEq)]
29pub enum SqliteValue {
30    Null,
31    Integer(i64),
32    Real(f64),
33    Text(String),
34    Blob(Vec<u8>),
35}
36
37impl SqliteValue {
38    /// The name SQLite's own `typeof()` gives this value, which is what the
39    /// import tests compare against.
40    pub fn type_name(&self) -> &'static str {
41        match self {
42            SqliteValue::Null => "null",
43            SqliteValue::Integer(_) => "integer",
44            SqliteValue::Real(_) => "real",
45            SqliteValue::Text(_) => "text",
46            SqliteValue::Blob(_) => "blob",
47        }
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52enum SerialType {
53    Null,
54    Int(u8),
55    Float,
56    /// The value 0, carried entirely by the serial type.
57    Zero,
58    /// The value 1, likewise.
59    One,
60    Blob(usize),
61    Text(usize),
62}
63
64impl SerialType {
65    fn from_code(code: u64) -> Result<SerialType> {
66        Ok(match code {
67            0 => SerialType::Null,
68            1 => SerialType::Int(1),
69            2 => SerialType::Int(2),
70            3 => SerialType::Int(3),
71            4 => SerialType::Int(4),
72            5 => SerialType::Int(6),
73            6 => SerialType::Int(8),
74            7 => SerialType::Float,
75            8 => SerialType::Zero,
76            9 => SerialType::One,
77            // sqlite reserves these for its own internal record formats;
78            // they never appear in a database file
79            10 | 11 => {
80                return Err(SqliteError::malformed(
81                    None,
82                    format!("serial type {code} is reserved for internal use"),
83                ))
84            }
85            even if even % 2 == 0 => {
86                let len = (even - 12) / 2;
87                SerialType::Blob(usize::try_from(len).map_err(|_| {
88                    SqliteError::malformed(None, format!("blob of {len} bytes is impossibly large"))
89                })?)
90            }
91            odd => {
92                let len = (odd - 13) / 2;
93                SerialType::Text(usize::try_from(len).map_err(|_| {
94                    SqliteError::malformed(None, format!("text of {len} bytes is impossibly large"))
95                })?)
96            }
97        })
98    }
99
100    /// Bytes this type occupies in the record body.
101    fn width(self) -> usize {
102        match self {
103            SerialType::Null | SerialType::Zero | SerialType::One => 0,
104            SerialType::Int(n) => n as usize,
105            SerialType::Float => 8,
106            SerialType::Blob(n) | SerialType::Text(n) => n,
107        }
108    }
109}
110
111/// Decode one record into its values.
112///
113/// `payload` must be the whole record, overflow pages already stitched back
114/// on. Anything that does not add up, a header that overruns the payload, a
115/// value that runs past the end, a reserved serial type, is a malformed
116/// file and comes back as an error.
117///
118/// `encoding` is the one the database header states, and it applies to
119/// every text value in the file. Text comes back as a Rust `String`
120/// whatever the file stores, so the encoding is a fact about the bytes on
121/// disk and not something callers have to carry around afterwards.
122pub fn decode(payload: &[u8], encoding: TextEncoding) -> Result<Vec<SqliteValue>> {
123    let (header_len, varint_len) = varint::read_unsigned(payload)?;
124    let header_len = usize::try_from(header_len)
125        .map_err(|_| SqliteError::malformed(None, "record header length does not fit in memory"))?;
126    if header_len < varint_len || header_len > payload.len() {
127        return Err(SqliteError::malformed(
128            None,
129            format!(
130                "record header claims {header_len} bytes of a {} byte payload",
131                payload.len()
132            ),
133        ));
134    }
135
136    // first pass: read the serial types and work out where the body starts
137    let mut types = Vec::new();
138    let mut at = varint_len;
139    while at < header_len {
140        let (code, len) = varint::read_unsigned(&payload[at..header_len])?;
141        types.push(SerialType::from_code(code)?);
142        at += len;
143    }
144
145    // second pass: cut the body into values
146    let mut values = Vec::with_capacity(types.len());
147    let mut at = header_len;
148    for ty in types {
149        let width = ty.width();
150        let end = at.checked_add(width).ok_or_else(|| {
151            SqliteError::malformed(None, "a record value overflows the payload length")
152        })?;
153        if end > payload.len() {
154            return Err(SqliteError::malformed(
155                None,
156                format!(
157                    "a {width} byte value at offset {at} runs past the {} byte payload",
158                    payload.len()
159                ),
160            ));
161        }
162        let bytes = &payload[at..end];
163        values.push(match ty {
164            SerialType::Null => SqliteValue::Null,
165            SerialType::Zero => SqliteValue::Integer(0),
166            SerialType::One => SqliteValue::Integer(1),
167            SerialType::Int(_) => SqliteValue::Integer(signed_be(bytes)),
168            SerialType::Float => {
169                let mut eight = [0u8; 8];
170                eight.copy_from_slice(bytes);
171                SqliteValue::Real(f64::from_bits(u64::from_be_bytes(eight)))
172            }
173            SerialType::Blob(_) => SqliteValue::Blob(bytes.to_vec()),
174            SerialType::Text(_) => SqliteValue::Text(text_from(bytes, encoding)?),
175        });
176        at = end;
177    }
178
179    Ok(values)
180}
181
182/// Turn the stored bytes of a text value into a string.
183///
184/// Both utf-16 encodings are decoded rather than transcoded loosely: an
185/// unpaired surrogate is an error, not a replacement character. Text that
186/// is almost right is the failure mode that survives an import and shows up
187/// months later in somebody's name field, so it stops here.
188fn text_from(bytes: &[u8], encoding: TextEncoding) -> Result<String> {
189    match encoding {
190        TextEncoding::Utf8 => match std::str::from_utf8(bytes) {
191            Ok(text) => Ok(text.to_string()),
192            Err(e) => Err(SqliteError::malformed(
193                None,
194                format!("a text value is not valid utf-8: {e}"),
195            )),
196        },
197        TextEncoding::Utf16Le | TextEncoding::Utf16Be => {
198            if !bytes.len().is_multiple_of(2) {
199                return Err(SqliteError::malformed(
200                    None,
201                    format!(
202                        "a utf-16 text value is {} bytes long, which is not a whole number of \
203                         code units",
204                        bytes.len()
205                    ),
206                ));
207            }
208            let big_endian = encoding == TextEncoding::Utf16Be;
209            let units: Vec<u16> = bytes
210                .as_chunks::<2>()
211                .0
212                .iter()
213                .map(|pair| {
214                    if big_endian {
215                        u16::from_be_bytes([pair[0], pair[1]])
216                    } else {
217                        u16::from_le_bytes([pair[0], pair[1]])
218                    }
219                })
220                .collect();
221            String::from_utf16(&units).map_err(|_| {
222                SqliteError::malformed(
223                    None,
224                    "a utf-16 text value holds an unpaired surrogate, so it is not text",
225                )
226            })
227        }
228    }
229}
230
231/// Big endian two's complement of 1, 2, 3, 4, 6 or 8 bytes.
232fn signed_be(bytes: &[u8]) -> i64 {
233    let mut value: u64 = 0;
234    for byte in bytes {
235        value = (value << 8) | *byte as u64;
236    }
237    let bits = 8 * bytes.len() as u32;
238    if bits < 64 && (value >> (bits - 1)) & 1 == 1 {
239        // sign extend the widths that are not a whole i64
240        (value | (!0u64 << bits)) as i64
241    } else {
242        value as i64
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn sign_extension_covers_every_width() {
252        assert_eq!(signed_be(&[0x7f]), 127);
253        assert_eq!(signed_be(&[0x80]), -128);
254        assert_eq!(signed_be(&[0xff]), -1);
255        assert_eq!(signed_be(&[0x80, 0x00]), -32768);
256        assert_eq!(signed_be(&[0x80, 0x00, 0x00]), -8388608);
257        assert_eq!(signed_be(&[0x7f, 0xff, 0xff]), 8388607);
258        assert_eq!(signed_be(&[0x80, 0, 0, 0]), -2147483648);
259        assert_eq!(signed_be(&[0x80, 0, 0, 0, 0, 0]), -140737488355328);
260        assert_eq!(
261            signed_be(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff]),
262            140737488355327
263        );
264        assert_eq!(signed_be(&[0x80, 0, 0, 0, 0, 0, 0, 0]), i64::MIN);
265        assert_eq!(signed_be(&[0xff; 8]), -1);
266    }
267
268    #[test]
269    fn a_record_of_every_cheap_type() {
270        // header: length 6, then serial types null, 0, 1, int8, text(2).
271        // a text of n characters is serial type 2n + 13, so two is 17.
272        let payload = [6u8, 0, 8, 9, 1, 17, 0x2a, b'h', b'i'];
273        let values = decode(&payload, TextEncoding::Utf8).unwrap();
274        assert_eq!(
275            values,
276            vec![
277                SqliteValue::Null,
278                SqliteValue::Integer(0),
279                SqliteValue::Integer(1),
280                SqliteValue::Integer(42),
281                SqliteValue::Text("hi".to_string()),
282            ]
283        );
284    }
285
286    #[test]
287    fn an_empty_record_has_no_values() {
288        assert_eq!(decode(&[1u8], TextEncoding::Utf8).unwrap(), vec![]);
289    }
290
291    #[test]
292    fn reserved_serial_types_are_refused() {
293        assert!(decode(&[2u8, 10], TextEncoding::Utf8).is_err());
294        assert!(decode(&[2u8, 11], TextEncoding::Utf8).is_err());
295    }
296
297    #[test]
298    fn a_header_longer_than_the_payload_is_refused() {
299        assert!(decode(&[99u8, 0], TextEncoding::Utf8).is_err());
300        assert!(decode(&[], TextEncoding::Utf8).is_err());
301    }
302
303    #[test]
304    fn a_value_running_past_the_payload_is_refused() {
305        // header says one text of 4 bytes, body holds one
306        assert!(decode(&[2u8, 21, b'a'], TextEncoding::Utf8).is_err());
307    }
308
309    #[test]
310    fn invalid_utf8_in_a_text_value_is_refused() {
311        assert!(decode(&[2u8, 15, 0xff, 0xfe], TextEncoding::Utf8).is_err());
312    }
313
314    #[test]
315    fn floats_round_trip_bit_for_bit() {
316        let mut payload = vec![2u8, 7];
317        payload.extend_from_slice(&(-2.25f64).to_be_bytes());
318        assert_eq!(
319            decode(&payload, TextEncoding::Utf8).unwrap(),
320            vec![SqliteValue::Real(-2.25)]
321        );
322    }
323}