serde_hkx/bytes/serde/
hkx_header.rs

1//! # HKX Header Format Specification
2//!
3//! The HKX header format is used for storing metadata information in HKX files.
4//! HKX files are binary files commonly used in video game development for storing animation and physics data.
5//! The header contains essential information about the structure and properties of the HKX file.
6//!
7//! Size: 64bytes
8//!
9//! | Field Name                     | Description                                                    | Size (bytes) | Offset (bytes) |
10//! | ------------------------------ | -------------------------------------------------------------- | ------------ | -------------- |
11//! | Magic0                         | First magic number (`0x57E0E057`)                              | 4            | 0              |
12//! | Magic1                         | Second magic number (`0x10C0C010`)                             | 4            | 4              |
13//! | UserTag                        | User-defined tag                                               | 4            | 8              |
14//! | FileVersion                    | Version of the file (LittleEndian e.g. 0x08 0x00 0x00 0x00)    | 4            | 12             |
15//! | PointerSize                    | Size of pointers in bytes (4 or 8)                             | 1            | 16             |
16//! | Endian                         | Endianness of the file (0 for big-endian, 1 for little-endian) | 1            | 17             |
17//! | PaddingOption                  | Padding option used in the file                                | 1            | 18             |
18//! | BaseClass                      | Base class                                                     | 1            | 19             |
19//! | SectionCount                   | Number of sections in the HKX file                             | 4            | 20             |
20//! | ContentsSectionIndex           | Index of the contents section within the file                  | 4            | 24             |
21//! | ContentsSectionOffset          | Offset of the contents section within the file                 | 4            | 28             |
22//! | ContentsClassNameSectionIndex  | Index of the contents class name section within the file       | 4            | 32             |
23//! | ContentsClassNameSectionOffset | Offset of the contents class name section within the file      | 4            | 36             |
24//! | ContentsVersionString          | Version string of the contents (fixed-size string, 16 bytes)   | 16           | 40             |
25//! | Flags                          | Various flags used in the file                                 | 4            | 56             |
26//! | MaxPredicate                   | Maximum predicate value. None if -1.                           | 2            | 60             |
27//! | SectionOffset                  | Section offset within the file. None if -1.                    | 2            | 62             |
28//!
29//! ## Paddings
30//! If SectionOffset number is 16, read 64bytes header + an extra 16bytes as padding.
31//!
32//! | Field Name                     | Description                                                    | Size (bytes) | Offset (bytes) |
33//! | ------------------------------ | -------------------------------------------------------------- | ------------ | -------------- |
34//! | Unk40                          | Unknown field (Hex offset: 40)                                 | 2            | 64             |
35//! | Unk42                          | Unknown field (Hex offset: 42)                                 | 2            | 66             |
36//! | Unk44                          | Unknown field (Hex offset: 44)                                 | 4            | 68             |
37//! | Unk48                          | Unknown field (Hex offset: 48)                                 | 4            | 72             |
38//! | Unk4C                          | Unknown field (Hex offset: 4C)                                 | 4            | 76             |
39
40use crate::{
41    bytes::hexdump,
42    errors::{de::Error, readable::ReadableError},
43};
44use winnow::{
45    Parser,
46    binary::{self, Endianness},
47    combinator::{dispatch, empty, fail},
48    error::{ContextError, StrContext, StrContextValue::*},
49    seq,
50    token::{take, take_until},
51};
52
53/// The 64bytes HKX header contains metadata information about the HKX file.
54#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
55#[repr(C)]
56pub struct HkxHeader {
57    /// First magic number (`0x57E0E057`)
58    pub magic0: i32,
59    /// Second magic number (`0x10C0C010`)
60    pub magic1: i32,
61    /// User-defined tag.
62    pub user_tag: i32,
63    /// Version of the file.
64    pub file_version: i32,
65    /// Size of pointers in bytes (4 or 8)
66    pub pointer_size: u8,
67    /// Endianness of the file (0 for big-endian, 1 for little-endian).
68    pub endian: u8,
69    /// Padding option used in the file.
70    pub padding_option: u8,
71    /// Base class.
72    pub base_class: u8,
73    /// Number of sections in the HKX file.
74    ///
75    /// # Examples
76    /// For SkyrimSE, the bytes are arranged in the following order.
77    /// - `__classnames__`
78    /// - `__types__`
79    /// - `__data__`
80    pub section_count: i32,
81    /// Index of the contents section.
82    pub contents_section_index: i32,
83    /// Offset of the contents section.
84    pub contents_section_offset: i32,
85    /// Index of the contents class name section.
86    pub contents_class_name_section_index: i32,
87    /// Offset of the contents class name section.
88    pub contents_class_name_section_offset: i32,
89    /// Version string of the contents.
90    ///
91    /// # Bytes Example
92    /// - SkyrimSE
93    /// ```rust
94    /// assert_eq!(
95    ///   *b"hk_2010.2.0-r1\0",
96    ///   [0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00]
97    /// );
98    /// ```
99    pub contents_version_string: [u8; 15],
100    /// Version string of the contents separator. Always 0xff
101    pub contents_version_string_separator: u8,
102    /// Various flags.
103    pub flags: i32,
104    /// Maximum predicate. None is -1 (== `0xFF 0xFF`)
105    pub max_predicate: i16,
106    /// Section offset. None is -1 (== `0xFF 0xFF`)
107    ///
108    /// If this number is 16, read 64bytes header plus an extra 16bytes as padding.
109    pub section_offset: i16,
110}
111
112impl HkxHeader {
113    /// Return Big-endian or little-endian
114    ///
115    /// # Note
116    /// Endian must be `0(big)` or `1(little)`.
117    /// - If you used the `from_bytes` constructor, it is not a problem because the endian check is already done.
118    pub const fn endian(&self) -> Endianness {
119        match self.endian {
120            0 => Endianness::Big,
121            _ => Endianness::Little,
122        }
123    }
124
125    /// Create a header by parsing 64bytes from bytes.
126    ///
127    /// # Errors
128    /// If invalid header format
129    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
130        let mut input = bytes;
131        let header = Self::parser().parse_next(&mut input).map_err(|err| {
132            let hex = hexdump::to_string(bytes);
133            let hex_pos = hexdump::to_hexdump_pos(input.len());
134            ReadableError::from_context(err, &hex, hex_pos)
135        })?;
136        Ok(header)
137    }
138
139    /// Check valid endian & Parse as hkx root header.
140    pub fn parser<'a>() -> impl Parser<&'a [u8], Self, ContextError> {
141        move |bytes: &mut &[u8]| {
142            let endianness = {
143                let (mut bytes, _) = take(17_usize).parse_peek(*bytes)?;
144                dispatch!(binary::u8; // 18th of bytes
145                    0 => empty.value(Endianness::Big),
146                    1 => empty.value(Endianness::Little),
147                    _ => fail.context(StrContext::Expected(Description("Big-Endian: 0")))
148                            .context(StrContext::Expected(Description("Little-Endian: 1")))
149                )
150                .context(StrContext::Label("Root header endianness"))
151                .parse_next(&mut bytes)?
152            };
153
154            let mut verify_magic0 = binary::i32(endianness)
155                .verify(|magic0| *magic0 == 0x57E0E057)
156                .context(StrContext::Label("magic0"))
157                .context(StrContext::Expected(StringLiteral("0x57E0E057")));
158            let mut verify_magic1 = binary::i32(endianness)
159                .verify(|magic0| *magic0 == 0x10C0C010)
160                .context(StrContext::Label("magic1"))
161                .context(StrContext::Expected(StringLiteral("0x10C0C010")));
162
163            seq! {
164                Self {
165                    magic0: verify_magic0,
166                    magic1: verify_magic1,
167                    user_tag: binary::i32(endianness).context(StrContext::Label("user_tag"))
168                        .context(StrContext::Expected(Description("i32"))),
169                    file_version: binary::i32(endianness).context(StrContext::Label("file_version"))
170                        .context(StrContext::Expected(Description("i32"))),
171                    pointer_size: binary::u8.context(StrContext::Label("pointer_size"))
172                        .context(StrContext::Expected(StringLiteral("4u8")))
173                        .context(StrContext::Expected(StringLiteral("8u8"))),
174                    endian: binary::u8.context(StrContext::Label("endian: u8"))
175                        .context(StrContext::Expected(Description("(0u8: big)")))
176                        .context(StrContext::Expected(Description("(1u8: little)"))),
177                    padding_option: binary::u8.context(StrContext::Label("padding_option"))
178                        .context(StrContext::Expected(Description("u8"))),
179                    base_class: binary::u8.context(StrContext::Label("base_class"))
180                        .context(StrContext::Expected(Description("u8"))),
181                    section_count: binary::i32(endianness).context(StrContext::Label("section_count"))
182                        .context(StrContext::Expected(Description("i32"))),
183                    contents_section_index: binary::i32(endianness).context(StrContext::Label("contents_section_index"))
184                        .context(StrContext::Expected(Description("i32"))),
185                    contents_section_offset: binary::i32(endianness).context(StrContext::Label("contents_section_offset"))
186                        .context(StrContext::Expected(Description("i32"))),
187                    contents_class_name_section_index: binary::i32(endianness).context(StrContext::Label("contents_class_name_section_index"))
188                        .context(StrContext::Expected(Description("i32"))),
189                    contents_class_name_section_offset: binary::i32(endianness).context(StrContext::Label("contents_class_name_section_offset"))
190                        .context(StrContext::Expected(Description("i32"))),
191                    contents_version_string: take(15_usize).try_map(TryFrom::try_from).context(StrContext::Label("contents_version_string"))
192                        .context(StrContext::Expected(Description("[u8; 15]"))),
193                    contents_version_string_separator: 0xff.context(StrContext::Label("contents_version_string_separator"))
194                        .context(StrContext::Expected(StringLiteral("0xFF"))),
195                    flags: binary::i32(endianness).context(StrContext::Label("flags"))
196                        .context(StrContext::Expected(Description("i32"))),
197                    max_predicate: binary::i16(endianness).context(StrContext::Label("max_predicate"))
198                        .context(StrContext::Expected(Description("i16"))),
199                    section_offset: binary::i16(endianness).context(StrContext::Label("section_offset"))
200                        .context(StrContext::Expected(Description("i16"))),
201                }
202            }.context(StrContext::Label("Hkx Root Header"))
203            .parse_next(bytes)
204        }
205    }
206
207    /// Convert to bytes.
208    ///
209    /// # Note
210    /// If `self.endian` is 0, the data is converted to binary data as little endian, otherwise as big endian.
211    pub fn to_bytes(&self) -> [u8; 64] {
212        let mut buffer = [0; 64];
213
214        if self.endian == 0 {
215            buffer[..4].copy_from_slice(&self.magic0.to_be_bytes());
216            buffer[4..8].copy_from_slice(&self.magic1.to_be_bytes());
217            buffer[8..12].copy_from_slice(&self.user_tag.to_be_bytes());
218            buffer[12..16].copy_from_slice(&self.file_version.to_be_bytes());
219            buffer[16] = self.pointer_size;
220            buffer[17] = self.endian;
221            buffer[18] = self.padding_option;
222            buffer[19] = self.base_class;
223            buffer[20..24].copy_from_slice(&self.section_count.to_be_bytes());
224            buffer[24..28].copy_from_slice(&self.contents_section_index.to_be_bytes());
225            buffer[28..32].copy_from_slice(&self.contents_section_offset.to_be_bytes());
226            buffer[32..36].copy_from_slice(&self.contents_class_name_section_index.to_be_bytes());
227            buffer[36..40].copy_from_slice(&self.contents_class_name_section_offset.to_be_bytes());
228            buffer[40..55].copy_from_slice(self.contents_version_string.as_slice());
229            buffer[55] = self.contents_version_string_separator;
230            buffer[56..60].copy_from_slice(&self.flags.to_be_bytes());
231            buffer[60..62].copy_from_slice(&self.max_predicate.to_be_bytes());
232            buffer[62..64].copy_from_slice(&self.section_offset.to_be_bytes());
233        } else {
234            buffer[..4].copy_from_slice(&self.magic0.to_le_bytes());
235            buffer[4..8].copy_from_slice(&self.magic1.to_le_bytes());
236            buffer[8..12].copy_from_slice(&self.user_tag.to_le_bytes());
237            buffer[12..16].copy_from_slice(&self.file_version.to_le_bytes());
238            buffer[16] = self.pointer_size;
239            buffer[17] = self.endian;
240            buffer[18] = self.padding_option;
241            buffer[19] = self.base_class;
242            buffer[20..24].copy_from_slice(&self.section_count.to_le_bytes());
243            buffer[24..28].copy_from_slice(&self.contents_section_index.to_le_bytes());
244            buffer[28..32].copy_from_slice(&self.contents_section_offset.to_le_bytes());
245            buffer[32..36].copy_from_slice(&self.contents_class_name_section_index.to_le_bytes());
246            buffer[36..40].copy_from_slice(&self.contents_class_name_section_offset.to_le_bytes());
247            buffer[40..55].copy_from_slice(self.contents_version_string.as_slice());
248            buffer[55] = self.contents_version_string_separator;
249            buffer[56..60].copy_from_slice(&self.flags.to_le_bytes());
250            buffer[60..62].copy_from_slice(&self.max_predicate.to_le_bytes());
251            buffer[62..64].copy_from_slice(&self.section_offset.to_le_bytes());
252        }
253        buffer
254    }
255
256    /// Get padding size.
257    ///
258    /// # Note
259    /// If `Self.section_offset` is negative, 0 is returned.
260    #[inline]
261    pub const fn padding_size(&self) -> u32 {
262        match self.section_offset {
263            i16::MIN..=0 => 0,
264            pad => pad as u32,
265        }
266    }
267
268    /// Get `contents_version_string` as [`str`]
269    ///
270    /// # Errors
271    /// Returns `Err` if the slice is not UTF-8.
272    ///
273    /// # Expected bytes examples
274    /// - SkyrimSE
275    /// ```rust:no_run
276    /// assert_eq!(
277    ///     b"hk_2010.2.0-r1\0",
278    ///     [0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00].as_slice()
279    /// ); // To "hk_2010.2.0-r1"
280    /// ```
281    pub fn contents_version_string(&self) -> winnow::ModalResult<&str> {
282        let mut bytes = self.contents_version_string.as_slice();
283        take_until(0.., b'\0')
284            .try_map(|bytes| core::str::from_utf8(bytes))
285            .parse_next(&mut bytes)
286    }
287
288    /// Create a new `HkXHeader` instance with default values for Skyrim Special Edition.
289    ///
290    /// # Features
291    /// - file version: 8
292    /// - pointer size: 8 bytes(64bit)
293    /// - endian: 1(little endian)
294    /// - base class: 1
295    /// - section count: 3(`__classnames__`, `__type__`, `__data__`)
296    /// - content section index: 2. In zero-based index, `data` section means the third section.
297    /// - content class name section offset: 0x4B
298    /// - max predicate: -1 (Always `0xff 0xff` in ver. hk2010)
299    /// - section offset: -1 (Always `0xff 0xff` in ver. hk2010)
300    pub const fn new_skyrim_se() -> Self {
301        Self {
302            magic0: i32::from_le_bytes([0x57, 0xE0, 0xE0, 0x57]),
303            magic1: i32::from_le_bytes([0x10, 0xC0, 0xC0, 0x10]),
304            user_tag: 0,
305            file_version: i32::from_le_bytes([0x08, 0x00, 0x00, 0x00]),
306            pointer_size: 8,
307            endian: 1,
308            padding_option: 0,
309            base_class: 1,
310            section_count: i32::from_le_bytes([0x03, 0x00, 0x00, 0x00]),
311            contents_section_index: i32::from_le_bytes([0x02, 0x00, 0x00, 0x00]),
312            contents_section_offset: 0,
313            contents_class_name_section_index: 0,
314            contents_class_name_section_offset: i32::from_le_bytes([0x4B, 0x00, 0x00, 0x00]),
315            contents_version_string: *b"hk_2010.2.0-r1\0",
316            contents_version_string_separator: 0xff,
317            flags: 0,
318            max_predicate: -1,
319            section_offset: -1,
320        }
321    }
322
323    /// Create a new `HkXHeader` instance with default values for Skyrim Legendary Edition.
324    ///
325    /// # Features
326    /// Almost the same as SkyrimSE, only the `pointer_size` is different, 4 instead of 8.
327    /// This means that the `pointer_size` is 32 bits(4 bytes), for a 32-bit application.
328    ///
329    /// - file version: 8
330    /// - pointer size: 4 bytes(32bit)
331    /// - endian: 1(little endian)
332    /// - base class: 1
333    /// - section count: 3(`__classnames__`, `__type__`, `__data__`)
334    /// - content section index: 2. In zero-based index, `data` section means the third section.
335    /// - content class name section offset: 0x4B
336    /// - max predicate: -1 (Always `0xff 0xff` in ver. hk2010)
337    /// - section offset: -1 (Always `0xff 0xff` in ver. hk2010)
338    pub const fn new_skyrim_le() -> Self {
339        let mut le_header = Self::new_skyrim_se();
340        le_header.pointer_size = 4;
341        le_header
342    }
343}
344
345/// Skyrim SpecialEdition(64bit) header binary
346/// ```txt
347/// 0x57, 0xE0, 0xE0, 0x57, // magic0(Always 0x57, 0xE0, 0xE0, 0x57)
348/// 0x10, 0xC0, 0xC0, 0x10, // magic1(Always 0x10, 0xC0, 0xC0, 0x10) 0x00,
349/// 0x00, 0x00, 0x00, // user tag
350/// 0x08, 0x00, 0x00, 0x00, // file version
351/// 0x08, // pointer size
352/// 0x01, // endian(1 is little)
353/// 0x00, // padding option
354/// 0x01, // base class
355/// 0x03, 0x00, 0x00, 0x00, // section count
356/// 0x02, 0x00, 0x00, 0x00, // contents section index
357/// 0x00, 0x00, 0x00, 0x00, // content section offset
358/// 0x00, 0x00, 0x00, 0x00, // contents class name section index
359/// 0x4b, 0x00, 0x00, 0x00, // contents class name section offset
360/// 0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00, // contents version: b"hk_2010.2.0-r1\0\0" =  ([u8;15])
361/// 0xFF, // separator always 0xFF
362/// 0x00, 0x00, 0x00, 0x00, // flags
363/// 0xFF, 0xFF, //  max predicate: -1 as i16. This means is none.
364/// 0xFF, 0xFF, // section offset: -1 as i16. This means is none.
365/// ```
366#[rustfmt::skip]
367pub const SKYRIM_SE_ROW_HEADER: [u8; 64] = [
368    0x57, 0xE0, 0xE0, 0x57, // magic0
369    0x10, 0xC0, 0xC0, 0x10, // magic1
370    0x00, 0x00, 0x00, 0x00, // user tag
371    0x08, 0x00, 0x00, 0x00, // file version
372    0x08, // pointer size
373    0x01, // endian
374    0x00, // padding option
375    0x01, // base class
376    0x03, 0x00, 0x00, 0x00, // section count
377    0x02, 0x00, 0x00, 0x00, // contents section index
378    0x00, 0x00, 0x00, 0x00, // content section offset
379    0x00, 0x00, 0x00, 0x00, // contents class name section index
380    0x4b, 0x00, 0x00, 0x00, // contents class name section offset
381    // contents version: b"hk_2010.2.0-r1\0\0" + separator 0xFF =  ([u8;16])
382    0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00, 0xFF,
383    0x00, 0x00, 0x00, 0x00, // flags
384    0xFF, 0xFF, //  max predicate: -1 as i16. This means is none.
385    0xFF, 0xFF, // section offset: -1 as i16. This means is none.
386];
387
388/// Skyrim LegendaryEdition(32bit) header binary
389/// ```txt
390/// 0x57, 0xE0, 0xE0, 0x57, // magic0(Always 0x57, 0xE0, 0xE0, 0x57)
391/// 0x10, 0xC0, 0xC0, 0x10, // magic1(Always 0x10, 0xC0, 0xC0, 0x10) 0x00,
392/// 0x00, 0x00, 0x00, // user tag
393/// 0x08, 0x00, 0x00, 0x00, // file version
394/// 0x04, // pointer size
395/// 0x01, // endian
396/// 0x00, // padding option
397/// 0x01, // base class
398/// 0x03, 0x00, 0x00, 0x00, // section count
399/// 0x02, 0x00, 0x00, 0x00, // contents section index
400/// 0x00, 0x00, 0x00, 0x00, // content section offset
401/// 0x00, 0x00, 0x00, 0x00, // contents class name section index
402/// 0x4b, 0x00, 0x00, 0x00, // contents class name section offset
403/// 0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00, // contents version: b"hk_2010.2.0-r1\0\0" =  ([u8;15])
404/// 0xFF, // separator always 0xFF
405/// 0x00, 0x00, 0x00, 0x00, // flags
406/// 0xFF, 0xFF, //  max predicate: -1 as i16. This means is none.
407/// 0xFF, 0xFF, // section offset: -1 as i16. This means is none.
408/// ```
409#[rustfmt::skip]
410pub const SKYRIM_LE_ROW_HEADER: [u8; 64] = [
411    0x57, 0xE0, 0xE0, 0x57, // magic0(Always 0x57, 0xE0, 0xE0, 0x57)
412    0x10, 0xC0, 0xC0, 0x10, // magic1(Always 0x10, 0xC0, 0xC0, 0x10)
413    0x00, 0x00, 0x00, 0x00, // user tag
414    0x08, 0x00, 0x00, 0x00, // file version
415    0x04, // pointer size
416    0x01, // endian
417    0x00, // padding option
418    0x01, // base class
419    0x03, 0x00, 0x00, 0x00, // section count
420    0x02, 0x00, 0x00, 0x00, // contents section index
421    0x00, 0x00, 0x00, 0x00, // content section offset
422    0x00, 0x00, 0x00, 0x00, // contents class name section index
423    0x4b, 0x00, 0x00, 0x00, // contents class name section offset
424    // contents version string: b"hk_2010.2.0-r1\0\0" =  ([u8;15])
425    0x68, 0x6B, 0x5F, 0x32, 0x30, 0x31, 0x30, 0x2E, 0x32, 0x2E, 0x30, 0x2D, 0x72, 0x31, 0x00,
426    0xFF, //  separator (always 0xFF)
427    0x00, 0x00, 0x00, 0x00, // flags
428    0xFF, 0xFF, //  max predicate: -1 as i16. This means is none.
429    0xFF, 0xFF, // section offset: -1 as i16. This means is none.
430];
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use pretty_assertions::assert_eq;
436
437    #[test]
438    fn should_parse_endian_bytes() {
439        assert_eq!(SKYRIM_SE_ROW_HEADER[16], 0x08); // pointer size
440        assert_eq!(SKYRIM_SE_ROW_HEADER[17], 0x01); // endian
441        assert_eq!(HkxHeader::new_skyrim_se().endian(), Endianness::Little);
442    }
443
444    #[test]
445    fn should_read_hkx_bytes() {
446        let header = HkxHeader::parser().parse(&SKYRIM_SE_ROW_HEADER).unwrap();
447
448        assert_eq!(header, HkxHeader::new_skyrim_se());
449        assert_eq!(header.padding_size(), 0); // SkyrimSE, no padding.
450        assert_eq!(header.contents_version_string(), Ok("hk_2010.2.0-r1"));
451    }
452
453    #[test]
454    fn should_write_hkx_bytes() {
455        assert_eq!(HkxHeader::new_skyrim_se().to_bytes(), SKYRIM_SE_ROW_HEADER);
456    }
457}