aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NEWS6
-rw-r--r--configure.ac12
-rw-r--r--src/command/AllCommands.cxx10
-rw-r--r--src/command/PlaylistCommands.cxx8
-rw-r--r--src/command/PlaylistCommands.hxx5
-rw-r--r--src/db/plugins/upnp/Directory.cxx60
-rw-r--r--src/db/plugins/upnp/Object.hxx27
-rw-r--r--src/db/plugins/upnp/UpnpDatabasePlugin.cxx14
-rw-r--r--src/decoder/plugins/WavpackDecoderPlugin.cxx6
-rw-r--r--src/output/plugins/PulseOutputPlugin.cxx6
-rw-r--r--src/playlist/plugins/SoundCloudPlaylistPlugin.cxx63
-rw-r--r--src/sticker/SongSticker.cxx8
-rw-r--r--src/util/Alloc.cxx63
-rw-r--r--src/util/Alloc.hxx19
14 files changed, 211 insertions, 96 deletions
diff --git a/NEWS b/NEWS
index bf6bf11b6..a04b255ee 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+ver 0.20 (not yet released)
+* protocol
+ - "commands" returns playlist commands only if playlist_directory configured
+* output
+ - pulse: set channel map to WAVE-EX
+
ver 0.19.3 (2014/11/11)
* protocol
- fix "(null)" result string to "list" when AlbumArtist is disabled
diff --git a/configure.ac b/configure.ac
index a0c1e32ed..199319974 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,10 +1,10 @@
AC_PREREQ(2.60)
-AC_INIT(mpd, 0.19.3, musicpd-dev-team@lists.sourceforge.net)
+AC_INIT(mpd, 0.20, musicpd-dev-team@lists.sourceforge.net)
VERSION_MAJOR=0
-VERSION_MINOR=19
-VERSION_REVISION=3
+VERSION_MINOR=20
+VERSION_REVISION=0
VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx])
@@ -454,8 +454,6 @@ AC_ARG_ENABLE(soundcloud,
AS_HELP_STRING([--enable-soundcloud],
[enable support for soundcloud.com]),,
[enable_soundcloud=auto])
-MPD_DEPENDS([enable_soundcloud], [enable_glib],
- [Cannot use --enable-soundcloud with --disable-glib])
AC_ARG_ENABLE(lame-encoder,
AS_HELP_STRING([--enable-lame-encoder],
@@ -577,8 +575,6 @@ AC_ARG_ENABLE(sqlite,
AS_HELP_STRING([--enable-sqlite],
[enable support for the SQLite database]),,
[enable_sqlite=$database_auto])
-MPD_DEPENDS([enable_sqlite], [enable_glib],
- [Cannot use --enable-sqlite with --disable-glib])
AC_ARG_ENABLE(systemd-daemon,
AS_HELP_STRING([--enable-systemd-daemon],
@@ -631,8 +627,6 @@ AC_ARG_ENABLE(wavpack,
AS_HELP_STRING([--enable-wavpack],
[enable WavPack support]),,
enable_wavpack=auto)
-MPD_DEPENDS([enable_wavpack], [enable_glib],
- [Cannot use --enable-wavpack with --disable-glib])
AC_ARG_ENABLE(werror,
AS_HELP_STRING([--enable-werror],
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index 6a4b18198..be3a343a5 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -210,6 +210,16 @@ command_available(gcc_unused const Partition &partition,
return neighbor_commands_available(partition.instance);
#endif
+ if (strcmp(cmd->cmd, "save") == 0 ||
+ strcmp(cmd->cmd, "rm") == 0 ||
+ strcmp(cmd->cmd, "rename") == 0 ||
+ strcmp(cmd->cmd, "playlistdelete") == 0 ||
+ strcmp(cmd->cmd, "playlistmove") == 0 ||
+ strcmp(cmd->cmd, "playlistclear") == 0 ||
+ strcmp(cmd->cmd, "playlistadd") == 0 ||
+ strcmp(cmd->cmd, "listplaylists") == 0)
+ return playlist_commands_available();
+
return true;
}
diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx
index c2b18064c..593eab865 100644
--- a/src/command/PlaylistCommands.cxx
+++ b/src/command/PlaylistCommands.cxx
@@ -35,9 +35,17 @@
#include "protocol/ArgParser.hxx"
#include "protocol/Result.hxx"
#include "ls.hxx"
+#include "Mapper.hxx"
+#include "fs/AllocatedPath.hxx"
#include "util/UriUtil.hxx"
#include "util/Error.hxx"
+bool
+playlist_commands_available()
+{
+ return !map_spl_path().IsNull();
+}
+
static void
print_spl_list(Client &client, const PlaylistVector &list)
{
diff --git a/src/command/PlaylistCommands.hxx b/src/command/PlaylistCommands.hxx
index fba4e1318..6dc589c8b 100644
--- a/src/command/PlaylistCommands.hxx
+++ b/src/command/PlaylistCommands.hxx
@@ -21,9 +21,14 @@
#define MPD_PLAYLIST_COMMANDS_HXX
#include "CommandResult.hxx"
+#include "Compiler.h"
class Client;
+gcc_const
+bool
+playlist_commands_available();
+
CommandResult
handle_save(Client &client, unsigned argc, char *argv[]);
diff --git a/src/db/plugins/upnp/Directory.cxx b/src/db/plugins/upnp/Directory.cxx
index e94a1a997..55f2693ae 100644
--- a/src/db/plugins/upnp/Directory.cxx
+++ b/src/db/plugins/upnp/Directory.cxx
@@ -89,18 +89,18 @@ ParseDuration(const char *duration)
* this. Twonky returns directory names (titles) like 'Artist/Album'.
*/
gcc_pure
-static std::string
-titleToPathElt(std::string &&s)
+static std::string &&
+TitleToPathSegment(std::string &&s)
{
std::replace(s.begin(), s.end(), '/', '_');
- return s;
+ return std::move(s);
}
/**
* An XML parser which builds directory contents from DIDL lite input.
*/
class UPnPDirParser final : public CommonExpatParser {
- UPnPDirContent &m_dir;
+ UPnPDirContent &directory;
enum {
NONE,
@@ -120,22 +120,22 @@ class UPnPDirParser final : public CommonExpatParser {
*/
std::string value;
- UPnPDirObject m_tobj;
+ UPnPDirObject object;
TagBuilder tag;
public:
- UPnPDirParser(UPnPDirContent& dir)
- :m_dir(dir),
+ UPnPDirParser(UPnPDirContent &_directory)
+ :directory(_directory),
state(NONE),
tag_type(TAG_NUM_OF_ITEM_TYPES)
{
- m_tobj.clear();
+ object.Clear();
}
protected:
virtual void StartElement(const XML_Char *name, const XML_Char **attrs)
{
- if (m_tobj.type != UPnPDirObject::Type::UNKNOWN &&
+ if (object.type != UPnPDirObject::Type::UNKNOWN &&
tag_type == TAG_NUM_OF_ITEM_TYPES) {
tag_type = tag_table_lookup(upnp_tags, name);
if (tag_type != TAG_NUM_OF_ITEM_TYPES)
@@ -147,31 +147,31 @@ protected:
switch (name[0]) {
case 'c':
if (!strcmp(name, "container")) {
- m_tobj.clear();
- m_tobj.type = UPnPDirObject::Type::CONTAINER;
+ object.Clear();
+ object.type = UPnPDirObject::Type::CONTAINER;
const char *id = GetAttribute(attrs, "id");
if (id != nullptr)
- m_tobj.m_id = id;
+ object.id = id;
const char *pid = GetAttribute(attrs, "parentID");
if (pid != nullptr)
- m_tobj.m_pid = pid;
+ object.parent_id = pid;
}
break;
case 'i':
if (!strcmp(name, "item")) {
- m_tobj.clear();
- m_tobj.type = UPnPDirObject::Type::ITEM;
+ object.Clear();
+ object.type = UPnPDirObject::Type::ITEM;
const char *id = GetAttribute(attrs, "id");
if (id != nullptr)
- m_tobj.m_id = id;
+ object.id = id;
const char *pid = GetAttribute(attrs, "parentID");
if (pid != nullptr)
- m_tobj.m_pid = pid;
+ object.parent_id = pid;
}
break;
@@ -197,25 +197,15 @@ protected:
}
}
- bool checkobjok() {
- if (m_tobj.m_id.empty() || m_tobj.m_pid.empty() ||
- m_tobj.name.empty() ||
- (m_tobj.type == UPnPDirObject::Type::ITEM &&
- m_tobj.item_class == UPnPDirObject::ItemClass::UNKNOWN))
- return false;
-
- return true;
- }
-
virtual void EndElement(const XML_Char *name)
{
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
- assert(m_tobj.type != UPnPDirObject::Type::UNKNOWN);
+ assert(object.type != UPnPDirObject::Type::UNKNOWN);
tag.AddItem(tag_type, value.c_str());
if (tag_type == TAG_TITLE)
- m_tobj.name = titleToPathElt(std::move(value));
+ object.name = TitleToPathSegment(std::move(value));
value.clear();
tag_type = TAG_NUM_OF_ITEM_TYPES;
@@ -223,9 +213,9 @@ protected:
}
if ((!strcmp(name, "container") || !strcmp(name, "item")) &&
- checkobjok()) {
- tag.Commit(m_tobj.tag);
- m_dir.objects.emplace_back(std::move(m_tobj));
+ object.Check()) {
+ tag.Commit(object.tag);
+ directory.objects.emplace_back(std::move(object));
}
state = NONE;
@@ -234,7 +224,7 @@ protected:
virtual void CharacterData(const XML_Char *s, int len)
{
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
- assert(m_tobj.type != UPnPDirObject::Type::UNKNOWN);
+ assert(object.type != UPnPDirObject::Type::UNKNOWN);
value.append(s, len);
return;
@@ -245,11 +235,11 @@ protected:
break;
case RES:
- m_tobj.url.assign(s, len);
+ object.url.assign(s, len);
break;
case CLASS:
- m_tobj.item_class = ParseItemClass(s, len);
+ object.item_class = ParseItemClass(s, len);
break;
}
}
diff --git a/src/db/plugins/upnp/Object.hxx b/src/db/plugins/upnp/Object.hxx
index 16a66c774..6d71c158b 100644
--- a/src/db/plugins/upnp/Object.hxx
+++ b/src/db/plugins/upnp/Object.hxx
@@ -21,6 +21,7 @@
#define MPD_UPNP_OBJECT_HXX
#include "tag/Tag.hxx"
+#include "Compiler.h"
#include <string>
@@ -50,8 +51,16 @@ public:
PLAYLIST,
};
- std::string m_id; // ObjectId
- std::string m_pid; // Parent ObjectId
+ /**
+ * ObjectId
+ */
+ std::string id;
+
+ /**
+ * Parent's ObjectId
+ */
+ std::string parent_id;
+
std::string url;
/**
@@ -71,15 +80,21 @@ public:
UPnPDirObject &operator=(UPnPDirObject &&) = default;
- void clear()
- {
- m_id.clear();
- m_pid.clear();
+ void Clear() {
+ id.clear();
+ parent_id.clear();
url.clear();
type = Type::UNKNOWN;
item_class = ItemClass::UNKNOWN;
tag.Clear();
}
+
+ gcc_pure
+ bool Check() const {
+ return !id.empty() && !parent_id.empty() && !name.empty() &&
+ (type != UPnPDirObject::Type::ITEM ||
+ item_class != UPnPDirObject::ItemClass::UNKNOWN);
+ }
};
#endif /* _UPNPDIRCONTENT_H_X_INCLUDED_ */
diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
index 21ddb8790..f038a668c 100644
--- a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
+++ b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
@@ -412,7 +412,7 @@ UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
// So we return synthetic and ugly paths based on the object id,
// which we later have to detect.
const std::string path = songPath(server.getFriendlyName(),
- dirent.m_id);
+ dirent.id);
if (!visitSong(std::move(dirent), path.c_str(),
selection, visit_song,
error))
@@ -447,13 +447,13 @@ UpnpDatabase::BuildPath(const ContentDirectoryService &server,
std::string &path,
Error &error) const
{
- const char *pid = idirent.m_id.c_str();
+ const char *pid = idirent.id.c_str();
path.clear();
UPnPDirObject dirent;
while (strcmp(pid, rootid) != 0) {
if (!ReadNode(server, pid, dirent, error))
return false;
- pid = dirent.m_pid.c_str();
+ pid = dirent.parent_id.c_str();
if (path.empty())
path = dirent.name;
@@ -509,7 +509,7 @@ UpnpDatabase::Namei(const ContentDirectoryService &server,
return false;
}
- objid = std::move(child->m_id);
+ objid = std::move(child->id);
}
}
@@ -621,7 +621,7 @@ UpnpDatabase::VisitServer(const ContentDirectoryService &server,
}
std::string path = songPath(server.getFriendlyName(),
- dirent.m_id);
+ dirent.id);
if (!visitSong(std::move(dirent), path.c_str(),
selection,
visit_song, error))
@@ -640,7 +640,7 @@ UpnpDatabase::VisitServer(const ContentDirectoryService &server,
recursion (1-deep) here, which will handle the "add dir"
case. */
if (selection.recursive && selection.filter)
- return SearchSongs(server, tdirent.m_id.c_str(), selection,
+ return SearchSongs(server, tdirent.id.c_str(), selection,
visit_song, error);
const char *const base_uri = selection.uri.empty()
@@ -658,7 +658,7 @@ UpnpDatabase::VisitServer(const ContentDirectoryService &server,
and loop here, but it's not useful as mpd will only return
data to the client when we're done anyway. */
UPnPDirContent dirbuf;
- if (!server.readDir(handle, tdirent.m_id.c_str(), dirbuf,
+ if (!server.readDir(handle, tdirent.id.c_str(), dirbuf,
error))
return false;
diff --git a/src/decoder/plugins/WavpackDecoderPlugin.cxx b/src/decoder/plugins/WavpackDecoderPlugin.cxx
index 67859bbd2..0b41d052e 100644
--- a/src/decoder/plugins/WavpackDecoderPlugin.cxx
+++ b/src/decoder/plugins/WavpackDecoderPlugin.cxx
@@ -28,10 +28,10 @@
#include "util/Error.hxx"
#include "util/Domain.hxx"
#include "util/Macros.hxx"
+#include "util/Alloc.hxx"
#include "Log.hxx"
#include <wavpack/wavpack.h>
-#include <glib.h>
#include <assert.h>
#include <stdio.h>
@@ -484,10 +484,10 @@ wavpack_open_wvc(Decoder &decoder, const char *uri)
if (uri == nullptr)
return nullptr;
- char *wvc_url = g_strconcat(uri, "c", nullptr);
+ char *wvc_url = xstrcatdup(uri, "c");
InputStream *is_wvc = decoder_open_uri(decoder, uri, IgnoreError());
- g_free(wvc_url);
+ free(wvc_url);
if (is_wvc == nullptr)
return nullptr;
diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx
index 120bad090..5dc733383 100644
--- a/src/output/plugins/PulseOutputPlugin.cxx
+++ b/src/output/plugins/PulseOutputPlugin.cxx
@@ -523,7 +523,11 @@ pulse_output_setup_stream(PulseOutput *po, const pa_sample_spec *ss,
assert(po != nullptr);
assert(po->context != nullptr);
- po->stream = pa_stream_new(po->context, po->name, ss, nullptr);
+ /* WAVE-EX is been adopted as the speaker map for most media files */
+ pa_channel_map chan_map;
+ pa_channel_map_init_auto(&chan_map, ss->channels,
+ PA_CHANNEL_MAP_WAVEEX);
+ po->stream = pa_stream_new(po->context, po->name, ss, &chan_map);
if (po->stream == nullptr) {
SetError(error, po->context, "pa_stream_new() has failed");
return false;
diff --git a/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
index ec4d240a5..3fe8d57b1 100644
--- a/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
+++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx
@@ -25,16 +25,17 @@
#include "input/InputStream.hxx"
#include "tag/TagBuilder.hxx"
#include "util/StringUtil.hxx"
+#include "util/Alloc.hxx"
#include "util/Error.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
-#include <glib.h>
#include <yajl/yajl_parse.h>
#include <string>
#include <string.h>
+#include <stdlib.h>
static struct {
std::string apikey;
@@ -60,7 +61,7 @@ soundcloud_init(const config_param &param)
/**
* Construct a full soundcloud resolver URL from the given fragment.
* @param uri uri of a soundcloud page (or just the path)
- * @return Constructed URL. Must be freed with g_free.
+ * @return Constructed URL. Must be freed with free().
*/
static char *
soundcloud_resolve(const char* uri)
@@ -68,18 +69,18 @@ soundcloud_resolve(const char* uri)
char *u, *ru;
if (StringStartsWith(uri, "https://")) {
- u = g_strdup(uri);
+ u = xstrdup(uri);
} else if (StringStartsWith(uri, "soundcloud.com")) {
- u = g_strconcat("https://", uri, nullptr);
+ u = xstrcatdup("https://", uri);
} else {
/* assume it's just a path on soundcloud.com */
- u = g_strconcat("https://soundcloud.com/", uri, nullptr);
+ u = xstrcatdup("https://soundcloud.com/", uri);
}
- ru = g_strconcat("https://api.soundcloud.com/resolve.json?url=",
- u, "&client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
- g_free(u);
+ ru = xstrcatdup("https://api.soundcloud.com/resolve.json?url=",
+ u, "&client_id=",
+ soundcloud_config.apikey.c_str());
+ free(u);
return ru;
}
@@ -145,12 +146,12 @@ handle_string(void *ctx, const unsigned char* stringval,
switch (data->key) {
case Title:
- g_free(data->title);
- data->title = g_strndup(s, stringlen);
+ free(data->title);
+ data->title = xstrndup(s, stringlen);
break;
case Stream_URL:
- g_free(data->stream_url);
- data->stream_url = g_strndup(s, stringlen);
+ free(data->stream_url);
+ data->stream_url = xstrndup(s, stringlen);
data->got_url = 1;
break;
default:
@@ -211,8 +212,8 @@ handle_end_map(void *ctx)
/* got_url == 1, track finished, make it into a song */
data->got_url = 0;
- char *u = g_strconcat(data->stream_url, "?client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
+ char *u = xstrcatdup(data->stream_url, "?client_id=",
+ soundcloud_config.apikey.c_str());
TagBuilder tag;
tag.SetDuration(SignedSongTime::FromMS(data->duration));
@@ -220,7 +221,7 @@ handle_end_map(void *ctx)
tag.AddItem(TAG_NAME, data->title);
data->songs.emplace_front(u, tag.Commit());
- g_free(u);
+ free(u);
return 1;
}
@@ -325,24 +326,24 @@ soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
char *u = nullptr;
if (memcmp(uri, "track/", 6) == 0) {
const char *rest = uri + 6;
- u = g_strconcat("https://api.soundcloud.com/tracks/",
- rest, ".json?client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
+ u = xstrcatdup("https://api.soundcloud.com/tracks/",
+ rest, ".json?client_id=",
+ soundcloud_config.apikey.c_str());
} else if (memcmp(uri, "playlist/", 9) == 0) {
const char *rest = uri + 9;
- u = g_strconcat("https://api.soundcloud.com/playlists/",
- rest, ".json?client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
+ u = xstrcatdup("https://api.soundcloud.com/playlists/",
+ rest, ".json?client_id=",
+ soundcloud_config.apikey.c_str());
} else if (memcmp(uri, "user/", 5) == 0) {
const char *rest = uri + 5;
- u = g_strconcat("https://api.soundcloud.com/users/",
- rest, "/tracks.json?client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
+ u = xstrcatdup("https://api.soundcloud.com/users/",
+ rest, "/tracks.json?client_id=",
+ soundcloud_config.apikey.c_str());
} else if (memcmp(uri, "search/", 7) == 0) {
const char *rest = uri + 7;
- u = g_strconcat("https://api.soundcloud.com/tracks.json?q=",
- rest, "&client_id=",
- soundcloud_config.apikey.c_str(), nullptr);
+ u = xstrcatdup("https://api.soundcloud.com/tracks.json?q=",
+ rest, "&client_id=",
+ soundcloud_config.apikey.c_str());
} else if (memcmp(uri, "url/", 4) == 0) {
const char *rest = uri + 4;
/* Translate to soundcloud resolver call. libcurl will automatically
@@ -368,10 +369,10 @@ soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
int ret = soundcloud_parse_json(u, hand, mutex, cond);
- g_free(u);
+ free(u);
yajl_free(hand);
- g_free(data.title);
- g_free(data.stream_url);
+ free(data.title);
+ free(data.stream_url);
if (ret == -1)
return nullptr;
diff --git a/src/sticker/SongSticker.cxx b/src/sticker/SongSticker.cxx
index b6f46f167..b0b74b1a6 100644
--- a/src/sticker/SongSticker.cxx
+++ b/src/sticker/SongSticker.cxx
@@ -23,11 +23,11 @@
#include "db/LightSong.hxx"
#include "db/Interface.hxx"
#include "util/Error.hxx"
-
-#include <glib.h>
+#include "util/Alloc.hxx"
#include <assert.h>
#include <string.h>
+#include <stdlib.h>
std::string
sticker_song_get_value(const LightSong &song, const char *name)
@@ -109,7 +109,7 @@ sticker_song_find(const Database &db, const char *base_uri, const char *name,
if (*data.base_uri != 0)
/* append slash to base_uri */
data.base_uri = allocated =
- g_strconcat(data.base_uri, "/", nullptr);
+ xstrcatdup(data.base_uri, "/");
else
/* searching in root directory - no trailing slash */
allocated = nullptr;
@@ -118,7 +118,7 @@ sticker_song_find(const Database &db, const char *base_uri, const char *name,
bool success = sticker_find("song", data.base_uri, name,
sticker_song_find_cb, &data);
- g_free(allocated);
+ free(allocated);
return success;
}
diff --git a/src/util/Alloc.cxx b/src/util/Alloc.cxx
index ec3579470..403ad535d 100644
--- a/src/util/Alloc.cxx
+++ b/src/util/Alloc.cxx
@@ -74,3 +74,66 @@ xstrndup(const char *s, size_t n)
return p;
}
+
+template<typename... Args>
+static inline size_t
+FillLengths(size_t *lengths, const char *a, Args&&... args)
+{
+ return FillLengths(lengths, a) + FillLengths(lengths + 1, args...);
+}
+
+template<>
+inline size_t
+FillLengths(gcc_unused size_t *lengths, const char *a)
+{
+ return *lengths = strlen(a);
+}
+
+template<typename... Args>
+static inline void
+StringCat(char *p, const size_t *lengths, const char *a, Args&&... args)
+{
+ StringCat(p, lengths, a);
+ StringCat(p + *lengths, lengths + 1, args...);
+}
+
+template<>
+inline void
+StringCat(char *p, const size_t *lengths, const char *a)
+{
+ memcpy(p, a, *lengths);
+}
+
+template<typename... Args>
+gcc_malloc gcc_nonnull_all
+static inline char *
+t_xstrcatdup(Args&&... args)
+{
+ constexpr size_t n = sizeof...(args);
+
+ size_t lengths[n];
+ const size_t total = FillLengths(lengths, args...);
+
+ char *p = (char *)xalloc(total + 1);
+ StringCat(p, lengths, args...);
+ p[total] = 0;
+ return p;
+}
+
+char *
+xstrcatdup(const char *a, const char *b)
+{
+ return t_xstrcatdup(a, b);
+}
+
+char *
+xstrcatdup(const char *a, const char *b, const char *c)
+{
+ return t_xstrcatdup(a, b, c);
+}
+
+char *
+xstrcatdup(const char *a, const char *b, const char *c, const char *d)
+{
+ return t_xstrcatdup(a, b, c, d);
+}
diff --git a/src/util/Alloc.hxx b/src/util/Alloc.hxx
index 15c123b7a..654b7d0fe 100644
--- a/src/util/Alloc.hxx
+++ b/src/util/Alloc.hxx
@@ -64,4 +64,23 @@ gcc_malloc gcc_nonnull_all
char *
xstrndup(const char *s, size_t n);
+/**
+ * Concatenate two strings, returning a new allocation. Use free() to
+ * free it.
+ *
+ * This function never fails; in out-of-memory situations, it aborts
+ * the process.
+ */
+gcc_malloc gcc_nonnull_all
+char *
+xstrcatdup(const char *a, const char *b);
+
+gcc_malloc gcc_nonnull_all
+char *
+xstrcatdup(const char *a, const char *b, const char *c);
+
+gcc_malloc gcc_nonnull_all
+char *
+xstrcatdup(const char *a, const char *b, const char *c, const char *d);
+
#endif