///|
/// Cumulative semantic budget for one workbook read. Every decoded XML source
/// is charged before downstream parsers inspect it. Opening elements account
/// for XML-backed items; range-like constructs that expand into additional
/// in-memory entries charge those entries separately.
priv struct ReadBudget {
limits : ReadLimits
cancelled : () -> Bool
mut parser_items : Int
mut parser_work_units : Int
}
///|
fn ReadBudget::new(
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> ReadBudget {
{ limits, cancelled, parser_items: 0, parser_work_units: 0 }
}
///|
fn read_budget_actual(current : Int, amount : Int, limit : Int) -> (Int, Bool) {
if amount > limit - current {
// `ReadLimits` accepts every positive Int. Keep the reported value
// representable when the ceiling is Int::max while carrying overflow as a
// separate bit; wrapping `limit + 1` would otherwise turn the rejection
// into a negative, apparently in-budget value.
(if limit < 0x7fffffff { limit + 1 } else { limit }, true)
} else {
(current + amount, false)
}
}
///|
fn ReadBudget::charge_items(
self : ReadBudget,
amount : Int,
) -> Unit raise XlsxError {
if amount <= 0 {
return
}
let (actual, exceeded) = read_budget_actual(
self.parser_items,
amount,
self.limits.max_parser_items,
)
if exceeded {
raise ResourceLimitExceeded(
kind="parser_items",
limit=self.limits.max_parser_items,
actual~,
)
}
self.parser_items = actual
}
///|
fn ReadBudget::charge_work(
self : ReadBudget,
amount : Int,
) -> Unit raise XlsxError {
if amount <= 0 {
return
}
let (actual, exceeded) = read_budget_actual(
self.parser_work_units,
amount,
self.limits.max_parser_work_units,
)
if exceeded {
raise ResourceLimitExceeded(
kind="parser_work_units",
limit=self.limits.max_parser_work_units,
actual~,
)
}
self.parser_work_units = actual
}
///|
fn ReadBudget::checkpoint(self : ReadBudget) -> Unit raise XlsxError {
if (self.cancelled)() {
raise ReadCancelled
}
}
///|
fn ReadBudget::tag_end(
self : ReadBudget,
xml : StringView,
start : Int,
) -> Int? raise XlsxError {
let mut index = start
let mut next_checkpoint = start
let mut quote : UInt16? = None
while index < xml.length() {
if index >= next_checkpoint {
self.checkpoint()
next_checkpoint = index + 4096
}
let unit = xml[index]
match quote {
Some(delimiter) => if unit == delimiter { quote = None }
None =>
if unit == ('"' : UInt16) || unit == ('\'' : UInt16) {
quote = Some(unit)
} else if unit == ('>' : UInt16) {
return Some(index)
}
}
index = index + 1
}
None
}
///|
fn xml_sequence_at(
xml : StringView,
start : Int,
sequence : StringView,
) -> Bool {
if start < 0 || start > xml.length() - sequence.length() {
return false
}
for offset in 0.. Int? raise XlsxError {
let mut index = start
let mut next_checkpoint = start
while index <= xml.length() - sequence.length() {
if index >= next_checkpoint {
self.checkpoint()
next_checkpoint = index + 4096
}
if xml_sequence_at(xml, index, sequence) {
return Some(index + sequence.length())
}
index = index + 1
}
None
}
///|
fn xml_name_local_start(xml : StringView, start : Int, end : Int) -> Int {
let mut local_start = start
for index in start.. Bool {
let local_start = xml_name_local_start(xml, start, end)
if end - local_start != expected.length() {
return false
}
for index in 0.. Unit raise XlsxError {
guard attr_value(tag, "min") is Some(min_text) &&
attr_value(tag, "max") is Some(max_text) else {
return
}
let min_col = @string.parse_int(min_text, base=10) catch { _ => return }
let max_col = @string.parse_int(max_text, base=10) catch { _ => return }
if min_col <= 0 ||
max_col < min_col ||
min_col > cell_ref_max_cols ||
max_col > cell_ref_max_cols {
return
}
let span = max_col - min_col + 1
self.charge_items(span)
self.charge_work(span)
}
///|
/// Preflights one decoded XML source in a single quote-aware pass. This runs
/// before any feature parser materializes arrays or maps from the source.
fn ReadBudget::scan_xml(
self : ReadBudget,
xml : StringView,
) -> Unit raise XlsxError {
self.checkpoint()
self.charge_work(xml.length())
let mut index = 0
let mut next_checkpoint = 0
while index < xml.length() {
if index >= next_checkpoint {
self.checkpoint()
next_checkpoint = index + 4096
}
if xml[index] != ('<' : UInt16) || index + 1 >= xml.length() {
index = index + 1
continue
}
let first = xml[index + 1]
if first == ('/' : UInt16) {
index = index + 2
continue
}
if first == ('?' : UInt16) {
index = match self.sequence_end(xml, index + 2, "?>") {
Some(value) => value
None => xml.length()
}
continue
}
if first == ('!' : UInt16) {
index = if xml_sequence_at(xml, index, "") {
Some(value) => value
None => xml.length()
}
} else if xml_sequence_at(xml, index, "") {
Some(value) => value
None => xml.length()
}
} else {
match self.tag_end(xml, index + 2) {
Some(value) => value + 1
None => xml.length()
}
}
continue
}
let name_start = index + 1
let mut name_end = name_start
while name_end < xml.length() &&
!is_xml_attr_space_unit(xml[name_end]) &&
xml[name_end] != ('/' : UInt16) &&
xml[name_end] != ('>' : UInt16) {
name_end = name_end + 1
}
if name_end == name_start {
index = index + 1
continue
}
self.charge_items(1)
let tag_end = match self.tag_end(xml, name_end) {
Some(value) => value
None => return
}
if xml_name_local_is(xml, name_start, name_end, "col") {
self.charge_col_expansion(xml[name_end:tag_end])
}
index = tag_end + 1
}
}
///|
test "read budget counts source, elements, and derived column expansion" {
let limits = ReadLimits::with_values(
max_parser_items=5,
max_parser_work_units=1024,
)
let budget = ReadBudget::new(limits)
budget.scan_xml("")
assert_eq(budget.parser_items, 5)
}
///|
test "read budget checks cancellation inside a long start tag" {
let checks = [0]
let budget = ReadBudget::new(ReadLimits::new(), cancelled=() => {
checks[0] += 1
checks[0] >= 3
})
let xml = " "
try budget.scan_xml(xml) catch {
ReadCancelled => assert_true(checks[0] >= 3)
_ => fail("expected read cancellation")
} noraise {
_ => fail("expected read cancellation")
}
}
///|
test "read budget ignores element-like text in comments and CDATA" {
let budget = ReadBudget::new(ReadLimits::with_values(max_parser_items=2))
budget.scan_xml(
"]]>",
)
assert_eq(budget.parser_items, 2)
}
///|
test "read budget rejects overflow at maximum Int limits" {
let limits = ReadLimits::with_values(
max_parser_items=0x7fffffff,
max_parser_work_units=0x7fffffff,
)
let item_budget = ReadBudget::new(limits)
item_budget.parser_items = 0x7ffffffe
try item_budget.charge_items(2) catch {
ResourceLimitExceeded(kind~, limit~, actual~) => {
assert_eq(kind, "parser_items")
assert_eq(limit, 0x7fffffff)
assert_eq(actual, 0x7fffffff)
assert_eq(item_budget.parser_items, 0x7ffffffe)
}
_ => fail("unexpected parser item overflow error")
} noraise {
_ => fail("expected parser item overflow rejection")
}
let work_budget = ReadBudget::new(limits)
work_budget.parser_work_units = 0x7ffffffe
try work_budget.charge_work(2) catch {
ResourceLimitExceeded(kind~, limit~, actual~) => {
assert_eq(kind, "parser_work_units")
assert_eq(limit, 0x7fffffff)
assert_eq(actual, 0x7fffffff)
assert_eq(work_budget.parser_work_units, 0x7ffffffe)
}
_ => fail("unexpected parser work overflow error")
} noraise {
_ => fail("expected parser work overflow rejection")
}
}