1use crate::{extract::NICE, xor::XorWriteStream};
2use byteordered::byteorder::{WriteBytesExt, BE};
3use std::{
4 error::Error,
5 io::{BufWriter, Seek, SeekFrom, Write},
6 str,
7};
8
9pub enum FsEntry {
10 Dir(Vec<String>),
11 File(Vec<u8>),
12}
13
14struct State {
15 src: String,
16 pending_lengths: Vec<(u64, i32)>,
17}
18
19pub fn build<S: Write + Seek>(
20 out: &mut S,
21 read: impl Fn(&str) -> Result<FsEntry, Box<dyn Error>> + Copy,
22) -> Result<(), Box<dyn Error>> {
23 let out = BufWriter::new(out);
24 let mut out = XorWriteStream::new(out, NICE);
25
26 let mut src = String::with_capacity(64);
27 src.push('.');
28 let mut state = State {
29 src,
30 pending_lengths: Vec::with_capacity(1 << 10),
31 };
32 let dir = match read(&state.src)? {
33 FsEntry::Dir(names) => names,
34 FsEntry::File(_) => panic!(),
35 };
36 slurp_dir(&mut out, read, &mut state, &dir)?;
37
38 for &(offset, value) in &state.pending_lengths {
39 out.seek(SeekFrom::Start(offset))?;
40 out.write_i32::<BE>(value)?;
41 }
42 out.flush()?;
43 Ok(())
44}
45
46fn slurp_dir<S: Write + Seek>(
47 out: &mut S,
48 read: impl Fn(&str) -> Result<FsEntry, Box<dyn Error>> + Copy,
49 state: &mut State,
50 names: &[String],
51) -> Result<(), Box<dyn Error>> {
52 let map = match read(&format!("{}/.map", state.src)) {
53 Ok(FsEntry::File(data)) => data,
54 _ => return Err("missing map".into()),
55 };
56 for file in String::from_utf8(map)?.lines() {
57 let file = names
58 .iter()
59 .find(|n| n.starts_with(file) && matches!(&n.as_bytes()[file.len()..], b"" | b".bin"))
60 .ok_or("missing block")?;
61
62 let id = parse_filename(file.as_bytes()).ok_or("bad block name")?;
63
64 let entry = read(&format!("{}/{}", state.src, file))?;
65 match entry {
66 FsEntry::Dir(names) => {
67 state.src.push('/');
68 state.src.push_str(file);
69
70 write_block(out, state, id, |out, state| {
71 slurp_dir(out, read, state, &names)?;
72 Ok(())
73 })?;
74
75 state.src.truncate(state.src.rfind('/').unwrap());
76 }
77 FsEntry::File(data) => {
78 write_block(out, state, id, |out, _state| {
79 out.write_all(&data)?;
80 Ok(())
81 })?;
82 }
83 }
84 }
85 Ok(())
86}
87
88fn parse_filename(name: &[u8]) -> Option<[u8; 4]> {
90 if name.len() < 5 || name[4] != b'_' {
91 return None;
92 }
93 Some(name[..4].try_into().unwrap())
94}
95
96fn write_block<S: Write + Seek>(
97 out: &mut S,
98 state: &mut State,
99 id: [u8; 4],
100 f: impl FnOnce(&mut S, &mut State) -> Result<(), Box<dyn Error>>,
101) -> Result<(), Box<dyn Error>> {
102 let start = out.stream_position()?;
103 out.write_all(&id)?;
104 out.write_i32::<BE>(0)?; f(out, state)?;
106 let end = out.stream_position()?;
107 let length: i32 = (end - start).try_into().unwrap();
108 state.pending_lengths.push((start + 4, length));
109 Ok(())
110}