use crate::{Result, RevitFile, compression, elem_table, streams};
use std::collections::BTreeMap;
pub const EPISODE_COUNT_OFFSET: usize = 0x0e;
pub const EPISODE_TABLE_OFFSET: usize = 0x66;
pub const EPISODE_ENTRY_LEN: usize = 17;
pub const EPISODE_TERMINATOR: u8 = 0x28;
pub const RECORD_EPISODE_OFFSET: usize = 0x18;
pub const RECORD_EPISODE_OFFSET_28: usize = 0x0c;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpisodeHistory {
guids: Vec<[u8; 16]>,
}
fn read_u32(buf: &[u8], at: usize) -> Option<u32> {
buf.get(at..at.checked_add(4)?)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
}
impl EpisodeHistory {
pub fn parse(history: &[u8]) -> Option<Self> {
let count = read_u32(history, EPISODE_COUNT_OFFSET)? as usize;
let skipped = read_u32(history, EPISODE_TABLE_OFFSET)? as usize;
let list_count_at = EPISODE_TABLE_OFFSET
.checked_add(4)?
.checked_add(skipped.checked_mul(4)?)?;
if read_u32(history, list_count_at)? as usize != count {
return None;
}
let start = list_count_at + 4;
let end = start.checked_add(count.checked_mul(EPISODE_ENTRY_LEN)?)?;
let list = history.get(start..end)?;
let mut guids = Vec::with_capacity(count);
for entry in list.chunks_exact(EPISODE_ENTRY_LEN) {
if entry[16] != EPISODE_TERMINATOR {
return None;
}
guids.push(entry[..16].try_into().expect("16 bytes"));
}
Some(Self { guids })
}
pub fn len(&self) -> usize {
self.guids.len()
}
pub fn is_empty(&self) -> bool {
self.guids.is_empty()
}
pub fn episode_guid(&self, episode: u32) -> Option<[u8; 16]> {
let index = self
.guids
.len()
.checked_sub(1)?
.checked_sub(episode as usize)?;
self.guids.get(index).copied()
}
}
pub fn canonical_guid(stored: [u8; 16]) -> [u8; 16] {
let mut out = stored;
out[0..4].reverse();
out[4..6].reverse();
out[6..8].reverse();
out
}
pub fn compress_ifc_guid(value: [u8; 16]) -> String {
const ALPHABET: &[u8; 64] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
let mut n = u128::from_be_bytes(value);
let mut out = [0u8; 22];
for slot in out.iter_mut().rev() {
*slot = ALPHABET[(n & 63) as usize];
n >>= 6;
}
out.iter().map(|&b| char::from(b)).collect()
}
pub fn revit_ifc_global_id(episode_guid: [u8; 16], element_id: u32) -> String {
let mut value = canonical_guid(episode_guid);
let low = u32::from_be_bytes(value[12..16].try_into().expect("4 bytes")) ^ element_id;
value[12..16].copy_from_slice(&low.to_be_bytes());
compress_ifc_guid(value)
}
fn inflate_history(rf: &mut RevitFile) -> Result<Vec<u8>> {
let stored = rf.read_stream(streams::GLOBAL_HISTORY)?;
let prepared = compression::prepare_stream_for_inflate(streams::GLOBAL_HISTORY, &stored);
let data = prepared.as_ref();
let mut last = None;
for offset in [0, 4, 8, 16] {
if compression::has_gzip_magic(data, offset) {
match compression::inflate_at(data, offset) {
Ok(out) => return Ok(out),
Err(error) => last = Some(error),
}
}
}
match last {
Some(error) => Err(error),
None => compression::inflate_at_auto(data).map(|(_, out)| out),
}
}
pub fn revit_global_ids(rf: &mut RevitFile) -> Result<BTreeMap<u32, String>> {
let records = elem_table::parse_records(rf)?;
let episode_offset = match records.first().map(|record| record.raw.len()) {
Some(elem_table::RECORD_LEN_40) => RECORD_EPISODE_OFFSET,
Some(28) => RECORD_EPISODE_OFFSET_28,
_ => return Ok(BTreeMap::new()),
};
let Some(history) = EpisodeHistory::parse(&inflate_history(rf)?) else {
return Ok(BTreeMap::new());
};
let mut out = BTreeMap::new();
for record in &records {
let Some(episode) = read_u32(&record.raw, episode_offset) else {
continue;
};
let Some(guid) = history.episode_guid(episode) else {
continue;
};
let global_id = revit_ifc_global_id(guid, record.id_primary);
for id in [record.id_primary, record.id_secondary] {
if id != 0 {
out.entry(id).or_insert_with(|| global_id.clone());
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
const EPISODE: [u8; 16] = [
0x00, 0x20, 0xf9, 0x50, 0xf8, 0x82, 0x58, 0x42, 0xb6, 0xa8, 0xbb, 0x42, 0xc0, 0x1b, 0x00,
0xa0,
];
#[test]
fn a_global_id_is_the_episode_guid_xor_the_element_id() {
assert_eq!(
revit_ifc_global_id(EPISODE, 619_340),
"1G_I00WlX2MBQekqB04dFi"
);
assert_eq!(
revit_ifc_global_id(EPISODE, 619_404),
"1G_I00WlX2MBQekqB04dCi"
);
assert_eq!(
canonical_guid(EPISODE),
[
0x50, 0xf9, 0x20, 0x00, 0x82, 0xf8, 0x42, 0x58, 0xb6, 0xa8, 0xbb, 0x42, 0xc0, 0x1b,
0x00, 0xa0
]
);
assert_eq!(compress_ifc_guid([0; 16]), "0000000000000000000000");
assert_eq!(compress_ifc_guid([0xff; 16]), "3$$$$$$$$$$$$$$$$$$$$$");
}
fn history(guids: &[[u8; 16]], skipped: &[u32]) -> Vec<u8> {
let mut out = vec![0u8; EPISODE_TABLE_OFFSET];
out[EPISODE_COUNT_OFFSET..EPISODE_COUNT_OFFSET + 4]
.copy_from_slice(&(guids.len() as u32).to_le_bytes());
out.extend_from_slice(&(skipped.len() as u32).to_le_bytes());
for value in skipped {
out.extend_from_slice(&value.to_le_bytes());
}
out.extend_from_slice(&(guids.len() as u32).to_le_bytes());
for guid in guids {
out.extend_from_slice(guid);
out.push(EPISODE_TERMINATOR);
}
out.extend_from_slice(&[0; 4]);
out
}
#[test]
fn the_history_lists_episodes_newest_first() {
let guids = [[1u8; 16], [2u8; 16], [3u8; 16]];
let parsed = EpisodeHistory::parse(&history(&guids, &[472, 481])).expect("history");
assert_eq!(parsed.len(), 3);
assert_eq!(parsed.episode_guid(0), Some([3u8; 16]));
assert_eq!(parsed.episode_guid(2), Some([1u8; 16]));
assert_eq!(parsed.episode_guid(3), None);
}
#[test]
fn a_malformed_history_is_rejected() {
let guids = [[1u8; 16], [2u8; 16]];
let mut bytes = history(&guids, &[]);
bytes[EPISODE_TABLE_OFFSET + 4] = 3;
assert_eq!(EpisodeHistory::parse(&bytes), None);
let mut bytes = history(&guids, &[]);
let last_terminator = bytes.len() - 5;
bytes[last_terminator] = 0;
assert_eq!(EpisodeHistory::parse(&bytes), None);
let mut bytes = history(&guids, &[]);
bytes[EPISODE_COUNT_OFFSET..EPISODE_COUNT_OFFSET + 4]
.copy_from_slice(&u32::MAX.to_le_bytes());
assert_eq!(EpisodeHistory::parse(&bytes), None);
}
}