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