1use crate::ModParameter;
2use crate::script_engine::PatchError;
3use encoding_rs::WINDOWS_1252;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22pub enum MemoryPatch {
23 ReplaceBytes { replacements: Vec<BytesReplacement> },
28 ReplaceString {
35 replacements: Vec<StringReplacement>,
36 },
37 Script {
42 source: ScriptSource,
43 #[serde(default)]
44 parameters: Vec<ModParameter>,
45 #[serde(default)]
46 configuration: HashMap<String, serde_json::Value>,
47 },
48}
49
50#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
53#[serde(untagged, rename_all = "snake_case")]
54pub enum ScriptSource {
55 Inline { inline: String },
56 File { file: PathBuf },
57}
58
59#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
61pub struct BytesReplacement {
62 pub find: Vec<u8>,
63 pub replace: Vec<u8>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub enable_if: Option<String>,
71}
72
73#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
76pub struct StringReplacement {
77 pub find: String,
78 pub replace: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub enable_if: Option<String>,
81}
82
83impl MemoryPatch {
84 pub fn apply(&self, data: &[u8], rel_key: &str) -> Result<Vec<u8>, PatchError> {
95 match self {
96 MemoryPatch::ReplaceBytes { replacements } => {
97 let pairs: Vec<(&[u8], &[u8])> = replacements
98 .iter()
99 .map(|r| (r.find.as_slice(), r.replace.as_slice()))
100 .collect();
101 Ok(replace_many(data, &pairs))
102 }
103 MemoryPatch::ReplaceString { replacements } => {
104 let encoded: Vec<(Vec<u8>, Vec<u8>)> = replacements
105 .iter()
106 .map(|r| {
107 (
108 encode_windows_1252(&r.find),
109 encode_windows_1252(&r.replace),
110 )
111 })
112 .collect();
113 let pairs: Vec<(&[u8], &[u8])> = encoded
114 .iter()
115 .map(|(find, replace)| (find.as_slice(), replace.as_slice()))
116 .collect();
117 Ok(replace_many(data, &pairs))
118 }
119 MemoryPatch::Script {
120 source,
121 parameters,
122 configuration,
123 } => {
124 let script = match source {
125 ScriptSource::Inline { inline } => inline.clone(),
126 ScriptSource::File { file } => std::fs::read_to_string(file).map_err(|e| {
127 PatchError::InvalidPatch(format!("Failed to read script file: {}", e))
128 })?,
129 };
130
131 let (input, _, had_errors) = WINDOWS_1252.decode(data);
133 if had_errors {
134 return Err(PatchError::Encoding(
135 "Input is not valid Windows-1252".to_string(),
136 ));
137 }
138
139 let (re_encoded, _, _) = WINDOWS_1252.encode(&input);
141 if re_encoded != data {
142 return Err(PatchError::Encoding(
143 "Input does not round-trip cleanly through Windows-1252".to_string(),
144 ));
145 }
146
147 let result = crate::script_engine::run_script_patch(
148 &script,
149 &input,
150 rel_key,
151 parameters,
152 configuration,
153 )?;
154
155 let (output, _, had_errors) = WINDOWS_1252.encode(&result);
156 if had_errors {
157 return Err(PatchError::Encoding(
158 "Output contains characters not encodable in Windows-1252".to_string(),
159 ));
160 }
161
162 Ok(output.into_owned())
163 }
164 }
165 }
166}
167
168fn encode_windows_1252(s: &str) -> Vec<u8> {
171 let (bytes, _, _) = WINDOWS_1252.encode(s);
172 bytes.into_owned()
173}
174
175pub fn apply_all(
182 patches: &[MemoryPatch],
183 data: &[u8],
184 rel_key: &str,
185) -> Result<Vec<u8>, PatchError> {
186 let mut acc = data.to_vec();
187 for patch in patches {
188 acc = patch.apply(&acc, rel_key)?;
189 }
190 Ok(acc)
191}
192
193fn replace_many(data: &[u8], pairs: &[(&[u8], &[u8])]) -> Vec<u8> {
202 let pairs: Vec<_> = pairs.iter().filter(|(find, _)| !find.is_empty()).collect();
203 if pairs.is_empty() {
204 return data.to_vec();
205 }
206
207 let mut out = Vec::with_capacity(data.len());
208 let mut i = 0;
209 'outer: while i < data.len() {
210 for (find, replace) in &pairs {
211 if data[i..].starts_with(find) {
212 out.extend_from_slice(replace);
213 i += find.len();
214 continue 'outer;
215 }
216 }
217 out.push(data[i]);
218 i += 1;
219 }
220 out
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 const REL_KEY: &str = "data/test_file.txt";
231
232 fn string_patch(pairs: &[(&str, &str)]) -> MemoryPatch {
233 MemoryPatch::ReplaceString {
234 replacements: pairs
235 .iter()
236 .map(|(find, replace)| StringReplacement {
237 find: (*find).to_string(),
238 replace: (*replace).to_string(),
239 enable_if: None,
240 })
241 .collect(),
242 }
243 }
244
245 #[test]
246 fn replaces_all_occurrences_of_a_single_pair() {
247 let patch = string_patch(&[("Level1", "CustomLevel")]);
248 let data = b"Level1 loaded, welcome to Level1!";
249 assert_eq!(
250 patch.apply(data, REL_KEY).unwrap(),
251 b"CustomLevel loaded, welcome to CustomLevel!"
252 );
253 }
254
255 #[test]
256 fn replaces_multiple_level_names_in_one_pass() {
257 let patch = string_patch(&[
260 ("\"Level1\"", "\"Frozen Peak\""),
261 ("\"Level2\"", "\"Sunset Valley\""),
262 ("\"Level3\"", "\"Iron Canyon\""),
263 ]);
264 let data = br#"{"levels":["Level1","Level2","Level3"]}"#;
265 assert_eq!(
266 patch.apply(data, REL_KEY).unwrap(),
267 br#"{"levels":["Frozen Peak","Sunset Valley","Iron Canyon"]}"#
268 );
269 }
270
271 #[test]
272 fn simultaneous_replacements_do_not_cascade_into_each_other() {
273 let patch = string_patch(&[("A", "B"), ("B", "C")]);
277 assert_eq!(patch.apply(b"AB", REL_KEY).unwrap(), b"BC");
278 }
279
280 #[test]
281 fn earlier_pair_wins_when_finds_overlap_at_same_position() {
282 let patch = string_patch(&[("Level10", "Ten"), ("Level1", "One")]);
283 assert_eq!(patch.apply(b"Level10", REL_KEY).unwrap(), b"Ten");
284 let patch_reordered = string_patch(&[("Level1", "One"), ("Level10", "Ten")]);
285 assert_eq!(patch_reordered.apply(b"Level10", REL_KEY).unwrap(), b"One0");
286 }
287
288 #[test]
289 fn empty_find_is_skipped() {
290 let patch = string_patch(&[("", "X"), ("A", "B")]);
291 assert_eq!(patch.apply(b"AAA", REL_KEY).unwrap(), b"BBB");
292 }
293
294 #[test]
295 fn all_empty_finds_is_noop() {
296 let patch = string_patch(&[("", "X")]);
297 assert_eq!(patch.apply(b"unchanged", REL_KEY).unwrap(), b"unchanged");
298 }
299
300 #[test]
301 fn chained_separate_patches_still_apply_in_order() {
302 let patches = vec![string_patch(&[("A", "B")]), string_patch(&[("B", "C")])];
303 assert_eq!(apply_all(&patches, b"AB", REL_KEY).unwrap(), b"CC");
306 }
307
308 #[test]
309 fn binary_replace_handles_non_utf8() {
310 let patch = MemoryPatch::ReplaceBytes {
311 replacements: vec![BytesReplacement {
312 find: vec![0xDE, 0xAD],
313 replace: vec![0xBE, 0xEF, 0xBE, 0xEF],
314 enable_if: None,
315 }],
316 };
317 assert_eq!(
318 patch.apply(&[0x01, 0xDE, 0xAD, 0x02], REL_KEY).unwrap(),
319 vec![0x01, 0xBE, 0xEF, 0xBE, 0xEF, 0x02]
320 );
321 }
322
323 #[test]
324 fn string_replacements_match_against_windows_1252_bytes() {
325 let patch = string_patch(&[("Piste Enneig\u{e9}e", "Sommet Gel\u{e9}")]);
330 let windows_1252_input = b"Piste Enneig\xE9e loaded";
331 assert_eq!(
332 patch.apply(windows_1252_input, REL_KEY).unwrap(),
333 b"Sommet Gel\xE9 loaded"
334 );
335 }
336}