use crate::ir::ChartSpec;
pub const DEFAULT_MAX_TOTAL_DATA_POINTS: usize = 1_000_000;
pub const DEFAULT_MAX_SERIES: usize = 1_000;
pub const DEFAULT_MAX_CATEGORIES: usize = 100_000;
pub const DEFAULT_MAX_CATEGORICAL_PRIMITIVES: usize = 1_000_000;
pub const DEFAULT_MAX_LABEL_BYTES: usize = 4_096;
pub const DEFAULT_MAX_DIMENSION_PX: f64 = 32_768.0;
pub const DEFAULT_MIN_DIMENSION_PX: f64 = 1.0;
#[derive(Debug, Clone)]
pub struct InputLimits {
pub max_total_data_points: usize,
pub max_series: usize,
pub max_categories: usize,
pub max_categorical_primitives: usize,
pub max_label_bytes: usize,
pub max_dimension_px: f64,
pub min_dimension_px: f64,
}
impl Default for InputLimits {
fn default() -> Self {
Self {
max_total_data_points: DEFAULT_MAX_TOTAL_DATA_POINTS,
max_series: DEFAULT_MAX_SERIES,
max_categories: DEFAULT_MAX_CATEGORIES,
max_categorical_primitives: DEFAULT_MAX_CATEGORICAL_PRIMITIVES,
max_label_bytes: DEFAULT_MAX_LABEL_BYTES,
max_dimension_px: DEFAULT_MAX_DIMENSION_PX,
min_dimension_px: DEFAULT_MIN_DIMENSION_PX,
}
}
}
pub fn validate_spec(spec: &ChartSpec, limits: &InputLimits) -> Result<(), String> {
if !spec.width.is_finite()
|| spec.width < limits.min_dimension_px
|| spec.width > limits.max_dimension_px
{
return Err(format!(
"width {:.0} は有効範囲 [{:.0}–{:.0}] を外れています",
spec.width, limits.min_dimension_px, limits.max_dimension_px,
));
}
if !spec.height.is_finite()
|| spec.height < limits.min_dimension_px
|| spec.height > limits.max_dimension_px
{
return Err(format!(
"height {:.0} は有効範囲 [{:.0}–{:.0}] を外れています",
spec.height, limits.min_dimension_px, limits.max_dimension_px,
));
}
if spec.series.len() > limits.max_series {
return Err(format!(
"系列数 {} が上限 {} を超えています",
spec.series.len(),
limits.max_series,
));
}
if spec.categories.len() > limits.max_categories {
return Err(format!(
"カテゴリ数 {} が上限 {} を超えています",
spec.categories.len(),
limits.max_categories,
));
}
let categorical_primitives = spec.series.len().saturating_mul(spec.categories.len());
if categorical_primitives > limits.max_categorical_primitives {
return Err(format!(
"系列数 {} × カテゴリ数 {} = {} が上限 {} を超えています",
spec.series.len(),
spec.categories.len(),
categorical_primitives,
limits.max_categorical_primitives,
));
}
if matches!(spec.kind, crate::ir::ChartKind::Progress) {
const PRIMS_PER_BAR: usize = 4; let bars = spec.series.first().map(|s| s.values.len()).unwrap_or(0);
let progress_primitives = bars.saturating_mul(PRIMS_PER_BAR);
if progress_primitives > limits.max_categorical_primitives {
return Err(format!(
"progress バー数 {} (プリミティブ {}) が上限 {} を超えています",
bars, progress_primitives, limits.max_categorical_primitives,
));
}
}
if matches!(spec.kind, crate::ir::ChartKind::BoxPlot) {
const PRIMS_PER_BOX: usize = 10;
let boxes = spec
.series
.iter()
.flat_map(|s| s.box_points.iter())
.filter(|p| {
p.min.is_finite()
&& p.q1.is_finite()
&& p.median.is_finite()
&& p.q3.is_finite()
&& p.max.is_finite()
})
.count();
let boxplot_primitives = boxes.saturating_mul(PRIMS_PER_BOX);
if boxplot_primitives > limits.max_categorical_primitives {
return Err(format!(
"boxplot ボックス数 {} (プリミティブ {}) が上限 {} を超えています",
boxes, boxplot_primitives, limits.max_categorical_primitives,
));
}
}
let total_points: usize = spec
.series
.iter()
.map(|s| s.values.len().max(s.points.len()).max(s.box_points.len()))
.sum();
if total_points > limits.max_total_data_points {
return Err(format!(
"全系列のデータ点数合計 {} が上限 {} を超えています",
total_points, limits.max_total_data_points,
));
}
if let Some(title) = &spec.title {
if title.len() > limits.max_label_bytes {
return Err(format!(
"タイトルの長さ {} バイトが上限 {} を超えています",
title.len(),
limits.max_label_bytes,
));
}
}
for cat in &spec.categories {
if cat.len() > limits.max_label_bytes {
return Err(format!(
"カテゴリラベルの長さ {} バイトが上限 {} を超えています",
cat.len(),
limits.max_label_bytes,
));
}
}
for ser in &spec.series {
if ser.name.len() > limits.max_label_bytes {
return Err(format!(
"系列名の長さ {} バイトが上限 {} を超えています",
ser.name.len(),
limits.max_label_bytes,
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frontend::chartjs;
fn base_spec() -> ChartSpec {
chartjs::parse(
r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#,
false,
)
.unwrap()
}
fn default_limits() -> InputLimits {
InputLimits::default()
}
#[test]
fn valid_spec_passes() {
assert!(validate_spec(&base_spec(), &default_limits()).is_ok());
}
#[test]
fn zero_width_is_rejected() {
let mut s = base_spec();
s.width = 0.0;
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn negative_height_is_rejected() {
let mut s = base_spec();
s.height = -1.0;
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn max_dimension_is_accepted() {
let mut s = base_spec();
s.width = DEFAULT_MAX_DIMENSION_PX;
s.height = DEFAULT_MAX_DIMENSION_PX;
assert!(validate_spec(&s, &default_limits()).is_ok());
}
#[test]
fn over_max_dimension_is_rejected() {
let mut s = base_spec();
s.width = DEFAULT_MAX_DIMENSION_PX + 1.0;
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn nan_dimension_is_rejected() {
let mut s = base_spec();
s.width = f64::NAN;
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn too_many_series_is_rejected() {
use crate::ir::{Color, Series, SeriesType};
let mut s = base_spec();
let dummy = Series {
name: String::new(),
values: vec![1.0],
points: vec![],
fill: vec![Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}],
stroke: vec![],
stroke_width: 1.0,
area: false,
tension: 0.0,
series_type: SeriesType::Bar,
point_radius: None,
box_points: vec![],
};
s.series = vec![dummy; DEFAULT_MAX_SERIES + 1];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn too_many_categories_is_rejected() {
let mut s = base_spec();
s.categories = vec!["x".to_string(); DEFAULT_MAX_CATEGORIES + 1];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn too_many_categorical_primitives_is_rejected() {
use crate::ir::{Color, Series, SeriesType};
let mut s = base_spec();
let dummy = Series {
name: String::new(),
values: vec![1.0],
points: vec![],
fill: vec![Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}],
stroke: vec![],
stroke_width: 1.0,
area: false,
tension: 0.0,
series_type: SeriesType::Bar,
point_radius: None,
box_points: vec![],
};
s.series = vec![dummy; 100];
s.categories = vec!["x".to_string(); 10_001];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn categorical_primitives_at_limit_is_accepted() {
use crate::ir::{Color, Series, SeriesType};
let mut s = base_spec();
let dummy = Series {
name: String::new(),
values: vec![1.0],
points: vec![],
fill: vec![Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}],
stroke: vec![],
stroke_width: 1.0,
area: false,
tension: 0.0,
series_type: SeriesType::Bar,
point_radius: None,
box_points: vec![],
};
s.series = vec![dummy; 100];
s.categories = vec!["x".to_string(); 10_000];
assert!(validate_spec(&s, &default_limits()).is_ok());
}
#[test]
fn too_many_total_data_points_is_rejected() {
use crate::ir::{Color, Series, SeriesType};
let mut s = base_spec();
let big_series = Series {
name: String::new(),
values: vec![1.0; DEFAULT_MAX_TOTAL_DATA_POINTS + 1],
points: vec![],
fill: vec![Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}],
stroke: vec![],
stroke_width: 1.0,
area: false,
tension: 0.0,
series_type: SeriesType::Bar,
point_radius: None,
box_points: vec![],
};
s.series = vec![big_series];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn too_many_progress_bars_is_rejected() {
let mut s = chartjs::parse(
r#"{"type":"progress","data":{"datasets":[{"data":[1]}]}}"#,
false,
)
.unwrap();
s.series[0].values = vec![1.0; DEFAULT_MAX_CATEGORICAL_PRIMITIVES / 4 + 1];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn progress_bars_within_limit_is_accepted() {
let mut s = chartjs::parse(
r#"{"type":"progress","data":{"datasets":[{"data":[1]}]}}"#,
false,
)
.unwrap();
s.series[0].values = vec![1.0; DEFAULT_MAX_CATEGORICAL_PRIMITIVES / 4];
assert!(validate_spec(&s, &default_limits()).is_ok());
}
#[test]
fn long_label_is_rejected() {
let mut s = base_spec();
s.categories = vec!["x".repeat(DEFAULT_MAX_LABEL_BYTES + 1)];
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn long_series_name_is_rejected() {
let mut s = base_spec();
s.series[0].name = "x".repeat(DEFAULT_MAX_LABEL_BYTES + 1);
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn long_title_is_rejected() {
let mut s = base_spec();
s.title = Some("x".repeat(DEFAULT_MAX_LABEL_BYTES + 1));
assert!(validate_spec(&s, &default_limits()).is_err());
}
#[test]
fn custom_limits_override_defaults() {
let mut s = base_spec();
let strict = InputLimits {
max_series: 0,
..InputLimits::default()
};
assert!(validate_spec(&s, &strict).is_err());
s.width = 50_000.0;
let relaxed = InputLimits {
max_dimension_px: 100_000.0,
..InputLimits::default()
};
assert!(validate_spec(&s, &relaxed).is_ok());
}
}