From b95ea4bb5f28624285dfec197388921df44bf393 Mon Sep 17 00:00:00 2001 From: Felipe <54341095+Pipe530@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:44:28 -0300 Subject: [PATCH] Support mountable-content DLC (addcont with real content) (#1581) Support mountable-content DLC (addcont) and fix PlayGo chunk count query --- src/kernel/fileSystem.cpp | 10 ++++++++-- src/libs/libAmpr.cpp | 22 ++++++++++++++++++++++ src/libs/libAppContent.cpp | 25 ++++++++++++++++++++----- src/libs/libPlayGo.cpp | 10 ++++++++-- tests/KernelFileSystemTests.cpp | 9 ++++++--- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index 17e9f0d6b..b44f64fea 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -338,14 +338,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) { Common::LockGuard lock(m_mutex); + // Guests join mount points and names with extra slashes (e.g. "/addcont0//file"). + std::string normalized = Common::FixFilenameSlash(mounted_name); + normalized.erase(std::unique(normalized.begin(), normalized.end(), + [](char a, char b) { return a == '/' && b == '/'; }), + normalized.end()); + // Match the entire guest path so a mount root works with or without a trailing slash. - const auto mounted_path = Common::FixDirectorySlash(mounted_name); + const auto mounted_path = Common::FixDirectorySlash(normalized); const auto it = std::find_if( m_mount_pairs.begin(), m_mount_pairs.end(), [&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); }); if (it != m_mount_pairs.end()) { const auto& p = *it; - auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size()); + auto rel_path = Common::RemoveFirst(normalized, p.point.size()); while (rel_path.starts_with('/')) { rel_path = Common::RemoveFirst(rel_path, 1); } diff --git a/src/libs/libAmpr.cpp b/src/libs/libAmpr.cpp index 276c76a8f..de96d6c75 100644 --- a/src/libs/libAmpr.cpp +++ b/src/libs/libAmpr.cpp @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include @@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi return OK; } +// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results. +void InvalidateFailedResolves() { + std::scoped_lock lock(g_mutex); + for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) { + it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it); + } +} + static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) { if (guest_path == nullptr || guest_path[0] == '\0') { return LibKernel::KERNEL_ERROR_EINVAL; @@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) log_missing = true; } } + if (!log_missing && info.result == OK && path.starts_with("/addcont")) { + LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path, + Common::PathToString(info.host_path).c_str(), info.file_size); + } if (log_missing) { LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str()); } @@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) { uint64_t bytes_read = 0; auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination, command.size, &bytes_read); + static std::atomic s_logged_dlc_reads {0}; + if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) { + LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64 + ", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n", + command.file_id, command.file_offset, command.size, bytes_read, + static_cast(result)); + } if (result != OK) { LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n", command.file_id, static_cast(result), Common::PathToString(host_path).c_str()); diff --git a/src/libs/libAppContent.cpp b/src/libs/libAppContent.cpp index ab956c78b..287b46a6b 100644 --- a/src/libs/libAppContent.cpp +++ b/src/libs/libAppContent.cpp @@ -22,13 +22,16 @@ namespace Libs { +namespace AprShared { +void InvalidateFailedResolves(); +} + LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1); namespace AppContent { constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3; constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */ -constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */ constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */ constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */ constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */ @@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic for (const auto& m: g_addcont_mounts) { if (m.label == entry->entitlement_label) { - return APP_CONTENT_ERROR_BUSY; + const auto guest_point = m.point.substr(0, m.point.size() - 1); + std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1); + LOGF("\t already mounted %s\n", guest_point.c_str()); + return OK; } } @@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic LOGF("\t mounted %s -> %s\n", mount.point.c_str(), Common::PathToString(entry->mount_dir).c_str()); - std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1); + AprShared::InvalidateFailedResolves(); + + // Titles scan /download0/pdlc for downloaded content; it must exist even when empty. + if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) { + std::filesystem::create_directories(pdlc, ec); + } + + const auto guest_point = mount.point.substr(0, mount.point.size() - 1); + std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1); g_addcont_mounts.push_back(std::move(mount)); return OK; @@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi return APP_CONTENT_ERROR_NOT_MOUNTED; } - const std::string point(mount_point->data, - strnlen(mount_point->data, sizeof(mount_point->data))); + const std::string point = Common::FixDirectorySlash( + std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data)))); std::lock_guard lock(g_addcont_mutex); @@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi } LibKernel::FileSystem::Umount(point); g_addcont_mounts.erase(it); + AprShared::InvalidateFailedResolves(); return OK; } diff --git a/src/libs/libPlayGo.cpp b/src/libs/libPlayGo.cpp index 94f0093bd..8cdc33a3c 100644 --- a/src/libs/libPlayGo.cpp +++ b/src/libs/libPlayGo.cpp @@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list, if (out_entries == nullptr) { return PLAYGO_ERROR_BAD_POINTER; } - if (number_of_entries != 0 && out_chunk_id_list == nullptr) { - return PLAYGO_ERROR_BAD_POINTER; + if (out_chunk_id_list != nullptr && number_of_entries == 0) { + return PLAYGO_ERROR_BAD_SIZE; } ensure_chunks_loaded(); + // A null output buffer asks for the total chunk count. + if (out_chunk_id_list == nullptr) { + *out_entries = g_chunks_num; + return OK; + } + const uint32_t entries = std::min(number_of_entries, g_chunks_num); for (uint32_t i = 0; i < entries; i++) { out_chunk_id_list[i] = static_cast(i); diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index 67dd32ffa..a6e14fe36 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -413,7 +413,9 @@ void TestAddContent(const std::filesystem::path& root) { Check(mount(0, nullptr, &point) == static_cast(0x80d90002u) && list(0, nullptr, 0, nullptr) == static_cast(0x817d0002u), "null add-on parameters return parameter errors"); - Check(mount(0, &info[3].label, &point) == OK, "mount owned add-on"); + Check(mount(0, &info[3].label, &point) == OK && + !std::string_view(point.data).ends_with('/'), + "mount owned add-on without trailing slash"); const std::string mounted_file = std::string(point.data) + "/payload.txt"; const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0); std::array payload {}; @@ -422,8 +424,9 @@ void TestAddContent(const std::filesystem::path& root) { FileSystem::KernelClose(fd) == OK, "guest reads files from the selected title's add-on"); MountPoint repeated {}; - Check(mount(0, &info[3].label, &repeated) == static_cast(0x80d90003u), - "duplicate mount returns BUSY"); + Check(mount(0, &info[3].label, &repeated) == OK && + std::strcmp(repeated.data, point.data) == 0, + "duplicate mount returns the same mount point"); Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() && unmount(&point) == static_cast(0x80d90004u), "unmount removes mapping and repeated unmount reports NOT_MOUNTED");