use serde::Deserialize;
use super::container;
use super::meta::ControlRef;
use super::{GeneratorRef, Limits, Meta, Metadata, PortableError, RendererRef};
#[derive(Debug, Deserialize)]
struct Header {
schema: u32,
generator: GeneratorRef,
renderer: RendererRef,
#[serde(default)]
meta: Option<Meta>,
#[serde(default)]
timeline: Option<serde::de::IgnoredAny>,
#[serde(default)]
controls: Vec<ControlRef>,
#[serde(default)]
figure: FigureHead,
}
#[derive(Debug, Default, Deserialize)]
struct FigureHead {
#[serde(default)]
suptitle_size: Option<serde::de::IgnoredAny>,
#[serde(default)]
axes: Vec<AxesHead>,
}
#[derive(Debug, Deserialize)]
struct AxesHead {
#[serde(default)]
title_style: Option<serde::de::IgnoredAny>,
#[serde(default)]
text_size: Option<serde::de::IgnoredAny>,
xaxis: AxisHead,
yaxis: AxisHead,
}
#[derive(Debug, Deserialize)]
struct AxisHead {
#[serde(default)]
minor: Option<serde::de::IgnoredAny>,
}
impl FigureHead {
fn uses_axis_styling(&self) -> bool {
self.axes.iter().any(|axes| {
axes.title_style.is_some() || axes.xaxis.minor.is_some() || axes.yaxis.minor.is_some()
})
}
fn uses_text_sizes(&self) -> bool {
self.suptitle_size.is_some() || self.axes.iter().any(|axes| axes.text_size.is_some())
}
}
pub fn inspect(bytes: &[u8], limits: &Limits) -> Result<Metadata, PortableError> {
let dir = container::directory(bytes, limits)?;
let json = container::chunk(bytes, &dir, container::TAG_JSON).expect("directory checked JSON");
if json.len() > limits.max_json_bytes {
return Err(PortableError::Budget(format!(
"JSON chunk is {} bytes, over the {} byte limit",
json.len(),
limits.max_json_bytes
)));
}
let header: Header = serde_json::from_slice(json)?;
if let Some(meta) = &header.meta {
if let Some(poster) = &meta.poster {
if poster.bytes > limits.max_poster_bytes {
return Err(PortableError::Budget(format!(
"poster is {} bytes, over the {} byte limit",
poster.bytes, limits.max_poster_bytes
)));
}
let actual = container::chunk(bytes, &dir, container::TAG_PSTR).map(<[u8]>::len);
if actual != Some(poster.bytes) {
return Err(PortableError::Malformed(format!(
"spec declares a {}-byte poster but the PSTR chunk holds {}",
poster.bytes,
actual.map_or_else(|| "nothing".to_string(), |n| n.to_string())
)));
}
}
let pixels = (meta.width_px as usize).saturating_mul(meta.height_px as usize);
if pixels > limits.max_canvas_pixels {
return Err(PortableError::Budget(format!(
"figure is {}x{} = {pixels} pixels, over the {} pixel limit",
meta.width_px, meta.height_px, limits.max_canvas_pixels
)));
}
}
let required = super::required_schema(
header.meta.is_some(),
header.timeline.is_some(),
!header.controls.is_empty(),
header.figure.uses_axis_styling(),
header.figure.uses_text_sizes(),
);
if header.schema < required {
return Err(PortableError::Malformed(format!(
"declares schema {} but carries features requiring schema {required}",
header.schema
)));
}
if header.controls.len() > limits.max_controls {
return Err(PortableError::Budget(format!(
"artifact declares {} controls, over the {} control limit",
header.controls.len(),
limits.max_controls
)));
}
if let Some(long) = header
.controls
.iter()
.find(|c| c.label.len() > limits.max_control_label_bytes)
{
return Err(PortableError::Budget(format!(
"a control label is {} bytes, over the {} byte limit",
long.label.len(),
limits.max_control_label_bytes
)));
}
for (i, c) in header.controls.iter().enumerate() {
if !(c.min.is_finite() && c.max.is_finite() && c.default.is_finite())
|| c.min >= c.max
|| !(c.min..=c.max).contains(&c.default)
{
return Err(PortableError::Malformed(format!(
"control {i} range [{}, {}] with default {} is not finite, \
increasing, and containing",
c.min, c.max, c.default
)));
}
if let Some(step) = c.step.filter(|step| !(step.is_finite() && *step > 0.0)) {
return Err(PortableError::Malformed(format!(
"control {i} step {step} is not finite and positive"
)));
}
}
Ok(Metadata {
schema: header.schema,
schema_supported: (super::SCHEMA_MIN..=super::SCHEMA_VERSION).contains(&header.schema),
generator: header.generator,
renderer: header.renderer,
meta: header.meta,
controls: header.controls,
chunks: dir,
total_bytes: bytes.len(),
})
}