Skip to main content

chat4n6_sqlite_forensics/
record.rs

1use chat4n6_plugin_api::EvidenceSource;
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum SqlValue {
5    Null,
6    Int(i64),
7    Real(f64),
8    Text(String),
9    Blob(Vec<u8>),
10}
11
12#[derive(Debug, Clone)]
13pub struct RecoveredRecord {
14    pub table: String,
15    pub row_id: Option<i64>,
16    pub values: Vec<SqlValue>,
17    pub source: EvidenceSource,
18    pub offset: u64,
19    /// Confidence in the recovery (1.0 = certain for live records).
20    pub confidence: f32,
21}
22
23/// Decode a SQLite serial type into a value, consuming bytes from `data` at `offset`.
24/// Returns (SqlValue, bytes_consumed) or None on truncation.
25pub fn decode_serial_type(
26    serial_type: u64,
27    data: &[u8],
28    offset: usize,
29) -> Option<(SqlValue, usize)> {
30    match serial_type {
31        0 => Some((SqlValue::Null, 0)),
32        1 => {
33            let b = *data.get(offset)?;
34            Some((SqlValue::Int(b as i8 as i64), 1))
35        }
36        2 => {
37            let bytes = data.get(offset..offset + 2)?;
38            let v = i16::from_be_bytes([bytes[0], bytes[1]]) as i64;
39            Some((SqlValue::Int(v), 2))
40        }
41        3 => {
42            let bytes = data.get(offset..offset + 3)?;
43            let v = ((bytes[0] as i32) << 16 | (bytes[1] as i32) << 8 | bytes[2] as i32) as i64;
44            // Sign-extend from 24 bits
45            let v = if v & 0x800000 != 0 { v | !0xFFFFFF } else { v };
46            Some((SqlValue::Int(v), 3))
47        }
48        4 => {
49            let bytes = data.get(offset..offset + 4)?;
50            let v = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64;
51            Some((SqlValue::Int(v), 4))
52        }
53        5 => {
54            let bytes = data.get(offset..offset + 6)?;
55            let v = (bytes[0] as i64) << 40
56                | (bytes[1] as i64) << 32
57                | (bytes[2] as i64) << 24
58                | (bytes[3] as i64) << 16
59                | (bytes[4] as i64) << 8
60                | bytes[5] as i64;
61            // Sign-extend from 48 bits
62            let v = if v & (1i64 << 47) != 0 {
63                v | !0xFFFFFFFFFFFFi64
64            } else {
65                v
66            };
67            Some((SqlValue::Int(v), 6))
68        }
69        6 => {
70            let bytes = data.get(offset..offset + 8)?;
71            let v = i64::from_be_bytes([
72                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
73            ]);
74            Some((SqlValue::Int(v), 8))
75        }
76        7 => {
77            let bytes = data.get(offset..offset + 8)?;
78            let v = f64::from_be_bytes([
79                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
80            ]);
81            Some((SqlValue::Real(v), 8))
82        }
83        8 => Some((SqlValue::Int(0), 0)),
84        9 => Some((SqlValue::Int(1), 0)),
85        n if n >= 12 && n % 2 == 0 => {
86            let len = ((n - 12) / 2) as usize;
87            let bytes = data.get(offset..offset + len)?;
88            Some((SqlValue::Blob(bytes.to_vec()), len))
89        }
90        n if n >= 13 && n % 2 == 1 => {
91            let len = ((n - 13) / 2) as usize;
92            let bytes = data.get(offset..offset + len)?;
93            let text = String::from_utf8_lossy(bytes).into_owned();
94            Some((SqlValue::Text(text), len))
95        }
96        _ => None,
97    }
98}