From c8df325c7038a8312dd22be800ac008d54f8466b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Mon, 1 Nov 2021 05:08:55 +0100 Subject: [PATCH] Split out xdg.{c,h} --- fastiv-io.c | 5 +- fastiv.c | 173 +----------------------------------------------- meson.build | 2 +- xdg.c | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++ xdg.h | 19 ++++++ 5 files changed, 210 insertions(+), 175 deletions(-) create mode 100644 xdg.c create mode 100644 xdg.h diff --git a/fastiv-io.c b/fastiv-io.c index 119d778..a450b73 100644 --- a/fastiv-io.c +++ b/fastiv-io.c @@ -37,6 +37,8 @@ #define WUFFS_CONFIG__MODULE__ZLIB #include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c" +#include "xdg.h" + // A subset of shared-mime-info that produces an appropriate list of // file extensions. Chiefly motivated by the suckiness of RAW images: // someone else will maintain the list of file extensions for us. @@ -448,9 +450,6 @@ fastiv_io_open(const gchar *path, GError **error) #include #include -// TODO(p): Reorganize the sources. -gchar *get_xdg_home_dir(const char *var, const char *default_); - static void redirect_png_error(png_structp pngp, const char *error) { diff --git a/fastiv.c b/fastiv.c index a035c19..b86cbef 100644 --- a/fastiv.c +++ b/fastiv.c @@ -30,6 +30,7 @@ #include "fastiv-browser.h" #include "fastiv-io.h" #include "fastiv-view.h" +#include "xdg.h" // --- Utilities --------------------------------------------------------------- @@ -49,176 +50,6 @@ exit_fatal(const gchar *format, ...) exit(EXIT_FAILURE); } -/// Add `element` to the `output` set. `relation` is a map of sets of strings -/// defining is-a relations, and is traversed recursively. -static void -add_applying_transitive_closure( - const gchar *element, GHashTable *relation, GHashTable *output) -{ - // Stop condition. - if (!g_hash_table_add(output, g_strdup(element))) - return; - - // TODO(p): Iterate over all aliases of `element` in addition to - // any direct match (and rename this no-longer-generic function). - GHashTable *targets = g_hash_table_lookup(relation, element); - if (!targets) - return; - - GHashTableIter iter; - g_hash_table_iter_init(&iter, targets); - - gpointer key = NULL, value = NULL; - while (g_hash_table_iter_next(&iter, &key, &value)) - add_applying_transitive_closure(key, relation, output); -} - -// --- XDG --------------------------------------------------------------------- - -gchar * -get_xdg_home_dir(const char *var, const char *default_) -{ - const char *env = getenv(var); - if (env && *env == '/') - return g_strdup(env); - - // The specification doesn't handle a missing HOME variable explicitly. - // Implicitly, assuming Bourne shell semantics, it simply resolves empty. - const char *home = getenv("HOME"); - return g_build_filename(home ? home : "", default_, NULL); -} - -static gchar ** -get_xdg_data_dirs(void) -{ - // GStrvBuilder is too new, it would help a little bit. - GPtrArray *output = g_ptr_array_new_with_free_func(g_free); - g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share")); - - const gchar *xdg_data_dirs; - if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs) - xdg_data_dirs = "/usr/local/share/:/usr/share/"; - - gchar **candidates = g_strsplit(xdg_data_dirs, ":", 0); - for (gchar **p = candidates; *p; p++) { - if (**p == '/') - g_ptr_array_add(output, *p); - else - g_free(*p); - } - g_free(candidates); - g_ptr_array_add(output, NULL); - return (gchar **) g_ptr_array_free(output, FALSE); -} - -// --- Filtering --------------------------------------------------------------- - -// Derived from shared-mime-info-spec 0.21. - -static void -read_mime_subclasses(const gchar *path, GHashTable *subclass_sets) -{ - gchar *data = NULL; - if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */)) - return; - - // The format of this file is unspecified, - // but in practice it's a list of space-separated media types. - gchar *datasave = NULL; - for (gchar *line = strtok_r(data, "\r\n", &datasave); line; - line = strtok_r(NULL, "\r\n", &datasave)) { - gchar *linesave = NULL, - *subclass = strtok_r(line, " ", &linesave), - *superclass = strtok_r(NULL, " ", &linesave); - - // Nothing about comments is specified, we're being nice. - if (!subclass || *subclass == '#' || !superclass) - continue; - - GHashTable *set = NULL; - if (!(set = g_hash_table_lookup(subclass_sets, superclass))) { - set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - g_hash_table_insert(subclass_sets, g_strdup(superclass), set); - } - g_hash_table_add(set, g_strdup(subclass)); - } - g_free(data); -} - -static gboolean -filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set, - GHashTable *output_set) -{ - gchar *data = NULL; - if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */)) - return FALSE; - - gchar *datasave = NULL; - for (const gchar *line = strtok_r(data, "\r\n", &datasave); line; - line = strtok_r(NULL, "\r\n", &datasave)) { - if (*line == '#') - continue; - - // We do not support __NOGLOBS__, nor even parse out the "cs" flag. - // The weight is irrelevant. - gchar **f = g_strsplit(line, ":", 0); - if (g_strv_length(f) >= is_globs2 + 2) { - const gchar *type = f[is_globs2 + 0], *glob = f[is_globs2 + 1]; - if (g_hash_table_contains(supported_set, type)) - g_hash_table_add(output_set, g_utf8_strdown(glob, -1)); - } - g_strfreev(f); - } - g_free(data); - return TRUE; -} - -static gchar ** -get_supported_globs(const char **media_types) -{ - gchar **data_dirs = get_xdg_data_dirs(); - - // The mime.cache format is inconvenient to parse, - // we'll do it from the text files manually, and once only. - GHashTable *subclass_sets = g_hash_table_new_full( - g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy); - for (gsize i = 0; data_dirs[i]; i++) { - gchar *path = - g_build_filename(data_dirs[i], "mime", "subclasses", NULL); - read_mime_subclasses(path, subclass_sets); - g_free(path); - } - - // A hash set of all supported media types, including subclasses, - // but not aliases. - GHashTable *supported = - g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - while (*media_types) { - add_applying_transitive_closure( - *media_types++, subclass_sets, supported); - } - g_hash_table_destroy(subclass_sets); - - // We do not support the distinction of case-sensitive globs (:cs). - GHashTable *globs = - g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - for (gsize i = 0; data_dirs[i]; i++) { - gchar *path2 = g_build_filename(data_dirs[i], "mime", "globs2", NULL); - gchar *path1 = g_build_filename(data_dirs[i], "mime", "globs", NULL); - if (!filter_mime_globs(path2, TRUE, supported, globs)) - filter_mime_globs(path1, FALSE, supported, globs); - g_free(path2); - g_free(path1); - } - g_strfreev(data_dirs); - g_hash_table_destroy(supported); - - gchar **result = (gchar **) g_hash_table_get_keys_as_array(globs, NULL); - g_hash_table_steal_all(globs); - g_hash_table_destroy(globs); - return result; -} - // --- Main -------------------------------------------------------------------- struct { @@ -478,7 +309,7 @@ main(int argc, char *argv[]) g_cclosure_new(G_CALLBACK(on_next), NULL, NULL)); gtk_window_add_accel_group(GTK_WINDOW(g.window), accel_group); - g.supported_globs = get_supported_globs(fastiv_io_supported_media_types); + g.supported_globs = extract_mime_globs(fastiv_io_supported_media_types); g.files = g_ptr_array_new_full(16, g_free); gchar *cwd = g_get_current_dir(); diff --git a/meson.build b/meson.build index 944413a..ec0741a 100644 --- a/meson.build +++ b/meson.build @@ -20,7 +20,7 @@ configure_file( ) executable('fastiv', 'fastiv.c', 'fastiv-view.c', 'fastiv-io.c', - 'fastiv-browser.c', + 'fastiv-browser.c', 'xdg.c', install : true, dependencies : [dependencies]) diff --git a/xdg.c b/xdg.c new file mode 100644 index 0000000..7ebd5de --- /dev/null +++ b/xdg.c @@ -0,0 +1,186 @@ +// +// xdg.c: various *nix desktop utilities +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include + +/// Add `element` to the `output` set. `relation` is a map of sets of strings +/// defining is-a relations, and is traversed recursively. +static void +add_applying_transitive_closure( + const gchar *element, GHashTable *relation, GHashTable *output) +{ + // Stop condition. + if (!g_hash_table_add(output, g_strdup(element))) + return; + + // TODO(p): Iterate over all aliases of `element` in addition to + // any direct match (and rename this no-longer-generic function). + GHashTable *targets = g_hash_table_lookup(relation, element); + if (!targets) + return; + + GHashTableIter iter; + g_hash_table_iter_init(&iter, targets); + + gpointer key = NULL, value = NULL; + while (g_hash_table_iter_next(&iter, &key, &value)) + add_applying_transitive_closure(key, relation, output); +} + +char * +get_xdg_home_dir(const char *var, const char *default_) +{ + const char *env = getenv(var); + if (env && *env == '/') + return g_strdup(env); + + // The specification doesn't handle a missing HOME variable explicitly. + // Implicitly, assuming Bourne shell semantics, it simply resolves empty. + const char *home = getenv("HOME"); + return g_build_filename(home ? home : "", default_, NULL); +} + +static gchar ** +get_xdg_data_dirs(void) +{ + // GStrvBuilder is too new, it would help a little bit. + GPtrArray *output = g_ptr_array_new_with_free_func(g_free); + g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share")); + + const gchar *xdg_data_dirs; + if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs) + xdg_data_dirs = "/usr/local/share/:/usr/share/"; + + gchar **candidates = g_strsplit(xdg_data_dirs, ":", 0); + for (gchar **p = candidates; *p; p++) { + if (**p == '/') + g_ptr_array_add(output, *p); + else + g_free(*p); + } + g_free(candidates); + g_ptr_array_add(output, NULL); + return (gchar **) g_ptr_array_free(output, FALSE); +} + +// --- Filtering --------------------------------------------------------------- + +// Derived from shared-mime-info-spec 0.21. + +static void +read_mime_subclasses(const gchar *path, GHashTable *subclass_sets) +{ + gchar *data = NULL; + if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */)) + return; + + // The format of this file is unspecified, + // but in practice it's a list of space-separated media types. + gchar *datasave = NULL; + for (gchar *line = strtok_r(data, "\r\n", &datasave); line; + line = strtok_r(NULL, "\r\n", &datasave)) { + gchar *linesave = NULL, + *subclass = strtok_r(line, " ", &linesave), + *superclass = strtok_r(NULL, " ", &linesave); + + // Nothing about comments is specified, we're being nice. + if (!subclass || *subclass == '#' || !superclass) + continue; + + GHashTable *set = NULL; + if (!(set = g_hash_table_lookup(subclass_sets, superclass))) { + set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_insert(subclass_sets, g_strdup(superclass), set); + } + g_hash_table_add(set, g_strdup(subclass)); + } + g_free(data); +} + +static gboolean +filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set, + GHashTable *output_set) +{ + gchar *data = NULL; + if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */)) + return FALSE; + + gchar *datasave = NULL; + for (const gchar *line = strtok_r(data, "\r\n", &datasave); line; + line = strtok_r(NULL, "\r\n", &datasave)) { + if (*line == '#') + continue; + + // We do not support __NOGLOBS__, nor even parse out the "cs" flag. + // The weight is irrelevant. + gchar **f = g_strsplit(line, ":", 0); + if (g_strv_length(f) >= is_globs2 + 2) { + const gchar *type = f[is_globs2 + 0], *glob = f[is_globs2 + 1]; + if (g_hash_table_contains(supported_set, type)) + g_hash_table_add(output_set, g_utf8_strdown(glob, -1)); + } + g_strfreev(f); + } + g_free(data); + return TRUE; +} + +char ** +extract_mime_globs(const char **media_types) +{ + gchar **data_dirs = get_xdg_data_dirs(); + + // The mime.cache format is inconvenient to parse, + // we'll do it from the text files manually, and once only. + GHashTable *subclass_sets = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy); + for (gsize i = 0; data_dirs[i]; i++) { + gchar *path = + g_build_filename(data_dirs[i], "mime", "subclasses", NULL); + read_mime_subclasses(path, subclass_sets); + g_free(path); + } + + // A hash set of all supported media types, including subclasses, + // but not aliases. + GHashTable *supported = + g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + while (*media_types) { + add_applying_transitive_closure( + *media_types++, subclass_sets, supported); + } + g_hash_table_destroy(subclass_sets); + + // We do not support the distinction of case-sensitive globs (:cs). + GHashTable *globs = + g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + for (gsize i = 0; data_dirs[i]; i++) { + gchar *path2 = g_build_filename(data_dirs[i], "mime", "globs2", NULL); + gchar *path1 = g_build_filename(data_dirs[i], "mime", "globs", NULL); + if (!filter_mime_globs(path2, TRUE, supported, globs)) + filter_mime_globs(path1, FALSE, supported, globs); + g_free(path2); + g_free(path1); + } + g_strfreev(data_dirs); + g_hash_table_destroy(supported); + + gchar **result = (gchar **) g_hash_table_get_keys_as_array(globs, NULL); + g_hash_table_steal_all(globs); + g_hash_table_destroy(globs); + return result; +} diff --git a/xdg.h b/xdg.h new file mode 100644 index 0000000..bc16ac3 --- /dev/null +++ b/xdg.h @@ -0,0 +1,19 @@ +// +// xdg.h: various *nix desktop utilities +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +char *get_xdg_home_dir(const char *var, const char *default_); +char **extract_mime_globs(const char **media_types);