From 6a46d3a3e9215ac44183582c1fb40b2e19bb8a88 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 15 Apr 2023 15:37:14 -0400 Subject: [PATCH] troposphere: add haze MTP server --- troposphere/Makefile | 2 +- troposphere/haze/Makefile | 226 ++++++ troposphere/haze/icon.jpg | Bin 0 -> 7823 bytes troposphere/haze/icon.svg | 2 + troposphere/haze/include/haze.hpp | 26 + troposphere/haze/include/haze/assert.hpp | 29 + .../haze/include/haze/async_usb_server.hpp | 47 ++ troposphere/haze/include/haze/common.hpp | 42 ++ .../haze/include/haze/console_main_loop.hpp | 150 ++++ .../haze/include/haze/event_reactor.hpp | 57 ++ .../haze/include/haze/filesystem_proxy.hpp | 123 +++ troposphere/haze/include/haze/ptp.hpp | 267 +++++++ .../haze/include/haze/ptp_data_builder.hpp | 166 +++++ .../haze/include/haze/ptp_data_parser.hpp | 112 +++ .../haze/include/haze/ptp_object_database.hpp | 99 +++ .../haze/include/haze/ptp_object_heap.hpp | 128 ++++ .../haze/include/haze/ptp_responder.hpp | 71 ++ troposphere/haze/include/haze/results.hpp | 39 + troposphere/haze/include/haze/usb_session.hpp | 51 ++ troposphere/haze/source/async_usb_server.cpp | 62 ++ troposphere/haze/source/event_reactor.cpp | 76 ++ troposphere/haze/source/main.cpp | 42 ++ .../haze/source/ptp_object_database.cpp | 103 +++ troposphere/haze/source/ptp_object_heap.cpp | 66 ++ troposphere/haze/source/ptp_responder.cpp | 698 ++++++++++++++++++ troposphere/haze/source/usb_session.cpp | 253 +++++++ 26 files changed, 2936 insertions(+), 1 deletion(-) create mode 100644 troposphere/haze/Makefile create mode 100644 troposphere/haze/icon.jpg create mode 100644 troposphere/haze/icon.svg create mode 100644 troposphere/haze/include/haze.hpp create mode 100644 troposphere/haze/include/haze/assert.hpp create mode 100644 troposphere/haze/include/haze/async_usb_server.hpp create mode 100644 troposphere/haze/include/haze/common.hpp create mode 100644 troposphere/haze/include/haze/console_main_loop.hpp create mode 100644 troposphere/haze/include/haze/event_reactor.hpp create mode 100644 troposphere/haze/include/haze/filesystem_proxy.hpp create mode 100644 troposphere/haze/include/haze/ptp.hpp create mode 100644 troposphere/haze/include/haze/ptp_data_builder.hpp create mode 100644 troposphere/haze/include/haze/ptp_data_parser.hpp create mode 100644 troposphere/haze/include/haze/ptp_object_database.hpp create mode 100644 troposphere/haze/include/haze/ptp_object_heap.hpp create mode 100644 troposphere/haze/include/haze/ptp_responder.hpp create mode 100644 troposphere/haze/include/haze/results.hpp create mode 100644 troposphere/haze/include/haze/usb_session.hpp create mode 100644 troposphere/haze/source/async_usb_server.cpp create mode 100644 troposphere/haze/source/event_reactor.cpp create mode 100644 troposphere/haze/source/main.cpp create mode 100644 troposphere/haze/source/ptp_object_database.cpp create mode 100644 troposphere/haze/source/ptp_object_heap.cpp create mode 100644 troposphere/haze/source/ptp_responder.cpp create mode 100644 troposphere/haze/source/usb_session.cpp 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 0000000000000000000000000000000000000000..c23abe907985b1f4e4965974dd53771fdf57513d GIT binary patch literal 7823 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 aqZZz`8kc$x8kQcE;3Dn+*u7mi#{U;AMO#|{ literal 0 HcmV?d00001 diff --git a/troposphere/haze/icon.svg b/troposphere/haze/icon.svg new file mode 100644 index 000000000..c8c2f6247 --- /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..7eaa1cf16 --- /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..233e31996 --- /dev/null +++ b/troposphere/haze/include/haze/assert.hpp @@ -0,0 +1,29 @@ +/* + * 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) \ +{ \ + if (const bool __tmp_haze_assert_val = static_cast(expr); (!__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)); \ +} 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..af62fd666 --- /dev/null +++ b/troposphere/haze/include/haze/async_usb_server.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 + +#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 { + return this->TransferPacketImpl(true, page, size, out_size_transferred); + } + + Result WritePacket(void *page, u32 size) const { + u32 size_transferred; + 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..ccab145ce --- /dev/null +++ b/troposphere/haze/include/haze/common.hpp @@ -0,0 +1,42 @@ +/* + * 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 +#include +#include + +#include + +namespace haze { + + using namespace ::ams::literals; + using namespace ::ams; + + using Result = ::ams::Result; + + static constexpr size_t 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..99de59382 --- /dev/null +++ b/troposphere/haze/include/haze/console_main_loop.hpp @@ -0,0 +1,150 @@ +/* + * 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 = 33333333; + + 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) { + Waiter cancel_waiter = waiterForUEvent(std::addressof(m_cancel_event)); + Result rc = waitObjects(std::addressof(idx), std::addressof(cancel_waiter), 1, FrameDelayNs); + + if (R_SUCCEEDED(rc)) break; + 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) { + m_reactor = reactor; + m_object_heap = object_heap; + + AppletType applet_type = appletGetAppletType(); + m_is_applet_mode = applet_type != AppletType_Application && applet_type != AppletType_SystemApplication; + + 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. */ + R_TRY(threadCreate(std::addressof(m_thread), ConsoleMainLoop::Run, this, nullptr, 0x1000, 0x2b, -2)); + + /* 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() { + ueventSignal(std::addressof(m_cancel_event)); + + HAZE_R_ABORT_UNLESS(threadWaitForExit(std::addressof(m_thread))); + + HAZE_R_ABORT_UNLESS(threadClose(std::addressof(m_thread))); + + m_reactor->RemoveConsumer(this); + } + + private: + void RedrawConsole() { + u32 heap_used = m_object_heap->GetSizeUsed(); + u32 heap_total = m_object_heap->GetSizeTotal(); + 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; + } + + m_last_heap_used = heap_used; + m_last_heap_total = heap_total; + + const char *used_unit = "B"; + if (heap_used > 1024) { heap_used >>= 10; used_unit = "KiB"; } + if (heap_used > 1024) { heap_used >>= 10; used_unit = "MiB"; } + + const char *total_unit = "B"; + if (heap_total > 1024) { heap_total >>= 10; total_unit = "KiB"; } + if (heap_total > 1024) { heap_total >>= 10; total_unit = "MiB"; } + + 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) { + printf("\n" CONSOLE_ESC(38;5;196m) "Applet Mode" CONSOLE_ESC(0m) "\n"); + } + + consoleUpdate(NULL); + } + + protected: + void ProcessEvent() override { + this->RedrawConsole(); + + /* Check buttons. */ + padUpdate(std::addressof(m_pad)); + + if (padGetButtonsDown(std::addressof(m_pad)) & HidNpadButton_Plus) { + m_reactor->RequestStop(); + } + + /* Pump applet event loop. */ + if (!appletMainLoop()) { + m_reactor->RequestStop(); + } + } + }; + +} diff --git a/troposphere/haze/include/haze/event_reactor.hpp b/troposphere/haze/include/haze/event_reactor.hpp new file mode 100644 index 000000000..5f762133a --- /dev/null +++ b/troposphere/haze/include/haze/event_reactor.hpp @@ -0,0 +1,57 @@ +/* + * 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[MAX_WAIT_OBJECTS]; + Waiter m_waiters[MAX_WAIT_OBJECTS]; + + s32 m_num_wait_objects; + bool m_stop_requested; + + public: + constexpr explicit EventReactor() : m_consumers(), m_waiters(), m_num_wait_objects(), m_stop_requested() { /* ... */ } + + bool AddConsumer(EventConsumer *consumer, Waiter waiter); + void RemoveConsumer(EventConsumer *consumer); + + public: + void RequestStop() { m_stop_requested = true; } + bool GetStopRequested() const { return m_stop_requested; } + + public: + template + Result WaitFor(s32 *out_arg_waiter, Args &&... arg_waiters) { + const Waiter arg_waiter_array[] = {arg_waiters...}; + return this->WaitForImpl(out_arg_waiter, sizeof...(Args), arg_waiter_array); + } + + private: + Result WaitForImpl(s32 *out_arg_waiter, s32 num_arg_waiters, const Waiter *arg_waiters); + }; + +} diff --git a/troposphere/haze/include/haze/filesystem_proxy.hpp b/troposphere/haze/include/haze/filesystem_proxy.hpp new file mode 100644 index 000000000..3fce92ad2 --- /dev/null +++ b/troposphere/haze/include/haze/filesystem_proxy.hpp @@ -0,0 +1,123 @@ +/* + * 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) { + Result rc = func(std::forward(args)...); + + R_UNLESS(!m_reactor->GetStopRequested(), haze::ResultStopRequested()); + + 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 OpenFile(const char *path, u32 mode, FsFile *out_file) { + R_RETURN(this->ForwardResult(fsFsOpenFile, m_filesystem, path, mode, out_file)); + } + + Result FileGetSize(FsFile *file, s64 *out_size) { + R_RETURN(this->ForwardResult(fsFileGetSize, file, out_size)); + } + + Result FileSetSize(FsFile *file, s64 size) { + R_RETURN(this->ForwardResult(fsFileSetSize, file, size)); + } + + Result FileRead(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 FileWrite(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) { + R_RETURN(this->ForwardResult(fsFileWrite, file, off, buf, write_size, option)); + } + + void FileClose(FsFile *file) { + fsFileClose(file); + } + + Result CreateDirectory(const char* path) { + R_RETURN(this->ForwardResult(fsFsCreateDirectory, m_filesystem, path)); + } + + Result DeleteDirectory(const char* path) { + R_RETURN(this->ForwardResult(fsFsDeleteDirectory, m_filesystem, path)); + } + + Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) { + R_RETURN(this->ForwardResult(fsFsOpenDirectory, m_filesystem, path, mode, out_dir)); + } + + Result DirectoryRead(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 DirectoryGetEntryCount(FsDir *d, s64 *out_count) { + R_RETURN(this->ForwardResult(fsDirGetEntryCount, d, out_count)); + } + + void DirectoryClose(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..d32ae5b2e --- /dev/null +++ b/troposphere/haze/include/haze/ptp.hpp @@ -0,0 +1,267 @@ +/* + * 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 size_t PtpUsbBulkHighSpeedMaxPacketLength = 0x200; + constexpr size_t PtpUsbBulkHeaderLength = 2 * sizeof(uint32_t) + 2 * sizeof(uint16_t); + constexpr size_t PtpStringMaxLength = 255; + + enum PtpUsbBulkContainerType : u16 { + PtpUsbBulkContainerType_Undefined = 0, + PtpUsbBulkContainerType_Command = 1, + PtpUsbBulkContainerType_Data = 2, + PtpUsbBulkContainerType_Response = 3, + PtpUsbBulkContainerType_Event = 4, + }; + + 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_MtpInvalid_ObjectPropCode = 0xA801, + PtpResponseCode_MtpInvalid_ObjectProp_Format = 0xA802, + PtpResponseCode_MtpInvalid_ObjectProp_Value = 0xA803, + PtpResponseCode_MtpInvalid_ObjectReference = 0xA804, + PtpResponseCode_MtpInvalid_Dataset = 0xA806, + PtpResponseCode_MtpSpecification_By_Group_Unsupported = 0xA807, + PtpResponseCode_MtpSpecification_By_Depth_Unsupported = 0xA808, + PtpResponseCode_MtpObject_Too_Large = 0xA809, + PtpResponseCode_MtpObjectProp_Not_Supported = 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 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 = 0x0, + PtpAssociationType_GenericFolder = 0x1, + }; + + enum PtpGetObjectHandles : u32 { + PtpGetObjectHandles_AllFormats = 0x0, + PtpGetObjectHandles_AllAssocs = 0x0, + 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 { + uint32_t length; + uint16_t type; + uint16_t code; + uint32_t trans_id; + }; + static_assert(sizeof(PtpUsbBulkContainer) == PtpUsbBulkHeaderLength); + + struct PtpNewObjectInfo { + uint32_t storage_id; + uint32_t parent_object_id; + uint32_t 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..972eda42b --- /dev/null +++ b/troposphere/haze/include/haze/ptp_data_builder.hpp @@ -0,0 +1,166 @@ +/* + * 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; + size_t m_transmitted_size; + size_t m_offset; + u8 *m_data; + bool m_disabled; + + private: + template + constexpr static size_t Strlen(const T *str) { + const T *s = str; + for (; *s; ++s) { + } + return s - str; + } + + Result Flush() { + ON_SCOPE_EXIT { + m_transmitted_size += m_offset; + m_offset = 0; + }; + + if (m_disabled) { + R_SUCCEED(); + } else { + 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 (m_transmitted_size % PtpUsbBulkHighSpeedMaxPacketLength == 0) { + /* 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. */ + 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 we cannot write more bytes now, flush. */ + if (m_offset == haze::UsbBulkPacketBufferSize) { + R_TRY(this->Flush()); + } + } + + R_SUCCEED(); + } + + template + Result Add(T value) { + const auto bytes = std::bit_cast>(value); + + R_RETURN(this->AddBuffer(bytes.data(), bytes.size())); + } + + 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) { + m_disabled = true; + R_TRY(func()); + R_TRY(this->Commit()); + + const size_t data_size = m_transmitted_size; + + m_transmitted_size = 0; + m_offset = 0; + m_disabled = false; + + R_TRY(this->AddDataHeader(request, data_size)); + R_TRY(func()); + R_TRY(this->Commit()); + + R_SUCCEED(); + } + + template + Result AddString(const T *str) { + u8 len = static_cast(std::min(Strlen(str), PtpStringMaxLength - 1)); + if (len > 0) { + /* Write 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 *ary, u32 count) { + R_TRY(this->Add(count)); + + for (size_t i = 0; i < count; i++) { + R_TRY(this->Add(ary[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..a642fefdd --- /dev/null +++ b/troposphere/haze/include/haze/ptp_data_parser.hpp @@ -0,0 +1,112 @@ +/* + * 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_eos; + + private: + Result Flush() { + R_UNLESS(!m_eos, haze::ResultEndOfTransmission()); + + m_received_size = 0; + m_offset = 0; + + ON_SCOPE_EXIT { m_eos = 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_eos() { /* ... */ } + + Result Finalize() { + /* Read until the transmission completes. */ + while (true) { + Result rc = this->Flush(); + + R_SUCCEED_IF(m_eos || haze::ResultEndOfTransmission::Includes(rc)); + R_TRY(rc); + } + } + + Result ReadBuffer(u8 *buffer, size_t count, size_t *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) { + size_t read_count; + std::array bytes = {}; + + R_TRY(this->ReadBuffer(bytes.data(), bytes.size(), std::addressof(read_count))); + + out_t = std::bit_cast(bytes); + + 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(len)); + + /* Read characters one by one. */ + for (size_t i = 0; i < len; i++) { + u16 chr; + R_TRY(this->Read(chr)); + + *out_string++ = static_cast(chr); + } + + /* Write null terminator. */ + *out_string++ = 0; + + 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..af8511b87 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_object_database.hpp @@ -0,0 +1,99 @@ +/* + * 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 PtpObjectDatabase { + public: + struct ObjectNode { + util::IntrusiveRedBlackTreeNode m_object_name_to_id_node; + util::IntrusiveRedBlackTreeNode m_object_id_to_name_node; + u32 m_parent_id; + u32 m_object_id; + char m_name[]; + + explicit ObjectNode(char *name, u32 parent_id, u32 object_id) : m_object_name_to_id_node(), m_object_id_to_name_node(), m_parent_id(parent_id), m_object_id(object_id) { /* ... */ } + + const char *GetName() const { return m_name; } + u32 GetParentId() const { return m_parent_id; } + u32 GetObjectId() const { return m_object_id; } + + 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 ObjectNode &rhs) { + return std::strcmp(lhs.GetName(), rhs.GetName()); + } + }; + + struct IdComparator { + 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 ObjectNode &rhs) { + return lhs.GetObjectId() - rhs.GetObjectId(); + } + }; + }; + + private: + using ObjectNameToIdTreeTraits = util::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&ObjectNode::m_object_name_to_id_node>; + using ObjectNameToIdTree = ObjectNameToIdTreeTraits::TreeType; + + using ObjectIdToNameTreeTraits = util::IntrusiveRedBlackTreeMemberTraitsDeferredAssert<&ObjectNode::m_object_id_to_name_node>; + using ObjectIdToNameTree = ObjectIdToNameTreeTraits::TreeType; + + PtpObjectHeap *m_object_heap; + ObjectNameToIdTree m_name_to_object_id; + ObjectIdToNameTree m_object_id_to_name; + u32 m_next_object_id; + + public: + constexpr explicit PtpObjectDatabase() : m_object_heap(), m_name_to_object_id(), m_object_id_to_name(), m_next_object_id() { /* ... */ } + + void Initialize(PtpObjectHeap *object_heap); + void Finalize(); + + public: + /* Object database API. */ + Result AddObjectId(const char *parent_name, const char *name, u32 *out_object_id, u32 parent_id, u32 desired_object_id = 0); + void RemoveObjectId(u32 object_id); + ObjectNode *GetObject(u32 object_id); + }; + +} 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..826893d59 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_object_heap.hpp @@ -0,0 +1,128 @@ +/* + * 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 arena 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; + + 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 GetSizeTotal() const { + return m_heap_block_size * NumHeapBlocks; + } + + constexpr size_t GetSizeUsed() 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 { + if (this->GetNextAddress() + n < this->GetNextAddress()) return false; + 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) { + if (n + 7 < n) return nullptr; + + /* Round up the amount to a multiple of 8. */ + n = (n + 7) & ~7ull; + + /* 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. */ + } + + template + constexpr T *New(Args&&... args) { + T *t = static_cast(this->Allocate(sizeof(T))); + std::construct_at(t, std::forward(args)...); + return t; + } + + template + constexpr void Delete(T *t) { + std::destroy_at(t); + this->Deallocate(t, sizeof(T)); + } + }; + +} diff --git a/troposphere/haze/include/haze/ptp_responder.hpp b/troposphere/haze/include/haze/ptp_responder.hpp new file mode 100644 index 000000000..79effa4d3 --- /dev/null +++ b/troposphere/haze/include/haze/ptp_responder.hpp @@ -0,0 +1,71 @@ +/* + * 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 HandleRequest(); + + private: + /* Request handling. */ + Result HandleRequestImpl(); + Result HandleCommandRequest(PtpDataParser &dp); + void ForceCloseSession(); + + template + Result WriteResponse(PtpResponseCode code, Data &&data); + Result WriteResponse(PtpResponseCode code); + + /* 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); + }; + +} diff --git a/troposphere/haze/include/haze/results.hpp b/troposphere/haze/include/haze/results.hpp new file mode 100644 index 000000000..c941e8e30 --- /dev/null +++ b/troposphere/haze/include/haze/results.hpp @@ -0,0 +1,39 @@ +/* + * 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 + +/* Please 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(EndOfTransmission, 5); + R_DEFINE_ERROR_RESULT(UnknownPacketType, 6); + R_DEFINE_ERROR_RESULT(SessionNotOpen, 7); + R_DEFINE_ERROR_RESULT(OutOfMemory, 8); + R_DEFINE_ERROR_RESULT(ObjectNotFound, 9); + R_DEFINE_ERROR_RESULT(StorageNotFound, 10); + R_DEFINE_ERROR_RESULT(OperationNotSupported, 11); + R_DEFINE_ERROR_RESULT(UnknownRequestType, 12); + R_DEFINE_ERROR_RESULT(GeneralFailure, 13); + +} diff --git a/troposphere/haze/include/haze/usb_session.hpp b/troposphere/haze/include/haze/usb_session.hpp new file mode 100644 index 000000000..55f414b1b --- /dev/null +++ b/troposphere/haze/include/haze/usb_session.hpp @@ -0,0 +1,51 @@ +/* + * 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, + UsbSessionEndpoint_Write, + UsbSessionEndpoint_Interrupt, + UsbSessionEndpoint_Count, + }; + + 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, size_t size, u32 *out_urb_id); + Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_transferred_size); + Result SetZeroLengthTermination(bool enable); + }; + +} diff --git a/troposphere/haze/source/async_usb_server.cpp b/troposphere/haze/source/async_usb_server.cpp new file mode 100644 index 000000000..ed465ad61 --- /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 { + + static constinit UsbSession s_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(s_usb_session.Initialize(interface_info, id_vendor, id_product)); + R_TRY(s_usb_session.SetZeroLengthTermination(false)); + + R_SUCCEED(); + } + + void AsyncUsbServer::Finalize() { + s_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 (!s_usb_session.GetConfigured()) { + R_TRY(m_reactor->WaitFor(std::addressof(waiter_idx), waiterForEvent(usbDsGetStateChangeEvent()))); + R_TRY(eventClear(usbDsGetStateChangeEvent())); + + R_THROW(haze::ResultNotConfigured()); + } + + UsbSessionEndpoint ep = read ? UsbSessionEndpoint_Read : UsbSessionEndpoint_Write; + R_TRY(s_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(s_usb_session.GetCompletionEvent(ep)))); + + /* Return what we transferred. */ + R_RETURN(s_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..724e878d5 --- /dev/null +++ b/troposphere/haze/source/event_reactor.cpp @@ -0,0 +1,76 @@ +/* + * 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 <= MAX_WAIT_OBJECTS); + + /* 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 input_index = 0, output_index = 0; + + /* Remove the consumer. */ + for (; 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, s32 num_arg_waiters, const Waiter *arg_waiters) { + HAZE_ASSERT(m_num_wait_objects + num_arg_waiters <= MAX_WAIT_OBJECTS); + + while (true) { + R_UNLESS(!m_stop_requested, haze::ResultStopRequested()); + + /* 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, -1)); + + /* If this refers to a waiter in the argument list, 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..2f568eb22 --- /dev/null +++ b/troposphere/haze/source/main.cpp @@ -0,0 +1,42 @@ +/* + * 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[]) { + haze::PtpObjectHeap ptp_object_heap; + + haze::EventReactor event_reactor; + haze::PtpResponder ptp_responder; + haze::ConsoleMainLoop console_main_loop; + + consoleInit(NULL); + appletSetAutoSleepDisabled(true); + + 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)); + + while (!event_reactor.GetStopRequested()) { + ptp_responder.HandleRequest(); + } + + console_main_loop.Finalize(); + ptp_responder.Finalize(); + + appletSetAutoSleepDisabled(false); + consoleExit(NULL); + 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..2f8a7f284 --- /dev/null +++ b/troposphere/haze/source/ptp_object_database.cpp @@ -0,0 +1,103 @@ +/* + * 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_to_object_id)); + std::construct_at(std::addressof(m_object_id_to_name)); + + m_next_object_id = 1; + } + + void PtpObjectDatabase::Finalize() { + std::destroy_at(std::addressof(m_object_id_to_name)); + std::destroy_at(std::addressof(m_name_to_object_id)); + + m_next_object_id = 0; + + m_object_heap->Finalize(); + m_object_heap = nullptr; + } + + Result PtpObjectDatabase::AddObjectId(const char *parent_name, const char *name, u32 *out_object_id, u32 parent_id, u32 desired_object_id) { + ObjectNode *node = nullptr; + + /* Calculate length of the name. */ + const size_t parent_name_len = std::strlen(parent_name); + const size_t name_len = std::strlen(name) + 1; + const size_t alloc_len = parent_name_len + 1 + name_len; + + /* Allocate memory for the node. */ + node = reinterpret_cast(m_object_heap->Allocate(sizeof(ObjectNode) + alloc_len)); + R_UNLESS(node != nullptr, haze::ResultOutOfMemory()); + + /* Ensure we maintain a clean state on failure. */ + auto guard = SCOPE_GUARD { m_object_heap->Deallocate(node, sizeof(ObjectNode) + alloc_len); }; + + /* Take ownership of the name. */ + std::strncpy(node->m_name, parent_name, parent_name_len + 1); + node->m_name[parent_name_len] = '/'; + std::strncpy(node->m_name + parent_name_len + 1, name, name_len); + + /* Check if an object with this name already exists. If it does, we can just return it here. */ + auto it = m_name_to_object_id.find_key(node->m_name); + if (it != m_name_to_object_id.end()) { + if (out_object_id) *out_object_id = it->GetObjectId(); + R_SUCCEED(); + } + + /* Persist the reference to the node. */ + guard.Cancel(); + + /* Insert node into trees. */ + node->m_parent_id = parent_id; + node->m_object_id = desired_object_id == 0 ? m_next_object_id++ : desired_object_id; + m_name_to_object_id.insert(*node); + m_object_id_to_name.insert(*node); + + /* Set output. */ + if (out_object_id) *out_object_id = node->GetObjectId(); + + /* We succeeded. */ + R_SUCCEED(); + } + + void PtpObjectDatabase::RemoveObjectId(u32 object_id) { + /* Find in forward mapping. */ + auto it = m_object_id_to_name.find_key(object_id); + if (it == m_object_id_to_name.end()) return; + + /* Free the node. */ + ObjectNode *node = std::addressof(*it); + m_object_id_to_name.erase(m_object_id_to_name.iterator_to(*node)); + m_name_to_object_id.erase(m_name_to_object_id.iterator_to(*node)); + m_object_heap->Deallocate(node, sizeof(ObjectNode) + std::strlen(node->GetName()) + 1); + } + + PtpObjectDatabase::ObjectNode *PtpObjectDatabase::GetObject(u32 object_id) { + /* Find in forward mapping. */ + auto it = m_object_id_to_name.find_key(object_id); + if (it == m_object_id_to_name.end()) return nullptr; + + return std::addressof(*it); + } + +} diff --git a/troposphere/haze/source/ptp_object_heap.cpp b/troposphere/haze/source/ptp_object_heap.cpp new file mode 100644 index 000000000..fd2f20966 --- /dev/null +++ b/troposphere/haze/source/ptp_object_heap.cpp @@ -0,0 +1,66 @@ +/* + * 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 LibnxReservedMem = 20_MB; + + } + + void PtpObjectHeap::Initialize() { + size_t mem_used = 0; + + /* Skip re-initialization if we are currently initialized. */ + if (m_heap_block_size != 0) return; + + /* Estimate how much memory we can reserve. */ + HAZE_R_ABORT_UNLESS(svcGetInfo(std::addressof(mem_used), InfoType_UsedMemorySize, CUR_PROCESS_HANDLE, 0)); + HAZE_ASSERT(mem_used > LibnxReservedMem); + mem_used -= LibnxReservedMem; + + /* Take the rest for ourselves. */ + 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..4f517d887 --- /dev/null +++ b/troposphere/haze/source/ptp_responder.cpp @@ -0,0 +1,698 @@ +/* + * 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 + +#define ARRAY_LEN(x) (sizeof(x)/sizeof(x[0])) + +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, + }; + + constexpr PtpEventCode SupportedEventCodes[] = {}; + constexpr PtpDevicePropertyCode SupportedPropertyCodes[] = {}; + constexpr PtpObjectFormatCode SupportedCaptureFormats[] = {}; + constexpr PtpObjectFormatCode SupportedPlaybackFormats[] = {}; + + constexpr 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; + + static constinit char s_filename_str[PtpStringMaxLength + 1]; + static constinit char s_capture_date_str[PtpStringMaxLength + 1]; + static constinit char s_modification_date_str[PtpStringMaxLength + 1]; + static constinit char s_keywords_str[PtpStringMaxLength + 1]; + + static constinit FsDirectoryEntry s_dir_entries[DirectoryReadSize] = {}; + static constinit u8 s_fs_buffer[FsBufferSize] = {}; + + alignas(0x1000) static constinit u8 s_bulk_write_buffer[haze::UsbBulkPacketBufferSize] = {}; + alignas(0x1000) static constinit u8 s_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::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::ResultStorageNotFound) { + R_TRY(this->WriteResponse(PtpResponseCode_InvalidStorageId)); + } + R_CATCH(haze::ResultObjectNotFound) { + R_TRY(this->WriteResponse(PtpResponseCode_InvalidObjectHandle)); + } + R_CATCH(fs::ResultPathNotFound, fs::ResultPathAlreadyExists, fs::ResultTargetLocked, fs::ResultDirectoryNotEmpty, fs::ResultNotEnoughFreeSpaceSdCard) { + R_TRY(this->WriteResponse(PtpResponseCode_GeneralError)); + } + R_CATCH_ALL() { + R_THROW(haze::ResultGeneralFailure()); + } + } R_END_TRY_CATCH; + + R_SUCCEED(); + } + + Result PtpResponder::HandleRequestImpl() { + PtpDataParser dp(s_bulk_read_buffer, std::addressof(m_usb_server)); + R_TRY(dp.Read(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; + 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(s_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(s_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(s_bulk_write_buffer, std::addressof(m_usb_server)); + + 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, ARRAY_LEN(SupportedOperationCodes))); + R_TRY(db.AddArray(SupportedEventCodes, ARRAY_LEN(SupportedEventCodes))); + R_TRY(db.AddArray(SupportedPropertyCodes, ARRAY_LEN(SupportedPropertyCodes))); + R_TRY(db.AddArray(SupportedCaptureFormats, ARRAY_LEN(SupportedCaptureFormats))); + R_TRY(db.AddArray(SupportedPlaybackFormats, ARRAY_LEN(SupportedPlaybackFormats))); + R_TRY(db.AddString(MtpDeviceManufacturer)); + R_TRY(db.AddString(MtpDeviceModel)); + R_TRY(db.AddString(MtpDeviceVersion)); + R_TRY(db.AddString(MtpDeviceSerialNumber)); + + R_SUCCEED(); + })); + + 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 with hardcoded storage IDs. */ + m_session_open = true; + m_object_database.Initialize(m_object_heap); + R_TRY(m_object_database.AddObjectId("", "", nullptr, PtpGetObjectHandles_RootParent, StorageId_SdmcFs)); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::CloseSession(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + this->ForceCloseSession(); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetStorageIds(PtpDataParser &dp) { + R_TRY(dp.Finalize()); + + PtpDataBuilder db(s_bulk_write_buffer, std::addressof(m_usb_server)); + + R_TRY(db.WriteVariableLengthData(m_request_header, [&] { + R_RETURN(db.AddArray(SupportedStorageIds, ARRAY_LEN(SupportedStorageIds))); + })); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetStorageInfo(PtpDataParser &dp) { + PtpDataBuilder db(s_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(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::ResultStorageNotFound()); + } + + /* Write the result. */ + 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(); + })); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectHandles(PtpDataParser &dp) { + PtpDataBuilder db(s_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(storage_id)); + R_TRY(dp.Read(object_format_code)); + R_TRY(dp.Read(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 *fileobj = m_object_database.GetObject(association_object_handle); + R_UNLESS(fileobj != nullptr, haze::ResultObjectNotFound()); + + /* Try to read the object as a directory. */ + FsDir dir; + R_TRY(m_fs.OpenDirectory(fileobj->GetName(), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, std::addressof(dir))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.DirectoryClose(std::addressof(dir)); }; + + /* Count how many entries are in the directory. */ + s64 entry_count = 0; + R_TRY(m_fs.DirectoryGetEntryCount(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))); + + /* 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.DirectoryRead(std::addressof(dir), std::addressof(read_count), DirectoryReadSize, s_dir_entries)); + + /* Write to output. */ + for (s64 i = 0; i < read_count; i++) { + u32 handle; + R_TRY(m_object_database.AddObjectId(fileobj->GetName(), s_dir_entries[i].name, std::addressof(handle), fileobj->GetObjectId())); + R_TRY(db.Add(handle)); + } + + /* If we read fewer than the batch size, we're done. */ + if (read_count < DirectoryReadSize) { + break; + } + } + + R_TRY(db.Commit()); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObjectInfo(PtpDataParser &dp) { + PtpDataBuilder db(s_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Get the object ID the client requested info for. */ + u32 object_id; + R_TRY(dp.Read(object_id)); + R_TRY(dp.Finalize()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto *fileobj = m_object_database.GetObject(object_id); + R_UNLESS(fileobj != nullptr, haze::ResultObjectNotFound()); + + /* 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(fileobj->GetName(), std::addressof(entry_type))); + + /* Get the size of the file. */ + s64 size = 0; + if (entry_type == FsDirEntryType_File) { + FsFile file; + R_TRY(m_fs.OpenFile(fileobj->GetName(), FsOpenMode_Read, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.FileClose(std::addressof(file)); }; + + R_TRY(m_fs.FileGetSize(std::addressof(file), std::addressof(size))); + } + + object_info.filename = std::strrchr(fileobj->GetName(), '/') + 1; + object_info.object_compressed_size = size; + object_info.parent_object = fileobj->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; + } + } + + 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(); + })); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::GetObject(PtpDataParser &dp) { + PtpDataBuilder db(s_bulk_write_buffer, std::addressof(m_usb_server)); + + /* Get the object ID the client requested. */ + u32 object_id; + R_TRY(dp.Read(object_id)); + R_TRY(dp.Finalize()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto *fileobj = m_object_database.GetObject(object_id); + R_UNLESS(fileobj != nullptr, haze::ResultObjectNotFound()); + + /* Lock the object as a file. */ + FsFile file; + R_TRY(m_fs.OpenFile(fileobj->GetName(), FsOpenMode_Read, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.FileClose(std::addressof(file)); }; + + /* Get the file's size. */ + s64 size = 0, offset = 0; + R_TRY(m_fs.FileGetSize(std::addressof(file), std::addressof(size))); + + /* Send the header and file size. */ + R_TRY(db.AddDataHeader(m_request_header, size)); + + while (true) { + /* Get the next batch. */ + size_t bytes_read; + R_TRY(m_fs.FileRead(std::addressof(file), offset, s_fs_buffer, FsBufferSize, FsReadOption_None, std::addressof(bytes_read))); + + offset += bytes_read; + + /* Write to output. */ + R_TRY(db.AddBuffer(s_fs_buffer, bytes_read)); + + /* If we read fewer bytes than the batch size, we're done. */ + if (bytes_read < FsBufferSize) { + break; + } + } + + R_TRY(db.Commit()); + + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::SendObjectInfo(PtpDataParser &rdp) { + u32 storage_id, parent_object; + R_TRY(rdp.Read(storage_id)); + R_TRY(rdp.Read(parent_object)); + R_TRY(rdp.Finalize()); + + PtpDataParser dp(s_bulk_read_buffer, std::addressof(m_usb_server)); + PtpObjectInfo info(DefaultObjectInfo); + + /* Ensure that we have a data header. */ + PtpUsbBulkContainer data_header; + R_TRY(dp.Read(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(info.storage_id)); + R_TRY(dp.Read(info.object_format)); + R_TRY(dp.Read(info.protection_status)); + R_TRY(dp.Read(info.object_compressed_size)); + R_TRY(dp.Read(info.thumb_format)); + R_TRY(dp.Read(info.thumb_compressed_size)); + R_TRY(dp.Read(info.thumb_width)); + R_TRY(dp.Read(info.thumb_height)); + R_TRY(dp.Read(info.image_width)); + R_TRY(dp.Read(info.image_height)); + R_TRY(dp.Read(info.image_depth)); + R_TRY(dp.Read(info.parent_object)); + R_TRY(dp.Read(info.association_type)); + R_TRY(dp.Read(info.association_desc)); + R_TRY(dp.Read(info.sequence_number)); + R_TRY(dp.ReadString(s_filename_str)); + R_TRY(dp.ReadString(s_capture_date_str)); + R_TRY(dp.ReadString(s_modification_date_str)); + R_TRY(dp.ReadString(s_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 *parentobj = m_object_database.GetObject(parent_object); + R_UNLESS(parentobj != nullptr, haze::ResultObjectNotFound()); + + /* 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; + + R_TRY(m_object_database.AddObjectId(parentobj->GetName(), s_filename_str, std::addressof(new_object_info.object_id), parentobj->GetObjectId())); + + /* Ensure we maintain a clean state on failure. */ + ON_RESULT_FAILURE { m_object_database.RemoveObjectId(new_object_info.object_id); }; + + /* Get the object name we just built. */ + auto *fileobj = m_object_database.GetObject(new_object_info.object_id); + R_UNLESS(fileobj != nullptr, haze::ResultGeneralFailure()); + + /* Create the object on the filesystem. */ + if (info.association_type == PtpAssociationType_GenericFolder) { + R_TRY(m_fs.CreateDirectory(fileobj->GetName())); + m_send_object_id = 0; + } else { + R_TRY(m_fs.CreateFile(fileobj->GetName(), 0, 0)); + m_send_object_id = new_object_info.object_id; + } + + /* We succeeded. */ + 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(s_bulk_read_buffer, std::addressof(m_usb_server)); + + /* Ensure that we have a data header. */ + PtpUsbBulkContainer data_header; + R_TRY(dp.Read(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 *fileobj = m_object_database.GetObject(m_send_object_id); + R_UNLESS(fileobj != nullptr, haze::ResultObjectNotFound()); + + /* Lock the object as a file. */ + FsFile file; + R_TRY(m_fs.OpenFile(fileobj->GetName(), FsOpenMode_Write | FsOpenMode_Append, std::addressof(file))); + + /* Ensure we maintain a clean state on exit. */ + ON_SCOPE_EXIT { m_fs.FileClose(std::addressof(file)); }; + + /* Truncate the file after locking for write. */ + s64 offset = 0; + R_TRY(m_fs.FileSetSize(std::addressof(file), 0)); + + /* Begin writing. */ + while (true) { + /* Read as many bytes as we can. */ + size_t bytes_received; + Result rc = dp.ReadBuffer(s_fs_buffer, FsBufferSize, std::addressof(bytes_received)); + + /* Write to the file. */ + R_TRY(m_fs.FileWrite(std::addressof(file), offset, s_fs_buffer, bytes_received, 0)); + + offset += bytes_received; + + /* If we received fewer bytes than the batch size, we're done. */ + if (haze::ResultEndOfTransmission::Includes(rc)) { + break; + } + + R_TRY(rc); + } + + /* We succeeded. */ + R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); + } + + Result PtpResponder::DeleteObject(PtpDataParser &dp) { + u32 object_id; + R_TRY(dp.Read(object_id)); + R_TRY(dp.Finalize()); + + /* Check if we know about the object. If we don't, it's an error. */ + auto *fileobj = m_object_database.GetObject(object_id); + R_UNLESS(fileobj != nullptr, haze::ResultObjectNotFound()); + + /* Figure out what type of object this is. */ + FsDirEntryType entry_type; + R_TRY(m_fs.GetEntryType(fileobj->GetName(), std::addressof(entry_type))); + + /* Remove the object from the filesystem. */ + if (entry_type == FsDirEntryType_Dir) { + R_TRY(m_fs.DeleteDirectory(fileobj->GetName())); + } else { + R_TRY(m_fs.DeleteFile(fileobj->GetName())); + } + + /* Remove the object from tracking. */ + m_object_database.RemoveObjectId(fileobj->GetObjectId()); + + /* We succeeded. */ + 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..6410d3aa6 --- /dev/null +++ b/troposphere/haze/source/usb_session.cpp @@ -0,0 +1,253 @@ +/* + * 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 { + + static constexpr 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 = 0x4, + }; + + struct usb_ss_endpoint_companion_descriptor endpoint_companion = { + .bLength = sizeof(struct usb_ss_endpoint_companion_descriptor), + .bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION, + .bMaxBurst = 0x0F, + .bmAttributes = 0x00, + .wBytesPerInterval = 0x00, + }; + + R_TRY(usbDsRegisterInterface(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. */ + 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), 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)) { + u8 iManufacturer, iProduct, iSerialNumber; + static const u16 supported_langs[1] = {0x0409}; + + R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16))); + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iManufacturer), "Nintendo")); + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iProduct), "Nintendo Switch")); + R_TRY(usbDsAddUsbStringDescriptor(std::addressof(iSerialNumber), "SerialNumber")); + + // 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))); + } + + R_TRY(hosversionAtLeast(5,0,0) ? this->Initialize5x(info) : this->Initialize1x(info)); + + if (hosversionAtLeast(5,0,0)) { + R_TRY(usbDsEnable()); + } + + 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, size_t size, u32 *out_urb_id) { + 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(); + } + + Result UsbSession::SetZeroLengthTermination(bool enable) { + R_RETURN(usbDsEndpoint_SetZlt(m_endpoints[UsbSessionEndpoint_Write], enable)); + } + +}