// SPDX-License-Identifier: Apache-2.0
///|
/// Allocation/work budgets, not a process RSS or wall-clock guarantee.
/// All fields must be nonnegative; component/XML depth is capped at 128.
/// Defaults cover the checked-in corpus and the 1m-triangle STL benchmark.
pub(all) struct ParseLimits {
max_input_bytes : Int32
max_vertices : Int32
max_triangles : Int32
max_list_items : Int32
max_polygon_vertices : Int32
max_uncompressed_bytes : Int32
max_zip_entries : Int32
max_component_depth : Int32
max_component_instances : Int32
max_records : Int32
max_line_bytes : Int32
max_xml_depth : Int32
}
///|
/// Default budgets. Override only the fields needed by the caller; increasing
/// a budget does not relax format validation or bound process RSS.
///
/// ```moonbit nocheck
/// let limits = { ..@stlkit.ParseLimits::default(), max_triangles: 100000, }
///
/// let result = @stlkit.parse_mesh(data, limits~)
/// ```
pub fn ParseLimits::default() -> ParseLimits {
{
max_input_bytes: 67108864,
max_vertices: 6000000,
max_triangles: 2000000,
max_list_items: 8000000,
max_polygon_vertices: 1024,
max_uncompressed_bytes: 104857600,
max_zip_entries: 4096,
max_component_depth: 32,
max_component_instances: 100000,
max_records: 4000000,
max_line_bytes: 1048576,
max_xml_depth: 128,
}
}
///|
fn resource_error(field : String, maximum : Int32) -> String {
"resource_limit: \{field} (max=\{maximum})"
}
///|
fn parse_limits_error(limits : ParseLimits) -> String? {
for
value in [
limits.max_input_bytes,
limits.max_vertices,
limits.max_triangles,
limits.max_list_items,
limits.max_polygon_vertices,
limits.max_uncompressed_bytes,
limits.max_zip_entries,
limits.max_component_depth,
limits.max_component_instances,
limits.max_records,
limits.max_line_bytes,
limits.max_xml_depth,
] {
if value < 0 {
return Some("invalid_limits: ParseLimits fields must be nonnegative")
}
}
if limits.max_component_depth > 128 || limits.max_xml_depth > 128 {
return Some("invalid_limits: depth limits must be <=128")
}
if limits.max_triangles > 715827882 {
return Some("invalid_limits: max_triangles must fit three Int32 corners")
}
None
}
///|
fn input_limit_error(data : Bytes, limits : ParseLimits) -> String? {
if parse_limits_error(limits) is Some(error) {
return Some(error)
}
if data.length() > limits.max_input_bytes {
return Some(resource_error("max_input_bytes", limits.max_input_bytes))
}
None
}
///|
// Scan before splitting into lines/tokens, including empty-line floods.
// Count UTF-8 bytes of String input without creating a second encoded buffer.
fn text_limit_error(text : String, limits : ParseLimits) -> String? {
if parse_limits_error(limits) is Some(error) {
return Some(error)
}
let mut bytes = 0L
let mut line_bytes = 0L
let mut lines = 0
let mut tokens = 0
let mut in_token = false
for c in text {
let code = c.to_int()
let width = if code < 128 {
1L
} else if code < 2048 {
2L
} else if code < 65536 {
3L
} else {
4L
}
bytes += width
if bytes > limits.max_input_bytes.to_int64() {
return Some(resource_error("max_input_bytes", limits.max_input_bytes))
}
if c == '\n' || c == '\r' {
line_bytes = 0L
lines += 1
if lines > limits.max_records {
return Some(resource_error("max_records", limits.max_records))
}
} else {
line_bytes += width
if line_bytes > limits.max_line_bytes.to_int64() {
return Some(resource_error("max_line_bytes", limits.max_line_bytes))
}
}
if is_ascii_space(code) {
in_token = false
} else if !in_token {
in_token = true
tokens += 1
if tokens > limits.max_records {
return Some(resource_error("max_records", limits.max_records))
}
}
}
None
}
///|
fn triangle_limit_error(count : Int32, limits : ParseLimits) -> String? {
if count > limits.max_triangles {
Some(resource_error("max_triangles", limits.max_triangles))
} else if count.to_int64() * 3L > limits.max_vertices.to_int64() {
Some(resource_error("max_vertices", limits.max_vertices))
} else {
None
}
}