add game dumping, add game transfer (switch2switch) via usb, add multi game selecting, fix bugs (see below).
- added more es commands. - fixed usb install potential hang if the exit command is sent, but the client stops responding (timeout is now 3s). - added multi select to the games menu. - added game dumping. - added switch2switch support by having a switch act as a usb client to transfer games. - replace std::find with std::ranges (in a few places). - fix rounding of icon in progress box being too round. - fix file copy helper in progress box not updating the progress bar.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@ struct Entry {
|
||||
char display_version[0x10]{};
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
bool selected{};
|
||||
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 control_size{};
|
||||
@@ -103,11 +104,39 @@ private:
|
||||
void FreeEntries();
|
||||
void OnLayoutChange();
|
||||
|
||||
auto GetSelectedEntries() const {
|
||||
std::vector<Entry> out;
|
||||
for (auto& e : m_entries) {
|
||||
if (e.selected) {
|
||||
out.emplace_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_entries.empty() && out.empty()) {
|
||||
out.emplace_back(m_entries[m_index]);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void ClearSelection() {
|
||||
for (auto& e : m_entries) {
|
||||
e.selected = false;
|
||||
}
|
||||
|
||||
m_selected_count = 0;
|
||||
}
|
||||
|
||||
void DeleteGames();
|
||||
void DumpGames(u32 flags);
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "games";
|
||||
static constexpr inline const char* INI_SECTION_DUMP = "dump";
|
||||
|
||||
std::vector<Entry> m_entries{};
|
||||
s64 m_index{}; // where i am in the array
|
||||
s64 m_selected_count{};
|
||||
std::unique_ptr<List> m_list{};
|
||||
Event m_event{};
|
||||
bool m_is_reversed{};
|
||||
|
||||
101
sphaira/include/usb/base.hpp
Normal file
101
sphaira/include/usb/base.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <new>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
struct Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_Cancelled = MAKERESULT(USBModule, 100),
|
||||
};
|
||||
|
||||
Base(u64 transfer_timeout);
|
||||
|
||||
// sets up usb.
|
||||
virtual Result Init() = 0;
|
||||
|
||||
// returns 0 if usb is connected to a device.
|
||||
virtual Result IsUsbConnected(u64 timeout) = 0;
|
||||
|
||||
// transfers a chunk of data, check out_size_transferred for how much was transferred.
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
|
||||
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
|
||||
}
|
||||
|
||||
// transfers all data.
|
||||
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
|
||||
Result TransferAll(bool read, void *data, u32 size) {
|
||||
return TransferAll(read, data, size, m_transfer_timeout);
|
||||
}
|
||||
|
||||
// returns the cancel event.
|
||||
auto GetCancelEvent() {
|
||||
return &m_uevent;
|
||||
}
|
||||
|
||||
// cancels an in progress transfer.
|
||||
void Cancel() {
|
||||
ueventSignal(GetCancelEvent());
|
||||
}
|
||||
|
||||
auto& GetTransferBuffer() {
|
||||
return m_aligned;
|
||||
}
|
||||
|
||||
auto GetTransferTimeout() const {
|
||||
return m_transfer_timeout;
|
||||
}
|
||||
|
||||
public:
|
||||
// custom allocator for std::vector that respects alignment.
|
||||
// https://en.cppreference.com/w/cpp/named_req/Allocator
|
||||
template <typename T, std::size_t Align>
|
||||
struct CustomVectorAllocator {
|
||||
public:
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_new
|
||||
auto allocate(std::size_t n) -> T* {
|
||||
n = (n + (Align - 1)) &~ (Align - 1);
|
||||
return new(align) T[n];
|
||||
}
|
||||
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
|
||||
auto deallocate(T* p, std::size_t n) noexcept -> void {
|
||||
// ::operator delete[] (p, n, align);
|
||||
::operator delete[] (p, align);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr inline std::align_val_t align{Align};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||
using value_type = T; // used by std::vector
|
||||
};
|
||||
|
||||
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||
|
||||
protected:
|
||||
enum UsbSessionEndpoint {
|
||||
UsbSessionEndpoint_In = 0,
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
|
||||
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
|
||||
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
|
||||
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
|
||||
|
||||
private:
|
||||
u64 m_transfer_timeout{};
|
||||
UEvent m_uevent{};
|
||||
PageAlignedVector m_aligned{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
47
sphaira/include/usb/tinfoil.hpp
Normal file
47
sphaira/include/usb/tinfoil.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb::tinfoil {
|
||||
|
||||
enum Magic : u32 {
|
||||
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
|
||||
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||
};
|
||||
|
||||
enum USBCmdType : u8 {
|
||||
REQUEST = 0,
|
||||
RESPONSE = 1
|
||||
};
|
||||
|
||||
enum USBCmdId : u32 {
|
||||
EXIT = 0,
|
||||
FILE_RANGE = 1
|
||||
};
|
||||
|
||||
struct TUSHeader {
|
||||
u32 magic; // TUL0 (Tinfoil Usb List 0)
|
||||
u32 nspListSize;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
struct NX_PACKED USBCmdHeader {
|
||||
u32 magic; // TUC0 (Tinfoil USB Command 0)
|
||||
USBCmdType type;
|
||||
u8 padding[0x3];
|
||||
u32 cmdId;
|
||||
u64 dataSize;
|
||||
u8 reserved[0xC];
|
||||
};
|
||||
|
||||
struct FileRangeCmdHeader {
|
||||
u64 size;
|
||||
u64 offset;
|
||||
u64 nspNameLen;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
|
||||
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
|
||||
|
||||
} // namespace sphaira::usb::tinfoil
|
||||
47
sphaira/include/usb/usb_uploader.hpp
Normal file
47
sphaira/include/usb/usb_uploader.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "usb/usbhs.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb::upload {
|
||||
|
||||
struct Usb {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||
Result_Exit = MAKERESULT(USBModule, 1),
|
||||
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||
Result_BadCommand = MAKERESULT(USBModule, 4),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
virtual ~Usb();
|
||||
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result IsUsbConnected(u64 timeout) {
|
||||
return m_usb->IsUsbConnected(timeout);
|
||||
}
|
||||
|
||||
// waits for connection and then sends file list.
|
||||
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
|
||||
|
||||
// polls for command, executes transfer if possible.
|
||||
// will return Result_Exit if exit command is recieved.
|
||||
Result PollCommands();
|
||||
|
||||
private:
|
||||
Result FileRangeCmd(u64 data_size);
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbHs> m_usb;
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
27
sphaira/include/usb/usbds.hpp
Normal file
27
sphaira/include/usb/usbds.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
// Device Host
|
||||
struct UsbDs final : Base {
|
||||
using Base::Base;
|
||||
~UsbDs();
|
||||
|
||||
Result Init() override;
|
||||
Result IsUsbConnected(u64 timeout) override;
|
||||
Result GetSpeed(UsbDeviceSpeed* out);
|
||||
|
||||
private:
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
33
sphaira/include/usb/usbhs.hpp
Normal file
33
sphaira/include/usb/usbhs.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
struct UsbHs final : Base {
|
||||
UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout);
|
||||
~UsbHs();
|
||||
|
||||
Result Init() override;
|
||||
Result IsUsbConnected(u64 timeout) override;
|
||||
|
||||
private:
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
|
||||
|
||||
Result Connect();
|
||||
void Close();
|
||||
|
||||
private:
|
||||
u8 m_index{};
|
||||
UsbHsInterfaceFilter m_filter{};
|
||||
UsbHsInterface m_interface{};
|
||||
UsbHsClientIfSession m_s{};
|
||||
UsbHsClientEpSession m_endpoints[2]{};
|
||||
Event m_event{};
|
||||
bool m_connected{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
#include "base.hpp"
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
|
||||
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<CollectionEntry> collections, s64& size) -> std::vector<u8>;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
|
||||
@@ -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<const u8> ticket, u64& out);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
56
sphaira/include/yati/nx/service_guard.h
Normal file
56
sphaira/include/yati/nx/service_guard.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include <switch/types.h>
|
||||
#include <switch/result.h>
|
||||
#include <switch/kernel/mutex.h>
|
||||
#include <switch/sf/service.h>
|
||||
#include <switch/services/sm.h>
|
||||
|
||||
typedef struct ServiceGuard {
|
||||
Mutex mutex;
|
||||
u32 refCount;
|
||||
} ServiceGuard;
|
||||
|
||||
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
|
||||
{
|
||||
mutexLock(&g->mutex);
|
||||
return (g->refCount++) == 0;
|
||||
}
|
||||
|
||||
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
|
||||
{
|
||||
if (R_FAILED(rc)) {
|
||||
cleanupFunc();
|
||||
--g->refCount;
|
||||
}
|
||||
mutexUnlock(&g->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
|
||||
{
|
||||
mutexLock(&g->mutex);
|
||||
if (g->refCount && (--g->refCount) == 0)
|
||||
cleanupFunc();
|
||||
mutexUnlock(&g->mutex);
|
||||
}
|
||||
|
||||
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
|
||||
\
|
||||
static ServiceGuard g_##name##Guard; \
|
||||
NX_INLINE Result _##name##Initialize _paramdecl; \
|
||||
static void _##name##Cleanup(void); \
|
||||
\
|
||||
Result name##Initialize _paramdecl \
|
||||
{ \
|
||||
Result rc = 0; \
|
||||
if (serviceGuardBeginInit(&g_##name##Guard)) \
|
||||
rc = _##name##Initialize _parampass; \
|
||||
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
|
||||
} \
|
||||
\
|
||||
void name##Exit(void) \
|
||||
{ \
|
||||
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
|
||||
}
|
||||
|
||||
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <new>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
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<std::string>& 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 <typename T, std::size_t Align>
|
||||
struct CustomVectorAllocator {
|
||||
public:
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_new
|
||||
auto allocate(std::size_t n) -> T* {
|
||||
return new(align) T[n];
|
||||
}
|
||||
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
|
||||
auto deallocate(T* p, std::size_t n) noexcept -> void {
|
||||
::operator delete[] (p, n, align);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr inline std::align_val_t align{Align};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||
using value_type = T; // used by std::vector
|
||||
};
|
||||
|
||||
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||
private:
|
||||
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<UEvent*> m_cancel_events{};
|
||||
// aligned buffer that transfer data is copied to and from.
|
||||
// a vector is used to avoid multiple alloc within the transfer loop.
|
||||
PageAlignedVector m_aligned{};
|
||||
std::unique_ptr<usb::UsbDs> m_usb;
|
||||
std::string m_transfer_file_name{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 <utility>
|
||||
#include <cstring>
|
||||
@@ -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<NsApplicationContentMetaStatus>;
|
||||
|
||||
struct ContentInfoEntry {
|
||||
NsApplicationContentMetaStatus status{};
|
||||
std::vector<NcmContentInfo> content_infos{};
|
||||
std::vector<FsRightsId> rights_ids{};
|
||||
};
|
||||
|
||||
struct TikEntry {
|
||||
FsRightsId id{};
|
||||
std::vector<u8> tik_data{};
|
||||
std::vector<u8> cert_data{};
|
||||
};
|
||||
|
||||
struct NspEntry {
|
||||
// application name.
|
||||
std::string application_name{};
|
||||
// name of the nsp (name [id][v0][BASE].nsp).
|
||||
fs::FsPath path{};
|
||||
// tickets and cert data, will be empty if title key crypto isn't used.
|
||||
std::vector<TikEntry> tickets{};
|
||||
// all the collections for this nsp, such as nca's and tickets.
|
||||
std::vector<yati::container::CollectionEntry> collections{};
|
||||
// raw nsp data (header, file table and string table).
|
||||
std::vector<u8> nsp_data{};
|
||||
// size of the entier nsp.
|
||||
s64 nsp_size{};
|
||||
// copy of ncm cs, it is not closed.
|
||||
NcmContentStorage cs{};
|
||||
|
||||
// todo: benchmark manual sdcard read and decryption vs ncm.
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
if (off < nsp_data.size()) {
|
||||
*bytes_read = size = ClipSize(off, size, nsp_data.size());
|
||||
std::memcpy(buf, nsp_data.data() + off, size);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// adjust offset.
|
||||
off -= nsp_data.size();
|
||||
|
||||
for (auto& collection : collections) {
|
||||
if (InRange(off, collection.offset, collection.size)) {
|
||||
// adjust offset relative to the collection.
|
||||
off -= collection.offset;
|
||||
*bytes_read = size = ClipSize(off, size, collection.size);
|
||||
|
||||
if (collection.name.ends_with(".nca")) {
|
||||
const auto id = ncm::GetContentIdFromStr(collection.name.c_str());
|
||||
return ncmContentStorageReadContentIdFile(&cs, buf, size, &id, off);
|
||||
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
|
||||
FsRightsId id;
|
||||
keys::parse_hex_key(&id, collection.name.c_str());
|
||||
|
||||
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
|
||||
return !std::memcmp(&id, &e.id, sizeof(id));
|
||||
});
|
||||
R_UNLESS(it != tickets.end(), 0x1);
|
||||
|
||||
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
|
||||
std::memcpy(buf, data.data() + off, size);
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_write("did not find collection...\n");
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
private:
|
||||
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
|
||||
static auto ClipSize(s64 off, s64 size, s64 file_size) -> s64 {
|
||||
return std::min(size, file_size - off);
|
||||
}
|
||||
};
|
||||
|
||||
struct BaseSource {
|
||||
virtual ~BaseSource() = default;
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
};
|
||||
|
||||
struct NspSource final : BaseSource {
|
||||
NspSource(std::span<NspEntry> entries) : m_entries{entries} { }
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path == e.path;
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), 0x1);
|
||||
|
||||
return it->Read(buf, off, size, bytes_read);
|
||||
}
|
||||
|
||||
auto GetEntries() const -> std::span<const NspEntry> {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<NspEntry> m_entries{};
|
||||
};
|
||||
|
||||
struct UsbTest final : usb::upload::Usb {
|
||||
UsbTest(ProgressBox* pbox, std::span<NspEntry> entries) : Usb{UINT64_MAX} {
|
||||
m_source = std::make_unique<NspSource>(entries);
|
||||
m_pbox = pbox;
|
||||
}
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
if (m_path != path) {
|
||||
m_path = path;
|
||||
m_pbox->NewTransfer(m_path);
|
||||
}
|
||||
|
||||
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
|
||||
m_offset += *bytes_read;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<NspSource> m_source{};
|
||||
ProgressBox* m_pbox{};
|
||||
std::string m_path{};
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
Result DumpNspToFile(ProgressBox* pbox, std::span<NspEntry> entries) {
|
||||
static constexpr fs::FsPath DUMP_PATH{"/dumps/NSP"};
|
||||
constexpr s64 BIG_FILE_SIZE = 1024ULL*1024ULL*1024ULL*4ULL;
|
||||
|
||||
fs::FsNativeSd fs{};
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
fs.CreateDirectoryRecursively(DUMP_PATH);
|
||||
|
||||
auto source = std::make_unique<NspSource>(entries);
|
||||
for (const auto& e : entries) {
|
||||
pbox->SetTitle(e.application_name);
|
||||
pbox->NewTransfer(e.path);
|
||||
|
||||
const auto temp_path = fs::AppendPath(DUMP_PATH, e.path + ".temp");
|
||||
fs.DeleteFile(temp_path);
|
||||
|
||||
const auto flags = e.nsp_size >= BIG_FILE_SIZE ? FsCreateOption_BigFile : 0;
|
||||
R_TRY(fs.CreateFile(temp_path, e.nsp_size, flags));
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
|
||||
|
||||
{
|
||||
FsFile file;
|
||||
R_TRY(fs.OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
ON_SCOPE_EXIT(fsFileClose(&file));
|
||||
|
||||
s64 offset{};
|
||||
std::vector<u8> buf(1024*1024*4); // 4MiB
|
||||
|
||||
while (offset < e.nsp_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
R_THROW(0xFFFF);
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
|
||||
pbox->Yield();
|
||||
|
||||
R_TRY(fsFileWrite(&file, offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
pbox->Yield();
|
||||
|
||||
pbox->UpdateTransfer(offset, e.nsp_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
const auto path = fs::AppendPath(DUMP_PATH, e.path);
|
||||
fs.DeleteFile(path);
|
||||
R_TRY(fs.RenameFile(temp_path, path));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpNspToUsbS2S(ProgressBox* pbox, std::span<NspEntry> entries) {
|
||||
std::vector<std::string> file_list;
|
||||
for (auto& e : entries) {
|
||||
file_list.emplace_back(e.path);
|
||||
}
|
||||
|
||||
auto usb = std::make_unique<UsbTest>(pbox, entries);
|
||||
constexpr u64 timeout = 1e+9;
|
||||
|
||||
// todo: display progress bar during usb transfer.
|
||||
while (!pbox->ShouldExit()) {
|
||||
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
|
||||
pbox->NewTransfer("USB connected, sending file list");
|
||||
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
|
||||
pbox->NewTransfer("Sent file list, waiting for command...");
|
||||
|
||||
while (!pbox->ShouldExit()) {
|
||||
const auto rc = usb->PollCommands();
|
||||
if (rc == usb->Result_Exit) {
|
||||
log_write("got exit command\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
R_TRY(rc);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
pbox->NewTransfer("waiting for usb connection...");
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpNspToDevNull(ProgressBox* pbox, std::span<NspEntry> entries) {
|
||||
auto source = std::make_unique<NspSource>(entries);
|
||||
for (const auto& e : entries) {
|
||||
pbox->SetTitle(e.application_name);
|
||||
pbox->NewTransfer(e.path);
|
||||
|
||||
s64 offset{};
|
||||
std::vector<u8> buf(1024*1024*4); // 4MiB
|
||||
|
||||
while (offset < e.nsp_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
R_THROW(0xFFFF);
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
|
||||
pbox->Yield();
|
||||
|
||||
pbox->UpdateTransfer(offset, e.nsp_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Notify(Result rc, const std::string& error_message) {
|
||||
if (R_FAILED(rc)) {
|
||||
App::Push(std::make_shared<ui::ErrorBox>(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<NspEntry>& out) {
|
||||
LoadControlEntry(e);
|
||||
|
||||
MetaEntries meta_entries;
|
||||
R_TRY(GetMetaEntries(e, meta_entries, flags));
|
||||
|
||||
for (const auto& status : meta_entries) {
|
||||
ContentInfoEntry info;
|
||||
R_TRY(BuildContentEntry(status, info));
|
||||
|
||||
NspEntry nsp;
|
||||
R_TRY(BuildNspEntry(e, info, nsp));
|
||||
out.emplace_back(nsp);
|
||||
}
|
||||
|
||||
R_UNLESS(!out.empty(), 0x1);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||
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<ThreadResultData>& 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<SidebarEntryCallback>("Dump"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
|
||||
DumpGames(ContentFlag_All);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
|
||||
DumpGames(ContentFlag_Application);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
|
||||
DumpGames(ContentFlag_Patch);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
|
||||
DumpGames(ContentFlag_AddOnContent);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
|
||||
DumpGames(ContentFlag_DataPatch);
|
||||
}, true));
|
||||
}, true));
|
||||
|
||||
// completely deletes the application record and all data.
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||
@@ -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<SidebarEntryCallback>("Delete entity"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
buf,
|
||||
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
const auto rc = nsDeleteApplicationEntity(m_entries[m_index].app_id);
|
||||
Notify(rc, "Failed to delete application");
|
||||
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<ProgressBox>(0, "Deleting Games"_i18n, "", [this](auto pbox) -> bool {
|
||||
auto targets = GetSelectedEntries();
|
||||
|
||||
for (s64 i = 0; i < std::size(targets); i++) {
|
||||
auto& e = targets[i];
|
||||
|
||||
LoadControlEntry(e);
|
||||
pbox->SetTitle(e.GetName());
|
||||
pbox->UpdateTransfer(i + 1, std::size(targets));
|
||||
nsDeleteApplicationCompletely(e.app_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool success){
|
||||
ClearSelection();
|
||||
m_dirty = true;
|
||||
|
||||
if (success) {
|
||||
App::Notify("Delete successfull!");
|
||||
} else {
|
||||
App::Notify("Delete failed!");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void Menu::DumpGames(u32 flags) {
|
||||
PopupList::Items items;
|
||||
for (const auto&p : DUMP_LOCATIONS) {
|
||||
items.emplace_back(p.display_name);
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
"Select dump location"_i18n, items, [this, flags](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = *op_index;
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Dumping Games"_i18n, "", [this, index, flags](auto pbox) -> bool {
|
||||
auto targets = GetSelectedEntries();
|
||||
|
||||
std::vector<NspEntry> nsp_entries;
|
||||
for (auto& e : targets) {
|
||||
BuildNspEntries(e, flags, nsp_entries);
|
||||
}
|
||||
|
||||
if (index == DumpLocationType_SdCard) {
|
||||
return R_SUCCEEDED(DumpNspToFile(pbox, nsp_entries));
|
||||
} else if (index == DumpLocationType_UsbS2S) {
|
||||
return R_SUCCEEDED(DumpNspToUsbS2S(pbox, nsp_entries));
|
||||
} else if (index == DumpLocationType_DevNull) {
|
||||
return R_SUCCEEDED(DumpNspToDevNull(pbox, nsp_entries));
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [this](bool success){
|
||||
ClearSelection();
|
||||
|
||||
if (success) {
|
||||
App::Notify("Dump successfull!");
|
||||
log_write("dump successfull!!!\n");
|
||||
} else {
|
||||
App::Notify("Dump failed!");
|
||||
log_write("dump failed!!!\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
));
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::game
|
||||
|
||||
@@ -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<Menu*>(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<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||
ON_SCOPE_EXIT(m_usb_source->Finished());
|
||||
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
|
||||
|
||||
log_write("inside progress box\n");
|
||||
for (const auto& file_name : m_names) {
|
||||
|
||||
@@ -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<u8> buf(1024*1024*8); // 8MiB
|
||||
std::vector<u8> 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;
|
||||
}
|
||||
|
||||
|
||||
98
sphaira/source/usb/base.cpp
Normal file
98
sphaira/source/usb/base.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||
|
||||
#include "usb/base.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
Base::Base(u64 transfer_timeout) {
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
ueventCreate(GetCancelEvent(), true);
|
||||
// this avoids allocations during transfers.
|
||||
m_aligned.reserve(1024 * 1024 * 16);
|
||||
}
|
||||
|
||||
Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
|
||||
u32 xfer_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
R_TRY(IsUsbConnected(timeout));
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
R_TRY(TransferAsync(ep, page, size, std::addressof(xfer_id)));
|
||||
|
||||
/* Try to wait for the event. */
|
||||
R_TRY(WaitTransferCompletion(ep, timeout));
|
||||
|
||||
/* Return what we transferred. */
|
||||
return GetTransferResult(ep, xfer_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
// while it may seem like a bad idea to transfer data to a buffer and copy it
|
||||
// in practice, this has no impact on performance.
|
||||
// the switch is *massively* bottlenecked by slow io (nand and sd).
|
||||
// so making usb transfers zero-copy provides no benefit other than increased
|
||||
// code complexity and the increase of future bugs if/when sphaira is forked
|
||||
// an changes are made.
|
||||
// yati already goes to great lengths to be zero-copy during installing
|
||||
// by swapping buffers and inflating in-place.
|
||||
|
||||
// NOTE: it is now possible to request the transfer buffer using GetTransferBuffer(),
|
||||
// which will always be aligned and have the size aligned.
|
||||
// this allows for zero-copy transferrs to take place.
|
||||
// this is used in usb_upload.cpp.
|
||||
// do note that this relies of the host sending / receiving buffers of an aligned size.
|
||||
Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
|
||||
auto buf = static_cast<u8*>(data);
|
||||
auto transfer_buf = m_aligned.data();
|
||||
const auto alias = buf == transfer_buf;
|
||||
|
||||
if (!alias) {
|
||||
m_aligned.resize(size);
|
||||
}
|
||||
|
||||
while (size) {
|
||||
if (!alias && !read) {
|
||||
std::memcpy(transfer_buf, buf, size);
|
||||
}
|
||||
|
||||
u32 out_size_transferred;
|
||||
R_TRY(TransferPacketImpl(read, transfer_buf, size, &out_size_transferred, timeout));
|
||||
|
||||
if (!alias && read) {
|
||||
std::memcpy(buf, transfer_buf, out_size_transferred);
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
transfer_buf += out_size_transferred;
|
||||
} else {
|
||||
buf += out_size_transferred;
|
||||
}
|
||||
|
||||
size -= out_size_transferred;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
112
sphaira/source/usb/usb_uploader.cpp
Normal file
112
sphaira/source/usb/usb_uploader.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||
|
||||
#include "usb/usb_uploader.hpp"
|
||||
#include "usb/tinfoil.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace sphaira::usb::upload {
|
||||
namespace {
|
||||
|
||||
namespace tinfoil = usb::tinfoil;
|
||||
|
||||
const UsbHsInterfaceFilter FILTER{
|
||||
.Flags = UsbHsInterfaceFilterFlags_idVendor |
|
||||
UsbHsInterfaceFilterFlags_idProduct |
|
||||
UsbHsInterfaceFilterFlags_bcdDevice_Min |
|
||||
UsbHsInterfaceFilterFlags_bcdDevice_Max |
|
||||
UsbHsInterfaceFilterFlags_bDeviceClass |
|
||||
UsbHsInterfaceFilterFlags_bDeviceSubClass |
|
||||
UsbHsInterfaceFilterFlags_bDeviceProtocol |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceClass |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceSubClass |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceProtocol,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice_Min = 0x0100,
|
||||
.bcdDevice_Max = 0x0100,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
constexpr u8 INDEX = 0;
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_usb = std::make_unique<usb::UsbHs>(INDEX, FILTER, transfer_timeout);
|
||||
m_usb->Init();
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
|
||||
R_TRY(m_usb->IsUsbConnected(timeout));
|
||||
|
||||
std::string names_list;
|
||||
for (auto& name : names) {
|
||||
names_list += name + '\n';
|
||||
}
|
||||
|
||||
tinfoil::TUSHeader header{};
|
||||
header.magic = tinfoil::Magic_List0;
|
||||
header.nspListSize = names_list.length();
|
||||
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
|
||||
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::PollCommands() {
|
||||
tinfoil::USBCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_BadMagic);
|
||||
|
||||
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
|
||||
return Result_Exit;
|
||||
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
|
||||
return FileRangeCmd(header.dataSize);
|
||||
} else {
|
||||
return Result_BadCommand;
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::FileRangeCmd(u64 data_size) {
|
||||
tinfoil::FileRangeCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
|
||||
std::string path(header.nspNameLen, '\0');
|
||||
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
|
||||
|
||||
// send response header.
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
|
||||
|
||||
s64 curr_off = 0x0;
|
||||
s64 end_off = header.size;
|
||||
s64 read_size = header.size;
|
||||
|
||||
// use transfer buffer directly to avoid copy overhead.
|
||||
auto& buf = m_usb->GetTransferBuffer();
|
||||
buf.resize(header.size);
|
||||
|
||||
while (curr_off < end_off) {
|
||||
if (curr_off + read_size >= end_off) {
|
||||
read_size = end_off - curr_off;
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(Read(path, buf.data(), header.offset + curr_off, read_size, &bytes_read));
|
||||
R_TRY(m_usb->TransferAll(false, buf.data(), bytes_read));
|
||||
curr_off += bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
272
sphaira/source/usb/usbds.cpp
Normal file
272
sphaira/source/usb/usbds.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
#include "usb/usbds.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
// untested, should work tho.
|
||||
// TODO: pr missing speed fields to libnx.
|
||||
Result usbDsGetSpeed(u32 *out) {
|
||||
if (hosversionBefore(8,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
}
|
||||
|
||||
serviceAssumeDomain(usbDsGetServiceSession());
|
||||
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbDs::~UsbDs() {
|
||||
usbDsExit();
|
||||
}
|
||||
|
||||
Result UsbDs::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(usbDsInitialize());
|
||||
|
||||
static SetSysSerialNumber serial_number{};
|
||||
R_TRY(setsysInitialize());
|
||||
ON_SCOPE_EXIT(setsysExit());
|
||||
R_TRY(setsysGetSerialNumber(&serial_number));
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static constexpr u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
R_TRY(usbDsAddUsbLanguageStringDescriptor(nullptr, supported_langs, std::size(supported_langs)));
|
||||
// Send manufacturer
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
.bLength = USB_DT_DEVICE_SIZE,
|
||||
.bDescriptorType = USB_DT_DEVICE,
|
||||
.bcdUSB = 0x0110,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = 0x40,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = iManufacturer,
|
||||
.iProduct = iProduct,
|
||||
.iSerialNumber = iSerialNumber,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// Full Speed is USB 1.1
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
|
||||
|
||||
// High Speed is USB 2.0
|
||||
device_descriptor.bcdUSB = 0x0200;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
|
||||
|
||||
// Super Speed is USB 3.0
|
||||
device_descriptor.bcdUSB = 0x0300;
|
||||
// Upgrade packet size to 512
|
||||
device_descriptor.bMaxPacketSize0 = 0x09;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
|
||||
|
||||
// Define Binary Object Store
|
||||
const u8 bos[0x16] = {
|
||||
0x05, // .bLength
|
||||
USB_DT_BOS, // .bDescriptorType
|
||||
0x16, 0x00, // .wTotalLength
|
||||
0x02, // .bNumDeviceCaps
|
||||
|
||||
// USB 2.0
|
||||
0x07, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x02, // .bDevCapabilityType
|
||||
0x02, 0x00, 0x00, 0x00, // dev_capability_data
|
||||
|
||||
// USB 3.0
|
||||
0x0A, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x03, /* .bDevCapabilityType */
|
||||
0x00, /* .bmAttributes */
|
||||
0x0E, 0x00, /* .wSpeedSupported */
|
||||
0x03, /* .bFunctionalitySupport */
|
||||
0x00, /* .bU1DevExitLat */
|
||||
0x00, 0x00 /* .bU2DevExitLat */
|
||||
};
|
||||
|
||||
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
|
||||
|
||||
struct usb_interface_descriptor interface_descriptor = {
|
||||
.bLength = USB_DT_INTERFACE_SIZE,
|
||||
.bDescriptorType = USB_DT_INTERFACE,
|
||||
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
|
||||
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x40;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x40;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// the below code is taken from libnx, with the addition of a uevent to cancel.
|
||||
Result UsbDs::IsUsbConnected(u64 timeout) {
|
||||
Result rc;
|
||||
UsbState state = UsbState_Detached;
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
if (R_FAILED(rc)) return rc;
|
||||
if (state == UsbState_Configured) return 0;
|
||||
|
||||
bool has_timeout = timeout != UINT64_MAX;
|
||||
u64 deadline = 0;
|
||||
|
||||
const std::array waiters{
|
||||
waiterForEvent(usbDsGetStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
if (has_timeout)
|
||||
deadline = armGetSystemTick() + armNsToTicks(timeout);
|
||||
|
||||
do {
|
||||
if (has_timeout) {
|
||||
s64 remaining = deadline - armGetSystemTick();
|
||||
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
|
||||
}
|
||||
|
||||
s32 idx;
|
||||
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
eventClear(usbDsGetStateChangeEvent());
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
rc = Result_Cancelled;
|
||||
break;
|
||||
}
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
|
||||
|
||||
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
|
||||
return KERNELRESULT(TimedOut);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbDs::GetSpeed(UsbDeviceSpeed* out) {
|
||||
return usbDsGetSpeed((u32*)out);
|
||||
}
|
||||
|
||||
Event *UsbDs::GetCompletionEvent(UsbSessionEndpoint ep) {
|
||||
return std::addressof(m_endpoints[ep]->CompletionEvent);
|
||||
}
|
||||
|
||||
Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
|
||||
const std::array waiters{
|
||||
waiterForEvent(GetCompletionEvent(ep)),
|
||||
waiterForEvent(usbDsGetStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
log_write("got usb cancel event\n");
|
||||
rc = Result_Cancelled;
|
||||
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
|
||||
log_write("got usbDsGetStateChangeEvent() event\n");
|
||||
rc = KERNELRESULT(TimedOut);
|
||||
}
|
||||
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
|
||||
eventClear(GetCompletionEvent(ep));
|
||||
eventClear(usbDsGetStateChangeEvent());
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) {
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) {
|
||||
UsbDsReportData report_data;
|
||||
|
||||
R_TRY(eventClear(GetCompletionEvent(ep)));
|
||||
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
|
||||
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
202
sphaira/source/usb/usbhs.cpp
Normal file
202
sphaira/source/usb/usbhs.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include "usb/usbhs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
struct Bcd {
|
||||
constexpr Bcd(u16 v) : value{v} {}
|
||||
|
||||
u8 major() const { return (value >> 8) & 0xFF; }
|
||||
u8 minor() const { return (value >> 4) & 0xF; }
|
||||
u8 macro() const { return (value >> 0) & 0xF; }
|
||||
|
||||
const u16 value;
|
||||
};
|
||||
|
||||
Result usbHsParseReportData(UsbHsXferReport* reports, u32 count, u32 xferId, u32 *requestedSize, u32 *transferredSize) {
|
||||
Result rc = 0;
|
||||
u32 pos;
|
||||
UsbHsXferReport *entry = NULL;
|
||||
if (count>8) count = 8;
|
||||
|
||||
for(pos=0; pos<count; pos++) {
|
||||
entry = &reports[pos];
|
||||
if (entry->xferId == xferId) break;
|
||||
}
|
||||
|
||||
if (pos == count) return MAKERESULT(Module_Libnx, LibnxError_NotFound);
|
||||
rc = entry->res;
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
if (requestedSize) *requestedSize = entry->requestedSize;
|
||||
if (transferredSize) *transferredSize = entry->transferredSize;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbHs::UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout)
|
||||
: Base{transfer_timeout}
|
||||
, m_index{index}
|
||||
, m_filter{filter} {
|
||||
|
||||
}
|
||||
|
||||
UsbHs::~UsbHs() {
|
||||
Close();
|
||||
usbHsDestroyInterfaceAvailableEvent(std::addressof(m_event), m_index);
|
||||
usbHsExit();
|
||||
}
|
||||
|
||||
Result UsbHs::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(usbHsInitialize());
|
||||
R_TRY(usbHsCreateInterfaceAvailableEvent(&m_event, true, m_index, &m_filter));
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result UsbHs::IsUsbConnected(u64 timeout) {
|
||||
if (m_connected) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
const std::array waiters{
|
||||
waiterForEvent(&m_event),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
R_TRY(waitObjects(&idx, waiters.data(), waiters.size(), timeout));
|
||||
|
||||
if (idx == waiters.size() - 1) {
|
||||
return Result_Cancelled;
|
||||
}
|
||||
|
||||
return Connect();
|
||||
}
|
||||
|
||||
Result UsbHs::Connect() {
|
||||
Close();
|
||||
|
||||
s32 total;
|
||||
R_TRY(usbHsQueryAvailableInterfaces(&m_filter, &m_interface, sizeof(m_interface), &total));
|
||||
R_TRY(usbHsAcquireUsbIf(&m_s, &m_interface));
|
||||
|
||||
const auto bcdUSB = Bcd{m_interface.device_desc.bcdUSB};
|
||||
const auto bcdDevice = Bcd{m_interface.device_desc.bcdDevice};
|
||||
|
||||
// log lsusb style.
|
||||
log_write("[USBHS] pathstr: %s\n", m_interface.pathstr);
|
||||
log_write("Bus: %03u Device: %03u ID: %04x:%04x\n\n", m_interface.busID, m_interface.deviceID, m_interface.device_desc.idVendor, m_interface.device_desc.idProduct);
|
||||
|
||||
log_write("Device Descriptor:\n");
|
||||
log_write("\tbLength: %u\n", m_interface.device_desc.bLength);
|
||||
log_write("\tbDescriptorType: %u\n", m_interface.device_desc.bDescriptorType);
|
||||
log_write("\tbcdUSB: %u:%u%u\n", bcdUSB.major(), bcdUSB.minor(), bcdUSB.macro());
|
||||
log_write("\tbDeviceClass: %u\n", m_interface.device_desc.bDeviceClass);
|
||||
log_write("\tbDeviceSubClass: %u\n", m_interface.device_desc.bDeviceSubClass);
|
||||
log_write("\tbDeviceProtocol: %u\n", m_interface.device_desc.bDeviceProtocol);
|
||||
log_write("\tbMaxPacketSize0: %u\n", m_interface.device_desc.bMaxPacketSize0);
|
||||
log_write("\tidVendor: 0x%x\n", m_interface.device_desc.idVendor);
|
||||
log_write("\tidProduct: 0x%x\n", m_interface.device_desc.idProduct);
|
||||
log_write("\tbcdDevice: %u:%u%u\n", bcdDevice.major(), bcdDevice.minor(), bcdDevice.macro());
|
||||
log_write("\tiManufacturer: %u\n", m_interface.device_desc.iManufacturer);
|
||||
log_write("\tiProduct: %u\n", m_interface.device_desc.iProduct);
|
||||
log_write("\tiSerialNumber: %u\n", m_interface.device_desc.iSerialNumber);
|
||||
log_write("\tbNumConfigurations: %u\n", m_interface.device_desc.bNumConfigurations);
|
||||
|
||||
log_write("\tConfiguration Descriptor:\n");
|
||||
log_write("\t\tbLength: %u\n", m_interface.config_desc.bLength);
|
||||
log_write("\t\tbDescriptorType: %u\n", m_interface.config_desc.bDescriptorType);
|
||||
log_write("\t\twTotalLength: %u\n", m_interface.config_desc.wTotalLength);
|
||||
log_write("\t\tbNumInterfaces: %u\n", m_interface.config_desc.bNumInterfaces);
|
||||
log_write("\t\tbConfigurationValue: %u\n", m_interface.config_desc.bConfigurationValue);
|
||||
log_write("\t\tiConfiguration: %u\n", m_interface.config_desc.iConfiguration);
|
||||
log_write("\t\tbmAttributes: 0x%x\n", m_interface.config_desc.bmAttributes);
|
||||
log_write("\t\tMaxPower: %u (%u mA)\n", m_interface.config_desc.MaxPower, m_interface.config_desc.MaxPower * 2);
|
||||
|
||||
struct usb_endpoint_descriptor invalid_desc{};
|
||||
for (u8 i = 0; i < std::size(m_s.inf.inf.input_endpoint_descs); i++) {
|
||||
const auto& desc = m_s.inf.inf.input_endpoint_descs[i];
|
||||
if (std::memcmp(&desc, &invalid_desc, sizeof(desc))) {
|
||||
log_write("\t[USBHS] desc[%u] wMaxPacketSize: 0x%X\n", i, desc.wMaxPacketSize);
|
||||
}
|
||||
}
|
||||
|
||||
auto& input_descs = m_s.inf.inf.input_endpoint_descs[0];
|
||||
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_Out], 1, input_descs.wMaxPacketSize, &input_descs));
|
||||
|
||||
auto& output_descs = m_s.inf.inf.output_endpoint_descs[0];
|
||||
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_In], 1, output_descs.wMaxPacketSize, &output_descs));
|
||||
|
||||
m_connected = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void UsbHs::Close() {
|
||||
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_In]));
|
||||
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_Out]));
|
||||
usbHsIfClose(std::addressof(m_s));
|
||||
|
||||
m_endpoints[UsbSessionEndpoint_In] = {};
|
||||
m_endpoints[UsbSessionEndpoint_Out] = {};
|
||||
m_s = {};
|
||||
m_connected = false;
|
||||
}
|
||||
|
||||
Event *UsbHs::GetCompletionEvent(UsbSessionEndpoint ep) {
|
||||
return usbHsEpGetXferEvent(&m_endpoints[ep]);
|
||||
}
|
||||
|
||||
Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
|
||||
const std::array waiters{
|
||||
waiterForEvent(GetCompletionEvent(ep)),
|
||||
waiterForEvent(usbHsGetInterfaceStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
log_write("got usb cancel event\n");
|
||||
rc = Result_Cancelled;
|
||||
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
|
||||
log_write("got usb timeout event\n");
|
||||
rc = KERNELRESULT(TimedOut);
|
||||
Close();
|
||||
}
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
log_write("failed to wait for event\n");
|
||||
eventClear(GetCompletionEvent(ep));
|
||||
eventClear(usbHsGetInterfaceStateChangeEvent());
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) {
|
||||
return usbHsEpPostBufferAsync(&m_endpoints[ep], buffer, size, 0, out_xfer_id);
|
||||
}
|
||||
|
||||
Result UsbHs::GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) {
|
||||
u32 count;
|
||||
UsbHsXferReport report_data[8];
|
||||
|
||||
R_TRY(eventClear(GetCompletionEvent(ep)));
|
||||
R_TRY(usbHsEpGetXferReport(&m_endpoints[ep], report_data, std::size(report_data), std::addressof(count)));
|
||||
R_TRY(usbHsParseReportData(report_data, count, xfer_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
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<const u8> data) {
|
||||
write(data);
|
||||
}
|
||||
|
||||
void write(const void* data, u64 size) {
|
||||
if (offset + size >= buf.size()) {
|
||||
buf.resize(offset + size);
|
||||
}
|
||||
std::memcpy(buf.data() + offset, data, size);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
void write(std::span<const u8> data) {
|
||||
write(data.data(), data.size());
|
||||
}
|
||||
|
||||
void seek(u64 where_to) {
|
||||
offset = where_to;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto tell() const {
|
||||
return offset;
|
||||
}
|
||||
|
||||
std::vector<u8> buf;
|
||||
u64 offset{};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Nsp::GetCollections(Collections& out) {
|
||||
@@ -56,4 +89,48 @@ Result Nsp::GetCollections(Collections& out) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8> {
|
||||
BufHelper buf;
|
||||
|
||||
Pfs0Header header{};
|
||||
std::vector<Pfs0FileTableEntry> file_table(entries.size());
|
||||
std::vector<char> string_table;
|
||||
|
||||
u64 string_offset{};
|
||||
u64 data_offset{};
|
||||
|
||||
for (u32 i = 0; i < entries.size(); i++) {
|
||||
file_table[i].data_offset = data_offset;
|
||||
file_table[i].data_size = entries[i].size;
|
||||
file_table[i].name_offset = string_offset;
|
||||
file_table[i].padding = 0;
|
||||
|
||||
string_table.resize(string_offset + entries[i].name.length() + 1);
|
||||
std::memcpy(string_table.data() + string_offset, entries[i].name.c_str(), entries[i].name.length() + 1);
|
||||
|
||||
data_offset += file_table[i].data_size;
|
||||
string_offset += entries[i].name.length() + 1;
|
||||
}
|
||||
|
||||
// align table
|
||||
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
|
||||
|
||||
header.magic = PFS0_MAGIC;
|
||||
header.total_files = entries.size();
|
||||
header.string_table_size = string_table.size();
|
||||
header.padding = 0;
|
||||
|
||||
buf.write(&header, sizeof(header));
|
||||
buf.write(file_table.data(), sizeof(Pfs0FileTableEntry) * file_table.size());
|
||||
buf.write(string_table.data(), string_table.size());
|
||||
|
||||
// calculate nsp size.
|
||||
size = buf.tell();
|
||||
for (const auto& e : file_table) {
|
||||
size += e.data_size;
|
||||
}
|
||||
|
||||
return buf.buf;
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
|
||||
@@ -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 <memory>
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <memory>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
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));
|
||||
|
||||
@@ -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 <ranges>
|
||||
|
||||
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<usb::UsbDs>(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<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x40;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x40;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// the blow code is taken from libnx, with the addition of a uevent to cancel.
|
||||
Result Usb::IsUsbConnected(u64 timeout) {
|
||||
Result rc;
|
||||
UsbState state = UsbState_Detached;
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
if (R_FAILED(rc)) return rc;
|
||||
if (state == UsbState_Configured) return 0;
|
||||
|
||||
bool has_timeout = timeout != UINT64_MAX;
|
||||
u64 deadline = 0;
|
||||
|
||||
const std::array waiters{
|
||||
waiterForEvent(usbDsGetStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
if (has_timeout)
|
||||
deadline = armGetSystemTick() + armNsToTicks(timeout);
|
||||
|
||||
do {
|
||||
if (has_timeout) {
|
||||
s64 remaining = deadline - armGetSystemTick();
|
||||
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
|
||||
}
|
||||
|
||||
s32 idx;
|
||||
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
eventClear(usbDsGetStateChangeEvent());
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx != 0) {
|
||||
rc = Result_Cancelled; // cancelled.
|
||||
break;
|
||||
}
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
|
||||
|
||||
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
|
||||
return KERNELRESULT(TimedOut);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
|
||||
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<char> names(header.nspListSize);
|
||||
R_TRY(TransferAll(true, names.data(), names.size(), timeout));
|
||||
R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
|
||||
|
||||
out_names.clear();
|
||||
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<u8*>(data);
|
||||
m_aligned.resize((size + 0xFFF) & ~0xFFF);
|
||||
|
||||
while (size) {
|
||||
if (!read) {
|
||||
std::memcpy(m_aligned.data(), buf, size);
|
||||
}
|
||||
|
||||
u32 out_size_transferred;
|
||||
R_TRY(TransferPacketImpl(read, m_aligned.data(), size, &out_size_transferred, timeout));
|
||||
|
||||
if (read) {
|
||||
std::memcpy(buf, m_aligned.data(), out_size_transferred);
|
||||
}
|
||||
|
||||
buf += out_size_transferred;
|
||||
size -= out_size_transferred;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
|
||||
USBCmdHeader header{
|
||||
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||
.type = USBCmdType::REQUEST,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -275,7 +275,6 @@ struct Yati {
|
||||
NcmContentMetaDatabase db{};
|
||||
NcmStorageId storage_id{};
|
||||
|
||||
Service es{};
|
||||
Service ns_app{};
|
||||
std::unique_ptr<container::Base> 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<TikCollection> 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<TikCollection> 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<TikCollection>& 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<TikCollection> 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<source::Base
|
||||
return lhs.offset < rhs.offset;
|
||||
};
|
||||
|
||||
std::sort(collections.begin(), collections.end(), sorter);
|
||||
std::ranges::sort(collections, sorter);
|
||||
|
||||
for (const auto& collection : collections) {
|
||||
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
|
||||
@@ -1334,7 +1338,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
keys::parse_hex_key(rights_id.c, collection.name.c_str());
|
||||
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
|
||||
|
||||
auto entry = std::find_if(tickets.begin(), tickets.end(), [&rights_id](auto& e){
|
||||
auto entry = std::ranges::find_if(tickets, [&rights_id](auto& e){
|
||||
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
|
||||
});
|
||||
|
||||
@@ -1353,7 +1357,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
for (auto& cnmt : cnmts) {
|
||||
// copy nca structs into cnmt.
|
||||
for (auto& cnmt_nca : cnmt.ncas) {
|
||||
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [&cnmt_nca](auto& e){
|
||||
auto it = std::ranges::find_if(ncas, [&cnmt_nca](auto& e){
|
||||
return e.name == cnmt_nca.name;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user