Skip to main content

inside_baseball/
build.rs

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
88// Parse filenames of the form "BLOK_123"
89fn 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)?; // placeholder
105    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}