diff --git a/troposphere/Makefile b/troposphere/Makefile index 25adf0da2..6a67c9e7d 100644 --- a/troposphere/Makefile +++ b/troposphere/Makefile @@ -1,4 +1,4 @@ -APPLICATIONS := daybreak reboot_to_payload +APPLICATIONS := daybreak haze reboot_to_payload SUBFOLDERS := $(APPLICATIONS) diff --git a/troposphere/haze/Makefile b/troposphere/haze/Makefile new file mode 100644 index 000000000..f80a28a07 --- /dev/null +++ b/troposphere/haze/Makefile @@ -0,0 +1,226 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. +#--------------------------------------------------------------------------------- +TARGET := $(notdir $(CURDIR)) +BUILD := build +SOURCES := source +DATA := data +INCLUDES := include ../../libraries/libvapours/include +#ROMFS := romfs + +APP_TITLE := USB File Transfer +APP_AUTHOR := Atmosphere-NX +APP_VERSION := 1.0.0 + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE + +CFLAGS := -g -Wall -O2 -ffunction-sections \ + $(ARCH) $(DEFINES) + +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++20 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lnx + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +ifeq ($(strip $(CONFIG_JSON)),) + jsons := $(wildcard *.json) + ifneq (,$(findstring $(TARGET).json,$(jsons))) + export APP_JSON := $(TOPDIR)/$(TARGET).json + else + ifneq (,$(findstring config.json,$(jsons))) + export APP_JSON := $(TOPDIR)/config.json + endif + endif +else + export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) +endif + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.jpg) + ifneq (,$(findstring $(TARGET).jpg,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).jpg + else + ifneq (,$(findstring icon.jpg,$(icons))) + export APP_ICON := $(TOPDIR)/icon.jpg + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_ICON)),) + export NROFLAGS += --icon=$(APP_ICON) +endif + +ifeq ($(strip $(NO_NACP)),) + export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp +endif + +ifneq ($(APP_TITLEID),) + export NACPFLAGS += --titleid=$(APP_TITLEID) +endif + +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: $(BUILD) clean all + +#--------------------------------------------------------------------------------- +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... +ifeq ($(strip $(APP_JSON)),) + @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf +else + @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf +endif + + +#--------------------------------------------------------------------------------- +else +.PHONY: all + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +ifeq ($(strip $(APP_JSON)),) + +all : $(OUTPUT).nro + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp +else +$(OUTPUT).nro : $(OUTPUT).elf +endif + +else + +all : $(OUTPUT).nsp + +$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm + +$(OUTPUT).nso : $(OUTPUT).elf + +endif + +$(OUTPUT).elf : $(OFILES) + +$(OFILES_SRC) : $(HFILES_BIN) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/troposphere/haze/icon.jpg b/troposphere/haze/icon.jpg new file mode 100644 index 000000000..6c64f740e Binary files /dev/null and b/troposphere/haze/icon.jpg differ diff --git a/troposphere/haze/icon.svg b/troposphere/haze/icon.svg new file mode 100644 index 000000000..e1478a7e3 --- /dev/null +++ b/troposphere/haze/icon.svg @@ -0,0 +1,2 @@ + + diff --git a/troposphere/haze/include/haze.hpp b/troposphere/haze/include/haze.hpp new file mode 100644 index 000000000..1d72415f1 --- /dev/null +++ b/troposphere/haze/include/haze.hpp @@ -0,0 +1,26 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/troposphere/haze/include/haze/assert.hpp b/troposphere/haze/include/haze/assert.hpp new file mode 100644 index 000000000..d7178276b --- /dev/null +++ b/troposphere/haze/include/haze/assert.hpp @@ -0,0 +1,32 @@ +/* + * 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 . + */ +#pragma once + +#define HAZE_ASSERT(expr) \ +{ \ + const bool __tmp_haze_assert_val = static_cast(expr); \ + if (AMS_UNLIKELY(!__tmp_haze_assert_val)) { \ + svcBreak(BreakReason_Assert, 0, 0); \ + } \ +} + +#define HAZE_R_ABORT_UNLESS(res_expr) \ +{ \ + const auto _tmp_r_abort_rc = (res_expr); \ + HAZE_ASSERT(R_SUCCEEDED(_tmp_r_abort_rc)); \ +} + +#define HAZE_UNREACHABLE_DEFAULT_CASE() default: HAZE_ASSERT(false) diff --git a/troposphere/haze/include/haze/async_usb_server.hpp b/troposphere/haze/include/haze/async_usb_server.hpp new file mode 100644 index 000000000..c99d299b7 --- /dev/null +++ b/troposphere/haze/include/haze/async_usb_server.hpp @@ -0,0 +1,44 @@ +/* + * 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 . + */ +#pragma once + +#include +#include + +namespace haze { + + class AsyncUsbServer final { + private: + EventReactor *m_reactor; + public: + constexpr explicit AsyncUsbServer() : m_reactor() { /* ... */ } + + Result Initialize(const UsbCommsInterfaceInfo *interface_info, u16 id_vendor, u16 id_product, EventReactor *reactor); + void Finalize(); + private: + Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) const; + public: + Result ReadPacket(void *page, u32 size, u32 *out_size_transferred) const { + R_RETURN(this->TransferPacketImpl(true, page, size, out_size_transferred)); + } + + Result WritePacket(void *page, u32 size) const { + u32 size_transferred; + R_RETURN(this->TransferPacketImpl(false, page, size, std::addressof(size_transferred))); + } + }; + +} diff --git a/troposphere/haze/include/haze/common.hpp b/troposphere/haze/include/haze/common.hpp new file mode 100644 index 000000000..5ebaf5861 --- /dev/null +++ b/troposphere/haze/include/haze/common.hpp @@ -0,0 +1,47 @@ +/* + * 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 . + */ +#pragma once + +#define ATMOSPHERE_OS_HORIZON +#define ATMOSPHERE_ARCH_ARM64 +#define ATMOSPHERE_ARCH_ARM_V8A + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace haze { + + using namespace ::ams::literals; + using namespace ::ams; + + using Result = ::ams::Result; + + static constexpr u32 UsbBulkPacketBufferSize = 1_MB; + +} diff --git a/troposphere/haze/include/haze/console_main_loop.hpp b/troposphere/haze/include/haze/console_main_loop.hpp new file mode 100644 index 000000000..b91281615 --- /dev/null +++ b/troposphere/haze/include/haze/console_main_loop.hpp @@ -0,0 +1,249 @@ +/* + * 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 . + */ +#pragma once + +#include +#include + +namespace haze { + + class ConsoleMainLoop : public EventConsumer { + private: + static constexpr size_t FrameDelayNs = 33'333'333; + private: + EventReactor *m_reactor; + PtpObjectHeap *m_object_heap; + + PadState m_pad; + + Thread m_thread; + UEvent m_event; + UEvent m_cancel_event; + + u32 m_last_heap_used; + u32 m_last_heap_total; + bool m_is_applet_mode; + private: + static void Run(void *arg) { + static_cast(arg)->Run(); + } + + void Run() { + int idx; + + while (true) { + /* Wait for up to 1 frame delay time to be cancelled. */ + Waiter cancel_waiter = waiterForUEvent(std::addressof(m_cancel_event)); + Result rc = waitObjects(std::addressof(idx), std::addressof(cancel_waiter), 1, FrameDelayNs); + + /* Finish if we were cancelled. */ + if (R_SUCCEEDED(rc)) { + break; + } + + /* Otherwise, signal the console update event. */ + if (svc::ResultTimedOut::Includes(rc)) { + ueventSignal(std::addressof(m_event)); + } + } + } + public: + explicit ConsoleMainLoop() : m_reactor(), m_pad(), m_thread(), m_event(), m_cancel_event(), m_last_heap_used(), m_last_heap_total(), m_is_applet_mode() { /* ... */ } + + Result Initialize(EventReactor *reactor, PtpObjectHeap *object_heap) { + /* Register event reactor and heap. */ + m_reactor = reactor; + m_object_heap = object_heap; + + /* Set cached use amounts to invalid values. */ + m_last_heap_used = 0xffffffffu; + m_last_heap_total = 0xffffffffu; + + /* Get whether we are launched in applet mode. */ + AppletType applet_type = appletGetAppletType(); + m_is_applet_mode = applet_type != AppletType_Application && applet_type != AppletType_SystemApplication; + + /* Initialize events. */ + ueventCreate(std::addressof(m_event), true); + ueventCreate(std::addressof(m_cancel_event), true); + + /* Set up pad inputs to allow exiting the program. */ + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + padInitializeAny(std::addressof(m_pad)); + + /* Create the delay thread with higher priority than the main thread (which runs at priority 0x2c). */ + R_TRY(threadCreate(std::addressof(m_thread), ConsoleMainLoop::Run, this, nullptr, 4_KB, 0x2b, svc::IdealCoreUseProcessValue)); + + /* Ensure we close the thread on failure. */ + ON_RESULT_FAILURE { threadClose(std::addressof(m_thread)); }; + + /* Connect ourselves to the event loop. */ + R_UNLESS(m_reactor->AddConsumer(this, waiterForUEvent(std::addressof(m_event))), haze::ResultRegistrationFailed()); + + /* Start the delay thread. */ + R_RETURN(threadStart(std::addressof(m_thread))); + } + + void Finalize() { + /* Signal the delay thread to shut down. */ + ueventSignal(std::addressof(m_cancel_event)); + + /* Wait for the delay thread to exit and close it. */ + HAZE_R_ABORT_UNLESS(threadWaitForExit(std::addressof(m_thread))); + + HAZE_R_ABORT_UNLESS(threadClose(std::addressof(m_thread))); + + /* Disconnect from the event loop.*/ + m_reactor->RemoveConsumer(this); + } + private: + void RedrawConsole() { + /* Get use amounts from the heap. */ + u32 heap_used = m_object_heap->GetUsedSize(); + u32 heap_total = m_object_heap->GetTotalSize(); + u32 heap_pct = heap_total > 0 ? static_cast((heap_used * 100ul) / heap_total) : 0; + + if (heap_used == m_last_heap_used && heap_total == m_last_heap_total) { + /* If usage didn't change, skip redrawing the console. */ + /* This provides a substantial performance improvement in file transfer speed. */ + return; + } + + /* Update cached use amounts. */ + m_last_heap_used = heap_used; + m_last_heap_total = heap_total; + + /* Determine units to use for printing to the console. */ + const char *used_unit = "B"; + if (heap_used >= 1_KB) { heap_used >>= 10; used_unit = "KiB"; } + if (heap_used >= (1_MB / 1_KB)) { heap_used >>= 10; used_unit = "MiB"; } + + const char *total_unit = "B"; + if (heap_total >= 1_KB) { heap_total >>= 10; total_unit = "KiB"; } + if (heap_total >= (1_MB / 1_KB)) { heap_total >>= 10; total_unit = "MiB"; } + + /* Draw the console UI. */ + consoleClear(); + printf("USB File Transfer\n\n"); + printf("Connect console to computer. Press [+] to exit.\n"); + printf("Heap used: %u %s / %u %s (%u%%)\n", heap_used, used_unit, heap_total, total_unit, heap_pct); + + if (m_is_applet_mode) { + /* Print "Applet Mode" in red text. */ + printf("\n" CONSOLE_ESC(38;5;196m) "Applet Mode" CONSOLE_ESC(0m) "\n"); + } + + consoleUpdate(nullptr); + } + protected: + void ProcessEvent() override { + /* Update the console. */ + this->RedrawConsole(); + + /* Check buttons. */ + padUpdate(std::addressof(m_pad)); + + /* If the plus button is held, request immediate exit. */ + if (padGetButtonsDown(std::addressof(m_pad)) & HidNpadButton_Plus) { + m_reactor->SetResult(haze::ResultStopRequested()); + } + + /* Pump applet events, and check if exit was requested. */ + if (!appletMainLoop()) { + m_reactor->SetResult(haze::ResultStopRequested()); + } + + /* Check if focus was lost. */ + if (appletGetFocusState() == AppletFocusState_Background) { + m_reactor->SetResult(haze::ResultFocusLost()); + } + } + private: + static bool SuspendAndWaitForFocus() { + /* Enable suspension with resume notification. */ + appletSetFocusHandlingMode(AppletFocusHandlingMode_SuspendHomeSleepNotify); + + /* Pump applet events. */ + while (appletMainLoop()) { + /* Check if focus was regained. */ + if (appletGetFocusState() != AppletFocusState_Background) { + return true; + } + } + + /* Exit was requested. */ + return false; + } + public: + static void RunApplication() { + /* Declare the object heap, to hold the database for an active session. */ + PtpObjectHeap ptp_object_heap; + + /* Declare the event reactor, and components which use it. */ + EventReactor event_reactor; + PtpResponder ptp_responder; + ConsoleMainLoop console_main_loop; + + /* Initialize the console.*/ + consoleInit(nullptr); + + while (true) { + /* Disable suspension. */ + appletSetFocusHandlingMode(AppletFocusHandlingMode_NoSuspend); + + /* Declare result from serving to use. */ + Result rc; + { + /* Ensure we don't go to sleep while transferring files. */ + appletSetAutoSleepDisabled(true); + + /* Clear the event reactor. */ + event_reactor.SetResult(ResultSuccess()); + + /* Configure the PTP responder and console main loop. */ + ptp_responder.Initialize(std::addressof(event_reactor), std::addressof(ptp_object_heap)); + console_main_loop.Initialize(std::addressof(event_reactor), std::addressof(ptp_object_heap)); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { + /* Finalize the console main loop and PTP responder. */ + console_main_loop.Finalize(); + ptp_responder.Finalize(); + + /* Restore auto sleep setting. */ + appletSetAutoSleepDisabled(false); + }; + + /* Begin processing requests. */ + rc = ptp_responder.LoopProcess(); + } + + /* If focus was lost, try to pump the applet main loop until we receive focus again. */ + if (haze::ResultFocusLost::Includes(rc) && SuspendAndWaitForFocus()) { + continue; + } + + /* Otherwise, enable suspension and finish. */ + appletSetFocusHandlingMode(AppletFocusHandlingMode_SuspendHomeSleep); + break; + } + + /* Finalize the console. */ + consoleExit(nullptr); + } + }; + +} diff --git a/troposphere/haze/include/haze/event_reactor.hpp b/troposphere/haze/include/haze/event_reactor.hpp new file mode 100644 index 000000000..563f9cb86 --- /dev/null +++ b/troposphere/haze/include/haze/event_reactor.hpp @@ -0,0 +1,52 @@ +/* + * 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 . + */ +#pragma once + +#include + +namespace haze { + + class EventConsumer { + public: + virtual ~EventConsumer() = default; + virtual void ProcessEvent() = 0; + }; + + class EventReactor { + private: + EventConsumer *m_consumers[svc::ArgumentHandleCountMax]; + Waiter m_waiters[svc::ArgumentHandleCountMax]; + s32 m_num_wait_objects; + Result m_result; + public: + constexpr explicit EventReactor() : m_consumers(), m_waiters(), m_num_wait_objects(), m_result(ResultSuccess()) { /* ... */ } + + bool AddConsumer(EventConsumer *consumer, Waiter waiter); + void RemoveConsumer(EventConsumer *consumer); + public: + void SetResult(Result r) { m_result = r; } + Result GetResult() const { return m_result; } + public: + template requires (sizeof...(Args) > 0) + Result WaitFor(s32 *out_arg_waiter, Args &&... arg_waiters) { + const Waiter arg_waiter_array[] = { arg_waiters... }; + return this->WaitForImpl(out_arg_waiter, arg_waiter_array, sizeof...(Args)); + } + private: + Result WaitForImpl(s32 *out_arg_waiter, const Waiter *arg_waiters, s32 num_arg_waiters); + }; + +} diff --git a/troposphere/haze/include/haze/file_system_proxy.hpp b/troposphere/haze/include/haze/file_system_proxy.hpp new file mode 100644 index 000000000..516ba1115 --- /dev/null +++ b/troposphere/haze/include/haze/file_system_proxy.hpp @@ -0,0 +1,131 @@ +/* + * 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 . + */ +#pragma once + +#include +#include + +namespace haze { + + class FileSystemProxy final { + private: + EventReactor *m_reactor; + FsFileSystem *m_filesystem; + public: + constexpr explicit FileSystemProxy() : m_reactor(), m_filesystem() { /* ... */ } + + void Initialize(EventReactor *reactor, FsFileSystem *fs) { + HAZE_ASSERT(fs != nullptr); + + m_reactor = reactor; + m_filesystem = fs; + } + + void Finalize() { + m_reactor = nullptr; + m_filesystem = nullptr; + } + private: + template + Result ForwardResult(F func, Args &&... args) { + /* Perform the method call, collecting its result. */ + const Result rc = func(std::forward(args)...); + + /* If the event loop was stopped, return that here. */ + R_TRY(m_reactor->GetResult()); + + /* Otherwise, return the call result. */ + R_RETURN(rc); + } + public: + Result GetTotalSpace(const char *path, s64 *out) { + R_RETURN(this->ForwardResult(fsFsGetTotalSpace, m_filesystem, path, out)); + } + + Result GetFreeSpace(const char *path, s64 *out) { + R_RETURN(this->ForwardResult(fsFsGetFreeSpace, m_filesystem, path, out)); + } + + Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) { + R_RETURN(this->ForwardResult(fsFsGetEntryType, m_filesystem, path, out_entry_type)); + } + + Result CreateFile(const char* path, s64 size, u32 option) { + R_RETURN(this->ForwardResult(fsFsCreateFile, m_filesystem, path, size, option)); + } + + Result DeleteFile(const char* path) { + R_RETURN(this->ForwardResult(fsFsDeleteFile, m_filesystem, path)); + } + + Result RenameFile(const char *old_path, const char *new_path) { + R_RETURN(this->ForwardResult(fsFsRenameFile, m_filesystem, old_path, new_path)); + } + + Result OpenFile(const char *path, u32 mode, FsFile *out_file) { + R_RETURN(this->ForwardResult(fsFsOpenFile, m_filesystem, path, mode, out_file)); + } + + Result GetFileSize(FsFile *file, s64 *out_size) { + R_RETURN(this->ForwardResult(fsFileGetSize, file, out_size)); + } + + Result SetFileSize(FsFile *file, s64 size) { + R_RETURN(this->ForwardResult(fsFileSetSize, file, size)); + } + + Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) { + R_RETURN(this->ForwardResult(fsFileRead, file, off, buf, read_size, option, out_bytes_read)); + } + + Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) { + R_RETURN(this->ForwardResult(fsFileWrite, file, off, buf, write_size, option)); + } + + void CloseFile(FsFile *file) { + fsFileClose(file); + } + + Result CreateDirectory(const char* path) { + R_RETURN(this->ForwardResult(fsFsCreateDirectory, m_filesystem, path)); + } + + Result DeleteDirectoryRecursively(const char* path) { + R_RETURN(this->ForwardResult(fsFsDeleteDirectoryRecursively, m_filesystem, path)); + } + + Result RenameDirectory(const char *old_path, const char *new_path) { + R_RETURN(this->ForwardResult(fsFsRenameDirectory, m_filesystem, old_path, new_path)); + } + + Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) { + R_RETURN(this->ForwardResult(fsFsOpenDirectory, m_filesystem, path, mode, out_dir)); + } + + Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) { + R_RETURN(this->ForwardResult(fsDirRead, d, out_total_entries, max_entries, buf)); + } + + Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) { + R_RETURN(this->ForwardResult(fsDirGetEntryCount, d, out_count)); + } + + void CloseDirectory(FsDir *d) { + fsDirClose(d); + } + }; + +} diff --git a/troposphere/haze/include/haze/ptp.hpp b/troposphere/haze/include/haze/ptp.hpp new file mode 100644 index 000000000..771b126ce --- /dev/null +++ b/troposphere/haze/include/haze/ptp.hpp @@ -0,0 +1,488 @@ +/* + * Copyright (c) Atmosphère-NX + * Copyright (c) libmtp project + * + * 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 . + */ +#pragma once + +#include + +namespace haze { + + constexpr inline u32 PtpUsbBulkHighSpeedMaxPacketLength = 0x200; + constexpr inline u32 PtpUsbBulkSuperSpeedMaxPacketLength = 0x400; + constexpr inline u32 PtpUsbBulkHeaderLength = 2 * sizeof(u32) + 2 * sizeof(u16); + constexpr inline u32 PtpStringMaxLength = 255; + + enum PtpUsbBulkContainerType : u16 { + PtpUsbBulkContainerType_Undefined = 0x0000, + PtpUsbBulkContainerType_Command = 0x0001, + PtpUsbBulkContainerType_Data = 0x0002, + PtpUsbBulkContainerType_Response = 0x0003, + PtpUsbBulkContainerType_Event = 0x0004, + }; + + enum PtpOperationCode : u16 { + PtpOperationCode_Undefined = 0x1000, + PtpOperationCode_GetDeviceInfo = 0x1001, + PtpOperationCode_OpenSession = 0x1002, + PtpOperationCode_CloseSession = 0x1003, + PtpOperationCode_GetStorageIds = 0x1004, + PtpOperationCode_GetStorageInfo = 0x1005, + PtpOperationCode_GetNumObjects = 0x1006, + PtpOperationCode_GetObjectHandles = 0x1007, + PtpOperationCode_GetObjectInfo = 0x1008, + PtpOperationCode_GetObject = 0x1009, + PtpOperationCode_GetThumb = 0x100a, + PtpOperationCode_DeleteObject = 0x100b, + PtpOperationCode_SendObjectInfo = 0x100c, + PtpOperationCode_SendObject = 0x100d, + PtpOperationCode_InitiateCapture = 0x100e, + PtpOperationCode_FormatStore = 0x100f, + PtpOperationCode_ResetDevice = 0x1010, + PtpOperationCode_SelfTest = 0x1011, + PtpOperationCode_SetObjectProtection = 0x1012, + PtpOperationCode_PowerDown = 0x1013, + PtpOperationCode_GetDevicePropDesc = 0x1014, + PtpOperationCode_GetDevicePropValue = 0x1015, + PtpOperationCode_SetDevicePropValue = 0x1016, + PtpOperationCode_ResetDevicePropValue = 0x1017, + PtpOperationCode_TerminateOpenCapture = 0x1018, + PtpOperationCode_MoveObject = 0x1019, + PtpOperationCode_CopyObject = 0x101a, + PtpOperationCode_GetPartialObject = 0x101b, + PtpOperationCode_InitiateOpenCapture = 0x101c, + PtpOperationCode_StartEnumHandles = 0x101d, + PtpOperationCode_EnumHandles = 0x101e, + PtpOperationCode_StopEnumHandles = 0x101f, + PtpOperationCode_GetVendorExtensionMaps = 0x1020, + PtpOperationCode_GetVendorDeviceInfo = 0x1021, + PtpOperationCode_GetResizedImageObject = 0x1022, + PtpOperationCode_GetFilesystemManifest = 0x1023, + PtpOperationCode_GetStreamInfo = 0x1024, + PtpOperationCode_GetStream = 0x1025, + PtpOperationCode_MtpGetObjectPropsSupported = 0x9801, + PtpOperationCode_MtpGetObjectPropDesc = 0x9802, + PtpOperationCode_MtpGetObjectPropValue = 0x9803, + PtpOperationCode_MtpSetObjectPropValue = 0x9804, + PtpOperationCode_MtpGetObjPropList = 0x9805, + PtpOperationCode_MtpSetObjPropList = 0x9806, + PtpOperationCode_MtpGetInterdependendPropdesc = 0x9807, + PtpOperationCode_MtpSendObjectPropList = 0x9808, + PtpOperationCode_MtpGetObjectReferences = 0x9810, + PtpOperationCode_MtpSetObjectReferences = 0x9811, + PtpOperationCode_MtpUpdateDeviceFirmware = 0x9812, + PtpOperationCode_MtpSkip = 0x9820, + }; + + enum PtpResponseCode : u16 { + PtpResponseCode_Undefined = 0x2000, + PtpResponseCode_Ok = 0x2001, + PtpResponseCode_GeneralError = 0x2002, + PtpResponseCode_SessionNotOpen = 0x2003, + PtpResponseCode_InvalidTransactionId = 0x2004, + PtpResponseCode_OperationNotSupported = 0x2005, + PtpResponseCode_ParameterNotSupported = 0x2006, + PtpResponseCode_IncompleteTransfer = 0x2007, + PtpResponseCode_InvalidStorageId = 0x2008, + PtpResponseCode_InvalidObjectHandle = 0x2009, + PtpResponseCode_DevicePropNotSupported = 0x200a, + PtpResponseCode_InvalidObjectFormatCode = 0x200b, + PtpResponseCode_StoreFull = 0x200c, + PtpResponseCode_ObjectWriteProtected = 0x200d, + PtpResponseCode_StoreReadOnly = 0x200e, + PtpResponseCode_AccessDenied = 0x200f, + PtpResponseCode_NoThumbnailPresent = 0x2010, + PtpResponseCode_SelfTestFailed = 0x2011, + PtpResponseCode_PartialDeletion = 0x2012, + PtpResponseCode_StoreNotAvailable = 0x2013, + PtpResponseCode_SpecificationByFormatUnsupported = 0x2014, + PtpResponseCode_NoValidObjectInfo = 0x2015, + PtpResponseCode_InvalidCodeFormat = 0x2016, + PtpResponseCode_UnknownVendorCode = 0x2017, + PtpResponseCode_CaptureAlreadyTerminated = 0x2018, + PtpResponseCode_DeviceBusy = 0x2019, + PtpResponseCode_InvalidParentObject = 0x201a, + PtpResponseCode_InvalidDevicePropFormat = 0x201b, + PtpResponseCode_InvalidDevicePropValue = 0x201c, + PtpResponseCode_InvalidParameter = 0x201d, + PtpResponseCode_SessionAlreadyOpened = 0x201e, + PtpResponseCode_TransactionCanceled = 0x201f, + PtpResponseCode_SpecificationOfDestinationUnsupported = 0x2020, + PtpResponseCode_InvalidEnumHandle = 0x2021, + PtpResponseCode_NoStreamEnabled = 0x2022, + PtpResponseCode_InvalidDataSet = 0x2023, + PtpResponseCode_MtpUndefined = 0xa800, + PtpResponseCode_MtpInvalidObjectPropCode = 0xa801, + PtpResponseCode_MtpInvalidObjectPropFormat = 0xa802, + PtpResponseCode_MtpInvalidObjectPropValue = 0xa803, + PtpResponseCode_MtpInvalidObjectReference = 0xa804, + PtpResponseCode_MtpInvalidDataset = 0xa806, + PtpResponseCode_MtpSpecificationByGroupUnsupported = 0xa807, + PtpResponseCode_MtpSpecificationByDepthUnsupported = 0xa808, + PtpResponseCode_MtpObjectTooLarge = 0xa809, + PtpResponseCode_MtpObjectPropNotSupported = 0xa80a, + }; + + enum PtpEventCode : u16 { + PtpEventCode_Undefined = 0x4000, + PtpEventCode_CancelTransaction = 0x4001, + PtpEventCode_ObjectAdded = 0x4002, + PtpEventCode_ObjectRemoved = 0x4003, + PtpEventCode_StoreAdded = 0x4004, + PtpEventCode_StoreRemoved = 0x4005, + PtpEventCode_DevicePropChanged = 0x4006, + PtpEventCode_ObjectInfoChanged = 0x4007, + PtpEventCode_DeviceInfoChanged = 0x4008, + PtpEventCode_RequestObjectTransfer = 0x4009, + PtpEventCode_StoreFull = 0x400a, + PtpEventCode_DeviceReset = 0x400b, + PtpEventCode_StorageInfoChanged = 0x400c, + PtpEventCode_CaptureComplete = 0x400d, + PtpEventCode_UnreportedStatus = 0x400e, + }; + + enum PtpDataTypeCode : u16 { + PtpDataTypeCode_Undefined = 0x0000, + PtpDataTypeCode_S8 = 0x0001, + PtpDataTypeCode_U8 = 0x0002, + PtpDataTypeCode_S16 = 0x0003, + PtpDataTypeCode_U16 = 0x0004, + PtpDataTypeCode_S32 = 0x0005, + PtpDataTypeCode_U32 = 0x0006, + PtpDataTypeCode_S64 = 0x0007, + PtpDataTypeCode_U64 = 0x0008, + PtpDataTypeCode_S128 = 0x0009, + PtpDataTypeCode_U128 = 0x000a, + + PtpDataTypeCode_ArrayMask = (1u << 14), + + PtpDataTypeCode_S8Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_S8, + PtpDataTypeCode_U8Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_U8, + PtpDataTypeCode_S16Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_S16, + PtpDataTypeCode_U16Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_U16, + PtpDataTypeCode_S32Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_S32, + PtpDataTypeCode_U32Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_U32, + PtpDataTypeCode_S64Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_S64, + PtpDataTypeCode_U64Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_U64, + PtpDataTypeCode_S128Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_S128, + PtpDataTypeCode_U128Array = PtpDataTypeCode_ArrayMask | PtpDataTypeCode_U128, + + PtpDataTypeCode_String = 0xffff, + }; + + enum PtpPropertyGetSetFlag : u8 { + PtpPropertyGetSetFlag_Get = 0x00, + PtpPropertyGetSetFlag_GetSet = 0x01, + }; + + enum PtpPropertyGroupCode : u32 { + PtpPropertyGroupCode_Default = 0x00000000, + }; + + enum PtpPropertyFormFlag : u8 { + PtpPropertyFormFlag_None = 0x00, + PtpPropertyFormFlag_Range = 0x01, + PtpPropertyFormFlag_Enumeration = 0x02, + PtpPropertyFormFlag_DateTime = 0x03, + PtpPropertyFormFlag_FixedLengthArray = 0x04, + PtpPropertyFormFlag_RegularExpression = 0x05, + PtpPropertyFormFlag_ByteArray = 0x06, + PtpPropertyFormFlag_LongString = 0xff, + }; + + enum PtpObjectPropertyCode : u16 { + PtpObjectPropertyCode_StorageId = 0xdc01, + PtpObjectPropertyCode_ObjectFormat = 0xdc02, + PtpObjectPropertyCode_ProtectionStatus = 0xdc03, + PtpObjectPropertyCode_ObjectSize = 0xdc04, + PtpObjectPropertyCode_AssociationType = 0xdc05, + PtpObjectPropertyCode_AssociationDesc = 0xdc06, + PtpObjectPropertyCode_ObjectFileName = 0xdc07, + PtpObjectPropertyCode_DateCreated = 0xdc08, + PtpObjectPropertyCode_DateModified = 0xdc09, + PtpObjectPropertyCode_Keywords = 0xdc0a, + PtpObjectPropertyCode_ParentObject = 0xdc0b, + PtpObjectPropertyCode_AllowedFolderContents = 0xdc0c, + PtpObjectPropertyCode_Hidden = 0xdc0d, + PtpObjectPropertyCode_SystemObject = 0xdc0e, + PtpObjectPropertyCode_PersistentUniqueObjectIdentifier = 0xdc41, + PtpObjectPropertyCode_SyncId = 0xdc42, + PtpObjectPropertyCode_PropertyBag = 0xdc43, + PtpObjectPropertyCode_Name = 0xdc44, + PtpObjectPropertyCode_CreatedBy = 0xdc45, + PtpObjectPropertyCode_Artist = 0xdc46, + PtpObjectPropertyCode_DateAuthored = 0xdc47, + PtpObjectPropertyCode_Description = 0xdc48, + PtpObjectPropertyCode_UrlReference = 0xdc49, + PtpObjectPropertyCode_LanguageLocale = 0xdc4a, + PtpObjectPropertyCode_CopyrightInformation = 0xdc4b, + PtpObjectPropertyCode_Source = 0xdc4c, + PtpObjectPropertyCode_OriginLocation = 0xdc4d, + PtpObjectPropertyCode_DateAdded = 0xdc4e, + PtpObjectPropertyCode_NonConsumable = 0xdc4f, + PtpObjectPropertyCode_CorruptOrUnplayable = 0xdc50, + PtpObjectPropertyCode_ProducerSerialNumber = 0xdc51, + PtpObjectPropertyCode_RepresentativeSampleFormat = 0xdc81, + PtpObjectPropertyCode_RepresentativeSampleSize = 0xdc82, + PtpObjectPropertyCode_RepresentativeSampleHeight = 0xdc83, + PtpObjectPropertyCode_RepresentativeSampleWidth = 0xdc84, + PtpObjectPropertyCode_RepresentativeSampleDuration = 0xdc85, + PtpObjectPropertyCode_RepresentativeSampleData = 0xdc86, + PtpObjectPropertyCode_Width = 0xdc87, + PtpObjectPropertyCode_Height = 0xdc88, + PtpObjectPropertyCode_Duration = 0xdc89, + PtpObjectPropertyCode_Rating = 0xdc8a, + PtpObjectPropertyCode_Track = 0xdc8b, + PtpObjectPropertyCode_Genre = 0xdc8c, + PtpObjectPropertyCode_Credits = 0xdc8d, + PtpObjectPropertyCode_Lyrics = 0xdc8e, + PtpObjectPropertyCode_SubscriptionContentId = 0xdc8f, + PtpObjectPropertyCode_ProducedBy = 0xdc90, + PtpObjectPropertyCode_UseCount = 0xdc91, + PtpObjectPropertyCode_SkipCount = 0xdc92, + PtpObjectPropertyCode_LastAccessed = 0xdc93, + PtpObjectPropertyCode_ParentalRating = 0xdc94, + PtpObjectPropertyCode_MetaGenre = 0xdc95, + PtpObjectPropertyCode_Composer = 0xdc96, + PtpObjectPropertyCode_EffectiveRating = 0xdc97, + PtpObjectPropertyCode_Subtitle = 0xdc98, + PtpObjectPropertyCode_OriginalReleaseDate = 0xdc99, + PtpObjectPropertyCode_AlbumName = 0xdc9a, + PtpObjectPropertyCode_AlbumArtist = 0xdc9b, + PtpObjectPropertyCode_Mood = 0xdc9c, + PtpObjectPropertyCode_DrmStatus = 0xdc9d, + PtpObjectPropertyCode_SubDescription = 0xdc9e, + PtpObjectPropertyCode_IsCropped = 0xdcd1, + PtpObjectPropertyCode_IsColorCorrected = 0xdcd2, + PtpObjectPropertyCode_ImageBitDepth = 0xdcd3, + PtpObjectPropertyCode_Fnumber = 0xdcd4, + PtpObjectPropertyCode_ExposureTime = 0xdcd5, + PtpObjectPropertyCode_ExposureIndex = 0xdcd6, + PtpObjectPropertyCode_DisplayName = 0xdce0, + PtpObjectPropertyCode_BodyText = 0xdce1, + PtpObjectPropertyCode_Subject = 0xdce2, + PtpObjectPropertyCode_Priority = 0xdce3, + PtpObjectPropertyCode_GivenName = 0xdd00, + PtpObjectPropertyCode_MiddleNames = 0xdd01, + PtpObjectPropertyCode_FamilyName = 0xdd02, + PtpObjectPropertyCode_Prefix = 0xdd03, + PtpObjectPropertyCode_Suffix = 0xdd04, + PtpObjectPropertyCode_PhoneticGivenName = 0xdd05, + PtpObjectPropertyCode_PhoneticFamilyName = 0xdd06, + PtpObjectPropertyCode_EmailPrimary = 0xdd07, + PtpObjectPropertyCode_EmailPersonal1 = 0xdd08, + PtpObjectPropertyCode_EmailPersonal2 = 0xdd09, + PtpObjectPropertyCode_EmailBusiness1 = 0xdd0a, + PtpObjectPropertyCode_EmailBusiness2 = 0xdd0b, + PtpObjectPropertyCode_EmailOthers = 0xdd0c, + PtpObjectPropertyCode_PhoneNumberPrimary = 0xdd0d, + PtpObjectPropertyCode_PhoneNumberPersonal = 0xdd0e, + PtpObjectPropertyCode_PhoneNumberPersonal2 = 0xdd0f, + PtpObjectPropertyCode_PhoneNumberBusiness = 0xdd10, + PtpObjectPropertyCode_PhoneNumberBusiness2 = 0xdd11, + PtpObjectPropertyCode_PhoneNumberMobile = 0xdd12, + PtpObjectPropertyCode_PhoneNumberMobile2 = 0xdd13, + PtpObjectPropertyCode_FaxNumberPrimary = 0xdd14, + PtpObjectPropertyCode_FaxNumberPersonal = 0xdd15, + PtpObjectPropertyCode_FaxNumberBusiness = 0xdd16, + PtpObjectPropertyCode_PagerNumber = 0xdd17, + PtpObjectPropertyCode_PhoneNumberOthers = 0xdd18, + PtpObjectPropertyCode_PrimaryWebAddress = 0xdd19, + PtpObjectPropertyCode_PersonalWebAddress = 0xdd1a, + PtpObjectPropertyCode_BusinessWebAddress = 0xdd1b, + PtpObjectPropertyCode_InstantMessengerAddress = 0xdd1c, + PtpObjectPropertyCode_InstantMessengerAddress2 = 0xdd1d, + PtpObjectPropertyCode_InstantMessengerAddress3 = 0xdd1e, + PtpObjectPropertyCode_PostalAddressPersonalFull = 0xdd1f, + PtpObjectPropertyCode_PostalAddressPersonalFullLine1 = 0xdd20, + PtpObjectPropertyCode_PostalAddressPersonalFullLine2 = 0xdd21, + PtpObjectPropertyCode_PostalAddressPersonalFullCity = 0xdd22, + PtpObjectPropertyCode_PostalAddressPersonalFullRegion = 0xdd23, + PtpObjectPropertyCode_PostalAddressPersonalFullPostalCode = 0xdd24, + PtpObjectPropertyCode_PostalAddressPersonalFullCountry = 0xdd25, + PtpObjectPropertyCode_PostalAddressBusinessFull = 0xdd26, + PtpObjectPropertyCode_PostalAddressBusinessLine1 = 0xdd27, + PtpObjectPropertyCode_PostalAddressBusinessLine2 = 0xdd28, + PtpObjectPropertyCode_PostalAddressBusinessCity = 0xdd29, + PtpObjectPropertyCode_PostalAddressBusinessRegion = 0xdd2a, + PtpObjectPropertyCode_PostalAddressBusinessPostalCode = 0xdd2b, + PtpObjectPropertyCode_PostalAddressBusinessCountry = 0xdd2c, + PtpObjectPropertyCode_PostalAddressOtherFull = 0xdd2d, + PtpObjectPropertyCode_PostalAddressOtherLine1 = 0xdd2e, + PtpObjectPropertyCode_PostalAddressOtherLine2 = 0xdd2f, + PtpObjectPropertyCode_PostalAddressOtherCity = 0xdd30, + PtpObjectPropertyCode_PostalAddressOtherRegion = 0xdd31, + PtpObjectPropertyCode_PostalAddressOtherPostalCode = 0xdd32, + PtpObjectPropertyCode_PostalAddressOtherCountry = 0xdd33, + PtpObjectPropertyCode_OrganizationName = 0xdd34, + PtpObjectPropertyCode_PhoneticOrganizationName = 0xdd35, + PtpObjectPropertyCode_Role = 0xdd36, + PtpObjectPropertyCode_Birthdate = 0xdd37, + PtpObjectPropertyCode_MessageTo = 0xdd40, + PtpObjectPropertyCode_MessageCC = 0xdd41, + PtpObjectPropertyCode_MessageBCC = 0xdd42, + PtpObjectPropertyCode_MessageRead = 0xdd43, + PtpObjectPropertyCode_MessageReceivedTime = 0xdd44, + PtpObjectPropertyCode_MessageSender = 0xdd45, + PtpObjectPropertyCode_ActivityBeginTime = 0xdd50, + PtpObjectPropertyCode_ActivityEndTime = 0xdd51, + PtpObjectPropertyCode_ActivityLocation = 0xdd52, + PtpObjectPropertyCode_ActivityRequiredAttendees = 0xdd54, + PtpObjectPropertyCode_ActivityOptionalAttendees = 0xdd55, + PtpObjectPropertyCode_ActivityResources = 0xdd56, + PtpObjectPropertyCode_ActivityAccepted = 0xdd57, + PtpObjectPropertyCode_Owner = 0xdd5d, + PtpObjectPropertyCode_Editor = 0xdd5e, + PtpObjectPropertyCode_Webmaster = 0xdd5f, + PtpObjectPropertyCode_UrlSource = 0xdd60, + PtpObjectPropertyCode_UrlDestination = 0xdd61, + PtpObjectPropertyCode_TimeBookmark = 0xdd62, + PtpObjectPropertyCode_ObjectBookmark = 0xdd63, + PtpObjectPropertyCode_ByteBookmark = 0xdd64, + PtpObjectPropertyCode_LastBuildDate = 0xdd70, + PtpObjectPropertyCode_TimetoLive = 0xdd71, + PtpObjectPropertyCode_MediaGuid = 0xdd72, + PtpObjectPropertyCode_TotalBitRate = 0xde91, + PtpObjectPropertyCode_BitRateType = 0xde92, + PtpObjectPropertyCode_SampleRate = 0xde93, + PtpObjectPropertyCode_NumberOfChannels = 0xde94, + PtpObjectPropertyCode_AudioBitDepth = 0xde95, + PtpObjectPropertyCode_ScanDepth = 0xde97, + PtpObjectPropertyCode_AudioWaveCodec = 0xde99, + PtpObjectPropertyCode_AudioBitRate = 0xde9a, + PtpObjectPropertyCode_VideoFourCcCodec = 0xde9b, + PtpObjectPropertyCode_VideoBitRate = 0xde9c, + PtpObjectPropertyCode_FramesPerThousandSeconds = 0xde9d, + PtpObjectPropertyCode_KeyFrameDistance = 0xde9e, + PtpObjectPropertyCode_BufferSize = 0xde9f, + PtpObjectPropertyCode_EncodingQuality = 0xdea0, + PtpObjectPropertyCode_EncodingProfile = 0xdea1, + PtpObjectPropertyCode_BuyFlag = 0xd901, + }; + + enum PtpDevicePropertyCode : u16 { + PtpDevicePropertyCode_Undefined = 0x5000, + PtpDevicePropertyCode_BatteryLevel = 0x5001, + PtpDevicePropertyCode_FunctionalMode = 0x5002, + PtpDevicePropertyCode_ImageSize = 0x5003, + PtpDevicePropertyCode_CompressionSetting = 0x5004, + PtpDevicePropertyCode_WhiteBalance = 0x5005, + PtpDevicePropertyCode_RgbGain = 0x5006, + PtpDevicePropertyCode_FNumber = 0x5007, + PtpDevicePropertyCode_FocalLength = 0x5008, + PtpDevicePropertyCode_FocusDistance = 0x5009, + PtpDevicePropertyCode_FocusMode = 0x500a, + PtpDevicePropertyCode_ExposureMeteringMode = 0x500b, + PtpDevicePropertyCode_FlashMode = 0x500c, + PtpDevicePropertyCode_ExposureTime = 0x500d, + PtpDevicePropertyCode_ExposureProgramMode = 0x500e, + PtpDevicePropertyCode_ExposureIndex = 0x500f, + PtpDevicePropertyCode_ExposureBiasCompensation = 0x5010, + PtpDevicePropertyCode_DateTime = 0x5011, + PtpDevicePropertyCode_CaptureDelay = 0x5012, + PtpDevicePropertyCode_StillCaptureMode = 0x5013, + PtpDevicePropertyCode_Contrast = 0x5014, + PtpDevicePropertyCode_Sharpness = 0x5015, + PtpDevicePropertyCode_DigitalZoom = 0x5016, + PtpDevicePropertyCode_EffectMode = 0x5017, + PtpDevicePropertyCode_BurstNumber = 0x5018, + PtpDevicePropertyCode_BurstInterval = 0x5019, + PtpDevicePropertyCode_TimelapseNumber = 0x501a, + PtpDevicePropertyCode_TimelapseInterval = 0x501b, + PtpDevicePropertyCode_FocusMeteringMode = 0x501c, + PtpDevicePropertyCode_UploadUrl = 0x501d, + PtpDevicePropertyCode_Artist = 0x501e, + PtpDevicePropertyCode_CopyrightInfo = 0x501f, + PtpDevicePropertyCode_SupportedStreams = 0x5020, + PtpDevicePropertyCode_EnabledStreams = 0x5021, + PtpDevicePropertyCode_VideoFormat = 0x5022, + PtpDevicePropertyCode_VideoResolution = 0x5023, + PtpDevicePropertyCode_VideoQuality = 0x5024, + PtpDevicePropertyCode_VideoFrameRate = 0x5025, + PtpDevicePropertyCode_VideoContrast = 0x5026, + PtpDevicePropertyCode_VideoBrightness = 0x5027, + PtpDevicePropertyCode_AudioFormat = 0x5028, + PtpDevicePropertyCode_AudioBitrate = 0x5029, + PtpDevicePropertyCode_AudioSamplingRate = 0x502a, + PtpDevicePropertyCode_AudioBitPerSample = 0x502b, + PtpDevicePropertyCode_AudioVolume = 0x502c, + }; + + enum PtpObjectFormatCode : u16 { + PtpObjectFormatCode_Undefined = 0x3000, + PtpObjectFormatCode_Association = 0x3001, + PtpObjectFormatCode_Defined = 0x3800, + PtpObjectFormatCode_MtpMediaCard = 0xb211, + }; + + enum PtpAssociationType : u16 { + PtpAssociationType_Undefined = 0x0000, + PtpAssociationType_GenericFolder = 0x0001, + }; + + enum PtpGetObjectHandles : u32 { + PtpGetObjectHandles_AllFormats = 0x00000000, + PtpGetObjectHandles_AllAssocs = 0x00000000, + PtpGetObjectHandles_AllStorage = 0xffffffff, + PtpGetObjectHandles_RootParent = 0xffffffff, + }; + + enum PtpStorageType : u16 { + PtpStorageType_Undefined = 0x0000, + PtpStorageType_FixedRom = 0x0001, + PtpStorageType_RemovableRom = 0x0002, + PtpStorageType_FixedRam = 0x0003, + PtpStorageType_RemovableRam = 0x0004, + }; + + enum PtpFilesystemType : u16 { + PtpFilesystemType_Undefined = 0x0000, + PtpFilesystemType_GenericFlat = 0x0001, + PtpFilesystemType_GenericHierarchical = 0x0002, + PtpFilesystemType_Dcf = 0x0003, + }; + + enum PtpAccessCapability : u16 { + PtpAccessCapability_ReadWrite = 0x0000, + PtpAccessCapability_ReadOnly = 0x0001, + PtpAccessCapability_ReadOnlyWithObjectDeletion = 0x0002, + }; + + enum PtpProtectionStatus : u16 { + PtpProtectionStatus_NoProtection = 0x0000, + PtpProtectionStatus_ReadOnly = 0x0001, + PtpProtectionStatus_MtpReadOnlyData = 0x8002, + PtpProtectionStatus_MtpNonTransferableData = 0x8003, + }; + + enum PtpThumbFormat : u16 { + PtpThumbFormat_Undefined = 0x0000, + }; + + struct PtpUsbBulkContainer { + u32 length; + u16 type; + u16 code; + u32 trans_id; + }; + static_assert(sizeof(PtpUsbBulkContainer) == PtpUsbBulkHeaderLength); + + struct PtpNewObjectInfo { + u32 storage_id; + u32 parent_object_id; + u32 object_id; + }; + +} diff --git a/troposphere/haze/include/haze/ptp_data_builder.hpp b/troposphere/haze/include/haze/ptp_data_builder.hpp new file mode 100644 index 000000000..6e1baa064 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_data_builder.hpp @@ -0,0 +1,174 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include + +namespace haze { + + class PtpDataBuilder final { + private: + AsyncUsbServer *m_server; + u32 m_transmitted_size; + u32 m_offset; + u8 *m_data; + bool m_disabled; + private: + Result Flush() { + ON_SCOPE_EXIT { + m_transmitted_size += m_offset; + m_offset = 0; + }; + + /* If we're disabled, we have nothing to do. */ + R_SUCCEED_IF(m_disabled); + + /* Otherwise, we should write our buffered data. */ + R_RETURN(m_server->WritePacket(m_data, m_offset)); + } + public: + constexpr explicit PtpDataBuilder(void *data, AsyncUsbServer *server) : m_server(server), m_transmitted_size(), m_offset(), m_data(static_cast(data)), m_disabled() { /* ... */ } + + Result Commit() { + if (m_offset > 0) { + /* If there is remaining data left to write, write it now. */ + R_TRY(this->Flush()); + } + + if (util::IsAligned(m_transmitted_size, PtpUsbBulkHighSpeedMaxPacketLength)) { + /* If the transmission size was a multiple of wMaxPacketSize, send a zero length packet. */ + R_TRY(this->Flush()); + } + + R_SUCCEED(); + } + + Result AddBuffer(const u8 *buffer, u32 count) { + while (count > 0) { + /* Calculate how many bytes we can write now. */ + const u32 write_size = std::min(count, haze::UsbBulkPacketBufferSize - m_offset); + + /* Write this number of bytes. */ + std::memcpy(m_data + m_offset, buffer, write_size); + m_offset += write_size; + buffer += write_size; + count -= write_size; + + /* If our buffer is full, flush it. */ + if (m_offset == haze::UsbBulkPacketBufferSize) { + R_TRY(this->Flush()); + } + } + + R_SUCCEED(); + } + + template + Result Add(T value) { + u8 bytes[sizeof(T)]; + + std::memcpy(bytes, std::addressof(value), sizeof(T)); + + R_RETURN(this->AddBuffer(bytes, sizeof(T))); + } + + Result AddDataHeader(PtpUsbBulkContainer &request, u32 data_size) { + R_TRY(this->Add(PtpUsbBulkHeaderLength + data_size)); + R_TRY(this->Add(PtpUsbBulkContainerType_Data)); + R_TRY(this->Add(request.code)); + R_TRY(this->Add(request.trans_id)); + + R_SUCCEED(); + } + + Result AddResponseHeader(PtpUsbBulkContainer &request, PtpResponseCode code, u32 params_size) { + R_TRY(this->Add(PtpUsbBulkHeaderLength + params_size)); + R_TRY(this->Add(PtpUsbBulkContainerType_Response)); + R_TRY(this->Add(code)); + R_TRY(this->Add(request.trans_id)); + + R_SUCCEED(); + } + + template + Result WriteVariableLengthData(PtpUsbBulkContainer &request, F &&func) { + HAZE_ASSERT(m_offset == 0 && m_transmitted_size == 0); + + /* Declare how many bytes the data will require to write. */ + u32 data_size = 0; + { + /* Temporarily disable writing to calculate the size.*/ + m_disabled = true; + + ON_SCOPE_EXIT { + /* Report how many bytes were required. */ + data_size = m_transmitted_size; + + /* On exit, enable writing and reset sizes. */ + m_transmitted_size = 0; + m_disabled = false; + m_offset = 0; + }; + + R_TRY(func()); + R_TRY(this->Commit()); + } + + /* Actually copy and write the data. */ + R_TRY(this->AddDataHeader(request, data_size)); + R_TRY(func()); + R_TRY(this->Commit()); + + /* We succeeded. */ + R_SUCCEED(); + } + + template + Result AddString(const T *str) { + /* Use one less than the maximum string length for maximum length with null terminator. */ + const u8 len = static_cast(std::min(util::Strlen(str), PtpStringMaxLength - 1)); + + if (len > 0) { + /* Length is padded by null terminator for non-empty strings. */ + R_TRY(this->Add(len + 1)); + + for (size_t i = 0; i < len; i++) { + R_TRY(this->Add(str[i])); + } + + R_TRY(this->Add(0)); + } else { + R_TRY(this->Add(len)); + } + + R_SUCCEED(); + } + + template + Result AddArray(const T *arr, u32 count) { + R_TRY(this->Add(count)); + + for (size_t i = 0; i < count; i++) { + R_TRY(this->Add(arr[i])); + } + + R_SUCCEED(); + } + }; + +} diff --git a/troposphere/haze/include/haze/ptp_data_parser.hpp b/troposphere/haze/include/haze/ptp_data_parser.hpp new file mode 100644 index 000000000..f151f237a --- /dev/null +++ b/troposphere/haze/include/haze/ptp_data_parser.hpp @@ -0,0 +1,114 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include + +namespace haze { + + class PtpDataParser final { + private: + AsyncUsbServer *m_server; + u32 m_received_size; + u32 m_offset; + u8 *m_data; + bool m_eot; + private: + Result Flush() { + R_UNLESS(!m_eot, haze::ResultEndOfTransmission()); + + m_received_size = 0; + m_offset = 0; + + ON_SCOPE_EXIT { + /* End of transmission occurs when receiving a bulk transfer less than the buffer size. */ + /* PTP uses zero-length termination, so zero is a possible size to receive. */ + m_eot = m_received_size < haze::UsbBulkPacketBufferSize; + }; + + R_RETURN(m_server->ReadPacket(m_data, haze::UsbBulkPacketBufferSize, std::addressof(m_received_size))); + } + public: + constexpr explicit PtpDataParser(void *data, AsyncUsbServer *server) : m_server(server), m_received_size(), m_offset(), m_data(static_cast(data)), m_eot() { /* ... */ } + + Result Finalize() { + /* Read until the transmission completes. */ + while (true) { + Result rc = this->Flush(); + + R_SUCCEED_IF(m_eot || haze::ResultEndOfTransmission::Includes(rc)); + R_TRY(rc); + } + } + + Result ReadBuffer(u8 *buffer, u32 count, u32 *out_read_count) { + *out_read_count = 0; + + while (count > 0) { + /* If we cannot read more bytes now, flush. */ + if (m_offset == m_received_size) { + R_TRY(this->Flush()); + } + + /* Calculate how many bytes we can read now. */ + u32 read_size = std::min(count, m_received_size - m_offset); + + /* Read this number of bytes. */ + std::memcpy(buffer + *out_read_count, m_data + m_offset, read_size); + *out_read_count += read_size; + m_offset += read_size; + count -= read_size; + } + + R_SUCCEED(); + } + + template + Result Read(T *out_t) { + u32 read_count; + u8 bytes[sizeof(T)]; + + R_TRY(this->ReadBuffer(bytes, sizeof(T), std::addressof(read_count))); + + std::memcpy(out_t, bytes, sizeof(T)); + + R_SUCCEED(); + } + + /* NOTE: out_string must contain room for 256 bytes. */ + /* The result will be null-terminated on successful completion. */ + Result ReadString(char *out_string) { + u8 len; + R_TRY(this->Read(std::addressof(len))); + + /* Read characters one by one. */ + for (size_t i = 0; i < len; i++) { + u16 chr; + R_TRY(this->Read(std::addressof(chr))); + + *out_string++ = static_cast(chr); + } + + /* Write null terminator. */ + *out_string++ = '\x00'; + + R_SUCCEED(); + } + }; + +} diff --git a/troposphere/haze/include/haze/ptp_object_database.hpp b/troposphere/haze/include/haze/ptp_object_database.hpp new file mode 100644 index 000000000..7443bab40 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_object_database.hpp @@ -0,0 +1,107 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include + +namespace haze { + + struct PtpObject { + public: + util::IntrusiveRedBlackTreeNode m_name_node; + util::IntrusiveRedBlackTreeNode m_object_id_node; + u32 m_parent_id; + u32 m_object_id; + char m_name[]; + public: + const char *GetName() const { return m_name; } + u32 GetParentId() const { return m_parent_id; } + u32 GetObjectId() const { return m_object_id; } + public: + bool GetIsRegistered() const { return m_object_id != 0; } + void Register(u32 object_id) { m_object_id = object_id; } + void Unregister() { m_object_id = 0; } + public: + struct NameComparator { + struct RedBlackKeyType { + const char *m_name; + + constexpr RedBlackKeyType(const char *name) : m_name(name) { /* ... */ } + + constexpr const char *GetName() const { + return m_name; + } + }; + + template requires (std::same_as || std::same_as) + static constexpr int Compare(const T &lhs, const PtpObject &rhs) { + /* All SD card filesystems supported by fs are case-insensitive and case-preserving. */ + /* Account for that in collation here. */ + return strcasecmp(lhs.GetName(), rhs.GetName()); + } + }; + + struct ObjectIdComparator { + struct RedBlackKeyType { + u32 m_object_id; + + constexpr RedBlackKeyType(u32 object_id) : m_object_id(object_id) { /* ... */ } + + constexpr u32 GetObjectId() const { + return m_object_id; + } + }; + + template requires (std::same_as || std::same_as) + static constexpr int Compare(const T &lhs, const PtpObject &rhs) { + return lhs.GetObjectId() - rhs.GetObjectId(); + } + }; + }; + + class PtpObjectDatabase { + private: + using ObjectNameTreeTraits = util::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&PtpObject::m_name_node>; + using ObjectNameTree = ObjectNameTreeTraits::TreeType; + + using ObjectIdTreeTraits = util::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&PtpObject::m_object_id_node>; + using ObjectIdTree = ObjectIdTreeTraits::TreeType; + + PtpObjectHeap *m_object_heap; + ObjectNameTree m_name_tree; + ObjectIdTree m_object_id_tree; + u32 m_next_object_id; + public: + constexpr explicit PtpObjectDatabase() : m_object_heap(), m_name_tree(), m_object_id_tree(), m_next_object_id() { /* ... */ } + + void Initialize(PtpObjectHeap *object_heap); + void Finalize(); + public: + /* Object database API. */ + Result CreateOrFindObject(const char *parent_name, const char *name, u32 parent_id, PtpObject **out_object); + void RegisterObject(PtpObject *object, u32 desired_id = 0); + void UnregisterObject(PtpObject *object); + void DeleteObject(PtpObject *obj); + + Result CreateAndRegisterObjectId(const char *parent_name, const char *name, u32 parent_id, u32 *out_object_id); + public: + PtpObject *GetObjectById(u32 object_id); + PtpObject *GetObjectByName(const char *name); + }; + +} diff --git a/troposphere/haze/include/haze/ptp_object_heap.hpp b/troposphere/haze/include/haze/ptp_object_heap.hpp new file mode 100644 index 000000000..fe1090a32 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_object_heap.hpp @@ -0,0 +1,124 @@ +/* + * 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 . + */ +#pragma once + +#include + +namespace haze { + + /* This simple linear allocator implementation allows us to rapidly reclaim the entire object graph. */ + /* This is critical for maintaining interactivity when a session is closed. */ + class PtpObjectHeap { + private: + static constexpr size_t NumHeapBlocks = 2; + private: + void *m_heap_blocks[NumHeapBlocks]; + void *m_next_address; + u32 m_heap_block_size; + u32 m_current_heap_block; + public: + constexpr explicit PtpObjectHeap() : m_heap_blocks(), m_next_address(), m_heap_block_size(), m_current_heap_block() { /* ... */ } + + void Initialize(); + void Finalize(); + public: + constexpr size_t GetTotalSize() const { + return m_heap_block_size * NumHeapBlocks; + } + + constexpr size_t GetUsedSize() const { + return (m_heap_block_size * m_current_heap_block) + this->GetNextAddress() - this->GetFirstAddress(); + } + private: + constexpr u8 *GetNextAddress() const { return static_cast(m_next_address); } + constexpr u8 *GetFirstAddress() const { return static_cast(m_heap_blocks[m_current_heap_block]); } + + constexpr u8 *GetCurrentBlockEnd() const { + return this->GetFirstAddress() + m_heap_block_size; + } + + constexpr bool AllocationIsPossible(size_t n) const { + return n <= m_heap_block_size; + } + + constexpr bool AllocationIsSatisfyable(size_t n) const { + /* Check for overflow. */ + if (!util::CanAddWithoutOverflow(reinterpret_cast(this->GetNextAddress()), n)) { + return false; + } + + /* Check if we would exceed the size of the current block. */ + if (this->GetNextAddress() + n > this->GetCurrentBlockEnd()) { + return false; + } + + return true; + } + + constexpr bool AdvanceToNextBlock() { + if (m_current_heap_block < NumHeapBlocks - 1) { + m_next_address = m_heap_blocks[++m_current_heap_block]; + return true; + } + + return false; + } + + constexpr void *AllocateFromCurrentBlock(size_t n) { + void *result = this->GetNextAddress(); + + m_next_address = this->GetNextAddress() + n; + + return result; + } + public: + template + constexpr T *Allocate(size_t n) { + /* Check for overflow in alignment. */ + if (!util::CanAddWithoutOverflow(n, alignof(u64) - 1)) { + return nullptr; + } + + /* Align the amount to satisfy allocation for u64. */ + n = util::AlignUp(n, alignof(u64)); + + /* Check if the allocation is possible. */ + if (!this->AllocationIsPossible(n)) { + return nullptr; + } + + /* If the allocation is not satisfyable now, we might be able to satisfy it on the next block. */ + /* However, if the next block would be empty, we won't be able to satisfy the request. */ + if (!this->AllocationIsSatisfyable(n) && !this->AdvanceToNextBlock()) { + return nullptr; + } + + /* Allocate the memory. */ + return static_cast(this->AllocateFromCurrentBlock(n)); + } + + constexpr void Deallocate(void *p, size_t n) { + /* If the pointer was the last allocation, return the memory to the heap. */ + if (static_cast(p) + n == this->GetNextAddress()) { + m_next_address = this->GetNextAddress() - n; + } + + /* Otherwise, do nothing. */ + /* ... */ + } + }; + +} diff --git a/troposphere/haze/include/haze/ptp_responder.hpp b/troposphere/haze/include/haze/ptp_responder.hpp new file mode 100644 index 000000000..eac5aa330 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_responder.hpp @@ -0,0 +1,75 @@ +/* + * 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 . + */ +#pragma once + +#include +#include +#include +#include + +namespace haze { + + class PtpDataParser; + + class PtpResponder final { + private: + AsyncUsbServer m_usb_server; + FileSystemProxy m_fs; + PtpUsbBulkContainer m_request_header; + PtpObjectHeap *m_object_heap; + u32 m_send_object_id; + bool m_session_open; + + PtpObjectDatabase m_object_database; + public: + constexpr explicit PtpResponder() : m_usb_server(), m_fs(), m_request_header(), m_object_heap(), m_send_object_id(), m_session_open(), m_object_database() { /* ... */ } + + Result Initialize(EventReactor *reactor, PtpObjectHeap *object_heap); + void Finalize(); + public: + Result LoopProcess(); + private: + /* Request handling. */ + Result HandleRequest(); + Result HandleRequestImpl(); + Result HandleCommandRequest(PtpDataParser &dp); + void ForceCloseSession(); + + template + Result WriteResponse(PtpResponseCode code, Data &&data); + Result WriteResponse(PtpResponseCode code); + + /* PTP operations. */ + Result GetDeviceInfo(PtpDataParser &dp); + Result OpenSession(PtpDataParser &dp); + Result CloseSession(PtpDataParser &dp); + Result GetStorageIds(PtpDataParser &dp); + Result GetStorageInfo(PtpDataParser &dp); + Result GetObjectHandles(PtpDataParser &dp); + Result GetObjectInfo(PtpDataParser &dp); + Result GetObject(PtpDataParser &dp); + Result SendObjectInfo(PtpDataParser &dp); + Result SendObject(PtpDataParser &dp); + Result DeleteObject(PtpDataParser &dp); + + /* MTP operations. */ + Result GetObjectPropsSupported(PtpDataParser &dp); + Result GetObjectPropDesc(PtpDataParser &dp); + Result GetObjectPropValue(PtpDataParser &dp); + Result SetObjectPropValue(PtpDataParser &dp); + }; + +} diff --git a/troposphere/haze/include/haze/results.hpp b/troposphere/haze/include/haze/results.hpp new file mode 100644 index 000000000..fec0e84de --- /dev/null +++ b/troposphere/haze/include/haze/results.hpp @@ -0,0 +1,41 @@ +/* + * 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 . + */ +#pragma once + +#include + +/* NOTE: These results are all custom, and not official. */ +R_DEFINE_NAMESPACE_RESULT_MODULE(haze, 420); + +namespace haze { + + R_DEFINE_ERROR_RESULT(RegistrationFailed, 1); + R_DEFINE_ERROR_RESULT(NotConfigured, 2); + R_DEFINE_ERROR_RESULT(TransferFailed, 3); + R_DEFINE_ERROR_RESULT(StopRequested, 4); + R_DEFINE_ERROR_RESULT(FocusLost, 5); + R_DEFINE_ERROR_RESULT(EndOfTransmission, 6); + R_DEFINE_ERROR_RESULT(UnknownPacketType, 7); + R_DEFINE_ERROR_RESULT(SessionNotOpen, 8); + R_DEFINE_ERROR_RESULT(OutOfMemory, 9); + R_DEFINE_ERROR_RESULT(InvalidObjectId, 10); + R_DEFINE_ERROR_RESULT(InvalidStorageId, 11); + R_DEFINE_ERROR_RESULT(OperationNotSupported, 12); + R_DEFINE_ERROR_RESULT(UnknownRequestType, 13); + R_DEFINE_ERROR_RESULT(UnknownPropertyCode, 14); + R_DEFINE_ERROR_RESULT(InvalidPropertyValue, 15); + +} diff --git a/troposphere/haze/include/haze/usb_session.hpp b/troposphere/haze/include/haze/usb_session.hpp new file mode 100644 index 000000000..e5874617b --- /dev/null +++ b/troposphere/haze/include/haze/usb_session.hpp @@ -0,0 +1,48 @@ +/* + * 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 . + */ +#pragma once + +#include + +namespace haze { + + enum UsbSessionEndpoint { + UsbSessionEndpoint_Read = 0, + UsbSessionEndpoint_Write = 1, + UsbSessionEndpoint_Interrupt = 2, + UsbSessionEndpoint_Count = 3, + }; + + class UsbSession { + private: + UsbDsInterface *m_interface; + UsbDsEndpoint *m_endpoints[UsbSessionEndpoint_Count]; + private: + Result Initialize1x(const UsbCommsInterfaceInfo *info); + Result Initialize5x(const UsbCommsInterfaceInfo *info); + public: + constexpr explicit UsbSession() : m_interface(), m_endpoints() { /* ... */ } + + Result Initialize(const UsbCommsInterfaceInfo *info, u16 id_vendor, u16 id_product); + void Finalize(); + + bool GetConfigured() const; + Event *GetCompletionEvent(UsbSessionEndpoint ep) const; + Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id); + Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_transferred_size); + }; + +} diff --git a/troposphere/haze/source/async_usb_server.cpp b/troposphere/haze/source/async_usb_server.cpp new file mode 100644 index 000000000..8d63468a6 --- /dev/null +++ b/troposphere/haze/source/async_usb_server.cpp @@ -0,0 +1,62 @@ +/* + * 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 . + */ +#include + +namespace haze { + + namespace { + + constinit UsbSession g_usb_session; + + } + + Result AsyncUsbServer::Initialize(const UsbCommsInterfaceInfo *interface_info, u16 id_vendor, u16 id_product, EventReactor *reactor) { + m_reactor = reactor; + + /* Set up a new USB session. */ + R_TRY(g_usb_session.Initialize(interface_info, id_vendor, id_product)); + + R_SUCCEED(); + } + + void AsyncUsbServer::Finalize() { + g_usb_session.Finalize(); + } + + Result AsyncUsbServer::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) const { + u32 urb_id; + s32 waiter_idx; + + /* If we're not configured yet, wait to become configured first. */ + if (!g_usb_session.GetConfigured()) { + R_TRY(m_reactor->WaitFor(std::addressof(waiter_idx), waiterForEvent(usbDsGetStateChangeEvent()))); + R_TRY(eventClear(usbDsGetStateChangeEvent())); + + R_THROW(haze::ResultNotConfigured()); + } + + /* Select the appropriate endpoint and begin a transfer. */ + UsbSessionEndpoint ep = read ? UsbSessionEndpoint_Read : UsbSessionEndpoint_Write; + R_TRY(g_usb_session.TransferAsync(ep, page, size, std::addressof(urb_id))); + + /* Try to wait for the event. */ + R_TRY(m_reactor->WaitFor(std::addressof(waiter_idx), waiterForEvent(g_usb_session.GetCompletionEvent(ep)))); + + /* Return what we transferred. */ + R_RETURN(g_usb_session.GetTransferResult(ep, urb_id, out_size_transferred)); + } + +} diff --git a/troposphere/haze/source/event_reactor.cpp b/troposphere/haze/source/event_reactor.cpp new file mode 100644 index 000000000..f43d7735d --- /dev/null +++ b/troposphere/haze/source/event_reactor.cpp @@ -0,0 +1,78 @@ +/* + * 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 . + */ +#include + +namespace haze { + + bool EventReactor::AddConsumer(EventConsumer *consumer, Waiter waiter) { + HAZE_ASSERT(m_num_wait_objects + 1 <= svc::ArgumentHandleCountMax); + + /* Add to the end of the list. */ + m_consumers[m_num_wait_objects] = consumer; + m_waiters[m_num_wait_objects] = waiter; + m_num_wait_objects++; + + return true; + } + + void EventReactor::RemoveConsumer(EventConsumer *consumer) { + s32 output_index = 0; + + /* Remove the consumer. */ + for (s32 input_index = 0; input_index < m_num_wait_objects; input_index++) { + if (m_consumers[input_index] == consumer) { + continue; + } + + if (output_index != input_index) { + m_consumers[output_index] = m_consumers[input_index]; + m_waiters[output_index] = m_waiters[input_index]; + } + + output_index++; + } + + m_num_wait_objects = output_index; + } + + Result EventReactor::WaitForImpl(s32 *out_arg_waiter, const Waiter *arg_waiters, s32 num_arg_waiters) { + HAZE_ASSERT(0 < num_arg_waiters && num_arg_waiters <= svc::ArgumentHandleCountMax); + HAZE_ASSERT(m_num_wait_objects + num_arg_waiters <= svc::ArgumentHandleCountMax); + + while (true) { + /* Check if we should wait for an event. */ + R_TRY(m_result); + + /* Insert waiters from argument list. */ + for (s32 i = 0; i < num_arg_waiters; i++) { + m_waiters[i + m_num_wait_objects] = arg_waiters[i]; + } + + s32 idx; + HAZE_R_ABORT_UNLESS(waitObjects(std::addressof(idx), m_waiters, m_num_wait_objects + num_arg_waiters, svc::WaitInfinite)); + + /* If a waiter in the argument list was signaled, return it. */ + if (idx >= m_num_wait_objects) { + *out_arg_waiter = idx - m_num_wait_objects; + R_SUCCEED(); + } + + /* Otherwise, process the event as normal. */ + m_consumers[idx]->ProcessEvent(); + } + } + +} diff --git a/troposphere/haze/source/main.cpp b/troposphere/haze/source/main.cpp new file mode 100644 index 000000000..f2eaf732c --- /dev/null +++ b/troposphere/haze/source/main.cpp @@ -0,0 +1,25 @@ +/* + * 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 . + */ +#include +#include + +int main(int argc, char **argv) { + /* Run the application. */ + haze::ConsoleMainLoop::RunApplication(); + + /* Return to the loader. */ + return 0; +} diff --git a/troposphere/haze/source/ptp_object_database.cpp b/troposphere/haze/source/ptp_object_database.cpp new file mode 100644 index 000000000..7e71a307b --- /dev/null +++ b/troposphere/haze/source/ptp_object_database.cpp @@ -0,0 +1,152 @@ +/* + * 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 . + */ +#include + +namespace haze { + + void PtpObjectDatabase::Initialize(PtpObjectHeap *object_heap) { + m_object_heap = object_heap; + m_object_heap->Initialize(); + + std::construct_at(std::addressof(m_name_tree)); + std::construct_at(std::addressof(m_object_id_tree)); + + m_next_object_id = 1; + } + + void PtpObjectDatabase::Finalize() { + std::destroy_at(std::addressof(m_object_id_tree)); + std::destroy_at(std::addressof(m_name_tree)); + + m_next_object_id = 0; + + m_object_heap->Finalize(); + m_object_heap = nullptr; + } + + Result PtpObjectDatabase::CreateOrFindObject(const char *parent_name, const char *name, u32 parent_id, PtpObject **out_object) { + constexpr auto separator = "/"; + + /* Calculate length of the new name with null terminator. */ + const size_t parent_name_len = util::Strlen(parent_name); + const size_t separator_len = util::Strlen(separator); + const size_t name_len = util::Strlen(name); + const size_t terminator_len = 1; + const size_t alloc_len = sizeof(PtpObject) + parent_name_len + separator_len + name_len + terminator_len; + + /* Allocate memory for the object. */ + PtpObject * const object = m_object_heap->Allocate(alloc_len); + R_UNLESS(object != nullptr, haze::ResultOutOfMemory()); + + /* Build the object name. */ + std::strncpy(object->m_name, parent_name, parent_name_len + terminator_len); + std::strncpy(object->m_name + parent_name_len, separator, separator_len + terminator_len); + std::strncpy(object->m_name + parent_name_len + separator_len, name, name_len + terminator_len); + + { + /* Ensure we maintain a clean state on failure. */ + auto guard = SCOPE_GUARD { m_object_heap->Deallocate(object, alloc_len); }; + + /* Check if an object with this name already exists. If it does, we can just return it here. */ + if (auto * const existing = this->GetObjectByName(object->GetName()); existing != nullptr) { + *out_object = existing; + R_SUCCEED(); + } + + /* Persist the reference to the object. */ + guard.Cancel(); + } + + /* Set object properties. */ + object->m_parent_id = parent_id; + object->m_object_id = 0; + + /* Set output. */ + *out_object = object; + + /* We succeeded. */ + R_SUCCEED(); + } + + void PtpObjectDatabase::RegisterObject(PtpObject *object, u32 desired_id) { + /* If the object is already registered, skip registration. */ + if (object->GetIsRegistered()) { + return; + } + + /* Set desired object ID. */ + if (desired_id == 0) { + desired_id = m_next_object_id++; + } + + /* Insert object into trees. */ + object->Register(desired_id); + m_object_id_tree.insert(*object); + m_name_tree.insert(*object); + } + + void PtpObjectDatabase::UnregisterObject(PtpObject *object) { + /* If the object is not registered, skip trying to unregister. */ + if (!object->GetIsRegistered()) { + return; + } + + /* Remove object from trees. */ + m_object_id_tree.erase(m_object_id_tree.iterator_to(*object)); + m_name_tree.erase(m_name_tree.iterator_to(*object)); + object->Unregister(); + } + + void PtpObjectDatabase::DeleteObject(PtpObject *object) { + /* Unregister the object as required. */ + this->UnregisterObject(object); + + /* Free the object. */ + m_object_heap->Deallocate(object, sizeof(PtpObject) + std::strlen(object->GetName()) + 1); + } + + Result PtpObjectDatabase::CreateAndRegisterObjectId(const char *parent_name, const char *name, u32 parent_id, u32 *out_object_id) { + /* Try to create the object. */ + PtpObject *object; + R_TRY(this->CreateOrFindObject(parent_name, name, parent_id, std::addressof(object))); + + /* We succeeded, so register it. */ + this->RegisterObject(object); + + /* Set the output ID. */ + *out_object_id = object->GetObjectId(); + + R_SUCCEED(); + } + + PtpObject *PtpObjectDatabase::GetObjectById(u32 object_id) { + /* Find in ID mapping. */ + if (auto it = m_object_id_tree.find_key(object_id); it != m_object_id_tree.end()) { + return std::addressof(*it); + } else { + return nullptr; + } + } + + PtpObject *PtpObjectDatabase::GetObjectByName(const char *name) { + /* Find in name mapping. */ + if (auto it = m_name_tree.find_key(name); it != m_name_tree.end()) { + return std::addressof(*it); + } else { + return nullptr; + } + } +} diff --git a/troposphere/haze/source/ptp_object_heap.cpp b/troposphere/haze/source/ptp_object_heap.cpp new file mode 100644 index 000000000..2157b59c5 --- /dev/null +++ b/troposphere/haze/source/ptp_object_heap.cpp @@ -0,0 +1,69 @@ +/* + * 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 . + */ +#include + +namespace haze { + + namespace { + + /* Allow 20MiB for use by libnx. */ + static constexpr size_t LibnxReservedMemorySize = 20_MB; + + } + + void PtpObjectHeap::Initialize() { + /* If we're already initialized, skip re-initialization. */ + if (m_heap_block_size != 0) { + return; + } + + /* Estimate how much memory we can reserve. */ + size_t mem_used = 0; + HAZE_R_ABORT_UNLESS(svcGetInfo(std::addressof(mem_used), InfoType_UsedMemorySize, svc::CurrentProcess, 0)); + HAZE_ASSERT(mem_used > LibnxReservedMemorySize); + mem_used -= LibnxReservedMemorySize; + + /* Calculate size of blocks. */ + m_heap_block_size = mem_used / NumHeapBlocks; + HAZE_ASSERT(m_heap_block_size > 0); + + /* Allocate the memory. */ + for (size_t i = 0; i < NumHeapBlocks; i++) { + m_heap_blocks[i] = std::malloc(m_heap_block_size); + HAZE_ASSERT(m_heap_blocks[i] != nullptr); + } + + /* Set the address to allocate from. */ + m_next_address = m_heap_blocks[0]; + } + + void PtpObjectHeap::Finalize() { + if (m_heap_block_size == 0) { + return; + } + + /* Tear down the heap, allowing a subsequent call to Initialize() if desired. */ + for (size_t i = 0; i < NumHeapBlocks; i++) { + std::free(m_heap_blocks[i]); + m_heap_blocks[i] = nullptr; + } + + m_next_address = nullptr; + m_heap_block_size = 0; + m_current_heap_block = 0; + } + +} diff --git a/troposphere/haze/source/ptp_responder.cpp b/troposphere/haze/source/ptp_responder.cpp new file mode 100644 index 000000000..591cdd950 --- /dev/null +++ b/troposphere/haze/source/ptp_responder.cpp @@ -0,0 +1,1039 @@ +/* + * 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 . + */ +#include +#include +#include + +namespace haze { + + namespace { + + constexpr UsbCommsInterfaceInfo MtpInterfaceInfo = { + .bInterfaceClass = 0x06, + .bInterfaceSubClass = 0x01, + .bInterfaceProtocol = 0x01, + }; + + /* This is a VID:PID recognized by libmtp. */ + constexpr u16 SwitchMtpIdVendor = 0x057e; + constexpr u16 SwitchMtpIdProduct = 0x201d; + + /* Constants used for MTP GetDeviceInfo response. */ + constexpr u16 MtpStandardVersion = 100; + constexpr u32 MtpVendorExtensionId = 6; + constexpr auto MtpVendorExtensionDesc = "microsoft.com: 1.0;"; + constexpr u16 MtpFunctionalModeDefault = 0; + constexpr auto MtpDeviceManufacturer = "Nintendo"; + constexpr auto MtpDeviceModel = "Switch"; + constexpr auto MtpDeviceVersion = "1.0.0"; + constexpr auto MtpDeviceSerialNumber = "SerialNumber"; + + enum StorageId : u32 { + StorageId_SdmcFs = 0xffffffffu - 1, + }; + + constexpr PtpOperationCode SupportedOperationCodes[] = { + PtpOperationCode_GetDeviceInfo, + PtpOperationCode_OpenSession, + PtpOperationCode_CloseSession, + PtpOperationCode_GetStorageIds, + PtpOperationCode_GetStorageInfo, + PtpOperationCode_GetObjectHandles, + PtpOperationCode_GetObjectInfo, + PtpOperationCode_GetObject, + PtpOperationCode_SendObjectInfo, + PtpOperationCode_SendObject, + PtpOperationCode_DeleteObject, + PtpOperationCode_MtpGetObjectPropsSupported, + PtpOperationCode_MtpGetObjectPropDesc, + PtpOperationCode_MtpGetObjectPropValue, + PtpOperationCode_MtpSetObjectPropValue, + }; + + constexpr const PtpEventCode SupportedEventCodes[] = { /* ... */ }; + constexpr const PtpDevicePropertyCode SupportedDeviceProperties[] = { /* ... */ }; + constexpr const PtpObjectFormatCode SupportedCaptureFormats[] = { /* ... */ }; + + constexpr const PtpObjectFormatCode SupportedPlaybackFormats[] = { + PtpObjectFormatCode_Undefined, + PtpObjectFormatCode_Association, + }; + + constexpr const PtpObjectPropertyCode SupportedObjectProperties[] = { + PtpObjectPropertyCode_StorageId, + PtpObjectPropertyCode_ObjectFormat, + PtpObjectPropertyCode_ObjectSize, + PtpObjectPropertyCode_ObjectFileName, + PtpObjectPropertyCode_ParentObject, + PtpObjectPropertyCode_PersistentUniqueObjectIdentifier, + }; + + constexpr bool IsSupportedObjectPropertyCode(PtpObjectPropertyCode c) { + for (size_t i = 0; i < util::size(SupportedObjectProperties); i++) { + if (SupportedObjectProperties[i] == c) { + return true; + } + } + + return false; + } + + constexpr const StorageId SupportedStorageIds[] = { + StorageId_SdmcFs, + }; + + struct PtpStorageInfo { + PtpStorageType storage_type; + PtpFilesystemType filesystem_type; + PtpAccessCapability access_capability; + u64 max_capacity; + u64 free_space_in_bytes; + u32 free_space_in_images; + const char *storage_description; + const char *volume_label; + }; + + constexpr PtpStorageInfo DefaultStorageInfo = { + .storage_type = PtpStorageType_FixedRam, + .filesystem_type = PtpFilesystemType_GenericHierarchical, + .access_capability = PtpAccessCapability_ReadWrite, + .max_capacity = 0, + .free_space_in_bytes = 0, + .free_space_in_images = 0, + .storage_description = "", + .volume_label = "", + }; + + struct PtpObjectInfo { + StorageId storage_id; + PtpObjectFormatCode object_format; + PtpProtectionStatus protection_status; + u32 object_compressed_size; + u16 thumb_format; + u32 thumb_compressed_size; + u32 thumb_width; + u32 thumb_height; + u32 image_width; + u32 image_height; + u32 image_depth; + u32 parent_object; + PtpAssociationType association_type; + u32 association_desc; + u32 sequence_number; + const char *filename; + const char *capture_date; + const char *modification_date; + const char *keywords; + }; + + constexpr PtpObjectInfo DefaultObjectInfo = { + .storage_id = StorageId_SdmcFs, + .object_format = {}, + .protection_status = PtpProtectionStatus_NoProtection, + .object_compressed_size = 0, + .thumb_format = 0, + .thumb_compressed_size = 0, + .thumb_width = 0, + .thumb_height = 0, + .image_width = 0, + .image_height = 0, + .image_depth = 0, + .parent_object = PtpGetObjectHandles_RootParent, + .association_type = PtpAssociationType_Undefined, + .association_desc = 0, + .sequence_number = 0, + .filename = nullptr, + .capture_date = "", + .modification_date = "", + .keywords = "", + }; + + constexpr s64 DirectoryReadSize = 32; + constexpr u64 FsBufferSize = haze::UsbBulkPacketBufferSize; + + constinit char g_filename_str[PtpStringMaxLength + 1] = {}; + constinit char g_capture_date_str[PtpStringMaxLength + 1] = {}; + constinit char g_modification_date_str[PtpStringMaxLength + 1] = {}; + constinit char g_keywords_str[PtpStringMaxLength + 1] = {}; + + constinit FsDirectoryEntry g_dir_entries[DirectoryReadSize] = {}; + constinit u8 g_fs_buffer[FsBufferSize] = {}; + + alignas(4_KB) constinit u8 g_bulk_write_buffer[haze::UsbBulkPacketBufferSize] = {}; + alignas(4_KB) constinit u8 g_bulk_read_buffer[haze::UsbBulkPacketBufferSize] = {}; + + } + + Result PtpResponder::Initialize(EventReactor *reactor, PtpObjectHeap *object_heap) { + m_object_heap = object_heap; + + /* Configure fs proxy. */ + m_fs.Initialize(reactor, fsdevGetDeviceFileSystem("sdmc")); + + R_RETURN(m_usb_server.Initialize(std::addressof(MtpInterfaceInfo), SwitchMtpIdVendor, SwitchMtpIdProduct, reactor)); + } + + void PtpResponder::Finalize() { + m_usb_server.Finalize(); + m_fs.Finalize(); + } + + Result PtpResponder::LoopProcess() { + while (true) { + /* Try to handle a request. */ + R_TRY_CATCH(this->HandleRequest()) { + R_CATCH(haze::ResultStopRequested, haze::ResultFocusLost) { + /* If we encountered a stop condition, we're done.*/ + R_THROW(R_CURRENT_RESULT); + } + R_CATCH_ALL() { + /* On other failures, try to handle another request. */ + continue; + } + } R_END_TRY_CATCH; + + /* Otherwise, handle the next request. */ + /* ... */ + } + } + + Result PtpResponder::HandleRequest() { + ON_RESULT_FAILURE { + /* For general failure modes, the failure is unrecoverable. Close the session. */ + this->ForceCloseSession(); + }; + + R_TRY_CATCH(this->HandleRequestImpl()) { + R_CATCH(haze::ResultUnknownRequestType) { + R_TRY(this->WriteResponse(PtpResponseCode_GeneralError)); + } + R_CATCH(haze::ResultSessionNotOpen) { + R_TRY(this->WriteResponse(PtpResponseCode_SessionNotOpen)); + } + R_CATCH(haze::ResultOperationNotSupported) { + R_TRY(this->WriteResponse(PtpResponseCode_OperationNotSupported)); + } + R_CATCH(haze::ResultInvalidStorageId) { + R_TRY(this->WriteResponse(PtpResponseCode_InvalidStorageId)); + } + R_CATCH(haze::ResultInvalidObjectId) { + R_TRY(this->WriteResponse(PtpResponseCode_InvalidObjectHandle)); + } + R_CATCH(haze::ResultUnknownPropertyCode) { + R_TRY(this->WriteResponse(PtpResponseCode_MtpObjectPropNotSupported)); + } + R_CATCH(haze::ResultInvalidPropertyValue) { + R_TRY(this->WriteResponse(PtpResponseCode_MtpInvalidObjectPropValue)); + } + R_CATCH_MODULE(fs) { + /* Errors from fs are typically recoverable. */ + R_TRY(this->WriteResponse(PtpResponseCode_GeneralError)); + } + } R_END_TRY_CATCH; + + R_SUCCEED(); + } + + Result PtpResponder::HandleRequestImpl() { + PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); + R_TRY(dp.Read(std::addressof(m_request_header))); + + switch (m_request_header.type) { + case PtpUsbBulkContainerType_Command: R_RETURN(this->HandleCommandRequest(dp)); + default: R_THROW(haze::ResultUnknownRequestType()); + } + } + + Result PtpResponder::HandleCommandRequest(PtpDataParser &dp) { + if (!m_session_open && m_request_header.code != PtpOperationCode_OpenSession && m_request_header.code != PtpOperationCode_GetDeviceInfo) { + R_THROW(haze::ResultSessionNotOpen()); + } + + switch (m_request_header.code) { + case PtpOperationCode_GetDeviceInfo: R_RETURN(this->GetDeviceInfo(dp)); break; + case PtpOperationCode_OpenSession: R_RETURN(this->OpenSession(dp)); break; + case PtpOperationCode_CloseSession: R_RETURN(this->CloseSession(dp)); break; + case PtpOperationCode_GetStorageIds: R_RETURN(this->GetStorageIds(dp)); break; + case PtpOperationCode_GetStorageInfo: R_RETURN(this->GetStorageInfo(dp)); break; + case PtpOperationCode_GetObjectHandles: R_RETURN(this->GetObjectHandles(dp)); break; + case PtpOperationCode_GetObjectInfo: R_RETURN(this->GetObjectInfo(dp)); break; + case PtpOperationCode_GetObject: R_RETURN(this->GetObject(dp)); break; + case PtpOperationCode_SendObjectInfo: R_RETURN(this->SendObjectInfo(dp)); break; + case PtpOperationCode_SendObject: R_RETURN(this->SendObject(dp)); break; + case PtpOperationCode_DeleteObject: R_RETURN(this->DeleteObject(dp)); break; + case PtpOperationCode_MtpGetObjectPropsSupported: R_RETURN(this->GetObjectPropsSupported(dp)); break; + case PtpOperationCode_MtpGetObjectPropDesc: R_RETURN(this->GetObjectPropDesc(dp)); break; + case PtpOperationCode_MtpGetObjectPropValue: R_RETURN(this->GetObjectPropValue(dp)); break; + case PtpOperationCode_MtpSetObjectPropValue: R_RETURN(this->SetObjectPropValue(dp)); break; + default: R_THROW(haze::ResultOperationNotSupported()); + } + } + + void PtpResponder::ForceCloseSession() { + if (m_session_open) { + m_session_open = false; + m_object_database.Finalize(); + } + } + + template + Result PtpResponder::WriteResponse(PtpResponseCode code, Data &&data) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + R_TRY(db.AddResponseHeader(m_request_header, code, sizeof(Data))); + R_TRY(db.Add(data)); + R_RETURN(db.Commit()); + } + + Result PtpResponder::WriteResponse(PtpResponseCode code) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + R_TRY(db.AddResponseHeader(m_request_header, code, 0)); + R_RETURN(db.Commit()); + } + + Result PtpResponder::GetDeviceInfo(PtpDataParser &dp) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Write the device info data. */ + R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { + R_TRY(db.Add(MtpStandardVersion)); + R_TRY(db.Add(MtpVendorExtensionId)); + R_TRY(db.Add(MtpStandardVersion)); + R_TRY(db.AddString(MtpVendorExtensionDesc)); + R_TRY(db.Add(MtpFunctionalModeDefault)); + R_TRY(db.AddArray(SupportedOperationCodes, util::size(SupportedOperationCodes))); + R_TRY(db.AddArray(SupportedEventCodes, util::size(SupportedEventCodes))); + R_TRY(db.AddArray(SupportedDeviceProperties, util::size(SupportedDeviceProperties))); + R_TRY(db.AddArray(SupportedCaptureFormats, util::size(SupportedCaptureFormats))); + R_TRY(db.AddArray(SupportedPlaybackFormats, util::size(SupportedPlaybackFormats))); + R_TRY(db.AddString(MtpDeviceManufacturer)); + R_TRY(db.AddString(MtpDeviceModel)); + R_TRY(db.AddString(MtpDeviceVersion)); + R_TRY(db.AddString(MtpDeviceSerialNumber)); + + R_SUCCEED(); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::OpenSession(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + /* Close, if we're already open. */ + this->ForceCloseSession(); + + /* Initialize the database. */ + m_session_open = true; + m_object_database.Initialize(m_object_heap); + + /* Create the root storages. */ + PtpObject *object; + R_TRY(m_object_database.CreateOrFindObject("", "", PtpGetObjectHandles_RootParent, std::addressof(object))); + + /* Register the root storages. */ + m_object_database.RegisterObject(object, StorageId_SdmcFs); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::CloseSession(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + this->ForceCloseSession(); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetStorageIds(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Write the storage ID array. */ + R_TRY(db.WriteVariableLengthData(m_request_header, [&] { + R_RETURN(db.AddArray(SupportedStorageIds, util::size(SupportedStorageIds))); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetStorageInfo(PtpDataParser &dp) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + PtpStorageInfo storage_info(DefaultStorageInfo); + + /* Get the storage ID the client requested information for. */ + u32 storage_id; + R_TRY(dp.Read(std::addressof(storage_id))); + R_TRY(dp.Finalize()); + + /* Get the info from fs. */ + switch (storage_id) { + case StorageId_SdmcFs: + { + s64 total_space, free_space; + R_TRY(m_fs.GetTotalSpace("/", std::addressof(total_space))); + R_TRY(m_fs.GetFreeSpace("/", std::addressof(free_space))); + + storage_info.max_capacity = total_space; + storage_info.free_space_in_bytes = free_space; + storage_info.free_space_in_images = 0; + storage_info.storage_description = "SD Card"; + } + break; + default: + R_THROW(haze::ResultInvalidStorageId()); + } + + /* Write the storage info data. */ + R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { + R_TRY(db.Add(storage_info.storage_type)); + R_TRY(db.Add(storage_info.filesystem_type)); + R_TRY(db.Add(storage_info.access_capability)); + R_TRY(db.Add(storage_info.max_capacity)); + R_TRY(db.Add(storage_info.free_space_in_bytes)); + R_TRY(db.Add(storage_info.free_space_in_images)); + R_TRY(db.AddString(storage_info.storage_description)); + R_TRY(db.AddString(storage_info.volume_label)); + + R_SUCCEED(); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectHandles(PtpDataParser &dp) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Get the object ID the client requested enumeration for. */ + u32 storage_id, object_format_code, association_object_handle; + R_TRY(dp.Read(std::addressof(storage_id))); + R_TRY(dp.Read(std::addressof(object_format_code))); + R_TRY(dp.Read(std::addressof(association_object_handle))); + R_TRY(dp.Finalize()); + + /* Handle top-level requests. */ + if (storage_id == PtpGetObjectHandles_AllStorage) { + storage_id = StorageId_SdmcFs; + } + + /* Rewrite requests for enumerating storage directories. */ + if (association_object_handle == PtpGetObjectHandles_RootParent) { + association_object_handle = storage_id; + } + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(association_object_handle); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Try to read the object as a directory. */ + FsDir dir; + R_TRY(m_fs.OpenDirectory(obj->GetName(), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, std::addressof(dir))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.CloseDirectory(std::addressof(dir)); }; + + /* Count how many entries are in the directory. */ + s64 entry_count = 0; + R_TRY(m_fs.GetDirectoryEntryCount(std::addressof(dir), std::addressof(entry_count))); + + /* Begin writing. */ + R_TRY(db.AddDataHeader(m_request_header, sizeof(u32) + (entry_count * sizeof(u32)))); + R_TRY(db.Add(static_cast(entry_count))); + + /* Enumerate the directory, writing results to the data builder as we progress. */ + /* TODO: How should we handle the directory contents changing during enumeration? */ + /* Is this even feasible to handle? */ + while (true) { + /* Get the next batch. */ + s64 read_count = 0; + R_TRY(m_fs.ReadDirectory(std::addressof(dir), std::addressof(read_count), DirectoryReadSize, g_dir_entries)); + + /* Write to output. */ + for (s64 i = 0; i < read_count; i++) { + u32 handle; + R_TRY(m_object_database.CreateAndRegisterObjectId(obj->GetName(), g_dir_entries[i].name, obj->GetObjectId(), std::addressof(handle))); + R_TRY(db.Add(handle)); + } + + /* If we read fewer than the batch size, we're done. */ + if (read_count < DirectoryReadSize) { + break; + } + } + + /* Flush the data response. */ + R_TRY(db.Commit()); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectInfo(PtpDataParser &dp) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Get the object ID the client requested info for. */ + u32 object_id; + R_TRY(dp.Read(std::addressof(object_id))); + R_TRY(dp.Finalize()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Build info about the object. */ + PtpObjectInfo object_info(DefaultObjectInfo); + + if (object_id == StorageId_SdmcFs) { + /* The SD Card directory has some special properties. */ + object_info.object_format = PtpObjectFormatCode_Association; + object_info.association_type = PtpAssociationType_GenericFolder; + object_info.filename = "SD Card"; + } else { + /* Figure out what type of object this is. */ + FsDirEntryType entry_type; + R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); + + /* Get the size, if we are requesting info about a file. */ + s64 size = 0; + if (entry_type == FsDirEntryType_File) { + FsFile file; + R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; + + R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(size))); + } + + object_info.filename = std::strrchr(obj->GetName(), '/') + 1; + object_info.object_compressed_size = size; + object_info.parent_object = obj->GetParentId(); + + if (entry_type == FsDirEntryType_Dir) { + object_info.object_format = PtpObjectFormatCode_Association; + object_info.association_type = PtpAssociationType_GenericFolder; + } else { + object_info.object_format = PtpObjectFormatCode_Undefined; + object_info.association_type = PtpAssociationType_Undefined; + } + } + + /* Write the object info data. */ + R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { + R_TRY(db.Add(object_info.storage_id)); + R_TRY(db.Add(object_info.object_format)); + R_TRY(db.Add(object_info.protection_status)); + R_TRY(db.Add(object_info.object_compressed_size)); + R_TRY(db.Add(object_info.thumb_format)); + R_TRY(db.Add(object_info.thumb_compressed_size)); + R_TRY(db.Add(object_info.thumb_width)); + R_TRY(db.Add(object_info.thumb_height)); + R_TRY(db.Add(object_info.image_width)); + R_TRY(db.Add(object_info.image_height)); + R_TRY(db.Add(object_info.image_depth)); + R_TRY(db.Add(object_info.parent_object)); + R_TRY(db.Add(object_info.association_type)); + R_TRY(db.Add(object_info.association_desc)); + R_TRY(db.Add(object_info.sequence_number)); + R_TRY(db.AddString(object_info.filename)); + R_TRY(db.AddString(object_info.capture_date)); + R_TRY(db.AddString(object_info.modification_date)); + R_TRY(db.AddString(object_info.keywords)); + + R_SUCCEED(); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObject(PtpDataParser &dp) { + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Get the object ID the client requested. */ + u32 object_id; + R_TRY(dp.Read(std::addressof(object_id))); + R_TRY(dp.Finalize()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Lock the object as a file. */ + FsFile file; + R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; + + /* Get the file's size. */ + s64 size = 0; + R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(size))); + + /* Send the header and file size. */ + R_TRY(db.AddDataHeader(m_request_header, size)); + + /* Begin reading the file, writing data to the builder as we progress. */ + s64 offset = 0; + while (true) { + /* Get the next batch. */ + u64 bytes_read; + R_TRY(m_fs.ReadFile(std::addressof(file), offset, g_fs_buffer, FsBufferSize, FsReadOption_None, std::addressof(bytes_read))); + + offset += bytes_read; + + /* Write to output. */ + R_TRY(db.AddBuffer(g_fs_buffer, bytes_read)); + + /* If we read fewer bytes than the batch size, we're done. */ + if (bytes_read < FsBufferSize) { + break; + } + } + + /* Flush the data response. */ + R_TRY(db.Commit()); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::SendObjectInfo(PtpDataParser &rdp) { + /* Get the storage ID and parent object and flush the request packet. */ + u32 storage_id, parent_object; + R_TRY(rdp.Read(std::addressof(storage_id))); + R_TRY(rdp.Read(std::addressof(parent_object))); + R_TRY(rdp.Finalize()); + + PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); + PtpObjectInfo info(DefaultObjectInfo); + + /* Ensure we have a data header. */ + PtpUsbBulkContainer data_header; + R_TRY(dp.Read(std::addressof(data_header))); + R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); + R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); + R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); + + /* Read in the object info. */ + R_TRY(dp.Read(std::addressof(info.storage_id))); + R_TRY(dp.Read(std::addressof(info.object_format))); + R_TRY(dp.Read(std::addressof(info.protection_status))); + R_TRY(dp.Read(std::addressof(info.object_compressed_size))); + R_TRY(dp.Read(std::addressof(info.thumb_format))); + R_TRY(dp.Read(std::addressof(info.thumb_compressed_size))); + R_TRY(dp.Read(std::addressof(info.thumb_width))); + R_TRY(dp.Read(std::addressof(info.thumb_height))); + R_TRY(dp.Read(std::addressof(info.image_width))); + R_TRY(dp.Read(std::addressof(info.image_height))); + R_TRY(dp.Read(std::addressof(info.image_depth))); + R_TRY(dp.Read(std::addressof(info.parent_object))); + R_TRY(dp.Read(std::addressof(info.association_type))); + R_TRY(dp.Read(std::addressof(info.association_desc))); + R_TRY(dp.Read(std::addressof(info.sequence_number))); + R_TRY(dp.ReadString(g_filename_str)); + R_TRY(dp.ReadString(g_capture_date_str)); + R_TRY(dp.ReadString(g_modification_date_str)); + R_TRY(dp.ReadString(g_keywords_str)); + R_TRY(dp.Finalize()); + + /* Rewrite requests for creating in storage directories. */ + if (parent_object == PtpGetObjectHandles_RootParent) { + parent_object = storage_id; + } + + /* Check if we know about the parent object. If we don't, it's an error. */ + auto * const parentobj = m_object_database.GetObjectById(parent_object); + R_UNLESS(parentobj != nullptr, haze::ResultInvalidObjectId()); + + /* Make a new object with the intended name. */ + PtpNewObjectInfo new_object_info; + new_object_info.storage_id = StorageId_SdmcFs; + new_object_info.parent_object_id = parent_object == storage_id ? 0 : parent_object; + + /* Create the object in the database. */ + PtpObject *obj; + R_TRY(m_object_database.CreateOrFindObject(parentobj->GetName(), g_filename_str, parentobj->GetObjectId(), std::addressof(obj))); + + /* Ensure we maintain a clean state on failure. */ + ON_RESULT_FAILURE { m_object_database.DeleteObject(obj); }; + + /* Register the object with a new ID. */ + m_object_database.RegisterObject(obj); + new_object_info.object_id = obj->GetObjectId(); + + /* Create the object on the filesystem. */ + if (info.object_format == PtpObjectFormatCode_Association) { + R_TRY(m_fs.CreateDirectory(obj->GetName())); + m_send_object_id = 0; + } else { + R_TRY(m_fs.CreateFile(obj->GetName(), 0, 0)); + m_send_object_id = new_object_info.object_id; + } + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok, new_object_info)); + } + + Result PtpResponder::SendObject(PtpDataParser &rdp) { + /* Reset SendObject object ID on exit. */ + ON_SCOPE_EXIT { m_send_object_id = 0; }; + + R_TRY(rdp.Finalize()); + + PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); + + /* Ensure we have a data header. */ + PtpUsbBulkContainer data_header; + R_TRY(dp.Read(std::addressof(data_header))); + R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); + R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); + R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(m_send_object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Lock the object as a file. */ + FsFile file; + R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Write | FsOpenMode_Append, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; + + /* Truncate the file after locking for write. */ + s64 offset = 0; + R_TRY(m_fs.SetFileSize(std::addressof(file), 0)); + + /* Expand to the needed size. */ + R_TRY(m_fs.SetFileSize(std::addressof(file), data_header.length)); + + /* Begin writing to the filesystem. */ + while (true) { + /* Read as many bytes as we can. */ + u32 bytes_received; + const Result read_res = dp.ReadBuffer(g_fs_buffer, FsBufferSize, std::addressof(bytes_received)); + + /* Write to the file. */ + R_TRY(m_fs.WriteFile(std::addressof(file), offset, g_fs_buffer, bytes_received, 0)); + + offset += bytes_received; + + /* If we received fewer bytes than the batch size, we're done. */ + if (haze::ResultEndOfTransmission::Includes(read_res)) { + break; + } + + R_TRY(read_res); + } + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::DeleteObject(PtpDataParser &dp) { + /* Get the object ID and flush the request packet. */ + u32 object_id; + R_TRY(dp.Read(std::addressof(object_id))); + R_TRY(dp.Finalize()); + + /* Disallow deleting the storage root. */ + R_UNLESS(object_id != StorageId_SdmcFs, haze::ResultInvalidObjectId()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Figure out what type of object this is. */ + FsDirEntryType entry_type; + R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); + + /* Remove the object from the filesystem. */ + if (entry_type == FsDirEntryType_Dir) { + R_TRY(m_fs.DeleteDirectoryRecursively(obj->GetName())); + } else { + R_TRY(m_fs.DeleteFile(obj->GetName())); + } + + /* Remove the object from the database. */ + m_object_database.DeleteObject(obj); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectPropsSupported(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Write information about all object properties we can support. */ + R_TRY(db.WriteVariableLengthData(m_request_header, [&] { + R_RETURN(db.AddArray(SupportedObjectProperties, util::size(SupportedObjectProperties))); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectPropDesc(PtpDataParser &dp) { + PtpObjectPropertyCode property_code; + u16 object_format; + + R_TRY(dp.Read(std::addressof(property_code))); + R_TRY(dp.Read(std::addressof(object_format))); + R_TRY(dp.Finalize()); + + /* Ensure we have a valid property code before continuing. */ + R_UNLESS(IsSupportedObjectPropertyCode(property_code), haze::ResultUnknownPropertyCode()); + + /* Begin writing information about the property code. */ + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + R_TRY(db.WriteVariableLengthData(m_request_header, [&] { + R_TRY(db.Add(property_code)); + + /* Each property code corresponds to a different pattern, which contains the data type, */ + /* whether the property can be set for an object, and the default value of the property. */ + switch (property_code) { + case PtpObjectPropertyCode_PersistentUniqueObjectIdentifier: + { + R_TRY(db.Add(PtpDataTypeCode_U128)); + R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); + R_TRY(db.Add(0)); + } + case PtpObjectPropertyCode_ObjectSize: + { + R_TRY(db.Add(PtpDataTypeCode_U64)); + R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); + R_TRY(db.Add(0)); + } + break; + case PtpObjectPropertyCode_StorageId: + case PtpObjectPropertyCode_ParentObject: + { + R_TRY(db.Add(PtpDataTypeCode_U32)); + R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); + R_TRY(db.Add(StorageId_SdmcFs)); + } + break; + case PtpObjectPropertyCode_ObjectFormat: + { + R_TRY(db.Add(PtpDataTypeCode_U16)); + R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); + R_TRY(db.Add(PtpObjectFormatCode_Undefined)); + } + break; + case PtpObjectPropertyCode_ObjectFileName: + { + R_TRY(db.Add(PtpDataTypeCode_String)); + R_TRY(db.Add(PtpPropertyGetSetFlag_GetSet)); + R_TRY(db.AddString("")); + } + break; + HAZE_UNREACHABLE_DEFAULT_CASE(); + } + + /* Group code is a required part of the response, but doesn't seem to be used for anything. */ + R_TRY(db.Add(PtpPropertyGroupCode_Default)); + + /* We don't use the form flag. */ + R_TRY(db.Add(PtpPropertyFormFlag_None)); + + R_SUCCEED(); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectPropValue(PtpDataParser &dp) { + u32 object_id; + PtpObjectPropertyCode property_code; + + R_TRY(dp.Read(std::addressof(object_id))); + R_TRY(dp.Read(std::addressof(property_code))); + R_TRY(dp.Finalize()); + + /* Disallow renaming the storage root. */ + R_UNLESS(object_id != StorageId_SdmcFs, haze::ResultInvalidObjectId()); + + /* Ensure we have a valid property code before continuing. */ + R_UNLESS(IsSupportedObjectPropertyCode(property_code), haze::ResultUnknownPropertyCode()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* Define helper for getting the object type. */ + const auto GetObjectType = [&] (FsDirEntryType *out_entry_type) { + R_RETURN(m_fs.GetEntryType(obj->GetName(), out_entry_type)); + }; + + /* Define helper for getting the object size. */ + const auto GetObjectSize = [&] (s64 *out_size) { + *out_size = 0; + + /* Check if this is a directory. */ + FsDirEntryType entry_type; + R_TRY(GetObjectType(std::addressof(entry_type))); + + /* If it is, we're done. */ + R_SUCCEED_IF(entry_type == FsDirEntryType_Dir); + + /* Otherwise, open as a file. */ + FsFile file; + R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; + + R_RETURN(m_fs.GetFileSize(std::addressof(file), out_size)); + }; + + /* Begin writing the requested object property. */ + PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); + + R_TRY(db.WriteVariableLengthData(m_request_header, [&] { + switch (property_code) { + case PtpObjectPropertyCode_PersistentUniqueObjectIdentifier: + { + R_TRY(db.Add(object_id)); + } + break; + case PtpObjectPropertyCode_ObjectSize: + { + s64 size; + R_TRY(GetObjectSize(std::addressof(size))); + R_TRY(db.Add(size)); + } + break; + case PtpObjectPropertyCode_StorageId: + { + R_TRY(db.Add(StorageId_SdmcFs)); + } + break; + case PtpObjectPropertyCode_ParentObject: + { + R_TRY(db.Add(obj->GetParentId())); + } + break; + case PtpObjectPropertyCode_ObjectFormat: + { + FsDirEntryType entry_type; + R_TRY(GetObjectType(std::addressof(entry_type))); + R_TRY(db.Add(entry_type == FsDirEntryType_File ? PtpObjectFormatCode_Undefined : PtpObjectFormatCode_Association)); + } + break; + case PtpObjectPropertyCode_ObjectFileName: + { + R_TRY(db.AddString(std::strrchr(obj->GetName(), '/') + 1)); + } + break; + HAZE_UNREACHABLE_DEFAULT_CASE(); + } + + R_SUCCEED(); + })); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::SetObjectPropValue(PtpDataParser &rdp) { + u32 object_id; + PtpObjectPropertyCode property_code; + + R_TRY(rdp.Read(std::addressof(object_id))); + R_TRY(rdp.Read(std::addressof(property_code))); + R_TRY(rdp.Finalize()); + + PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); + + /* Ensure we have a data header. */ + PtpUsbBulkContainer data_header; + R_TRY(dp.Read(std::addressof(data_header))); + R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); + R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); + R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); + + /* Ensure we have a valid property code before continuing. */ + R_UNLESS(property_code == PtpObjectPropertyCode_ObjectFileName, haze::ResultUnknownPropertyCode()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto * const obj = m_object_database.GetObjectById(object_id); + R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); + + /* We are reading a file name. */ + R_TRY(dp.ReadString(g_filename_str)); + R_TRY(dp.Finalize()); + + /* Ensure we can actually process the new name. */ + const bool is_empty = g_filename_str[0] == '\x00'; + const bool contains_slashes = std::strchr(g_filename_str, '/') != nullptr; + R_UNLESS(!is_empty && !contains_slashes, haze::ResultInvalidPropertyValue()); + + /* Add a new object in the database with the new name. */ + PtpObject *newobj; + { + /* Find the last path separator in the existing object name. */ + char *pathsep = std::strrchr(obj->m_name, '/'); + HAZE_ASSERT(pathsep != nullptr); + + /* Temporarily mark the path separator as null to facilitate processing. */ + *pathsep = '\x00'; + ON_SCOPE_EXIT { *pathsep = '/'; }; + + R_TRY(m_object_database.CreateOrFindObject(obj->GetName(), g_filename_str, obj->GetParentId(), std::addressof(newobj))); + } + + { + /* Ensure we maintain a clean state on failure. */ + ON_RESULT_FAILURE { + if (!newobj->GetIsRegistered()) { + /* Only delete if the object was not registered. */ + /* Otherwise, we would remove an object that still exists. */ + m_object_database.DeleteObject(newobj); + } + }; + + /* Get the old object type. */ + FsDirEntryType entry_type; + R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); + + /* Attempt to rename the object on the filesystem. */ + if (entry_type == FsDirEntryType_Dir) { + R_TRY(m_fs.RenameDirectory(obj->GetName(), newobj->GetName())); + } else { + R_TRY(m_fs.RenameFile(obj->GetName(), newobj->GetName())); + } + } + + /* Unregister and free the old object. */ + m_object_database.DeleteObject(obj); + + /* Register the new object. */ + m_object_database.RegisterObject(newobj, object_id); + + /* Write the success response. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } +} diff --git a/troposphere/haze/source/usb_session.cpp b/troposphere/haze/source/usb_session.cpp new file mode 100644 index 000000000..e6e98b041 --- /dev/null +++ b/troposphere/haze/source/usb_session.cpp @@ -0,0 +1,268 @@ +/* + * 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 . + */ +#include + +namespace haze { + + namespace { + + constexpr const u32 DefaultInterfaceNumber = 0; + + } + + Result UsbSession::Initialize1x(const UsbCommsInterfaceInfo *info) { + struct usb_interface_descriptor interface_descriptor = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = DefaultInterfaceNumber, + .bInterfaceClass = info->bInterfaceClass, + .bInterfaceSubClass = info->bInterfaceSubClass, + .bInterfaceProtocol = info->bInterfaceProtocol, + }; + + 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, + .wMaxPacketSize = PtpUsbBulkHighSpeedMaxPacketLength, + }; + + 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, + .wMaxPacketSize = PtpUsbBulkHighSpeedMaxPacketLength, + }; + + struct usb_endpoint_descriptor endpoint_descriptor_interrupt = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_IN, + .bmAttributes = USB_TRANSFER_TYPE_INTERRUPT, + .wMaxPacketSize = 0x18, + .bInterval = 0x4, + }; + + /* Set up interface. */ + R_TRY(usbDsGetDsInterface(std::addressof(m_interface), std::addressof(interface_descriptor), "usb")); + + /* Set up endpoints. */ + R_TRY(usbDsInterface_GetDsEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Write]), std::addressof(endpoint_descriptor_in))); + R_TRY(usbDsInterface_GetDsEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Read]), std::addressof(endpoint_descriptor_out))); + R_TRY(usbDsInterface_GetDsEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Interrupt]), std::addressof(endpoint_descriptor_interrupt))); + + R_RETURN(usbDsInterface_EnableInterface(m_interface)); + } + + Result UsbSession::Initialize5x(const UsbCommsInterfaceInfo *info) { + struct usb_interface_descriptor interface_descriptor = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = DefaultInterfaceNumber, + .bNumEndpoints = 3, + .bInterfaceClass = info->bInterfaceClass, + .bInterfaceSubClass = info->bInterfaceSubClass, + .bInterfaceProtocol = info->bInterfaceProtocol, + }; + + 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, + .wMaxPacketSize = PtpUsbBulkHighSpeedMaxPacketLength, + }; + + 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, + .wMaxPacketSize = PtpUsbBulkHighSpeedMaxPacketLength, + }; + + struct usb_endpoint_descriptor endpoint_descriptor_interrupt = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_IN, + .bmAttributes = USB_TRANSFER_TYPE_INTERRUPT, + .wMaxPacketSize = 0x18, + .bInterval = 0x6, + }; + + 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, + }; + + struct usb_ss_endpoint_companion_descriptor endpoint_companion_interrupt = { + .bLength = sizeof(struct usb_ss_endpoint_companion_descriptor), + .bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION, + .bMaxBurst = 0x00, + .bmAttributes = 0x00, + .wBytesPerInterval = 0x00, + }; + + R_TRY(usbDsRegisterInterface(std::addressof(m_interface))); + + u8 iInterface; + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iInterface), "MTP")); + + interface_descriptor.bInterfaceNumber = m_interface->interface_index; + interface_descriptor.iInterface = iInterface; + endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1; + endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1; + endpoint_descriptor_interrupt.bEndpointAddress += interface_descriptor.bInterfaceNumber + 2; + + /* High speed config. */ + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, std::addressof(interface_descriptor), USB_DT_INTERFACE_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, std::addressof(endpoint_descriptor_in), USB_DT_ENDPOINT_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, std::addressof(endpoint_descriptor_out), USB_DT_ENDPOINT_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, std::addressof(endpoint_descriptor_interrupt), USB_DT_ENDPOINT_SIZE)); + + /* Super speed config. */ + endpoint_descriptor_in.wMaxPacketSize = PtpUsbBulkSuperSpeedMaxPacketLength; + endpoint_descriptor_out.wMaxPacketSize = PtpUsbBulkSuperSpeedMaxPacketLength; + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(interface_descriptor), USB_DT_INTERFACE_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_descriptor_in), USB_DT_ENDPOINT_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_companion), USB_DT_SS_ENDPOINT_COMPANION_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_descriptor_out), USB_DT_ENDPOINT_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_companion), USB_DT_SS_ENDPOINT_COMPANION_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_descriptor_interrupt), USB_DT_ENDPOINT_SIZE)); + R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, std::addressof(endpoint_companion_interrupt), USB_DT_SS_ENDPOINT_COMPANION_SIZE)); + + /* Set up endpoints. */ + R_TRY(usbDsInterface_RegisterEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Write]), endpoint_descriptor_in.bEndpointAddress)); + R_TRY(usbDsInterface_RegisterEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Read]), endpoint_descriptor_out.bEndpointAddress)); + R_TRY(usbDsInterface_RegisterEndpoint(m_interface, std::addressof(m_endpoints[UsbSessionEndpoint_Interrupt]), endpoint_descriptor_interrupt.bEndpointAddress)); + + R_RETURN(usbDsInterface_EnableInterface(m_interface)); + } + + Result UsbSession::Initialize(const UsbCommsInterfaceInfo *info, u16 id_vendor, u16 id_product) { + R_TRY(usbDsInitialize()); + + if (hosversionAtLeast(5, 0, 0)) { + /* Report language as US English. */ + static const u16 supported_langs[1] = { 0x0409 }; + R_TRY(usbDsAddUsbLanguageStringDescriptor(nullptr, supported_langs, util::size(supported_langs))); + + /* Get the device serial number. */ + SetSysSerialNumber serial; + R_TRY(setsysInitialize()); + R_TRY(setsysGetSerialNumber(std::addressof(serial))); + setsysExit(); + + /* Report strings. */ + u8 iManufacturer, iProduct, iSerialNumber; + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iManufacturer), "Nintendo")); + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iProduct), "Nintendo Switch")); + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iSerialNumber), serial.number)); + + /* Send device descriptors */ + struct usb_device_descriptor device_descriptor = { + .bLength = USB_DT_DEVICE_SIZE, + .bDescriptorType = USB_DT_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 0x40, + .idVendor = id_vendor, + .idProduct = id_product, + .bcdDevice = 0x0100, + .iManufacturer = iManufacturer, + .iProduct = iProduct, + .iSerialNumber = iSerialNumber, + .bNumConfigurations = 0x01 + }; + R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, std::addressof(device_descriptor))); + + device_descriptor.bcdUSB = 0x0300; + R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, std::addressof(device_descriptor))); + + /* Binary Object Store */ + 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, /* .bmAttributes */ + + /* USB 3.0 */ + 0x0a, /* .bLength */ + USB_DT_DEVICE_CAPABILITY, /* .bDescriptorType */ + 0x03, /* .bDevCapabilityType */ + 0x00, /* .bmAttributes */ + 0x0c, 0x00, /* .wSpeedSupported */ + 0x03, /* .bFunctionalitySupport */ + 0x00, /* .bU1DevExitLat */ + 0x00, 0x00 /* .bU2DevExitLat */ + }; + R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos))); + } + + if (hosversionAtLeast(5, 0, 0)) { + R_TRY(this->Initialize5x(info)); + R_TRY(usbDsEnable()); + } else { + R_TRY(this->Initialize1x(info)); + } + + R_SUCCEED(); + } + + void UsbSession::Finalize() { + usbDsExit(); + } + + bool UsbSession::GetConfigured() const { + UsbState usb_state; + + HAZE_R_ABORT_UNLESS(usbDsGetState(std::addressof(usb_state))); + + return usb_state == UsbState_Configured; + } + + Event *UsbSession::GetCompletionEvent(UsbSessionEndpoint ep) const { + return std::addressof(m_endpoints[ep]->CompletionEvent); + } + + Result UsbSession::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) { + R_RETURN(usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id)); + } + + Result UsbSession::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_transferred_size) { + 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, nullptr, out_transferred_size)); + + R_SUCCEED(); + } + +}