Skip to main content

document_svg/document/
reqif.rs

1//! Bounded, inert previews for OMG Requirements Interchange Format (ReqIF).
2
3use 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}