1use crate::EseError;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ChecksumResult {
8 Valid,
10 LegacyXorMismatch { stored: u32, computed: u32 },
12 EccMismatch,
14 Unknown,
16}
17
18pub fn verify_page_checksum(page_data: &[u8], _page_number: u32) -> ChecksumResult {
32 if page_data.len() < 8 {
33 return ChecksumResult::Unknown;
34 }
35 let stored_xor = u32::from_le_bytes([
36 page_data[0], page_data[1], page_data[2], page_data[3],
37 ]);
38 if stored_xor == 0 {
39 return ChecksumResult::Unknown;
40 }
41 let stored_ecc = u32::from_le_bytes([
42 page_data[4], page_data[5], page_data[6], page_data[7],
43 ]);
44
45 if stored_ecc == 0 {
46 let computed = xor_page_checksum(&page_data[4..]);
48 if computed == stored_xor {
49 ChecksumResult::Valid
50 } else {
51 ChecksumResult::LegacyXorMismatch { stored: stored_xor, computed }
52 }
53 } else {
54 if page_data.len() < 9 {
56 return ChecksumResult::Unknown;
57 }
58 let computed_xor = xor_page_checksum(&page_data[8..]);
59 let computed_ecc = column_parity_ecc(&page_data[8..]);
60 if computed_xor == stored_xor && computed_ecc == stored_ecc {
61 ChecksumResult::Valid
62 } else {
63 ChecksumResult::EccMismatch
64 }
65 }
66}
67
68const XOR_CHECKSUM_SEED: u32 = 0x89AB_CDEF;
69
70fn xor_page_checksum(data: &[u8]) -> u32 {
71 let mut csum = XOR_CHECKSUM_SEED;
72 for chunk in data.chunks_exact(4) {
73 csum ^= u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
74 }
75 csum
76}
77
78fn column_parity_ecc(data: &[u8]) -> u32 {
80 let mut ecc: u32 = 0;
81 for (i, chunk) in data.chunks_exact(4).enumerate() {
82 let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
83 ecc ^= word.rotate_left((i % 32) as u32);
84 }
85 ecc
86}
87
88pub mod coltyp {
90 pub const BIT: u8 = 1;
91 pub const UNSIGNED_BYTE: u8 = 2;
92 pub const SHORT: u8 = 3;
93 pub const LONG: u8 = 4;
94 pub const CURRENCY: u8 = 5;
95 pub const IEEE_SINGLE: u8 = 6;
96 pub const IEEE_DOUBLE: u8 = 7;
97 pub const DATE_TIME: u8 = 8;
98 pub const BINARY: u8 = 9;
99 pub const TEXT: u8 = 10;
100 pub const LONG_BINARY: u8 = 11;
101 pub const LONG_TEXT: u8 = 12;
102 pub const GUID: u8 = 16;
103 pub const UNSIGNED_SHORT: u8 = 17;
104 pub const UNSIGNED_LONG: u8 = 14;
105 pub const LONG_LONG: u8 = 15;
106 pub const UNSIGNED_LONG_LONG: u8 = 18;
107}
108
109#[derive(Debug, Clone)]
111pub struct ColumnDef {
112 pub column_id: u32,
114 pub name: String,
116 pub coltyp: u8,
118}
119
120#[derive(Debug, Clone, serde::Serialize)]
122pub enum EseValue {
123 Null,
124 Bool(bool),
125 U8(u8),
126 I16(i16),
127 I32(i32),
128 I64(i64),
129 U16(u16),
130 U32(u32),
131 U64(u64),
132 F32(f32),
133 F64(f64),
134 DateTime(f64),
136 Binary(Vec<u8>),
137 Text(String),
138 Guid([u8; 16]),
139}
140
141pub fn fixed_col_size(coltyp: u8) -> Option<usize> {
144 match coltyp {
145 coltyp::BIT | coltyp::UNSIGNED_BYTE => Some(1),
146 coltyp::SHORT | coltyp::UNSIGNED_SHORT => Some(2),
147 coltyp::LONG | coltyp::UNSIGNED_LONG | coltyp::IEEE_SINGLE => Some(4),
148 coltyp::CURRENCY | coltyp::IEEE_DOUBLE | coltyp::DATE_TIME
149 | coltyp::LONG_LONG | coltyp::UNSIGNED_LONG_LONG => Some(8),
150 coltyp::GUID => Some(16),
151 _ => None, }
153}
154
155fn decode_fixed(data: &[u8], coltyp: u8) -> EseValue {
157 match coltyp {
158 coltyp::BIT => EseValue::Bool(data[0] != 0),
159 coltyp::UNSIGNED_BYTE => EseValue::U8(data[0]),
160 coltyp::SHORT => EseValue::I16(i16::from_le_bytes([data[0], data[1]])),
161 coltyp::UNSIGNED_SHORT => EseValue::U16(u16::from_le_bytes([data[0], data[1]])),
162 coltyp::LONG => EseValue::I32(i32::from_le_bytes([data[0], data[1], data[2], data[3]])),
163 coltyp::UNSIGNED_LONG => {
164 EseValue::U32(u32::from_le_bytes([data[0], data[1], data[2], data[3]]))
165 }
166 coltyp::IEEE_SINGLE => {
167 EseValue::F32(f32::from_le_bytes([data[0], data[1], data[2], data[3]]))
168 }
169 coltyp::CURRENCY | coltyp::LONG_LONG => EseValue::I64(i64::from_le_bytes([
170 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
171 ])),
172 coltyp::UNSIGNED_LONG_LONG => EseValue::U64(u64::from_le_bytes([
173 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
174 ])),
175 coltyp::IEEE_DOUBLE => EseValue::F64(f64::from_le_bytes([
176 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
177 ])),
178 coltyp::DATE_TIME => EseValue::DateTime(f64::from_le_bytes([
179 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
180 ])),
181 coltyp::GUID => {
182 let mut g = [0u8; 16];
183 g.copy_from_slice(&data[..16]);
184 EseValue::Guid(g)
185 }
186 _ => EseValue::Binary(data.to_vec()),
187 }
188}
189
190pub fn decode_record(data: &[u8], columns: &[ColumnDef]) -> Result<Vec<(String, EseValue)>, EseError> {
216 if data.len() < 4 {
217 return Ok(Vec::new());
218 }
219
220 let last_fixed_col = data[0] as u32;
221 let num_var_cols = data[1] as usize;
222 let var_data_offset = u16::from_le_bytes([data[2], data[3]]) as usize;
223
224 let mut result = Vec::new();
225
226 let mut fixed_cursor = 4usize; let mut fixed_col_idx = 1u32; for col in columns {
231 if fixed_col_size(col.coltyp).is_none() {
232 continue; }
234 if col.column_id > last_fixed_col {
235 break; }
237 while fixed_col_idx < col.column_id {
241 fixed_col_idx += 1;
246 if fixed_col_idx > last_fixed_col {
247 break;
248 }
249 }
250 if fixed_col_idx > last_fixed_col {
251 break;
252 }
253
254 let Some(size) = fixed_col_size(col.coltyp) else {
257 continue; };
259 if fixed_cursor + size > data.len() {
260 break;
261 }
262 let val = decode_fixed(&data[fixed_cursor..fixed_cursor + size], col.coltyp);
263 result.push((col.name.clone(), val));
264 fixed_cursor += size;
265 fixed_col_idx += 1;
266 }
267
268 if num_var_cols == 0 || var_data_offset > data.len() {
270 return Ok(result);
271 }
272 let offsets_area_start = var_data_offset;
274 let offsets_area_end = offsets_area_start + num_var_cols * 2;
275 if offsets_area_end > data.len() {
276 return Ok(result);
277 }
278 let var_payload_start = offsets_area_end;
280
281 let mut var_col_idx = 0usize; let mut prev_end = 0u16; for col in columns {
285 if fixed_col_size(col.coltyp).is_some() {
286 continue; }
288 if var_col_idx >= num_var_cols {
289 break;
290 }
291 let off_pos = offsets_area_start + var_col_idx * 2;
292 let raw_end = u16::from_le_bytes([data[off_pos], data[off_pos + 1]]);
293 let is_null = raw_end & 0x8000 != 0;
294 let end_offset = (raw_end & 0x7FFF) as usize;
295
296 if !is_null {
297 let start = var_payload_start + prev_end as usize;
298 let end = var_payload_start + end_offset;
299 if end <= data.len() && start <= end {
300 let bytes = &data[start..end];
301 let val = match col.coltyp {
302 coltyp::TEXT => {
303 let s = String::from_utf8_lossy(bytes).into_owned();
304 EseValue::Text(s)
305 }
306 _ => EseValue::Binary(bytes.to_vec()),
307 };
308 result.push((col.name.clone(), val));
309 }
310 }
311 prev_end = raw_end & 0x7FFF;
312 var_col_idx += 1;
313 }
314
315 Ok(result)
316}