use super::primitives::{
SymbolicCircle, SymbolicData, SymbolicFillArea, SymbolicGridAxis, SymbolicPolyline,
SymbolicText,
};
use serde::{Deserialize, Serialize};
pub const MAX_SYMBOLIC_ELEMENTS: usize = 2_000_000;
pub const MAX_SYMBOLIC_BYTES: usize = 256 * 1024 * 1024;
const PRIMITIVE_OVERHEAD_BYTES: usize = 64;
const BYTES_PER_COORD: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SymbolicTruncationReason {
ElementCount,
OutputBytes,
ItemDepth,
ItemRevisits,
ItemCycle,
}
impl SymbolicTruncationReason {
pub fn as_wire_str(self) -> &'static str {
match self {
Self::ElementCount => "element-count",
Self::OutputBytes => "output-bytes",
Self::ItemDepth => "item-depth",
Self::ItemRevisits => "item-revisits",
Self::ItemCycle => "item-cycle",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolicTruncation {
pub reason: SymbolicTruncationReason,
pub emitted: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
pub(super) struct SymbolicAccumulator {
data: SymbolicData,
limit: usize,
#[cfg(test)]
refusals: usize,
bytes: usize,
byte_limit: usize,
reason: Option<SymbolicTruncationReason>,
exhausted: bool,
}
impl SymbolicAccumulator {
pub(super) fn new() -> Self {
Self {
data: SymbolicData::default(),
limit: MAX_SYMBOLIC_ELEMENTS,
bytes: 0,
byte_limit: MAX_SYMBOLIC_BYTES,
reason: None,
exhausted: false,
#[cfg(test)]
refusals: 0,
}
}
#[cfg(test)]
pub(super) fn with_limits(limit: usize, byte_limit: usize) -> Self {
Self { limit, byte_limit, ..Self::new() }
}
#[cfg(test)]
pub(super) fn with_limit(limit: usize) -> Self {
Self { limit, ..Self::new() }
}
pub(super) fn is_exhausted(&self) -> bool {
self.exhausted
}
pub(super) fn note_item_bound(&mut self, reason: SymbolicTruncationReason) {
self.record(reason);
}
fn record(&mut self, reason: SymbolicTruncationReason) {
let severity = |r: SymbolicTruncationReason| match r {
SymbolicTruncationReason::ElementCount | SymbolicTruncationReason::OutputBytes => 1,
SymbolicTruncationReason::ItemDepth
| SymbolicTruncationReason::ItemRevisits
| SymbolicTruncationReason::ItemCycle => 0,
};
match self.reason {
Some(existing) if severity(existing) >= severity(reason) => {}
_ => self.reason = Some(reason),
}
}
#[cfg(test)]
pub(super) fn refusals(&self) -> usize {
self.refusals
}
fn len(&self) -> usize {
self.data.len()
}
fn exceeded_by(&self, payload: usize) -> Option<SymbolicTruncationReason> {
if self.len() >= self.limit {
return Some(SymbolicTruncationReason::ElementCount);
}
if self.bytes + PRIMITIVE_OVERHEAD_BYTES + payload * BYTES_PER_COORD > self.byte_limit {
return Some(SymbolicTruncationReason::OutputBytes);
}
None
}
fn charge(&mut self, payload: usize) {
self.bytes += PRIMITIVE_OVERHEAD_BYTES + payload * BYTES_PER_COORD;
}
fn try_push<F>(&mut self, payload: usize, push: F)
where
F: FnOnce(&mut SymbolicData),
{
if let Some(reason) = self.exceeded_by(payload) {
self.record(reason);
self.exhausted = true;
#[cfg(test)]
{
self.refusals += 1;
}
} else {
self.charge(payload);
push(&mut self.data);
}
}
pub(super) fn push_grid_axis(&mut self, axis: SymbolicGridAxis) {
let payload = axis.tag.len();
self.try_push(payload, |data| data.grid_axes.push(axis));
}
pub(super) fn push_polyline(&mut self, polyline: SymbolicPolyline) {
let payload = polyline.points.len()
+ polyline.ifc_type.len()
+ polyline.representation.len();
self.try_push(payload, |data| data.polylines.push(polyline));
}
pub(super) fn push_circle(&mut self, circle: SymbolicCircle) {
let payload = 8 + circle.ifc_type.len() + circle.representation.len();
self.try_push(payload, |data| data.circles.push(circle));
}
pub(super) fn push_text(&mut self, text: SymbolicText) {
let payload = text.content.len()
+ text.alignment.len()
+ text.ifc_type.len()
+ text.representation.len();
self.try_push(payload, |data| data.texts.push(text));
}
pub(super) fn push_fill(&mut self, fill: SymbolicFillArea) {
let payload = fill.points.len()
+ fill.holes_offsets.len()
+ fill.ifc_type.len()
+ fill.representation.len();
self.try_push(payload, |data| data.fills.push(fill));
}
pub(super) fn into_data(mut self) -> SymbolicData {
if let Some(reason) = self.reason {
let emitted = self.data.len();
let limit = match reason {
SymbolicTruncationReason::ElementCount => Some(self.limit),
SymbolicTruncationReason::OutputBytes => Some(self.byte_limit),
SymbolicTruncationReason::ItemDepth
| SymbolicTruncationReason::ItemRevisits
| SymbolicTruncationReason::ItemCycle => None,
};
self.data.truncated = Some(SymbolicTruncation { reason, emitted, limit });
}
self.data
}
}