From: Andre Noll Date: Sun, 24 Dec 2023 15:43:04 +0000 (+0100) Subject: Merge topic branch t/crypt-cleanups into master X-Git-Tag: v0.7.3~9 X-Git-Url: http://git.tuebingen.mpg.de/?a=commitdiff_plain;h=a7f9189eff6a035849ee95ea6b3492f96d4e0c16;hp=1065059dac18aa6bf2f86c22b64543b98bc8e968;p=paraslash.git Merge topic branch t/crypt-cleanups into master This bunch of mostry trivial changes can be merged early, before the openssl code is converted to use the EVP API. The topic was cooking in next for six months. * refs/heads/t/crypt-cleanups: openssl: Assign bignums in canonical order. openssl: Unify naming of public key structures. openssl: Rename read_private_rsa_params() -> read_openssh_private_key(). openssl: Rename read_rsa_bignums() -> read_public_key(). openssl: Dedox crypt_init(). server: Improve "loading pubkey" log message. gcrypt: Remove pointless state variable. client: Reduce line length. --- diff --git a/NEWS.md b/NEWS.md index a259ef3e..4b718d8a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,16 @@ NEWS - Version 1.0 of the openssl library has been deprecated. A warning 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. +- The select server command is now quiet by default, The new --verbose + option can be used to show information about the newly loaded mood + or playlist. +- The ls server command gained the --limit option to force a limit + on the number of files listed. +- Cleanup of the openssl-specific code. Downloads: [tarball](./releases/paraslash-git.tar.xz) diff --git a/afs.c b/afs.c index 78e42eae..445d5871 100644 --- a/afs.c +++ b/afs.c @@ -437,18 +437,18 @@ 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); - msg = make_message("%s: parse error", arg); + msg = make_message("%s: parse error\n", arg); } if (pb) para_printf(pb, "%s", msg); @@ -580,17 +580,6 @@ static void get_database_dir(void) PARA_INFO_LOG("afs_database dir %s\n", database_dir); } -static int make_database_dir(void) -{ - int ret; - - get_database_dir(); - ret = para_mkdir(database_dir, 0777); - if (ret >= 0 || ret == -ERRNO_TO_PARA_ERROR(EEXIST)) - return 1; - return ret; -} - static int open_afs_tables(void) { int i, ret; @@ -962,7 +951,8 @@ __noreturn void afs_init(int socket_fd) } ret = schedule(&s); sched_shutdown(&s); - mood_unload(); + mood_unload(NULL); + playlist_unload(NULL); out_close: close_afs_tables(); out: @@ -980,29 +970,32 @@ static int com_select_callback(struct afs_callback_arg *aca) const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT); const char *arg; int ret; + struct para_buffer *pbout; ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr); assert(ret >= 0); arg = lls_input(0, aca->lpr); + pbout = SERVER_CMD_OPT_GIVEN(SELECT, VERBOSE, aca->lpr)? + &aca->pbout : NULL; score_clear(); if (current_play_mode == PLAY_MODE_MOOD) - mood_unload(); + mood_unload(NULL); else - playlist_unload(); - ret = activate_mood_or_playlist(arg, &aca->pbout); + playlist_unload(NULL); + ret = activate_mood_or_playlist(arg, pbout); if (ret >= 0) goto free_lpr; /* ignore subsequent errors (but log them) */ if (current_mop && strcmp(current_mop, arg) != 0) { int ret2; afs_error(aca, "switching back to %s\n", current_mop); - ret2 = activate_mood_or_playlist(current_mop, &aca->pbout); + ret2 = activate_mood_or_playlist(current_mop, pbout); if (ret2 >= 0) goto free_lpr; afs_error(aca, "could not reactivate %s: %s\n", current_mop, para_strerror(-ret2)); } - activate_mood_or_playlist(NULL, &aca->pbout); + activate_mood_or_playlist(NULL, pbout); free_lpr: lls_free_parse_result(aca->lpr, cmd); return ret; @@ -1061,7 +1054,8 @@ static int com_init(struct command_context *cc, struct lls_parse_result *lpr) .size = sizeof(table_mask)}; unsigned num_inputs = lls_num_inputs(lpr); - ret = make_database_dir(); + get_database_dir(); + ret = para_mkdir(database_dir); if (ret < 0) return ret; if (num_inputs > 0) { diff --git a/afs.h b/afs.h index 9a1d7d9c..e8b8c865 100644 --- 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 b4d8c2dd..f1aca7fb 100644 --- a/aft.c +++ b/aft.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -1361,28 +1362,67 @@ 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; + int ret; time_t current_time; - const struct lls_opt_result *r_r; + const struct lls_opt_result *r_r, *r_a; + uint32_t limit, k, n; 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) goto out; - if (opts->num_matching_paths == 0) { + n = opts->num_matching_paths; + if (n == 0) { ret = lls_num_inputs(opts->lpr) > 0? -E_NO_MATCH : 0; goto out; } @@ -1390,20 +1430,14 @@ static int com_ls_callback(struct afs_callback_arg *aca) if (ret < 0) goto out; time(¤t_time); - if (lls_opt_given(r_r)) - for (i = opts->num_matching_paths - 1; i >= 0; i--) { - ret = print_list_item(opts->data_ptr[i], opts, - &aca->pbout, current_time); - if (ret < 0) - goto out; - } - else - for (i = 0; i < opts->num_matching_paths; i++) { - ret = print_list_item(opts->data_ptr[i], opts, - &aca->pbout, current_time); - if (ret < 0) - goto out; - } + limit = SERVER_CMD_UINT32_VAL(LS, LIMIT, opts->lpr); + for (k = 0; k < n && (limit == 0 || k < limit); k++) { + uint32_t idx = lls_opt_given(r_r)? n - 1 - k : k; + ret = print_list_item(opts->data_ptr[idx], opts, &aca->pbout, + current_time); + if (ret < 0) + goto out; + } out: lls_free_parse_result(opts->lpr, cmd); free(opts->data); @@ -1906,6 +1940,53 @@ out_free: return send_ret; } +/* + * Call back once for each regular file below a directory. + * + * Traverse the given directory recursively and call the supplied callback for + * each regular file encountered. The first argument to the callback will be + * the path to the regular file and the second argument will be the data + * pointer. All file types except regular files and directories are ignored. In + * particular, symlinks are not followed. Subdirectories are ignored silently + * if the calling process has insufficient access permissions. + */ +static int for_each_file_in_dir(const char *dirname, + int (*func)(const char *, void *), void *data) +{ + int ret; + DIR *dir; + struct dirent *entry; + + dir = opendir(dirname); + if (!dir) + return errno == EACCES? 1 : -ERRNO_TO_PARA_ERROR(errno); + /* scan cwd recursively */ + while ((entry = readdir(dir))) { + char *tmp; + struct stat s; + + if (!strcmp(entry->d_name, ".")) + continue; + if (!strcmp(entry->d_name, "..")) + continue; + tmp = make_message("%s/%s", dirname, entry->d_name); + ret = 0; + if (lstat(tmp, &s) != -1) { + if (S_ISREG(s.st_mode)) + ret = func(tmp, data); + else if (S_ISDIR(s.st_mode)) + ret = for_each_file_in_dir(tmp, func, data); + } + free(tmp); + if (ret < 0) + goto out; + } + ret = 1; +out: + closedir(dir); + return ret; +} + static int com_add(struct command_context *cc, struct lls_parse_result *lpr) { int i, ret; diff --git a/audiod.c b/audiod.c index 0e8e5981..0d8f039c 100644 --- a/audiod.c +++ b/audiod.c @@ -958,7 +958,7 @@ static int init_default_filters(void) */ if (strcmp(name, "udp") == 0 || strcmp(name, "dccp") == 0) { tmp = para_strdup("fecdec"); - add_filter(i, tmp); + ret = add_filter(i, tmp); free(tmp); if (ret < 0) goto out; diff --git a/audiod_command.c b/audiod_command.c index 743d73a9..df357c7a 100644 --- a/audiod_command.c +++ b/audiod_command.c @@ -81,13 +81,6 @@ static int num_clients; /** The list of all status items used by para_{server,audiod,gui}. */ const char *status_item_list[] = {STATUS_ITEMS}; -static void dump_stat_client_list(void) -{ - struct stat_client *sc; - - list_for_each_entry(sc, &client_list, node) - PARA_INFO_LOG("stat client on fd %d\n", sc->fd); -} /** * Add a status client to the list. * @@ -121,7 +114,6 @@ static int stat_client_add(int fd, uint64_t mask, int parser_friendly) if (parser_friendly) new_client->flags = SCF_PARSER_FRIENDLY; para_list_add(&new_client->node, &client_list); - dump_stat_client_list(); num_clients++; return 1; } @@ -180,7 +172,6 @@ void stat_client_write_item(int item_num) continue; /* write error or short write */ close_stat_client(sc); - dump_stat_client_list(); } free(pb.buf); free(pfpb.buf); diff --git a/client_common.c b/client_common.c index eeb47c60..fe8234f9 100644 --- a/client_common.c +++ b/client_common.c @@ -581,8 +581,9 @@ int client_parse_config(int argc, char *argv[], struct client_task **ct_ptr, if (CLIENT_OPT_GIVEN(KEY_FILE, lpr)) kf = para_strdup(CLIENT_OPT_STRING_VAL(KEY_FILE, lpr)); else { + struct stat statbuf; kf = make_message("%s/.paraslash/key.%s", home, user); - if (!file_exists(kf)) { + if (stat(kf, &statbuf) != 0) { /* assume file does not exist */ free(kf); kf = make_message("%s/.ssh/id_rsa", home); } diff --git a/fd.c b/fd.c index 763f756c..1af902f9 100644 --- a/fd.c +++ b/fd.c @@ -37,26 +37,27 @@ int xrename(const char *oldpath, const char *newpath) } /** - * Write an array of buffers to a file descriptor. + * Write an array of buffers, handling non-fatal errors. * - * \param fd The file descriptor. + * \param fd The file descriptor to write to. * \param iov Pointer to one or more buffers. * \param iovcnt The number of buffers. * - * EAGAIN/EWOULDBLOCK is not considered a fatal error condition. For example - * DCCP CCID3 has a sending wait queue which fills up and is emptied - * asynchronously. The EAGAIN case means that there is currently no space in - * the wait queue, but this can change at any moment. + * EAGAIN, EWOULDBLOCK and EINTR are not considered error conditions. If a + * write operation fails with EAGAIN or EWOULDBLOCK, the number of bytes that + * have been written so far is returned. In the EINTR case the operation is + * retried. Short writes are handled by issuing a subsequent write operation + * for the remaining part. * * \return Negative on fatal errors, number of bytes written else. * * For blocking file descriptors, this function returns either the sum of all - * buffer sizes, or the error code of the fatal error that caused the last - * write call to fail. + * buffer sizes or a negative error code which indicates the fatal error that + * caused a write call to fail. * - * For nonblocking file descriptors there is a third possibility: Any positive - * return value less than the sum of the buffer sizes indicates that some bytes - * have been written but the next write would block. + * For nonblocking file descriptors there is a third possibility: Any + * non-negative return value less than the sum of the buffer sizes indicates + * that a write operation returned EAGAIN/EWOULDBLOCK. * * \sa writev(2), \ref xwrite(). */ @@ -126,14 +127,15 @@ int xwrite(int fd, const char *buf, size_t len) } /** - * Write all data to a file descriptor. + * Write to a file descriptor, fail on short writes. * * \param fd The file descriptor. - * \param buf The buffer to be sent. - * \param len The length of \a buf. + * \param buf The buffer to be written. + * \param len The length of the buffer. * - * This is like \ref xwrite() but returns \p -E_SHORT_WRITE if not - * all data could be written. + * For blocking file descriptors this function behaves identical to \ref + * xwrite(). For non-blocking file descriptors it returns -E_SHORT_WRITE + * (rather than a value less than len) if not all data could be written. * * \return Number of bytes written on success, negative error code else. */ @@ -149,12 +151,20 @@ int write_all(int fd, const char *buf, size_t len) } /** - * Write a buffer given by a format string. + * A fprintf-like function for raw file descriptors. + * + * This function creates a string buffer according to the given format and + * writes this buffer to a file descriptor. * * \param fd The file descriptor. * \param fmt A format string. * + * The difference to fprintf(3) is that the first argument is a file + * descriptor, not a FILE pointer. This function does not rely on stdio. + * * \return The return value of the underlying call to \ref write_all(). + * + * \sa fprintf(3), \ref xvasprintf(). */ __printf_2_3 int write_va_buffer(int fd, const char *fmt, ...) { @@ -250,64 +260,44 @@ int read_nonblock(int fd, void *buf, size_t sz, size_t *num_bytes) } /** - * Read a buffer and check its content for a pattern. - * - * \param fd The file descriptor to receive from. - * \param pattern The expected pattern. - * \param bufsize The size of the internal buffer. + * Read a buffer and compare its contents to a string, ignoring case. * - * This function tries to read at most \a bufsize bytes from the non-blocking - * file descriptor \a fd. If at least \p strlen(\a pattern) bytes have been - * received, the beginning of the received buffer is compared with \a pattern, - * ignoring case. + * \param fd The file descriptor to read from. + * \param expectation The expected string to compare to. * - * \return Positive if \a pattern was received, negative on errors, zero if no data - * was available to read. + * The given file descriptor is expected to be in non-blocking mode. The string + * comparison is performed using strncasecmp(3). * - * \sa \ref read_nonblock(), \sa strncasecmp(3). + * \return Zero if no data was available, positive if a buffer was read whose + * contents compare as equal to the expected string, negative otherwise. + * Possible errors: (a) not enough data was read, (b) the buffer contents + * compared as non-equal, (c) a read error occurred. In the first two cases, + * -E_READ_PATTERN is returned. In the read error case the (negative) return + * value of the underlying call to \ref read_nonblock() is returned. */ -int read_pattern(int fd, const char *pattern, size_t bufsize) +int read_and_compare(int fd, const char *expectation) { - size_t n, len; - char *buf = alloc(bufsize + 1); - int ret = read_nonblock(fd, buf, bufsize, &n); + size_t n, len = strlen(expectation); + char *buf = alloc(len + 1); + int ret = read_nonblock(fd, buf, len, &n); - buf[n] = '\0'; if (ret < 0) goto out; + buf[n] = '\0'; ret = 0; if (n == 0) goto out; ret = -E_READ_PATTERN; - len = strlen(pattern); if (n < len) goto out; - if (strncasecmp(buf, pattern, len) != 0) + if (strncasecmp(buf, expectation, len) != 0) goto out; ret = 1; out: - if (ret < 0) { - PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); - PARA_NOTICE_LOG("recvd %zu bytes: %s\n", n, buf); - } free(buf); return ret; } -/** - * Check whether a file exists. - * - * \param fn The file name. - * - * \return True iff file exists. - */ -bool file_exists(const char *fn) -{ - struct stat statbuf; - - return !stat(fn, &statbuf); -} - /** * Set a file descriptor to blocking mode. * @@ -397,86 +387,32 @@ int para_open(const char *path, int flags, mode_t mode) } /** - * Wrapper for chdir(2). - * - * \param path The specified directory. + * Create a directory, don't fail if it already exists. * - * \return Standard. - */ -int para_chdir(const char *path) -{ - int ret = chdir(path); - - if (ret >= 0) - return 1; - return -ERRNO_TO_PARA_ERROR(errno); -} - -/** - * Save the cwd and open a given directory. - * - * \param dirname Path to the directory to open. - * \param dir Result pointer. - * \param cwd File descriptor of the current working directory. - * - * \return Standard. - * - * Opening the current directory (".") and calling fchdir() to return is - * usually faster and more reliable than saving cwd in some buffer and calling - * chdir() afterwards. - * - * If \a cwd is not \p NULL "." is opened and the resulting file descriptor is - * stored in \a cwd. If the function returns success, and \a cwd is not \p - * NULL, the caller must close this file descriptor (probably after calling - * fchdir(*cwd)). - * - * On errors, the function undos everything, so the caller needs neither close - * any files, nor change back to the original working directory. + * \param path Name of the directory to create. * - * \sa getcwd(3). + * This function passes the fixed mode value 0777 to mkdir(3) (which consults + * the file creation mask and restricts this value). * + * \return Zero if the path already existed as a directory or as a symbolic + * link which leads to a directory, one if the path did not exist and the + * directory has been created successfully, negative error code else. */ -static int para_opendir(const char *dirname, DIR **dir, int *cwd) +int para_mkdir(const char *path) { - int ret; + /* + * We call opendir(3) rather than relying on stat(2) because this way + * we don't need extra code to get the symlink case right. + */ + DIR *dir = opendir(path); - *dir = NULL; - if (cwd) { - ret = para_open(".", O_RDONLY, 0); - if (ret < 0) - return ret; - *cwd = ret; - } - ret = para_chdir(dirname); - if (ret < 0) - goto close_cwd; - *dir = opendir("."); - if (*dir) - return 1; - ret = -ERRNO_TO_PARA_ERROR(errno); - /* Ignore return value of fchdir() and close(). We're busted anyway. */ - if (cwd) { - int __a_unused ret2 = fchdir(*cwd); /* STFU, gcc */ + if (dir) { + closedir(dir); + return 0; } -close_cwd: - if (cwd) - close(*cwd); - return ret; -} - -/** - * A wrapper for mkdir(2). - * - * \param path Name of the directory to create. - * \param mode The permissions to use. - * - * \return Standard. - */ -int para_mkdir(const char *path, mode_t mode) -{ - if (!mkdir(path, mode)) - return 1; - return -ERRNO_TO_PARA_ERROR(errno); + if (errno != ENOENT) + return -ERRNO_TO_PARA_ERROR(errno); + return mkdir(path, 0777) == 0? 1 : -ERRNO_TO_PARA_ERROR(errno); } /** @@ -549,22 +485,21 @@ out: * \param start The start address of the memory mapping. * \param length The size of the mapping. * - * \return Standard. + * If NULL is passed as the start address, the length value is ignored and the + * function does nothing. + * + * \return Zero if NULL was passed, one if the memory area was successfully + * unmapped, a negative error code otherwise. * * \sa munmap(2), \ref mmap_full_file(). */ int para_munmap(void *start, size_t length) { - int err; - if (!start) return 0; if (munmap(start, length) >= 0) return 1; - err = errno; - PARA_ERROR_LOG("munmap (%p/%zu) failed: %s\n", start, length, - strerror(err)); - return -ERRNO_TO_PARA_ERROR(err); + return -ERRNO_TO_PARA_ERROR(errno); } /** @@ -643,64 +578,3 @@ void valid_fd_012(void) } } } - -/** - * Traverse the given directory recursively. - * - * \param dirname The directory to traverse. - * \param func The function to call for each entry. - * \param private_data Pointer to an arbitrary data structure. - * - * For each regular file under \a dirname, the supplied function \a func is - * called. The full path of the regular file and the \a private_data pointer - * are passed to \a func. Directories for which the calling process has no - * permissions to change to are silently ignored. - * - * \return Standard. - */ -int for_each_file_in_dir(const char *dirname, - int (*func)(const char *, void *), void *private_data) -{ - DIR *dir; - struct dirent *entry; - int cwd_fd, ret = para_opendir(dirname, &dir, &cwd_fd); - - if (ret < 0) - return ret == -ERRNO_TO_PARA_ERROR(EACCES)? 1 : ret; - /* scan cwd recursively */ - while ((entry = readdir(dir))) { - mode_t m; - char *tmp; - struct stat s; - - if (!strcmp(entry->d_name, ".")) - continue; - if (!strcmp(entry->d_name, "..")) - continue; - if (lstat(entry->d_name, &s) == -1) - continue; - m = s.st_mode; - if (!S_ISREG(m) && !S_ISDIR(m)) - continue; - tmp = make_message("%s/%s", dirname, entry->d_name); - if (!S_ISDIR(m)) { - ret = func(tmp, private_data); - free(tmp); - if (ret < 0) - goto out; - continue; - } - /* directory */ - ret = for_each_file_in_dir(tmp, func, private_data); - free(tmp); - if (ret < 0) - goto out; - } - ret = 1; -out: - closedir(dir); - if (fchdir(cwd_fd) < 0 && ret >= 0) - ret = -ERRNO_TO_PARA_ERROR(errno); - close(cwd_fd); - return ret; -} diff --git a/fd.h b/fd.h index 270d0ce2..e4f30903 100644 --- a/fd.h +++ b/fd.h @@ -5,14 +5,12 @@ int xrename(const char *oldpath, const char *newpath); int write_all(int fd, const char *buf, size_t len); __printf_2_3 int write_va_buffer(int fd, const char *fmt, ...); -bool file_exists(const char *); int xpoll(struct pollfd *fds, nfds_t nfds, int timeout); __must_check int mark_fd_nonblocking(int fd); __must_check int mark_fd_blocking(int fd); int para_mmap(size_t length, int prot, int flags, int fd, void *map); int para_open(const char *path, int flags, mode_t mode); -int para_mkdir(const char *path, mode_t mode); -int para_chdir(const char *path); +int para_mkdir(const char *path); int mmap_full_file(const char *filename, int open_mode, void **map, size_t *size, int *fd_ptr); int para_munmap(void *start, size_t length); @@ -21,11 +19,10 @@ int write_ok(int fd); void valid_fd_012(void); int readv_nonblock(int fd, struct iovec *iov, int iovcnt, size_t *num_bytes); int read_nonblock(int fd, void *buf, size_t sz, size_t *num_bytes); -int read_pattern(int fd, const char *pattern, size_t bufsize); +int read_and_compare(int fd, const char *expectation); int xwrite(int fd, const char *buf, size_t len); int xwritev(int fd, struct iovec *iov, int iovcnt); -int for_each_file_in_dir(const char *dirname, - int (*func)(const char *, void *), void *private_data); + /** * Write a \p NULL-terminated buffer. * diff --git a/http_recv.c b/http_recv.c index 32c9e7b9..8d2add19 100644 --- a/http_recv.c +++ b/http_recv.c @@ -105,7 +105,7 @@ static int http_recv_post_monitor(struct sched *s, void *context) return 0; } if (phd->status == HTTP_SENT_GET_REQUEST) { - ret = read_pattern(rn->fd, HTTP_OK_MSG, strlen(HTTP_OK_MSG)); + ret = read_and_compare(rn->fd, HTTP_OK_MSG); if (ret < 0) { PARA_ERROR_LOG("did not receive HTTP OK message\n"); goto out; diff --git a/http_send.c b/http_send.c index 90e3ee57..429b4662 100644 --- a/http_send.c +++ b/http_send.c @@ -170,7 +170,7 @@ static void http_post_monitor(__a_unused struct sched *s) case HTTP_STREAMING: /* nothing to do */ break; case HTTP_CONNECTED: /* need to recv get request */ - ret = read_pattern(sc->fd, HTTP_GET_MSG, MAXLINE); + ret = read_and_compare(sc->fd, HTTP_GET_MSG); if (ret < 0) phsd->status = HTTP_INVALID_GET_REQUEST; else if (ret > 0) { diff --git a/m4/lls/server_cmd.suite.m4 b/m4/lls/server_cmd.suite.m4 index 8200c624..02afaabb 100644 --- a/m4/lls/server_cmd.suite.m4 +++ b/m4/lls/server_cmd.suite.m4 @@ -222,6 +222,16 @@ m4_include(`com_ll.m4') also given), chunk time and chunk offsets. [/help] + [option limit] + short_opt = L + summary = list at most this many files + arg_type = uint32 + arg_info = required_arg + typestr = num + [help] + An argument of zero means "unlimited". This is also the default which + applies if the option is not given. + [/help] [option basename] short_opt = b summary = list and match basenames only @@ -233,9 +243,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 @@ -407,6 +427,9 @@ m4_include(`com_ll.m4') activates the mood named 'foo'. [/description] + [option verbose] + short_opt = v + summary = print information about the loaded mood or playlist [subcommand sender] purpose = control paraslash senders diff --git a/mood.c b/mood.c index e85cf36a..1e15ef0e 100644 --- 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, ¤t_mood->stats); if (ret < 0) return ret; - return add_to_score_table(aft_row, ¤t_mood->stats); + return add_to_score_table(aft_row, current_mood); } /* update score */ ret = get_afsi_of_row(aft_row, &afsi); @@ -529,24 +533,37 @@ 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( "loaded mood %s (%u files)\n" "last_played mean/sigma: %d/%d days\n" "num_played mean/sigma: %" PRId64 "/%" PRIu64 "\n" + "correction factor ratio: %.2lf\n" , m->name? m->name : "(dummy)", n, mean_days, sigma_days, m->stats.num_played_sum / n, - int_sqrt(m->stats.num_played_qd / n) + int_sqrt(m->stats.num_played_qd / n), + 86400.0 * m->stats.last_played_correction / + m->stats.num_played_correction ); } -/** 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 +585,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 +645,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 +660,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 +674,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 +709,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; } diff --git a/play.c b/play.c index 4024c8ea..bd183b6b 100644 --- a/play.c +++ b/play.c @@ -1048,9 +1048,9 @@ static void session_open(void) char *dot_para = make_message("%s/.paraslash", home); free(home); - ret = para_mkdir(dot_para, 0777); + ret = para_mkdir(dot_para); /* warn, but otherwise ignore mkdir error */ - if (ret < 0 && ret != -ERRNO_TO_PARA_ERROR(EEXIST)) + if (ret < 0) PARA_WARNING_LOG("Can not create %s: %s\n", dot_para, para_strerror(-ret)); history_file = make_message("%s/play.history", dot_para); diff --git a/playlist.c b/playlist.c index d02ade3b..c145b0fd 100644 --- a/playlist.c +++ b/playlist.c @@ -14,12 +14,18 @@ /** \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 = ¤t_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 10cd254a..c03e3472 100644 --- 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, }; diff --git a/t/test-lib.sh b/t/test-lib.sh index f98d76c7..1ba70632 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -17,9 +17,12 @@ get_audio_file_paths() say_color() { + local severity=$1 + + shift if [[ "$o_nocolor" != "true" && -n "$1" ]]; then export TERM=$ORIGINAL_TERM - case "$1" in + case "$severity" in error) tput $C_BOLD; tput $C_SETAF 1;; skip) tput $C_SETAF 5;; ok) @@ -32,8 +35,11 @@ say_color() tput $C_SETAF 6;; esac fi - shift - printf "%s\n" "$*" + if [[ "$severity" == 'error' ]]; then + printf "%s\n" "$*" 1>&2 + else + printf "%s\n" "$*" + fi if [[ "$o_nocolor" != "true" && -n "$1" ]]; then tput $C_SGR0 export TERM=dumb