libgm 0.5.1

A tool for modding, unpacking and decompiling GameMaker games
Documentation
use std::fmt::Display;
use std::fmt::Formatter;

use crate::prelude::*;
use crate::util::fmt::hexdump;

#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ChunkName {
    bytes: [u8; 4],
}

impl ChunkName {
    /// This function panics on invalid chunk names.
    /// It is only meant to be used in `const` contexts
    /// where panics result in a compile error.
    pub const fn new(name: &'static str) -> Self {
        // TODO(const-hack) Switch to assert_eq once assert_failed is const-stable.
        assert!(name.len() == 4, "Expected string of length 4");

        let bytes = name.as_bytes();

        // TODO(const-hack): Iterators are not const stable.
        let mut i = 0;
        while i < 4 {
            assert!(
                validate_char(bytes[i]),
                "Expected chunk name to only consist of uppercase ASCII letters and digits",
            );
            i += 1;
        }

        // TODO(const-hack): `try_into` is not const stable.
        let bytes: [u8; 4] = [bytes[0], bytes[1], bytes[2], bytes[3]];
        Self { bytes }
    }

    pub fn from_bytes(bytes: [u8; 4]) -> Result<Self> {
        let valid: bool = bytes.iter().all(|&byte| validate_char(byte));
        if valid {
            return Ok(Self { bytes });
        }
        let hexdump = hexdump(&bytes);
        let msg = "only consist of uppercase ASCII digits and letters";
        if let Some(string) = try_display(&bytes) {
            bail!("Expected chunk name {string:?} [{hexdump}] to {msg}");
        }
        bail!("Expected chunk name [{hexdump}] to {msg}");
    }

    #[inline]
    #[must_use]
    pub const fn as_bytes(self) -> [u8; 4] {
        self.bytes
    }

    #[inline]
    #[must_use]
    pub const fn as_str(&self) -> &str {
        // SAFETY: We validated UTF-8 in constructor (ASCII subset).
        unsafe { str::from_utf8_unchecked(&self.bytes) }
    }
}

impl Display for ChunkName {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

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

#[inline]
const fn validate_char(byte: u8) -> bool {
    matches!(byte, b'A'..=b'Z' | b'0'..=b'9')
}

#[inline]
fn try_display(chunk_name: &[u8; 4]) -> Option<&str> {
    // if all bytes are null/unprintable, then don't bother with a string
    // representation.
    if chunk_name.iter().all(u8::is_ascii_control) {
        return None;
    }
    str::from_utf8(chunk_name).ok()
}