/* * Copyright 2008-2013 Various Authors * Copyright 2004-2005 Timo Hirvonen * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include "convert.h" #include "input.h" #include "ip.h" #include "pcm.h" #include "http.h" #include "xmalloc.h" #include "file.h" #include "path.h" #include "utils.h" #include "cmus.h" #include "options.h" #include "list.h" #include "mergesort.h" #include "misc.h" #include "debug.h" #include "ui_curses.h" #include "locking.h" #include "xstrjoin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct input_plugin { const struct input_plugin_ops *ops; struct input_plugin_data data; unsigned int open : 1; unsigned int eof : 1; int http_code; char *http_reason; /* cached duration, -1 = unset */ int duration; /* cached bitrate, -1 = unset */ long bitrate; /* cached codec, NULL = unset */ char *codec; /* cached codec_profile, NULL = unset */ char *codec_profile; /* * pcm is converted to 16-bit signed little-endian stereo * NOTE: no conversion is done if channels > 2 or bits > 16 */ void (*pcm_convert)(void *, const void *, int); void (*pcm_convert_in_place)(void *, int); /* * 4 if 8-bit mono * 2 if 8-bit stereo or 16-bit mono * 1 otherwise */ int pcm_convert_scale; }; struct ip { struct list_head node; char *name; void *handle; int priority; const char * const *extensions; const char * const *mime_types; const struct input_plugin_ops *ops; const struct input_plugin_opt *options; }; static const char *plugin_dir; static LIST_HEAD(ip_head); /* protects ip->priority and ip_head */ static pthread_rwlock_t ip_lock = CMUS_RWLOCK_INITIALIZER; #define ip_rdlock() cmus_rwlock_rdlock(&ip_lock) #define ip_wrlock() cmus_rwlock_wrlock(&ip_lock) #define ip_unlock() cmus_rwlock_unlock(&ip_lock) /* timeouts (ms) */ static int http_connection_timeout = 5e3; static int http_read_timeout = 5e3; static const char *pl_mime_types[] = { "audio/m3u", "audio/x-scpls", "audio/x-mpegurl" }; static const struct input_plugin_ops * get_ops_by_extension_locked(const char *ext, struct list_head **headp) { struct list_head *node = *headp; for (node = node->next; node != &ip_head; node = node->next) { struct ip *ip = list_entry(node, struct ip, node); const char * const *exts = ip->extensions; int i; if (ip->priority <= 0) { break; } for (i = 0; exts[i]; i++) { if (strcasecmp(ext, exts[i]) == 0 || strcmp("*", exts[i]) == 0) { *headp = node; return ip->ops; } } } return NULL; } static const struct input_plugin_ops * get_ops_by_extension(const char *ext, struct list_head **headp) { ip_rdlock(); const struct input_plugin_ops *rv = get_ops_by_extension_locked(ext, headp); ip_unlock(); return rv; } static const struct input_plugin_ops * get_ops_by_mime_type_locked(const char *mime_type) { struct ip *ip; list_for_each_entry(ip, &ip_head, node) { const char * const *types = ip->mime_types; int i; if (ip->priority <= 0) { break; } for (i = 0; types[i]; i++) { if (strcasecmp(mime_type, types[i]) == 0) return ip->ops; } } return NULL; } static const struct input_plugin_ops * get_ops_by_mime_type(const char *mime_type) { ip_rdlock(); const struct input_plugin_ops *rv = get_ops_by_mime_type_locked(mime_type); ip_unlock(); return rv; } static void keyvals_add_basic_auth(struct growing_keyvals *c, const char *user, const char *pass, const char *header) { char buf[256]; char *encoded; snprintf(buf, sizeof(buf), "%s:%s", user, pass); encoded = base64_encode(buf); if (encoded == NULL) { d_print("couldn't base64 encode '%s'\n", buf); } else { snprintf(buf, sizeof(buf), "Basic %s", encoded); free(encoded); keyvals_add(c, header, xstrdup(buf)); } } static int do_http_get(struct http_get *hg, const char *uri, int redirections) { GROWING_KEYVALS(h); int i, rc; const char *val; char *redirloc; d_print("%s\n", uri); hg->headers = NULL; hg->reason = NULL; hg->proxy = NULL; hg->code = -1; hg->fd = -1; if (http_parse_uri(uri, &hg->uri)) return -IP_ERROR_INVALID_URI; if (http_open(hg, http_connection_timeout)) return -IP_ERROR_ERRNO; keyvals_add(&h, "Host", xstrdup(hg->uri.host)); if (hg->proxy && hg->proxy->user && hg->proxy->pass) keyvals_add_basic_auth(&h, hg->proxy->user, hg->proxy->pass, "Proxy-Authorization"); keyvals_add(&h, "User-Agent", xstrdup("cmus/" VERSION)); keyvals_add(&h, "Icy-MetaData", xstrdup("1")); if (hg->uri.user && hg->uri.pass) keyvals_add_basic_auth(&h, hg->uri.user, hg->uri.pass, "Authorization"); keyvals_terminate(&h); rc = http_get(hg, h.keyvals, http_read_timeout); keyvals_free(h.keyvals); switch (rc) { case -1: return -IP_ERROR_ERRNO; case -2: return -IP_ERROR_HTTP_RESPONSE; } d_print("HTTP response: %d %s\n", hg->code, hg->reason); for (i = 0; hg->headers[i].key != NULL; i++) d_print(" %s: %s\n", hg->headers[i].key, hg->headers[i].val); switch (hg->code) { case 200: /* OK */ return 0; /* * 3xx Codes (Redirections) * unhandled: 300 Multiple Choices */ case 301: /* Moved Permanently */ case 302: /* Found */ case 303: /* See Other */ case 307: /* Temporary Redirect */ val = keyvals_get_val(hg->headers, "location"); if (!val) return -IP_ERROR_HTTP_RESPONSE; redirections++; if (redirections > 2) return -IP_ERROR_HTTP_REDIRECT_LIMIT; redirloc = xstrdup(val); http_get_free(hg); close(hg->fd); rc = do_http_get(hg, redirloc, redirections); free(redirloc); return rc; default: return -IP_ERROR_HTTP_STATUS; } } static int setup_remote(struct input_plugin *ip, const struct keyval *headers, int sock) { const char *val; val = keyvals_get_val(headers, "Content-Type"); if (val) { d_print("Content-Type: %s\n", val); ip->ops = get_ops_by_mime_type(val); if (ip->ops == NULL) { d_print("unsupported content type: %s\n", val); close(sock); return -IP_ERROR_FILE_FORMAT; } } else { const char *type = "audio/mpeg"; d_print("assuming %s content type\n", type); ip->ops = get_ops_by_mime_type(type); if (ip->ops == NULL) { d_print("unsupported content type: %s\n", type); close(sock); return -IP_ERROR_FILE_FORMAT; } } ip->data.fd = sock; ip->data.metadata = xnew(char, 16 * 255 + 1); val = keyvals_get_val(headers, "icy-metaint"); if (val) { long int lint; if (str_to_int(val, &lint) == 0 && lint >= 0) { ip->data.metaint = lint; d_print("metaint: %d\n", ip->data.metaint); } } val = keyvals_get_val(headers, "icy-name"); if (val) ip->data.icy_name = to_utf8(val, icecast_default_charset); val = keyvals_get_val(headers, "icy-genre"); if (val) ip->data.icy_genre = to_utf8(val, icecast_default_charset); val = keyvals_get_val(headers, "icy-url"); if (val) ip->data.icy_url = to_utf8(val, icecast_default_charset); val = keyvals_get_val(headers, "icy-br"); if (val) ip->data.icy_br = to_utf8(val, icecast_default_charset); return 0; } struct read_playlist_data { struct input_plugin *ip; int rc; int count; }; static int handle_line(void *data, const char *uri) { struct read_playlist_data *rpd = data; struct http_get hg; rpd->count++; rpd->rc = do_http_get(&hg, uri, 0); if (rpd->rc) { rpd->ip->http_code = hg.code; rpd->ip->http_reason = hg.reason; if (hg.fd >= 0) close(hg.fd); hg.reason = NULL; http_get_free(&hg); return 0; } rpd->rc = setup_remote(rpd->ip, hg.headers, hg.fd); http_get_free(&hg); return 1; } static int read_playlist(struct input_plugin *ip, int sock) { struct read_playlist_data rpd = { ip, 0, 0 }; char *body; size_t size; body = http_read_body(sock, &size, http_read_timeout); close(sock); if (!body) return -IP_ERROR_ERRNO; cmus_playlist_for_each(body, size, 0, handle_line, &rpd); free(body); if (!rpd.count) { d_print("empty playlist\n"); rpd.rc = -IP_ERROR_HTTP_RESPONSE; } return rpd.rc; } static int open_remote(struct input_plugin *ip) { struct input_plugin_data *d = &ip->data; struct http_get hg; const char *val; int rc; rc = do_http_get(&hg, d->filename, 0); if (rc) { ip->http_code = hg.code; ip->http_reason = hg.reason; hg.reason = NULL; http_get_free(&hg); return rc; } val = keyvals_get_val(hg.headers, "Content-Type"); if (val) { int i; for (i = 0; i < N_ELEMENTS(pl_mime_types); i++) { if (!strcasecmp(val, pl_mime_types[i])) { d_print("Content-Type: %s\n", val); http_get_free(&hg); return read_playlist(ip, hg.fd); } } } rc = setup_remote(ip, hg.headers, hg.fd); http_get_free(&hg); return rc; } static void ip_init(struct input_plugin *ip, char *filename) { const struct input_plugin t = { .http_code = -1, .pcm_convert_scale = -1, .duration = -1, .bitrate = -1, .data = { .fd = -1, .filename = filename, .remote = is_http_url(filename), .channel_map = CHANNEL_MAP_INIT } }; *ip = t; } static void ip_reset(struct input_plugin *ip, int close_fd) { int fd = ip->data.fd; free(ip->data.metadata); ip_init(ip, ip->data.filename); if (fd != -1) { if (close_fd) close(fd); else { lseek(fd, 0, SEEK_SET); ip->data.fd = fd; } } } static int open_file_locked(struct input_plugin *ip) { const struct input_plugin_ops *ops; struct list_head *head = &ip_head; const char *ext; int rc = 0; ext = get_extension(ip->data.filename); if (!ext) return -IP_ERROR_UNRECOGNIZED_FILE_TYPE; ops = get_ops_by_extension(ext, &head); if (!ops) return -IP_ERROR_UNRECOGNIZED_FILE_TYPE; ip->data.fd = open(ip->data.filename, O_RDONLY); if (ip->data.fd == -1) return -IP_ERROR_ERRNO; while (1) { ip->ops = ops; rc = ip->ops->open(&ip->data); if (rc != -IP_ERROR_UNSUPPORTED_FILE_TYPE) break; ops = get_ops_by_extension(ext, &head); if (!ops) break; ip_reset(ip, 0); d_print("fallback: try next plugin for `%s'\n", ip->data.filename); } return rc; } static int open_file(struct input_plugin *ip) { ip_rdlock(); int rv = open_file_locked(ip); ip_unlock(); return rv; } static int sort_ip(const struct list_head *a_, const struct list_head *b_) { const struct ip *a = list_entry(a_, struct ip, node); const struct ip *b = list_entry(b_, struct ip, node); return b->priority - a->priority; } void ip_load_plugins(void) { DIR *dir; struct dirent *d; plugin_dir = xstrjoin(cmus_lib_dir, "/ip"); dir = opendir(plugin_dir); if (dir == NULL) { error_msg("couldn't open directory `%s': %s", plugin_dir, strerror(errno)); return; } ip_wrlock(); while ((d = (struct dirent *) readdir(dir)) != NULL) { char filename[512]; struct ip *ip; void *so; char *ext; const int *priority_ptr; const unsigned *abi_version_ptr; bool err = false; if (d->d_name[0] == '.') continue; ext = strrchr(d->d_name, '.'); if (ext == NULL) continue; if (strcmp(ext, ".so")) continue; snprintf(filename, sizeof(filename), "%s/%s", plugin_dir, d->d_name); so = dlopen(filename, RTLD_NOW); if (so == NULL) { d_print("%s: %s\n", filename, dlerror()); continue; } ip = xnew(struct ip, 1); abi_version_ptr = dlsym(so, "ip_abi_version"); priority_ptr = dlsym(so, "ip_priority"); ip->extensions = dlsym(so, "ip_extensions"); ip->mime_types = dlsym(so, "ip_mime_types"); ip->ops = dlsym(so, "ip_ops"); ip->options = dlsym(so, "ip_options"); if (!priority_ptr || !ip->extensions || !ip->mime_types || !ip->ops || !ip->options) { error_msg("%s: missing symbol", filename); err = true; } if (!abi_version_ptr || *abi_version_ptr != IP_ABI_VERSION) { error_msg("%s: incompatible plugin version", filename); err = true; } if (err) { free(ip); dlclose(so); continue; } ip->priority = *priority_ptr; ip->name = xstrndup(d->d_name, ext - d->d_name); ip->handle = so; list_add_tail(&ip->node, &ip_head); } list_mergesort(&ip_head, sort_ip); closedir(dir); ip_unlock(); } struct input_plugin *ip_new(const char *filename) { struct input_plugin *ip = xnew(struct input_plugin, 1); ip_init(ip, xstrdup(filename)); return ip; } void ip_delete(struct input_plugin *ip) { if (ip->open) ip_close(ip); free(ip->data.filename); free(ip); } int ip_open(struct input_plugin *ip) { int rc; BUG_ON(ip->open); /* set fd and ops, call ops->open */ if (ip->data.remote) { rc = open_remote(ip); if (rc == 0) rc = ip->ops->open(&ip->data); } else { if (is_cdda_url(ip->data.filename)) { ip->ops = get_ops_by_mime_type("x-content/audio-cdda"); rc = ip->ops ? ip->ops->open(&ip->data) : 1; } else if (is_cue_url(ip->data.filename)) { ip->ops = get_ops_by_mime_type("application/x-cue"); rc = ip->ops ? ip->ops->open(&ip->data) : 1; } else rc = open_file(ip); } if (rc) { d_print("opening `%s' failed: %d %s\n", ip->data.filename, rc, rc == -1 ? strerror(errno) : ""); ip_reset(ip, 1); return rc; } unsigned bits = sf_get_bits(ip->data.sf); unsigned channels = sf_get_channels(ip->data.sf); unsigned rate = sf_get_rate(ip->data.sf); if (!bits || !channels || !rate) { d_print("corrupt file: bits = %u, channels = %u, rate = %u\n", bits, channels, rate); return -IP_ERROR_FILE_FORMAT; } ip->open = 1; return 0; } void ip_setup(struct input_plugin *ip) { unsigned int bits, is_signed, channels; sample_format_t sf = ip->data.sf; bits = sf_get_bits(sf); is_signed = sf_get_signed(sf); channels = sf_get_channels(sf); ip->pcm_convert_scale = 1; ip->pcm_convert = NULL; ip->pcm_convert_in_place = NULL; if (bits <= 16 && channels <= 2) { unsigned int mask = ((bits >> 2) & 4) | (is_signed << 1); ip->pcm_convert = pcm_conv[mask | (channels - 1)]; ip->pcm_convert_in_place = pcm_conv_in_place[mask | sf_get_bigendian(sf)]; ip->pcm_convert_scale = (3 - channels) * (3 - bits / 8); } d_print("pcm convert: scale=%d convert=%d convert_in_place=%d\n", ip->pcm_convert_scale, ip->pcm_convert != NULL, ip->pcm_convert_in_place != NULL); } int ip_close(struct input_plugin *ip) { int rc; rc = ip->ops->close(&ip->data); BUG_ON(ip->data.private); if (ip->data.fd != -1) close(ip->data.fd); free(ip->data.metadata); free(ip->data.icy_name); free(ip->data.icy_genre); free(ip->data.icy_url); free(ip->data.icy_br); free(ip->http_reason); ip_init(ip, ip->data.filename); return rc; } int ip_read(struct input_plugin *ip, char *buffer, int count) { struct timeval tv; fd_set readfds; /* 4608 seems to be optimal for mp3s, 4096 for oggs */ char tmp[8 * 1024]; char *buf; int sample_size; int rc; BUG_ON(count <= 0); FD_ZERO(&readfds); FD_SET(ip->data.fd, &readfds); /* zero timeout -> return immediately */ tv.tv_sec = 0; tv.tv_usec = 50e3; rc = select(ip->data.fd + 1, &readfds, NULL, NULL, &tv); if (rc == -1) { if (errno == EINTR) errno = EAGAIN; return -1; } if (rc == 0) { errno = EAGAIN; return -1; } buf = buffer; if (ip->pcm_convert_scale > 1) { /* use tmp buffer for 16-bit mono and 8-bit */ buf = tmp; count /= ip->pcm_convert_scale; if (count > sizeof(tmp)) count = sizeof(tmp); } rc = ip->ops->read(&ip->data, buf, count); if (rc == -1 && (errno == EAGAIN || errno == EINTR)) { errno = EAGAIN; return -1; } if (rc <= 0) { ip->eof = 1; return rc; } BUG_ON(rc % sf_get_frame_size(ip->data.sf) != 0); sample_size = sf_get_sample_size(ip->data.sf); if (ip->pcm_convert_in_place != NULL) ip->pcm_convert_in_place(buf, rc / sample_size); if (ip->pcm_convert != NULL) ip->pcm_convert(buffer, tmp, rc / sample_size); return rc * ip->pcm_convert_scale; } int ip_seek(struct input_plugin *ip, double offset) { int rc; if (ip->data.remote) return -IP_ERROR_FUNCTION_NOT_SUPPORTED; rc = ip->ops->seek(&ip->data, offset); if (rc == 0) ip->eof = 0; return rc; } int ip_read_comments(struct input_plugin *ip, struct keyval **comments) { struct keyval *kv = NULL; int rc; rc = ip->ops->read_comments(&ip->data, &kv); if (ip->data.remote) { GROWING_KEYVALS(c); if (kv) { keyvals_init(&c, kv); keyvals_free(kv); } if (ip->data.icy_name && !keyvals_get_val_growing(&c, "title")) keyvals_add(&c, "title", xstrdup(ip->data.icy_name)); if (ip->data.icy_genre && !keyvals_get_val_growing(&c, "genre")) keyvals_add(&c, "genre", xstrdup(ip->data.icy_genre)); if (ip->data.icy_url && !keyvals_get_val_growing(&c, "comment")) keyvals_add(&c, "comment", xstrdup(ip->data.icy_url)); if (ip->data.icy_br && !keyvals_get_val_growing(&c, "bitrate")) keyvals_add(&c, "bitrate", xstrdup(ip->data.icy_br)); keyvals_terminate(&c); kv = c.keyvals; } *comments = kv; return ip->data.remote ? 0 : rc; } int ip_duration(struct input_plugin *ip) { if (ip->data.remote) return -1; if (ip->duration == -1) ip->duration = ip->ops->duration(&ip->data); if (ip->duration < 0) return -1; return ip->duration; } int ip_bitrate(struct input_plugin *ip) { if (ip->data.remote) return -1; if (ip->bitrate == -1) ip->bitrate = ip->ops->bitrate(&ip->data); if (ip->bitrate < 0) return -1; return ip->bitrate; } int ip_current_bitrate(struct input_plugin *ip) { return ip->ops->bitrate_current(&ip->data); } char *ip_codec(struct input_plugin *ip) { if (ip->data.remote) return NULL; if (!ip->codec) ip->codec = ip->ops->codec(&ip->data); return ip->codec; } char *ip_codec_profile(struct input_plugin *ip) { if (ip->data.remote) return NULL; if (!ip->codec_profile) ip->codec_profile = ip->ops->codec_profile(&ip->data); return ip->codec_profile; } sample_format_t ip_get_sf(struct input_plugin *ip) { BUG_ON(!ip->open); return ip->data.sf; } void ip_get_channel_map(struct input_plugin *ip, channel_position_t *channel_map) { BUG_ON(!ip->open); channel_map_copy(channel_map, ip->data.channel_map); } const char *ip_get_filename(struct input_plugin *ip) { return ip->data.filename; } const char *ip_get_metadata(struct input_plugin *ip) { BUG_ON(!ip->open); return ip->data.metadata; } int ip_is_remote(struct input_plugin *ip) { return ip->data.remote; } int ip_metadata_changed(struct input_plugin *ip) { int ret = ip->data.metadata_changed; BUG_ON(!ip->open); ip->data.metadata_changed = 0; return ret; } int ip_eof(struct input_plugin *ip) { BUG_ON(!ip->open); return ip->eof; } static void option_error(int rc) { char *msg = ip_get_error_msg(NULL, rc, "setting option"); error_msg("%s", msg); free(msg); } static void set_ip_option(void *data, const char *val) { const struct input_plugin_opt *ipo = data; int rc; rc = ipo->set(val); if (rc) option_error(rc); } static void get_ip_option(void *data, char *buf, size_t size) { const struct input_plugin_opt *ipo = data; char *val = NULL; ipo->get(&val); if (val) { strscpy(buf, val, size); free(val); } } static void set_ip_priority(void *data, const char *val) { /* warn only once during the lifetime of the program. */ static bool warned = false; long tmp; struct ip *ip = data; if (str_to_int(val, &tmp) == -1 || tmp < 0 || (long)(int)tmp != tmp) { error_msg("non-negative integer expected"); return; } if (ui_initialized) { if (!warned) { static const char *msg = "Metadata might become inconsistent " "after this change. Continue? [y/N]"; if (yes_no_query("%s", msg) != UI_QUERY_ANSWER_YES) { info_msg("Aborted"); return; } warned = true; } info_msg("Run \":update-cache -f\" to refresh the metadata."); } ip_wrlock(); ip->priority = (int)tmp; list_mergesort(&ip_head, sort_ip); ip_unlock(); } static void get_ip_priority(void *data, char *val, size_t size) { const struct ip *ip = data; ip_rdlock(); snprintf(val, size, "%d", ip->priority); ip_unlock(); } void ip_add_options(void) { struct ip *ip; const struct input_plugin_opt *ipo; char key[64]; ip_rdlock(); list_for_each_entry(ip, &ip_head, node) { for (ipo = ip->options; ipo->name; ipo++) { snprintf(key, sizeof(key), "input.%s.%s", ip->name, ipo->name); option_add(xstrdup(key), ipo, get_ip_option, set_ip_option, NULL, 0); } snprintf(key, sizeof(key), "input.%s.priority", ip->name); option_add(xstrdup(key), ip, get_ip_priority, set_ip_priority, NULL, 0); } ip_unlock(); } char *ip_get_error_msg(struct input_plugin *ip, int rc, const char *arg) { char buffer[1024]; switch (-rc) { case IP_ERROR_ERRNO: snprintf(buffer, sizeof(buffer), "%s: %s", arg, strerror(errno)); break; case IP_ERROR_UNRECOGNIZED_FILE_TYPE: snprintf(buffer, sizeof(buffer), "%s: unrecognized filename extension", arg); break; case IP_ERROR_UNSUPPORTED_FILE_TYPE: snprintf(buffer, sizeof(buffer), "%s: unsupported file format", arg); break; case IP_ERROR_FUNCTION_NOT_SUPPORTED: snprintf(buffer, sizeof(buffer), "%s: function not supported", arg); break; case IP_ERROR_FILE_FORMAT: snprintf(buffer, sizeof(buffer), "%s: file format not supported or corrupted file", arg); break; case IP_ERROR_INVALID_URI: snprintf(buffer, sizeof(buffer), "%s: invalid URI", arg); break; case IP_ERROR_SAMPLE_FORMAT: snprintf(buffer, sizeof(buffer), "%s: input plugin doesn't support the sample format", arg); break; case IP_ERROR_WRONG_DISC: snprintf(buffer, sizeof(buffer), "%s: wrong disc inserted, aborting!", arg); break; case IP_ERROR_NO_DISC: snprintf(buffer, sizeof(buffer), "%s: could not read disc", arg); break; case IP_ERROR_HTTP_RESPONSE: snprintf(buffer, sizeof(buffer), "%s: invalid HTTP response", arg); break; case IP_ERROR_HTTP_STATUS: snprintf(buffer, sizeof(buffer), "%s: %d %s", arg, ip->http_code, ip->http_reason); free(ip->http_reason); ip->http_reason = NULL; ip->http_code = -1; break; case IP_ERROR_HTTP_REDIRECT_LIMIT: snprintf(buffer, sizeof(buffer), "%s: too many HTTP redirections", arg); break; case IP_ERROR_NOT_OPTION: snprintf(buffer, sizeof(buffer), "%s: no such option", arg); break; case IP_ERROR_INTERNAL: snprintf(buffer, sizeof(buffer), "%s: internal error", arg); break; case IP_ERROR_SUCCESS: default: snprintf(buffer, sizeof(buffer), "%s: this is not an error (%d), this is a bug", arg, rc); break; } return xstrdup(buffer); } char **ip_get_supported_extensions(void) { struct ip *ip; char **exts; int i, size; int count = 0; size = 8; exts = xnew(char *, size); ip_rdlock(); list_for_each_entry(ip, &ip_head, node) { const char * const *e = ip->extensions; for (i = 0; e[i]; i++) { if (count == size - 1) { size *= 2; exts = xrenew(char *, exts, size); } exts[count++] = xstrdup(e[i]); } } ip_unlock(); exts[count] = NULL; qsort(exts, count, sizeof(char *), strptrcmp); return exts; } void ip_dump_plugins(void) { struct ip *ip; int i; printf("Input Plugins: %s\n", plugin_dir); ip_rdlock(); list_for_each_entry(ip, &ip_head, node) { printf(" %s:\n Default Priority: %d\n File Types:", ip->name, ip->priority); for (i = 0; ip->extensions[i]; i++) printf(" %s", ip->extensions[i]); printf("\n MIME Types:"); for (i = 0; ip->mime_types[i]; i++) printf(" %s", ip->mime_types[i]); printf("\n"); } ip_unlock(); }