image4 0.8.2

A no_std-friendly library for parsing and generation of Image4 images written in pure Rust.
Documentation
use core::{
    fmt::{self, Formatter},
    str::FromStr,
};

/// An error returned by the [`TryFrom`] implementations on the [`Tag`] type when the length of the
/// source value is not 4.
#[derive(Debug)]
pub struct InvalidTagLength;

impl fmt::Display for InvalidTagLength {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str("invalid 4CC tag length")
    }
}

#[cfg(feature = "std")]
impl std::error::Error for InvalidTagLength {}

/// A 4CC tag that is common in the Image4 format.
///
/// The tag is stored as an unsigned big-endian 32-bit integer. That means the comparison will
/// behave in the same way as for `[u8; 4]`.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct Tag(u32);

impl Tag {
    /// Converts an array of 4 bytes into a 4CC tag.
    #[inline(always)]
    pub const fn from_bytes(bytes: [u8; 4]) -> Self {
        Self(u32::from_be_bytes(bytes))
    }

    /// Converts the tag into an array of 4 bytes.
    #[inline(always)]
    pub const fn to_bytes(self) -> [u8; 4] {
        self.0.to_be_bytes()
    }
}

impl From<[u8; 4]> for Tag {
    fn from(value: [u8; 4]) -> Self {
        Tag(u32::from_be_bytes(value))
    }
}

impl From<Tag> for [u8; 4] {
    fn from(value: Tag) -> Self {
        value.0.to_be_bytes()
    }
}

impl From<u32> for Tag {
    fn from(value: u32) -> Self {
        Tag(value)
    }
}

impl From<Tag> for u32 {
    fn from(value: Tag) -> Self {
        value.0
    }
}

impl<'a> From<&'a [u8; 4]> for Tag {
    fn from(value: &'a [u8; 4]) -> Self {
        (*value).into()
    }
}

impl<'a> TryFrom<&'a [u8]> for Tag {
    type Error = InvalidTagLength;

    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
        let value: &[u8; 4] = value.try_into().map_err(|_| InvalidTagLength)?;
        Ok(value.into())
    }
}

impl<'a> TryFrom<&'a str> for Tag {
    type Error = InvalidTagLength;

    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
        value.as_bytes().try_into()
    }
}

impl FromStr for Tag {
    type Err = InvalidTagLength;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.as_bytes().try_into()
    }
}

impl fmt::Display for Tag {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.to_bytes().escape_ascii(), f)
    }
}

impl fmt::Debug for Tag {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "Tag('{}')", self.to_bytes().escape_ascii())
    }
}

#[cfg(feature = "serde")]
mod serde {
    use super::Tag;
    use core::fmt::Formatter;
    use serde::{
        de::{Error, Visitor},
        Deserializer, Serializer,
    };

    impl serde::Serialize for Tag {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            let bytes = self.to_bytes();

            if bytes.iter().all(u8::is_ascii_graphic) {
                serializer.serialize_str(core::str::from_utf8(&bytes).unwrap())
            } else {
                serializer.serialize_bytes(&bytes)
            }
        }
    }

    struct TagVisitor;

    impl Visitor<'_> for TagVisitor {
        type Value = Tag;

        fn expecting(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
            formatter.write_str("either a 4-byte string or a 4-byte buffer")
        }

        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: Error,
        {
            TryInto::try_into(v).map_err(|_| E::invalid_length(v.len(), &self))
        }

        fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
        where
            E: Error,
        {
            TryInto::try_into(v).map_err(|_| E::invalid_length(v.len(), &self))
        }
    }

    impl<'de> serde::Deserialize<'de> for Tag {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            deserializer.deserialize_any(TagVisitor)
        }
    }
}