Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/formats/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@

use crate::error::ConvertError;
use crate::model::{Block, Cell, Document, Inline, Table, TableKind};
use crate::package::materialization::MaterializationBudget;
use crate::shared::header::resolve_header_rows;
use crate::shared::text::clean_text;
use csv::ReaderBuilder;
use std::borrow::Cow;

pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
parse_with_budget(bytes, MaterializationBudget::default())
}

fn parse_with_budget(
bytes: &[u8],
mut budget: MaterializationBudget,
) -> Result<Document, ConvertError> {
budget.check_input(bytes.len())?;
let text = decode(bytes);
let delimiter = sniff_delimiter(&text);

Expand All @@ -34,8 +43,13 @@ pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
continue;
}
};
let cells: Vec<Cell> =
record.iter().map(|f| Cell::from_inlines(vec![Inline::plain(clean_text(f))])).collect();
let mut cells = Vec::new();
for field in &record {
budget.charge_cell()?;
budget.charge_text(field.len())?;
budget.charge_text_run()?;
cells.push(Cell::from_inlines(vec![Inline::plain(clean_text(field))]));
}
rows.push(cells);
}

Expand Down Expand Up @@ -150,4 +164,25 @@ mod tests {
let doc = parse(&bytes).unwrap();
assert!(!doc.blocks.is_empty());
}

fn resource_limit_name(error: ConvertError) -> &'static str {
match error {
ConvertError::ResourceLimit { limit, .. } => limit,
other => panic!("expected resource limit, got {other}"),
}
}

#[test]
fn cell_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 1024, 3, 10);
let error = parse_with_budget(b"a,b\nc,d\n", budget).unwrap_err();
assert_eq!(resource_limit_name(error), "max_materialized_cells");
}

#[test]
fn copied_text_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 5, 10, 10);
let error = parse_with_budget(b"abcdef\n", budget).unwrap_err();
assert_eq!(resource_limit_name(error), "max_materialized_text_bytes");
}
}
112 changes: 111 additions & 1 deletion src/formats/rtf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod tables;

use crate::error::ConvertError;
use crate::model::{Block, Document, Inline, Note, NoteKind, Style, inlines_are_empty};
use crate::package::materialization::MaterializationBudget;
use crate::package::xml::{Element, Node, ns};
use crate::shared::blockstyle::{BlockStyle, StyledRun};
use crate::shared::delta::rebase_emphasis;
Expand All @@ -23,14 +24,22 @@ use table::TableState;
use tables::{LIST_LEVELS, Prelude, codepage_encoding, parse_prelude};

pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
parse_with_budget(bytes, MaterializationBudget::default())
}

fn parse_with_budget(
bytes: &[u8],
budget: MaterializationBudget,
) -> Result<Document, ConvertError> {
budget.check_input(bytes.len())?;
if !bytes.starts_with(b"{\\rtf") {
return Err(ConvertError::malformed("not an RTF file"));
}
// The code page must be known before the font table decodes; scan the
// header for \ansicpg first.
let default_encoding = scan_codepage(bytes);
let prelude = parse_prelude(bytes, default_encoding);
let mut parser = Parser::new(bytes, prelude, default_encoding);
let mut parser = Parser::new(bytes, prelude, default_encoding, budget);
parser.run()?;
parser.finish()
}
Expand Down Expand Up @@ -612,6 +621,8 @@ struct Parser<'a> {
prelude: Prelude,
decoder: TextDecoder,
recovered: bool,
budget: MaterializationBudget,
materialization_error: Option<ConvertError>,

inlines: Vec<Inline>,
blocks: Vec<Block>,
Expand All @@ -628,6 +639,7 @@ impl<'a> Parser<'a> {
bytes: &'a [u8],
prelude: Prelude,
default_encoding: &'static encoding_rs::Encoding,
budget: MaterializationBudget,
) -> Self {
Parser {
lexer: Lexer::new(bytes),
Expand All @@ -636,6 +648,8 @@ impl<'a> Parser<'a> {
prelude,
decoder: TextDecoder::new(default_encoding),
recovered: false,
budget,
materialization_error: None,
inlines: Vec::new(),
blocks: Vec::new(),
list_run: Vec::new(),
Expand Down Expand Up @@ -707,6 +721,9 @@ impl<'a> Parser<'a> {
}
}
}
if let Some(error) = self.materialization_error.take() {
return Err(error);
}
}
if !self.stack.is_empty() {
self.recovered = true;
Expand All @@ -715,6 +732,9 @@ impl<'a> Parser<'a> {
log::warn!("recovered unbalanced rtf groups");
}
self.flush_pending();
if let Some(error) = self.materialization_error.take() {
return Err(error);
}
self.finish_math();
self.end_paragraph()
}
Expand Down Expand Up @@ -1049,6 +1069,14 @@ impl<'a> Parser<'a> {
};
let (lines, display) = math.finish();
for (i, tex) in lines.into_iter().enumerate() {
if let Err(error) =
self.budget.charge_text(tex.len()).and_then(|_| self.budget.charge_text_run())
{
if self.materialization_error.is_none() {
self.materialization_error = Some(error);
}
return;
}
if i > 0 {
self.inlines.push(Inline::LineBreak);
}
Expand Down Expand Up @@ -1127,6 +1155,12 @@ impl<'a> Parser<'a> {
}

fn push_text(&mut self, text: String) {
if let Err(error) = self.budget.charge_text(text.len()) {
if self.materialization_error.is_none() {
self.materialization_error = Some(error);
}
return;
}
let text = clean_text(&text);
if text.is_empty() {
return;
Expand All @@ -1152,6 +1186,12 @@ impl<'a> Parser<'a> {
}
Capture::None => {
if !self.state.suppress {
if let Err(error) = self.budget.charge_text_run() {
if self.materialization_error.is_none() {
self.materialization_error = Some(error);
}
return;
}
self.inlines.push(Inline::Text { text, style: self.state.style });
}
}
Expand All @@ -1168,6 +1208,9 @@ impl<'a> Parser<'a> {
}

fn end_paragraph(&mut self) -> Result<(), ConvertError> {
if let Some(error) = self.materialization_error.take() {
return Err(error);
}
let inlines = std::mem::take(&mut self.inlines);
let listtext = self.dest.listtext.take();
let math_display = std::mem::take(&mut self.dest.math_display);
Expand Down Expand Up @@ -1203,12 +1246,20 @@ impl<'a> Parser<'a> {
{
let text =
format!("{} ", label.clone().unwrap_or_else(|| key.marker.label(*number)));
self.budget.charge_text(text.len())?;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self.budget.charge_text_run()?;
content.insert(0, Inline::Text { text, style: Style::PLAIN });
}
self.blocks.push(Block::Heading { level, anchor: None, content });
return Ok(());
}
if let Some((key, level, number, label)) = entry {
if key.marker.ordered()
&& let Some(label) = label.as_ref()
{
self.budget.charge_text(label.len())?;
self.budget.charge_text_run()?;
}
self.list_run.push(ListEntry {
level,
key,
Expand Down Expand Up @@ -1292,6 +1343,10 @@ impl<'a> Parser<'a> {
}

fn end_cell(&mut self, depth: usize) -> Result<(), ConvertError> {
if let Some(error) = self.materialization_error.take() {
return Err(error);
}
self.budget.charge_cell()?;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
let inlines = std::mem::take(&mut self.inlines);
self.dest.listtext = None;
self.table.end_cell(depth, self.state.block, inlines)
Expand Down Expand Up @@ -1414,4 +1469,59 @@ mod tests {
assert_eq!(doc.assets.len(), 1, "only the preferred picture: {:?}", doc.assets);
assert_eq!(doc.assets[0].media_type, "image/png");
}

fn rtf_resource_limit_name(error: ConvertError) -> &'static str {
match error {
ConvertError::ResourceLimit { limit, .. } => limit,
other => panic!("expected resource limit, got {other}"),
}
}

#[test]
fn rtf_text_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 5, 10, 10);
let error = parse_with_budget(br"{\rtf1 abcdef}", budget).unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_text_bytes");
}

#[test]
fn rtf_text_run_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 1024, 10, 2);
let error = parse_with_budget(br"{\rtf1 a\b b\b0 c\par}", budget).unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_text_runs");
}

#[test]
fn rtf_math_run_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 1024, 10, 0);
let error = parse_with_budget(br"{\rtf1{\mmath{\*\moMath{\mr x}}}}", budget).unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_text_runs");
}

#[test]
fn rtf_cell_materialization_is_bounded() {
let budget = MaterializationBudget::with_limits(1024, 1024, 1, 10);
let error =
parse_with_budget(br"{\rtf1\trowd\cellx1000\cellx2000 a\cell b\cell\row}", budget)
.unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_cells");
}

#[test]
fn rtf_generated_list_labels_are_bounded() {
let budget = MaterializationBudget::with_limits(4096, 4, 10, 10);
let error = parse_with_budget(
br"{\rtf1{\*\listtable{\list{\listlevel\levelnfc0\levelstartat1{\leveltext \'03\'00.\'00;}{\levelnumbers \'01\'03;}}\listid1}}{\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}\pard\ls1\ilvl0 item\par}",
budget,
)
.unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_text_bytes");
}

#[test]
fn pending_text_limit_precedes_cell_limit() {
let budget = MaterializationBudget::with_limits(1024, 1, 0, 10);
let error = parse_with_budget(br"{\rtf1\trowd\cellx1000 ab\cell\row}", budget).unwrap_err();
assert_eq!(rtf_resource_limit_name(error), "max_materialized_text_bytes");
}
}
16 changes: 16 additions & 0 deletions src/package/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@
/// Maximum decompressed size of a single archive entry: 128 MiB.
pub const MAX_ENTRY_BYTES: u64 = 128 * 1024 * 1024;

/// Maximum size of a standalone, non-archive input: 128 MiB.
///
/// CSV and RTF do not pass through the archive layer, so this prevents
/// them from duplicating an already-unbounded input before their model
/// materialization budgets can take effect.
pub const MAX_STANDALONE_INPUT_BYTES: usize = 128 * 1024 * 1024;

/// Maximum text bytes copied into the document model: 64 MiB.
pub const MAX_MATERIALIZED_TEXT_BYTES: usize = 64 * 1024 * 1024;

/// Maximum table cells materialized by a standalone frontend.
pub const MAX_MATERIALIZED_CELLS: usize = 4_000_000;

/// Maximum separately allocated text runs in the document model.
pub const MAX_MATERIALIZED_TEXT_RUNS: usize = 2_000_000;

/// Maximum total decompressed bytes read from one archive: 512 MiB.
pub const MAX_TOTAL_BYTES: u64 = 512 * 1024 * 1024;

Expand Down
Loading