/* SPDX-License-Identifier: GPL-2.0 */ /** \file aft.c Audio file table functions. */ #include #include #include #include #include #include #include "server_cmd.lsg.h" #include "para.h" #include "error.h" #include "crypt.h" #include "string.h" #include "afh.h" #include "afs.h" #include "fd.h" #include "ipc.h" #include "portable_io.h" #include "sideband.h" #include "command.h" /* Data about one audio file. Needed for ls and stat output. */ struct ls_data { /* Usual audio format handler information. */ struct afh_info afhi; /* Audio file selector information. */ struct afs_info afsi; /* The full path of the audio file. */ char *path; /* The score value (if -a was given). */ long score; /* The hash value of the audio file data. */ unsigned char *hash; }; /* * The internal state of the audio file table is described by the following * variables which are private to aft.c. */ static struct osl_table *audio_file_table; /* NULL if table not open */ static struct osl_row *current_aft_row; /* NULL if no audio file open */ static unsigned char current_hash[HASH_SIZE]; static char *status_items; static char *parser_friendly_status_items; /* * ->afsi, ->score, and ->hash are initialized when a new audio file is * opened. ->afhi and ->path are initialized when the status items are created. */ static struct ls_data status_item_ls_data = {.hash = current_hash}; /** The different sorting methods of the ls command. */ enum ls_sorting_method { LS_SORT_BY_PATH, /**< -s=p (default) */ LS_SORT_BY_SCORE, /**< -s=s */ LS_SORT_BY_LAST_PLAYED, /**< -s=l */ LS_SORT_BY_NUM_PLAYED, /**< -s=n */ LS_SORT_BY_FREQUENCY, /**< -s=f */ LS_SORT_BY_CHANNELS, /**< -s=c */ LS_SORT_BY_IMAGE_ID, /**< -s=i */ LS_SORT_BY_LYRICS_ID, /**< -s=y */ LS_SORT_BY_BITRATE, /**< -s=b */ LS_SORT_BY_DURATION, /**< -s=d */ LS_SORT_BY_AUDIO_FORMAT, /**< -s=a */ LS_SORT_BY_HASH, /**< -s=h */ }; /** The different listing modes of the ls command. */ enum ls_listing_mode { LS_MODE_SHORT, /**< Default listing mode. */ LS_MODE_LONG, /**< -l or -l=l */ LS_MODE_VERBOSE, /** -l=v */ LS_MODE_PARSER, /** -l=p */ }; /** * The size of the individual output fields of the ls command. * * These depend on the content being listed. For example, if each listed file * is shorter than an hour, the duration format is set to mm:ss. Otherwise it * is set to hh:mm:ss. */ struct ls_widths { /** size of the score field. */ unsigned short score_width; /** size of the image id field. */ unsigned short image_id_width; /** size of the lyrics id field. */ unsigned short lyrics_id_width; /** size of the bitrate field. */ unsigned short bitrate_width; /** size of the frequency field. */ unsigned short frequency_width; /** size of the duration field. */ unsigned short duration_width; /** size of the num played field. */ unsigned short num_played_width; /** size of the amp field. */ unsigned short amp_width; /** size of the audio format field. */ unsigned short audio_format_width; }; /* Data passed from the ls command handler to its callback function. */ struct ls_options { struct lls_parse_result *lpr; /* Derived from lpr */ enum ls_sorting_method sorting; /* Derived from lpr */ enum ls_listing_mode mode; /* Used for long listing mode to align the output fields. */ struct ls_widths widths; /* Size of the \a data array. */ uint32_t array_size; /* Number of used entries in the data array. */ uint32_t num_matching_paths; /* Array of matching entries. */ struct ls_data *data; /* Used to sort the array. */ struct ls_data **data_ptr; }; /** * Describes the layout of the mmapped-afs info struct. * * \sa struct \ref afs_info. */ enum afsi_offsets { /** Where .last_played is stored. */ AFSI_LAST_PLAYED_OFFSET = 0, /** Storage position of the attributes bitmap. */ AFSI_ATTRIBUTES_OFFSET = 8, /** Storage position of the .num_played field. */ AFSI_NUM_PLAYED_OFFSET = 16, /** Storage position of the .image_id field. */ AFSI_IMAGE_ID_OFFSET = 20, /** Storage position of the .lyrics_id field. */ AFSI_LYRICS_ID_OFFSET = 24, /** Storage position of the .audio_format_id field. */ AFSI_AUDIO_FORMAT_ID_OFFSET = 28, /** Storage position of the amplification field. */ AFSI_AMP_OFFSET = 29, /** 2 bytes reserved space for future usage. */ AFSI_AUDIO_FORMAT_UNUSED_OFFSET = 30, /** On-disk storage space needed. */ AFSI_SIZE = 32 }; /* * Convert a struct afs_info to an osl object. * * \param afsi Pointer to the audio file info to be converted. * \param obj Result pointer. * * \sa \ref load_afsi(). */ static void save_afsi(struct afs_info *afsi, struct osl_object *obj) { char *buf = obj->data; write_u64(buf + AFSI_LAST_PLAYED_OFFSET, afsi->last_played); write_u64(buf + AFSI_ATTRIBUTES_OFFSET, afsi->attributes); write_u32(buf + AFSI_NUM_PLAYED_OFFSET, afsi->num_played); write_u32(buf + AFSI_IMAGE_ID_OFFSET, afsi->image_id); write_u32(buf + AFSI_LYRICS_ID_OFFSET, afsi->lyrics_id); write_u8(buf + AFSI_AUDIO_FORMAT_ID_OFFSET, afsi->audio_format_id); write_u8(buf + AFSI_AMP_OFFSET, afsi->amp); memset(buf + AFSI_AUDIO_FORMAT_UNUSED_OFFSET, 0, 2); } /* * Get the audio file selector info struct stored in an osl object. * * \param afsi Points to the audio_file info structure to be filled in. * \param obj The osl object holding the data. * * \return Standard. * * \sa \ref save_afsi(). */ static int load_afsi(struct afs_info *afsi, struct osl_object *obj) { char *buf = obj->data; if (obj->size < AFSI_SIZE) return -E_BAD_AFSI; afsi->last_played = read_u64(buf + AFSI_LAST_PLAYED_OFFSET); afsi->attributes = read_u64(buf + AFSI_ATTRIBUTES_OFFSET); afsi->num_played = read_u32(buf + AFSI_NUM_PLAYED_OFFSET); afsi->image_id = read_u32(buf + AFSI_IMAGE_ID_OFFSET); afsi->lyrics_id = read_u32(buf + AFSI_LYRICS_ID_OFFSET); afsi->audio_format_id = read_u8(buf + AFSI_AUDIO_FORMAT_ID_OFFSET); afsi->amp = read_u8(buf + AFSI_AMP_OFFSET); return 1; } /** The columns of the audio file table. */ enum audio_file_table_columns { /** The hash on the content of the audio file. */ AFTCOL_HASH, /** The full path in the filesystem. */ AFTCOL_PATH, /** The audio file selector info. */ AFTCOL_AFSI, /** The audio format handler info. */ AFTCOL_AFHI, /** The chunk table info and the chunk table of the audio file. */ AFTCOL_CHUNKS, /** The number of columns of this table. */ NUM_AFT_COLUMNS }; /* compare function for the hash column */ static int aft_hash_compare(const struct osl_object *obj1, const struct osl_object *obj2) { return hash_compare((unsigned char *)obj1->data, (unsigned char *)obj2->data); } static struct osl_column_description aft_cols[] = { [AFTCOL_HASH] = { .storage_type = OSL_MAPPED_STORAGE, .storage_flags = OSL_RBTREE | OSL_FIXED_SIZE | OSL_UNIQUE, .name = "hash", .compare_function = aft_hash_compare, .data_size = HASH_SIZE }, [AFTCOL_PATH] = { .storage_type = OSL_MAPPED_STORAGE, .storage_flags = OSL_RBTREE | OSL_UNIQUE, .name = "path", .compare_function = string_compare, }, [AFTCOL_AFSI] = { .storage_type = OSL_MAPPED_STORAGE, .storage_flags = OSL_FIXED_SIZE, .name = "afs_info", .data_size = AFSI_SIZE }, [AFTCOL_AFHI] = { .storage_type = OSL_MAPPED_STORAGE, .name = "afh_info", }, [AFTCOL_CHUNKS] = { .storage_type = OSL_DISK_STORAGE, .name = "chunks", } }; static struct osl_table_description audio_file_table_desc = { .name = "audio-files", .num_columns = NUM_AFT_COLUMNS, .flags = OSL_LARGE_TABLE, .column_descriptions = aft_cols }; /* * Produce a canonicalized absolute pathname. * * Returns one if the resolved path a directory, zero if it is a regular file, * negative on errors. */ static int verify_path(const char *orig_path, char **resolved_path) { int ret; char *path = NULL; struct stat statbuf; if (*orig_path != '/') /* we only accept absolute paths */ goto fail; path = realpath(orig_path, NULL); if (!path) goto fail; if (stat(path, &statbuf) < 0) goto fail; if (S_ISREG(statbuf.st_mode)) ret = 0; else if (S_ISDIR(statbuf.st_mode)) ret = 1; else goto fail; *resolved_path = path; return ret; fail: *resolved_path = NULL; free(path); return -E_BAD_PATH; } /** The on-disk layout of a afhi struct. */ enum afhi_offsets { /** Where the number of seconds is stored. */ AFHI_SECONDS_TOTAL_OFFSET = 0, /** Position of the bitrate. */ AFHI_BITRATE_OFFSET = 4, /** Position of the frequency. */ AFHI_FREQUENCY_OFFSET = 8, /** Was: Location of the audio file header. */ AFHI_UNUSED1_OFFSET = 12, /* Length of the audio file header. Zero means: No header. */ AFHI_HEADER_LEN_OFFSET = 16, /** The total number of chunks (4 bytes). */ CHUNKS_TOTAL_OFFSET = 20, /** The length of the audio file header (4 bytes). */ HEADER_LEN_OFFSET = 24, /** Size of the largest chunk in bytes. (4 bytes). */ AFHI_MAX_CHUNK_SIZE_OFFSET = 28, /** The seconds part of the chunk time (4 bytes). */ CHUNK_TV_TV_SEC_OFFSET = 32, /** The microseconds part of the chunk time (4 bytes). */ CHUNK_TV_TV_USEC_OFFSET = 36, /** Number of channels is stored here. (1 byte) */ AFHI_CHANNELS_OFFSET = 40, /** The tag info position. */ AFHI_INFO_STRING_OFFSET = 41, /** Minimal on-disk size of a valid afhi struct. */ MIN_AFHI_SIZE = 47, /* at least 6 null bytes for techinfo/tags */ }; static unsigned sizeof_afhi_buf(const struct afh_info *afhi) { if (!afhi) return 0; return MIN_AFHI_SIZE + strlen(afhi->techinfo) + strlen(afhi->tags.artist) + strlen(afhi->tags.title) + strlen(afhi->tags.year) + strlen(afhi->tags.album) + strlen(afhi->tags.comment); } static void save_afhi(struct afh_info *afhi, char *buf) { char *p; if (!afhi) return; write_u32(buf + AFHI_SECONDS_TOTAL_OFFSET, afhi->seconds_total); write_u32(buf + AFHI_BITRATE_OFFSET, afhi->bitrate); write_u32(buf + AFHI_FREQUENCY_OFFSET, afhi->frequency); write_u32(buf + AFHI_UNUSED1_OFFSET, 0); write_u32(buf + AFHI_HEADER_LEN_OFFSET, afhi->header_len); write_u8(buf + AFHI_CHANNELS_OFFSET, afhi->channels); write_u32(buf + CHUNKS_TOTAL_OFFSET, afhi->chunks_total); write_u32(buf + HEADER_LEN_OFFSET, afhi->header_len); write_u32(buf + AFHI_MAX_CHUNK_SIZE_OFFSET, afhi->max_chunk_size); write_u32(buf + CHUNK_TV_TV_SEC_OFFSET, afhi->chunk_tv.tv_sec); write_u32(buf + CHUNK_TV_TV_USEC_OFFSET, afhi->chunk_tv.tv_usec); p = buf + AFHI_INFO_STRING_OFFSET; /* * The below sprintf(3) calls are OK because our caller already made * sure that buf is large enough. */ p += sprintf(p, "%s", afhi->techinfo) + 1; p += sprintf(p, "%s", afhi->tags.artist) + 1; p += sprintf(p, "%s", afhi->tags.title) + 1; p += sprintf(p, "%s", afhi->tags.year) + 1; p += sprintf(p, "%s", afhi->tags.album) + 1; sprintf(p, "%s", afhi->tags.comment); } /* does not load the chunk table */ static void load_afhi(const char *buf, struct afh_info *afhi) { afhi->seconds_total = read_u32(buf + AFHI_SECONDS_TOTAL_OFFSET); afhi->bitrate = read_u32(buf + AFHI_BITRATE_OFFSET); afhi->frequency = read_u32(buf + AFHI_FREQUENCY_OFFSET); afhi->header_len = read_u32(buf + AFHI_HEADER_LEN_OFFSET); afhi->channels = read_u8(buf + AFHI_CHANNELS_OFFSET); afhi->chunks_total = read_u32(buf + CHUNKS_TOTAL_OFFSET); afhi->header_len = read_u32(buf + HEADER_LEN_OFFSET); afhi->max_chunk_size = read_u32(buf + AFHI_MAX_CHUNK_SIZE_OFFSET); afhi->chunk_tv.tv_sec = read_u32(buf + CHUNK_TV_TV_SEC_OFFSET); afhi->chunk_tv.tv_usec = read_u32(buf + CHUNK_TV_TV_USEC_OFFSET); afhi->techinfo = (char *)buf + AFHI_INFO_STRING_OFFSET; afhi->tags.artist = afhi->techinfo + strlen(afhi->techinfo) + 1; afhi->tags.title = afhi->tags.artist + strlen(afhi->tags.artist) + 1; afhi->tags.year = afhi->tags.title + strlen(afhi->tags.title) + 1; afhi->tags.album = afhi->tags.year + strlen(afhi->tags.year) + 1; afhi->tags.comment = afhi->tags.album + strlen(afhi->tags.album) + 1; } /* Only used for saving the chunk table, but not for loading. */ static unsigned sizeof_chunk_table(struct afh_info *afhi) { if (!afhi || !afhi->chunk_table) return 0; return 4 * (afhi->chunks_total + 1); } static void save_chunk_table(struct afh_info *afhi, char *buf) { uint32_t n; if (!afhi->chunk_table || afhi->chunks_total == 0) return; for (n = 0; n <= afhi->chunks_total; n++) write_u32(buf + 4 * n, afhi->chunk_table[n]); } static void load_chunk_table(struct afh_info *afhi, const struct osl_object *ct) { int i; size_t sz; if (!ct->data || ct->size < 4 * (afhi->chunks_total + 1)) { afhi->chunk_table = NULL; return; } sz = PARA_MIN(((size_t)afhi->chunks_total + 1) * 4, ct->size) + 1; afhi->chunk_table = alloc(sz); for (i = 0; i <= afhi->chunks_total && i * 4 + 3 < ct->size; i++) afhi->chunk_table[i] = read_u32(ct->data + 4 * i); } /** * Get the row of the audio file table corresponding to the given path. * * \param path The full path of the audio file. * \param row Result pointer. * * \return Standard. */ int aft_get_row_of_path(const char *path, struct osl_row **row) { struct osl_object obj = {.data = (char *)path, .size = strlen(path) + 1}; return osl(osl_get_row(audio_file_table, AFTCOL_PATH, &obj, row)); } /* Get the row of the audio file table, given its hash. */ static int aft_get_row_of_hash(unsigned char *hash, struct osl_row **row) { const struct osl_object obj = {.data = hash, .size = HASH_SIZE}; return osl(osl_get_row(audio_file_table, AFTCOL_HASH, &obj, row)); } /* * Get the audio file selector info object of a row. * * \param row Pointer to a row in the audio file table. * \param obj Result pointer. * * \return Standard. */ static int get_afsi_object_of_row(const struct osl_row *row, struct osl_object *obj) { return osl(osl_get_object(audio_file_table, row, AFTCOL_AFSI, obj)); } /* Get the osl object containing the audio file selector info. */ static int get_afsi_object_of_path(const char *path, struct osl_object *obj) { struct osl_row *row; int ret = aft_get_row_of_path(path, &row); if (ret < 0) return ret; return get_afsi_object_of_row(row, obj); } /** * Get the audio file selector info, given a row of the audio file table. * * \param row Pointer to a row in the audio file table. * \param afsi Result pointer. * * \return Positive on success, negative on errors. */ int get_afsi_of_row(const struct osl_row *row, struct afs_info *afsi) { struct osl_object obj; int ret = get_afsi_object_of_row(row, &obj); if (ret < 0) return ret; return load_afsi(afsi, &obj); } /* Get the audio file selector info, given the path of an audio table. */ static int get_afsi_of_path(const char *path, struct afs_info *afsi) { struct osl_object obj; int ret = get_afsi_object_of_path(path, &obj); if (ret < 0) return ret; return load_afsi(afsi, &obj); } /** * Get the path of an audio file, given a row of the audio file table. * * \param row Pointer to a row in the audio file table. * \param path Result pointer. * * The result is a pointer to memory-mapped data. The caller must not attempt * to free it. * * \return Standard. */ int get_audio_file_path_of_row(const struct osl_row *row, char **path) { struct osl_object path_obj; int ret = osl(osl_get_object(audio_file_table, row, AFTCOL_PATH, &path_obj)); if (ret < 0) *path = NULL; else *path = path_obj.data; return ret; } /* * Get the object that contains the hash value of an audio file. * * Returns the sanitized return value of the underlying call to * osl_get_object(). */ static int get_hash_object_of_aft_row(const struct osl_row *row, struct osl_object *obj) { return osl(osl_get_object(audio_file_table, row, AFTCOL_HASH, obj)); } /* * Get the hash value of an audio file, given a row of the audio file table. * The address returned through the hash pointer argument refers to mapped * data and must not be freed by the caller. */ static int get_hash_of_row(const struct osl_row *row, unsigned char **hash) { struct osl_object obj; int ret = get_hash_object_of_aft_row(row, &obj); if (ret < 0) return ret; *hash = obj.data; return 1; } /** * Get the audio format handler info, given a row of the audio file table. * * \param row Pointer to a row of the audio file table. * \param afhi Result pointer. * * \return The return value of the underlying call to osl_get_object(). * * After the call the members of the afhi structure point to mapped memory * which is owned by the osl table, Hence the caller must not attempt to free * this memory by calling \ref clear_afhi(). */ int get_afhi_of_row(const struct osl_row *row, struct afh_info *afhi) { struct osl_object obj; int ret; assert(row); ret = osl(osl_get_object(audio_file_table, row, AFTCOL_AFHI, &obj)); if (ret < 0) return ret; load_afhi(obj.data, afhi); return 1; } /* returns shmid on success */ static int save_afd(struct audio_file_data *afd) { size_t size = sizeof(*afd) + sizeof_chunk_table(&afd->afhi); int shmid, ret = shm_new(size); void *shm_afd; char *buf; if (ret < 0) return ret; shmid = ret; ret = shm_attach(shmid, ATTACH_RW, &shm_afd); if (ret < 0) goto err; buf = shm_afd; buf += sizeof(*afd); save_chunk_table(&afd->afhi, buf); *(struct audio_file_data *)shm_afd = *afd; shm_detach(shm_afd); return shmid; err: shm_destroy(shmid); return ret; } /** * Extract an audio file data structure from a shared memory area. * * This is called by the virtual streaming system each time a new audio file * is about to be streamed. * * \param shmid Identifies a serialized version of the audio file data. * \param afd Result pointer. * * Attach the shared memory area and populate the fields of the audio file * data structure from the contents of the area, allocating the necessary * memory on the heap. Then detach the area. * * \return Standard. */ int load_afd(int shmid, struct audio_file_data *afd) { void *shm_afd; int ret; struct osl_object obj; ret = shm_attach(shmid, ATTACH_RO, &shm_afd); if (ret < 0) return ret; ret = shm_size(shmid, &obj.size); if (ret < 0) goto detach; assert(obj.size >= sizeof(*afd)); *afd = *(struct audio_file_data *)shm_afd; obj.data = shm_afd + sizeof(*afd); obj.size -= sizeof(*afd); load_chunk_table(&afd->afhi, &obj); ret = 1; detach: shm_detach(shm_afd); return ret; } static int get_local_time(uint64_t *seconds, char *buf, size_t size, time_t current_time) { struct tm *tm; /* * Omit year but show time if the given value is closer to the current * time than this many seconds. */ const time_t m = 6 * 30 * 24 * 3600; /* six months */ tm = localtime((time_t *)seconds); if (!tm) return -E_LOCALTIME; if (*seconds > current_time - m && *seconds < current_time + m) { if (!strftime(buf, size, "%b %e %k:%M", tm)) return -E_STRFTIME; return 1; } /* * If the given time is more than six month away from the current time, * we print only the year. The additional space character in the format * string below makes the formatted date align nicely with dates that * contain the time (those written by the above strftime() statement). */ if (!strftime(buf, size, "%b %e %Y", tm)) return -E_STRFTIME; return 1; } /** Compute the number of (decimal) digits of a number. */ #define GET_NUM_DIGITS(x, num) { \ typeof((x)) _tmp = PARA_ABS(x); \ *num = 1; \ if ((_tmp)) \ while ((_tmp) > 9) { \ (_tmp) /= 10; \ (*num)++; \ } \ } __a_const static short unsigned get_duration_width(int seconds) { short unsigned width; unsigned hours = seconds / 3600, mins = (seconds % 3600) / 60; if (!hours) /* less than one hour => m:ss or mm:ss => 4 or 5 digits */ return 4 + (mins > 9); /* more than one hour => h:mm:ss, hh:mm:ss, hhh:mm:ss, ... */ GET_NUM_DIGITS(hours, &width); return width + 6; } static void get_duration_buf(int seconds, char *buf, size_t bufsize, struct ls_options *opts) { unsigned hours = seconds / 3600, mins = (seconds % 3600) / 60; short unsigned max_width; if (!hours) { /* m:ss or mm:ss */ max_width = opts->mode == LS_MODE_LONG? opts->widths.duration_width : 4; assert(max_width < bufsize - 1); sprintf(buf, "%*u:%02d", max_width - 3, mins, seconds % 60); } else { /* more than one hour => h:mm:ss, hh:mm:ss, hhh:mm:ss, ... */ max_width = opts->mode == LS_MODE_LONG? opts->widths.duration_width : 7; assert(max_width < bufsize - 1); sprintf(buf, "%*u:%02u:%02d", max_width - 6, hours, mins, seconds % 60); } } static int write_attribute_items(struct para_buffer *b, const char *att_bitmap, struct afs_info *afsi) { char *att_text; int ret; WRITE_STATUS_ITEM(b, SI_attributes_bitmap, "%s\n", att_bitmap); ret = attr_bitmap_to_text(&afsi->attributes, &att_text); if (ret < 0) return ret; WRITE_STATUS_ITEM(b, SI_attributes_txt, "%s\n", att_text); free(att_text); return ret; } static void write_lyrics_items(struct para_buffer *b, struct afs_info *afsi) { char *lyrics_name; WRITE_STATUS_ITEM(b, SI_lyrics_id, "%u\n", afsi->lyrics_id); lyr_get_name_by_id(afsi->lyrics_id, &lyrics_name); WRITE_STATUS_ITEM(b, SI_lyrics_name, "%s\n", lyrics_name? lyrics_name : "(none)"); } static void write_image_items(struct para_buffer *b, struct afs_info *afsi) { char *image_name; WRITE_STATUS_ITEM(b, SI_image_id, "%u\n", afsi->image_id); img_get_name_by_id(afsi->image_id, &image_name); WRITE_STATUS_ITEM(b, SI_image_name, "%s\n", image_name? image_name : "(none)"); } static void write_filename_items(struct para_buffer *b, const char *path, bool basename) { const char *slash; if (basename) { WRITE_STATUS_ITEM(b, SI_basename, "%s\n", path); return; } WRITE_STATUS_ITEM(b, SI_path, "%s\n", path); slash = strrchr(path, '/'); WRITE_STATUS_ITEM(b, SI_basename, "%s\n", slash? slash + 1 : path); WRITE_STATUS_ITEM(b, SI_directory, "%.*s\n", slash? (int)(slash - path) : (int)strlen(path), path); } /* * Print the binary representation of the given attribute value as a string of * at most 64 characters into the given buffer. If no attributes are defined, * max_attr_bitnum will be negative. Return the empty string in this case. */ static void get_attribute_bitmap(const uint64_t *atts, int max_attr_bitnum, char *buf) { int i; for (i = 0; i <= max_attr_bitnum; i++) buf[max_attr_bitnum - i] = (*atts & (1ULL << i))? 'x' : '-'; buf[i] = '\0'; } static int print_list_item(struct ls_data *d, struct ls_options *opts, struct para_buffer *b, time_t current_time, int max_attr_bitnum) { const struct lls_opt_result *r_a = SERVER_CMD_OPT_RESULT(LS, ADMISSIBLE, opts->lpr); const struct lls_opt_result *r_b = SERVER_CMD_OPT_RESULT(LS, BASENAME, opts->lpr); const struct lls_opt_result *r_d = SERVER_CMD_OPT_RESULT(LS, UNIX_DATE, opts->lpr); int ret; char att_buf[65]; char last_played_time[30]; char duration_buf[30]; /* nobody has an audio file long enough to overflow this */ struct afs_info *afsi = &d->afsi; struct afh_info *afhi = &d->afhi; char asc_hash[2 * HASH_SIZE + 1]; if (opts->mode == LS_MODE_SHORT) { para_printf(b, "%s\n", d->path); return 1; } get_attribute_bitmap(&afsi->attributes, max_attr_bitnum, att_buf); if (lls_opt_given(r_d)) sprintf(last_played_time, "%llu", (long long unsigned)afsi->last_played); else { ret = get_local_time(&afsi->last_played, last_played_time, sizeof(last_played_time), current_time); if (ret < 0) return ret; } get_duration_buf(afhi->seconds_total, duration_buf, sizeof(duration_buf), opts); if (opts->mode == LS_MODE_LONG) { struct ls_widths *w = &opts->widths; if (lls_opt_given(r_a)) para_printf(b, "%*li ", opts->widths.score_width, d->score); para_printf(b, "%s " /* attributes */ "%*u " /* amp */ "%*u " /* image_id */ "%*u " /* lyrics_id */ "%*u " /* bitrate */ "%*s " /* audio format */ "%*u " /* frequency */ "%u " /* channels */ "%s " /* duration */ "%*u " /* num_played */ "%s " /* last_played */ "%s\n", /* path */ att_buf, w->amp_width, afsi->amp, w->image_id_width, afsi->image_id, w->lyrics_id_width, afsi->lyrics_id, w->bitrate_width, afhi->bitrate, w->audio_format_width, audio_format_name(afsi->audio_format_id), w->frequency_width, afhi->frequency, afhi->channels, duration_buf, w->num_played_width, afsi->num_played, last_played_time, d->path ); return 1; } write_filename_items(b, d->path, lls_opt_given(r_b)); if (lls_opt_given(r_a)) WRITE_STATUS_ITEM(b, SI_score, "%li\n", d->score); ret = write_attribute_items(b, att_buf, afsi); if (ret < 0) return ret; write_image_items(b, afsi); write_lyrics_items(b, afsi); hash_to_asc(d->hash, asc_hash); WRITE_STATUS_ITEM(b, SI_hash, "%s\n", asc_hash); WRITE_STATUS_ITEM(b, SI_bitrate, "%dkbit/s\n", afhi->bitrate); WRITE_STATUS_ITEM(b, SI_format, "%s\n", audio_format_name(afsi->audio_format_id)); WRITE_STATUS_ITEM(b, SI_frequency, "%dHz\n", afhi->frequency); WRITE_STATUS_ITEM(b, SI_channels, "%d\n", afhi->channels); WRITE_STATUS_ITEM(b, SI_duration, "%s\n", duration_buf); WRITE_STATUS_ITEM(b, SI_seconds_total, "%" PRIu32 "\n", afhi->seconds_total); WRITE_STATUS_ITEM(b, SI_last_played, "%s\n", last_played_time); WRITE_STATUS_ITEM(b, SI_num_played, "%u\n", afsi->num_played); WRITE_STATUS_ITEM(b, SI_amplification, "%u\n", afsi->amp); WRITE_STATUS_ITEM(b, SI_chunk_time, "%lu\n", tv2ms(&afhi->chunk_tv)); WRITE_STATUS_ITEM(b, SI_num_chunks, "%" PRIu32 "\n", afhi->chunks_total); WRITE_STATUS_ITEM(b, SI_max_chunk_size, "%" PRIu32 "\n", afhi->max_chunk_size); WRITE_STATUS_ITEM(b, SI_techinfo, "%s\n", afhi->techinfo); WRITE_STATUS_ITEM(b, SI_artist, "%s\n", afhi->tags.artist); WRITE_STATUS_ITEM(b, SI_title, "%s\n", afhi->tags.title); WRITE_STATUS_ITEM(b, SI_year, "%s\n", afhi->tags.year); WRITE_STATUS_ITEM(b, SI_album, "%s\n", afhi->tags.album); WRITE_STATUS_ITEM(b, SI_comment, "%s\n", afhi->tags.comment); return 1; } static void make_inode_status_items(struct para_buffer *pb, const char *mtime_str, const struct stat *statbuf) { WRITE_STATUS_ITEM(pb, SI_mtime, "%s\n", mtime_str); WRITE_STATUS_ITEM(pb, SI_file_size, "%ld\n", statbuf->st_size / 1024); } /** * Deallocate and invalidate the status item strings. * * This needs to be a public function so that afs.c can call it on shutdown. */ void free_status_items(void) { freep(&status_items); freep(&parser_friendly_status_items); } static void make_status_items(void) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS); char *argv[] = {"ls", "--admissible", "--listing-mode=verbose"}; struct ls_options opts = {.mode = LS_MODE_VERBOSE}; struct para_buffer pb = {.max_size = shm_get_shmmax() - 1}; time_t current_time; int ret; struct stat statbuf; char mtime_str[30]; struct tm mtime_tm; struct ls_data *d = &status_item_ls_data; int max_attr_bitnum; free_status_items(); if (!current_aft_row) /* no audio file open */ return; /* * d->score, d->hash and d->afsi are already initialized, but d->path * and d->afhi are not. */ ret = get_audio_file_path_of_row(current_aft_row, &d->path); if (ret < 0) goto out; ret = get_afhi_of_row(current_aft_row, &d->afhi); if (ret < 0) goto out; ret = lls_parse(ARRAY_SIZE(argv), argv, cmd, &opts.lpr, NULL); assert(ret >= 0); time(¤t_time); max_attr_bitnum = attr_get_max_bitnum(); ret = print_list_item(d, &opts, &pb, current_time, max_attr_bitnum); if (ret < 0) goto out; if (stat(d->path, &statbuf) < 0) { ret = -ERRNO_TO_PARA_ERROR(errno); goto out; } localtime_r(&statbuf.st_mtime, &mtime_tm); ret = strftime(mtime_str, 29, "%b %d %Y", &mtime_tm); assert(ret > 0); /* number of bytes placed in mtime_str */ make_inode_status_items(&pb, mtime_str, &statbuf); status_items = pb.buf; memset(&pb, 0, sizeof(pb)); pb.max_size = shm_get_shmmax() - 1; pb.flags = PBF_SIZE_PREFIX; ret = print_list_item(d, &opts, &pb, current_time, max_attr_bitnum); if (ret < 0) goto out; make_inode_status_items(&pb, mtime_str, &statbuf); parser_friendly_status_items = pb.buf; ret = 1; out: if (ret < 0) { PARA_WARNING_LOG("could not create status items: %s\n", para_strerror(-ret)); free_status_items(); } lls_free_parse_result(opts.lpr, cmd); } /** * Open the audio file with highest score and set up an afd structure. * * This determines and opens the next audio file, verifies that it did not * change by comparing the recomputed the hash value of the file contents * against the value stored in the audio file table. If all goes well, it * creates a shared memory area containing the serialized version of the afd * structure, including the chunk table, if any. The caller can then send the * ID of this area and the open fd to the server process. * * \param fd Result pointer for the file descriptor of the audio file. * * On success, the numplayed field of the audio file selector info is increased * and the lastplayed time is set to the current time. Finally, the score of * the audio file is updated. * * \return Positive shmid on success, negative on errors. */ int open_and_update_audio_file(int *fd) { unsigned char file_hash[HASH_SIZE]; struct osl_object afsi_obj; struct afs_info old_afsi; int ret; struct osl_object map, chunk_table_obj; struct ls_data *d = &status_item_ls_data; unsigned char *tmp_hash; struct audio_file_data afd; char *path = NULL; again: ret = current_selector()->get_best(¤t_aft_row, &d->score, current_selector_instance); if (ret < 0) return ret; ret = get_hash_of_row(current_aft_row, &tmp_hash); if (ret < 0) goto delete_aft_row_from_score_table; /* tmp_hash points to a memory map which may become stale. */ memcpy(d->hash, tmp_hash, HASH_SIZE); ret = get_audio_file_path_of_row(current_aft_row, &path); if (ret < 0) goto delete_aft_row_from_score_table; PARA_NOTICE_LOG("%s\n", path); ret = get_afsi_object_of_row(current_aft_row, &afsi_obj); if (ret < 0) goto delete_aft_row_from_score_table; ret = load_afsi(&d->afsi, &afsi_obj); if (ret < 0) goto delete_aft_row_from_score_table; ret = get_afhi_of_row(current_aft_row, &afd.afhi); if (ret < 0) goto delete_aft_row_from_score_table; afd.afhi.chunk_table = NULL; ret = osl(osl_open_disk_object(audio_file_table, current_aft_row, AFTCOL_CHUNKS, &chunk_table_obj)); if (ret < 0) { if (!afh_supports_dynamic_chunks(d->afsi.audio_format_id)) goto delete_aft_row_from_score_table; PARA_INFO_LOG("no chunk table for %s\n", path); chunk_table_obj.data = NULL; chunk_table_obj.size = 0; } else { PARA_INFO_LOG("chunk table: %zu bytes\n", chunk_table_obj.size); } ret = mmap_full_file(path, O_RDONLY, &map.data, &map.size, fd); if (ret < 0) goto free_chunk_table; hash_function(map.data, map.size, file_hash); ret = hash_compare(file_hash, d->hash); para_munmap(map.data, map.size); if (ret) { ret = -E_HASH_MISMATCH; goto free_chunk_table; } old_afsi = d->afsi; d->afsi.num_played++; d->afsi.last_played = time(NULL); save_afsi(&d->afsi, &afsi_obj); /* in-place update */ d->afsi.last_played = old_afsi.last_played; afd.audio_format_id = d->afsi.audio_format_id; load_chunk_table(&afd.afhi, &chunk_table_obj); make_status_items(); ret = afs_event(VSS_NEW_AUDIO_FILE, current_aft_row); if (ret < 0) goto free_chunk_table; ret = save_afd(&afd); free_chunk_table: free(afd.afhi.chunk_table); if (chunk_table_obj.data) osl_close_disk_object(&chunk_table_obj); delete_aft_row_from_score_table: if (ret < 0) { if (path) PARA_ERROR_LOG("failed to add %s\n", path); path = NULL; PARA_ERROR_LOG("%s, disabling file\n", para_strerror(-ret)); ret = current_selector()->invalidate(current_aft_row, current_selector_instance); if (ret >= 0) goto again; } return ret; } static int ls_hash_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return memcmp(d1->hash, d2->hash, HASH_SIZE); } static int ls_audio_format_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afsi.audio_format_id, d2->afsi.audio_format_id); } static int ls_duration_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afhi.seconds_total, d2->afhi.seconds_total); } static int ls_bitrate_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afhi.bitrate, d2->afhi.bitrate); } static int ls_lyrics_id_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afsi.lyrics_id, d2->afsi.lyrics_id); } static int ls_image_id_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afsi.image_id, d2->afsi.image_id); } static int ls_channels_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afhi.channels, d2->afhi.channels); } static int ls_frequency_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afhi.frequency, d2->afhi.frequency); } static int ls_num_played_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afsi.num_played, d2->afsi.num_played); } static int ls_last_played_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->afsi.last_played, d2->afsi.last_played); } static int ls_score_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return NUM_COMPARE(d1->score, d2->score); } static int ls_path_compare(const void *a, const void *b) { struct ls_data *d1 = *(struct ls_data **)a, *d2 = *(struct ls_data **)b; return strcmp(d1->path, d2->path); } static inline bool admissible_only(struct ls_options *opts) { return SERVER_CMD_OPT_GIVEN(LS, ADMISSIBLE, opts->lpr) || opts->sorting == LS_SORT_BY_SCORE; } static void sort_matching_paths(struct ls_options *options) { const struct lls_opt_result *r_b = SERVER_CMD_OPT_RESULT(LS, BASENAME, options->lpr); size_t nmemb = options->num_matching_paths; size_t size = sizeof(*options->data_ptr); int (*compar)(const void *, const void *); int i; options->data_ptr = arr_alloc(nmemb, sizeof(*options->data_ptr)); for (i = 0; i < nmemb; i++) options->data_ptr[i] = options->data + i; /* In these cases the array is already sorted */ if (admissible_only(options)) { if (options->sorting == LS_SORT_BY_SCORE) return; } else { if (options->sorting == LS_SORT_BY_PATH && !lls_opt_given(r_b)) return; } switch (options->sorting) { case LS_SORT_BY_PATH: compar = ls_path_compare; break; case LS_SORT_BY_SCORE: compar = ls_score_compare; break; case LS_SORT_BY_LAST_PLAYED: compar = ls_last_played_compare; break; case LS_SORT_BY_NUM_PLAYED: compar = ls_num_played_compare; break; case LS_SORT_BY_FREQUENCY: compar = ls_frequency_compare; break; case LS_SORT_BY_CHANNELS: compar = ls_channels_compare; break; case LS_SORT_BY_IMAGE_ID: compar = ls_image_id_compare; break; case LS_SORT_BY_LYRICS_ID: compar = ls_lyrics_id_compare; break; case LS_SORT_BY_BITRATE: compar = ls_bitrate_compare; break; case LS_SORT_BY_DURATION: compar = ls_duration_compare; break; case LS_SORT_BY_AUDIO_FORMAT: compar = ls_audio_format_compare; break; case LS_SORT_BY_HASH: compar = ls_hash_compare; break; default: assert(0); /* command handler already checked the value. */ } qsort(options->data_ptr, nmemb, size, compar); } /* TODO: Only compute widths if we need them */ /* Returns: 1: match, 0: no match, <0: error */ static int prepare_ls_row(struct osl_row *aft_row, void *ls_opts) { int ret, i; struct ls_options *options = ls_opts; bool basename_given = SERVER_CMD_OPT_GIVEN(LS, BASENAME, options->lpr); struct ls_data *d; struct ls_widths *w; unsigned short num_digits; unsigned num_inputs; char *path; ret = get_audio_file_path_of_row(aft_row, &path); if (ret < 0) return ret; if (basename_given) { char *p = strrchr(path, '/'); if (p) path = p + 1; } num_inputs = lls_num_inputs(options->lpr); if (num_inputs > 0) { for (i = 0; i < num_inputs; i++) { ret = fnmatch(lls_input(i, options->lpr), path, 0); if (!ret) break; if (ret == FNM_NOMATCH) continue; return -E_FNMATCH; } if (i >= num_inputs) /* no match */ return 0; } if (options->num_matching_paths >= options->array_size) { options->array_size++; options->array_size *= 2; options->data = arr_realloc(options->data, options->array_size, sizeof(*options->data)); } d = options->data + options->num_matching_paths++; ret = get_afsi_of_row(aft_row, &d->afsi); if (ret < 0) return ret; ret = get_afhi_of_row(aft_row, &d->afhi); if (ret < 0) return ret; d->path = path; ret = get_hash_of_row(aft_row, &d->hash); if (ret < 0) goto err; w = &options->widths; GET_NUM_DIGITS(d->afsi.image_id, &num_digits); w->image_id_width = PARA_MAX(w->image_id_width, num_digits); GET_NUM_DIGITS(d->afsi.lyrics_id, &num_digits); w->lyrics_id_width = PARA_MAX(w->lyrics_id_width, num_digits); GET_NUM_DIGITS(d->afhi.bitrate, &num_digits); w->bitrate_width = PARA_MAX(w->bitrate_width, num_digits); GET_NUM_DIGITS(d->afhi.frequency, &num_digits); w->frequency_width = PARA_MAX(w->frequency_width, num_digits); GET_NUM_DIGITS(d->afsi.num_played, &num_digits); w->num_played_width = PARA_MAX(w->num_played_width, num_digits); /* get the number of chars to print this amount of time */ num_digits = get_duration_width(d->afhi.seconds_total); w->duration_width = PARA_MAX(w->duration_width, num_digits); GET_NUM_DIGITS(d->afsi.amp, &num_digits); w->amp_width = PARA_MAX(w->amp_width, num_digits); num_digits = strlen(audio_format_name(d->afsi.audio_format_id)); w->audio_format_width = PARA_MAX(w->audio_format_width, num_digits); return 1; err: return ret; } static int prepare_admissible_row(struct osl_row *aft_row, long score, void *data) { struct ls_options *options = data; struct ls_widths *w = &options->widths; uint32_t idx = options->num_matching_paths; unsigned short num_digits; int ret; ret = prepare_ls_row(aft_row, data); if (ret <= 0) return ret; GET_NUM_DIGITS(score, &num_digits); num_digits++; /* add one for the sign (space or "-") */ w->score_width = PARA_MAX(w->score_width, num_digits); options->data[idx].score = score; return 1; } static int mop_loop(const char *arg, struct afs_callback_arg *aca, struct ls_options *opts) { int ret; struct selector_instance *si; const struct selector_operations *ops; if (!arg || strcmp(arg, ".") == 0) { si = current_selector_instance; if (!si) return 0; return current_selector()->loop(prepare_admissible_row, si, opts); } if (!strcmp(arg, "-")) { /* load previous mop */ ops = selector_ops[previous_selector_id]; if (!previous_mop) { afs_error(aca, "no previous mood/playlist\n"); return -ERRNO_TO_PARA_ERROR(EINVAL); } arg = previous_mop; } else if (!strncmp(arg, "m/", 2)) ops = selector_ops[SEL_MOOD]; else if (!strncmp(arg, "p/", 2)) ops = selector_ops[SEL_PLAYLIST]; else { afs_error(aca, "bad mood/playlist specifier: %s\n", arg); return -ERRNO_TO_PARA_ERROR(EINVAL); } ret = ops->load(arg + 2, NULL, aca, &si); if (ret < 0) return ret; ret = ops->loop(prepare_admissible_row, si, opts); ops->unload(si); return ret; } 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 ret; time_t current_time; const struct lls_opt_result *r_r, *r_a; uint32_t limit, k, n; int max_attr_bitnum; 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)) { 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; n = opts->num_matching_paths; if (n == 0) { ret = lls_num_inputs(opts->lpr) > 0? -E_NO_MATCH : 0; goto out; } sort_matching_paths(opts); time(¤t_time); limit = SERVER_CMD_UINT32_VAL(LS, LIMIT, opts->lpr); max_attr_bitnum = attr_get_max_bitnum(); 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, max_attr_bitnum); if (ret < 0) goto out; } out: lls_free_parse_result(opts->lpr, cmd); free(opts->data); free(opts->data_ptr); return ret; } static int com_ls(struct command_context *cc, struct lls_parse_result *lpr) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS); struct ls_options *opts; struct osl_object query; const struct lls_opt_result *r_l = SERVER_CMD_OPT_RESULT(LS, LISTING_MODE, lpr); const struct lls_opt_result *r_s = SERVER_CMD_OPT_RESULT(LS, SORT, lpr); int ret; char *slpr; ret = lls_serialize_parse_result(lpr, cmd, NULL, &query.size); assert(ret >= 0); query.size += sizeof(*opts); query.data = alloc(query.size); opts = query.data; memset(opts, 0, sizeof(*opts)); slpr = query.data + sizeof(*opts); ret = lls_serialize_parse_result(lpr, cmd, &slpr, NULL); assert(ret >= 0); opts->mode = LS_MODE_SHORT; opts->sorting = LS_SORT_BY_PATH; if (lls_opt_given(r_l)) { const char *val = lls_string_val(0, r_l); if (!strcmp(val, "l") || !strcmp(val, "long")) opts->mode = LS_MODE_LONG; else if (!strcmp(val, "s") || !strcmp(val, "short")) opts->mode = LS_MODE_SHORT; else if (!strcmp(val, "v") || !strcmp(val, "verbose")) opts->mode = LS_MODE_VERBOSE; else if (!strcmp(val, "p") || !strcmp(val, "parser-friendly")) opts->mode = LS_MODE_PARSER; else { ret = -ERRNO_TO_PARA_ERROR(EINVAL); goto out; } } if (lls_opt_given(r_s)) { const char *val = lls_string_val(0, r_s); if (!strcmp(val, "p") || !strcmp(val, "path")) opts->sorting = LS_SORT_BY_PATH; else if (!strcmp(val, "s") || !strcmp(val, "score")) opts->sorting = LS_SORT_BY_SCORE; else if (!strcmp(val, "l") || !strcmp(val, "lastplayed")) opts->sorting = LS_SORT_BY_LAST_PLAYED; else if (!strcmp(val, "n") || !strcmp(val, "numplayed")) opts->sorting = LS_SORT_BY_NUM_PLAYED; else if (!strcmp(val, "f") || !strcmp(val, "frquency")) opts->sorting = LS_SORT_BY_FREQUENCY; else if (!strcmp(val, "c") || !strcmp(val, "channels")) opts->sorting = LS_SORT_BY_CHANNELS; else if (!strcmp(val, "i") || !strcmp(val, "image-id")) opts->sorting = LS_SORT_BY_IMAGE_ID; else if (!strcmp(val, "y") || !strcmp(val, "lyrics-id")) opts->sorting = LS_SORT_BY_LYRICS_ID; else if (!strcmp(val, "b") || !strcmp(val, "bitrate")) opts->sorting = LS_SORT_BY_BITRATE; else if (!strcmp(val, "d") || !strcmp(val, "duration")) opts->sorting = LS_SORT_BY_DURATION; else if (!strcmp(val, "a") || !strcmp(val, "audio-format")) opts->sorting = LS_SORT_BY_AUDIO_FORMAT; else if (!strcmp(val, "h") || !strcmp(val, "hash")) opts->sorting = LS_SORT_BY_HASH; else { ret = -ERRNO_TO_PARA_ERROR(EINVAL); goto out; } } ret = send_callback_request(com_ls_callback, cc->afs_fd, &query, afs_cb_result_handler, cc); out: free(query.data); return ret; } EXPORT_SERVER_CMD_HANDLER(ls); /** * Call the given function for each file in the audio file table. * * \param private_data An arbitrary data pointer, passed to \a func. * \param func The custom function to be called. * * \return Standard. */ int audio_file_loop(void *private_data, osl_rbtree_loop_func *func) { return osl(osl_rbtree_loop(audio_file_table, AFTCOL_HASH, private_data, func)); } static int find_hash_sister(unsigned char *hash, struct osl_row **result) { int ret = aft_get_row_of_hash(hash, result); if (ret == osl(-E_OSL_RB_KEY_NOT_FOUND)) return 0; return ret; } static int find_path_brother(const char *path, struct osl_row **result) { int ret = aft_get_row_of_path(path, result); if (ret == osl(-E_OSL_RB_KEY_NOT_FOUND)) return 0; return ret; } /** The format of the data stored by save_audio_file_data(). */ enum com_add_buffer_offsets { /* afhi (if present) starts at this offset. */ CAB_AFHI_OFFSET_POS = 0, /** Start of the chunk table (if present). */ CAB_CHUNKS_OFFSET_POS = 4, /** Start of the (serialized) lopsub parse result. */ CAB_LPR_OFFSET = 8, /** Audio format id. */ CAB_AUDIO_FORMAT_ID_OFFSET = 12, /** The hash of the audio file being added. */ CAB_HASH_OFFSET = 13, /** Start of the path of the audio file. */ CAB_PATH_OFFSET = (CAB_HASH_OFFSET + HASH_SIZE), }; /* * Store the given data to a single buffer. Doesn't need the audio file selector * info struct as the server knows it as well. * * It's OK to call this with afhi == NULL. In this case, the audio format * handler info won't be stored in the buffer. */ static void save_add_callback_buffer(unsigned char *hash, const char *path, struct afh_info *afhi, const char *slpr, size_t slpr_size, uint8_t audio_format_num, struct osl_object *obj) { size_t path_len = strlen(path) + 1; size_t afhi_size = sizeof_afhi_buf(afhi); size_t size = CAB_PATH_OFFSET + path_len + afhi_size + sizeof_chunk_table(afhi) + slpr_size; char *buf = alloc(size); uint32_t pos; assert(size <= ~(uint32_t)0); write_u8(buf + CAB_AUDIO_FORMAT_ID_OFFSET, audio_format_num); memcpy(buf + CAB_HASH_OFFSET, hash, HASH_SIZE); strcpy(buf + CAB_PATH_OFFSET, path); pos = CAB_PATH_OFFSET + path_len; write_u32(buf + CAB_AFHI_OFFSET_POS, pos); save_afhi(afhi, buf + pos); pos += afhi_size; write_u32(buf + CAB_CHUNKS_OFFSET_POS, pos); if (afhi) { save_chunk_table(afhi, buf + pos); pos += sizeof_chunk_table(afhi); } write_u32(buf + CAB_LPR_OFFSET, pos); memcpy(buf + pos, slpr, slpr_size); assert(pos + slpr_size == size); obj->data = buf; obj->size = size; } /* Overview of the add command. Input: What was passed to the callback by the command handler. ~~~~~~ HS: Hash sister. Whether an audio file with identical hash already exists in the osl database. PB: Path brother. Whether a file with the given path exists in the table. F: Force flag given. Whether add was called with -f. output: Action performed by the callback. ~~~~~~~ AFHI: Whether afhi and chunk table are computed and sent. ACTION: Table modifications to be done by the callback. +----+----+---+------+---------------------------------------------------+ | HS | PB | F | AFHI | ACTION +----+----+---+------+---------------------------------------------------+ | Y | Y | Y | Y | if HS != PB: remove PB. HS: force afhi update, | | update path, keep afsi +----+----+---+------+---------------------------------------------------+ | Y | Y | N | N | if HS == PB: do not send callback request at all. | | otherwise: remove PB, HS: update path, keep afhi, | | afsi. +----+----+---+------+---------------------------------------------------+ | Y | N | Y | Y | (rename) force afhi update of HS, update path of | | HS, keep afsi +----+----+---+------+---------------------------------------------------+ | Y | N | N | N | (file rename) update path of HS, keep afsi, afhi +----+----+---+------+---------------------------------------------------+ | N | Y | Y | Y | (file change) update afhi, hash, of PB, keep afsi | | (force has no effect) +----+----+---+------+---------------------------------------------------+ | N | Y | N | Y | (file change) update afhi, hash of PB, keep afsi +----+----+---+------+---------------------------------------------------+ | N | N | Y | Y | (new file) create new entry (force has no effect) +----+----+---+------+---------------------------------------------------+ | N | N | N | Y | (new file) create new entry +----+----+---+------+---------------------------------------------------+ Notes: afhi <=> force or no HS F => AFHI */ static int com_add_callback(struct afs_callback_arg *aca) { char *buf = aca->query.data, *path; struct osl_row *pb, *aft_row; struct osl_row *hs; struct osl_object objs[NUM_AFT_COLUMNS]; unsigned char *hash; char asc[2 * HASH_SIZE + 1]; int ret; char afsi_buf[AFSI_SIZE]; uint32_t slpr_offset = read_u32(buf + CAB_LPR_OFFSET); char *slpr = buf + slpr_offset; struct afs_info default_afsi = {.last_played = 0}; uint16_t afhi_offset, chunks_offset; const struct lls_command *cmd = SERVER_CMD_CMD_PTR(ADD); const struct lls_opt_result *r_f, *r_v; ret = lls_deserialize_parse_result(slpr, cmd, &aca->lpr); assert(ret >= 0); r_f = SERVER_CMD_OPT_RESULT(ADD, FORCE, aca->lpr); r_v = SERVER_CMD_OPT_RESULT(ADD, VERBOSE, aca->lpr); hash = (unsigned char *)buf + CAB_HASH_OFFSET; hash_to_asc(hash, asc); objs[AFTCOL_HASH].data = buf + CAB_HASH_OFFSET; objs[AFTCOL_HASH].size = HASH_SIZE; path = buf + CAB_PATH_OFFSET; objs[AFTCOL_PATH].data = path; objs[AFTCOL_PATH].size = strlen(path) + 1; PARA_INFO_LOG("request to add %s\n", path); ret = find_hash_sister(hash, &hs); if (ret < 0) goto out; ret = find_path_brother(path, &pb); if (ret < 0) goto out; if (hs && pb && hs == pb && !lls_opt_given(r_f)) { if (lls_opt_given(r_v)) para_printf(&aca->pbout, "ignoring duplicate\n"); ret = 1; goto out; } if (hs && hs != pb) { struct osl_object obj; if (pb) { /* hs trumps pb, remove pb */ if (lls_opt_given(r_v)) para_printf(&aca->pbout, "removing %s\n", path); if (current_aft_row == pb) current_aft_row = NULL; ret = afs_event(AUDIO_FILE_REMOVE, pb); if (ret < 0) goto out; ret = osl(osl_del_row(audio_file_table, pb)); if (ret < 0) goto out; pb = NULL; } /* file rename, update hs' path */ if (lls_opt_given(r_v)) { ret = osl(osl_get_object(audio_file_table, hs, AFTCOL_PATH, &obj)); if (ret < 0) goto out; para_printf(&aca->pbout, "renamed from %s\n", (char *)obj.data); } ret = osl(osl_update_object(audio_file_table, hs, AFTCOL_PATH, &objs[AFTCOL_PATH])); if (ret < 0) goto out; if (hs == current_aft_row) make_status_items(); ret = afs_event(AUDIO_FILE_RENAME, hs); if (ret < 0) goto out; if (!lls_opt_given(r_f)) goto out; } /* no hs or force mode, child must have sent afhi */ afhi_offset = read_u32(buf + CAB_AFHI_OFFSET_POS); chunks_offset = read_u32(buf + CAB_CHUNKS_OFFSET_POS); assert(chunks_offset <= slpr_offset); objs[AFTCOL_AFHI].data = buf + afhi_offset; objs[AFTCOL_AFHI].size = chunks_offset - afhi_offset; ret = -E_NO_AFHI; if (!objs[AFTCOL_AFHI].size) /* "impossible" */ goto out; objs[AFTCOL_CHUNKS].data = buf + chunks_offset; objs[AFTCOL_CHUNKS].size = slpr_offset - chunks_offset; if (pb && !hs) { /* update pb's hash */ char old_asc[2 * HASH_SIZE + 1]; unsigned char *old_hash; ret = get_hash_of_row(pb, &old_hash); if (ret < 0) goto out; hash_to_asc(old_hash, old_asc); if (lls_opt_given(r_v)) para_printf(&aca->pbout, "file change: %s -> %s\n", old_asc, asc); ret = osl(osl_update_object(audio_file_table, pb, AFTCOL_HASH, &objs[AFTCOL_HASH])); if (ret < 0) goto out; } if (hs || pb) { /* (hs != NULL and pb != NULL) implies hs == pb */ struct osl_row *row = pb? pb : hs; /* update afhi and chunk_table */ if (lls_opt_given(r_v)) para_printf(&aca->pbout, "updating afhi and chunk table\n"); ret = osl(osl_update_object(audio_file_table, row, AFTCOL_AFHI, &objs[AFTCOL_AFHI])); if (ret < 0) goto out; /* truncate the file to size zero if there is no chunk table */ ret = osl(osl_update_object(audio_file_table, row, AFTCOL_CHUNKS, &objs[AFTCOL_CHUNKS])); if (ret < 0) goto out; if (row == current_aft_row) make_status_items(); ret = afs_event(AFHI_CHANGE, row); goto out; } /* new entry, use default afsi */ if (lls_opt_given(r_v)) para_printf(&aca->pbout, "new file\n"); default_afsi.last_played = time(NULL) - 365 * 24 * 60 * 60; default_afsi.audio_format_id = read_u8(buf + CAB_AUDIO_FORMAT_ID_OFFSET); objs[AFTCOL_AFSI].data = &afsi_buf; objs[AFTCOL_AFSI].size = AFSI_SIZE; save_afsi(&default_afsi, &objs[AFTCOL_AFSI]); ret = osl(osl_add_and_get_row(audio_file_table, objs, &aft_row)); if (ret < 0) goto out; ret = afs_event(AUDIO_FILE_ADD, aft_row); out: if (ret < 0) afs_error(aca, "could not add %s\n", path); lls_free_parse_result(aca->lpr, cmd); return ret; } /* Used by com_add(). */ struct private_add_data { /* The pointer passed to the original command handler. */ struct command_context *cc; /* Contains the flags given at the command line. */ struct lls_parse_result *lpr; /* Serialized lopsub parse result. */ char *slpr; /* Number of bytes. */ size_t slpr_size; }; static int path_brother_callback(struct afs_callback_arg *aca) { char *path = aca->query.data; struct osl_row *path_brother; int ret = find_path_brother(path, &path_brother); if (ret <= 0) return ret; return pass_buffer_as_shm(aca->fd, SBD_OUTPUT, (char *)&path_brother, sizeof(path_brother)); } static int hash_sister_callback(struct afs_callback_arg *aca) { unsigned char *hash = aca->query.data; struct osl_row *hash_sister; int ret = find_hash_sister(hash, &hash_sister); if (ret <= 0) return ret; return pass_buffer_as_shm(aca->fd, SBD_OUTPUT, (char *)&hash_sister, sizeof(hash_sister)); } static int get_row_pointer_from_result(struct osl_object *result, __a_unused uint8_t band, void *private) { struct osl_row **row = private; if (band == SBD_OUTPUT) *row = *(struct osl_row **)(result->data); return 1; } static int add_one_audio_file(const char *path, void *private_data) { int ret, send_ret = 1, fd; uint8_t format_num = -1; struct private_add_data *pad = private_data; struct afh_info afhi, *afhi_ptr = NULL; struct osl_row *pb = NULL, *hs = NULL; /* path brother/hash sister */ struct osl_object map, obj = {.data = NULL}, query; unsigned char hash[HASH_SIZE]; bool a_given = SERVER_CMD_OPT_GIVEN(ADD, ALL, pad->lpr); bool f_given = SERVER_CMD_OPT_GIVEN(ADD, FORCE, pad->lpr); bool l_given = SERVER_CMD_OPT_GIVEN(ADD, LAZY, pad->lpr); bool v_given = SERVER_CMD_OPT_GIVEN(ADD, VERBOSE, pad->lpr); ret = guess_audio_format(path); if (ret < 0 && !a_given) { ret = 0; if (v_given) send_ret = send_sb_va(&pad->cc->scc, SBD_OUTPUT, "suffix-ignore: %s\n", path); goto out_free; } query.data = (char *)path; query.size = strlen(path) + 1; ret = send_callback_request(path_brother_callback, pad->cc->afs_fd, &query, get_row_pointer_from_result, &pb); if (ret < 0 && ret != osl(-E_OSL_RB_KEY_NOT_FOUND)) goto out_free; ret = 1; if (pb && l_given) { /* lazy is really cheap */ if (v_given) send_ret = send_sb_va(&pad->cc->scc, SBD_OUTPUT, "lazy-ignore: %s\n", path); goto out_free; } /* We still want to add this file. Compute its hash. */ ret = mmap_full_file(path, O_RDONLY, &map.data, &map.size, &fd); if (ret < 0) goto out_free; hash_function(map.data, map.size, hash); /* Check whether the database contains a file with the same hash. */ query.data = hash; query.size = HASH_SIZE; ret = send_callback_request(hash_sister_callback, pad->cc->afs_fd, &query, get_row_pointer_from_result, &hs); if (ret < 0) goto out_unmap; /* Return success if we already know this file. */ ret = 1; if (pb && hs && hs == pb && !f_given) { if (v_given) send_ret = send_sb_va(&pad->cc->scc, SBD_OUTPUT, "%s exists, not forcing update\n", path); goto out_unmap; } /* * We won't recalculate the audio format info and the chunk table if * there is a hash sister and FORCE was not given. */ if (!hs || f_given) { ret = compute_afhi(path, map.data, map.size, &afhi); if (ret < 0) goto out_unmap; format_num = ret; afhi_ptr = &afhi; } munmap(map.data, map.size); close(fd); if (v_given) { send_ret = send_sb_va(&pad->cc->scc, SBD_OUTPUT, "adding %s\n", path); if (send_ret < 0) goto out_free; } save_add_callback_buffer(hash, path, afhi_ptr, pad->slpr, pad->slpr_size, format_num, &obj); /* Ask afs to consider this entry for adding. */ ret = send_callback_request(com_add_callback, pad->cc->afs_fd, &obj, afs_cb_result_handler, pad->cc); goto out_free; out_unmap: close(fd); munmap(map.data, map.size); out_free: if (ret < 0 && send_ret >= 0) send_ret = send_sb_va(&pad->cc->scc, SBD_ERROR_LOG, "failed to add %s (%s)\n", path, para_strerror(-ret)); free(obj.data); clear_afhi(afhi_ptr); /* Stop adding files only on send errors. */ 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; struct private_add_data pad = {.cc = cc, .lpr = lpr}; const struct lls_command *cmd = SERVER_CMD_CMD_PTR(ADD); unsigned num_inputs; char *errctx; ret = lls(lls_check_arg_count(lpr, 1, INT_MAX, &errctx)); if (ret < 0) { send_errctx(cc, errctx); return ret; } ret = lls_serialize_parse_result(lpr, cmd, &pad.slpr, &pad.slpr_size); assert(ret >= 0); num_inputs = lls_num_inputs(lpr); for (i = 0; i < num_inputs; i++) { char *path; ret = verify_path(lls_input(i, lpr), &path); if (ret < 0) { ret = send_sb_va(&cc->scc, SBD_ERROR_LOG, "%s: %s\n", lls_input(i, lpr), para_strerror(-ret)); if (ret < 0) goto out; continue; } if (ret == 1) /* directory */ ret = for_each_file_in_dir(path, add_one_audio_file, &pad); else /* regular file */ ret = add_one_audio_file(path, &pad); if (ret < 0) { send_sb_va(&cc->scc, SBD_OUTPUT, "%s: %s\n", path, para_strerror(-ret)); free(path); return ret; } free(path); } ret = 1; out: free(pad.slpr); return ret; } EXPORT_SERVER_CMD_HANDLER(add); struct change_atts_data { uint64_t add_mask, del_mask; struct afs_callback_arg *aca; bool verbose, dry_run; }; static int afsi_change(struct osl_row *aft_row) { int ret; if (current_aft_row == aft_row) { uint64_t old_last_played = status_item_ls_data.afsi.last_played; ret = get_afsi_of_row(aft_row, &status_item_ls_data.afsi); if (ret < 0) return ret; status_item_ls_data.afsi.last_played = old_last_played; make_status_items(); } return afs_event(AFSI_CHANGE, aft_row); /* notify other tables */ } static int touch_audio_file(__a_unused struct osl_table *table, struct osl_row *row, const char *name, void *data) { struct change_atts_data *cad = data; struct afs_callback_arg *aca = cad->aca; const struct lls_opt_result *r_n, *r_l, *r_i, *r_y, *r_a, *r_s, *r_u; int ret; struct osl_object obj; struct afs_info old_afsi, new_afsi; bool no_options; r_n = SERVER_CMD_OPT_RESULT(TOUCH, NUMPLAYED, aca->lpr); r_l = SERVER_CMD_OPT_RESULT(TOUCH, LASTPLAYED, aca->lpr); r_i = SERVER_CMD_OPT_RESULT(TOUCH, IMAGE_ID, aca->lpr); r_y = SERVER_CMD_OPT_RESULT(TOUCH, LYRICS_ID, aca->lpr); r_a = SERVER_CMD_OPT_RESULT(TOUCH, AMP, aca->lpr); r_s = SERVER_CMD_OPT_RESULT(TOUCH, SET_ATTRIBUTE, aca->lpr); r_u = SERVER_CMD_OPT_RESULT(TOUCH, UNSET_ATTRIBUTE, aca->lpr); no_options = !lls_opt_given(r_n) && !lls_opt_given(r_l) && !lls_opt_given(r_i) && !lls_opt_given(r_y) && !lls_opt_given(r_a) && !lls_opt_given(r_s) && !lls_opt_given(r_u); ret = get_afsi_object_of_row(row, &obj); if (ret < 0) { afs_error(aca, "cannot touch %s\n", name); return ret; } ret = load_afsi(&old_afsi, &obj); if (ret < 0) { afs_error(aca, "cannot touch %s\n", name); return ret; } new_afsi = old_afsi; if (no_options) { new_afsi.num_played++; new_afsi.last_played = time(NULL); if (cad->verbose) para_printf(&aca->pbout, "%s: num_played = %u, " "last_played = now()\n", name, new_afsi.num_played); } else { if (lls_opt_given(r_l)) new_afsi.last_played = lls_uint64_val(0, r_l); if (lls_opt_given(r_n)) new_afsi.num_played = lls_uint32_val(0, r_n); if (lls_opt_given(r_i)) new_afsi.image_id = lls_uint32_val(0, r_i); if (lls_opt_given(r_y)) new_afsi.lyrics_id = lls_uint32_val(0, r_y); if (lls_opt_given(r_a)) new_afsi.amp = lls_uint32_val(0, r_a); new_afsi.attributes |= cad->add_mask; new_afsi.attributes &= ~cad->del_mask; if (cad->verbose) para_printf(&aca->pbout, "touching %s\n", name); } if (!cad->dry_run) save_afsi(&new_afsi, &obj); /* in-place update */ return afsi_change(row); } /* * Embed a change_atts_data structure into a pattern_match_data structure * for the for_each_matching_row() iterator, with touch_audio_file() as * the iterator callback for matching files. */ static int com_touch_callback(struct afs_callback_arg *aca) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(TOUCH); bool p_given; const struct lls_opt_result *r_s, *r_u, *r_i, *r_y; int ret, s_given, u_given; struct change_atts_data cad = {.aca = aca}; struct pattern_match_data pmd = { .table = audio_file_table, .loop_col_num = AFTCOL_HASH, .match_col_num = AFTCOL_PATH, .data = &cad, .action = touch_audio_file }; ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr); assert(ret >= 0); pmd.lpr = aca->lpr; cad.dry_run = SERVER_CMD_OPT_GIVEN(TOUCH, DRY_RUN, aca->lpr); cad.verbose = cad.dry_run || SERVER_CMD_OPT_GIVEN(TOUCH, VERBOSE, aca->lpr); r_s = SERVER_CMD_OPT_RESULT(TOUCH, SET_ATTRIBUTE, aca->lpr); s_given = SERVER_CMD_OPT_GIVEN(TOUCH, SET_ATTRIBUTE, aca->lpr); for (int i = 0; i < s_given; i++) { const char *arg = lls_string_val(i, r_s); ret = attr_name_to_bitnum(arg); if (ret < 0) { afs_error(aca, "cannot get bit number of %s\n", arg); goto out; } cad.add_mask |= 1ULL << ret; } r_u = SERVER_CMD_OPT_RESULT(TOUCH, UNSET_ATTRIBUTE, aca->lpr); u_given = SERVER_CMD_OPT_GIVEN(TOUCH, UNSET_ATTRIBUTE, aca->lpr); for (int i = 0; i < u_given; i++) { const char *arg = lls_string_val(i, r_u); ret = attr_name_to_bitnum(arg); if (ret < 0) { afs_error(aca, "cannot get bit number of %s\n", arg); goto out; } cad.del_mask |= 1ULL << ret; } r_i = SERVER_CMD_OPT_RESULT(TOUCH, IMAGE_ID, aca->lpr); if (lls_opt_given(r_i)) { uint32_t id = lls_uint32_val(0, r_i); ret = img_get_name_by_id(id, NULL); if (ret < 0) { afs_error(aca, "invalid image ID: %u\n", id); return ret; } } r_y = SERVER_CMD_OPT_RESULT(TOUCH, LYRICS_ID, aca->lpr); if (lls_opt_given(r_y)) { uint32_t id = lls_uint32_val(0, r_y); ret = lyr_get_name_by_id(id, NULL); if (ret < 0) { afs_error(aca, "invalid lyrics ID: %u\n", id); return ret; } } p_given = SERVER_CMD_OPT_GIVEN(TOUCH, PATHNAME_MATCH, aca->lpr); if (p_given) pmd.fnmatch_flags |= FNM_PATHNAME; ret = for_each_matching_row(&pmd); if (ret >= 0 && pmd.num_matches == 0) ret = -E_NO_MATCH; out: lls_free_parse_result(aca->lpr, cmd); return ret; } static int com_touch(struct command_context *cc, struct lls_parse_result *lpr) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(TOUCH); int ret; char *errctx; ret = lls(lls_check_arg_count(lpr, 1, INT_MAX, &errctx)); if (ret < 0) { send_errctx(cc, errctx); return ret; } return send_lls_callback_request(com_touch_callback, cc->afs_fd, cmd, lpr, cc); } EXPORT_SERVER_CMD_HANDLER(touch); static int remove_audio_file(__a_unused struct osl_table *table, struct osl_row *row, const char *name, void *data) { struct afs_callback_arg *aca = data; bool v_given = SERVER_CMD_OPT_GIVEN(RM, VERBOSE, aca->lpr); int ret; if (v_given) para_printf(&aca->pbout, "removing %s\n", name); if (current_aft_row == row) current_aft_row = NULL; ret = afs_event(AUDIO_FILE_REMOVE, row); if (ret < 0) return ret; ret = osl(osl_del_row(audio_file_table, row)); if (ret < 0) afs_error(aca, "cannot remove %s\n", name); return ret; } static int com_rm_callback(struct afs_callback_arg *aca) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(RM); int ret; struct pattern_match_data pmd = { .table = audio_file_table, .loop_col_num = AFTCOL_HASH, .match_col_num = AFTCOL_PATH, .data = aca, .action = remove_audio_file }; bool v_given, p_given, f_given; ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr); assert(ret >= 0); pmd.lpr = aca->lpr; v_given = SERVER_CMD_OPT_GIVEN(RM, VERBOSE, aca->lpr); p_given = SERVER_CMD_OPT_GIVEN(RM, PATHNAME_MATCH, aca->lpr); f_given = SERVER_CMD_OPT_GIVEN(RM, FORCE, aca->lpr); if (p_given) pmd.fnmatch_flags |= FNM_PATHNAME; ret = for_each_matching_row(&pmd); if (ret < 0) goto out; if (pmd.num_matches == 0) { if (!f_given) ret = -E_NO_MATCH; } else if (v_given) para_printf(&aca->pbout, "removed %u file(s)\n", pmd.num_matches); out: lls_free_parse_result(aca->lpr, cmd); return ret; } /* TODO options: -r (recursive) */ static int com_rm(struct command_context *cc, struct lls_parse_result *lpr) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(RM); char *errctx; int ret; ret = lls(lls_check_arg_count(lpr, 1, INT_MAX, &errctx)); if (ret < 0) { send_errctx(cc, errctx); return ret; } return send_lls_callback_request(com_rm_callback, cc->afs_fd, cmd, lpr, cc); } EXPORT_SERVER_CMD_HANDLER(rm); /* passed to the action handler of the cpsi() command */ struct cpsi_action_data { /* Values are copied from here. */ struct afs_info source_afsi; /* What was passed to com_cpsi_callback(). */ struct afs_callback_arg *aca; bool copy_all; }; static int copy_selector_info(__a_unused struct osl_table *table, struct osl_row *row, const char *name, void *data) { struct cpsi_action_data *cad = data; struct osl_object target_afsi_obj; int ret; struct afs_info target_afsi; bool a_given, y_given, i_given, l_given, n_given, v_given; a_given = SERVER_CMD_OPT_GIVEN(CPSI, ATTRIBUTE_BITMAP, cad->aca->lpr); y_given = SERVER_CMD_OPT_GIVEN(CPSI, LYRICS_ID, cad->aca->lpr); i_given = SERVER_CMD_OPT_GIVEN(CPSI, IMAGE_ID, cad->aca->lpr); l_given = SERVER_CMD_OPT_GIVEN(CPSI, LASTPLAYED, cad->aca->lpr); n_given = SERVER_CMD_OPT_GIVEN(CPSI, NUMPLAYED, cad->aca->lpr); v_given = SERVER_CMD_OPT_GIVEN(CPSI, VERBOSE, cad->aca->lpr); ret = get_afsi_object_of_row(row, &target_afsi_obj); if (ret < 0) return ret; ret = load_afsi(&target_afsi, &target_afsi_obj); if (ret < 0) return ret; if (cad->copy_all || y_given) target_afsi.lyrics_id = cad->source_afsi.lyrics_id; if (cad->copy_all || i_given) target_afsi.image_id = cad->source_afsi.image_id; if (cad->copy_all || l_given) target_afsi.last_played = cad->source_afsi.last_played; if (cad->copy_all || n_given) target_afsi.num_played = cad->source_afsi.num_played; if (cad->copy_all || a_given) target_afsi.attributes = cad->source_afsi.attributes; save_afsi(&target_afsi, &target_afsi_obj); /* in-place update */ if (v_given) para_printf(&cad->aca->pbout, "copied afsi to %s\n", name); return afsi_change(row); } static int com_cpsi_callback(struct afs_callback_arg *aca) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(CPSI); bool a_given, y_given, i_given, l_given, n_given, v_given; struct cpsi_action_data cad = {.aca = aca}; int ret; struct pattern_match_data pmd = { .table = audio_file_table, .loop_col_num = AFTCOL_HASH, .match_col_num = AFTCOL_PATH, .input_skip = 1, /* skip first argument (source file) */ .data = &cad, .action = copy_selector_info }; ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr); assert(ret >= 0); pmd.lpr = aca->lpr; a_given = SERVER_CMD_OPT_GIVEN(CPSI, ATTRIBUTE_BITMAP, aca->lpr); y_given = SERVER_CMD_OPT_GIVEN(CPSI, LYRICS_ID, aca->lpr); i_given = SERVER_CMD_OPT_GIVEN(CPSI, IMAGE_ID, aca->lpr); l_given = SERVER_CMD_OPT_GIVEN(CPSI, LASTPLAYED, aca->lpr); n_given = SERVER_CMD_OPT_GIVEN(CPSI, NUMPLAYED, aca->lpr); v_given = SERVER_CMD_OPT_GIVEN(CPSI, VERBOSE, aca->lpr); cad.copy_all = !a_given && !y_given && !i_given && !l_given && !n_given; ret = get_afsi_of_path(lls_input(0, aca->lpr), &cad.source_afsi); if (ret < 0) goto out; ret = for_each_matching_row(&pmd); if (ret < 0) goto out; if (pmd.num_matches > 0) { if (v_given) para_printf(&aca->pbout, "updated afsi of %u file(s)\n", pmd.num_matches); } else ret = -E_NO_MATCH; out: lls_free_parse_result(aca->lpr, cmd); return ret; } static int com_cpsi(struct command_context *cc, struct lls_parse_result *lpr) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(CPSI); char *errctx; int ret = lls(lls_check_arg_count(lpr, 2, INT_MAX, &errctx)); if (ret < 0) { send_errctx(cc, errctx); return ret; } return send_lls_callback_request(com_cpsi_callback, cc->afs_fd, cmd, lpr, cc); } EXPORT_SERVER_CMD_HANDLER(cpsi); static int afs_stat_callback(struct afs_callback_arg *aca) { bool *parser_friendly = aca->query.data; char *buf = *parser_friendly? parser_friendly_status_items : status_items; if (!buf) return 0; return pass_buffer_as_shm(aca->fd, SBD_OUTPUT, buf, strlen(buf)); } /** * Get the current afs status items from the afs process and send it. * * \param cc The command context, used e.g. for data encryption. * \param parser_friendly Whether parser-friendly output format should be used. * * As the contents of the afs status items change in time and the command * handler only has a COW version created at fork time, it can not send * up-to-date afs status items directly. Therefore the usual callback mechanism * is used to pass the status items from the afs process to the command handler * via a shared memory area and a pipe. * * \return The return value of the underlying call to \ref send_callback_request(). */ int send_afs_status(struct command_context *cc, bool parser_friendly) { struct osl_object query = {.data = &parser_friendly, .size = sizeof(parser_friendly)}; return send_callback_request(afs_stat_callback, cc->afs_fd, &query, afs_cb_result_handler, cc); } struct check_callback_data { struct para_buffer *pbout; uint64_t defined_attributes; }; /* returns success on non-fatal errors to keep the loop going */ static int check_audio_file(struct osl_row *row, void *data) { char *path; struct stat statbuf; struct afs_info afsi; char *blob_name; struct check_callback_data *ccd = data; struct para_buffer *pb = ccd->pbout; int ret = get_audio_file_path_of_row(row, &path); if (ret < 0) { para_printf(pb, "%s\n", para_strerror(-ret)); return ret; } if (stat(path, &statbuf) < 0) para_printf(pb, "%s: stat error (%s)\n", path, strerror(errno)); else if (!S_ISREG(statbuf.st_mode)) para_printf(pb, "%s: not a regular file\n", path); ret = get_afsi_of_row(row, &afsi); if (ret < 0) { para_printf(pb, "%s: %s\n", path, para_strerror(-ret)); return 1; } /* * Check that all bits set in the on-disk afsi correspond to a defined * attribute of the attribute table. */ if (afsi.attributes & ~ccd->defined_attributes) para_printf(pb, "%s: invalid attribute bits\n", path); ret = lyr_get_name_by_id(afsi.lyrics_id, &blob_name); if (ret < 0) para_printf(pb, "%s lyrics id %u: %s\n", path, afsi.lyrics_id, para_strerror(-ret)); ret = img_get_name_by_id(afsi.image_id, &blob_name); if (ret < 0) para_printf(pb, "%s image id %u: %s\n", path, afsi.image_id, para_strerror(-ret)); return 0; } /** * Check the audio file table for inconsistencies. * * \param aca Only ->pbout is used for diagnostics. * * \return Standard. Inconsistencies are reported but not regarded as an error. */ int aft_check_callback(struct afs_callback_arg *aca) { struct check_callback_data ccd = {.pbout = &aca->pbout}; int ret = attr_get_defined_mask(&ccd.defined_attributes); if (ret < 0) return ret; para_printf(&aca->pbout, "checking audio file table...\n"); return audio_file_loop(&ccd, check_audio_file); } /* * This leaves current_aft_row unmodified, though stale (pointing to unmapped * memory). If the table is being closed because we received SIGHUP, * \ref aft_open() below will be called to reload the table, detect that * current_aft_row is not NULL, and look up the audio file via the saved * hash value to re-create the status items. */ static void aft_close(void) { osl_close_table(audio_file_table, OSL_MARK_CLEAN); audio_file_table = NULL; } static int aft_open(const char *dir) { int ret; unsigned num; audio_file_table_desc.dir = dir; ret = osl(osl_open_table(&audio_file_table_desc, &audio_file_table)); if (ret < 0) return ret; assert(osl_get_num_rows(audio_file_table, &num) >= 0); PARA_NOTICE_LOG("audio file table contains %u files\n", num); if (!current_aft_row) { PARA_DEBUG_LOG("no current aft row\n"); return 1; } /* SIGHUP case, update current_aft_row */ ret = aft_get_row_of_hash(current_hash, ¤t_aft_row); if (ret < 0) { /* not fatal */ PARA_WARNING_LOG("current hash lookup failure: %s\n", para_strerror(-ret)); current_aft_row = NULL; return 1; } PARA_NOTICE_LOG("current audio file hash lookup: success\n"); return 1; } static int aft_create(const char *dir) { audio_file_table_desc.dir = dir; return osl(osl_create_table(&audio_file_table_desc)); } static int aft_event_handler(enum afs_events event, __a_unused void *data) { switch (event) { /* * These events are rare. We don't bother to check whether the current * status items are affected and simply recreate them whenever an * audio file is open. */ case ATTRIBUTE_REMOVE: case BLOB_RENAME: case BLOB_REMOVE: case BLOB_ADD: make_status_items(); return 0; default: return 0; } } /** The audio file table contains information about known audio files. */ const struct afs_table_operations aft_ops = { .open = aft_open, .close = aft_close, .create = aft_create, .event_handler = aft_event_handler, };