Skip to main content

document_svg/document/
dif.rs

1//! Bounded DIF (Data Interchange Format) spreadsheet preview.
2//!
3//! DIF stores a small header followed by two-line values grouped into BOT
4//! (beginning-of-tuple) rows and an EOD marker.  Cached values are rendered as
5//! inert table text; no formulas, links, macros, or external resources run.
6
7use 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}