zipatch-rs 1.7.0

Parser for FFXIV ZiPatch patch files
Documentation
//! Strongly-typed wrappers around primitive identifiers that appear in the
//! public API.
//!
//! Each newtype here exists to give a domain concept a single canonical Rust
//! type, instead of leaking the raw primitive (`u32`, `[u8; 4]`, …) at every
//! call site. The wrappers are `#[repr(transparent)]` so they carry no runtime
//! cost compared to the underlying primitive.
//!
//! - [`PatchIndex`](crate::newtypes::PatchIndex) — a 0-based index into a
//!   multi-patch chain.
//! - [`ChunkTag`](crate::newtypes::ChunkTag) — a 4-byte ASCII chunk tag
//!   (`FHDR`, `APLY`, `SQPK`, …).
//! - [`SchemaVersion`](crate::newtypes::SchemaVersion) — a persisted record's
//!   schema-format version.

use std::fmt;

// ---------------------------------------------------------------------------
// PatchIndex
// ---------------------------------------------------------------------------

/// Zero-based index of a patch within a multi-patch chain.
///
/// Used by [`crate::index::PatchSource::read`] and surfaced in
/// [`crate::IndexError::PatchIndexOutOfRange`] to identify which patch in the
/// chain a [`crate::index::Plan`] is referring to. The first patch added to a
/// [`crate::index::PlanBuilder`] is `PatchIndex(0)`, the second
/// `PatchIndex(1)`, and so on.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PatchIndex(u32);

impl PatchIndex {
    /// Construct a [`PatchIndex`] from a raw `u32`.
    #[must_use]
    pub const fn new(idx: u32) -> Self {
        Self(idx)
    }

    /// Return the wrapped `u32`.
    #[must_use]
    pub const fn get(self) -> u32 {
        self.0
    }
}

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

impl From<u32> for PatchIndex {
    fn from(v: u32) -> Self {
        Self(v)
    }
}

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

// ---------------------------------------------------------------------------
// ChunkTag
// ---------------------------------------------------------------------------

/// A 4-byte ASCII chunk tag identifying the wire-format kind of a `ZiPatch`
/// chunk frame (`FHDR`, `APLY`, `SQPK`, `EOF_`, …).
///
/// Construct via [`ChunkTag::new`] / [`ChunkTag::from_bytes`] or one of the
/// well-known constants (e.g. [`ChunkTag::SQPK`]). Inspect the raw bytes via
/// [`ChunkTag::as_bytes`].
///
/// The [`Display`](std::fmt::Display) impl renders printable ASCII bytes
/// directly, NUL bytes as `_`, and any other byte as `.` — matching the
/// `zipatch dump` CLI's chunk-tag rendering.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChunkTag([u8; 4]);

impl ChunkTag {
    /// `FHDR` — the file-header chunk that opens every `ZiPatch` stream.
    pub const FHDR: ChunkTag = ChunkTag(*b"FHDR");
    /// `APLY` — sets/clears a boolean apply-time flag.
    pub const APLY: ChunkTag = ChunkTag(*b"APLY");
    /// `APFS` — apply free-space book-keeping (no-op at apply time).
    pub const APFS: ChunkTag = ChunkTag(*b"APFS");
    /// `ADIR` — create a directory under the install root.
    pub const ADIR: ChunkTag = ChunkTag(*b"ADIR");
    /// `DELD` — delete a directory under the install root.
    pub const DELD: ChunkTag = ChunkTag(*b"DELD");
    /// `SQPK` — the SQPK workhorse chunk wrapping one of eight sub-commands.
    pub const SQPK: ChunkTag = ChunkTag(*b"SQPK");
    /// `EOF_` — end-of-stream terminator.
    pub const EOF: ChunkTag = ChunkTag(*b"EOF_");

    /// Construct a [`ChunkTag`] from a 4-byte array.
    #[must_use]
    pub const fn new(bytes: [u8; 4]) -> Self {
        Self(bytes)
    }

    /// Construct a [`ChunkTag`] from a borrowed 4-byte slice.
    #[must_use]
    pub const fn from_bytes(bytes: &[u8; 4]) -> Self {
        Self(*bytes)
    }

    /// Return the raw 4 bytes of the tag.
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8; 4] {
        &self.0
    }

    /// Return the tag as a `&str` if every byte is valid UTF-8.
    ///
    /// All wire tags defined by the `ZiPatch` format are 4-byte ASCII, so
    /// this returns `Some(&str)` in practice; the fallible variant exists
    /// because the underlying field type does not constrain bytes to UTF-8.
    #[must_use]
    pub fn as_str(&self) -> Option<&str> {
        std::str::from_utf8(&self.0).ok()
    }
}

impl fmt::Display for ChunkTag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut buf = [0u8; 4];
        for (out, b) in buf.iter_mut().zip(self.0.iter()) {
            *out = if b.is_ascii_graphic() || *b == b' ' {
                *b
            } else if *b == 0 {
                b'_'
            } else {
                b'.'
            };
        }
        // SAFETY: every byte written into `buf` is ASCII (graphic, space, `_`,
        // or `.`), so the slice is valid UTF-8.
        f.write_str(std::str::from_utf8(&buf).unwrap_or("????"))
    }
}

impl From<[u8; 4]> for ChunkTag {
    fn from(v: [u8; 4]) -> Self {
        Self(v)
    }
}

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

impl PartialEq<[u8; 4]> for ChunkTag {
    fn eq(&self, other: &[u8; 4]) -> bool {
        &self.0 == other
    }
}

impl PartialEq<ChunkTag> for [u8; 4] {
    fn eq(&self, other: &ChunkTag) -> bool {
        self == &other.0
    }
}

// ---------------------------------------------------------------------------
// SchemaVersion
// ---------------------------------------------------------------------------

/// Schema-format version stamped on a persisted record (e.g. a
/// [`crate::apply::Checkpoint`] or a [`crate::index::Plan`]).
///
/// Strict-equality compatibility: a persisted record with a `SchemaVersion`
/// that does not equal the build's `CURRENT_SCHEMA_VERSION` is refused rather
/// than silently re-interpreted. Use [`SchemaVersion::compatible_with`] to
/// perform the comparison.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SchemaVersion(u32);

impl SchemaVersion {
    /// Construct a [`SchemaVersion`] from a raw `u32`.
    #[must_use]
    pub const fn new(v: u32) -> Self {
        Self(v)
    }

    /// Return the wrapped `u32`.
    #[must_use]
    pub const fn get(self) -> u32 {
        self.0
    }

    /// Returns `true` if `self` is compatible with `other`.
    ///
    /// Compatibility is strict equality: persisted records carrying a
    /// different version cannot be silently consumed against the current
    /// schema, because the layout may have shifted in either direction.
    #[must_use]
    pub const fn compatible_with(self, other: Self) -> bool {
        self.0 == other.0
    }

    /// Return a [`SchemaVersion`] whose inner `u32` is
    /// `self.get().wrapping_add(rhs)`. Intended for tests that need to
    /// fabricate a "version off by one" value.
    #[must_use]
    pub const fn wrapping_add(self, rhs: u32) -> Self {
        Self(self.0.wrapping_add(rhs))
    }

    /// Return a [`SchemaVersion`] whose inner `u32` is
    /// `self.get().wrapping_sub(rhs)`. Intended for tests that need to
    /// fabricate a "version off by one" value.
    #[must_use]
    pub const fn wrapping_sub(self, rhs: u32) -> Self {
        Self(self.0.wrapping_sub(rhs))
    }
}

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

impl From<u32> for SchemaVersion {
    fn from(v: u32) -> Self {
        Self(v)
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn patch_index_round_trip() {
        let p = PatchIndex::new(7);
        assert_eq!(p.get(), 7);
        assert_eq!(u32::from(p), 7);
        assert_eq!(PatchIndex::from(7u32), p);
    }

    #[test]
    fn chunk_tag_display_printable_ascii() {
        assert_eq!(format!("{}", ChunkTag::SQPK), "SQPK");
        assert_eq!(format!("{}", ChunkTag::EOF), "EOF_");
        assert_eq!(format!("{}", ChunkTag::new([0, b'A', b'B', b'C'])), "_ABC");
        assert_eq!(
            format!("{}", ChunkTag::new([0xff, b'A', b'B', b'C'])),
            ".ABC"
        );
    }

    #[test]
    fn chunk_tag_constants_match_ascii() {
        assert_eq!(ChunkTag::FHDR.as_bytes(), b"FHDR");
        assert_eq!(ChunkTag::SQPK.as_bytes(), b"SQPK");
        assert_eq!(ChunkTag::EOF.as_bytes(), b"EOF_");
    }

    #[test]
    fn chunk_tag_eq_array() {
        let tag = ChunkTag::SQPK;
        assert!(tag == *b"SQPK");
        assert!(*b"SQPK" == tag);
    }

    #[test]
    fn schema_version_compat_is_strict_equality() {
        let a = SchemaVersion::new(1);
        let b = SchemaVersion::new(1);
        let c = SchemaVersion::new(2);
        assert!(a.compatible_with(b));
        assert!(!a.compatible_with(c));
    }
}