diff --git a/NEWS b/NEWS index bf3fba46d..a4ed8fc86 100644 --- a/NEWS +++ b/NEWS @@ -10,7 +10,7 @@ ver 0.24 (not yet released) - show PCRE support in "config" response - apply Unicode normalization to case-insensitive filter expressions - stickers on playlists and some tag types - - new commands "stickernames", "stickertypes", "playlistlength", "searchplaylist" + - new commands "stickernames", "stickertypes", "playlistlength", "searchplaylist", "protocol" - new "search"/"find" filter "added-since" - allow range in listplaylist and listplaylistinfo - "sticker find" supports sort and window parameter and new sticker compare operators "eq", "lt" and "gt" diff --git a/doc/protocol.rst b/doc/protocol.rst index 6b9b59eac..c1a3c8d56 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -1678,6 +1678,44 @@ Connection settings Announce that this client is interested in all tag types. This is the default setting for new clients. +.. _command_protocol: + +:command:`protocol` + Shows a list of enabled protocol features. + + Available features: + + - ``hide_playlists_in_root``: disables the listing of + stored playlists for the :ref:`lsinfo `. + + The following ``protocol`` sub commands configure the + protocol features. + +.. _command_protocol_disable: + +:command:`protocol disable {FEATURE...}` + Disables one or more features. + +.. _command_protocol_enable: + +:command:`protocol enable {FEATURE...}` + Enables one or more features. + +.. _command_protocol_clear: + +:command:`protocol clear` + Disables all protocol features. + +.. _command_protocol_all: + +:command:`protocol all` + Enables all protocol features. + +.. _command_protocol_available: + +:command:`protocol available` + Lists all available protocol features. + .. _partition_commands: Partition commands diff --git a/meson.build b/meson.build index 38483eaf8..16658c770 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 d440aad0e..9524b2003 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 SetProtocolFeatures(ProtocolFeature features, bool enable) noexcept { + if (enable) + protocol_feature.Set(features); + else + protocol_feature.Unset(features); + } + + 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 000000000..e1fba19eb --- /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 000000000..49febb761 --- /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 feature) const noexcept { + return (*this & feature).TestAny(); + } + + constexpr void Set(ProtocolFeature features) noexcept { + *this |= features; + } + + constexpr void Unset(ProtocolFeature features) noexcept { + *this &= ~ProtocolFeature(features); + } + + 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 82bd4f0b8..7a19af4bf 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 6be2bd76a..d4240e494 100644 --- a/src/command/ClientCommands.cxx +++ b/src/command/ClientCommands.cxx @@ -109,3 +109,67 @@ handle_tagtypes(Client &client, Request request, Response &r) return CommandResult::ERROR; } } + +static ProtocolFeature +ParseProtocolFeature(Request request) +{ + if (request.empty()) + throw ProtocolError(ACK_ERROR_ARG, "Not enough arguments"); + + ProtocolFeature result = ProtocolFeature::None(); + + for (const char *name : request) { + auto type = protocol_feature_parse_i(name); + if (type == PF_NUM_OF_ITEM_TYPES) + throw ProtocolError(ACK_ERROR_ARG, "Unknown protcol feature"); + + result |= type; + } + + return result; +} + +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.SetProtocolFeatures(ParseProtocolFeature(request), true); + return CommandResult::OK; + } else if (StringIsEqual(cmd, "disable")) { + client.SetProtocolFeatures(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 1ce14ebd3..5742e32b7 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 b956b134c..58aa38410 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 (...) {