serde_hkx/bytes/serde/
section_header.rs

1//! The 48bytes each HKX section header contains metadata information about the HKX file.
2//!
3//! This information is placed immediately after the Hkx header. (In some cases, padding is inserted in between.)
4use crate::{lib::*, tri};
5
6use byteorder::{ByteOrder, WriteBytesExt};
7use std::io;
8use winnow::{
9    Parser,
10    binary::{self, Endianness},
11    error::{ContextError, StrContext, StrContextValue::*},
12    seq,
13    token::take,
14};
15
16/// The 48bytes each HKX section header contains metadata information about the HKX file.
17///
18/// For SkyrimSE, the bytes are arranged in the following order.
19/// - `__classnames__` 48bytes
20/// - `__types__` 48bytes
21/// - `__data__` 48bytes
22///
23/// # Note
24/// This information is placed immediately after the Hkx header. (In some cases, padding is inserted in between.)
25///
26/// Depending on the havok version, there may be padding after the section header group.
27/// (at least not in SkyrimSE).
28#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
29#[repr(C)]
30pub struct SectionHeader {
31    /// Section name.
32    ///
33    /// For SkyrimSE, the bytes are arranged in the following order.
34    /// - `__classnames__`
35    /// - `__types__`
36    /// - `__data__`
37    ///
38    /// # Bytes Example
39    /// ```
40    /// assert_eq!(
41    ///   [0x5F, 0x5F, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x6E, 0x61, 0x6D, 0x65, 0x73, 0x5F, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00],
42    ///   *b"__classnames__\0\0\0\0\0"
43    /// );
44    /// ```
45    pub section_tag: [u8; 19],
46    /// Always must be `0xFF`
47    pub section_tag_separator: u8,
48    /// Section start & fixup base offset.
49    ///
50    /// # Example of position
51    /// `hkx_header.section_count:3` & `hkx_header.section_offset:0` => `0x000000D0` bytes.
52    ///
53    /// - Calculation formula
54    ///
55    ///   Hkx header 64bytes + 48bytes * 3 sections = 208bytes == `0xD0`(`__classnames__` section abs)
56    pub absolute_data_start: u32,
57    /// Offset from absolute offset to local fixup map.
58    pub local_fixups_offset: u32,
59    /// Offset from absolute offset to global fixup map.
60    pub global_fixups_offset: u32,
61    /// Offset from absolute offset to virtual class fixup map.
62    pub virtual_fixups_offset: u32,
63
64    /// Unknown offset information.
65    ///
66    /// Known information.
67    /// - This value is the end position of the virtual_fixups_offset.
68    /// - The `exports`, `imports` and `end` offsets are all the same value.
69    pub exports_offset: u32,
70    /// Unknown offset information.
71    ///
72    /// Known information.
73    /// - This value is the end position of the virtual_fixups_offset.
74    /// - The `exports`, `imports` and `end` offsets are all the same value.
75    pub imports_offset: u32,
76    /// Unknown offset information.
77    ///
78    /// Known information.
79    /// - This value is the end position of the virtual_fixups_offset.
80    /// - The `exports`, `imports` and `end` offsets are all the same value.
81    pub end_offset: u32,
82}
83static_assertions::assert_eq_size!(SectionHeader, [u8; 48]); // Must be 48bytes.
84
85impl SectionHeader {
86    /// `*b"__data__\0\0\0\0\0\0\0\0\0\0\0"`
87    pub const DATA_SECTION_HEADER_TAG: [u8; 19] = *b"__data__\0\0\0\0\0\0\0\0\0\0\0";
88
89    pub fn from_bytes<'a>(endian: Endianness) -> impl Parser<&'a [u8], Self, ContextError> {
90        move |bytes: &mut &[u8]| {
91            {
92                seq! {
93                    Self {
94                        section_tag: take(19_usize).try_map(TryFrom::try_from).context(StrContext::Label("section_tag"))
95                            .context(StrContext::Expected(StringLiteral("[u8; 19]"))),
96                        section_tag_separator: 0xff.context(StrContext::Label("section_tag_separator"))
97                            .context(StrContext::Expected(StringLiteral("0xFF"))),
98                        absolute_data_start: binary::u32(endian).context(StrContext::Label("absolute_data_start"))
99                            .context(StrContext::Expected(StringLiteral("u32"))),
100                        local_fixups_offset: binary::u32(endian).context(StrContext::Label("local_fixups_offset"))
101                            .context(StrContext::Expected(StringLiteral("u32"))),
102                        global_fixups_offset: binary::u32(endian).context(StrContext::Label("global_fixups_offset"))
103                            .context(StrContext::Expected(StringLiteral("u32"))),
104                        virtual_fixups_offset: binary::u32(endian).context(StrContext::Label("virtual_fixups_offset"))
105                            .context(StrContext::Expected(StringLiteral("u32"))),
106                        exports_offset: binary::u32(endian).context(StrContext::Label("exports_offset"))
107                            .context(StrContext::Expected(StringLiteral("u32"))),
108                        imports_offset: binary::u32(endian).context(StrContext::Label("imports_offset"))
109                            .context(StrContext::Expected(StringLiteral("u32"))),
110                        end_offset: binary::u32(endian).context(StrContext::Label("end_offset"))
111                            .context(StrContext::Expected(StringLiteral("u32"))),
112                    }
113                }
114            }
115            .context(StrContext::Label("Hkx Section Header"))
116            .parse_next(bytes)
117        }
118    }
119
120    /// Write section header to writer.
121    ///
122    /// # Errors
123    /// This method returns the same errors as [`Write::write_all`].
124    ///
125    /// [`Write::write_all`]: https://doc.rust-lang.org/std/io/trait.Write.html#method.write_all
126    pub fn write_bytes<O>(&self, mut writer: impl WriteBytesExt) -> io::Result<()>
127    where
128        O: ByteOrder,
129    {
130        writer.write_all(&self.section_tag)?;
131        writer.write_u8(self.section_tag_separator)?;
132        writer.write_u32::<O>(self.absolute_data_start)?;
133        writer.write_u32::<O>(self.local_fixups_offset)?;
134        writer.write_u32::<O>(self.global_fixups_offset)?;
135        writer.write_u32::<O>(self.virtual_fixups_offset)?;
136        writer.write_u32::<O>(self.exports_offset)?;
137        writer.write_u32::<O>(self.imports_offset)?;
138        writer.write_u32::<O>(self.end_offset)?;
139        Ok(())
140    }
141
142    /// Create new `__classnames__` section header
143    ///
144    /// - `section_offset`: usually 0xff(ver. hk2010), this case padding is none.
145    ///
146    /// # Errors
147    ///
148    /// This function will return the first error of
149    /// non-[`ErrorKind::Interrupted`] kind that [`write`] returns.
150    ///
151    /// [`write`]: Write::write
152    pub fn write_classnames<O>(
153        mut writer: impl WriteBytesExt,
154        section_offset: i16,
155        section_end_abs: u32,
156    ) -> io::Result<()>
157    where
158        O: ByteOrder,
159    {
160        writer.write_all(b"__classnames__\0\0\0\0\0\xff")?; // with separator(0xff)
161        let section_offset = match section_offset {
162            i16::MIN..=0_i16 => 0,
163            1.. => section_offset as u32,
164        };
165        const ABSOLUTE_CLASSNAMES_OFFSET: u32 = 0xd0;
166        writer.write_u32::<O>(ABSOLUTE_CLASSNAMES_OFFSET + section_offset)?; // write absolute_data_start
167
168        let fixups_offset = section_end_abs - 0xd0;
169        writer.write_u32::<O>(fixups_offset)?; // local_fixups_offset
170        writer.write_u32::<O>(fixups_offset)?; // global_fixups_offset
171        writer.write_u32::<O>(fixups_offset)?; // virtual_fixups_offset
172        writer.write_u32::<O>(fixups_offset)?; // exports_offset
173        writer.write_u32::<O>(fixups_offset)?; // imports_offset
174        writer.write_u32::<O>(fixups_offset) // end_offset
175    }
176
177    /// Write `__types__` section header.
178    ///
179    /// # Errors
180    ///
181    /// This function will return the first error of
182    /// non-[`ErrorKind::Interrupted`] kind that [`write`] returns.
183    ///
184    /// [`write`]: Write::write
185    pub fn write_types<O>(mut writer: impl WriteBytesExt, abs_offset: u32) -> io::Result<()>
186    where
187        O: ByteOrder,
188    {
189        writer.write_all(b"__types__\0\0\0\0\0\0\0\0\0\0\xff")?; // with separator(0xff)
190        writer.write_u32::<O>(abs_offset)?; // same as `__data__` section's `absolute_data_offset`
191        tri!(writer.write_all([0_u8; 24].as_slice())); // Fixup does not exist in `types` section, always 0.
192        Ok(())
193    }
194}
195
196// To improve visualization of hex dump.
197impl Display for SectionHeader {
198    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199        let Self {
200            section_tag,
201            section_tag_separator,
202            absolute_data_start,
203            local_fixups_offset,
204            global_fixups_offset,
205            virtual_fixups_offset,
206            exports_offset,
207            imports_offset,
208            end_offset,
209        } = *self;
210
211        let section_tag = core::str::from_utf8(&section_tag)
212            .unwrap_or_default()
213            .trim_matches(char::from(0));
214        let l_offset = absolute_data_start + local_fixups_offset;
215        let g_offset = absolute_data_start + global_fixups_offset;
216        let v_offset = absolute_data_start + virtual_fixups_offset;
217        let e_offset = absolute_data_start + exports_offset;
218        let i_offset = absolute_data_start + imports_offset;
219        let end_off = absolute_data_start + end_offset;
220
221        write!(
222            f,
223            r#"
224          section tag: {section_tag}
225section tag separator: {section_tag_separator:#02x}
226
227Offsets:
228  absolute data start: {absolute_data_start:#02x}
229         local fixups: {local_fixups_offset:#02x}
230        global fixups: {global_fixups_offset:#02x}
231       virtual fixups: {virtual_fixups_offset:#02x}
232              exports: {exports_offset:#02x}
233              imports; {imports_offset:#02x}
234                  end: {end_offset:#02x}
235        abs +   local: {l_offset:#02x}
236        abs +  global: {g_offset:#02x}
237        abs + virtual: {v_offset:#02x}
238        abs + exports: {e_offset:#02x}
239        abs + imports: {i_offset:#02x}
240        abs +     end: {end_off:#02x}
241"#
242        )
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use byteorder::LittleEndian;
250    use std::io::Cursor;
251
252    #[test]
253    fn test_write_classnames() -> io::Result<()> {
254        let mut buffer = Cursor::new(Vec::new());
255        SectionHeader::write_classnames::<LittleEndian>(&mut buffer, 0, 0x160)?;
256        let written = buffer.into_inner();
257
258        #[rustfmt::skip]
259        const CLASSNAMES_SECTION_HEADER: [u8; 48] = [
260            // __classnames__\0\0\0\0\0: [u8; 19]
261            0x5F, 0x5F, 0x63, 0x6C, 0x61, 0x73, 0x73, 0x6E, 0x61, 0x6D, 0x65, 0x73, 0x5F, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00,
262            0xff, // separator
263            0xd0, 0x00, 0x00, 0x00, // absolute_data_start
264            0x90, 0x00, 0x00, 0x00, // local_fixups_offset
265            0x90, 0x00, 0x00, 0x00, // global_fixups_offset
266            0x90, 0x00, 0x00, 0x00, // virtual_fixups_offset
267            0x90, 0x00, 0x00, 0x00, // exports_offset
268            0x90, 0x00, 0x00, 0x00, // imports_offset
269            0x90, 0x00, 0x00, 0x00, // end_offset
270        ];
271        assert_eq!(&written, CLASSNAMES_SECTION_HEADER.as_slice());
272        Ok(())
273    }
274
275    #[test]
276    fn test_write_types() -> io::Result<()> {
277        let mut buffer = Cursor::new(Vec::new());
278        SectionHeader::write_types::<LittleEndian>(&mut buffer, 0x160)?;
279        let written = buffer.into_inner();
280
281        #[rustfmt::skip]
282        const TYPES_SECTION_HEADER: [u8; 48] = [
283            // __types__\0\0\0\0\0\0\0\0\0\0: [u8; 19]
284            0x5f, 0x5f,  0x74, 0x79, 0x70, 0x65, 0x73, 0x5f, 0x5f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
285            0xff, // separator
286            // # Fixups
287            // These are pending because the location cannot be determined without actually writing the data.
288            0x60, 0x01, 0x00, 0x00, // absolute_data_start(0x160)
289            0x00, 0x00, 0x00, 0x00, // local_fixups_offset
290            0x00, 0x00, 0x00, 0x00, // global_fixups_offset
291            0x00, 0x00, 0x00, 0x00, // virtual_fixups_offset
292            0x00, 0x00, 0x00, 0x00, // exports_offset
293            0x00, 0x00, 0x00, 0x00, // imports_offset
294            0x00, 0x00, 0x00, 0x00, // end_offset
295        ];
296        assert_eq!(&written, TYPES_SECTION_HEADER.as_slice());
297        Ok(())
298    }
299
300    #[test]
301    fn test_write_data() -> io::Result<()> {
302        let mut buffer = Cursor::new(Vec::new());
303        SectionHeader {
304            section_tag: SectionHeader::DATA_SECTION_HEADER_TAG,
305            section_tag_separator: 0xff,
306            absolute_data_start: 0x160,
307            local_fixups_offset: 0x170,
308            global_fixups_offset: 0x1C0,
309            virtual_fixups_offset: 0x1E0,
310            exports_offset: 0x210,
311            imports_offset: 0x210,
312            end_offset: 0x210,
313        }
314        .write_bytes::<LittleEndian>(&mut buffer)?;
315        let written = buffer.into_inner();
316
317        #[rustfmt::skip]
318        const DATA_SECTION_HEADER: [u8; 48] = [
319            // __data__\0\0\0\0\0\0\0\0\0\0\0: [u8; 19]
320            0x5f, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x5f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
321            0xff, // separator
322            // # Fixups
323            // These are pending because the location cannot be determined without actually writing the data.
324            0x60, 0x01, 0x00, 0x00, // absolute_data_start
325            0x70, 0x01, 0x00, 0x00, // local_fixups_offset
326            0xC0, 0x01, 0x00, 0x00, // global_fixups_offset
327            0xE0, 0x01, 0x00, 0x00, // virtual_fixups_offset
328            0x10, 0x02, 0x00, 0x00, // exports_offset
329            0x10, 0x02, 0x00, 0x00, // imports_offset
330            0x10, 0x02, 0x00, 0x00, // end_offset
331        ];
332        assert_eq!(&written, DATA_SECTION_HEADER.as_slice());
333        Ok(())
334    }
335
336    #[test]
337    fn test_ref_from_bytes_invalid_length() {
338        let written = vec![0; 40];
339        let result =
340            SectionHeader::from_bytes(Endianness::Little).parse_next(&mut written.as_slice());
341        assert!(result.is_err());
342    }
343
344    #[test]
345    fn test_ref_from_bytes_invalid_separator() {
346        let mut written = vec![0; 48];
347        written[19] = 0x00; // Invalid separator
348        let result =
349            SectionHeader::from_bytes(Endianness::Little).parse_next(&mut written.as_slice());
350        assert!(result.is_err());
351    }
352}