add game dumping, add game transfer (switch2switch) via usb, add multi game selecting, fix bugs (see below).

- added more es commands.
- fixed usb install potential hang if the exit command is sent, but the client stops responding (timeout is now 3s).
- added multi select to the games menu.
- added game dumping.
- added switch2switch support by having a switch act as a usb client to transfer games.
- replace std::find with std::ranges (in a few places).
- fix rounding of icon in progress box being too round.
- fix file copy helper in progress box not updating the progress bar.
This commit is contained in:
ITotalJustice
2025-05-18 13:46:10 +01:00
parent 544272925d
commit bd7eadc6a0
24 changed files with 2018 additions and 485 deletions

View File

@@ -79,6 +79,11 @@ add_executable(sphaira
source/i18n.cpp source/i18n.cpp
source/ftpsrv_helper.cpp source/ftpsrv_helper.cpp
source/usb/base.cpp
source/usb/usbds.cpp
source/usb/usbhs.cpp
source/usb/usb_uploader.cpp
source/yati/yati.cpp source/yati/yati.cpp
source/yati/container/nsp.cpp source/yati/container/nsp.cpp
source/yati/container/xci.cpp source/yati/container/xci.cpp

View File

@@ -25,6 +25,7 @@ struct Entry {
char display_version[0x10]{}; char display_version[0x10]{};
NacpLanguageEntry lang{}; NacpLanguageEntry lang{};
int image{}; int image{};
bool selected{};
std::shared_ptr<NsApplicationControlData> control{}; std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{}; u64 control_size{};
@@ -103,11 +104,39 @@ private:
void FreeEntries(); void FreeEntries();
void OnLayoutChange(); void OnLayoutChange();
auto GetSelectedEntries() const {
std::vector<Entry> out;
for (auto& e : m_entries) {
if (e.selected) {
out.emplace_back(e);
}
}
if (!m_entries.empty() && out.empty()) {
out.emplace_back(m_entries[m_index]);
}
return out;
}
void ClearSelection() {
for (auto& e : m_entries) {
e.selected = false;
}
m_selected_count = 0;
}
void DeleteGames();
void DumpGames(u32 flags);
private: private:
static constexpr inline const char* INI_SECTION = "games"; static constexpr inline const char* INI_SECTION = "games";
static constexpr inline const char* INI_SECTION_DUMP = "dump";
std::vector<Entry> m_entries{}; std::vector<Entry> m_entries{};
s64 m_index{}; // where i am in the array s64 m_index{}; // where i am in the array
s64 m_selected_count{};
std::unique_ptr<List> m_list{}; std::unique_ptr<List> m_list{};
Event m_event{}; Event m_event{};
bool m_is_reversed{}; bool m_is_reversed{};

View File

@@ -0,0 +1,101 @@
#pragma once
#include <vector>
#include <string>
#include <new>
#include <switch.h>
namespace sphaira::usb {
struct Base {
enum { USBModule = 523 };
enum : Result {
Result_Cancelled = MAKERESULT(USBModule, 100),
};
Base(u64 transfer_timeout);
// sets up usb.
virtual Result Init() = 0;
// returns 0 if usb is connected to a device.
virtual Result IsUsbConnected(u64 timeout) = 0;
// transfers a chunk of data, check out_size_transferred for how much was transferred.
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
}
// transfers all data.
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
Result TransferAll(bool read, void *data, u32 size) {
return TransferAll(read, data, size, m_transfer_timeout);
}
// returns the cancel event.
auto GetCancelEvent() {
return &m_uevent;
}
// cancels an in progress transfer.
void Cancel() {
ueventSignal(GetCancelEvent());
}
auto& GetTransferBuffer() {
return m_aligned;
}
auto GetTransferTimeout() const {
return m_transfer_timeout;
}
public:
// custom allocator for std::vector that respects alignment.
// https://en.cppreference.com/w/cpp/named_req/Allocator
template <typename T, std::size_t Align>
struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
n = (n + (Align - 1)) &~ (Align - 1);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
// ::operator delete[] (p, n, align);
::operator delete[] (p, align);
}
private:
static constexpr inline std::align_val_t align{Align};
};
template <typename T>
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
using value_type = T; // used by std::vector
};
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
protected:
enum UsbSessionEndpoint {
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
private:
u64 m_transfer_timeout{};
UEvent m_uevent{};
PageAlignedVector m_aligned{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,47 @@
#pragma once
#include <switch.h>
namespace sphaira::usb::tinfoil {
enum Magic : u32 {
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
};
enum USBCmdType : u8 {
REQUEST = 0,
RESPONSE = 1
};
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
};
struct NX_PACKED USBCmdHeader {
u32 magic; // TUC0 (Tinfoil USB Command 0)
USBCmdType type;
u8 padding[0x3];
u32 cmdId;
u64 dataSize;
u8 reserved[0xC];
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace sphaira::usb::tinfoil

View File

@@ -0,0 +1,47 @@
#pragma once
#include "usb/usbhs.hpp"
#include <string>
#include <memory>
#include <span>
#include <switch.h>
namespace sphaira::usb::upload {
struct Usb {
enum { USBModule = 523 };
enum : Result {
Result_BadMagic = MAKERESULT(USBModule, 0),
Result_Exit = MAKERESULT(USBModule, 1),
Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4),
Result_BadCommand = MAKERESULT(USBModule, 4),
};
Usb(u64 transfer_timeout);
virtual ~Usb();
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
// waits for connection and then sends file list.
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
// polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved.
Result PollCommands();
private:
Result FileRangeCmd(u64 data_size);
private:
std::unique_ptr<usb::UsbHs> m_usb;
};
} // namespace sphaira::usb::upload

View File

@@ -0,0 +1,27 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
// Device Host
struct UsbDs final : Base {
using Base::Base;
~UsbDs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
Result GetSpeed(UsbDeviceSpeed* out);
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,33 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
struct UsbHs final : Base {
UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout);
~UsbHs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
Result Connect();
void Close();
private:
u8 m_index{};
UsbHsInterfaceFilter m_filter{};
UsbHsInterface m_interface{};
UsbHsClientIfSession m_s{};
UsbHsClientEpSession m_endpoints[2]{};
Event m_event{};
bool m_connected{};
};
} // namespace sphaira::usb

View File

@@ -2,12 +2,16 @@
#include "base.hpp" #include "base.hpp"
#include <switch.h> #include <switch.h>
#include <span>
namespace sphaira::yati::container { namespace sphaira::yati::container {
struct Nsp final : Base { struct Nsp final : Base {
using Base::Base; using Base::Base;
Result GetCollections(Collections& out) override; Result GetCollections(Collections& out) override;
// builds nsp meta data and the size of the entier nsp.
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
}; };
} // namespace sphaira::yati::container } // namespace sphaira::yati::container

View File

@@ -69,7 +69,25 @@ struct EticketRsaDeviceKey {
static_assert(sizeof(EticketRsaDeviceKey) == 0x240); static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
// es functions. // es functions.
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size); Result Initialize();
void Exit();
Service* GetServiceSession();
// todo: find the ipc that gets personalised tickets.
// todo: if ipc doesn't exist, manually parse es personalised save.
// todo: add personalised -> common ticket conversion.
// todo: make the above an option for both dump and install.
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
Result CountCommonTicket(s32* count);
Result CountPersonalizedTicket(s32* count);
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count); // untested
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId);
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId); // [4.0.0+]
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+]
// ticket functions. // ticket functions.
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out); Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);

View File

@@ -33,11 +33,14 @@ union ExtendedHeader {
auto GetMetaTypeStr(u8 meta_type) -> const char*; auto GetMetaTypeStr(u8 meta_type) -> const char*;
auto GetStorageIdStr(u8 storage_id) -> const char*; auto GetStorageIdStr(u8 storage_id) -> const char*;
auto GetMetaTypeShortStr(u8 meta_type) -> const char*;
auto GetAppId(u8 meta_type, u64 id) -> u64; auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64; auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64; auto GetAppId(const PackagedContentMeta& meta) -> u64;
auto GetContentIdFromStr(const char* str) -> NcmContentId;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id); Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id); Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);

View File

@@ -0,0 +1,56 @@
#pragma once
#include <switch/types.h>
#include <switch/result.h>
#include <switch/kernel/mutex.h>
#include <switch/sf/service.h>
#include <switch/services/sm.h>
typedef struct ServiceGuard {
Mutex mutex;
u32 refCount;
} ServiceGuard;
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
{
mutexLock(&g->mutex);
return (g->refCount++) == 0;
}
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
{
if (R_FAILED(rc)) {
cleanupFunc();
--g->refCount;
}
mutexUnlock(&g->mutex);
return rc;
}
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
{
mutexLock(&g->mutex);
if (g->refCount && (--g->refCount) == 0)
cleanupFunc();
mutexUnlock(&g->mutex);
}
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
\
static ServiceGuard g_##name##Guard; \
NX_INLINE Result _##name##Initialize _paramdecl; \
static void _##name##Cleanup(void); \
\
Result name##Initialize _paramdecl \
{ \
Result rc = 0; \
if (serviceGuardBeginInit(&g_##name##Guard)) \
rc = _##name##Initialize _parampass; \
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
} \
\
void name##Exit(void) \
{ \
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
}
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())

View File

@@ -2,10 +2,10 @@
#include "base.hpp" #include "base.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "usb/usbds.hpp"
#include <vector>
#include <string> #include <string>
#include <new> #include <memory>
#include <switch.h> #include <switch.h>
namespace sphaira::yati::source { namespace sphaira::yati::source {
@@ -19,80 +19,31 @@ struct Usb final : Base {
Result_BadCount = MAKERESULT(USBModule, 2), Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3), Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4), Result_BadTotalSize = MAKERESULT(USBModule, 4),
Result_Cancelled = MAKERESULT(USBModule, 11),
}; };
Usb(u64 transfer_timeout); Usb(u64 transfer_timeout);
~Usb(); ~Usb();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override; Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished(); Result Finished(u64 timeout);
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result Init();
Result IsUsbConnected(u64 timeout);
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names); Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name); void SetFileNameForTranfser(const std::string& name);
auto GetCancelEvent() {
return &m_uevent;
}
void SignalCancel() override { void SignalCancel() override {
ueventSignal(GetCancelEvent()); m_usb->Cancel();
} }
public: private:
// custom allocator for std::vector that respects alignment. Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
// https://en.cppreference.com/w/cpp/named_req/Allocator Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
template <typename T, std::size_t Align>
struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
::operator delete[] (p, n, align);
}
private:
static constexpr inline std::align_val_t align{Align};
};
template <typename T>
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
using value_type = T; // used by std::vector
};
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
private: private:
enum UsbSessionEndpoint { std::unique_ptr<usb::UsbDs> m_usb;
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
Result SendCmdHeader(u32 cmdId, size_t dataSize);
Result SendFileRangeCmd(u64 offset, u64 size);
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout);
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
u64 m_transfer_timeout{};
UEvent m_uevent{};
// std::vector<UEvent*> m_cancel_events{};
// aligned buffer that transfer data is copied to and from.
// a vector is used to avoid multiple alloc within the transfer loop.
PageAlignedVector m_aligned{};
std::string m_transfer_file_name{}; std::string m_transfer_file_name{};
}; };

View File

@@ -10,8 +10,14 @@
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "defines.hpp" #include "defines.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "yati/nx/ncm.hpp" #include "yati/nx/ncm.hpp"
#include "yati/nx/nca.hpp" #include "yati/nx/nca.hpp"
#include "yati/nx/es.hpp"
#include "yati/container/base.hpp"
#include "yati/container/nsp.hpp"
#include "usb/usb_uploader.hpp"
#include <utility> #include <utility>
#include <cstring> #include <cstring>
@@ -20,30 +26,340 @@
namespace sphaira::ui::menu::game { namespace sphaira::ui::menu::game {
namespace { namespace {
constexpr NcmStorageId NCM_STORAGE_IDS[]{ constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
NcmStorageId_BuiltInUser, if (meta_type & 0x80) {
NcmStorageId_SdCard, return 1 << (meta_type - 0x80);
}
return 0;
}
enum ContentFlag {
ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application),
ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch),
ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent),
ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch),
ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch,
}; };
NcmContentStorage ncm_cs[2]; enum DumpLocationType {
NcmContentMetaDatabase ncm_db[2]; DumpLocationType_SdCard,
DumpLocationType_UsbS2S,
DumpLocationType_DevNull,
};
struct DumpLocation {
const DumpLocationType type;
const char* display_name;
};
constexpr DumpLocation DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/NSP/)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
};
struct NcmEntry {
const NcmStorageId storage_id;
NcmContentStorage cs{};
NcmContentMetaDatabase db{};
void Open() {
if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) {
log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id);
}
if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) {
log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id);
}
}
void Close() {
ncmContentMetaDatabaseClose(std::addressof(db));
ncmContentStorageClose(std::addressof(cs));
db = {};
cs = {};
}
};
constinit NcmEntry ncm_entries[] = {
// on memory, will become invalid on the gamecard being inserted / removed.
{ NcmStorageId_GameCard },
// normal (save), will remain valid.
{ NcmStorageId_BuiltInUser },
{ NcmStorageId_SdCard },
};
auto& GetNcmEntry(u8 storage_id) {
auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){
return storage_id == e.storage_id;
});
if (it == std::end(ncm_entries)) {
log_write("unable to find valid ncm entry: %u\n", storage_id);
return ncm_entries[0];
}
return *it;
}
auto& GetNcmCs(u8 storage_id) { auto& GetNcmCs(u8 storage_id) {
if (storage_id == NcmStorageId_SdCard) { return GetNcmEntry(storage_id).cs;
return ncm_cs[1];
}
return ncm_cs[0];
} }
auto& GetNcmDb(u8 storage_id) { auto& GetNcmDb(u8 storage_id) {
if (storage_id == NcmStorageId_SdCard) { return GetNcmEntry(storage_id).db;
return ncm_db[1];
}
return ncm_db[0];
} }
using MetaEntries = std::vector<NsApplicationContentMetaStatus>; using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
struct ContentInfoEntry {
NsApplicationContentMetaStatus status{};
std::vector<NcmContentInfo> content_infos{};
std::vector<FsRightsId> rights_ids{};
};
struct TikEntry {
FsRightsId id{};
std::vector<u8> tik_data{};
std::vector<u8> cert_data{};
};
struct NspEntry {
// application name.
std::string application_name{};
// name of the nsp (name [id][v0][BASE].nsp).
fs::FsPath path{};
// tickets and cert data, will be empty if title key crypto isn't used.
std::vector<TikEntry> tickets{};
// all the collections for this nsp, such as nca's and tickets.
std::vector<yati::container::CollectionEntry> collections{};
// raw nsp data (header, file table and string table).
std::vector<u8> nsp_data{};
// size of the entier nsp.
s64 nsp_size{};
// copy of ncm cs, it is not closed.
NcmContentStorage cs{};
// todo: benchmark manual sdcard read and decryption vs ncm.
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < nsp_data.size()) {
*bytes_read = size = ClipSize(off, size, nsp_data.size());
std::memcpy(buf, nsp_data.data() + off, size);
R_SUCCEED();
}
// adjust offset.
off -= nsp_data.size();
for (auto& collection : collections) {
if (InRange(off, collection.offset, collection.size)) {
// adjust offset relative to the collection.
off -= collection.offset;
*bytes_read = size = ClipSize(off, size, collection.size);
if (collection.name.ends_with(".nca")) {
const auto id = ncm::GetContentIdFromStr(collection.name.c_str());
return ncmContentStorageReadContentIdFile(&cs, buf, size, &id, off);
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
FsRightsId id;
keys::parse_hex_key(&id, collection.name.c_str());
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
return !std::memcmp(&id, &e.id, sizeof(id));
});
R_UNLESS(it != tickets.end(), 0x1);
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
std::memcpy(buf, data.data() + off, size);
R_SUCCEED();
}
}
}
log_write("did not find collection...\n");
return 0x1;
}
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
static auto ClipSize(s64 off, s64 size, s64 file_size) -> s64 {
return std::min(size, file_size - off);
}
};
struct BaseSource {
virtual ~BaseSource() = default;
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
};
struct NspSource final : BaseSource {
NspSource(std::span<NspEntry> entries) : m_entries{entries} { }
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path == e.path;
});
R_UNLESS(it != m_entries.end(), 0x1);
return it->Read(buf, off, size, bytes_read);
}
auto GetEntries() const -> std::span<const NspEntry> {
return m_entries;
}
private:
std::span<NspEntry> m_entries{};
};
struct UsbTest final : usb::upload::Usb {
UsbTest(ProgressBox* pbox, std::span<NspEntry> entries) : Usb{UINT64_MAX} {
m_source = std::make_unique<NspSource>(entries);
m_pbox = pbox;
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (m_path != path) {
m_path = path;
m_pbox->NewTransfer(m_path);
}
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read;
R_SUCCEED();
}
private:
std::unique_ptr<NspSource> m_source{};
ProgressBox* m_pbox{};
std::string m_path{};
s64 m_offset{};
};
Result DumpNspToFile(ProgressBox* pbox, std::span<NspEntry> entries) {
static constexpr fs::FsPath DUMP_PATH{"/dumps/NSP"};
constexpr s64 BIG_FILE_SIZE = 1024ULL*1024ULL*1024ULL*4ULL;
fs::FsNativeSd fs{};
R_TRY(fs.GetFsOpenResult());
fs.CreateDirectoryRecursively(DUMP_PATH);
auto source = std::make_unique<NspSource>(entries);
for (const auto& e : entries) {
pbox->SetTitle(e.application_name);
pbox->NewTransfer(e.path);
const auto temp_path = fs::AppendPath(DUMP_PATH, e.path + ".temp");
fs.DeleteFile(temp_path);
const auto flags = e.nsp_size >= BIG_FILE_SIZE ? FsCreateOption_BigFile : 0;
R_TRY(fs.CreateFile(temp_path, e.nsp_size, flags));
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
{
FsFile file;
R_TRY(fs.OpenFile(temp_path, FsOpenMode_Write, &file));
ON_SCOPE_EXIT(fsFileClose(&file));
s64 offset{};
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < e.nsp_size) {
if (pbox->ShouldExit()) {
R_THROW(0xFFFF);
}
u64 bytes_read;
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
pbox->Yield();
R_TRY(fsFileWrite(&file, offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->Yield();
pbox->UpdateTransfer(offset, e.nsp_size);
offset += bytes_read;
}
}
const auto path = fs::AppendPath(DUMP_PATH, e.path);
fs.DeleteFile(path);
R_TRY(fs.RenameFile(temp_path, path));
}
R_SUCCEED();
}
Result DumpNspToUsbS2S(ProgressBox* pbox, std::span<NspEntry> entries) {
std::vector<std::string> file_list;
for (auto& e : entries) {
file_list.emplace_back(e.path);
}
auto usb = std::make_unique<UsbTest>(pbox, entries);
constexpr u64 timeout = 1e+9;
// todo: display progress bar during usb transfer.
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list");
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
pbox->NewTransfer("Sent file list, waiting for command...");
while (!pbox->ShouldExit()) {
const auto rc = usb->PollCommands();
if (rc == usb->Result_Exit) {
log_write("got exit command\n");
R_SUCCEED();
}
R_TRY(rc);
}
}
} else {
pbox->NewTransfer("waiting for usb connection...");
}
}
R_SUCCEED();
}
Result DumpNspToDevNull(ProgressBox* pbox, std::span<NspEntry> entries) {
auto source = std::make_unique<NspSource>(entries);
for (const auto& e : entries) {
pbox->SetTitle(e.application_name);
pbox->NewTransfer(e.path);
s64 offset{};
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < e.nsp_size) {
if (pbox->ShouldExit()) {
R_THROW(0xFFFF);
}
u64 bytes_read;
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
pbox->Yield();
pbox->UpdateTransfer(offset, e.nsp_size);
offset += bytes_read;
}
}
R_SUCCEED();
}
Result Notify(Result rc, const std::string& error_message) { Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc, App::Push(std::make_shared<ui::ErrorBox>(rc,
@@ -56,19 +372,26 @@ Result Notify(Result rc, const std::string& error_message) {
return rc; return rc;
} }
Result GetMetaEntries(u64 id, MetaEntries& out) { Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) {
s32 count; for (s32 i = 0; ; i++) {
R_TRY(nsCountApplicationContentMeta(id, &count)); s32 count;
NsApplicationContentMetaStatus status;
R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count));
out.resize(count); if (!count) {
R_TRY(nsListApplicationContentMetaStatus(id, 0, out.data(), out.size(), &count)); break;
}
if (flags & ContentMetaTypeToContentFlag(status.meta_type)) {
out.emplace_back(status);
}
}
out.resize(count);
R_SUCCEED(); R_SUCCEED();
} }
Result GetMetaEntries(const Entry& e, MetaEntries& out) { Result GetMetaEntries(const Entry& e, MetaEntries& out, u32 flags = ContentFlag_All) {
return GetMetaEntries(e.app_id, out); return GetMetaEntries(e.app_id, out, flags);
} }
// also sets the status to error. // also sets the status to error.
@@ -102,7 +425,7 @@ Result LoadControlManual(u64 id, ThreadResultData& data) {
R_UNLESS(!entries.empty(), 0x1); R_UNLESS(!entries.empty(), 0x1);
const auto& ee = entries.back(); const auto& ee = entries.back();
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser) { if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) {
return 0x1; return 0x1;
} }
@@ -193,6 +516,200 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
} }
} }
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
auto isRightsIdValid(FsRightsId id) -> bool {
FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
if (status.meta_type == NcmContentMetaType_Patch) {
std::snprintf(version, sizeof(version), "%s ", e.GetDisplayVersion());
}
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
return path;
}
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
auto& cs = GetNcmCs(status.storageID);
auto& db = GetNcmDb(status.storageID);
const auto app_id = ncm::GetAppId(status.meta_type, status.application_id);
auto id_min = status.application_id;
auto id_max = status.application_id;
// workaround N bug where they don't check the full range in the ID filter.
// https://github.com/Atmosphere-NX/Atmosphere/blob/1d3f3c6e56b994b544fc8cd330c400205d166159/libraries/libstratosphere/source/ncm/ncm_on_memory_content_meta_database_impl.cpp#L22
if (status.storageID == NcmStorageId_None || status.storageID == NcmStorageId_GameCard) {
id_min -= 1;
id_max += 1;
}
s32 meta_total;
s32 meta_entries_written;
NcmContentMetaKey key;
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(meta_total), std::addressof(meta_entries_written), std::addressof(key), 1, (NcmContentMetaType)status.meta_type, app_id, id_min, id_max, NcmContentInstallType_Full));
log_write("ncmContentMetaDatabaseList(): AppId: %016lX Id: %016lX total: %d written: %d storageID: %u key.id %016lX\n", app_id, status.application_id, meta_total, meta_entries_written, status.storageID, key.id);
R_UNLESS(meta_total == 1, 0x1);
R_UNLESS(meta_entries_written == 1, 0x1);
for (s32 i = 0; ; i++) {
s32 entries_written;
NcmContentInfo info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(entries_written), std::addressof(info_out), 1, std::addressof(key), i));
if (!entries_written) {
break;
}
// check if we need to fetch tickets.
NcmRightsId ncm_rights_id;
R_TRY(ncmContentStorageGetRightsIdFromContentId(std::addressof(cs), std::addressof(ncm_rights_id), std::addressof(info_out.content_id), FsContentAttributes_All));
const auto rights_id = ncm_rights_id.rights_id;
if (isRightsIdValid(rights_id)) {
const auto it = std::ranges::find_if(out.rights_ids, [&rights_id](auto& e){
return !std::memcmp(&e, &rights_id, sizeof(rights_id));
});
if (it == out.rights_ids.end()) {
out.rights_ids.emplace_back(rights_id);
}
}
out.content_infos.emplace_back(info_out);
}
out.status = status;
R_SUCCEED();
}
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out) {
out.application_name = e.GetName();
out.path = BuildNspPath(e, info.status);
s64 offset{};
for (auto& rights_id : info.rights_ids) {
TikEntry entry{rights_id};
log_write("rights id is valid, fetching common ticket and cert\n");
u64 tik_size;
u64 cert_size;
R_TRY(es::GetCommonTicketAndCertificateSize(&tik_size, &cert_size, &rights_id));
log_write("got tik_size: %zu cert_size: %zu\n", tik_size, cert_size);
entry.tik_data.resize(tik_size);
entry.cert_data.resize(cert_size);
R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id));
log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size);
char tik_name[0x200];
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
char cert_name[0x200];
std::snprintf(cert_name, sizeof(cert_name), "%s%s", hexIdToStr(rights_id).str, ".cert");
out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
offset += entry.tik_data.size();
out.collections.emplace_back(cert_name, offset, entry.cert_data.size());
offset += entry.cert_data.size();
out.tickets.emplace_back(entry);
}
for (auto& e : info.content_infos) {
char nca_name[0x200];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
out.collections.emplace_back(nca_name, offset, size);
offset += size;
}
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
out.cs = GetNcmCs(info.status.storageID);
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
LoadControlEntry(e);
MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
for (const auto& status : meta_entries) {
ContentInfoEntry info;
R_TRY(BuildContentEntry(status, info));
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, nsp));
out.emplace_back(nsp);
}
R_UNLESS(!out.empty(), 0x1);
R_SUCCEED();
}
void FreeEntry(NVGcontext* vg, Entry& e) { void FreeEntry(NVGcontext* vg, Entry& e) {
nvgDeleteImage(vg, e.image); nvgDeleteImage(vg, e.image);
e.image = 0; e.image = 0;
@@ -267,7 +784,7 @@ void ThreadData::Push(u64 id) {
mutexLock(&m_mutex_id); mutexLock(&m_mutex_id);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id)); ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id));
const auto it = std::find(m_ids.begin(), m_ids.end(), id); const auto it = std::ranges::find(m_ids, id);
if (it == m_ids.end()) { if (it == m_ids.end()) {
m_ids.emplace_back(id); m_ids.emplace_back(id);
ueventSignal(&m_uevent); ueventSignal(&m_uevent);
@@ -290,6 +807,33 @@ void ThreadData::Pop(std::vector<ThreadResultData>& out) {
Menu::Menu() : grid::Menu{"Games"_i18n} { Menu::Menu() : grid::Menu{"Games"_i18n} {
this->SetActions( this->SetActions(
std::make_pair(Button::L3, Action{[this](){
if (m_entries.empty()) {
return;
}
m_entries[m_index].selected ^= 1;
if (m_entries[m_index].selected) {
m_selected_count++;
} else {
m_selected_count--;
}
}}),
std::make_pair(Button::R3, Action{[this](){
if (m_entries.empty()) {
return;
}
if (m_selected_count == m_entries.size()) {
ClearSelection();
} else {
m_selected_count = m_entries.size();
for (auto& e : m_entries) {
e.selected = true;
}
}
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){ std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop(); SetPop();
}}), }}),
@@ -395,6 +939,27 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
)); ));
})); }));
options->Add(std::make_shared<SidebarEntryCallback>("Dump"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
DumpGames(ContentFlag_All);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
DumpGames(ContentFlag_Application);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
DumpGames(ContentFlag_Patch);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
DumpGames(ContentFlag_AddOnContent);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
DumpGames(ContentFlag_DataPatch);
}, true));
}, true));
// completely deletes the application record and all data. // completely deletes the application record and all data.
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?"; const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
@@ -402,22 +967,7 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
buf, buf,
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){ "Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
const auto rc = nsDeleteApplicationCompletely(m_entries[m_index].app_id); DeleteGames();
Notify(rc, "Failed to delete application");
}
}, m_entries[m_index].image
));
}, true));
// removes installed data but keeps the record, basically archiving.
options->Add(std::make_shared<SidebarEntryCallback>("Delete entity"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
App::Push(std::make_shared<OptionBox>(
buf,
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
const auto rc = nsDeleteApplicationEntity(m_entries[m_index].app_id);
Notify(rc, "Failed to delete application");
} }
}, m_entries[m_index].image }, m_entries[m_index].image
)); ));
@@ -430,10 +980,10 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
nsInitialize(); nsInitialize();
nsGetApplicationRecordUpdateSystemEvent(&m_event); nsGetApplicationRecordUpdateSystemEvent(&m_event);
es::Initialize();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { for (auto& e : ncm_entries) {
ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]); e.Open();
ncmOpenContentStorage(std::addressof(ncm_cs[i]), NCM_STORAGE_IDS[i]);
} }
threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, 0x30, 1); threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, 0x30, 1);
@@ -443,14 +993,14 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
Menu::~Menu() { Menu::~Menu() {
m_thread_data.Close(); m_thread_data.Close();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { for (auto& e : ncm_entries) {
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i])); e.Close();
ncmContentStorageClose(std::addressof(ncm_cs[i]));
} }
FreeEntries(); FreeEntries();
eventClose(&m_event); eventClose(&m_event);
nsExit(); nsExit();
es::Exit();
threadWaitForExit(&m_thread); threadWaitForExit(&m_thread);
threadClose(&m_thread); threadClose(&m_thread);
@@ -489,7 +1039,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
m_thread_data.Pop(data); m_thread_data.Pop(data);
for (const auto& d : data) { for (const auto& d : data) {
const auto it = std::find_if(m_entries.begin(), m_entries.end(), [&d](auto& e) { const auto it = std::ranges::find_if(m_entries, [&d](auto& e) {
return e.app_id == d.id; return e.app_id == d.id;
}); });
@@ -499,7 +1049,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) { m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
// const auto& [x, y, w, h] = v; const auto& [x, y, w, h] = v;
auto& e = m_entries[pos]; auto& e = m_entries[pos];
if (e.status == NacpLoadStatus::None) { if (e.status == NacpLoadStatus::None) {
@@ -516,6 +1066,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
const auto selected = pos == m_index; const auto selected = pos == m_index;
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), e.GetDisplayVersion()); DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), e.GetDisplayVersion());
if (e.selected) {
gfx::drawRect(vg, v, nvgRGBA(0, 0, 0, 180), 5);
gfx::drawText(vg, x + w / 2, y + h / 2, 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
}
}); });
} }
@@ -594,8 +1149,7 @@ void Menu::ScanHomebrew() {
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs()); log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
this->Sort(); this->Sort();
SetIndex(0); SetIndex(0);
ClearSelection();
// m_thread_data.Push(m_entries);
} }
void Menu::Sort() { void Menu::Sort() {
@@ -604,12 +1158,12 @@ void Menu::Sort() {
if (order == OrderType_Ascending) { if (order == OrderType_Ascending) {
if (!m_is_reversed) { if (!m_is_reversed) {
std::reverse(m_entries.begin(), m_entries.end()); std::ranges::reverse(m_entries);
m_is_reversed = true; m_is_reversed = true;
} }
} else { } else {
if (m_is_reversed) { if (m_is_reversed) {
std::reverse(m_entries.begin(), m_entries.end()); std::ranges::reverse(m_entries);
m_is_reversed = false; m_is_reversed = false;
} }
} }
@@ -660,4 +1214,76 @@ void Menu::OnLayoutChange() {
grid::Menu::OnLayoutChange(m_list, m_layout.Get()); grid::Menu::OnLayoutChange(m_list, m_layout.Get());
} }
void Menu::DeleteGames() {
App::Push(std::make_shared<ProgressBox>(0, "Deleting Games"_i18n, "", [this](auto pbox) -> bool {
auto targets = GetSelectedEntries();
for (s64 i = 0; i < std::size(targets); i++) {
auto& e = targets[i];
LoadControlEntry(e);
pbox->SetTitle(e.GetName());
pbox->UpdateTransfer(i + 1, std::size(targets));
nsDeleteApplicationCompletely(e.app_id);
}
return true;
}, [this](bool success){
ClearSelection();
m_dirty = true;
if (success) {
App::Notify("Delete successfull!");
} else {
App::Notify("Delete failed!");
}
}));
}
void Menu::DumpGames(u32 flags) {
PopupList::Items items;
for (const auto&p : DUMP_LOCATIONS) {
items.emplace_back(p.display_name);
}
App::Push(std::make_shared<PopupList>(
"Select dump location"_i18n, items, [this, flags](auto op_index){
if (!op_index) {
return;
}
const auto index = *op_index;
App::Push(std::make_shared<ProgressBox>(0, "Dumping Games"_i18n, "", [this, index, flags](auto pbox) -> bool {
auto targets = GetSelectedEntries();
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
BuildNspEntries(e, flags, nsp_entries);
}
if (index == DumpLocationType_SdCard) {
return R_SUCCEEDED(DumpNspToFile(pbox, nsp_entries));
} else if (index == DumpLocationType_UsbS2S) {
return R_SUCCEEDED(DumpNspToUsbS2S(pbox, nsp_entries));
} else if (index == DumpLocationType_DevNull) {
return R_SUCCEEDED(DumpNspToDevNull(pbox, nsp_entries));
}
return false;
}, [this](bool success){
ClearSelection();
if (success) {
App::Notify("Dump successfull!");
log_write("dump successfull!!!\n");
} else {
App::Notify("Dump failed!");
log_write("dump failed!!!\n");
}
}));
}
));
}
} // namespace sphaira::ui::menu::game } // namespace sphaira::ui::menu::game

View File

@@ -12,6 +12,7 @@ namespace {
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX; constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX; constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds.
void thread_func(void* user) { void thread_func(void* user) {
auto app = static_cast<Menu*>(user); auto app = static_cast<Menu*>(user);
@@ -22,7 +23,7 @@ void thread_func(void* user) {
} }
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT); const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
if (rc == app->m_usb_source->Result_Cancelled) { if (rc == ::sphaira::usb::UsbDs::Result_Cancelled) {
break; break;
} }
@@ -71,11 +72,6 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
if (R_FAILED(m_usb_source->GetOpenResult())) { if (R_FAILED(m_usb_source->GetOpenResult())) {
log_write("usb init open\n"); log_write("usb init open\n");
m_state = State::Failed; m_state = State::Failed;
} else {
if (R_FAILED(m_usb_source->Init())) {
log_write("usb init failed\n");
m_state = State::Failed;
}
} }
mutexInit(&m_mutex); mutexInit(&m_mutex);
@@ -114,7 +110,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
m_state = State::Progress; m_state = State::Progress;
log_write("got connection\n"); log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool { App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
ON_SCOPE_EXIT(m_usb_source->Finished()); ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
log_write("inside progress box\n"); log_write("inside progress box\n");
for (const auto& file_name : m_names) { for (const auto& file_name : m_names) {

View File

@@ -108,7 +108,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH()); nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
if (m_image) { if (m_image) {
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10); gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 5);
} }
// shapes. // shapes.
@@ -234,7 +234,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
R_TRY(fsFileSetSize(&dst_file, src_size)); R_TRY(fsFileSetSize(&dst_file, src_size));
s64 offset{}; s64 offset{};
std::vector<u8> buf(1024*1024*8); // 8MiB std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < src_size) { while (offset < src_size) {
if (ShouldExit()) { if (ShouldExit()) {
@@ -248,6 +248,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None)); R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
Yield(); Yield();
UpdateTransfer(offset, src_size);
offset += bytes_read; offset += bytes_read;
} }

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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 <http://www.gnu.org/licenses/>.
*/
// The USB transfer code was taken from Haze (part of Atmosphere).
#include "usb/base.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
Base::Base(u64 transfer_timeout) {
m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16);
}
Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
u32 xfer_id;
/* If we're not configured yet, wait to become configured first. */
R_TRY(IsUsbConnected(timeout));
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(xfer_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
/* Return what we transferred. */
return GetTransferResult(ep, xfer_id, nullptr, out_size_transferred);
}
// while it may seem like a bad idea to transfer data to a buffer and copy it
// in practice, this has no impact on performance.
// the switch is *massively* bottlenecked by slow io (nand and sd).
// so making usb transfers zero-copy provides no benefit other than increased
// code complexity and the increase of future bugs if/when sphaira is forked
// an changes are made.
// yati already goes to great lengths to be zero-copy during installing
// by swapping buffers and inflating in-place.
// NOTE: it is now possible to request the transfer buffer using GetTransferBuffer(),
// which will always be aligned and have the size aligned.
// this allows for zero-copy transferrs to take place.
// this is used in usb_upload.cpp.
// do note that this relies of the host sending / receiving buffers of an aligned size.
Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
auto buf = static_cast<u8*>(data);
auto transfer_buf = m_aligned.data();
const auto alias = buf == transfer_buf;
if (!alias) {
m_aligned.resize(size);
}
while (size) {
if (!alias && !read) {
std::memcpy(transfer_buf, buf, size);
}
u32 out_size_transferred;
R_TRY(TransferPacketImpl(read, transfer_buf, size, &out_size_transferred, timeout));
if (!alias && read) {
std::memcpy(buf, transfer_buf, out_size_transferred);
}
if (alias) {
transfer_buf += out_size_transferred;
} else {
buf += out_size_transferred;
}
size -= out_size_transferred;
}
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -0,0 +1,112 @@
// The USB protocol was taken from Tinfoil, by Adubbz.
#include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp"
#include "log.hpp"
#include "defines.hpp"
namespace sphaira::usb::upload {
namespace {
namespace tinfoil = usb::tinfoil;
const UsbHsInterfaceFilter FILTER{
.Flags = UsbHsInterfaceFilterFlags_idVendor |
UsbHsInterfaceFilterFlags_idProduct |
UsbHsInterfaceFilterFlags_bcdDevice_Min |
UsbHsInterfaceFilterFlags_bcdDevice_Max |
UsbHsInterfaceFilterFlags_bDeviceClass |
UsbHsInterfaceFilterFlags_bDeviceSubClass |
UsbHsInterfaceFilterFlags_bDeviceProtocol |
UsbHsInterfaceFilterFlags_bInterfaceClass |
UsbHsInterfaceFilterFlags_bInterfaceSubClass |
UsbHsInterfaceFilterFlags_bInterfaceProtocol,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice_Min = 0x0100,
.bcdDevice_Max = 0x0100,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
constexpr u8 INDEX = 0;
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_usb = std::make_unique<usb::UsbHs>(INDEX, FILTER, transfer_timeout);
m_usb->Init();
}
Usb::~Usb() {
}
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
R_TRY(m_usb->IsUsbConnected(timeout));
std::string names_list;
for (auto& name : names) {
names_list += name + '\n';
}
tinfoil::TUSHeader header{};
header.magic = tinfoil::Magic_List0;
header.nspListSize = names_list.length();
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
R_SUCCEED();
}
Result Usb::PollCommands() {
tinfoil::USBCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_BadMagic);
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
return Result_Exit;
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
return FileRangeCmd(header.dataSize);
} else {
return Result_BadCommand;
}
}
Result Usb::FileRangeCmd(u64 data_size) {
tinfoil::FileRangeCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
std::string path(header.nspNameLen, '\0');
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
// send response header.
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
s64 curr_off = 0x0;
s64 end_off = header.size;
s64 read_size = header.size;
// use transfer buffer directly to avoid copy overhead.
auto& buf = m_usb->GetTransferBuffer();
buf.resize(header.size);
while (curr_off < end_off) {
if (curr_off + read_size >= end_off) {
read_size = end_off - curr_off;
}
u64 bytes_read;
R_TRY(Read(path, buf.data(), header.offset + curr_off, read_size, &bytes_read));
R_TRY(m_usb->TransferAll(false, buf.data(), bytes_read));
curr_off += bytes_read;
}
R_SUCCEED();
}
} // namespace sphaira::usb::upload

View File

@@ -0,0 +1,272 @@
#include "usb/usbds.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
// untested, should work tho.
// TODO: pr missing speed fields to libnx.
Result usbDsGetSpeed(u32 *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
serviceAssumeDomain(usbDsGetServiceSession());
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
}
} // namespace
UsbDs::~UsbDs() {
usbDsExit();
}
Result UsbDs::Init() {
log_write("doing USB init\n");
R_TRY(usbDsInitialize());
static SetSysSerialNumber serial_number{};
R_TRY(setsysInitialize());
ON_SCOPE_EXIT(setsysExit());
R_TRY(setsysGetSerialNumber(&serial_number));
u8 iManufacturer, iProduct, iSerialNumber;
static constexpr u16 supported_langs[1] = {0x0409};
// Send language descriptor
R_TRY(usbDsAddUsbLanguageStringDescriptor(nullptr, supported_langs, std::size(supported_langs)));
// Send manufacturer
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
.bLength = USB_DT_DEVICE_SIZE,
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x0110,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = 0x40,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice = 0x0100,
.iManufacturer = iManufacturer,
.iProduct = iProduct,
.iSerialNumber = iSerialNumber,
.bNumConfigurations = 0x01
};
// Full Speed is USB 1.1
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
// High Speed is USB 2.0
device_descriptor.bcdUSB = 0x0200;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
// Super Speed is USB 3.0
device_descriptor.bcdUSB = 0x0300;
// Upgrade packet size to 512
device_descriptor.bMaxPacketSize0 = 0x09;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
// Define Binary Object Store
const u8 bos[0x16] = {
0x05, // .bLength
USB_DT_BOS, // .bDescriptorType
0x16, 0x00, // .wTotalLength
0x02, // .bNumDeviceCaps
// USB 2.0
0x07, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x02, // .bDevCapabilityType
0x02, 0x00, 0x00, 0x00, // dev_capability_data
// USB 3.0
0x0A, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x03, /* .bDevCapabilityType */
0x00, /* .bmAttributes */
0x0E, 0x00, /* .wSpeedSupported */
0x03, /* .bFunctionalitySupport */
0x00, /* .bU1DevExitLat */
0x00, 0x00 /* .bU2DevExitLat */
};
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
struct usb_interface_descriptor interface_descriptor = {
.bLength = USB_DT_INTERFACE_SIZE,
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
struct usb_endpoint_descriptor endpoint_descriptor_in = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_IN,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
struct usb_endpoint_descriptor endpoint_descriptor_out = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
.bMaxBurst = 0x0F,
.bmAttributes = 0x00,
.wBytesPerInterval = 0x00,
};
R_TRY(usbDsRegisterInterface(&m_interface));
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x40;
endpoint_descriptor_out.wMaxPacketSize = 0x40;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
//Setup endpoints.
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
log_write("success USB init\n");
R_SUCCEED();
}
// the below code is taken from libnx, with the addition of a uevent to cancel.
Result UsbDs::IsUsbConnected(u64 timeout) {
Result rc;
UsbState state = UsbState_Detached;
rc = usbDsGetState(&state);
if (R_FAILED(rc)) return rc;
if (state == UsbState_Configured) return 0;
bool has_timeout = timeout != UINT64_MAX;
u64 deadline = 0;
const std::array waiters{
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
if (has_timeout)
deadline = armGetSystemTick() + armNsToTicks(timeout);
do {
if (has_timeout) {
s64 remaining = deadline - armGetSystemTick();
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
}
s32 idx;
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
eventClear(usbDsGetStateChangeEvent());
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
rc = Result_Cancelled;
break;
}
rc = usbDsGetState(&state);
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
return KERNELRESULT(TimedOut);
return rc;
}
Result UsbDs::GetSpeed(UsbDeviceSpeed* out) {
return usbDsGetSpeed((u32*)out);
}
Event *UsbDs::GetCompletionEvent(UsbSessionEndpoint ep) {
return std::addressof(m_endpoints[ep]->CompletionEvent);
}
Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
const std::array waiters{
waiterForEvent(GetCompletionEvent(ep)),
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usbDsGetStateChangeEvent() event\n");
rc = KERNELRESULT(TimedOut);
}
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
eventClear(GetCompletionEvent(ep));
eventClear(usbDsGetStateChangeEvent());
}
return rc;
}
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) {
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) {
UsbDsReportData report_data;
R_TRY(eventClear(GetCompletionEvent(ep)));
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -0,0 +1,202 @@
#include "usb/usbhs.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
struct Bcd {
constexpr Bcd(u16 v) : value{v} {}
u8 major() const { return (value >> 8) & 0xFF; }
u8 minor() const { return (value >> 4) & 0xF; }
u8 macro() const { return (value >> 0) & 0xF; }
const u16 value;
};
Result usbHsParseReportData(UsbHsXferReport* reports, u32 count, u32 xferId, u32 *requestedSize, u32 *transferredSize) {
Result rc = 0;
u32 pos;
UsbHsXferReport *entry = NULL;
if (count>8) count = 8;
for(pos=0; pos<count; pos++) {
entry = &reports[pos];
if (entry->xferId == xferId) break;
}
if (pos == count) return MAKERESULT(Module_Libnx, LibnxError_NotFound);
rc = entry->res;
if (R_SUCCEEDED(rc)) {
if (requestedSize) *requestedSize = entry->requestedSize;
if (transferredSize) *transferredSize = entry->transferredSize;
}
return rc;
}
} // namespace
UsbHs::UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout)
: Base{transfer_timeout}
, m_index{index}
, m_filter{filter} {
}
UsbHs::~UsbHs() {
Close();
usbHsDestroyInterfaceAvailableEvent(std::addressof(m_event), m_index);
usbHsExit();
}
Result UsbHs::Init() {
log_write("doing USB init\n");
R_TRY(usbHsInitialize());
R_TRY(usbHsCreateInterfaceAvailableEvent(&m_event, true, m_index, &m_filter));
log_write("success USB init\n");
R_SUCCEED();
}
Result UsbHs::IsUsbConnected(u64 timeout) {
if (m_connected) {
R_SUCCEED();
}
const std::array waiters{
waiterForEvent(&m_event),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
R_TRY(waitObjects(&idx, waiters.data(), waiters.size(), timeout));
if (idx == waiters.size() - 1) {
return Result_Cancelled;
}
return Connect();
}
Result UsbHs::Connect() {
Close();
s32 total;
R_TRY(usbHsQueryAvailableInterfaces(&m_filter, &m_interface, sizeof(m_interface), &total));
R_TRY(usbHsAcquireUsbIf(&m_s, &m_interface));
const auto bcdUSB = Bcd{m_interface.device_desc.bcdUSB};
const auto bcdDevice = Bcd{m_interface.device_desc.bcdDevice};
// log lsusb style.
log_write("[USBHS] pathstr: %s\n", m_interface.pathstr);
log_write("Bus: %03u Device: %03u ID: %04x:%04x\n\n", m_interface.busID, m_interface.deviceID, m_interface.device_desc.idVendor, m_interface.device_desc.idProduct);
log_write("Device Descriptor:\n");
log_write("\tbLength: %u\n", m_interface.device_desc.bLength);
log_write("\tbDescriptorType: %u\n", m_interface.device_desc.bDescriptorType);
log_write("\tbcdUSB: %u:%u%u\n", bcdUSB.major(), bcdUSB.minor(), bcdUSB.macro());
log_write("\tbDeviceClass: %u\n", m_interface.device_desc.bDeviceClass);
log_write("\tbDeviceSubClass: %u\n", m_interface.device_desc.bDeviceSubClass);
log_write("\tbDeviceProtocol: %u\n", m_interface.device_desc.bDeviceProtocol);
log_write("\tbMaxPacketSize0: %u\n", m_interface.device_desc.bMaxPacketSize0);
log_write("\tidVendor: 0x%x\n", m_interface.device_desc.idVendor);
log_write("\tidProduct: 0x%x\n", m_interface.device_desc.idProduct);
log_write("\tbcdDevice: %u:%u%u\n", bcdDevice.major(), bcdDevice.minor(), bcdDevice.macro());
log_write("\tiManufacturer: %u\n", m_interface.device_desc.iManufacturer);
log_write("\tiProduct: %u\n", m_interface.device_desc.iProduct);
log_write("\tiSerialNumber: %u\n", m_interface.device_desc.iSerialNumber);
log_write("\tbNumConfigurations: %u\n", m_interface.device_desc.bNumConfigurations);
log_write("\tConfiguration Descriptor:\n");
log_write("\t\tbLength: %u\n", m_interface.config_desc.bLength);
log_write("\t\tbDescriptorType: %u\n", m_interface.config_desc.bDescriptorType);
log_write("\t\twTotalLength: %u\n", m_interface.config_desc.wTotalLength);
log_write("\t\tbNumInterfaces: %u\n", m_interface.config_desc.bNumInterfaces);
log_write("\t\tbConfigurationValue: %u\n", m_interface.config_desc.bConfigurationValue);
log_write("\t\tiConfiguration: %u\n", m_interface.config_desc.iConfiguration);
log_write("\t\tbmAttributes: 0x%x\n", m_interface.config_desc.bmAttributes);
log_write("\t\tMaxPower: %u (%u mA)\n", m_interface.config_desc.MaxPower, m_interface.config_desc.MaxPower * 2);
struct usb_endpoint_descriptor invalid_desc{};
for (u8 i = 0; i < std::size(m_s.inf.inf.input_endpoint_descs); i++) {
const auto& desc = m_s.inf.inf.input_endpoint_descs[i];
if (std::memcmp(&desc, &invalid_desc, sizeof(desc))) {
log_write("\t[USBHS] desc[%u] wMaxPacketSize: 0x%X\n", i, desc.wMaxPacketSize);
}
}
auto& input_descs = m_s.inf.inf.input_endpoint_descs[0];
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_Out], 1, input_descs.wMaxPacketSize, &input_descs));
auto& output_descs = m_s.inf.inf.output_endpoint_descs[0];
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_In], 1, output_descs.wMaxPacketSize, &output_descs));
m_connected = true;
R_SUCCEED();
}
void UsbHs::Close() {
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_In]));
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_Out]));
usbHsIfClose(std::addressof(m_s));
m_endpoints[UsbSessionEndpoint_In] = {};
m_endpoints[UsbSessionEndpoint_Out] = {};
m_s = {};
m_connected = false;
}
Event *UsbHs::GetCompletionEvent(UsbSessionEndpoint ep) {
return usbHsEpGetXferEvent(&m_endpoints[ep]);
}
Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
const std::array waiters{
waiterForEvent(GetCompletionEvent(ep)),
waiterForEvent(usbHsGetInterfaceStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usb timeout event\n");
rc = KERNELRESULT(TimedOut);
Close();
}
if (R_FAILED(rc)) {
log_write("failed to wait for event\n");
eventClear(GetCompletionEvent(ep));
eventClear(usbHsGetInterfaceStateChangeEvent());
}
return rc;
}
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) {
return usbHsEpPostBufferAsync(&m_endpoints[ep], buffer, size, 0, out_xfer_id);
}
Result UsbHs::GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) {
u32 count;
UsbHsXferReport report_data[8];
R_TRY(eventClear(GetCompletionEvent(ep)));
R_TRY(usbHsEpGetXferReport(&m_endpoints[ep], report_data, std::size(report_data), std::addressof(count)));
R_TRY(usbHsParseReportData(report_data, count, xfer_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -2,6 +2,7 @@
#include "defines.hpp" #include "defines.hpp"
#include "log.hpp" #include "log.hpp"
#include <memory> #include <memory>
#include <cstring>
namespace sphaira::yati::container { namespace sphaira::yati::container {
namespace { namespace {
@@ -22,6 +23,38 @@ struct Pfs0FileTableEntry {
u32 padding; u32 padding;
}; };
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
BufHelper(std::span<const u8> data) {
write(data);
}
void write(const void* data, u64 size) {
if (offset + size >= buf.size()) {
buf.resize(offset + size);
}
std::memcpy(buf.data() + offset, data, size);
offset += size;
}
void write(std::span<const u8> data) {
write(data.data(), data.size());
}
void seek(u64 where_to) {
offset = where_to;
}
[[nodiscard]]
auto tell() const {
return offset;
}
std::vector<u8> buf;
u64 offset{};
};
} // namespace } // namespace
Result Nsp::GetCollections(Collections& out) { Result Nsp::GetCollections(Collections& out) {
@@ -56,4 +89,48 @@ Result Nsp::GetCollections(Collections& out) {
R_SUCCEED(); R_SUCCEED();
} }
auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8> {
BufHelper buf;
Pfs0Header header{};
std::vector<Pfs0FileTableEntry> file_table(entries.size());
std::vector<char> string_table;
u64 string_offset{};
u64 data_offset{};
for (u32 i = 0; i < entries.size(); i++) {
file_table[i].data_offset = data_offset;
file_table[i].data_size = entries[i].size;
file_table[i].name_offset = string_offset;
file_table[i].padding = 0;
string_table.resize(string_offset + entries[i].name.length() + 1);
std::memcpy(string_table.data() + string_offset, entries[i].name.c_str(), entries[i].name.length() + 1);
data_offset += file_table[i].data_size;
string_offset += entries[i].name.length() + 1;
}
// align table
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
header.magic = PFS0_MAGIC;
header.total_files = entries.size();
header.string_table_size = string_table.size();
header.padding = 0;
buf.write(&header, sizeof(header));
buf.write(file_table.data(), sizeof(Pfs0FileTableEntry) * file_table.size());
buf.write(string_table.data(), string_table.size());
// calculate nsp size.
size = buf.tell();
for (const auto& e : file_table) {
size += e.data_size;
}
return buf.buf;
}
} // namespace sphaira::yati::container } // namespace sphaira::yati::container

View File

@@ -1,6 +1,7 @@
#include "yati/nx/es.hpp" #include "yati/nx/es.hpp"
#include "yati/nx/crypto.hpp" #include "yati/nx/crypto.hpp"
#include "yati/nx/nxdumptool_rsa.h" #include "yati/nx/nxdumptool_rsa.h"
#include "yati/nx/service_guard.h"
#include "defines.hpp" #include "defines.hpp"
#include "log.hpp" #include "log.hpp"
#include <memory> #include <memory>
@@ -9,12 +10,124 @@
namespace sphaira::es { namespace sphaira::es {
namespace { namespace {
Service g_esSrv;
NX_GENERATE_SERVICE_GUARD(es);
Result _esInitialize() {
return smGetService(&g_esSrv, "es");
}
void _esCleanup() {
serviceClose(&g_esSrv);
}
Result ListTicket(u32 cmd_id, s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
struct {
u32 num_rights_ids_written;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, cmd_id, *out_entries_written, out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { out_ids, count * sizeof(*out_ids) } },
);
if (R_SUCCEEDED(rc) && out_entries_written) *out_entries_written = out.num_rights_ids_written;
return rc;
}
} // namespace } // namespace
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) { Result Initialize() {
return serviceDispatch(srv, 1, return esInitialize();
}
void Exit() {
esExit();
}
Service* GetServiceSession() {
return &g_esSrv;
}
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
return serviceDispatch(&g_esSrv, 1,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In }, .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }); .buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
);
}
Result CountCommonTicket(s32* count) {
return serviceDispatchOut(&g_esSrv, 9, *count);
}
Result CountPersonalizedTicket(s32* count) {
return serviceDispatchOut(&g_esSrv, 10, *count);
}
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(11, out_entries_written, out_ids, count);
}
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(12, out_entries_written, out_ids, count);
}
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(13, out_entries_written, out_ids, count);
}
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId) {
return serviceDispatchInOut(&g_esSrv, 14, *rightsId, *size_out);
}
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId) {
return serviceDispatchInOut(&g_esSrv, 16, *rightsId, *size_out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { tik_data, tik_size } },
);
}
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId) {
if (hosversionBefore(4,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
struct {
u64 ticket_size;
u64 cert_size;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, 22, *rightsId, out);
if (R_SUCCEEDED(rc)) {
*tik_size_out = out.ticket_size;
*cert_size_out = out.cert_size;
}
return rc;
}
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId) {
if (hosversionBefore(4,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
struct {
u64 ticket_size;
u64 cert_size;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, 23, *rightsId, out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out, SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
);
if (R_SUCCEEDED(rc)) {
*tik_size_out = out.ticket_size;
*cert_size_out = out.cert_size;
}
return rc;
} }
typedef enum { typedef enum {

View File

@@ -1,6 +1,9 @@
#include "yati/nx/ncm.hpp" #include "yati/nx/ncm.hpp"
#include "defines.hpp" #include "defines.hpp"
#include <memory> #include <memory>
#include <bit>
#include <cstring>
#include <cstdlib>
namespace sphaira::ncm { namespace sphaira::ncm {
namespace { namespace {
@@ -25,6 +28,25 @@ auto GetMetaTypeStr(u8 meta_type) -> const char* {
return "Unknown"; return "Unknown";
} }
// taken from nxdumptool
auto GetMetaTypeShortStr(u8 meta_type) -> const char* {
switch (meta_type) {
case NcmContentMetaType_Unknown: return "UNK";
case NcmContentMetaType_SystemProgram: return "SYSPRG";
case NcmContentMetaType_SystemData: return "SYSDAT";
case NcmContentMetaType_SystemUpdate: return "SYSUPD";
case NcmContentMetaType_BootImagePackage: return "BIP";
case NcmContentMetaType_BootImagePackageSafe: return "BIPS";
case NcmContentMetaType_Application: return "BASE";
case NcmContentMetaType_Patch: return "UPD";
case NcmContentMetaType_AddOnContent: return "DLC";
case NcmContentMetaType_Delta: return "DELTA";
case NcmContentMetaType_DataPatch: return "DLCUPD";
}
return "UNK";
}
auto GetStorageIdStr(u8 storage_id) -> const char* { auto GetStorageIdStr(u8 storage_id) -> const char* {
switch (storage_id) { switch (storage_id) {
case NcmStorageId_None: return "None"; case NcmStorageId_None: return "None";
@@ -57,6 +79,18 @@ auto GetAppId(const PackagedContentMeta& meta) -> u64 {
return GetAppId(meta.meta_type, meta.title_id); return GetAppId(meta.meta_type, meta.title_id);
} }
auto GetContentIdFromStr(const char* str) -> NcmContentId {
char lowerU64[0x11]{};
char upperU64[0x11]{};
std::memcpy(lowerU64, str, 0x10);
std::memcpy(upperU64, str + 0x10, 0x10);
NcmContentId nca_id{};
*(u64*)nca_id.c = std::byteswap(std::strtoul(lowerU64, nullptr, 0x10));
*(u64*)(nca_id.c + 8) = std::byteswap(std::strtoul(upperU64, nullptr, 0x10));
return nca_id;
}
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) { Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
bool has; bool has;
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id)); R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));

View File

@@ -18,266 +18,34 @@
// The USB protocol was taken from Tinfoil, by Adubbz. // The USB protocol was taken from Tinfoil, by Adubbz.
#include "yati/source/usb.hpp" #include "yati/source/usb.hpp"
#include "usb/tinfoil.hpp"
#include "log.hpp" #include "log.hpp"
#include <ranges> #include <ranges>
namespace sphaira::yati::source { namespace sphaira::yati::source {
namespace { namespace {
enum USBCmdType : u8 { namespace tinfoil = usb::tinfoil;
REQUEST = 0,
RESPONSE = 1
};
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
struct NX_PACKED USBCmdHeader {
u32 magic;
USBCmdType type;
u8 padding[0x3] = {0};
u32 cmdId;
u64 dataSize;
u8 reserved[0xC] = {0};
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace } // namespace
Usb::Usb(u64 transfer_timeout) { Usb::Usb(u64 transfer_timeout) {
m_open_result = usbDsInitialize(); m_usb = std::make_unique<usb::UsbDs>(transfer_timeout);
m_transfer_timeout = transfer_timeout; m_open_result = m_usb->Init();
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16);
} }
Usb::~Usb() { Usb::~Usb() {
if (R_SUCCEEDED(GetOpenResult())) {
usbDsExit();
}
}
Result Usb::Init() {
log_write("doing USB init\n");
R_TRY(m_open_result);
SetSysSerialNumber serial_number;
R_TRY(setsysInitialize());
ON_SCOPE_EXIT(setsysExit());
R_TRY(setsysGetSerialNumber(&serial_number));
u8 iManufacturer, iProduct, iSerialNumber;
static const u16 supported_langs[1] = {0x0409};
// Send language descriptor
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
// Send manufacturer
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
.bLength = USB_DT_DEVICE_SIZE,
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x0110,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = 0x40,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice = 0x0100,
.iManufacturer = iManufacturer,
.iProduct = iProduct,
.iSerialNumber = iSerialNumber,
.bNumConfigurations = 0x01
};
// Full Speed is USB 1.1
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
// High Speed is USB 2.0
device_descriptor.bcdUSB = 0x0200;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
// Super Speed is USB 3.0
device_descriptor.bcdUSB = 0x0300;
// Upgrade packet size to 512
device_descriptor.bMaxPacketSize0 = 0x09;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
// Define Binary Object Store
const u8 bos[0x16] = {
0x05, // .bLength
USB_DT_BOS, // .bDescriptorType
0x16, 0x00, // .wTotalLength
0x02, // .bNumDeviceCaps
// USB 2.0
0x07, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x02, // .bDevCapabilityType
0x02, 0x00, 0x00, 0x00, // dev_capability_data
// USB 3.0
0x0A, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x03, /* .bDevCapabilityType */
0x00, /* .bmAttributes */
0x0E, 0x00, /* .wSpeedSupported */
0x03, /* .bFunctionalitySupport */
0x00, /* .bU1DevExitLat */
0x00, 0x00 /* .bU2DevExitLat */
};
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
struct usb_interface_descriptor interface_descriptor = {
.bLength = USB_DT_INTERFACE_SIZE,
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
struct usb_endpoint_descriptor endpoint_descriptor_in = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_IN,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
struct usb_endpoint_descriptor endpoint_descriptor_out = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
.bMaxBurst = 0x0F,
.bmAttributes = 0x00,
.wBytesPerInterval = 0x00,
};
R_TRY(usbDsRegisterInterface(&m_interface));
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x40;
endpoint_descriptor_out.wMaxPacketSize = 0x40;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
//Setup endpoints.
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
log_write("success USB init\n");
R_SUCCEED();
}
// the blow code is taken from libnx, with the addition of a uevent to cancel.
Result Usb::IsUsbConnected(u64 timeout) {
Result rc;
UsbState state = UsbState_Detached;
rc = usbDsGetState(&state);
if (R_FAILED(rc)) return rc;
if (state == UsbState_Configured) return 0;
bool has_timeout = timeout != UINT64_MAX;
u64 deadline = 0;
const std::array waiters{
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
if (has_timeout)
deadline = armGetSystemTick() + armNsToTicks(timeout);
do {
if (has_timeout) {
s64 remaining = deadline - armGetSystemTick();
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
}
s32 idx;
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
eventClear(usbDsGetStateChangeEvent());
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx != 0) {
rc = Result_Cancelled; // cancelled.
break;
}
rc = usbDsGetState(&state);
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
return KERNELRESULT(TimedOut);
return rc;
} }
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) { Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
TUSHeader header; tinfoil::TUSHeader header;
R_TRY(TransferAll(true, &header, sizeof(header), timeout)); R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout));
R_UNLESS(header.magic == 0x304C5554, Result_BadMagic); R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic);
R_UNLESS(header.nspListSize > 0, Result_BadCount); R_UNLESS(header.nspListSize > 0, Result_BadCount);
log_write("USB got header\n"); log_write("USB got header\n");
std::vector<char> names(header.nspListSize); std::vector<char> names(header.nspListSize);
R_TRY(TransferAll(true, names.data(), names.size(), timeout)); R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
out_names.clear(); out_names.clear();
for (const auto& name : std::views::split(names, '\n')) { for (const auto& name : std::views::split(names, '\n')) {
@@ -299,133 +67,42 @@ void Usb::SetFileNameForTranfser(const std::string& name) {
m_transfer_file_name = name; m_transfer_file_name = name;
} }
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const { Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout) {
return std::addressof(m_endpoints[ep]->CompletionEvent); tinfoil::USBCmdHeader header{
} .magic = tinfoil::Magic_Command0,
.type = tinfoil::USBCmdType::REQUEST,
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
auto event = GetCompletionEvent(ep);
const std::array waiters{
waiterForEvent(event),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx != 0) {
log_write("got usb cancel event\n");
rc = Result_Cancelled; // cancelled.
}
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
eventClear(event);
}
return rc;
}
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
UsbDsReportData report_data;
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
u32 urb_id;
/* If we're not configured yet, wait to become configured first. */
R_TRY(IsUsbConnected(timeout));
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
/* Return what we transferred. */
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
}
// while it may seem like a bad idea to transfer data to a buffer and copy it
// in practice, this has no impact on performance.
// the switch is *massively* bottlenecked by slow io (nand and sd).
// so making usb transfers zero-copy provides no benefit other than increased
// code complexity and the increase of future bugs if/when sphaira is forked
// an changes are made.
// yati already goes to great lengths to be zero-copy during installing
// by swapping buffers and inflating in-place.
Result Usb::TransferAll(bool read, void *data, u32 size, u64 timeout) {
auto buf = static_cast<u8*>(data);
m_aligned.resize((size + 0xFFF) & ~0xFFF);
while (size) {
if (!read) {
std::memcpy(m_aligned.data(), buf, size);
}
u32 out_size_transferred;
R_TRY(TransferPacketImpl(read, m_aligned.data(), size, &out_size_transferred, timeout));
if (read) {
std::memcpy(buf, m_aligned.data(), out_size_transferred);
}
buf += out_size_transferred;
size -= out_size_transferred;
}
R_SUCCEED();
}
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
USBCmdHeader header{
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
.type = USBCmdType::REQUEST,
.cmdId = cmdId, .cmdId = cmdId,
.dataSize = dataSize, .dataSize = dataSize,
}; };
return TransferAll(false, &header, sizeof(header), m_transfer_timeout); return m_usb->TransferAll(false, &header, sizeof(header), timeout);
} }
Result Usb::SendFileRangeCmd(u64 off, u64 size) { Result Usb::SendFileRangeCmd(u64 off, u64 size, u64 timeout) {
FileRangeCmdHeader fRangeHeader; tinfoil::FileRangeCmdHeader fRangeHeader;
fRangeHeader.size = size; fRangeHeader.size = size;
fRangeHeader.offset = off; fRangeHeader.offset = off;
fRangeHeader.nspNameLen = m_transfer_file_name.size(); fRangeHeader.nspNameLen = m_transfer_file_name.size();
fRangeHeader.padding = 0; fRangeHeader.padding = 0;
R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen)); R_TRY(SendCmdHeader(tinfoil::USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen, timeout));
R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout)); R_TRY(m_usb->TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), timeout));
R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout)); R_TRY(m_usb->TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), timeout));
USBCmdHeader responseHeader; tinfoil::USBCmdHeader responseHeader;
R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout)); R_TRY(m_usb->TransferAll(true, &responseHeader, sizeof(responseHeader), timeout));
R_SUCCEED(); R_SUCCEED();
} }
Result Usb::Finished() { Result Usb::Finished(u64 timeout) {
return SendCmdHeader(USBCmdId::EXIT, 0); return SendCmdHeader(tinfoil::USBCmdId::EXIT, 0, timeout);
} }
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) { Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult()); R_TRY(GetOpenResult());
R_TRY(SendFileRangeCmd(off, size)); R_TRY(SendFileRangeCmd(off, size, m_usb->GetTransferTimeout()));
R_TRY(TransferAll(true, buf, size, m_transfer_timeout)); R_TRY(m_usb->TransferAll(true, buf, size));
*bytes_read = size; *bytes_read = size;
R_SUCCEED(); R_SUCCEED();
} }

View File

@@ -275,7 +275,6 @@ struct Yati {
NcmContentMetaDatabase db{}; NcmContentMetaDatabase db{};
NcmStorageId storage_id{}; NcmStorageId storage_id{};
Service es{};
Service ns_app{}; Service ns_app{};
std::unique_ptr<container::Base> container{}; std::unique_ptr<container::Base> container{};
Config config{}; Config config{};
@@ -452,7 +451,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
for (s64 off = 0; off < size;) { for (s64 off = 0; off < size;) {
// log_write("looking for section\n"); // log_write("looking for section\n");
if (!ncz_section || !ncz_section->InRange(written)) { if (!ncz_section || !ncz_section->InRange(written)) {
auto it = std::find_if(t->ncz_sections.cbegin(), t->ncz_sections.cend(), [written](auto& e){ auto it = std::ranges::find_if(t->ncz_sections, [written](auto& e){
return e.InRange(written); return e.InRange(written);
}); });
@@ -504,6 +503,8 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
if (!is_ncz || !decompress_buf_off) { if (!is_ncz || !decompress_buf_off) {
// check nca header // check nca header
if (!decompress_buf_off) { if (!decompress_buf_off) {
log_write("reading nca header\n");
nca::Header header{}; nca::Header header{};
crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false); crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false);
log_write("verifying nca header magic\n"); log_write("verifying nca header magic\n");
@@ -522,6 +523,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
} }
t->write_size = header.size; t->write_size = header.size;
log_write("setting placeholder size: %zu\n", header.size);
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size)); R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) { if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
@@ -531,11 +533,13 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
TikCollection* ticket = nullptr; TikCollection* ticket = nullptr;
if (isRightsIdValid(header.rights_id)) { if (isRightsIdValid(header.rights_id)) {
auto it = std::find_if(t->tik.begin(), t->tik.end(), [&header](auto& e){ auto it = std::ranges::find_if(t->tik, [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id)); return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
}); });
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
R_UNLESS(it != t->tik.end(), Result_TicketNotFound); R_UNLESS(it != t->tik.end(), Result_TicketNotFound);
log_write("ticket found\n");
it->required = true; it->required = true;
ticket = &(*it); ticket = &(*it);
} }
@@ -607,7 +611,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
// todo: blocks need to use read offset, as the offset + size is compressed range. // todo: blocks need to use read offset, as the offset + size is compressed range.
if (t->ncz_blocks.size()) { if (t->ncz_blocks.size()) {
if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) { if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) {
auto it = std::find_if(t->ncz_blocks.cbegin(), t->ncz_blocks.cend(), [decompress_buf_off](auto& e){ auto it = std::ranges::find_if(t->ncz_blocks, [decompress_buf_off](auto& e){
return e.InRange(decompress_buf_off); return e.InRange(decompress_buf_off);
}); });
@@ -757,13 +761,13 @@ Yati::~Yati() {
splCryptoExit(); splCryptoExit();
serviceClose(std::addressof(ns_app)); serviceClose(std::addressof(ns_app));
nsExit(); nsExit();
es::Exit();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i])); ncmContentMetaDatabaseClose(std::addressof(ncm_db[i]));
ncmContentStorageClose(std::addressof(ncm_cs[i])); ncmContentStorageClose(std::addressof(ncm_cs[i]));
} }
serviceClose(std::addressof(es));
appletSetMediaPlaybackState(false); appletSetMediaPlaybackState(false);
if (config.boost_mode) { if (config.boost_mode) {
@@ -799,7 +803,7 @@ Result Yati::Setup(const ConfigOverride& override) {
R_TRY(splCryptoInitialize()); R_TRY(splCryptoInitialize());
R_TRY(nsInitialize()); R_TRY(nsInitialize());
R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app))); R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app)));
R_TRY(smGetService(std::addressof(es), "es")); R_TRY(es::Initialize());
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i])); R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]));
@@ -960,7 +964,7 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
} }
const auto str = hexIdToStr(info.content_id); const auto str = hexIdToStr(info.content_id);
const auto it = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){ const auto it = std::ranges::find_if(collections, [&str](auto& e){
return e.name.find(str.str) != e.name.npos; return e.name.find(str.str) != e.name.npos;
}); });
@@ -1004,7 +1008,7 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
return lhs.type > rhs.type; return lhs.type > rhs.type;
}; };
std::sort(cnmt.ncas.begin(), cnmt.ncas.end(), sorter); std::ranges::sort(cnmt.ncas, sorter);
log_write("found all cnmts\n"); log_write("found all cnmts\n");
R_SUCCEED(); R_SUCCEED();
@@ -1017,7 +1021,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, con
keys::parse_hex_key(entry.rights_id.c, collection.name.c_str()); keys::parse_hex_key(entry.rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert"; const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){ const auto cert = std::ranges::find_if(collections, [&str](auto& e){
return e.name.find(str) != e.name.npos; return e.name.find(str) != e.name.npos;
}); });
@@ -1117,7 +1121,7 @@ Result Yati::ImportTickets(std::span<TikCollection> tickets) {
log_write("patching ticket\n"); log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, keys)); R_TRY(es::PatchTicket(ticket.ticket, keys));
log_write("installing ticket\n"); log_write("installing ticket\n");
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size())); R_TRY(es::ImportTicket(ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false; ticket.required = false;
} }
} }
@@ -1317,7 +1321,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
return lhs.offset < rhs.offset; return lhs.offset < rhs.offset;
}; };
std::sort(collections.begin(), collections.end(), sorter); std::ranges::sort(collections, sorter);
for (const auto& collection : collections) { for (const auto& collection : collections) {
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) { if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
@@ -1334,7 +1338,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
keys::parse_hex_key(rights_id.c, collection.name.c_str()); keys::parse_hex_key(rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert"; const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
auto entry = std::find_if(tickets.begin(), tickets.end(), [&rights_id](auto& e){ auto entry = std::ranges::find_if(tickets, [&rights_id](auto& e){
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id)); return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
}); });
@@ -1353,7 +1357,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
for (auto& cnmt : cnmts) { for (auto& cnmt : cnmts) {
// copy nca structs into cnmt. // copy nca structs into cnmt.
for (auto& cnmt_nca : cnmt.ncas) { for (auto& cnmt_nca : cnmt.ncas) {
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [&cnmt_nca](auto& e){ auto it = std::ranges::find_if(ncas, [&cnmt_nca](auto& e){
return e.name == cnmt_nca.name; return e.name == cnmt_nca.name;
}); });