diff --git a/NEWS b/NEWS index d60469c65..59c899c6e 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,17 @@ ver 0.22 (not yet released) * switch to C++17 - GCC 7 or clang 4 (or newer) recommended +ver 0.21.16 (2019/10/16) +* queue + - fix relative destination offset when moving a range +* storage + - curl: request the "resourcetype" property to fix database update + - curl: URL-encode more paths + - curl: follow redirects for collections without trailing slash +* update + - fix crash when music_directory is not a directory +* fix build with iconv() instead of ICU + ver 0.21.15 (2019/09/25) * decoder - dsdiff, dsf: fix displayed bit rate diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 282707c61..e6bf686a9 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="39" + android:versionName="0.21.16"> diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx index fbdb2957e..b540114eb 100644 --- a/src/db/update/Walk.cxx +++ b/src/db/update/Walk.cxx @@ -475,6 +475,12 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept if (!GetInfo(storage, "", info)) return false; + if (!info.IsDirectory()) { + FormatError(update_domain, "Not a directory: %s", + storage.MapUTF8("").c_str()); + return false; + } + ExcludeList exclude_list; UpdateDirectory(root, exclude_list, info); diff --git a/src/lib/curl/Easy.hxx b/src/lib/curl/Easy.hxx index 479f8f530..20e25a5c2 100644 --- a/src/lib/curl/Easy.hxx +++ b/src/lib/curl/Easy.hxx @@ -30,6 +30,7 @@ #ifndef CURL_EASY_HXX #define CURL_EASY_HXX +#include "String.hxx" #include "util/Compiler.h" #include @@ -208,8 +209,8 @@ public: return ::curl_easy_pause(handle, CURLPAUSE_CONT) == CURLE_OK; } - char *Escape(const char *string, int length=0) const noexcept { - return curl_easy_escape(handle, string, length); + CurlString Escape(const char *string, int length=0) const noexcept { + return CurlString(curl_easy_escape(handle, string, length)); } }; diff --git a/src/lib/curl/Escape.cxx b/src/lib/curl/Escape.cxx new file mode 100644 index 000000000..ab832eac9 --- /dev/null +++ b/src/lib/curl/Escape.cxx @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Max Kellermann + * + * 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 "Escape.hxx" +#include "Easy.hxx" +#include "String.hxx" +#include "util/IterableSplitString.hxx" + +std::string +CurlEscapeUriPath(CURL *curl, StringView src) noexcept +{ + std::string dest; + + for (const auto i : IterableSplitString(src, '/')) { + CurlString escaped(curl_easy_escape(curl, i.data, i.size)); + if (!dest.empty()) + dest.push_back('/'); + dest += escaped.c_str(); + } + + return dest; +} + +std::string +CurlEscapeUriPath(StringView src) noexcept +{ + CurlEasy easy; + return CurlEscapeUriPath(easy.Get(), src); +} + +std::string +CurlUnescape(CURL *curl, StringView src) noexcept +{ + int outlength; + CurlString tmp(curl_easy_unescape(curl, src.data, src.size, + &outlength)); + return std::string(tmp.c_str(), outlength); +} + +std::string +CurlUnescape(StringView src) noexcept +{ + CurlEasy easy; + return CurlUnescape(easy.Get(), src); +} diff --git a/src/lib/curl/Escape.hxx b/src/lib/curl/Escape.hxx new file mode 100644 index 000000000..1a7c9ab70 --- /dev/null +++ b/src/lib/curl/Escape.hxx @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 Max Kellermann + * + * 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_ESCAPE_HXX +#define CURL_ESCAPE_HXX + +#include + +#include + +struct StringView; + +std::string +CurlEscapeUriPath(CURL *curl, StringView src) noexcept; + +std::string +CurlEscapeUriPath(StringView src) noexcept; + +std::string +CurlUnescape(CURL *curl, StringView src) noexcept; + +std::string +CurlUnescape(StringView src) noexcept; + +#endif diff --git a/src/lib/curl/Form.cxx b/src/lib/curl/Form.cxx index 7e9d84753..65430641d 100644 --- a/src/lib/curl/Form.cxx +++ b/src/lib/curl/Form.cxx @@ -28,6 +28,7 @@ */ #include "Form.hxx" +#include "String.hxx" std::string EncodeForm(CURL *curl, @@ -43,12 +44,10 @@ EncodeForm(CURL *curl, result.push_back('='); if (!i.second.empty()) { - char *value = curl_easy_escape(curl, i.second.data(), - i.second.length()); - if (value != nullptr) { + CurlString value(curl_easy_escape(curl, i.second.data(), + i.second.length())); + if (value) result.append(value); - curl_free(value); - } } } diff --git a/src/lib/curl/String.hxx b/src/lib/curl/String.hxx new file mode 100644 index 000000000..f39c28992 --- /dev/null +++ b/src/lib/curl/String.hxx @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Max Kellermann + * + * 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_STRING_HXX +#define CURL_STRING_HXX + +#include + +#include + +/** + * An OO wrapper for an allocated string to be freed with curl_free(). + */ +class CurlString { + char *p = nullptr; + +public: + CurlString() noexcept = default; + CurlString(std::nullptr_t) noexcept {} + + explicit CurlString(char *_p) noexcept + :p(_p) {} + + CurlString(CurlString &&src) noexcept + :p(std::exchange(src.p, nullptr)) {} + + ~CurlString() noexcept { + if (p != nullptr) + curl_free(p); + } + + CurlString &operator=(CurlString &&src) noexcept { + using std::swap; + swap(p, src.p); + return *this; + } + + operator bool() const noexcept { + return p != nullptr; + } + + operator const char *() const noexcept { + return p; + } + + const char *c_str() const noexcept { + return p; + } +}; + +#endif diff --git a/src/lib/curl/meson.build b/src/lib/curl/meson.build index 3116b6512..2b38957b4 100644 --- a/src/lib/curl/meson.build +++ b/src/lib/curl/meson.build @@ -11,6 +11,7 @@ curl = static_library( 'Init.cxx', 'Global.cxx', 'Request.cxx', + 'Escape.cxx', 'Form.cxx', include_directories: inc, dependencies: [ diff --git a/src/lib/icu/meson.build b/src/lib/icu/meson.build index 3232266ec..5386a62b4 100644 --- a/src/lib/icu/meson.build +++ b/src/lib/icu/meson.build @@ -20,7 +20,7 @@ if icu_dep.found() elif not get_option('iconv').disabled() have_iconv = compiler.has_function('iconv') conf.set('HAVE_ICONV', have_iconv) - if get_option('iconv').enabled() + if not have_iconv and get_option('iconv').enabled() error('iconv() not available') endif endif diff --git a/src/queue/PlaylistEdit.cxx b/src/queue/PlaylistEdit.cxx index d6ab55d15..d53286a25 100644 --- a/src/queue/PlaylistEdit.cxx +++ b/src/queue/PlaylistEdit.cxx @@ -352,7 +352,7 @@ playlist::MoveRange(PlayerControl &pc, return; to = (currentSong + abs(to)) % GetLength(); if (start < (unsigned)to) - to--; + to -= end - start; } queue.MoveRange(start, end, to); diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx index 664abb591..72ad439f4 100644 --- a/src/storage/plugins/CurlStorage.cxx +++ b/src/storage/plugins/CurlStorage.cxx @@ -25,8 +25,10 @@ #include "lib/curl/Init.hxx" #include "lib/curl/Global.hxx" #include "lib/curl/Slist.hxx" +#include "lib/curl/String.hxx" #include "lib/curl/Request.hxx" #include "lib/curl/Handler.hxx" +#include "lib/curl/Escape.hxx" #include "lib/expat/ExpatParser.hxx" #include "fs/Traits.hxx" #include "event/DeferEvent.hxx" @@ -34,7 +36,6 @@ #include "thread/Cond.hxx" #include "time/Parser.hxx" #include "util/ASCII.hxx" -#include "util/IterableSplitString.hxx" #include "util/RuntimeError.hxx" #include "util/StringCompare.hxx" #include "util/StringFormat.hxx" @@ -74,26 +75,15 @@ CurlStorage::MapUTF8(const char *uri_utf8) const noexcept if (StringIsEmpty(uri_utf8)) return base; - CurlEasy easy; - std::string path_esc; - - for (auto elt: IterableSplitString(uri_utf8, '/')) { - char *elt_esc = easy.Escape(elt.data, elt.size); - if (!path_esc.empty()) - path_esc.push_back('/'); - path_esc += elt_esc; - curl_free(elt_esc); - } - + std::string path_esc = CurlEscapeUriPath(uri_utf8); return PathTraitsUTF8::Build(base.c_str(), path_esc.c_str()); } const char * CurlStorage::MapToRelativeUTF8(const char *uri_utf8) const noexcept { - // TODO: escape/unescape? - - return PathTraitsUTF8::Relative(base.c_str(), uri_utf8); + return PathTraitsUTF8::Relative(base.c_str(), + CurlUnescape(uri_utf8).c_str()); } class BlockingHttpRequest : protected CurlResponseHandler { @@ -128,6 +118,10 @@ public: std::rethrow_exception(postponed_error); } + CURL *GetEasy() noexcept { + return request.Get(); + } + protected: void SetDone() { assert(!done); @@ -265,6 +259,8 @@ public: CommonExpatParser(ExpatNamespaceSeparator{'|'}) { request.SetOption(CURLOPT_CUSTOMREQUEST, "PROPFIND"); + request.SetOption(CURLOPT_FOLLOWLOCATION, 1l); + request.SetOption(CURLOPT_MAXREDIRS, 1l); request_headers.Append(StringFormat<40>("depth: %u", depth)); @@ -273,6 +269,7 @@ public: request.SetOption(CURLOPT_POSTFIELDS, "\n" "" + "" "" "" ""); @@ -280,6 +277,7 @@ public: // TODO: send request body } + using BlockingHttpRequest::GetEasy; using BlockingHttpRequest::Wait; protected: @@ -450,9 +448,7 @@ CurlStorage::GetInfo(const char *uri_utf8, gcc_unused bool follow) { // TODO: escape the given URI - std::string uri = base; - uri += uri_utf8; - + const auto uri = MapUTF8(uri_utf8); return HttpGetInfoOperation(*curl, uri.c_str()).Perform(); } @@ -499,7 +495,11 @@ private: if (path == nullptr) return nullptr; - path = StringAfterPrefix(path, base_path.c_str()); + /* kludge: ignoring case in this comparison to avoid + false negatives if the web server uses a different + case in hex digits in escaped characters; TODO: + implement properly */ + path = StringAfterPrefixIgnoreCase(path, base_path.c_str()); if (path == nullptr || *path == 0) return nullptr; @@ -525,10 +525,7 @@ protected: if (escaped_name.IsNull()) return; - // TODO: unescape - const auto name = escaped_name; - - entries.emplace_front(std::string(name.data, name.size)); + entries.emplace_front(CurlUnescape(GetEasy(), escaped_name)); auto &info = entries.front().info; info = StorageFileInfo(r.collection @@ -542,10 +539,7 @@ protected: std::unique_ptr CurlStorage::OpenDirectory(const char *uri_utf8) { - // TODO: escape the given URI - - std::string uri = base; - uri += uri_utf8; + std::string uri = MapUTF8(uri_utf8); /* collection URIs must end with a slash */ if (uri.back() != '/')