mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-11-02 23:11:20 +01:00
577 lines
20 KiB
C
577 lines
20 KiB
C
/*
|
|
* mime-cache.c
|
|
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
|
*
|
|
* Distributed under terms of the GPL3 license.
|
|
*/
|
|
|
|
// Fast lookup of MIME types for file names using the binary mime.cache files
|
|
// generated by update-mime-database from shared-mime-info. The lookup
|
|
// algorithm mirrors the one used by xdgmime (GLib/GIO). See
|
|
// https://specifications.freedesktop.org/shared-mime-info-spec/latest/ar01s02.html
|
|
|
|
#include "data-types.h"
|
|
#include "monotonic.h"
|
|
|
|
#ifdef __APPLE__
|
|
|
|
static PyObject *
|
|
mime_type_for_filename(PyObject *self UNUSED, PyObject *name UNUSED) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *
|
|
mime_type_aliases(PyObject *self UNUSED, PyObject *mime) {
|
|
if (!PyUnicode_Check(mime)) {
|
|
PyErr_SetString(PyExc_TypeError, "mime must be a string");
|
|
return NULL;
|
|
}
|
|
return PyTuple_Pack(1, mime);
|
|
}
|
|
|
|
static PyObject *
|
|
clear_mime_cache_data(PyObject *self UNUSED, PyObject *args UNUSED) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
#else
|
|
|
|
#include <fcntl.h>
|
|
#include <fnmatch.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include "safe-wrappers.h"
|
|
|
|
#define CASE_SENSITIVE_FLAG 0x100
|
|
#define WEIGHT_MASK 0xff
|
|
#define MAX_MATCHES 16
|
|
#define MAX_SUFFIX_TREE_DEPTH 256
|
|
#define HEADER_SIZE 40
|
|
#define ALIAS_LIST_OFFSET_POS 4
|
|
#define LITERAL_LIST_OFFSET_POS 12
|
|
#define REVERSE_SUFFIX_TREE_OFFSET_POS 16
|
|
#define GLOB_LIST_OFFSET_POS 20
|
|
|
|
typedef struct {
|
|
char *path;
|
|
const uint8_t *data;
|
|
size_t sz;
|
|
bool exists;
|
|
dev_t dev;
|
|
ino_t ino;
|
|
off_t size;
|
|
struct timespec mtime;
|
|
} MimeCache;
|
|
|
|
typedef struct {
|
|
const char *mime;
|
|
uint32_t weight;
|
|
} Match;
|
|
|
|
typedef struct {
|
|
Match items[MAX_MATCHES];
|
|
unsigned count;
|
|
} Matches;
|
|
|
|
static struct {
|
|
MimeCache *items;
|
|
size_t count;
|
|
bool loaded;
|
|
monotonic_t last_check_at;
|
|
} caches = {0};
|
|
|
|
// Reading data {{{
|
|
static inline bool
|
|
read_u32(const MimeCache *c, size_t offset, uint32_t *ans) {
|
|
if (offset > c->sz || c->sz - offset < 4) return false;
|
|
const uint8_t *p = c->data + offset;
|
|
*ans = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
|
return true;
|
|
}
|
|
|
|
static inline const char *
|
|
read_string(const MimeCache *c, uint32_t offset) {
|
|
if (offset >= c->sz || !memchr(c->data + offset, 0, c->sz - offset)) return NULL;
|
|
return (const char *)c->data + offset;
|
|
}
|
|
|
|
static inline bool
|
|
read_entry(const MimeCache *c, size_t offset, uint32_t *a, uint32_t *b, uint32_t *weight) {
|
|
return read_u32(c, offset, a) && read_u32(c, offset + 4, b) && read_u32(c, offset + 8, weight);
|
|
}
|
|
|
|
static void
|
|
add_match(const MimeCache *c, Matches *m, uint32_t mime_offset, uint32_t weight) {
|
|
if (m->count >= MAX_MATCHES) return;
|
|
const char *mime = read_string(c, mime_offset);
|
|
if (!mime) return;
|
|
m->items[m->count].mime = mime;
|
|
m->items[m->count++].weight = weight & WEIGHT_MASK;
|
|
}
|
|
// }}}
|
|
|
|
// Lookups {{{
|
|
static bool
|
|
lookup_literal(const MimeCache *c, const char *name, bool case_sensitive_check, Matches *ans) {
|
|
uint32_t list_offset, n;
|
|
if (!read_u32(c, LITERAL_LIST_OFFSET_POS, &list_offset) || !read_u32(c, list_offset, &n)) return false;
|
|
size_t lo = 0, hi = n;
|
|
while (lo < hi) {
|
|
size_t mid = lo + (hi - lo) / 2;
|
|
uint32_t literal_offset, mime_offset, weight;
|
|
if (!read_entry(c, (size_t)list_offset + 4 + 12 * mid, &literal_offset, &mime_offset, &weight)) return false;
|
|
const char *literal = read_string(c, literal_offset);
|
|
if (!literal) return false;
|
|
int cmp = strcmp(literal, name);
|
|
if (cmp < 0) lo = mid + 1;
|
|
else if (cmp > 0) hi = mid;
|
|
else {
|
|
if (!case_sensitive_check && (weight & CASE_SENSITIVE_FLAG)) return false;
|
|
unsigned before = ans->count;
|
|
add_match(c, ans, mime_offset, weight);
|
|
return ans->count > before;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
lookup_suffix_node(
|
|
const MimeCache *c, uint32_t num_nodes, uint32_t offset, const char_type *name, size_t len, bool case_sensitive_check, Matches *ans, unsigned depth) {
|
|
if (!len || depth > MAX_SUFFIX_TREE_DEPTH) return;
|
|
const char_type ch = name[len - 1];
|
|
size_t lo = 0, hi = num_nodes;
|
|
while (lo < hi) {
|
|
size_t mid = lo + (hi - lo) / 2;
|
|
uint32_t node_char, num_children, first_child_offset;
|
|
if (!read_entry(c, (size_t)offset + 12 * mid, &node_char, &num_children, &first_child_offset)) return;
|
|
if (ch < node_char) hi = mid;
|
|
else if (ch > node_char) lo = mid + 1;
|
|
else {
|
|
const unsigned before = ans->count;
|
|
lookup_suffix_node(c, num_children, first_child_offset, name, len - 1, case_sensitive_check, ans, depth + 1);
|
|
if (ans->count == before) {
|
|
// No longer suffix matched, use the leaf nodes of this node,
|
|
// which are sorted before all other children.
|
|
for (uint32_t i = 0; i < num_children; i++) {
|
|
uint32_t leaf_char, mime_offset, weight;
|
|
if (!read_entry(c, (size_t)first_child_offset + 12 * (size_t)i, &leaf_char, &mime_offset, &weight) || leaf_char) break;
|
|
if (case_sensitive_check || !(weight & CASE_SENSITIVE_FLAG)) add_match(c, ans, mime_offset, weight);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
lookup_suffix(const MimeCache *c, const char_type *name, size_t len, bool case_sensitive_check, Matches *ans) {
|
|
uint32_t tree_offset, num_roots, first_root_offset;
|
|
if (!read_u32(c, REVERSE_SUFFIX_TREE_OFFSET_POS, &tree_offset) || !read_u32(c, tree_offset, &num_roots) ||
|
|
!read_u32(c, (size_t)tree_offset + 4, &first_root_offset))
|
|
return;
|
|
lookup_suffix_node(c, num_roots, first_root_offset, name, len, case_sensitive_check, ans, 0);
|
|
}
|
|
|
|
static void
|
|
lookup_glob(const MimeCache *c, const char *name, bool case_sensitive_check, Matches *ans) {
|
|
uint32_t list_offset, n;
|
|
if (!read_u32(c, GLOB_LIST_OFFSET_POS, &list_offset) || !read_u32(c, list_offset, &n)) return;
|
|
for (size_t i = 0; i < n && ans->count < MAX_MATCHES; i++) {
|
|
uint32_t glob_offset, mime_offset, weight;
|
|
if (!read_entry(c, (size_t)list_offset + 4 + 12 * i, &glob_offset, &mime_offset, &weight)) return;
|
|
if (!case_sensitive_check && (weight & CASE_SENSITIVE_FLAG)) continue;
|
|
const char *glob = read_string(c, glob_offset);
|
|
if (glob && fnmatch(glob, name, 0) == 0) add_match(c, ans, mime_offset, weight);
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
const char *utf8;
|
|
const char_type *ucs4;
|
|
size_t ucs4_len;
|
|
} FileName;
|
|
|
|
static const char *
|
|
lookup(const FileName *name, const FileName *lowercased) {
|
|
Matches m = {0};
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data && lookup_literal(caches.items + i, name->utf8, true, &m)) return m.items[0].mime;
|
|
}
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data && lookup_literal(caches.items + i, lowercased->utf8, false, &m)) return m.items[0].mime;
|
|
}
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data) lookup_suffix(caches.items + i, lowercased->ucs4, lowercased->ucs4_len, false, &m);
|
|
}
|
|
if (m.count < 2) {
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data) lookup_suffix(caches.items + i, name->ucs4, name->ucs4_len, true, &m);
|
|
}
|
|
}
|
|
if (!m.count) {
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data) lookup_glob(caches.items + i, name->utf8, true, &m);
|
|
}
|
|
}
|
|
if (!m.count) {
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data) lookup_glob(caches.items + i, lowercased->utf8, false, &m);
|
|
}
|
|
}
|
|
if (!m.count) return NULL;
|
|
// Highest weight wins, ties go to the earliest match, which means the
|
|
// longest suffix and the highest priority cache file.
|
|
const Match *best = m.items;
|
|
for (unsigned i = 1; i < m.count; i++) {
|
|
if (m.items[i].weight > best->weight) best = m.items + i;
|
|
}
|
|
return best->mime;
|
|
}
|
|
|
|
static bool
|
|
read_alias_list(const MimeCache *c, uint32_t *list_offset, uint32_t *n) {
|
|
return read_u32(c, ALIAS_LIST_OFFSET_POS, list_offset) && read_u32(c, *list_offset, n);
|
|
}
|
|
|
|
static bool
|
|
read_alias_entry(const MimeCache *c, uint32_t list_offset, size_t idx, const char **alias, const char **mime) {
|
|
uint32_t alias_offset, mime_offset;
|
|
const size_t offset = (size_t)list_offset + 4 + 8 * idx;
|
|
if (!read_u32(c, offset, &alias_offset) || !read_u32(c, offset + 4, &mime_offset)) return false;
|
|
*alias = read_string(c, alias_offset);
|
|
*mime = read_string(c, mime_offset);
|
|
return *alias && *mime;
|
|
}
|
|
|
|
// Returns the canonical MIME type for the specified alias or NULL if mime is
|
|
// not an alias. The alias list is sorted by alias.
|
|
static const char *
|
|
canonical_mime_type(const MimeCache *c, const char *mime) {
|
|
uint32_t list_offset, n;
|
|
if (!read_alias_list(c, &list_offset, &n)) return NULL;
|
|
size_t lo = 0, hi = n;
|
|
while (lo < hi) {
|
|
size_t mid = lo + (hi - lo) / 2;
|
|
const char *alias, *canonical;
|
|
if (!read_alias_entry(c, list_offset, mid, &alias, &canonical)) return NULL;
|
|
int cmp = strcmp(alias, mime);
|
|
if (cmp < 0) lo = mid + 1;
|
|
else if (cmp > 0) hi = mid;
|
|
else return canonical;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Corrupt cache files can contain strings that are not valid UTF-8. These are
|
|
// treated as missing: NULL is returned without setting an exception.
|
|
static PyObject *
|
|
string_from_cache(const char *val) {
|
|
PyObject *ans = PyUnicode_DecodeUTF8(val, (Py_ssize_t)strlen(val), NULL);
|
|
if (!ans && PyErr_ExceptionMatches(PyExc_UnicodeDecodeError)) PyErr_Clear();
|
|
return ans;
|
|
}
|
|
|
|
static bool
|
|
add_unique_string(PyObject *list, const char *val) {
|
|
RAII_PyObject(s, string_from_cache(val));
|
|
if (!s) return !PyErr_Occurred();
|
|
int ret = PySequence_Contains(list, s);
|
|
if (ret < 0) return false;
|
|
return ret == 1 || PyList_Append(list, s) == 0;
|
|
}
|
|
|
|
static bool
|
|
add_aliases(const MimeCache *c, const char *canonical, PyObject *ans) {
|
|
uint32_t list_offset, n;
|
|
if (!read_alias_list(c, &list_offset, &n)) return true;
|
|
for (size_t i = 0; i < n; i++) {
|
|
const char *alias, *mime;
|
|
if (!read_alias_entry(c, list_offset, i, &alias, &mime)) break;
|
|
if (strcmp(mime, canonical) == 0 && !add_unique_string(ans, alias)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
// }}}
|
|
|
|
// Loading {{{
|
|
static void
|
|
unmap_cache(MimeCache *c) {
|
|
if (c->data) munmap((void *)c->data, c->sz);
|
|
c->data = NULL;
|
|
c->sz = 0;
|
|
}
|
|
|
|
static bool
|
|
stat_cache(const MimeCache *c, struct stat *st) {
|
|
return stat(c->path, st) == 0 && S_ISREG(st->st_mode);
|
|
}
|
|
|
|
static bool
|
|
cache_changed(const MimeCache *c) {
|
|
struct stat st;
|
|
if (!stat_cache(c, &st)) return c->exists;
|
|
return !c->exists || st.st_dev != c->dev || st.st_ino != c->ino || st.st_size != c->size || st.st_mtim.tv_sec != c->mtime.tv_sec ||
|
|
st.st_mtim.tv_nsec != c->mtime.tv_nsec;
|
|
}
|
|
|
|
// Note for security reviews: if a mapped cache file is truncated in place,
|
|
// accessing the mapped pages beyond the new end of file raises SIGBUS. This
|
|
// is deliberately not handled. update-mime-database replaces cache files
|
|
// atomically via rename(), so the mapped data stays valid, and truncating them
|
|
// in place requires write access to the XDG data directories. GLib (xdgmime)
|
|
// and Qt (QMimeBinaryProvider) mmap these files in exactly the same way,
|
|
// without handling SIGBUS. All reads from the mapped data are bounds checked,
|
|
// so corrupt or malicious cache file contents are safe.
|
|
static void
|
|
map_cache(MimeCache *c) {
|
|
unmap_cache(c);
|
|
c->exists = false;
|
|
int fd = safe_open(c->path, O_RDONLY | O_CLOEXEC, 0);
|
|
if (fd < 0) {
|
|
if (errno != ENOENT && errno != ENOTDIR) log_error("Failed to open MIME cache file %s with error: %s", c->path, strerror(errno));
|
|
return;
|
|
}
|
|
struct stat st;
|
|
if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
return;
|
|
}
|
|
c->exists = true;
|
|
c->dev = st.st_dev;
|
|
c->ino = st.st_ino;
|
|
c->size = st.st_size;
|
|
c->mtime = st.st_mtim;
|
|
if (st.st_size < HEADER_SIZE) {
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
log_error("The MIME cache file %s is too small, ignoring it", c->path);
|
|
return;
|
|
}
|
|
void *addr = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
if (addr == MAP_FAILED) {
|
|
log_error("Failed to mmap MIME cache file %s with error: %s", c->path, strerror(errno));
|
|
return;
|
|
}
|
|
const uint8_t *p = addr;
|
|
const unsigned major = (p[0] << 8) | p[1], minor = (p[2] << 8) | p[3];
|
|
if (major != 1 || minor < 1) {
|
|
log_error("The MIME cache file %s has unsupported version %u.%u, ignoring it", c->path, major, minor);
|
|
munmap(addr, (size_t)st.st_size);
|
|
return;
|
|
}
|
|
c->data = p;
|
|
c->sz = (size_t)st.st_size;
|
|
}
|
|
|
|
static void
|
|
free_caches(void) {
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
unmap_cache(caches.items + i);
|
|
free(caches.items[i].path);
|
|
}
|
|
free(caches.items);
|
|
zero_at_ptr(&caches);
|
|
}
|
|
|
|
static bool
|
|
add_cache_path(const char *data_dir, size_t len) {
|
|
if (!len || data_dir[0] != '/') return true; // relative paths are ignored as per the XDG spec
|
|
static const char suffix[] = "/mime/mime.cache";
|
|
char *path = malloc(len + sizeof(suffix));
|
|
if (!path) return false;
|
|
memcpy(path, data_dir, len);
|
|
memcpy(path + len, suffix, sizeof(suffix));
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (strcmp(caches.items[i].path, path) == 0) {
|
|
free(path);
|
|
return true;
|
|
}
|
|
}
|
|
MimeCache *items = realloc(caches.items, sizeof(caches.items[0]) * (caches.count + 1));
|
|
if (!items) {
|
|
free(path);
|
|
return false;
|
|
}
|
|
caches.items = items;
|
|
zero_at_ptr(caches.items + caches.count);
|
|
caches.items[caches.count++].path = path;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
find_cache_paths(void) {
|
|
// Cache files are in order of decreasing priority
|
|
const char *data_home = getenv("XDG_DATA_HOME");
|
|
if (data_home && data_home[0]) {
|
|
if (!add_cache_path(data_home, strlen(data_home))) return false;
|
|
} else {
|
|
const char *home = getenv("HOME");
|
|
if (home && home[0]) {
|
|
static const char rel[] = "/.local/share";
|
|
const size_t hl = strlen(home);
|
|
RAII_ALLOC(char, buf, malloc(hl + sizeof(rel)));
|
|
if (!buf) return false;
|
|
memcpy(buf, home, hl);
|
|
memcpy(buf + hl, rel, sizeof(rel));
|
|
if (!add_cache_path(buf, hl + sizeof(rel) - 1)) return false;
|
|
}
|
|
}
|
|
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (!data_dirs || !data_dirs[0]) data_dirs = "/usr/local/share/:/usr/share/";
|
|
for (const char *p = data_dirs; *p;) {
|
|
const char *end = strchr(p, ':');
|
|
size_t len = end ? (size_t)(end - p) : strlen(p);
|
|
while (len > 1 && p[len - 1] == '/') len--;
|
|
if (!add_cache_path(p, len)) return false;
|
|
if (!end) break;
|
|
p = end + 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
ensure_caches_loaded(void) {
|
|
const monotonic_t now = monotonic();
|
|
if (caches.loaded) {
|
|
// Check for updated cache files at most once every few seconds, like xdgmime
|
|
if (now - caches.last_check_at < s_to_monotonic_t(5)) return true;
|
|
caches.last_check_at = now;
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (cache_changed(caches.items + i)) map_cache(caches.items + i);
|
|
}
|
|
return true;
|
|
}
|
|
free_caches();
|
|
if (!find_cache_paths()) {
|
|
free_caches();
|
|
PyErr_NoMemory();
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < caches.count; i++) map_cache(caches.items + i);
|
|
caches.loaded = true;
|
|
caches.last_check_at = now;
|
|
return true;
|
|
}
|
|
// }}}
|
|
|
|
// Python API {{{
|
|
static bool
|
|
filename_from_python(PyObject *name, FileName *ans, PyObject **utf8_holder, Py_UCS4 **ucs4_holder) {
|
|
// Use surrogateescape so that file names that are not valid UTF-8 round trip to their original bytes
|
|
// Lone surrogates outside the surrogateescape range cannot be encoded and
|
|
// cannot come from a real file name, so they are treated as not matching:
|
|
// false is returned without setting an exception.
|
|
*utf8_holder = PyUnicode_AsEncodedString(name, "utf-8", "surrogateescape");
|
|
if (!*utf8_holder) {
|
|
if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) PyErr_Clear();
|
|
return false;
|
|
}
|
|
*ucs4_holder = PyUnicode_AsUCS4Copy(name);
|
|
if (!*ucs4_holder) return false;
|
|
ans->utf8 = PyBytes_AS_STRING(*utf8_holder);
|
|
ans->ucs4 = *ucs4_holder;
|
|
ans->ucs4_len = (size_t)PyUnicode_GET_LENGTH(name);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
cleanup_ucs4(Py_UCS4 **p) {
|
|
PyMem_Free(*p);
|
|
*p = NULL;
|
|
}
|
|
#define RAII_UCS4(name) __attribute__((cleanup(cleanup_ucs4))) Py_UCS4 *name = NULL
|
|
|
|
static PyObject *
|
|
mime_type_for_filename(PyObject *self UNUSED, PyObject *path) {
|
|
if (!PyUnicode_Check(path)) {
|
|
PyErr_SetString(PyExc_TypeError, "path must be a string");
|
|
return NULL;
|
|
}
|
|
const Py_ssize_t path_len = PyUnicode_GET_LENGTH(path);
|
|
const Py_ssize_t slash = PyUnicode_FindChar(path, '/', 0, path_len, -1);
|
|
if (slash == -2) return NULL;
|
|
RAII_PyObject(name, PyUnicode_Substring(path, slash + 1, path_len));
|
|
if (!name) return NULL;
|
|
const Py_ssize_t name_len = PyUnicode_GET_LENGTH(name);
|
|
if (!name_len) Py_RETURN_NONE;
|
|
const Py_ssize_t nul = PyUnicode_FindChar(name, 0, 0, name_len, 1);
|
|
if (nul == -2) return NULL;
|
|
if (nul != -1) Py_RETURN_NONE; // embedded NUL cannot be part of a valid file name
|
|
RAII_PyObject(lowercased, PyObject_CallMethod(name, "lower", NULL));
|
|
if (!lowercased) return NULL;
|
|
if (!ensure_caches_loaded()) return NULL;
|
|
if (!caches.count) Py_RETURN_NONE;
|
|
RAII_PyObject(utf8, NULL);
|
|
RAII_PyObject(lutf8, NULL);
|
|
RAII_UCS4(ucs4);
|
|
RAII_UCS4(lucs4);
|
|
FileName fname, lname;
|
|
if (!filename_from_python(name, &fname, &utf8, &ucs4) || !filename_from_python(lowercased, &lname, &lutf8, &lucs4)) {
|
|
if (PyErr_Occurred()) return NULL;
|
|
Py_RETURN_NONE;
|
|
}
|
|
const char *ans = lookup(&fname, &lname);
|
|
if (!ans) Py_RETURN_NONE;
|
|
PyObject *ret = string_from_cache(ans);
|
|
if (!ret && !PyErr_Occurred()) Py_RETURN_NONE;
|
|
return ret;
|
|
}
|
|
|
|
static PyObject *
|
|
mime_type_aliases(PyObject *self UNUSED, PyObject *mime) {
|
|
if (!PyUnicode_Check(mime)) {
|
|
PyErr_SetString(PyExc_TypeError, "mime must be a string");
|
|
return NULL;
|
|
}
|
|
const char *q = PyUnicode_AsUTF8(mime);
|
|
if (!q) {
|
|
// Lone surrogates cannot be encoded and so cannot match anything in the cache
|
|
if (!PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) return NULL;
|
|
PyErr_Clear();
|
|
return PyTuple_Pack(1, mime);
|
|
}
|
|
if (!ensure_caches_loaded()) return NULL;
|
|
const char *canonical = NULL;
|
|
for (size_t i = 0; i < caches.count && !canonical; i++) {
|
|
if (caches.items[i].data) canonical = canonical_mime_type(caches.items + i, q);
|
|
}
|
|
if (!canonical) canonical = q;
|
|
RAII_PyObject(ans, PyList_New(0));
|
|
if (!ans || !add_unique_string(ans, canonical)) return NULL;
|
|
for (size_t i = 0; i < caches.count; i++) {
|
|
if (caches.items[i].data && !add_aliases(caches.items + i, canonical, ans)) return NULL;
|
|
}
|
|
// Ensure the queried name is always present, even if a cache entry is corrupt
|
|
if (!add_unique_string(ans, q)) return NULL;
|
|
return PyList_AsTuple(ans);
|
|
}
|
|
|
|
static PyObject *
|
|
clear_mime_cache_data(PyObject *self UNUSED, PyObject *args UNUSED) {
|
|
free_caches();
|
|
Py_RETURN_NONE;
|
|
}
|
|
// }}}
|
|
|
|
#endif
|
|
|
|
static PyMethodDef methods[] = {
|
|
{"mime_type_for_filename",
|
|
mime_type_for_filename,
|
|
METH_O,
|
|
"Return the MIME type for the specified file name based on the shared-mime-info mime.cache files or None if no match is found"},
|
|
{"mime_type_aliases",
|
|
mime_type_aliases,
|
|
METH_O,
|
|
"Return all names for the specified MIME type, starting with its canonical name, followed by all its aliases"},
|
|
{"clear_mime_cache_data", clear_mime_cache_data, METH_NOARGS, "Release all loaded MIME cache data, it will be re-loaded on next use"},
|
|
{NULL, NULL, 0, NULL},
|
|
};
|
|
|
|
bool
|
|
init_mime_cache(PyObject *module) {
|
|
return PyModule_AddFunctions(module, methods) == 0;
|
|
}
|