Skip to main content

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///
16/// # Errors
17///
18/// The same as [`write_erf_with_options`] under the default options, which
19/// write `MOD ` archives tightly packed.
20#[cfg_attr(
21    feature = "tracing",
22    tracing::instrument(level = "debug", skip(writer, erf))
23)]
24pub fn write_erf<W: Write>(writer: &mut W, erf: &Erf) -> Result<(), ErfBinaryError> {
25    write_erf_with_options(writer, erf, ErfWriteOptions::default())
26}
27
28/// Writes an ERF/MOD/HAK archive to a writer with explicit layout options.
29///
30/// # Errors
31///
32/// [`ErfBinaryError::ValueOverflow`] is the arm nearly everything lands on,
33/// naming the field: the entry count, the language count, any table offset or
34/// size, and the running data offset all live in `u32` on disk, so an archive
35/// large enough to push one past that cannot be written. The offsets are
36/// computed before any byte goes out, so an overflow there leaves the writer
37/// untouched.
38///
39/// [`ErfBinaryError::TextEncoding`] when a localized string has no
40/// Windows-1252 form, naming its index, and [`ErfBinaryError::Io`] when the
41/// writer fails. The I/O arm can leave a partial archive.
42#[cfg_attr(
43    feature = "tracing",
44    tracing::instrument(level = "debug", skip(writer, erf), fields(file_type = ?erf.file_type, mod_layout = ?options.mod_layout))
45)]
46pub fn write_erf_with_options<W: Write>(
47    writer: &mut W,
48    erf: &Erf,
49    options: ErfWriteOptions,
50) -> Result<(), ErfBinaryError> {
51    let entry_count = u32::try_from(erf.resources.len())
52        .map_err(|_| ErfBinaryError::ValueOverflow("entry_count"))?;
53
54    let mut localized_bytes = Vec::new();
55    for (index, entry) in erf.localized_strings.iter().enumerate() {
56        push_u32(&mut localized_bytes, entry.language_id.raw());
57        let encoded = encode_text(&entry.text, ERF_TEXT_ENCODING).map_err(|source| {
58            ErfBinaryError::TextEncoding {
59                context: format!("localized_strings[{index}]"),
60                source,
61            }
62        })?;
63        let encoded_len = u32::try_from(encoded.len())
64            .map_err(|_| ErfBinaryError::ValueOverflow("localized_string_len"))?;
65        push_u32(&mut localized_bytes, encoded_len);
66        localized_bytes.extend_from_slice(&encoded);
67    }
68    let language_count = u32::try_from(erf.localized_strings.len())
69        .map_err(|_| ErfBinaryError::ValueOverflow("language_count"))?;
70    let localized_size = u32::try_from(localized_bytes.len())
71        .map_err(|_| ErfBinaryError::ValueOverflow("localized_string_size"))?;
72
73    let localized_offset = u32::try_from(FILE_HEADER_SIZE)
74        .map_err(|_| ErfBinaryError::ValueOverflow("localized_offset"))?;
75    let keys_offset = localized_offset
76        .checked_add(localized_size)
77        .ok_or(ErfBinaryError::ValueOverflow("keys_offset"))?;
78    let mod_blank_block_size = if erf.file_type == ErfFileType::Mod
79        && options.mod_layout == ModLayout::WithBlankBlock
80    {
81        entry_count
82            .checked_mul(u32::try_from(MOD_BLANK_BLOCK_ENTRY_SIZE).expect("constant fits in u32"))
83            .ok_or(ErfBinaryError::ValueOverflow("mod_blank_block_size"))?
84    } else {
85        0
86    };
87    let resources_offset = keys_offset
88        .checked_add(
89            entry_count
90                .checked_mul(u32::try_from(KEY_ENTRY_SIZE).expect("constant fits in u32"))
91                .ok_or(ErfBinaryError::ValueOverflow("resources_offset"))?,
92        )
93        .and_then(|offset| offset.checked_add(mod_blank_block_size))
94        .ok_or(ErfBinaryError::ValueOverflow("resources_offset"))?;
95
96    let serialized_file_type = match options.output {
97        ErfWriteMode::CanonicalK1 if erf.file_type == ErfFileType::Sav => ErfFileType::Mod,
98        _ => erf.file_type,
99    };
100
101    writer.write_all(&serialized_file_type.fourcc())?;
102    writer.write_all(&ERF_VERSION_V10)?;
103    write_u32(writer, language_count)?;
104    write_u32(writer, localized_size)?;
105    write_u32(writer, entry_count)?;
106    write_u32(writer, localized_offset)?;
107    write_u32(writer, keys_offset)?;
108    write_u32(writer, resources_offset)?;
109    write_u32(writer, erf.build_year)?;
110    write_u32(writer, erf.build_day)?;
111    write_u32(
112        writer,
113        u32::from_le_bytes(erf.description_strref.raw().to_le_bytes()),
114    )?;
115    writer.write_all(&erf.reserved)?;
116
117    writer.write_all(&localized_bytes)?;
118
119    for (index, resource) in erf.resources.iter().enumerate() {
120        // The key the file spelled, where this archive came from one. A
121        // `ResRef` folds case, so deriving the key renames every entry of a
122        // rewritten archive: the engine spells most keys uppercase and its
123        // extractor takes an extracted file's name straight off this field.
124        // An entry built rather than read has nothing to put back and gets the
125        // derived form, which is what this writer would produce anyway.
126        let key = resource
127            .name_as_read
128            .unwrap_or_else(|| resource.resref.to_padded());
129        writer.write_all(&key)?;
130        write_u32(
131            writer,
132            u32::try_from(index).map_err(|_| ErfBinaryError::ValueOverflow("resource_id"))?,
133        )?;
134        write_u16(writer, resource.resource_type.raw_id())?;
135        write_u16(writer, 0)?;
136    }
137
138    if mod_blank_block_size > 0 {
139        writer.write_all(&vec![
140            0_u8;
141            usize::try_from(mod_blank_block_size).map_err(
142                |_| { ErfBinaryError::ValueOverflow("mod_blank_block_size") }
143            )?
144        ])?;
145    }
146
147    let mut next_data_offset = resources_offset
148        .checked_add(
149            entry_count
150                .checked_mul(u32::try_from(RESOURCE_ENTRY_SIZE).expect("constant fits in u32"))
151                .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?,
152        )
153        .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?;
154    for resource in &erf.resources {
155        write_u32(writer, next_data_offset)?;
156        let data_len = u32::try_from(resource.data.len())
157            .map_err(|_| ErfBinaryError::ValueOverflow("resource_size"))?;
158        write_u32(writer, data_len)?;
159        next_data_offset = next_data_offset
160            .checked_add(data_len)
161            .ok_or(ErfBinaryError::ValueOverflow("data_offset"))?;
162    }
163
164    for resource in &erf.resources {
165        writer.write_all(&resource.data)?;
166    }
167    crate::trace_debug!(
168        file_type = ?erf.file_type,
169        localized_string_count = erf.localized_strings.len(),
170        resource_count = erf.resources.len(),
171        "wrote erf-family archive to writer"
172    );
173    Ok(())
174}
175
176/// Writes a save archive to a writer.
177///
178/// Output is canonicalized to the KotOR `MOD ` header signature.
179///
180/// # Errors
181///
182/// The same as [`write_erf`]. The signature is rewritten rather than checked,
183/// so an `Erf` carrying any file type writes as a save.
184#[cfg_attr(
185    feature = "tracing",
186    tracing::instrument(level = "debug", skip(writer, erf))
187)]
188pub fn write_save_archive<W: Write>(writer: &mut W, erf: &Erf) -> Result<(), ErfBinaryError> {
189    let mut archive = erf.clone();
190    archive.file_type = ErfFileType::Mod;
191    write_erf(writer, &archive)
192}
193
194/// Serializes an ERF/MOD/HAK archive to bytes.
195///
196/// # Errors
197///
198/// The same as [`write_erf_to_vec_with_options`] under the default options.
199#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(erf)))]
200pub fn write_erf_to_vec(erf: &Erf) -> Result<Vec<u8>, ErfBinaryError> {
201    write_erf_to_vec_with_options(erf, ErfWriteOptions::default())
202}
203
204/// Serializes an ERF/MOD/HAK archive to bytes with explicit layout options.
205///
206/// # Errors
207///
208/// Every non-I/O failure [`write_erf_with_options`] describes. The `Vec`
209/// target has no I/O to fail at.
210#[cfg_attr(
211    feature = "tracing",
212    tracing::instrument(level = "debug", skip(erf), fields(file_type = ?erf.file_type, mod_layout = ?options.mod_layout))
213)]
214pub fn write_erf_to_vec_with_options(
215    erf: &Erf,
216    options: ErfWriteOptions,
217) -> Result<Vec<u8>, ErfBinaryError> {
218    let mut cursor = Cursor::new(Vec::new());
219    write_erf_with_options(&mut cursor, erf, options)?;
220    let bytes = cursor.into_inner();
221    crate::trace_debug!(
222        bytes_len = bytes.len(),
223        "serialized erf-family archive to vec"
224    );
225    Ok(bytes)
226}
227
228/// Serializes a save archive to bytes.
229///
230/// Output is canonicalized to the KotOR `MOD ` header signature.
231///
232/// # Errors
233///
234/// Every non-I/O failure [`write_save_archive`] describes. The `Vec` target
235/// has no I/O to fail at.
236#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(erf)))]
237pub fn write_save_archive_to_vec(erf: &Erf) -> Result<Vec<u8>, ErfBinaryError> {
238    let mut cursor = Cursor::new(Vec::new());
239    write_save_archive(&mut cursor, erf)?;
240    let bytes = cursor.into_inner();
241    crate::trace_debug!(bytes_len = bytes.len(), "serialized save archive to vec");
242    Ok(bytes)
243}
244
245fn push_u32(bytes: &mut Vec<u8>, value: u32) {
246    bytes.extend_from_slice(&value.to_le_bytes());
247}