///|
/// Cheap preflight checks for untrusted GIF data before full decoding.
pub(all) struct GifPreflight {
signature : Bool
header : GifHeader?
bytes : Int
dimensions_ok : Bool
estimated_pixels : Int
frame_count : Int
extension_count : Int
complete : Bool
safe : Bool
issues : Array[String]
} derive(Debug)
///|
pub fn GifPreflight::signature(self : GifPreflight) -> Bool {
self.signature
}
///|
pub fn GifPreflight::header(self : GifPreflight) -> GifHeader? {
self.header
}
///|
pub fn GifPreflight::bytes(self : GifPreflight) -> Int {
self.bytes
}
///|
pub fn GifPreflight::dimensions_ok(self : GifPreflight) -> Bool {
self.dimensions_ok
}
///|
pub fn GifPreflight::estimated_pixels(self : GifPreflight) -> Int {
self.estimated_pixels
}
///|
pub fn GifPreflight::frame_count(self : GifPreflight) -> Int {
self.frame_count
}
///|
pub fn GifPreflight::extension_count(self : GifPreflight) -> Int {
self.extension_count
}
///|
pub fn GifPreflight::complete(self : GifPreflight) -> Bool {
self.complete
}
///|
pub fn GifPreflight::safe(self : GifPreflight) -> Bool {
self.safe
}
///|
pub fn GifPreflight::issues(self : GifPreflight) -> ArrayView[String] {
self.issues[:]
}
///|
/// Inspect basic structure before allocating decoded pixel buffers.
pub fn preflight_gif(data : Bytes, limits : GifLimits) -> GifPreflight {
let issues : Array[String] = []
let signature = has_gif_signature(data)
if !signature {
issues.push("missing GIF signature")
}
let header : GifHeader? = try Some(parse_header(data)) catch {
_ => None
} noraise {
_ => None
}
let dimensions_ok = match header {
None => false
Some(value) => limits.valid_dimensions(value.width(), value.height())
}
if signature && !dimensions_ok {
issues.push("logical screen exceeds configured limits")
}
let index : GifByteIndex? = if signature {
try Some(index_gif(data)) catch {
_ => None
} noraise {
_ => None
}
} else {
None
}
let frames = match index {
None => 0
Some(value) => value.frames().length()
}
let extensions = match index {
None => 0
Some(value) => value.extension_count()
}
let complete = match index {
None => false
Some(value) => value.is_complete()
}
if signature && !complete {
issues.push("GIF stream is not complete")
}
if frames > limits.frames() {
issues.push("frame count exceeds configured limit")
}
let pixels = match header {
None => 0
Some(value) => value.area() * frames
}
if pixels > 0 &&
pixels > limits.dimension() * limits.dimension() * limits.frames() {
issues.push("estimated decoded pixel count is too large")
}
GifPreflight::{
signature,
header,
bytes: data.length(),
dimensions_ok,
estimated_pixels: pixels,
frame_count: frames,
extension_count: extensions,
complete,
safe: issues.length() == 0,
issues,
}
}
///|
/// Decide whether full decode is allowed under resource limits.
pub fn GifPreflight::allows_decode(self : GifPreflight) -> Bool {
self.safe && self.complete && self.frame_count > 0
}
///|
/// Return a stable reason string for rejected data.
pub fn GifPreflight::reason(self : GifPreflight) -> String {
if self.safe {
"preflight passed"
} else {
let mut result = "preflight failed"
for issue in self.issues {
result = result + ": " + issue
}
result
}
}
///|
/// Decode only after a successful preflight.
pub fn decode_after_preflight(data : Bytes, limits : GifLimits) -> Animation? {
let report = preflight_gif(data, limits)
if !report.allows_decode() {
None
} else {
decode_checked(data)
}
}
///|
/// Return a conservative byte budget for decoded RGBA storage.
pub fn GifPreflight::decoded_memory_bytes(self : GifPreflight) -> Int {
self.estimated_pixels * 4
}