use super::database::*;
use super::import_dats::reimport_orphan_romfiles;
use super::iso9660::{FileEntry, Iso9660Filesystem, SECTOR_SIZE, SectorReader};
use super::mimetype::*;
use super::model::*;
use super::progress::*;
use super::prompt::*;
use super::util::*;
use anyhow::{Result, bail};
use clap::value_parser;
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::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,
) -> Result<()> {
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, header) = parse_ird(ird_path).await?;
print_header(progress_bar, &format!("IRD: {}", irdfile.game_name));
print_info(progress_bar, &format!("IRD Version: {}", irdfile.version));
print_info(progress_bar, &format!("Game ID: {}", irdfile.game_id));
print_info(
progress_bar,
&format!("Update Version: {}", irdfile.update_version),
);
print_info(
progress_bar,
&format!("Game Version: {}", irdfile.game_version),
);
print_info(
progress_bar,
&format!("App Version: {}", irdfile.app_version),
);
print_info(progress_bar, &format!("Regions: {}", irdfile.regions_count));
print_info(progress_bar, &format!("Files: {}", irdfile.files_count));
if irdfile.version != IRD_VERSION {
print_warning(progress_bar, "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") {
print_skip(progress_bar, "IRD already exists");
continue;
}
import_ird(connection, progress_bar, game, &irdfile, header).await?;
}
}
print_separator(progress_bar);
}
compute_system_completion(connection, progress_bar, &system).await?;
Ok(())
}
pub async fn parse_ird<P: AsRef<Path>>(path: &P) -> Result<(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,
))
}
struct HeaderSectorReader {
header: Vec<u8>,
}
impl SectorReader for HeaderSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
let mut sector = vec![0u8; SECTOR_SIZE as usize];
let offset = usize::try_from(lba.saturating_mul(SECTOR_SIZE)).unwrap_or(usize::MAX);
if let Some(available) = self.header.get(offset..) {
let length = available.len().min(SECTOR_SIZE as usize);
sector[..length].copy_from_slice(&available[..length]);
}
Ok(sector)
}
}
fn walk_directory(
filesystem: &mut Iso9660Filesystem,
directory: &FileEntry,
) -> Result<HashMap<String, Vec<FileEntry>>> {
let mut files: HashMap<String, Vec<FileEntry>> = HashMap::new();
let mut directories: Vec<FileEntry> = vec![];
for entry in filesystem.list_directory(directory)? {
if entry.is_directory() {
directories.push(entry);
continue;
}
let path = entry.path.trim_start_matches('/');
let path = path.strip_suffix('.').unwrap_or(path).to_string();
match files.entry(path.clone()) {
Entry::Vacant(e) => {
e.insert(Vec::from([entry]));
}
_ => {
files.get_mut(&path).unwrap().push(entry);
}
}
}
for directory in directories {
files.extend(walk_directory(filesystem, &directory)?);
}
Ok(files)
}
pub async fn import_ird(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
game: &Game,
irdfile: &Irdfile,
header: Vec<u8>,
) -> Result<()> {
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 mut filesystem = Iso9660Filesystem::new(Box::new(HeaderSectorReader { header }))?;
let root = filesystem.root();
let files = walk_directory(&mut filesystem, &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::<u64>() as i64;
let location = file.1.first().unwrap().location;
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),
false,
)
.await;
if (size != rom.size
|| irdfile.files_hashes.get(&location).unwrap() != rom.md5.as_ref().unwrap())
&& 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),
false,
)
.await
}
};
}
update_game_jbfolder(&mut transaction, game.id, true).await;
if !orphan_romfile_ids.is_empty() {
print_subheader(progress_bar, "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;