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}