diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index b877734..de3319b 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -79,6 +79,11 @@ add_executable(sphaira source/i18n.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/container/nsp.cpp source/yati/container/xci.cpp diff --git a/sphaira/include/ui/menus/game_menu.hpp b/sphaira/include/ui/menus/game_menu.hpp index 0d745db..8bc5ba8 100644 --- a/sphaira/include/ui/menus/game_menu.hpp +++ b/sphaira/include/ui/menus/game_menu.hpp @@ -25,6 +25,7 @@ struct Entry { char display_version[0x10]{}; NacpLanguageEntry lang{}; int image{}; + bool selected{}; std::shared_ptr control{}; u64 control_size{}; @@ -103,11 +104,39 @@ private: void FreeEntries(); void OnLayoutChange(); + auto GetSelectedEntries() const { + std::vector 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: static constexpr inline const char* INI_SECTION = "games"; + static constexpr inline const char* INI_SECTION_DUMP = "dump"; std::vector m_entries{}; s64 m_index{}; // where i am in the array + s64 m_selected_count{}; std::unique_ptr m_list{}; Event m_event{}; bool m_is_reversed{}; diff --git a/sphaira/include/usb/base.hpp b/sphaira/include/usb/base.hpp new file mode 100644 index 0000000..addcf3e --- /dev/null +++ b/sphaira/include/usb/base.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include + +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 + 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 + struct PageAllocator : CustomVectorAllocator { + using value_type = T; // used by std::vector + }; + + using PageAlignedVector = std::vector>; + +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 diff --git a/sphaira/include/usb/tinfoil.hpp b/sphaira/include/usb/tinfoil.hpp new file mode 100644 index 0000000..78df699 --- /dev/null +++ b/sphaira/include/usb/tinfoil.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +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 diff --git a/sphaira/include/usb/usb_uploader.hpp b/sphaira/include/usb/usb_uploader.hpp new file mode 100644 index 0000000..d7be621 --- /dev/null +++ b/sphaira/include/usb/usb_uploader.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "usb/usbhs.hpp" + +#include +#include +#include +#include + +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 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 m_usb; +}; + +} // namespace sphaira::usb::upload diff --git a/sphaira/include/usb/usbds.hpp b/sphaira/include/usb/usbds.hpp new file mode 100644 index 0000000..c0eb0f9 --- /dev/null +++ b/sphaira/include/usb/usbds.hpp @@ -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 diff --git a/sphaira/include/usb/usbhs.hpp b/sphaira/include/usb/usbhs.hpp new file mode 100644 index 0000000..630d0f6 --- /dev/null +++ b/sphaira/include/usb/usbhs.hpp @@ -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 diff --git a/sphaira/include/yati/container/nsp.hpp b/sphaira/include/yati/container/nsp.hpp index 84b7d30..a468901 100644 --- a/sphaira/include/yati/container/nsp.hpp +++ b/sphaira/include/yati/container/nsp.hpp @@ -2,12 +2,16 @@ #include "base.hpp" #include +#include namespace sphaira::yati::container { struct Nsp final : Base { using Base::Base; Result GetCollections(Collections& out) override; + + // builds nsp meta data and the size of the entier nsp. + static auto Build(std::span collections, s64& size) -> std::vector; }; } // namespace sphaira::yati::container diff --git a/sphaira/include/yati/nx/es.hpp b/sphaira/include/yati/nx/es.hpp index 74f079d..315222c 100644 --- a/sphaira/include/yati/nx/es.hpp +++ b/sphaira/include/yati/nx/es.hpp @@ -69,7 +69,25 @@ struct EticketRsaDeviceKey { static_assert(sizeof(EticketRsaDeviceKey) == 0x240); // 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. Result GetTicketDataOffset(std::span ticket, u64& out); diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp index 1287bd1..a79222e 100644 --- a/sphaira/include/yati/nx/ncm.hpp +++ b/sphaira/include/yati/nx/ncm.hpp @@ -33,11 +33,14 @@ union ExtendedHeader { auto GetMetaTypeStr(u8 meta_type) -> 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(const NcmContentMetaKey& key) -> u64; auto GetAppId(const PackagedContentMeta& meta) -> u64; +auto GetContentIdFromStr(const char* str) -> NcmContentId; + Result Delete(NcmContentStorage* cs, const NcmContentId *content_id); Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id); diff --git a/sphaira/include/yati/nx/service_guard.h b/sphaira/include/yati/nx/service_guard.h new file mode 100644 index 0000000..5fbc5fc --- /dev/null +++ b/sphaira/include/yati/nx/service_guard.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include + +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), ()) \ No newline at end of file diff --git a/sphaira/include/yati/source/usb.hpp b/sphaira/include/yati/source/usb.hpp index 820bb76..08009c8 100644 --- a/sphaira/include/yati/source/usb.hpp +++ b/sphaira/include/yati/source/usb.hpp @@ -2,10 +2,10 @@ #include "base.hpp" #include "fs.hpp" +#include "usb/usbds.hpp" -#include #include -#include +#include #include namespace sphaira::yati::source { @@ -19,80 +19,31 @@ struct Usb final : Base { Result_BadCount = MAKERESULT(USBModule, 2), Result_BadTransferSize = MAKERESULT(USBModule, 3), Result_BadTotalSize = MAKERESULT(USBModule, 4), - Result_Cancelled = MAKERESULT(USBModule, 11), }; Usb(u64 transfer_timeout); ~Usb(); 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& out_names); void SetFileNameForTranfser(const std::string& name); - auto GetCancelEvent() { - return &m_uevent; - } - void SignalCancel() override { - ueventSignal(GetCancelEvent()); + m_usb->Cancel(); } -public: - // custom allocator for std::vector that respects alignment. - // https://en.cppreference.com/w/cpp/named_req/Allocator - template - 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 - struct PageAllocator : CustomVectorAllocator { - using value_type = T; // used by std::vector - }; - - using PageAlignedVector = std::vector>; +private: + Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout); + Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout); private: - enum UsbSessionEndpoint { - 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 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::unique_ptr m_usb; std::string m_transfer_file_name{}; }; diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 9fc896c..9753ede 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -10,8 +10,14 @@ #include "ui/nvg_util.hpp" #include "defines.hpp" #include "i18n.hpp" + #include "yati/nx/ncm.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 #include @@ -20,30 +26,340 @@ namespace sphaira::ui::menu::game { namespace { -constexpr NcmStorageId NCM_STORAGE_IDS[]{ - NcmStorageId_BuiltInUser, - NcmStorageId_SdCard, +constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) { + if (meta_type & 0x80) { + 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]; -NcmContentMetaDatabase ncm_db[2]; +enum DumpLocationType { + 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) { - if (storage_id == NcmStorageId_SdCard) { - return ncm_cs[1]; - } - return ncm_cs[0]; + return GetNcmEntry(storage_id).cs; } auto& GetNcmDb(u8 storage_id) { - if (storage_id == NcmStorageId_SdCard) { - return ncm_db[1]; - } - return ncm_db[0]; + return GetNcmEntry(storage_id).db; } using MetaEntries = std::vector; +struct ContentInfoEntry { + NsApplicationContentMetaStatus status{}; + std::vector content_infos{}; + std::vector rights_ids{}; +}; + +struct TikEntry { + FsRightsId id{}; + std::vector tik_data{}; + std::vector 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 tickets{}; + // all the collections for this nsp, such as nca's and tickets. + std::vector collections{}; + // raw nsp data (header, file table and string table). + std::vector 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 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 { + return m_entries; + } + +private: + std::span m_entries{}; +}; + +struct UsbTest final : usb::upload::Usb { + UsbTest(ProgressBox* pbox, std::span entries) : Usb{UINT64_MAX} { + m_source = std::make_unique(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 m_source{}; + ProgressBox* m_pbox{}; + std::string m_path{}; + s64 m_offset{}; +}; + +Result DumpNspToFile(ProgressBox* pbox, std::span 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(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 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 entries) { + std::vector file_list; + for (auto& e : entries) { + file_list.emplace_back(e.path); + } + + auto usb = std::make_unique(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 entries) { + auto source = std::make_unique(entries); + for (const auto& e : entries) { + pbox->SetTitle(e.application_name); + pbox->NewTransfer(e.path); + + s64 offset{}; + std::vector 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) { if (R_FAILED(rc)) { App::Push(std::make_shared(rc, @@ -56,19 +372,26 @@ Result Notify(Result rc, const std::string& error_message) { return rc; } -Result GetMetaEntries(u64 id, MetaEntries& out) { - s32 count; - R_TRY(nsCountApplicationContentMeta(id, &count)); +Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) { + for (s32 i = 0; ; i++) { + s32 count; + NsApplicationContentMetaStatus status; + R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count)); - out.resize(count); - R_TRY(nsListApplicationContentMetaStatus(id, 0, out.data(), out.size(), &count)); + if (!count) { + break; + } + + if (flags & ContentMetaTypeToContentFlag(status.meta_type)) { + out.emplace_back(status); + } + } - out.resize(count); R_SUCCEED(); } -Result GetMetaEntries(const Entry& e, MetaEntries& out) { - return GetMetaEntries(e.app_id, out); +Result GetMetaEntries(const Entry& e, MetaEntries& out, u32 flags = ContentFlag_All) { + return GetMetaEntries(e.app_id, out, flags); } // also sets the status to error. @@ -102,7 +425,7 @@ Result LoadControlManual(u64 id, ThreadResultData& data) { R_UNLESS(!entries.empty(), 0x1); 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; } @@ -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& 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) { nvgDeleteImage(vg, e.image); e.image = 0; @@ -267,7 +784,7 @@ void ThreadData::Push(u64 id) { mutexLock(&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()) { m_ids.emplace_back(id); ueventSignal(&m_uevent); @@ -290,6 +807,33 @@ void ThreadData::Pop(std::vector& out) { Menu::Menu() : grid::Menu{"Games"_i18n} { 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](){ SetPop(); }}), @@ -395,6 +939,27 @@ Menu::Menu() : grid::Menu{"Games"_i18n} { )); })); + options->Add(std::make_shared("Dump"_i18n, [this](){ + auto options = std::make_shared("Select content to dump"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + options->Add(std::make_shared("Dump All"_i18n, [this](){ + DumpGames(ContentFlag_All); + }, true)); + options->Add(std::make_shared("Dump Application"_i18n, [this](){ + DumpGames(ContentFlag_Application); + }, true)); + options->Add(std::make_shared("Dump Patch"_i18n, [this](){ + DumpGames(ContentFlag_Patch); + }, true)); + options->Add(std::make_shared("Dump AddOnContent"_i18n, [this](){ + DumpGames(ContentFlag_AddOnContent); + }, true)); + options->Add(std::make_shared("Dump DataPatch"_i18n, [this](){ + DumpGames(ContentFlag_DataPatch); + }, true)); + }, true)); + // completely deletes the application record and all data. options->Add(std::make_shared("Delete"_i18n, [this](){ 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, "Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { - const auto rc = nsDeleteApplicationCompletely(m_entries[m_index].app_id); - 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("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( - 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"); + DeleteGames(); } }, m_entries[m_index].image )); @@ -430,10 +980,10 @@ Menu::Menu() : grid::Menu{"Games"_i18n} { nsInitialize(); nsGetApplicationRecordUpdateSystemEvent(&m_event); + es::Initialize(); - for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { - ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]); - ncmOpenContentStorage(std::addressof(ncm_cs[i]), NCM_STORAGE_IDS[i]); + for (auto& e : ncm_entries) { + e.Open(); } threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, 0x30, 1); @@ -443,14 +993,14 @@ Menu::Menu() : grid::Menu{"Games"_i18n} { Menu::~Menu() { m_thread_data.Close(); - for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { - ncmContentMetaDatabaseClose(std::addressof(ncm_db[i])); - ncmContentStorageClose(std::addressof(ncm_cs[i])); + for (auto& e : ncm_entries) { + e.Close(); } FreeEntries(); eventClose(&m_event); nsExit(); + es::Exit(); threadWaitForExit(&m_thread); threadClose(&m_thread); @@ -489,7 +1039,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { m_thread_data.Pop(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; }); @@ -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) { - // const auto& [x, y, w, h] = v; + const auto& [x, y, w, h] = v; auto& e = m_entries[pos]; if (e.status == NacpLoadStatus::None) { @@ -516,6 +1066,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto selected = pos == m_index; 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()); this->Sort(); SetIndex(0); - - // m_thread_data.Push(m_entries); + ClearSelection(); } void Menu::Sort() { @@ -604,12 +1158,12 @@ void Menu::Sort() { if (order == OrderType_Ascending) { if (!m_is_reversed) { - std::reverse(m_entries.begin(), m_entries.end()); + std::ranges::reverse(m_entries); m_is_reversed = true; } } else { if (m_is_reversed) { - std::reverse(m_entries.begin(), m_entries.end()); + std::ranges::reverse(m_entries); m_is_reversed = false; } } @@ -660,4 +1214,76 @@ void Menu::OnLayoutChange() { grid::Menu::OnLayoutChange(m_list, m_layout.Get()); } +void Menu::DeleteGames() { + App::Push(std::make_shared(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( + "Select dump location"_i18n, items, [this, flags](auto op_index){ + if (!op_index) { + return; + } + + const auto index = *op_index; + App::Push(std::make_shared(0, "Dumping Games"_i18n, "", [this, index, flags](auto pbox) -> bool { + auto targets = GetSelectedEntries(); + + std::vector 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 diff --git a/sphaira/source/ui/menus/usb_menu.cpp b/sphaira/source/ui/menus/usb_menu.cpp index 142a5a6..b55b7a3 100644 --- a/sphaira/source/ui/menus/usb_menu.cpp +++ b/sphaira/source/ui/menus/usb_menu.cpp @@ -12,6 +12,7 @@ namespace { constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX; constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX; +constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds. void thread_func(void* user) { auto app = static_cast(user); @@ -22,7 +23,7 @@ void thread_func(void* user) { } 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; } @@ -71,11 +72,6 @@ Menu::Menu() : MenuBase{"USB"_i18n} { if (R_FAILED(m_usb_source->GetOpenResult())) { log_write("usb init open\n"); 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); @@ -114,7 +110,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { m_state = State::Progress; log_write("got connection\n"); App::Push(std::make_shared(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"); for (const auto& file_name : m_names) { diff --git a/sphaira/source/ui/progress_box.cpp b/sphaira/source/ui/progress_box.cpp index af2ef45..0a0662d 100644 --- a/sphaira/source/ui/progress_box.cpp +++ b/sphaira/source/ui/progress_box.cpp @@ -108,7 +108,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH()); 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. @@ -234,7 +234,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat R_TRY(fsFileSetSize(&dst_file, src_size)); s64 offset{}; - std::vector buf(1024*1024*8); // 8MiB + std::vector buf(1024*1024*4); // 4MiB while (offset < src_size) { 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)); Yield(); + UpdateTransfer(offset, src_size); offset += bytes_read; } diff --git a/sphaira/source/usb/base.cpp b/sphaira/source/usb/base.cpp new file mode 100644 index 0000000..1f37380 --- /dev/null +++ b/sphaira/source/usb/base.cpp @@ -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 . + */ + +// The USB transfer code was taken from Haze (part of Atmosphere). + +#include "usb/base.hpp" +#include "log.hpp" +#include "defines.hpp" +#include +#include + +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(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 diff --git a/sphaira/source/usb/usb_uploader.cpp b/sphaira/source/usb/usb_uploader.cpp new file mode 100644 index 0000000..66f801d --- /dev/null +++ b/sphaira/source/usb/usb_uploader.cpp @@ -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(INDEX, FILTER, transfer_timeout); + m_usb->Init(); +} + +Usb::~Usb() { +} + +Result Usb::WaitForConnection(u64 timeout, std::span 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 diff --git a/sphaira/source/usb/usbds.cpp b/sphaira/source/usb/usbds.cpp new file mode 100644 index 0000000..bc10e61 --- /dev/null +++ b/sphaira/source/usb/usbds.cpp @@ -0,0 +1,272 @@ +#include "usb/usbds.hpp" +#include "log.hpp" +#include "defines.hpp" +#include +#include + +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(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 diff --git a/sphaira/source/usb/usbhs.cpp b/sphaira/source/usb/usbhs.cpp new file mode 100644 index 0000000..c3eacee --- /dev/null +++ b/sphaira/source/usb/usbhs.cpp @@ -0,0 +1,202 @@ +#include "usb/usbhs.hpp" +#include "log.hpp" +#include "defines.hpp" +#include +#include + +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; posxferId == 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 diff --git a/sphaira/source/yati/container/nsp.cpp b/sphaira/source/yati/container/nsp.cpp index 4c5f663..2a52ba5 100644 --- a/sphaira/source/yati/container/nsp.cpp +++ b/sphaira/source/yati/container/nsp.cpp @@ -2,6 +2,7 @@ #include "defines.hpp" #include "log.hpp" #include +#include namespace sphaira::yati::container { namespace { @@ -22,6 +23,38 @@ struct Pfs0FileTableEntry { u32 padding; }; +// stdio-like wrapper for std::vector +struct BufHelper { + BufHelper() = default; + BufHelper(std::span 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 data) { + write(data.data(), data.size()); + } + + void seek(u64 where_to) { + offset = where_to; + } + + [[nodiscard]] + auto tell() const { + return offset; + } + + std::vector buf; + u64 offset{}; +}; + } // namespace Result Nsp::GetCollections(Collections& out) { @@ -56,4 +89,48 @@ Result Nsp::GetCollections(Collections& out) { R_SUCCEED(); } +auto Nsp::Build(std::span entries, s64& size) -> std::vector { + BufHelper buf; + + Pfs0Header header{}; + std::vector file_table(entries.size()); + std::vector 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 diff --git a/sphaira/source/yati/nx/es.cpp b/sphaira/source/yati/nx/es.cpp index 4bc4650..86ee5d4 100644 --- a/sphaira/source/yati/nx/es.cpp +++ b/sphaira/source/yati/nx/es.cpp @@ -1,6 +1,7 @@ #include "yati/nx/es.hpp" #include "yati/nx/crypto.hpp" #include "yati/nx/nxdumptool_rsa.h" +#include "yati/nx/service_guard.h" #include "defines.hpp" #include "log.hpp" #include @@ -9,12 +10,124 @@ namespace sphaira::es { 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 -Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) { - return serviceDispatch(srv, 1, +Result Initialize() { + 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 }, - .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 { diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp index 2013ded..16049f6 100644 --- a/sphaira/source/yati/nx/ncm.cpp +++ b/sphaira/source/yati/nx/ncm.cpp @@ -1,6 +1,9 @@ #include "yati/nx/ncm.hpp" #include "defines.hpp" #include +#include +#include +#include namespace sphaira::ncm { namespace { @@ -25,6 +28,25 @@ auto GetMetaTypeStr(u8 meta_type) -> const char* { 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* { switch (storage_id) { case NcmStorageId_None: return "None"; @@ -57,6 +79,18 @@ auto GetAppId(const PackagedContentMeta& meta) -> u64 { 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) { bool has; R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id)); diff --git a/sphaira/source/yati/source/usb.cpp b/sphaira/source/yati/source/usb.cpp index 6e8516c..e8673da 100644 --- a/sphaira/source/yati/source/usb.cpp +++ b/sphaira/source/yati/source/usb.cpp @@ -18,266 +18,34 @@ // The USB protocol was taken from Tinfoil, by Adubbz. #include "yati/source/usb.hpp" +#include "usb/tinfoil.hpp" #include "log.hpp" #include namespace sphaira::yati::source { namespace { -enum USBCmdType : u8 { - 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 tinfoil = usb::tinfoil; } // namespace Usb::Usb(u64 transfer_timeout) { - m_open_result = usbDsInitialize(); - m_transfer_timeout = transfer_timeout; - ueventCreate(GetCancelEvent(), true); - // this avoids allocations during transfers. - m_aligned.reserve(1024 * 1024 * 16); + m_usb = std::make_unique(transfer_timeout); + m_open_result = m_usb->Init(); } 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(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& out_names) { - TUSHeader header; - R_TRY(TransferAll(true, &header, sizeof(header), timeout)); - R_UNLESS(header.magic == 0x304C5554, Result_BadMagic); + tinfoil::TUSHeader header; + R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout)); + R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic); R_UNLESS(header.nspListSize > 0, Result_BadCount); log_write("USB got header\n"); std::vector 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(); 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; } -Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const { - return std::addressof(m_endpoints[ep]->CompletionEvent); -} - -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(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, +Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout) { + tinfoil::USBCmdHeader header{ + .magic = tinfoil::Magic_Command0, + .type = tinfoil::USBCmdType::REQUEST, .cmdId = cmdId, .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) { - FileRangeCmdHeader fRangeHeader; +Result Usb::SendFileRangeCmd(u64 off, u64 size, u64 timeout) { + tinfoil::FileRangeCmdHeader fRangeHeader; fRangeHeader.size = size; fRangeHeader.offset = off; fRangeHeader.nspNameLen = m_transfer_file_name.size(); fRangeHeader.padding = 0; - R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen)); - R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout)); - R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout)); + R_TRY(SendCmdHeader(tinfoil::USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen, timeout)); + R_TRY(m_usb->TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), timeout)); + R_TRY(m_usb->TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), timeout)); - USBCmdHeader responseHeader; - R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout)); + tinfoil::USBCmdHeader responseHeader; + R_TRY(m_usb->TransferAll(true, &responseHeader, sizeof(responseHeader), timeout)); R_SUCCEED(); } -Result Usb::Finished() { - return SendCmdHeader(USBCmdId::EXIT, 0); +Result Usb::Finished(u64 timeout) { + return SendCmdHeader(tinfoil::USBCmdId::EXIT, 0, timeout); } Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) { R_TRY(GetOpenResult()); - R_TRY(SendFileRangeCmd(off, size)); - R_TRY(TransferAll(true, buf, size, m_transfer_timeout)); + R_TRY(SendFileRangeCmd(off, size, m_usb->GetTransferTimeout())); + R_TRY(m_usb->TransferAll(true, buf, size)); *bytes_read = size; R_SUCCEED(); } diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index cc16f0e..6c818f8 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -275,7 +275,6 @@ struct Yati { NcmContentMetaDatabase db{}; NcmStorageId storage_id{}; - Service es{}; Service ns_app{}; std::unique_ptr container{}; Config config{}; @@ -452,7 +451,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) { for (s64 off = 0; off < size;) { // log_write("looking for section\n"); 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); }); @@ -504,6 +503,8 @@ Result Yati::decompressFuncInternal(ThreadData* t) { if (!is_ncz || !decompress_buf_off) { // check nca header if (!decompress_buf_off) { + log_write("reading nca header\n"); + nca::Header header{}; crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false); log_write("verifying nca header magic\n"); @@ -522,6 +523,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) { } 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)); if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) { @@ -531,11 +533,13 @@ Result Yati::decompressFuncInternal(ThreadData* t) { TikCollection* ticket = nullptr; 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)); }); + log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str); R_UNLESS(it != t->tik.end(), Result_TicketNotFound); + log_write("ticket found\n"); it->required = true; 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. if (t->ncz_blocks.size()) { 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); }); @@ -757,13 +761,13 @@ Yati::~Yati() { splCryptoExit(); serviceClose(std::addressof(ns_app)); nsExit(); + es::Exit(); for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { ncmContentMetaDatabaseClose(std::addressof(ncm_db[i])); ncmContentStorageClose(std::addressof(ncm_cs[i])); } - serviceClose(std::addressof(es)); appletSetMediaPlaybackState(false); if (config.boost_mode) { @@ -799,7 +803,7 @@ Result Yati::Setup(const ConfigOverride& override) { R_TRY(splCryptoInitialize()); R_TRY(nsInitialize()); 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++) { R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i])); @@ -960,7 +964,7 @@ Result Yati::InstallCnmtNca(std::span tickets, CnmtCollection& cn } 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; }); @@ -1004,7 +1008,7 @@ Result Yati::InstallCnmtNca(std::span tickets, CnmtCollection& cn 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"); R_SUCCEED(); @@ -1017,7 +1021,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector& tickets, con 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 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; }); @@ -1117,7 +1121,7 @@ Result Yati::ImportTickets(std::span tickets) { log_write("patching ticket\n"); R_TRY(es::PatchTicket(ticket.ticket, keys)); 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; } } @@ -1317,7 +1321,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr