xaizek / vifm (License: GPLv2+) (since 2018-12-07)
Vifm is a file manager with curses interface, which provides Vi[m]-like environment for managing objects within file systems, extended with some useful ideas from mutt.
<root> / src / int / file_magic.c (5aff3ac3667017eb6a0c8cad3e5b3dfb7128bdea) (7,518B) (mode 100644) [raw]
/* vifm
 * Copyright (C) 2011 xaizek.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "file_magic.h"

#ifdef HAVE_GLIB
#include <gio/gio.h>
#include <glib.h>
#endif

#ifdef HAVE_LIBMAGIC
#include <magic.h>
#endif

#include <stddef.h> /* size_t */
#include <stdlib.h> /* free() malloc() */
#include <stdio.h> /* popen() */
#include <string.h> /* strdup() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../utils/filemon.h"
#include "../utils/fsddata.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/utils.h"
#include "../filetype.h"
#include "../status.h"
#include "desktop.h"

/* Cache entry. */
typedef struct
{
	char *mime;        /* Mime-type. */
	filemon_t filemon; /* Timestamp. */
}
cache_data_t;

static fsddata_t * get_cache(void);
static int lookup_in_cache(fsddata_t *cache, const char path[],
		filemon_t *filemon, cache_data_t **data);
static void update_cache(fsddata_t *cache, const char path[],
		const char mimetype[], filemon_t *filemon, cache_data_t *data);
static int get_gtk_mimetype(const char filename[], char buf[], size_t buf_sz);
static int get_magic_mimetype(const char filename[], char buf[], size_t buf_sz);
static int get_file_mimetype(const char filename[], char buf[], size_t buf_sz);
static assoc_records_t get_handlers(const char mime_type[]);
#if !defined(_WIN32) && defined(ENABLE_DESKTOP_FILES)
static void parse_app_dir(const char directory[], const char mime_type[],
		assoc_records_t *result);
#endif

assoc_records_t
get_magic_handlers(const char file[])
{
	return get_handlers(get_mimetype(file, 1));
}

const char *
get_mimetype(const char file[], int resolve_symlinks)
{
	static char mimetype[128];

	char target[PATH_MAX + 1];
	if(resolve_symlinks)
	{
		if(os_realpath(file, target) == target)
		{
			file = target;
		}
	}

	fsddata_t *mime_cache = get_cache();

	filemon_t filemon;
	cache_data_t *cache_data = NULL;
	if(lookup_in_cache(mime_cache, file, &filemon, &cache_data))
	{
		copy_str(mimetype, sizeof(mimetype), cache_data->mime);
		return mimetype;
	}

	if(get_gtk_mimetype(file, mimetype, sizeof(mimetype)) == -1)
	{
		if(get_magic_mimetype(file, mimetype, sizeof(mimetype)) == -1)
		{
			if(get_file_mimetype(file, mimetype, sizeof(mimetype)) == -1)
			{
				return NULL;
			}
		}
	}

	update_cache(mime_cache, file, mimetype, &filemon, cache_data);

	return mimetype;
}

/* Retrieves mime-type cache, creating it on first call.  Returns the cache. */
static fsddata_t *
get_cache(void)
{
	static fsddata_t *mime_cache;
	if(mime_cache == NULL)
	{
		mime_cache = fsddata_create(0, 0);
	}
	return mime_cache;
}

/* Looks up cache entry for the specified path.  On success, *filemon is
 * initialized from the file.  Returns non-zero if entry is found, otherwise
 * zero is returned. */
static int
lookup_in_cache(fsddata_t *cache, const char path[], filemon_t *filemon,
		cache_data_t **data)
{
	void *value = NULL;
	if(fsddata_get(cache, path, &value) == 0)
	{
		*data = value;

		(void)filemon_from_file(path, FMT_MODIFIED, filemon);
		return filemon_equal(filemon, &(*data)->filemon);
	}
	return 0;
}

/* Updates cache for the path. */
static void
update_cache(fsddata_t *cache, const char path[], const char mimetype[],
		filemon_t *filemon, cache_data_t *data)
{
	if(data != NULL)
	{
		/* Simply update cache entry in place. */
		replace_string(&data->mime, mimetype);
		data->filemon = *filemon;
		return;
	}

	(void)filemon_from_file(path, FMT_MODIFIED, filemon);

	data = malloc(sizeof(*data));
	if(data == NULL)
	{
		return;
	}

	data->filemon = *filemon;
	data->mime = strdup(mimetype);
	if(data->mime == NULL)
	{
		free(data);
		return;
	}

	fsddata_set(cache, path, data);
}

static int
get_gtk_mimetype(const char filename[], char buf[], size_t buf_sz)
{
#ifdef HAVE_GLIB
	GFile *file;
	GFileInfo *info;

	file = g_file_new_for_path(filename);
	info = g_file_query_info(file, "standard::", G_FILE_QUERY_INFO_NONE, NULL,
			NULL);
	if(info == NULL)
	{
		g_object_unref(file);
		return -1;
	}

	copy_str(buf, buf_sz, g_file_info_get_content_type(info));
	g_object_unref(info);
	g_object_unref(file);
	return 0;
#else /* #ifdef HAVE_GLIB */
	return -1;
#endif /* #ifdef HAVE_GLIB */
}

static int
get_magic_mimetype(const char filename[], char buf[], size_t buf_sz)
{
#ifdef HAVE_LIBMAGIC
	static magic_t magic;
	const char *descr;

	if(magic == NULL)
	{
#if HAVE_DECL_MAGIC_MIME_TYPE
		magic = magic_open(MAGIC_MIME_TYPE);
#else
		magic = magic_open(MAGIC_MIME);
#endif

		if(magic == NULL)
		{
			return -1;
		}

		if(magic_load(magic, NULL) != 0)
		{
			magic_close(magic);
			magic = NULL;
			return -1;
		}
	}

	descr = magic_file(magic, filename);
	if(descr == NULL)
	{
		return -1;
	}

	copy_str(buf, buf_sz, descr);
#if !HAVE_DECL_MAGIC_MIME_TYPE
	break_atr(buf, ';');
#endif

	return 0;
#else /* #ifdef HAVE_LIBMAGIC */
	return -1;
#endif /* #ifdef HAVE_LIBMAGIC */
}

static int
get_file_mimetype(const char filename[], char buf[], size_t buf_sz)
{
#ifdef HAVE_FILE_PROG
	FILE *pipe;
	char command[1024];
	ShellType shell_type = (get_env_type() == ET_UNIX ? ST_POSIX : ST_CMD);
	char *const escaped_filename = shell_arg_escape(filename, shell_type);

	/* Use the file command to get mimetype */
	snprintf(command, sizeof(command), "file %s -b --mime-type",
			escaped_filename);
	free(escaped_filename);

	if((pipe = popen(command, "r")) == NULL)
	{
		return -1;
	}

	if(fgets(buf, buf_sz, pipe) != buf)
	{
		pclose(pipe);
		return -1;
	}

	pclose(pipe);
	chomp(buf);

	return 0;
#else /* #ifdef HAVE_FILE_PROG */
	return -1;
#endif /* #ifdef HAVE_FILE_PROG */
}

static assoc_records_t
get_handlers(const char mime_type[])
{
	static assoc_records_t handlers;
	ft_assoc_records_free(&handlers);

	if(mime_type == NULL)
	{
		return handlers;
	}

#if !defined(_WIN32) && defined(ENABLE_DESKTOP_FILES)
	parse_app_dir("/usr/share/applications", mime_type, &handlers);
	parse_app_dir("/usr/local/share/applications", mime_type, &handlers);

	char local_dir[PATH_MAX + 1];
	const char *xdg_data_home = getenv("XDG_DATA_HOME");
	if(is_null_or_empty(xdg_data_home))
	{
		build_path(local_dir, sizeof(local_dir), cfg.home_dir,
				".local/share/applications");
	}
	else
	{
		build_path(local_dir, sizeof(local_dir), xdg_data_home, "applications");
	}
	parse_app_dir(local_dir, mime_type, &handlers);
#endif

	return handlers;
}

#if !defined(_WIN32) && defined(ENABLE_DESKTOP_FILES)
static void
parse_app_dir(const char directory[], const char mime_type[],
		assoc_records_t *result)
{
	assoc_records_t desktop_assocs = parse_desktop_files(directory, mime_type);
	ft_assoc_record_add_all(result, &desktop_assocs);
	ft_assoc_records_free(&desktop_assocs);
}
#endif

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/vifm

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/vifm

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master