act2pal 0.1.1

Converts Adobe Colour Tables to .pal files.
Documentation
#![doc = include_str!("../README.md")]

use std::{
    error,
    fmt::{self, Write},
    ops::Deref,
};

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl From<(u8, u8, u8)> for Color {
    fn from((r, g, b): (u8, u8, u8)) -> Self {
        Self { r, g, b }
    }
}

impl PartialEq<(u8, u8, u8)> for Color {
    fn eq(&self, &rhs: &(u8, u8, u8)) -> bool {
        (self.r, self.g, self.b) == rhs
    }
}

#[derive(Debug, PartialEq)]
pub struct Colors {
    colors: Vec<Color>,
}

impl Colors {
    pub fn new(colors: Vec<Color>) -> Self {
        Self { colors }
    }

    pub fn from_act(bytes: &[u8]) -> Result<Self, ParseError> {
        const BYTES_PER_COLOR: usize = 3;
        const COUNT_OFFSET_FROM_END: usize = 3;

        let count = *bytes
            .get(bytes.len() - COUNT_OFFSET_FROM_END)
            .ok_or(ParseError)?;

        let colors = bytes
            .chunks_exact(BYTES_PER_COLOR)
            .take(count as _)
            .map(|chunk| {
                Ok(Color {
                    r: *chunk.first().ok_or(ParseError)?,
                    g: *chunk.get(1).ok_or(ParseError)?,
                    b: *chunk.get(2).ok_or(ParseError)?,
                })
            })
            .collect::<Result<Vec<_>, ParseError>>()?;

        if colors.len() as u8 != count {
            return Err(ParseError);
        }

        Ok(Self { colors })
    }

    pub fn to_pal_string(&self) -> Result<String, fmt::Error> {
        const MAGIC: &str = "JASC-PAL\n0100\n";

        let mut s = String::from(MAGIC);
        writeln!(s, "{}", self.colors.len())?;

        for Color { r, g, b } in &self.colors {
            writeln!(s, "{r} {g} {b}")?;
        }

        Ok(s)
    }
}

impl Deref for Colors {
    type Target = [Color];

    fn deref(&self) -> &Self::Target {
        &self.colors
    }
}

#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct ParseError;

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unprocessable bytes")
    }
}

impl error::Error for ParseError {}