diff options
-rw-r--r-- | NEWS | 6 | ||||
-rw-r--r-- | configure.ac | 12 | ||||
-rw-r--r-- | src/command/AllCommands.cxx | 10 | ||||
-rw-r--r-- | src/command/PlaylistCommands.cxx | 8 | ||||
-rw-r--r-- | src/command/PlaylistCommands.hxx | 5 | ||||
-rw-r--r-- | src/db/plugins/upnp/Directory.cxx | 60 | ||||
-rw-r--r-- | src/db/plugins/upnp/Object.hxx | 27 | ||||
-rw-r--r-- | src/db/plugins/upnp/UpnpDatabasePlugin.cxx | 14 | ||||
-rw-r--r-- | src/decoder/plugins/WavpackDecoderPlugin.cxx | 6 | ||||
-rw-r--r-- | src/output/plugins/PulseOutputPlugin.cxx | 6 | ||||
-rw-r--r-- | src/playlist/plugins/SoundCloudPlaylistPlugin.cxx | 63 | ||||
-rw-r--r-- | src/sticker/SongSticker.cxx | 8 | ||||
-rw-r--r-- | src/util/Alloc.cxx | 63 | ||||
-rw-r--r-- | src/util/Alloc.hxx | 19 |
14 files changed, 211 insertions, 96 deletions
@@ -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 ¶m) /** * 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 |