use crate::kernel_header::RefWidth;
use crate::sab::{Record, Token};
use crate::stream_error::StreamFailure;
use cadmpeg_core::decode::{DecodeContext, View};
use cadmpeg_core::CodecError;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityChange {
Insert {
new: i64,
},
Delete {
old: i64,
},
Update {
old: i64,
new: i64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BulletinBoardItem {
Board {
offset: usize,
owner_ref: i64,
number: i64,
},
Change {
offset: usize,
kind: EntityChange,
},
}
fn take_int(bytes: &[u8], position: &mut usize, tag: u8, width: RefWidth) -> Option<i64> {
if bytes.get(*position) != Some(&tag) {
return None;
}
let at = *position + 1;
let value = match width {
RefWidth::Four => i64::from(View::i32_le_at(bytes, at)?),
RefWidth::Eight => View::i64_le_at(bytes, at)?,
};
*position += 1 + width.bytes();
Some(value)
}
pub fn walk_bulletin_boards(
bytes: &[u8],
mut position: usize,
width: RefWidth,
mut visit: impl FnMut(BulletinBoardItem),
) -> Option<usize> {
if bytes.get(position) == Some(&0x11) {
return Some(position);
}
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)?;
visit(BulletinBoardItem::Board {
offset: board_offset,
owner_ref,
number,
});
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) => EntityChange::Insert { new },
(true, false) => EntityChange::Delete { old },
(true, true) => EntityChange::Update { old, new },
(false, false) => return None,
};
visit(BulletinBoardItem::Change {
offset: change_offset,
kind,
});
}
}
Some(position)
}
pub fn materialize_record_table<'a>(
versions: &BTreeMap<i64, i64>,
lookup: impl Fn(i64) -> Option<&'a Record>,
) -> Option<Vec<Record>> {
if versions.is_empty() {
return None;
}
let mut records = Vec::new();
for (&entity_ref, &record_ref) in versions {
let record = lookup(record_ref)?;
if i64::try_from(record.index).ok() != Some(entity_ref) {
return None;
}
for token in record.tokens.iter() {
let Token::Ref(reference) = token else {
continue;
};
if *reference >= 0 && !versions.contains_key(reference) {
return None;
}
}
records.push(record.clone());
}
records.sort_unstable_by_key(|a| a.index);
Some(records)
}
const HISTORY_SECTION_END: &str = "End-of-ASM-History-Section";
const ACTIVE_DATA_END: &str = "End-of-ASM-data";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StateRecordView<'a> {
pub revision_id: Option<i64>,
pub byte_offset: u64,
pub raw_bytes: &'a [u8],
pub name: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeltaStateView<'a> {
pub state_id: i64,
pub node_index: i64,
pub previous_ref: Option<i64>,
pub next_ref: Option<i64>,
pub changes: Vec<EntityChange>,
pub records: Vec<StateRecordView<'a>>,
}
impl EntityChange {
fn old_ref(self) -> Option<i64> {
match self {
Self::Insert { .. } => None,
Self::Delete { old } | Self::Update { old, .. } => Some(old),
}
}
fn new_ref(self) -> Option<i64> {
match self {
Self::Delete { .. } => None,
Self::Insert { new } | Self::Update { new, .. } => Some(new),
}
}
}
fn is_history_boundary_record(record: &StateRecordView<'_>) -> bool {
matches!(record.name, HISTORY_SECTION_END | ACTIVE_DATA_END)
}
pub fn archived_active_record_count(
states: &[DeltaStateView<'_>],
) -> Result<Option<usize>, CodecError> {
let mut archived: Vec<i64> = states
.iter()
.flat_map(|state| &state.records)
.filter_map(|record| record.revision_id)
.collect();
archived.sort_unstable_by(Ord::cmp);
let Some(&active_count) = archived.first() else {
return Ok(None);
};
let count = i64::try_from(archived.len())
.map_err(|_| CodecError::malformed("archived revision count exceeds i64"))?;
let Some(end) = active_count.checked_add(count) else {
return Ok(None);
};
if active_count <= 0 || archived.iter().copied().ne(active_count..end) {
return Ok(None);
}
Ok(usize::try_from(active_count).ok())
}
pub fn insert_only_active_record_count(states: &[DeltaStateView<'_>]) -> Option<usize> {
let mut has_boundary_record = false;
for record in states.iter().flat_map(|state| &state.records) {
if record.revision_id.is_some() || !is_history_boundary_record(record) {
return None;
}
has_boundary_record = true;
}
if !has_boundary_record {
return None;
}
let mut inserted = BTreeSet::new();
for change in states.iter().flat_map(|state| &state.changes) {
let new_ref = change.new_ref().filter(|_| change.old_ref().is_none())?;
if new_ref <= 0 || inserted.contains(&new_ref) {
return None;
}
inserted.insert(new_ref);
}
let &last = inserted.last()?;
if inserted.iter().copied().ne(1..=last) {
return None;
}
last.checked_add(1)
.and_then(|count| usize::try_from(count).ok())
}
pub fn walk_entity_versions(
states: &[DeltaStateView<'_>],
active_count: i64,
archived_ids: &[i64],
mut visit: impl FnMut(usize, &BTreeMap<i64, i64>),
) -> bool {
let mut by_node = HashMap::new();
for (ordinal, state) in states.iter().enumerate() {
by_node.insert(state.node_index, ordinal);
}
if by_node.len() != states.len() {
return false;
}
let mut heads = states
.iter()
.enumerate()
.filter(|(_, state)| state.previous_ref.is_none())
.map(|(ordinal, _)| ordinal);
let Some(mut ordinal) = heads.next() else {
return false;
};
if heads.next().is_some() {
return false;
}
let mut versions = BTreeMap::new();
for id in 0..active_count {
versions.insert(id, id);
}
let mut visited = HashSet::new();
loop {
let state = &states[ordinal];
if !visited.insert(state.node_index) {
return false;
}
visit(ordinal, &versions);
for change in &state.changes {
match *change {
EntityChange::Update { old, new } => {
if !versions.contains_key(&new) || archived_ids.binary_search(&old).is_err() {
return false;
}
versions.insert(new, old);
}
EntityChange::Insert { new } => {
if versions.remove(&new).is_none() {
return false;
}
}
EntityChange::Delete { old } => {
if versions.contains_key(&old) || archived_ids.binary_search(&old).is_err() {
return false;
}
versions.insert(old, old);
}
}
}
let Some(next) = state.next_ref else {
break;
};
let Some(&next_ordinal) = by_node.get(&next) else {
return false;
};
ordinal = next_ordinal;
}
visited.len() == states.len() && versions.len() == 1 && versions.get(&0) == Some(&0)
}
#[derive(Debug)]
pub struct RecordArchiveFrames {
pub active: Vec<Record>,
pub archived: HashMap<i64, Record>,
}
fn propagated_error(failure: StreamFailure) -> Option<CodecError> {
match failure {
StreamFailure::Resource(error) => Some(error.into()),
StreamFailure::Operation(error) => Some(error.into_codec_error()),
_ => None,
}
}
pub fn decode_record_archive(
ctx: &DecodeContext<'_>,
states: &[DeltaStateView<'_>],
bytes: &[u8],
width: RefWidth,
) -> Result<Option<RecordArchiveFrames>, CodecError> {
let Some(start) = crate::asm_header::record_stream_start(bytes) else {
return Ok(None);
};
let active_limit = crate::asm_header::solved_record_limit(bytes).unwrap_or(bytes.len());
let mut framed = match crate::sab::frame(ctx, bytes, start, active_limit, width, None) {
Ok(records) => records,
Err(failure) => return propagated_error(failure).map_or(Ok(None), Err),
};
let insert_only = insert_only_active_record_count(states);
let archived_count = archived_active_record_count(states)?;
let Some(active_count) = archived_count.or(insert_only) else {
return Ok(None);
};
if insert_only.is_some() && framed.len() != active_count {
return Ok(None);
}
if framed.len() < active_count {
return Ok(None);
}
framed.truncate(active_count);
let mut archived_frames = HashMap::new();
for record in states
.iter()
.flat_map(|state| &state.records)
.filter(|record| record.revision_id.is_some())
{
let Some(revision_id) = record.revision_id else {
return Ok(None);
};
let Some(offset) = usize::try_from(record.byte_offset).ok() else {
return Ok(None);
};
let Some(limit) = offset.checked_add(record.raw_bytes.len()) else {
return Ok(None);
};
if bytes.get(offset..limit) != Some(record.raw_bytes) {
return Ok(None);
}
let mut archived = match crate::sab::frame(ctx, bytes, offset, limit, width, None) {
Ok(records) => records,
Err(failure) => return propagated_error(failure).map_or(Ok(None), Err),
};
if archived.len() != 1 {
return Ok(None);
}
let Some(archived) = archived.pop() else {
return Ok(None);
};
if archived.name != record.name {
return Ok(None);
}
if archived_frames.insert(revision_id, archived).is_some() {
return Ok(None);
}
}
if framed
.iter()
.enumerate()
.any(|(index, record)| record.index != index)
{
return Ok(None);
}
Ok(Some(RecordArchiveFrames {
active: framed,
archived: archived_frames,
}))
}
const DELTA_STATE_MARKER: &[u8] = b"\x11\x0d\x0bdelta_state";
pub fn delta_state_offsets(bytes: &[u8]) -> Vec<usize> {
let mut offsets = Vec::new();
let mut search = 0usize;
while let Some(relative) = bytes[search..]
.windows(DELTA_STATE_MARKER.len())
.position(|window| window == DELTA_STATE_MARKER)
{
let offset = search + relative;
offsets.push(offset);
search = offset + DELTA_STATE_MARKER.len();
}
offsets
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeltaStateHeader {
pub offset: usize,
pub state_id: i64,
pub version_flag: i64,
pub state_flag: i64,
pub previous: i64,
pub next: i64,
pub node_index: i64,
pub partner: i64,
pub owner_ref: i64,
pub body_start: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeltaStateHeaderRead {
Framed(DeltaStateHeader),
Unframed,
Invalid,
}
pub fn read_delta_state_header(
bytes: &[u8],
offset: usize,
width: RefWidth,
) -> DeltaStateHeaderRead {
let mut position = offset + DELTA_STATE_MARKER.len();
let Some([state_id, version_flag, state_flag, previous, next, node_index, partner, owner_ref]) =
(|| {
Some([
take_int(bytes, &mut position, 0x04, width)?,
take_int(bytes, &mut position, 0x04, width)?,
take_int(bytes, &mut position, 0x04, width)?,
take_int(bytes, &mut position, 0x0c, width)?,
take_int(bytes, &mut position, 0x0c, width)?,
take_int(bytes, &mut position, 0x0c, width)?,
take_int(bytes, &mut position, 0x0c, width)?,
take_int(bytes, &mut position, 0x0c, width)?,
])
})()
else {
return DeltaStateHeaderRead::Invalid;
};
if bytes.get(position) != Some(&0x0b) {
return DeltaStateHeaderRead::Unframed;
}
DeltaStateHeaderRead::Framed(DeltaStateHeader {
offset,
state_id,
version_flag,
state_flag,
previous,
next,
node_index,
partner,
owner_ref,
body_start: position + 1,
})
}
#[derive(Debug)]
pub enum StateRecords {
Empty,
Framed(Vec<Record>),
Unframed {
start: usize,
limit: usize,
error: String,
},
}
pub fn frame_state_records(
ctx: &DecodeContext<'_>,
bytes: &[u8],
state_end: usize,
next_delta: Option<usize>,
width: RefWidth,
) -> Result<StateRecords, CodecError> {
let mut start = state_end + usize::from(bytes.get(state_end) == Some(&0x11));
if bytes.get(start) == Some(&0x04)
&& int_at(bytes, start + 1, width) == Some(0)
&& bytes.get(start + 1 + width.bytes()) == Some(&0x11)
{
start += 2 + width.bytes();
}
let limit = next_delta.map_or(bytes.len(), |offset| offset + 1);
if start >= limit {
return Ok(StateRecords::Empty);
}
match crate::sab::frame_history(ctx, bytes, start, limit, width, None) {
Ok(records) => Ok(StateRecords::Framed(records)),
Err(StreamFailure::Resource(error)) => Err(error.into()),
Err(StreamFailure::Operation(error)) => Err(error.into_codec_error()),
Err(error) => Ok(StateRecords::Unframed {
start,
limit,
error: format!("{error}"),
}),
}
}
fn int_at(bytes: &[u8], offset: usize, width: RefWidth) -> Option<i64> {
match width {
RefWidth::Four => Some(i64::from(View::i32_le_at(bytes, offset)?)),
RefWidth::Eight => View::i64_le_at(bytes, offset),
}
}
pub fn snapshot_revision_ids(
changes: impl Iterator<Item = EntityChange>,
snapshot_record_count: usize,
) -> Option<std::ops::Range<i64>> {
let mut old_references: Vec<i64> = changes.filter_map(EntityChange::old_ref).collect();
old_references.sort_unstable_by(Ord::cmp);
let &first = old_references.first()?;
let count = i64::try_from(old_references.len()).ok()?;
let end = first.checked_add(count)?;
if old_references.iter().copied().ne(first..end)
|| snapshot_record_count != old_references.len()
{
return None;
}
Some(first..end)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainRecord {
pub revision_id: Option<i64>,
pub offset: usize,
pub len: usize,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainState {
pub header: DeltaStateHeader,
pub changes: Vec<EntityChange>,
pub records: Vec<ChainRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryChain {
pub states: Vec<ChainState>,
}
#[derive(Debug)]
pub enum ChainFraming {
Absent,
Unframed,
Framed(HistoryChain),
}
impl HistoryChain {
#[must_use]
pub fn views<'a>(&'a self, bytes: &'a [u8]) -> Vec<DeltaStateView<'a>> {
self.states
.iter()
.map(|state| DeltaStateView {
state_id: state.header.state_id,
node_index: state.header.node_index,
previous_ref: (state.header.previous >= 0).then_some(state.header.previous),
next_ref: (state.header.next >= 0).then_some(state.header.next),
changes: state.changes.clone(),
records: state
.records
.iter()
.map(|record| StateRecordView {
revision_id: record.revision_id,
byte_offset: cadmpeg_core::decode::u64_from_index(record.offset),
raw_bytes: record
.offset
.checked_add(record.len)
.and_then(|end| bytes.get(record.offset..end))
.unwrap_or_default(),
name: record.name.as_str(),
})
.collect(),
})
.collect()
}
}
pub fn frame_history_chain(
ctx: &DecodeContext<'_>,
bytes: &[u8],
width: RefWidth,
) -> Result<ChainFraming, CodecError> {
let offsets = delta_state_offsets(bytes);
if offsets.is_empty() {
return Ok(ChainFraming::Absent);
}
let mut states = Vec::new();
for (ordinal, &offset) in offsets.iter().enumerate() {
let header = match read_delta_state_header(bytes, offset, width) {
DeltaStateHeaderRead::Framed(header) => header,
DeltaStateHeaderRead::Unframed => continue,
DeltaStateHeaderRead::Invalid => return Ok(ChainFraming::Unframed),
};
let mut changes = Vec::new();
let Some(body_end) = walk_bulletin_boards(bytes, header.body_start, width, |item| {
if let BulletinBoardItem::Change { kind, .. } = item {
changes.push(kind);
}
}) else {
return Ok(ChainFraming::Unframed);
};
let framed = match frame_state_records(
ctx,
bytes,
body_end,
offsets.get(ordinal + 1).copied(),
width,
)? {
StateRecords::Empty => Vec::new(),
StateRecords::Framed(records) => records,
StateRecords::Unframed { .. } => return Ok(ChainFraming::Unframed),
};
states.push(ChainState {
header,
changes,
records: framed
.into_iter()
.map(|record| ChainRecord {
revision_id: None,
offset: record.offset,
len: record.len,
name: record.name,
})
.collect(),
});
}
if states.is_empty() {
return Ok(ChainFraming::Absent);
}
let snapshot_count = states
.iter()
.flat_map(|state| &state.records)
.filter(|record| record.name != ACTIVE_DATA_END)
.count();
let changes = states
.iter()
.flat_map(|state| state.changes.iter().copied());
if let Some(ids) = snapshot_revision_ids(changes, snapshot_count) {
for (record, id) in states
.iter_mut()
.flat_map(|state| &mut state.records)
.filter(|record| record.name != ACTIVE_DATA_END)
.zip(ids)
{
record.revision_id = Some(id);
}
}
Ok(ChainFraming::Framed(HistoryChain { states }))
}
#[cfg(test)]
mod tests;