letterbomb 5.0.0

A fork of the classic Wii hacking tool from fail0verflow
Documentation
use std::collections::HashMap;
use std::io::{self, Cursor, Read};
use std::sync::OnceLock;
use thiserror::Error;
use zip::ZipArchive;

static BUNDLE_ZIP_URL: &str = "https://bootmii.org/hackmii_installer_v1.2.zip";

#[derive(Debug, Error)]
pub enum BundleError {
    #[error("network error: {0}")]
    Reqwest(#[from] reqwest::Error),

    #[error("zip decoding error: {0}")]
    Zip(#[from] zip::result::ZipError),

    #[error("io error: {0}")]
    Io(#[from] io::Error),
}
/// Lazily downloads and unpacks the remote bundle ZIP.
///
/// On the **first** call, this function performs a **blocking** HTTP GET
/// request to `BUNDLE_ZIP_URL`, reads the entire response into memory,
/// and then parses it as a ZIP archive. Each file entry (except directories)
/// is extracted into a `HashMap<String, Vec<u8>>` mapping the file's path
/// (as stored in the archive) to its raw bytes.
///
/// The resulting map - or any error encountered during download or extraction
/// - is cached in a `OnceLock`, so subsequent calls return instantly without re-downloading.
///
/// If this function fails, re-calling it will attempt to load and unzip again, although this is
/// useless if nothing changes between calls.
///
/// # Examples
///
/// ```no_run
/// use letterbomb::bundle::bundle;
///
/// match bundle() {
///     Ok(files) => {
///         // Access a bundled file by its ZIP path
///         if let Some(data) = files.get("boot.elf") {
///             println!("boot.elf size = {} bytes", data.len());
///         }
///     }
///     Err(e) => eprintln!("bundle load error: {}", e),
/// }
/// ```
///
/// # Errors
///
/// - Returns `Err(BundleError::Reqwest(_))` if the HTTP GET fails.
/// - Returns `Err(BundleError::Io(_))` if reading the response or ZIP entries fails.
/// - Returns `Err(BundleError::Zip(_))` if the response is not a valid ZIP archive.
///
/// # Blocking
///
/// This function blocks the calling thread during the network request
/// and ZIP decompression. For non-blocking usage, fetch and unpack
/// on a separate thread or use an async HTTP client.
///
/// # Panics
///
/// This function does not panic. All failures are reported via the returned `Err`.
///
/// # Note on licensing
///
/// The reason why these files cannot simply be included with the library is that
/// they are not licensed for redistribution or bundled. Therefore, the files themselves cannot
/// be put in the library. This is a license-compliant solution.
pub fn bundle() -> &'static Result<HashMap<String, Vec<u8>>, BundleError> {
    static BUNDLE: OnceLock<Result<HashMap<String, Vec<u8>>, BundleError>> = OnceLock::new();
    BUNDLE.get_or_init(|| {
        let prefix = BUNDLE_ZIP_URL
            .split('/')
            .next_back()
            .unwrap_or("")
            .split(".zip")
            .next()
            .unwrap_or("")
            .to_owned()
            + "/";
        let resp = reqwest::blocking::get(BUNDLE_ZIP_URL)?;
        let bytes = resp.bytes()?;
        let reader = Cursor::new(bytes);
        let mut zip = ZipArchive::new(reader)?;
        let mut map = HashMap::new();
        for idx in 0..zip.len() {
            let mut file = zip.by_index(idx)?;
            let file_name = file.name().to_owned();
            if file_name.ends_with('/') {
                continue;
            }
            let mut buf = Vec::with_capacity(file.size() as usize);
            file.read_to_end(&mut buf)?;
            map.insert(
                file_name
                    .strip_prefix(&prefix)
                    .unwrap_or(file_name.as_str())
                    .to_string(),
                buf,
            );
        }
        Ok(map)
    })
}

#[cfg(test)]
mod tests {
    use super::bundle;

    #[test]
    fn test_bundle() {
        let bundle = bundle().as_ref().ok();
        assert!(&bundle.unwrap().len() > &0);
        for (name, data) in bundle.unwrap() {
            println!("in bundle: {:<26}: {:>11} bytes", name, data.len());
        }
    }
}