diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11b031940..1765f9853 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,3 @@ -# CMake modules -include(ExternalProject) - # External dependencies. find_package(Qt6 COMPONENTS Widgets REQUIRED) find_package(Threads REQUIRED) @@ -33,6 +30,8 @@ add_executable(obliteration WIN32 MACOSX_BUNDLE main.cpp main_window.cpp path.cpp + pkg_extractor.cpp + pkg_installer.cpp progress_dialog.cpp resources.qrc settings.cpp diff --git a/src/core.hpp b/src/core.hpp index 4e2f453ee..40bb78481 100644 --- a/src/core.hpp +++ b/src/core.hpp @@ -4,12 +4,13 @@ #include #include +#include struct error; struct param; struct pkg; -typedef void (*pkg_extract_status_t) (const char *status, std::size_t current, std::size_t total, void *ud); +typedef void (*pkg_extract_status_t) (const char *status, std::size_t bar, std::uint64_t current, std::uint64_t total, void *ud); extern "C" { void error_free(error *err); @@ -171,7 +172,16 @@ class Pkg final { public: Pkg() : m_obj(nullptr) {} Pkg(const Pkg &) = delete; - ~Pkg() { close(); } + Pkg(Pkg &&other) + { + m_obj = other.m_obj; + other.m_obj = nullptr; + } + + ~Pkg() + { + close(); + } public: Pkg &operator=(const Pkg &) = delete; diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index cdf7b1e1a..7df8b2746 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["staticlib"] [dependencies] error = { path = "../error" } ftp = { path = "../ftp" } +humansize = "2.1.3" param = { path = "../param" } pkg = { path = "../pkg" } thiserror = "1.0" diff --git a/src/core/src/pkg.rs b/src/core/src/pkg.rs index afa8492f3..ff396be19 100644 --- a/src/core/src/pkg.rs +++ b/src/core/src/pkg.rs @@ -1,8 +1,10 @@ use error::Error; +use humansize::{SizeFormatter, DECIMAL}; use param::Param; -use pkg::Pkg; -use std::ffi::{c_char, c_void, CStr}; -use std::ptr::null_mut; +use pkg::{Pkg, PkgProgress}; +use std::ffi::{c_char, c_void, CStr, CString}; +use std::path::Path; +use std::ptr::{null, null_mut}; #[no_mangle] pub unsafe extern "C" fn pkg_open(file: *const c_char, error: *mut *mut Error) -> *mut Pkg { @@ -40,13 +42,97 @@ pub unsafe extern "C" fn pkg_get_param(pkg: &Pkg, error: *mut *mut Error) -> *mu pub unsafe extern "C" fn pkg_extract( pkg: &Pkg, dir: *const c_char, - status: extern "C" fn(*const c_char, usize, usize, *mut c_void), + status: extern "C" fn(*const c_char, usize, u64, u64, *mut c_void), ud: *mut c_void, ) -> *mut Error { - let dir = CStr::from_ptr(dir); + let root: &Path = CStr::from_ptr(dir).to_str().unwrap().as_ref(); + let progress = ExtractProgress { + status, + ud, + root, + total: 0, + progress: 0, + }; - match pkg.extract(dir.to_str().unwrap(), status, ud) { + match pkg.extract(root, progress) { Ok(_) => null_mut(), Err(e) => Error::new(e), } } + +struct ExtractProgress<'a> { + status: extern "C" fn(*const c_char, usize, u64, u64, *mut c_void), + ud: *mut c_void, + root: &'a Path, + total: u64, + progress: u64, +} + +impl<'a> PkgProgress for ExtractProgress<'a> { + fn entry_start(&mut self, path: &Path, current: usize, total: usize) { + let path = path.strip_prefix(self.root).unwrap(); + let log = format!("Extracting {}", path.display()); + let log = CString::new(log).unwrap(); + + (self.status)( + log.as_ptr(), + 0, + current.try_into().unwrap(), + total.try_into().unwrap(), + self.ud, + ); + } + + fn entries_completed(&mut self, total: usize) { + let total = total.try_into().unwrap(); + + (self.status)( + c"Entries extraction completed".as_ptr(), + 0, + total, + total, + self.ud, + ); + } + + fn pfs_start(&mut self, files: usize) { + self.total = files.try_into().unwrap(); + } + + fn pfs_directory(&mut self, path: &Path) { + let path = path.strip_prefix(self.root).unwrap(); + let log = format!("Creating {}", path.display()); + let log = CString::new(log).unwrap(); + + (self.status)(log.as_ptr(), 0, self.progress, self.total, self.ud); + (self.status)(null(), 1, 0, 0, self.ud); + + self.progress += 1; + } + + fn pfs_file(&mut self, path: &Path, len: u64) { + let path = path.strip_prefix(self.root).unwrap(); + let size = SizeFormatter::new(len, DECIMAL); + let log = format!("Extracting {} ({})", path.display(), size); + let log = CString::new(log).unwrap(); + + (self.status)(log.as_ptr(), 0, self.progress, self.total, self.ud); + (self.status)(null(), 1, 0, len, self.ud); + + self.progress += 1; + } + + fn pfs_write(&mut self, current: u64, len: u64) { + (self.status)(null(), 1, current, len, self.ud); + } + + fn pfs_completed(&mut self) { + (self.status)( + c"PFS extraction completed".as_ptr(), + 0, + self.total, + self.total, + self.ud, + ); + } +} diff --git a/src/kernel/src/fs/dev/vnode.rs b/src/kernel/src/fs/dev/vnode.rs index 710c2238d..8680229b5 100644 --- a/src/kernel/src/fs/dev/vnode.rs +++ b/src/kernel/src/fs/dev/vnode.rs @@ -2,7 +2,7 @@ use super::dirent::Dirent; use super::{AllocVnodeError, DevFs}; use crate::errno::{Errno, EIO, ENOENT, ENOTDIR, ENXIO}; use crate::fs::{ - check_access, Access, IoCmd, OpenFlags, RevokeFlags, VFile, Vnode, VnodeAttrs, VnodeItem, + check_access, Access, IoCmd, OpenFlags, RevokeFlags, VFileType, Vnode, VnodeAttrs, VnodeItem, VnodeType, }; use crate::process::VThread; @@ -163,32 +163,6 @@ impl crate::fs::VnodeBackend for VnodeBackend { } } - fn open( - &self, - vn: &Arc, - td: Option<&VThread>, - mode: OpenFlags, - file: Option<&mut VFile>, - ) -> Result<(), Box> { - if !vn.is_character() { - return Ok(()); - } - - // Not sure why FreeBSD check if vnode is VBLK because all of vnode here always be VCHR. - let item = vn.item(); - let Some(VnodeItem::Device(dev)) = item.as_ref() else { - unreachable!(); - }; - - // Execute switch handler. - dev.open(mode, 0x2000, td)?; - - // Set file OP. - let Some(file) = file else { return Ok(()) }; - - todo!() - } - fn revoke(&self, vn: &Arc, flags: RevokeFlags) -> Result<(), Box> { // TODO: Implement this. todo!() diff --git a/src/kernel/src/fs/host/vnode.rs b/src/kernel/src/fs/host/vnode.rs index df901e86e..087d7b296 100644 --- a/src/kernel/src/fs/host/vnode.rs +++ b/src/kernel/src/fs/host/vnode.rs @@ -1,7 +1,7 @@ use super::file::HostFile; use super::{GetVnodeError, HostFs}; use crate::errno::{Errno, EEXIST, EIO, ENOENT, ENOTDIR}; -use crate::fs::{Access, IoCmd, Mode, OpenFlags, VFile, Vnode, VnodeAttrs, VnodeType}; +use crate::fs::{Access, IoCmd, Mode, OpenFlags, VFileType, Vnode, VnodeAttrs, VnodeType}; use crate::process::VThread; use crate::ucred::{Gid, Uid}; use macros::Errno; @@ -123,17 +123,6 @@ impl crate::fs::VnodeBackend for VnodeBackend { Ok(vn) } - - #[allow(unused_variables)] // TODO: remove when implementing. - fn open( - &self, - vn: &Arc, - td: Option<&VThread>, - mode: OpenFlags, - file: Option<&mut VFile>, - ) -> Result<(), Box> { - todo!() - } } /// Represents an error when [`getattr()`] fails. diff --git a/src/kernel/src/fs/mod.rs b/src/kernel/src/fs/mod.rs index 97d21133f..7eff3955b 100644 --- a/src/kernel/src/fs/mod.rs +++ b/src/kernel/src/fs/mod.rs @@ -175,7 +175,13 @@ impl Fs { .lookup(path, true, td) .map_err(OpenError::LookupFailed)?; - todo!(); + let ty = if let Some(VnodeItem::Device(dev)) = vnode.item().as_ref() { + VFileType::Device(dev.clone()) + } else { + VFileType::Vnode(vnode.clone()) + }; + + Ok(VFile::new(ty)) } pub fn lookup( diff --git a/src/kernel/src/fs/null/vnode.rs b/src/kernel/src/fs/null/vnode.rs index a844e40a8..5b1af1b80 100644 --- a/src/kernel/src/fs/null/vnode.rs +++ b/src/kernel/src/fs/null/vnode.rs @@ -1,8 +1,7 @@ use crate::{ errno::{Errno, EISDIR, EROFS}, fs::{ - null::hash::NULL_HASHTABLE, perm::Access, Mount, MountFlags, OpenFlags, VFile, Vnode, - VnodeAttrs, VnodeType, + null::hash::NULL_HASHTABLE, perm::Access, Mount, MountFlags, Vnode, VnodeAttrs, VnodeType, }, process::VThread, }; @@ -90,21 +89,6 @@ impl crate::fs::VnodeBackend for VnodeBackend { Ok(vnode) } - - /// This function tries to mimic what calling `null_bypass` would do. - fn open( - &self, - _: &Arc, - td: Option<&VThread>, - mode: OpenFlags, - file: Option<&mut VFile>, - ) -> Result<(), Box> { - self.lower - .open(td, mode, file) - .map_err(OpenError::OpenFromLowerFailed)?; - - Ok(()) - } } /// See `null_nodeget` on the PS4 for a reference. diff --git a/src/kernel/src/fs/tmp/node.rs b/src/kernel/src/fs/tmp/node.rs index a8b67bb5f..7f373a306 100644 --- a/src/kernel/src/fs/tmp/node.rs +++ b/src/kernel/src/fs/tmp/node.rs @@ -1,5 +1,6 @@ +use super::{AllocVnodeError, TempFs}; use crate::errno::{Errno, ENOENT, ENOSPC}; -use crate::fs::{Access, OpenFlags, VFile, Vnode, VnodeAttrs, VnodeType}; +use crate::fs::{Access, OpenFlags, VFileType, Vnode, VnodeAttrs, VnodeType}; use crate::process::VThread; use gmtx::{Gutex, GutexGroup, GutexWriteGuard}; use macros::Errno; @@ -7,8 +8,6 @@ use std::collections::VecDeque; use std::sync::{Arc, RwLock}; use thiserror::Error; -use super::{AllocVnodeError, TempFs}; - /// A collection of [`Node`]. #[derive(Debug)] pub struct Nodes { @@ -202,17 +201,6 @@ impl crate::fs::VnodeBackend for VnodeBackend { Ok(vnode) } - - #[allow(unused_variables)] // TODO: remove when implementing - fn open( - &self, - vn: &Arc, - td: Option<&VThread>, - mode: OpenFlags, - #[allow(unused_variables)] file: Option<&mut VFile>, - ) -> Result<(), Box> { - todo!() - } } /// Represents an error when [`Nodes::alloc()`] fails. diff --git a/src/kernel/src/fs/vnode.rs b/src/kernel/src/fs/vnode.rs index 93c937352..211d5aaae 100644 --- a/src/kernel/src/fs/vnode.rs +++ b/src/kernel/src/fs/vnode.rs @@ -1,6 +1,6 @@ use super::{ unixify_access, Access, CharacterDevice, FileBackend, IoCmd, Mode, Mount, OpenFlags, - RevokeFlags, Stat, TruncateLength, Uio, UioMut, VFile, + RevokeFlags, Stat, TruncateLength, Uio, UioMut, VFile, VFileType, }; use crate::arnd; use crate::errno::{Errno, ENOTDIR, ENOTTY, EOPNOTSUPP, EPERM}; @@ -135,15 +135,6 @@ impl Vnode { self.backend.mkdir(self, name, mode, td) } - pub fn open( - self: &Arc, - td: Option<&VThread>, - mode: OpenFlags, - file: Option<&mut VFile>, - ) -> Result<(), Box> { - self.backend.open(self, td, mode, file) - } - pub fn revoke(self: &Arc, flags: RevokeFlags) -> Result<(), Box> { self.backend.revoke(self, flags) } @@ -300,17 +291,6 @@ pub(super) trait VnodeBackend: Debug + Send + Sync + 'static { Err(Box::new(DefaultError::NotSupported)) } - /// An implementation of `vop_open`. - fn open( - &self, - #[allow(unused_variables)] vn: &Arc, - #[allow(unused_variables)] td: Option<&VThread>, - #[allow(unused_variables)] mode: OpenFlags, - #[allow(unused_variables)] file: Option<&mut VFile>, - ) -> Result<(), Box> { - Ok(()) - } - /// An implementation of `vop_revoke`. fn revoke( &self, diff --git a/src/kernel/src/rtld/mod.rs b/src/kernel/src/rtld/mod.rs index d36bc73af..383802b42 100644 --- a/src/kernel/src/rtld/mod.rs +++ b/src/kernel/src/rtld/mod.rs @@ -461,13 +461,17 @@ impl RuntimeLinker { fn sys_dynlib_load_prx(self: &Arc, td: &VThread, i: &SysIn) -> Result { // Check if application is a dynamic SELF. - let mut bin = td.proc().bin_mut(); - let bin = bin.as_mut().ok_or(SysErr::Raw(EPERM))?; + let mut bin_guard = td.proc().bin_mut(); + let bin = bin_guard.as_mut().ok_or(SysErr::Raw(EPERM))?; + + let sdk_ver = bin.app().sdk_ver(); if bin.app().file_info().is_none() { return Err(SysErr::Raw(EPERM)); } + drop(bin_guard); + // Not sure what is this. Maybe kernel only flags? let mut flags: u32 = i.args[1].try_into().unwrap(); @@ -544,8 +548,7 @@ impl RuntimeLinker { let resolver = SymbolResolver::new( &mains, &globals, - bin.app().sdk_ver() >= 0x5000000 - || self.flags.read().contains(LinkerFlags::HAS_ASAN), + sdk_ver >= 0x5000000 || self.flags.read().contains(LinkerFlags::HAS_ASAN), ); self.init_dag(&md); diff --git a/src/main_window.cpp b/src/main_window.cpp index 7db21be9f..a501d590a 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -6,7 +6,7 @@ #include "game_settings_dialog.hpp" #include "log_formatter.hpp" #include "path.hpp" -#include "progress_dialog.hpp" +#include "pkg_installer.hpp" #include "settings.hpp" #include "string.hpp" @@ -239,109 +239,25 @@ void MainWindow::tabChanged() void MainWindow::installPkg() { // Browse a PKG. - auto pkgPath = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, "Install PKG", QString(), "PKG Files (*.pkg)")).toStdString(); + auto path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, "Install PKG", QString(), "PKG Files (*.pkg)")); - if (pkgPath.empty()) { + if (path.isEmpty()) { return; } - // Show dialog to display progress. - ProgressDialog progress("Install PKG", "Opening PKG...", this); + // Run installer. + PkgInstaller installer(readGamesDirectorySetting(), path, this); - // Open a PKG. - Pkg pkg; - Error error; - - pkg = pkg_open(pkgPath.c_str(), &error); - - if (!pkg) { - QMessageBox::critical(&progress, "Error", QString("Cannot open %1: %2").arg(pkgPath.c_str()).arg(error.message())); - return; - } - - // Get game ID. - Param param(pkg_get_param(pkg, &error)); - - if (!param) { - QMessageBox::critical(&progress, "Error", QString("Failed to get param.sfo from %1: %2").arg(pkgPath.c_str()).arg(error.message())); - return; - } - - // Create game directory. - auto gamesDirectory = readGamesDirectorySetting(); - - // Get Param information - auto appver = param.appver(); - auto category = param.category(); - auto shortContentId = param.shortContentId(); - auto title = param.title(); - auto titleId = param.titleId(); - - // 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; - } - - // Extract items. - progress.setWindowTitle(title); - - error = pkg_extract(pkg, directory.c_str(), [](const char *status, std::size_t current, std::size_t total, void *ud) { - auto progress = reinterpret_cast(ud); - - progress->setStatusText(status); - - if (current) { - progress->setValue(static_cast(current)); - } else { - progress->setValue(0); - progress->setMaximum(static_cast(total)); - } - }, &progress); - - pkg.close(); - progress.complete(); - - if (error) { - QMessageBox::critical(this, "Error", QString("Failed to extract %1: %2").arg(pkgPath.c_str()).arg(error.message())); + if (!installer.exec()) { return; } // Add to game list if new game. + auto &id = installer.gameId(); bool success = false; - if (!patchOrDlc) { - success = loadGame(titleId); + + if (!id.isEmpty()) { + success = loadGame(id); } else { success = true; } diff --git a/src/param/src/lib.rs b/src/param/src/lib.rs index 2e736d7ff..1e9e7429f 100644 --- a/src/param/src/lib.rs +++ b/src/param/src/lib.rs @@ -141,7 +141,7 @@ impl Param { 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)?); + category = Some(Self::read_utf8(&mut raw, i, format, len, 4)?); } b"CONTENT_ID" => { content_id = Some(Self::read_utf8(&mut raw, i, format, len, 48)?); diff --git a/src/pkg/src/lib.rs b/src/pkg/src/lib.rs index 7ca2234b8..1fe3f7e23 100644 --- a/src/pkg/src/lib.rs +++ b/src/pkg/src/lib.rs @@ -7,11 +7,9 @@ use param::Param; use sha2::Digest; use std::convert::Infallible; use std::error::Error; -use std::ffi::{c_void, CString}; use std::fmt::{Display, Formatter}; 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}; use thiserror::Error; @@ -79,13 +77,12 @@ impl Pkg { pub fn extract( &self, dir: impl AsRef, - status: extern "C" fn(*const c_char, usize, usize, *mut c_void), - ud: *mut c_void, + mut progress: impl PkgProgress, ) -> Result<(), ExtractError> { let dir = dir.as_ref(); - self.extract_entries(dir.join("sce_sys"), status, ud)?; - self.extract_pfs(dir, status, ud)?; + self.extract_entries(dir.join("sce_sys"), &mut progress)?; + self.extract_pfs(dir, &mut progress)?; Ok(()) } @@ -93,9 +90,10 @@ impl Pkg { fn extract_entries( &self, dir: impl AsRef, - status: extern "C" fn(*const c_char, usize, usize, *mut c_void), - ud: *mut c_void, + progress: &mut impl PkgProgress, ) -> Result<(), ExtractError> { + let dir = dir.as_ref(); + for num in 0..self.header.entry_count() { // Check offset. let offset = self.header.table_offset() + num * Entry::RAW_SIZE; @@ -108,7 +106,7 @@ impl Pkg { let entry = unsafe { Entry::read(raw) }; // Get file path. - let path = match entry.to_path(dir.as_ref()) { + let path = match entry.to_path(dir) { Some(v) => v, None => continue, }; @@ -132,9 +130,7 @@ impl Pkg { }; // Report status. - let name = CString::new(path.to_string_lossy().as_ref()).unwrap(); - - status(name.as_ptr(), num, self.header.entry_count(), ud); + progress.entry_start(&path, num, self.header.entry_count()); // Create a directory for destination file. let dir = path.parent().unwrap(); @@ -161,9 +157,7 @@ impl Pkg { // Report completion. if self.header.entry_count() != 0 { - let total = self.header.entry_count(); - - status(c"Entries extraction completed".as_ptr(), total, total, ud); + progress.entries_completed(self.header.entry_count()); } Ok(()) @@ -172,8 +166,7 @@ impl Pkg { fn extract_pfs( &self, dir: impl AsRef, - status: extern "C" fn(*const c_char, usize, usize, *mut c_void), - ud: *mut c_void, + progress: &mut impl PkgProgress, ) -> Result<(), ExtractError> { use pfs::directory::Item; @@ -247,18 +240,12 @@ impl Pkg { }; // Extract inner uroot. - let mut progress = 0; - - self.extract_directory(Vec::new(), uroot, dir, &mut |path| { - let path = CString::new(path.to_string_lossy().into_owned()).unwrap(); - - status(path.as_ptr(), progress, inodes, ud); - progress += 1; - })?; + if inodes != 0 { + let dir = dir.as_ref(); - // Report completion. - if progress != 0 { - status(c"PFS extraction completed".as_ptr(), inodes, inodes, ud); + progress.pfs_start(inodes); + self.extract_directory(dir, Vec::new(), uroot, dir, progress)?; + progress.pfs_completed(); } Ok(()) @@ -266,10 +253,11 @@ impl Pkg { fn extract_directory( &self, + root: &Path, path: Vec<&[u8]>, dir: pfs::directory::Directory, output: impl AsRef, - status: &mut impl FnMut(&Path), + progress: &mut impl PkgProgress, ) -> Result<(), ExtractError> { // Open PFS directory. let items = match dir.open() { @@ -325,19 +313,20 @@ impl Pkg { }; // Create output directory. - status(&output); + progress.pfs_directory(&output); if let Err(e) = create_dir_all(&output) { return Err(ExtractError::CreateDirectoryFailed(output, e)); } // Extract files. - self.extract_directory(path, i, &output, status)?; + self.extract_directory(root, path, i, &output, progress)?; meta } Item::File(mut file) => { // Construct metadata. + let len = file.len(); let meta = fs::Metadata { mode: file.mode().into(), atime: file.atime(), @@ -352,7 +341,7 @@ impl Pkg { gid: file.gid(), }; - status(&output); + progress.pfs_file(&output, len); // Open destination file. let mut dest = match OpenOptions::new() @@ -365,6 +354,8 @@ impl Pkg { }; // Copy. + let mut copied = 0u64; + loop { // Read source. let read = match file.read(&mut buffer) { @@ -389,6 +380,10 @@ impl Pkg { if let Err(e) = dest.write_all(&buffer[..read]) { return Err(ExtractError::WriteFileFailed(output, e)); } + + // Report progress. + copied += TryInto::::try_into(read).unwrap(); + progress.pfs_write(copied, len); } meta @@ -574,6 +569,16 @@ impl Pkg { } } +pub trait PkgProgress { + fn entry_start(&mut self, path: &Path, current: usize, total: usize); + fn entries_completed(&mut self, total: usize); + fn pfs_start(&mut self, files: usize); + fn pfs_directory(&mut self, path: &Path); + fn pfs_file(&mut self, path: &Path, len: u64); + fn pfs_write(&mut self, current: u64, len: u64); + fn pfs_completed(&mut self); +} + #[derive(Debug)] pub enum OpenError { OpenFailed(std::io::Error), diff --git a/src/pkg_extractor.cpp b/src/pkg_extractor.cpp new file mode 100644 index 000000000..412f9d2ed --- /dev/null +++ b/src/pkg_extractor.cpp @@ -0,0 +1,33 @@ +#include "pkg_extractor.hpp" + +PkgExtractor::PkgExtractor(Pkg &&pkg, std::string &&dst) : + m_pkg(std::move(pkg)), + m_dst(std::move(dst)) +{ +} + +PkgExtractor::~PkgExtractor() +{ +} + +void PkgExtractor::exec() +{ + Error e = pkg_extract( + m_pkg, + m_dst.c_str(), + [](const char *status, std::size_t bar, std::uint64_t current, std::uint64_t total, void *ud) { + reinterpret_cast(ud)->update(status, bar, current, total); + }, + this); + + if (e) { + emit finished(e.message()); + } else { + emit finished(QString()); + } +} + +void PkgExtractor::update(const char *status, std::size_t bar, std::uint64_t current, std::uint64_t total) +{ + emit statusChanged(status ? QString(status) : QString(), bar, current, total); +} diff --git a/src/pkg_extractor.hpp b/src/pkg_extractor.hpp new file mode 100644 index 000000000..e0406a281 --- /dev/null +++ b/src/pkg_extractor.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "core.hpp" + +class PkgExtractor final : public QObject { + Q_OBJECT +public: + PkgExtractor(Pkg &&pkg, std::string &&dst); + ~PkgExtractor() override; + +public slots: + void exec(); + +signals: + void statusChanged(const QString &status, std::size_t bar, std::uint64_t current, std::uint64_t total); + void finished(const QString &error); + +private: + void update(const char *status, std::size_t bar, std::uint64_t current, std::uint64_t total); + +private: + Pkg m_pkg; + std::string m_dst; +}; diff --git a/src/pkg_installer.cpp b/src/pkg_installer.cpp new file mode 100644 index 000000000..a0eabe9ce --- /dev/null +++ b/src/pkg_installer.cpp @@ -0,0 +1,240 @@ +#include "pkg_installer.hpp" +#include "core.hpp" +#include "path.hpp" +#include "pkg_extractor.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +PkgInstaller::PkgInstaller(const QString &games, const QString &pkg, QWidget *parent) : + QDialog(parent), + m_games(games), + m_pkg(pkg), + m_bar1(nullptr), + m_bar2(nullptr), + m_log(nullptr), + m_completed(false) +{ + auto layout = new QVBoxLayout(this); + + // Primary bar. + m_bar1 = new QProgressBar(); + m_bar1->setMaximum(0); + m_bar1->setTextVisible(false); + m_bar1->setMinimumWidth(500); + + layout->addWidget(m_bar1); + + // Secondary bar. + m_bar2 = new QProgressBar(); + m_bar2->setMaximum(0); + m_bar2->setTextVisible(false); + + layout->addWidget(m_bar2); + + // Log. + m_log = new QPlainTextEdit(); + m_log->setReadOnly(true); + m_log->setLineWrapMode(QPlainTextEdit::NoWrap); + m_log->setMinimumHeight(200); + +#ifdef _WIN32 + m_log->document()->setDefaultFont(QFont("Courier New", 10)); +#elif __APPLE__ + m_log->document()->setDefaultFont(QFont("menlo", 10)); +#else + m_log->document()->setDefaultFont(QFont("monospace", 10)); +#endif + + layout->addWidget(m_log); + + setWindowTitle("Install PKG"); +} + +PkgInstaller::~PkgInstaller() +{ +} + +int PkgInstaller::exec() +{ + // Show the dialog. + setModal(true); + show(); + + // Wait until the dialog is visible otherwise the user will see noting until the pkg_open is + // returned, which can take a couple of seconds. + while (!isVisible()) { + QCoreApplication::processEvents(); + } + + // Open a PKG. + Pkg pkg; + Error error; + + log(QString("Opening %1").arg(m_pkg)); + pkg = pkg_open(m_pkg.toStdString().c_str(), &error); + + if (!pkg) { + QMessageBox::critical(this, "Error", QString("Couldn't open %1: %2").arg(m_pkg).arg(error.message())); + return Rejected; + } + + // Get param.sfo. + Param param(pkg_get_param(pkg, &error)); + + if (!param) { + QMessageBox::critical(this, "Error", QString("Couldn't get param.sfo from %1: %2").arg(m_pkg).arg(error.message())); + return Rejected; + } + + // Get path to install. + auto id = param.titleId(); + auto directory = joinPath(m_games, id); + auto category = param.category(); + + if (category == "gp") { + directory.append("-PATCH-").append(param.appver().toStdString()); + } else if (category == "ac") { + // TODO: Add DLC support, short_content_id is most likely to be used. + QMessageBox::critical(this, "Error", "DLC PKG support is not yet implemented."); + return Rejected; + } else if (category != "gd") { + QMessageBox::critical(this, "Error", QString("Don't know how to install a PKG with category = %1.").arg(category)); + return Rejected; + } + + // Create game directory. + auto path = QString::fromStdString(directory); + + log(QString("Creating %1").arg(path)); + + if (!QDir().mkdir(path)) { + QMessageBox::critical(this, "Error", QString("Couldn't create %1").arg(path)); + return Rejected; + } + + setWindowTitle(param.title()); + + // Setup extractor. + QThread background; + QObject context; + QString fail; + auto finished = false; + auto extractor = new PkgExtractor(std::move(pkg), std::move(directory)); + + extractor->moveToThread(&background); + + connect(&background, &QThread::started, extractor, &PkgExtractor::exec); + connect(&background, &QThread::finished, extractor, &QObject::deleteLater); + connect(extractor, &PkgExtractor::statusChanged, this, &PkgInstaller::update); + connect(extractor, &PkgExtractor::finished, &context, [&](const QString &e) { + fail = e; + finished = true; + }); + + // Start extraction. + background.start(); + + while (!finished) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + + // Clean up. + background.quit(); + background.wait(); + + // Check if failed. + if (!fail.isEmpty()) { + QMessageBox::critical(this, "Error", QString("Failed to extract %1: %2").arg(m_pkg).arg(fail)); + return Rejected; + } + + // Close the dialog. + m_completed = true; + + close(); + + while (isVisible()) { + QCoreApplication::processEvents(); + } + + // Set success data. + if (category == "gd") { + m_gameId = std::move(id); + } + + return Accepted; +} + +void PkgInstaller::closeEvent(QCloseEvent *event) +{ + // Do not allow the user to close the dialog until completed. + if (!m_completed) { + event->ignore(); + } else { + QDialog::closeEvent(event); + } +} + +void PkgInstaller::keyPressEvent(QKeyEvent *event) +{ + // Do not allow the user to close the dialog until completed. + event->ignore(); +} + +void PkgInstaller::update(const QString &status, std::size_t bar, std::uint64_t current, std::uint64_t total) +{ + switch (bar) { + case 0: + if (current) { + m_bar1->setValue(static_cast(current)); + } else { + m_bar1->setValue(0); + m_bar1->setMaximum(static_cast(total)); + } + break; + case 1: + if (current) { + if (current == total) { + m_bar2->setValue(1000000); + } else { + auto scale = static_cast(current) / static_cast(total); + m_bar2->setValue(static_cast(scale * 1000000.0)); + } + } else if (total) { + m_bar2->setValue(0); + m_bar2->setMaximum(1000000); + } else { + m_bar2->setValue(0); + m_bar2->setMaximum(0); + } + break; + } + + if (status.isEmpty()) { + QCoreApplication::processEvents(); + } else { + log(status); + } +} + +void PkgInstaller::log(const QString &msg) +{ + auto scroll = m_log->verticalScrollBar(); + + m_log->appendPlainText(msg); + scroll->setValue(scroll->maximum()); + + QCoreApplication::processEvents(); +} diff --git a/src/pkg_installer.hpp b/src/pkg_installer.hpp new file mode 100644 index 000000000..85cad536d --- /dev/null +++ b/src/pkg_installer.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +class QPlainTextEdit; +class QProgressBar; + +class PkgInstaller final : public QDialog { +public: + PkgInstaller(const QString &games, const QString &pkg, QWidget *parent = nullptr); + ~PkgInstaller() override; + +public: + int exec() override; + +public: + const QString &gameId() const { return m_gameId; } + +protected: + void closeEvent(QCloseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + +private slots: + void update(const QString &status, std::size_t bar, std::uint64_t current, std::uint64_t total); + +private: + void log(const QString &msg); + +private: + QString m_games; + QString m_pkg; + QProgressBar *m_bar1; + QProgressBar *m_bar2; + QPlainTextEdit *m_log; + QString m_gameId; + bool m_completed; +}; diff --git a/src/system.cpp b/src/system.cpp index 00a39da7c..fc6e6130f 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -10,11 +10,6 @@ #include #include -static const char *MkpathTarget[] = { - "dev", - "mnt/app0" -}; - bool isSystemInitialized() { return isSystemInitialized(readSystemDirectorySetting()); @@ -41,16 +36,6 @@ bool initSystem(const QString &path, const QString &from, bool explicitDecryptio // Setup progress dialog. ProgressDialog progress("Initializing system", QString("Connecting to %1").arg(from), parent); - // Create empty directories. - QDir base(path); - - for (auto relative : MkpathTarget) { - if (!base.mkpath(relative)) { - QMessageBox::critical(parent, "Error", QString("Cannot create a directory inside %1").arg(path)); - return false; - } - } - // Setup the system downloader. QThread background; QObject context;