zipatch-rs 1.2.0

Parser for FFXIV ZiPatch patch files
Documentation
use binrw::BinRead;

use super::util::read_null_trimmed_utf8;

/// `ADIR` chunk: create a directory under the game install root.
///
/// When applied, the patcher calls `create_dir_all` for
/// `<game_root>/<name>`. The directory is created recursively, so intermediate
/// path components are created as needed. If the directory already exists the
/// apply step succeeds silently.
///
/// `ADIR` chunks appear rarely in modern FFXIV patch files; the reference notes
/// they are theoretically possible but seldom emitted by SE's current patch
/// tooling. See `lib/FFXIVQuickLauncher/.../Chunk/AddDirectoryChunk.cs`.
///
/// # Wire format
///
/// ```text
/// [name_len: u32 BE] [name: name_len bytes, NUL-padded]
/// ```
///
/// `name_len` includes any trailing NUL bytes used for alignment padding.
/// The parsed [`AddDirectory::name`] field has those NULs stripped.
///
/// # Errors
///
/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if:
/// - the body is too short to contain the `name_len` field or the declared
///   number of name bytes (truncated input), or
/// - the name bytes are not valid UTF-8.
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct AddDirectory {
    /// Directory path relative to the game install root.
    ///
    /// Encoded as UTF-8 on the wire, length-prefixed by a `u32` big-endian
    /// byte count. Trailing NUL bytes used as alignment padding are stripped
    /// before this field is populated. Example: `"sqpack/ffxiv"`.
    #[br(parse_with = read_null_trimmed_utf8)]
    pub name: String,
}

pub(crate) fn parse(body: &[u8]) -> crate::Result<AddDirectory> {
    super::util::parse_be(body)
}

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

    #[test]
    fn parses_add_directory() {
        let mut body = Vec::new();
        body.extend_from_slice(&5u32.to_be_bytes());
        body.extend_from_slice(b"sqex\0");
        assert_eq!(parse(&body).unwrap().name, "sqex");
    }

    #[test]
    fn null_padding_trimmed() {
        let mut body = Vec::new();
        body.extend_from_slice(&8u32.to_be_bytes());
        body.extend_from_slice(b"ex\0\0\0\0\0\0");
        assert_eq!(parse(&body).unwrap().name, "ex");
    }
}