/* * Copyright 2008-2013 Various Authors * Copyright 2004 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 "cache.h" #include "misc.h" #include "file.h" #include "input.h" #include "track_info.h" #include "utils.h" #include "xmalloc.h" #include "xstrjoin.h" #include "gbuf.h" #include "options.h" #include "pl_env.h" #include #include #include #include #include #include #include #include #include #include #define CACHE_VERSION 0x0d #define CACHE_64_BIT 0x01 #define CACHE_BE 0x02 #define CACHE_RESERVED_PATTERN 0xff #define CACHE_ENTRY_USED_SIZE 28 #define CACHE_ENTRY_RESERVED_SIZE 52 #define CACHE_ENTRY_TOTAL_SIZE (CACHE_ENTRY_RESERVED_SIZE + CACHE_ENTRY_USED_SIZE) // Cmus Track Cache version X + 4 bytes flags static char cache_header[8] CMUS_NONSTRING = "CTC\0\0\0\0\0"; // host byte order // mtime is either 32 or 64 bits struct cache_entry { // size of this struct including size itself uint32_t size; int32_t play_count; int64_t mtime; int32_t duration; int32_t bitrate; int32_t bpm; // when introducing new fields decrease the reserved space accordingly uint8_t _reserved[CACHE_ENTRY_RESERVED_SIZE]; // filename, codec, codec_profile and N * (key, val) char strings[]; }; // make sure our mmap/sizeof-based code works STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == sizeof(struct cache_entry)); STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == offsetof(struct cache_entry, strings)); #define ALIGN(size) (((size) + sizeof(long) - 1) & ~(sizeof(long) - 1)) #define HASH_SIZE 1023 static struct track_info *hash_table[HASH_SIZE]; static char *cache_filename; static int total; struct fifo_mutex cache_mutex = FIFO_MUTEX_INITIALIZER; static void add_ti(struct track_info *ti, unsigned int hash) { unsigned int pos = hash % HASH_SIZE; struct track_info *next = hash_table[pos]; ti->next = next; hash_table[pos] = ti; total++; } static int valid_cache_entry(const struct cache_entry *e, unsigned int avail) { unsigned int min_size = sizeof(*e); unsigned int str_size; int i, count; if (avail < min_size) return 0; if (e->size < min_size || e->size > avail) return 0; str_size = e->size - min_size; count = 0; for (i = 0; i < str_size; i++) { if (!e->strings[i]) count++; } if (count % 2 == 0) return 0; if (e->strings[str_size - 1]) return 0; return 1; } static struct track_info *cache_entry_to_ti(struct cache_entry *e) { const char *strings = e->strings; struct track_info *ti; struct keyval *kv; int str_size = e->size - sizeof(*e); int pos, i, count; char *proc_filename; if (pl_env_var(strings, NULL) && (proc_filename = pl_env_expand(strings))) { ti = track_info_new(proc_filename); free(proc_filename); } else { ti = track_info_new(strings); } ti->duration = e->duration; ti->bitrate = e->bitrate; ti->mtime = e->mtime; ti->play_count = e->play_count; ti->bpm = e->bpm; // count strings (filename + codec + codec_profile + key/val pairs) count = 0; for (i = 0; i < str_size; i++) { if (!strings[i]) count++; } count = (count - 3) / 2; // NOTE: filename already copied by track_info_new() pos = strlen(strings) + 1; ti->codec = strings[pos] ? xstrdup(strings + pos) : NULL; pos += strlen(strings + pos) + 1; ti->codec_profile = strings[pos] ? xstrdup(strings + pos) : NULL; pos += strlen(strings + pos) + 1; kv = xnew(struct keyval, count + 1); for (i = 0; i < count; i++) { int size; size = strlen(strings + pos) + 1; kv[i].key = xstrdup(strings + pos); pos += size; size = strlen(strings + pos) + 1; kv[i].val = xstrdup(strings + pos); pos += size; } kv[i].key = NULL; kv[i].val = NULL; track_info_set_comments(ti, kv); return ti; } struct track_info *lookup_cache_entry(const char *filename, unsigned int hash) { struct track_info *ti = hash_table[hash % HASH_SIZE]; while (ti) { if (!strcmp(filename, ti->filename)) return ti; ti = ti->next; } return NULL; } static void do_cache_remove_ti(struct track_info *ti, unsigned int hash) { unsigned int pos = hash % HASH_SIZE; struct track_info *t = hash_table[pos]; struct track_info *next, *prev = NULL; while (t) { next = t->next; if (t == ti) { if (prev) { prev->next = next; } else { hash_table[pos] = next; } total--; track_info_unref(ti); return; } prev = t; t = next; } } void cache_remove_ti(struct track_info *ti) { do_cache_remove_ti(ti, hash_str(ti->filename)); } static int read_cache(void) { unsigned int size, offset = 0; struct stat st = {}; char *buf; int fd; fd = open(cache_filename, O_RDONLY); if (fd < 0) { if (errno == ENOENT) return 0; return -1; } fstat(fd, &st); if (st.st_size < sizeof(cache_header)) goto close; size = st.st_size; buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (buf == MAP_FAILED) { close(fd); return -1; } if (memcmp(buf, cache_header, sizeof(cache_header))) goto corrupt; offset = sizeof(cache_header); while (offset < size) { struct cache_entry *e = (void *)(buf + offset); struct track_info *ti; if (!valid_cache_entry(e, size - offset)) goto corrupt; ti = cache_entry_to_ti(e); add_ti(ti, hash_str(ti->filename)); offset += ALIGN(e->size); } munmap(buf, size); close(fd); return 0; corrupt: munmap(buf, size); close: close(fd); // corrupt return -2; } int cache_init(void) { unsigned int flags = 0; #ifdef WORDS_BIGENDIAN flags |= CACHE_BE; #endif if (sizeof(long) == 8) flags |= CACHE_64_BIT; cache_header[7] = flags & 0xff; flags >>= 8; cache_header[6] = flags & 0xff; flags >>= 8; cache_header[5] = flags & 0xff; flags >>= 8; cache_header[4] = flags & 0xff; /* assumed version */ cache_header[3] = CACHE_VERSION; cache_filename = xstrjoin(cmus_config_dir, "/cache"); return read_cache(); } static int ti_filename_cmp(const void *a, const void *b) { const struct track_info *ai = *(const struct track_info **)a; const struct track_info *bi = *(const struct track_info **)b; return strcmp(ai->filename, bi->filename); } static struct track_info **get_track_infos(bool reference) { struct track_info **tis; int i, c; tis = xnew(struct track_info *, total); c = 0; for (i = 0; i < HASH_SIZE; i++) { struct track_info *ti = hash_table[i]; while (ti) { if (reference) track_info_ref(ti); tis[c++] = ti; ti = ti->next; } } qsort(tis, total, sizeof(struct track_info *), ti_filename_cmp); return tis; } static void flush_buffer(int fd, struct gbuf *buf) { if (buf->len) { write_all(fd, buf->buffer, buf->len); gbuf_clear(buf); } } static void write_ti(int fd, struct gbuf *buf, struct track_info *ti, unsigned int *offsetp) { char *proc_filename = pl_env_reduce(ti->filename); const struct keyval *kv = ti->comments; unsigned int offset = *offsetp; unsigned int pad; struct cache_entry e; int *len, alloc = 64, count, i; memset(e._reserved, CACHE_RESERVED_PATTERN, sizeof(e._reserved)); count = 0; len = xnew(int, alloc); e.size = sizeof(e); e.duration = ti->duration; e.bitrate = ti->bitrate; e.mtime = ti->mtime; e.play_count = ti->play_count; e.bpm = ti->bpm; len[count] = strlen(proc_filename) + 1; e.size += len[count++]; len[count] = (ti->codec ? strlen(ti->codec) : 0) + 1; e.size += len[count++]; len[count] = (ti->codec_profile ? strlen(ti->codec_profile) : 0) + 1; e.size += len[count++]; for (i = 0; kv[i].key; i++) { if (count + 2 > alloc) { alloc *= 2; len = xrenew(int, len, alloc); } len[count] = strlen(kv[i].key) + 1; e.size += len[count++]; len[count] = strlen(kv[i].val) + 1; e.size += len[count++]; } pad = ALIGN(offset) - offset; if (gbuf_avail(buf) < pad + e.size) flush_buffer(fd, buf); count = 0; if (pad) gbuf_set(buf, 0, pad); gbuf_add_bytes(buf, &e, sizeof(e)); gbuf_add_bytes(buf, proc_filename, len[count++]); gbuf_add_bytes(buf, ti->codec ? ti->codec : "", len[count++]); gbuf_add_bytes(buf, ti->codec_profile ? ti->codec_profile : "", len[count++]); for (i = 0; kv[i].key; i++) { gbuf_add_bytes(buf, kv[i].key, len[count++]); gbuf_add_bytes(buf, kv[i].val, len[count++]); } free(len); *offsetp = offset + pad + e.size; free(proc_filename); } int cache_close(void) { GBUF(buf); struct track_info **tis; unsigned int offset; int i, fd, rc; char *tmp; tmp = xstrjoin(cmus_config_dir, "/cache.tmp"); fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd < 0) { free(tmp); return -1; } tis = get_track_infos(false); gbuf_grow(&buf, 64 * 1024 - 1); gbuf_add_bytes(&buf, cache_header, sizeof(cache_header)); offset = sizeof(cache_header); for (i = 0; i < total; i++) write_ti(fd, &buf, tis[i], &offset); flush_buffer(fd, &buf); gbuf_free(&buf); free(tis); close(fd); rc = rename(tmp, cache_filename); free(tmp); return rc; } static struct track_info *ip_get_ti(const char *filename) { struct track_info *ti = NULL; struct input_plugin *ip; struct keyval *comments; int rc; ip = ip_new(filename); rc = ip_open(ip); if (rc) { ip_delete(ip); return NULL; } rc = ip_read_comments(ip, &comments); if (!rc) { ti = track_info_new(filename); track_info_set_comments(ti, comments); ti->duration = ip_duration(ip); ti->bitrate = ip_bitrate(ip); ti->codec = ip_codec(ip); ti->codec_profile = ip_codec_profile(ip); ti->mtime = ip_is_remote(ip) ? -1 : file_get_mtime(filename); } ip_delete(ip); return ti; } struct track_info *cache_get_ti(const char *filename, int force) { unsigned int hash = hash_str(filename); struct track_info *ti; int reload = 0; if (pl_env_var(filename, NULL)) { struct growing_keyvals c = {NULL, 0, 0}; keyvals_terminate(&c); ti = track_info_new(filename); ti->duration = 0; track_info_set_comments(ti, c.keyvals); track_info_ref(ti); return ti; } ti = lookup_cache_entry(filename, hash); if (ti) { if ((!skip_track_info && ti->duration == 0 && !is_http_url(filename)) || force){ do_cache_remove_ti(ti, hash); ti = NULL; reload = 1; } } if (!ti) { if (skip_track_info && !reload && !force) { struct growing_keyvals c = {NULL, 0, 0}; ti = track_info_new(filename); keyvals_terminate(&c); track_info_set_comments(ti, c.keyvals); ti->duration = 0; } else { ti = ip_get_ti(filename); } if (!ti) return NULL; add_ti(ti, hash); } track_info_ref(ti); return ti; } struct track_info **cache_refresh(int *count, int force) { struct track_info **tis = get_track_infos(true); int i, n = total; for (i = 0; i < n; i++) { unsigned int hash; struct track_info *ti = tis[i]; struct stat st; int rc = 0; cache_yield(); /* * If no-one else has reference to tis[i] then it is set to NULL * otherwise: * * unchanged: tis[i] = NULL * deleted: tis[i]->next = NULL * changed: tis[i]->next = new */ if (!is_url(ti->filename)) { rc = stat(ti->filename, &st); if (!rc && !force && ti->mtime == st.st_mtime) { // unchanged track_info_unref(ti); tis[i] = NULL; continue; } } hash = hash_str(ti->filename); do_cache_remove_ti(ti, hash); if (!rc) { // changed struct track_info *new_ti; // clear cache-only entries if (force && track_info_unique_ref(ti)) { track_info_unref(ti); tis[i] = NULL; continue; } new_ti = ip_get_ti(ti->filename); if (new_ti) { add_ti(new_ti, hash); if (track_info_unique_ref(ti)) { track_info_unref(ti); tis[i] = NULL; } else { track_info_ref(new_ti); ti->next = new_ti; } continue; } // treat as deleted } // deleted if (track_info_unique_ref(ti)) { track_info_unref(ti); tis[i] = NULL; } else { ti->next = NULL; } } *count = n; return tis; }