use std::io::{Read, Seek, SeekFrom};
use crate::{
common::{cocoa_to_unix_secs, le_f64, le_i32, le_u32, EntryState, MAGIC},
error::{Result, SegbError},
};
pub const HEADER_LENGTH: usize = 56;
pub const RECORD_HEADER_LENGTH: usize = 32;
pub const ALIGNMENT: u64 = 8;
#[derive(Debug, Clone)]
pub struct SegbV1Record {
pub data_offset: u64,
pub state: EntryState,
pub timestamp1_unix: Option<f64>,
pub timestamp2_unix: Option<f64>,
pub stored_crc32: u32,
pub computed_crc32: u32,
pub payload: Vec<u8>,
}
impl SegbV1Record {
#[inline]
pub fn crc_ok(&self) -> bool {
self.stored_crc32 == self.computed_crc32
}
}
pub fn is_segb_v1<R: Read + Seek>(r: &mut R) -> bool {
let start = match r.stream_position() {
Ok(p) => p,
Err(_) => return false,
};
let mut buf = [0u8; HEADER_LENGTH];
let n = r.read(&mut buf).unwrap_or(0);
let _ = r.seek(SeekFrom::Start(start));
if n < HEADER_LENGTH {
return false;
}
buf[HEADER_LENGTH - 4..] == *MAGIC
}
pub fn read_v1<R: Read + Seek>(r: &mut R) -> Result<Vec<SegbV1Record>> {
let mut header = vec![0u8; HEADER_LENGTH];
let n = r.read(&mut header)?;
if n < HEADER_LENGTH {
return Err(SegbError::TruncatedHeader {
need: HEADER_LENGTH,
got: n,
});
}
if &header[HEADER_LENGTH - 4..] != MAGIC {
return Err(SegbError::BadMagic {
found: hex::encode(&header[HEADER_LENGTH - 4..]),
});
}
let end_of_data: u64 = u64::from(le_u32(&header, 0));
let mut records = Vec::new();
loop {
let record_start = r.stream_position()?;
if record_start >= end_of_data {
break;
}
let mut rec_hdr = vec![0u8; RECORD_HEADER_LENGTH];
let n = r.read(&mut rec_hdr)?;
if n < RECORD_HEADER_LENGTH {
if n == 0 {
break;
}
return Err(SegbError::TruncatedRecordHeader {
offset: record_start,
need: RECORD_HEADER_LENGTH,
got: n,
});
}
let record_length = le_i32(&rec_hdr, 0);
let entry_state_raw = le_i32(&rec_hdr, 4);
let timestamp1_raw = le_f64(&rec_hdr, 8);
let timestamp2_raw = le_f64(&rec_hdr, 16);
let crc32_stored = le_u32(&rec_hdr, 24);
if record_length < 0 {
return Err(SegbError::InvalidLength {
offset: record_start,
length: record_length,
});
}
let payload_len = record_length as usize;
let state = EntryState::from_raw(entry_state_raw)?;
let data_offset = r.stream_position()?;
let mut payload = vec![0u8; payload_len];
let n = r.read(&mut payload)?;
if n < payload_len {
return Err(SegbError::TruncatedPayload {
offset: data_offset,
need: payload_len,
got: n,
});
}
let computed_crc32 = crc32_of(&payload);
records.push(SegbV1Record {
data_offset,
state,
timestamp1_unix: cocoa_to_unix_secs(timestamp1_raw),
timestamp2_unix: cocoa_to_unix_secs(timestamp2_raw),
stored_crc32: crc32_stored,
computed_crc32,
payload,
});
let pos = r.stream_position()?;
let remainder = pos % ALIGNMENT;
if remainder != 0 {
let skip = ALIGNMENT - remainder;
r.seek(SeekFrom::Current(skip as i64))?;
}
}
Ok(records)
}
pub(crate) fn crc32_of(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
if crc & 1 == 1 {
crc = (crc >> 1) ^ 0xEDB8_8320;
} else {
crc >>= 1;
}
}
}
!crc
}
mod hex {
use std::fmt::Write as _;
pub fn encode(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc32_known_value() {
assert_eq!(crc32_of(b"hello world"), 0x0d4a_1185);
}
#[test]
fn crc32_empty() {
assert_eq!(crc32_of(b""), 0x0000_0000);
}
}