Compare commits

...

38 Commits

Author SHA1 Message Date
Max Kellermann
ed80863eac release v0.20.1 2017-01-09 18:10:18 +01:00
Max Kellermann
c3fc84de12 input/curl: wake up client thread after seek to end of file
Call SeekDone() to avoid the freeze bug.
2017-01-09 18:08:33 +01:00
Max Kellermann
904f83cd85 doc/developer: add GitHub reference 2017-01-09 17:19:15 +01:00
Max Kellermann
28bf100a50 doc/developer: more code style 2017-01-09 17:13:28 +01:00
Max Kellermann
accbd4e82a doc/developer: change C++11 to C++14 2017-01-09 17:13:28 +01:00
Max Kellermann
d7f478c154 doc/developer: add XML ids 2017-01-09 17:13:28 +01:00
Wieland Hoffmann
8f7f13fea4 doc/user: Replace "It used used" with "It is used" 2017-01-08 18:23:13 +01:00
Max Kellermann
c82b03a74c decoder/wavpack: fix crash bug 2017-01-08 14:54:12 +01:00
Max Kellermann
58fb36bdb9 storage/http: new storage plugin 2017-01-08 14:40:20 +01:00
Max Kellermann
4297a7b0a4 lib/curl/Request: move exception handling out of the WRITEFUNCTION
libcurl's WRITEFUNCTION is pretty fragile; if we destroy the CURL*
instance or even unregister it using curl_multi_remove_handle(),
libcurl will crash instantly.  But still we need to be able to handle
exceptions from inside the WRITEFUNCTION, and call
CurlResponseHandler::OnError(), which may destroy the whole thing.  As
a workaround, I use DeferredMonitor to postpone the OnError() call
into a stack frame which is allowed to destroy the request.
2017-01-08 14:36:27 +01:00
Max Kellermann
1bab6d0dd7 lib/curl/Request: move catch clause out of FinishHeaders
Let the caller decide what to do with the exception.
2017-01-08 14:36:27 +01:00
Max Kellermann
13b85edbe2 lib/curl/Request: postpone the curl_easy_cleanup() call
When the request is done, only unregister the CURL* handle, but do not
delete it yet - it may still be needed for CURLINFO_RESPONSE_CODE.
2017-01-08 13:51:53 +01:00
Max Kellermann
dc53098e43 lib/curl/Request: allow Stop() to be called twice
Convert assertion to runtime check.  This is useful because this is a
public method, and the caller has no chance to check if the object is
still registered.
2017-01-08 13:51:53 +01:00
Max Kellermann
3c66feff5a lib/curl/Global: defer the ReadInfo() call
Fixes a crash that can occur due to recursion from InvalidateSockets()
to ReadInfo() to CurlRequest callbacks.
2017-01-08 12:46:35 +01:00
Max Kellermann
218c3bc0d5 lib/curl/Multi: fix typo 2017-01-08 12:46:35 +01:00
Max Kellermann
9f5eddcd13 lib/curl/Global: move code to UpdateTimeout() 2017-01-08 12:44:07 +01:00
Max Kellermann
3cba76552b lib/curl/Global: drop redundant ">=0" check 2017-01-08 12:44:04 +01:00
Max Kellermann
e98a8b624b lib/curl/Global: drop redundant "virtual" 2017-01-08 12:41:26 +01:00
Max Kellermann
6c6947b01f util/UriUtil: add uri_get_path() 2017-01-08 11:05:58 +01:00
Max Kellermann
78c91e9e5b test/run_storage: don't print unknown time stamps 2017-01-08 10:41:08 +01:00
Max Kellermann
44493ca0c4 util/TimeParser: add "pure" attribute 2017-01-08 10:41:08 +01:00
Max Kellermann
42acf78b09 util/TimeParser: wrapper for strptime()
Move code from SongFilter.cxx.
2017-01-07 22:11:45 +01:00
TermeHansen
3aa9f8af18 Rewrite of AlsaMixerPlugin to use volume_mapping
Changed AlsaMixerPlugin to use the get and set normalized functions from volume_mapping of alsa-utils/alsamixer
Changed volume_mapping set volume to be for all channels and not per channel
added volume_mapping files to Makefile.am
2017-01-07 16:30:19 +01:00
TermeHansen
8a32ee30a5 Adding volume_mapping from alsa-utils/alsamixer
source:
http://git.alsa-project.org/?p=alsa-utils.git;a=blob_plain;f=alsamixer/volume_mapping.c;hb=HEAD
http://git.alsa-project.org/?p=alsa-utils.git;a=blob_plain;f=alsamixer/volume_mapping.h;hb=HEAD
2017-01-07 16:26:36 +01:00
Max Kellermann
981dc0626b lib/expat/ExpatParser: add constructor overload for XML_ParserCreateNS() 2017-01-07 16:17:53 +01:00
Max Kellermann
8986d14e98 lib/expat/ExpatParser: make constructors "explicit" 2017-01-07 16:15:11 +01:00
Max Kellermann
5163b1a624 lib/curl/Request: require the caller to explicitly register the request
This allows constructing an instance in any thread, and register it
inside the IOThread later.
2017-01-07 16:01:58 +01:00
Max Kellermann
860aa9d6d0 lib/expat/ExpatParser: move InputStream overload to separate source file
Eliminate one unnecessary dependency for debug programs which don't
need the InputStream API.
2017-01-07 15:46:36 +01:00
Max Kellermann
64dc5212f9 Makefile.am: add variable CURL_SOURCES 2017-01-07 14:19:24 +01:00
Max Kellermann
6cff3214f3 lib/curl/Slist: new wrapper for curl_slist 2017-01-06 19:37:31 +01:00
Max Kellermann
fd910bd5e9 db/upnp: use "override" instead of "virtual" 2017-01-06 19:35:58 +01:00
Max Kellermann
c6086bed41 filter/Internal: remove the default constructor
Not used.  Force implementations to initialize out_audio_format.
2017-01-06 12:45:52 +01:00
Max Kellermann
1a9dfdfab8 filter/AutoConvert: initialize Filter::out_audio_format 2017-01-06 12:44:55 +01:00
Max Kellermann
5284cd11a9 filter/AutoConvert: remove obsolete NULL check 2017-01-06 12:35:06 +01:00
Max Kellermann
d1a47cffad filter/convert: remove obsolete method prototype 2017-01-06 12:34:39 +01:00
Max Kellermann
f469595eee filter/Internal: remove obsolete doxygen line 2017-01-06 12:34:39 +01:00
Max Kellermann
9cfc52f114 filter/Internal: add assertion to constructor 2017-01-06 11:17:55 +01:00
Max Kellermann
30bfb756c2 configure.ac: prepare for 0.20.1 2017-01-05 19:36:32 +01:00
33 changed files with 1423 additions and 185 deletions

View File

@@ -233,6 +233,15 @@ libmpd_a_SOURCES += \
src/db/Selection.cxx src/db/Selection.hxx
endif
CURL_SOURCES = \
src/lib/curl/Version.cxx src/lib/curl/Version.hxx \
src/lib/curl/Global.cxx src/lib/curl/Global.hxx \
src/lib/curl/Request.cxx src/lib/curl/Request.hxx \
src/lib/curl/Handler.hxx \
src/lib/curl/Easy.hxx \
src/lib/curl/Multi.hxx \
src/lib/curl/Slist.hxx
UPNP_SOURCES = \
src/lib/upnp/Init.cxx src/lib/upnp/Init.hxx \
src/lib/upnp/ClientInit.cxx src/lib/upnp/ClientInit.hxx \
@@ -420,6 +429,7 @@ libutil_a_SOURCES = \
src/util/FormatString.cxx src/util/FormatString.hxx \
src/util/Tokenizer.cxx src/util/Tokenizer.hxx \
src/util/TextFile.hxx \
src/util/TimeParser.cxx src/util/TimeParser.hxx \
src/util/UriUtil.cxx src/util/UriUtil.hxx \
src/util/Manual.hxx \
src/util/RefCount.hxx \
@@ -689,6 +699,8 @@ libstorage_a_CPPFLAGS = $(AM_CPPFLAGS) \
STORAGE_LIBS = \
libstorage.a \
$(CURL_LIBS) \
$(EXPAT_LIBS) \
$(NFS_LIBS) \
$(SMBCLIENT_LIBS)
@@ -704,6 +716,12 @@ libstorage_a_SOURCES += \
src/storage/plugins/NfsStorage.cxx src/storage/plugins/NfsStorage.hxx
endif
if ENABLE_WEBDAV
libstorage_a_SOURCES += \
src/lib/expat/ExpatParser.cxx \
src/storage/plugins/CurlStorage.cxx src/storage/plugins/CurlStorage.hxx
endif
endif
# neighbor plugins
@@ -1279,12 +1297,7 @@ if ENABLE_CURL
libinput_a_SOURCES += \
src/input/IcyInputStream.cxx src/input/IcyInputStream.hxx \
src/input/plugins/CurlInputPlugin.cxx src/input/plugins/CurlInputPlugin.hxx \
src/lib/curl/Version.cxx src/lib/curl/Version.hxx \
src/lib/curl/Global.cxx src/lib/curl/Global.hxx \
src/lib/curl/Request.cxx src/lib/curl/Request.hxx \
src/lib/curl/Handler.hxx \
src/lib/curl/Easy.hxx \
src/lib/curl/Multi.hxx \
$(CURL_SOURCES) \
src/IcyMetaDataParser.cxx src/IcyMetaDataParser.hxx
endif
@@ -1382,6 +1395,7 @@ libmixer_plugins_a_SOURCES = \
src/mixer/plugins/NullMixerPlugin.cxx \
src/mixer/plugins/SoftwareMixerPlugin.cxx \
src/mixer/plugins/SoftwareMixerPlugin.hxx
libmixer_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \
$(ALSA_CFLAGS) \
$(PULSE_CFLAGS)
@@ -1390,7 +1404,10 @@ if ENABLE_ALSA
liboutput_plugins_a_SOURCES += \
src/output/plugins/AlsaOutputPlugin.cxx \
src/output/plugins/AlsaOutputPlugin.hxx
libmixer_plugins_a_SOURCES += src/mixer/plugins/AlsaMixerPlugin.cxx
libmixer_plugins_a_SOURCES += \
src/mixer/plugins/volume_mapping.h \
src/mixer/plugins/volume_mapping.c \
src/mixer/plugins/AlsaMixerPlugin.cxx
endif
if ANDROID
@@ -1573,6 +1590,7 @@ endif
if ENABLE_EXPAT
libplaylist_plugins_a_SOURCES += \
src/lib/expat/StreamExpatParser.cxx \
src/lib/expat/ExpatParser.cxx src/lib/expat/ExpatParser.hxx \
src/playlist/plugins/XspfPlaylistPlugin.cxx \
src/playlist/plugins/XspfPlaylistPlugin.hxx \
@@ -1758,6 +1776,10 @@ test_run_storage_SOURCES = \
test/ScopeIOThread.hxx \
test/run_storage.cxx
if ENABLE_WEBDAV
test_run_storage_SOURCES += $(CURL_SOURCES)
endif
endif
test_run_input_LDADD = \

12
NEWS
View File

@@ -1,3 +1,15 @@
ver 0.20.1 (2017/01/09)
* input
- curl: fix crash bug
- curl: fix freeze bug
* decoder
- wavpack: fix crash bug
* storage
- curl: new storage plugin for WebDAV (work in progress)
* mixer
- alsa: normalize displayed volume according to human perception
* fix crash with volume_normalization enabled
ver 0.20 (2017/01/04)
* protocol
- "commands" returns playlist commands only if playlist_directory configured

View File

@@ -1,10 +1,10 @@
AC_PREREQ(2.60)
AC_INIT(mpd, 0.20, musicpd-dev-team@lists.sourceforge.net)
AC_INIT(mpd, 0.20.1, musicpd-dev-team@lists.sourceforge.net)
VERSION_MAJOR=0
VERSION_MINOR=20
VERSION_REVISION=0
VERSION_REVISION=1
VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx])
@@ -720,6 +720,19 @@ fi
MPD_ENABLE_AUTO_PKG(mms, MMS, [libmms >= 0.4],
[libmms mms:// protocol support], [libmms not found])
dnl ---------------------------------------------------------------------------
dnl Storage Plugins
dnl ---------------------------------------------------------------------------
MPD_ENABLE_AUTO(webdav, WEBDAV, [WebDAV storage plugin],
[WebDAV requires libcurl and libexpat],
[auto],
[if test x$enable_curl = xyes && test x$enable_expat = xyes; then
found_webdav=yes
else
found_webdav=no
fi])
dnl ---------------------------------------------------------------------------
dnl Playlist Plugins
dnl ---------------------------------------------------------------------------

View File

@@ -5,7 +5,7 @@
<book>
<title>The Music Player Daemon - Developer's Manual</title>
<chapter>
<chapter id="introduction">
<title>Introduction</title>
<para>
@@ -21,7 +21,7 @@
</para>
</chapter>
<chapter>
<chapter id="code_style">
<title>Code Style</title>
<itemizedlist>
@@ -40,21 +40,47 @@
<listitem>
<para>
the code should be C++11 compliant, and must compile with
comment your code, document your APIs
</para>
</listitem>
<listitem>
<para>
the code should be C++14 compliant, and must compile with
<application>GCC</application> 4.9 and
<application>clang</application> 3.4
</para>
</listitem>
<listitem>
<para>
report error conditions with C++ exceptions, preferable
derived from <varname>std::runtime_error</varname>
</para>
</listitem>
<listitem>
<para>
all code must be exception-safe
</para>
</listitem>
<listitem>
<para>
classes and functions names use CamelCase; variables are
lower-case with words separated by underscore
</para>
</listitem>
<listitem>
<para>
Some example code:
</para>
<programlisting lang="C">static inline int
foo(const char *abc, int xyz)
Foo(const char *abc, int xyz)
{
if (abc == NULL) {
if (abc == nullptr) {
LogWarning("Foo happened!");
return -1;
}
@@ -66,7 +92,7 @@ foo(const char *abc, int xyz)
</itemizedlist>
</chapter>
<chapter>
<chapter id="hacking">
<title>Hacking The Source</title>
<para>
@@ -151,7 +177,7 @@ foo(const char *abc, int xyz)
</section>
</chapter>
<chapter>
<chapter id="submitting_patches">
<title>Submitting Patches</title>
<para>
@@ -167,9 +193,16 @@ foo(const char *abc, int xyz)
url="http://git.musicpd.org/account-policy.html">an account on
git.musicpd.org</ulink>, but any public git repository will do.
</para>
<para>
There is <ulink url="https://github.com/MaxKellermann/MPD">a
mirror of the <application>MPD</application> git repository on
GitHub</ulink>, and you can use that as well to submit pull
requests.
</para>
</chapter>
<chapter>
<chapter id="tools">
<title>Development Tools</title>
<section>

View File

@@ -1868,11 +1868,23 @@ run</programlisting>
</para>
</section>
<section id="curl_storage">
<title><varname>curl</varname></title>
<para>
A WebDAV client using <filename>libcurl</filename>. It is
used when <varname>music_directory</varname> contains a
<parameter>http://</parameter> or
<parameter>https://</parameter> URI, for example
"<parameter>https://the.server/dav/</parameter>".
</para>
</section>
<section id="smbclient_storage">
<title><varname>smbclient</varname></title>
<para>
Load music files from a SMB/CIFS server. It used used when
Load music files from a SMB/CIFS server. It is used when
<varname>music_directory</varname> contains a
<parameter>smb://</parameter> URI, for example
"<parameter>smb://myfileserver/Music</parameter>".
@@ -1883,7 +1895,7 @@ run</programlisting>
<title><varname>nfs</varname></title>
<para>
Load music files from a NFS server. It used used when
Load music files from a NFS server. It is used when
<varname>music_directory</varname> contains a
<parameter>nfs://</parameter> URI according to <ulink
url="http://tools.ietf.org/html/rfc2224">RFC2224</ulink>,

View File

@@ -25,9 +25,12 @@
#include "util/ConstBuffer.hxx"
#include "util/StringAPI.hxx"
#include "util/ASCII.hxx"
#include "util/TimeParser.hxx"
#include "util/UriUtil.hxx"
#include "lib/icu/Collate.hxx"
#include <stdexcept>
#include <assert.h>
#include <stdlib.h>
@@ -180,24 +183,6 @@ SongFilter::~SongFilter()
/* this destructor exists here just so it won't get inlined */
}
#if !defined(__GLIBC__) && !defined(WIN32)
/**
* Determine the time zone offset in a portable way.
*/
gcc_const
static time_t
GetTimeZoneOffset()
{
time_t t = 1234567890;
struct tm tm;
tm.tm_isdst = 0;
gmtime_r(&t, &tm);
return t - mktime(&tm);
}
#endif
gcc_pure
static time_t
ParseTimeStamp(const char *s)
@@ -210,26 +195,13 @@ ParseTimeStamp(const char *s)
/* it's an integral UNIX time stamp */
return (time_t)value;
#ifdef WIN32
/* TODO: emulate strptime()? */
return 0;
#else
/* try ISO 8601 */
struct tm tm;
const char *end = strptime(s, "%FT%TZ", &tm);
if (end == nullptr || *end != 0)
try {
/* try ISO 8601 */
const auto t = ParseTimePoint(s, "%FT%TZ");
return std::chrono::system_clock::to_time_t(t);
} catch (const std::runtime_error &) {
return 0;
#ifdef __GLIBC__
/* timegm() is a GNU extension */
return timegm(&tm);
#else
tm.tm_isdst = 0;
return mktime(&tm) + GetTimeZoneOffset();
#endif /* !__GLIBC__ */
#endif /* !WIN32 */
}
}
bool

View File

@@ -124,7 +124,7 @@ public:
}
protected:
virtual void StartElement(const XML_Char *name, const XML_Char **attrs)
void StartElement(const XML_Char *name, const XML_Char **attrs) override
{
if (object.type != UPnPDirObject::Type::UNKNOWN &&
tag_type == TAG_NUM_OF_ITEM_TYPES) {
@@ -188,7 +188,7 @@ protected:
}
}
virtual void EndElement(const XML_Char *name)
void EndElement(const XML_Char *name) override
{
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
assert(object.type != UPnPDirObject::Type::UNKNOWN);
@@ -212,7 +212,7 @@ protected:
state = NONE;
}
virtual void CharacterData(const XML_Char *s, int len)
void CharacterData(const XML_Char *s, int len) override
{
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
assert(object.type != UPnPDirObject::Type::UNKNOWN);

View File

@@ -77,9 +77,9 @@ public:
static Database *Create(EventLoop &loop, DatabaseListener &listener,
const ConfigBlock &block);
virtual void Open() override;
virtual void Close() override;
virtual const LightSong *GetSong(const char *uri_utf8) const override;
void Open() override;
void Close() override;
const LightSong *GetSong(const char *uri_utf8) const override;
void ReturnSong(const LightSong *song) const override;
void Visit(const DatabaseSelection &selection,

View File

@@ -536,7 +536,7 @@ wavpack_streamdecode(DecoderClient &client, InputStream &is)
auto is_wvc = wavpack_open_wvc(client, is.GetURI());
if (is_wvc) {
open_flags |= OPEN_WVC;
can_seek &= wvc->is.IsSeekable();
can_seek &= is_wvc->IsSeekable();
wvc.reset(new WavpackInput(&client, *is_wvc));
}

View File

@@ -27,6 +27,7 @@
#include "AudioFormat.hxx"
#include <assert.h>
#include <stddef.h>
struct AudioFormat;
@@ -36,9 +37,10 @@ class Filter {
protected:
AudioFormat out_audio_format;
Filter() = default;
explicit Filter(AudioFormat _out_audio_format)
:out_audio_format(_out_audio_format) {}
:out_audio_format(_out_audio_format) {
assert(out_audio_format.IsValid());
}
public:
virtual ~Filter() {}
@@ -56,7 +58,6 @@ public:
* Throws std::runtime_error on error.
*
* @param src the input buffer
* @param error location to store the error occurring
* @return the destination buffer on success (will be
* invalidated by deleting this object or the next FilterPCM()
* call)

View File

@@ -45,7 +45,8 @@ class AutoConvertFilter final : public Filter {
public:
AutoConvertFilter(std::unique_ptr<Filter> &&_filter,
std::unique_ptr<Filter> &&_convert)
:filter(std::move(_filter)), convert(std::move(_convert)) {}
:Filter(_filter->GetOutAudioFormat()),
filter(std::move(_filter)), convert(std::move(_convert)) {}
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
};
@@ -92,11 +93,8 @@ PreparedAutoConvertFilter::Open(AudioFormat &in_audio_format)
ConstBuffer<void>
AutoConvertFilter::FilterPCM(ConstBuffer<void> src)
{
if (convert != nullptr) {
if (convert != nullptr)
src = convert->FilterPCM(src);
if (src.IsNull())
return nullptr;
}
return filter->FilterPCM(src);
}

View File

@@ -57,8 +57,6 @@ public:
class PreparedConvertFilter final : public PreparedFilter {
public:
void Set(const AudioFormat &_out_audio_format);
Filter *Open(AudioFormat &af) override;
};

View File

@@ -23,6 +23,7 @@
#include "lib/curl/Global.hxx"
#include "lib/curl/Request.hxx"
#include "lib/curl/Handler.hxx"
#include "lib/curl/Slist.hxx"
#include "../AsyncInputStream.hxx"
#include "../IcyInputStream.hxx"
#include "../InputPlugin.hxx"
@@ -64,7 +65,7 @@ struct CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
/* some buffers which were passed to libcurl, which we have
too free */
char range[32];
struct curl_slist *request_headers;
CurlSlist request_headers;
CurlRequest *request = nullptr;
@@ -75,7 +76,6 @@ struct CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
:AsyncInputStream(_url, _mutex, _cond,
CURL_MAX_BUFFERED,
CURL_RESUME_AT),
request_headers(nullptr),
icy(new IcyInputStream(this)) {
}
@@ -155,8 +155,7 @@ CurlInputStream::FreeEasy()
delete request;
request = nullptr;
curl_slist_free_all(request_headers);
request_headers = nullptr;
request_headers.Clear();
}
void
@@ -371,10 +370,11 @@ CurlInputStream::InitEasy()
request->SetOption(CURLOPT_SSL_VERIFYPEER, verify_peer ? 1l : 0l);
request->SetOption(CURLOPT_SSL_VERIFYHOST, verify_host ? 2l : 0l);
request_headers = nullptr;
request_headers = curl_slist_append(request_headers,
"Icy-Metadata: 1");
request->SetOption(CURLOPT_HTTPHEADER, request_headers);
request_headers.Clear();
request_headers.Append("Icy-Metadata: 1");
request->SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
request->Start();
}
void
@@ -385,11 +385,13 @@ CurlInputStream::SeekInternal(offset_type new_offset)
FreeEasy();
offset = new_offset;
if (offset == size)
if (offset == size) {
/* seek to EOF: simulate empty result; avoid
triggering a "416 Requested Range Not Satisfiable"
response */
SeekDone();
return;
}
InitEasy();

View File

@@ -96,7 +96,7 @@ private:
};
CurlGlobal::CurlGlobal(EventLoop &_loop)
:TimeoutMonitor(_loop)
:TimeoutMonitor(_loop), DeferredMonitor(_loop)
{
multi.SetOption(CURLMOPT_SOCKETFUNCTION, CurlSocket::SocketFunction);
multi.SetOption(CURLMOPT_SOCKETDATA, this);
@@ -217,25 +217,31 @@ CurlGlobal::ReadInfo()
}
}
int
CurlGlobal::TimerFunction(gcc_unused CURLM *_global, long timeout_ms, void *userp)
inline void
CurlGlobal::UpdateTimeout(long timeout_ms)
{
auto &global = *(CurlGlobal *)userp;
assert(_global == global.multi.Get());
if (timeout_ms < 0) {
global.Cancel();
return 0;
TimeoutMonitor::Cancel();
return;
}
if (timeout_ms >= 0 && timeout_ms < 10)
if (timeout_ms < 10)
/* CURL 7.21.1 likes to report "timeout=0", which
means we're running in a busy loop. Quite a bad
idea to waste so much CPU. Let's use a lower limit
of 10ms. */
timeout_ms = 10;
global.Schedule(std::chrono::milliseconds(timeout_ms));
TimeoutMonitor::Schedule(std::chrono::milliseconds(timeout_ms));
}
int
CurlGlobal::TimerFunction(gcc_unused CURLM *_global, long timeout_ms, void *userp)
{
auto &global = *(CurlGlobal *)userp;
assert(_global == global.multi.Get());
global.UpdateTimeout(timeout_ms);
return 0;
}
@@ -256,5 +262,11 @@ CurlGlobal::SocketAction(curl_socket_t fd, int ev_bitmask)
"curl_multi_socket_action() failed: %s",
curl_multi_strerror(mcode));
DeferredMonitor::Schedule();
}
void
CurlGlobal::RunDeferred()
{
ReadInfo();
}

View File

@@ -32,6 +32,7 @@
#include "Multi.hxx"
#include "event/TimeoutMonitor.hxx"
#include "event/DeferredMonitor.hxx"
class CurlSocket;
class CurlRequest;
@@ -39,12 +40,14 @@ class CurlRequest;
/**
* Manager for the global CURLM object.
*/
class CurlGlobal final : private TimeoutMonitor {
class CurlGlobal final : TimeoutMonitor, DeferredMonitor {
CurlMulti multi;
public:
explicit CurlGlobal(EventLoop &_loop);
using TimeoutMonitor::GetEventLoop;
void Add(CURL *easy, CurlRequest &request);
void Remove(CURL *easy);
@@ -76,9 +79,14 @@ public:
}
private:
void UpdateTimeout(long timeout_ms);
static int TimerFunction(CURLM *global, long timeout_ms, void *userp);
virtual void OnTimeout() override;
/* virtual methods from class TimeoutMonitor */
void OnTimeout() override;
/* virtual methods from class DeferredMonitor */
void RunDeferred() override;
};
#endif

View File

@@ -76,7 +76,7 @@ public:
return *this;
}
CURL *Get() {
CURLM *Get() {
return handle;
}

View File

@@ -46,7 +46,8 @@
CurlRequest::CurlRequest(CurlGlobal &_global, const char *url,
CurlResponseHandler &_handler)
:global(_global), handler(_handler)
:DeferredMonitor(_global.GetEventLoop()),
global(_global), handler(_handler)
{
error_buffer[0] = 0;
@@ -62,8 +63,6 @@ CurlRequest::CurlRequest(CurlGlobal &_global, const char *url,
easy.SetOption(CURLOPT_NOSIGNAL, 1l);
easy.SetOption(CURLOPT_CONNECTTIMEOUT, 10l);
easy.SetOption(CURLOPT_URL, url);
global.Add(easy.Get(), *this);
}
CurlRequest::~CurlRequest()
@@ -71,19 +70,40 @@ CurlRequest::~CurlRequest()
FreeEasy();
}
void
CurlRequest::Start()
{
assert(!registered);
global.Add(easy.Get(), *this);
registered = true;
}
void
CurlRequest::Stop()
{
if (!registered)
return;
global.Remove(easy.Get());
registered = false;
}
void
CurlRequest::FreeEasy()
{
if (!easy)
return;
global.Remove(easy.Get());
Stop();
easy = nullptr;
}
void
CurlRequest::Resume()
{
assert(registered);
curl_easy_pause(easy.Get(), CURLPAUSE_CONT);
if (IsCurlOlderThan(0x072000))
@@ -95,32 +115,24 @@ CurlRequest::Resume()
global.InvalidateSockets();
}
bool
void
CurlRequest::FinishHeaders()
{
if (state != State::HEADERS)
return true;
return;
state = State::BODY;
long status = 0;
curl_easy_getinfo(easy.Get(), CURLINFO_RESPONSE_CODE, &status);
try {
handler.OnHeaders(status, std::move(headers));
return true;
} catch (...) {
state = State::CLOSED;
handler.OnError(std::current_exception());
return false;
}
handler.OnHeaders(status, std::move(headers));
}
void
CurlRequest::FinishBody()
{
if (!FinishHeaders())
return;
FinishHeaders();
if (state != State::BODY)
return;
@@ -132,7 +144,7 @@ CurlRequest::FinishBody()
void
CurlRequest::Done(CURLcode result)
{
FreeEasy();
Stop();
try {
if (result != CURLE_OK) {
@@ -148,7 +160,12 @@ CurlRequest::Done(CURLcode result)
return;
}
FinishBody();
try {
FinishBody();
} catch (...) {
state = State::CLOSED;
handler.OnError(std::current_exception());
}
}
inline void
@@ -202,18 +219,19 @@ CurlRequest::DataReceived(const void *ptr, size_t received_size)
{
assert(received_size > 0);
if (!FinishHeaders())
return 0;
try {
FinishHeaders();
handler.OnData({ptr, received_size});
return received_size;
} catch (Pause) {
return CURL_WRITEFUNC_PAUSE;
} catch (...) {
state = State::CLOSED;
handler.OnError(std::current_exception());
return 0;
/* move the CurlResponseHandler::OnError() call into a
"safe" stack frame */
postponed_error = std::current_exception();
DeferredMonitor::Schedule();
return CURL_WRITEFUNC_PAUSE;
}
}
@@ -229,3 +247,11 @@ CurlRequest::WriteFunction(void *ptr, size_t size, size_t nmemb, void *stream)
return c.DataReceived(ptr, size);
}
void
CurlRequest::RunDeferred()
{
assert(postponed_error);
handler.OnError(postponed_error);
}

View File

@@ -31,15 +31,17 @@
#define CURL_REQUEST_HXX
#include "Easy.hxx"
#include "event/DeferredMonitor.hxx"
#include <map>
#include <string>
#include <exception>
struct StringView;
class CurlGlobal;
class CurlResponseHandler;
class CurlRequest {
class CurlRequest final : DeferredMonitor {
CurlGlobal &global;
CurlResponseHandler &handler;
@@ -55,10 +57,25 @@ class CurlRequest {
std::multimap<std::string, std::string> headers;
/**
* An exception caught by DataReceived(), which will be
* forwarded into a "safe" stack frame by
* DeferredMonitor::RunDeferred(). This works around the
* problem that libcurl crashes if you call
* curl_multi_remove_handle() from within the WRITEFUNCTION
* (i.e. DataReceived()).
*/
std::exception_ptr postponed_error;
/** error message provided by libcurl */
char error_buffer[CURL_ERROR_SIZE];
bool registered = false;
public:
/**
* To start sending the request, call Start().
*/
CurlRequest(CurlGlobal &_global, const char *url,
CurlResponseHandler &_handler);
~CurlRequest();
@@ -66,6 +83,21 @@ public:
CurlRequest(const CurlRequest &) = delete;
CurlRequest &operator=(const CurlRequest &) = delete;
/**
* Register this request via CurlGlobal::Add(), which starts
* the request.
*
* This method must be called in the event loop thread.
*/
void Start();
/**
* Unregister this request via CurlGlobal::Remove().
*
* This method must be called in the event loop thread.
*/
void Stop();
CURL *Get() {
return easy.Get();
}
@@ -95,7 +127,7 @@ private:
*/
void FreeEasy();
bool FinishHeaders();
void FinishHeaders();
void FinishBody();
size_t DataReceived(const void *ptr, size_t size);
@@ -109,6 +141,9 @@ private:
/** called by curl when new data is available */
static size_t WriteFunction(void *ptr, size_t size, size_t nmemb,
void *stream);
/* virtual methods from class DeferredMonitor */
void RunDeferred() override;
};
#endif

76
src/lib/curl/Slist.hxx Normal file
View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2008-2017 Max Kellermann <max@duempel.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CURL_SLIST_HXX
#define CURL_SLIST_HXX
#include <curl/curl.h>
#include <algorithm>
/**
* OO wrapper for "struct curl_slist *".
*/
class CurlSlist {
struct curl_slist *head = nullptr;
public:
CurlSlist() = default;
CurlSlist(CurlSlist &&src)
:head(std::exchange(src.head, nullptr)) {}
~CurlSlist() {
if (head != nullptr)
curl_slist_free_all(head);
}
CurlSlist &operator=(CurlSlist &&src) {
std::swap(head, src.head);
return *this;
}
struct curl_slist *Get() {
return head;
}
void Clear() {
curl_slist_free_all(head);
head = nullptr;
}
void Append(const char *value) {
auto *new_head = curl_slist_append(head, value);
if (new_head == nullptr)
throw std::runtime_error("curl_slist_append() failed");
head = new_head;
}
};
#endif

View File

@@ -19,7 +19,6 @@
#include "config.h"
#include "ExpatParser.hxx"
#include "input/InputStream.hxx"
#include "util/ASCII.hxx"
#include <string.h>
@@ -31,23 +30,6 @@ ExpatParser::Parse(const char *data, size_t length, bool is_final)
throw ExpatError(parser);
}
void
ExpatParser::Parse(InputStream &is)
{
assert(is.IsReady());
while (true) {
char buffer[4096];
size_t nbytes = is.LockRead(buffer, sizeof(buffer));
if (nbytes == 0)
break;
Parse(buffer, nbytes, false);
}
Parse("", 0, true);
}
const char *
ExpatParser::GetAttribute(const XML_Char **atts,
const char *name)

View File

@@ -31,22 +31,31 @@ class InputStream;
class ExpatError final : public std::runtime_error {
public:
ExpatError(XML_Error code)
explicit ExpatError(XML_Error code)
:std::runtime_error(XML_ErrorString(code)) {}
ExpatError(XML_Parser parser)
explicit ExpatError(XML_Parser parser)
:ExpatError(XML_GetErrorCode(parser)) {}
};
struct ExpatNamespaceSeparator {
char separator;
};
class ExpatParser final {
const XML_Parser parser;
public:
ExpatParser(void *userData)
explicit ExpatParser(void *userData)
:parser(XML_ParserCreate(nullptr)) {
XML_SetUserData(parser, userData);
}
ExpatParser(ExpatNamespaceSeparator ns, void *userData)
:parser(XML_ParserCreateNS(nullptr, ns.separator)) {
XML_SetUserData(parser, userData);
}
~ExpatParser() {
XML_ParserFree(parser);
}
@@ -89,6 +98,12 @@ public:
parser.SetCharacterDataHandler(CharacterData);
}
explicit CommonExpatParser(ExpatNamespaceSeparator ns)
:parser(ns, this) {
parser.SetElementHandler(StartElement, EndElement);
parser.SetCharacterDataHandler(CharacterData);
}
void Parse(const char *data, size_t length, bool is_final) {
parser.Parse(data, length, is_final);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2003-2017 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "config.h"
#include "ExpatParser.hxx"
#include "input/InputStream.hxx"
void
ExpatParser::Parse(InputStream &is)
{
assert(is.IsReady());
while (true) {
char buffer[4096];
size_t nbytes = is.LockRead(buffer, sizeof(buffer));
if (nbytes == 0)
break;
Parse(buffer, nbytes, false);
}
Parse("", 0, true);
}

View File

@@ -25,15 +25,18 @@
#include "event/DeferredMonitor.hxx"
#include "util/ASCII.hxx"
#include "util/ReusableArray.hxx"
#include "util/Clamp.hxx"
#include "util/Domain.hxx"
#include "util/RuntimeError.hxx"
#include "Log.hxx"
#include <algorithm>
extern "C" {
#include "volume_mapping.h"
}
#include <alsa/asoundlib.h>
#include <math.h>
#define VOLUME_MIXER_ALSA_DEFAULT "default"
#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT "PCM"
static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0;
@@ -68,9 +71,6 @@ class AlsaMixer final : public Mixer {
snd_mixer_t *handle;
snd_mixer_elem_t *elem;
long volume_min;
long volume_max;
int volume_set;
AlsaMixerMonitor *monitor;
@@ -228,9 +228,6 @@ AlsaMixer::Setup()
if (elem == nullptr)
throw FormatRuntimeError("no such mixer control: %s", control);
snd_mixer_selem_get_playback_volume_range(elem, &volume_min,
&volume_max);
snd_mixer_elem_set_callback_private(elem, this);
snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback);
@@ -242,8 +239,6 @@ AlsaMixer::Open()
{
int err;
volume_set = -1;
err = snd_mixer_open(&handle, 0);
if (err < 0)
throw FormatRuntimeError("snd_mixer_open() failed: %s",
@@ -272,8 +267,6 @@ int
AlsaMixer::GetVolume()
{
int err;
int ret;
long level;
assert(handle != nullptr);
@@ -282,43 +275,15 @@ AlsaMixer::GetVolume()
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
snd_strerror(err));
err = snd_mixer_selem_get_playback_volume(elem,
SND_MIXER_SCHN_FRONT_LEFT,
&level);
if (err < 0)
throw FormatRuntimeError("failed to read ALSA volume: %s",
snd_strerror(err));
ret = ((volume_set / 100.0) * (volume_max - volume_min)
+ volume_min) + 0.5;
if (volume_set > 0 && ret == level) {
ret = volume_set;
} else {
ret = (int)(100 * (((float)(level - volume_min)) /
(volume_max - volume_min)) + 0.5);
}
return ret;
return lrint(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT));
}
void
AlsaMixer::SetVolume(unsigned volume)
{
float vol;
long level;
int err;
assert(handle != nullptr);
vol = volume;
volume_set = vol + 0.5;
level = (long)(((vol / 100.0) * (volume_max - volume_min) +
volume_min) + 0.5);
level = Clamp(level, volume_min, volume_max);
err = snd_mixer_selem_set_playback_volume_all(elem, level);
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
if (err < 0)
throw FormatRuntimeError("failed to set ALSA volume: %s",
snd_strerror(err));

View File

@@ -0,0 +1,180 @@
/*
* Copyright (c) 2010 Clemens Ladisch <clemens@ladisch.de>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* The functions in this file map the value ranges of ALSA mixer controls onto
* the interval 0..1.
*
* The mapping is designed so that the position in the interval is proportional
* to the volume as a human ear would perceive it (i.e., the position is the
* cubic root of the linear sample multiplication factor). For controls with
* a small range (24 dB or less), the mapping is linear in the dB values so
* that each step has the same size visually. Only for controls without dB
* information, a linear mapping of the hardware volume register values is used
* (this is the same algorithm as used in the old alsamixer).
*
* When setting the volume, 'dir' is the rounding direction:
* -1/0/1 = down/nearest/up.
*/
#include <math.h>
#include <stdbool.h>
#include "volume_mapping.h"
#ifdef __UCLIBC__
/* 10^x = 10^(log e^x) = (e^x)^log10 = e^(x * log 10) */
#define exp10(x) (exp((x) * log(10)))
#endif /* __UCLIBC__ */
#define MAX_LINEAR_DB_SCALE 24
static inline bool use_linear_dB_scale(long dBmin, long dBmax)
{
return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100;
}
static long lrint_dir(double x, int dir)
{
if (dir > 0)
return lrint(ceil(x));
else if (dir < 0)
return lrint(floor(x));
else
return lrint(x);
}
enum ctl_dir { PLAYBACK, CAPTURE };
static int (* const get_dB_range[2])(snd_mixer_elem_t *, long *, long *) = {
snd_mixer_selem_get_playback_dB_range,
snd_mixer_selem_get_capture_dB_range,
};
static int (* const get_raw_range[2])(snd_mixer_elem_t *, long *, long *) = {
snd_mixer_selem_get_playback_volume_range,
snd_mixer_selem_get_capture_volume_range,
};
static int (* const get_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = {
snd_mixer_selem_get_playback_dB,
snd_mixer_selem_get_capture_dB,
};
static int (* const get_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = {
snd_mixer_selem_get_playback_volume,
snd_mixer_selem_get_capture_volume,
};
static int (* const set_dB[2])(snd_mixer_elem_t *, long, int) = {
snd_mixer_selem_set_playback_dB_all,
snd_mixer_selem_set_capture_dB_all,
};
static int (* const set_raw[2])(snd_mixer_elem_t *, long) = {
snd_mixer_selem_set_playback_volume_all,
snd_mixer_selem_set_capture_volume_all,
};
static double get_normalized_volume(snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel,
enum ctl_dir ctl_dir)
{
long min, max, value;
double normalized, min_norm;
int err;
err = get_dB_range[ctl_dir](elem, &min, &max);
if (err < 0 || min >= max) {
err = get_raw_range[ctl_dir](elem, &min, &max);
if (err < 0 || min == max)
return 0;
err = get_raw[ctl_dir](elem, channel, &value);
if (err < 0)
return 0;
return (value - min) / (double)(max - min);
}
err = get_dB[ctl_dir](elem, channel, &value);
if (err < 0)
return 0;
if (use_linear_dB_scale(min, max))
return (value - min) / (double)(max - min);
normalized = exp10((value - max) / 6000.0);
if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
min_norm = exp10((min - max) / 6000.0);
normalized = (normalized - min_norm) / (1 - min_norm);
}
return normalized;
}
static int set_normalized_volume(snd_mixer_elem_t *elem,
double volume,
int dir,
enum ctl_dir ctl_dir)
{
long min, max, value;
double min_norm;
int err;
err = get_dB_range[ctl_dir](elem, &min, &max);
if (err < 0 || min >= max) {
err = get_raw_range[ctl_dir](elem, &min, &max);
if (err < 0)
return err;
value = lrint_dir(volume * (max - min), dir) + min;
return set_raw[ctl_dir](elem, value);
}
if (use_linear_dB_scale(min, max)) {
value = lrint_dir(volume * (max - min), dir) + min;
return set_dB[ctl_dir](elem, value, dir);
}
if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
min_norm = exp10((min - max) / 6000.0);
volume = volume * (1 - min_norm) + min_norm;
}
value = lrint_dir(6000.0 * log10(volume), dir) + max;
return set_dB[ctl_dir](elem, value, dir);
}
double get_normalized_playback_volume(snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel)
{
return get_normalized_volume(elem, channel, PLAYBACK);
}
double get_normalized_capture_volume(snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel)
{
return get_normalized_volume(elem, channel, CAPTURE);
}
int set_normalized_playback_volume(snd_mixer_elem_t *elem,
double volume,
int dir)
{
return set_normalized_volume(elem, volume, dir, PLAYBACK);
}
int set_normalized_capture_volume(snd_mixer_elem_t *elem,
double volume,
int dir)
{
return set_normalized_volume(elem, volume, dir, CAPTURE);
}

View File

@@ -0,0 +1,17 @@
#ifndef VOLUME_MAPPING_H_INCLUDED
#define VOLUME_MAPPING_H_INCLUDED
#include <alsa/asoundlib.h>
double get_normalized_playback_volume(snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel);
double get_normalized_capture_volume(snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel);
int set_normalized_playback_volume(snd_mixer_elem_t *elem,
double volume,
int dir);
int set_normalized_capture_volume(snd_mixer_elem_t *elem,
double volume,
int dir);
#endif

View File

@@ -23,6 +23,7 @@
#include "plugins/LocalStorage.hxx"
#include "plugins/SmbclientStorage.hxx"
#include "plugins/NfsStorage.hxx"
#include "plugins/CurlStorage.hxx"
#include <assert.h>
#include <string.h>
@@ -34,6 +35,9 @@ const StoragePlugin *const storage_plugins[] = {
#endif
#ifdef ENABLE_NFS
&nfs_storage_plugin,
#endif
#ifdef ENABLE_WEBDAV
&curl_storage_plugin,
#endif
nullptr
};

View File

@@ -0,0 +1,590 @@
/*
* Copyright 2003-2016 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "config.h"
#include "CurlStorage.hxx"
#include "storage/StoragePlugin.hxx"
#include "storage/StorageInterface.hxx"
#include "storage/FileInfo.hxx"
#include "storage/MemoryDirectoryReader.hxx"
#include "lib/curl/Global.hxx"
#include "lib/curl/Slist.hxx"
#include "lib/curl/Request.hxx"
#include "lib/curl/Handler.hxx"
#include "lib/expat/ExpatParser.hxx"
#include "fs/Traits.hxx"
#include "event/Call.hxx"
#include "event/DeferredMonitor.hxx"
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx"
#include "util/TimeParser.hxx"
#include "util/UriUtil.hxx"
#include <algorithm>
#include <memory>
#include <string>
#include <list>
#include <assert.h>
class CurlStorage final : public Storage {
const std::string base;
CurlGlobal *const curl;
public:
CurlStorage(EventLoop &_loop, const char *_base)
:base(_base),
curl(new CurlGlobal(_loop)) {}
~CurlStorage() {
BlockingCall(curl->GetEventLoop(), [this](){ delete curl; });
}
/* virtual methods from class Storage */
StorageFileInfo GetInfo(const char *uri_utf8, bool follow) override;
StorageDirectoryReader *OpenDirectory(const char *uri_utf8) override;
std::string MapUTF8(const char *uri_utf8) const override;
const char *MapToRelativeUTF8(const char *uri_utf8) const override;
};
std::string
CurlStorage::MapUTF8(const char *uri_utf8) const
{
assert(uri_utf8 != nullptr);
if (StringIsEmpty(uri_utf8))
return base;
// TODO: escape the given URI
return PathTraitsUTF8::Build(base.c_str(), uri_utf8);
}
const char *
CurlStorage::MapToRelativeUTF8(const char *uri_utf8) const
{
// TODO: escape/unescape?
return PathTraitsUTF8::Relative(base.c_str(), uri_utf8);
}
class BlockingHttpRequest : protected CurlResponseHandler, DeferredMonitor {
std::exception_ptr postponed_error;
bool done = false;
protected:
CurlRequest request;
Mutex mutex;
Cond cond;
public:
BlockingHttpRequest(CurlGlobal &curl, const char *uri)
:DeferredMonitor(curl.GetEventLoop()),
request(curl, uri, *this) {
// TODO: use CurlInputStream's configuration
/* start the transfer inside the IOThread */
DeferredMonitor::Schedule();
}
void Wait() {
const std::lock_guard<Mutex> lock(mutex);
while (!done)
cond.wait(mutex);
if (postponed_error)
std::rethrow_exception(postponed_error);
}
protected:
void SetDone() {
assert(!done);
request.Stop();
done = true;
cond.signal();
}
void LockSetDone() {
const std::lock_guard<Mutex> lock(mutex);
SetDone();
}
private:
/* virtual methods from DeferredMonitor */
void RunDeferred() final {
assert(!done);
request.Start();
}
/* virtual methods from CurlResponseHandler */
void OnError(std::exception_ptr e) final {
const std::lock_guard<Mutex> lock(mutex);
postponed_error = std::move(e);
SetDone();
}
};
/**
* A helper class which feeds a (foreign) memory buffer into the
* CURLOPT_READFUNCTION.
*/
class CurlRequestBody {
ConstBuffer<char> data;
public:
explicit CurlRequestBody(ConstBuffer<void> _data)
:data(ConstBuffer<char>::FromVoid(_data)) {}
explicit constexpr CurlRequestBody(StringView _data)
:data(_data) {}
template<typename T>
CurlRequestBody(CurlRequest &request, T _data)
:CurlRequestBody(_data) {
request.SetOption(CURLOPT_READFUNCTION, Callback);
request.SetOption(CURLOPT_READDATA, this);
}
private:
size_t Read(char *buffer, size_t size) {
size_t n = std::min(size, data.size);
std::copy_n(data.begin(), n, buffer);
return n;
}
static size_t Callback(char *buffer, size_t size, size_t nitems,
void *instream) {
auto &rb = *(CurlRequestBody *)instream;
return rb.Read(buffer, size * nitems);
}
};
/**
* The (relevant) contents of a "<D:response>" element.
*/
struct DavResponse {
std::string href;
unsigned status = 0;
bool collection = false;
std::chrono::system_clock::time_point mtime =
std::chrono::system_clock::time_point::min();
uint64_t length = 0;
bool Check() const {
return !href.empty();
}
};
static unsigned
ParseStatus(const char *s)
{
/* skip the "HTTP/1.1" prefix */
const char *space = strchr(s, ' ');
if (space == nullptr)
return 0;
return strtoul(space + 1, nullptr, 10);
}
static unsigned
ParseStatus(const char *s, size_t length)
{
return ParseStatus(std::string(s, length).c_str());
}
static std::chrono::system_clock::time_point
ParseTimeStamp(const char *s)
{
try {
// TODO: make this more robust
return ParseTimePoint(s, "%a, %d %b %Y %T %Z");
} catch (const std::runtime_error &) {
return std::chrono::system_clock::time_point::min();
}
}
static std::chrono::system_clock::time_point
ParseTimeStamp(const char *s, size_t length)
{
return ParseTimeStamp(std::string(s, length).c_str());
}
static uint64_t
ParseU64(const char *s)
{
return strtoull(s, nullptr, 10);
}
static uint64_t
ParseU64(const char *s, size_t length)
{
return ParseU64(std::string(s, length).c_str());
}
/**
* A WebDAV PROPFIND request. Each "response" element will be passed
* to OnDavResponse() (to be implemented by a derived class).
*/
class PropfindOperation : BlockingHttpRequest, CommonExpatParser {
CurlSlist request_headers;
CurlRequestBody request_body;
enum class State {
ROOT,
RESPONSE,
HREF,
STATUS,
TYPE,
MTIME,
LENGTH,
} state = State::ROOT;
DavResponse response;
public:
PropfindOperation(CurlGlobal &_curl, const char *_uri, unsigned depth)
:BlockingHttpRequest(_curl, _uri),
CommonExpatParser(ExpatNamespaceSeparator{'|'}),
request_body(request,
"<?xml version=\"1.0\"?>\n"
"<a:propfind xmlns:a=\"DAV:\">"
"<a:prop><a:getcontenttype/></a:prop>"
"<a:prop><a:getcontentlength/></a:prop>"
"</a:propfind>")
{
request.SetOption(CURLOPT_CUSTOMREQUEST, "PROPFIND");
char buffer[40];
sprintf(buffer, "depth: %u", depth);
request_headers.Append(buffer);
request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
// TODO: send request body
}
using BlockingHttpRequest::Wait;
protected:
virtual void OnDavResponse(DavResponse &&r) = 0;
private:
void FinishResponse() {
if (response.Check())
OnDavResponse(std::move(response));
response = DavResponse();
}
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) final {
if (status != 207)
throw FormatRuntimeError("Status %d from WebDAV server; expected \"207 Multi-Status\"",
status);
auto i = headers.find("content-type");
if (i == headers.end() ||
strncmp(i->second.c_str(), "text/xml", 8) != 0)
throw std::runtime_error("Unexpected Content-Type from WebDAV server");
}
void OnData(ConstBuffer<void> _data) final {
const auto data = ConstBuffer<char>::FromVoid(_data);
Parse(data.data, data.size, false);
}
void OnEnd() final {
Parse("", 0, true);
LockSetDone();
}
/* virtual methods from CommonExpatParser */
void StartElement(const XML_Char *name,
gcc_unused const XML_Char **attrs) final {
switch (state) {
case State::ROOT:
if (strcmp(name, "DAV:|response") == 0)
state = State::RESPONSE;
break;
case State::RESPONSE:
if (strcmp(name, "DAV:|href") == 0)
state = State::HREF;
else if (strcmp(name, "DAV:|status") == 0)
state = State::STATUS;
else if (strcmp(name, "DAV:|resourcetype") == 0)
state = State::TYPE;
else if (strcmp(name, "DAV:|getlastmodified") == 0)
state = State::MTIME;
else if (strcmp(name, "DAV:|getcontentlength") == 0)
state = State::LENGTH;
break;
case State::TYPE:
if (strcmp(name, "DAV:|collection") == 0)
response.collection = true;
break;
case State::HREF:
case State::STATUS:
case State::LENGTH:
case State::MTIME:
break;
}
}
void EndElement(const XML_Char *name) final {
switch (state) {
case State::ROOT:
break;
case State::RESPONSE:
if (strcmp(name, "DAV:|response") == 0) {
FinishResponse();
state = State::ROOT;
}
break;
case State::HREF:
if (strcmp(name, "DAV:|href") == 0)
state = State::RESPONSE;
break;
case State::STATUS:
if (strcmp(name, "DAV:|status") == 0)
state = State::RESPONSE;
break;
case State::TYPE:
if (strcmp(name, "DAV:|resourcetype") == 0)
state = State::RESPONSE;
break;
case State::MTIME:
if (strcmp(name, "DAV:|getlastmodified") == 0)
state = State::RESPONSE;
break;
case State::LENGTH:
if (strcmp(name, "DAV:|getcontentlength") == 0)
state = State::RESPONSE;
break;
}
}
void CharacterData(const XML_Char *s, int len) final {
switch (state) {
case State::ROOT:
case State::RESPONSE:
case State::TYPE:
break;
case State::HREF:
response.href.assign(s, len);
break;
case State::STATUS:
response.status = ParseStatus(s, len);
break;
case State::MTIME:
response.mtime = ParseTimeStamp(s, len);
break;
case State::LENGTH:
response.length = ParseU64(s, len);
break;
}
}
};
/**
* Obtain information about a single file using WebDAV PROPFIND.
*/
class HttpGetInfoOperation final : public PropfindOperation {
StorageFileInfo info;
public:
HttpGetInfoOperation(CurlGlobal &curl, const char *uri)
:PropfindOperation(curl, uri, 0) {
info.type = StorageFileInfo::Type::OTHER;
info.size = 0;
info.mtime = 0;
info.device = info.inode = 0;
}
const StorageFileInfo &Perform() {
Wait();
return info;
}
protected:
/* virtual methods from PropfindOperation */
void OnDavResponse(DavResponse &&r) override {
if (r.status != 200)
return;
info.type = r.collection
? StorageFileInfo::Type::DIRECTORY
: StorageFileInfo::Type::REGULAR;
info.size = r.length;
info.mtime = r.mtime > std::chrono::system_clock::time_point()
? std::chrono::system_clock::to_time_t(r.mtime)
: 0;
info.device = info.inode = 0;
}
};
StorageFileInfo
CurlStorage::GetInfo(const char *uri_utf8, gcc_unused bool follow)
{
// TODO: escape the given URI
std::string uri = base;
uri += uri_utf8;
return HttpGetInfoOperation(*curl, uri.c_str()).Perform();
}
gcc_pure
static const char *
UriPathOrSlash(const char *uri)
{
const char *path = uri_get_path(uri);
if (path == nullptr)
path = "/";
return path;
}
/**
* Obtain a directory listing using WebDAV PROPFIND.
*/
class HttpListDirectoryOperation final : public PropfindOperation {
const std::string base_path;
MemoryStorageDirectoryReader::List entries;
public:
HttpListDirectoryOperation(CurlGlobal &curl, const char *uri)
:PropfindOperation(curl, uri, 1),
base_path(UriPathOrSlash(uri)) {}
StorageDirectoryReader *Perform() {
Wait();
return ToReader();
}
private:
StorageDirectoryReader *ToReader() {
return new MemoryStorageDirectoryReader(std::move(entries));
}
/**
* Convert a "href" attribute (which may be an absolute URI)
* to the base file name.
*/
gcc_pure
StringView HrefToEscapedName(const char *href) const {
const char *path = uri_get_path(href);
if (path == nullptr)
return nullptr;
path = StringAfterPrefix(path, base_path.c_str());
if (path == nullptr || *path == 0)
return nullptr;
const char *slash = strchr(path, '/');
if (slash == nullptr)
/* regular file */
return path;
else if (slash[1] == 0)
/* trailing slash: collection; strip the slash */
return {path, slash};
else
/* strange, better ignore it */
return nullptr;
}
protected:
/* virtual methods from PropfindOperation */
void OnDavResponse(DavResponse &&r) override {
if (r.status != 200)
return;
const auto escaped_name = HrefToEscapedName(r.href.c_str());
if (escaped_name.IsNull())
return;
// TODO: unescape
const auto name = escaped_name;
entries.emplace_front(std::string(name.data, name.size));
auto &info = entries.front().info;
info.type = r.collection
? StorageFileInfo::Type::DIRECTORY
: StorageFileInfo::Type::REGULAR;
info.size = r.length;
info.mtime = r.mtime > std::chrono::system_clock::time_point()
? std::chrono::system_clock::to_time_t(r.mtime)
: 0;
info.device = info.inode = 0;
}
};
StorageDirectoryReader *
CurlStorage::OpenDirectory(const char *uri_utf8)
{
// TODO: escape the given URI
std::string uri = base;
uri += uri_utf8;
/* collection URIs must end with a slash */
if (uri.back() != '/')
uri.push_back('/');
return HttpListDirectoryOperation(*curl, uri.c_str()).Perform();
}
static Storage *
CreateCurlStorageURI(EventLoop &event_loop, const char *uri)
{
if (strncmp(uri, "http://", 7) != 0 &&
strncmp(uri, "https://", 8) != 0)
return nullptr;
return new CurlStorage(event_loop, uri);
}
const StoragePlugin curl_storage_plugin = {
"curl",
CreateCurlStorageURI,
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2003-2016 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_STORAGE_CURL_HXX
#define MPD_STORAGE_CURL_HXX
#include "check.h"
struct StoragePlugin;
extern const StoragePlugin curl_storage_plugin;
#endif

81
src/util/TimeParser.cxx Normal file
View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "TimeParser.hxx"
#include <stdexcept>
#include <assert.h>
#include <time.h>
#if !defined(__GLIBC__) && !defined(WIN32)
/**
* Determine the time zone offset in a portable way.
*/
gcc_const
static time_t
GetTimeZoneOffset()
{
time_t t = 1234567890;
struct tm tm;
tm.tm_isdst = 0;
gmtime_r(&t, &tm);
return t - mktime(&tm);
}
#endif
std::chrono::system_clock::time_point
ParseTimePoint(const char *s, const char *format)
{
assert(s != nullptr);
assert(format != nullptr);
#ifdef WIN32
/* TODO: emulate strptime()? */
throw std::runtime_error("Time parsing not implemented on Windows");
#else
struct tm tm;
const char *end = strptime(s, format, &tm);
if (end == nullptr || *end != 0)
throw std::runtime_error("Failed to parse time stamp");
#ifdef __GLIBC__
/* timegm() is a GNU extension */
const auto t = timegm(&tm);
#else
tm.tm_isdst = 0;
const auto t = mktime(&tm) + GetTimeZoneOffset();
#endif /* !__GLIBC__ */
return std::chrono::system_clock::from_time_t(t);
#endif /* !WIN32 */
}

46
src/util/TimeParser.hxx Normal file
View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef TIME_PARSER_HXX
#define TIME_PARSER_HXX
#include "Compiler.h"
#include <chrono>
/**
* Parse a time stamp.
*
* Throws std::runtime_error on error.
*/
gcc_pure
std::chrono::system_clock::time_point
ParseTimePoint(const char *s, const char *format);
#endif

View File

@@ -19,10 +19,57 @@
#include "UriUtil.hxx"
#include "StringCompare.hxx"
#include "CharUtil.hxx"
#include <assert.h>
#include <string.h>
static constexpr bool
IsValidSchemeStart(char ch)
{
return IsLowerAlphaASCII(ch);
}
static constexpr bool
IsValidSchemeChar(char ch)
{
return IsLowerAlphaASCII(ch) || IsDigitASCII(ch) ||
ch == '+' || ch == '.' || ch == '-';
}
gcc_pure
static bool
IsValidScheme(StringView p)
{
if (p.IsEmpty() || !IsValidSchemeStart(p.front()))
return false;
for (size_t i = 1; i < p.size; ++i)
if (!IsValidSchemeChar(p[i]))
return false;
return true;
}
/**
* Return the URI part after the scheme specification (and after the
* double slash).
*/
gcc_pure
static const char *
uri_after_scheme(const char *uri)
{
if (uri[0] == '/' && uri[1] == '/' && uri[2] != '/')
return uri + 2;
const char *colon = strchr(uri, ':');
return colon != nullptr &&
IsValidScheme({uri, colon}) &&
colon[1] == '/' && colon[2] == '/'
? colon + 3
: nullptr;
}
bool uri_has_scheme(const char *uri)
{
return strstr(uri, "://") != nullptr;
@@ -38,6 +85,16 @@ uri_get_scheme(const char *uri)
return std::string(uri, end);
}
const char *
uri_get_path(const char *uri)
{
const char *ap = uri_after_scheme(uri);
if (ap != nullptr)
return strchr(ap, '/');
return uri;
}
/* suffixes should be ascii only characters */
const char *
uri_get_suffix(const char *uri)

View File

@@ -38,6 +38,14 @@ gcc_pure
std::string
uri_get_scheme(const char *uri);
/**
* Returns the URI path (including the query string) or nullptr if the
* given URI has no path.
*/
gcc_pure gcc_nonnull_all
const char *
uri_get_path(const char *uri);
gcc_pure
const char *
uri_get_suffix(const char *uri);

View File

@@ -67,8 +67,13 @@ Ls(Storage &storage, const char *path)
break;
}
char mtime[32];
strftime(mtime, sizeof(mtime), "%F", gmtime(&info.mtime));
char mtime_buffer[32];
const char *mtime = " ";
if (info.mtime > 0) {
strftime(mtime_buffer, sizeof(mtime_buffer), "%F",
gmtime(&info.mtime));
mtime = mtime_buffer;
}
printf("%s %10llu %s %s\n",
type, (unsigned long long)info.size,