#[path = "header_scan.rs"]
mod scan;
pub use scan::{find_record_open, find_section_marker, Lex};
pub const MAX_HEADER_BYTES: usize = 64 * 1024;
pub fn split_top_level(inner: &str) -> Vec<String> {
let bytes = inner.as_bytes();
let mut lex = Lex::new(bytes);
let mut args: Vec<String> = Vec::new();
let mut depth: i32 = 0;
let mut current = String::new();
let mut run = 0;
let mut i = 0;
while i < bytes.len() {
if let Some(end) = lex.skip_lexical_at(i) {
if bytes[i] != b'\'' {
current.push_str(&inner[run..i]); run = end;
}
i = end;
continue;
}
if bytes[i] == b',' && depth == 0 {
current.push_str(&inner[run..i]);
args.push(current.trim().to_string());
current.clear();
run = i + 1;
} else {
match bytes[i] {
b'(' | b'[' => depth += 1,
b')' | b']' => depth -= 1,
_ => {}
}
}
i += 1;
}
current.push_str(&inner[run..]);
if !current.trim().is_empty() || !args.is_empty() {
args.push(current.trim().to_string());
}
args
}
fn decode_literal(inner: &str) -> String {
let undoubled = if inner.contains("''") {
inner.replace("''", "'")
} else {
inner.to_string()
};
crate::decode_ifc_string(&undoubled).into_owned()
}
pub fn decode_opt_string(arg: &str) -> Option<String> {
let t = arg.trim();
if t.is_empty() || t == "$" || t == "*" {
return None;
}
if t.len() >= 2 && t.starts_with('\'') && t.ends_with('\'') {
return Some(decode_literal(&t[1..t.len() - 1]));
}
Some(t.to_string())
}
pub fn decode_string_list(arg: &str) -> Vec<String> {
decode_opt_string_list(arg).unwrap_or_default()
}
pub fn decode_opt_string_list(arg: &str) -> Option<Vec<String>> {
let t = arg.trim();
if t.is_empty() || t == "$" || t == "*" {
return None;
}
if !(t.starts_with('(') && t.ends_with(')')) {
return Some(decode_opt_string(t).into_iter().collect());
}
let entries = split_top_level(&t[1..t.len() - 1]);
let values: Vec<String> = entries.iter().filter_map(|a| decode_opt_string(a)).collect();
if !entries.is_empty() && values.is_empty() {
return None;
}
Some(values)
}
pub fn declared_schemas(content: &[u8]) -> Vec<String> {
schema_values_before_section(content, content.len().min(MAX_HEADER_BYTES))
}
fn schema_values_before_section(content: &[u8], fallback: usize) -> Vec<String> {
let boundary = [
find_section_marker(content, b"ENDSEC"),
find_section_marker(content, b"DATA"),
]
.into_iter()
.flatten()
.min()
.unwrap_or(fallback);
let text = String::from_utf8_lossy(&content[..boundary]);
extract_record_args(&text, "FILE_SCHEMA")
.map(|record| decode_string_list(&record))
.unwrap_or_default()
}
pub fn declared_schemas_bounded(content: &[u8]) -> Vec<String> {
let window = &content[..content.len().min(MAX_HEADER_BYTES)];
schema_values_before_section(window, window.len())
}
pub fn declared_schema_bounded(content: &[u8]) -> Option<String> {
declared_schemas_bounded(content).into_iter().next()
}
pub fn declared_schema(content: &[u8]) -> Option<String> {
declared_schemas(content).into_iter().next()
}
pub fn extract_record_args(text: &str, keyword: &str) -> Option<String> {
let bytes = text.as_bytes();
let mut i = find_record_open(bytes, keyword.as_bytes())?;
let mut lex = Lex::new(bytes);
let start = i;
let mut depth: i32 = 0;
while i < bytes.len() {
if let Some(end) = lex.skip_lexical_at(i) {
i = end;
continue;
}
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(text[start + 1..i].to_string());
}
}
_ => {}
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::{declared_schema, declared_schema_bounded, MAX_HEADER_BYTES};
#[test]
fn header_schema_reader_skips_decoys_and_respects_window() {
let source = b"ISO-10303-21;\nHEADER;\n\
FILE_DESCRIPTION(('mentions FILE_SCHEMA((''IFC4X3''));'),'2;1');\n\
/* FILE_SCHEMA(('IFC4')); */\n\
file_schema /* actual */ (('IFC2X3'));\nENDSEC;\nDATA;\n\
#1=IFCPROJECT('g',$,$,$,$,$,$,$,$);\nENDSEC;";
assert_eq!(declared_schema_bounded(source).as_deref(), Some("IFC2X3"));
let mut long_header = b"ISO-10303-21;\nHEADER;\n".to_vec();
long_header.extend(std::iter::repeat_n(b' ', MAX_HEADER_BYTES));
long_header.extend_from_slice(b"FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;");
assert_eq!(declared_schema_bounded(&long_header), None);
assert_eq!(declared_schema(&long_header).as_deref(), Some("IFC4"));
}
}