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}