use crate::EseError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChecksumResult {
Valid,
LegacyXorMismatch { stored: u32, computed: u32 },
EccMismatch,
Unknown,
}
pub fn verify_page_checksum(page_data: &[u8], _page_number: u32) -> ChecksumResult {
if page_data.len() < 8 {
return ChecksumResult::Unknown;
}
let stored_xor = u32::from_le_bytes([
page_data[0], page_data[1], page_data[2], page_data[3],
]);
if stored_xor == 0 {
return ChecksumResult::Unknown;
}
let stored_ecc = u32::from_le_bytes([
page_data[4], page_data[5], page_data[6], page_data[7],
]);
if stored_ecc == 0 {
let computed = xor_page_checksum(&page_data[4..]);
if computed == stored_xor {
ChecksumResult::Valid
} else {
ChecksumResult::LegacyXorMismatch { stored: stored_xor, computed }
}
} else {
if page_data.len() < 9 {
return ChecksumResult::Unknown;
}
let computed_xor = xor_page_checksum(&page_data[8..]);
let computed_ecc = column_parity_ecc(&page_data[8..]);
if computed_xor == stored_xor && computed_ecc == stored_ecc {
ChecksumResult::Valid
} else {
ChecksumResult::EccMismatch
}
}
}
const XOR_CHECKSUM_SEED: u32 = 0x89AB_CDEF;
fn xor_page_checksum(data: &[u8]) -> u32 {
let mut csum = XOR_CHECKSUM_SEED;
for chunk in data.chunks_exact(4) {
csum ^= u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
csum
}
fn column_parity_ecc(data: &[u8]) -> u32 {
let mut ecc: u32 = 0;
for (i, chunk) in data.chunks_exact(4).enumerate() {
let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
ecc ^= word.rotate_left((i % 32) as u32);
}
ecc
}
pub mod coltyp {
pub const BIT: u8 = 1;
pub const UNSIGNED_BYTE: u8 = 2;
pub const SHORT: u8 = 3;
pub const LONG: u8 = 4;
pub const CURRENCY: u8 = 5;
pub const IEEE_SINGLE: u8 = 6;
pub const IEEE_DOUBLE: u8 = 7;
pub const DATE_TIME: u8 = 8;
pub const BINARY: u8 = 9;
pub const TEXT: u8 = 10;
pub const LONG_BINARY: u8 = 11;
pub const LONG_TEXT: u8 = 12;
pub const GUID: u8 = 16;
pub const UNSIGNED_SHORT: u8 = 17;
pub const UNSIGNED_LONG: u8 = 14;
pub const LONG_LONG: u8 = 15;
pub const UNSIGNED_LONG_LONG: u8 = 18;
}
#[derive(Debug, Clone)]
pub struct ColumnDef {
pub column_id: u32,
pub name: String,
pub coltyp: u8,
}
#[derive(Debug, Clone, serde::Serialize)]
pub enum EseValue {
Null,
Bool(bool),
U8(u8),
I16(i16),
I32(i32),
I64(i64),
U16(u16),
U32(u32),
U64(u64),
F32(f32),
F64(f64),
DateTime(f64),
Binary(Vec<u8>),
Text(String),
Guid([u8; 16]),
}
pub fn fixed_col_size(coltyp: u8) -> Option<usize> {
match coltyp {
coltyp::BIT | coltyp::UNSIGNED_BYTE => Some(1),
coltyp::SHORT | coltyp::UNSIGNED_SHORT => Some(2),
coltyp::LONG | coltyp::UNSIGNED_LONG | coltyp::IEEE_SINGLE => Some(4),
coltyp::CURRENCY | coltyp::IEEE_DOUBLE | coltyp::DATE_TIME
| coltyp::LONG_LONG | coltyp::UNSIGNED_LONG_LONG => Some(8),
coltyp::GUID => Some(16),
_ => None, }
}
fn decode_fixed(data: &[u8], coltyp: u8) -> EseValue {
match coltyp {
coltyp::BIT => EseValue::Bool(data[0] != 0),
coltyp::UNSIGNED_BYTE => EseValue::U8(data[0]),
coltyp::SHORT => EseValue::I16(i16::from_le_bytes([data[0], data[1]])),
coltyp::UNSIGNED_SHORT => EseValue::U16(u16::from_le_bytes([data[0], data[1]])),
coltyp::LONG => EseValue::I32(i32::from_le_bytes([data[0], data[1], data[2], data[3]])),
coltyp::UNSIGNED_LONG => {
EseValue::U32(u32::from_le_bytes([data[0], data[1], data[2], data[3]]))
}
coltyp::IEEE_SINGLE => {
EseValue::F32(f32::from_le_bytes([data[0], data[1], data[2], data[3]]))
}
coltyp::CURRENCY | coltyp::LONG_LONG => EseValue::I64(i64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])),
coltyp::UNSIGNED_LONG_LONG => EseValue::U64(u64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])),
coltyp::IEEE_DOUBLE => EseValue::F64(f64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])),
coltyp::DATE_TIME => EseValue::DateTime(f64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])),
coltyp::GUID => {
let mut g = [0u8; 16];
g.copy_from_slice(&data[..16]);
EseValue::Guid(g)
}
_ => EseValue::Binary(data.to_vec()),
}
}
pub fn decode_record(data: &[u8], columns: &[ColumnDef]) -> Result<Vec<(String, EseValue)>, EseError> {
if data.len() < 4 {
return Ok(Vec::new());
}
let last_fixed_col = data[0] as u32;
let num_var_cols = data[1] as usize;
let var_data_offset = u16::from_le_bytes([data[2], data[3]]) as usize;
let mut result = Vec::new();
let mut fixed_cursor = 4usize; let mut fixed_col_idx = 1u32;
for col in columns {
if fixed_col_size(col.coltyp).is_none() {
continue; }
if col.column_id > last_fixed_col {
break; }
while fixed_col_idx < col.column_id {
fixed_col_idx += 1;
if fixed_col_idx > last_fixed_col {
break;
}
}
if fixed_col_idx > last_fixed_col {
break;
}
let Some(size) = fixed_col_size(col.coltyp) else {
continue; };
if fixed_cursor + size > data.len() {
break;
}
let val = decode_fixed(&data[fixed_cursor..fixed_cursor + size], col.coltyp);
result.push((col.name.clone(), val));
fixed_cursor += size;
fixed_col_idx += 1;
}
if num_var_cols == 0 || var_data_offset > data.len() {
return Ok(result);
}
let offsets_area_start = var_data_offset;
let offsets_area_end = offsets_area_start + num_var_cols * 2;
if offsets_area_end > data.len() {
return Ok(result);
}
let var_payload_start = offsets_area_end;
let mut var_col_idx = 0usize; let mut prev_end = 0u16;
for col in columns {
if fixed_col_size(col.coltyp).is_some() {
continue; }
if var_col_idx >= num_var_cols {
break;
}
let off_pos = offsets_area_start + var_col_idx * 2;
let raw_end = u16::from_le_bytes([data[off_pos], data[off_pos + 1]]);
let is_null = raw_end & 0x8000 != 0;
let end_offset = (raw_end & 0x7FFF) as usize;
if !is_null {
let start = var_payload_start + prev_end as usize;
let end = var_payload_start + end_offset;
if end <= data.len() && start <= end {
let bytes = &data[start..end];
let val = match col.coltyp {
coltyp::TEXT => {
let s = String::from_utf8_lossy(bytes).into_owned();
EseValue::Text(s)
}
_ => EseValue::Binary(bytes.to_vec()),
};
result.push((col.name.clone(), val));
}
}
prev_end = raw_end & 0x7FFF;
var_col_idx += 1;
}
Ok(result)
}