From ca5638a1426ce560f3896b3ff0d3efd02b654585 Mon Sep 17 00:00:00 2001 From: Zach Hilman Date: Tue, 18 Dec 2018 09:07:25 -0500 Subject: common: Extract UUID to its own class Since the Mii database uses UUIDs very similar to the Accounts database, it makes no sense to not share code between them. --- src/common/CMakeLists.txt | 2 ++ src/common/uuid.cpp | 33 +++++++++++++++++++++++++++++++++ src/common/uuid.h | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/common/uuid.cpp create mode 100644 src/common/uuid.h (limited to 'src/common') diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 1e8e1b215..cb514a0d2 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -123,6 +123,8 @@ add_library(common STATIC timer.h uint128.cpp uint128.h + uuid.cpp + uuid.h vector_math.h web_result.h zstd_compression.cpp diff --git a/src/common/uuid.cpp b/src/common/uuid.cpp new file mode 100644 index 000000000..8e63b58b8 --- /dev/null +++ b/src/common/uuid.cpp @@ -0,0 +1,33 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include + +#include + +#include "common/uuid.h" + +namespace Common { + +UUID UUID::Generate() { + std::random_device device; + std::mt19937 gen(device()); + std::uniform_int_distribution distribution(1, std::numeric_limits::max()); + return UUID{distribution(gen), distribution(gen)}; +} + +std::string UUID::Format() const { + return fmt::format("0x{:016X}{:016X}", uuid[1], uuid[0]); +} + +std::string UUID::FormatSwitch() const { + std::array s{}; + std::memcpy(s.data(), uuid.data(), sizeof(u128)); + return fmt::format("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{" + ":02x}{:02x}{:02x}{:02x}{:02x}", + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], + s[12], s[13], s[14], s[15]); +} + +} // namespace Common diff --git a/src/common/uuid.h b/src/common/uuid.h new file mode 100644 index 000000000..4a5e5fa7c --- /dev/null +++ b/src/common/uuid.h @@ -0,0 +1,45 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" + +namespace Common { + +constexpr u128 INVALID_UUID{{0, 0}}; + +struct UUID { + // UUIDs which are 0 are considered invalid! + u128 uuid = INVALID_UUID; + constexpr UUID() = default; + constexpr explicit UUID(const u128& id) : uuid{id} {} + constexpr explicit UUID(const u64 lo, const u64 hi) : uuid{{lo, hi}} {} + + explicit operator bool() const { + return uuid != INVALID_UUID; + } + + bool operator==(const UUID& rhs) const { + return uuid == rhs.uuid; + } + + bool operator!=(const UUID& rhs) const { + return !operator==(rhs); + } + + // TODO(ogniK): Properly generate uuids based on RFC-4122 + static UUID Generate(); + + // Set the UUID to {0,0} to be considered an invalid user + void Invalidate() { + uuid = INVALID_UUID; + } + + std::string Format() const; + std::string FormatSwitch() const; +}; +static_assert(sizeof(UUID) == 16, "UUID is an invalid size!"); + +} // namespace Common -- cgit v1.2.3 From f0db2e3ef36a77f2f3eaf2dca15ddfe8851edecb Mon Sep 17 00:00:00 2001 From: Zach Hilman Date: Mon, 24 Dec 2018 13:30:07 -0500 Subject: mii_manager: Cleanup and optimization --- src/common/uuid.cpp | 2 +- src/common/uuid.h | 6 ++- src/core/hle/service/acc/profile_manager.cpp | 8 ++-- src/core/hle/service/mii/mii_manager.cpp | 68 ++++++++++++++++------------ src/core/hle/service/mii/mii_manager.h | 10 +++- 5 files changed, 55 insertions(+), 39 deletions(-) (limited to 'src/common') diff --git a/src/common/uuid.cpp b/src/common/uuid.cpp index 8e63b58b8..26db03fba 100644 --- a/src/common/uuid.cpp +++ b/src/common/uuid.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright 2018 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/common/uuid.h b/src/common/uuid.h index 4a5e5fa7c..b8864b34f 100644 --- a/src/common/uuid.h +++ b/src/common/uuid.h @@ -1,9 +1,11 @@ -// Copyright 2018 Citra Emulator Project +// Copyright 2018 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include + #include "common/common_types.h" namespace Common { @@ -33,7 +35,7 @@ struct UUID { static UUID Generate(); // Set the UUID to {0,0} to be considered an invalid user - void Invalidate() { + constexpr void Invalidate() { uuid = INVALID_UUID; } diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index 767523dbc..49aa5908b 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -13,7 +13,7 @@ namespace Service::Account { -using namespace Common; +using Common::UUID; struct UserRaw { UUID uuid; @@ -199,7 +199,7 @@ bool ProfileManager::UserExists(UUID uuid) const { bool ProfileManager::UserExistsIndex(std::size_t index) const { if (index >= MAX_USERS) return false; - return profiles[index].user_uuid.uuid != INVALID_UUID; + return profiles[index].user_uuid.uuid != Common::INVALID_UUID; } /// Opens a specific user @@ -293,7 +293,7 @@ bool ProfileManager::RemoveUser(UUID uuid) { bool ProfileManager::SetProfileBase(UUID uuid, const ProfileBase& profile_new) { const auto index = GetUserIndex(uuid); - if (!index || profile_new.user_uuid == UUID(INVALID_UUID)) { + if (!index || profile_new.user_uuid == UUID(Common::INVALID_UUID)) { return false; } @@ -324,7 +324,7 @@ void ProfileManager::ParseUserSaveFile() { } for (const auto& user : data.users) { - if (user.uuid == UUID(INVALID_UUID)) { + if (user.uuid == UUID(Common::INVALID_UUID)) { continue; } diff --git a/src/core/hle/service/mii/mii_manager.cpp b/src/core/hle/service/mii/mii_manager.cpp index 25dfd8d48..083c62b1e 100644 --- a/src/core/hle/service/mii/mii_manager.cpp +++ b/src/core/hle/service/mii/mii_manager.cpp @@ -12,8 +12,10 @@ namespace Service::Mii { +namespace { + constexpr char MII_SAVE_DATABASE_PATH[] = "/system/save/8000000000000030/MiiDatabase.dat"; -constexpr std::array DEFAULT_MII_NAME = {'y', 'u', 'z', 'u', '\0'}; +constexpr std::array DEFAULT_MII_NAME = {u'y', u'u', u'z', u'u', u'\0'}; // This value was retrieved from HW test constexpr MiiStoreData DEFAULT_MII = { @@ -30,10 +32,10 @@ constexpr MiiStoreData DEFAULT_MII = { // Default values taken from multiple real databases const MiiDatabase DEFAULT_MII_DATABASE{Common::MakeMagic('N', 'F', 'D', 'B'), {}, {1}, 0, 0}; -template -std::array ResizeArray(const std::array& in) { - std::array out{}; - std::memcpy(out.data(), in.data(), sizeof(T) * std::min(s1, s2)); +template +std::array ResizeArray(const std::array& in) { + std::array out{}; + std::memcpy(out.data(), in.data(), sizeof(T) * std::min(SourceArraySize, DestArraySize)); return out; } @@ -163,12 +165,14 @@ MiiStoreData ConvertInfoToStoreData(const MiiInfo& info) { return out; } +} // namespace + std::u16string MiiInfo::Name() const { return Common::UTF16StringFromFixedZeroTerminatedBuffer(name.data(), name.size()); } bool operator==(const MiiInfo& lhs, const MiiInfo& rhs) { - return std::memcmp(&lhs, &rhs, sizeof(MiiInfo)); + return std::memcmp(&lhs, &rhs, sizeof(MiiInfo)) == 0; } bool operator!=(const MiiInfo& lhs, const MiiInfo& rhs) { @@ -188,27 +192,15 @@ MiiInfo MiiManager::CreateRandom(RandomParameters params) { "(STUBBED) called with params={:08X}{:08X}{:08X}, returning default Mii", params.unknown_1, params.unknown_2, params.unknown_3); - auto new_mii = DEFAULT_MII; - - do { - new_mii.uuid = Common::UUID::Generate(); - } while (IndexOf(new_mii.uuid) == INVALID_INDEX); - - return ConvertStoreDataToInfo(new_mii); + return ConvertStoreDataToInfo(CreateMiiWithUniqueUUID()); } MiiInfo MiiManager::CreateDefault(u32 index) { - auto new_mii = DEFAULT_MII; - - do { - new_mii.uuid = Common::UUID::Generate(); - } while (IndexOf(new_mii.uuid) == INVALID_INDEX); + const auto new_mii = CreateMiiWithUniqueUUID(); - ASSERT(index < MAX_MIIS); - database.miis[index] = new_mii; - std::stable_partition(database.miis.begin(), database.miis.end(), - [](const MiiStoreData& elem) { return elem.uuid; }); + database.miis.at(index) = new_mii; + EnsureDatabasePartition(); return ConvertStoreDataToInfo(new_mii); } @@ -253,8 +245,7 @@ bool MiiManager::Remove(Common::UUID uuid) { return false; *iter = MiiStoreData{}; - std::stable_partition(database.miis.begin(), database.miis.end(), - [](const MiiStoreData& elem) { return elem.uuid; }); + EnsureDatabasePartition(); return true; } @@ -268,9 +259,9 @@ u32 MiiManager::IndexOf(Common::UUID uuid) const { return static_cast(std::distance(database.miis.begin(), iter)); } -u32 MiiManager::IndexOf(MiiInfo info) const { +u32 MiiManager::IndexOf(const MiiInfo& info) const { const auto iter = - std::find_if(database.miis.begin(), database.miis.end(), [info](const MiiStoreData& elem) { + std::find_if(database.miis.begin(), database.miis.end(), [&info](const MiiStoreData& elem) { return ConvertStoreDataToInfo(elem) == info; }); @@ -296,12 +287,11 @@ bool MiiManager::Move(Common::UUID uuid, u32 new_index) { database.miis[new_index] = moving; } - std::stable_partition(database.miis.begin(), database.miis.end(), - [](const MiiStoreData& elem) { return elem.uuid; }); + EnsureDatabasePartition(); return true; } -bool MiiManager::AddOrReplace(MiiStoreData data) { +bool MiiManager::AddOrReplace(const MiiStoreData& data) { const auto index = IndexOf(data.uuid); if (index == INVALID_INDEX) { @@ -341,7 +331,11 @@ void MiiManager::WriteToFile() { } save.Resize(sizeof(MiiDatabase)); - save.WriteBytes(&database, sizeof(MiiDatabase)); + if (save.WriteBytes(&database, sizeof(MiiDatabase)) != sizeof(MiiDatabase)) { + LOG_WARNING(Service_Mii, "Failed to write all data to save file... Data may be malformed " + "and/or regenerated on next run."); + save.Resize(0); + } } void MiiManager::ReadFromFile() { @@ -362,6 +356,20 @@ void MiiManager::ReadFromFile() { return; } + EnsureDatabasePartition(); +} + +MiiStoreData MiiManager::CreateMiiWithUniqueUUID() const { + auto new_mii = DEFAULT_MII; + + do { + new_mii.uuid = Common::UUID::Generate(); + } while (IndexOf(new_mii.uuid) == INVALID_INDEX); + + return new_mii; +} + +void MiiManager::EnsureDatabasePartition() { std::stable_partition(database.miis.begin(), database.miis.end(), [](const MiiStoreData& elem) { return elem.uuid; }); } diff --git a/src/core/hle/service/mii/mii_manager.h b/src/core/hle/service/mii/mii_manager.h index 069247cb6..f7e3d2cf9 100644 --- a/src/core/hle/service/mii/mii_manager.h +++ b/src/core/hle/service/mii/mii_manager.h @@ -84,6 +84,8 @@ struct MiiInfo { std::u16string Name() const; }; static_assert(sizeof(MiiInfo) == 0x58, "MiiInfo has incorrect size."); +static_assert(std::has_unique_object_representations_v, + "All bits of MiiInfo must contribute to its value."); bool operator==(const MiiInfo& lhs, const MiiInfo& rhs); bool operator!=(const MiiInfo& lhs, const MiiInfo& rhs); @@ -238,15 +240,19 @@ public: bool Remove(Common::UUID uuid); u32 IndexOf(Common::UUID uuid) const; - u32 IndexOf(MiiInfo info) const; + u32 IndexOf(const MiiInfo& info) const; bool Move(Common::UUID uuid, u32 new_index); - bool AddOrReplace(MiiStoreData data); + bool AddOrReplace(const MiiStoreData& data); private: void WriteToFile(); void ReadFromFile(); + MiiStoreData CreateMiiWithUniqueUUID() const; + + void EnsureDatabasePartition(); + MiiDatabase database; }; -- cgit v1.2.3 From 1aa2b99a982e83022c9aae23c6a47eae119d21a4 Mon Sep 17 00:00:00 2001 From: Zach Hilman Date: Thu, 27 Dec 2018 20:54:44 -0500 Subject: mii: Implement Delete and Destroy file --- src/common/uuid.h | 11 ++-- src/core/hle/service/mii/mii.cpp | 87 +++++++++++++++++++++++++++++--- src/core/hle/service/mii/mii_manager.cpp | 27 ++++++++++ src/core/hle/service/mii/mii_manager.h | 10 ++++ 4 files changed, 122 insertions(+), 13 deletions(-) (limited to 'src/common') diff --git a/src/common/uuid.h b/src/common/uuid.h index b8864b34f..f6ad064fb 100644 --- a/src/common/uuid.h +++ b/src/common/uuid.h @@ -19,15 +19,16 @@ struct UUID { constexpr explicit UUID(const u128& id) : uuid{id} {} constexpr explicit UUID(const u64 lo, const u64 hi) : uuid{{lo, hi}} {} - explicit operator bool() const { - return uuid != INVALID_UUID; + constexpr explicit operator bool() const { + return uuid[0] != INVALID_UUID[0] && uuid[1] != INVALID_UUID[1]; } - bool operator==(const UUID& rhs) const { - return uuid == rhs.uuid; + constexpr bool operator==(const UUID& rhs) const { + // TODO(DarkLordZach): Replace with uuid == rhs.uuid with C++20 + return uuid[0] == rhs.uuid[0] && uuid[1] == rhs.uuid[1]; } - bool operator!=(const UUID& rhs) const { + constexpr bool operator!=(const UUID& rhs) const { return !operator==(rhs); } diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp index 39e4e937a..ce84e25ed 100644 --- a/src/core/hle/service/mii/mii.cpp +++ b/src/core/hle/service/mii/mii.cpp @@ -4,6 +4,8 @@ #include +#include + #include "common/logging/log.h" #include "common/string_util.h" #include "core/hle/ipc_helpers.h" @@ -15,6 +17,10 @@ namespace Service::Mii { +constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::Mii, 1}; +constexpr ResultCode ERROR_CANNOT_FIND_ENTRY{ErrorModule::Mii, 4}; +constexpr ResultCode ERROR_NOT_IN_TEST_MODE{ErrorModule::Mii, 99}; + class IDatabaseService final : public ServiceFramework { public: explicit IDatabaseService() : ServiceFramework{"IDatabaseService"} { @@ -35,8 +41,8 @@ public: {12, &IDatabaseService::Move, "Move"}, {13, &IDatabaseService::AddOrReplace, "AddOrReplace"}, {14, &IDatabaseService::Delete, "Delete"}, - {15, nullptr, "DestroyFile"}, - {16, nullptr, "DeleteFile"}, + {15, &IDatabaseService::DestroyFile, "DestroyFile"}, + {16, &IDatabaseService::DeleteFile, "DeleteFile"}, {17, &IDatabaseService::Format, "Format"}, {18, nullptr, "Import"}, {19, nullptr, "Export"}, @@ -135,12 +141,33 @@ private: void BuildRandom(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto random_params{rp.PopRaw()}; + const auto [unknown1, unknown2, unknown3] = rp.PopRaw(); + + if (unknown1 > 3) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + LOG_ERROR(Service_Mii, "Invalid unknown1 value: {}", unknown1); + return; + } + + if (unknown2 > 2) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + LOG_ERROR(Service_Mii, "Invalid unknown2 value: {}", unknown2); + return; + } + + if (unknown3 > 3) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + LOG_ERROR(Service_Mii, "Invalid unknown3 value: {}", unknown3); + return; + } LOG_DEBUG(Service_Mii, "called with param_1={:08X}, param_2={:08X}, param_3={:08X}", - random_params.unknown_1, random_params.unknown_2, random_params.unknown_3); + unknown1, unknown2, unknown3); - const auto info = db.CreateRandom(random_params); + const auto info = db.CreateRandom({unknown1, unknown2, unknown3}); IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; rb.Push(RESULT_SUCCESS); rb.PushRaw(info); @@ -150,6 +177,14 @@ private: IPC::RequestParser rp{ctx}; const auto index{rp.PopRaw()}; + if (index > 5) { + LOG_ERROR(Service_Mii, "invalid argument, index cannot be greater than 5 but is {:08X}", + index); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } + LOG_DEBUG(Service_Mii, "called with index={:08X}", index); const auto info = db.CreateDefault(index); @@ -218,7 +253,14 @@ private: void Move(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto uuid{rp.PopRaw()}; - const auto index{rp.PopRaw()}; + const auto index{rp.PopRaw()}; + + if (index < 0) { + LOG_ERROR(Service_Mii, "Index cannot be negative but is {:08X}!", index); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } LOG_DEBUG(Service_Mii, "called with uuid={}, index={:08X}", uuid.FormatSwitch(), index); @@ -252,8 +294,37 @@ private: const auto success = db.Remove(uuid); IPC::ResponseBuilder rb{ctx, 2}; - // TODO(DarkLordZach): Find a better error code - rb.Push(success ? RESULT_SUCCESS : ResultCode(-1)); + rb.Push(success ? RESULT_SUCCESS : ERROR_CANNOT_FIND_ENTRY); + } + + void DestroyFile(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Mii, "called"); + + if (!db.IsTestModeEnabled()) { + LOG_ERROR(Service_Mii, "Database is not in test mode -- cannot destory database file."); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NOT_IN_TEST_MODE); + return; + } + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(db.DestroyFile()); + } + + void DeleteFile(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Mii, "called"); + + if (!db.IsTestModeEnabled()) { + LOG_ERROR(Service_Mii, "Database is not in test mode -- cannot delete database file."); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_NOT_IN_TEST_MODE); + return; + } + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(db.DeleteFile()); } void Format(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/mii/mii_manager.cpp b/src/core/hle/service/mii/mii_manager.cpp index 7011ea2bd..04fc2180b 100644 --- a/src/core/hle/service/mii/mii_manager.cpp +++ b/src/core/hle/service/mii/mii_manager.cpp @@ -32,6 +32,13 @@ constexpr MiiStoreData DEFAULT_MII = { // Default values taken from multiple real databases const MiiDatabase DEFAULT_MII_DATABASE{Common::MakeMagic('N', 'F', 'D', 'B'), {}, {1}, 0, 0}; +constexpr std::array SOURCE_NAMES{ + "Database", + "Default", + "Account", + "Friend", +}; + template std::array ResizeArray(const std::array& in) { std::array out{}; @@ -167,6 +174,11 @@ MiiStoreData ConvertInfoToStoreData(const MiiInfo& info) { } // namespace +std::ostream& operator<<(std::ostream& os,Source source) { + os << SOURCE_NAMES.at(static_cast(source)); + return os; +} + std::u16string MiiInfo::Name() const { return Common::UTF16StringFromFixedZeroTerminatedBuffer(name.data(), name.size()); } @@ -212,6 +224,10 @@ void MiiManager::ResetUpdatedFlag() { updated_flag = false; } +bool MiiManager::IsTestModeEnabled() const { + return is_test_mode_enabled; +} + bool MiiManager::Empty() const { return Size() == 0; } @@ -318,6 +334,17 @@ bool MiiManager::AddOrReplace(const MiiStoreData& data) { return true; } +bool MiiManager::DestroyFile() { + database = DEFAULT_MII_DATABASE; + updated_flag = false; + return DeleteFile(); +} + +bool MiiManager::DeleteFile() { + const auto path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + MII_SAVE_DATABASE_PATH; + return FileUtil::Exists(path) && FileUtil::Delete(path); +} + void MiiManager::WriteToFile() { const auto raw_path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/8000000000000030"; diff --git a/src/core/hle/service/mii/mii_manager.h b/src/core/hle/service/mii/mii_manager.h index bf955930d..38ad78a0d 100644 --- a/src/core/hle/service/mii/mii_manager.h +++ b/src/core/hle/service/mii/mii_manager.h @@ -27,6 +27,8 @@ enum class Source : u32 { Friend = 3, }; +std::ostream& operator<<(std::ostream& os, Source source); + struct MiiInfo { Common::UUID uuid; std::array name; @@ -183,6 +185,8 @@ struct MiiStoreBitFields { }; }; static_assert(sizeof(MiiStoreBitFields) == 0x1C, "MiiStoreBitFields has incorrect size."); +static_assert(std::is_trivially_copyable_v, + "MiiStoreBitFields is not trivially copyable."); struct MiiStoreData { // This corresponds to the above structure MiiStoreBitFields. I did it like this because the @@ -229,6 +233,8 @@ public: bool CheckUpdatedFlag() const; void ResetUpdatedFlag(); + bool IsTestModeEnabled() const; + bool Empty() const; bool Full() const; @@ -248,6 +254,9 @@ public: bool Move(Common::UUID uuid, u32 new_index); bool AddOrReplace(const MiiStoreData& data); + bool DestroyFile(); + bool DeleteFile(); + private: void WriteToFile(); void ReadFromFile(); @@ -258,6 +267,7 @@ private: MiiDatabase database; bool updated_flag = false; + bool is_test_mode_enabled = false; }; }; // namespace Service::Mii -- cgit v1.2.3