use crate::parser::{read_u16, read_u32};
use crate::Error;
#[derive(Debug, Clone)]
pub struct CpalTable<'a> {
bytes: &'a [u8],
version: u16,
num_palette_entries: u16,
num_palettes: u16,
num_color_records: u16,
color_records_array_offset: u32,
color_record_indices_offset: usize,
palette_types_array_offset: u32,
}
impl<'a> CpalTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < 12 {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
let num_palette_entries = read_u16(bytes, 2)?;
let num_palettes = read_u16(bytes, 4)?;
let num_color_records = read_u16(bytes, 6)?;
let color_records_array_offset = read_u32(bytes, 8)?;
let indices_off = 12usize;
let indices_end = indices_off
.checked_add(num_palettes as usize * 2)
.ok_or(Error::BadOffset)?;
if bytes.len() < indices_end {
return Err(Error::UnexpectedEof);
}
let color_records_end = (color_records_array_offset as u64)
.checked_add(num_color_records as u64 * 4)
.ok_or(Error::BadOffset)?;
if color_records_end > bytes.len() as u64 {
return Err(Error::BadOffset);
}
let palette_types_array_offset = if version >= 1 {
let trailer = indices_end;
if bytes.len() < trailer + 12 {
0
} else {
read_u32(bytes, trailer)?
}
} else {
0
};
Ok(Self {
bytes,
version,
num_palette_entries,
num_palettes,
num_color_records,
color_records_array_offset,
color_record_indices_offset: indices_off,
palette_types_array_offset,
})
}
pub fn version(&self) -> u16 {
self.version
}
pub fn num_palette_entries(&self) -> u16 {
self.num_palette_entries
}
pub fn num_palettes(&self) -> u16 {
self.num_palettes
}
fn first_record(&self, palette_index: u16) -> Option<u16> {
if palette_index >= self.num_palettes {
return None;
}
let off = self.color_record_indices_offset + (palette_index as usize) * 2;
read_u16(self.bytes, off).ok()
}
pub fn color(&self, palette_index: u16, color_index: u16) -> Option<[u8; 4]> {
let first = self.first_record(palette_index)?;
if color_index >= self.num_palette_entries {
return None;
}
let abs = first as u32 + color_index as u32;
if abs >= self.num_color_records as u32 {
return None;
}
let off = self.color_records_array_offset as usize + (abs as usize) * 4;
let b = self.bytes.get(off)?;
let g = self.bytes.get(off + 1)?;
let r = self.bytes.get(off + 2)?;
let a = self.bytes.get(off + 3)?;
Some([*r, *g, *b, *a])
}
pub fn palette(&self, palette_index: u16) -> Option<Vec<[u8; 4]>> {
let n = self.num_palette_entries;
let mut out = Vec::with_capacity(n as usize);
for i in 0..n {
out.push(self.color(palette_index, i)?);
}
Some(out)
}
pub fn palette_type(&self, palette_index: u16) -> u32 {
if palette_index >= self.num_palettes
|| self.palette_types_array_offset == 0
|| self.version < 1
{
return 0;
}
let off = self.palette_types_array_offset as usize + palette_index as usize * 4;
if off + 4 > self.bytes.len() {
return 0;
}
read_u32(self.bytes, off).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_cpal_v0_two_palettes() -> Vec<u8> {
let header_end = 12 + 4;
let records_off = header_end;
let total = records_off + 6 * 4;
let mut bytes = vec![0u8; total];
bytes[0..2].copy_from_slice(&0u16.to_be_bytes()); bytes[2..4].copy_from_slice(&3u16.to_be_bytes()); bytes[4..6].copy_from_slice(&2u16.to_be_bytes()); bytes[6..8].copy_from_slice(&6u16.to_be_bytes()); bytes[8..12].copy_from_slice(&(records_off as u32).to_be_bytes());
bytes[12..14].copy_from_slice(&0u16.to_be_bytes());
bytes[14..16].copy_from_slice(&3u16.to_be_bytes());
let recs = [
(0xFF, 0x00, 0x00, 0xFF), (0x00, 0xFF, 0x00, 0xFF), (0x00, 0x00, 0xFF, 0xFF), (0x80, 0x80, 0x80, 0x80), (0x00, 0x00, 0x00, 0xFF), (0xFF, 0xFF, 0xFF, 0xFF), ];
for (i, (r, g, b, a)) in recs.iter().enumerate() {
let off = records_off + i * 4;
bytes[off] = *b;
bytes[off + 1] = *g;
bytes[off + 2] = *r;
bytes[off + 3] = *a;
}
bytes
}
#[test]
fn parses_v0_header() {
let bytes = synth_cpal_v0_two_palettes();
let cpal = CpalTable::parse(&bytes).expect("parse");
assert_eq!(cpal.version(), 0);
assert_eq!(cpal.num_palettes(), 2);
assert_eq!(cpal.num_palette_entries(), 3);
}
#[test]
fn color_lookup_palette0() {
let bytes = synth_cpal_v0_two_palettes();
let cpal = CpalTable::parse(&bytes).expect("parse");
assert_eq!(cpal.color(0, 0), Some([0xFF, 0x00, 0x00, 0xFF])); assert_eq!(cpal.color(0, 1), Some([0x00, 0xFF, 0x00, 0xFF])); assert_eq!(cpal.color(0, 2), Some([0x00, 0x00, 0xFF, 0xFF])); }
#[test]
fn color_lookup_palette1() {
let bytes = synth_cpal_v0_two_palettes();
let cpal = CpalTable::parse(&bytes).expect("parse");
assert_eq!(cpal.color(1, 0), Some([0x80, 0x80, 0x80, 0x80])); assert_eq!(cpal.color(1, 1), Some([0x00, 0x00, 0x00, 0xFF])); assert_eq!(cpal.color(1, 2), Some([0xFF, 0xFF, 0xFF, 0xFF])); }
#[test]
fn out_of_range_lookup_returns_none() {
let bytes = synth_cpal_v0_two_palettes();
let cpal = CpalTable::parse(&bytes).expect("parse");
assert!(cpal.color(2, 0).is_none()); assert!(cpal.color(0, 3).is_none()); assert!(cpal.color(99, 99).is_none());
}
#[test]
fn palette_returns_full_vec() {
let bytes = synth_cpal_v0_two_palettes();
let cpal = CpalTable::parse(&bytes).expect("parse");
let p1 = cpal.palette(1).expect("palette 1");
assert_eq!(p1.len(), 3);
assert_eq!(p1[0], [0x80, 0x80, 0x80, 0x80]);
assert_eq!(p1[1], [0x00, 0x00, 0x00, 0xFF]);
assert_eq!(p1[2], [0xFF, 0xFF, 0xFF, 0xFF]);
assert!(cpal.palette(2).is_none());
}
fn synth_cpal_v1_with_types() -> Vec<u8> {
let header_fixed = 12;
let indices = 4; let trailer = 12;
let records_off = header_fixed + indices + trailer;
let records_len = 6 * 4;
let types_off = records_off + records_len;
let total = types_off + 4 * 2;
let mut bytes = vec![0u8; total];
bytes[0..2].copy_from_slice(&1u16.to_be_bytes()); bytes[2..4].copy_from_slice(&3u16.to_be_bytes());
bytes[4..6].copy_from_slice(&2u16.to_be_bytes());
bytes[6..8].copy_from_slice(&6u16.to_be_bytes());
bytes[8..12].copy_from_slice(&(records_off as u32).to_be_bytes());
bytes[12..14].copy_from_slice(&0u16.to_be_bytes());
bytes[14..16].copy_from_slice(&3u16.to_be_bytes());
bytes[16..20].copy_from_slice(&(types_off as u32).to_be_bytes()); bytes[20..24].copy_from_slice(&0u32.to_be_bytes()); bytes[24..28].copy_from_slice(&0u32.to_be_bytes()); bytes[types_off..types_off + 4].copy_from_slice(&0x0001u32.to_be_bytes()); bytes[types_off + 4..types_off + 8].copy_from_slice(&0x0002u32.to_be_bytes()); bytes
}
#[test]
fn v1_palette_types() {
let bytes = synth_cpal_v1_with_types();
let cpal = CpalTable::parse(&bytes).expect("parse");
assert_eq!(cpal.version(), 1);
assert_eq!(cpal.palette_type(0), 0x0001);
assert_eq!(cpal.palette_type(1), 0x0002);
assert_eq!(cpal.palette_type(2), 0); }
#[test]
fn rejects_truncated_header() {
assert!(matches!(
CpalTable::parse(&[0u8; 8]),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_records_past_end() {
let mut bytes = vec![0u8; 12 + 4]; bytes[2..4].copy_from_slice(&3u16.to_be_bytes());
bytes[4..6].copy_from_slice(&2u16.to_be_bytes());
bytes[6..8].copy_from_slice(&100u16.to_be_bytes());
bytes[8..12].copy_from_slice(&100u32.to_be_bytes());
assert!(matches!(CpalTable::parse(&bytes), Err(Error::BadOffset)));
}
}