use crate::partition_element_records::{
BUILTIN_CATEGORY_MAX, BUILTIN_CATEGORY_MIN, PartitionRecordSpan, bbox_marker,
partition_record_chain, supports_revit_version,
};
use crate::{Result, RevitFile};
use std::collections::{BTreeMap, BTreeSet};
pub const NAME_MAX_UNITS: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NameEntry {
pub element_id: u32,
pub name: String,
pub builtin_category: i64,
}
#[derive(Debug, Clone, Default)]
pub struct ElementNames {
pub entries: BTreeMap<u32, NameEntry>,
pub type_family_candidates: BTreeMap<u32, BTreeSet<u32>>,
}
pub const NAME_MAX_UNITS_2023: usize = 128;
pub fn name_entries_2023(rf: &mut RevitFile, declared: &BTreeSet<u32>) -> BTreeMap<u32, String> {
let mut names: BTreeMap<u32, Option<String>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for at in memchr::memmem::find_iter(buf, &[1u8, 0, 0, 0]) {
let (Some(id), Some(units)) = (read_u32(buf, at + 4), read_u32(buf, at + 8)) else {
continue;
};
let units = units as usize;
if !(1..=NAME_MAX_UNITS_2023).contains(&units) || !declared.contains(&id) {
continue;
}
let Some(raw) = buf.get(at + 12..at + 12 + 2 * units) else {
continue;
};
let code: Vec<u16> = raw
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let Ok(name) = String::from_utf16(&code) else {
continue;
};
let printable = name.chars().all(|c| {
!c.is_control() && !('\u{e000}'..='\u{f8ff}').contains(&c) && c < '\u{fff0}'
});
if !printable {
continue;
}
names
.entry(id)
.and_modify(|held| {
if held.as_deref() != Some(name.as_str()) {
*held = None;
}
})
.or_insert(Some(name));
}
}
names
.into_iter()
.filter_map(|(id, name)| Some((id, name?)))
.collect()
}
fn read_u32(buf: &[u8], at: usize) -> Option<u32> {
buf.get(at..at.checked_add(4)?)
.map(|b| u32::from_le_bytes(b.try_into().expect("4 bytes")))
}
fn read_u64(buf: &[u8], at: usize) -> Option<u64> {
buf.get(at..at.checked_add(8)?)
.map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")))
}
pub fn find_name_entries(buf: &[u8], declared_ids: &BTreeSet<u32>) -> Vec<NameEntry> {
let mut out = Vec::new();
for hit in memchr::memmem::find_iter(buf, &[0xff; 5]) {
let Some(at) = hit.checked_sub(3) else {
continue;
};
let Some(category) = read_u64(buf, at).map(|v| v as i64) else {
continue;
};
if !(BUILTIN_CATEGORY_MIN..=BUILTIN_CATEGORY_MAX).contains(&category) {
continue;
}
for units in 1..=NAME_MAX_UNITS {
let Some(start) = at.checked_sub(2 * units) else {
break;
};
let Some(length_at) = start.checked_sub(4) else {
break;
};
if read_u32(buf, length_at) != Some(units as u32) {
continue;
}
let Some(id_at) = length_at.checked_sub(8) else {
break;
};
let Some(element_id) = read_u64(buf, id_at).and_then(|v| u32::try_from(v).ok()) else {
continue;
};
if !declared_ids.contains(&element_id) {
continue;
}
let code_units: Vec<u16> = buf[start..at]
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let Ok(name) = String::from_utf16(&code_units) else {
continue;
};
if name.trim().is_empty() || name.chars().any(char::is_control) {
continue;
}
out.push(NameEntry {
element_id,
name,
builtin_category: category,
});
break;
}
}
out
}
fn unique_entries(entries: impl IntoIterator<Item = NameEntry>) -> BTreeMap<u32, NameEntry> {
let mut by_id: BTreeMap<u32, Option<NameEntry>> = BTreeMap::new();
for entry in entries {
match by_id.get_mut(&entry.element_id) {
None => {
by_id.insert(entry.element_id, Some(entry));
}
Some(slot) => {
if slot.as_ref() != Some(&entry) {
*slot = None;
}
}
}
}
by_id
.into_iter()
.filter_map(|(id, entry)| entry.map(|e| (id, e)))
.collect()
}
fn named_ids_in(
body: &[u8],
entries: &BTreeMap<u32, NameEntry>,
category: i64,
own: u32,
) -> BTreeSet<u32> {
let mut out = BTreeSet::new();
for at in 0..body.len().saturating_sub(7) {
let Some(id) = read_u64(body, at).and_then(|v| u32::try_from(v).ok()) else {
continue;
};
if id != own
&& entries
.get(&id)
.is_some_and(|e| e.builtin_category == category)
{
out.insert(id);
}
}
out
}
pub fn compute_element_names(rf: &mut RevitFile) -> Result<ElementNames> {
let mut out = ElementNames::default();
let version = rf.basic_file_info()?.version;
let Some(marker) = bbox_marker(version).filter(|_| supports_revit_version(version)) else {
return Ok(out);
};
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(out),
};
let mut found = Vec::new();
let mut spans: Vec<(String, PartitionRecordSpan)> = Vec::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
found.extend(find_name_entries(buf, &declared));
spans.extend(
partition_record_chain(buf, &marker)
.into_iter()
.map(|span| (stream.clone(), span)),
);
}
out.entries = unique_entries(found);
for (stream, span) in spans {
let Ok(type_id) = u32::try_from(span.element_id) else {
continue;
};
let Some(entry) = out.entries.get(&type_id) else {
continue;
};
let category = entry.builtin_category;
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let Some(body) = inflated.bytes().get(span.start..span.end) else {
continue;
};
let candidates = named_ids_in(body, &out.entries, category, type_id);
out.type_family_candidates
.entry(type_id)
.or_default()
.extend(candidates);
}
Ok(out)
}
pub const ELEMENT_DATA_HEADER: [u8; 10] =
[0xff, 0xff, 0xff, 0xff, 0xd3, 0x02, 0x01, 0x00, 0x00, 0x00];
pub const ELEMENT_DATA_HEADER_2025: [u8; 10] =
[0xff, 0xff, 0xff, 0xff, 0xef, 0x02, 0x01, 0x00, 0x00, 0x00];
pub fn element_data_header(revit_version: u32) -> Option<[u8; 10]> {
match revit_version {
2024 => Some(ELEMENT_DATA_HEADER),
2025 => Some(ELEMENT_DATA_HEADER_2025),
_ => None,
}
}
pub const ELEMENT_DATA_NAME_WINDOW: usize = 0x600;
pub fn find_element_data_names(
buf: &[u8],
header: &[u8; 10],
wanted: &BTreeSet<u32>,
) -> BTreeMap<u32, String> {
element_data_names(buf, header, wanted, |buf, at| {
read_u64(buf, at).and_then(|v| u32::try_from(v).ok())
})
}
pub const ELEMENT_DATA_HEADER_2023: [u8; 10] =
[0xff, 0xff, 0xff, 0xff, 0xc0, 0x02, 0x01, 0x00, 0x00, 0x00];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementDataLayout {
pub header: [u8; 10],
pub wide_id: bool,
}
impl ElementDataLayout {
pub fn id_at(&self, buf: &[u8], at: usize) -> Option<u32> {
if self.wide_id {
read_u64(buf, at).and_then(|id| u32::try_from(id).ok())
} else {
read_u32(buf, at)
}
}
}
pub fn element_data_layout(revit_version: u32) -> Option<ElementDataLayout> {
match revit_version {
2023 => Some(ElementDataLayout {
header: ELEMENT_DATA_HEADER_2023,
wide_id: false,
}),
_ => element_data_header(revit_version).map(|header| ElementDataLayout {
header,
wide_id: true,
}),
}
}
pub fn find_element_data_names_2023(buf: &[u8], wanted: &BTreeSet<u32>) -> BTreeMap<u32, String> {
element_data_names(buf, &ELEMENT_DATA_HEADER_2023, wanted, read_u32)
}
fn element_data_names(
buf: &[u8],
header: &[u8; 10],
wanted: &BTreeSet<u32>,
read_id: impl Fn(&[u8], usize) -> Option<u32>,
) -> BTreeMap<u32, String> {
let mut found: BTreeMap<u32, Option<String>> = BTreeMap::new();
for hit in memchr::memmem::find_iter(buf, header) {
let id_at = hit + header.len();
let Some(id) = read_id(buf, id_at) else {
continue;
};
if !wanted.contains(&id) {
continue;
}
let Some(name) = first_framed_name(buf, id_at + 8, header) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(name));
}
Some(slot) => {
if slot.as_deref() != Some(name.as_str()) {
*slot = None;
}
}
}
}
found
.into_iter()
.filter_map(|(id, name)| name.map(|n| (id, n)))
.collect()
}
pub const TYPE_OBJECT_TAG: [u8; 2] = [0xdb, 0x0f];
pub const TYPE_OBJECT_TAG_OFFSET: usize = 0x47;
pub const RAILING_TYPE_NAME_WINDOW: std::ops::Range<usize> = 0x100..0x300;
pub const RAILING_TYPE_NAME_END: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0x0b, 0x02];
pub fn find_railing_type_names(buf: &[u8], wanted: &BTreeSet<u32>) -> BTreeMap<u32, String> {
let mut found: BTreeMap<u32, Option<String>> = BTreeMap::new();
for (id, id_at) in crate::partition_id_objects::find_id_objects(buf, wanted) {
let tag_at = id_at + TYPE_OBJECT_TAG_OFFSET;
if buf.get(tag_at..tag_at + 2) != Some(&TYPE_OBJECT_TAG[..]) {
continue;
}
let Some(name) = railing_type_name_at(buf, id_at) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(name));
}
Some(slot) => {
if slot.as_deref() != Some(name.as_str()) {
*slot = None;
}
}
}
}
found
.into_iter()
.filter_map(|(id, name)| name.map(|n| (id, n)))
.collect()
}
pub const PANEL_TYPE_MATERIAL_OFFSET: usize = TYPE_OBJECT_TAG_OFFSET + 2;
pub fn find_panel_type_materials(buf: &[u8], wanted: &BTreeSet<u32>) -> BTreeMap<u32, Option<u64>> {
crate::partition_id_objects::find_id_objects(buf, wanted)
.into_iter()
.filter_map(|(id, id_at)| {
let tag_at = id_at + TYPE_OBJECT_TAG_OFFSET;
(buf.get(tag_at..tag_at + 2) == Some(&TYPE_OBJECT_TAG[..])).then_some(())?;
let raw = buf
.get(id_at + PANEL_TYPE_MATERIAL_OFFSET..id_at + PANEL_TYPE_MATERIAL_OFFSET + 8)?;
let material = u64::from_le_bytes(raw.try_into().ok()?);
Some((id, (material != u64::MAX).then_some(material)))
})
.collect()
}
pub fn find_type_object_ids(buf: &[u8], wanted: &BTreeSet<u32>) -> BTreeSet<u32> {
crate::partition_id_objects::find_id_objects(buf, wanted)
.into_iter()
.filter(|(_, id_at)| {
let tag_at = id_at + TYPE_OBJECT_TAG_OFFSET;
buf.get(tag_at..tag_at + 2) == Some(&TYPE_OBJECT_TAG[..])
})
.map(|(id, _)| id)
.collect()
}
pub const TYPE_NAME_FRAME_TAG: [u8; 2] = [0x49, 0x01];
pub const FONT_FRAME_TAG: [u8; 2] = [0xeb, 0x07];
pub const TEXT_TYPE_WINDOW: std::ops::Range<usize> = 0x40..0x200;
pub fn find_text_type_names(buf: &[u8], wanted: &BTreeSet<u32>) -> BTreeMap<u32, String> {
let mut found: BTreeMap<u32, Option<String>> = BTreeMap::new();
for (id, id_at) in crate::partition_id_objects::find_id_objects(buf, wanted) {
let tag_at = id_at + TYPE_OBJECT_TAG_OFFSET;
if buf.get(tag_at..tag_at + 2) != Some(&TYPE_OBJECT_TAG[..]) {
continue;
}
let start = id_at + TEXT_TYPE_WINDOW.start;
let stop = (id_at + TEXT_TYPE_WINDOW.end).min(buf.len());
let Some((name, name_end)) = framed_string(buf, start, stop, TYPE_NAME_FRAME_TAG) else {
continue;
};
if framed_string(buf, name_end, stop, FONT_FRAME_TAG).is_none() {
continue;
}
match found.get_mut(&id) {
None => {
found.insert(id, Some(name));
}
Some(slot) => {
if slot.as_deref() != Some(name.as_str()) {
*slot = None;
}
}
}
}
found
.into_iter()
.filter_map(|(id, name)| name.map(|n| (id, n)))
.collect()
}
fn framed_string(buf: &[u8], start: usize, stop: usize, tag: [u8; 2]) -> Option<(String, usize)> {
let frame = [0xff, 0xff, 0xff, 0xff, tag[0], tag[1]];
let window = buf.get(start..stop)?;
let at = start + memchr::memmem::find(window, &frame)? + frame.len();
let units = read_u32(buf, at)? as usize;
if !(1..=NAME_MAX_UNITS).contains(&units) {
return None;
}
let end = at + 4 + 2 * units;
let code_units: Vec<u16> = buf
.get(at + 4..end)?
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let text = String::from_utf16(&code_units).ok()?;
if text.trim().is_empty()
|| text
.chars()
.any(|c| c.is_control() || matches!(c, '\u{fffd}' | '\u{fffe}' | '\u{ffff}'))
{
return None;
}
Some((text, end))
}
fn railing_type_name_at(buf: &[u8], id_at: usize) -> Option<String> {
let start = id_at.checked_add(RAILING_TYPE_NAME_WINDOW.start)?;
let stop = id_at
.checked_add(RAILING_TYPE_NAME_WINDOW.end)?
.min(buf.len());
let end = start + memchr::memmem::find(buf.get(start..stop)?, &RAILING_TYPE_NAME_END)?;
let mut names = (1..=NAME_MAX_UNITS).filter_map(|units| {
let at = end.checked_sub(4 + 2 * units)?;
if read_u32(buf, at)? as usize != units {
return None;
}
let code_units: Vec<u16> = buf
.get(at + 4..end)?
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let name = String::from_utf16(&code_units).ok()?;
(!name.trim().is_empty()
&& !name
.chars()
.any(|c| c.is_control() || matches!(c, '\u{fffd}' | '\u{fffe}' | '\u{ffff}')))
.then_some(name)
});
let name = names.next()?;
names.next().is_none().then_some(name)
}
fn first_framed_name(buf: &[u8], start: usize, header: &[u8; 10]) -> Option<String> {
let end = start
.saturating_add(ELEMENT_DATA_NAME_WINDOW)
.min(buf.len());
let window = buf.get(start..end)?;
for at in memchr::memmem::find_iter(window, &[0xff; 4]) {
let at = start + at;
let Some(tag) = buf.get(at + 4..at + 6) else {
continue;
};
if tag == [0xff, 0xff] || buf.get(at..at + header.len()) == Some(&header[..]) {
continue;
}
let Some(units) = read_u32(buf, at + 6).map(|n| n as usize) else {
continue;
};
if !(1..=NAME_MAX_UNITS).contains(&units) {
continue;
}
let Some(bytes) = buf.get(at + 10..at + 10 + units * 2) else {
continue;
};
let code_units: Vec<u16> = bytes
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let Ok(name) = String::from_utf16(&code_units) else {
continue;
};
if name.trim().is_empty()
|| name
.chars()
.any(|c| c.is_control() || matches!(c, '\u{fffd}' | '\u{fffe}' | '\u{ffff}'))
{
continue;
}
return Some(name);
}
None
}
pub fn resolve_type(
names: &ElementNames,
references: &[u64],
category: i64,
own: u32,
) -> Option<u32> {
let mut found = references
.iter()
.filter_map(|&id| u32::try_from(id).ok())
.filter(|&id| id != own)
.filter(|id| {
names
.entries
.get(id)
.is_some_and(|e| e.builtin_category == category)
})
.collect::<BTreeSet<u32>>()
.into_iter();
let first = found.next()?;
found.next().is_none().then_some(first)
}
pub fn resolve_family(names: &ElementNames, type_id: u32) -> Option<u32> {
let candidates = names.type_family_candidates.get(&type_id)?;
let mut iter = candidates.iter();
let first = *iter.next()?;
if iter.next().is_none() {
return Some(first);
}
let mut hosts = candidates.iter().copied().filter(|family| {
names
.type_family_candidates
.get(family)
.is_some_and(|nested| {
candidates
.iter()
.all(|other| other == family || nested.contains(other))
})
});
let host = hosts.next()?;
hosts.next().is_none().then_some(host)
}
#[cfg(test)]
mod tests {
use super::*;
fn entry_bytes(id: u32, name: &str, category: i64) -> Vec<u8> {
let units: Vec<u16> = name.encode_utf16().collect();
let mut out = u64::from(id).to_le_bytes().to_vec();
out.extend_from_slice(&(units.len() as u32).to_le_bytes());
for unit in units {
out.extend_from_slice(&unit.to_le_bytes());
}
out.extend_from_slice(&(category as u64).to_le_bytes());
out
}
const PANELS: i64 = -2_000_170;
#[test]
fn a_2025_type_name_skips_frames_whose_length_is_unset() {
let mut buf = ELEMENT_DATA_HEADER_2025.to_vec();
buf.extend_from_slice(&381_192u64.to_le_bytes());
buf.extend_from_slice(&[0xff; 58]);
buf.extend_from_slice(&[0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x3c, 0x10, 0xff, 0xff, 0xff, 0xff]);
buf.extend_from_slice(&[0xff; 4]);
buf.extend_from_slice(&[0x09, 0xd1, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x43, 0x03, 0xff, 0xff, 0xff, 0xff]);
buf.extend_from_slice(&[0xff; 20]);
buf.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x6a, 0x10, 0x01, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[b'-', 0x00]);
let wanted = BTreeSet::from([381_192]);
let names = find_element_data_names(&buf, &ELEMENT_DATA_HEADER_2025, &wanted);
assert_eq!(names.get(&381_192).map(String::as_str), Some("-"));
assert!(find_element_data_names(&buf, &ELEMENT_DATA_HEADER, &wanted).is_empty());
assert_eq!(element_data_header(2025), Some(ELEMENT_DATA_HEADER_2025));
assert_eq!(element_data_header(2023), None);
}
#[test]
fn a_system_type_name_follows_its_element_data_header() {
let framed = |tag: u16, name: &str| {
let units: Vec<u16> = name.encode_utf16().collect();
let mut out = vec![0xff; 4];
out.extend_from_slice(&tag.to_le_bytes());
out.extend_from_slice(&(units.len() as u32).to_le_bytes());
for unit in units {
out.extend_from_slice(&unit.to_le_bytes());
}
out
};
let data = |id: u32, name: &str| {
let mut out = ELEMENT_DATA_HEADER.to_vec();
out.extend_from_slice(&u64::from(id).to_le_bytes());
out.extend_from_slice(&[0xff; 12]);
out.extend_from_slice(&[0x00, 0x10]);
out.extend(framed(0x1002, name));
out
};
let mut buf = vec![0u8; 7];
buf.extend(data(17328, "8\" Interior Partition 3 Hour"));
buf.extend(data(22757, "Basement Slab"));
let names =
find_element_data_names(&buf, &ELEMENT_DATA_HEADER, &BTreeSet::from([17328, 22757]));
assert_eq!(
names.get(&17328).map(String::as_str),
Some("8\" Interior Partition 3 Hour")
);
assert_eq!(names.get(&22757).map(String::as_str), Some("Basement Slab"));
assert!(
find_element_data_names(&buf, &ELEMENT_DATA_HEADER, &BTreeSet::from([1])).is_empty()
);
buf.extend(data(17328, "Another name"));
let names = find_element_data_names(&buf, &ELEMENT_DATA_HEADER, &BTreeSet::from([17328]));
assert!(names.is_empty(), "disagreeing copies give no name");
}
#[test]
fn a_name_entry_decodes_between_filler() {
let declared: BTreeSet<u32> = [21944, 21943].into_iter().collect();
let buf = [
vec![0xff; 20],
entry_bytes(21944, "Empty", PANELS),
vec![0u8; 7],
entry_bytes(21943, "Empty System Panel", PANELS),
vec![0xff; 9],
]
.concat();
let found = find_name_entries(&buf, &declared);
let names: Vec<(u32, &str)> = found
.iter()
.map(|e| (e.element_id, e.name.as_str()))
.collect();
assert_eq!(names, vec![(21944, "Empty"), (21943, "Empty System Panel")]);
assert!(found.iter().all(|e| e.builtin_category == PANELS));
}
#[test]
fn an_undeclared_id_or_a_category_outside_the_band_is_not_an_entry() {
let declared: BTreeSet<u32> = [7].into_iter().collect();
let buf = [
entry_bytes(8, "Undeclared", PANELS),
entry_bytes(7, "Not a category", -5),
]
.concat();
assert!(find_name_entries(&buf, &declared).is_empty());
}
#[test]
fn a_non_ascii_name_decodes_and_a_control_character_does_not() {
let declared: BTreeSet<u32> = [1, 2].into_iter().collect();
let buf = [
entry_bytes(1, "Porta simples 0,90 × 2,10", -2_000_023),
entry_bytes(2, "bad\u{7}name", -2_000_023),
]
.concat();
let found = find_name_entries(&buf, &declared);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "Porta simples 0,90 × 2,10");
}
#[test]
fn disagreeing_entries_for_one_id_are_dropped() {
let a = NameEntry {
element_id: 5,
name: "A".into(),
builtin_category: PANELS,
};
let b = NameEntry {
name: "B".into(),
..a.clone()
};
let c = NameEntry {
element_id: 6,
..a.clone()
};
let unique = unique_entries([a.clone(), a.clone(), b, c.clone()]);
assert_eq!(unique.into_values().collect::<Vec<_>>(), vec![c]);
}
#[test]
fn type_and_family_resolve_only_when_unique() {
let entry = |id, name: &str, category| {
(
id,
NameEntry {
element_id: id,
name: name.into(),
builtin_category: category,
},
)
};
let names = ElementNames {
entries: BTreeMap::from([
entry(100, "Single-Flush", -2_000_023),
entry(101, "36\" x 84\"", -2_000_023),
entry(102, "30\" x 84\"", -2_000_023),
entry(103, "Bi-Fold", -2_000_023),
entry(200, "Wall type", -2_000_011),
]),
type_family_candidates: BTreeMap::from([
(101, BTreeSet::from([100])),
(102, BTreeSet::from([100, 103])),
]),
};
assert_eq!(
resolve_type(&names, &[3, 101, 200, 500], -2_000_023, 500),
Some(101)
);
assert_eq!(
resolve_type(&names, &[3, 101, 102, 500], -2_000_023, 500),
None
);
assert_eq!(resolve_family(&names, 101), Some(100));
assert_eq!(resolve_family(&names, 102), None);
}
#[test]
fn a_nesting_family_is_told_from_the_families_nested_in_it() {
let mut names = ElementNames::default();
let set = |ids: &[u32]| ids.iter().copied().collect::<BTreeSet<u32>>();
names
.type_family_candidates
.insert(444_100, set(&[51_892, 755_868]));
names.type_family_candidates.insert(755_868, set(&[51_892]));
assert_eq!(resolve_family(&names, 444_100), Some(755_868));
names.type_family_candidates.insert(21_944, set(&[21_943]));
assert_eq!(resolve_family(&names, 21_944), Some(21_943));
names.type_family_candidates.insert(1, set(&[10, 20]));
names.type_family_candidates.insert(10, set(&[30]));
assert_eq!(resolve_family(&names, 1), None);
names.type_family_candidates.insert(2, set(&[40, 50]));
names.type_family_candidates.insert(40, set(&[50]));
names.type_family_candidates.insert(50, set(&[40]));
assert_eq!(resolve_family(&names, 2), None);
}
#[test]
fn named_ids_are_found_at_any_offset_of_a_record_body() {
let names = BTreeMap::from([(
21943,
NameEntry {
element_id: 21943,
name: "Empty System Panel".into(),
builtin_category: PANELS,
},
)]);
let mut body = vec![0xffu8; 3];
body.extend_from_slice(&21943u64.to_le_bytes());
body.extend_from_slice(&21944u64.to_le_bytes());
assert_eq!(
named_ids_in(&body, &names, PANELS, 21944),
BTreeSet::from([21943])
);
}
}