wiiload 0.3.1

A minimal wiiload implementation in rust
Documentation
// SPDX-FileCopyrightText: 2026 Manuel Quarneti <mq1@ik.me>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::{
    io::{BufWriter, Write},
    net::{Ipv4Addr, SocketAddr, TcpStream},
    time::Duration,
};
use thiserror::Error;

const WIILOAD_PORT: u16 = 4299;
const WIILOAD_MAGIC: &[u8] = b"HAXX";
const WIILOAD_VERSION: [u8; 3] = [0, 5, 0];
const WIILOAD_TIMEOUT: Duration = Duration::from_secs(10);

#[derive(Error, Debug)]
pub enum WiiloadError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Network error: {0}")]
    Net(#[from] std::net::AddrParseError),
    #[error("Timeout")]
    Timeout,
    #[error("File too big")]
    TryFromIntError(#[from] std::num::TryFromIntError),
    #[error("Filename too long")]
    FileNameTooLong,
}

fn push(
    filename: &str,
    body: &[u8],
    wii_ip: Ipv4Addr,
    uncompressed_size: u32,
) -> Result<(), WiiloadError> {
    let compressed_size: u32 = body.len().try_into()?;
    let filename_len: u8 = filename
        .len()
        .try_into()
        .map_err(|_| WiiloadError::FileNameTooLong)?;

    // Parse the address
    let wii_addr = SocketAddr::from((wii_ip, WIILOAD_PORT));

    // Connect to the Wii via tcp
    let mut stream = {
        let stream = TcpStream::connect_timeout(&wii_addr, WIILOAD_TIMEOUT)?;
        stream.set_read_timeout(Some(WIILOAD_TIMEOUT))?;
        stream.set_write_timeout(Some(WIILOAD_TIMEOUT))?;
        BufWriter::new(stream)
    };

    // Send Wiiload header
    stream.write_all(WIILOAD_MAGIC)?;
    stream.write_all(&WIILOAD_VERSION[..])?;
    stream.write_all(&[filename_len])?;
    stream.write_all(&compressed_size.to_be_bytes())?;
    stream.write_all(&uncompressed_size.to_be_bytes())?;

    // Send the data
    stream.write_all(body)?;

    // Send arguments
    stream.write_all(filename.as_bytes())?;
    if !filename.ends_with('\0') {
        stream.write_all(&[0])?;
    }

    stream.flush()?;

    Ok(())
}

/// Sends a file to the Wii without applying any compression.
///
/// # Arguments
/// * `filename` - The name of the file to send.
/// * `body` - The raw byte content of the file.
/// * `wii_ip` - The IPv4 address of the target Wii.
///
/// # Errors
/// Returns a [`WiiloadError`] if:
/// * The file size exceeds `u32::MAX`.
/// * The filename length exceeds `u8::MAX`.
/// * The TCP connection to the Wii cannot be established or times out.
/// * An I/O error occurs while writing data to the network stream.
pub fn send(
    filename: impl AsRef<str>,
    body: impl AsRef<[u8]>,
    wii_ip: impl Into<Ipv4Addr>,
) -> Result<(), WiiloadError> {
    push(filename.as_ref(), body.as_ref(), wii_ip.into(), 0)
}

/// Compresses the file data using Zlib and then sends it to the Wii.
///
/// This uses deflate -9 to minimize network transfer time.
///
/// # Arguments
/// * `filename` - The name of the file to send.
/// * `body` - The raw byte content of the file to be compressed.
/// * `wii_ip` - The IPv4 address of the target Wii.
///
/// # Errors
/// Returns a [`WiiloadError`] if:
/// * The uncompressed file size exceeds `u32::MAX`.
/// * The filename length exceeds `u8::MAX`.
/// * The TCP connection to the Wii cannot be established or times out.
/// * An I/O error occurs while writing data to the network stream.
#[cfg(feature = "compression")]
pub fn compress_then_send(
    filename: impl AsRef<str>,
    body: impl AsRef<[u8]>,
    wii_ip: impl Into<Ipv4Addr>,
) -> Result<(), WiiloadError> {
    use miniz_oxide::deflate::compress_to_vec_zlib;

    let body = body.as_ref();
    let uncompressed_size = body.len().try_into()?;
    let compressed_body = compress_to_vec_zlib(body, 9);

    push(
        filename.as_ref(),
        &compressed_body,
        wii_ip.into(),
        uncompressed_size,
    )
}