]> git.tuebingen.mpg.de Git - paraslash.git/commitdiff
Merge topic branch t/afs-ls-a into master
authorAndre Noll <maan@tuebingen.mpg.de>
Sun, 15 Oct 2023 15:54:41 +0000 (17:54 +0200)
committerAndre Noll <maan@tuebingen.mpg.de>
Sun, 15 Oct 2023 15:55:31 +0000 (17:55 +0200)
A new feature for the ls command. Unfortunately, several bugs were
found after the topic graduated to next, so the series contains a few
fixup commits on top of the single patch which implements the feature.

* refs/heads/t/afs-ls-a:
  afs: Really fix memory leak in mood_load().
  afs: Fix memory leak in mood_load().
  playlist: Fix error handling of playlist_load().
  server: Fix NULL pointer dereference in com_ls().
  Implement ls --admissible=m/foo.

NEWS.md
afs.c
afs.h
aft.c
m4/lls/server_cmd.suite.m4
mood.c
playlist.c
score.c

diff --git a/NEWS.md b/NEWS.md
index a0484f09d177cb3f7effdd4962650cdce28281ad..22816b1911a7613a233481df0ff4ac7e07bb6a81 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -12,6 +12,9 @@ NEWS
   is printed at compile-time on systems which have this outdated version
   because it will no longer be supported once paraslash-0.8.0 comes out.
 - A spring cleanup for the senescent code in fd.c.
+- The --admissible option of the ls command now takes an optional
+  argument. When invoked like --admissible=m/foo, only files which are
+  admissible with respect to mood foo are listed.
 
 Downloads:
 [tarball](./releases/paraslash-git.tar.xz)
diff --git a/afs.c b/afs.c
index 865effde671a4848298ab09d502898c6225a937c..3083084c25ac793cfd3a5cad1f93c6e78f0fa413 100644 (file)
--- a/afs.c
+++ b/afs.c
@@ -437,14 +437,14 @@ static int activate_mood_or_playlist(const char *arg, struct para_buffer *pb)
        int ret;
        char *msg;
 
-       if (!arg) {
-               ret = mood_load(NULL, &msg);
+       if (!arg) { /* load dummy mood */
+               ret = mood_load(NULL, NULL, &msg);
                mode = PLAY_MODE_MOOD;
        } else if (!strncmp(arg, "p/", 2)) {
-               ret = playlist_load(arg + 2, &msg);
+               ret = playlist_load(arg + 2, NULL, &msg);
                mode = PLAY_MODE_PLAYLIST;
        } else if (!strncmp(arg, "m/", 2)) {
-               ret = mood_load(arg + 2, &msg);
+               ret = mood_load(arg + 2, NULL, &msg);
                mode = PLAY_MODE_MOOD;
        } else {
                ret = -ERRNO_TO_PARA_ERROR(EINVAL);
@@ -951,8 +951,8 @@ __noreturn void afs_init(int socket_fd)
        }
        ret = schedule(&s);
        sched_shutdown(&s);
-       mood_unload();
-       playlist_unload();
+       mood_unload(NULL);
+       playlist_unload(NULL);
 out_close:
        close_afs_tables();
 out:
@@ -976,9 +976,9 @@ static int com_select_callback(struct afs_callback_arg *aca)
        arg = lls_input(0, aca->lpr);
        score_clear();
        if (current_play_mode == PLAY_MODE_MOOD)
-               mood_unload();
+               mood_unload(NULL);
        else
-               playlist_unload();
+               playlist_unload(NULL);
        ret = activate_mood_or_playlist(arg, &aca->pbout);
        if (ret >= 0)
                goto free_lpr;
diff --git a/afs.h b/afs.h
index 9a1d7d9ca0f130c4590f55c23abc181b632c6483..e8b8c865bda36b9f905b2c1cb435d03bc12d3cbd 100644 (file)
--- a/afs.h
+++ b/afs.h
@@ -238,10 +238,12 @@ int for_each_matching_row(struct pattern_match_data *pmd);
 
 /* score */
 extern const struct afs_table_operations score_ops;
-int score_loop(osl_rbtree_loop_func *func, void *data);
+void score_open(struct osl_table **result);
+void score_close(struct osl_table *t);
+int score_loop(osl_rbtree_loop_func *func, struct osl_table *t, void *data);
 int score_get_best(struct osl_row **aft_row, long *score);
 int get_score_and_aft_row(struct osl_row *score_row, long *score, struct osl_row **aft_row);
-int score_add(const struct osl_row *row, long score);
+int score_add(const struct osl_row *aft_row, long score, struct osl_table *t);
 int score_update(const struct osl_row *aft_row, long new_score);
 int score_delete(const struct osl_row *aft_row);
 void score_clear(void);
@@ -268,13 +270,17 @@ int aft_check_callback(struct afs_callback_arg *aca);
 void free_status_items(void);
 
 /* mood */
-int mood_load(const char *mood_name, char **msg);
-void mood_unload(void);
+struct mood_instance;
+int mood_load(const char *mood_name, struct mood_instance **result, char **msg);
+int mood_loop(struct mood_instance *m, osl_rbtree_loop_func *func, void *data);
+void mood_unload(struct mood_instance *m);
 int mood_check_callback(struct afs_callback_arg *aca);
 
 /* playlist */
-int playlist_load(const char *name, char **msg);
-void playlist_unload(void);
+struct playlist_instance;
+int playlist_load(const char *name, struct playlist_instance **result, char **msg);
+int playlist_loop(struct playlist_instance *pi, osl_rbtree_loop_func *func, void *data);
+void playlist_unload(struct playlist_instance *pi);
 int playlist_check_callback(struct afs_callback_arg *aca);
 
 /** evaluates to 1 if x < y, to -1 if x > y and to 0 if x == y */
diff --git a/aft.c b/aft.c
index 0aa9c6ba7ac4588f1889bf4dda8ec52d3b9cf4d7..4ea8641b2acce666e53d7005bd755ae2099cb07a 100644 (file)
--- a/aft.c
+++ b/aft.c
@@ -1362,23 +1362,60 @@ err:
        return ret;
 }
 
+static int mop_loop(const char *arg, struct afs_callback_arg *aca,
+               struct ls_options *opts)
+{
+       int ret;
+       char *msg;
+
+       if (!arg || strcmp(arg, ".") == 0)
+               return score_loop(prepare_ls_row, NULL, opts);
+       if (!strncmp(arg, "m/", 2)) {
+               struct mood_instance *m;
+               ret = mood_load(arg + 2, &m, &msg);
+               if (ret < 0)
+                       afs_error(aca, "%s", msg);
+               free(msg);
+               if (ret < 0)
+                       return ret;
+               ret = mood_loop(m, prepare_ls_row, opts);
+               mood_unload(m);
+               return ret;
+       }
+       if (!strncmp(arg, "p/", 2)) {
+               struct playlist_instance *pi;
+               ret = playlist_load(arg + 2, &pi, &msg);
+               if (ret < 0)
+                       afs_error(aca, "%s", msg);
+               free(msg);
+               if (ret < 0)
+                       return ret;
+               ret = playlist_loop(pi, prepare_ls_row, opts);
+               playlist_unload(pi);
+               return ret;
+       }
+       afs_error(aca, "bad mood/playlist specifier: %s\n", arg);
+       return -ERRNO_TO_PARA_ERROR(EINVAL);
+}
+
 static int com_ls_callback(struct afs_callback_arg *aca)
 {
        const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS);
        struct ls_options *opts = aca->query.data;
        int i = 0, ret;
        time_t current_time;
-       const struct lls_opt_result *r_r;
+       const struct lls_opt_result *r_r, *r_a;
 
        ret = lls_deserialize_parse_result(
                (char *)aca->query.data + sizeof(*opts), cmd, &opts->lpr);
        assert(ret >= 0);
        r_r = SERVER_CMD_OPT_RESULT(LS, REVERSE, opts->lpr);
-
+       r_a = SERVER_CMD_OPT_RESULT(LS, ADMISSIBLE, opts->lpr);
        aca->pbout.flags = (opts->mode == LS_MODE_PARSER)? PBF_SIZE_PREFIX : 0;
-       if (admissible_only(opts))
-               ret = score_loop(prepare_ls_row, opts);
-       else
+       if (admissible_only(opts)) {
+               const char *arg = lls_string_val(0, r_a);
+               ret = mop_loop(arg, aca, opts);
+       } else
                ret = osl(osl_rbtree_loop(audio_file_table, AFTCOL_PATH, opts,
                        prepare_ls_row));
        if (ret < 0)
index 8200c6249449f81b5743775f04796462fc912b83..1cec68164a13d46fdd96bfcb50c2b0ea160a0585 100644 (file)
@@ -233,9 +233,19 @@ m4_include(`com_ll.m4')
        [option admissible]
                short_opt = a
                summary = list only admissible files
+               arg_type = string
+               arg_info = optional_arg
+               typestr = specifier/name
+               default_val = .
                [help]
-                       List only files which are admissible with respect to the current mood
-                       or playlist.
+                       If the optional argument is supplied, it must be of the form "p/foo"
+                       or "m/bar" (which refer to the playlist named "foo" and the mood named
+                       "bar", respectively). The command then restricts its output to the set
+                       of files which are admissible with respect to the thusly identified
+                       mood or playlist.
+
+                       If no argument is given, or if the argument is the special value ".",
+                       the current mood or playlist is assumed.
                [/help]
        [option reverse]
                short_opt = r
diff --git a/mood.c b/mood.c
index e85cf36a35b9e533370bddd95c5c5db74a214e85..b4d50c88e73533c69e7473c072e3aaef1c196d99 100644 (file)
--- a/mood.c
+++ b/mood.c
@@ -59,6 +59,8 @@ struct mood_instance {
        struct mp_context *parser_context;
        /** To compute the score. */
        struct afs_statistics stats;
+       /** NULL means to operate on the global score table. */
+       struct osl_table *score_table;
 };
 
 /*
@@ -132,6 +134,8 @@ static void destroy_mood(struct mood_instance *m)
        if (!m)
                return;
        mp_shutdown(m->parser_context);
+       if (m->score_table)
+               score_close(m->score_table);
        free(m->name);
        free(m);
 }
@@ -437,7 +441,7 @@ static void update_afs_statistics(struct afs_info *old_afsi,
 }
 
 static int add_to_score_table(const struct osl_row *aft_row,
-               const struct afs_statistics *stats)
+               struct mood_instance *m)
 {
        long score;
        struct afs_info afsi;
@@ -445,8 +449,8 @@ static int add_to_score_table(const struct osl_row *aft_row,
 
        if (ret < 0)
                return ret;
-       score = compute_score(&afsi, stats);
-       return score_add(aft_row, score);
+       score = compute_score(&afsi, &m->stats);
+       return score_add(aft_row, score, m->score_table);
 }
 
 static int delete_from_statistics_and_score_table(const struct osl_row *aft_row)
@@ -504,7 +508,7 @@ static int mood_update_audio_file(const struct osl_row *aft_row,
                ret = add_afs_statistics(aft_row, &current_mood->stats);
                if (ret < 0)
                        return ret;
-               return add_to_score_table(aft_row, &current_mood->stats);
+               return add_to_score_table(aft_row, current_mood);
        }
        /* update score */
        ret = get_afsi_of_row(aft_row, &afsi);
@@ -529,6 +533,8 @@ static char *get_statistics(struct mood_instance *m, int64_t sse)
        unsigned n = m->stats.num;
        int mean_days, sigma_days;
 
+       if (n == 0)
+               return make_message("no admissible files\n");
        mean_days = (sse - m->stats.last_played_sum / n) / 3600 / 24;
        sigma_days = int_sqrt(m->stats.last_played_qd / n) / 3600 / 24;
        return make_message(
@@ -544,9 +550,17 @@ static char *get_statistics(struct mood_instance *m, int64_t sse)
        );
 }
 
-/** Free all resources of the current mood, if any. */
-void mood_unload(void)
+/**
+ * Free all resources of a mood instance.
+ *
+ * \param m As obtained by \ref mood_load(). If NULL, unload the current mood.
+ *
+ * It's OK to call this with m == NULL even if no current mood is loaded.
+ */
+void mood_unload(struct mood_instance *m)
 {
+       if (m)
+               return destroy_mood(m);
        destroy_mood(current_mood);
        current_mood = NULL;
 }
@@ -568,23 +582,42 @@ static void compute_correction_factors(int64_t sse, struct afs_statistics *s)
 }
 
 /**
- * Change the current mood.
+ * Populate a score table with admissible files for the given mood.
+ *
+ * This consults the mood table to initialize the mood parser with the mood
+ * expression stored in the blob object which corresponds to the given name. A
+ * score table is allocated and populated with references to those entries of
+ * the audio file table which evaluate as admissible with respect to the mood
+ * expression. For each audio file a score value is computed and stored along
+ * with the file reference.
  *
  * \param mood_name The name of the mood to load.
+ * \param result Opaque, refers to the mood parser and the score table.
  * \param msg Error message or mood info is returned here.
  *
- * If \a mood_name is \a NULL, load the dummy mood that accepts every audio file
- * and uses a scoring method based only on the \a last_played information.
+ * If the mood name is NULL, the dummy mood is loaded. This mood regards every
+ * audio file as admissible.
+ *
+ * A NULL result pointer instructs the function to operate on the current mood.
+ * That is, on the mood instance which is used by the server to select the next
+ * audio file for streaming. In this mode of operation, the mood which was
+ * active before the call, if any, is unloaded on success.
+ *
+ * If result is not NULL, the current mood is unaffected and *result points to
+ * an initialized mood instance on success. The caller can pass this reference
+ * to \ref mood_loop() to iterate over the admissible files, and should call
+ * \ref mood_unload() to free the mood instance afterwards.
  *
  * If the message pointer is not NULL, a suitable message is returned there in
  * all cases. The caller must free this string.
  *
- * \return The number of admissible files on success, negative on errors. It is
+ * \return The number of admissible files on success, negative on errors. On
+ * errors, the current mood remains unaffected even if result is NULL. It is
  * not considered an error if no files are admissible.
  *
- * \sa struct \ref afs_info::last_played, \ref mp_eval_row().
+ * \sa \ref mp_eval_row().
  */
-int mood_load(const char *mood_name, char **msg)
+int mood_load(const char *mood_name, struct mood_instance **result, char **msg)
 {
        int i, ret;
        struct admissible_array aa = {.size = 0};
@@ -609,14 +642,10 @@ int mood_load(const char *mood_name, char **msg)
        }
        clock_get_realtime(&rnow);
        compute_correction_factors(rnow.tv_sec, &aa.m->stats);
-       if (aa.m->stats.num == 0) {
-               if (msg)
-                       *msg = make_message("no admissible files\n");
-               ret = 0;
-               goto out;
-       }
+       if (result)
+               score_open(&aa.m->score_table);
        for (i = 0; i < aa.m->stats.num; i++) {
-               ret = add_to_score_table(aa.array[i], &aa.m->stats);
+               ret = add_to_score_table(aa.array[i], aa.m);
                if (ret < 0) {
                        if (msg)
                                *msg = make_message(
@@ -628,8 +657,12 @@ int mood_load(const char *mood_name, char **msg)
        if (msg)
                *msg = get_statistics(aa.m, rnow.tv_sec);
        ret = aa.m->stats.num;
-       mood_unload();
-       current_mood = aa.m;
+       if (result)
+               *result = aa.m;
+       else {
+               mood_unload(NULL);
+               current_mood = aa.m;
+       }
        ret = 1;
 out:
        free(aa.array);
@@ -638,12 +671,29 @@ out:
        return ret;
 }
 
+/**
+ * Iterate over the admissible files of a mood instance.
+ *
+ * This wrapper around \ref score_loop() is the mood counterpart of \ref
+ * playlist_loop().
+ *
+ * \param m Determines the score table to iterate. Must not be NULL.
+ * \param func See \ref score_loop().
+ * \param data See \ref score_loop().
+ *
+ * \return See \ref score_loop(), \ref playlist_loop().
+ */
+int mood_loop(struct mood_instance *m, osl_rbtree_loop_func *func, void *data)
+{
+       return score_loop(func, m->score_table, data);
+}
+
 /*
  * Empty the score table and start over.
  *
- * This function is called on events which render the current list of
- * admissible files useless, for example if an attribute is removed from the
- * attribute table.
+ * This function is called on events which render the current set of admissible
+ * files invalid, for example if an attribute is removed from the attribute
+ * table.
  */
 static int reload_current_mood(void)
 {
@@ -656,8 +706,8 @@ static int reload_current_mood(void)
                current_mood->name : "(dummy)");
        if (current_mood->name)
                mood_name = para_strdup(current_mood->name);
-       mood_unload();
-       ret = mood_load(mood_name, NULL);
+       mood_unload(NULL);
+       ret = mood_load(mood_name, NULL, NULL);
        free(mood_name);
        return ret;
 }
index d02ade3ba911a186f0f7e5a57f419ce9074092d0..c145b0fd80ce0520bda58f0949c46211eeee5fdd 100644 (file)
 
 /** \file playlist.c Functions for loading and saving playlists. */
 
-/** Structure used for adding entries to a playlist. */
+/**
+ * The state of a playlist instance.
+ *
+ * A structure of this type is allocated and initialized at playlist load time.
+ */
 struct playlist_instance {
        /** The name of the playlist. */
        char *name;
        /** The number of entries currently in the playlist. */
        unsigned length;
+       /** Contains all valid paths of the playlist. */
+       struct osl_table *score_table;
 };
 static struct playlist_instance current_playlist;
 
@@ -38,7 +44,7 @@ static int playlist_update_audio_file(const struct osl_row *aft_row)
 
 static int add_playlist_entry(char *path, void *data)
 {
-       struct playlist_instance *playlist = data;
+       struct playlist_instance *pi = data;
        struct osl_row *aft_row;
        int ret = aft_get_row_of_path(path, &aft_row);
 
@@ -46,12 +52,12 @@ static int add_playlist_entry(char *path, void *data)
                PARA_NOTICE_LOG("%s: %s\n", path, para_strerror(-ret));
                return 1;
        }
-       ret = score_add(aft_row, -playlist->length);
+       ret = score_add(aft_row, -pi->length, pi->score_table);
        if (ret < 0) {
                PARA_ERROR_LOG("failed to add %s: %s\n", path, para_strerror(-ret));
                return ret;
        }
-       playlist->length++;
+       pi->length++;
        return 1;
 }
 
@@ -103,11 +109,22 @@ int playlist_check_callback(struct afs_callback_arg *aca)
                check_playlist));
 }
 
-/** Free all resources of the current playlist, if any. */
-void playlist_unload(void)
+/**
+ * Free all resources of the given/current playlist.
+ *
+ * \param pi NULL means to unload the current playlist.
+ */
+void playlist_unload(struct playlist_instance *pi)
 {
+       if (pi) {
+               score_close(pi->score_table);
+               free(pi->name);
+               free(pi);
+               return;
+       }
        if (!current_playlist.name)
                return;
+       score_clear();
        free(current_playlist.name);
        current_playlist.name = NULL;
        current_playlist.length = 0;
@@ -122,43 +139,78 @@ void playlist_unload(void)
  * corresponding row of the audio file table is added to the score table.
  *
  * \param name The name of the playlist to load.
+ * \param result Opaque, refers to the underlying score table.
  * \param msg Error message or playlist info is returned here.
  *
  * \return The length of the loaded playlist on success, negative error code
  * else. Files which are listed in the playlist, but are not contained in the
  * database are ignored. This is not considered an error.
  */
-int playlist_load(const char *name, char **msg)
+int playlist_load(const char *name, struct playlist_instance **result, char **msg)
 {
        int ret;
-       struct playlist_instance *playlist = &current_playlist;
+       struct playlist_instance *pi;
        struct osl_object playlist_def;
 
-       ret = pl_get_def_by_name(name, &playlist_def);
-       if (ret < 0) {
-               *msg = make_message("could not read playlist %s\n", name);
-               return ret;
+       if (!name || !*name) {
+               if (msg)
+                       *msg = make_message("empty playlist name\n");
+               return -ERRNO_TO_PARA_ERROR(EINVAL);
        }
-       playlist_unload();
+       ret = pl_get_def_by_name(name, &playlist_def);
+       if (ret < 0)
+               goto err;
+       pi = zalloc(sizeof(*pi));
+       if (result)
+               score_open(&pi->score_table);
        ret = for_each_line(FELF_READ_ONLY, playlist_def.data,
-               playlist_def.size, add_playlist_entry, playlist);
+               playlist_def.size, add_playlist_entry, pi);
        osl_close_disk_object(&playlist_def);
        if (ret < 0)
-               goto err;
+               goto close_score_table;
        ret = -E_PLAYLIST_EMPTY;
-       if (!playlist->length)
-               goto err;
-       playlist->name = para_strdup(name);
-       *msg = make_message("loaded playlist %s (%u files)\n", playlist->name,
-               playlist->length);
+       if (pi->length == 0)
+               goto close_score_table;
        /* success */
-       return current_playlist.length;
+       if (msg)
+               *msg = make_message("loaded playlist %s (%u files)\n", name,
+                       pi->length);
+       pi->name = para_strdup(name);
+       if (result)
+               *result = pi;
+       else {
+               playlist_unload(NULL);
+               current_playlist = *pi;
+       }
+       return pi->length;
+close_score_table:
+       if (result)
+               score_close(pi->score_table);
+       free(pi);
 err:
        PARA_NOTICE_LOG("unable to load playlist %s\n", name);
-       *msg = make_message("unable to load playlist %s\n", name);
+       if (msg)
+               *msg = make_message("unable to load playlist %s\n", name);
        return ret;
 }
 
+/**
+ * Iterate over all admissible audio files of a playlist instance.
+ *
+ * This wrapper around \ref score_loop() is the playlist counterpart of \ref
+ * mood_loop().
+ *
+ * \param pi Determines the score table to iterate. Must not be NULL.
+ * \param func See \ref score_loop().
+ * \param data See \ref score_loop().
+ *
+ * \return See \ref score_loop(), \ref mood_loop().
+ */
+int playlist_loop(struct playlist_instance *pi, osl_rbtree_loop_func *func, void *data)
+{
+       return score_loop(func, pi->score_table, data);
+}
+
 static int search_path(char *path, void *data)
 {
        if (strcmp(path, data))
@@ -196,7 +248,7 @@ static int handle_audio_file_event(enum afs_events event, void *data)
        }
        /* !was_admissible && is_admissible */
        current_playlist.length++;
-       return score_add(row, 0); /* play it immediately */
+       return score_add(row, 0, NULL); /* play it immediately */
 }
 
 /**
diff --git a/score.c b/score.c
index 10cd254a8dba2926614d0a76ffa29cad90e74792..c03e3472da306a1d535b201ea2d23c90ccf8f90a 100644 (file)
--- a/score.c
+++ b/score.c
@@ -77,10 +77,10 @@ static struct osl_table_description score_table_desc = {
 };
 
 /* On errors (negative return value) the content of score is undefined. */
-static int get_score_of_row(void *score_row, long *score)
+static int get_score_of_row(struct osl_table *t, void *score_row, long *score)
 {
        struct osl_object obj;
-       int ret = osl(osl_get_object(score_table, score_row, SCORECOL_SCORE, &obj));
+       int ret = osl(osl_get_object(t, score_row, SCORECOL_SCORE, &obj));
 
        if (ret >= 0)
                *score = *(long *)obj.data;
@@ -88,14 +88,15 @@ static int get_score_of_row(void *score_row, long *score)
 }
 
 /**
- * Add an entry to the table of admissible files.
+ * Add a (row, score) pair to the score table.
  *
- * \param aft_row The audio file to be added.
- * \param score The score for this file.
+ * \param aft_row Identifies the audio file to be added.
+ * \param score The score value of the audio file.
+ * \param t NULL means to operate on the currently active table.
  *
  * \return The return value of the underlying call to osl_add_row().
  */
-int score_add(const struct osl_row *aft_row, long score)
+int score_add(const struct osl_row *aft_row, long score, struct osl_table *t)
 {
        int ret;
        struct osl_object score_objs[NUM_SCORE_COLUMNS];
@@ -112,7 +113,7 @@ int score_add(const struct osl_row *aft_row, long score)
        *(long *)(score_objs[SCORECOL_SCORE].data) = score;
 
 //     PARA_DEBUG_LOG("adding %p\n", *(void **) (score_objs[SCORECOL_AFT_ROW].data));
-       ret = osl(osl_add_row(score_table, score_objs));
+       ret = osl(osl_add_row(t? t : score_table, score_objs));
        if (ret < 0) {
                PARA_ERROR_LOG("%s\n", para_strerror(-ret));
                free(score_objs[SCORECOL_SCORE].data);
@@ -152,7 +153,7 @@ int score_update(const struct osl_row *aft_row, long percent)
        ret = osl(osl_get_nth_row(score_table, SCORECOL_SCORE, new_pos, &rrow));
        if (ret < 0)
                return ret;
-       ret = get_score_of_row(rrow, &new_score);
+       ret = get_score_of_row(score_table, rrow, &new_score);
        if (ret < 0)
                return ret;
        new_score--;
@@ -176,7 +177,7 @@ int get_score_and_aft_row(struct osl_row *score_row, long *score,
                struct osl_row **aft_row)
 {
        struct osl_object obj;
-       int ret = get_score_of_row(score_row, score);
+       int ret = get_score_of_row(score_table, score_row, score);
 
        if (ret < 0)
                return ret;
@@ -187,26 +188,28 @@ int get_score_and_aft_row(struct osl_row *score_row, long *score,
        return 1;
 }
 
-static int get_score_row_from_aft_row(const struct osl_row *aft_row,
-               struct osl_row **score_row)
+static int get_score_row_from_aft_row(struct osl_table *t,
+               const struct osl_row *aft_row, struct osl_row **score_row)
 {
        struct osl_object obj = {.data = (struct osl_row *)aft_row,
                .size = sizeof(aft_row)};
-       return osl(osl_get_row(score_table, SCORECOL_AFT_ROW, &obj, score_row));
+       return osl(osl_get_row(t, SCORECOL_AFT_ROW, &obj, score_row));
 }
 
 /**
  * Call the given function for each row of the score table.
  *
  * \param func Callback, called once per row.
+ * \param t NULL means to use the currently active score table.
  * \param data Passed verbatim to the callback.
  *
  * \return The return value of the underlying call to osl_rbtree_loop(). The
  * loop terminates early if the callback returns negative.
  */
-int score_loop(osl_rbtree_loop_func *func, void *data)
+int score_loop(osl_rbtree_loop_func *func, struct osl_table *t, void *data)
 {
-       return osl(osl_rbtree_loop(score_table, SCORECOL_SCORE, data, func));
+       return osl(osl_rbtree_loop(t? t : score_table, SCORECOL_SCORE, data,
+               func));
 }
 
 /**
@@ -229,7 +232,7 @@ int score_get_best(struct osl_row **aft_row, long *score)
        if (ret < 0)
                return ret;
        *aft_row = obj.data;
-       return get_score_of_row(row, score);
+       return get_score_of_row(score_table, row, score);
 }
 
 /**
@@ -244,7 +247,7 @@ int score_get_best(struct osl_row **aft_row, long *score)
 int score_delete(const struct osl_row *aft_row)
 {
        struct osl_row *score_row;
-       int ret = get_score_row_from_aft_row(aft_row, &score_row);
+       int ret = get_score_row_from_aft_row(score_table, aft_row, &score_row);
 
        if (ret < 0)
                return ret;
@@ -263,7 +266,7 @@ int score_delete(const struct osl_row *aft_row)
 bool row_belongs_to_score_table(const struct osl_row *aft_row)
 {
        struct osl_row *score_row;
-       int ret = get_score_row_from_aft_row(aft_row, &score_row);
+       int ret = get_score_row_from_aft_row(score_table, aft_row, &score_row);
 
        if (ret == -OSL_ERRNO_TO_PARA_ERROR(E_OSL_RB_KEY_NOT_FOUND))
                return false;
@@ -271,29 +274,56 @@ bool row_belongs_to_score_table(const struct osl_row *aft_row)
        return true;
 }
 
-static void score_close(void)
+/**
+ * Free all volatile objects, then close the table.
+ *
+ * \param t As returned from \ref score_open().
+ *
+ * This either succeeds or terminates the calling process.
+ */
+void score_close(struct osl_table *t)
+{
+       assert(osl_close_table(t? t : score_table, OSL_FREE_VOLATILE) >= 0);
+}
+
+static void close_global_table(void)
 {
-       osl_close_table(score_table, OSL_FREE_VOLATILE);
-       score_table = NULL;
+       score_close(NULL);
 }
 
-static int score_open(__a_unused const char *dir)
+static int open_global_table(__a_unused const char *dir)
 {
-       assert(osl_open_table(&score_table_desc, &score_table) >= 0);
+       assert(osl(osl_open_table(&score_table_desc, &score_table)) >= 0);
        return 1;
 }
 
+/**
+ * Allocate a score table instance.
+ *
+ * \param result NULL means to open the currently active score table.
+ *
+ * Since the score table does no filesystem I/O, this function always succeeds.
+ * \sa \ref score_close().
+ */
+void score_open(struct osl_table **result)
+{
+       if (result)
+               assert(osl(osl_open_table(&score_table_desc, result)) >= 0);
+       else
+               open_global_table(NULL);
+}
+
 /**
  * Remove all entries from the score table, but keep the table open.
  */
 void score_clear(void)
 {
-       score_close();
-       score_open(NULL);
+       close_global_table();
+       open_global_table(NULL);
 }
 
 /** The score table stores (aft row, score) pairs in memory. */
 const struct afs_table_operations score_ops = {
-       .open = score_open,
-       .close = score_close,
+       .open = open_global_table,
+       .close = close_global_table,
 };