gnome-shell/src/hotplug-sniffer/shell-mime-sniffer.c
Marco Trevisan (Treviño) 46547ae027 shell-mime-sniffer: Ignore invalid file content type
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
2020-06-05 16:27:29 +02:00

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);
}