use super::types::*;
pub mod tags {
pub const INTEGER: u8 = 0x04;
pub const DOUBLE: u8 = 0x06;
pub const STRING: u8 = 0x07;
pub const FALSE: u8 = 0x0A;
pub const TRUE: u8 = 0x0B;
pub const POINTER: u8 = 0x0C;
pub const ENTITY_TYPE: u8 = 0x0D;
pub const SUBTYPE: u8 = 0x0E;
pub const END_OF_RECORD: u8 = 0x11;
pub const POSITION: u8 = 0x13;
pub const DIRECTION: u8 = 0x14;
}
const SAB_MAGIC: &[u8] = b"ACIS BinaryFile";
pub struct SabWriter;
impl SabWriter {
pub fn write(doc: &SatDocument) -> Vec<u8> {
let mut buf = Vec::with_capacity(8192);
Self::write_header(&mut buf, &doc.header);
for record in &doc.records {
Self::write_record(&mut buf, record);
}
Self::write_entity_type(&mut buf, "End-of-ACIS-data");
buf
}
fn write_header(buf: &mut Vec<u8>, header: &SatHeader) {
buf.extend_from_slice(SAB_MAGIC);
let ver = header.version.sat_version_number();
buf.extend_from_slice(&ver.to_le_bytes());
let num_records: u32 = if header.version.has_explicit_indices() {
0
} else {
header.num_records as u32
};
buf.extend_from_slice(&num_records.to_le_bytes());
buf.extend_from_slice(&(header.num_bodies as u32).to_le_bytes());
let history: u32 = if header.has_history { 1 } else { 0 };
buf.extend_from_slice(&history.to_le_bytes());
Self::write_string(buf, &header.product_id);
Self::write_string(buf, &header.product_version);
Self::write_string(buf, &header.date);
Self::write_double(buf, header.spatial_resolution);
Self::write_double(buf, header.normal_tolerance);
if let Some(resfit) = header.resfit_tolerance {
Self::write_double(buf, resfit);
}
}
fn write_record(buf: &mut Vec<u8>, record: &SatRecord) {
if record.entity_type.contains('-') {
let parts: Vec<&str> = record.entity_type.split('-').collect();
for &part in &parts[..parts.len() - 1] {
Self::write_subtype(buf, part);
}
Self::write_entity_type(buf, parts[parts.len() - 1]);
} else {
Self::write_entity_type(buf, &record.entity_type);
}
Self::write_pointer(buf, record.attribute.0);
Self::write_integer(buf, record.subtype_id);
let layout = CoordLayout::for_entity(&record.entity_type);
let ints_as_doubles = Self::integers_are_doubles(&record.entity_type);
Self::write_tokens_with_coord_grouping(buf, &record.tokens, &layout, ints_as_doubles);
buf.push(tags::END_OF_RECORD);
}
fn write_tokens_with_coord_grouping(
buf: &mut Vec<u8>,
tokens: &[SatToken],
layout: &CoordLayout,
ints_as_doubles: bool,
) {
let mut i = 0;
let mut step_index = 0;
let geom_start = tokens.iter().position(|t| Self::is_numeric(t));
while i < tokens.len() {
let in_geom = geom_start.map(|gs| i >= gs).unwrap_or(false);
if in_geom && step_index < layout.steps.len() {
match layout.steps[step_index] {
Some(tag) => {
if i + 2 < tokens.len()
&& Self::is_numeric(&tokens[i])
&& Self::is_numeric(&tokens[i + 1])
&& Self::is_numeric(&tokens[i + 2])
{
let x = Self::numeric_value(&tokens[i]);
let y = Self::numeric_value(&tokens[i + 1]);
let z = Self::numeric_value(&tokens[i + 2]);
buf.push(tag);
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
buf.extend_from_slice(&z.to_le_bytes());
i += 3;
step_index += 1;
} else {
Self::write_token(buf, &tokens[i], ints_as_doubles);
i += 1;
}
}
None => {
Self::write_token(buf, &tokens[i], ints_as_doubles);
i += 1;
step_index += 1;
}
}
} else {
Self::write_token(buf, &tokens[i], ints_as_doubles);
i += 1;
}
}
}
fn is_numeric(token: &SatToken) -> bool {
matches!(token, SatToken::Float(_) | SatToken::Integer(_) | SatToken::Position(_, _, _))
}
fn numeric_value(token: &SatToken) -> f64 {
match token {
SatToken::Float(v) => *v,
SatToken::Integer(v) => *v as f64,
_ => 0.0,
}
}
fn write_token(buf: &mut Vec<u8>, token: &SatToken, ints_as_doubles: bool) {
match token {
SatToken::Pointer(p) => Self::write_pointer(buf, p.0),
SatToken::Integer(v) => {
if ints_as_doubles {
Self::write_double(buf, *v as f64);
} else {
Self::write_integer(buf, *v as i32);
}
}
SatToken::Float(v) => Self::write_double(buf, *v),
SatToken::String(s) => {
if let Some(val) = Self::string_to_boolean(s) {
buf.push(if val { tags::TRUE } else { tags::FALSE });
} else {
Self::write_string(buf, s);
}
}
SatToken::Position(x, y, z) => Self::write_position(buf, *x, *y, *z),
SatToken::True => buf.push(tags::TRUE),
SatToken::False => buf.push(tags::FALSE),
SatToken::Terminator => buf.push(tags::END_OF_RECORD),
SatToken::Ident(s) => Self::write_ident_token(buf, s),
SatToken::Enum(s) => Self::write_enum_token(buf, s),
}
}
fn string_to_boolean(s: &str) -> Option<bool> {
match s {
"forward_v" | "I" | "forward" | "single" | "in" => Some(true),
"reverse_v" | "reversed_v" | "reversed" | "double" | "out" | "F" => Some(false),
_ => None,
}
}
fn write_ident_token(buf: &mut Vec<u8>, ident: &str) {
if let Some(val) = Self::string_to_boolean(ident) {
buf.push(if val { tags::TRUE } else { tags::FALSE });
} else {
Self::write_string(buf, ident);
}
}
fn write_enum_token(buf: &mut Vec<u8>, name: &str) {
match name {
"forward" | "single" | "in" => buf.push(tags::TRUE),
"reversed" | "double" | "out" => buf.push(tags::FALSE),
_ => Self::write_string(buf, name),
}
}
fn write_entity_type(buf: &mut Vec<u8>, name: &str) {
buf.push(tags::ENTITY_TYPE);
buf.push(name.len() as u8);
buf.extend_from_slice(name.as_bytes());
}
fn integers_are_doubles(entity_type: &str) -> bool {
let base = if let Some(pos) = entity_type.rfind('-') {
&entity_type[pos + 1..]
} else {
entity_type
};
matches!(base, "surface" | "curve" | "edge" | "pcurve" | "point")
}
fn write_subtype(buf: &mut Vec<u8>, name: &str) {
buf.push(tags::SUBTYPE);
buf.push(name.len() as u8);
buf.extend_from_slice(name.as_bytes());
}
fn write_pointer(buf: &mut Vec<u8>, value: i32) {
buf.push(tags::POINTER);
buf.extend_from_slice(&value.to_le_bytes());
}
fn write_integer(buf: &mut Vec<u8>, value: i32) {
buf.push(tags::INTEGER);
buf.extend_from_slice(&value.to_le_bytes());
}
fn write_double(buf: &mut Vec<u8>, value: f64) {
buf.push(tags::DOUBLE);
buf.extend_from_slice(&value.to_le_bytes());
}
fn write_string(buf: &mut Vec<u8>, s: &str) {
buf.push(tags::STRING);
buf.push(s.len() as u8);
buf.extend_from_slice(s.as_bytes());
}
fn write_position(buf: &mut Vec<u8>, x: f64, y: f64, z: f64) {
buf.push(tags::POSITION);
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
buf.extend_from_slice(&z.to_le_bytes());
}
#[allow(dead_code)]
fn write_direction(buf: &mut Vec<u8>, x: f64, y: f64, z: f64) {
buf.push(tags::DIRECTION);
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
buf.extend_from_slice(&z.to_le_bytes());
}
}
struct CoordLayout {
steps: &'static [Option<u8>],
}
impl CoordLayout {
const EMPTY: Self = Self { steps: &[] };
const POS: Self = Self {
steps: &[Some(tags::POSITION)],
};
const POS_DIR: Self = Self {
steps: &[Some(tags::POSITION), Some(tags::DIRECTION)],
};
const POS_DIR_DIR: Self = Self {
steps: &[Some(tags::POSITION), Some(tags::DIRECTION), Some(tags::DIRECTION)],
};
#[allow(dead_code)]
const POS_DIR_POS: Self = Self {
steps: &[Some(tags::POSITION), Some(tags::DIRECTION), Some(tags::POSITION)],
};
const POS_S_DIR_DIR: Self = Self {
steps: &[
Some(tags::POSITION),
None, Some(tags::DIRECTION),
Some(tags::DIRECTION),
],
};
const POS_DIR_SS_DIR: Self = Self {
steps: &[
Some(tags::POSITION),
Some(tags::DIRECTION),
None, None, Some(tags::DIRECTION),
],
};
fn for_entity(entity_type: &str) -> Self {
match entity_type {
"point" => Self::POS,
"straight-curve" => Self::POS_DIR,
"plane-surface" => Self::POS_DIR_DIR,
"cone-surface" => Self::POS_DIR_DIR,
"sphere-surface" => Self::POS_S_DIR_DIR,
"torus-surface" => Self::POS_DIR_SS_DIR,
"ellipse-curve" => Self::POS_DIR_DIR,
"intcurve-curve" | "spline-surface" => Self::POS_DIR_DIR,
_ => Self::EMPTY,
}
}
}
#[derive(Debug)]
pub enum SabError {
UnexpectedEof,
InvalidMagic,
UnknownTag(u8, usize),
InvalidString,
}
impl std::fmt::Display for SabError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SabError::UnexpectedEof => write!(f, "Unexpected end of SAB data"),
SabError::InvalidMagic => write!(f, "Invalid SAB magic header"),
SabError::UnknownTag(tag, pos) => {
write!(f, "Unknown SAB tag 0x{:02X} at position {}", tag, pos)
}
SabError::InvalidString => write!(f, "Invalid UTF-8 string in SAB data"),
}
}
}
impl std::error::Error for SabError {}
pub struct SabReader;
impl SabReader {
pub fn read(data: &[u8]) -> Result<SatDocument, SabError> {
if data.len() < SAB_MAGIC.len() {
return Err(SabError::InvalidMagic);
}
if &data[..SAB_MAGIC.len()] != SAB_MAGIC {
return Err(SabError::InvalidMagic);
}
let mut pos = SAB_MAGIC.len();
let version_num = read_u32(data, &mut pos)?;
let num_records = read_u32(data, &mut pos)? as usize;
let num_bodies = read_u32(data, &mut pos)? as usize;
let has_history = read_u32(data, &mut pos)? != 0;
let version = SatVersion::from_sat_number(version_num);
let product_id = read_tagged_string(data, &mut pos)?;
let product_version = read_tagged_string(data, &mut pos)?;
let date = read_tagged_string(data, &mut pos)?;
let spatial_resolution = read_tagged_double(data, &mut pos)?;
let normal_tolerance = read_tagged_double(data, &mut pos)?;
let resfit_tolerance = if version.major >= 7 {
Some(read_tagged_double(data, &mut pos)?)
} else {
None
};
let header = SatHeader {
version,
num_records,
num_bodies,
has_history,
product_id,
product_version,
date,
spatial_resolution,
normal_tolerance,
resfit_tolerance,
};
let mut records = Vec::new();
let mut record_index: i32 = 0;
while pos < data.len() {
let tag = data[pos];
if tag == tags::ENTITY_TYPE || tag == tags::SUBTYPE {
let (record, new_pos) = Self::read_record(data, pos, record_index)?;
if record.entity_type == "End-of-ACIS-data" {
break;
}
pos = new_pos;
records.push(record);
record_index += 1;
} else {
return Err(SabError::UnknownTag(tag, pos));
}
}
Ok(SatDocument {
header,
records,
})
}
fn read_record(
data: &[u8],
mut pos: usize,
index: i32,
) -> Result<(SatRecord, usize), SabError> {
let mut subtype_parts: Vec<String> = Vec::new();
let entity_type;
while pos < data.len() && data[pos] == tags::SUBTYPE {
pos += 1;
let (sub, new_pos) = read_length_string(data, pos)?;
subtype_parts.push(sub);
pos = new_pos;
}
if pos >= data.len() || data[pos] != tags::ENTITY_TYPE {
return Err(SabError::UnknownTag(
if pos < data.len() { data[pos] } else { 0 },
pos,
));
}
pos += 1;
let (base_type, new_pos) = read_length_string(data, pos)?;
pos = new_pos;
if subtype_parts.is_empty() {
entity_type = base_type;
} else {
subtype_parts.push(base_type);
entity_type = subtype_parts.join("-");
}
let subtype_name = if entity_type.contains('-') {
Some(entity_type.split('-').next().unwrap().to_string())
} else {
None
};
if entity_type == "End-of-ACIS-data" {
return Ok((
SatRecord {
index,
entity_type,
sub_type: subtype_name,
attribute: SatPointer::NULL,
subtype_id: -1,
tokens: Vec::new(),
raw_text: None,
},
pos,
));
}
let attribute = if pos < data.len() && data[pos] == tags::POINTER {
pos += 1;
let val = read_i32(data, &mut pos)?;
SatPointer::new(val)
} else {
SatPointer::NULL
};
let subtype_id = if pos < data.len() && data[pos] == tags::INTEGER {
pos += 1;
read_i32(data, &mut pos)?
} else {
-1
};
let mut tokens = Vec::new();
while pos < data.len() {
let tag = data[pos];
if tag == tags::END_OF_RECORD {
pos += 1;
break;
}
let (token, new_pos) = Self::read_token(data, pos)?;
tokens.push(token);
pos = new_pos;
}
Ok((
SatRecord {
index,
entity_type,
sub_type: subtype_name,
attribute,
subtype_id,
tokens,
raw_text: None,
},
pos,
))
}
fn read_token(data: &[u8], pos: usize) -> Result<(SatToken, usize), SabError> {
if pos >= data.len() {
return Err(SabError::UnexpectedEof);
}
let tag = data[pos];
let mut pos = pos + 1;
match tag {
tags::POINTER => {
let val = read_i32(data, &mut pos)?;
Ok((SatToken::Pointer(SatPointer::new(val)), pos))
}
tags::INTEGER => {
let val = read_i32(data, &mut pos)?;
Ok((SatToken::Integer(val as i64), pos))
}
tags::DOUBLE => {
let val = read_f64(data, &mut pos)?;
Ok((SatToken::Float(val), pos))
}
tags::STRING => {
let (s, new_pos) = read_length_string(data, pos)?;
Ok((SatToken::String(s), new_pos))
}
tags::TRUE => Ok((SatToken::True, pos)),
tags::FALSE => Ok((SatToken::False, pos)),
tags::POSITION => {
let x = read_f64(data, &mut pos)?;
let y = read_f64(data, &mut pos)?;
let z = read_f64(data, &mut pos)?;
Ok((SatToken::Position(x, y, z), pos))
}
tags::DIRECTION => {
let x = read_f64(data, &mut pos)?;
let y = read_f64(data, &mut pos)?;
let z = read_f64(data, &mut pos)?;
Ok((SatToken::Position(x, y, z), pos))
}
tags::END_OF_RECORD => Ok((SatToken::Terminator, pos)),
_ => Err(SabError::UnknownTag(tag, pos - 1)),
}
}
}
fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, SabError> {
if *pos + 4 > data.len() {
return Err(SabError::UnexpectedEof);
}
let val = u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos += 4;
Ok(val)
}
fn read_i32(data: &[u8], pos: &mut usize) -> Result<i32, SabError> {
if *pos + 4 > data.len() {
return Err(SabError::UnexpectedEof);
}
let val = i32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos += 4;
Ok(val)
}
fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, SabError> {
if *pos + 8 > data.len() {
return Err(SabError::UnexpectedEof);
}
let val = f64::from_le_bytes([
data[*pos],
data[*pos + 1],
data[*pos + 2],
data[*pos + 3],
data[*pos + 4],
data[*pos + 5],
data[*pos + 6],
data[*pos + 7],
]);
*pos += 8;
Ok(val)
}
fn read_length_string(data: &[u8], pos: usize) -> Result<(String, usize), SabError> {
if pos >= data.len() {
return Err(SabError::UnexpectedEof);
}
let len = data[pos] as usize;
let start = pos + 1;
if start + len > data.len() {
return Err(SabError::UnexpectedEof);
}
let s = std::str::from_utf8(&data[start..start + len])
.map_err(|_| SabError::InvalidString)?
.to_string();
Ok((s, start + len))
}
fn read_tagged_string(data: &[u8], pos: &mut usize) -> Result<String, SabError> {
if *pos >= data.len() || data[*pos] != tags::STRING {
return Err(SabError::UnknownTag(
if *pos < data.len() { data[*pos] } else { 0 },
*pos,
));
}
*pos += 1;
let (s, new_pos) = read_length_string(data, *pos)?;
*pos = new_pos;
Ok(s)
}
fn read_tagged_double(data: &[u8], pos: &mut usize) -> Result<f64, SabError> {
if *pos >= data.len() || data[*pos] != tags::DOUBLE {
return Err(SabError::UnknownTag(
if *pos < data.len() { data[*pos] } else { 0 },
*pos,
));
}
*pos += 1;
read_f64(data, pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sat_to_sab_header() {
let mut doc = SatDocument::new();
doc.header.product_id = "TestApp".to_string();
doc.header.product_version = "ACIS 7.0".to_string();
doc.header.date = "Thu Jan 01 00:00:00 2023".to_string();
doc.header.spatial_resolution = 10.0;
doc.header.normal_tolerance = 1e-06;
doc.header.resfit_tolerance = Some(1e-10);
let sab = SabWriter::write(&doc);
assert_eq!(&sab[..15], b"ACIS BinaryFile");
let ver = u32::from_le_bytes([sab[15], sab[16], sab[17], sab[18]]);
assert_eq!(ver, 700);
let end_str = b"End-of-ACIS-data";
assert!(sab.windows(end_str.len()).any(|w| w == end_str));
}
#[test]
fn test_sat_to_sab_roundtrip() {
let sat_text = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
10 9.9999999999999995e-007 1e-010\n\
body $-1 -1 $-1 $1 $-1 $-1 #\n\
lump $-1 -1 $-1 $-1 $2 $0 #\n\
shell $-1 -1 $-1 $-1 $-1 $3 $-1 $1 #\n\
face $-1 -1 $-1 $-1 $-1 $2 $-1 $4 forward single #\n\
plane-surface $-1 -1 $-1 0 0 5 0 0 1 1 0 0 forward_v I I I I #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat_text).unwrap();
let sab = SabWriter::write(&doc);
let roundtrip = SabReader::read(&sab).unwrap();
assert_eq!(roundtrip.header.version, doc.header.version);
assert_eq!(roundtrip.records.len(), doc.records.len());
assert_eq!(roundtrip.records[0].entity_type, "body");
assert_eq!(roundtrip.records[3].entity_type, "face");
assert_eq!(roundtrip.records[4].entity_type, "plane-surface");
}
#[test]
fn test_compound_entity_types() {
let sat_text = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
10 9.9999999999999995e-007 1e-010\n\
plane-surface $-1 -1 $-1 0 0 5 0 0 1 1 0 0 forward_v I I I I #\n\
straight-curve $-1 -1 $-1 0 0 0 1 0 0 I I #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat_text).unwrap();
let sab = SabWriter::write(&doc);
assert!(sab.contains(&tags::SUBTYPE));
let roundtrip = SabReader::read(&sab).unwrap();
assert_eq!(roundtrip.records[0].entity_type, "plane-surface");
assert_eq!(roundtrip.records[1].entity_type, "straight-curve");
}
#[test]
fn test_sab_boolean_mapping() {
let sat_text = "700 0 1 0\n\
@8 acadrust @8 ACIS 7.0 @24 Thu Jan 01 00:00:00 2023\n\
10 9.9999999999999995e-007 1e-010\n\
face $-1 -1 $-1 $-1 $-1 $-1 $-1 $-1 forward single #\n\
face $-1 -1 $-1 $-1 $-1 $-1 $-1 $-1 reversed double #\n\
End-of-ACIS-data\n";
let doc = SatDocument::parse(sat_text).unwrap();
let sab = SabWriter::write(&doc);
let roundtrip = SabReader::read(&sab).unwrap();
let face1 = &roundtrip.records[0];
let last_two: Vec<_> = face1.tokens.iter().rev().take(2).collect();
assert!(matches!(last_two[0], SatToken::True));
assert!(matches!(last_two[1], SatToken::True));
let face2 = &roundtrip.records[1];
let last_two: Vec<_> = face2.tokens.iter().rev().take(2).collect();
assert!(matches!(last_two[0], SatToken::False));
assert!(matches!(last_two[1], SatToken::False));
}
}