From 2563dc1f46fad1640e2472151a9f1cf918f841b9 Mon Sep 17 00:00:00 2001 Message-Id: <2563dc1f46fad1640e2472151a9f1cf918f841b9.1715852563.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sat, 19 Jul 2008 18:47:08 +0100 Subject: [PATCH] Low level playlist database operations: read, createwrite, list, delete. Totally untested, lots more to do. Organization: Straylight/Edgeware From: Richard Kettlewell --- lib/Makefile.am | 1 + lib/configuration.c | 2 + lib/configuration.h | 3 + lib/trackdb-int.h | 3 + lib/trackdb-playlists.c | 464 ++++++++++++++++++++++++++++++++++++++++ lib/trackdb.c | 14 +- lib/trackdb.h | 19 ++ 7 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 lib/trackdb-playlists.c diff --git a/lib/Makefile.am b/lib/Makefile.am index f2943cc..9df944f 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -80,6 +80,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ table.c table.h \ timeval.h \ $(TRACKDB) trackdb.h trackdb-int.h \ + trackdb-playlists.c \ trackname.c trackorder.c trackname.h \ tracksort.c \ url.h url.c \ diff --git a/lib/configuration.c b/lib/configuration.c index 540084c..2e75ecb 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -952,6 +952,7 @@ static const struct conf conf[] = { { C(noticed_history), &type_integer, validate_positive }, { C(password), &type_string, validate_any }, { C(player), &type_stringlist_accum, validate_player }, + { C(playlist_max) , &type_integer, validate_positive }, { C(plugins), &type_string_accum, validate_isdir }, { C(prefsync), &type_integer, validate_positive }, { C(queue_pad), &type_integer, validate_positive }, @@ -1198,6 +1199,7 @@ static struct config *config_default(void) { c->reminder_interval = 600; /* 10m */ c->new_bias_age = 7 * 86400; /* 1 week */ c->new_bias = 9000000; /* 100 times the base weight */ + c->playlist_max = INT_MAX; /* effectively no limit */ /* Default stopwords */ if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords)) exit(1); diff --git a/lib/configuration.h b/lib/configuration.h index ab8917a..d269beb 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -185,6 +185,9 @@ struct config { */ int api; + /** @brief Maximum size of a playlist */ + long playlist_max; + /* These values had better be non-negative */ #define BACKEND_ALSA 0 /**< Use ALSA (Linux only) */ #define BACKEND_COMMAND 1 /**< Execute a command */ diff --git a/lib/trackdb-int.h b/lib/trackdb-int.h index 32a6ca4..9431406 100644 --- a/lib/trackdb-int.h +++ b/lib/trackdb-int.h @@ -23,6 +23,7 @@ #include +#include "trackdb.h" #include "kvp.h" struct vector; /* forward declaration */ @@ -37,6 +38,7 @@ extern DB *trackdb_noticeddb; extern DB *trackdb_globaldb; extern DB *trackdb_usersdb; extern DB *trackdb_scheduledb; +extern DB *trackdb_playlistsdb; DBC *trackdb_opencursor(DB *db, DB_TXN *tid); /* open a transaction */ @@ -152,6 +154,7 @@ int trackdb_get_global_tid(const char *name, char **parsetags(const char *s); int tag_intersection(char **a, char **b); +int valid_username(const char *user); #endif /* TRACKDB_INT_H */ diff --git a/lib/trackdb-playlists.c b/lib/trackdb-playlists.c new file mode 100644 index 0000000..1492f8c --- /dev/null +++ b/lib/trackdb-playlists.c @@ -0,0 +1,464 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2008 Richard Kettlewell + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +/** @file lib/trackdb-playlists.c + * @brief Track database playlist support + * + * This file implements reading and modification of playlists, including access + * control, but not locking or event logging (at least yet). + */ +#include "common.h" + +#include + +#include "trackdb-int.h" +#include "mem.h" +#include "log.h" +#include "configuration.h" +#include "vector.h" + +static int trackdb_playlist_get_tid(const char *name, + const char *who, + char ***tracksp, + int *ntracksp, + char **sharep, + DB_TXN *tid); +static int trackdb_playlist_set_tid(const char *name, + const char *who, + char **tracks, + int ntracks, + const char *share, + DB_TXN *tid); +static int trackdb_playlist_list_tid(const char *who, + char ***playlistsp, + int *nplaylistsp, + DB_TXN *tid); +static int trackdb_playlist_delete_tid(const char *name, + const char *who, + DB_TXN *tid); + +/** @brief Parse a playlist name + * @param name Playlist name + * @param ownerp Where to put owner, or NULL + * @param sharep Where to put default sharing, or NULL + * @return 0 on success, -1 on error + * + * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part + * is alphanumeric and nonempty. USER is a username (see valid_username()). + */ +int playlist_parse_name(const char *name, + char **ownerp, + char **sharep) { + const char *dot = strchr(name, '.'), *share; + char *owner; + + if(dot) { + /* Owned playlist */ + owner = xstrndup(name, dot - name); + if(!valid_username(owner)) + return -1; + if(!valid_username(dot + 1)) + return -1; + share = "private"; + } else { + /* Shared playlist */ + if(!valid_username(name)) + return -1; + owner = 0; + share = "public"; + } + if(ownerp) + *ownerp = owner; + if(sharep) + *sharep = xstrdup(share); + return 0; +} + +/** @brief Check read access rights + * @param name Playlist name + * @param who Who wants to read + * @param share Playlist share status + */ +static int playlist_may_read(const char *name, + const char *who, + const char *share) { + char *owner; + + if(!playlist_parse_name(name, &owner, 0)) + return 0; + /* Anyone can read shared playlists */ + if(!owner) + return 1; + /* You can always read playlists you own */ + if(!strcmp(owner, who)) + return 1; + /* You can read public playlists */ + if(!strcmp(share, "public")) + return 1; + /* Anything else is prohibited */ + return 0; +} + +/** @brief Check modify access rights + * @param name Playlist name + * @param who Who wants to modify + * @param share Playlist share status + */ +static int playlist_may_write(const char *name, + const char *who, + const char attribute((unused)) *share) { + char *owner; + + if(!playlist_parse_name(name, &owner, 0)) + return 0; + /* Anyone can modify shared playlists */ + if(!owner) + return 1; + /* You can always modify playlists you own */ + if(!strcmp(owner, who)) + return 1; + /* Anything else is prohibited */ + return 0; +} + +/** @brief Get playlist data + * @param name Name of playlist + * @param who Who wants to know + * @param tracksp Where to put list of tracks, or NULL + * @param ntracksp Where to put count of tracks, or NULL + * @param sharep Where to put sharing type, or NULL + * @return 0 on success, non-0 on error + * + * Possible return values: + * - @c 0 on success + * - @c DB_NOTFOUND if the playlist doesn't exist + * - @c EINVAL if the playlist name is invalid + * - @c EACCES if the playlist cannot be read by @p who + */ +int trackdb_playlist_get(const char *name, + const char *who, + char ***tracksp, + int *ntracksp, + char **sharep) { + int e; + + if(!playlist_parse_name(name, 0, 0)) { + error(0, "invalid playlist name '%s'", name); + return EINVAL; + } + WITH_TRANSACTION(trackdb_playlist_get_tid(name, who, + tracksp, ntracksp, sharep, + tid)); + return e; +} + +static int trackdb_playlist_get_tid(const char *name, + const char *who, + char ***tracksp, + int *ntracksp, + char **sharep, + DB_TXN *tid) { + struct kvp *k; + int e, ntracks; + const char *s; + + if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid))) + return e; + /* Get sharability */ + if(!(s = kvp_get(k, "sharing"))) { + error(0, "playlist '%s' has no 'sharing' key", name); + s = "private"; + } + /* Check the read is allowed */ + if(!playlist_may_read(name, who, s)) + return EACCES; + /* Return sharability */ + if(sharep) + *sharep = xstrdup(s); + /* Get track count */ + if(!(s = kvp_get(k, "count"))) { + error(0, "playlist '%s' has no 'count' key", name); + s = "0"; + } + ntracks = atoi(s); + if(ntracks < 0) { + error(0, "playlist '%s' has negative count", name); + ntracks = 0; + } + /* Return track count */ + if(ntracksp) + *ntracksp = ntracks; + if(tracksp) { + /* Get track list */ + char **tracks = xcalloc(ntracks + 1, sizeof (char *)); + char b[16]; + + for(int n = 0; n < ntracks; ++n) { + snprintf(b, sizeof b, "%d", n); + if(!(s = kvp_get(k, b))) { + error(0, "playlist '%s' lacks track %d", name, n); + s = "unknown"; + } + tracks[n] = xstrdup(s); + } + tracks[ntracks] = 0; + /* Return track list */ + *tracksp = tracks; + } + return 0; +} + +/** @brief Modify or create a playlist + * @param name Playlist name + * @param tracks List of tracks to set, or NULL to leave alone + * @param ntracks Length of @p tracks + * @param share Sharing status, or NULL to leave alone + * @return 0 on success, non-0 on error + * + * If the playlist exists it is just modified. + * + * If the playlist does not exist it is created. The default set of tracks is + * none, and the default sharing is private (if it is an owned one) or shared + * (otherwise). + * + * Possible return values: + * - @c 0 on success + * - @c EINVAL if the playlist name is invalid + * - @c EACCES if the playlist cannot be modified by @p who + */ +int trackdb_playlist_set(const char *name, + const char *who, + char **tracks, + int ntracks, + const char *share) { + int e; + char *owner; + + if(!playlist_parse_name(name, &owner, 0)) { + error(0, "invalid playlist name '%s'", name); + return EINVAL; + } + /* Check valid share types */ + if(share) { + if(owner) { + /* Playlists with an owner must be public or private */ + if(strcmp(share, "public") + && strcmp(share, "private")) { + error(0, "playlist '%s' must be public or private", name); + return EINVAL; + } + } else { + /* Playlists with no owner must be shared */ + if(strcmp(share, "shared")) { + error(0, "playlist '%s' must be shared", name); + return EINVAL; + } + } + } + /* We've checked as much as we can for now, now go and attempt the change */ + WITH_TRANSACTION(trackdb_playlist_set_tid(name, who, tracks, ntracks, share, + tid)); + return e; +} + +static int trackdb_playlist_set_tid(const char *name, + const char *who, + char **tracks, + int ntracks, + const char *share, + DB_TXN *tid) { + struct kvp *k; + int e; + const char *s; + + if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)) + && e != DB_NOTFOUND) + return e; + /* If the playlist doesn't exist set some defaults */ + if(e == DB_NOTFOUND) { + char *defshare, *owner; + + if(playlist_parse_name(name, &owner, &defshare)) + return EINVAL; + /* Can't create a non-shared playlist belonging to someone else. In fact + * this should be picked up by playlist_may_write() below but it's clearer + * to do it here. */ + if(owner && strcmp(owner, who)) + return EACCES; + k = 0; + kvp_set(&k, "count", 0); + kvp_set(&k, "sharing", defshare); + } + /* Check that the modification is allowed */ + if(!(s = kvp_get(k, "sharing"))) { + error(0, "playlist '%s' has no 'sharing' key", name); + s = "private"; + } + if(!playlist_may_write(name, who, s)) + return EACCES; + /* Set the new values */ + if(share) + kvp_set(&k, "share", share); + if(tracks) { + char b[16]; + int oldcount, n; + + /* Sanity check track count */ + if(ntracks < 0 || ntracks > config->playlist_max) { + error(0, "invalid track count %d", ntracks); + return EINVAL; + } + /* Set the tracks */ + for(n = 0; n < ntracks; ++n) { + snprintf(b, sizeof b, "%d", n); + kvp_set(&k, b, tracks[n]); + } + /* Get the old track count */ + if((s = kvp_get(k, "count"))) + oldcount = atoi(s); + else + oldcount = 0; + /* Delete old slots */ + for(; n < oldcount; ++n) { + snprintf(b, sizeof b, "%d", n); + kvp_set(&k, b, NULL); + } + /* Set the new count */ + snprintf(b, sizeof b, "%d", ntracks); + kvp_set(&k, "count", b); + } + /* Store the resulting record */ + return trackdb_putdata(trackdb_playlistsdb, name, k, tid, 0); +} + +/** @brief Get a list of playlists + * @param who Who wants to know + * @param playlistsp Where to put list of playlists + * @param nplaylistsp Where to put count of playlists, or NULL + */ +void trackdb_playlist_list(const char *who, + char ***playlistsp, + int *nplaylistsp) { + int e; + + WITH_TRANSACTION(trackdb_playlist_list_tid(who, playlistsp, nplaylistsp, + tid)); +} + +static int trackdb_playlist_list_tid(const char *who, + char ***playlistsp, + int *nplaylistsp, + DB_TXN *tid) { + struct vector v[1]; + DBC *c; + DBT k[1], d[1]; + int e; + + vector_init(v); + c = trackdb_opencursor(trackdb_playlistsdb, tid); + memset(k, 0, sizeof k); + while(!(e = c->c_get(c, k, prepare_data(d), DB_NEXT))) { + char *name = xstrndup(k->data, k->size), *owner; + const char *share = kvp_get(kvp_urldecode(d->data, d->size), + "share"); + + /* Extract owner; malformed names are skipped */ + if(playlist_parse_name(name, &owner, 0)) { + error(0, "invalid playlist name '%s' found in database", name); + continue; + } + /* Always list public and shared playlists + * Only list private ones to their owner + * Don't list anything else + */ + if(!strcmp(share, "public") + || !strcmp(share, "shared") + || (!strcmp(share, "private") + && owner && !strcmp(owner, who))) + vector_append(v, name); + } + switch(e) { + case DB_NOTFOUND: + break; + case DB_LOCK_DEADLOCK: + return e; + default: + fatal(0, "c->c_get: %s", db_strerror(e)); + } + vector_terminate(v); + if(playlistsp) + *playlistsp = v->vec; + if(nplaylistsp) + *nplaylistsp = v->nvec; + return 0; +} + +/** @brief Delete a playlist + * @param name Playlist name + * @param who Who is deleting it + * @return 0 on success, non-0 on error + * + * Possible return values: + * - @c 0 on success + * - @c EINVAL if the playlist name is invalid + * - @c EACCES if the playlist cannot be modified by @p who + * - @c DB_NOTFOUND if the playlist doesn't exist + */ +int trackdb_playlist_delete(const char *name, + const char *who) { + int e; + char *owner; + + if(!playlist_parse_name(name, &owner, 0)) { + error(0, "invalid playlist name '%s'", name); + return EINVAL; + } + /* We've checked as much as we can for now, now go and attempt the change */ + WITH_TRANSACTION(trackdb_playlist_delete_tid(name, who, tid)); + return e; +} + +static int trackdb_playlist_delete_tid(const char *name, + const char *who, + DB_TXN *tid) { + struct kvp *k; + int e; + const char *s; + + if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid))) + return e; + /* Check that modification is allowed */ + if(!(s = kvp_get(k, "sharing"))) { + error(0, "playlist '%s' has no 'sharing' key", name); + s = "private"; + } + if(!playlist_may_write(name, who, s)) + return EACCES; + /* Delete the playlist */ + return trackdb_delkey(trackdb_playlistsdb, name, tid); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/trackdb.c b/lib/trackdb.c index 718970a..a364446 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -159,6 +159,13 @@ DB *trackdb_scheduledb; */ DB *trackdb_usersdb; +/** @brief The playlists database + * - Keys are playlist names + * - Values are encoded key-value pairs + * - Data is user data and cannot be reconstructed + */ +DB *trackdb_playlistsdb; + static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ static pid_t rescan_pid = -1; /* rescanner PID */ static int initialized, opened; /* state */ @@ -468,6 +475,7 @@ void trackdb_open(int flags) { trackdb_noticeddb = open_db("noticed.db", DB_DUPSORT, DB_BTREE, dbflags, 0666); trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666); + trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666); if(!trackdb_existing_database) { /* Stash the database version */ char buf[32]; @@ -502,6 +510,8 @@ void trackdb_close(void) { fatal(0, "error closing schedule.db: %s", db_strerror(err)); if((err = trackdb_usersdb->close(trackdb_usersdb, 0))) fatal(0, "error closing users.db: %s", db_strerror(err)); + if((err = trackdb_playlistsdb->close(trackdb_playlistsdb, 0))) + fatal(0, "error closing playlists.db: %s", db_strerror(err)); trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0; trackdb_tagsdb = trackdb_globaldb = 0; D(("closed databases")); @@ -2551,8 +2561,10 @@ static int trusted(const char *user) { * Currently we only allow the letters and digits in ASCII. We could be more * liberal than this but it is a nice simple test. It is critical that * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! */ -static int valid_username(const char *user) { +int valid_username(const char *user) { if(!*user) return 0; while(*user) { diff --git a/lib/trackdb.h b/lib/trackdb.h index e3d61b5..97c761e 100644 --- a/lib/trackdb.h +++ b/lib/trackdb.h @@ -186,6 +186,25 @@ void trackdb_add_rescanned(void (*rescanned)(void *ru), void *ru); int trackdb_rescan_underway(void); +int playlist_parse_name(const char *name, + char **ownerp, + char **sharep); +int trackdb_playlist_get(const char *name, + const char *who, + char ***tracksp, + int *ntracksp, + char **sharep); +int trackdb_playlist_set(const char *name, + const char *who, + char **tracks, + int ntracks, + const char *share); +void trackdb_playlist_list(const char *who, + char ***playlistsp, + int *nplaylistsp); +int trackdb_playlist_delete(const char *name, + const char *who); + #endif /* TRACKDB_H */ /* -- [mdw]