Changes to PKG installer (#514)

Co-authored-by: SuchAFuriousDeath <48620541+SuchAFuriousDeath@users.noreply.github.com>
This commit is contained in:
VocalFan 2023-12-10 13:35:40 -05:00 committed by GitHub
parent b5722ffe50
commit dff58d46be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 262 additions and 51 deletions

View file

@ -13,8 +13,13 @@ extern "C" {
param *param_open(const char *file, error **error);
void param_close(param *param);
void param_app_ver_get(const param *param, QString &buf);
void param_category_get(const param *param, QString &buf);
void param_content_id_get(const param *param, QString &buf);
void param_short_content_id_get(const param *param, QString &buf);
void param_title_get(const param *param, QString &buf);
void param_title_id_get(const param *param, QString &buf);
void param_version_get(const param *param, QString &buf);
error *system_download(
const char *from,
@ -90,6 +95,34 @@ public:
operator param *() const { return m_obj; }
public:
QString appver() const
{
QString s;
param_app_ver_get(m_obj, s);
return s;
}
QString category() const
{
QString s;
param_category_get(m_obj, s);
return s;
}
QString contentId() const
{
QString s;
param_content_id_get(m_obj, s);
return s;
}
QString shortContentId() const
{
QString s;
param_short_content_id_get(m_obj, s);
return s;
}
QString title() const
{
QString s;
@ -104,6 +137,13 @@ public:
return s;
}
QString version() const
{
QString s;
param_version_get(m_obj, s);
return s;
}
void close()
{
if (m_obj) {

View file

@ -33,12 +33,43 @@ pub unsafe extern "C" fn param_close(param: *mut Param) {
drop(Box::from_raw(param));
}
#[no_mangle]
pub unsafe extern "C" fn param_app_ver_get(param: &Param, buf: &mut QString) {
match &param.app_ver() {
Some(app_ver) => buf.set(app_ver),
None => buf.set(""),
}
}
#[no_mangle]
pub unsafe extern "C" fn param_category_get(param: &Param, buf: &mut QString) {
buf.set(param.category());
}
#[no_mangle]
pub unsafe extern "C" fn param_content_id_get(param: &Param, buf: &mut QString) {
buf.set(param.content_id());
}
#[no_mangle]
pub unsafe extern "C" fn param_short_content_id_get(param: &Param, buf: &mut QString) {
buf.set(param.shortcontent_id());
}
#[no_mangle]
pub unsafe extern "C" fn param_title_get(param: &Param, buf: &mut QString) {
buf.set(param.title());
match &param.title() {
Some(title) => buf.set(title),
None => buf.set(""),
}
}
#[no_mangle]
pub unsafe extern "C" fn param_title_id_get(param: &Param, buf: &mut QString) {
buf.set(param.title_id());
buf.set(param.title_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_version_get(param: &Param, buf: &mut QString) {
buf.set(param.version());
}

View file

@ -145,8 +145,20 @@ fn main() -> ExitCode {
}
// Param information
writeln!(log, "Application Title : {}", param.title()).unwrap();
writeln!(
log,
"Application Title : {}",
param.title().as_ref().unwrap()
)
.unwrap();
writeln!(log, "Application ID : {}", param.title_id()).unwrap();
writeln!(log, "Application Category: {}", param.category()).unwrap();
writeln!(
log,
"Application Version : {}",
param.app_ver().as_ref().unwrap()
)
.unwrap();
// Hardware information
writeln!(
@ -401,7 +413,11 @@ fn discord_presence(param: &Param) {
}
// Create details about game.
let details = format!("Playing {} - {}", param.title(), param.title_id());
let details = format!(
"Playing {} - {}",
param.title().as_ref().unwrap(),
param.title_id()
);
let start = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()

View file

@ -11,7 +11,7 @@ use elf::{
};
use gmtx::{Gutex, GutexGroup, GutexReadGuard, GutexWriteGuard};
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
@ -322,7 +322,11 @@ impl<E: ExecutionEngine> Module<E> {
pub fn dump<P: AsRef<Path>>(&mut self, path: P) -> Result<(), std::io::Error> {
let path = path.as_ref();
let mut file = File::create(path)?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
file.write_all(unsafe { self.memory.as_bytes() })
}

View file

@ -264,19 +264,52 @@ void MainWindow::installPkg()
// Create game directory.
auto gamesDirectory = readGamesDirectorySetting();
if (!QDir(gamesDirectory).mkdir(param.titleId())) {
QString msg(
"Cannot create directory %1 inside %2. "
"If you have a failed installation from a previous attempt, you will need to remove this directory before trying again.");
// Get Param information
auto appver = param.appver();
auto category = param.category();
auto shortContentId = param.shortContentId();
auto title = param.title();
auto titleId = param.titleId();
QMessageBox::critical(&progress, "Error", msg.arg(param.titleId()).arg(gamesDirectory));
// Check if file is Patch/DLC.
bool patchOrDlc = false;
if (category.startsWith("gp") || category.contains("ac")) {
patchOrDlc = true;
}
// If PKG isn't a game, DLC, or Patch, don't allow.
if (!patchOrDlc && !category.startsWith("gd")) {
QString msg("PKG file is not a Patch, DLC, or a Game. Possibly a corrupted PKG?");
QMessageBox::critical(&progress, "Error", msg);
return;
}
auto directory = joinPath(gamesDirectory, titleId);
// Setup folders for DLC and Patch PKGs
if (patchOrDlc == true) {
if (category.contains("ac")) {
// TODO: Add DLC support, short_content_id is most likely to be used.
QString msg("DLC PKG support is not yet implemented.");
QMessageBox::critical(&progress, "Error", msg);
return;
} else {
// If our PKG is for Patching, add -PATCH- to the end of the foldername along with the patch APPVER. (-PATCH-01.01)
directory += "-PATCH-" + appver.toStdString();
}
}
if (!QDir().mkdir(QString::fromStdString(directory))) {
QString msg("Install directory could not be created at\n%1");
QMessageBox::critical(&progress, "Error", msg.arg(QString::fromStdString(directory)));
return;
}
auto directory = joinPath(gamesDirectory, param.titleId());
// Extract items.
progress.setWindowTitle(param.title());
progress.setWindowTitle(title);
error = pkg_extract(pkg, directory.c_str(), [](const char *name, std::uint64_t total, std::uint64_t written, void *ud) {
auto toProgress = [total](std::uint64_t v) -> int {
@ -317,8 +350,13 @@ void MainWindow::installPkg()
return;
}
// Add to game list.
auto success = loadGame(param.titleId());
// Add to game list if new game.
bool success = false;
if (!patchOrDlc) {
success = loadGame(titleId);
} else {
success = true;
}
if (success) {
QMessageBox::information(this, "Success", "Package installed successfully.");
@ -390,7 +428,7 @@ void MainWindow::requestGamesContextMenu(const QPoint &pos)
void MainWindow::startGame(const QModelIndex &index)
{
if (!requireEmulatorStopped()) {
if (requireEmulatorStopped()) {
return;
}
@ -525,22 +563,30 @@ bool MainWindow::loadGame(const QString &gameId)
{
auto gamesDirectory = readGamesDirectorySetting();
auto gamePath = joinPath(gamesDirectory, gameId);
auto gameList = reinterpret_cast<GameListModel *>(m_games->model());
// Read game title from param.sfo.
auto paramDir = joinPath(gamePath.c_str(), "sce_sys");
auto paramPath = joinPath(paramDir.c_str(), "param.sfo");
Error error;
Param param(param_open(paramPath.c_str(), &error));
// Ignore entry if it is DLC or Patch.
auto lastSlashPos = gamePath.find_last_of("/\\");
auto lastFolder = (lastSlashPos != std::string::npos) ? gamePath.substr(lastSlashPos + 1) : gamePath;
bool isPatch = lastFolder.find("-PATCH-") != std::string::npos;
bool isAddCont = lastFolder.size() >= 8 && lastFolder.substr(lastFolder.size() - 8) == "-ADDCONT";
if (!param) {
QMessageBox::critical(this, "Error", QString("Cannot open %1: %2").arg(paramPath.c_str()).arg(error.message()));
return false;
if (!isPatch && !isAddCont) {
// Read game title from param.sfo.
auto paramDir = joinPath(gamePath.c_str(), "sce_sys");
auto paramPath = joinPath(paramDir.c_str(), "param.sfo");
Error error;
Param param(param_open(paramPath.c_str(), &error));
if (!param) {
QMessageBox::critical(this, "Error", QString("Cannot open %1: %2").arg(paramPath.c_str()).arg(error.message()));
return false;
}
// Add to list if not a DLC/Patch refresh.
auto gameList = reinterpret_cast<GameListModel *>(m_games->model());
gameList->add(new Game(param.title(), gamePath.c_str()));
}
// Add to list.
gameList->add(new Game(param.title(), gamePath.c_str()));
return true;
}
@ -584,9 +630,20 @@ void MainWindow::restoreGeometry()
bool MainWindow::requireEmulatorStopped()
{
if (m_kernel) {
QMessageBox::critical(this, "Error", "This function is not available while a game is running.");
return false;
QMessageBox killPrompt(this);
killPrompt.setText("Action requires kernel to be stopped to continue.");
killPrompt.setInformativeText("Do you want to kill the kernel?");
killPrompt.setStandardButtons(QMessageBox::Cancel | QMessageBox::Yes);
killPrompt.setDefaultButton(QMessageBox::Cancel);
killPrompt.setIcon(QMessageBox::Warning);
if (killPrompt.exec() == QMessageBox::Yes) {
killKernel();
return false; // Kernel was killed
}
return true; // Kernel left running
}
return true;
return false; // Kernel isn't running
}

View file

@ -6,8 +6,12 @@ use thiserror::Error;
///
/// See https://www.psdevwiki.com/ps4/Param.sfo#Internal_Structure for more information.
pub struct Param {
title: String,
app_ver: Option<String>,
category: String,
content_id: String,
title: Option<String>,
title_id: String,
version: String,
}
impl Param {
@ -72,8 +76,12 @@ impl Param {
keys.drain(i..);
// Read entries.
let mut app_ver: Option<String> = None;
let mut category: Option<String> = None;
let mut content_id: Option<String> = None;
let mut title: Option<String> = None;
let mut title_id: Option<String> = None;
let mut version: Option<String> = None;
for i in 0..entries {
// Seek to the entry.
@ -128,26 +136,77 @@ impl Param {
// Parse value.
match key {
b"TITLE" => title = Some(Self::read_utf8(&mut raw, i, format, len, 128)?),
b"TITLE_ID" => title_id = Some(Self::read_utf8(&mut raw, i, format, len, 12)?),
b"APP_VER" => {
app_ver = Some(Self::read_utf8(&mut raw, i, format, len, 8)?);
}
b"CATEGORY" => {
category = Some(Self::read_utf8(&mut raw, i, format, 4, 4)?);
}
b"CONTENT_ID" => {
content_id = Some(Self::read_utf8(&mut raw, i, format, len, 48)?);
}
b"TITLE" => {
title = Some(Self::read_utf8(&mut raw, i, format, len, 128)?);
}
b"TITLE_ID" => {
title_id = Some(Self::read_utf8(&mut raw, i, format, len, 12)?);
}
b"VERSION" => {
version = Some(Self::read_utf8(&mut raw, i, format, len, 8)?);
}
_ => continue,
}
}
Ok(Self {
title: title.ok_or(ReadError::MissingTitle)?,
// App_Ver for Games and Patches, for DLC, use version. Anything else is abnormal.
app_ver: app_ver,
category: category.ok_or(ReadError::MissingCategory)?,
content_id: content_id.ok_or(ReadError::MissingContentId)?,
title: title,
title_id: title_id.ok_or(ReadError::MissingTitleId)?,
version: version.ok_or(ReadError::MissingVersion)?,
})
}
pub fn title(&self) -> &str {
/// Fetches the value APP_VER from given Param.SFO
pub fn app_ver(&self) -> &Option<String> {
&self.app_ver
}
/// Fetches the value CATEGORY from given Param.SFO
pub fn category(&self) -> &str {
&self.category
}
/// Fetches the value CONTENT_ID from given Param.SFO
pub fn content_id(&self) -> &str {
&self.content_id
}
/// Fetches a shortened variant of value CONTENT_ID from given Param.SFO
pub fn shortcontent_id(&self) -> &str {
self.content_id
.split('-')
.last()
.unwrap_or(&self.content_id)
}
/// Fetches the value TITLE from given Param.SFO
pub fn title(&self) -> &Option<String> {
&self.title
}
/// Fetches the value TITLE_ID from given Param.SFO
pub fn title_id(&self) -> &str {
&self.title_id
}
/// Fetches the value VERSION from given Param.SFO
pub fn version(&self) -> &str {
&self.version
}
fn read_utf8<R: Read>(
raw: &mut R,
i: u64,
@ -206,9 +265,15 @@ pub enum ReadError {
#[error("entry #{0} has invalid value")]
InvalidValue(usize),
#[error("TITLE is not found")]
MissingTitle,
#[error("CATEGORY parameter not found")]
MissingCategory,
#[error("TITLE_ID is not found")]
#[error("CONTENT_ID parameter not found")]
MissingContentId,
#[error("TITLE_ID parameter not found")]
MissingTitleId,
#[error("VERSION parameter not found")]
MissingVersion,
}

View file

@ -7,6 +7,5 @@
// Do not throw. Return empty string is case of error.
std::string joinPath(const QString &base, const QString &name);
// v must be native format.
std::filesystem::path toPath(const QString &v);

View file

@ -8,7 +8,7 @@ use sha2::Digest;
use std::error::Error;
use std::ffi::{c_void, CStr, CString};
use std::fmt::{Display, Formatter};
use std::fs::{create_dir_all, File};
use std::fs::{create_dir_all, File, OpenOptions};
use std::io::{Cursor, Read, Write};
use std::os::raw::c_char;
use std::path::{Path, PathBuf};
@ -191,8 +191,8 @@ impl Pkg {
}
// Open destination file.
let mut file = match File::create(&path) {
Ok(v) => v,
let mut file = match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => file,
Err(e) => return Err(ExtractError::CreateEntryFailed(path, e)),
};
@ -389,12 +389,11 @@ impl Pkg {
status(status_name.as_ptr(), size, 0, ud);
// Open destination file.
let mut dest = std::fs::OpenOptions::new();
dest.create_new(true);
dest.write(true);
let mut dest = match dest.open(&output) {
let mut dest = match OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)
{
Ok(v) => v,
Err(e) => return Err(ExtractError::CreateFileFailed(output, e)),
};