use crate::color::{Cicp, ColorMetadata, MatrixCoefficients, Primaries, TransferFunction};
use crate::error::DecodeError;
use crate::limits::ParseLimits;
use crate::metadata::{CleanAperture, ContentLightLevel, Orientation, PixelAspectRatio};
#[derive(Debug, Clone)]
pub(crate) struct HeifItem {
pub(crate) _item_id: u16,
pub(crate) data_offset: u64,
pub(crate) data_length: u64,
pub(crate) hvcc: Vec<u8>,
pub(crate) display_w: u32,
pub(crate) display_h: u32,
pub(crate) color: ColorMetadata,
pub(crate) orientation: Orientation,
pub(crate) cll: Option<ContentLightLevel>,
pub(crate) clap: Option<CleanAperture>,
pub(crate) pasp: Option<PixelAspectRatio>,
pub(crate) _is_alpha: bool,
}
#[derive(Debug)]
pub(crate) struct GridInfo {
pub(crate) rows: u32,
pub(crate) cols: u32,
pub(crate) output_width: u32,
pub(crate) output_height: u32,
pub(crate) tiles: Vec<HeifItem>,
pub(crate) orientation: Orientation,
}
pub(crate) struct HeifFile {
pub(crate) primary: HeifItem,
pub(crate) alpha: Option<HeifItem>,
pub(crate) exif: Option<Vec<u8>>,
pub(crate) grid: Option<GridInfo>,
}
#[allow(dead_code)]
fn read_u8(b: &[u8], off: usize) -> Option<u8> {
b.get(off).copied()
}
fn read_u16(b: &[u8], off: usize) -> Option<u16> {
Some(u16::from_be_bytes(b.get(off..off + 2)?.try_into().ok()?))
}
fn read_u32(b: &[u8], off: usize) -> Option<u32> {
Some(u32::from_be_bytes(b.get(off..off + 4)?.try_into().ok()?))
}
fn read_u64(b: &[u8], off: usize) -> Option<u64> {
Some(u64::from_be_bytes(b.get(off..off + 8)?.try_into().ok()?))
}
struct Boxes<'a> {
data: &'a [u8],
pos: usize,
max_box_size: u64,
}
impl<'a> Boxes<'a> {
fn with_limit(data: &'a [u8], max_box_size: u64) -> Self {
Boxes {
data,
pos: 0,
max_box_size,
}
}
}
struct BoxEntry<'a> {
fourcc: [u8; 4],
payload: &'a [u8], }
impl<'a> Iterator for Boxes<'a> {
type Item = BoxEntry<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos + 8 > self.data.len() {
return None;
}
let size = read_u32(self.data, self.pos)? as usize;
if size < 8 || self.pos + size > self.data.len() {
return None;
}
if size as u64 > self.max_box_size {
return None;
}
let fourcc: [u8; 4] = self.data[self.pos + 4..self.pos + 8].try_into().ok()?;
let payload = &self.data[self.pos + 8..self.pos + size];
self.pos += size;
Some(BoxEntry { fourcc, payload })
}
}
fn fullbox_header(payload: &[u8]) -> Option<(u8, u32, &[u8])> {
if payload.len() < 4 {
return None;
}
let version = payload[0];
let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]);
Some((version, flags, &payload[4..]))
}
pub(crate) fn parse(file: &[u8], limits: &ParseLimits) -> Result<HeifFile, DecodeError> {
let mbs = limits.max_box_size;
let mut has_heic = false;
for b in Boxes::with_limit(file, mbs) {
if &b.fourcc == b"ftyp" {
let brands: Vec<_> = b
.payload
.chunks(4)
.map(|c| c.try_into().unwrap_or([0; 4]))
.collect();
has_heic = brands
.iter()
.any(|br: &[u8; 4]| matches!(br, b"heic" | b"mif1" | b"miaf" | b"heif"));
break;
}
}
if !has_heic {
return Err(DecodeError::NotHeif);
}
let meta_payload = Boxes::with_limit(file, mbs)
.find(|b| &b.fourcc == b"meta")
.map(|b| b.payload)
.ok_or(DecodeError::MissingBox("meta"))?;
let meta = fullbox_header(meta_payload)
.map(|(_, _, rest)| rest)
.ok_or(DecodeError::MissingBox("meta body"))?;
let mut pitm: u16 = 1;
let mut iloc_data: Option<&[u8]> = None;
let mut iinf_data: Option<&[u8]> = None;
let mut iref_data: Option<&[u8]> = None;
let mut iprp_data: Option<&[u8]> = None;
let mut idat_file_offset: u64 = 0;
for b in Boxes::with_limit(meta, mbs) {
match &b.fourcc {
b"pitm" => {
if let Some((version, _, rest)) = fullbox_header(b.payload) {
pitm = if version == 1 {
read_u32(rest, 0).map(|v| v as u16).unwrap_or(pitm)
} else {
read_u16(rest, 0).unwrap_or(pitm)
};
}
}
b"iloc" => iloc_data = Some(b.payload),
b"iinf" => iinf_data = Some(b.payload),
b"iref" => iref_data = Some(b.payload),
b"iprp" => iprp_data = Some(b.payload),
b"idat" => {
let content_start = b.payload.as_ptr() as usize;
let file_start = file.as_ptr() as usize;
idat_file_offset = (content_start - file_start) as u64;
}
_ => {}
}
}
let mut extents: std::collections::HashMap<u16, (u64, u64)> = std::collections::HashMap::new();
if let Some((version, _, rest)) = iloc_data.and_then(fullbox_header) {
parse_iloc(rest, version, idat_file_offset, &mut extents, limits);
}
let mut item_types: std::collections::HashMap<u16, [u8; 4]> = std::collections::HashMap::new();
if let Some((_, _, rest)) = iinf_data.and_then(fullbox_header) {
let count = (read_u16(rest, 0).unwrap_or(0) as usize).min(limits.max_items);
let mut pos = 2usize;
for _ in 0..count {
if pos + 8 > rest.len() {
break;
}
let bsz = read_u32(rest, pos).unwrap_or(0) as usize;
if bsz < 12 || pos + bsz > rest.len() {
break;
}
let iid = read_u16(rest, pos + 8 + 2).unwrap_or(0); if bsz >= 20 {
let itype: [u8; 4] = rest[pos + 16..pos + 20].try_into().unwrap_or([0; 4]);
let iid2 = read_u16(rest, pos + 12).unwrap_or(0);
item_types.insert(iid2, itype);
}
let _ = iid;
pos += bsz;
}
}
let mut auxl_items: Vec<u16> = Vec::new();
let mut exif_item: Option<u16> = None;
let mut dimg_map: std::collections::HashMap<u16, Vec<u16>> = std::collections::HashMap::new();
if let Some((_, _, rest)) = iref_data.and_then(fullbox_header) {
let mut pos = 0;
while pos + 8 <= rest.len() {
let bsz = read_u32(rest, pos).unwrap_or(0) as usize;
if bsz < 12 || pos + bsz > rest.len() {
break;
}
let fourcc = &rest[pos + 4..pos + 8];
let from_id = read_u16(rest, pos + 8).unwrap_or(0);
if fourcc == b"auxl" {
auxl_items.push(from_id);
}
if fourcc == b"cdsc" {
exif_item = Some(from_id);
}
if fourcc == b"dimg" {
let n = read_u16(rest, pos + 10).unwrap_or(0) as usize;
let to: Vec<u16> = (0..n)
.filter_map(|i| read_u16(rest, pos + 12 + i * 2))
.collect();
dimg_map.insert(from_id, to);
}
pos += bsz;
}
}
let (props, prop_assoc) = if let Some(iprp) = iprp_data {
parse_iprp(iprp, limits)
} else {
(vec![], std::collections::HashMap::new())
};
let primary_type = item_types.get(&pitm).copied().unwrap_or(*b"hvc1");
let is_grid = &primary_type == b"grid";
let (primary, grid) = if is_grid {
let g = parse_grid_item(pitm, &extents, &dimg_map, &props, &prop_assoc, file, limits)?;
let first = g
.tiles
.first()
.cloned()
.unwrap_or_else(|| build_fallback_item(pitm));
(first, Some(g))
} else {
let p = build_item(pitm, &extents, &props, &prop_assoc, false, file, limits)?;
(p, None)
};
let alpha_item = auxl_items
.iter()
.copied()
.find(|&id| item_is_alpha(id, &props, &prop_assoc));
let alpha = if let Some(aid) = alpha_item {
build_item(aid, &extents, &props, &prop_assoc, true, file, limits).ok()
} else {
None
};
let exif = if let Some(eid) = exif_item {
if let Some(&(off, len)) = extents.get(&eid) {
let sum = off.saturating_add(len);
let start = off as usize;
let end = sum as usize;
if len.saturating_sub(4) > limits.max_exif_size as u64 {
return Err(DecodeError::LimitExceeded {
what: "exif payload",
value: len.saturating_sub(4),
limit: limits.max_exif_size as u64,
});
}
if sum <= file.len() as u64 && len > 4 {
let raw = &file[start + 4..end];
Some(raw.to_vec())
} else {
None
}
} else {
None
}
} else {
None
};
Ok(HeifFile {
primary,
alpha,
exif,
grid,
})
}
fn parse_iloc(
data: &[u8],
version: u8,
idat_file_offset: u64,
out: &mut std::collections::HashMap<u16, (u64, u64)>,
limits: &ParseLimits,
) {
if data.len() < 4 {
return;
}
let offset_size = ((data[0] >> 4) & 0xF) as usize;
let length_size = (data[0] & 0xF) as usize;
let base_offset_size = ((data[1] >> 4) & 0xF) as usize;
let index_size = if version == 1 || version == 2 {
(data[1] & 0xF) as usize
} else {
0
};
let (item_count, mut pos) = if version == 2 {
(read_u32(data, 2).unwrap_or(0) as usize, 6usize)
} else {
(read_u16(data, 2).unwrap_or(0) as usize, 4usize)
};
let item_count = item_count.min(limits.max_items);
for _ in 0..item_count {
if pos >= data.len() {
break;
}
let pos_before = pos;
let item_id = if version == 2 {
let v = read_u32(data, pos).unwrap_or(0) as u16;
pos += 4;
v
} else {
let v = read_u16(data, pos).unwrap_or(0);
pos += 2;
v
};
let cm = if version == 1 || version == 2 {
let m = read_u16(data, pos).unwrap_or(0) & 0xF;
pos += 2;
m
} else {
0
};
let _data_ref_index = read_u16(data, pos).unwrap_or(0);
pos += 2;
let base_offset = read_n(data, pos, base_offset_size);
pos += base_offset_size;
let extent_count = read_u16(data, pos).unwrap_or(0);
pos += 2;
let extent_count = (extent_count as usize).min(limits.max_extents_per_item);
for _ in 0..extent_count {
if pos >= data.len() {
break;
}
if index_size > 0 {
pos += index_size;
}
let ext_offset = read_n(data, pos, offset_size);
pos += offset_size;
let ext_length = read_n(data, pos, length_size);
pos += length_size;
let abs_offset = if cm == 1 {
idat_file_offset
.saturating_add(base_offset)
.saturating_add(ext_offset)
} else {
base_offset.saturating_add(ext_offset)
};
out.insert(item_id, (abs_offset, ext_length));
}
if pos == pos_before {
break;
}
}
}
fn read_n(data: &[u8], off: usize, n: usize) -> u64 {
match n {
1 => data.get(off).copied().unwrap_or(0) as u64,
2 => read_u16(data, off).unwrap_or(0) as u64,
4 => read_u32(data, off).unwrap_or(0) as u64,
8 => read_u64(data, off).unwrap_or(0),
_ => 0,
}
}
#[derive(Clone, Debug, Default)]
struct Prop {
kind: [u8; 4],
data: Vec<u8>,
}
fn parse_iprp(
iprp: &[u8],
limits: &ParseLimits,
) -> (Vec<Prop>, std::collections::HashMap<u16, Vec<u8>>) {
let mut props: Vec<Prop> = vec![Prop::default()]; let mut assoc: std::collections::HashMap<u16, Vec<u8>> = std::collections::HashMap::new();
let mbs = limits.max_box_size;
for b in Boxes::with_limit(iprp, mbs) {
if &b.fourcc == b"ipco" {
for pb in Boxes::with_limit(b.payload, mbs) {
if props.len() >= limits.max_items.saturating_mul(4).max(64) {
break;
}
props.push(Prop {
kind: pb.fourcc,
data: pb.payload.to_vec(),
});
}
}
if &b.fourcc == b"ipma"
&& let Some((_, _, rest)) = fullbox_header(b.payload)
{
let entry_count = read_u32(rest, 0).unwrap_or(0);
let mut pos = 4usize;
for _ in 0..entry_count {
if pos + 3 > rest.len() {
break;
}
let item_id = read_u16(rest, pos).unwrap_or(0);
pos += 2;
let assoc_count = rest[pos] as usize;
pos += 1;
let mut indices = Vec::with_capacity(assoc_count);
for _ in 0..assoc_count {
if pos >= rest.len() {
break;
}
let raw = rest[pos];
pos += 1;
let idx = raw & 0x7F;
indices.push(idx);
}
assoc.insert(item_id, indices);
}
}
}
(props, assoc)
}
fn item_is_alpha(
item_id: u16,
props: &[Prop],
prop_assoc: &std::collections::HashMap<u16, Vec<u8>>,
) -> bool {
let Some(indices) = prop_assoc.get(&item_id) else {
return false;
};
for &pidx in indices {
let pidx = pidx as usize;
if pidx == 0 || pidx >= props.len() {
continue;
}
let p = &props[pidx];
if &p.kind != b"auxC" {
continue;
}
if p.data.len() <= 4 {
continue;
}
let urn_bytes = &p.data[4..];
let end = urn_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(urn_bytes.len());
let urn = &urn_bytes[..end];
if urn == b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
|| urn == b"urn:mpeg:hevc:2015:auxid:1"
{
return true;
}
}
false
}
fn read_item_orientation(
item_id: u16,
props: &[Prop],
prop_assoc: &std::collections::HashMap<u16, Vec<u8>>,
) -> Orientation {
use crate::metadata::Orientation;
let mut orientation = Orientation::Normal;
if let Some(indices) = prop_assoc.get(&item_id) {
for &pidx in indices {
let pidx = pidx as usize;
if pidx == 0 || pidx >= props.len() {
continue;
}
let p = &props[pidx];
match &p.kind {
b"irot" => {
if let Some(&steps) = p.data.first() {
orientation = irot_to_orientation(steps & 3, orientation);
}
}
b"imir" => {
if let Some(&axis) = p.data.first() {
orientation = imir_to_orientation(axis & 1, orientation);
}
}
_ => {}
}
}
}
orientation
}
fn parse_grid_item(
grid_id: u16,
extents: &std::collections::HashMap<u16, (u64, u64)>,
dimg_map: &std::collections::HashMap<u16, Vec<u16>>,
props: &[Prop],
prop_assoc: &std::collections::HashMap<u16, Vec<u8>>,
file: &[u8],
limits: &ParseLimits,
) -> Result<GridInfo, DecodeError> {
let (rows, cols, ow, oh) = if let Some(&(off, len)) = extents.get(&grid_id) {
let start = off as usize;
let end = off.saturating_add(len) as usize;
if end <= file.len() && len >= 4 {
let b = &file[start..end];
let flags = b[1];
let rows = b[2] as u32 + 1;
let cols = b[3] as u32 + 1;
let (ow, oh) = if flags & 1 != 0 {
let w = if b.len() >= 8 {
u32::from_be_bytes(b[4..8].try_into().unwrap_or([0; 4]))
} else {
0
};
let h = if b.len() >= 12 {
u32::from_be_bytes(b[8..12].try_into().unwrap_or([0; 4]))
} else {
0
};
(w, h)
} else {
let w = if b.len() >= 6 {
u16::from_be_bytes(b[4..6].try_into().unwrap_or([0; 2])) as u32
} else {
0
};
let h = if b.len() >= 8 {
u16::from_be_bytes(b[6..8].try_into().unwrap_or([0; 2])) as u32
} else {
0
};
(w, h)
};
(rows, cols, ow, oh)
} else {
(1, 1, 0, 0)
}
} else {
(1, 1, 0, 0)
};
limits.check_image(ow, oh)?;
let mut tile_ids = dimg_map.get(&grid_id).cloned().unwrap_or_default();
if tile_ids.len() > limits.max_tiles {
return Err(DecodeError::LimitExceeded {
what: "grid tiles",
value: tile_ids.len() as u64,
limit: limits.max_tiles as u64,
});
}
let max_needed = (rows as usize).saturating_mul(cols as usize);
tile_ids.truncate(max_needed);
let tiles: Vec<HeifItem> = tile_ids
.iter()
.filter_map(|&tid| build_item(tid, extents, props, prop_assoc, false, file, limits).ok())
.collect();
let orientation = read_item_orientation(grid_id, props, prop_assoc);
Ok(GridInfo {
rows,
cols,
output_width: ow,
output_height: oh,
tiles,
orientation,
})
}
fn build_fallback_item(item_id: u16) -> HeifItem {
HeifItem {
_item_id: item_id,
data_offset: 0,
data_length: 0,
hvcc: vec![],
display_w: 0,
display_h: 0,
color: ColorMetadata::default(),
orientation: Orientation::Normal,
cll: None,
clap: None,
pasp: None,
_is_alpha: false,
}
}
fn build_item(
item_id: u16,
extents: &std::collections::HashMap<u16, (u64, u64)>,
props: &[Prop],
prop_assoc: &std::collections::HashMap<u16, Vec<u8>>,
is_alpha: bool,
_file: &[u8],
limits: &ParseLimits,
) -> Result<HeifItem, DecodeError> {
let &(offset, length) = extents
.get(&item_id)
.ok_or(DecodeError::MissingBox("iloc entry for item"))?;
if length > limits.max_item_size {
return Err(DecodeError::LimitExceeded {
what: "item data size",
value: length,
limit: limits.max_item_size,
});
}
let mut hvcc = Vec::new();
let mut display_w = 0u32;
let mut display_h = 0u32;
let mut color = ColorMetadata::default(); let mut orientation = Orientation::Normal;
let mut cll = None;
let mut clap: Option<CleanAperture> = None;
let mut pasp: Option<PixelAspectRatio> = None;
if let Some(indices) = prop_assoc.get(&item_id) {
for &pidx in indices {
let pidx = pidx as usize;
if pidx == 0 || pidx >= props.len() {
continue;
}
let p = &props[pidx];
match &p.kind {
b"hvcC" => {
if p.data.len() > limits.max_hvcc_size {
return Err(DecodeError::LimitExceeded {
what: "hvcC size",
value: p.data.len() as u64,
limit: limits.max_hvcc_size as u64,
});
}
hvcc = p.data.clone();
}
b"ispe" => {
if p.data.len() >= 12 {
display_w = read_u32(&p.data, 4).unwrap_or(0);
display_h = read_u32(&p.data, 8).unwrap_or(0);
limits.check_image(display_w, display_h)?;
}
}
b"colr" => {
color = parse_colr_into(color, &p.data);
}
b"irot" => {
if let Some(&steps) = p.data.first() {
orientation = irot_to_orientation(steps & 3, orientation);
}
}
b"imir" => {
if let Some(&axis) = p.data.first() {
orientation = imir_to_orientation(axis & 1, orientation);
}
}
b"clli" if p.data.len() >= 4 => {
let maxcll = read_u16(&p.data, 0).unwrap_or(0);
let maxfall = read_u16(&p.data, 2).unwrap_or(0);
cll = Some(ContentLightLevel::new(maxcll, maxfall));
}
b"clap" if p.data.len() >= 32 => {
let width_n = read_u32(&p.data, 0).unwrap_or(0);
let width_d = read_u32(&p.data, 4).unwrap_or(1);
let height_n = read_u32(&p.data, 8).unwrap_or(0);
let height_d = read_u32(&p.data, 12).unwrap_or(1);
let horiz_off_n =
i32::from_be_bytes(p.data[16..20].try_into().unwrap_or([0; 4]));
let horiz_off_d = read_u32(&p.data, 20).unwrap_or(1);
let vert_off_n =
i32::from_be_bytes(p.data[24..28].try_into().unwrap_or([0; 4]));
let vert_off_d = read_u32(&p.data, 28).unwrap_or(1);
clap = Some(CleanAperture {
width_n,
width_d,
height_n,
height_d,
horiz_off_n,
horiz_off_d,
vert_off_n,
vert_off_d,
});
}
b"pasp" if p.data.len() >= 8 => {
let h = read_u32(&p.data, 0).unwrap_or(1);
let v = read_u32(&p.data, 4).unwrap_or(1);
pasp = Some(PixelAspectRatio {
h_spacing: h.max(1),
v_spacing: v.max(1),
});
}
_ => {}
}
}
}
if hvcc.is_empty() {
return Err(DecodeError::MissingBox("hvcC property"));
}
Ok(HeifItem {
_item_id: item_id,
data_offset: offset,
data_length: length,
hvcc,
display_w,
display_h,
color,
orientation,
cll,
clap,
pasp,
_is_alpha: is_alpha,
})
}
fn parse_colr_into(mut color: ColorMetadata, data: &[u8]) -> ColorMetadata {
if data.len() < 4 {
return color;
}
match &data[..4] {
b"nclx" if data.len() >= 11 => {
color.cicp = Some(Cicp {
primaries: Primaries::from_u8(read_u16(data, 4).unwrap_or(2) as u8),
transfer: TransferFunction::from_u8(read_u16(data, 6).unwrap_or(2) as u8),
matrix: MatrixCoefficients::from_u8(read_u16(data, 8).unwrap_or(2) as u8),
full_range: (data[10] & 0x80) != 0,
});
}
b"prof" | b"rICC" => {
color.icc = Some(data[4..].to_vec());
}
_ => {}
}
color
}
fn irot_to_orientation(steps: u8, prev: Orientation) -> Orientation {
match (prev, steps) {
(Orientation::Normal, 0) => Orientation::Normal,
(Orientation::Normal, 1) => Orientation::Rotate270,
(Orientation::Normal, 2) => Orientation::Rotate180,
(Orientation::Normal, 3) => Orientation::Rotate90,
_ => prev, }
}
fn imir_to_orientation(axis: u8, prev: Orientation) -> Orientation {
match (prev, axis) {
(Orientation::Normal, 0) => Orientation::FlipH, (Orientation::Normal, 1) => Orientation::FlipV, _ => prev,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn boxed(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = 8 + payload.len();
let mut v = (size as u32).to_be_bytes().to_vec();
v.extend_from_slice(fourcc);
v.extend_from_slice(payload);
v
}
#[test]
fn box_size_limit_stops_iteration() {
let data = boxed(b"ftyp", b"heic"); let seen: Vec<_> = Boxes::with_limit(&data, 10).collect();
assert!(seen.is_empty(), "oversized box should be rejected");
let seen2: Vec<_> = Boxes::with_limit(&data, 4096).collect();
assert_eq!(seen2.len(), 1);
}
#[test]
fn non_heif_input_is_rejected() {
let limits = ParseLimits::default();
let junk = boxed(b"ftyp", b"mp42");
assert!(matches!(parse(&junk, &limits), Err(DecodeError::NotHeif)));
}
#[test]
fn item_size_limit_rejects_large_item() {
let mut limits = ParseLimits::default();
limits.max_item_size = 100;
let mut extents = std::collections::HashMap::new();
extents.insert(1u16, (0u64, 200u64)); let props: Vec<Prop> = vec![Prop::default()];
let assoc = std::collections::HashMap::new();
let r = build_item(1, &extents, &props, &assoc, false, &[], &limits);
assert!(matches!(
r,
Err(DecodeError::LimitExceeded {
what: "item data size",
..
})
));
}
}