46547ae027
The shell mime sniffer goes through all the files in a directory, however in case a file content type is not recognized, the GIO function g_file_info_get_content_type() may return NULL, causing a crash when looking up into the content type tables, as they are supposed to contain strings only and they use `g_str_hash` has func, which doesn't support NULL values. So, in case we get an invalid content type, let's just ignore it, without adding it to the cache as we do in the nautilus code that was inspiring the sniffer. Fixes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2643
591 lines
16 KiB
C
591 lines
16 KiB
C
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
|
|
/*
|
|
* Copyright (C) 1999, 2000, 2001 Eazel, Inc.
|
|
* Copyright (C) 2011 Red Hat, Inc.
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Author: Cosimo Cecchi <cosimoc@redhat.com>
|
|
*
|
|
* The code for crawling the directory hierarchy is based on
|
|
* nautilus/libnautilus-private/nautilus-directory-async.c, with
|
|
* the following copyright and author:
|
|
*
|
|
* Copyright (C) 1999, 2000, 2001 Eazel, Inc.
|
|
* Author: Darin Adler <darin@bentspoon.com>
|
|
*
|
|
*/
|
|
|
|
#include "shell-mime-sniffer.h"
|
|
#include "hotplug-mimetypes.h"
|
|
|
|
#include <glib/gi18n.h>
|
|
|
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
|
|
|
#define LOADER_ATTRS \
|
|
G_FILE_ATTRIBUTE_STANDARD_TYPE "," \
|
|
G_FILE_ATTRIBUTE_STANDARD_NAME "," \
|
|
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
|
|
|
|
#define WATCHDOG_TIMEOUT 1500
|
|
#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100
|
|
#define HIGH_SCORE_RATIO 0.10
|
|
|
|
enum {
|
|
PROP_FILE = 1,
|
|
NUM_PROPERTIES
|
|
};
|
|
|
|
static GHashTable *image_type_table = NULL;
|
|
static GHashTable *audio_type_table = NULL;
|
|
static GHashTable *video_type_table = NULL;
|
|
static GHashTable *docs_type_table = NULL;
|
|
|
|
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
|
|
|
|
typedef struct {
|
|
ShellMimeSniffer *self;
|
|
|
|
GFile *file;
|
|
GFileEnumerator *enumerator;
|
|
GList *deep_count_subdirectories;
|
|
|
|
gint audio_count;
|
|
gint image_count;
|
|
gint document_count;
|
|
gint video_count;
|
|
|
|
gint total_items;
|
|
} DeepCountState;
|
|
|
|
typedef struct _ShellMimeSnifferPrivate ShellMimeSnifferPrivate;
|
|
|
|
struct _ShellMimeSniffer
|
|
{
|
|
GObject parent_instance;
|
|
|
|
ShellMimeSnifferPrivate *priv;
|
|
};
|
|
|
|
struct _ShellMimeSnifferPrivate {
|
|
GFile *file;
|
|
|
|
GCancellable *cancellable;
|
|
guint watchdog_id;
|
|
|
|
GTask *task;
|
|
};
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE (ShellMimeSniffer, shell_mime_sniffer, G_TYPE_OBJECT);
|
|
|
|
static void deep_count_load (DeepCountState *state,
|
|
GFile *file);
|
|
|
|
static void
|
|
init_mimetypes (void)
|
|
{
|
|
static gsize once_init = 0;
|
|
|
|
if (g_once_init_enter (&once_init))
|
|
{
|
|
GSList *formats, *l;
|
|
GdkPixbufFormat *format;
|
|
gchar **types;
|
|
gint idx;
|
|
|
|
image_type_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
|
video_type_table = g_hash_table_new (g_str_hash, g_str_equal);
|
|
audio_type_table = g_hash_table_new (g_str_hash, g_str_equal);
|
|
docs_type_table = g_hash_table_new (g_str_hash, g_str_equal);
|
|
|
|
formats = gdk_pixbuf_get_formats ();
|
|
|
|
for (l = formats; l != NULL; l = l->next)
|
|
{
|
|
format = l->data;
|
|
types = gdk_pixbuf_format_get_mime_types (format);
|
|
|
|
for (idx = 0; types[idx] != NULL; idx++)
|
|
g_hash_table_insert (image_type_table, g_strdup (types[idx]), GINT_TO_POINTER (1));
|
|
|
|
g_strfreev (types);
|
|
}
|
|
|
|
g_slist_free (formats);
|
|
|
|
for (idx = 0; audio_mimetypes[idx] != NULL; idx++)
|
|
g_hash_table_insert (audio_type_table, (gpointer) audio_mimetypes[idx], GINT_TO_POINTER (1));
|
|
|
|
for (idx = 0; video_mimetypes[idx] != NULL; idx++)
|
|
g_hash_table_insert (video_type_table, (gpointer) video_mimetypes[idx], GINT_TO_POINTER (1));
|
|
|
|
for (idx = 0; docs_mimetypes[idx] != NULL; idx++)
|
|
g_hash_table_insert (docs_type_table, (gpointer) docs_mimetypes[idx], GINT_TO_POINTER (1));
|
|
|
|
g_once_init_leave (&once_init, 1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
add_content_type_to_cache (DeepCountState *state,
|
|
const gchar *content_type)
|
|
{
|
|
gboolean matched = TRUE;
|
|
|
|
if (g_hash_table_lookup (image_type_table, content_type))
|
|
state->image_count++;
|
|
else if (g_hash_table_lookup (video_type_table, content_type))
|
|
state->video_count++;
|
|
else if (g_hash_table_lookup (docs_type_table, content_type))
|
|
state->document_count++;
|
|
else if (g_hash_table_lookup (audio_type_table, content_type))
|
|
state->audio_count++;
|
|
else
|
|
matched = FALSE;
|
|
|
|
if (matched)
|
|
state->total_items++;
|
|
}
|
|
|
|
typedef struct {
|
|
const gchar *type;
|
|
gdouble ratio;
|
|
} SniffedResult;
|
|
|
|
static gint
|
|
results_cmp_func (gconstpointer a,
|
|
gconstpointer b)
|
|
{
|
|
const SniffedResult *sniffed_a = a;
|
|
const SniffedResult *sniffed_b = b;
|
|
|
|
if (sniffed_a->ratio < sniffed_b->ratio)
|
|
return 1;
|
|
|
|
if (sniffed_a->ratio > sniffed_b->ratio)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
prepare_async_result (DeepCountState *state)
|
|
{
|
|
ShellMimeSniffer *self = state->self;
|
|
GArray *results;
|
|
GPtrArray *sniffed_mime;
|
|
SniffedResult result;
|
|
char **mimes;
|
|
|
|
sniffed_mime = g_ptr_array_new ();
|
|
results = g_array_new (TRUE, TRUE, sizeof (SniffedResult));
|
|
|
|
if (state->total_items == 0)
|
|
goto out;
|
|
|
|
result.type = "x-content/video";
|
|
result.ratio = (gdouble) state->video_count / (gdouble) state->total_items;
|
|
g_array_append_val (results, result);
|
|
|
|
result.type = "x-content/audio";
|
|
result.ratio = (gdouble) state->audio_count / (gdouble) state->total_items;
|
|
g_array_append_val (results, result);
|
|
|
|
result.type = "x-content/pictures";
|
|
result.ratio = (gdouble) state->image_count / (gdouble) state->total_items;
|
|
g_array_append_val (results, result);
|
|
|
|
result.type = "x-content/documents";
|
|
result.ratio = (gdouble) state->document_count / (gdouble) state->total_items;
|
|
g_array_append_val (results, result);
|
|
|
|
g_array_sort (results, results_cmp_func);
|
|
|
|
result = g_array_index (results, SniffedResult, 0);
|
|
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
|
|
|
|
/* if other types score high in ratio, add them, up to three */
|
|
result = g_array_index (results, SniffedResult, 1);
|
|
if (result.ratio < HIGH_SCORE_RATIO)
|
|
goto out;
|
|
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
|
|
|
|
result = g_array_index (results, SniffedResult, 2);
|
|
if (result.ratio < HIGH_SCORE_RATIO)
|
|
goto out;
|
|
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
|
|
|
|
out:
|
|
g_ptr_array_add (sniffed_mime, NULL);
|
|
mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE);
|
|
|
|
g_array_free (results, TRUE);
|
|
g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev);
|
|
}
|
|
|
|
/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */
|
|
static void
|
|
deep_count_one (DeepCountState *state,
|
|
GFileInfo *info)
|
|
{
|
|
GFile *subdir;
|
|
const char *content_type;
|
|
|
|
if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
|
|
{
|
|
/* record the fact that we have to descend into this directory */
|
|
subdir = g_file_get_child (state->file, g_file_info_get_name (info));
|
|
state->deep_count_subdirectories =
|
|
g_list_append (state->deep_count_subdirectories, subdir);
|
|
}
|
|
else
|
|
{
|
|
content_type = g_file_info_get_content_type (info);
|
|
|
|
if (content_type)
|
|
add_content_type_to_cache (state, content_type);
|
|
}
|
|
}
|
|
|
|
static void
|
|
deep_count_finish (DeepCountState *state)
|
|
{
|
|
prepare_async_result (state);
|
|
|
|
if (state->enumerator)
|
|
{
|
|
if (!g_file_enumerator_is_closed (state->enumerator))
|
|
g_file_enumerator_close_async (state->enumerator,
|
|
0, NULL, NULL, NULL);
|
|
|
|
g_object_unref (state->enumerator);
|
|
}
|
|
|
|
g_cancellable_reset (state->self->priv->cancellable);
|
|
g_clear_object (&state->file);
|
|
|
|
g_list_free_full (state->deep_count_subdirectories, g_object_unref);
|
|
|
|
g_free (state);
|
|
}
|
|
|
|
static void
|
|
deep_count_next_dir (DeepCountState *state)
|
|
{
|
|
GFile *new_file;
|
|
|
|
g_clear_object (&state->file);
|
|
|
|
if (state->deep_count_subdirectories != NULL)
|
|
{
|
|
/* Work on a new directory. */
|
|
new_file = state->deep_count_subdirectories->data;
|
|
state->deep_count_subdirectories =
|
|
g_list_remove (state->deep_count_subdirectories, new_file);
|
|
|
|
deep_count_load (state, new_file);
|
|
g_object_unref (new_file);
|
|
}
|
|
else
|
|
{
|
|
deep_count_finish (state);
|
|
}
|
|
}
|
|
|
|
static void
|
|
deep_count_more_files_callback (GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
DeepCountState *state;
|
|
GList *files, *l;
|
|
GFileInfo *info;
|
|
|
|
state = user_data;
|
|
|
|
if (g_cancellable_is_cancelled (state->self->priv->cancellable))
|
|
{
|
|
deep_count_finish (state);
|
|
return;
|
|
}
|
|
|
|
files = g_file_enumerator_next_files_finish (state->enumerator,
|
|
res, NULL);
|
|
|
|
for (l = files; l != NULL; l = l->next)
|
|
{
|
|
info = l->data;
|
|
deep_count_one (state, info);
|
|
g_object_unref (info);
|
|
}
|
|
|
|
if (files == NULL)
|
|
{
|
|
g_file_enumerator_close_async (state->enumerator, 0, NULL, NULL, NULL);
|
|
g_object_unref (state->enumerator);
|
|
state->enumerator = NULL;
|
|
|
|
deep_count_next_dir (state);
|
|
}
|
|
else
|
|
{
|
|
g_file_enumerator_next_files_async (state->enumerator,
|
|
DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
|
|
G_PRIORITY_LOW,
|
|
state->self->priv->cancellable,
|
|
deep_count_more_files_callback,
|
|
state);
|
|
}
|
|
|
|
g_list_free (files);
|
|
}
|
|
|
|
static void
|
|
deep_count_callback (GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
DeepCountState *state;
|
|
GFileEnumerator *enumerator;
|
|
|
|
state = user_data;
|
|
|
|
if (g_cancellable_is_cancelled (state->self->priv->cancellable))
|
|
{
|
|
deep_count_finish (state);
|
|
return;
|
|
}
|
|
|
|
enumerator = g_file_enumerate_children_finish (G_FILE (source_object),
|
|
res, NULL);
|
|
|
|
if (enumerator == NULL)
|
|
{
|
|
deep_count_next_dir (state);
|
|
}
|
|
else
|
|
{
|
|
state->enumerator = enumerator;
|
|
g_file_enumerator_next_files_async (state->enumerator,
|
|
DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
|
|
G_PRIORITY_LOW,
|
|
state->self->priv->cancellable,
|
|
deep_count_more_files_callback,
|
|
state);
|
|
}
|
|
}
|
|
|
|
static void
|
|
deep_count_load (DeepCountState *state,
|
|
GFile *file)
|
|
{
|
|
state->file = g_object_ref (file);
|
|
|
|
g_file_enumerate_children_async (state->file,
|
|
LOADER_ATTRS,
|
|
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, /* flags */
|
|
G_PRIORITY_LOW, /* prio */
|
|
state->self->priv->cancellable,
|
|
deep_count_callback,
|
|
state);
|
|
}
|
|
|
|
static void
|
|
deep_count_start (ShellMimeSniffer *self)
|
|
{
|
|
DeepCountState *state;
|
|
|
|
state = g_new0 (DeepCountState, 1);
|
|
state->self = self;
|
|
|
|
deep_count_load (state, self->priv->file);
|
|
}
|
|
|
|
static void
|
|
query_info_async_ready_cb (GObject *source,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GFileInfo *info;
|
|
GError *error = NULL;
|
|
ShellMimeSniffer *self = user_data;
|
|
|
|
info = g_file_query_info_finish (G_FILE (source),
|
|
res, &error);
|
|
|
|
if (error != NULL)
|
|
{
|
|
g_task_return_error (self->priv->task, error);
|
|
|
|
return;
|
|
}
|
|
|
|
if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
|
|
{
|
|
g_task_return_new_error (self->priv->task,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_DIRECTORY,
|
|
"Not a directory");
|
|
|
|
return;
|
|
}
|
|
|
|
deep_count_start (self);
|
|
}
|
|
|
|
static gboolean
|
|
watchdog_timeout_reached_cb (gpointer user_data)
|
|
{
|
|
ShellMimeSniffer *self = user_data;
|
|
|
|
self->priv->watchdog_id = 0;
|
|
g_cancellable_cancel (self->priv->cancellable);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
start_loading_file (ShellMimeSniffer *self)
|
|
{
|
|
g_file_query_info_async (self->priv->file,
|
|
LOADER_ATTRS,
|
|
G_FILE_QUERY_INFO_NONE,
|
|
G_PRIORITY_DEFAULT,
|
|
self->priv->cancellable,
|
|
query_info_async_ready_cb,
|
|
self);
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_set_file (ShellMimeSniffer *self,
|
|
GFile *file)
|
|
{
|
|
g_clear_object (&self->priv->file);
|
|
self->priv->file = g_object_ref (file);
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_dispose (GObject *object)
|
|
{
|
|
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
|
|
|
|
g_clear_object (&self->priv->file);
|
|
g_clear_object (&self->priv->cancellable);
|
|
g_clear_object (&self->priv->task);
|
|
|
|
g_clear_handle_id (&self->priv->watchdog_id, g_source_remove);
|
|
|
|
G_OBJECT_CLASS (shell_mime_sniffer_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_FILE:
|
|
g_value_set_object (value, self->priv->file);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_FILE:
|
|
shell_mime_sniffer_set_file (self, g_value_get_object (value));
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_class_init (ShellMimeSnifferClass *klass)
|
|
{
|
|
GObjectClass *oclass;
|
|
|
|
oclass = G_OBJECT_CLASS (klass);
|
|
oclass->dispose = shell_mime_sniffer_dispose;
|
|
oclass->get_property = shell_mime_sniffer_get_property;
|
|
oclass->set_property = shell_mime_sniffer_set_property;
|
|
|
|
properties[PROP_FILE] =
|
|
g_param_spec_object ("file",
|
|
"File",
|
|
"The loaded file",
|
|
G_TYPE_FILE,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
|
|
|
g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
|
|
}
|
|
|
|
static void
|
|
shell_mime_sniffer_init (ShellMimeSniffer *self)
|
|
{
|
|
self->priv = shell_mime_sniffer_get_instance_private (self);
|
|
init_mimetypes ();
|
|
}
|
|
|
|
ShellMimeSniffer *
|
|
shell_mime_sniffer_new (GFile *file)
|
|
{
|
|
return g_object_new (SHELL_TYPE_MIME_SNIFFER,
|
|
"file", file,
|
|
NULL);
|
|
}
|
|
|
|
void
|
|
shell_mime_sniffer_sniff_async (ShellMimeSniffer *self,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
g_assert (self->priv->watchdog_id == 0);
|
|
g_assert (self->priv->task == NULL);
|
|
|
|
self->priv->cancellable = g_cancellable_new ();
|
|
self->priv->task = g_task_new (self, self->priv->cancellable,
|
|
callback, user_data);
|
|
|
|
self->priv->watchdog_id =
|
|
g_timeout_add (WATCHDOG_TIMEOUT,
|
|
watchdog_timeout_reached_cb, self);
|
|
g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb");
|
|
|
|
start_loading_file (self);
|
|
}
|
|
|
|
gchar **
|
|
shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_pointer (self->priv->task, error);
|
|
}
|