use super::common::*;
use super::config::*;
use super::mimetype::*;
use super::model::*;
use super::progress::*;
use super::util::*;
use anyhow::{Context, Result, bail};
use indicatif::ProgressBar;
use nsz_rs::keys::Keys;
use nsz_rs::pipeline::{Compression, compress_nsp, decompress_nsz};
use sqlx::SqliteConnection;
use std::io;
use std::path::{Path, PathBuf};
const COMPRESSION_LEVEL: i32 = 18;
fn keys_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".switch").join("prod.keys"))
}
fn load_keys() -> nsz_rs::Result<Keys> {
let path = keys_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"no home directory to look for .switch/prod.keys in",
)
})?;
Keys::load(path, true)
}
pub struct NspRomfile {
pub romfile: CommonRomfile,
}
impl GetRomfile for NspRomfile {
fn romfile(&self) -> &CommonRomfile {
&self.romfile
}
}
pub struct NszRomfile {
pub romfile: CommonRomfile,
}
impl GetRomfile for NszRomfile {
fn romfile(&self) -> &CommonRomfile {
&self.romfile
}
}
impl HashAndSize for NszRomfile {
async fn get_hash_and_size(
&self,
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
position: usize,
total: usize,
hash_algorithm: &HashAlgorithm,
) -> Result<(String, u64)> {
let tmp_directory = create_tmp_directory(connection).await?;
let nsp_romfile = self.to_nsp(progress_bar, &tmp_directory).await?;
let (hash, size) = nsp_romfile
.romfile
.get_hash_and_size(connection, progress_bar, position, total, hash_algorithm)
.await?;
nsp_romfile.romfile.delete(progress_bar, true).await?;
Ok((hash, size))
}
}
impl Check for NszRomfile {
async fn check(
&self,
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
header: &Option<Header>,
roms: &[&Rom],
) -> Result<()> {
print_action(progress_bar, &format!("Checking \"{}\"", self.romfile));
let tmp_directory = create_tmp_directory(connection).await?;
let nsp_romfile = self.to_nsp(progress_bar, &tmp_directory).await?;
nsp_romfile
.romfile
.check(connection, progress_bar, header, roms)
.await?;
Ok(())
}
}
pub trait ToNsp {
async fn to_nsp<P: AsRef<Path>>(
&self,
progress_bar: &ProgressBar,
destination_directory: &P,
) -> Result<NspRomfile>;
}
impl ToNsp for NszRomfile {
async fn to_nsp<P: AsRef<Path>>(
&self,
progress_bar: &ProgressBar,
destination_directory: &P,
) -> Result<NspRomfile> {
start_action(progress_bar, Some("Extracting nsz"));
print_action(
progress_bar,
&format!(
"Extracting \"{}\"",
self.romfile.path.file_name().unwrap().to_str().unwrap()
),
);
let path = destination_directory
.as_ref()
.join(self.romfile.path.file_name().unwrap())
.with_extension(NSP_EXTENSION);
let (input, output) = (self.romfile.path.clone(), path.clone());
run_blocking(progress_bar, input.metadata()?.len(), move |progress| {
decompress_nsz(&input, &output, load_keys, true, false, false, progress)
})
.await
.with_context(|| format!("Failed to decompress \"{}\"", self.romfile.path.display()))?;
stop_action(progress_bar);
CommonRomfile::from_path(&path)?.as_nsp()
}
}
pub trait ToNsz {
async fn to_nsz<P: AsRef<Path>>(
&self,
progress_bar: &ProgressBar,
destination_directory: &P,
) -> Result<NszRomfile>;
}
impl ToNsz for NspRomfile {
async fn to_nsz<P: AsRef<Path>>(
&self,
progress_bar: &ProgressBar,
destination_directory: &P,
) -> Result<NszRomfile> {
start_action(progress_bar, Some("Creating nsz"));
let path = destination_directory
.as_ref()
.join(self.romfile.path.file_name().unwrap())
.with_extension(NSZ_EXTENSION);
print_action(
progress_bar,
&format!(
"Creating \"{}\"",
path.file_name().unwrap().to_str().unwrap()
),
);
let (input, output) = (self.romfile.path.clone(), path.clone());
let compression = Compression {
level: COMPRESSION_LEVEL,
ldm: true,
block_size_exponent: None,
};
run_blocking(progress_bar, input.metadata()?.len(), move |progress| {
compress_nsp(&input, &output, load_keys, &compression, false, progress)
})
.await
.with_context(|| format!("Failed to compress \"{}\"", self.romfile.path.display()))?;
stop_action(progress_bar);
CommonRomfile::from_path(&path)?.as_nsz()
}
}
pub trait AsNsp {
fn as_nsp(self) -> Result<NspRomfile>;
}
impl AsNsp for CommonRomfile {
fn as_nsp(self) -> Result<NspRomfile> {
if self
.path
.extension()
.unwrap()
.to_str()
.unwrap()
.to_lowercase()
!= NSP_EXTENSION
{
bail!("Not a valid nsp");
}
Ok(NspRomfile { romfile: self })
}
}
pub trait AsNsz {
fn as_nsz(self) -> Result<NszRomfile>;
}
impl AsNsz for CommonRomfile {
fn as_nsz(self) -> Result<NszRomfile> {
if self
.path
.extension()
.unwrap()
.to_str()
.unwrap()
.to_lowercase()
!= NSZ_EXTENSION
{
bail!("Not a valid nsz");
}
Ok(NszRomfile { romfile: self })
}
}
pub async fn get_version() -> Result<String> {
Ok(match keys_path().is_some_and(|path| path.is_file()) {
true => String::from("built-in"),
false => String::from("built-in, prod.keys not found"),
})
}
#[cfg(test)]
mod test_as_nsp_as_nsz;
#[cfg(test)]
mod test_check;
#[cfg(test)]
mod test_check_mismatch;
#[cfg(test)]
mod test_hash_and_size;
#[cfg(test)]
mod test_to_nsp;
#[cfg(test)]
mod test_to_nsz;