Skip to main content

document_svg/document/
fds.rs

1//! Bounded Fire Dynamics Simulator (FDS) input previews.
2//!
3//! FDS input uses Fortran-style namelist blocks. This adapter counts block
4//! types and lines without executing FDS, reading meshes, resolving paths or
5//! evaluating fire/flow equations.
6
7use std::collections::BTreeMap;
8use std::path::Path;
9
10use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
11use crate::document::html::{HtmlBlock, render_blocks_to_pages};
12use crate::error::{Error, Result};
13use crate::table::{TableAlign, TableData};
14
15const MAX_FDS_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_FDS_LINES: usize = 2_000_000;
17const MAX_FDS_LINE_BYTES: usize = 1024 * 1024;
18const MAX_FDS_BLOCKS: usize = 500_000;
19const MAX_FDS_ROWS: usize = 200_000;
20const MAX_FDS_DISPLAY_BYTES: usize = 512;
21
22#[derive(Default)]
23struct Summary {
24    lines: usize,
25    blocks: usize,
26    types: BTreeMap<String, usize>,
27    rows: Vec<Vec<String>>,
28}
29
30struct FdsPageSink<'a> {
31    inner: &'a mut dyn PageConsumer,
32    warnings: &'a [String],
33}
34impl PageConsumer for FdsPageSink<'_> {
35    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
36        page.source_format = "fds".into();
37        if page.title.is_empty() {
38            page.title = "FDS input deck".into();
39        }
40        page.description = "Fire Dynamics Simulator namelist structure is rendered as bounded inert metadata; no CFD or fire calculation runs".into();
41        for warning in self.warnings {
42            page.warn(warning.clone());
43        }
44        self.inner.consume(page)
45    }
46}
47
48pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
49    let text = String::from_utf8_lossy(prefix).to_ascii_uppercase();
50    text.contains("&HEAD") && text.contains("CHID")
51}
52
53pub(crate) fn convert(
54    path: &Path,
55    options: &ConvertOptions,
56    sink: &mut dyn PageConsumer,
57) -> Result<Vec<String>> {
58    let bytes = read_limited_file(
59        path,
60        options.max_input_bytes.min(MAX_FDS_BYTES),
61        "FDS input",
62    )?;
63    let text = String::from_utf8(bytes)
64        .map_err(|error| Error::InvalidInput(format!("FDS input must be UTF-8/ASCII: {error}")))?;
65    let summary = parse(&text)?;
66    let blocks = vec![HtmlBlock::Heading { level: 1, text: "FDS input deck".into() }, HtmlBlock::Paragraph { text: "Fire Dynamics Simulator namelist blocks are summarized without executing fire/flow calculations or opening referenced files.".into() }, HtmlBlock::Table(TableData { headers: vec!["Block".into(), "Count".into(), "Detail".into()], rows: summary.rows, alignments: vec![TableAlign::Left; 3], raw_source: String::new() })];
67    let warnings = vec!["FDS block names are shown but CHID, paths, coordinates, fire parameters, device values and solver settings are omitted or redacted".into(), "FDS meshes, CSV/SMV output, external files, scripts, MPI execution and CFD simulation never run".into()];
68    let mut page_sink = FdsPageSink {
69        inner: sink,
70        warnings: &warnings,
71    };
72    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
73    Ok(warnings)
74}
75
76fn parse(text: &str) -> Result<Summary> {
77    let mut summary = Summary::default();
78    for (line_no, raw) in text.lines().enumerate() {
79        if line_no >= MAX_FDS_LINES {
80            return Err(Error::LimitExceeded(format!(
81                "FDS lines exceed {MAX_FDS_LINES}"
82            )));
83        }
84        if raw.len() > MAX_FDS_LINE_BYTES {
85            return Err(Error::LimitExceeded(format!(
86                "FDS line exceeds {MAX_FDS_LINE_BYTES} bytes"
87            )));
88        }
89        summary.lines = summary.lines.saturating_add(1);
90        let line = raw.split('!').next().unwrap_or("").trim();
91        if !line.starts_with('&') {
92            continue;
93        }
94        let name = line[1..]
95            .split(|ch: char| ch.is_ascii_whitespace() || ch == ',')
96            .next()
97            .unwrap_or("")
98            .trim();
99        if name.is_empty() {
100            continue;
101        }
102        summary.blocks = summary.blocks.saturating_add(1);
103        if summary.blocks > MAX_FDS_BLOCKS {
104            return Err(Error::LimitExceeded(format!(
105                "FDS blocks exceed {MAX_FDS_BLOCKS}"
106            )));
107        }
108        *summary.types.entry(name.to_ascii_uppercase()).or_default() += 1;
109    }
110    if summary.blocks == 0 {
111        return Err(Error::InvalidInput(
112            "FDS input contains no namelist blocks".into(),
113        ));
114    }
115    push_row(
116        &mut summary.rows,
117        "Blocks",
118        &summary.blocks.to_string(),
119        &format!("lines={} types={}", summary.lines, summary.types.len()),
120    )?;
121    for (name, count) in summary.types.iter().take(MAX_FDS_ROWS.saturating_sub(1)) {
122        push_row(
123            &mut summary.rows,
124            name,
125            &count.to_string(),
126            "values omitted",
127        )?;
128    }
129    Ok(summary)
130}
131
132fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
133    if rows.len() >= MAX_FDS_ROWS {
134        return Err(Error::LimitExceeded(format!(
135            "FDS rows exceed {MAX_FDS_ROWS}"
136        )));
137    }
138    rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
139    Ok(())
140}
141fn truncate(value: &str) -> String {
142    if value.len() <= MAX_FDS_DISPLAY_BYTES {
143        return value.to_owned();
144    }
145    let mut end = MAX_FDS_DISPLAY_BYTES;
146    while end > 0 && !value.is_char_boundary(end) {
147        end -= 1;
148    }
149    format!("{}…", &value[..end])
150}