use std::io::{Read, Seek, SeekFrom};
use crate::{
common::{cocoa_to_unix_secs, le_f64, le_i32, le_i32_at, le_u32, EntryState, MAGIC},
error::{Result, SegbError},
segb1,
};
pub const HEADER_LENGTH: usize = 32;
pub const ENTRY_HEADER_LENGTH: usize = 8;
pub const TRAILER_ENTRY_LENGTH: usize = 16;
pub const ALIGNMENT: u64 = 4;
#[derive(Debug, Clone)]
pub struct SegbV2Record {
pub data_offset: u64,
pub state: EntryState,
pub timestamp_unix: Option<f64>,
pub stored_crc32: u32,
pub computed_crc32: u32,
pub payload: Vec<u8>,
}
impl SegbV2Record {
#[inline]
pub fn crc_ok(&self) -> bool {
self.stored_crc32 == self.computed_crc32
}
}
pub fn is_segb_v2<R: Read + Seek>(r: &mut R) -> bool {
let start = match r.stream_position() {
Ok(p) => p,
Err(_) => return false,
};
let mut buf = [0u8; 4];
let n = r.read(&mut buf).unwrap_or(0);
let _ = r.seek(SeekFrom::Start(start));
n == 4 && &buf == MAGIC
}
pub fn read_v2<R: Read + Seek>(r: &mut R) -> Result<Vec<SegbV2Record>> {
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[0..4] != MAGIC {
return Err(SegbError::BadMagic {
found: hex_encode(&header[0..4]),
});
}
let entries_count_raw = le_i32_at(&header, 4);
if entries_count_raw < 0 {
return Err(SegbError::InvalidEntryCount {
count: entries_count_raw,
});
}
let entries_count = entries_count_raw as u64;
let _creation_ts = le_f64(&header, 8);
let stream_len = r.seek(SeekFrom::End(0))?;
let trailer_bytes = entries_count * TRAILER_ENTRY_LENGTH as u64;
if trailer_bytes > stream_len {
return Err(SegbError::TrailerOverflow {
trailer_bytes,
stream_bytes: stream_len,
});
}
let trailer_start = stream_len - trailer_bytes;
r.seek(SeekFrom::Start(trailer_start))?;
struct TrailerEntry {
end_offset: i32, state: EntryState,
timestamp_unix: Option<f64>,
}
let mut trailer: Vec<TrailerEntry> = Vec::with_capacity(entries_count as usize);
let mut trailer_raw = vec![0u8; TRAILER_ENTRY_LENGTH];
for _ in 0..entries_count {
let n = r.read(&mut trailer_raw)?;
if n < TRAILER_ENTRY_LENGTH {
break;
}
let end_offset = le_i32(&trailer_raw, 0);
let state_raw = le_i32(&trailer_raw, 4);
let ts_cocoa = le_f64(&trailer_raw, 8);
let state = EntryState::from_raw(state_raw)?;
trailer.push(TrailerEntry {
end_offset,
state,
timestamp_unix: cocoa_to_unix_secs(ts_cocoa),
});
}
trailer.sort_by_key(|e| e.end_offset);
r.seek(SeekFrom::Start(HEADER_LENGTH as u64))?;
let mut records = Vec::with_capacity(trailer.len());
for t in &trailer {
if t.state == EntryState::Unknown {
continue;
}
let current_pos = r.stream_position()?;
let abs_end = HEADER_LENGTH as u64 + t.end_offset as u64;
if abs_end < current_pos {
continue;
}
let entry_total = (abs_end - current_pos) as usize;
if entry_total < ENTRY_HEADER_LENGTH {
continue;
}
let mut sub_hdr = vec![0u8; ENTRY_HEADER_LENGTH];
let n = r.read(&mut sub_hdr)?;
if n < ENTRY_HEADER_LENGTH {
break; }
let crc32_stored = le_u32(&sub_hdr, 0);
let payload_len = entry_total - ENTRY_HEADER_LENGTH;
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 = segb1::crc32_of(&payload);
records.push(SegbV2Record {
data_offset,
state: t.state,
timestamp_unix: t.timestamp_unix,
stored_crc32: crc32_stored,
computed_crc32,
payload,
});
let remainder = t.end_offset as u64 % ALIGNMENT;
if remainder != 0 {
let skip = ALIGNMENT - remainder;
r.seek(SeekFrom::Current(skip as i64))?;
}
}
Ok(records)
}
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}