mini-sqlite-dump 0.1.2

creating sqlite3 dump files from Rust
Documentation

use itertools::iproduct;
use rusqlite::Connection;
use rusqlite::types::ValueRef;
use testresult::TestResult;

use super::*;

fn sqlite3_parse(conn: &Connection, repr: &str) -> TestResult<Vec<u8>> {
    let v = conn.query_row(
        &format!("SELECT {repr}"), [],
        |row| {
            let v: ValueRef = row.get_ref(0)?;
            let v = v.as_bytes()?.to_owned();
            Ok(v)
        }
    )?;
    Ok(v)
}

fn minidump_print(data: &[u8]) -> TestResult<String> {
    let mut repr: Vec<u8> = vec![];
    write_text(&mut repr, data)?;
    let repr = String::from_utf8(repr)?;
    Ok(repr)
}

fn test_roundtrip(
    conn: &Connection,
    data: &[u8],
    debug: &mut impl DebugWrite,
) -> TestResult<()> {
    if debug.enabled() {
        let s = String::from_utf8(
            data.iter()
                .map(|c| c.escape_ascii())
                .flatten().collect()
        )?;

        write!(debug, "\"{s}\"\t\t{}\t\t", HexFmt(data))?;
    }
    let repr = minidump_print(data)?;
    writeln!(debug, "{repr}")?;
    let reparsed = sqlite3_parse(conn, &repr)?;
    assert_eq!(data, reparsed);
    Ok(())
}

static CASE_FRAGS_DECOMPOSE: &[&[u8]] = &[
    b"\0",
    b"\n",
    b"\t",
    b"\r",
    b"'",
    b"a",
    b" ",
    b"abcd",
    b"abcdefgh",
    &[0xff],
    &[0xc3], // sequence too short
    "é".as_bytes(),
    "één".as_bytes(),
    &[0xf0, 0x9f, 0x96, 0x8d], // U+1F58D LOWER LEFT CRAYON
    &[0xf0, 0x9f, 0x96      ], // U+1F58D LOWER LEFT CRAYON but truncated
    &[0xf3, 0xa0, 0x81, 0xbf], // U+E007F CANCEL TAG
];

static CASE_FRAGS_REASSEMBLE: &[&[u8]] = &[
    // TextFragment::Squote
    b"'",
    // TextFragment::Char (several lengths)
    b"\0", // 0
    b"\x7f", // 127
    &[0xf3, 0xa0, 0x81, 0xbf], // U+E007F CANCEL TAG
    // TextFragment::Nice
    b"a",
    b"ab",
    b"abcd",
    b"abcdefgh",
    &[0xf0, 0x9f, 0x96, 0x8d], // U+1F58D LOWER LEFT CRAYON
    // TextFragment::Bad
    &[0xff],
    &[0xc3], // sequence too short
];

fn roundtrips<FRAGS>(
    max_l: usize,
    #[allow(unused_variables)]
    max_l_release: usize,
    frags: FRAGS,
    debug: &mut impl DebugWrite,
) -> TestResult<()>
where
    FRAGS: IntoIterator<Item = &'static &'static [u8]> + Clone,
    FRAGS::IntoIter: Clone,
{
    let conn = Connection::open_in_memory()?;

    #[cfg(not(debug_assertions))]
    let max_l = {
        let _ = max_l;
        max_l_release
    };

    for l in 0..max_l {
        for case in Itertools::multi_cartesian_product(
            iter::repeat(frags.clone()).take(l)
        ) {
            let data: Vec<u8> = case.into_iter()
                .copied().flatten().copied().collect();
            test_roundtrip(&conn, &data, debug)?;
        }
    }

    Ok(())
}

#[test]
fn roundtrips_decompose() -> TestResult<()> {
    roundtrips(3, 6, CASE_FRAGS_DECOMPOSE, &mut io::sink())
}

#[test]
fn roundtrips_reassemble() -> TestResult<()> {
    roundtrips(5, 7, CASE_FRAGS_REASSEMBLE, &mut io::sink())
}

struct EprintWriter;
impl io::Write for EprintWriter {
    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
        let s = str::from_utf8(data)
            .expect("EprintlnWriter can only do UTF-8 in each write!");
        eprint!("{s}");
        Ok(data.len())
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
fn roundtrips_report() -> TestResult<()> {
    let conn = Connection::open_in_memory()?;

    let mut debug = io::LineWriter::new(EprintWriter);

    for (d0, d1) in iproduct!(
        [b"", &[0xff][..]],
        chain!(
            CASE_FRAGS_DECOMPOSE,
            CASE_FRAGS_REASSEMBLE,
        ).unique()
    ) {
        let data: Vec<u8> = chain!(
            d0,
            d1.into_iter(),
        ).copied().collect();
        test_roundtrip(&conn, &data, &mut debug)?;
    }

    Ok(())
}