document_svg/document/
fds.rs1use 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}