/* * Copyright (c) Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include namespace haze { namespace { constexpr UsbCommsInterfaceInfo MtpInterfaceInfo = { .bInterfaceClass = 0x06, .bInterfaceSubClass = 0x01, .bInterfaceProtocol = 0x01, }; /* This is a VID:PID recognized by libmtp. */ constexpr u16 SwitchMtpIdVendor = 0x057e; constexpr u16 SwitchMtpIdProduct = 0x201d; /* Constants used for MTP GetDeviceInfo response. */ constexpr u16 MtpStandardVersion = 100; constexpr u32 MtpVendorExtensionId = 6; constexpr auto MtpVendorExtensionDesc = "microsoft.com: 1.0;"; constexpr u16 MtpFunctionalModeDefault = 0; constexpr auto MtpDeviceManufacturer = "Nintendo"; constexpr auto MtpDeviceModel = "Nintendo Switch"; enum StorageId : u32 { StorageId_SdmcFs = 0xffffffffu - 1, }; constexpr PtpOperationCode SupportedOperationCodes[] = { PtpOperationCode_GetDeviceInfo, PtpOperationCode_OpenSession, PtpOperationCode_CloseSession, PtpOperationCode_GetStorageIds, PtpOperationCode_GetStorageInfo, PtpOperationCode_GetObjectHandles, PtpOperationCode_GetObjectInfo, PtpOperationCode_GetObject, PtpOperationCode_SendObjectInfo, PtpOperationCode_SendObject, PtpOperationCode_DeleteObject, PtpOperationCode_MtpGetObjectPropsSupported, PtpOperationCode_MtpGetObjectPropDesc, PtpOperationCode_MtpGetObjectPropValue, PtpOperationCode_MtpSetObjectPropValue, }; constexpr const PtpEventCode SupportedEventCodes[] = { /* ... */ }; constexpr const PtpDevicePropertyCode SupportedDeviceProperties[] = { /* ... */ }; constexpr const PtpObjectFormatCode SupportedCaptureFormats[] = { /* ... */ }; constexpr const PtpObjectFormatCode SupportedPlaybackFormats[] = { PtpObjectFormatCode_Undefined, PtpObjectFormatCode_Association, }; constexpr const PtpObjectPropertyCode SupportedObjectProperties[] = { PtpObjectPropertyCode_StorageId, PtpObjectPropertyCode_ObjectFormat, PtpObjectPropertyCode_ObjectSize, PtpObjectPropertyCode_ObjectFileName, PtpObjectPropertyCode_ParentObject, PtpObjectPropertyCode_PersistentUniqueObjectIdentifier, }; constexpr bool IsSupportedObjectPropertyCode(PtpObjectPropertyCode c) { for (size_t i = 0; i < util::size(SupportedObjectProperties); i++) { if (SupportedObjectProperties[i] == c) { return true; } } return false; } constexpr const StorageId SupportedStorageIds[] = { StorageId_SdmcFs, }; struct PtpStorageInfo { PtpStorageType storage_type; PtpFilesystemType filesystem_type; PtpAccessCapability access_capability; u64 max_capacity; u64 free_space_in_bytes; u32 free_space_in_images; const char *storage_description; const char *volume_label; }; constexpr PtpStorageInfo DefaultStorageInfo = { .storage_type = PtpStorageType_FixedRam, .filesystem_type = PtpFilesystemType_GenericHierarchical, .access_capability = PtpAccessCapability_ReadWrite, .max_capacity = 0, .free_space_in_bytes = 0, .free_space_in_images = 0, .storage_description = "", .volume_label = "", }; struct PtpObjectInfo { StorageId storage_id; PtpObjectFormatCode object_format; PtpProtectionStatus protection_status; u32 object_compressed_size; u16 thumb_format; u32 thumb_compressed_size; u32 thumb_width; u32 thumb_height; u32 image_width; u32 image_height; u32 image_depth; u32 parent_object; PtpAssociationType association_type; u32 association_desc; u32 sequence_number; const char *filename; const char *capture_date; const char *modification_date; const char *keywords; }; constexpr PtpObjectInfo DefaultObjectInfo = { .storage_id = StorageId_SdmcFs, .object_format = {}, .protection_status = PtpProtectionStatus_NoProtection, .object_compressed_size = 0, .thumb_format = 0, .thumb_compressed_size = 0, .thumb_width = 0, .thumb_height = 0, .image_width = 0, .image_height = 0, .image_depth = 0, .parent_object = PtpGetObjectHandles_RootParent, .association_type = PtpAssociationType_Undefined, .association_desc = 0, .sequence_number = 0, .filename = nullptr, .capture_date = "", .modification_date = "", .keywords = "", }; constexpr s64 DirectoryReadSize = 32; constexpr u64 FsBufferSize = haze::UsbBulkPacketBufferSize; constinit char g_filename_str[PtpStringMaxLength + 1] = {}; constinit char g_capture_date_str[PtpStringMaxLength + 1] = {}; constinit char g_modification_date_str[PtpStringMaxLength + 1] = {}; constinit char g_keywords_str[PtpStringMaxLength + 1] = {}; constinit FsDirectoryEntry g_dir_entries[DirectoryReadSize] = {}; constinit u8 g_fs_buffer[FsBufferSize] = {}; alignas(4_KB) constinit u8 g_bulk_write_buffer[haze::UsbBulkPacketBufferSize] = {}; alignas(4_KB) constinit u8 g_bulk_read_buffer[haze::UsbBulkPacketBufferSize] = {}; } Result PtpResponder::Initialize(EventReactor *reactor, PtpObjectHeap *object_heap) { m_object_heap = object_heap; /* Configure fs proxy. */ m_fs.Initialize(reactor, fsdevGetDeviceFileSystem("sdmc")); R_RETURN(m_usb_server.Initialize(std::addressof(MtpInterfaceInfo), SwitchMtpIdVendor, SwitchMtpIdProduct, reactor)); } void PtpResponder::Finalize() { m_usb_server.Finalize(); m_fs.Finalize(); } Result PtpResponder::LoopProcess() { while (true) { /* Try to handle a request. */ R_TRY_CATCH(this->HandleRequest()) { R_CATCH(haze::ResultStopRequested, haze::ResultFocusLost) { /* If we encountered a stop condition, we're done.*/ R_THROW(R_CURRENT_RESULT); } R_CATCH_ALL() { /* On other failures, try to handle another request. */ continue; } } R_END_TRY_CATCH; /* Otherwise, handle the next request. */ /* ... */ } } Result PtpResponder::HandleRequest() { ON_RESULT_FAILURE { /* For general failure modes, the failure is unrecoverable. Close the session. */ this->ForceCloseSession(); }; R_TRY_CATCH(this->HandleRequestImpl()) { R_CATCH(haze::ResultUnknownRequestType) { R_TRY(this->WriteResponse(PtpResponseCode_GeneralError)); } R_CATCH(haze::ResultSessionNotOpen) { R_TRY(this->WriteResponse(PtpResponseCode_SessionNotOpen)); } R_CATCH(haze::ResultOperationNotSupported) { R_TRY(this->WriteResponse(PtpResponseCode_OperationNotSupported)); } R_CATCH(haze::ResultInvalidStorageId) { R_TRY(this->WriteResponse(PtpResponseCode_InvalidStorageId)); } R_CATCH(haze::ResultInvalidObjectId) { R_TRY(this->WriteResponse(PtpResponseCode_InvalidObjectHandle)); } R_CATCH(haze::ResultUnknownPropertyCode) { R_TRY(this->WriteResponse(PtpResponseCode_MtpObjectPropNotSupported)); } R_CATCH(haze::ResultInvalidPropertyValue) { R_TRY(this->WriteResponse(PtpResponseCode_MtpInvalidObjectPropValue)); } R_CATCH_MODULE(fs) { /* Errors from fs are typically recoverable. */ R_TRY(this->WriteResponse(PtpResponseCode_GeneralError)); } } R_END_TRY_CATCH; R_SUCCEED(); } Result PtpResponder::HandleRequestImpl() { PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); R_TRY(dp.Read(std::addressof(m_request_header))); switch (m_request_header.type) { case PtpUsbBulkContainerType_Command: R_RETURN(this->HandleCommandRequest(dp)); default: R_THROW(haze::ResultUnknownRequestType()); } } Result PtpResponder::HandleCommandRequest(PtpDataParser &dp) { if (!m_session_open && m_request_header.code != PtpOperationCode_OpenSession && m_request_header.code != PtpOperationCode_GetDeviceInfo) { R_THROW(haze::ResultSessionNotOpen()); } switch (m_request_header.code) { case PtpOperationCode_GetDeviceInfo: R_RETURN(this->GetDeviceInfo(dp)); break; case PtpOperationCode_OpenSession: R_RETURN(this->OpenSession(dp)); break; case PtpOperationCode_CloseSession: R_RETURN(this->CloseSession(dp)); break; case PtpOperationCode_GetStorageIds: R_RETURN(this->GetStorageIds(dp)); break; case PtpOperationCode_GetStorageInfo: R_RETURN(this->GetStorageInfo(dp)); break; case PtpOperationCode_GetObjectHandles: R_RETURN(this->GetObjectHandles(dp)); break; case PtpOperationCode_GetObjectInfo: R_RETURN(this->GetObjectInfo(dp)); break; case PtpOperationCode_GetObject: R_RETURN(this->GetObject(dp)); break; case PtpOperationCode_SendObjectInfo: R_RETURN(this->SendObjectInfo(dp)); break; case PtpOperationCode_SendObject: R_RETURN(this->SendObject(dp)); break; case PtpOperationCode_DeleteObject: R_RETURN(this->DeleteObject(dp)); break; case PtpOperationCode_MtpGetObjectPropsSupported: R_RETURN(this->GetObjectPropsSupported(dp)); break; case PtpOperationCode_MtpGetObjectPropDesc: R_RETURN(this->GetObjectPropDesc(dp)); break; case PtpOperationCode_MtpGetObjectPropValue: R_RETURN(this->GetObjectPropValue(dp)); break; case PtpOperationCode_MtpSetObjectPropValue: R_RETURN(this->SetObjectPropValue(dp)); break; default: R_THROW(haze::ResultOperationNotSupported()); } } void PtpResponder::ForceCloseSession() { if (m_session_open) { m_session_open = false; m_object_database.Finalize(); } } template Result PtpResponder::WriteResponse(PtpResponseCode code, Data &&data) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); R_TRY(db.AddResponseHeader(m_request_header, code, sizeof(Data))); R_TRY(db.Add(data)); R_RETURN(db.Commit()); } Result PtpResponder::WriteResponse(PtpResponseCode code) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); R_TRY(db.AddResponseHeader(m_request_header, code, 0)); R_RETURN(db.Commit()); } Result PtpResponder::GetDeviceInfo(PtpDataParser &dp) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Write the device info data. */ R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { R_TRY(db.Add(MtpStandardVersion)); R_TRY(db.Add(MtpVendorExtensionId)); R_TRY(db.Add(MtpStandardVersion)); R_TRY(db.AddString(MtpVendorExtensionDesc)); R_TRY(db.Add(MtpFunctionalModeDefault)); R_TRY(db.AddArray(SupportedOperationCodes, util::size(SupportedOperationCodes))); R_TRY(db.AddArray(SupportedEventCodes, util::size(SupportedEventCodes))); R_TRY(db.AddArray(SupportedDeviceProperties, util::size(SupportedDeviceProperties))); R_TRY(db.AddArray(SupportedCaptureFormats, util::size(SupportedCaptureFormats))); R_TRY(db.AddArray(SupportedPlaybackFormats, util::size(SupportedPlaybackFormats))); R_TRY(db.AddString(MtpDeviceManufacturer)); R_TRY(db.AddString(MtpDeviceModel)); R_TRY(db.AddString(GetFirmwareVersion())); R_TRY(db.AddString(GetSerialNumber())); R_SUCCEED(); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::OpenSession(PtpDataParser &dp) { R_TRY(dp.Finalize()); /* Close, if we're already open. */ this->ForceCloseSession(); /* Initialize the database. */ m_session_open = true; m_object_database.Initialize(m_object_heap); /* Create the root storages. */ PtpObject *object; R_TRY(m_object_database.CreateOrFindObject("", "", PtpGetObjectHandles_RootParent, std::addressof(object))); /* Register the root storages. */ m_object_database.RegisterObject(object, StorageId_SdmcFs); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::CloseSession(PtpDataParser &dp) { R_TRY(dp.Finalize()); this->ForceCloseSession(); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetStorageIds(PtpDataParser &dp) { R_TRY(dp.Finalize()); PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Write the storage ID array. */ R_TRY(db.WriteVariableLengthData(m_request_header, [&] { R_RETURN(db.AddArray(SupportedStorageIds, util::size(SupportedStorageIds))); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetStorageInfo(PtpDataParser &dp) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); PtpStorageInfo storage_info(DefaultStorageInfo); /* Get the storage ID the client requested information for. */ u32 storage_id; R_TRY(dp.Read(std::addressof(storage_id))); R_TRY(dp.Finalize()); /* Get the info from fs. */ switch (storage_id) { case StorageId_SdmcFs: { s64 total_space, free_space; R_TRY(m_fs.GetTotalSpace("/", std::addressof(total_space))); R_TRY(m_fs.GetFreeSpace("/", std::addressof(free_space))); storage_info.max_capacity = total_space; storage_info.free_space_in_bytes = free_space; storage_info.free_space_in_images = 0; storage_info.storage_description = "SD Card"; } break; default: R_THROW(haze::ResultInvalidStorageId()); } /* Write the storage info data. */ R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { R_TRY(db.Add(storage_info.storage_type)); R_TRY(db.Add(storage_info.filesystem_type)); R_TRY(db.Add(storage_info.access_capability)); R_TRY(db.Add(storage_info.max_capacity)); R_TRY(db.Add(storage_info.free_space_in_bytes)); R_TRY(db.Add(storage_info.free_space_in_images)); R_TRY(db.AddString(storage_info.storage_description)); R_TRY(db.AddString(storage_info.volume_label)); R_SUCCEED(); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObjectHandles(PtpDataParser &dp) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Get the object ID the client requested enumeration for. */ u32 storage_id, object_format_code, association_object_handle; R_TRY(dp.Read(std::addressof(storage_id))); R_TRY(dp.Read(std::addressof(object_format_code))); R_TRY(dp.Read(std::addressof(association_object_handle))); R_TRY(dp.Finalize()); /* Handle top-level requests. */ if (storage_id == PtpGetObjectHandles_AllStorage) { storage_id = StorageId_SdmcFs; } /* Rewrite requests for enumerating storage directories. */ if (association_object_handle == PtpGetObjectHandles_RootParent) { association_object_handle = storage_id; } /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(association_object_handle); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Try to read the object as a directory. */ FsDir dir; R_TRY(m_fs.OpenDirectory(obj->GetName(), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, std::addressof(dir))); /* Ensure we maintain a clean state on exit. */ ON_SCOPE_EXIT { m_fs.CloseDirectory(std::addressof(dir)); }; /* Count how many entries are in the directory. */ s64 entry_count = 0; R_TRY(m_fs.GetDirectoryEntryCount(std::addressof(dir), std::addressof(entry_count))); /* Begin writing. */ R_TRY(db.AddDataHeader(m_request_header, sizeof(u32) + (entry_count * sizeof(u32)))); R_TRY(db.Add(static_cast(entry_count))); /* Enumerate the directory, writing results to the data builder as we progress. */ /* TODO: How should we handle the directory contents changing during enumeration? */ /* Is this even feasible to handle? */ while (true) { /* Get the next batch. */ s64 read_count = 0; R_TRY(m_fs.ReadDirectory(std::addressof(dir), std::addressof(read_count), DirectoryReadSize, g_dir_entries)); /* Write to output. */ for (s64 i = 0; i < read_count; i++) { u32 handle; R_TRY(m_object_database.CreateAndRegisterObjectId(obj->GetName(), g_dir_entries[i].name, obj->GetObjectId(), std::addressof(handle))); R_TRY(db.Add(handle)); } /* If we read fewer than the batch size, we're done. */ if (read_count < DirectoryReadSize) { break; } } /* Flush the data response. */ R_TRY(db.Commit()); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObjectInfo(PtpDataParser &dp) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Get the object ID the client requested info for. */ u32 object_id; R_TRY(dp.Read(std::addressof(object_id))); R_TRY(dp.Finalize()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Build info about the object. */ PtpObjectInfo object_info(DefaultObjectInfo); if (object_id == StorageId_SdmcFs) { /* The SD Card directory has some special properties. */ object_info.object_format = PtpObjectFormatCode_Association; object_info.association_type = PtpAssociationType_GenericFolder; object_info.filename = "SD Card"; } else { /* Figure out what type of object this is. */ FsDirEntryType entry_type; R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); /* Get the size, if we are requesting info about a file. */ s64 size = 0; if (entry_type == FsDirEntryType_File) { FsFile file; R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); /* Ensure we maintain a clean state on exit. */ ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(size))); } object_info.filename = std::strrchr(obj->GetName(), '/') + 1; object_info.object_compressed_size = size; object_info.parent_object = obj->GetParentId(); if (entry_type == FsDirEntryType_Dir) { object_info.object_format = PtpObjectFormatCode_Association; object_info.association_type = PtpAssociationType_GenericFolder; } else { object_info.object_format = PtpObjectFormatCode_Undefined; object_info.association_type = PtpAssociationType_Undefined; } } /* Write the object info data. */ R_TRY(db.WriteVariableLengthData(m_request_header, [&] () { R_TRY(db.Add(object_info.storage_id)); R_TRY(db.Add(object_info.object_format)); R_TRY(db.Add(object_info.protection_status)); R_TRY(db.Add(object_info.object_compressed_size)); R_TRY(db.Add(object_info.thumb_format)); R_TRY(db.Add(object_info.thumb_compressed_size)); R_TRY(db.Add(object_info.thumb_width)); R_TRY(db.Add(object_info.thumb_height)); R_TRY(db.Add(object_info.image_width)); R_TRY(db.Add(object_info.image_height)); R_TRY(db.Add(object_info.image_depth)); R_TRY(db.Add(object_info.parent_object)); R_TRY(db.Add(object_info.association_type)); R_TRY(db.Add(object_info.association_desc)); R_TRY(db.Add(object_info.sequence_number)); R_TRY(db.AddString(object_info.filename)); R_TRY(db.AddString(object_info.capture_date)); R_TRY(db.AddString(object_info.modification_date)); R_TRY(db.AddString(object_info.keywords)); R_SUCCEED(); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObject(PtpDataParser &dp) { PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Get the object ID the client requested. */ u32 object_id; R_TRY(dp.Read(std::addressof(object_id))); R_TRY(dp.Finalize()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Lock the object as a file. */ FsFile file; R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); /* Ensure we maintain a clean state on exit. */ ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; /* Get the file's size. */ s64 size = 0; R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(size))); /* Send the header and file size. */ R_TRY(db.AddDataHeader(m_request_header, size)); /* Begin reading the file, writing data to the builder as we progress. */ s64 offset = 0; while (true) { /* Get the next batch. */ u64 bytes_read; R_TRY(m_fs.ReadFile(std::addressof(file), offset, g_fs_buffer, FsBufferSize, FsReadOption_None, std::addressof(bytes_read))); offset += bytes_read; /* Write to output. */ R_TRY(db.AddBuffer(g_fs_buffer, bytes_read)); /* If we read fewer bytes than the batch size, we're done. */ if (bytes_read < FsBufferSize) { break; } } /* Flush the data response. */ R_TRY(db.Commit()); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::SendObjectInfo(PtpDataParser &rdp) { /* Get the storage ID and parent object and flush the request packet. */ u32 storage_id, parent_object; R_TRY(rdp.Read(std::addressof(storage_id))); R_TRY(rdp.Read(std::addressof(parent_object))); R_TRY(rdp.Finalize()); PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); PtpObjectInfo info(DefaultObjectInfo); /* Ensure we have a data header. */ PtpUsbBulkContainer data_header; R_TRY(dp.Read(std::addressof(data_header))); R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); /* Read in the object info. */ R_TRY(dp.Read(std::addressof(info.storage_id))); R_TRY(dp.Read(std::addressof(info.object_format))); R_TRY(dp.Read(std::addressof(info.protection_status))); R_TRY(dp.Read(std::addressof(info.object_compressed_size))); R_TRY(dp.Read(std::addressof(info.thumb_format))); R_TRY(dp.Read(std::addressof(info.thumb_compressed_size))); R_TRY(dp.Read(std::addressof(info.thumb_width))); R_TRY(dp.Read(std::addressof(info.thumb_height))); R_TRY(dp.Read(std::addressof(info.image_width))); R_TRY(dp.Read(std::addressof(info.image_height))); R_TRY(dp.Read(std::addressof(info.image_depth))); R_TRY(dp.Read(std::addressof(info.parent_object))); R_TRY(dp.Read(std::addressof(info.association_type))); R_TRY(dp.Read(std::addressof(info.association_desc))); R_TRY(dp.Read(std::addressof(info.sequence_number))); R_TRY(dp.ReadString(g_filename_str)); R_TRY(dp.ReadString(g_capture_date_str)); R_TRY(dp.ReadString(g_modification_date_str)); R_TRY(dp.ReadString(g_keywords_str)); R_TRY(dp.Finalize()); /* Rewrite requests for creating in storage directories. */ if (parent_object == PtpGetObjectHandles_RootParent) { parent_object = storage_id; } /* Check if we know about the parent object. If we don't, it's an error. */ auto * const parentobj = m_object_database.GetObjectById(parent_object); R_UNLESS(parentobj != nullptr, haze::ResultInvalidObjectId()); /* Make a new object with the intended name. */ PtpNewObjectInfo new_object_info; new_object_info.storage_id = StorageId_SdmcFs; new_object_info.parent_object_id = parent_object == storage_id ? 0 : parent_object; /* Create the object in the database. */ PtpObject *obj; R_TRY(m_object_database.CreateOrFindObject(parentobj->GetName(), g_filename_str, parentobj->GetObjectId(), std::addressof(obj))); /* Ensure we maintain a clean state on failure. */ ON_RESULT_FAILURE { m_object_database.DeleteObject(obj); }; /* Register the object with a new ID. */ m_object_database.RegisterObject(obj); new_object_info.object_id = obj->GetObjectId(); /* Create the object on the filesystem. */ if (info.object_format == PtpObjectFormatCode_Association) { R_TRY(m_fs.CreateDirectory(obj->GetName())); m_send_object_id = 0; } else { R_TRY(m_fs.CreateFile(obj->GetName(), 0, 0)); m_send_object_id = new_object_info.object_id; } /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok, new_object_info)); } Result PtpResponder::SendObject(PtpDataParser &rdp) { /* Reset SendObject object ID on exit. */ ON_SCOPE_EXIT { m_send_object_id = 0; }; R_TRY(rdp.Finalize()); PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); /* Ensure we have a data header. */ PtpUsbBulkContainer data_header; R_TRY(dp.Read(std::addressof(data_header))); R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(m_send_object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Lock the object as a file. */ FsFile file; R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Write | FsOpenMode_Append, std::addressof(file))); /* Ensure we maintain a clean state on exit. */ ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; /* Truncate the file after locking for write. */ s64 offset = 0; R_TRY(m_fs.SetFileSize(std::addressof(file), 0)); /* Expand to the needed size. */ if (data_header.length > sizeof(PtpUsbBulkContainer)) { R_TRY(m_fs.SetFileSize(std::addressof(file), data_header.length - sizeof(PtpUsbBulkContainer))); } /* Begin writing to the filesystem. */ while (true) { /* Read as many bytes as we can. */ u32 bytes_received; const Result read_res = dp.ReadBuffer(g_fs_buffer, FsBufferSize, std::addressof(bytes_received)); /* Write to the file. */ R_TRY(m_fs.WriteFile(std::addressof(file), offset, g_fs_buffer, bytes_received, 0)); offset += bytes_received; /* If we received fewer bytes than the batch size, we're done. */ if (haze::ResultEndOfTransmission::Includes(read_res)) { break; } R_TRY(read_res); } /* Truncate the file to the received size. */ R_TRY(m_fs.SetFileSize(std::addressof(file), offset)); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::DeleteObject(PtpDataParser &dp) { /* Get the object ID and flush the request packet. */ u32 object_id; R_TRY(dp.Read(std::addressof(object_id))); R_TRY(dp.Finalize()); /* Disallow deleting the storage root. */ R_UNLESS(object_id != StorageId_SdmcFs, haze::ResultInvalidObjectId()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Figure out what type of object this is. */ FsDirEntryType entry_type; R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); /* Remove the object from the filesystem. */ if (entry_type == FsDirEntryType_Dir) { R_TRY(m_fs.DeleteDirectoryRecursively(obj->GetName())); } else { R_TRY(m_fs.DeleteFile(obj->GetName())); } /* Remove the object from the database. */ m_object_database.DeleteObject(obj); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObjectPropsSupported(PtpDataParser &dp) { R_TRY(dp.Finalize()); PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); /* Write information about all object properties we can support. */ R_TRY(db.WriteVariableLengthData(m_request_header, [&] { R_RETURN(db.AddArray(SupportedObjectProperties, util::size(SupportedObjectProperties))); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObjectPropDesc(PtpDataParser &dp) { PtpObjectPropertyCode property_code; u16 object_format; R_TRY(dp.Read(std::addressof(property_code))); R_TRY(dp.Read(std::addressof(object_format))); R_TRY(dp.Finalize()); /* Ensure we have a valid property code before continuing. */ R_UNLESS(IsSupportedObjectPropertyCode(property_code), haze::ResultUnknownPropertyCode()); /* Begin writing information about the property code. */ PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); R_TRY(db.WriteVariableLengthData(m_request_header, [&] { R_TRY(db.Add(property_code)); /* Each property code corresponds to a different pattern, which contains the data type, */ /* whether the property can be set for an object, and the default value of the property. */ switch (property_code) { case PtpObjectPropertyCode_PersistentUniqueObjectIdentifier: { R_TRY(db.Add(PtpDataTypeCode_U128)); R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); R_TRY(db.Add(0)); } case PtpObjectPropertyCode_ObjectSize: { R_TRY(db.Add(PtpDataTypeCode_U64)); R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); R_TRY(db.Add(0)); } break; case PtpObjectPropertyCode_StorageId: case PtpObjectPropertyCode_ParentObject: { R_TRY(db.Add(PtpDataTypeCode_U32)); R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); R_TRY(db.Add(StorageId_SdmcFs)); } break; case PtpObjectPropertyCode_ObjectFormat: { R_TRY(db.Add(PtpDataTypeCode_U16)); R_TRY(db.Add(PtpPropertyGetSetFlag_Get)); R_TRY(db.Add(PtpObjectFormatCode_Undefined)); } break; case PtpObjectPropertyCode_ObjectFileName: { R_TRY(db.Add(PtpDataTypeCode_String)); R_TRY(db.Add(PtpPropertyGetSetFlag_GetSet)); R_TRY(db.AddString("")); } break; HAZE_UNREACHABLE_DEFAULT_CASE(); } /* Group code is a required part of the response, but doesn't seem to be used for anything. */ R_TRY(db.Add(PtpPropertyGroupCode_Default)); /* We don't use the form flag. */ R_TRY(db.Add(PtpPropertyFormFlag_None)); R_SUCCEED(); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::GetObjectPropValue(PtpDataParser &dp) { u32 object_id; PtpObjectPropertyCode property_code; R_TRY(dp.Read(std::addressof(object_id))); R_TRY(dp.Read(std::addressof(property_code))); R_TRY(dp.Finalize()); /* Disallow renaming the storage root. */ R_UNLESS(object_id != StorageId_SdmcFs, haze::ResultInvalidObjectId()); /* Ensure we have a valid property code before continuing. */ R_UNLESS(IsSupportedObjectPropertyCode(property_code), haze::ResultUnknownPropertyCode()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* Define helper for getting the object type. */ const auto GetObjectType = [&] (FsDirEntryType *out_entry_type) { R_RETURN(m_fs.GetEntryType(obj->GetName(), out_entry_type)); }; /* Define helper for getting the object size. */ const auto GetObjectSize = [&] (s64 *out_size) { *out_size = 0; /* Check if this is a directory. */ FsDirEntryType entry_type; R_TRY(GetObjectType(std::addressof(entry_type))); /* If it is, we're done. */ R_SUCCEED_IF(entry_type == FsDirEntryType_Dir); /* Otherwise, open as a file. */ FsFile file; R_TRY(m_fs.OpenFile(obj->GetName(), FsOpenMode_Read, std::addressof(file))); /* Ensure we maintain a clean state on exit. */ ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); }; R_RETURN(m_fs.GetFileSize(std::addressof(file), out_size)); }; /* Begin writing the requested object property. */ PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server)); R_TRY(db.WriteVariableLengthData(m_request_header, [&] { switch (property_code) { case PtpObjectPropertyCode_PersistentUniqueObjectIdentifier: { R_TRY(db.Add(object_id)); } break; case PtpObjectPropertyCode_ObjectSize: { s64 size; R_TRY(GetObjectSize(std::addressof(size))); R_TRY(db.Add(size)); } break; case PtpObjectPropertyCode_StorageId: { R_TRY(db.Add(StorageId_SdmcFs)); } break; case PtpObjectPropertyCode_ParentObject: { R_TRY(db.Add(obj->GetParentId())); } break; case PtpObjectPropertyCode_ObjectFormat: { FsDirEntryType entry_type; R_TRY(GetObjectType(std::addressof(entry_type))); R_TRY(db.Add(entry_type == FsDirEntryType_File ? PtpObjectFormatCode_Undefined : PtpObjectFormatCode_Association)); } break; case PtpObjectPropertyCode_ObjectFileName: { R_TRY(db.AddString(std::strrchr(obj->GetName(), '/') + 1)); } break; HAZE_UNREACHABLE_DEFAULT_CASE(); } R_SUCCEED(); })); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } Result PtpResponder::SetObjectPropValue(PtpDataParser &rdp) { u32 object_id; PtpObjectPropertyCode property_code; R_TRY(rdp.Read(std::addressof(object_id))); R_TRY(rdp.Read(std::addressof(property_code))); R_TRY(rdp.Finalize()); PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server)); /* Ensure we have a data header. */ PtpUsbBulkContainer data_header; R_TRY(dp.Read(std::addressof(data_header))); R_UNLESS(data_header.type == PtpUsbBulkContainerType_Data, haze::ResultUnknownRequestType()); R_UNLESS(data_header.code == m_request_header.code, haze::ResultOperationNotSupported()); R_UNLESS(data_header.trans_id == m_request_header.trans_id, haze::ResultOperationNotSupported()); /* Ensure we have a valid property code before continuing. */ R_UNLESS(property_code == PtpObjectPropertyCode_ObjectFileName, haze::ResultUnknownPropertyCode()); /* Check if we know about the object. If we don't, it's an error. */ auto * const obj = m_object_database.GetObjectById(object_id); R_UNLESS(obj != nullptr, haze::ResultInvalidObjectId()); /* We are reading a file name. */ R_TRY(dp.ReadString(g_filename_str)); R_TRY(dp.Finalize()); /* Ensure we can actually process the new name. */ const bool is_empty = g_filename_str[0] == '\x00'; const bool contains_slashes = std::strchr(g_filename_str, '/') != nullptr; R_UNLESS(!is_empty && !contains_slashes, haze::ResultInvalidPropertyValue()); /* Add a new object in the database with the new name. */ PtpObject *newobj; { /* Find the last path separator in the existing object name. */ char *pathsep = std::strrchr(obj->m_name, '/'); HAZE_ASSERT(pathsep != nullptr); /* Temporarily mark the path separator as null to facilitate processing. */ *pathsep = '\x00'; ON_SCOPE_EXIT { *pathsep = '/'; }; R_TRY(m_object_database.CreateOrFindObject(obj->GetName(), g_filename_str, obj->GetParentId(), std::addressof(newobj))); } { /* Ensure we maintain a clean state on failure. */ ON_RESULT_FAILURE { if (!newobj->GetIsRegistered()) { /* Only delete if the object was not registered. */ /* Otherwise, we would remove an object that still exists. */ m_object_database.DeleteObject(newobj); } }; /* Get the old object type. */ FsDirEntryType entry_type; R_TRY(m_fs.GetEntryType(obj->GetName(), std::addressof(entry_type))); /* Attempt to rename the object on the filesystem. */ if (entry_type == FsDirEntryType_Dir) { R_TRY(m_fs.RenameDirectory(obj->GetName(), newobj->GetName())); } else { R_TRY(m_fs.RenameFile(obj->GetName(), newobj->GetName())); } } /* Unregister and free the old object. */ m_object_database.DeleteObject(obj); /* Register the new object. */ m_object_database.RegisterObject(newobj, object_id); /* Write the success response. */ R_RETURN(this->WriteResponse(PtpResponseCode_Ok)); } }