ifc_lite_core/parser/
header.rs1#[path = "header_scan.rs"]
9mod scan;
10
11pub use scan::{find_record_open, find_section_marker, Lex};
12
13pub const MAX_HEADER_BYTES: usize = 64 * 1024;
15
16pub fn split_top_level(inner: &str) -> Vec<String> {
20 let bytes = inner.as_bytes();
21 let mut lex = Lex::new(bytes);
22 let mut args: Vec<String> = Vec::new();
23 let mut depth: i32 = 0;
24 let mut current = String::new();
25 let mut run = 0;
30 let mut i = 0;
31 while i < bytes.len() {
32 if let Some(end) = lex.skip_lexical_at(i) {
33 if bytes[i] != b'\'' {
34 current.push_str(&inner[run..i]); run = end;
36 }
37 i = end;
38 continue;
39 }
40 if bytes[i] == b',' && depth == 0 {
41 current.push_str(&inner[run..i]);
42 args.push(current.trim().to_string());
43 current.clear();
44 run = i + 1;
45 } else {
46 match bytes[i] {
47 b'(' | b'[' => depth += 1,
48 b')' | b']' => depth -= 1,
49 _ => {}
50 }
51 }
52 i += 1;
53 }
54 current.push_str(&inner[run..]);
55 if !current.trim().is_empty() || !args.is_empty() {
56 args.push(current.trim().to_string());
57 }
58 args
59}
60
61fn decode_literal(inner: &str) -> String {
69 let undoubled = if inner.contains("''") {
70 inner.replace("''", "'")
71 } else {
72 inner.to_string()
73 };
74 crate::decode_ifc_string(&undoubled).into_owned()
75}
76
77pub fn decode_opt_string(arg: &str) -> Option<String> {
80 let t = arg.trim();
81 if t.is_empty() || t == "$" || t == "*" {
82 return None;
83 }
84 if t.len() >= 2 && t.starts_with('\'') && t.ends_with('\'') {
85 return Some(decode_literal(&t[1..t.len() - 1]));
86 }
87 Some(t.to_string())
88}
89
90pub fn decode_string_list(arg: &str) -> Vec<String> {
94 decode_opt_string_list(arg).unwrap_or_default()
95}
96
97pub fn decode_opt_string_list(arg: &str) -> Option<Vec<String>> {
101 let t = arg.trim();
102 if t.is_empty() || t == "$" || t == "*" {
103 return None;
104 }
105 if !(t.starts_with('(') && t.ends_with(')')) {
106 return Some(decode_opt_string(t).into_iter().collect());
107 }
108 let entries = split_top_level(&t[1..t.len() - 1]);
109 let values: Vec<String> = entries.iter().filter_map(|a| decode_opt_string(a)).collect();
110 if !entries.is_empty() && values.is_empty() {
113 return None;
114 }
115 Some(values)
116}
117
118pub fn declared_schemas(content: &[u8]) -> Vec<String> {
126 schema_values_before_section(content, content.len().min(MAX_HEADER_BYTES))
127}
128
129fn schema_values_before_section(content: &[u8], fallback: usize) -> Vec<String> {
130 let boundary = [
131 find_section_marker(content, b"ENDSEC"),
132 find_section_marker(content, b"DATA"),
133 ]
134 .into_iter()
135 .flatten()
136 .min()
137 .unwrap_or(fallback);
138 let text = String::from_utf8_lossy(&content[..boundary]);
139 extract_record_args(&text, "FILE_SCHEMA")
140 .map(|record| decode_string_list(&record))
141 .unwrap_or_default()
142}
143
144pub fn declared_schemas_bounded(content: &[u8]) -> Vec<String> {
150 let window = &content[..content.len().min(MAX_HEADER_BYTES)];
151 schema_values_before_section(window, window.len())
152}
153
154pub fn declared_schema_bounded(content: &[u8]) -> Option<String> {
156 declared_schemas_bounded(content).into_iter().next()
157}
158
159pub fn declared_schema(content: &[u8]) -> Option<String> {
165 declared_schemas(content).into_iter().next()
166}
167
168pub fn extract_record_args(text: &str, keyword: &str) -> Option<String> {
175 let bytes = text.as_bytes();
176 let mut i = find_record_open(bytes, keyword.as_bytes())?;
177 let mut lex = Lex::new(bytes);
178 let start = i;
179 let mut depth: i32 = 0;
180 while i < bytes.len() {
181 if let Some(end) = lex.skip_lexical_at(i) {
182 i = end;
183 continue;
184 }
185 match bytes[i] {
186 b'(' => depth += 1,
187 b')' => {
188 depth -= 1;
189 if depth == 0 {
190 return Some(text[start + 1..i].to_string());
191 }
192 }
193 _ => {}
194 }
195 i += 1;
196 }
197 None
198}
199
200#[cfg(test)]
201mod tests {
202 use super::{declared_schema, declared_schema_bounded, MAX_HEADER_BYTES};
203
204 #[test]
205 fn page_quote_operands_keep_header_decoys_inside_literals_7540() {
206 let source = r"'\S\' FILE_SCHEMA((fake)); DATA;' FILE_SCHEMA(('IFC4')); DATA;";
207 assert_eq!(super::find_record_open(source.as_bytes(), b"FILE_SCHEMA"),
208 Some(source.rfind("FILE_SCHEMA").unwrap() + "FILE_SCHEMA".len()));
209 assert_eq!(super::find_section_marker(source.as_bytes(), b"DATA"),
210 source.rfind("DATA"));
211 let vectors: serde_json::Value = serde_json::from_str(include_str!(
212 "../../tests/fixtures/step-page-directive.json"
213 )).unwrap();
214 for vector in vectors.as_array().unwrap() {
215 let literal = vector["literal"].as_str().unwrap();
216 let text = format!("{literal},NEXT");
217 assert_eq!(super::Lex::new(text.as_bytes()).skip_lexical_at(0), Some(literal.len()),
218 "{}", vector["name"]);
219 }
220 }
221
222 #[test]
223 fn header_schema_reader_skips_decoys_and_respects_window() {
224 let source = b"ISO-10303-21;\nHEADER;\n\
225 FILE_DESCRIPTION(('mentions FILE_SCHEMA((''IFC4X3''));'),'2;1');\n\
226 /* FILE_SCHEMA(('IFC4')); */\n\
227 file_schema /* actual */ (('IFC2X3'));\nENDSEC;\nDATA;\n\
228 #1=IFCPROJECT('g',$,$,$,$,$,$,$,$);\nENDSEC;";
229 assert_eq!(declared_schema_bounded(source).as_deref(), Some("IFC2X3"));
230
231 let mut long_header = b"ISO-10303-21;\nHEADER;\n".to_vec();
232 long_header.extend(std::iter::repeat_n(b' ', MAX_HEADER_BYTES));
233 long_header.extend_from_slice(b"FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;");
234 assert_eq!(declared_schema_bounded(&long_header), None);
235 assert_eq!(declared_schema(&long_header).as_deref(), Some("IFC4"));
236 }
237}