use crate::{Error, Result, RevitFile, compression, streams::GLOBAL_ELEM_TABLE};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElemTableHeader {
pub element_count: u16,
pub record_count: u16,
pub header_flag: u16,
pub decompressed_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecordFraming {
Implicit,
Explicit { marker_len: usize },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ElemTableLayout {
pub start: usize,
pub stride: usize,
pub marker_offset: usize,
pub framing: RecordFraming,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElemRecord {
pub offset: usize,
pub id_primary: u32,
pub id_secondary: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner_id: Option<u32>,
pub raw: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElemRecordRough {
pub offset: usize,
pub presumptive_u32_triple: [u32; 3],
}
fn parse_header_bytes(d: &[u8]) -> Result<ElemTableHeader> {
if d.len() < 0x30 {
return Err(Error::BasicFileInfo(
"Global/ElemTable stream too short for header".into(),
));
}
let element_count = u16::from_le_bytes([d[0], d[1]]);
let record_count = u16::from_le_bytes([d[2], d[3]]);
let header_flag = [0x1eusize, 0x22]
.iter()
.find_map(|&off| {
let v = u16::from_le_bytes([d[off], d[off + 1]]);
if v == 0x0011 { Some(v) } else { None }
})
.unwrap_or(0);
Ok(ElemTableHeader {
element_count,
record_count,
header_flag,
decompressed_bytes: d.len(),
})
}
fn flush_origin(d: &[u8], marker_start: usize, stride: usize) -> Option<(usize, usize)> {
if stride == 0 || d.len() < 4 {
return None;
}
let record_count = u16::from_le_bytes([d[2], d[3]]) as usize;
if record_count == 0 {
return None;
}
let span = record_count.checked_mul(stride)?;
if span > d.len() {
return None;
}
let origin = d.len() - span;
if origin > marker_start {
return None;
}
let marker_offset = marker_start - origin;
if marker_offset >= stride || marker_offset % 4 != 0 {
return None;
}
Some((origin, marker_offset))
}
const KNOWN_EXPLICIT_STRIDES: [usize; 2] = [40, 28];
pub fn detect_layout(d: &[u8]) -> ElemTableLayout {
let scan_start = 0x10usize;
let scan_end = d.len().min(512);
let mut markers: Vec<(usize, usize)> = Vec::with_capacity(3);
let mut i = scan_start;
while i + 4 <= scan_end && markers.len() < 3 {
if d[i] == 0xFF && d[i + 1] == 0xFF && d[i + 2] == 0xFF && d[i + 3] == 0xFF {
let eight_ff = i + 8 <= d.len()
&& d[i + 4] == 0xFF
&& d[i + 5] == 0xFF
&& d[i + 6] == 0xFF
&& d[i + 7] == 0xFF;
let len = if eight_ff { 8 } else { 4 };
markers.push((i, len));
i += len;
} else {
i += 1;
}
}
if markers.len() >= 2 {
let (m0, marker_len) = markers[0];
let (m1, _) = markers[1];
let spacing = m1 - m0;
let (stride, start, marker_offset) = match flush_origin(d, m0, spacing) {
Some((start, offset)) => (spacing, start, offset),
None => KNOWN_EXPLICIT_STRIDES
.iter()
.filter(|&&s| s != spacing && spacing % s == 0)
.find_map(|&s| flush_origin(d, m0, s).map(|(start, offset)| (s, start, offset)))
.unwrap_or((spacing, m0, 0)),
};
ElemTableLayout {
start,
stride,
marker_offset,
framing: RecordFraming::Explicit { marker_len },
}
} else {
id_progression_layout(d).unwrap_or(ElemTableLayout {
start: 0x30,
stride: 12,
marker_offset: 0,
framing: RecordFraming::Implicit,
})
}
}
const EXPLICIT_ORIGIN: usize = 0x1E;
fn id_progression_layout(d: &[u8]) -> Option<ElemTableLayout> {
const SAMPLE: usize = 64;
let record_count = usize::from(u16::from_le_bytes([*d.get(2)?, *d.get(3)?]));
let sample = record_count.min(SAMPLE);
if sample < 8 {
return None;
}
let read_u32 = |at: usize| {
d.get(at..at.checked_add(4)?)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
};
for (stride, id_at, id2_at, marker_offset, marker_len) in [(40, 16, 36, 4, 8), (28, 4, 8, 0, 4)]
{
let span = stride * record_count;
if EXPLICIT_ORIGIN.checked_add(span)? > d.len() {
continue;
}
let pairs: Option<Vec<(u32, u32)>> = (0..sample)
.map(|k| {
let record = EXPLICIT_ORIGIN + k * stride;
Some((read_u32(record + id_at)?, read_u32(record + id2_at)?))
})
.collect();
let Some(pairs) = pairs else {
continue;
};
let ascending = pairs.windows(2).all(|w| w[1].0 > w[0].0);
let paired = pairs.iter().all(|(a, b)| a == b);
if pairs[0].0 != 0 && ascending && paired {
return Some(ElemTableLayout {
start: EXPLICIT_ORIGIN,
stride,
marker_offset,
framing: RecordFraming::Explicit { marker_len },
});
}
}
None
}
pub fn parse_header(rf: &mut RevitFile) -> Result<ElemTableHeader> {
let d = inflate(rf)?;
parse_header_bytes(&d)
}
pub fn parse_records_from_bytes(
d: &[u8],
layout: ElemTableLayout,
limit: usize,
) -> Vec<ElemRecord> {
let mut records = Vec::new();
if layout.stride == 0 {
return records;
}
let mut i = layout.start;
while records.len() < limit {
let Some(record_end) = i.checked_add(layout.stride) else {
break;
};
if record_end > d.len() {
break;
}
let (id_primary, id_secondary) = match layout.framing {
RecordFraming::Implicit => {
let a = u32::from_le_bytes([d[i], d[i + 1], d[i + 2], d[i + 3]]);
let b = u32::from_le_bytes([d[i + 4], d[i + 5], d[i + 6], d[i + 7]]);
(a, b)
}
RecordFraming::Explicit { marker_len } => {
let Some(body) = i
.checked_add(layout.marker_offset)
.and_then(|m| m.checked_add(marker_len))
else {
break;
};
if body > record_end {
break;
}
if layout.stride == 28 {
let Some(body_end) = body.checked_add(8) else {
break;
};
if body_end > record_end {
break;
}
let a = u32::from_le_bytes([d[body], d[body + 1], d[body + 2], d[body + 3]]);
let b =
u32::from_le_bytes([d[body + 4], d[body + 5], d[body + 6], d[body + 7]]);
(a, b)
} else if layout.stride == 40 {
let Some(body_end) = body.checked_add(28) else {
break;
};
if body_end > record_end {
break;
}
let a =
u32::from_le_bytes([d[body + 4], d[body + 5], d[body + 6], d[body + 7]]);
let b = u32::from_le_bytes([
d[body + 24],
d[body + 25],
d[body + 26],
d[body + 27],
]);
(a, b)
} else {
let Some(body_end) = body.checked_add(4) else {
break;
};
if body_end > record_end {
break;
}
let a = u32::from_le_bytes([d[body], d[body + 1], d[body + 2], d[body + 3]]);
(a, a)
}
}
};
let owner_id = match layout.framing {
RecordFraming::Implicit => None,
RecordFraming::Explicit { marker_len } => {
owner_field(&d[i..record_end], layout.marker_offset, marker_len)
}
};
let raw = d[i..record_end].to_vec();
records.push(ElemRecord {
offset: i,
id_primary,
id_secondary,
owner_id,
raw,
});
i = record_end;
}
if let RecordFraming::Explicit { marker_len } = layout.framing {
let tail = d
.get(i..)
.and_then(|rest| owner_field(rest, layout.marker_offset, marker_len))
.filter(|owner| records.iter().any(|r| r.id_primary == *owner));
let following: Vec<Option<u32>> = records
.iter()
.skip(1)
.map(|record| record.owner_id)
.chain(std::iter::once(tail))
.collect();
for (record, owner) in records.iter_mut().zip(following) {
record.owner_id = owner;
}
}
records
}
fn owner_field(record: &[u8], at: usize, width: usize) -> Option<u32> {
let field = record.get(at..at.checked_add(width)?)?;
let value = match width {
4 => u64::from(u32::from_le_bytes(field.try_into().ok()?)),
8 => u64::from_le_bytes(field.try_into().ok()?),
_ => return None,
};
if field.iter().all(|&b| b == 0xFF) || value == 0 {
return None;
}
u32::try_from(value).ok()
}
pub fn read_layout(rf: &mut RevitFile) -> Result<ElemTableLayout> {
Ok(detect_layout(&inflate(rf)?))
}
fn inflate(rf: &mut RevitFile) -> Result<Vec<u8>> {
let raw = rf.read_stream(GLOBAL_ELEM_TABLE)?;
compression::inflate_stream_at(GLOBAL_ELEM_TABLE, &raw, 8)
.or_else(|_| compression::inflate_stream_at(GLOBAL_ELEM_TABLE, &raw, 0))
}
pub fn parse_records(rf: &mut RevitFile) -> Result<Vec<ElemRecord>> {
let d = inflate(rf)?;
let header = parse_header_bytes(&d)?;
let layout = detect_layout(&d);
let limit = header.record_count as usize;
Ok(parse_records_from_bytes(&d, layout, limit))
}
pub fn index_by_element_id(records: &[ElemRecord]) -> std::collections::BTreeMap<u32, &ElemRecord> {
let mut map = std::collections::BTreeMap::new();
for record in records {
map.entry(record.id_primary).or_insert(record);
}
map
}
#[derive(Debug, Clone)]
pub struct LinkedArcWallElement<'a> {
pub element_id: u32,
pub elem_record: &'a ElemRecord,
pub partition_ref: crate::partition_arc_walls::PartitionArcWallRef,
}
pub fn link_arcwall_element_ids<'a>(
elem_by_id: &std::collections::BTreeMap<u32, &'a ElemRecord>,
partition_by_id: &std::collections::BTreeMap<
u32,
crate::partition_arc_walls::PartitionArcWallRef,
>,
) -> Vec<LinkedArcWallElement<'a>> {
let mut out = Vec::new();
for (id, partition_ref) in partition_by_id {
if let Some(&elem) = elem_by_id.get(id) {
out.push(LinkedArcWallElement {
element_id: *id,
elem_record: elem,
partition_ref: partition_ref.clone(),
});
}
}
out
}
pub fn link_partition_element_ids<'a>(
elem_by_id: &std::collections::BTreeMap<u32, &'a ElemRecord>,
partition_by_id: &std::collections::BTreeMap<u32, crate::partition_scanner::PartitionRecordRef>,
) -> Vec<crate::partition_scanner::LinkedPartitionElement<'a>> {
crate::partition_scanner::link_elem_table_to_partitions(elem_by_id, partition_by_id)
}
pub fn declared_element_ids(rf: &mut RevitFile) -> Result<Vec<u32>> {
let records = parse_records(rf)?;
Ok(declared_ids(&records).into_iter().collect())
}
pub const RECORD_LEN_40: usize = 40;
pub fn declared_ids(records: &[ElemRecord]) -> std::collections::BTreeSet<u32> {
let mut ids = std::collections::BTreeSet::new();
for record in records {
ids.insert(record.id_primary);
if record.raw.len() == RECORD_LEN_40 && record.id_secondary != 0 {
ids.insert(record.id_secondary);
}
}
ids
}
pub fn parse_records_rough(rf: &mut RevitFile, max_records: usize) -> Result<Vec<ElemRecordRough>> {
let d = inflate(rf)?;
let layout = detect_layout(&d);
let mut records = Vec::new();
let mut i = layout.start;
while i + 12 <= d.len() && records.len() < max_records {
let a = u32::from_le_bytes([d[i], d[i + 1], d[i + 2], d[i + 3]]);
let b = u32::from_le_bytes([d[i + 4], d[i + 5], d[i + 6], d[i + 7]]);
let c = u32::from_le_bytes([d[i + 8], d[i + 9], d[i + 10], d[i + 11]]);
records.push(ElemRecordRough {
offset: i,
presumptive_u32_triple: [a, b, c],
});
i += layout.stride.max(12);
}
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_has_element_count() {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(&[0x96, 0x04]); buf.extend_from_slice(&[0x3c, 0x06]); buf.resize(0x1e, 0);
buf.extend_from_slice(&[0x11, 0x00]); buf.resize(0x30, 0);
assert_eq!(u16::from_le_bytes([buf[0], buf[1]]), 1174);
assert_eq!(u16::from_le_bytes([buf[2], buf[3]]), 1596);
assert_eq!(u16::from_le_bytes([buf[0x1e], buf[0x1f]]), 0x0011);
}
#[test]
fn family_tables_are_recognised_by_their_ids() {
for (stride, id_at, id2_at, owner_at) in
[(40usize, 16usize, 36usize, 4usize), (28, 4, 8, 0)]
{
const RECORDS: usize = 20;
let mut buf = vec![0u8; 0x1e + RECORDS * stride + 96];
buf[2..4].copy_from_slice(&(RECORDS as u16).to_le_bytes());
buf[0x1e + owner_at] = 0x11;
let mut id = 1u32;
for k in 0..RECORDS {
let rec = 0x1e + k * stride;
buf[rec + id_at..rec + id_at + 4].copy_from_slice(&id.to_le_bytes());
buf[rec + id2_at..rec + id2_at + 4].copy_from_slice(&id.to_le_bytes());
id += if k == 0 { 2 } else { 1 };
}
buf[0x1e + RECORDS * stride..].fill(0x5a);
let layout = detect_layout(&buf);
assert_eq!(layout.stride, stride);
assert_eq!(layout.start, 0x1e);
assert!(matches!(layout.framing, RecordFraming::Explicit { .. }));
let records = parse_records_from_bytes(&buf, layout, RECORDS);
assert_eq!(records.len(), RECORDS);
assert_eq!(records[0].id_primary, 1);
assert_eq!(records[1].id_primary, 3);
assert!(records.iter().all(|r| r.owner_id.is_none()));
}
}
#[test]
fn unrecognised_table_falls_back_to_implicit_12b() {
let mut buf = vec![0u8; 0x80];
buf[0] = 0x83;
buf[1] = 0x05;
buf[2] = 0xb7;
buf[3] = 0x07;
buf[0x22] = 0x11;
buf[0x23] = 0x00;
let layout = detect_layout(&buf);
assert_eq!(layout.framing, RecordFraming::Implicit);
assert_eq!(layout.start, 0x30);
assert_eq!(layout.stride, 12);
}
#[test]
fn detect_project_2023_layout_28b_4byte_marker() {
let mut buf = vec![0u8; 0x80];
buf[0x1e] = 0xff;
buf[0x1f] = 0xff;
buf[0x20] = 0xff;
buf[0x21] = 0xff;
buf[0x22] = 0x01;
buf[0x3a] = 0xff;
buf[0x3b] = 0xff;
buf[0x3c] = 0xff;
buf[0x3d] = 0xff;
buf[0x3e] = 0x02;
let layout = detect_layout(&buf);
assert_eq!(layout.framing, RecordFraming::Explicit { marker_len: 4 });
assert_eq!(layout.start, 0x1e);
assert_eq!(layout.stride, 28);
assert_eq!(layout.marker_offset, 0);
}
#[test]
fn detect_project_2024_layout_40b_8byte_marker() {
let mut buf = vec![0u8; 0x80];
buf[0x22..0x2a].fill(0xff);
buf[0x2e] = 0x01;
buf[0x4a..0x52].fill(0xff);
buf[0x56] = 0x02;
let layout = detect_layout(&buf);
assert_eq!(layout.framing, RecordFraming::Explicit { marker_len: 8 });
assert_eq!(layout.start, 0x22);
assert_eq!(layout.stride, 40);
assert_eq!(layout.marker_offset, 0);
}
#[test]
fn sparse_sentinels_do_not_triple_the_2024_stride() {
const RECORDS: usize = 6;
let mut buf = vec![0u8; 30 + RECORDS * 40];
buf[2..4].copy_from_slice(&(RECORDS as u16).to_le_bytes());
for r in 0..RECORDS {
let rec = 30 + r * 40;
if r == 1 || r == 2 {
buf[rec + 4] = 0x10;
} else {
buf[rec + 4..rec + 12].fill(0xff);
}
let id = 0x10_0000 + r as u32;
buf[rec + 16..rec + 20].copy_from_slice(&id.to_le_bytes());
buf[rec + 36..rec + 40].copy_from_slice(&id.to_le_bytes());
}
let layout = detect_layout(&buf);
assert_eq!(layout.stride, 40);
assert_eq!(layout.start, 30);
assert_eq!(layout.marker_offset, 4);
let records = parse_records_from_bytes(&buf, layout, RECORDS);
let ids: Vec<u32> = records.iter().map(|r| r.id_primary).collect();
assert_eq!(
ids,
(0..RECORDS as u32)
.map(|r| 0x10_0000 + r)
.collect::<Vec<_>>()
);
}
#[test]
fn declared_ids_add_a_differing_secondary_id_on_the_40_byte_layout() {
let record = |len: usize, id_primary: u32, id_secondary: u32| ElemRecord {
offset: 0,
id_primary,
id_secondary,
owner_id: None,
raw: vec![0; len],
};
let records = [
record(RECORD_LEN_40, 2_120_309, 2_120_399),
record(RECORD_LEN_40, 2_059_967, 2_059_967),
record(RECORD_LEN_40, 18, 0),
record(28, 18, 5151),
];
assert_eq!(
declared_ids(&records).into_iter().collect::<Vec<_>>(),
vec![18, 2_059_967, 2_120_309, 2_120_399]
);
}
#[test]
fn owner_id_reads_the_40_byte_marker_field() {
let mut buf = vec![0u8; 30 + 3 * 40];
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
for r in 0..3usize {
let rec = 30 + r * 40;
if r == 1 {
buf[rec + 4..rec + 12].copy_from_slice(&786_352u64.to_le_bytes());
} else {
buf[rec + 4..rec + 12].fill(0xff);
}
buf[rec + 16] = r as u8 + 1;
}
let layout = detect_layout(&buf);
assert_eq!((layout.stride, layout.marker_offset), (40, 4));
let owners: Vec<Option<u32>> = parse_records_from_bytes(&buf, layout, 3)
.iter()
.map(|r| r.owner_id)
.collect();
assert_eq!(owners, [Some(786_352), None, None]);
}
#[test]
fn owner_id_reads_the_28_byte_marker_field_and_not_the_family_layout() {
let mut buf = vec![0u8; 0x1e + 3 * 28];
buf[2] = 3;
for r in 0..3usize {
let rec = 0x1e + r * 28;
if r == 2 {
buf[rec..rec + 4].copy_from_slice(&7u32.to_le_bytes());
} else {
buf[rec..rec + 4].fill(0xff);
}
buf[rec + 4] = r as u8 + 1;
}
let layout = detect_layout(&buf);
assert_eq!(layout.stride, 28);
let owners: Vec<Option<u32>> = parse_records_from_bytes(&buf, layout, 3)
.iter()
.map(|r| r.owner_id)
.collect();
assert_eq!(owners, [None, Some(7), None]);
let family = ElemTableLayout {
start: 0x30,
stride: 12,
marker_offset: 0,
framing: RecordFraming::Implicit,
};
let buf = vec![0xffu8; 0x30 + 24];
assert!(
parse_records_from_bytes(&buf, family, 2)
.iter()
.all(|r| r.owner_id.is_none())
);
}
#[test]
fn parse_records_honors_header_record_count_on_project_2023_layout() {
let mut buf = vec![0u8; 0x200];
buf[2] = 0x02; buf[3] = 0x00;
buf[0x1e] = 0xff;
buf[0x1f] = 0xff;
buf[0x20] = 0xff;
buf[0x21] = 0xff;
buf[0x22] = 0x01; buf[0x26] = 0x01; buf[0x3a] = 0xff;
buf[0x3b] = 0xff;
buf[0x3c] = 0xff;
buf[0x3d] = 0xff;
buf[0x3e] = 0x02; buf[0x42] = 0x02; buf[0x56] = 0xff; buf[0x57] = 0xff;
buf[0x58] = 0xff;
buf[0x59] = 0xff;
let layout = detect_layout(&buf);
let records = parse_records_from_bytes(&buf, layout, 2);
assert_eq!(records.len(), 2);
assert_eq!(records[0].id_primary, 1);
assert_eq!(records[0].id_secondary, 1);
assert_eq!(records[1].id_primary, 2);
assert_eq!(records[1].id_secondary, 2);
assert_eq!(records[0].offset, 0x1e);
assert_eq!(records[1].offset, 0x3a);
}
#[test]
fn parse_records_project_2024_layout_reads_id_at_offset_plus_12_and_32() {
let mut buf = vec![0u8; 0x200];
buf[2] = 0x02;
buf[3] = 0x00;
buf[0x22..0x2a].fill(0xff);
buf[0x2e] = 0x01;
buf[0x42] = 0x01;
buf[0x4a..0x52].fill(0xff);
buf[0x56] = 0x02;
buf[0x6a] = 0x02;
let layout = detect_layout(&buf);
assert_eq!(layout.stride, 40);
let records = parse_records_from_bytes(&buf, layout, 2);
assert_eq!(records.len(), 2);
assert_eq!(records[0].id_primary, 1);
assert_eq!(records[0].id_secondary, 1);
assert_eq!(records[1].id_primary, 2);
assert_eq!(records[1].id_secondary, 2);
}
#[test]
fn flush_origin_recovers_the_u32_ahead_of_the_marker() {
let mut buf = vec![0u8; 0x1e + 3 * 40];
buf[2] = 0x03; for k in 0..3usize {
let rs = 0x1e + 40 * k;
buf[rs + 4..rs + 12].fill(0xff);
buf[rs + 16] = (k + 1) as u8;
buf[rs + 36] = (k + 1) as u8;
}
let layout = detect_layout(&buf);
assert_eq!(layout.stride, 40);
assert_eq!(layout.framing, RecordFraming::Explicit { marker_len: 8 });
assert_eq!(layout.start, 0x1e, "origin is the marker minus one u32");
assert_eq!(layout.marker_offset, 4);
let records = parse_records_from_bytes(&buf, layout, 3);
assert_eq!(records.len(), 3, "no record is lost at the tail");
assert_eq!(records[0].offset, 0x1e);
assert_eq!(records[2].offset, 0x1e + 80);
for (k, r) in records.iter().enumerate() {
assert_eq!(r.id_primary, k as u32 + 1);
assert_eq!(r.id_secondary, k as u32 + 1);
}
}
#[test]
fn flush_origin_rejects_a_non_u32_aligned_difference() {
let mut buf = vec![0u8; 0x19 + 3 * 28];
buf[2] = 0x03; buf[0x1e..0x22].fill(0xff);
buf[0x22] = 0x01;
buf[0x26] = 0x01;
buf[0x3a..0x3e].fill(0xff);
buf[0x3e] = 0x02;
buf[0x42] = 0x02;
let layout = detect_layout(&buf);
assert_eq!(layout.stride, 28);
assert_eq!(layout.start, 0x1e);
assert_eq!(layout.marker_offset, 0);
let records = parse_records_from_bytes(&buf, layout, 3);
assert_eq!(records.len(), 2);
}
#[test]
fn flush_origin_ignores_a_stream_shorter_than_the_declared_span() {
let mut buf = vec![0u8; 0x200];
buf[2] = 0xff; buf[3] = 0xff;
buf[0x22..0x2a].fill(0xff);
buf[0x4a..0x52].fill(0xff);
let layout = detect_layout(&buf);
assert_eq!(layout.start, 0x22);
assert_eq!(layout.marker_offset, 0);
}
#[test]
fn index_by_element_id_keeps_first_duplicate() {
let records = vec![
ElemRecord {
offset: 0,
id_primary: 7,
id_secondary: 7,
owner_id: None,
raw: vec![1],
},
ElemRecord {
offset: 28,
id_primary: 7,
id_secondary: 8,
owner_id: None,
raw: vec![2],
},
ElemRecord {
offset: 56,
id_primary: 9,
id_secondary: 9,
owner_id: None,
raw: vec![3],
},
];
let index = index_by_element_id(&records);
assert_eq!(index.len(), 2);
assert_eq!(index.get(&7).map(|r| r.offset), Some(0));
assert_eq!(index.get(&9).map(|r| r.id_secondary), Some(9));
}
}