use crate::{Result, RevitFile};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const ROOM_PARAMETER_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2024];
pub const OWNER_CONFIRM_OFFSET: usize = 0x3c;
pub const LEVEL_OFFSET: usize = 0x44;
pub const NUMBER_LENGTH_OFFSET: usize = 505;
pub const NAME_GAP_AFTER_NUMBER: usize = 8;
pub const MAX_VALUE_CHARS: usize = 64;
pub const ROOM_PARAMETER_SOURCE: &str = "partition_room_parameter_block";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomParameters {
pub stream: String,
pub offset: usize,
pub element_id: u32,
pub number: String,
pub name: String,
pub level_slot: u64,
}
impl RoomParameters {
pub fn level_element_id(&self, level_ids: &BTreeSet<u32>) -> Option<u32> {
let id = u32::try_from(self.level_slot).ok()?;
level_ids.contains(&id).then_some(id)
}
}
pub fn supports_revit_version(revit_version: u32) -> bool {
ROOM_PARAMETER_SUPPORTED_REVIT_VERSIONS.contains(&revit_version)
}
fn read_u32(buf: &[u8], at: usize) -> Option<u32> {
buf.get(at..at + 4)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
}
fn read_u64(buf: &[u8], at: usize) -> Option<u64> {
buf.get(at..at + 8)
.map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
}
fn utf16_at(buf: &[u8], at: usize, chars: usize) -> Option<String> {
if !(1..=MAX_VALUE_CHARS).contains(&chars) {
return None;
}
let bytes = buf.get(at..at.checked_add(chars.checked_mul(2)?)?)?;
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let text = String::from_utf16(&units).ok()?;
if text.chars().any(char::is_control) {
return None;
}
Some(text)
}
pub fn decode_at(
stream: &str,
buf: &[u8],
owner_at: usize,
element_id: u32,
) -> Option<RoomParameters> {
if read_u64(buf, owner_at)? != u64::from(element_id) {
return None;
}
let confirm_at = owner_at.checked_add(OWNER_CONFIRM_OFFSET)?;
if read_u64(buf, confirm_at)? != u64::from(element_id) {
return None;
}
let level_slot = read_u64(buf, owner_at.checked_add(LEVEL_OFFSET)?)?;
let number_len_at = owner_at.checked_add(NUMBER_LENGTH_OFFSET)?;
let number_chars = read_u32(buf, number_len_at)? as usize;
let number_at = number_len_at.checked_add(4)?;
let number = utf16_at(buf, number_at, number_chars)?;
let name_len_at = number_at
.checked_add(number_chars.checked_mul(2)?)?
.checked_add(NAME_GAP_AFTER_NUMBER)?;
let name_chars = read_u32(buf, name_len_at)? as usize;
let name = utf16_at(buf, name_len_at.checked_add(4)?, name_chars)?;
Some(RoomParameters {
stream: stream.to_string(),
offset: owner_at,
element_id,
number,
name,
level_slot,
})
}
pub fn find_room_parameters(
stream: &str,
buf: &[u8],
rooms: &BTreeSet<u32>,
) -> Vec<RoomParameters> {
let mut out = Vec::new();
let (Some(&low), Some(&high)) = (rooms.first(), rooms.last()) else {
return out;
};
if buf.len() < 8 {
return out;
}
let (low, high) = (u64::from(low), u64::from(high));
for owner_at in 0..=(buf.len() - 8) {
let raw = u64::from_le_bytes(buf[owner_at..owner_at + 8].try_into().expect("8 bytes"));
if raw == 0 || raw < low || raw > high {
continue;
}
let element_id = raw as u32;
if !rooms.contains(&element_id) {
continue;
}
if let Some(block) = decode_at(stream, buf, owner_at, element_id) {
out.push(block);
}
}
out
}
pub fn scan_room_parameters(
rf: &mut RevitFile,
revit_version: u32,
rooms: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, RoomParameters>> {
if !supports_revit_version(revit_version) || rooms.is_empty() {
return Ok(BTreeMap::new());
}
let mut found: BTreeMap<u32, Vec<RoomParameters>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for block in find_room_parameters(&stream, inflated.bytes(), rooms) {
found.entry(block.element_id).or_default().push(block);
}
}
Ok(resolve_unique(found))
}
pub fn resolve_unique(found: BTreeMap<u32, Vec<RoomParameters>>) -> BTreeMap<u32, RoomParameters> {
let mut out = BTreeMap::new();
for (id, blocks) in found {
let Some(first) = blocks.first() else {
continue;
};
let agree = blocks.iter().all(|block| {
block.number == first.number
&& block.name == first.name
&& block.level_slot == first.level_slot
});
if agree {
out.insert(id, first.clone());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_block(element_id: u32, number: &str, name: &str, level: u64) -> Vec<u8> {
let number_units: Vec<u16> = number.encode_utf16().collect();
let name_units: Vec<u16> = name.encode_utf16().collect();
let name_len_at = NUMBER_LENGTH_OFFSET + 4 + number_units.len() * 2 + NAME_GAP_AFTER_NUMBER;
let mut buf = vec![0xabu8; name_len_at + 4 + name_units.len() * 2 + 16];
buf[0..8].copy_from_slice(&u64::from(element_id).to_le_bytes());
buf[OWNER_CONFIRM_OFFSET..OWNER_CONFIRM_OFFSET + 8]
.copy_from_slice(&u64::from(element_id).to_le_bytes());
buf[LEVEL_OFFSET..LEVEL_OFFSET + 8].copy_from_slice(&level.to_le_bytes());
buf[NUMBER_LENGTH_OFFSET..NUMBER_LENGTH_OFFSET + 4]
.copy_from_slice(&(number_units.len() as u32).to_le_bytes());
for (index, unit) in number_units.iter().enumerate() {
let at = NUMBER_LENGTH_OFFSET + 4 + index * 2;
buf[at..at + 2].copy_from_slice(&unit.to_le_bytes());
}
buf[name_len_at..name_len_at + 4].copy_from_slice(&(name_units.len() as u32).to_le_bytes());
for (index, unit) in name_units.iter().enumerate() {
let at = name_len_at + 4 + index * 2;
buf[at..at + 2].copy_from_slice(&unit.to_le_bytes());
}
buf
}
#[test]
fn decodes_a_well_formed_room_block() {
let buf = synth_block(20_822, "66", "Stair 2", 20_308);
let block = decode_at("Partitions/46", &buf, 0, 20_822).expect("decodes");
assert_eq!(block.element_id, 20_822);
assert_eq!(block.number, "66");
assert_eq!(block.name, "Stair 2");
assert_eq!(block.level_slot, 20_308);
assert_eq!(
block.level_element_id(&[20_308u32].into_iter().collect()),
Some(20_308)
);
}
#[test]
fn a_longer_number_still_finds_the_name() {
let buf = synth_block(60_221, "1005", "Cooridor Suite 1", 20_304);
let block = decode_at("Partitions/59", &buf, 0, 60_221).expect("decodes");
assert_eq!(block.number, "1005");
assert_eq!(block.name, "Cooridor Suite 1");
}
#[test]
fn a_disagreeing_confirmation_slot_fails_closed() {
let mut buf = synth_block(20_822, "66", "Stair 2", 20_308);
buf[OWNER_CONFIRM_OFFSET..OWNER_CONFIRM_OFFSET + 8]
.copy_from_slice(&20_823u64.to_le_bytes());
assert!(decode_at("Partitions/46", &buf, 0, 20_822).is_none());
}
#[test]
fn a_wrong_anchor_id_fails_closed() {
let buf = synth_block(20_822, "66", "Stair 2", 20_308);
assert!(decode_at("Partitions/46", &buf, 0, 20_823).is_none());
}
#[test]
fn an_implausible_length_fails_closed() {
let mut buf = synth_block(20_822, "66", "Stair 2", 20_308);
buf[NUMBER_LENGTH_OFFSET..NUMBER_LENGTH_OFFSET + 4]
.copy_from_slice(&9_999u32.to_le_bytes());
assert!(decode_at("Partitions/46", &buf, 0, 20_822).is_none());
buf[NUMBER_LENGTH_OFFSET..NUMBER_LENGTH_OFFSET + 4].copy_from_slice(&0u32.to_le_bytes());
assert!(decode_at("Partitions/46", &buf, 0, 20_822).is_none());
}
#[test]
fn a_control_character_in_a_value_fails_closed() {
let buf = synth_block(20_822, "66", "Stair\u{7}2", 20_308);
assert!(decode_at("Partitions/46", &buf, 0, 20_822).is_none());
}
#[test]
fn a_truncated_block_fails_closed() {
let buf = synth_block(20_822, "66", "Stair 2", 20_308);
for cut in [8usize, OWNER_CONFIRM_OFFSET + 4, NUMBER_LENGTH_OFFSET + 2] {
assert!(decode_at("Partitions/46", &buf[..cut], 0, 20_822).is_none());
}
}
#[test]
fn the_scan_only_accepts_ids_it_was_given() {
let buf = synth_block(20_822, "66", "Stair 2", 20_308);
let hits = find_room_parameters("Partitions/46", &buf, &[20_822u32].into_iter().collect());
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].name, "Stair 2");
assert!(find_room_parameters("Partitions/46", &buf, &BTreeSet::new()).is_empty());
assert!(
find_room_parameters("Partitions/46", &buf, &[999u32].into_iter().collect()).is_empty()
);
}
#[test]
fn two_blocks_that_agree_resolve_to_one() {
let block = |offset: usize| RoomParameters {
stream: "Partitions/46".into(),
offset,
element_id: 20_822,
number: "66".into(),
name: "Stair 2".into(),
level_slot: 20_308,
};
let found = [(20_822u32, vec![block(0), block(4096)])]
.into_iter()
.collect();
let resolved = resolve_unique(found);
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[&20_822].name, "Stair 2");
}
#[test]
fn two_blocks_that_disagree_resolve_to_nothing() {
let mut a = RoomParameters {
stream: "Partitions/46".into(),
offset: 0,
element_id: 20_822,
number: "66".into(),
name: "Stair 2".into(),
level_slot: 20_308,
};
let b = RoomParameters {
name: "Stair 3".into(),
offset: 4096,
..a.clone()
};
a.stream = "Partitions/46".into();
let found = [(20_822u32, vec![a, b])].into_iter().collect();
assert!(resolve_unique(found).is_empty());
}
#[test]
fn an_unsupported_release_recovers_nothing() {
assert!(!supports_revit_version(2023));
assert!(supports_revit_version(2024));
}
#[test]
fn a_level_slot_outside_the_recovered_set_binds_nothing() {
let buf = synth_block(20_822, "66", "Stair 2", 99_999);
let block = decode_at("Partitions/46", &buf, 0, 20_822).expect("decodes");
assert_eq!(
block.level_element_id(&[20_308u32].into_iter().collect()),
None
);
let huge = synth_block(20_822, "66", "Stair 2", u64::MAX);
let block = decode_at("Partitions/46", &huge, 0, 20_822).expect("decodes");
assert_eq!(
block.level_element_id(&[20_308u32].into_iter().collect()),
None
);
}
}