1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct EntryOffsets {
30 pub next_entry_offset: usize,
31 pub file_name_length: usize,
32 pub file_name: usize,
33}
34
35fn read_u32(buf: &[u8], pos: usize) -> u32 {
36 u32::from_ne_bytes(buf[pos..pos + 4].try_into().unwrap())
37}
38
39fn write_u32(buf: &mut [u8], pos: usize, value: u32) {
40 buf[pos..pos + 4].copy_from_slice(&value.to_ne_bytes());
41}
42
43#[derive(Debug, PartialEq, Eq)]
45pub struct FilterResult {
46 pub bytes_written: usize,
48 pub last_entry_offset: Option<usize>,
53}
54
55pub fn filter_redirected_entries(
67 buf: &mut [u8],
68 valid_len: usize,
69 offsets: EntryOffsets,
70 rel_dir: &str,
71 mut is_redirected: impl FnMut(&str) -> bool,
72) -> FilterResult {
73 let mut read_pos = 0usize;
74 let mut write_pos = 0usize;
75 let mut last_entry_offset: Option<usize> = None;
76
77 loop {
78 if read_pos + offsets.file_name_length + 4 > valid_len {
79 break;
80 }
81 let next_offset = read_u32(buf, read_pos + offsets.next_entry_offset) as usize;
82 let entry_name_len = read_u32(buf, read_pos + offsets.file_name_length) as usize;
83
84 if read_pos + offsets.file_name + entry_name_len > valid_len {
97 break;
98 }
99
100 let entry_size = if next_offset > 0 {
101 next_offset
102 } else {
103 (offsets.file_name + entry_name_len + 7) & !7
104 };
105
106 let name_pos = read_pos + offsets.file_name;
107 let name_u16: Vec<u16> = buf[name_pos..name_pos + entry_name_len]
108 .as_chunks::<2>()
109 .0
110 .iter()
111 .map(|pair| u16::from_ne_bytes(*pair))
112 .collect();
113 let name = String::from_utf16_lossy(&name_u16).to_lowercase();
114
115 let rel_key = if rel_dir.is_empty() {
116 name
117 } else {
118 format!("{rel_dir}/{name}")
119 };
120
121 if !is_redirected(&rel_key) {
122 if write_pos != read_pos {
123 buf.copy_within(read_pos..read_pos + entry_size, write_pos);
124 }
125 if let Some(prev) = last_entry_offset {
126 write_u32(
127 buf,
128 prev + offsets.next_entry_offset,
129 (write_pos - prev) as u32,
130 );
131 }
132 last_entry_offset = Some(write_pos);
133 write_pos += entry_size;
134 }
135
136 if next_offset == 0 {
137 break;
138 }
139 if read_pos + next_offset >= valid_len {
140 break;
141 }
142 read_pos += next_offset;
143 }
144
145 if let Some(prev) = last_entry_offset {
146 write_u32(buf, prev + offsets.next_entry_offset, 0);
147 }
148
149 FilterResult {
150 bytes_written: write_pos,
151 last_entry_offset,
152 }
153}
154
155pub fn write_directory_entry(
175 buf: &mut [u8],
176 pos: usize,
177 offsets: EntryOffsets,
178 name: &str,
179 prev_entry_offset: Option<usize>,
180) -> Option<usize> {
181 const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
182
183 let wide_name: Vec<u16> = name.encode_utf16().collect();
184 let name_bytes = wide_name.len() * 2;
185 let entry_size = (offsets.file_name + name_bytes + 7) & !7;
186
187 if pos + entry_size > buf.len() {
188 return None;
189 }
190
191 buf[pos..pos + entry_size].fill(0);
192 write_u32(buf, pos + offsets.next_entry_offset, 0);
193 if offsets.file_name >= 60 {
194 write_u32(buf, pos + 56, FILE_ATTRIBUTE_NORMAL);
195 }
196 write_u32(buf, pos + offsets.file_name_length, name_bytes as u32);
197
198 let name_pos = pos + offsets.file_name;
199 for (i, unit) in wide_name.iter().enumerate() {
200 buf[name_pos + i * 2..name_pos + i * 2 + 2].copy_from_slice(&unit.to_ne_bytes());
201 }
202
203 if let Some(prev) = prev_entry_offset {
204 write_u32(buf, prev + offsets.next_entry_offset, (pos - prev) as u32);
205 }
206
207 Some(entry_size)
208}
209
210pub fn wildcard_match(name: &str, pattern: &str) -> bool {
219 let n: Vec<char> = name.chars().collect();
220 let p: Vec<char> = pattern.chars().collect();
221 let mut dp = vec![vec![false; p.len() + 1]; n.len() + 1];
222 dp[0][0] = true;
223 for j in 1..=p.len() {
224 if p[j - 1] == '*' {
225 dp[0][j] = dp[0][j - 1];
226 }
227 }
228 for i in 1..=n.len() {
229 for j in 1..=p.len() {
230 dp[i][j] = if p[j - 1] == '*' {
231 dp[i - 1][j] || dp[i][j - 1]
232 } else if p[j - 1] == '?' || p[j - 1] == n[i - 1] {
233 dp[i - 1][j - 1]
234 } else {
235 false
236 };
237 }
238 }
239 dp[n.len()][p.len()]
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 const NAMES_OFFSETS: EntryOffsets = EntryOffsets {
251 next_entry_offset: 0,
252 file_name_length: 8,
253 file_name: 12,
254 };
255
256 fn build_names_buffer(names: &[&str]) -> Vec<u8> {
260 let mut buf = Vec::new();
261 let mut entry_starts = Vec::new();
262 for name in names {
263 entry_starts.push(buf.len());
264 let wide: Vec<u16> = name.encode_utf16().collect();
265 let name_bytes = wide.len() * 2;
266 let entry_size = (NAMES_OFFSETS.file_name + name_bytes + 7) & !7;
267 let mut entry = vec![0u8; entry_size];
268 entry[8..12].copy_from_slice(&(name_bytes as u32).to_ne_bytes());
269 for (i, unit) in wide.iter().enumerate() {
270 entry[12 + i * 2..12 + i * 2 + 2].copy_from_slice(&unit.to_ne_bytes());
271 }
272 buf.extend_from_slice(&entry);
273 }
274 for i in 0..entry_starts.len() {
275 let next = if i + 1 < entry_starts.len() {
276 (entry_starts[i + 1] - entry_starts[i]) as u32
277 } else {
278 0
279 };
280 buf[entry_starts[i]..entry_starts[i] + 4].copy_from_slice(&next.to_ne_bytes());
281 }
282 buf
283 }
284
285 fn read_all_names(buf: &[u8], valid_len: usize, offsets: EntryOffsets) -> Vec<String> {
288 let mut names = Vec::new();
289 let mut pos = 0;
290 loop {
291 if pos + offsets.file_name_length + 4 > valid_len {
292 break;
293 }
294 let next = read_u32(buf, pos + offsets.next_entry_offset) as usize;
295 let name_len = read_u32(buf, pos + offsets.file_name_length) as usize;
296 let name_pos = pos + offsets.file_name;
297 let wide: Vec<u16> = buf[name_pos..name_pos + name_len]
298 .as_chunks::<2>()
299 .0
300 .iter()
301 .map(|pair| u16::from_ne_bytes(*pair))
302 .collect();
303 names.push(String::from_utf16_lossy(&wide));
304 if next == 0 {
305 break;
306 }
307 pos += next;
308 }
309 names
310 }
311
312 #[test]
315 fn filter_keeps_all_entries_when_nothing_is_redirected() {
316 let mut buf = build_names_buffer(&["save1.dat", "save2.dat", "save3.dat"]);
317 let valid_len = buf.len();
318 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |_| false);
319 assert_eq!(result.bytes_written, valid_len);
320 assert_eq!(
321 read_all_names(&buf, result.bytes_written, NAMES_OFFSETS),
322 vec!["save1.dat", "save2.dat", "save3.dat"]
323 );
324 }
325
326 #[test]
327 fn filter_drops_a_middle_entry_and_rechains_around_it() {
328 let mut buf = build_names_buffer(&["keep1.dat", "drop.dat", "keep2.dat"]);
329 let valid_len = buf.len();
330 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |name| {
331 name == "drop.dat"
332 });
333 assert_eq!(
334 read_all_names(&buf, result.bytes_written, NAMES_OFFSETS),
335 vec!["keep1.dat", "keep2.dat"]
336 );
337 }
338
339 #[test]
340 fn filter_drops_the_first_entry() {
341 let mut buf = build_names_buffer(&["drop.dat", "keep.dat"]);
342 let valid_len = buf.len();
343 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |name| {
344 name == "drop.dat"
345 });
346 assert_eq!(
347 read_all_names(&buf, result.bytes_written, NAMES_OFFSETS),
348 vec!["keep.dat"]
349 );
350 }
351
352 #[test]
353 fn filter_drops_the_last_entry_and_renulls_the_new_last_next_offset() {
354 let mut buf = build_names_buffer(&["keep.dat", "drop.dat"]);
355 let valid_len = buf.len();
356 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |name| {
357 name == "drop.dat"
358 });
359 assert_eq!(
360 read_all_names(&buf, result.bytes_written, NAMES_OFFSETS),
361 vec!["keep.dat"]
362 );
363 let last = result.last_entry_offset.unwrap();
367 assert_eq!(read_u32(&buf, last + NAMES_OFFSETS.next_entry_offset), 0);
368 }
369
370 #[test]
371 fn filter_drops_everything() {
372 let mut buf = build_names_buffer(&["drop1.dat", "drop2.dat"]);
373 let valid_len = buf.len();
374 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |_| true);
375 assert_eq!(result.bytes_written, 0);
376 assert_eq!(result.last_entry_offset, None);
377 }
378
379 #[test]
380 fn filter_passes_lowercased_rel_dir_prefixed_key_to_the_predicate() {
381 let mut buf = build_names_buffer(&["Save1.DAT"]);
382 let valid_len = buf.len();
383 let mut seen = None;
384 filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "data/saves", |key| {
385 seen = Some(key.to_string());
386 false
387 });
388 assert_eq!(seen.as_deref(), Some("data/saves/save1.dat"));
389 }
390
391 #[test]
392 fn filter_with_empty_rel_dir_uses_bare_lowercased_name_as_key() {
393 let mut buf = build_names_buffer(&["Save1.DAT"]);
394 let valid_len = buf.len();
395 let mut seen = None;
396 filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |key| {
397 seen = Some(key.to_string());
398 false
399 });
400 assert_eq!(seen.as_deref(), Some("save1.dat"));
401 }
402
403 #[test]
404 fn filter_stops_at_a_truncated_final_entry_instead_of_reading_out_of_bounds() {
405 let mut buf = build_names_buffer(&["ok.dat"]);
410 let ok_entry_size = buf.len();
411
412 buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&1000u32.to_ne_bytes()); let valid_len = buf.len(); buf[0..4].copy_from_slice(&(ok_entry_size as u32).to_ne_bytes());
423
424 let result = filter_redirected_entries(&mut buf, valid_len, NAMES_OFFSETS, "", |_| false);
425 assert_eq!(
426 read_all_names(&buf, result.bytes_written, NAMES_OFFSETS),
427 vec!["ok.dat"]
428 );
429 }
430
431 #[test]
434 fn write_directory_entry_writes_name_and_length() {
435 let mut buf = vec![0u8; 64];
436 let written = write_directory_entry(&mut buf, 0, NAMES_OFFSETS, "mod.dat", None);
437 assert!(written.is_some());
438 let name_len = read_u32(&buf, NAMES_OFFSETS.file_name_length) as usize;
439 assert_eq!(name_len, "mod.dat".encode_utf16().count() * 2);
440 assert_eq!(read_all_names(&buf, buf.len(), NAMES_OFFSETS)[0], "mod.dat");
441 }
442
443 #[test]
444 fn write_directory_entry_chains_previous_entrys_next_offset() {
445 let mut buf = vec![0u8; 128];
446 let first_size =
447 write_directory_entry(&mut buf, 0, NAMES_OFFSETS, "first.dat", None).unwrap();
448 write_directory_entry(&mut buf, first_size, NAMES_OFFSETS, "second.dat", Some(0));
449
450 assert_eq!(
451 read_all_names(&buf, buf.len(), NAMES_OFFSETS),
452 vec!["first.dat", "second.dat"]
453 );
454 }
455
456 #[test]
457 fn write_directory_entry_returns_none_when_it_would_not_fit() {
458 let mut buf = vec![0u8; 16]; let written = write_directory_entry(&mut buf, 0, NAMES_OFFSETS, "toolong.dat", None);
460 assert_eq!(written, None);
461 }
462
463 #[test]
464 fn write_directory_entry_sets_file_attribute_normal_when_offset_supports_it() {
465 let dir_offsets = EntryOffsets {
469 next_entry_offset: 0,
470 file_name_length: 60,
471 file_name: 64,
472 };
473 let mut buf = vec![0u8; 128];
474 write_directory_entry(&mut buf, 0, dir_offsets, "mod.dat", None);
475 assert_eq!(read_u32(&buf, 56), 0x0000_0080); }
477
478 #[test]
479 fn write_directory_entry_skips_file_attribute_write_for_small_headers() {
480 let mut buf = vec![0xAAu8; 32];
485 write_directory_entry(&mut buf, 0, NAMES_OFFSETS, "x.dat", None);
486 let name_len = read_u32(&buf, NAMES_OFFSETS.file_name_length) as usize;
487 assert_eq!(name_len, "x.dat".encode_utf16().count() * 2);
488 }
489}
490
491#[cfg(test)]
492mod wildcard_match_tests {
493 use super::wildcard_match;
494
495 #[test]
496 fn exact_match() {
497 assert!(wildcard_match("save.dat", "save.dat"));
498 }
499
500 #[test]
501 fn no_match_different_name() {
502 assert!(!wildcard_match("save.dat", "config.dat"));
503 }
504
505 #[test]
506 fn star_matches_everything() {
507 assert!(wildcard_match("save.dat", "*"));
508 assert!(wildcard_match("", "*"));
509 }
510
511 #[test]
512 fn star_matches_suffix() {
513 assert!(wildcard_match("save.dat", "*.dat"));
514 assert!(!wildcard_match("save.txt", "*.dat"));
515 }
516
517 #[test]
518 fn star_matches_prefix() {
519 assert!(wildcard_match("save1.dat", "save*"));
520 assert!(!wildcard_match("config.dat", "save*"));
521 }
522
523 #[test]
524 fn star_matches_middle() {
525 assert!(wildcard_match("save_001_final.dat", "save*final.dat"));
526 }
527
528 #[test]
529 fn star_can_match_empty_string() {
530 assert!(wildcard_match("save.dat", "save*.dat"));
531 }
532
533 #[test]
534 fn question_mark_matches_exactly_one_char() {
535 assert!(wildcard_match("save1.dat", "save?.dat"));
536 assert!(!wildcard_match("save12.dat", "save?.dat"));
537 assert!(!wildcard_match("save.dat", "save?.dat"));
538 }
539
540 #[test]
541 fn multiple_stars() {
542 assert!(wildcard_match(
543 "save_001_backup_final.dat",
544 "save*backup*.dat"
545 ));
546 }
547
548 #[test]
549 fn empty_pattern_only_matches_empty_name() {
550 assert!(wildcard_match("", ""));
551 assert!(!wildcard_match("save.dat", ""));
552 }
553
554 #[test]
555 fn empty_name_matches_only_all_star_pattern() {
556 assert!(wildcard_match("", "***"));
557 assert!(!wildcard_match("", "save*"));
558 }
559
560 #[test]
566 fn is_case_sensitive_by_itself() {
567 assert!(!wildcard_match("Save.dat", "save.dat"));
568 }
569}