libpna 0.32.1

PNA(Portable-Network-Archive) decoding and encoding library
Documentation
use std::{
    error::Error,
    fmt::{self, Debug, Display, Formatter},
    io,
};

/// [ChunkType] validation error.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum ChunkTypeError {
    /// Contains non-ascii alphabet error.
    NonAsciiAlphabetic,
    /// The second character is not lowercase error.
    NonPrivateChunkType,
    /// The third character is not uppercase error.
    Reserved,
}

impl Display for ChunkTypeError {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Display::fmt(
            match self {
                Self::NonAsciiAlphabetic => "All characters must be ASCII alphabetic",
                Self::NonPrivateChunkType => "The second character must be lowercase",
                Self::Reserved => "The third character must be uppercase",
            },
            f,
        )
    }
}

impl Error for ChunkTypeError {}

impl From<ChunkTypeError> for io::Error {
    #[inline]
    fn from(e: ChunkTypeError) -> Self {
        io::Error::new(io::ErrorKind::InvalidData, e)
    }
}

/// A 4-byte chunk type code.
///
/// PNA uses a chunk-based format inspired by PNG. Each chunk has a 4-character
/// type code that determines how the chunk should be interpreted.
///
/// # Chunk Type Naming Convention
///
/// The case of each letter in the chunk type encodes important properties:
///
/// | Position | Uppercase | Lowercase |
/// |----------|-----------|-----------|
/// | 1st | Critical (must understand) | Ancillary (can ignore) |
/// | 2nd | Public (standardized) | Private (application-specific) |
/// | 3rd | Reserved (must be uppercase) | - |
/// | 4th | Unsafe to copy | Safe to copy if unknown |
///
/// # Critical Chunks
///
/// These chunks are essential for reading the archive structure:
///
/// - **Archive structure**: [`AHED`](Self::AHED) (header), [`AEND`](Self::AEND) (end),
///   [`ANXT`](Self::ANXT) (next part)
/// - **Entry structure**: [`FHED`](Self::FHED) (header), [`FDAT`](Self::FDAT) (data),
///   [`FEND`](Self::FEND) (end)
/// - **Solid mode**: [`SHED`](Self::SHED) (header), [`SDAT`](Self::SDAT) (data),
///   [`SEND`](Self::SEND) (end)
/// - **Encryption**: [`PHSF`](Self::PHSF) (password hash string format)
///
/// # Ancillary Chunks
///
/// These chunks contain optional metadata that can be safely ignored:
///
/// - **Timestamps**: [`cTIM`](Self::cTIM), [`mTIM`](Self::mTIM), [`aTIM`](Self::aTIM)
///   (seconds), [`cTNS`](Self::cTNS), [`mTNS`](Self::mTNS), [`aTNS`](Self::aTNS) (nanoseconds)
/// - **File info**: [`fSIZ`](Self::fSIZ) (size), [`fPRM`](Self::fPRM) (permissions)
/// - **Extended attributes**: [`xATR`](Self::xATR)
///
/// # Creating Private Chunks
///
/// Use [`ChunkType::private`] to create application-specific chunk types:
///
/// ```rust
/// use libpna::ChunkType;
///
/// // Private chunk type must have lowercase second letter
/// let my_chunk = ChunkType::private(*b"myTy").unwrap();
/// assert!(my_chunk.is_private());
/// ```
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ChunkType([u8; 4]);

impl ChunkType {
    // -- Critical chunks --
    /// Archive header.
    pub const AHED: ChunkType = ChunkType(*b"AHED");
    /// Archive end marker.
    pub const AEND: ChunkType = ChunkType(*b"AEND");
    /// Archive next part marker.
    pub const ANXT: ChunkType = ChunkType(*b"ANXT");
    /// Entry header.
    pub const FHED: ChunkType = ChunkType(*b"FHED");
    /// Password hash string format.
    pub const PHSF: ChunkType = ChunkType(*b"PHSF");
    /// Entry data stream.
    pub const FDAT: ChunkType = ChunkType(*b"FDAT");
    /// Entry data stream end marker.
    pub const FEND: ChunkType = ChunkType(*b"FEND");
    /// Solid mode data header.
    pub const SHED: ChunkType = ChunkType(*b"SHED");
    /// Solid mode data stream.
    pub const SDAT: ChunkType = ChunkType(*b"SDAT");
    /// Solid mode data stream end marker.
    pub const SEND: ChunkType = ChunkType(*b"SEND");

    // -- Auxiliary chunks --
    /// Raw file size.
    #[allow(non_upper_case_globals)]
    pub const fSIZ: ChunkType = ChunkType(*b"fSIZ");
    /// Creation datetime.
    #[allow(non_upper_case_globals)]
    pub const cTIM: ChunkType = ChunkType(*b"cTIM");
    /// Last modified datetime.
    #[allow(non_upper_case_globals)]
    pub const mTIM: ChunkType = ChunkType(*b"mTIM");
    /// Last accessed datetime.
    #[allow(non_upper_case_globals)]
    pub const aTIM: ChunkType = ChunkType(*b"aTIM");
    /// Nanoseconds for creation datetime.
    #[allow(non_upper_case_globals)]
    pub const cTNS: ChunkType = ChunkType(*b"cTNS");
    /// Nanoseconds for last modified datetime.
    #[allow(non_upper_case_globals)]
    pub const mTNS: ChunkType = ChunkType(*b"mTNS");
    /// Nanoseconds for last accessed datetime.
    #[allow(non_upper_case_globals)]
    pub const aTNS: ChunkType = ChunkType(*b"aTNS");
    /// Entry permissions.
    #[allow(non_upper_case_globals)]
    pub const fPRM: ChunkType = ChunkType(*b"fPRM");
    /// Extended attribute.
    #[allow(non_upper_case_globals)]
    pub const xATR: ChunkType = ChunkType(*b"xATR");

    /// Returns the length of the chunk type code.
    ///
    /// # Returns
    ///
    /// An integer value representing the length of the chunk type code.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use libpna::ChunkType;
    ///
    /// let chunk_type = ChunkType::AHED;
    ///
    /// assert_eq!(chunk_type.len(), 4);
    /// ```
    #[allow(clippy::len_without_is_empty)]
    #[inline]
    pub const fn len(&self) -> usize {
        self.0.len()
    }

    /// Returns the raw 4-byte chunk type code.
    #[inline]
    pub(crate) const fn as_bytes(&self) -> &[u8; 4] {
        &self.0
    }

    /// Creates a [`ChunkType`] from raw bytes, validating that all bytes are ASCII alphabetic.
    ///
    /// # Errors
    ///
    /// Returns [`ChunkTypeError::NonAsciiAlphabetic`] if any byte is not in `a..=z` or `A..=Z`.
    #[inline]
    pub(crate) const fn new(ty: [u8; 4]) -> Result<Self, ChunkTypeError> {
        // NOTE: use a while statement for const context.
        let mut idx = 0;
        while idx < ty.len() {
            if !ty[idx].is_ascii_alphabetic() {
                return Err(ChunkTypeError::NonAsciiAlphabetic);
            }
            idx += 1;
        }
        Ok(Self(ty))
    }

    /// Creates private [ChunkType].
    ///
    /// # Errors
    ///
    /// This function will return an error in the following cases:
    /// - Value contains non-ASCII alphabet characters
    /// - The second character is not lowercase
    /// - The third character is not uppercase
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use libpna::{ChunkType, ChunkTypeError};
    /// assert!(ChunkType::private(*b"myTy").is_ok());
    /// assert_eq!(
    ///     ChunkType::private(*b"zeR\0").unwrap_err(),
    ///     ChunkTypeError::NonAsciiAlphabetic
    /// );
    /// assert_eq!(
    ///     ChunkType::private(*b"pRIv").unwrap_err(),
    ///     ChunkTypeError::NonPrivateChunkType
    /// );
    /// assert_eq!(
    ///     ChunkType::private(*b"rese").unwrap_err(),
    ///     ChunkTypeError::Reserved
    /// );
    /// ```
    #[inline]
    pub const fn private(ty: [u8; 4]) -> Result<Self, ChunkTypeError> {
        // NOTE: use a while statement for const context.
        let mut idx = 0;
        while idx < ty.len() {
            if !ty[idx].is_ascii_alphabetic() {
                return Err(ChunkTypeError::NonAsciiAlphabetic);
            }
            idx += 1;
        }
        if !ty[1].is_ascii_lowercase() {
            return Err(ChunkTypeError::NonPrivateChunkType);
        }
        if !ty[2].is_ascii_uppercase() {
            return Err(ChunkTypeError::Reserved);
        }
        Ok(Self(ty))
    }

    /// Creates a custom [`ChunkType`] without validation.
    ///
    /// # Display behavior
    /// If bytes are invalid UTF-8, they are rendered as lowercase hex bytes.
    /// ```rust
    /// # use libpna::ChunkType;
    ///
    /// let custom_chunk_type = unsafe { ChunkType::from_unchecked([0xe3, 0x81, 0x82, 0xe3]) };
    /// assert_eq!(format!("{}", custom_chunk_type), "[e3, 81, 82, e3]");
    /// ```
    ///
    /// # Safety
    /// Callers must ensure the value consists only of ASCII alphabetic
    /// characters ('a'..'z' and 'A'..'Z').
    /// ```rust
    /// # use libpna::ChunkType;
    ///
    /// let custom_chunk_type = unsafe { ChunkType::from_unchecked(*b"myTy") };
    /// format!("{}", custom_chunk_type);
    /// ```
    #[inline]
    pub const unsafe fn from_unchecked(ty: [u8; 4]) -> Self {
        Self(ty)
    }

    // -- Chunk type determination --

    /// Returns true if the chunk is critical.
    #[inline]
    pub const fn is_critical(&self) -> bool {
        self.0[0] & 32 == 0
    }

    /// Returns true if the chunk is private.
    #[inline]
    pub const fn is_private(&self) -> bool {
        self.0[1] & 32 != 0
    }

    /// Checks whether the reserved bit of the chunk name is set.
    /// If it is set, the chunk name is invalid.
    #[inline]
    pub const fn is_set_reserved(&self) -> bool {
        self.0[2] & 32 != 0
    }

    /// Returns true if the chunk is safe to copy if unknown.
    #[inline]
    pub const fn is_safe_to_copy(&self) -> bool {
        self.0[3] & 32 != 0
    }
}

impl Debug for ChunkType {
    #[inline]
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        struct DebugType([u8; 4]);

        impl Debug for DebugType {
            #[inline]
            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
                for &c in &self.0[..] {
                    write!(f, "{}", char::from(c).escape_debug())?;
                }
                Ok(())
            }
        }

        f.debug_struct("ChunkType")
            .field("type", &DebugType(self.0))
            .field("critical", &self.is_critical())
            .field("private", &self.is_private())
            .field("reserved", &self.is_set_reserved())
            .field("safe_to_copy", &self.is_safe_to_copy())
            .finish()
    }
}

impl Display for ChunkType {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match std::str::from_utf8(&self.0) {
            Ok(s) => Display::fmt(s, f),
            Err(_) => write!(f, "{:02x?}", self.0),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    use wasm_bindgen_test::wasm_bindgen_test as test;

    #[test]
    fn to_string() {
        assert_eq!("AHED", ChunkType::AHED.to_string());
    }

    #[test]
    fn is_critical() {
        assert!(ChunkType::AHED.is_critical());
        assert!(!ChunkType::cTIM.is_critical());
    }

    #[test]
    fn is_private() {
        assert!(!ChunkType::AHED.is_private());
        assert!(ChunkType::private(*b"myTy").unwrap().is_private());
    }

    #[test]
    fn is_set_reserved() {
        assert!(!ChunkType::AHED.is_set_reserved());
    }

    #[test]
    fn is_safe_to_copy() {
        assert!(!ChunkType::AHED.is_safe_to_copy());
    }

    #[test]
    fn new_rejects_non_ascii() {
        assert_eq!(
            ChunkType::new(*b"AB\x00D"),
            Err(ChunkTypeError::NonAsciiAlphabetic)
        );
        assert_eq!(
            ChunkType::new(*b"AB1D"),
            Err(ChunkTypeError::NonAsciiAlphabetic)
        );
    }

    #[test]
    fn new_accepts_valid() {
        assert!(ChunkType::new(*b"AHED").is_ok());
        assert!(ChunkType::new(*b"myTy").is_ok());
    }
}