use crate::walker::{WalkerLimitHit, WalkerLimits};
use crate::{Error, Result};
pub const ARC_WALL_TAG: u16 = 0x0191;
pub const ARC_WALL_VARIANT_STANDARD: u16 = 0x07fa;
pub const ARC_WALL_VARIANT_COMPOUND: u16 = 0x0821;
pub const SCHEMA_FAMILY_MARKER: u32 = 0x0008_8004;
pub const RECORD_TRAILER: u8 = 0x03;
pub const STANDARD_RECORD_MIN_SIZE: usize = 0x73;
pub const STANDARD_RECORD_SINGLETON_STRIDE: usize = 292;
pub const STANDARD_TRAILER_START: usize = 0x73;
pub const TRAILER_ELEMENT_ID_OFFSET: usize = 0x10e;
pub const TRAILER_ELEMENT_ID_DUP_OFFSET: usize = 0x11c;
pub const TRAILER_TYPE_ID_OFFSET: usize = 0xfe;
pub const TRAILER_BASE_ELEVATION_OFFSET: usize = 0xf6;
pub const STANDARD_TRAILER_DECODE_END: usize = TRAILER_ELEMENT_ID_DUP_OFFSET + 4;
pub const ARC_WALL_STANDARD_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2023];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ArcWallTrailer {
pub element_id: Option<u32>,
pub type_id: Option<u32>,
pub base_elevation_feet: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArcWallScanStatus {
Supported { revit_version: u32 },
UnsupportedVersion { revit_version: u32 },
}
impl ArcWallScanStatus {
pub fn diagnostic_message(self) -> Option<String> {
match self {
ArcWallScanStatus::Supported { .. } => None,
ArcWallScanStatus::UnsupportedVersion { revit_version } => Some(format!(
"ArcWall standard decoder skipped: Revit {revit_version} is outside supported versions {ARC_WALL_STANDARD_SUPPORTED_REVIT_VERSIONS:?}"
)),
}
}
pub fn is_supported(self) -> bool {
matches!(self, ArcWallScanStatus::Supported { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArcWallFindResult {
pub offsets: Vec<usize>,
pub scanned_bytes: usize,
pub limit_hit: Option<WalkerLimitHit>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArcWallScanReport {
pub status: ArcWallScanStatus,
pub offsets: Vec<usize>,
pub scanned_bytes: usize,
pub limit_hit: Option<WalkerLimitHit>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ArcWallRecord {
pub tag: u16,
pub variant: u16,
pub fixed_header_0: u32,
pub count_version: u32,
pub type_code: u32,
pub coords: [f64; 6],
pub coords_dup: [f64; 6],
pub trailer: u8,
}
impl ArcWallRecord {
pub fn standard_decoder_status(revit_version: u32) -> ArcWallScanStatus {
if ARC_WALL_STANDARD_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
ArcWallScanStatus::Supported { revit_version }
} else {
ArcWallScanStatus::UnsupportedVersion { revit_version }
}
}
pub fn supports_revit_version(revit_version: u32) -> bool {
Self::standard_decoder_status(revit_version).is_supported()
}
pub fn decode_standard(buf: &[u8], offset: usize) -> Result<Self> {
let record_end = offset
.checked_add(STANDARD_RECORD_MIN_SIZE)
.ok_or_else(|| {
Error::Cfb(format!(
"ArcWall decode: offset overflow at offset {offset}"
))
})?;
if record_end > buf.len() {
return Err(Error::Cfb(format!(
"ArcWall decode: buffer too short ({} < {} at offset {})",
buf.len(),
record_end,
offset
)));
}
let tag = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
if tag != ARC_WALL_TAG {
return Err(Error::Cfb(format!(
"ArcWall decode: expected tag 0x{ARC_WALL_TAG:04x}, got 0x{tag:04x} at offset {offset}"
)));
}
let filter_pad = u16::from_le_bytes([buf[offset + 2], buf[offset + 3]]);
if filter_pad != 0 {
return Err(Error::Cfb(format!(
"ArcWall decode: expected filter_pad 0x0000, got 0x{filter_pad:04x} at offset {offset}"
)));
}
let variant = u16::from_le_bytes([buf[offset + 0x10], buf[offset + 0x11]]);
if variant != ARC_WALL_VARIANT_STANDARD {
return Err(Error::Cfb(format!(
"ArcWall decode: expected variant 0x{ARC_WALL_VARIANT_STANDARD:04x}, got 0x{variant:04x} at offset {offset}"
)));
}
let fixed_header_0 = u32::from_le_bytes([
buf[offset + 0x04],
buf[offset + 0x05],
buf[offset + 0x06],
buf[offset + 0x07],
]);
let count_version = u32::from_le_bytes([
buf[offset + 0x08],
buf[offset + 0x09],
buf[offset + 0x0a],
buf[offset + 0x0b],
]);
let type_code = u32::from_le_bytes([
buf[offset + 0x0c],
buf[offset + 0x0d],
buf[offset + 0x0e],
buf[offset + 0x0f],
]);
let mut coords = [0f64; 6];
for (i, slot) in coords.iter_mut().enumerate() {
let p = offset + 0x12 + i * 8;
*slot = f64::from_le_bytes([
buf[p],
buf[p + 1],
buf[p + 2],
buf[p + 3],
buf[p + 4],
buf[p + 5],
buf[p + 6],
buf[p + 7],
]);
}
let mut coords_dup = [0f64; 6];
for (i, slot) in coords_dup.iter_mut().enumerate() {
let p = offset + 0x42 + i * 8;
*slot = f64::from_le_bytes([
buf[p],
buf[p + 1],
buf[p + 2],
buf[p + 3],
buf[p + 4],
buf[p + 5],
buf[p + 6],
buf[p + 7],
]);
}
let trailer = buf[offset + 0x72];
Ok(ArcWallRecord {
tag,
variant,
fixed_header_0,
count_version,
type_code,
coords,
coords_dup,
trailer,
})
}
pub fn find_all(buf: &[u8]) -> Vec<usize> {
Self::find_all_with_limits(buf, WalkerLimits::default()).offsets
}
pub fn find_all_with_limits(buf: &[u8], limits: WalkerLimits) -> ArcWallFindResult {
let mut out = Vec::new();
let mut limit_hit = None;
let scan_len = buf.len().min(limits.max_scan_bytes);
if buf.len() > limits.max_scan_bytes {
limit_hit = Some(WalkerLimitHit::MaxScanBytes);
}
if scan_len < STANDARD_RECORD_MIN_SIZE {
return ArcWallFindResult {
offsets: out,
scanned_bytes: scan_len,
limit_hit,
};
}
for i in 0..=(scan_len - STANDARD_RECORD_MIN_SIZE) {
let tag = u16::from_le_bytes([buf[i], buf[i + 1]]);
if tag != ARC_WALL_TAG {
continue;
}
if buf[i + 2] != 0 || buf[i + 3] != 0 {
continue;
}
let variant = u16::from_le_bytes([buf[i + 0x10], buf[i + 0x11]]);
if variant != ARC_WALL_VARIANT_STANDARD {
continue;
}
out.push(i);
if out.len() >= limits.max_candidates {
limit_hit.get_or_insert(WalkerLimitHit::MaxCandidates);
break;
}
}
ArcWallFindResult {
offsets: out,
scanned_bytes: scan_len,
limit_hit,
}
}
pub fn scan_standard_for_revit_version(revit_version: u32, buf: &[u8]) -> ArcWallScanReport {
Self::scan_standard_for_revit_version_with_limits(
revit_version,
buf,
WalkerLimits::default(),
)
}
pub fn scan_standard_for_revit_version_with_limits(
revit_version: u32,
buf: &[u8],
limits: WalkerLimits,
) -> ArcWallScanReport {
let status = Self::standard_decoder_status(revit_version);
if !status.is_supported() {
return ArcWallScanReport {
status,
offsets: Vec::new(),
scanned_bytes: 0,
limit_hit: None,
};
}
let found = Self::find_all_with_limits(buf, limits);
ArcWallScanReport {
status,
offsets: found.offsets,
scanned_bytes: found.scanned_bytes,
limit_hit: found.limit_hit,
}
}
pub fn start_point(&self) -> (f64, f64, f64) {
(self.coords[0], self.coords[1], self.coords[2])
}
pub fn end_point(&self) -> (f64, f64, f64) {
(self.coords[3], self.coords[4], self.coords[5])
}
pub fn coords_match(&self) -> bool {
self.coords == self.coords_dup
}
pub fn height_feet(&self) -> Option<f64> {
let (_, _, sz) = self.start_point();
let (_, _, ez) = self.end_point();
if !sz.is_finite() || !ez.is_finite() {
return None;
}
let height = (ez - sz).abs();
(height >= 0.1).then_some(height)
}
pub fn decode_trailer(buf: &[u8], offset: usize) -> Option<ArcWallTrailer> {
let end = offset.checked_add(STANDARD_TRAILER_DECODE_END)?;
if end > buf.len() {
return None;
}
let element_id_raw = u32::from_le_bytes([
buf[offset + TRAILER_ELEMENT_ID_OFFSET],
buf[offset + TRAILER_ELEMENT_ID_OFFSET + 1],
buf[offset + TRAILER_ELEMENT_ID_OFFSET + 2],
buf[offset + TRAILER_ELEMENT_ID_OFFSET + 3],
]);
let element_id_dup = u32::from_le_bytes([
buf[offset + TRAILER_ELEMENT_ID_DUP_OFFSET],
buf[offset + TRAILER_ELEMENT_ID_DUP_OFFSET + 1],
buf[offset + TRAILER_ELEMENT_ID_DUP_OFFSET + 2],
buf[offset + TRAILER_ELEMENT_ID_DUP_OFFSET + 3],
]);
let element_id =
(element_id_raw == element_id_dup && element_id_raw != 0 && element_id_raw != u32::MAX)
.then_some(element_id_raw);
let type_id_raw = u32::from_le_bytes([
buf[offset + TRAILER_TYPE_ID_OFFSET],
buf[offset + TRAILER_TYPE_ID_OFFSET + 1],
buf[offset + TRAILER_TYPE_ID_OFFSET + 2],
buf[offset + TRAILER_TYPE_ID_OFFSET + 3],
]);
let type_id = (type_id_raw != 0 && type_id_raw != u32::MAX).then_some(type_id_raw);
let base_elevation = f64::from_le_bytes([
buf[offset + TRAILER_BASE_ELEVATION_OFFSET],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 1],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 2],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 3],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 4],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 5],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 6],
buf[offset + TRAILER_BASE_ELEVATION_OFFSET + 7],
]);
let base_elevation_feet = base_elevation.is_finite().then_some(base_elevation);
Some(ArcWallTrailer {
element_id,
type_id,
base_elevation_feet,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const RECORD_4_HEX: &[u8] = &[
0x91, 0x01, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0xfa, 0x07, 0x63, 0x7f, 0x48, 0x57, 0x8a, 0x77, 0x22, 0x40, 0x9c, 0xd5, 0xb6, 0x13,
0x76, 0xaa, 0x39, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x7a,
0x96, 0x54, 0x07, 0x29, 0x40, 0x32, 0xf5, 0x9b, 0x5b, 0x6f, 0x7c, 0x3a, 0x40, 0x8f, 0xf2,
0xa3, 0xfc, 0x28, 0x3f, 0x1a, 0x40, 0x63, 0x7f, 0x48, 0x57, 0x8a, 0x77, 0x22, 0x40, 0x9c,
0xd5, 0xb6, 0x13, 0x76, 0xaa, 0x39, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0e, 0x13, 0x7a, 0x96, 0x54, 0x07, 0x29, 0x40, 0x32, 0xf5, 0x9b, 0x5b, 0x6f, 0x7c, 0x3a,
0x40, 0x8f, 0xf2, 0xa3, 0xfc, 0x28, 0x3f, 0x1a, 0x40, 0x03,
];
#[test]
fn decodes_fixture_record_4() {
let rec = ArcWallRecord::decode_standard(RECORD_4_HEX, 0).expect("record 4 must decode");
assert_eq!(rec.tag, ARC_WALL_TAG);
assert_eq!(rec.variant, ARC_WALL_VARIANT_STANDARD);
assert_eq!(rec.fixed_header_0, SCHEMA_FAMILY_MARKER);
assert_eq!(rec.count_version, 1);
assert_eq!(rec.type_code, 3);
assert_eq!(rec.trailer, RECORD_TRAILER);
}
#[test]
fn record_4_coords_are_finite() {
let rec = ArcWallRecord::decode_standard(RECORD_4_HEX, 0).unwrap();
for c in &rec.coords {
assert!(c.is_finite(), "coord must be finite, got {c}");
}
assert_eq!(rec.coords[2], 0.0);
assert!(rec.coords[0] > 0.0);
assert!(rec.coords[1] > 0.0);
}
#[test]
fn record_4_coords_match_duplicate() {
let rec = ArcWallRecord::decode_standard(RECORD_4_HEX, 0).unwrap();
assert!(
rec.coords_match(),
"record 4 has identical coords/coords_dup per RE-14.3"
);
}
#[test]
fn start_and_end_point_helpers() {
let rec = ArcWallRecord::decode_standard(RECORD_4_HEX, 0).unwrap();
let (sx, sy, sz) = rec.start_point();
let (ex, ey, ez) = rec.end_point();
assert_eq!((sx, sy, sz), (rec.coords[0], rec.coords[1], rec.coords[2]));
assert_eq!((ex, ey, ez), (rec.coords[3], rec.coords[4], rec.coords[5]));
}
#[test]
fn rejects_wrong_tag() {
let mut buf = RECORD_4_HEX.to_vec();
buf[0] = 0xff; let err = ArcWallRecord::decode_standard(&buf, 0).unwrap_err();
assert!(err.to_string().contains("tag"));
}
#[test]
fn rejects_nonzero_filter_pad() {
let mut buf = RECORD_4_HEX.to_vec();
buf[2] = 0x01; let err = ArcWallRecord::decode_standard(&buf, 0).unwrap_err();
assert!(err.to_string().contains("filter_pad"));
}
#[test]
fn rejects_wrong_variant() {
let mut buf = RECORD_4_HEX.to_vec();
buf[0x10] = 0x21; buf[0x11] = 0x00;
let err = ArcWallRecord::decode_standard(&buf, 0).unwrap_err();
assert!(err.to_string().contains("variant"));
}
#[test]
fn rejects_buffer_too_short() {
let err = ArcWallRecord::decode_standard(&RECORD_4_HEX[..20], 0).unwrap_err();
assert!(err.to_string().contains("too short"));
}
#[test]
fn find_all_finds_embedded_record() {
let mut buf = vec![0u8; 100];
buf.extend_from_slice(RECORD_4_HEX);
buf.extend_from_slice(&[0u8; 100]);
let found = ArcWallRecord::find_all(&buf);
assert_eq!(found, vec![100]);
}
#[test]
fn version_gated_scan_allows_supported_2023_pattern() {
let mut buf = vec![0u8; 100];
buf.extend_from_slice(RECORD_4_HEX);
buf.extend_from_slice(&[0u8; 100]);
let report = ArcWallRecord::scan_standard_for_revit_version(2023, &buf);
assert_eq!(
report.status,
ArcWallScanStatus::Supported {
revit_version: 2023
}
);
assert_eq!(report.offsets, vec![100]);
assert_eq!(report.status.diagnostic_message(), None);
}
#[test]
fn version_gated_scan_suppresses_unsupported_2024_pattern() {
let mut buf = vec![0u8; 100];
buf.extend_from_slice(RECORD_4_HEX);
buf.extend_from_slice(&[0u8; 100]);
let report = ArcWallRecord::scan_standard_for_revit_version(2024, &buf);
assert_eq!(
report.status,
ArcWallScanStatus::UnsupportedVersion {
revit_version: 2024
}
);
assert!(
report.offsets.is_empty(),
"2024 must not reuse the 2023 ArcWall record envelope"
);
let diagnostic = report
.status
.diagnostic_message()
.expect("unsupported status should produce a diagnostic");
assert!(diagnostic.contains("Revit 2024"));
assert!(diagnostic.contains("[2023]"));
}
#[test]
fn find_all_empty_on_noise() {
let buf = vec![0xff_u8; 10_000];
let found = ArcWallRecord::find_all(&buf);
assert!(found.is_empty());
}
#[test]
fn height_feet_from_z_delta() {
let rec = ArcWallRecord::decode_standard(RECORD_4_HEX, 0).unwrap();
let height = rec.height_feet().expect("record 4 has Z delta");
assert!((height - 6.5617).abs() < 1e-3);
}
#[test]
fn decode_trailer_requires_singleton_span() {
assert!(ArcWallRecord::decode_trailer(RECORD_4_HEX, 0).is_none());
}
#[test]
fn decode_trailer_validates_element_id_echo() {
let mut buf = vec![0u8; STANDARD_RECORD_SINGLETON_STRIDE];
buf[..RECORD_4_HEX.len()].copy_from_slice(RECORD_4_HEX);
buf[TRAILER_TYPE_ID_OFFSET..TRAILER_TYPE_ID_OFFSET + 4]
.copy_from_slice(&0x217au32.to_le_bytes());
buf[TRAILER_BASE_ELEVATION_OFFSET..TRAILER_BASE_ELEVATION_OFFSET + 8]
.copy_from_slice(&0.0f64.to_le_bytes());
buf[TRAILER_ELEMENT_ID_OFFSET..TRAILER_ELEMENT_ID_OFFSET + 4]
.copy_from_slice(&0x1c79u32.to_le_bytes());
buf[TRAILER_ELEMENT_ID_DUP_OFFSET..TRAILER_ELEMENT_ID_DUP_OFFSET + 4]
.copy_from_slice(&0x1c79u32.to_le_bytes());
let trailer = ArcWallRecord::decode_trailer(&buf, 0).expect("trailer span present");
assert_eq!(trailer.element_id, Some(0x1c79));
assert_eq!(trailer.type_id, Some(0x217a));
assert_eq!(trailer.base_elevation_feet, Some(0.0));
buf[TRAILER_ELEMENT_ID_DUP_OFFSET..TRAILER_ELEMENT_ID_DUP_OFFSET + 4]
.copy_from_slice(&0x2u32.to_le_bytes());
let trailer = ArcWallRecord::decode_trailer(&buf, 0).unwrap();
assert_eq!(trailer.element_id, None);
}
#[test]
fn find_all_with_limits_honors_max_candidates() {
let mut buf = vec![0u8; 500];
for off in [0usize, 200] {
buf[off] = (ARC_WALL_TAG & 0xff) as u8;
buf[off + 1] = (ARC_WALL_TAG >> 8) as u8;
buf[off + 0x10] = (ARC_WALL_VARIANT_STANDARD & 0xff) as u8;
buf[off + 0x11] = (ARC_WALL_VARIANT_STANDARD >> 8) as u8;
}
let limits = WalkerLimits {
max_candidates: 1,
..WalkerLimits::default()
};
let found = ArcWallRecord::find_all_with_limits(&buf, limits);
assert_eq!(found.offsets.len(), 1);
assert_eq!(found.limit_hit, Some(WalkerLimitHit::MaxCandidates));
}
#[test]
fn find_all_with_limits_honors_max_scan_bytes() {
let mut buf = vec![0u8; 400];
buf[300] = (ARC_WALL_TAG & 0xff) as u8;
buf[301] = (ARC_WALL_TAG >> 8) as u8;
buf[300 + 0x10] = (ARC_WALL_VARIANT_STANDARD & 0xff) as u8;
buf[300 + 0x11] = (ARC_WALL_VARIANT_STANDARD >> 8) as u8;
let limits = WalkerLimits {
max_scan_bytes: 200,
..WalkerLimits::default()
};
let found = ArcWallRecord::find_all_with_limits(&buf, limits);
assert!(found.offsets.is_empty());
assert_eq!(found.limit_hit, Some(WalkerLimitHit::MaxScanBytes));
assert_eq!(found.scanned_bytes, 200);
}
}