SongFilter.cxx 6.87 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 The Music Player Daemon Project
3
 * http://www.musicpd.org
4 5 6 7 8 9 10 11 12 13
 *
 * 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.
14 15 16 17
 *
 * 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.
18 19
 */

20
#include "config.h"
21
#include "SongFilter.hxx"
Max Kellermann's avatar
Max Kellermann committed
22
#include "db/LightSong.hxx"
23
#include "DetachedSong.hxx"
24
#include "tag/ParseName.hxx"
25
#include "util/ChronoUtil.hxx"
26
#include "util/ConstBuffer.hxx"
27
#include "util/StringAPI.hxx"
28 29
#include "util/StringCompare.hxx"
#include "util/StringView.hxx"
30
#include "util/ASCII.hxx"
31
#include "util/TimeParser.hxx"
32
#include "util/UriUtil.hxx"
33
#include "lib/icu/CaseFold.hxx"
34

35
#include <exception>
36

37
#include <assert.h>
38 39
#include <stdlib.h>

40 41
#define LOCATE_TAG_FILE_KEY     "file"
#define LOCATE_TAG_FILE_KEY_OLD "filename"
42 43
#define LOCATE_TAG_ANY_KEY      "any"

44
unsigned
45
locate_parse_type(const char *str) noexcept
46
{
47 48
	if (StringEqualsCaseASCII(str, LOCATE_TAG_FILE_KEY) ||
	    StringEqualsCaseASCII(str, LOCATE_TAG_FILE_KEY_OLD))
49 50
		return LOCATE_TAG_FILE_TYPE;

51
	if (StringEqualsCaseASCII(str, LOCATE_TAG_ANY_KEY))
52 53
		return LOCATE_TAG_ANY_TYPE;

54 55 56
	if (strcmp(str, "base") == 0)
		return LOCATE_TAG_BASE_TYPE;

57 58 59
	if (strcmp(str, "modified-since") == 0)
		return LOCATE_TAG_MODIFIED_SINCE;

60
	return tag_name_parse_i(str);
61 62
}

63
SongFilter::Item::Item(unsigned _tag, const char *_value, bool _fold_case)
64
	:tag(_tag),
65
	 value(_value),
66
	 fold_case(_fold_case ? IcuCompare(value.c_str()) : IcuCompare())
67
{
68 69
}

70 71
SongFilter::Item::Item(unsigned _tag,
		       std::chrono::system_clock::time_point _time)
72
	:tag(_tag), value(nullptr), time(_time)
73 74 75
{
}

76
bool
77
SongFilter::Item::StringMatch(const char *s) const noexcept
78
{
79 80
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
81
	assert(s != nullptr);
82
#endif
Eric Wong's avatar
Eric Wong committed
83

84 85
	assert(tag != LOCATE_TAG_MODIFIED_SINCE);

86
	if (fold_case) {
87
		return fold_case.IsIn(s);
88
	} else {
89
		return StringIsEqual(s, value.c_str());
90
	}
91
}
92

93
bool
94
SongFilter::Item::Match(const TagItem &item) const noexcept
95
{
96 97 98
	return (tag == LOCATE_TAG_ANY_TYPE || (unsigned)item.type == tag) &&
		StringMatch(item.value);
}
99

100
bool
101
SongFilter::Item::Match(const Tag &_tag) const noexcept
102
{
103
	bool visited_types[TAG_NUM_OF_ITEM_TYPES];
104
	std::fill_n(visited_types, size_t(TAG_NUM_OF_ITEM_TYPES), false);
105

106 107
	for (const auto &i : _tag) {
		visited_types[i.type] = true;
108

109
		if (Match(i))
110
			return true;
111 112
	}

113 114 115 116
	if (tag < TAG_NUM_OF_ITEM_TYPES && !visited_types[tag]) {
		/* If the search critieron was not visited during the
		   sweep through the song's tag, it means this field
		   is absent from the tag or empty. Thus, if the
117
		   searched string is also empty
118 119
		   then it's a match as well and we should return
		   true. */
120
		if (value.empty())
121 122 123 124 125
			return true;

		if (tag == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) {
			/* if we're looking for "album artist", but
			   only "artist" exists, use that */
126
			for (const auto &item : _tag)
127 128 129 130 131
				if (item.type == TAG_ARTIST &&
				    StringMatch(item.value))
					return true;
		}
	}
132

133
	return false;
134 135
}

136
bool
137
SongFilter::Item::Match(const DetachedSong &song) const noexcept
138 139 140 141
{
	if (tag == LOCATE_TAG_BASE_TYPE)
		return uri_is_child_or_same(value.c_str(), song.GetURI());

142
	if (tag == LOCATE_TAG_MODIFIED_SINCE)
143
		return song.GetLastModified() >= time;
144

145 146 147 148 149 150 151
	if (tag == LOCATE_TAG_FILE_TYPE)
		return StringMatch(song.GetURI());

	return Match(song.GetTag());
}

bool
152
SongFilter::Item::Match(const LightSong &song) const noexcept
153
{
154 155 156 157 158
	if (tag == LOCATE_TAG_BASE_TYPE) {
		const auto uri = song.GetURI();
		return uri_is_child_or_same(value.c_str(), uri.c_str());
	}

159
	if (tag == LOCATE_TAG_MODIFIED_SINCE)
160
		return song.mtime >= time;
161

162
	if (tag == LOCATE_TAG_FILE_TYPE) {
163
		const auto uri = song.GetURI();
164
		return StringMatch(uri.c_str());
165 166
	}

167
	return Match(*song.tag);
168 169
}

170 171 172 173 174 175 176 177 178 179
SongFilter::SongFilter(unsigned tag, const char *value, bool fold_case)
{
	items.push_back(Item(tag, value, fold_case));
}

SongFilter::~SongFilter()
{
	/* this destructor exists here just so it won't get inlined */
}

180
gcc_pure
181 182
static std::chrono::system_clock::time_point
ParseTimeStamp(const char *s)
183 184 185 186 187 188 189
{
	assert(s != nullptr);

	char *endptr;
	unsigned long long value = strtoull(s, &endptr, 10);
	if (*endptr == 0 && endptr > s)
		/* it's an integral UNIX time stamp */
190
		return std::chrono::system_clock::from_time_t((time_t)value);
191

192 193
	try {
		/* try ISO 8601 */
194
		return ParseTimePoint(s, "%FT%TZ");
195
	} catch (...) {
196
		return std::chrono::system_clock::time_point::min();
197
	}
198 199
}

200 201 202 203 204 205 206
bool
SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
{
	unsigned tag = locate_parse_type(tag_string);
	if (tag == TAG_NUM_OF_ITEM_TYPES)
		return false;

207 208 209 210 211 212 213 214
	if (tag == LOCATE_TAG_BASE_TYPE) {
		if (!uri_safe_local(value))
			return false;

		/* case folding doesn't work with "base" */
		fold_case = false;
	}

215
	if (tag == LOCATE_TAG_MODIFIED_SINCE) {
216 217
		const auto t = ParseTimeStamp(value);
		if (IsNegative(t))
218 219 220 221 222 223
			return false;

		items.push_back(Item(tag, t));
		return true;
	}

224 225 226 227 228
	items.push_back(Item(tag, value, fold_case));
	return true;
}

bool
229
SongFilter::Parse(ConstBuffer<const char *> args, bool fold_case)
230
{
231
	if (args.size == 0 || args.size % 2 != 0)
232 233
		return false;

234 235
	for (unsigned i = 0; i < args.size; i += 2)
		if (!Parse(args[i], args[i + 1], fold_case))
236 237 238
			return false;

	return true;
239 240
}

241
bool
242
SongFilter::Match(const DetachedSong &song) const noexcept
243
{
244 245
	for (const auto &i : items)
		if (!i.Match(song))
246
			return false;
247

248
	return true;
249
}
250

251
bool
252
SongFilter::Match(const LightSong &song) const noexcept
253 254 255 256 257 258 259 260
{
	for (const auto &i : items)
		if (!i.Match(song))
			return false;

	return true;
}

261
bool
262
SongFilter::HasOtherThanBase() const noexcept
263 264 265 266 267 268 269 270
{
	for (const auto &i : items)
		if (i.GetTag() != LOCATE_TAG_BASE_TYPE)
			return true;

	return false;
}

271
const char *
272
SongFilter::GetBase() const noexcept
273 274 275 276 277
{
	for (const auto &i : items)
		if (i.GetTag() == LOCATE_TAG_BASE_TYPE)
			return i.GetValue();

278
	return nullptr;
279
}
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

SongFilter
SongFilter::WithoutBasePrefix(const char *_prefix) const noexcept
{
	const StringView prefix(_prefix);
	SongFilter result;

	for (const auto &i : items) {
		if (i.GetTag() == LOCATE_TAG_BASE_TYPE) {
			const char *s = StringAfterPrefix(i.GetValue(), prefix);
			if (s != nullptr) {
				if (*s == 0)
					continue;

				if (*s == '/') {
					++s;

					if (*s != 0)
						result.items.emplace_back(LOCATE_TAG_BASE_TYPE, s);

					continue;
				}
			}
		}

		result.items.emplace_back(i);
	}

	return result;
}