Skip to main content

rakata_formats/erf/
rewrite.rs

1//! Rewrites an archive by substituting some entries and copying the rest.
2//!
3//! Changing one resource inside a module archive today means reading the
4//! whole thing into an [`Erf`](super::Erf) and calling
5//! [`write_erf`](super::write_erf), which holds every entry in memory to
6//! change one of them. For a save with dozens of module archives that is the
7//! entire save resident to edit two fields.
8//!
9//! This path never holds more than the tables and one entry at a time. It
10//! copies bytes it was not asked to change, and a byte copy does not have to
11//! understand what it is copying, so an entry this crate cannot parse comes
12//! out the far side intact. Reading into an `Erf` cannot offer that.
13//!
14//! The eager path stays as it is. It is the authoring path, the way
15//! `to_gff()` is at the GFF layer, and the two are kept apart on purpose.
16//!
17//! ## What it will not do
18//!
19//! **Adding and removing entries is left out deliberately.** Committing an
20//! edited save never changes an archive's entry set: editing a creature
21//! changes a GIT's bytes, not which resources the module holds. Both
22//! operations would resize the two tables and move every payload after them,
23//! and nothing needs it yet.
24//!
25//! **Header metadata is preserved verbatim, build date included.** There is
26//! no option to stamp a fresh one. A commit that silently rewrites when the
27//! archive was built has changed something nobody asked it to.
28//!
29//! ## Layout rules this has to respect
30//!
31//! Both from `docs/src/formats/archives/erf.md`.
32//!
33//! The header and the three tables are read *sequentially* by the engine,
34//! from the fixed end of the header, sized from the entry counts alone. The
35//! block offsets in the header are parsed and then never read back. So those
36//! four regions have to stay contiguous and in that order whatever the offset
37//! fields say, which is why they are copied as one verbatim span rather than
38//! rebuilt: preserving them is easier than reproducing them, and it carries
39//! the reserved zone and any blank block along for free.
40//!
41//! Key entry `n` and resource entry `n` must describe the same resource, and
42//! nothing in the format checks that they do. Rows are located here through
43//! the key entry's own `resource_id` rather than by assuming it equals the
44//! entry's position, so the pairing the source had is the pairing the output
45//! gets.
46//!
47//! Payload placement is the one free part of the layout, since each entry's
48//! bytes are found through its own `data_offset`. Payloads are emitted in the
49//! order the source stored them and packed with no gaps. Every archive
50//! measured for this already tiles that way, so substituting nothing
51//! reproduces the input byte for byte.
52
53use std::collections::HashMap;
54use std::io::{self, Read, Seek, Write};
55
56use rakata_core::ResRef;
57use thiserror::Error;
58
59use super::index::ErfIndex;
60use super::layout::ErfHeader;
61use super::{KEY_ENTRY_SIZE, RESOURCE_ENTRY_SIZE};
62use crate::archive::EntryDefect;
63
64/// Byte offset of `resource_id` within a key-table entry.
65const KEY_RESOURCE_ID_OFFSET: usize = 16;
66
67/// Errors produced while rewriting an ERF-family archive.
68#[derive(Debug, Error)]
69pub enum ErfRewriteError {
70    /// I/O read/write failure.
71    #[error(transparent)]
72    Io(#[from] std::io::Error),
73    /// A substitution named an entry the archive does not have.
74    #[error("substitution names entry {index}, but the archive holds {entry_count}")]
75    NoSuchEntry {
76        /// Table position the caller supplied.
77        index: usize,
78        /// Number of entries in the archive.
79        entry_count: usize,
80    },
81    /// Two substitutions named the same entry.
82    #[error("entry {index} was given more than one substitution")]
83    DuplicateSubstitution {
84        /// Table position named twice.
85        index: usize,
86    },
87    /// An entry that was not being replaced cannot be read.
88    ///
89    /// Substituting the entry is the way past this: supplying its bytes means
90    /// the unreadable range is never touched, which is how a damaged archive
91    /// gets repaired rather than refused.
92    #[error("entry {index} ({resref}) cannot be copied: {defect}")]
93    UnreadableEntry {
94        /// Table position of the entry.
95        index: usize,
96        /// Name the archive gives it.
97        resref: ResRef,
98        /// Why its bytes are unreachable.
99        defect: EntryDefect,
100    },
101    /// Two entries claim overlapping byte ranges.
102    #[error("entry {index} starts at {offset}, inside the range ending at {earliest}")]
103    OverlappingPayload {
104        /// Table position of the entry that starts too early.
105        index: usize,
106        /// Offset it declares.
107        offset: u64,
108        /// First offset that was still free.
109        earliest: u64,
110    },
111    /// A key entry points at a resource row outside the resource table.
112    #[error("key entry {index} names resource row {row}, which is not in the table")]
113    KeyNamesMissingResourceRow {
114        /// Table position of the key entry.
115        index: usize,
116        /// Row it names.
117        row: usize,
118    },
119    /// A table lies outside the span copied verbatim from the source.
120    ///
121    /// The header, the localized string block and both tables are copied as
122    /// one region ending at the resource table. A file that puts one of them
123    /// somewhere else is one the engine's own sequential reader mis-parses,
124    /// so it is refused rather than silently truncated.
125    #[error("the {region} ends at {region_end}, past the copied region ending at {prefix_end}")]
126    RegionOutsidePrefix {
127        /// Which region is misplaced.
128        region: &'static str,
129        /// Where it ends.
130        region_end: usize,
131        /// Where the copied region ends.
132        prefix_end: usize,
133    },
134    /// An entry's bytes ran out before its declared size.
135    #[error("entry {index} declares {expected} bytes but only {copied} could be read")]
136    TruncatedEntry {
137        /// Table position of the entry.
138        index: usize,
139        /// Size the table declares.
140        expected: u64,
141        /// Bytes actually copied.
142        copied: u64,
143    },
144    /// Value cannot fit the on-disk integer width.
145    #[error("value overflow while writing field `{0}`")]
146    ValueOverflow(&'static str),
147}
148
149/// Writes `source` to `writer`, replacing the named entries' bytes.
150///
151/// `substitutions` pairs a table position, as given by
152/// [`ErfIndex::entries`] or [`ErfIndex::position`], with the bytes to store
153/// there. Every other entry is copied verbatim, as are the header, the
154/// localized string block and both tables.
155///
156/// # Errors
157///
158/// [`ErfRewriteError::NoSuchEntry`] and
159/// [`ErfRewriteError::DuplicateSubstitution`] for a bad substitution set,
160/// both checked before anything is written.
161///
162/// [`ErfRewriteError::UnreadableEntry`] when an entry that is *not* being
163/// replaced cannot be read. Supplying its bytes as a substitution is the way
164/// past this, which is how a damaged archive gets repaired rather than
165/// refused. [`ErfRewriteError::TruncatedEntry`] when such an entry reads and
166/// runs out before its declared size, and
167/// [`ErfRewriteError::OverlappingPayload`] when two entries claim the same
168/// bytes.
169///
170/// [`ErfRewriteError::RegionOutsidePrefix`] when the source's string block or
171/// key table sits outside the span copied verbatim, since copying it would
172/// truncate them, and [`ErfRewriteError::KeyNamesMissingResourceRow`] when a
173/// key points at a row the resource table does not have.
174///
175/// [`ErfRewriteError::ValueOverflow`] when a recomputed offset or size will
176/// not fit its on-disk width, and [`ErfRewriteError::Io`] from either side.
177/// The I/O arm can leave a partial archive.
178#[cfg_attr(
179    feature = "tracing",
180    tracing::instrument(level = "debug", skip(source, substitutions, writer))
181)]
182pub fn rewrite_erf<'a, R, W>(
183    source: &ErfIndex<R>,
184    substitutions: impl IntoIterator<Item = (usize, &'a [u8])>,
185    writer: &mut W,
186) -> Result<(), ErfRewriteError>
187where
188    R: Read + Seek,
189    W: Write,
190{
191    let entry_count = source.len();
192    let mut replacements: HashMap<usize, &[u8]> = HashMap::new();
193    for (index, bytes) in substitutions {
194        if index >= entry_count {
195            return Err(ErfRewriteError::NoSuchEntry { index, entry_count });
196        }
197        if replacements.insert(index, bytes).is_some() {
198            return Err(ErfRewriteError::DuplicateSubstitution { index });
199        }
200    }
201
202    let header = source.header();
203    let mut prefix = read_prefix(source, header, entry_count)?;
204    let prefix_len =
205        u64::try_from(prefix.len()).map_err(|_| ErfRewriteError::ValueOverflow("data_offset"))?;
206
207    // Payloads keep the order the source gave them, so an equal-sized
208    // substitution changes nothing but the bytes themselves.
209    let mut order: Vec<usize> = (0..entry_count).collect();
210    order.sort_by_key(|&index| (source.entries()[index].offset, index));
211
212    // Two cursors: one walks the source ranges to catch entries that claim
213    // the same bytes, the other assigns the packed output offsets.
214    let mut source_cursor = prefix_len;
215    let mut out_cursor = prefix_len;
216    for &index in &order {
217        let entry = &source.entries()[index];
218        let size = match replacements.get(&index) {
219            Some(bytes) => u64::try_from(bytes.len())
220                .map_err(|_| ErfRewriteError::ValueOverflow("data_size"))?,
221            None => {
222                if let Some(defect) = &entry.defect {
223                    return Err(ErfRewriteError::UnreadableEntry {
224                        index,
225                        resref: entry.resref,
226                        defect: defect.clone(),
227                    });
228                }
229                if entry.offset < source_cursor {
230                    return Err(ErfRewriteError::OverlappingPayload {
231                        index,
232                        offset: entry.offset,
233                        earliest: source_cursor,
234                    });
235                }
236                // The index only admits an entry whose range fits the
237                // archive, so this addition cannot overflow.
238                source_cursor = entry.offset + entry.size;
239                entry.size
240            }
241        };
242        write_resource_row(&mut prefix, header, index, out_cursor, size)?;
243        out_cursor = out_cursor
244            .checked_add(size)
245            .ok_or(ErfRewriteError::ValueOverflow("data_offset"))?;
246    }
247
248    writer.write_all(&prefix)?;
249    for &index in &order {
250        match replacements.get(&index) {
251            Some(bytes) => writer.write_all(bytes)?,
252            None => copy_entry(source, index, writer)?,
253        }
254    }
255
256    crate::trace_debug!(
257        entry_count,
258        substituted = replacements.len(),
259        "rewrote erf-family archive"
260    );
261    Ok(())
262}
263
264/// Serializes a rewritten archive to bytes.
265///
266/// See [`rewrite_erf`] for the semantics.
267///
268/// # Errors
269///
270/// The same as [`rewrite_erf`]. The `Vec` target has no I/O to fail at, but
271/// [`ErfRewriteError::Io`] still arrives from reading `source`.
272#[cfg_attr(
273    feature = "tracing",
274    tracing::instrument(level = "debug", skip(source, substitutions))
275)]
276pub fn rewrite_erf_to_vec<'a, R: Read + Seek>(
277    source: &ErfIndex<R>,
278    substitutions: impl IntoIterator<Item = (usize, &'a [u8])>,
279) -> Result<Vec<u8>, ErfRewriteError> {
280    let mut bytes = Vec::new();
281    rewrite_erf(source, substitutions, &mut bytes)?;
282    crate::trace_debug!(bytes_len = bytes.len(), "serialized rewritten archive");
283    Ok(bytes)
284}
285
286/// Reads the header, string block and both tables as one verbatim span.
287///
288/// Refuses a source whose string block or key table falls outside that span,
289/// since copying it would truncate them.
290fn read_prefix<R: Read + Seek>(
291    source: &ErfIndex<R>,
292    header: &ErfHeader,
293    entry_count: usize,
294) -> Result<Vec<u8>, ErfRewriteError> {
295    let keys_end = table_end(
296        header.keys_offset,
297        entry_count,
298        KEY_ENTRY_SIZE,
299        "keys_offset",
300    )?;
301    let prefix_end = table_end(
302        header.resources_offset,
303        entry_count,
304        RESOURCE_ENTRY_SIZE,
305        "resources_offset",
306    )?;
307    let strings_end = header
308        .localized_strings_offset
309        .checked_add(header.localized_string_size)
310        .ok_or(ErfRewriteError::ValueOverflow("localized_string_size"))?;
311
312    for (region, region_end) in [
313        ("localized string block", strings_end),
314        ("key table", keys_end),
315    ] {
316        if region_end > prefix_end {
317            return Err(ErfRewriteError::RegionOutsidePrefix {
318                region,
319                region_end,
320                prefix_end,
321            });
322        }
323    }
324
325    let len = u64::try_from(prefix_end)
326        .map_err(|_| ErfRewriteError::ValueOverflow("resources_offset"))?;
327    Ok(source
328        .section()
329        .section(0, len)
330        .expect("the index read the resource table out of this span, so it is inside the archive")
331        .read_all()?)
332}
333
334/// Returns the end offset of a table of `entry_count` fixed-size records.
335fn table_end(
336    offset: usize,
337    entry_count: usize,
338    stride: usize,
339    field: &'static str,
340) -> Result<usize, ErfRewriteError> {
341    entry_count
342        .checked_mul(stride)
343        .and_then(|size| offset.checked_add(size))
344        .ok_or(ErfRewriteError::ValueOverflow(field))
345}
346
347/// Points the resource row belonging to key entry `index` at `offset`/`size`.
348///
349/// The row is the one the key entry names rather than the one sharing its
350/// position, so a source whose two tables are permuted stays paired.
351fn write_resource_row(
352    prefix: &mut [u8],
353    header: &ErfHeader,
354    index: usize,
355    offset: u64,
356    size: u64,
357) -> Result<(), ErfRewriteError> {
358    // The key table was checked to lie inside the prefix and `index` is below
359    // the entry count, so neither the multiply nor the slice can fail.
360    let id_at = header.keys_offset + index * KEY_ENTRY_SIZE + KEY_RESOURCE_ID_OFFSET;
361    let raw: [u8; 4] = prefix[id_at..id_at + 4]
362        .try_into()
363        .expect("a four-byte slice converts to a four-byte array");
364    let row = usize::try_from(u32::from_le_bytes(raw))
365        .map_err(|_| ErfRewriteError::ValueOverflow("resource_id"))?;
366
367    let row_at = row
368        .checked_mul(RESOURCE_ENTRY_SIZE)
369        .and_then(|by| header.resources_offset.checked_add(by))
370        .ok_or(ErfRewriteError::ValueOverflow("resource_id"))?;
371    let slot = prefix
372        .get_mut(row_at..row_at + RESOURCE_ENTRY_SIZE)
373        .ok_or(ErfRewriteError::KeyNamesMissingResourceRow { index, row })?;
374
375    let offset =
376        u32::try_from(offset).map_err(|_| ErfRewriteError::ValueOverflow("data_offset"))?;
377    let size = u32::try_from(size).map_err(|_| ErfRewriteError::ValueOverflow("data_size"))?;
378    slot[..4].copy_from_slice(&offset.to_le_bytes());
379    slot[4..].copy_from_slice(&size.to_le_bytes());
380    Ok(())
381}
382
383/// Streams one entry's bytes from the source into `writer`.
384fn copy_entry<R: Read + Seek, W: Write>(
385    source: &ErfIndex<R>,
386    index: usize,
387    writer: &mut W,
388) -> Result<(), ErfRewriteError> {
389    let expected = source.entries()[index].size;
390    let mut section = source
391        .entry_section(index)
392        .expect("the entry is in range and carries no defect, both checked above");
393    let copied = io::copy(&mut section, writer)?;
394    if copied != expected {
395        return Err(ErfRewriteError::TruncatedEntry {
396            index,
397            expected,
398            copied,
399        });
400    }
401    Ok(())
402}