1use std::collections::{HashMap, HashSet};
4use std::path::Path;
5
6use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
7use crate::document::html::{HtmlBlock, render_blocks_to_pages};
8use crate::error::{Error, Result};
9use crate::geospatial::xml_tree::{XmlElement, XmlLimits, parse_xml_tree};
10use crate::table::{TableAlign, TableData};
11
12const REQIF_NS: &str = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd";
13const REQIF_ROOT: &str = "REQ-IF";
14
15const MAX_REQIF_BYTES: u64 = 64 * 1024 * 1024;
16const MAX_REQIF_EVENTS: usize = 1_000_000;
17const MAX_REQIF_NODES: usize = 500_000;
18const MAX_REQIF_DEPTH: usize = 96;
19const MAX_REQIF_TEXT_BYTES: usize = 64 * 1024 * 1024;
20const MAX_REQIF_OBJECTS: usize = 200_000;
21const MAX_REQIF_RELATIONS: usize = 200_000;
22const MAX_REQIF_ATTRIBUTES: usize = 128;
23const MAX_REQIF_VALUE_BYTES: usize = 2 * 1024 * 1024;
24const MAX_REQIF_RENDERED_BYTES: usize = 64 * 1024 * 1024;
25
26#[derive(Clone, Debug)]
27struct Requirement {
28 id: String,
29 name: String,
30 description: String,
31 type_name: String,
32 attributes: Vec<(String, String)>,
33}
34
35#[derive(Clone, Debug)]
36struct Relation {
37 id: String,
38 source: String,
39 target: String,
40 type_name: String,
41}
42
43struct ReqifPageSink<'a> {
44 inner: &'a mut dyn PageConsumer,
45 warnings: &'a [String],
46}
47
48impl PageConsumer for ReqifPageSink<'_> {
49 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
50 page.source_format = "reqif".into();
51 if page.title.is_empty() {
52 page.title = "ReqIF requirements".into();
53 }
54 for warning in self.warnings {
55 page.warn(warning.clone());
56 }
57 self.inner.consume(page)
58 }
59}
60
61pub(crate) fn convert(
62 path: &Path,
63 options: &ConvertOptions,
64 sink: &mut dyn PageConsumer,
65) -> Result<Vec<String>> {
66 let bytes = read_limited_file(
67 path,
68 options.max_input_bytes.min(MAX_REQIF_BYTES),
69 "ReqIF input",
70 )?;
71 let root = parse_reqif(&bytes)?;
72 let (blocks, warnings) = render_reqif(&root)?;
73 if options.max_pages == 0 {
74 return Err(Error::LimitExceeded(
75 "ReqIF conversion requires at least one page; max_pages is zero".into(),
76 ));
77 }
78 let mut page_sink = ReqifPageSink {
79 inner: sink,
80 warnings: &warnings,
81 };
82 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
83 Ok(warnings)
84}
85
86pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
87 let mut reader = quick_xml::NsReader::from_reader(std::io::Cursor::new(bytes));
88 let mut buffer = Vec::new();
89 loop {
90 match reader.read_resolved_event_into(&mut buffer) {
91 Ok((namespace, quick_xml::events::Event::Start(element)))
92 | Ok((namespace, quick_xml::events::Event::Empty(element))) => {
93 return element.name().as_ref() == REQIF_ROOT.as_bytes()
94 && matches!(
95 namespace,
96 quick_xml::name::ResolveResult::Bound(value)
97 if value.as_ref() == REQIF_NS.as_bytes()
98 );
99 }
100 Ok((_, quick_xml::events::Event::Eof)) | Err(_) => return false,
101 _ => buffer.clear(),
102 }
103 buffer.clear();
104 }
105}
106
107fn parse_reqif(bytes: &[u8]) -> Result<XmlElement> {
108 let root = parse_xml_tree(
109 bytes,
110 &XmlLimits {
111 max_events: MAX_REQIF_EVENTS,
112 max_nodes: MAX_REQIF_NODES,
113 max_depth: MAX_REQIF_DEPTH,
114 max_text_bytes: MAX_REQIF_TEXT_BYTES,
115 },
116 "ReqIF",
117 )?;
118 if root.name != REQIF_ROOT || root.namespace.as_deref() != Some(REQIF_NS) {
119 return Err(Error::Unsupported(
120 "ReqIF input must have the OMG ReqIF 1.0.1/1.2 namespace".into(),
121 ));
122 }
123 Ok(root)
124}
125
126fn render_reqif(root: &XmlElement) -> Result<(Vec<HtmlBlock>, Vec<String>)> {
127 let mut elements = Vec::new();
128 collect_elements(root, &mut elements);
129 let model_ns = root.namespace.as_deref();
130
131 let title = find_descendant(root, "TITLE")
132 .map(element_value)
133 .filter(|value| !value.is_empty())
134 .unwrap_or_else(|| "ReqIF requirements".into());
135 let version = find_descendant(root, "REQ-IF-VERSION")
136 .map(element_value)
137 .filter(|value| !value.is_empty());
138 let mut blocks = vec![HtmlBlock::Heading {
139 level: 1,
140 text: title.clone(),
141 }];
142 let mut rendered_bytes = title.len();
143 if let Some(version) = version {
144 push_text(
145 &mut blocks,
146 &mut rendered_bytes,
147 format!("ReqIF exchange version: {version}"),
148 )?;
149 }
150
151 let mut type_names = HashMap::<String, String>::new();
152 let mut attribute_names = HashMap::<String, String>::new();
153 let mut requirements = HashMap::<String, Requirement>::new();
154 let mut relations = Vec::new();
155 let mut warnings = Vec::new();
156 for element in &elements {
157 if element.namespace.as_deref() != model_ns {
158 continue;
159 }
160 if let Some(id) = attr(element, "IDENTIFIER")
161 && let Some(name) = attr_any(element, &["LONG-NAME", "NAME"])
162 {
163 if matches!(
164 element.name.as_str(),
165 "SPEC-OBJECT-TYPE" | "SPECIFICATION-TYPE" | "SPEC-RELATION-TYPE"
166 ) {
167 type_names.insert(id.to_owned(), name.to_owned());
168 } else if element.name.starts_with("ATTRIBUTE-DEFINITION-") {
169 attribute_names.insert(id.to_owned(), name.to_owned());
170 }
171 }
172 }
173 for element in &elements {
174 if element.namespace.as_deref() != model_ns || element.name != "SPEC-OBJECT" {
175 continue;
176 }
177 let Some(id) = attr(element, "IDENTIFIER") else {
178 warnings.push("ReqIF SPEC-OBJECT without IDENTIFIER was omitted".into());
179 continue;
180 };
181 if requirements.len() >= MAX_REQIF_OBJECTS {
182 return Err(Error::LimitExceeded(format!(
183 "ReqIF input exceeds {MAX_REQIF_OBJECTS} specification objects"
184 )));
185 }
186 let type_id = find_descendant(element, "SPEC-OBJECT-TYPE-REF")
187 .map(element_value)
188 .unwrap_or_default();
189 let type_name = type_names
190 .get(&type_id)
191 .cloned()
192 .unwrap_or_else(|| type_id.clone());
193 let mut attributes = Vec::new();
194 if let Some(values) = find_child(element, "VALUES") {
195 for value in &values.children {
196 if !value.name.starts_with("ATTRIBUTE-VALUE-") {
197 continue;
198 }
199 let definition_id = find_descendant(value, "ATTRIBUTE-DEFINITION-STRING-REF")
200 .or_else(|| find_descendant(value, "ATTRIBUTE-DEFINITION-ENUMERATION-REF"))
201 .or_else(|| find_descendant(value, "ATTRIBUTE-DEFINITION-INTEGER-REF"))
202 .or_else(|| find_descendant(value, "ATTRIBUTE-DEFINITION-REAL-REF"))
203 .or_else(|| find_descendant(value, "ATTRIBUTE-DEFINITION-DATE-REF"))
204 .map(element_value)
205 .unwrap_or_default();
206 let name = attribute_names
207 .get(&definition_id)
208 .cloned()
209 .unwrap_or_else(|| definition_id.clone());
210 let value_text = attribute_value(value);
211 if value_text.len() > MAX_REQIF_VALUE_BYTES {
212 return Err(Error::LimitExceeded(format!(
213 "ReqIF attribute value exceeds {MAX_REQIF_VALUE_BYTES} bytes"
214 )));
215 }
216 if value.name == "ATTRIBUTE-VALUE-XHTML" {
217 warnings
218 .push("ReqIF XHTML attribute values are flattened to inert text".into());
219 }
220 if attributes.len() < MAX_REQIF_ATTRIBUTES {
221 attributes.push((name, value_text));
222 } else {
223 warnings.push(format!(
224 "ReqIF object {id:?} has more than {MAX_REQIF_ATTRIBUTES} attributes; extras were omitted"
225 ));
226 break;
227 }
228 }
229 }
230 requirements.insert(
231 id.to_owned(),
232 Requirement {
233 id: id.to_owned(),
234 name: attr_any(element, &["LONG-NAME", "NAME"])
235 .unwrap_or(id)
236 .to_owned(),
237 description: attr(element, "DESC").unwrap_or_default().to_owned(),
238 type_name,
239 attributes,
240 },
241 );
242 }
243 for element in &elements {
244 if element.namespace.as_deref() != model_ns || element.name != "SPEC-RELATION" {
245 continue;
246 }
247 if relations.len() >= MAX_REQIF_RELATIONS {
248 return Err(Error::LimitExceeded(format!(
249 "ReqIF input exceeds {MAX_REQIF_RELATIONS} specification relations"
250 )));
251 }
252 let id = attr(element, "IDENTIFIER").unwrap_or("relation").to_owned();
253 let source = find_descendant(element, "SPEC-OBJECT-REF")
254 .map(element_value)
255 .unwrap_or_default();
256 let target = element
257 .children
258 .iter()
259 .filter(|child| child.name == "TARGET")
260 .find_map(|target| find_descendant(target, "SPEC-OBJECT-REF"))
261 .map(element_value)
262 .unwrap_or_default();
263 let type_id = find_descendant(element, "SPEC-RELATION-TYPE-REF")
264 .map(element_value)
265 .unwrap_or_default();
266 let type_name = type_names.get(&type_id).cloned().unwrap_or(type_id);
267 relations.push(Relation {
268 id,
269 source,
270 target,
271 type_name,
272 });
273 }
274
275 let mut ordered_ids = Vec::new();
276 let mut seen = HashSet::new();
277 if let Some(specifications) = find_descendant(root, "SPECIFICATIONS") {
278 for specification in specifications
279 .children
280 .iter()
281 .filter(|child| child.name == "SPECIFICATION")
282 {
283 let name = attr_any(specification, &["LONG-NAME", "NAME"])
284 .unwrap_or("Specification")
285 .to_owned();
286 blocks.push(HtmlBlock::Heading {
287 level: 2,
288 text: name,
289 });
290 if let Some(children) = find_child(specification, "CHILDREN") {
291 collect_hierarchy(
292 children,
293 0,
294 &requirements,
295 &mut ordered_ids,
296 &mut seen,
297 &mut warnings,
298 );
299 }
300 }
301 }
302 for id in requirements.keys() {
303 if seen.insert(id.clone()) {
304 ordered_ids.push((0, id.clone()));
305 }
306 }
307 let mut rows = Vec::with_capacity(ordered_ids.len());
308 let mut details = Vec::with_capacity(ordered_ids.len());
309 for (level, id) in ordered_ids {
310 let Some(requirement) = requirements.get(&id) else {
311 warnings.push(format!(
312 "ReqIF hierarchy references missing SPEC-OBJECT {id:?}"
313 ));
314 continue;
315 };
316 let attribute_summary = requirement
317 .attributes
318 .iter()
319 .map(|(name, value)| format!("{name}: {value}"))
320 .collect::<Vec<_>>()
321 .join("; ");
322 charge_rendered(
323 &mut rendered_bytes,
324 requirement.id.len()
325 + requirement.name.len()
326 + requirement.type_name.len()
327 + requirement.description.len()
328 + attribute_summary.len(),
329 )?;
330 rows.push(vec![
331 level.to_string(),
332 requirement.id.clone(),
333 requirement.name.clone(),
334 requirement.type_name.clone(),
335 ]);
336 details.push((
337 requirement.id.clone(),
338 requirement.name.clone(),
339 requirement.description.clone(),
340 attribute_summary,
341 ));
342 }
343 if !rows.is_empty() {
344 blocks.push(HtmlBlock::Table(TableData {
345 headers: vec![
346 "Lv".into(),
347 "Identifier".into(),
348 "Requirement".into(),
349 "Type".into(),
350 ],
351 rows,
352 alignments: vec![TableAlign::Left; 4],
353 raw_source: String::new(),
354 }));
355 blocks.push(HtmlBlock::Heading {
356 level: 2,
357 text: "Requirement details".into(),
358 });
359 for (id, name, description, attributes) in details {
360 if !description.is_empty() {
361 push_text(
362 &mut blocks,
363 &mut rendered_bytes,
364 format!("{id} — {name}: {description}"),
365 )?;
366 } else if !name.is_empty() {
367 push_text(&mut blocks, &mut rendered_bytes, format!("{id} — {name}"))?;
368 }
369 if !attributes.is_empty() {
370 push_text(
371 &mut blocks,
372 &mut rendered_bytes,
373 format!("{id} attributes: {attributes}"),
374 )?;
375 }
376 }
377 }
378 if !relations.is_empty() {
379 blocks.push(HtmlBlock::Heading {
380 level: 2,
381 text: "Relations".into(),
382 });
383 blocks.push(HtmlBlock::Table(TableData {
384 headers: vec![
385 "Identifier".into(),
386 "Type".into(),
387 "Source".into(),
388 "Target".into(),
389 ],
390 rows: relations
391 .iter()
392 .map(|relation| {
393 vec![
394 relation.id.clone(),
395 relation.type_name.clone(),
396 relation.source.clone(),
397 relation.target.clone(),
398 ]
399 })
400 .collect(),
401 alignments: vec![TableAlign::Left; 4],
402 raw_source: String::new(),
403 }));
404 }
405 if requirements.is_empty() && relations.is_empty() {
406 return Err(Error::Unsupported(
407 "ReqIF document contains no SPEC-OBJECT or SPEC-RELATION content".into(),
408 ));
409 }
410 warnings.sort();
411 warnings.dedup();
412 Ok((blocks, warnings))
413}
414
415fn collect_hierarchy(
416 element: &XmlElement,
417 level: usize,
418 requirements: &HashMap<String, Requirement>,
419 ordered_ids: &mut Vec<(usize, String)>,
420 seen: &mut HashSet<String>,
421 warnings: &mut Vec<String>,
422) {
423 for hierarchy in element
424 .children
425 .iter()
426 .filter(|child| child.name == "SPEC-HIERARCHY")
427 {
428 let id = find_child(hierarchy, "OBJECT")
429 .and_then(|object| find_descendant(object, "SPEC-OBJECT-REF"))
430 .map(element_value)
431 .unwrap_or_default();
432 if id.is_empty() {
433 warnings.push("ReqIF SPEC-HIERARCHY without SPEC-OBJECT-REF was omitted".into());
434 } else if !requirements.contains_key(&id) {
435 warnings.push(format!(
436 "ReqIF hierarchy references missing SPEC-OBJECT {id:?}"
437 ));
438 } else if seen.insert(id.clone()) {
439 ordered_ids.push((level, id));
440 }
441 if let Some(children) = find_child(hierarchy, "CHILDREN") {
442 collect_hierarchy(
443 children,
444 level + 1,
445 requirements,
446 ordered_ids,
447 seen,
448 warnings,
449 );
450 }
451 }
452}
453
454fn collect_elements<'a>(element: &'a XmlElement, output: &mut Vec<&'a XmlElement>) {
455 output.push(element);
456 for child in &element.children {
457 collect_elements(child, output);
458 }
459}
460
461fn find_descendant<'a>(element: &'a XmlElement, name: &str) -> Option<&'a XmlElement> {
462 if element.name == name {
463 return Some(element);
464 }
465 element
466 .children
467 .iter()
468 .find_map(|child| find_descendant(child, name))
469}
470
471fn find_child<'a>(element: &'a XmlElement, name: &str) -> Option<&'a XmlElement> {
472 element.children.iter().find(|child| child.name == name)
473}
474
475fn attr<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
476 element.attributes.get(name).map(String::as_str)
477}
478
479fn attr_any<'a>(element: &'a XmlElement, names: &[&str]) -> Option<&'a str> {
480 names.iter().find_map(|name| attr(element, name))
481}
482
483fn element_value(element: &XmlElement) -> String {
484 if let Some(value) = attr_any(element, &["THE-VALUE", "VALUE", "LONG-NAME", "DESC"]) {
485 return value.to_owned();
486 }
487 let mut value = element.text.trim().to_owned();
488 for child in &element.children {
489 let child_value = element_value(child);
490 if !child_value.is_empty() {
491 if !value.is_empty() {
492 value.push(' ');
493 }
494 value.push_str(&child_value);
495 }
496 }
497 value
498}
499
500fn attribute_value(element: &XmlElement) -> String {
501 if let Some(value) = attr(element, "THE-VALUE") {
502 return value.to_owned();
503 }
504 if let Some(value) = find_descendant(element, "THE-VALUE") {
505 return element_value(value);
506 }
507 if element.name == "ATTRIBUTE-VALUE-ENUMERATION" {
508 let values = element
509 .children
510 .iter()
511 .filter(|child| child.name == "VALUES")
512 .flat_map(|values| values.children.iter())
513 .filter(|child| child.name == "ENUM-VALUE-REF")
514 .map(element_value)
515 .collect::<Vec<_>>();
516 return values.join(", ");
517 }
518 element_value(element)
519}
520
521fn push_text(blocks: &mut Vec<HtmlBlock>, rendered_bytes: &mut usize, text: String) -> Result<()> {
522 charge_rendered(rendered_bytes, text.len())?;
523 blocks.push(HtmlBlock::Paragraph { text });
524 Ok(())
525}
526
527fn charge_rendered(rendered_bytes: &mut usize, additional: usize) -> Result<()> {
528 *rendered_bytes = rendered_bytes.saturating_add(additional);
529 if *rendered_bytes > MAX_REQIF_RENDERED_BYTES {
530 return Err(Error::LimitExceeded(format!(
531 "ReqIF rendered text exceeds {MAX_REQIF_RENDERED_BYTES} bytes"
532 )));
533 }
534 Ok(())
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 const SAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
542<REQ-IF xmlns="http://www.omg.org/spec/ReqIF/20110401/reqif.xsd">
543 <THE-HEADER><REQ-IF-HEADER><TITLE>Vehicle requirements</TITLE><REQ-IF-VERSION>1.0</REQ-IF-VERSION></REQ-IF-HEADER></THE-HEADER>
544 <CORE-CONTENT><REQ-IF-CONTENT>
545 <SPEC-TYPES><SPEC-OBJECT-TYPE IDENTIFIER="REQ-TYPE" LONG-NAME="System Requirement"><SPEC-ATTRIBUTES><ATTRIBUTE-DEFINITION-STRING IDENTIFIER="REQ-TEXT" LONG-NAME="Text"/></SPEC-ATTRIBUTES></SPEC-OBJECT-TYPE><SPEC-RELATION-TYPE IDENTIFIER="DERIVE" LONG-NAME="Derives"/></SPEC-TYPES>
546 <SPEC-OBJECTS><SPEC-OBJECT IDENTIFIER="R1" LONG-NAME="Brake response" DESC="The vehicle shall stop within the target distance."><TYPE><SPEC-OBJECT-TYPE-REF>REQ-TYPE</SPEC-OBJECT-TYPE-REF></TYPE><VALUES><ATTRIBUTE-VALUE-STRING THE-VALUE="Stop within 40 m"><DEFINITION><ATTRIBUTE-DEFINITION-STRING-REF>REQ-TEXT</ATTRIBUTE-DEFINITION-STRING-REF></DEFINITION></ATTRIBUTE-VALUE-STRING></VALUES></SPEC-OBJECT><SPEC-OBJECT IDENTIFIER="R2" LONG-NAME="Brake warning"><TYPE><SPEC-OBJECT-TYPE-REF>REQ-TYPE</SPEC-OBJECT-TYPE-REF></TYPE><VALUES><ATTRIBUTE-VALUE-STRING THE-VALUE="Warn before pad wear limit"><DEFINITION><ATTRIBUTE-DEFINITION-STRING-REF>REQ-TEXT</ATTRIBUTE-DEFINITION-STRING-REF></DEFINITION></ATTRIBUTE-VALUE-STRING></VALUES></SPEC-OBJECT></SPEC-OBJECTS>
547 <SPEC-RELATIONS><SPEC-RELATION IDENTIFIER="REL1"><SOURCE><SPEC-OBJECT-REF>R1</SPEC-OBJECT-REF></SOURCE><TARGET><SPEC-OBJECT-REF>R2</SPEC-OBJECT-REF></TARGET><TYPE><SPEC-RELATION-TYPE-REF>DERIVE</SPEC-RELATION-TYPE-REF></TYPE></SPEC-RELATION></SPEC-RELATIONS>
548 <SPECIFICATIONS><SPECIFICATION IDENTIFIER="SPEC1" LONG-NAME="Braking"><CHILDREN><SPEC-HIERARCHY IDENTIFIER="H1"><OBJECT><SPEC-OBJECT-REF>R1</SPEC-OBJECT-REF></OBJECT><CHILDREN><SPEC-HIERARCHY IDENTIFIER="H2"><OBJECT><SPEC-OBJECT-REF>R2</SPEC-OBJECT-REF></OBJECT></SPEC-HIERARCHY></CHILDREN></SPEC-HIERARCHY></CHILDREN></SPECIFICATION></SPECIFICATIONS>
549 </REQ-IF-CONTENT></CORE-CONTENT>
550</REQ-IF>"#;
551
552 #[test]
553 fn renders_requirements_hierarchy_values_and_relations() {
554 let root = parse_reqif(SAMPLE.as_bytes()).unwrap();
555 let (blocks, warnings) = render_reqif(&root).unwrap();
556 let tables = blocks
557 .iter()
558 .filter_map(|block| match block {
559 HtmlBlock::Table(table) => Some(table),
560 _ => None,
561 })
562 .collect::<Vec<_>>();
563 assert_eq!(tables.len(), 2);
564 assert_eq!(tables[0].rows.len(), 2);
565 assert_eq!(tables[0].rows[0][2], "Brake response");
566 assert_eq!(tables[0].rows[1][0], "1");
567 assert!(blocks.iter().any(|block| matches!(
568 block,
569 HtmlBlock::Paragraph { text } if text.contains("The vehicle shall stop within the target distance")
570 )));
571 assert_eq!(tables[1].rows[0][1], "Derives");
572 assert_eq!(tables[1].rows[0][2], "R1");
573 assert!(warnings.is_empty());
574 }
575
576 #[test]
577 fn rejects_wrong_namespace_and_doctype() {
578 assert!(!looks_like_prefix(b"<REQ-IF/>"));
579 assert!(parse_reqif(b"<REQ-IF xmlns=\"urn:wrong\"/>").is_err());
580 let source = format!(
581 "<!DOCTYPE REQ-IF SYSTEM \"https://example.invalid/reqif.dtd\"><REQ-IF xmlns=\"{REQIF_NS}\"/>"
582 );
583 assert!(parse_reqif(source.as_bytes()).is_err());
584 }
585}