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
}
pub const ROOM_NUMBER_PARAMETER: i64 = -1_006_901;
pub const ROOM_NAME_PARAMETER: i64 = -1_006_900;
pub const ROOM_PARAMETER_ENTRY_WINDOW: usize = 0x400;
pub const ROOM_PARAMETER_ENTRY_SOURCE: &str = "partition_room_parameter_entries";
pub fn scan_room_parameter_entries(
rf: &mut RevitFile,
revit_version: u32,
rooms: &BTreeSet<u32>,
) -> BTreeMap<u32, (String, String)> {
let Some(layout) = crate::partition_names::element_data_layout(revit_version) else {
return BTreeMap::new();
};
let (id_len, number_tag, name_tag) = if layout.wide_id {
(
8,
ROOM_NUMBER_PARAMETER.to_le_bytes().to_vec(),
ROOM_NAME_PARAMETER.to_le_bytes().to_vec(),
)
} else {
let narrow = |id: i64| i32::try_from(id).expect("a 32-bit BuiltInParameter");
(
4,
narrow(ROOM_NUMBER_PARAMETER).to_le_bytes().to_vec(),
narrow(ROOM_NAME_PARAMETER).to_le_bytes().to_vec(),
)
};
let string_at = |buf: &[u8], at: usize, limit: usize| -> Option<(String, usize)> {
let n = usize::try_from(u32::from_le_bytes(buf.get(at..at + 4)?.try_into().ok()?)).ok()?;
if !(1..=limit).contains(&n) {
return None;
}
let units: Vec<u16> = buf
.get(at + 4..at + 4 + 2 * n)?
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let text = String::from_utf16(&units).ok()?;
(!text.chars().any(char::is_control)).then_some((text, at + 4 + 2 * n))
};
let mut found: BTreeMap<u32, Option<(String, String)>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for hit in memchr::memmem::find_iter(buf, &[1u8, 0, 0, 0]) {
let Some(id) = layout.id_at(buf, hit + 4).filter(|id| rooms.contains(id)) else {
continue;
};
let start = hit + 4 + id_len;
let end = (start + ROOM_PARAMETER_ENTRY_WINDOW).min(buf.len());
let Some(window) = buf.get(start..end) else {
continue;
};
let pair = memchr::memmem::find_iter(window, &number_tag).find_map(|at| {
let at = start + at;
let (number, after) = string_at(buf, at + number_tag.len(), 64)?;
(buf.get(after..after + name_tag.len())? == &name_tag[..]).then_some(())?;
let (name, _) = string_at(buf, after + name_tag.len(), 256)?;
Some((number, name))
});
let Some(pair) = pair else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(pair));
}
Some(held) => {
if held.as_ref() != Some(&pair) {
*held = None;
}
}
}
}
}
found
.into_iter()
.filter_map(|(id, pair)| pair.map(|p| (id, p)))
.collect()
}
#[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
);
}
}