use serde::{Deserialize, Serialize};
use super::{
ImageDimensions, MediaKind, MediaPolicy, base64_encoded_len, detect_image_dimensions,
detect_media_kind, policy::media_policy_reason, raw_budget_from_base64_limit,
};
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaPreflight {
pub detected_kind: MediaKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub declared_media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub corrected_media_type: Option<String>,
pub raw_bytes: usize,
pub base64_bytes: usize,
pub media_type_corrected: bool,
pub animated_capable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dimensions: Option<ImageDimensions>,
pub corrupt: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub corruption_reason: Option<String>,
pub over_base64_budget: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget_raw_bytes: Option<usize>,
pub allowed_by_policy: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_reason: Option<String>,
}
impl MediaPreflight {
#[must_use]
pub fn inspect(bytes: &[u8], declared_media_type: Option<&str>) -> Self {
Self::inspect_with_policy(bytes, declared_media_type, &MediaPolicy::default())
}
#[must_use]
pub fn inspect_with_policy(
bytes: &[u8],
declared_media_type: Option<&str>,
policy: &MediaPolicy,
) -> Self {
let detected_kind = detect_media_kind(bytes);
let detected_media_type = detected_kind.media_type();
let corrected_media_type = detected_media_type
.or(declared_media_type)
.map(ToOwned::to_owned);
let media_type_corrected = match (declared_media_type, detected_media_type) {
(Some(declared), Some(detected)) => declared != detected,
_ => false,
};
let dimensions = detect_image_dimensions(bytes, detected_kind);
let corruption_reason = corruption_reason(bytes, detected_kind, dimensions);
let base64_bytes = base64_encoded_len(bytes.len());
let budget_raw_bytes = policy
.max_inline_base64_bytes
.map(raw_budget_from_base64_limit);
let over_base64_budget = policy
.max_inline_base64_bytes
.is_some_and(|limit| base64_bytes > limit);
let policy_reason = media_policy_reason(detected_kind, declared_media_type, policy);
Self {
detected_kind,
declared_media_type: declared_media_type.map(ToOwned::to_owned),
corrected_media_type,
raw_bytes: bytes.len(),
base64_bytes,
media_type_corrected,
animated_capable: detected_kind.can_be_animated(),
dimensions,
corrupt: corruption_reason.is_some(),
corruption_reason,
over_base64_budget,
budget_raw_bytes,
allowed_by_policy: policy_reason.is_none(),
policy_reason,
}
}
}
fn corruption_reason(
bytes: &[u8],
kind: MediaKind,
dimensions: Option<ImageDimensions>,
) -> Option<String> {
match kind {
MediaKind::Png => {
if bytes.len() < 33 || &bytes[12..16] != b"IHDR" {
Some("png payload is missing an IHDR header".to_string())
} else if dimensions.is_none() {
Some("png dimensions could not be parsed".to_string())
} else {
None
}
}
MediaKind::Jpeg => {
if bytes.len() < 4 || !bytes.ends_with(b"\xff\xd9") {
Some("jpeg payload is missing an end marker".to_string())
} else if dimensions.is_none() {
Some("jpeg dimensions could not be parsed".to_string())
} else {
None
}
}
MediaKind::Gif => {
if bytes.len() < 13 || dimensions.is_none() {
Some("gif payload is missing a logical screen descriptor".to_string())
} else {
None
}
}
MediaKind::Webp => {
if bytes.len() < 16 || dimensions.is_none() {
Some("webp payload is missing a supported image header".to_string())
} else {
None
}
}
MediaKind::Mp4 => {
if bytes.len() < 12 {
Some("mp4 payload is missing an ftyp box".to_string())
} else {
None
}
}
MediaKind::Webm => {
if bytes.len() < 4 {
Some("webm payload is missing an EBML header".to_string())
} else {
None
}
}
MediaKind::Unknown => {
if bytes.is_empty() {
Some("media payload is empty".to_string())
} else {
None
}
}
}
}