Files
sphaira/sphaira/source/dumper.cpp
ITotalJustice 5edc3869cd display storage sizes, properly colour info text, and more (see below)
- display internal and sd card storage sizes.
- removed battery info.
- removed current time info.
- fix dumping save to sd card due to not opening the file with append.
- change all sizes to display GB instead of GiB.
- change progress bar units to 1000 rather than 1024.
- all info text, such as sizes and timestamps now use the info text colouring.
- shorten the ncm content type names.
2025-09-21 18:54:08 +01:00

581 lines
18 KiB
C++

#include "dumper.hpp"
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "download.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "location.hpp"
#include "threaded_file_transfer.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "yati/source/stream.hpp"
#include "usb/usb_uploader.hpp"
#include "usb/usb_dumper.hpp"
#include "usb/usbds.hpp"
namespace sphaira::dump {
namespace {
struct ZipInternal {
WriteSource* writer;
s64 offset;
s64 size;
Result rc;
};
voidpf zopen64_file(voidpf opaque, const void* filename, int mode)
{
ZipInternal* fs = (ZipInternal*)calloc(1, sizeof(*fs));
fs->writer = (WriteSource*)opaque;
return fs;
}
ZPOS64_T ztell64_file(voidpf opaque, voidpf stream)
{
auto fs = (ZipInternal*)stream;
return fs->offset;
}
long zseek64_file(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)
{
auto fs = (ZipInternal*)stream;
switch (origin) {
case SEEK_SET: {
fs->offset = offset;
} break;
case SEEK_CUR: {
fs->offset += offset;
} break;
case SEEK_END: {
fs->offset = fs->size + offset;
} break;
}
return 0;
}
uLong zwrite_file(voidpf opaque, voidpf stream, const void* buf, uLong size) {
auto fs = (ZipInternal*)stream;
if (R_FAILED(fs->rc = fs->writer->Write(buf, fs->offset, size))) {
return 0;
}
fs->offset += size;
fs->size = std::max(fs->size, fs->offset);
return size;
}
int zclose_file(voidpf opaque, voidpf stream) {
if (stream) {
auto fs = (ZipInternal*)stream;
std::memset(fs, 0, sizeof(*fs));
std::free(fs);
}
return 0;
}
int zerror_file(voidpf opaque, voidpf stream) {
auto fs = (ZipInternal*)stream;
if (R_FAILED(fs->rc)) {
return -1;
}
return 0;
}
constexpr zlib_filefunc64_def zlib_filefunc = {
.zopen64_file = zopen64_file,
.zwrite_file = zwrite_file,
.ztell64_file = ztell64_file,
.zseek64_file = zseek64_file,
.zclose_file = zclose_file,
.zerror_file = zerror_file,
};
struct DumpLocationEntry {
const DumpLocationType type;
const char* name;
};
struct WriteFileSource final : WriteSource {
WriteFileSource(fs::File* file) : m_file{file} {
}
Result Write(const void* buf, s64 off, s64 size) override {
return m_file->Write(off, buf, size, FsWriteOption_None);
}
Result SetSize(s64 size) override {
return m_file->SetSize(size);
}
private:
fs::File* m_file;
};
struct WriteNullSource final : WriteSource {
Result Write(const void* buf, s64 off, s64 size) override {
R_SUCCEED();
}
Result SetSize(s64 size) override {
R_SUCCEED();
}
};
struct WriteUsbSource final : WriteSource {
WriteUsbSource(u64 transfer_timeout) {
// disable mtp if enabled.
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
App::SetMtpEnable(false);
}
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
}
~WriteUsbSource() {
m_usb.reset();
if (m_was_mtp_enabled) {
App::SetMtpEnable(true);
}
}
Result WaitForConnection(std::string_view path, u64 timeout) {
return m_usb->WaitForConnection(path, timeout);
}
Result CloseFile() {
return m_usb->CloseFile();
}
Result Write(const void* buf, s64 off, s64 size) override {
return m_usb->Write(buf, off, size);
}
Result SetSize(s64 size) override {
R_SUCCEED();
}
auto GetOpenResult() const {
return m_usb->GetOpenResult();
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
std::unique_ptr<usb::dump::Usb> m_usb{};
bool m_was_mtp_enabled{};
};
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
{ DumpLocationType_Usb, "USB export to PC (usb_export.py)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
};
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, u64 timeout)
: Usb{timeout}
, m_pbox{pbox}
, m_source{source}
, m_paths{paths} {
}
auto& GetPath(u32 index) const {
return m_paths[index];
}
auto& GetPath() const {
return GetPath(m_current_file_index);
}
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override {
R_TRY(m_pull(buf, size, bytes_read));
m_pull_offset += *bytes_read;
R_SUCCEED();
}
Result Read(void* buf, u64 off, u32 size, u64* bytes_read) override {
if (m_pull) {
return Stream::Read(buf, off, size, bytes_read);
} else {
return ReadInternal(GetPath(), buf, off, size, bytes_read);
}
}
Result Open(u32 index, s64& out_size, u16& out_flags) override {
const auto path = m_paths[index];
const auto size = m_source->GetSize(path);
m_progress = 0;
m_pull_offset = 0;
Stream::Reset();
m_size = size;
m_pbox->SetImage(m_source->GetIcon(path));
m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(path);
m_current_file_index = index;
out_size = size;
out_flags = usb::api::FLAG_STREAM;
R_SUCCEED();
}
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read;
m_progress += *bytes_read;
m_pbox->UpdateTransfer(m_progress, m_size);
R_SUCCEED();
}
void SetPullCallback(thread::PullCallback pull) {
m_pull = pull;
}
auto* GetSource() {
return m_source;
}
auto GetPullOffset() const {
return m_pull_offset;
}
auto GetOpenResult() const {
return Usb::GetOpenResult();
}
private:
ui::ProgressBox* m_pbox{};
BaseSource* m_source{};
std::span<const fs::FsPath> m_paths{};
thread::PullCallback m_pull{};
s64 m_offset{};
s64 m_size{};
s64 m_progress{};
s64 m_pull_offset{};
u32 m_current_file_index{};
};
Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
// create write source and verify that it opened.
constexpr u64 timeout = UINT64_MAX;
auto write_source = std::make_unique<WriteUsbSource>(timeout);
R_TRY(write_source->GetOpenResult());
// add cancel event.
pbox->AddCancelEvent(write_source->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(write_source->GetCancelEvent()));
for (const auto& path : paths) {
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer("Waiting for USB connection...");
// wait until usb is ready.
while (true) {
R_TRY(pbox->ShouldExitResult());
const auto rc = write_source->WaitForConnection(path, timeout);
if (R_SUCCEEDED(rc)) {
break;
}
}
pbox->NewTransfer(path);
ON_SCOPE_EXIT(write_source->CloseFile());
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return write_source->Write(data, off, size);
}
));
}
}
R_SUCCEED();
}
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
const auto is_file_based_emummc = App::IsFileBaseEmummc();
for (const auto& path : paths) {
const auto base_path = fs::AppendPath(root, path);
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(base_path);
const auto temp_path = base_path + ".temp";
fs->CreateDirectoryRecursivelyWithPath(temp_path);
fs->DeleteFile(temp_path);
R_TRY(fs->CreateFile(temp_path, file_size));
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
{
fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write|FsOpenMode_Append, &file));
auto write_source = std::make_unique<WriteFileSource>(&file);
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
const auto rc = write_source->Write(data, off, size);
if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
}
));
}
}
fs->DeleteFile(base_path);
R_TRY(fs->RenameFile(temp_path, base_path));
}
R_SUCCEED();
}
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsNativeSd fs{};
return DumpToFile(pbox, &fs, "/", source, paths, custom_transfer);
}
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsStdio fs{};
const auto mount_path = fs::AppendPath(loc.mount, loc.dump_path);
return DumpToFile(pbox, &fs, mount_path, source, paths, custom_transfer);
}
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
auto source = usb->GetSource();
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
const auto path = usb->GetPath();
const auto file_size = source->GetSize(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return usb->ReadInternal(path, data, off, size, bytes_read);
},
[&](const thread::StartThreadCallback& start, const thread::PullCallback& pull) -> Result {
usb->SetPullCallback(pull);
R_TRY(start());
while (!pbox->ShouldExit()) {
const auto rc = usb->file_transfer_loop();
if (R_FAILED(rc)) {
if (rc == Result_UsbUploadExit) {
break;
} else {
R_THROW(rc);
}
}
}
return pbox->ShouldExitResult();
}
));
}
return pbox->ShouldExitResult();
}
Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
std::vector<std::string> file_list;
for (const auto& path : paths) {
file_list.emplace_back(path);
}
// create usb test instance and verify that it opened.
constexpr u64 timeout = UINT64_MAX;
auto usb = std::make_unique<UsbTest>(pbox, source, paths, timeout);
R_TRY(usb->GetOpenResult());
// add cancel event.
pbox->AddCancelEvent(usb->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(usb->GetCancelEvent()));
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list"_i18n);
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
pbox->NewTransfer("Sent file list, waiting for command..."_i18n);
Result rc = DumpToUsbS2SInternal(pbox, usb.get());
// wait for exit command.
if (R_SUCCEEDED(rc)) {
log_write("waiting for exit command\n");
rc = usb->PollCommands();
log_write("finished polling for exit command\n");
} else {
log_write("skipped polling for exit command\n");
}
if (rc == Result_UsbUploadExit) {
log_write("got exit command\n");
R_SUCCEED();
}
return rc;
}
} else {
pbox->NewTransfer("waiting for usb connection..."_i18n);
}
}
return pbox->ShouldExitResult();
}
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
auto write_source = std::make_unique<WriteNullSource>();
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return write_source->Write(data, off, size);
}
));
}
}
R_SUCCEED();
}
} // namespace
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) {
DumpLocation out;
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
const auto stdio_entries = location::GetStdio(true);
if (location_flags & (1 << DumpLocationType_Stdio)) {
for (auto& e : stdio_entries) {
if (e.dump_hidden) {
continue;
}
const auto index = out.stdio.size();
dump_entries.emplace_back(DumpLocationType_Stdio, index);
items.emplace_back(e.name);
out.stdio.emplace_back(e);
}
}
for (s32 i = 0; i < std::size(DUMP_LOCATIONS); i++) {
if (location_flags & (1 << DUMP_LOCATIONS[i].type)) {
log_write("[dump] got name: %s\n", DUMP_LOCATIONS[i].name);
if (!custom_transfer || DUMP_LOCATIONS[i].type != DumpLocationType_UsbS2S) {
log_write("[dump] got name 2: %s\n", DUMP_LOCATIONS[i].name);
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
}
}
}
App::Push<ui::PopupList>(
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
out.entry = dump_entries[*op_index];
log_write("got entry: %u index: %zu\n", out.entry.type, *op_index);
on_loc(out);
}
);
}
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_Usb) {
R_TRY(DumpToUsb(pbox, source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_UsbS2S) {
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
} else if (location.entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths, custom_transfer));
}
R_SUCCEED();
}
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, const CustomTransfer& custom_transfer) {
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location, custom_transfer](auto pbox) -> Result {
return Dump(pbox, source, location, paths, custom_transfer);
}, [on_exit](Result rc){
App::PushErrorBox(rc, "Export failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Export successfull!"_i18n);
log_write("dump successfull!!!\n");
}
if (on_exit) {
on_exit(rc);
}
});
}
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, u32 location_flags) {
DumpGetLocation("Select export location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc) {
Dump(source, loc, paths, on_exit);
});
}
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer, const OnExit& on_exit, u32 location_flags) {
DumpGetLocation("Select export location"_i18n, location_flags, [source, paths, on_exit, custom_transfer](const DumpLocation& loc) {
Dump(source, loc, paths, on_exit, custom_transfer);
}, custom_transfer);
}
void FileFuncWriter(WriteSource* writer, zlib_filefunc64_def* funcs) {
*funcs = zlib_filefunc;
funcs->opaque = writer;
}
} // namespace sphaira::dump