From 02ee1b29a8baca185a9688efba2218ba60cdcbb7 Mon Sep 17 00:00:00 2001 From: jcorporation Date: Sat, 28 Sep 2024 16:36:43 +0200 Subject: [PATCH] This commit adds a new protocol command to toggle protocol features for a client connection. It works like the tag_mask and the associated tagtypes command. New commands: - protocol Shows enabled protocol features. - protocol available Show all available protocol features. - protocol enable Disables a protocol feature. - protocol disable Disables a protocol feature. - protocol all Enables all available protocol features. - protocol clear Disables all protocol features. This commit adds also the first protocol feature. hide_playlists_in_root Disables the listing of playlists in the root folder for the lsinfo command. --- meson.build | 1 + src/client/Client.hxx | 29 +++++++++ src/client/ProtocolFeature.cxx | 74 ++++++++++++++++++++++ src/client/ProtocolFeature.hxx | 110 +++++++++++++++++++++++++++++++++ src/command/AllCommands.cxx | 1 + src/command/ClientCommands.cxx | 59 ++++++++++++++++++ src/command/ClientCommands.hxx | 3 + src/command/OtherCommands.cxx | 2 +- 8 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/client/ProtocolFeature.cxx create mode 100644 src/client/ProtocolFeature.hxx diff --git a/meson.build b/meson.build index 38483eaf8f..16658c770a 100644 --- a/meson.build +++ b/meson.build @@ -379,6 +379,7 @@ sources = [ 'src/client/File.cxx', 'src/client/Response.cxx', 'src/client/ThreadBackgroundCommand.cxx', + 'src/client/ProtocolFeature.cxx', 'src/Listen.cxx', 'src/LogInit.cxx', 'src/ls.cxx', diff --git a/src/client/Client.hxx b/src/client/Client.hxx index d440aad0e6..0634d29980 100644 --- a/src/client/Client.hxx +++ b/src/client/Client.hxx @@ -5,6 +5,7 @@ #include "IClient.hxx" #include "Message.hxx" +#include "ProtocolFeature.hxx" #include "command/CommandResult.hxx" #include "command/CommandListBuilder.hxx" #include "input/LastInputStream.hxx" @@ -111,6 +112,11 @@ private: */ std::unique_ptr background_command; + /** + * Bitmask of protocol features. + */ + ProtocolFeature protocol_feature = ProtocolFeature::None(); + public: Client(EventLoop &loop, Partition &partition, UniqueSocketDescriptor fd, int uid, @@ -167,6 +173,29 @@ public: permission = _permission; } + ProtocolFeature GetProtocolFeatures() const noexcept { + return protocol_feature; + } + + void SetProtocolFeature(enum ProtocolFeatureType value, bool enable) noexcept { + if (enable) + protocol_feature.Set(value); + else + protocol_feature.Unset(value); + } + + void AllProtocolFeatures() noexcept { + protocol_feature.SetAll(); + } + + void ClearProtocolFeatures() noexcept { + protocol_feature.Clear(); + } + + bool ProtocolFeatureEnabled(enum ProtocolFeatureType value) noexcept { + return protocol_feature.Test(value); + } + /** * Send "idle" response to this client. */ diff --git a/src/client/ProtocolFeature.cxx b/src/client/ProtocolFeature.cxx new file mode 100644 index 0000000000..e1fba19eb0 --- /dev/null +++ b/src/client/ProtocolFeature.cxx @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "ProtocolFeature.hxx" +#include "Client.hxx" +#include "Response.hxx" +#include "util/StringAPI.hxx" + +#include +#include + + +struct feature_type_table { + const char *name; + + ProtocolFeatureType type; +}; + +static constexpr struct feature_type_table protocol_feature_names_init[] = { + {"hide_playlists_in_root", PF_HIDE_PLAYLISTS_IN_ROOT}, +}; + +/** + * This function converts the #tag_item_names_init array to an + * associative array at compile time. This is a kludge because C++20 + * doesn't support designated initializers for arrays, unlike C99. + */ +static constexpr auto +MakeProtocolFeatureNames() noexcept +{ + std::array result{}; + + static_assert(std::size(protocol_feature_names_init) == result.size()); + + for (const auto &i : protocol_feature_names_init) { + /* no duplicates allowed */ + assert(result[i.type] == nullptr); + + result[i.type] = i.name; + } + + return result; +} + +constinit const std::array protocol_feature_names = MakeProtocolFeatureNames(); + +void +protocol_features_print(Client &client, Response &r) noexcept +{ + const auto protocol_feature = client.GetProtocolFeatures(); + for (unsigned i = 0; i < PF_NUM_OF_ITEM_TYPES; i++) + if (protocol_feature.Test(ProtocolFeatureType(i))) + r.Fmt(FMT_STRING("feature: {}\n"), protocol_feature_names[i]); +} + +void +protocol_features_print_all(Response &r) noexcept +{ + for (unsigned i = 0; i < PF_NUM_OF_ITEM_TYPES; i++) + r.Fmt(FMT_STRING("feature: {}\n"), protocol_feature_names[i]); +} + +ProtocolFeatureType +protocol_feature_parse_i(const char *name) noexcept +{ + for (unsigned i = 0; i < PF_NUM_OF_ITEM_TYPES; ++i) { + assert(protocol_feature_names[i] != nullptr); + + if (StringIsEqualIgnoreCase(name, protocol_feature_names[i])) + return (ProtocolFeatureType)i; + } + + return PF_NUM_OF_ITEM_TYPES; +} diff --git a/src/client/ProtocolFeature.hxx b/src/client/ProtocolFeature.hxx new file mode 100644 index 0000000000..1e984a7bcd --- /dev/null +++ b/src/client/ProtocolFeature.hxx @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include +#include + +class Client; +class Response; + +/** + * Codes for the type of a protocol feature. + */ +enum ProtocolFeatureType : uint8_t { + PF_HIDE_PLAYLISTS_IN_ROOT, + + PF_NUM_OF_ITEM_TYPES +}; + +class ProtocolFeature { + using protocol_feature_t = uint_least8_t; + + /* must have enough bits to represent all protocol features + supported by MPD */ + static_assert(PF_NUM_OF_ITEM_TYPES <= sizeof(protocol_feature_t) * 8); + + protocol_feature_t value; + + explicit constexpr ProtocolFeature(protocol_feature_t _value) noexcept + :value(_value) {} + +public: + constexpr ProtocolFeature() noexcept = default; + + constexpr ProtocolFeature(ProtocolFeatureType _value) noexcept + :value(protocol_feature_t(1) << protocol_feature_t(_value)) {} + + static constexpr ProtocolFeature None() noexcept { + return ProtocolFeature(protocol_feature_t(0)); + } + + static constexpr ProtocolFeature All() noexcept { + return ~None(); + } + + constexpr ProtocolFeature operator~() const noexcept { + return ProtocolFeature(~value); + } + + constexpr ProtocolFeature operator&(ProtocolFeature other) const noexcept { + return ProtocolFeature(value & other.value); + } + + constexpr ProtocolFeature operator|(ProtocolFeature other) const noexcept { + return ProtocolFeature(value | other.value); + } + + constexpr ProtocolFeature operator^(ProtocolFeature other) const noexcept { + return ProtocolFeature(value ^ other.value); + } + + constexpr ProtocolFeature &operator&=(ProtocolFeature other) noexcept { + value &= other.value; + return *this; + } + + constexpr ProtocolFeature &operator|=(ProtocolFeature other) noexcept { + value |= other.value; + return *this; + } + + constexpr ProtocolFeature &operator^=(ProtocolFeature other) noexcept { + value ^= other.value; + return *this; + } + + constexpr bool TestAny() const noexcept { + return value != 0; + } + + constexpr bool Test(ProtocolFeatureType _value) const noexcept { + return (*this & _value).TestAny(); + } + + constexpr void Set(ProtocolFeatureType _value) noexcept { + *this |= _value; + } + + constexpr void Unset(ProtocolFeatureType _value) noexcept { + *this &= ~ProtocolFeature(_value); + } + + constexpr void SetAll() noexcept { + *this = ProtocolFeature::All(); + } + + constexpr void Clear() noexcept { + *this = ProtocolFeature::None(); + } +}; + +void +protocol_features_print(Client &client, Response &r) noexcept; + +void +protocol_features_print_all(Response &r) noexcept; + +ProtocolFeatureType +protocol_feature_parse_i(const char *name) noexcept; diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx index 82bd4f0b85..7a19af4bf5 100644 --- a/src/command/AllCommands.cxx +++ b/src/command/AllCommands.cxx @@ -156,6 +156,7 @@ static constexpr struct command commands[] = { { "previous", PERMISSION_PLAYER, 0, 0, handle_previous }, { "prio", PERMISSION_PLAYER, 2, -1, handle_prio }, { "prioid", PERMISSION_PLAYER, 2, -1, handle_prioid }, + { "protocol", PERMISSION_NONE, 0, -1, handle_protocol }, { "random", PERMISSION_PLAYER, 1, 1, handle_random }, { "rangeid", PERMISSION_ADD, 2, 2, handle_rangeid }, { "readcomments", PERMISSION_READ, 1, 1, handle_read_comments }, diff --git a/src/command/ClientCommands.cxx b/src/command/ClientCommands.cxx index 6be2bd76a2..a820c89fc6 100644 --- a/src/command/ClientCommands.cxx +++ b/src/command/ClientCommands.cxx @@ -109,3 +109,62 @@ handle_tagtypes(Client &client, Request request, Response &r) return CommandResult::ERROR; } } + +static ProtocolFeatureType +ParseProtocolFeature(Request request) +{ + if (request.empty()) + throw ProtocolError(ACK_ERROR_ARG, "Not enough arguments"); + + const char *name = request.shift(); + auto feature = protocol_feature_parse_i(name); + if (feature == PF_NUM_OF_ITEM_TYPES) + throw ProtocolError(ACK_ERROR_ARG, "Unknown protocol feature"); + + return feature; +} + +CommandResult +handle_protocol(Client &client, Request request, Response &r) +{ + if (request.empty()) { + protocol_features_print(client, r); + return CommandResult::OK; + } + + const char *cmd = request.shift(); + if (StringIsEqual(cmd, "all")) { + if (!request.empty()) { + r.Error(ACK_ERROR_ARG, "Too many arguments"); + return CommandResult::ERROR; + } + + client.AllProtocolFeatures(); + return CommandResult::OK; + } else if (StringIsEqual(cmd, "clear")) { + if (!request.empty()) { + r.Error(ACK_ERROR_ARG, "Too many arguments"); + return CommandResult::ERROR; + } + + client.ClearProtocolFeatures(); + return CommandResult::OK; + } else if (StringIsEqual(cmd, "enable")) { + client.SetProtocolFeature(ParseProtocolFeature(request), true); + return CommandResult::OK; + } else if (StringIsEqual(cmd, "disable")) { + client.SetProtocolFeature(ParseProtocolFeature(request), false); + return CommandResult::OK; + } else if (StringIsEqual(cmd, "available")) { + if (!request.empty()) { + r.Error(ACK_ERROR_ARG, "Too many arguments"); + return CommandResult::ERROR; + } + + protocol_features_print_all(r); + return CommandResult::OK; + } else { + r.Error(ACK_ERROR_ARG, "Unknown sub command"); + return CommandResult::ERROR; + } +} diff --git a/src/command/ClientCommands.hxx b/src/command/ClientCommands.hxx index 1ce14ebd3a..5742e32b77 100644 --- a/src/command/ClientCommands.hxx +++ b/src/command/ClientCommands.hxx @@ -25,4 +25,7 @@ handle_password(Client &client, Request request, Response &response); CommandResult handle_tagtypes(Client &client, Request request, Response &response); +CommandResult +handle_protocol(Client &client, Request request, Response &response); + #endif diff --git a/src/command/OtherCommands.cxx b/src/command/OtherCommands.cxx index b956b134c8..58aa38410f 100644 --- a/src/command/OtherCommands.cxx +++ b/src/command/OtherCommands.cxx @@ -160,7 +160,7 @@ handle_lsinfo_relative(Client &client, Response &r, const char *uri) (void)client; #endif - if (isRootDirectory(uri)) { + if (!client.ProtocolFeatureEnabled(PF_HIDE_PLAYLISTS_IN_ROOT) && isRootDirectory(uri)) { try { print_spl_list(r, ListPlaylistFiles()); } catch (...) {