From 32c097c0556784662c07533e00fade046aa2397b Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Thu, 25 Apr 2024 01:49:04 +0200 Subject: [PATCH] [ci skip] Add GameCardImageDumpTask and FileWriter classes GameCardImageDumpTask is a derived class of DataTransferTask, and it's designed to dump a gamecard image using the options selected by the user (which must be passed from a GameCardImageDumpOptionsFrame object). It uses std::optional as its return type -- the idea behind this is to return error strings that may later be displayed by an ErrorFrame during the dump process (views not yet implemented). FileWriter is a class that encapsulates write operations to different storage mediums (SD card, USB host and UMS devices), based on the provided input path. It is used by GameCardImageDumpTask to painlessly write data to the right output storage without explicitly having to implement multiple code paths for all storage types as part of the actual dump code. Furthermore, FileWriter also supports writing split files to FAT-formatted UMS devices if an output file is >= 4 GiB -- part file handling is completely abstracted away from any callers. Other changes include: * AsyncTask: rename all class methods to use PascalCase naming. * AsyncTask: rename get() -> GetResult(). * DataTransferTask: reflect the changes made to AsyncTask. * DataTransferTask: pause the RepeatingTask right after LoopCallback() returns true instead of pausing it in the cancel/post-execute callbacks. * DataTransferTask: add private FormatTimeString() method. * DataTransferTask: remove superfluous override for DoInBackground() -- classes derived from DataTransferTask must provide it on their own, anyway. * DataTransferTask: add public GetDurationString() method. * defines: update FAT32_FILESIZE_LIMIT macro to use UINT32_MAX. * defines: add CONCATENATION_FILE_PART_SIZE macro (used by the new FileWriter class). * DownloadTask: reflect the changes made to AsyncTask. * DumpOptionsFrame: file extension is no longer stored as a class member, nor required by the class constructor. * DumpOptionsFrame: change the return type for GetOutputFilePath() to bool. The method now saves its output to a variable passed by reference. * GameCardImageDumpOptionsFrame: reflect the changes made to DumpOptionsFrame. * i18n: update localization strings where applicable. * nxdt_utils: fix a potential buffer overflow in utilsGetFileSystemStatsByPath(). * OptionsTab: reflect the changes made to AsyncTask. * usb: add const qualifier to the input buffer required by usbSendFileData(). * usb: add const qualifier to the input buffer required by usbSendNspHeader(). --- include/async_task.hpp | 90 ++--- include/core/usb.h | 4 +- include/data_transfer_task.hpp | 49 ++- include/defines.h | 3 +- include/download_task.hpp | 12 +- include/dump_options_frame.hpp | 8 +- include/file_writer.hpp | 96 +++++ include/gamecard_dump_tasks.hpp | 73 ++++ include/layered_error_frame.hpp | 2 + libs/libusbhsfs | 2 +- romfs/i18n/en-US/dump_options.json | 56 +-- romfs/i18n/en-US/generic.json | 15 +- romfs/i18n/en-US/options_tab.json | 7 +- romfs/i18n/en-US/tasks.json | 9 + romfs/i18n/en-US/utils.json | 22 ++ source/core/nxdt_utils.c | 2 +- source/core/usb.c | 6 +- source/dump_options_frame.cpp | 42 ++- source/exception_handler.cpp | 4 +- source/file_writer.cpp | 354 +++++++++++++++++++ source/gamecard_dump_tasks.cpp | 140 ++++++++ source/gamecard_image_dump_options_frame.cpp | 50 ++- source/gamecard_tab.cpp | 1 + source/options_tab.cpp | 34 +- 24 files changed, 901 insertions(+), 180 deletions(-) create mode 100644 include/file_writer.hpp create mode 100644 include/gamecard_dump_tasks.hpp create mode 100644 romfs/i18n/en-US/utils.json create mode 100644 source/file_writer.cpp create mode 100644 source/gamecard_dump_tasks.cpp diff --git a/include/async_task.hpp b/include/async_task.hpp index c25de8e..2bda542 100644 --- a/include/async_task.hpp +++ b/include/async_task.hpp @@ -34,10 +34,10 @@ namespace nxdt::tasks { /* Used by AsyncTask to throw exceptions whenever required. */ - class AsyncTaskException : std::exception + class AsyncTaskException: std::exception { public: - enum class eEx : int + enum class eEx: int { TaskIsAlreadyRunning, ///< Task is already running. TaskIsAlreadyFinished, ///< Task is already finished. @@ -53,7 +53,7 @@ namespace nxdt::tasks }; /* Used by AsyncTask to indicate the current status of the asynchronous task. */ - enum class AsyncTaskStatus : int + enum class AsyncTaskStatus: int { PENDING, ///< The task hasn't been executed yet. RUNNING, ///< The task is currently running. @@ -73,8 +73,8 @@ namespace nxdt::tasks bool m_cancelled = false, m_rethrowException = false; std::exception_ptr m_exceptionPtr{}; - /* Runs on the calling thread after doInBackground() finishes execution. */ - void finish(Result&& result) + /* Runs on the calling thread after DoInBackground() finishes execution. */ + void Finish(Result&& result) { std::lock_guard lock(this->m_mtx); @@ -85,11 +85,11 @@ namespace nxdt::tasks this->m_status = AsyncTaskStatus::FINISHED; /* Run appropiate post-execution callback. */ - if (this->isCancelled()) + if (this->IsCancelled()) { - this->onCancelled(this->m_result); + this->OnCancelled(this->m_result); } else { - this->onPostExecute(this->m_result); + this->OnPostExecute(this->m_result); } /* Rethrow asynchronous task exception (if available). */ @@ -104,10 +104,10 @@ namespace nxdt::tasks virtual ~AsyncTask() noexcept { /* Return right away if the task isn't running. */ - if (this->getStatus() != AsyncTaskStatus::RUNNING) return; + if (this->GetStatus() != AsyncTaskStatus::RUNNING) return; /* Cancel task. This won't do anything if it has already been cancelled. */ - this->cancel(); + this->Cancel(); /* Return right away if the result was already retrieved. */ if (!this->m_future.valid()) return; @@ -118,41 +118,41 @@ namespace nxdt::tasks } /* Asynchronous task function. */ - /* This function should periodically call isCancelled() to determine if it should end prematurely. */ - virtual Result doInBackground(const Params&... params) = 0; + /* This function should periodically call IsCancelled() to determine if it should end prematurely. */ + virtual Result DoInBackground(const Params&... params) = 0; /* Posts asynchronous task result. Runs on the asynchronous task thread. */ - virtual Result postResult(Result&& result) + virtual Result PostResult(Result&& result) { return std::move(result); } /* Cleanup function called if the task is cancelled. Runs on the calling thread. */ - virtual void onCancelled(const Result& result) { } + virtual void OnCancelled(const Result& result) { } /* Post-execution function called right after the task finishes. Runs on the calling thread. */ - virtual void onPostExecute(const Result& result) { } + virtual void OnPostExecute(const Result& result) { } /* Pre-execution function called right before the task starts. Runs on the calling thread. */ - virtual void onPreExecute(void) { } + virtual void OnPreExecute(void) { } /* Progress update function. Runs on the calling thread. */ - virtual void onProgressUpdate(const Progress& progress) { } + virtual void OnProgressUpdate(const Progress& progress) { } /* Stores the current progress inside the class. Runs on the asynchronous task thread. */ - virtual void publishProgress(const Progress& progress) + virtual void PublishProgress(const Progress& progress) { std::lock_guard lock(this->m_mtx); /* Don't proceed if the task isn't running. */ - if (this->getStatus() != AsyncTaskStatus::RUNNING || this->isCancelled()) return; + if (this->GetStatus() != AsyncTaskStatus::RUNNING || this->IsCancelled()) return; /* Update progress. */ this->m_progress = progress; } /* Returns the current progress. May run on both threads. */ - Progress getProgress(void) + Progress GetProgress(void) { std::lock_guard lock(this->m_mtx); return this->m_progress; @@ -162,25 +162,25 @@ namespace nxdt::tasks AsyncTask() = default; /* Cancels the task. Runs on the calling thread. */ - void cancel(void) noexcept + void Cancel(void) noexcept { std::lock_guard lock(this->m_mtx); /* Return right away if the task has already completed, or if it has already been cancelled. */ - if (this->getStatus() == AsyncTaskStatus::FINISHED || this->isCancelled()) return; + if (this->GetStatus() == AsyncTaskStatus::FINISHED || this->IsCancelled()) return; /* Update cancel flag. */ this->m_cancelled = true; } /* Starts the asynchronous task. Runs on the calling thread. */ - AsyncTask& execute(const Params&... params) + AsyncTask& Execute(const Params&... params) { /* Return right away if the task was cancelled before starting. */ - if (this->isCancelled()) return *this; + if (this->IsCancelled()) return *this; /* Verify task status. */ - switch(this->getStatus()) + switch(this->GetStatus()) { case AsyncTaskStatus::RUNNING: throw AsyncTaskException(AsyncTaskException::eEx::TaskIsAlreadyRunning); @@ -194,16 +194,16 @@ namespace nxdt::tasks this->m_status = AsyncTaskStatus::RUNNING; /* Run pre-execution callback. */ - this->onPreExecute(); + this->OnPreExecute(); /* Start asynchronous task on a new thread. */ this->m_future = std::async(std::launch::async, [this](const Params&... params) -> Result { /* Catch any exceptions thrown by the asynchronous task. */ try { - return this->postResult(this->doInBackground(params...)); + return this->PostResult(this->DoInBackground(params...)); } catch(...) { std::lock_guard lock(this->m_mtx); - this->cancel(); + this->Cancel(); this->m_rethrowException = true; this->m_exceptionPtr = std::current_exception(); } @@ -216,20 +216,20 @@ namespace nxdt::tasks /* Waits for the asynchronous task to complete, then returns its result. Runs on the calling thread. */ /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ - Result get(void) + Result GetResult(void) { - auto status = this->getStatus(); + auto status = this->GetStatus(); /* Throw an exception if the asynchronous task hasn't been executed. */ if (status == AsyncTaskStatus::PENDING) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsPending); /* If the task is still running, wait until it finishes. */ - /* get() calls wait() on its own if the result hasn't been retrieved. */ - /* finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ - if (status == AsyncTaskStatus::RUNNING) this->finish(this->m_future.get()); + /* std::future::get() calls std::future::wait() on its own if the result hasn't been retrieved. */ + /* Finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ + if (status == AsyncTaskStatus::RUNNING) this->Finish(this->m_future.get()); /* Throw an exception if the asynchronous task was cancelled. */ - if (this->isCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); + if (this->IsCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); /* Return result. */ return this->m_result; @@ -238,9 +238,9 @@ namespace nxdt::tasks /* Waits for at most the given time for the asynchronous task to complete, then returns its result. Runs on the calling thread. */ /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ template - Result get(const std::chrono::duration& timeout) + Result GetResult(const std::chrono::duration& timeout) { - auto status = this->getStatus(); + auto status = this->GetStatus(); /* Throw an exception if the asynchronous task hasn't been executed. */ if (status == AsyncTaskStatus::PENDING) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsPending); @@ -257,11 +257,11 @@ namespace nxdt::tasks throw AsyncTaskException(AsyncTaskException::eEx::TaskWaitTimeout); case std::future_status::ready: /* Retrieve the task result. */ - /* finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ - this->finish(this->m_future.get()); + /* Finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ + this->Finish(this->m_future.get()); /* Throw an exception if the asynchronous task was cancelled. */ - if (this->isCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); + if (this->IsCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); break; default: @@ -274,14 +274,14 @@ namespace nxdt::tasks } /* Returns the current task status. Runs on both threads. */ - AsyncTaskStatus getStatus(void) noexcept + AsyncTaskStatus GetStatus(void) noexcept { return this->m_status; } /* Returns true if the task was cancelled before it completed normally. May be used on both threads. */ /* Can be used by the asynchronous task to return prematurely. */ - bool isCancelled(void) noexcept + bool IsCancelled(void) noexcept { std::lock_guard lock(this->m_mtx); return this->m_cancelled; @@ -289,11 +289,11 @@ namespace nxdt::tasks /* Used by the calling thread to refresh the task progress, preferrably inside a loop. Returns true if the task finished. */ /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ - bool loopCallback(void) + bool LoopCallback(void) { std::lock_guard lock(this->m_mtx); - auto status = this->getStatus(); + auto status = this->GetStatus(); /* Return immediately if the task already finished. */ if (status == AsyncTaskStatus::FINISHED) return true; @@ -307,11 +307,11 @@ namespace nxdt::tasks { case std::future_status::timeout: /* Update progress. */ - this->onProgressUpdate(this->m_progress); + this->OnProgressUpdate(this->m_progress); break; case std::future_status::ready: /* Finish task. */ - this->finish(this->m_future.get()); + this->Finish(this->m_future.get()); return true; default: break; diff --git a/include/core/usb.h b/include/core/usb.h index 6d85bb7..33c45e0 100644 --- a/include/core/usb.h +++ b/include/core/usb.h @@ -71,7 +71,7 @@ bool usbSendNspProperties(u64 nsp_size, const char *filename, u32 nsp_header_siz /// Data chunk size must not exceed USB_TRANSFER_BUFFER_SIZE. /// If the last file data chunk is aligned to the endpoint max packet size, the host device should expect a Zero Length Termination (ZLT) packet. /// Calling this function if there's no remaining data to transfer will result in an error. -bool usbSendFileData(void *data, u64 data_size); +bool usbSendFileData(const void *data, u64 data_size); /// Used to gracefully cancel an ongoing file transfer. The current USB session is kept alive. void usbCancelFileTransfer(void); @@ -79,7 +79,7 @@ void usbCancelFileTransfer(void); /// Sends NSP header data to the host device, making it rewind the NSP file pointer to write this data, essentially finishing the NSP transfer process. /// Must be called after the data from all NSP file entries has been transferred using both usbSendNspProperties() and usbSendFileData() calls. /// If the NSP header size is aligned to the endpoint max packet size, the host device should expect a Zero Length Termination (ZLT) packet. -bool usbSendNspHeader(void *nsp_header, u32 nsp_header_size); +bool usbSendNspHeader(const void *nsp_header, u32 nsp_header_size); /// Informs the host device that an extracted filesystem dump (e.g. HFS, PFS, RomFS) is about to begin. bool usbStartExtractedFsDump(u64 extracted_fs_size, const char *extracted_fs_root_path); diff --git a/include/data_transfer_task.hpp b/include/data_transfer_task.hpp index 62429c6..efd8c3b 100644 --- a/include/data_transfer_task.hpp +++ b/include/data_transfer_task.hpp @@ -62,7 +62,12 @@ namespace nxdt::tasks void run(retro_time_t current_time) override final { brls::RepeatingTask::run(current_time); - if (this->task && !this->finished) this->finished = this->task->loopCallback(); + + if (this->task && !this->finished) + { + this->finished = this->task->LoopCallback(); + if (this->finished) this->pause(); + } } public: @@ -83,50 +88,51 @@ namespace nxdt::tasks SteadyTimePoint start_time{}, prev_time{}, end_time{}; size_t prev_xfer_size = 0; + ALWAYS_INLINE std::string FormatTimeString(double seconds) + { + return fmt::format("{:02.0F}H{:02.0F}M{:02.0F}S", std::fmod(seconds, 86400.0) / 3600.0, std::fmod(seconds, 3600.0) / 60.0, std::fmod(seconds, 60.0)); + } + protected: /* Set class as non-copyable and non-moveable. */ NON_COPYABLE(DataTransferTask); NON_MOVEABLE(DataTransferTask); - /* Make the background function overridable. */ - virtual Result doInBackground(const Params&... params) override = 0; - /* Runs on the calling thread. */ - void onCancelled(const Result& result) override final + void OnCancelled(const Result& result) override final { NX_IGNORE_ARG(result); /* Set end time. */ this->end_time = CurrentSteadyTimePoint(); - /* Pause task handler. */ - this->task_handler->pause(); - /* Unset long running process state. */ utilsSetLongRunningProcessState(false); } /* Runs on the calling thread. */ - void onPostExecute(const Result& result) override final + void OnPostExecute(const Result& result) override final { NX_IGNORE_ARG(result); /* Set end time. */ this->end_time = CurrentSteadyTimePoint(); - /* Fire task handler immediately to get the last result from AsyncTask::loopCallback(), then pause it. */ + /* Fire task handler immediately to make it store the last result from AsyncTask::LoopCallback(). */ + /* We do this here because all subscriptors to our progress event will most likely call IsFinished() to check if the task is complete. */ + /* That being the case, if the `finished` flag returned by the task handler isn't updated before the progress event subscriptors receive the last progress update, */ + /* they won't be able to determine if the task has already finished, leading to unsuspected consequences. */ this->task_handler->fireNow(); - this->task_handler->pause(); /* Update progress one last time. */ - this->onProgressUpdate(this->getProgress()); + this->OnProgressUpdate(this->GetProgress()); /* Unset long running process state. */ utilsSetLongRunningProcessState(false); } /* Runs on the calling thread. */ - void onPreExecute(void) override final + void OnPreExecute(void) override final { /* Set long running process state. */ utilsSetLongRunningProcessState(true); @@ -139,13 +145,13 @@ namespace nxdt::tasks } /* Runs on the calling thread. */ - void onProgressUpdate(const DataTransferProgress& progress) override final + void OnProgressUpdate(const DataTransferProgress& progress) override final { - AsyncTaskStatus status = this->getStatus(); + AsyncTaskStatus status = this->GetStatus(); /* Return immediately if there has been no progress at all, or if it the task has been cancelled. */ bool proceed = (progress.xfer_size > prev_xfer_size || (progress.xfer_size == prev_xfer_size && (!progress.total_size || progress.xfer_size >= progress.total_size))); - if (!proceed || this->isCancelled()) return; + if (!proceed || this->IsCancelled()) return; /* Calculate time difference between the last progress update and the current one. */ /* Return immediately if it's less than 1 second, but only if this isn't the last chunk; or if we don't know the total size and the task is still running . */ @@ -168,7 +174,7 @@ namespace nxdt::tasks /* Calculate remaining data size and ETA if we know the total size. */ double remaining = static_cast(progress.total_size - progress.xfer_size); double eta = (remaining / speed); - new_progress.eta = fmt::format("{:02.0F}H{:02.0F}M{:02.0F}S", std::fmod(eta, 86400.0) / 3600.0, std::fmod(eta, 3600.0) / 60.0, std::fmod(eta, 60.0)); + new_progress.eta = this->FormatTimeString(eta); } else { /* No total size means no ETA calculation, sadly. */ new_progress.eta = ""; @@ -205,7 +211,7 @@ namespace nxdt::tasks this->progress_event.unsubscribeAll(); } - /* Returns the last result from AsyncTask::loopCallback(). Runs on the calling thread. */ + /* Returns the last result from AsyncTask::LoopCallback(). Runs on the calling thread. */ ALWAYS_INLINE bool IsFinished(void) { return this->task_handler->IsFinished(); @@ -218,6 +224,13 @@ namespace nxdt::tasks return std::chrono::duration(this->IsFinished() ? (this->end_time - this->start_time) : (CurrentSteadyTimePoint() - this->start_time)).count(); } + /* Returns a human-readable string that represents the task duration. */ + /* If the task hasn't finished yet, the string represents the time that has passed since the task was started. */ + ALWAYS_INLINE std::string GetDurationString(void) + { + return this->FormatTimeString(this->GetDuration()); + } + ALWAYS_INLINE DataTransferProgressEvent::Subscription RegisterListener(DataTransferProgressEvent::Callback cb) { return this->progress_event.subscribe(cb); diff --git a/include/defines.h b/include/defines.h index 6e6a7c1..6301dba 100644 --- a/include/defines.h +++ b/include/defines.h @@ -71,7 +71,8 @@ #define SYSTEM_UPDATE_TID (u64)0x0100000000000816 #define QLAUNCH_TID (u64)0x0100000000001000 -#define FAT32_FILESIZE_LIMIT (u64)0xFFFFFFFF /* 4 GiB - 1 (4294967295 bytes). */ +#define FAT32_FILESIZE_LIMIT (u64)UINT32_MAX /* 4 GiB - 1 (4294967295 bytes). */ +#define CONCATENATION_FILE_PART_SIZE (u64)0xFFFF0000 /* 4 GiB - 65536 (4294901760 bytes). */ #define UTF8_BOM "\xEF\xBB\xBF" #define CRLF "\r\n" diff --git a/include/download_task.hpp b/include/download_task.hpp index 2aef901..d7dd756 100644 --- a/include/download_task.hpp +++ b/include/download_task.hpp @@ -55,7 +55,7 @@ namespace nxdt::tasks DownloadTask* task = static_cast*>(clientp); /* Don't proceed if we're dealing with an invalid task pointer, or if the task has been cancelled. */ - if (!task || task->isCancelled()) return 1; + if (!task || task->IsCancelled()) return 1; /* Fill struct. */ progress.total_size = static_cast(dltotal); @@ -63,13 +63,13 @@ namespace nxdt::tasks progress.percentage = (progress.total_size ? static_cast((progress.xfer_size * 100) / progress.total_size) : 0); /* Push progress onto the class. */ - task->publishProgress(progress); + task->PublishProgress(progress); return 0; } }; - /* Asynchronous task to download a file using an output path and a URL. */ + /* Asynchronous task used to download a file using an output path and a URL. */ class DownloadFileTask: public DownloadTask { protected: @@ -78,7 +78,7 @@ namespace nxdt::tasks NON_MOVEABLE(DownloadFileTask); /* Runs in the background thread. */ - bool doInBackground(const std::string& path, const std::string& url, const bool& force_https) override final + bool DoInBackground(const std::string& path, const std::string& url, const bool& force_https) override final { /* If the process fails or if it's cancelled, httpDownloadFile() will take care of closing the incomplete output file and deleting it. */ return httpDownloadFile(path.c_str(), url.c_str(), force_https, DownloadFileTask::HttpProgressCallback, this); @@ -88,7 +88,7 @@ namespace nxdt::tasks DownloadFileTask() = default; }; - /* Asynchronous task to store downloaded data into a dynamically allocated buffer using a URL. */ + /* Asynchronous task used to store downloaded data into a dynamically allocated buffer using a URL. */ /* The buffer returned by std::pair::first() must be manually freed by the calling function using free(). */ class DownloadDataTask: public DownloadTask { @@ -98,7 +98,7 @@ namespace nxdt::tasks NON_MOVEABLE(DownloadDataTask); /* Runs in the background thread. */ - DownloadDataResult doInBackground(const std::string& url, const bool& force_https) override final + DownloadDataResult DoInBackground(const std::string& url, const bool& force_https) override final { char *buf = nullptr; size_t buf_size = 0; diff --git a/include/dump_options_frame.hpp b/include/dump_options_frame.hpp index cfce7aa..3e159fb 100644 --- a/include/dump_options_frame.hpp +++ b/include/dump_options_frame.hpp @@ -36,7 +36,7 @@ namespace nxdt::views RootView *root_view = nullptr; private: - std::string storage_prefix{}, base_output_path{}, raw_filename{}, extension{}; + std::string storage_prefix{}, base_output_path{}, raw_filename{}; brls::List *list = nullptr; brls::InputListItem *filename = nullptr; @@ -55,15 +55,15 @@ namespace nxdt::views void UpdateStoragePrefix(u32 selected); protected: - DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension); - DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension); + DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename); + DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename); ~DumpOptionsFrame(); bool onCancel(void) override final; void addView(brls::View *view, bool fill = false); - const std::string GetOutputFilePath(void); + bool GetOutputFilePath(const std::string& extension, std::string& output); ALWAYS_INLINE brls::GenericEvent::Subscription RegisterButtonListener(brls::GenericEvent::Callback cb) { diff --git a/include/file_writer.hpp b/include/file_writer.hpp new file mode 100644 index 0000000..4ffe2fe --- /dev/null +++ b/include/file_writer.hpp @@ -0,0 +1,96 @@ +/* + * file_writer.hpp + * + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifndef __FILE_WRITER_HPP__ +#define __FILE_WRITER_HPP__ + +#include +#include + +#include "core/nxdt_utils.h" +#include "core/usb.h" + +namespace nxdt::utils +{ + /* Writes output files to different storage locations based on the provided input path. */ + /* It also handles file splitting in FAT-based UMS volumes. */ + class FileWriter + { + public: + /* Determines the output storage type used by this file. */ + typedef enum : u8 { + None = 0, + SdCard = 1, + UsbHost = 2, + UmsDevice = 3 + } StorageType; + + private: + std::string output_path{}; + size_t total_size = 0, cur_size = 0; + + u32 nsp_header_size = 0; + bool nsp_header_written = false; + + StorageType storage_type = StorageType::None; + + bool split_file = false, file_created = false, file_closed = false; + + FILE *fp = nullptr; + u8 split_file_part_cnt = 0, split_file_part_idx = 0; + size_t split_file_part_size = 0; + + std::optional CheckFreeSpace(void); + + void CloseCurrentFile(void); + + bool OpenNextFile(void); + + bool CreateInitialFile(void); + + protected: + /* Set class as non-copyable and non-moveable. */ + NON_COPYABLE(FileWriter); + NON_MOVEABLE(FileWriter); + + public: + FileWriter(const std::string& output_path, const size_t& total_size, const u32& nsp_header_size = 0); + ~FileWriter(); + + /* Writes data to the output file. */ + /* Takes care of seamlessly switching to a new part file if needed. */ + bool Write(const void *data, const size_t& data_size); + + /* Writes NSP header data to offset 0. */ + /* Only valid if dealing with a NSP file. */ + bool WriteNspHeader(const void *nsp_header, const u32& nsp_header_size); + + /* Closes the file and deletes it if it's incomplete (or if forcefully requested). */ + void Close(bool force_delete = false); + + /* Returns the storage type for this file. */ + StorageType GetStorageType(void); + }; +} + +#endif /* __FILE_WRITER_HPP__ */ diff --git a/include/gamecard_dump_tasks.hpp b/include/gamecard_dump_tasks.hpp new file mode 100644 index 0000000..af03fbb --- /dev/null +++ b/include/gamecard_dump_tasks.hpp @@ -0,0 +1,73 @@ +/* + * gamecard_dump_tasks.hpp + * + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifndef __GAMECARD_DUMP_TASKS_HPP__ +#define __GAMECARD_DUMP_TASKS_HPP__ + +#include +#include + +#include "data_transfer_task.hpp" + +namespace nxdt::tasks +{ + typedef std::optional GameCardDumpTaskError; + + class GameCardImageDumpTask: public DataTransferTask + { + private: + bool calculate_checksum = false; + u32 gc_img_crc = 0, full_gc_img_crc = 0; + std::mutex crc_mtx; + + protected: + /* Set class as non-copyable and non-moveable. */ + NON_COPYABLE(GameCardImageDumpTask); + NON_MOVEABLE(GameCardImageDumpTask); + + /* Runs in the background thread. */ + GameCardDumpTaskError DoInBackground(const std::string& output_path, const bool& prepend_key_area, const bool& keep_certificate, const bool& trim_dump, + const bool& calculate_checksum) override final; + + public: + GameCardImageDumpTask() = default; + + /* Returns the CRC32 calculated over the gamecard image. */ + /* Returns zero if checksum calculation wasn't enabled, if the task hasn't finished yet or if the task was cancelled. */ + ALWAYS_INLINE u32 GetImageChecksum(void) + { + std::scoped_lock lock(this->crc_mtx); + return ((this->calculate_checksum && this->IsFinished() && !this->IsCancelled()) ? this->gc_img_crc : 0); + } + + /* Returns the CRC32 calculated over the gamecard image with prepended key area data. */ + /* Returns zero if checksum calculation wasn't enabled, if the task hasn't finished yet or if the task was cancelled. */ + ALWAYS_INLINE u32 GetFullImageChecksum(void) + { + std::scoped_lock lock(this->crc_mtx); + return ((this->calculate_checksum && this->IsFinished() && !this->IsCancelled()) ? this->full_gc_img_crc : 0); + } + }; +} + +#endif /* __GAMECARD_DUMP_TASKS_HPP__ */ diff --git a/include/layered_error_frame.hpp b/include/layered_error_frame.hpp index 402c685..48e503d 100644 --- a/include/layered_error_frame.hpp +++ b/include/layered_error_frame.hpp @@ -29,6 +29,8 @@ namespace nxdt::views { /* Extended class to switch between ErrorFrame and List views on demand. */ + /* Intended to be used with lists that need to be updated dynamically based on runtime events. */ + /* Implements some hacky workarounds to prevent borealis from crashing and/or going out of focus. */ class LayeredErrorFrame: public brls::LayerView { private: diff --git a/libs/libusbhsfs b/libs/libusbhsfs index b1ff881..1a17488 160000 --- a/libs/libusbhsfs +++ b/libs/libusbhsfs @@ -1 +1 @@ -Subproject commit b1ff8811a762adf1b585b762b21308d458d43bfa +Subproject commit 1a17488663388e4fff6bdd0ee02fcf34244704ef diff --git a/romfs/i18n/en-US/dump_options.json b/romfs/i18n/en-US/dump_options.json index 88d7769..a58465c 100644 --- a/romfs/i18n/en-US/dump_options.json +++ b/romfs/i18n/en-US/dump_options.json @@ -12,31 +12,39 @@ "value_02": "{0} ({1} free / {2} total)" }, - "prepend_key_area": { - "label": "Prepend KeyArea data", - "description": "Prepends the full, 4 KiB long KeyArea block to the output XCI dump, which includes the InitialData area. XCI dumps with KeyArea data are also known as \"Full XCIs\". Disabled by default." + "gamecard": { + "image": { + "prepend_key_area": { + "label": "Prepend KeyArea data", + "description": "Prepends the full, 4 KiB long KeyArea block to the output XCI dump, which includes the InitialData area. XCI dumps with KeyArea data are also known as \"Full XCIs\". Disabled by default." + }, + + "keep_certificate": { + "label": "Keep certificate", + "description": "Preserves the gamecard certificate in the output XCI dump, which is used to unequivocally identify each individual gamecard. Disabled by default." + }, + + "trim_dump": { + "label": "Trim dump", + "description": "Trims the output XCI dump by removing padding data beyond the end of the last HFS partition. Disabled by default." + }, + + "calculate_checksum": { + "label": "Calculate checksum", + "description": "Calculates one or more CRC32 checksums over the dumped data, depending on the selected configuration. Checksums are useful to verify data integrity. Enabled by default." + }, + + "checksum_lookup_method": { + "label": "Checksum lookup method", + "description": "If \"{0}\" is enabled, this option determines which lookup method is used to validate the calculated CRC32 checksum at the end of the dump process.\n\nIf \"{1}\" is selected, the calculated checksum will be looked up in \"{2}\", which must have been previously downloaded.\n\nIf \"{3}\" is selected, the calculated checksum will be looked up using an Internet connection and a public HTTP endpoint.", + "value_00": "None" + } + } }, - "keep_certificate": { - "label": "Keep certificate", - "description": "Preserves the gamecard certificate in the output XCI dump, which is used to unequivocally identify each individual gamecard. Disabled by default." - }, + "start_dump": "Start dump", - "trim_dump": { - "label": "Trim dump", - "description": "Trims the output XCI dump by removing padding data beyond the end of the last HFS partition. Disabled by default." - }, - - "calculate_checksum": { - "label": "Calculate checksum", - "description": "Calculates one or more CRC32 checksums over the dumped data, depending on the selected configuration. Checksums are useful to verify data integrity. Enabled by default." - }, - - "checksum_lookup_method": { - "label": "Checksum lookup method", - "description": "If \"{0}\" is enabled, this option determines which lookup method is used to validate the calculated CRC32 checksum at the end of the dump process.\n\nIf \"{1}\" is selected, the calculated checksum will be looked up in \"{2}\", which must have been previously downloaded.\n\nIf \"{3}\" is selected, the calculated checksum will be looked up using an Internet connection and a public HTTP endpoint.", - "value_00": "None" - }, - - "start_dump": "Start dump" + "notifications": { + "get_output_path_error": "Failed to generate output path." + } } diff --git a/romfs/i18n/en-US/generic.json b/romfs/i18n/en-US/generic.json index dc4317d..6c16929 100644 --- a/romfs/i18n/en-US/generic.json +++ b/romfs/i18n/en-US/generic.json @@ -14,9 +14,16 @@ "exception_caught": "Exception caught! ({}).", "unknown_exception": "unknown", - "libnx_abort": "Fatal error triggered in libnx!\nError code: 0x{:08X}.", - "exception_triggered": "Fatal exception triggered!\nReason: {} (0x{:X}).", - "value_enabled": "Yes", - "value_disabled": "No" + "value_disabled": "No", + + "cancel": "Cancel", + "close": "Close", + + "mem_alloc_failed": "Failed to allocate memory buffer.", + + "process_cancelled": "Process cancelled.", + + "read": "read", + "write": "write" } diff --git a/romfs/i18n/en-US/options_tab.json b/romfs/i18n/en-US/options_tab.json index fd80e2a..319e139 100644 --- a/romfs/i18n/en-US/options_tab.json +++ b/romfs/i18n/en-US/options_tab.json @@ -34,15 +34,10 @@ } }, - "update_dialog": { - "cancel": "Cancel", - "close": "Close" - }, - "notifications": { "no_ums_devices": "No USB Mass Storage devices available.", "ums_device_unmount_success": "USB Mass Storage device successfully unmounted!", - "ums_device_unmount_failure": "Failed to unmount USB Mass Storage device!", + "ums_device_unmount_failed": "Failed to unmount USB Mass Storage device!", "no_internet_connection": "Internet connection unavailable. Unable to update.", "update_failed": "Update failed! Check the logfile for more info.", "nswdb_xml_updated": "NSWDB XML successfully updated!", diff --git a/romfs/i18n/en-US/tasks.json b/romfs/i18n/en-US/tasks.json index 35a1d28..3839cfb 100644 --- a/romfs/i18n/en-US/tasks.json +++ b/romfs/i18n/en-US/tasks.json @@ -1,4 +1,13 @@ { + "gamecard": { + "image": { + "get_size_failed": "Failed to retrieve gamecard image size.", + "get_security_info_failed": "Failed to retrieve gamecard security information.", + "write_key_area_failed": "Failed to write gamecard key area.", + "io_failed": "Failed to {0} 0x{1:X}-byte long gamecard block at offset 0x{2:X}." + } + }, + "notifications": { "gamecard_status_updated": "Gamecard status updated.", "gamecard_ejected": "Gamecard ejected.", diff --git a/romfs/i18n/en-US/utils.json b/romfs/i18n/en-US/utils.json new file mode 100644 index 0000000..d656616 --- /dev/null +++ b/romfs/i18n/en-US/utils.json @@ -0,0 +1,22 @@ +{ + "exception_handler": { + "libnx_abort": "Fatal error triggered in libnx!\nError code: 0x{:08X}.", + "exception_triggered": "Fatal exception triggered!\nReason: {} (0x{:X})." + }, + + "file_writer": { + "ums_device_info_error": "Failed to retrieve UMS device info.", + + "free_space_check": { + "retrieve_error": "Failed to retrieve free space from the selected storage.", + "insufficient_space_error": "File size exceeds the available free space ({} required)." + }, + + "initial_file": { + "usb_host_error": "Failed to send file details to USB host.", + "generic_error": "Failed to create output file." + }, + + "nsp_header_placeholder_error": "Failed to write placeholder NSP header." + } +} diff --git a/source/core/nxdt_utils.c b/source/core/nxdt_utils.c index ce10398..8828a79 100644 --- a/source/core/nxdt_utils.c +++ b/source/core/nxdt_utils.c @@ -756,7 +756,7 @@ bool utilsGetFileSystemStatsByPath(const char *path, u64 *out_total, u64 *out_fr } name_end += 2; - sprintf(stat_path, "%.*s", (int)(name_end - path), path); + snprintf(stat_path, MAX_ELEMENTS(stat_path), "%.*s", (int)(name_end - path), path); if ((ret = statvfs(stat_path, &info)) != 0) { diff --git a/source/core/usb.c b/source/core/usb.c index 32c9925..2e5c9d1 100644 --- a/source/core/usb.c +++ b/source/core/usb.c @@ -364,7 +364,7 @@ bool usbSendNspProperties(u64 nsp_size, const char *filename, u32 nsp_header_siz return ret; } -bool usbSendFileData(void *data, u64 data_size) +bool usbSendFileData(const void *data, u64 data_size) { bool ret = false; @@ -383,7 +383,7 @@ bool usbSendFileData(void *data, u64 data_size) /* Optimization for buffers that already are page aligned. */ if (IS_ALIGNED((u64)data, USB_TRANSFER_ALIGNMENT)) { - buf = data; + buf = (void*)data; } else { buf = g_usbTransferBuffer; memcpy(buf, data, data_size); @@ -478,7 +478,7 @@ void usbCancelFileTransfer(void) } } -bool usbSendNspHeader(void *nsp_header, u32 nsp_header_size) +bool usbSendNspHeader(const void *nsp_header, u32 nsp_header_size) { bool ret = false; diff --git a/source/dump_options_frame.cpp b/source/dump_options_frame.cpp index 974e9a4..d9892a8 100644 --- a/source/dump_options_frame.cpp +++ b/source/dump_options_frame.cpp @@ -26,8 +26,8 @@ using namespace i18n::literals; /* For _i18n. */ namespace nxdt::views { - DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension) : - brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename), extension(extension) + DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename) : + brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename) { /* Generate icon using the default image. */ brls::Image *icon = new brls::Image(); @@ -38,8 +38,8 @@ namespace nxdt::views this->Initialize(title, icon); } - DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension) : - brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename), extension(extension) + DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename) : + brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename) { /* Initialize the rest of the elements. */ this->Initialize(title, icon); @@ -77,7 +77,7 @@ namespace nxdt::views this->list->addView(this->filename); /* Output storage. */ - this->output_storage = new brls::SelectListItem("dump_options/output_storage/label"_i18n, { "dummy0", "dummy1" }, configGetInteger("output_storage"), brls::i18n::getStr("dump_options/output_storage/description", GITHUB_REPOSITORY_URL)); + this->output_storage = new brls::SelectListItem("dump_options/output_storage/label"_i18n, { "dummy0", "dummy1" }, configGetInteger("output_storage"), i18n::getStr("dump_options/output_storage/description", GITHUB_REPOSITORY_URL)); this->output_storage->getValueSelectedEvent()->subscribe([this](int selected) { /* Make sure the current value isn't out of bounds. */ @@ -152,21 +152,21 @@ namespace nxdt::views } u64 total_sz = 0, free_sz = 0; - char total_sz_str[64] = {0}, free_sz_str[64] = {0}; + char total_sz_str[0x40] = {0}, free_sz_str[0x40] = {0}; const nxdt::tasks::UmsDeviceVectorEntry *ums_device_entry = (i >= ConfigOutputStorage_Count ? &(ums_devices.at(i - ConfigOutputStorage_Count)) : nullptr); const UsbHsFsDevice *cur_ums_device = (ums_device_entry ? ums_device_entry->first : nullptr); sprintf(total_sz_str, "%s/", cur_ums_device ? cur_ums_device->name : DEVOPTAB_SDMC_DEVICE); utilsGetFileSystemStatsByPath(total_sz_str, &total_sz, &free_sz); - utilsGenerateFormattedSizeString(total_sz, total_sz_str, sizeof(total_sz_str)); - utilsGenerateFormattedSizeString(free_sz, free_sz_str, sizeof(free_sz_str)); + utilsGenerateFormattedSizeString(static_cast(total_sz), total_sz_str, sizeof(total_sz_str)); + utilsGenerateFormattedSizeString(static_cast(free_sz), free_sz_str, sizeof(free_sz_str)); if (cur_ums_device) { - storages.push_back(brls::i18n::getStr("dump_options/output_storage/value_02", ums_device_entry->second, free_sz_str, total_sz_str)); + storages.push_back(i18n::getStr("dump_options/output_storage/value_02", ums_device_entry->second, free_sz_str, total_sz_str)); } else { - storages.push_back(brls::i18n::getStr("dump_options/output_storage/value_00", free_sz_str, total_sz_str)); + storages.push_back(i18n::getStr("dump_options/output_storage/value_00", free_sz_str, total_sz_str)); } } @@ -219,28 +219,32 @@ namespace nxdt::views this->list->addView(view, fill); } - const std::string DumpOptionsFrame::GetOutputFilePath(void) + bool DumpOptionsFrame::GetOutputFilePath(const std::string& extension, std::string& output) { - std::string output = this->storage_prefix; + std::string tmp = this->storage_prefix; u32 selected = this->output_storage->getSelectedValue(); char *sanitized_path = nullptr; if (selected == ConfigOutputStorage_SdCard || selected >= ConfigOutputStorage_Count) { /* Remove the trailing path separator (if available) and append the application's base path if we're dealing with an SD card or a UMS device. */ - if (output.back() == '/') output.pop_back(); - output += APP_BASE_PATH; + if (tmp.back() == '/') tmp.pop_back(); + tmp += APP_BASE_PATH; } /* Append a path separator, if needed. */ - if (output.back() != '/' && this->base_output_path.front() != '/') output.push_back('/'); + if (tmp.back() != '/' && this->base_output_path.front() != '/') tmp.push_back('/'); /* Append the base output path string. */ - output += this->base_output_path; + tmp += this->base_output_path; /* Generate the sanitized file path. */ - sanitized_path = utilsGeneratePath(output.c_str(), this->filename->getValue().c_str(), this->extension.c_str()); - if (!sanitized_path) throw fmt::format("Failed to generate sanitized file path."); + sanitized_path = utilsGeneratePath(tmp.c_str(), this->filename->getValue().c_str(), extension.c_str()); + if (!sanitized_path) + { + brls::Application::notify("dump_options/notifications/get_output_path_error"_i18n); + return false; + } /* Update output. */ output = std::string(sanitized_path); @@ -248,6 +252,6 @@ namespace nxdt::views /* Free sanitized path. */ free(sanitized_path); - return output; + return true; } } diff --git a/source/exception_handler.cpp b/source/exception_handler.cpp index b8dff9e..9a0ca54 100644 --- a/source/exception_handler.cpp +++ b/source/exception_handler.cpp @@ -123,7 +123,7 @@ extern "C" { LOG_MSG_ERROR("*** libnx aborted with error code: 0x%X ***", res); /* Abort program execution. */ - std::string crash_str = (g_borealisInitialized ? i18n::getStr("generic/libnx_abort"_i18n, res) : fmt::format("Fatal error triggered in libnx!\nError code: 0x{:08X}.", res)); + std::string crash_str = (g_borealisInitialized ? i18n::getStr("utils/exception_handler/libnx_abort"_i18n, res) : fmt::format("Fatal error triggered in libnx!\nError code: 0x{:08X}.", res)); nxdt::utils::AbortProgramExecution(crash_str); } @@ -223,7 +223,7 @@ extern "C" { #endif /* LOG_LEVEL < LOG_LEVEL_NONE */ /* Abort program execution. */ - crash_str = (g_borealisInitialized ? i18n::getStr("generic/exception_triggered"_i18n, error_desc_str, ctx->error_desc) : \ + crash_str = (g_borealisInitialized ? i18n::getStr("utils/exception_handler/exception_triggered"_i18n, error_desc_str, ctx->error_desc) : \ fmt::format("Fatal exception triggered!\nReason: {} (0x{:X}).", error_desc_str, ctx->error_desc)); crash_str += (fmt::format("\nPC: 0x{:X}", ctx->pc.x) + (IS_HB_ADDR(ctx->pc.x) ? fmt::format(" (BASE + 0x{:X}).", ctx->pc.x - info.addr) : ".")); nxdt::utils::AbortProgramExecution(crash_str); diff --git a/source/file_writer.cpp b/source/file_writer.cpp new file mode 100644 index 0000000..1041eb0 --- /dev/null +++ b/source/file_writer.cpp @@ -0,0 +1,354 @@ +/* + * file_writer.cpp + * + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +namespace i18n = brls::i18n; /* For getStr(). */ +using namespace i18n::literals; /* For _i18n. */ + +namespace nxdt::utils +{ + FileWriter::FileWriter(const std::string& output_path, const size_t& total_size, const u32& nsp_header_size) : output_path(output_path), total_size(total_size), nsp_header_size(nsp_header_size) + { + /* Determine the storage device based on the input path. */ + this->storage_type = (this->output_path.starts_with(DEVOPTAB_SDMC_DEVICE) ? StorageType::SdCard : + (this->output_path.starts_with('/') ? StorageType::UsbHost : StorageType::UmsDevice)); + + if (this->storage_type != StorageType::UsbHost) + { + if (this->storage_type == StorageType::SdCard) + { + /* Always split big files if we're dealing with the SD card. */ + this->split_file = (this->total_size > FAT32_FILESIZE_LIMIT); + } else { + /* Get UMS device info. */ + UsbHsFsDevice ums_device{}; + if (!usbHsFsGetDeviceByPath(this->output_path.c_str(), &ums_device)) throw "utils/file_writer/ums_device_info_error"_i18n; + + /* Determine if we should split the output file based on the UMS device's filesystem type. */ + this->split_file = (this->total_size > FAT32_FILESIZE_LIMIT && ums_device.fs_type < UsbHsFsDeviceFileSystemType_exFAT); + + /* Calculate the number of part files we'll need, if applicable. */ + if (this->split_file) this->split_file_part_cnt = static_cast(ceil(static_cast(this->total_size) / static_cast(CONCATENATION_FILE_PART_SIZE))); + } + } + + /* Check free space. */ + if (auto chk = this->CheckFreeSpace()) throw *chk; + + /* Create initial file. */ + if (!this->CreateInitialFile()) + { + this->Close(true); + throw (this->storage_type == StorageType::UsbHost ? "utils/file_writer/initial_file/usb_host_error"_i18n : "utils/file_writer/initial_file/generic_error"_i18n); + } + + /* Handle NSP header placeholder if a NSP header size was provided. */ + if (this->nsp_header_size) + { + if (this->storage_type != StorageType::UsbHost) + { + /* Write placeholder NSP header. */ + std::vector zeroes(static_cast(this->nsp_header_size), 0); + if (!this->Write(zeroes.data(), zeroes.size())) + { + zeroes.clear(); + this->Close(true); + throw "utils/file_writer/nsp_header_placeholder_error"_i18n; + } + } + + /* Manually adjust current file offset. */ + this->cur_size += this->nsp_header_size; + } + } + + FileWriter::~FileWriter() + { + this->Close(); + } + + std::optional FileWriter::CheckFreeSpace(void) + { + u64 free_space = 0; + + /* Short-circuit: don't perform any free space check if we're dealing with a USB host or if the file size is zero. */ + if (this->storage_type == StorageType::UsbHost || !this->total_size) return {}; + + /* Retrieve free space from the target storage device. */ + if (!utilsGetFileSystemStatsByPath(this->output_path.c_str(), nullptr, &free_space)) return "utils/file_writer/free_space_check/retrieve_error"_i18n; + + /* Perform the actual free space check. */ + bool ret = (free_space > this->total_size); + if (!ret) + { + char needed_size_str[0x40] = {0}; + utilsGenerateFormattedSizeString(static_cast(this->total_size), needed_size_str, sizeof(needed_size_str)); + return i18n::getStr("utils/file_writer/free_space_check/insufficiente_space_error", needed_size_str); + } + + return {}; + } + + void FileWriter::CloseCurrentFile(void) + { + if (this->fp) + { + fclose(this->fp); + this->fp = nullptr; + } + } + + bool FileWriter::OpenNextFile(void) + { + /* Return immediately if: */ + /* 1. We're dealing with a USB host. */ + /* 2. We already created the initial file and we're dealing with the SD card. */ + /* 3. We already created the initial file and we're dealing with a UMS device and: */ + /* 3.1. We're not dealing with a split file. */ + /* 3.2. We already hit the total number of part files. */ + /* 3.3. The size for the current part file has not reached its limit. */ + if (this->storage_type == StorageType::UsbHost || (this->file_created && (this->storage_type == StorageType::SdCard || + (this->storage_type == StorageType::UmsDevice && (!this->split_file || this->split_file_part_idx >= this->split_file_part_cnt || + this->split_file_part_size < CONCATENATION_FILE_PART_SIZE))))) return true; + + /* Close current file. */ + this->CloseCurrentFile(); + + if (this->storage_type == StorageType::SdCard || !this->split_file) + { + /* Open file using the provided output path, but only if we're dealing with the SD card or if we're not dealing with a split file. */ + this->fp = fopen(this->output_path.c_str(), "wb"); + } else { + /* Open current part file. */ + std::string part_file_path = fmt::format("{}/{:02u}", this->output_path, this->split_file_part_idx); + this->fp = fopen(part_file_path.c_str(), "wb"); + if (this->fp) + { + /* Update part file index. */ + this->split_file_part_idx++; + + /* Reset part file size. */ + this->split_file_part_size = 0; + } + } + + /* Return immediately if we couldn't open the file in creation mode. */ + if (!this->fp) return false; + + /* Close file and return immediately if we're dealing with an empty file. */ + if (!this->file_created && !this->total_size) + { + this->CloseCurrentFile(); + return true; + } + + /* Disable file stream buffering. */ + setvbuf(this->fp, nullptr, _IONBF, 0); + + /* Truncate file to the right size. */ + off_t truncate_size = 0; + + if (this->storage_type == StorageType::SdCard || !this->split_file) + { + truncate_size = static_cast(this->total_size); + } else { + if (this->split_file_part_idx < this->split_file_part_cnt) + { + truncate_size = static_cast(CONCATENATION_FILE_PART_SIZE); + } else { + truncate_size = static_cast(this->total_size - (CONCATENATION_FILE_PART_SIZE * static_cast(this->split_file_part_cnt - 1))); + } + } + + ftruncate(fileno(this->fp), truncate_size); + + return true; + } + + bool FileWriter::CreateInitialFile(void) + { + /* Don't proceed if the file has already been created. */ + if (this->file_created) return true; + + const char *output_path_str = this->output_path.c_str(); + + if (this->storage_type == StorageType::UsbHost) + { + /* Send file properties to USB host. */ + if ((!this->nsp_header_size && !usbSendFileProperties(this->total_size, output_path_str)) || + (this->nsp_header_size && !usbSendNspProperties(this->total_size, output_path_str, this->nsp_header_size))) return false; + } else { + /* Check if we're supposed to create a split file. */ + if (this->split_file) + { + if (this->storage_type == StorageType::SdCard) + { + /* Create directory tree. */ + utilsCreateDirectoryTree(output_path_str, false); + + /* Create concatenation file on the SD card. */ + if (!utilsCreateConcatenationFile(output_path_str)) return false; + } else { + /* Create directory tree, including the last path element. */ + /* We'll handle split file creation ourselves at a later point. */ + utilsCreateDirectoryTree(output_path_str, true); + } + } else { + /* Create directory tree. */ + utilsCreateDirectoryTree(output_path_str, false); + } + + /* Open initial file. */ + if (!this->OpenNextFile()) return false; + } + + /* Update flag. */ + this->file_created = true; + + return true; + } + + bool FileWriter::Write(const void *data, const size_t& data_size) + { + /* Sanity check. */ + if (!data || !data_size || !this->file_created || this->cur_size >= this->total_size || (this->storage_type != StorageType::UsbHost && !this->fp)) return false; + + /* Make sure we don't write past the established file size. */ + size_t write_size = ((this->cur_size + data_size) > this->total_size ? (this->total_size - this->cur_size) : data_size); + + if (this->storage_type == StorageType::UmsDevice && this->split_file) + { + /* Switch to the next part file if we need to. */ + if (this->split_file_part_size >= CONCATENATION_FILE_PART_SIZE && !this->OpenNextFile()) return false; + + /* Make sure we don't write past the part file size limit. */ + size_t part_file_write_size = ((this->split_file_part_size + write_size) > CONCATENATION_FILE_PART_SIZE ? (CONCATENATION_FILE_PART_SIZE - this->split_file_part_size) : write_size); + + /* Write data to current part file. */ + if (fwrite(data, 1, part_file_write_size, this->fp) != part_file_write_size) return false; + + /* Update part file size. */ + this->split_file_part_size += part_file_write_size; + + /* Update the written data size. */ + this->cur_size += part_file_write_size; + + /* Write the rest of the data to the next part file if we need to. */ + if (part_file_write_size < write_size && !this->Write(static_cast(data) + part_file_write_size, write_size - part_file_write_size)) return false; + } else { + if (this->storage_type == StorageType::UsbHost) + { + /* Send data to USB host. */ + if (!usbSendFileData(data, write_size)) return false; + } else { + /* Write data to output file. */ + if (fwrite(data, 1, write_size, this->fp) != write_size) return false; + } + + /* Update the written data size. */ + this->cur_size += write_size; + } + + return true; + } + + bool FileWriter::WriteNspHeader(const void *nsp_header, const u32& nsp_header_size) + { + /* Sanity check. */ + if (!nsp_header || !nsp_header_size || nsp_header_size != this->nsp_header_size || !this->file_created || this->cur_size < this->total_size || this->nsp_header_written || + (this->storage_type != StorageType::UsbHost && !this->fp)) return false; + + if (this->storage_type == StorageType::UmsDevice && this->split_file) + { + /* Close current file. */ + this->CloseCurrentFile(); + + /* Open first part file. */ + std::string part_file_path = fmt::format("{}/00", this->output_path); + this->fp = fopen(part_file_path.c_str(), "rb+"); + if (!this->fp) return false; + } + + if (this->storage_type == StorageType::UsbHost) + { + /* Send NSP header to USB host. */ + if (!usbSendNspHeader(nsp_header, this->nsp_header_size)) return false; + } else { + /* Seek to the beginning of the file stream. */ + rewind(this->fp); + + /* Write NSP header. */ + if (fwrite(nsp_header, 1, this->nsp_header_size, this->fp) != this->nsp_header_size) return false; + + /* Close current file. */ + this->CloseCurrentFile(); + } + + /* Update flag. */ + this->nsp_header_written = true; + + return true; + } + + void FileWriter::Close(bool force_delete) + { + /* Return immediately if the file has already been closed. */ + if (this->file_closed) return; + + /* Close current file. */ + this->CloseCurrentFile(); + + /* Delete created file(s), if needed. */ + if (this->cur_size != this->total_size && (this->file_created || force_delete)) + { + if (this->storage_type == StorageType::UsbHost) + { + usbCancelFileTransfer(); + } else { + const char *output_path_str = this->output_path.c_str(); + + if (this->storage_type == StorageType::SdCard) + { + utilsRemoveConcatenationFile(output_path_str); + } else { + if (this->split_file) + { + utilsDeleteDirectoryRecursively(output_path_str); + } else { + remove(output_path_str); + } + } + } + } + + /* Commit SD card filesystem changes, if needed. */ + if (this->storage_type == StorageType::SdCard) utilsCommitSdCardFileSystemChanges(); + + /* Update flag. */ + this->file_closed = true; + } + + FileWriter::StorageType FileWriter::GetStorageType(void) + { + return this->storage_type; + } +} diff --git a/source/gamecard_dump_tasks.cpp b/source/gamecard_dump_tasks.cpp new file mode 100644 index 0000000..7811271 --- /dev/null +++ b/source/gamecard_dump_tasks.cpp @@ -0,0 +1,140 @@ +/* + * gamecard_dump_tasks.cpp + * + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +namespace i18n = brls::i18n; /* For getStr(). */ +using namespace i18n::literals; /* For _i18n. */ + +#define BLOCK_SIZE 0x800000 + +namespace nxdt::tasks +{ + GameCardDumpTaskError GameCardImageDumpTask::DoInBackground(const std::string& output_path, const bool& prepend_key_area, const bool& keep_certificate, const bool& trim_dump, + const bool& calculate_checksum) + { + std::scoped_lock lock(this->crc_mtx); + + GameCardKeyArea gc_key_area{}; + GameCardSecurityInformation gc_security_information{}; + + u32 gc_key_area_crc = 0; + size_t gc_img_size = 0; + + nxdt::utils::FileWriter *file = nullptr; + void *buf = nullptr; + + DataTransferProgress progress{}; + + this->calculate_checksum = calculate_checksum; + + LOG_MSG_DEBUG("Starting dump with parameters:\n- Output path: \"%s\".\n- Prepend key area: %u.\n- Keep certificate: %u.\n- Trim dump: %u.\n- Calculate checksum: %u.", \ + output_path.c_str(), prepend_key_area, keep_certificate, trim_dump, calculate_checksum); + + /* Retrieve gamecard image size. */ + if ((!trim_dump && !gamecardGetTotalSize(&gc_img_size)) || (trim_dump && !gamecardGetTrimmedSize(&gc_img_size)) || !gc_img_size) return "tasks/gamecard/image/get_size_failed"_i18n; + + /* Check if we're supposed to prepend the key area to the gamecard image. */ + if (prepend_key_area) + { + /* Update gamecard image size. */ + gc_img_size += sizeof(GameCardKeyArea); + + /* Retrieve the GameCardSecurityInformation area. */ + if (!gamecardGetSecurityInformation(&gc_security_information)) return "tasks/gamecard/image/get_security_info_failed"_i18n; + + /* Copy the GameCardInitialData area from the GameCardSecurityInformation area to our GameCardKeyArea object. */ + memcpy(&(gc_key_area.initial_data), &(gc_security_information.initial_data), sizeof(GameCardInitialData)); + + if (calculate_checksum) + { + /* Update gamecard image checksum if we're prepending the key area to it. */ + gc_key_area_crc = crc32Calculate(&gc_key_area, sizeof(GameCardKeyArea)); + this->full_gc_img_crc = gc_key_area_crc; + } + } + + /* Open output file. */ + try { + file = new nxdt::utils::FileWriter(output_path, gc_img_size); + } catch(const std::string& msg) { + return msg; + } + + ON_SCOPE_EXIT { delete file; }; + + /* Push progress onto the class. */ + progress.total_size = gc_img_size; + this->PublishProgress(progress); + + if (prepend_key_area) + { + /* Write GameCardKeyArea object. */ + if (!file->Write(&gc_key_area, sizeof(GameCardKeyArea))) return "tasks/gamecard/image/write_key_area_failed"_i18n; + + /* Push progress onto the class. */ + progress.xfer_size = sizeof(GameCardKeyArea); + this->PublishProgress(progress); + } + + /* Allocate memory buffer for the dump process. */ + buf = usbAllocatePageAlignedBuffer(BLOCK_SIZE); + if (!buf) return "generic/mem_alloc_failed"_i18n; + + ON_SCOPE_EXIT { free(buf); }; + + /* Dump gamecard image. */ + for(size_t offset = 0, blksize = BLOCK_SIZE; offset < gc_img_size; offset += blksize) + { + /* Don't proceed if the task has been cancelled. */ + if (this->IsCancelled()) return "generic/process_cancelled"_i18n; + + /* Adjust current block size, if needed. */ + if (blksize > (gc_img_size - offset)) blksize = (gc_img_size - offset); + + /* Read current block. */ + if (!gamecardReadStorage(buf, blksize, offset)) return i18n::getStr("tasks/gamecard/image/io_failed", "generic/read"_i18n, blksize, offset); + + /* Remove certificate, if needed. */ + if (!keep_certificate && offset == 0) memset(static_cast(buf) + GAMECARD_CERT_OFFSET, 0xFF, sizeof(FsGameCardCertificate)); + + /* Update image checksum. */ + if (calculate_checksum) + { + this->gc_img_crc = crc32CalculateWithSeed(this->gc_img_crc, buf, blksize); + if (prepend_key_area) this->full_gc_img_crc = crc32CalculateWithSeed(this->full_gc_img_crc, buf, blksize); + } + + /* Write current block. */ + if (!file->Write(buf, blksize)) return i18n::getStr("tasks/gamecard/image/io_failed", "generic/write"_i18n, blksize, offset); + + /* Push progress onto the class. */ + progress.xfer_size += blksize; + progress.percentage = static_cast((progress.xfer_size * 100) / progress.total_size); + this->PublishProgress(progress); + } + + return {}; + } +} diff --git a/source/gamecard_image_dump_options_frame.cpp b/source/gamecard_image_dump_options_frame.cpp index aa58c30..2fa91bc 100644 --- a/source/gamecard_image_dump_options_frame.cpp +++ b/source/gamecard_image_dump_options_frame.cpp @@ -27,7 +27,7 @@ using namespace i18n::literals; /* For _i18n. */ namespace nxdt::views { GameCardImageDumpOptionsFrame::GameCardImageDumpOptionsFrame(RootView *root_view, std::string raw_filename) : - DumpOptionsFrame(root_view, "gamecard_tab/list/dump_card_image/label"_i18n, std::string(GAMECARD_SUBDIR), raw_filename, std::string(".xci")) + DumpOptionsFrame(root_view, "gamecard_tab/list/dump_card_image/label"_i18n, std::string(GAMECARD_SUBDIR), raw_filename) { /* Subscribe to the gamecard task event. */ this->gc_task_sub = this->root_view->RegisterGameCardTaskListener([this](const GameCardStatus& gc_status) { @@ -42,8 +42,8 @@ namespace nxdt::views }); /* Prepend KeyArea data. */ - this->prepend_key_area = new brls::ToggleListItem("dump_options/prepend_key_area/label"_i18n, configGetBoolean("gamecard/prepend_key_area"), "dump_options/prepend_key_area/description"_i18n, - "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); + this->prepend_key_area = new brls::ToggleListItem("dump_options/gamecard/image/prepend_key_area/label"_i18n, configGetBoolean("gamecard/prepend_key_area"), + "dump_options/gamecard/image/prepend_key_area/description"_i18n, "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); this->prepend_key_area->getClickEvent()->subscribe([](brls::View* view) { /* Get current value. */ @@ -59,8 +59,8 @@ namespace nxdt::views this->addView(this->prepend_key_area); /* Keep certificate. */ - this->keep_certificate = new brls::ToggleListItem("dump_options/keep_certificate/label"_i18n, configGetBoolean("gamecard/keep_certificate"), "dump_options/keep_certificate/description"_i18n, - "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); + this->keep_certificate = new brls::ToggleListItem("dump_options/gamecard/image/keep_certificate/label"_i18n, configGetBoolean("gamecard/keep_certificate"), + "dump_options/gamecard/image/keep_certificate/description"_i18n, "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); this->keep_certificate->getClickEvent()->subscribe([](brls::View* view) { /* Get current value. */ @@ -76,8 +76,8 @@ namespace nxdt::views this->addView(this->keep_certificate); /* Trim dump. */ - this->trim_dump = new brls::ToggleListItem("dump_options/trim_dump/label"_i18n, configGetBoolean("gamecard/trim_dump"), "dump_options/trim_dump/description"_i18n, "generic/value_enabled"_i18n, - "generic/value_disabled"_i18n); + this->trim_dump = new brls::ToggleListItem("dump_options/gamecard/image/trim_dump/label"_i18n, configGetBoolean("gamecard/trim_dump"), "dump_options/gamecard/image/trim_dump/description"_i18n, + "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); this->trim_dump->getClickEvent()->subscribe([](brls::View* view) { /* Get current value. */ @@ -92,8 +92,8 @@ namespace nxdt::views this->addView(this->trim_dump); - this->calculate_checksum = new brls::ToggleListItem("dump_options/calculate_checksum/label"_i18n, configGetBoolean("gamecard/calculate_checksum"), "dump_options/calculate_checksum/description"_i18n, - "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); + this->calculate_checksum = new brls::ToggleListItem("dump_options/gamecard/image/calculate_checksum/label"_i18n, configGetBoolean("gamecard/calculate_checksum"), + "dump_options/gamecard/image/calculate_checksum/description"_i18n, "generic/value_enabled"_i18n, "generic/value_disabled"_i18n); this->calculate_checksum->getClickEvent()->subscribe([](brls::View* view) { /* Get current value. */ @@ -109,12 +109,13 @@ namespace nxdt::views this->addView(this->calculate_checksum); /* Checksum lookup method. */ - this->checksum_lookup_method = new brls::SelectListItem("dump_options/checksum_lookup_method/label"_i18n, { - "dump_options/checksum_lookup_method/value_00"_i18n, + this->checksum_lookup_method = new brls::SelectListItem("dump_options/gamecard/image/checksum_lookup_method/label"_i18n, { + "dump_options/gamecard/image/checksum_lookup_method/value_00"_i18n, "NSWDB", "No-Intro" - }, configGetInteger("gamecard/checksum_lookup_method"), brls::i18n::getStr("dump_options/checksum_lookup_method/description", - "dump_options/calculate_checksum/label"_i18n, "NSWDB", NSWDB_XML_NAME, "No-Intro")); + }, configGetInteger("gamecard/checksum_lookup_method"), + i18n::getStr("dump_options/gamecard/image/checksum_lookup_method/description", "dump_options/gamecard/image/calculate_checksum/label"_i18n, + "NSWDB", NSWDB_XML_NAME, "No-Intro")); this->checksum_lookup_method->getValueSelectedEvent()->subscribe([this](int selected) { /* Make sure the current value isn't out of bounds. */ @@ -129,28 +130,23 @@ namespace nxdt::views /* Register dump button callback. */ this->RegisterButtonListener([this](brls::View *view) { /* Retrieve configuration values set by the user. */ - //bool prepend_key_area_val = this->prepend_key_area->getToggleState(); - //bool keep_certificate_val = this->keep_certificate->getToggleState(); + bool prepend_key_area_val = this->prepend_key_area->getToggleState(); + bool keep_certificate_val = this->keep_certificate->getToggleState(); bool trim_dump_val = this->trim_dump->getToggleState(); //bool calculate_checksum_val = this->calculate_checksum->getToggleState(); //int checksum_lookup_method_val = static_cast(this->checksum_lookup_method->getSelectedValue()); - /* Get gamecard size. */ - u64 gc_size = 0; - if ((!trim_dump_val && !gamecardGetTotalSize(&gc_size)) || (trim_dump_val && !gamecardGetTrimmedSize(&gc_size)) || !gc_size) - { - brls::Application::notify("fail"); - return; - } + /* Generate file extension. */ + std::string extension = fmt::format(" [{}][{}][{}].xci", prepend_key_area_val ? "KA" : "NKA", keep_certificate_val ? "C" : "NC", trim_dump_val ? "T" : "NT"); + + /* Get output path. */ + std::string output_path{}; + if (!this->GetOutputFilePath(extension, output_path)) return; /* Display update frame. */ //brls::Application::pushView(new OptionsTabUpdateApplicationFrame(), brls::ViewAnimation::SLIDE_LEFT, false); - brls::Application::notify(fmt::format("0x{:X}", gc_size)); - - - - LOG_MSG_DEBUG("Output file path: %s", this->GetOutputFilePath().c_str()); + LOG_MSG_DEBUG("Output file path: %s", output_path.c_str()); }); } diff --git a/source/gamecard_tab.cpp b/source/gamecard_tab.cpp index 99c2e8a..0fa32a5 100644 --- a/source/gamecard_tab.cpp +++ b/source/gamecard_tab.cpp @@ -256,6 +256,7 @@ namespace nxdt::views brls::ListItem *dump_card_image = new brls::ListItem("gamecard_tab/list/dump_card_image/label"_i18n, "gamecard_tab/list/dump_card_image/description"_i18n); dump_card_image->getClickEvent()->subscribe([this](brls::View *view) { + /* Display gamecard image dump options. */ std::string& raw_filename = (configGetInteger("naming_convention") == TitleNamingConvention_Full ? raw_filename_full : raw_filename_id_only); brls::Application::pushView(new GameCardImageDumpOptionsFrame(this->root_view, raw_filename), brls::ViewAnimation::SLIDE_LEFT); }); diff --git a/source/options_tab.cpp b/source/options_tab.cpp index e1768e7..cfa25e5 100644 --- a/source/options_tab.cpp +++ b/source/options_tab.cpp @@ -36,9 +36,9 @@ namespace nxdt::views this->setContentView(this->update_progress); /* Add cancel button. */ - this->addButton("options_tab/update_dialog/cancel"_i18n, [&](brls::View* view) { + this->addButton("generic/cancel"_i18n, [&](brls::View* view) { /* Cancel download task. */ - this->download_task.cancel(); + this->download_task.Cancel(); /* Close dialog. */ this->close(); @@ -59,15 +59,15 @@ namespace nxdt::views this->update_progress->willDisappear(); /* Update button label. */ - this->setButtonText(0, "options_tab/update_dialog/close"_i18n); + this->setButtonText(0, "generic/close"_i18n); /* Display notification. */ - brls::Application::notify(this->download_task.get() ? this->success_str : "options_tab/notifications/update_failed"_i18n); + brls::Application::notify(this->download_task.GetResult() ? this->success_str : "options_tab/notifications/update_failed"_i18n); } }); /* Start download task. */ - this->download_task.execute(path, url, force_https); + this->download_task.Execute(path, url, force_https); } OptionsTabUpdateApplicationFrame::OptionsTabUpdateApplicationFrame() : brls::StagedAppletFrame(false) @@ -93,13 +93,13 @@ namespace nxdt::views /* Subscribe to the JSON task. */ this->json_task.RegisterListener([&](const nxdt::tasks::DataTransferProgress& progress) { - /* Return immediately if the JSON task hasn't finished. */ - if (!this->json_task.IsFinished()) return; + /* Return immediately if the JSON task hasn't finished yet or if it was cancelled. */ + if (!this->json_task.IsFinished() || this->json_task.IsCancelled()) return; std::string notification = ""; /* Retrieve task result. */ - nxdt::tasks::DownloadDataResult json_task_result = this->json_task.get(); + nxdt::tasks::DownloadDataResult json_task_result = this->json_task.GetResult(); this->json_buf = json_task_result.first; this->json_buf_size = json_task_result.second; @@ -135,7 +135,7 @@ namespace nxdt::views }); /* Start JSON task. */ - this->json_task.execute(GITHUB_API_RELEASE_URL, true); + this->json_task.Execute(GITHUB_API_RELEASE_URL, true); } OptionsTabUpdateApplicationFrame::~OptionsTabUpdateApplicationFrame() @@ -162,10 +162,10 @@ namespace nxdt::views bool OptionsTabUpdateApplicationFrame::onCancel(void) { /* Cancel NRO task. */ - this->nro_task.cancel(); + this->nro_task.Cancel(); /* Cancel JSON task. */ - this->json_task.cancel(); + this->json_task.Cancel(); /* Pop view. */ brls::Application::popView(brls::ViewAnimation::SLIDE_RIGHT); @@ -176,8 +176,8 @@ namespace nxdt::views void OptionsTabUpdateApplicationFrame::DisplayChangelog(void) { int line = 0; - std::string item; - std::stringstream ss(std::string(this->json_data.changelog)); + std::string item{}; + std::stringstream ss(this->json_data.changelog); /* Display version string at the top. */ FocusableLabel *version_lbl = new FocusableLabel(false, false, brls::LabelStyle::CRASH, std::string(this->json_data.version), true); @@ -240,7 +240,7 @@ namespace nxdt::views this->unregisterAction(brls::Key::PLUS); /* Update cancel action label. */ - this->updateActionHint(brls::Key::B, "options_tab/update_dialog/cancel"_i18n); + this->updateActionHint(brls::Key::B, "generic/cancel"_i18n); /* Subscribe to the NRO task. */ this->nro_task.RegisterListener([&](const nxdt::tasks::DataTransferProgress& progress) { @@ -251,7 +251,7 @@ namespace nxdt::views if (this->nro_task.IsFinished()) { /* Get NRO task result and immediately set application updated state if the task succeeded. */ - bool ret = this->nro_task.get(); + bool ret = this->nro_task.GetResult(); if (ret) utilsSetApplicationUpdatedState(); /* Display notification. */ @@ -263,7 +263,7 @@ namespace nxdt::views }); /* Start NRO task. */ - this->nro_task.execute(NRO_TMP_PATH, std::string(this->json_data.download_url), true); + this->nro_task.Execute(NRO_TMP_PATH, std::string(this->json_data.download_url), true); /* Go to the next stage. */ this->nextStage(); @@ -351,7 +351,7 @@ namespace nxdt::views { this->DisplayNotification("options_tab/notifications/ums_device_unmount_success"_i18n); } else { - this->DisplayNotification("options_tab/notifications/ums_device_unmount_failure"_i18n); + this->DisplayNotification("options_tab/notifications/ums_device_unmount_failed"_i18n); } }); });