use crate::history_records::{
AsmBulletinBoard, AsmDeltaState, AsmEntityChange, AsmEntityChangeKind, AsmHistory,
AsmHistoryRecord,
};
use cadmpeg_ir::le::int_at;
const DELTA: &[u8] = b"\x11\x0d\x0bdelta_state";
const PREAMBLE: &[u8] = b"\x0d\x0ehistory_stream";
pub(crate) fn decode(bytes: &[u8], stream: &str, width: usize) -> Option<AsmHistory> {
let preamble_offset = bytes
.windows(PREAMBLE.len())
.position(|window| window == PREAMBLE);
let history_offset = preamble_offset.unwrap_or(0);
let history_id = format!("f3d:{stream}:asm-history#{history_offset:010}");
let mut delta_offsets = Vec::new();
let mut search = 0usize;
while let Some(relative) = bytes[search..]
.windows(DELTA.len())
.position(|window| window == DELTA)
{
let offset = search + relative;
delta_offsets.push(offset);
search = offset + DELTA.len();
}
let mut states = Vec::new();
for (ordinal, &offset) in delta_offsets.iter().enumerate() {
let state_record_id = format!("f3d:{stream}:asm-delta-state#{offset:010}");
let mut position = offset + DELTA.len();
let state_id = take_int(bytes, &mut position, 0x04, width)?;
let version_flag = take_int(bytes, &mut position, 0x04, width)?;
let state_flag = take_int(bytes, &mut position, 0x04, width)?;
let previous = take_int(bytes, &mut position, 0x0c, width)?;
let next = take_int(bytes, &mut position, 0x0c, width)?;
let node_index = take_int(bytes, &mut position, 0x0c, width)?;
let partner = take_int(bytes, &mut position, 0x0c, width)?;
let owner_ref = take_int(bytes, &mut position, 0x0c, width)?;
if bytes.get(position) != Some(&0x0b) {
continue;
}
let (bulletin_boards, body_end) =
decode_bulletin_boards(bytes, position + 1, stream, offset, &state_record_id, width)?;
let records = decode_history_records(
bytes,
body_end,
delta_offsets.get(ordinal + 1).copied(),
stream,
&state_record_id,
width,
);
states.push(AsmDeltaState {
id: state_record_id,
parent: history_id.clone(),
byte_offset: offset as u64,
state_id,
version_flag,
state_flag,
previous_ref: (previous >= 0).then_some(previous),
next_ref: (next >= 0).then_some(next),
node_index,
partner_ref: (partner >= 0).then_some(partner),
owner_ref,
bulletin_boards,
records,
});
}
if states.is_empty() {
return None;
}
let (stream_size, high_water_mark) = preamble_offset
.and_then(|offset| decode_preamble(bytes, offset + PREAMBLE.len(), width))
.map_or((None, None), |(size, high)| (Some(size), Some(high)));
let offset = history_offset;
Some(AsmHistory {
id: history_id,
byte_offset: offset as u64,
stream_size,
high_water_mark,
states,
})
}
fn decode_bulletin_boards(
bytes: &[u8],
mut position: usize,
stream: &str,
state_offset: usize,
state_id: &str,
width: usize,
) -> Option<(Vec<AsmBulletinBoard>, usize)> {
if bytes.get(position) == Some(&0x11) {
return Some((Vec::new(), position));
}
let mut boards = Vec::new();
loop {
let board_offset = position;
let present = take_int(bytes, &mut position, 0x04, width)?;
if present == 0 {
break;
}
let owner_ref = take_int(bytes, &mut position, 0x0c, width)?;
let number = take_int(bytes, &mut position, 0x04, width)?;
let board_id = format!(
"f3d:{stream}:asm-bulletin-board#{state_offset:010}:{:06}",
boards.len()
);
let mut changes = Vec::new();
loop {
let change_offset = position;
let present = take_int(bytes, &mut position, 0x04, width)?;
if present == 0 {
break;
}
let old = take_int(bytes, &mut position, 0x0c, width)?;
let new = take_int(bytes, &mut position, 0x0c, width)?;
let kind = match (old >= 0, new >= 0) {
(false, true) => AsmEntityChangeKind::Insert,
(true, false) => AsmEntityChangeKind::Delete,
(true, true) => AsmEntityChangeKind::Update,
(false, false) => return None,
};
changes.push(AsmEntityChange {
id: format!(
"f3d:{stream}:asm-entity-change#{state_offset:010}:{:06}:{:06}",
boards.len(),
changes.len()
),
parent: board_id.clone(),
byte_offset: change_offset as u64,
kind,
old_ref: (old >= 0).then_some(old),
new_ref: (new >= 0).then_some(new),
});
}
boards.push(AsmBulletinBoard {
id: board_id,
parent: state_id.to_string(),
byte_offset: board_offset as u64,
owner_ref,
number,
changes,
});
}
Some((boards, position))
}
fn decode_history_records(
bytes: &[u8],
state_end: usize,
next_delta: Option<usize>,
stream: &str,
state_id: &str,
width: usize,
) -> Vec<AsmHistoryRecord> {
let start = state_end + usize::from(bytes.get(state_end) == Some(&0x11));
let limit = next_delta.map_or(bytes.len(), |offset| offset + 1);
if start >= limit {
return Vec::new();
}
match crate::sab::frame(bytes, start, limit, width) {
Ok(records) => records
.into_iter()
.map(|record| AsmHistoryRecord {
id: format!("f3d:{stream}:asm-history-record#{:010}", record.offset),
parent: state_id.to_string(),
index: record.index as u64,
name: record.name,
raw_bytes: bytes[record.offset..record.offset + record.len].to_vec(),
})
.collect(),
Err(_) => vec![AsmHistoryRecord {
id: format!("f3d:{stream}:asm-history-record#{start:010}"),
parent: state_id.to_string(),
index: 0,
name: "opaque_history_payload".into(),
raw_bytes: bytes[start..limit].to_vec(),
}],
}
}
fn decode_preamble(bytes: &[u8], mut position: usize, width: usize) -> Option<(i64, i64)> {
let size = take_int(bytes, &mut position, 0x04, width)?;
let duplicate = take_int(bytes, &mut position, 0x04, width)?;
let zero = take_int(bytes, &mut position, 0x04, width)?;
let high_water = take_int(bytes, &mut position, 0x04, width)?;
(size == duplicate && zero == 0).then_some((size, high_water))
}
fn take_int(bytes: &[u8], position: &mut usize, tag: u8, width: usize) -> Option<i64> {
if bytes.get(*position) != Some(&tag) {
return None;
}
let value = int_at(bytes, *position + 1, width)?;
*position += 1 + width;
Some(value)
}