zipatch_rs/chunk/ddir.rs
1use binrw::BinRead;
2
3use super::util::read_null_trimmed_utf8;
4
5/// `DELD` chunk: remove a directory under the game install root.
6///
7/// When applied, the patcher calls `remove_dir` for `<game_root>/<name>`.
8/// Unlike `ADIR`, only an empty directory can be deleted this way; if the
9/// directory is non-empty the OS returns an error. If
10/// [`crate::ApplyContext::ignore_missing`] is set, a missing directory is
11/// silently skipped instead of returning an error.
12///
13/// Like `ADIR`, `DELD` chunks are rare in modern patches. The reference
14/// implementation logs failures rather than rethrowing in some paths; this
15/// crate propagates them. See
16/// `lib/FFXIVQuickLauncher/.../Chunk/DeleteDirectoryChunk.cs`.
17///
18/// # Wire format
19///
20/// ```text
21/// [name_len: u32 BE] [name: name_len bytes, NUL-padded]
22/// ```
23///
24/// Identical layout to [`crate::chunk::adir::AddDirectory`]. `name_len`
25/// includes any trailing NUL padding bytes; the parsed field has them stripped.
26///
27/// # Errors
28///
29/// Parsing fails with [`crate::ZiPatchError::BinrwError`] if:
30/// - the body is too short to contain the `name_len` field or the declared
31/// number of name bytes (truncated input), or
32/// - the name bytes are not valid UTF-8.
33#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
34#[br(big)]
35pub struct DeleteDirectory {
36 /// Directory path relative to the game install root.
37 ///
38 /// Encoded as UTF-8 on the wire, length-prefixed by a `u32` big-endian
39 /// byte count. Trailing NUL bytes used as alignment padding are stripped
40 /// before this field is populated. Example: `"sqpack/ex3"`.
41 #[br(parse_with = read_null_trimmed_utf8)]
42 pub name: String,
43}
44
45pub(crate) fn parse(body: &[u8]) -> crate::Result<DeleteDirectory> {
46 super::util::parse_be(body)
47}
48
49#[cfg(test)]
50mod tests {
51 use super::parse;
52
53 #[test]
54 fn parses_delete_directory() {
55 let mut body = Vec::new();
56 body.extend_from_slice(&4u32.to_be_bytes());
57 body.extend_from_slice(b"data");
58 assert_eq!(parse(&body).unwrap().name, "data");
59 }
60}