cargo-psp 0.2.6

`cargo build` wrapper for creating Sony PSP executables
use clap::Parser;
use std::{fs, mem, path::PathBuf};

const SIGNATURE: [u8; 4] = *b"\0PBP";
const VERSION: u32 = 0x1_0000;

struct PbpHeader {
    signature: [u8; 4],
    version: u32,
    offsets: [u32; 8],
}

impl PbpHeader {
    fn to_bytes(&self) -> [u8; mem::size_of::<Self>()] {
        let mut bytes = [0; mem::size_of::<Self>()];

        bytes[0..4].copy_from_slice(&self.signature);
        bytes[4..8].copy_from_slice(&self.version.to_le_bytes());

        for (i, offset) in self.offsets.iter().enumerate() {
            let idx = i * 4 + 8;
            bytes[idx..idx + 4].copy_from_slice(&offset.to_le_bytes());
        }

        bytes
    }
}

#[derive(Parser, Debug)]
#[command(
    name = "pack-pbp",
    author = "Marko Mijalkovic <marko.mijalkovic97@gmail.com>",
    version = "0.1",
    about = "Create Sony PSP packages"
)]
struct Args {
    #[arg(name = "output.pbp", help = "Output PBP file")]
    output: PathBuf,
    #[arg(name = "param.sfo", help = "Input PARAM.SFO file created with mksfo")]
    param: PathBuf,
    #[arg(name = "icon0.png", help = "Optional XMB icon")]
    icon0: PathBuf,
    #[arg(name = "icon1.pmf", help = "Optional animated XMB icon")]
    icon1: PathBuf,
    #[arg(
        name = "pic0.png",
        help = "Optional XMB background (overlayed on top of PIC1.PNG)"
    )]
    pic0: PathBuf,
    #[arg(name = "pic1.png", help = "Optional XMB background")]
    pic1: PathBuf,
    #[arg(
        name = "snd0.at3",
        help = "Optional XMB music (when present, sound from ICON1.PMF is ignored)"
    )]
    snd0: PathBuf,
    #[arg(name = "data.psp", help = "Executable file")]
    data_psp: PathBuf,
    #[arg(name = "data.psar", help = "Optional archive data")]
    data_psar: PathBuf,
}

fn main() {
    let args = Args::parse();

    let read = |value: PathBuf| {
        if value == PathBuf::from("NULL") {
            None
        } else {
            match fs::read(&value) {
                Ok(res) => Some(res),
                Err(err) => panic!("failed to read {}: {}", value.display(), err),
            }
        }
    };

    let output_path = args.output;

    let files = vec![
        read(args.param),
        read(args.icon0),
        read(args.icon1),
        read(args.pic0),
        read(args.pic1),
        read(args.snd0),
        read(args.data_psp),
        read(args.data_psar),
    ];

    let mut payload = Vec::new();
    let mut offsets = [0; 8];
    let mut current_offset = mem::size_of::<PbpHeader>() as u32;

    for (i, bytes) in files.into_iter().enumerate() {
        offsets[i] = current_offset;

        if let Some(bytes) = bytes {
            let len = bytes.len();
            payload.extend(bytes);
            current_offset += len as u32;
        }
    }

    let header = PbpHeader {
        signature: SIGNATURE,
        version: VERSION,
        offsets,
    };

    let mut output = Vec::new();
    output.extend(&header.to_bytes()[..]);
    output.extend(payload);

    if let Err(err) = fs::write(&output_path, output) {
        panic!("couldn't write to {}: {}", output_path.display(), err);
    }

    println!("Saved to {output_path:?}");
}