[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<std::string> 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().
This commit is contained in:
Pablo Curiel 2024-04-25 01:49:04 +02:00
parent 56053e8105
commit 32c097c055
24 changed files with 901 additions and 180 deletions

View File

@ -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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<Progress, Result, Params...>& execute(const Params&... params)
AsyncTask<Progress, Result, Params...>& 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<std::recursive_mutex> 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<typename Rep, typename Period>
Result get(const std::chrono::duration<Rep, Period>& timeout)
Result GetResult(const std::chrono::duration<Rep, Period>& 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<std::recursive_mutex> 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<std::recursive_mutex> 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;

View File

@ -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);

View File

@ -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<double>(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<double>(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);

View File

@ -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"

View File

@ -55,7 +55,7 @@ namespace nxdt::tasks
DownloadTask<Result, Params...>* task = static_cast<DownloadTask<Result, Params...>*>(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<size_t>(dltotal);
@ -63,13 +63,13 @@ namespace nxdt::tasks
progress.percentage = (progress.total_size ? static_cast<int>((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<bool, std::string, std::string, bool>
{
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<DownloadDataResult, std::string, bool>
{
@ -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;

View File

@ -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)
{

96
include/file_writer.hpp Normal file
View File

@ -0,0 +1,96 @@
/*
* file_writer.hpp
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef __FILE_WRITER_HPP__
#define __FILE_WRITER_HPP__
#include <borealis.hpp>
#include <optional>
#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<std::string> 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__ */

View File

@ -0,0 +1,73 @@
/*
* gamecard_dump_tasks.hpp
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef __GAMECARD_DUMP_TASKS_HPP__
#define __GAMECARD_DUMP_TASKS_HPP__
#include <optional>
#include <mutex>
#include "data_transfer_task.hpp"
namespace nxdt::tasks
{
typedef std::optional<std::string> GameCardDumpTaskError;
class GameCardImageDumpTask: public DataTransferTask<GameCardDumpTaskError, std::string, bool, bool, bool, bool>
{
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__ */

View File

@ -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:

@ -1 +1 @@
Subproject commit b1ff8811a762adf1b585b762b21308d458d43bfa
Subproject commit 1a17488663388e4fff6bdd0ee02fcf34244704ef

View File

@ -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."
}
}

View File

@ -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"
}

View File

@ -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!",

View File

@ -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.",

View File

@ -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."
}
}

View File

@ -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)
{

View File

@ -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;

View File

@ -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<double>(total_sz), total_sz_str, sizeof(total_sz_str));
utilsGenerateFormattedSizeString(static_cast<double>(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;
}
}

View File

@ -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);

354
source/file_writer.cpp Normal file
View File

@ -0,0 +1,354 @@
/*
* file_writer.cpp
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <file_writer.hpp>
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<u8>(ceil(static_cast<double>(this->total_size) / static_cast<double>(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<u8> zeroes(static_cast<size_t>(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<std::string> 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<double>(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<off_t>(this->total_size);
} else {
if (this->split_file_part_idx < this->split_file_part_cnt)
{
truncate_size = static_cast<off_t>(CONCATENATION_FILE_PART_SIZE);
} else {
truncate_size = static_cast<off_t>(this->total_size - (CONCATENATION_FILE_PART_SIZE * static_cast<u64>(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<const u8*>(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;
}
}

View File

@ -0,0 +1,140 @@
/*
* gamecard_dump_tasks.cpp
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <gamecard_dump_tasks.hpp>
#include <core/gamecard.h>
#include <scope_guard.hpp>
#include <file_writer.hpp>
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<u8*>(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<int>((progress.xfer_size * 100) / progress.total_size);
this->PublishProgress(progress);
}
return {};
}
}

View File

@ -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<int>(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());
});
}

View File

@ -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);
});

View File

@ -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);
}
});
});