use std::collections::BTreeMap;
use cadmpeg_ir::codec::{CodecError, ContainerEntry, ContainerSummary, ReadSeek};
use crate::variant::Variant;
pub const OUTER_MAGIC: &[u8; 8] = b"V5_CFV2\0";
pub const DIR_MAGIC: &[u8; 16] = b"CATIA_V5 CB0001\0";
const FBB_MARKER: &[u8; 4] = &[0x30, 0x04, 0x04, 0xff];
const EDGE_DELIMITER: &[u8; 8] = &[0x10, 0x24, 0x04, 0xff, 0xff, 0x00, 0x00, 0x00];
const VERTEX_MARKER: &[u8; 3] = &[0x05, 0x08, 0x01];
const A9_MARKER: &[u8; 2] = &[0xa9, 0x03];
const E5_MARKER: &[u8; 3] = &[0xe5, 0x0d, 0x03];
pub mod role {
pub const STREAM: &str = "stream";
}
#[derive(Debug, Clone)]
pub struct Extent {
pub phys_off: u32,
pub phys_len: u32,
pub log_len: u32,
pub log_off: u32,
pub flags: u32,
}
#[derive(Debug, Clone)]
pub struct Descriptor {
pub name: String,
pub desc_offset: usize,
pub logical_length: u32,
pub extents: Vec<Extent>,
}
#[derive(Debug, Clone)]
pub struct InnerDir {
pub inner: usize,
pub dir_offset: usize,
pub dir_length: u32,
pub descriptors: Vec<Descriptor>,
}
#[derive(Debug, Clone, Default)]
pub struct Census {
pub fbb_runs: usize,
pub edge_delimiters: usize,
pub vertex_markers: usize,
pub a9_markers: usize,
pub e5_markers: usize,
}
pub struct ContainerScan {
pub data: Vec<u8>,
pub outer_dir_offset: u32,
pub outer_dir_length: u32,
pub inner: Option<InnerDir>,
pub brep: Option<Vec<u8>>,
pub census: Census,
pub variant: Variant,
}
pub fn looks_like_catia(prefix: &[u8]) -> bool {
prefix.starts_with(OUTER_MAGIC)
}
fn u32_be(bytes: &[u8], at: usize) -> Option<u32> {
bytes
.get(at..at + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
}
fn count_stride8_fbb(body: &[u8]) -> usize {
let mut count = 0;
let mut i = 0;
while i + 4 <= body.len() {
if &body[i..i + 4] == FBB_MARKER {
count += 1;
i += 8;
} else {
i += 1;
}
}
count
}
fn count_subslice(haystack: &[u8], needle: &[u8]) -> usize {
if needle.is_empty() || haystack.len() < needle.len() {
return 0;
}
haystack
.windows(needle.len())
.filter(|w| *w == needle)
.count()
}
pub fn parse_stream_directory(data: &[u8]) -> Option<InnerDir> {
if data.len() < 16 {
return None;
}
let inner = find_subslice(data, OUTER_MAGIC, OUTER_MAGIC.len())?;
if inner + 16 > data.len() {
return None;
}
let a = u32_be(data, inner + 8)? as usize;
let b = u32_be(data, inner + 12)?;
let dir_offset = inner + a;
if dir_offset + 16 > data.len() || &data[dir_offset..dir_offset + 16] != DIR_MAGIC {
return None;
}
let b_usize = b as usize;
if b == 0 || dir_offset + b_usize > data.len() {
return None;
}
let dirbuf = &data[dir_offset..dir_offset + b_usize];
let file_len = data.len();
let mut descriptors = Vec::new();
let mut o = 0usize;
while o + 4 <= dirbuf.len() {
let Some(k) = u32_be(dirbuf, o).map(|value| value as usize) else {
break;
};
if (1..=64).contains(&k) && o + 4 + 20 * k <= dirbuf.len() {
if let Some((extents, cum)) = parse_extents(dirbuf, o, k, inner, file_len) {
if cum > 0 && o >= 0x50 {
let ds = o - 0x50;
let logical_length = u32_be(dirbuf, ds + 0x0c).unwrap_or(0);
if logical_length as usize == cum {
descriptors.push(Descriptor {
name: descriptor_name(dirbuf, ds),
desc_offset: ds,
logical_length,
extents,
});
}
}
}
}
o += 1;
}
if descriptors.is_empty() {
return None;
}
Some(InnerDir {
inner,
dir_offset,
dir_length: b,
descriptors,
})
}
fn parse_extents(
dirbuf: &[u8],
o: usize,
k: usize,
inner: usize,
file_len: usize,
) -> Option<(Vec<Extent>, usize)> {
let mut extents = Vec::with_capacity(k);
let mut cum: usize = 0;
for i in 0..k {
let base = o + 4 + 20 * i;
let phys_off = u32_be(dirbuf, base)?;
let phys_len = u32_be(dirbuf, base + 4)?;
let log_len = u32_be(dirbuf, base + 8)?;
let log_off = u32_be(dirbuf, base + 12)?;
let flags = u32_be(dirbuf, base + 16)?;
if phys_len == 0
|| inner + phys_off as usize + phys_len as usize > file_len
|| log_off as usize != cum
|| log_len != phys_len
{
return None;
}
cum += log_len as usize;
extents.push(Extent {
phys_off,
phys_len,
log_len,
log_off,
flags,
});
}
Some((extents, cum))
}
fn descriptor_name(dirbuf: &[u8], ds: usize) -> String {
let start = ds.saturating_sub(40);
let window = &dirbuf[start..ds + 0x50.min(dirbuf.len() - ds)];
let mut best = String::new();
let mut i = 0;
while i + 1 < window.len() {
let mut chars = String::new();
let mut j = i;
while j + 1 < window.len() && (0x20..0x7f).contains(&window[j]) && window[j + 1] == 0 {
chars.push(window[j] as char);
j += 2;
}
if chars.len() >= 3 {
if chars.len() > best.len() {
best = chars;
}
i = j;
} else {
i += 1;
}
}
best
}
pub fn reconstruct_logical_stream(data: &[u8], descriptor: &Descriptor, inner: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(descriptor.logical_length as usize);
for e in &descriptor.extents {
let start = inner + e.phys_off as usize;
let end = start + e.phys_len as usize;
if end <= data.len() {
out.extend_from_slice(&data[start..end]);
}
}
out
}
pub fn brep_stream(data: &[u8], dir: &InnerDir) -> Option<Vec<u8>> {
let main = dir
.descriptors
.iter()
.filter(|d| d.name == "MainDataStream")
.max_by_key(|d| d.logical_length)?;
let surf = dir
.descriptors
.iter()
.filter(|d| d.name.contains("Surf"))
.max_by_key(|d| d.logical_length)?;
let mut out = reconstruct_logical_stream(data, main, dir.inner);
out.extend(reconstruct_logical_stream(data, surf, dir.inner));
Some(out)
}
fn find_subslice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
if from >= haystack.len() || needle.is_empty() {
return None;
}
haystack[from..]
.windows(needle.len())
.position(|w| w == needle)
.map(|p| p + from)
}
fn identify_variant(inner: Option<&InnerDir>, brep: Option<&[u8]>, census: &Census) -> Variant {
match (inner, brep) {
(None, _) => {
if census.a9_markers > 0 {
Variant::ZeroEntity
} else {
Variant::Unknown
}
}
(Some(_), None) => Variant::InnerNoDirectory,
(Some(_), Some(_)) => {
if census.fbb_runs > 0 {
if census.edge_delimiters > 0 {
Variant::StandardNested
} else {
Variant::FbbOnly
}
} else if census.e5_markers > 0 {
Variant::E5Stream
} else {
Variant::FloatPackedInnerNoFbb
}
}
}
}
pub fn scan(reader: &mut dyn ReadSeek) -> Result<ContainerScan, CodecError> {
reader
.seek(std::io::SeekFrom::Start(0))
.map_err(CodecError::Io)?;
let mut data = Vec::new();
reader.read_to_end(&mut data).map_err(CodecError::Io)?;
Ok(scan_bytes(data))
}
pub fn scan_bytes(data: Vec<u8>) -> ContainerScan {
let outer_dir_offset = u32_be(&data, 8).unwrap_or(0);
let outer_dir_length = u32_be(&data, 12).unwrap_or(0);
let inner = parse_stream_directory(&data);
let brep = inner.as_ref().and_then(|dir| brep_stream(&data, dir));
let mut census = Census {
a9_markers: count_subslice(&data, A9_MARKER),
e5_markers: count_subslice(&data, E5_MARKER),
..Default::default()
};
if let Some(b) = &brep {
census.fbb_runs = count_stride8_fbb(b);
census.edge_delimiters = count_subslice(b, EDGE_DELIMITER);
census.vertex_markers = count_subslice(b, VERTEX_MARKER);
}
let variant = identify_variant(inner.as_ref(), brep.as_deref(), &census);
ContainerScan {
data,
outer_dir_offset,
outer_dir_length,
inner,
brep,
census,
variant,
}
}
pub fn summarize(scan: &ContainerScan) -> ContainerSummary {
let mut entries = Vec::new();
if let Some(dir) = &scan.inner {
for d in &dir.descriptors {
let mut attributes = BTreeMap::new();
attributes.insert("desc_offset".to_string(), d.desc_offset.to_string());
attributes.insert("extent_count".to_string(), d.extents.len().to_string());
let phys: u64 = d.extents.iter().map(|e| e.phys_len as u64).sum();
entries.push(ContainerEntry {
name: if d.name.is_empty() {
format!("stream@{}", d.desc_offset)
} else {
d.name.clone()
},
role: role::STREAM.to_string(),
compression: "none".to_string(),
compressed_size: phys,
uncompressed_size: d.logical_length as u64,
attributes,
});
}
}
let mut notes = vec![format!(
"outer V5_CFV2 container: directory offset {} + length {} = {} (file size {}); variant: {}",
scan.outer_dir_offset,
scan.outer_dir_length,
scan.outer_dir_offset as u64 + scan.outer_dir_length as u64,
scan.data.len(),
scan.variant.description(),
)];
match &scan.inner {
Some(dir) => notes.push(format!(
"nested V5_CFV2 at file offset {} with a CATIA_V5 CB0001 directory of {} stream(s)",
dir.inner,
dir.descriptors.len()
)),
None => notes.push(
"no nested V5_CFV2 sub-container (outer-preamble record families only)".to_string(),
),
}
if scan.brep.is_some() {
notes.push(format!(
"reconstructed BREP stream from MainDataStream + SurfacicReps: {} FBB run(s), {} \
vertex record(s), {} edge-table delimiter(s)",
scan.census.fbb_runs, scan.census.vertex_markers, scan.census.edge_delimiters
));
}
if scan.census.a9_markers > 0 || scan.census.e5_markers > 0 {
notes.push(format!(
"record-family census: {} a9 03, {} e5 0d 03",
scan.census.a9_markers, scan.census.e5_markers
));
}
notes.push(
"container-level enumeration; run `decode` to build geometry from the standard-nested \
BREP stream (other variants are container-only)"
.to_string(),
);
ContainerSummary {
format: "catia".to_string(),
container_kind: "v5-cfv2".to_string(),
entries,
notes,
}
}