PlaylistFile.cxx 10.2 KB
Newer Older
1
/*
2
 * Copyright (C) 2003-2013 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"
Max Kellermann's avatar
Max Kellermann committed
21
#include "PlaylistFile.hxx"
22
#include "PlaylistSave.hxx"
23 24
#include "PlaylistInfo.hxx"
#include "PlaylistVector.hxx"
25 26
#include "DatabasePlugin.hxx"
#include "DatabaseGlue.hxx"
27
#include "DetachedSong.hxx"
Max Kellermann's avatar
Max Kellermann committed
28
#include "Mapper.hxx"
29
#include "fs/TextFile.hxx"
30 31
#include "ConfigGlobal.hxx"
#include "ConfigOption.hxx"
32
#include "ConfigDefaults.hxx"
Max Kellermann's avatar
Max Kellermann committed
33
#include "Idle.hxx"
34
#include "fs/Limits.hxx"
35
#include "fs/AllocatedPath.hxx"
36
#include "fs/Traits.hxx"
37
#include "fs/Charset.hxx"
38
#include "fs/FileSystem.hxx"
39
#include "fs/DirectoryReader.hxx"
Max Kellermann's avatar
Max Kellermann committed
40
#include "util/UriUtil.hxx"
41
#include "util/Error.hxx"
42

43 44
#include <glib.h>

Max Kellermann's avatar
Max Kellermann committed
45 46 47
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
48
#include <errno.h>
49

50 51
static const char PLAYLIST_COMMENT = '#';

52 53 54 55 56 57 58 59 60 61 62 63 64 65
static unsigned playlist_max_length;
bool playlist_saveAbsolutePaths = DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS;

void
spl_global_init(void)
{
	playlist_max_length = config_get_positive(CONF_MAX_PLAYLIST_LENGTH,
						  DEFAULT_PLAYLIST_MAX_LENGTH);

	playlist_saveAbsolutePaths =
		config_get_bool(CONF_SAVE_ABSOLUTE_PATHS,
				DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS);
}

66 67 68 69 70 71 72 73 74 75 76 77 78 79
bool
spl_valid_name(const char *name_utf8)
{
	/*
	 * Not supporting '/' was done out of laziness, and we should
	 * really strive to support it in the future.
	 *
	 * Not supporting '\r' and '\n' is done out of protocol
	 * limitations (and arguably laziness), but bending over head
	 * over heels to modify the protocol (and compatibility with
	 * all clients) to support idiots who put '\r' and '\n' in
	 * filenames isn't going to happen, either.
	 */

80 81 82
	return strchr(name_utf8, '/') == nullptr &&
		strchr(name_utf8, '\n') == nullptr &&
		strchr(name_utf8, '\r') == nullptr;
83 84
}

85
static const AllocatedPath &
86
spl_map(Error &error)
87
{
88
	const AllocatedPath &path_fs = map_spl_path();
89
	if (path_fs.IsNull())
90
		error.Set(playlist_domain, int(PlaylistResult::DISABLED),
91
			  "Stored playlists are disabled");
92
	return path_fs;
93 94 95
}

static bool
96
spl_check_name(const char *name_utf8, Error &error)
97 98
{
	if (!spl_valid_name(name_utf8)) {
99
		error.Set(playlist_domain, int(PlaylistResult::BAD_NAME),
100 101 102 103 104 105 106
				    "Bad playlist name");
		return false;
	}

	return true;
}

107
static AllocatedPath
108
spl_map_to_fs(const char *name_utf8, Error &error)
109
{
110
	if (spl_map(error).IsNull() || !spl_check_name(name_utf8, error))
111
		return AllocatedPath::Null();
112

113
	auto path_fs = map_spl_utf8_to_fs(name_utf8);
114
	if (path_fs.IsNull())
115
		error.Set(playlist_domain, int(PlaylistResult::BAD_NAME),
116
			  "Bad playlist name");
117 118 119 120 121

	return path_fs;
}

/**
122
 * Create an #Error for the current errno.
123 124
 */
static void
125
playlist_errno(Error &error)
126 127 128
{
	switch (errno) {
	case ENOENT:
129
		error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_LIST),
130
			  "No such playlist");
131 132 133
		break;

	default:
134
		error.SetErrno();
135 136 137 138
		break;
	}
}

139
static bool
140
LoadPlaylistFileInfo(PlaylistInfo &info,
141 142
		     const AllocatedPath &parent_path_fs,
		     const AllocatedPath &name_fs)
143
{
144 145
	const char *name_fs_str = name_fs.c_str();
	size_t name_length = strlen(name_fs_str);
146

147
	if (name_length < sizeof(PLAYLIST_FILE_SUFFIX) ||
148
	    memchr(name_fs_str, '\n', name_length) != nullptr)
149
		return false;
150

151
	if (!g_str_has_suffix(name_fs_str, PLAYLIST_FILE_SUFFIX))
152
		return false;
153

154
	const auto path_fs = AllocatedPath::Build(parent_path_fs, name_fs);
155
	struct stat st;
156
	if (!StatFile(path_fs, st) || !S_ISREG(st.st_mode))
157
		return false;
158

159 160 161
	std::string name(name_fs_str,
			 name_length + 1 - sizeof(PLAYLIST_FILE_SUFFIX));
	std::string name_utf8 = PathToUTF8(name.c_str());
162
	if (name_utf8.empty())
163
		return false;
164

165
	info.name = std::move(name_utf8);
166 167
	info.mtime = st.st_mtime;
	return true;
168 169
}

170
PlaylistVector
171
ListPlaylistFiles(Error &error)
172
{
173
	PlaylistVector list;
174

175
	const auto &parent_path_fs = spl_map(error);
176
	if (parent_path_fs.IsNull())
177
		return list;
178

179 180
	DirectoryReader reader(parent_path_fs);
	if (reader.HasFailed()) {
181
		error.SetErrno();
182
		return list;
183
	}
184

185
	PlaylistInfo info;
186
	while (reader.ReadEntry()) {
187
		const auto entry = reader.GetEntry();
188
		if (LoadPlaylistFileInfo(info, parent_path_fs, entry))
189
			list.push_back(std::move(info));
190 191 192 193 194
	}

	return list;
}

195
static bool
196
SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path,
197
		 Error &error)
198
{
199
	assert(utf8path != nullptr);
200

201
	if (spl_map(error).IsNull())
202
		return false;
203

204
	const auto path_fs = spl_map_to_fs(utf8path, error);
205
	if (path_fs.IsNull())
206
		return false;
207

208
	FILE *file = FOpen(path_fs, FOpenMode::WriteText);
209
	if (file == nullptr) {
210
		playlist_errno(error);
211 212
		return false;
	}
213

214 215
	for (const auto &uri_utf8 : contents)
		playlist_print_uri(file, uri_utf8.c_str());
216

217
	fclose(file);
218
	return true;
219 220
}

221
PlaylistFileContents
222
LoadPlaylistFile(const char *utf8path, Error &error)
223
{
224 225
	PlaylistFileContents contents;

226
	if (spl_map(error).IsNull())
227
		return contents;
228

229
	const auto path_fs = spl_map_to_fs(utf8path, error);
230
	if (path_fs.IsNull())
231
		return contents;
232

233 234
	TextFile file(path_fs);
	if (file.HasFailed()) {
235
		playlist_errno(error);
236
		return contents;
237
	}
238

239
	char *s;
240
	while ((s = file.ReadLine()) != nullptr) {
241
		if (*s == 0 || *s == PLAYLIST_COMMENT)
242
			continue;
243

244 245
		std::string uri_utf8;

246
		if (!uri_has_scheme(s)) {
247
			uri_utf8 = map_fs_to_utf8(s);
248
			if (uri_utf8.empty()) {
249
				if (PathTraitsFS::IsAbsolute(s)) {
250
					uri_utf8 = PathToUTF8(s);
251 252 253 254 255 256 257
					if (uri_utf8.empty())
						continue;

					uri_utf8.insert(0, "file://");
				} else
					continue;
			}
258
		} else {
259
			uri_utf8 = PathToUTF8(s);
260
			if (uri_utf8.empty())
261 262
				continue;
		}
263

264
		contents.emplace_back(std::move(uri_utf8));
265
		if (contents.size() >= playlist_max_length)
266
			break;
267 268
	}

269
	return contents;
270 271
}

272 273
bool
spl_move_index(const char *utf8path, unsigned src, unsigned dest,
274
	       Error &error)
275
{
276 277 278
	if (src == dest)
		/* this doesn't check whether the playlist exists, but
		   what the hell.. */
279
		return true;
280

281 282
	auto contents = LoadPlaylistFile(utf8path, error);
	if (contents.empty() && error.IsDefined())
283
		return false;
284

285
	if (src >= contents.size() || dest >= contents.size()) {
286
		error.Set(playlist_domain, int(PlaylistResult::BAD_RANGE),
287
			  "Bad range");
288
		return false;
289 290
	}

291 292 293
	const auto src_i = std::next(contents.begin(), src);
	auto value = std::move(*src_i);
	contents.erase(src_i);
294

295 296
	const auto dest_i = std::next(contents.begin(), dest);
	contents.insert(dest_i, std::move(value));
297

298
	bool result = SavePlaylistFile(contents, utf8path, error);
299 300

	idle_add(IDLE_STORED_PLAYLIST);
301
	return result;
302 303
}

304
bool
305
spl_clear(const char *utf8path, Error &error)
306
{
307
	if (spl_map(error).IsNull())
308
		return false;
309

310
	const auto path_fs = spl_map_to_fs(utf8path, error);
311
	if (path_fs.IsNull())
312
		return false;
313

314
	FILE *file = FOpen(path_fs, FOpenMode::WriteText);
315
	if (file == nullptr) {
316
		playlist_errno(error);
317 318
		return false;
	}
319

320
	fclose(file);
321 322

	idle_add(IDLE_STORED_PLAYLIST);
323
	return true;
324 325
}

326
bool
327
spl_delete(const char *name_utf8, Error &error)
328
{
329
	const auto path_fs = spl_map_to_fs(name_utf8, error);
330
	if (path_fs.IsNull())
331
		return false;
332

333
	if (!RemoveFile(path_fs)) {
334
		playlist_errno(error);
335 336
		return false;
	}
337

338
	idle_add(IDLE_STORED_PLAYLIST);
339
	return true;
340 341
}

342
bool
343
spl_remove_index(const char *utf8path, unsigned pos, Error &error)
344
{
345 346
	auto contents = LoadPlaylistFile(utf8path, error);
	if (contents.empty() && error.IsDefined())
347
		return false;
348

349
	if (pos >= contents.size()) {
350
		error.Set(playlist_domain, int(PlaylistResult::BAD_RANGE),
351
			  "Bad range");
352
		return false;
353 354
	}

355
	contents.erase(std::next(contents.begin(), pos));
356

357
	bool result = SavePlaylistFile(contents, utf8path, error);
358 359

	idle_add(IDLE_STORED_PLAYLIST);
360
	return result;
361 362
}

363
bool
364
spl_append_song(const char *utf8path, const DetachedSong &song, Error &error)
365
{
366
	if (spl_map(error).IsNull())
367
		return false;
368

369
	const auto path_fs = spl_map_to_fs(utf8path, error);
370
	if (path_fs.IsNull())
371
		return false;
372

373
	FILE *file = FOpen(path_fs, FOpenMode::AppendText);
374
	if (file == nullptr) {
375
		playlist_errno(error);
376 377
		return false;
	}
378

379
	struct stat st;
380
	if (fstat(fileno(file), &st) < 0) {
381
		playlist_errno(error);
382
		fclose(file);
383
		return false;
384
	}
385

386
	if (st.st_size / off_t(MPD_PATH_MAX + 1) >= (off_t)playlist_max_length) {
387
		fclose(file);
388
		error.Set(playlist_domain, int(PlaylistResult::TOO_LARGE),
389
			  "Stored playlist is too large");
390
		return false;
391
	}
392

393
	playlist_print_song(file, song);
394

395
	fclose(file);
396 397

	idle_add(IDLE_STORED_PLAYLIST);
398
	return true;
399 400
}

401
bool
402
spl_append_uri(const char *url, const char *utf8file, Error &error)
403
{
404
	if (uri_has_scheme(url)) {
405 406
		return spl_append_song(utf8file, DetachedSong(url),
				       error);
407
	} else {
408
		const Database *db = GetDatabase(error);
409 410 411
		if (db == nullptr)
			return false;

412 413
		Song *tmp = db->GetSong(url, error);
		if (tmp == nullptr)
414 415
			return false;

416 417 418 419
		const DetachedSong song(*tmp);
		db->ReturnSong(tmp);

		return spl_append_song(utf8file, song, error);
420
	}
421 422
}

423
static bool
424
spl_rename_internal(Path from_path_fs, Path to_path_fs,
425
		    Error &error)
426
{
427
	if (!FileExists(from_path_fs)) {
428
		error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_LIST),
429
			  "No such playlist");
430 431
		return false;
	}
432

433
	if (FileExists(to_path_fs)) {
434
		error.Set(playlist_domain, int(PlaylistResult::LIST_EXISTS),
435
			  "Playlist exists already");
436 437
		return false;
	}
438

439
	if (!RenameFile(from_path_fs, to_path_fs)) {
440
		playlist_errno(error);
441 442
		return false;
	}
443

444
	idle_add(IDLE_STORED_PLAYLIST);
445
	return true;
446
}
447

448
bool
449
spl_rename(const char *utf8from, const char *utf8to, Error &error)
450
{
451
	if (spl_map(error).IsNull())
452
		return false;
453

454
	const auto from_path_fs = spl_map_to_fs(utf8from, error);
455
	if (from_path_fs.IsNull())
456
		return false;
457

458
	const auto to_path_fs = spl_map_to_fs(utf8to, error);
459
	if (to_path_fs.IsNull())
460
		return false;
461

462
	return spl_rename_internal(from_path_fs, to_path_fs, error);
463
}