gcn_disk 0.5.0

Gamecube file header library and utilities.
Documentation
// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>

use gcn_disk::DirectoryEntry;
use gcn_disk::Entry;
use gcn_disk::Fst;
use gcn_disk::Metadata;

use std::fs::File;
use std::io;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::path::PathBuf;

use clap::Parser;
use rvz::HeaderRead;
use rvz::Rvz;

#[derive(Parser)]
struct Cli {
    #[arg(short, long)]
    fetch_icon: bool,
    files: Vec<PathBuf>,
}

enum Io {
    File(File),
    Rvz(Box<Rvz<File>>),
}

impl Read for Io {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Self::File(file) => file.read(buf),
            Self::Rvz(file) => file.read(buf),
        }
    }
}

impl Seek for Io {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        match self {
            Self::File(file) => file.seek(pos),
            Self::Rvz(file) => file.seek(pos),
        }
    }
}

fn ls_file<T: Read + Seek>(fst: &Fst, io: &mut T, entry: &Entry, cwd: &str) {
    match entry {
        Entry::File(file) => {
            println!(
                "index: {}, filename_offset: {}, offset: 0x{:X}, size: {}, filename: {cwd}/{}",
                file.index,
                file.filename_offset,
                file.offset,
                file.size,
                fst.get_entry_filename(io, entry).unwrap()
            );
        }
        Entry::Directory(_) => {}
    }
}

fn ls_dir<T: Read + Seek>(fst: &Fst, io: &mut T, directory: &DirectoryEntry, cwd: &str) -> u32 {
    let mut index = directory.index + 1;
    while index != directory.end_index {
        let entry = &fst.entries[index as usize];
        match entry {
            Entry::File(_) => {
                ls_file(fst, io, entry, cwd);
                index += 1;
            }
            Entry::Directory(directory) => {
                println!(
                    "index: {}, filename_offset: {}, parent_index: {}, end_index: {}, dirname: {}",
                    directory.index,
                    directory.filename_offset,
                    directory.parent_index,
                    directory.end_index,
                    fst.get_entry_filename(io, entry).unwrap()
                );
                let dir_name = fst.get_entry_filename(io, entry).unwrap();
                index += ls_dir(fst, io, directory, &format!("{cwd}/{dir_name}"));
            }
        }
    }
    index - directory.index
}

pub fn main() {
    let args = Cli::parse();

    if args.files.is_empty() {
        return;
    }

    println!("[");
    for path in args.files {
        let mut f = File::open(path).unwrap();

        let mut io = if f.has_rvz_magic() {
            let io = Rvz::new(f);
            if let Err(err) = io {
                eprintln!("Error: {err}");
                continue;
            }
            Io::Rvz(Box::new(io.unwrap()))
        } else {
            Io::File(f)
        };

        let disk = Metadata::try_from(&mut io);
        if let Err(ref err) = disk {
            eprintln!("Error: {err}");
            continue;
        }
        let disk = disk.unwrap();
        let fst = &disk.filesystem;
        let root = &fst.entries[0];
        if !matches!(root, Entry::Directory(_)) {
            eprintln!("Error: root directory is not a directory");
            continue;
        }

        if let Entry::Directory(root) = root {
            ls_dir(fst, &mut io, root, "");
        }
    }
    println!("]");
}