document_svg/document/
dif.rs1use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::error::{Error, Result};
11use crate::table::{TableAlign, TableData, convert_table_pages};
12
13const MAX_DIF_BYTES: u64 = 64 * 1024 * 1024;
14const MAX_DIF_LINES: usize = 1_000_000;
15const MAX_DIF_LINE_BYTES: usize = 1024 * 1024;
16const MAX_DIF_ROWS: usize = 50_000;
17const MAX_DIF_COLUMNS: usize = 256;
18const MAX_DIF_CELLS: usize = 1_000_000;
19const MAX_DIF_VALUE_BYTES: usize = 64 * 1024;
20
21pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
22 let Ok(text) = std::str::from_utf8(prefix) else {
23 return false;
24 };
25 let mut lines = text.lines().map(str::trim).filter(|line| !line.is_empty());
26 matches!(lines.next(), Some("TABLE")) && lines.take(16).any(|line| line == "DATA")
27}
28
29pub(crate) fn convert(
30 path: &Path,
31 options: &ConvertOptions,
32 sink: &mut dyn PageConsumer,
33) -> Result<Vec<String>> {
34 let bytes = read_limited_file(
35 path,
36 options.max_input_bytes.min(MAX_DIF_BYTES),
37 "DIF input",
38 )?;
39 let text = String::from_utf8(bytes)
40 .map_err(|error| Error::InvalidInput(format!("DIF input must be UTF-8/ASCII: {error}")))?;
41 let (mut table, warnings) = parse_dif(&text)?;
42 let mut page_sink = DifPageSink {
43 inner: sink,
44 warnings: &warnings,
45 };
46 convert_table_pages(&mut table, "dif", options, &mut page_sink)?;
47 Ok(warnings)
48}
49
50struct DifPageSink<'a> {
51 inner: &'a mut dyn PageConsumer,
52 warnings: &'a [String],
53}
54
55impl PageConsumer for DifPageSink<'_> {
56 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
57 page.source_format = "dif".into();
58 page.title = "DIF spreadsheet".into();
59 page.description = "DIF data interchange table with inert cached values".into();
60 for warning in self.warnings {
61 page.warn(warning.clone());
62 }
63 self.inner.consume(page)
64 }
65}
66
67fn parse_dif(text: &str) -> Result<(TableData, Vec<String>)> {
68 if text.len() as u64 > MAX_DIF_BYTES {
69 return Err(Error::LimitExceeded(format!(
70 "DIF input exceeds {MAX_DIF_BYTES} bytes"
71 )));
72 }
73 let raw_lines = text.lines().collect::<Vec<_>>();
74 if raw_lines.len() > MAX_DIF_LINES {
75 return Err(Error::LimitExceeded(format!(
76 "DIF input exceeds {MAX_DIF_LINES} lines"
77 )));
78 }
79 if !raw_lines
80 .iter()
81 .all(|line| line.len() <= MAX_DIF_LINE_BYTES)
82 {
83 return Err(Error::LimitExceeded(format!(
84 "DIF line exceeds {MAX_DIF_LINE_BYTES} bytes"
85 )));
86 }
87 let lines = raw_lines
88 .iter()
89 .map(|line| line.trim_end_matches('\r'))
90 .collect::<Vec<_>>();
91 let mut index = 0usize;
92 let mut saw_table = false;
93 let mut saw_vectors = false;
94 let mut saw_tuples = false;
95 let mut vector_count = None;
96 let mut tuple_count = None;
97 let mut warnings = Vec::new();
98 let mut unknown_header_seen = false;
99 while index < lines.len() {
100 let keyword = lines[index].trim();
101 if keyword.eq_ignore_ascii_case("DATA") {
102 index += 1;
103 break;
104 }
105 if !keyword
106 .chars()
107 .all(|character| character.is_ascii_uppercase() || character == '_')
108 {
109 return Err(Error::InvalidInput(format!(
110 "DIF header keyword {keyword:?} is invalid"
111 )));
112 }
113 index += 1;
114 let (pair, string) = read_chunk(&lines, &mut index, "header")?;
115 match keyword {
116 "TABLE" => saw_table = true,
117 "VECTORS" => {
118 saw_vectors = true;
119 vector_count = Some(validate_dimension(pair.1, "vectors")?);
120 }
121 "TUPLES" => {
122 saw_tuples = true;
123 tuple_count = Some(validate_dimension(pair.1, "tuples")?);
124 }
125 _ => unknown_header_seen = true,
126 }
127 let _ = string;
128 }
129 if !saw_table || !saw_vectors || !saw_tuples || index == 0 {
130 return Err(Error::InvalidInput(
131 "DIF header must contain TABLE, VECTORS, TUPLES, and DATA".into(),
132 ));
133 }
134 let _ = read_chunk(&lines, &mut index, "DATA preamble")?;
135 let mut rows = Vec::<Vec<String>>::new();
136 let mut current = None::<Vec<String>>;
137 let mut saw_eod = false;
138 let mut cells = 0usize;
139 let mut numeric_columns = std::collections::HashSet::new();
140 let mut unknown_directive_seen = false;
141 while index < lines.len() {
142 let (pair, string) = read_chunk(&lines, &mut index, "data value")?;
143 match pair.0 {
144 -1 => match string.as_str() {
145 "BOT" => {
146 if let Some(row) = current.take() {
147 rows.push(row);
148 }
149 if rows.len() >= MAX_DIF_ROWS {
150 return Err(Error::LimitExceeded(format!(
151 "DIF exceeds {MAX_DIF_ROWS} rows"
152 )));
153 }
154 current = Some(Vec::new());
155 }
156 "EOD" => {
157 if let Some(row) = current.take() {
158 rows.push(row);
159 }
160 saw_eod = true;
161 break;
162 }
163 _other => unknown_directive_seen = true,
164 },
165 0 => {
166 let rendered = match string.as_str() {
167 "NA" | "ERROR" => string,
168 "TRUE" => "true".into(),
169 "FALSE" => "false".into(),
170 _ => pair.1.to_string(),
171 };
172 append_cell(
173 &mut current,
174 rendered,
175 &mut cells,
176 &mut numeric_columns,
177 true,
178 )?;
179 }
180 1 => append_cell(
181 &mut current,
182 string,
183 &mut cells,
184 &mut numeric_columns,
185 false,
186 )?,
187 other => {
188 return Err(Error::Unsupported(format!(
189 "DIF value type {other} is unsupported"
190 )));
191 }
192 }
193 }
194 if !saw_eod {
195 warnings.push("DIF EOD marker was missing; input ended after data".into());
196 }
197 if unknown_header_seen {
198 warnings.push("DIF nonstandard header records were ignored".into());
199 }
200 if unknown_directive_seen {
201 warnings.push("DIF nonstandard data directives were ignored".into());
202 }
203 if let Some(count) = tuple_count
204 && rows.len() > count
205 {
206 warnings.push("DIF data rows exceeded the advisory TUPLES count".into());
207 }
208 if let Some(count) = vector_count
209 && rows.iter().any(|row| row.len() > count)
210 {
211 warnings.push("DIF data columns exceeded the advisory VECTORS count".into());
212 }
213 if rows.is_empty() {
214 return Err(Error::InvalidInput("DIF contains no data tuples".into()));
215 }
216 let width = rows.iter().map(Vec::len).max().unwrap_or(0);
217 let headers = rows.remove(0);
218 let alignments = (0..width)
219 .map(|column| {
220 if numeric_columns.contains(&column) {
221 TableAlign::Right
222 } else {
223 TableAlign::Left
224 }
225 })
226 .collect();
227 Ok((
228 TableData {
229 headers,
230 rows,
231 alignments,
232 raw_source: String::new(),
233 },
234 dedup_warnings(warnings),
235 ))
236}
237
238fn append_cell(
239 current: &mut Option<Vec<String>>,
240 value: String,
241 cells: &mut usize,
242 numeric_columns: &mut std::collections::HashSet<usize>,
243 numeric: bool,
244) -> Result<()> {
245 if value.len() > MAX_DIF_VALUE_BYTES {
246 return Err(Error::LimitExceeded(format!(
247 "DIF cell value exceeds {MAX_DIF_VALUE_BYTES} bytes"
248 )));
249 }
250 let row = current
251 .as_mut()
252 .ok_or_else(|| Error::InvalidInput("DIF data value appears before BOT".into()))?;
253 if row.len() >= MAX_DIF_COLUMNS {
254 return Err(Error::LimitExceeded(format!(
255 "DIF exceeds {MAX_DIF_COLUMNS} columns"
256 )));
257 }
258 *cells = cells
259 .checked_add(1)
260 .ok_or_else(|| Error::LimitExceeded("DIF cell count overflowed".into()))?;
261 if *cells > MAX_DIF_CELLS {
262 return Err(Error::LimitExceeded(format!(
263 "DIF exceeds {MAX_DIF_CELLS} cells"
264 )));
265 }
266 if numeric {
267 numeric_columns.insert(row.len());
268 }
269 row.push(value);
270 Ok(())
271}
272
273fn read_chunk(lines: &[&str], index: &mut usize, context: &str) -> Result<((i32, f64), String)> {
274 while *index < lines.len() && lines[*index].trim().is_empty() {
275 *index += 1;
276 }
277 let pair_line = *lines
278 .get(*index)
279 .ok_or_else(|| Error::InvalidInput(format!("DIF {context} pair is missing")))?;
280 *index += 1;
281 let pair = pair_line.split_once(',').ok_or_else(|| {
282 Error::InvalidInput(format!("DIF {context} pair {pair_line:?} is invalid"))
283 })?;
284 let kind = pair
285 .0
286 .trim()
287 .parse::<i32>()
288 .map_err(|_| Error::InvalidInput(format!("DIF {context} type is invalid")))?;
289 let number = pair
290 .1
291 .trim()
292 .parse::<f64>()
293 .map_err(|_| Error::InvalidInput(format!("DIF {context} numeric value is invalid")))?;
294 if !number.is_finite() {
295 return Err(Error::InvalidInput(format!(
296 "DIF {context} value is non-finite"
297 )));
298 }
299 let string_line = *lines
300 .get(*index)
301 .ok_or_else(|| Error::InvalidInput(format!("DIF {context} string is missing")))?;
302 *index += 1;
303 Ok(((kind, number), decode_string(string_line, context)?))
304}
305
306fn decode_string(line: &str, context: &str) -> Result<String> {
307 let line = line.trim();
308 let value = if line.starts_with('"') {
309 if !line.ends_with('"') || line.len() < 2 {
310 return Err(Error::InvalidInput(format!(
311 "DIF {context} string quote is unterminated"
312 )));
313 }
314 line[1..line.len() - 1].replace("\"\"", "\"")
315 } else {
316 line.to_owned()
317 };
318 if value.len() > MAX_DIF_VALUE_BYTES {
319 return Err(Error::LimitExceeded(format!(
320 "DIF {context} string exceeds {MAX_DIF_VALUE_BYTES} bytes"
321 )));
322 }
323 Ok(value)
324}
325
326fn validate_dimension(value: f64, context: &str) -> Result<usize> {
327 if value < 0.0 || value.fract() != 0.0 {
328 return Err(Error::InvalidInput(format!(
329 "DIF {context} count is invalid"
330 )));
331 }
332 let count = value as usize;
333 if context == "vectors" && count > MAX_DIF_COLUMNS
334 || context == "tuples" && count > MAX_DIF_ROWS
335 {
336 return Err(Error::LimitExceeded(format!(
337 "DIF {context} count exceeds configured limit"
338 )));
339 }
340 Ok(count)
341}
342
343fn dedup_warnings(warnings: Vec<String>) -> Vec<String> {
344 let mut seen = std::collections::HashSet::new();
345 warnings
346 .into_iter()
347 .filter(|warning| seen.insert(warning.clone()))
348 .collect()
349}