Compare commits

..

46 Commits

Author SHA1 Message Date
Michael Scire
8a9eb85e05 git subrepo push libraries
subrepo:
  subdir:   "libraries"
  merged:   "132558c33"
upstream:
  origin:   "https://github.com/Atmosphere-NX/Atmosphere-libs"
  branch:   "master"
  commit:   "132558c33"
git-subrepo:
  version:  "0.4.1"
  origin:   "???"
  commit:   "???"
2023-10-12 09:23:31 -07:00
Michael Scire
d389ef639e docs: add changelog for 1.6.0 2023-10-12 09:19:26 -07:00
Michael Scire
719858ad18 git subrepo push emummc
subrepo:
  subdir:   "emummc"
  merged:   "9513a5412"
upstream:
  origin:   "https://github.com/m4xw/emummc"
  branch:   "develop"
  commit:   "9513a5412"
git-subrepo:
  version:  "0.4.1"
  origin:   "???"
  commit:   "???"
2023-10-12 09:02:30 -07:00
Michael Scire
e4d08ae0c5 erpt: amend min-version for latest CreateReportWithAttachments 2023-10-12 08:55:58 -07:00
Michael Scire
0c063db926 loader: add usb3 patches for 17.0.0 2023-10-12 08:55:58 -07:00
Michael Scire
02e987819b ncm: work around change in Nintendo save handling behavior
Static save files do not require an entry in the save data indexer to mount.
Prior to 17.0.0, save data files were considered static if userid was 0.
In 17.0.0+, only 8000000000000000 is static.

However, some users using cfw do not have an entry for 8000000000000120 in the indexer,
for various reasons (but mostly manual nand-restore, I think). Thus, on boot of 17.0.0+,
FS will say 8000000000000120 is not present (not in indexer), and NCM will create it anew.

The 8000000000000120 save will then be empty, and then the firmware can't boot.

To workaround this, logic has been re-enabled on 17.0.0+ for building the content meta database.
Thus, if the user encounters this error, the 8000000000000120 save will be emptied, but then
it will be automatically reconstructed, fixing the problem.
2023-10-12 08:55:58 -07:00
Michael Scire
2ec3e141c7 bpc.mitm/exo: support pmic reboot/shutdown on mariko (thanks @CTCaer) 2023-10-12 08:55:58 -07:00
Michael Scire
71d0274884 erpt: remove deprecated fields, they didn't actually change IDs, just the mapping between id and name table index 2023-10-12 08:55:58 -07:00
Michael Scire
05259b7519 emummc: fix offsets for 17.0.0 2023-10-12 08:55:58 -07:00
Michael Scire
59a24fa646 fusee: support parsing 17.0.0+ INI 2023-10-12 08:55:58 -07:00
Michael Scire
f5b2eab4a8 exo: fix up new titlekey option extents 2023-10-12 08:55:58 -07:00
Michael Scire
e96e1063e2 jpegdec: stop bundling (TODO post-prerelease) 2023-10-12 08:55:58 -07:00
Michael Scire
aa170a72a9 erpt: Add basic (TODO-impl post-prerelease) support for 17.0.0 changes 2023-10-12 08:55:58 -07:00
Michael Scire
9d4cb685a7 fs: update OpenCodeFileSystem abi for 17.0.0 2023-10-12 08:55:58 -07:00
Michael Scire
c95741142e ncm: update for new 17.0.0 apis 2023-10-12 08:55:58 -07:00
Michael Scire
ef9b111bbf emummc: update for 17.0.0 2023-10-12 08:55:58 -07:00
Michael Scire
114b82284d exo/spl: Add new EsCommonKeyType 2023-10-12 08:55:58 -07:00
Michael Scire
c5d7ca5159 fusee/exo: implement the usual changes for new firmware support 2023-10-12 08:55:58 -07:00
Michael Scire
6d0bf70783 kern: fix assert usage in process load 2023-10-12 08:55:58 -07:00
Michael Scire
aba6ca7329 kern: bump supported version to 17.x 2023-10-12 08:55:58 -07:00
Michael Scire
06a840e550 kern: fix operation type enum-value whoops 2023-10-12 08:55:58 -07:00
Michael Scire
11c02e22e0 kern: implement support for applying relr relocations 2023-10-12 08:55:58 -07:00
Michael Scire
f93aea4c06 kern: split Process/Thread exit to separate WorkerTaskManagers 2023-10-12 08:55:58 -07:00
Michael Scire
4ddfb6183c kern: split out GetInstructionDataUserMode in exception handler 2023-10-12 08:55:58 -07:00
Michael Scire
3737151a2f kern: Add special-case for InvalidateProcessDataCache on current process 2023-10-12 08:55:58 -07:00
Michael Scire
2a4d68f916 kern: KPageTable: remove MapFirst operation, replace with MapFirstGroup 2023-10-12 08:55:58 -07:00
Michael Scire
7b523cfc8d kern: note OnFinalize calls in KPageTable::Finalize 2023-10-12 08:55:58 -07:00
Michael Scire
39a95d4023 kern: implement new default application system resource field in KProcess 2023-10-12 08:55:58 -07:00
Michael Scire
2c5002ce50 kern: update KMemoryRegionType values for new ids + SecureUnknown region 2023-10-12 08:55:58 -07:00
Michael Scire
b7384a8667 kern: KSupervisorPageTable now checks wxn instead of setting it 2023-10-12 08:55:58 -07:00
Michael Scire
85b5f20395 kern: KPageTable::Initialize no longer takes unused process id 2023-10-12 08:55:58 -07:00
Michael Scire
ad5bd81d3f kern: implement PermissionLock, update KPageTableBase attribute/alignment checks 2023-10-12 08:55:58 -07:00
Michael Scire
777b6d285c kern: KPageTableBase::CheckMemoryState now invokes a helper 2023-10-12 08:55:58 -07:00
Michael Scire
ae2c25e9c8 kern: update KMemoryState, remove bijection (separate IoRegister/IoMemory) 2023-10-12 08:55:58 -07:00
Michael Scire
3b8f65d502 kern: update initial process load logic to do per-segment mapping/decompression 2023-10-12 08:55:58 -07:00
Michael Scire
cfd2d5b012 kern: clear new pages in init page allocator, not init page table 2023-10-12 08:55:58 -07:00
Michael Scire
c72ba35684 kern: add speculation barriers after eret 2023-10-12 08:55:58 -07:00
Michael Scire
ec96203cb7 kern: remove unnecessary fields from InitArgs (0x80 -> 0x40) 2023-10-12 08:55:58 -07:00
Michael Scire
1491a7b159 kern: on second thought, move vectors back to end of text 2023-10-12 08:55:58 -07:00
Michael Scire
0daef4a6e8 kern/ldr: move crt0 into .rodata 2023-10-12 08:55:58 -07:00
Michael Scire
4ca3c44e5f kern: pass ini1 size from loader to kernel, remove slab memset from init0 2023-10-12 08:55:58 -07:00
Michael Scire
add4b3fdc3 utils: update erpt script 2023-10-12 08:55:58 -07:00
Liam
159f8d384b dmnt.gen2: enable attach to arbitrary program id 2023-10-11 19:50:09 -07:00
Liam
92a8c8eb88 haze: implement android operations 2023-10-11 18:57:49 -07:00
Liam
9e0daff46e haze: split operations by type 2023-10-11 18:57:49 -07:00
Liam
6b72dbd22d haze: refactor constant use for cleaner separation 2023-10-11 18:57:49 -07:00
15 changed files with 1258 additions and 906 deletions

View File

@@ -1,4 +1,20 @@
# Changelog
## 1.6.0
+ Basic support was added for 17.0.0.
+ The console should boot and atmosphère should be fully functional. However, not all modules have been fully updated to reflect the latest changes.
+ There shouldn't be anything user visible resulting from this, but it will be addressed in a soon-to-come atmosphère update.
+ `exosphère` was updated to reflect the latest official secure monitor behavior.
+ `mesosphère` was updated to reflect the latest official kernel behavior.
+ `ncm` was updated to reflect the latest official behavior.
+ `erpt` was partially updated to support the latest official behavior.
+ Atmosphere's gdbstub now supports waiting to attach to a specific program id on launch (as opposed to any application).
+ The monitor command for this is `monitor wait <hex program id>`, where program id can optionally have an `0x` prefix.
+ Support was added to `haze` for editing files in-place and performing 64-bit transfers (files larger than 4 GB).
+ `bpc.mitm` was enabled on Mariko units, and now triggers pmic-based shutdowns/reboots (thanks @CTCaer).
+ This should cause the console to no longer wake ~15 seconds after shutdown on Mariko.
+ A number of minor issues were fixed and improvements were made, including:
+ A workaround was added for a change in 17.0.0 that would cause consoles which had previously re-built their SYSTEM partition to brick on update-to-17.0.0.
+ General system stability improvements to enhance the user's experience.
## 1.5.5
+ Support was added for 16.1.0.
+ General system stability improvements to enhance the user's experience.

4
emummc/.gitrepo vendored
View File

@@ -6,7 +6,7 @@
[subrepo]
remote = https://github.com/m4xw/emummc
branch = develop
commit = 30205111ee375bef96f0f76cb6a3130a2f0fc85c
parent = 81e9154a52a976f85317bddd0131426599d26a62
commit = 9513a5412057b1f1bc44ed8e717c57c726763a88
parent = e4d08ae0c5342cdb0875d164522a63ec9d233052
method = merge
cmdver = 0.4.1

View File

@@ -6,7 +6,7 @@
[subrepo]
remote = https://github.com/Atmosphere-NX/Atmosphere-libs
branch = master
commit = c3dc418a28e390bc57426016aa2c9e7e87d7a584
parent = e488b6ee478f5b3a0380e75e4b468e1e4b1d816f
commit = 132558c33865f6a21f06caa31bdfc6b1f92bd9b2
parent = d389ef639ed2988325523f1d95f90030a3370541
method = merge
cmdver = 0.4.1

View File

@@ -28,6 +28,7 @@ namespace ams::pm::dmnt {
Result GetProcessId(os::ProcessId *out_process_id, const ncm::ProgramId program_id);
Result GetApplicationProcessId(os::ProcessId *out_process_id);
Result HookToCreateApplicationProcess(os::NativeHandle *out_handle);
Result HookToCreateProcess(os::NativeHandle *out_handle, const ncm::ProgramId program_id);
Result AtmosphereGetProcessInfo(os::NativeHandle *out_handle, ncm::ProgramLocation *out_loc, cfg::OverrideStatus *out_status, os::ProcessId process_id);
#if defined(ATMOSPHERE_OS_HORIZON)

View File

@@ -44,6 +44,13 @@ namespace ams::pm::dmnt {
R_SUCCEED();
}
Result HookToCreateProcess(os::NativeHandle *out_handle, const ncm::ProgramId program_id) {
Event evt;
R_TRY(pmdmntHookToCreateProcess(std::addressof(evt), static_cast<u64>(program_id)));
*out_handle = evt.revent;
R_SUCCEED();
}
Result AtmosphereGetProcessInfo(os::NativeHandle *out_handle, ncm::ProgramLocation *out_loc, cfg::OverrideStatus *out_status, os::ProcessId process_id) {
*out_handle = os::InvalidNativeHandle;
*out_loc = {};

View File

@@ -2105,9 +2105,24 @@ namespace ams::dmnt {
ParsePrefix(command, "0x");
/* Decode program id. */
const u64 program_id = DecodeHex(command);
const ncm::ProgramId program_id(DecodeHex(command));
AppendReplyFormat(reply_cur, reply_end, "[TODO] wait for program id 0x%lx\n", program_id);
/* Wait for the process to be created. */
{
/* Get hook to creation of process with program id. */
os::NativeHandle h;
R_ABORT_UNLESS(pm::dmnt::HookToCreateProcess(std::addressof(h), program_id));
/* Wait for event. */
os::SystemEvent hook_event(h, true, os::InvalidNativeHandle, false, os::EventClearMode_AutoClear);
hook_event.Wait();
}
/* Get process id. */
R_ABORT_UNLESS(pm::dmnt::GetProcessId(std::addressof(m_wait_process_id), program_id));
/* Note that we're attaching. */
AppendReplyFormat(reply_cur, reply_end, "Send `attach 0x%lx` to attach.\n", m_wait_process_id.value);
} else {
std::memcpy(m_reply_cur, command, std::strlen(command) + 1);
AppendReplyFormat(reply_cur, reply_end, "Unknown command `%s`\n", m_reply_cur);

View File

@@ -42,6 +42,4 @@ namespace haze {
using Result = ::ams::Result;
static constexpr u32 UsbBulkPacketBufferSize = 1_MB;
}

View File

@@ -72,6 +72,11 @@ namespace haze {
PtpOperationCode_GetFilesystemManifest = 0x1023,
PtpOperationCode_GetStreamInfo = 0x1024,
PtpOperationCode_GetStream = 0x1025,
PtpOperationCode_AndroidGetPartialObject64 = 0x95c1,
PtpOperationCode_AndroidSendPartialObject = 0x95c2,
PtpOperationCode_AndroidTruncateObject = 0x95c3,
PtpOperationCode_AndroidBeginEditObject = 0x95c4,
PtpOperationCode_AndroidEndEditObject = 0x95c5,
PtpOperationCode_MtpGetObjectPropsSupported = 0x9801,
PtpOperationCode_MtpGetObjectPropDesc = 0x9802,
PtpOperationCode_MtpGetObjectPropValue = 0x9803,

View File

@@ -19,6 +19,7 @@
#include <haze/async_usb_server.hpp>
#include <haze/ptp_object_heap.hpp>
#include <haze/ptp_object_database.hpp>
#include <haze/ptp_responder_types.hpp>
namespace haze {
@@ -30,12 +31,13 @@ namespace haze {
FileSystemProxy m_fs;
PtpUsbBulkContainer m_request_header;
PtpObjectHeap *m_object_heap;
PtpBuffers* m_buffers;
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() { /* ... */ }
constexpr explicit PtpResponder() : m_usb_server(), m_fs(), m_request_header(), m_object_heap(), m_buffers(), m_send_object_id(), m_session_open(), m_object_database() { /* ... */ }
Result Initialize(EventReactor *reactor, PtpObjectHeap *object_heap);
void Finalize();
@@ -48,10 +50,14 @@ namespace haze {
Result HandleCommandRequest(PtpDataParser &dp);
void ForceCloseSession();
template <typename Data>
Result WriteResponse(PtpResponseCode code, Data &&data);
Result WriteResponse(PtpResponseCode code, const void* data, size_t size);
Result WriteResponse(PtpResponseCode code);
template <typename Data> requires (util::is_pod<Data>::value)
Result WriteResponse(PtpResponseCode code, const Data &data) {
R_RETURN(this->WriteResponse(code, std::addressof(data), sizeof(data)));
}
/* PTP operations. */
Result GetDeviceInfo(PtpDataParser &dp);
Result OpenSession(PtpDataParser &dp);
@@ -65,6 +71,13 @@ namespace haze {
Result SendObject(PtpDataParser &dp);
Result DeleteObject(PtpDataParser &dp);
/* Android operations. */
Result GetPartialObject64(PtpDataParser &dp);
Result SendPartialObject(PtpDataParser &dp);
Result TruncateObject(PtpDataParser &dp);
Result BeginEditObject(PtpDataParser &dp);
Result EndEditObject(PtpDataParser &dp);
/* MTP operations. */
Result GetObjectPropsSupported(PtpDataParser &dp);
Result GetObjectPropDesc(PtpDataParser &dp);

View File

@@ -0,0 +1,182 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <haze.hpp>
namespace haze {
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,
PtpOperationCode_AndroidGetPartialObject64,
PtpOperationCode_AndroidSendPartialObject,
PtpOperationCode_AndroidTruncateObject,
PtpOperationCode_AndroidBeginEditObject,
PtpOperationCode_AndroidEndEditObject,
};
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 u32 UsbBulkPacketBufferSize = 1_MB;
constexpr u64 FsBufferSize = UsbBulkPacketBufferSize;
constexpr s64 DirectoryReadSize = 32;
struct PtpBuffers {
char filename_string_buffer[PtpStringMaxLength + 1];
char capture_date_string_buffer[PtpStringMaxLength + 1];
char modification_date_string_buffer[PtpStringMaxLength + 1];
char keywords_string_buffer[PtpStringMaxLength + 1];
FsDirectoryEntry file_system_entry_buffer[DirectoryReadSize];
u8 file_system_data_buffer[FsBufferSize];
alignas(4_KB) u8 usb_bulk_write_buffer[UsbBulkPacketBufferSize];
alignas(4_KB) u8 usb_bulk_read_buffer[UsbBulkPacketBufferSize];
};
}

View File

@@ -37,5 +37,6 @@ namespace haze {
R_DEFINE_ERROR_RESULT(UnknownRequestType, 13);
R_DEFINE_ERROR_RESULT(UnknownPropertyCode, 14);
R_DEFINE_ERROR_RESULT(InvalidPropertyValue, 15);
R_DEFINE_ERROR_RESULT(InvalidArgument, 16);
}

View File

@@ -16,167 +16,22 @@
#include <haze.hpp>
#include <haze/ptp_data_builder.hpp>
#include <haze/ptp_data_parser.hpp>
#include <haze/ptp_responder_types.hpp>
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;
PtpBuffers *GetBuffers() {
static constinit PtpBuffers buffers = {};
return std::addressof(buffers);
}
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;
m_buffers = GetBuffers();
/* Configure fs proxy. */
m_fs.Initialize(reactor, fsdevGetDeviceFileSystem("sdmc"));
@@ -236,6 +91,9 @@ namespace haze {
R_CATCH(haze::ResultInvalidPropertyValue) {
R_TRY(this->WriteResponse(PtpResponseCode_MtpInvalidObjectPropValue));
}
R_CATCH(haze::ResultInvalidArgument) {
R_TRY(this->WriteResponse(PtpResponseCode_GeneralError));
}
R_CATCH_MODULE(fs) {
/* Errors from fs are typically recoverable. */
R_TRY(this->WriteResponse(PtpResponseCode_GeneralError));
@@ -246,7 +104,7 @@ namespace haze {
}
Result PtpResponder::HandleRequestImpl() {
PtpDataParser dp(g_bulk_read_buffer, std::addressof(m_usb_server));
PtpDataParser dp(m_buffers->usb_bulk_read_buffer, std::addressof(m_usb_server));
R_TRY(dp.Read(std::addressof(m_request_header)));
switch (m_request_header.type) {
@@ -276,6 +134,11 @@ namespace haze {
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;
case PtpOperationCode_AndroidGetPartialObject64: R_RETURN(this->GetPartialObject64(dp)); break;
case PtpOperationCode_AndroidSendPartialObject: R_RETURN(this->SendPartialObject(dp)); break;
case PtpOperationCode_AndroidTruncateObject: R_RETURN(this->TruncateObject(dp)); break;
case PtpOperationCode_AndroidBeginEditObject: R_RETURN(this->BeginEditObject(dp)); break;
case PtpOperationCode_AndroidEndEditObject: R_RETURN(this->EndEditObject(dp)); break;
default: R_THROW(haze::ResultOperationNotSupported());
}
}
@@ -287,756 +150,17 @@ namespace haze {
}
}
template <typename Data>
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));
Result PtpResponder::WriteResponse(PtpResponseCode code, const void* data, size_t size) {
PtpDataBuilder db(m_buffers->usb_bulk_write_buffer, std::addressof(m_usb_server));
R_TRY(db.AddResponseHeader(m_request_header, code, size));
R_TRY(db.AddBuffer(reinterpret_cast<const u8*>(data), size));
R_RETURN(db.Commit());
}
Result PtpResponder::WriteResponse(PtpResponseCode code) {
PtpDataBuilder db(g_bulk_write_buffer, std::addressof(m_usb_server));
PtpDataBuilder db(m_buffers->usb_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<u32>(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<u128>(0));
}
case PtpObjectPropertyCode_ObjectSize:
{
R_TRY(db.Add(PtpDataTypeCode_U64));
R_TRY(db.Add(PtpPropertyGetSetFlag_Get));
R_TRY(db.Add<u64>(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<u128>(object_id));
}
break;
case PtpObjectPropertyCode_ObjectSize:
{
s64 size;
R_TRY(GetObjectSize(std::addressof(size)));
R_TRY(db.Add<u64>(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));
}
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <haze.hpp>
#include <haze/ptp_data_builder.hpp>
#include <haze/ptp_data_parser.hpp>
#include <haze/ptp_responder_types.hpp>
namespace haze {
Result PtpResponder::GetPartialObject64(PtpDataParser &dp) {
PtpDataBuilder db(m_buffers->usb_bulk_write_buffer, std::addressof(m_usb_server));
/* Get the object ID, offset, and size for the file we want to read. */
u32 object_id, size;
u64 offset;
R_TRY(dp.Read(std::addressof(object_id)));
R_TRY(dp.Read(std::addressof(offset)));
R_TRY(dp.Read(std::addressof(size)));
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 file_size = 0;
R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(file_size)));
/* Ensure the requested offset and size are within range. */
R_UNLESS(offset + size > offset, haze::ResultInvalidArgument());
R_UNLESS(static_cast<u64>(file_size) <= offset + size, haze::ResultInvalidArgument());
/* Send the header and data size. */
R_TRY(db.AddDataHeader(m_request_header, size));
/* Begin reading the file, writing data to the builder as we progress. */
s64 size_remaining = size;
while (true) {
/* Get the next batch. */
u64 bytes_to_read = std::min<s64>(FsBufferSize, size_remaining);
u64 bytes_read;
R_TRY(m_fs.ReadFile(std::addressof(file), offset, m_buffers->file_system_data_buffer, bytes_to_read, FsReadOption_None, std::addressof(bytes_read)));
size_remaining -= bytes_read;
offset += bytes_read;
/* Write to output. */
R_TRY(db.AddBuffer(m_buffers->file_system_data_buffer, bytes_read));
/* If we read fewer bytes than the batch size, or have read enough data, we're done. */
if (bytes_read < FsBufferSize || size_remaining == 0) {
break;
}
}
/* Flush the data response. */
R_TRY(db.Commit());
/* Write the success response. */
R_RETURN(this->WriteResponse(PtpResponseCode_Ok));
}
Result PtpResponder::SendPartialObject(PtpDataParser &rdp) {
/* Get the object ID, offset, and size for the file we want to write. */
u32 object_id, size;
u64 offset;
R_TRY(rdp.Read(std::addressof(object_id)));
R_TRY(rdp.Read(std::addressof(size)));
R_TRY(rdp.Read(std::addressof(offset)));
R_TRY(rdp.Finalize());
/* 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)); };
/* Get the file's size. */
s64 file_size = 0;
R_TRY(m_fs.GetFileSize(std::addressof(file), std::addressof(file_size)));
/* Ensure the requested offset and size are within range. */
R_UNLESS(offset + size > offset, haze::ResultInvalidArgument());
R_UNLESS(static_cast<u64>(file_size) <= offset, haze::ResultInvalidArgument());
/* Prepare a data parser for the data we are about to receive. */
PtpDataParser dp(m_buffers->usb_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());
/* Begin writing to the filesystem. */
s64 size_remaining = size;
while (true) {
/* Read as many bytes as we can. */
u32 bytes_received;
const Result read_res = dp.ReadBuffer(m_buffers->file_system_data_buffer, FsBufferSize, std::addressof(bytes_received));
/* Write to the file. */
u32 bytes_to_write = std::min<s64>(size_remaining, bytes_received);
R_TRY(m_fs.WriteFile(std::addressof(file), offset, m_buffers->file_system_data_buffer, bytes_to_write, 0));
size_remaining -= bytes_to_write;
offset += bytes_to_write;
/* If we received fewer bytes than the batch size, or have written enough data, we're done. */
if (haze::ResultEndOfTransmission::Includes(read_res) || size_remaining == 0) {
break;
}
R_TRY(read_res);
}
/* Write the success response. */
R_RETURN(this->WriteResponse(PtpResponseCode_Ok));
}
Result PtpResponder::TruncateObject(PtpDataParser &dp) {
/* Get the object ID and size for the file we want to truncate. */
u32 object_id;
u64 size;
R_TRY(dp.Read(std::addressof(object_id)));
R_TRY(dp.Read(std::addressof(size)));
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_Write, std::addressof(file)));
/* Ensure we maintain a clean state on exit. */
ON_SCOPE_EXIT { m_fs.CloseFile(std::addressof(file)); };
/* Truncate the file. */
R_TRY(m_fs.SetFileSize(std::addressof(file), size));
/* Write the success response. */
R_RETURN(this->WriteResponse(PtpResponseCode_Ok));
}
Result PtpResponder::BeginEditObject(PtpDataParser &dp) {
/* Get the object ID we are going to begin editing. */
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());
/* We don't implement transactions, so write the success response. */
R_RETURN(this->WriteResponse(PtpResponseCode_Ok));
}
Result PtpResponder::EndEditObject(PtpDataParser &dp) {
/* Get the object ID we are going to finish editing. */
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());
/* We don't implement transactions, so write the success response. */
R_RETURN(this->WriteResponse(PtpResponseCode_Ok));
}
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <haze.hpp>
#include <haze/ptp_data_builder.hpp>
#include <haze/ptp_data_parser.hpp>
#include <haze/ptp_responder_types.hpp>
namespace haze {
Result PtpResponder::GetObjectPropsSupported(PtpDataParser &dp) {
R_TRY(dp.Finalize());
PtpDataBuilder db(m_buffers->usb_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(m_buffers->usb_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<u128>(0));
}
case PtpObjectPropertyCode_ObjectSize:
{
R_TRY(db.Add(PtpDataTypeCode_U64));
R_TRY(db.Add(PtpPropertyGetSetFlag_Get));
R_TRY(db.Add<u64>(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(m_buffers->usb_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<u128>(object_id));
}
break;
case PtpObjectPropertyCode_ObjectSize:
{
s64 size;
R_TRY(GetObjectSize(std::addressof(size)));
R_TRY(db.Add<u64>(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(m_buffers->usb_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(m_buffers->filename_string_buffer));
R_TRY(dp.Finalize());
/* Ensure we can actually process the new name. */
const bool is_empty = m_buffers->filename_string_buffer[0] == '\x00';
const bool contains_slashes = std::strchr(m_buffers->filename_string_buffer, '/') != 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(), m_buffers->filename_string_buffer, 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));
}
}

View File

@@ -0,0 +1,507 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <haze.hpp>
#include <haze/ptp_data_builder.hpp>
#include <haze/ptp_data_parser.hpp>
#include <haze/ptp_responder_types.hpp>
namespace haze {
Result PtpResponder::GetDeviceInfo(PtpDataParser &dp) {
PtpDataBuilder db(m_buffers->usb_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(m_buffers->usb_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(m_buffers->usb_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(m_buffers->usb_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<u32>(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, m_buffers->file_system_entry_buffer));
/* Write to output. */
for (s64 i = 0; i < read_count; i++) {
const char *name = m_buffers->file_system_entry_buffer[i].name;
u32 handle;
R_TRY(m_object_database.CreateAndRegisterObjectId(obj->GetName(), 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(m_buffers->usb_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(m_buffers->usb_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, m_buffers->file_system_data_buffer, FsBufferSize, FsReadOption_None, std::addressof(bytes_read)));
offset += bytes_read;
/* Write to output. */
R_TRY(db.AddBuffer(m_buffers->file_system_data_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(m_buffers->usb_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(m_buffers->filename_string_buffer));
R_TRY(dp.ReadString(m_buffers->capture_date_string_buffer));
R_TRY(dp.ReadString(m_buffers->modification_date_string_buffer));
R_TRY(dp.ReadString(m_buffers->keywords_string_buffer));
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(), m_buffers->filename_string_buffer, 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(m_buffers->usb_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(m_buffers->file_system_data_buffer, FsBufferSize, std::addressof(bytes_received));
/* Write to the file. */
R_TRY(m_fs.WriteFile(std::addressof(file), offset, m_buffers->file_system_data_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));
}
}