rakata_formats/erf/
writer.rs

1//! ERF/MOD/HAK binary writer.
2
3use std::io::{Cursor, Write};
4
5use rakata_core::encode_text;
6
7use super::{
8    binary::{write_u16, write_u32},
9    Erf, ErfBinaryError, ErfFileType, ErfWriteMode, ErfWriteOptions, ModLayout, ERF_TEXT_ENCODING,
10    ERF_VERSION_V10, FILE_HEADER_SIZE, KEY_ENTRY_SIZE, MOD_BLANK_BLOCK_ENTRY_SIZE,
11    RESOURCE_ENTRY_SIZE,
12};
13
14/// Writes an ERF/MOD/HAK archive to a writer.
15#[cfg_attr(
16    feature = "tracing",
17    tracing::instrument(level = "debug", skip(writer, erf))
18)]
19pub fn write_erf<W: Write>(writer: &mut W, erf: &Erf) -> Result<(), ErfBinaryError> {
20    write_erf_with_options(writer, erf, ErfWriteOptions::default())
21}
22
23/// Writes an ERF/MOD/HAK archive to a writer with explicit layout options.
24#[cfg_attr(
25    feature = "tracing",
26    tracing::instrument(level = "debug", skip(writer, erf), fields(file_type = ?erf.file_type, mod_layout = ?options.mod_layout))
27)]
28pub fn write_erf_with_options<W: Write>(
29    writer: &mut W,
30    erf: &Erf,
31    options: ErfWriteOptions,
32) -> Result<(), ErfBinaryError> {
33    let entry_count = u32::try_from(erf.resources.len())
34        .map_err(|_| ErfBinaryError::ValueOverflow("entry_count"))?;
35
36    let mut localized_bytes = Vec::new();
37    for (index, entry) in erf.localized_strings.iter().enumerate() {
38        push_u32(&mut localized_bytes, entry.language_id.raw());
39        let encoded = encode_text(&entry.text, ERF_TEXT_ENCODING).map_err(|source| {
40            ErfBinaryError::TextEncoding {
41                context: format!("localized_strings[{index}]"),
42                source,
43            }
44        })?;
45        let encoded_len = u32::try_from(encoded.len())
46            .map_err(|_| ErfBinaryError::ValueOverflow("localized_string_len"))?;
47        push_u32(&mut localized_bytes, encoded_len);
48        localized_bytes.extend_from_slice(&encoded);
49    }
50    let language_count = u32::try_from(erf.localized_strings.len())
51        .map_err(|_| ErfBinaryError::ValueOverflow("language_count"))?;
52    let localized_size = u32::try_from(localized_bytes.len())
53        .map_err(|_| ErfBinaryError::ValueOverflow("localized_string_size"))?;
54
55    let localized_offset = u32::try_from(FILE_HEADER_SIZE)
56        .map_err(|_| ErfBinaryError::ValueOverflow("localized_offset"))?;
57    let keys_offset = localized_offset
58        .checked_add(localized_size)
59        .ok_or(ErfBinaryError::ValueOverflow("keys_offset"))?;
60    let mod_blank_block_size = if erf.file_type == ErfFileType::Mod
61        && options.mod_layout == ModLayout::WithBlankBlock
62    {
63        entry_count
64            .checked_mul(u32::try_from(MOD_BLANK_BLOCK_ENTRY_SIZE).expect("constant fits in u32"))
65            .ok_or(ErfBinaryError::ValueOverflow("mod_blank_block_size"))?
66    } else {
67        0
68    };
69    let resources_offset = keys_offset
70        .checked_add(
71            entry_count
72                .checked_mul(u32::try_from(KEY_ENTRY_SIZE).expect("constant fits in u32"))
73                .ok_or(ErfBinaryError::ValueOverflow("resources_offset"))?,
74        )
75        .and_then(|offset| offset.checked_add(mod_blank_block_size))
76        .ok_or(ErfBinaryError::ValueOverflow("resources_offset"))?;
77
78    let serialized_file_type = match options.output {
79        ErfWriteMode::CanonicalK1 if erf.file_type == ErfFileType::Sav => ErfFileType::Mod,
80        _ => erf.file_type,
81    };
82
83    writer.write_all(&serialized_file_type.fourcc())?;
84    writer.write_all(&ERF_VERSION_V10)?;
85    write_u32(writer, language_count)?;
86    write_u32(writer, localized_size)?;
87    write_u32(writer, entry_count)?;
88    write_u32(writer, localized_offset)?;
89    write_u32(writer, keys_offset)?;
90    write_u32(writer, resources_offset)?;
91    write_u32(writer, erf.build_year)?;
92    write_u32(writer, erf.build_day)?;
93    write_u32(
94        writer,
95        u32::from_le_bytes(erf.description_strref.raw().to_le_bytes()),
96    )?;
97    writer.write_all(&erf.reserved)?;
98
99    writer.write_all(&localized_bytes)?;
100
101    for (index, resource) in erf.resources.iter().enumerate() {
102        // ResRef guarantees ASCII-only content and length <= 16, so we can
103        // write the bytes directly without encoding or validation.
104        let resref_bytes = resource.resref.as_str().as_bytes();
105        let mut key_resref = [0_u8; 16];
106        key_resref[..resref_bytes.len()].copy_from_slice(resref_bytes);
107        writer.write_all(&key_resref)?;
108        write_u32(
109            writer,
110            u32::try_from(index).map_err(|_| ErfBinaryError::ValueOverflow("resource_id"))?,
111        )?;
112        write_u16(writer, resource.resource_type.raw_id())?;
113        write_u16(writer, 0)?;
114    }
115
116    if mod_blank_block_size > 0 {
117        writer.write_all(&vec![
118            0_u8;
119            usize::try_from(mod_blank_block_size).map_err(
120                |_| { ErfBinaryError::ValueOverflow("mod_blank_block_size") }
121            )?
122        ])?;
123    }
124
125    let mut next_data_offset = resources_offset
126        .checked_add(
127            entry_count
128                .checked_mul(u32::try_from(RESOURCE_ENTRY_SIZE).expect("constant fits in u32"))
129                .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?,
130        )
131        .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?;
132    for resource in &erf.resources {
133        write_u32(writer, next_data_offset)?;
134        let data_len = u32::try_from(resource.data.len())
135            .map_err(|_| ErfBinaryError::ValueOverflow("resource_size"))?;
136        write_u32(writer, data_len)?;
137        next_data_offset = next_data_offset
138            .checked_add(data_len)
139            .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?;
140    }
141
142    for resource in &erf.resources {
143        writer.write_all(&resource.data)?;
144    }
145    crate::trace_debug!(
146        file_type = ?erf.file_type,
147        localized_string_count = erf.localized_strings.len(),
148        resource_count = erf.resources.len(),
149        "wrote erf-family archive to writer"
150    );
151    Ok(())
152}
153
154/// Writes a save archive to a writer.
155///
156/// Output is canonicalized to the KotOR `MOD ` header signature.
157#[cfg_attr(
158    feature = "tracing",
159    tracing::instrument(level = "debug", skip(writer, erf))
160)]
161pub fn write_save_archive<W: Write>(writer: &mut W, erf: &Erf) -> Result<(), ErfBinaryError> {
162    let mut archive = erf.clone();
163    archive.file_type = ErfFileType::Mod;
164    write_erf(writer, &archive)
165}
166
167/// Serializes an ERF/MOD/HAK archive to bytes.
168#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(erf)))]
169pub fn write_erf_to_vec(erf: &Erf) -> Result<Vec<u8>, ErfBinaryError> {
170    write_erf_to_vec_with_options(erf, ErfWriteOptions::default())
171}
172
173/// Serializes an ERF/MOD/HAK archive to bytes with explicit layout options.
174#[cfg_attr(
175    feature = "tracing",
176    tracing::instrument(level = "debug", skip(erf), fields(file_type = ?erf.file_type, mod_layout = ?options.mod_layout))
177)]
178pub fn write_erf_to_vec_with_options(
179    erf: &Erf,
180    options: ErfWriteOptions,
181) -> Result<Vec<u8>, ErfBinaryError> {
182    let mut cursor = Cursor::new(Vec::new());
183    write_erf_with_options(&mut cursor, erf, options)?;
184    let bytes = cursor.into_inner();
185    crate::trace_debug!(
186        bytes_len = bytes.len(),
187        "serialized erf-family archive to vec"
188    );
189    Ok(bytes)
190}
191
192/// Serializes a save archive to bytes.
193///
194/// Output is canonicalized to the KotOR `MOD ` header signature.
195#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(erf)))]
196pub fn write_save_archive_to_vec(erf: &Erf) -> Result<Vec<u8>, ErfBinaryError> {
197    let mut cursor = Cursor::new(Vec::new());
198    write_save_archive(&mut cursor, erf)?;
199    let bytes = cursor.into_inner();
200    crate::trace_debug!(bytes_len = bytes.len(), "serialized save archive to vec");
201    Ok(bytes)
202}
203
204fn push_u32(bytes: &mut Vec<u8>, value: u32) {
205    bytes.extend_from_slice(&value.to_le_bytes());
206}