gc_fst 0.2.1

Gamecube ISO unpacker and rebuilder.
Documentation
use gc_fst::*;

const HELP: &'static str = 
"Usage: gc_fst extract <iso path>
       gc_fst rebuild <root path> [iso path]
       gc_fst get-header <ISO.hdr path | iso path>
       gc_fst set-header <ISO.hdr path | iso path> <game ID> [game title]
       gc_fst read <iso path> [ <path in iso> <path to file> ] * n
       gc_fst tree <iso path> [--size|-s] [--offset|-o] [--hex|-x] [--directories|-d] [--filename|-f]
       gc_fst fs <iso path> [
           insert <path in iso> <path to file>
           delete <path in iso>
       ] * n";

fn usage() -> ! {
    eprintln!("{}", HELP);
    std::process::exit(1);
}

macro_rules! unwrap_usage {
    ($e:expr) => {
        match $e {
            Some(e) => e,
            None => usage(),
        }
    }
}

fn main() {
    let args = std::env::args().collect::<Vec<_>>();
    match args.get(1).map(|s| s.as_str()) {
        Some("tree") => {
            let iso = unwrap_usage!(args.get(2).map(|s| s.as_str()));

            let mut i = 3;
            let mut options = TreeOptions {
                print_directories  : false,
                print_files        : true,
                print_file_offsets : false,
                print_file_sizes   : false,
                print_full_paths   : true,
                print_hex          : false,
            };

            while let Some(arg) = args.get(i) {
                match arg.as_str() {
                    "--directories" | "-d" => {
                        options.print_directories = true;
                        i += 1;
                    }
                    "--size" | "-s" => {
                        options.print_file_sizes = true;
                        i += 1;
                    }
                    "--hex" | "-x" => {
                        options.print_hex = true;
                        i += 1;
                    }
                    "--offset" | "-o" => {
                        options.print_file_offsets = true;
                        i += 1;
                    }
                    "--filename" | "-f" => {
                        options.print_full_paths = false;
                        i += 1;
                    }
                    _ => {
                        eprintln!("Error: unknown argument '{}'\n\n{}", arg, HELP);
                        std::process::exit(1);
                    }
                }
            }

            match tree_iso(std::path::Path::new(iso), &options) {
                Ok(()) => {},
                Err(TreeISOError::IOError(e)) => {
                    eprintln!("Error: {}", e);
                    std::process::exit(1);
                }
                Err(TreeISOError::InvalidISO) => {
                    eprintln!("Error: file is not an iso or is corrupted");
                    std::process::exit(1);
                },
                Err(TreeISOError::OpenError { path, e }) => {
                    eprintln!("Error: could not open file '{}': {}", path.display(), e);
                    std::process::exit(1);
                },
            }
        }
        Some("read") => {
            let iso = unwrap_usage!(args.get(2).map(|s| s.as_str()));
            let mut files = Vec::with_capacity(args[3..].len() / 2);

            let mut i = 3;
            while i < args.len() {
                let iso_path = std::path::Path::new(&args[i]);
                let read_path = std::path::Path::new(unwrap_usage!(args.get(i+1)));
                files.push((iso_path, read_path));
                i += 2;
            }

            match read_iso_files(std::path::Path::new(iso), &files) {
                Ok(()) => {},
                Err(ReadISOFilesError::IOError(e)) => {
                    eprintln!("Error: {}", e);
                    std::process::exit(1);
                },
                Err(ReadISOFilesError::InvalidISO) => {
                    eprintln!("Error: file is not an iso or is corrupted");
                    std::process::exit(1);
                },
                Err(ReadISOFilesError::InvalidFSPath(path)) => {
                    eprintln!("Error: file path '{}' does not exist", path.display());
                    std::process::exit(1);
                }
            }
        }
        Some("fs") => {
            let iso = unwrap_usage!(args.get(2).map(|s| s.as_str()));

            let mut cmds = Vec::with_capacity(args[3..].len() / 2);

            let mut i = 3;
            while i < args.len() {
                match args[i].as_str() {
                    "insert" => {
                        cmds.push(IsoOp::Insert {
                            iso_path: std::path::Path::new(unwrap_usage!(args.get(i+1))),
                            input_path: std::path::Path::new(unwrap_usage!(args.get(i+2))),
                        });
                        i += 3;
                    },
                    "delete" => {
                        cmds.push(IsoOp::Delete {
                            iso_path: std::path::Path::new(unwrap_usage!(args.get(i+1))),
                        });
                        i += 2;
                    }
                    _ => usage()
                }
            }

            match operate_on_iso(std::path::Path::new(iso), &cmds) {
                Ok(_) => (),
                Err(OperateISOError::IOError(e)) => {
                    eprintln!("Error: {}", e);
                    std::process::exit(1);
                },
                Err(OperateISOError::OpenError { path, e }) => {
                    eprintln!("Error: could not open file '{}': {}", path.display(), e);
                    std::process::exit(1);
                },
                Err(OperateISOError::FileInsertionReplicatesFolder(path)) => {
                    eprintln!("Error: insertion path '{}' already exists as a folder", path.display());
                    std::process::exit(1);
                }
                Err(OperateISOError::InvalidISOPath(path)) => {
                    eprintln!("Error: iso path '{}' does not exist", path.display());
                    std::process::exit(1);
                }
                Err(OperateISOError::InvalidFSPath(path)) => {
                    eprintln!("Error: file path '{}' does not exist", path.display());
                    std::process::exit(1);
                }
                Err(OperateISOError::InvalidISO) => {
                    eprintln!("Error: file is not an iso or is corrupted");
                    std::process::exit(1);
                }
                Err(OperateISOError::TOCTooLarge) => {
                    eprintln!("Error: table of contents is too large, too many files added.");
                    std::process::exit(1);
                }
            }
        }

        Some("get-header") => {
            let path = unwrap_usage!(args.get(2).map(|s| s.as_str()));

            let mut f = match std::fs::File::options().read(true).open(path) {
                Ok(f) => f,
                Err(e) => {
                    eprintln!("Error: Could not open file '{}'", e);
                    std::process::exit(1);
                }
            };

            let mut header = [0u8; 6];
            use std::io::Read;
            match f.read_exact(&mut header) {
                Ok(()) => match std::str::from_utf8(&header) {
                    Ok(str) => println!("{}", str),
                    Err(e) => {
                        eprintln!("Error: Could not parse header: {}", e);
                        std::process::exit(1);
                    }
                },
                Err(e) => {
                    eprintln!("Error: Could not read header: {}", e);
                    std::process::exit(1);
                }
            }
        }

        Some("set-header") => {
            let path = unwrap_usage!(args.get(2).map(|s| s.as_str()));
            let game_id = unwrap_usage!(args.get(3).map(|s| s.as_str()));

            let mut f = match std::fs::File::options().write(true).open(path) {
                Ok(f) => f,
                Err(e) => {
                    eprintln!("Error: Could not open file '{}'", e);
                    std::process::exit(1);
                }
            };

            if game_id.len() != 6 
                || game_id[0..4].chars().any(|c| !c.is_ascii_uppercase()) 
                || game_id[4..6].chars().any(|c| !c.is_ascii_digit()) 
            {
                eprintln!("Error: Invalid game ID: '{}'. Expected ID such as 'GALE01'", game_id);
                std::process::exit(1);
            }

            use std::io::{Seek, Write};
            if let Err(e) = f.write_all(game_id.as_bytes()) {
                eprintln!("Error: Could not write file '{}'", e);
                std::process::exit(1);
            }

            match args.get(4).map(|s| s.as_str()) {
                Some(title) if title.len() >= 0x20 => {
                    eprintln!("Error: game title is too long");
                    std::process::exit(1);
                }
                Some(title) => {
                    let mut bytes = [0u8; 0x20];
                    bytes[0..title.len()].copy_from_slice(title.as_bytes());

                    if let Err(e) = f.seek(std::io::SeekFrom::Start(0x20)) {
                        eprintln!("Error: Could not seek file '{}'", e);
                        std::process::exit(1);
                    }

                    if let Err(e) = f.write_all(&bytes) {
                        eprintln!("Error: Could not write file '{}'", e);
                        std::process::exit(1);
                    }
                },
                None => (),
            };
        }
        Some("extract") => {
            let iso_path = unwrap_usage!(args.get(2).map(|s| s.as_str()));

            let iso = match std::fs::read(&iso_path) {
                Ok(i) => i,
                Err(e) => {
                    eprintln!("Error: Could not read iso '{}'", e);
                    std::process::exit(1);
                }
            };

            match read_iso(&iso) {
                Ok(_) => (),
                Err(ReadISOError::RootDirNotEmpty) => {
                    eprintln!("Error: root directory is not empty");
                    std::process::exit(1);
                }
                Err(ReadISOError::InvalidISO) => {
                    eprintln!("Error: iso path does not exist");
                    std::process::exit(1);
                },
                Err(ReadISOError::WriteFileError(e)) => {
                    eprintln!("Error: Could not write file '{}'", e);
                    std::process::exit(1);
                },
                Err(ReadISOError::CreateDirError(e)) => {
                    eprintln!("Error: Could not create directory '{}'", e);
                    std::process::exit(1);
                },
            }
        }
        Some("rebuild") => {
            let root_path = unwrap_usage!(args.get(2).map(|s| s.as_str()));

            let iso_path = match args.get(3).map(|s| s.as_str()) {
                Some(p) => p,
                None => "out.iso",
            };

            let bytes = match write_iso(std::path::Path::new(root_path)) {
                Ok(b) => b,
                Err(WriteISOError::InvalidFilename(f)) => {
                    eprintln!("Error: Filename '{:?}' cannot be written in an ISO", f);
                    std::process::exit(1);
                },
                Err(WriteISOError::ReadFileError(e)) => {
                    eprintln!("Error: Could not read file '{}'", e);
                    std::process::exit(1);
                },
                Err(WriteISOError::ReadDirError(e)) => {
                    eprintln!("Error: Could not read directory '{}'", e);
                    std::process::exit(1);
                },
            };

            std::fs::write(&iso_path, &bytes).unwrap();
        }
        _ => usage(),
    }
}