use super::database::*;
use super::import_dats::reimport_orphan_romfiles;
use super::mimetype::*;
use super::model::*;
use super::prompt::*;
use super::util::*;
use super::SimpleResult;
use cdfs::{DirectoryEntry, ExtraAttributes, ISO9660Reader, ISODirectory, ISOFile, ISO9660};
use clap::{Arg, ArgAction, ArgMatches, Command};
use flate2::read::GzDecoder;
use indicatif::ProgressBar;
use sqlx::sqlite::SqliteConnection;
use std::collections::hash_map::{Entry, HashMap};
use std::io;
use std::io::prelude::*;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::str;
use strsim::jaro_winkler;
const IRD_VERSION: u8 = 9;
pub fn subcommand() -> Command {
Command::new("import-irds")
.about("Parse and import PlayStation 3 IRD files into oxyromon")
.arg(
Arg::new("IRDS")
.help("Set the IRD files to import")
.required(true)
.num_args(1..)
.index(1)
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("INFO")
.short('i')
.long("info")
.help("Show the IRD information and exit")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("FORCE")
.short('f')
.long("force")
.help("Force import of already imported IRD files")
.required(false)
.action(ArgAction::SetTrue),
)
}
pub async fn main(
connection: &mut SqliteConnection,
matches: &ArgMatches,
progress_bar: &ProgressBar,
) -> SimpleResult<()> {
let ird_paths: Vec<&PathBuf> = matches.get_many::<PathBuf>("IRDS").unwrap().collect();
let system = prompt_for_system_like(connection, None, "%PlayStation 3%").await?;
let mut games = find_wanted_games_by_system_id(connection, system.id).await;
for ird_path in ird_paths {
let (irdfile, mut header) = parse_ird(ird_path).await?;
progress_bar.println(format!("IRD Version: {}", &irdfile.version));
progress_bar.println(format!("Game ID: {}", &irdfile.game_id));
progress_bar.println(format!("Game Name: {}", &irdfile.game_name));
progress_bar.println(format!("Update Version: {}", &irdfile.update_version));
progress_bar.println(format!("Game Version: {}", &irdfile.game_version));
progress_bar.println(format!("App Version: {}", &irdfile.app_version));
progress_bar.println(format!("Regions: {}", &irdfile.regions_count));
progress_bar.println(format!("Files: {}", &irdfile.files_count));
if irdfile.version != IRD_VERSION {
progress_bar.println("IRD version unsupported");
continue;
}
if !matches.get_flag("INFO") {
games.sort_by(|a, b| {
jaro_winkler(&b.name.to_lowercase(), &irdfile.game_name.to_lowercase())
.partial_cmp(&jaro_winkler(
&a.name.to_lowercase(),
&irdfile.game_name.to_lowercase(),
))
.unwrap()
});
if let Some(game) = prompt_for_game(&games, None)? {
if game.jbfolder && !matches.get_flag("FORCE") {
progress_bar.println("IRD already exists");
continue;
}
import_ird(connection, progress_bar, game, &irdfile, &mut header).await?;
}
}
progress_bar.println("");
}
compute_system_completion(connection, progress_bar, &system).await;
Ok(())
}
pub async fn parse_ird<P: AsRef<Path>>(path: &P) -> SimpleResult<(Irdfile, Vec<u8>)> {
let mimetype = get_mimetype(path).await?;
if mimetype.is_none() {
bail!("Not an IRD file");
}
let mut reader = match mimetype.unwrap().extension() {
GZ_EXTENSION => Box::new(GzDecoder::new(get_reader_sync(&path)?)) as Box<dyn Read>,
IRD_EXTENSION => Box::new(get_reader_sync(&path)?) as Box<dyn Read>,
_ => bail!("Not an IRD file"),
};
let mut magic = [0u8; 4];
reader.read_exact(&mut magic).unwrap();
let mut version = [0u8];
reader.read_exact(&mut version).unwrap();
let mut game_id = [0u8; 9];
reader.read_exact(&mut game_id).unwrap();
let mut game_name_length = [0u8];
reader.read_exact(&mut game_name_length).unwrap();
let mut game_name = vec![0u8; game_name_length[0] as usize];
reader.read_exact(&mut game_name).unwrap();
let mut update_version = [0u8; 4];
reader.read_exact(&mut update_version).unwrap();
let mut game_version = [0u8; 5];
reader.read_exact(&mut game_version).unwrap();
let mut app_version = [0u8; 5];
reader.read_exact(&mut app_version).unwrap();
let mut gzipped_header_length = [0u8; 4];
reader.read_exact(&mut gzipped_header_length).unwrap();
let gzipped_header_length = u32::from_le_bytes(gzipped_header_length);
let mut gzipped_header = vec![0u8; gzipped_header_length as usize];
reader.read_exact(&mut gzipped_header).unwrap();
let mut gzipped_header_decoder = GzDecoder::new(io::Cursor::new(gzipped_header));
let mut header: Vec<u8> = vec![];
gzipped_header_decoder.read_to_end(&mut header).unwrap();
let mut footer_length = [0u8; 4];
reader.read_exact(&mut footer_length).unwrap();
let footer_length = u32::from_le_bytes(footer_length);
let mut footer = vec![0u8; footer_length as usize];
reader.read_exact(&mut footer).unwrap();
let mut regions_count = [0u8];
reader.read_exact(&mut regions_count).unwrap();
let regions_count = regions_count[0] as usize;
let mut regions_hashes: Vec<String> = Vec::with_capacity(regions_count);
while regions_hashes.len() < regions_count {
let mut region_hash = [0u8; 16];
reader.read_exact(&mut region_hash).unwrap();
#[allow(clippy::format_collect)]
regions_hashes.push(region_hash.iter().map(|b| format!("{:02x}", b)).collect());
}
let mut files_count = [0u8; 4];
reader.read_exact(&mut files_count).unwrap();
let files_count = u32::from_le_bytes(files_count) as usize;
let mut files_hashes: HashMap<u64, String> = HashMap::with_capacity(files_count);
while files_hashes.len() < files_count {
let mut sector = [0u8; 8];
reader.read_exact(&mut sector).unwrap();
let sector = u64::from_le_bytes(sector);
let mut file_hash = [0u8; 16];
reader.read_exact(&mut file_hash).unwrap();
files_hashes.insert(
sector,
#[allow(clippy::format_collect)]
file_hash.iter().map(|b| format!("{:02x}", b)).collect(),
);
}
Ok((
Irdfile {
version: version[0],
game_id: str::from_utf8(&game_id).unwrap().trim_end().to_string(),
game_name: str::from_utf8(&game_name).unwrap().trim_end().to_string(),
update_version: str::from_utf8(&update_version)
.unwrap()
.trim_end()
.to_string(),
game_version: str::from_utf8(&game_version)
.unwrap()
.trim_end()
.to_string(),
app_version: str::from_utf8(&app_version).unwrap().trim_end().to_string(),
regions_count,
regions_hashes,
files_count,
files_hashes,
},
header,
))
}
fn walk_directory<T: ISO9660Reader>(
directory: &ISODirectory<T>,
prefix: &str,
) -> HashMap<String, Vec<ISOFile<T>>> {
let mut files: HashMap<String, Vec<ISOFile<T>>> = HashMap::new();
let mut directories: Vec<ISODirectory<T>> = vec![];
for entry in directory.contents().flatten() {
match entry {
DirectoryEntry::Directory(directory) => {
if directory.identifier != "." && directory.identifier != ".." {
directories.push(directory);
}
}
DirectoryEntry::File(file) => {
let path = format!("{}{}", prefix, file.identifier);
if let Entry::Vacant(e) = files.entry(path.clone()) {
e.insert(Vec::from([file]));
} else {
files.get_mut(&path).unwrap().push(file);
}
}
_ => {}
}
}
for directory in directories {
files.extend(walk_directory(
&directory,
&format!("{}{}/", prefix, directory.identifier),
));
}
files
}
pub async fn import_ird(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
game: &Game,
irdfile: &Irdfile,
header: &mut [u8],
) -> SimpleResult<()> {
let roms = find_roms_by_game_id_no_parents(connection, game.id).await;
let parent_rom = prompt_for_rom(&roms, None)?;
if parent_rom.is_none() {
return Ok(());
}
let mut transaction = begin_transaction(connection).await;
let iso = ISO9660::new(Cursor::new(header)).unwrap();
let files = walk_directory(iso.root(), "");
if files.len() != irdfile.files_count {
bail!(
"Files count differ, expected {}, actual {}",
irdfile.files_count,
files.len()
);
}
let mut orphan_romfile_ids: Vec<i64> = vec![];
for file in files {
let size = file.1.iter().map(|file| file.size()).sum::<u32>() as i64;
let location = file.1.first().unwrap().header().extent_loc as u64;
match find_rom_by_name_and_game_id(&mut transaction, &file.0, game.id).await {
Some(rom) => {
update_rom(
&mut transaction,
rom.id,
&file.0,
size,
irdfile.files_hashes.get(&location).unwrap(),
game.id,
parent_rom.as_ref().map(|rom| rom.id),
)
.await;
if size != rom.size
|| irdfile.files_hashes.get(&location).unwrap() != rom.md5.as_ref().unwrap()
{
if let Some(romfile_id) = rom.romfile_id {
orphan_romfile_ids.push(romfile_id);
update_rom_romfile(&mut transaction, rom.id, None).await;
}
}
rom.id
}
None => {
create_rom(
&mut transaction,
&file.0,
size,
irdfile.files_hashes.get(&location).unwrap(),
game.id,
parent_rom.as_ref().map(|rom| rom.id),
)
.await
}
};
}
update_game_jbfolder(&mut transaction, game.id, true).await;
if !orphan_romfile_ids.is_empty() {
progress_bar.println("Processing orphan romfiles");
reimport_orphan_romfiles(
&mut transaction,
progress_bar,
game.system_id,
orphan_romfile_ids,
)
.await?;
}
commit_transaction(transaction).await;
Ok(())
}
#[cfg(test)]
mod test_ird;