glib/gio/gosxcontenttype.m
Philip Withnall 42c9e8218b gio: Change content type of zero-sized files to application/x-zerosize
That’s what xdgmime uses for zero-sized files (see `XDG_MIME_TYPE_EMPTY`).

Historically, GLib explicitly used `text/plain` for empty files, to
ensure they would open in a text editor. But `text/plain` is not really
correct for an empty file: the content isn’t text because there is no
content. The file could eventually become something else when written
to.

Text editors which want to be opened for new, empty files should add
`application/x-zerosize` to their list of supported content types.

Users who want to set a handler for `application/x-zerosize` on their
desktop should use
```sh
gio mime application/x-zerosize  # to see the current handler
gio mime application/x-zerosize org.gnome.gedit.desktop  # to set it
```

Signed-off-by: Philip Withnall <pwithnall@endlessos.org>

Fixes: #2777
2022-11-07 13:21:28 +00:00

609 lines
16 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2014 Patrick Griffis
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "gcontenttype.h"
#include "gicon.h"
#include "gthemedicon.h"
#include <CoreServices/CoreServices.h>
#define XDG_PREFIX _gio_xdg
#include "xdgmime/xdgmime.h"
/* We lock this mutex whenever we modify global state in this module. */
G_LOCK_DEFINE_STATIC (gio_xdgmime);
/*< internal >
* create_cfstring_from_cstr:
* @cstr: a #gchar
*
* Converts a cstr to a utf8 cfstring
* It must be CFReleased()'d.
*
*/
static CFStringRef
create_cfstring_from_cstr (const gchar *cstr)
{
return CFStringCreateWithCString (NULL, cstr, kCFStringEncodingUTF8);
}
/*< internal >
* create_cstr_from_cfstring:
* @str: a #CFStringRef
*
* Converts a cfstring to a utf8 cstring.
* The incoming cfstring is released for you.
* The returned string must be g_free()'d.
*
*/
static gchar *
create_cstr_from_cfstring (CFStringRef str)
{
CFIndex length;
CFIndex maxlen;
gchar *buffer;
Boolean success;
g_return_val_if_fail (str != NULL, NULL);
length = CFStringGetLength (str);
maxlen = CFStringGetMaximumSizeForEncoding (length, kCFStringEncodingUTF8);
buffer = g_malloc (maxlen + 1);
success = CFStringGetCString (str, (char *) buffer, maxlen,
kCFStringEncodingUTF8);
CFRelease (str);
if (success)
return buffer;
else
{
g_free (buffer);
return NULL;
}
}
/*< internal >
* create_cstr_from_cfstring_with_fallback:
* @str: a #CFStringRef
* @fallback: a #gchar
*
* Tries to convert a cfstring to a utf8 cstring.
* If @str is NULL or conversion fails @fallback is returned.
* The incoming cfstring is released for you.
* The returned string must be g_free()'d.
*
*/
static gchar *
create_cstr_from_cfstring_with_fallback (CFStringRef str,
const gchar *fallback)
{
gchar *cstr = NULL;
if (str)
cstr = create_cstr_from_cfstring (str);
if (!cstr)
return g_strdup (fallback);
return cstr;
}
/*< private >*/
void
g_content_type_set_mime_dirs (const gchar * const *dirs)
{
/* noop on macOS */
}
/*< private >*/
const gchar * const *
g_content_type_get_mime_dirs (void)
{
const gchar * const *mime_dirs = { NULL };
return mime_dirs;
}
gboolean
g_content_type_equals (const gchar *type1,
const gchar *type2)
{
CFStringRef str1, str2;
gboolean ret;
g_return_val_if_fail (type1 != NULL, FALSE);
g_return_val_if_fail (type2 != NULL, FALSE);
if (g_ascii_strcasecmp (type1, type2) == 0)
return TRUE;
str1 = create_cfstring_from_cstr (type1);
str2 = create_cfstring_from_cstr (type2);
ret = UTTypeEqual (str1, str2);
CFRelease (str1);
CFRelease (str2);
return ret;
}
gboolean
g_content_type_is_a (const gchar *ctype,
const gchar *csupertype)
{
CFStringRef type, supertype;
gboolean ret;
g_return_val_if_fail (ctype != NULL, FALSE);
g_return_val_if_fail (csupertype != NULL, FALSE);
type = create_cfstring_from_cstr (ctype);
supertype = create_cfstring_from_cstr (csupertype);
ret = UTTypeConformsTo (type, supertype);
CFRelease (type);
CFRelease (supertype);
return ret;
}
gboolean
g_content_type_is_mime_type (const gchar *type,
const gchar *mime_type)
{
gchar *content_type;
gboolean ret;
g_return_val_if_fail (type != NULL, FALSE);
g_return_val_if_fail (mime_type != NULL, FALSE);
content_type = g_content_type_from_mime_type (mime_type);
ret = g_content_type_is_a (type, content_type);
g_free (content_type);
return ret;
}
gboolean
g_content_type_is_unknown (const gchar *type)
{
g_return_val_if_fail (type != NULL, FALSE);
/* Should dynamic types be considered "unknown"? */
if (g_str_has_prefix (type, "dyn."))
return TRUE;
/* application/octet-stream */
else if (g_strcmp0 (type, "public.data") == 0)
return TRUE;
return FALSE;
}
gchar *
g_content_type_get_description (const gchar *type)
{
CFStringRef str;
CFStringRef desc_str;
g_return_val_if_fail (type != NULL, NULL);
str = create_cfstring_from_cstr (type);
desc_str = UTTypeCopyDescription (str);
CFRelease (str);
return create_cstr_from_cfstring_with_fallback (desc_str, "unknown");
}
/* <internal>
* _get_generic_icon_name_from_mime_type
*
* This function produces a generic icon name from a @mime_type.
* If no generic icon name is found in the xdg mime database, the
* generic icon name is constructed.
*
* Background:
* generic-icon elements specify the icon to use as a generic icon for this
* particular mime-type, given by the name attribute. This is used if there
* is no specific icon (see icon for how these are found). These are used
* for categories of similar types (like spreadsheets or archives) that can
* use a common icon. The Icon Naming Specification lists a set of such
* icon names. If this element is not specified then the mimetype is used
* to generate the generic icon by using the top-level media type
* (e.g. "video" in "video/ogg") and appending "-x-generic"
* (i.e. "video-x-generic" in the previous example).
*
* From: https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-0.18.html
*/
static gchar *
_get_generic_icon_name_from_mime_type (const gchar *mime_type)
{
const gchar *xdg_icon_name;
gchar *icon_name;
G_LOCK (gio_xdgmime);
xdg_icon_name = xdg_mime_get_generic_icon (mime_type);
G_UNLOCK (gio_xdgmime);
if (xdg_icon_name == NULL)
{
const char *p;
const char *suffix = "-x-generic";
gsize prefix_len;
p = strchr (mime_type, '/');
if (p == NULL)
prefix_len = strlen (mime_type);
else
prefix_len = p - mime_type;
icon_name = g_malloc (prefix_len + strlen (suffix) + 1);
memcpy (icon_name, mime_type, prefix_len);
memcpy (icon_name + prefix_len, suffix, strlen (suffix));
icon_name[prefix_len + strlen (suffix)] = 0;
}
else
{
icon_name = g_strdup (xdg_icon_name);
}
return icon_name;
}
static GIcon *
g_content_type_get_icon_internal (const gchar *uti,
gboolean symbolic)
{
char *mimetype_icon;
char *mime_type;
char *generic_mimetype_icon = NULL;
char *q;
char *icon_names[6];
int n = 0;
GIcon *themed_icon;
const char *xdg_icon;
int i;
g_return_val_if_fail (uti != NULL, NULL);
mime_type = g_content_type_get_mime_type (uti);
G_LOCK (gio_xdgmime);
xdg_icon = xdg_mime_get_icon (mime_type);
G_UNLOCK (gio_xdgmime);
if (xdg_icon)
icon_names[n++] = g_strdup (xdg_icon);
mimetype_icon = g_strdup (mime_type);
while ((q = strchr (mimetype_icon, '/')) != NULL)
*q = '-';
icon_names[n++] = mimetype_icon;
generic_mimetype_icon = _get_generic_icon_name_from_mime_type (mime_type);
if (generic_mimetype_icon)
icon_names[n++] = generic_mimetype_icon;
if (symbolic)
{
for (i = 0; i < n; i++)
{
icon_names[n + i] = icon_names[i];
icon_names[i] = g_strconcat (icon_names[i], "-symbolic", NULL);
}
n += n;
}
themed_icon = g_themed_icon_new_from_names (icon_names, n);
for (i = 0; i < n; i++)
g_free (icon_names[i]);
g_free(mime_type);
return themed_icon;
}
GIcon *
g_content_type_get_icon (const gchar *type)
{
return g_content_type_get_icon_internal (type, FALSE);
}
GIcon *
g_content_type_get_symbolic_icon (const gchar *type)
{
return g_content_type_get_icon_internal (type, TRUE);
}
gchar *
g_content_type_get_generic_icon_name (const gchar *type)
{
return NULL;
}
gboolean
g_content_type_can_be_executable (const gchar *type)
{
CFStringRef uti;
gboolean ret = FALSE;
g_return_val_if_fail (type != NULL, FALSE);
uti = create_cfstring_from_cstr (type);
if (UTTypeConformsTo (uti, kUTTypeApplication))
ret = TRUE;
else if (UTTypeConformsTo (uti, CFSTR("public.executable")))
ret = TRUE;
else if (UTTypeConformsTo (uti, CFSTR("public.script")))
ret = TRUE;
/* Our tests assert that all text can be executable... */
else if (UTTypeConformsTo (uti, CFSTR("public.text")))
ret = TRUE;
CFRelease (uti);
return ret;
}
gchar *
g_content_type_from_mime_type (const gchar *mime_type)
{
CFStringRef mime_str;
CFStringRef uti_str;
g_return_val_if_fail (mime_type != NULL, NULL);
/* Their api does not handle globs but they are common. */
if (g_str_has_suffix (mime_type, "*"))
{
if (g_str_has_prefix (mime_type, "audio"))
return g_strdup ("public.audio");
if (g_str_has_prefix (mime_type, "image"))
return g_strdup ("public.image");
if (g_str_has_prefix (mime_type, "text"))
return g_strdup ("public.text");
if (g_str_has_prefix (mime_type, "video"))
return g_strdup ("public.movie");
}
/* Some exceptions are needed for gdk-pixbuf.
* This list is not exhaustive.
*/
if (g_str_has_prefix (mime_type, "image"))
{
if (g_str_has_suffix (mime_type, "x-icns"))
return g_strdup ("com.apple.icns");
if (g_str_has_suffix (mime_type, "x-tga"))
return g_strdup ("com.truevision.tga-image");
if (g_str_has_suffix (mime_type, "x-ico"))
return g_strdup ("com.microsoft.ico ");
}
/* These are also not supported...
* Used in glocalfileinfo.c
*/
if (g_str_has_prefix (mime_type, "inode"))
{
if (g_str_has_suffix (mime_type, "directory"))
return g_strdup ("public.folder");
if (g_str_has_suffix (mime_type, "symlink"))
return g_strdup ("public.symlink");
}
/* This is correct according to the Apple docs:
https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
*/
if (strcmp (mime_type, "text/plain") == 0)
return g_strdup ("public.text");
/* I dont know of an appropriate equivalent for application/x-zerosize, but
* historically GLib has returned public.text for zero-sized files, so lets
* continue doing that. */
if (strcmp (mime_type, "application/x-zerosize") == 0)
return g_strdup ("public.text");
/* Non standard type */
if (strcmp (mime_type, "application/x-executable") == 0)
return g_strdup ("public.executable");
mime_str = create_cfstring_from_cstr (mime_type);
uti_str = UTTypeCreatePreferredIdentifierForTag (kUTTagClassMIMEType, mime_str, NULL);
CFRelease (mime_str);
return create_cstr_from_cfstring_with_fallback (uti_str, "public.data");
}
gchar *
g_content_type_get_mime_type (const gchar *type)
{
CFStringRef uti_str;
CFStringRef mime_str;
g_return_val_if_fail (type != NULL, NULL);
/* We must match the additions above
* so conversions back and forth work.
*/
if (g_str_has_prefix (type, "public"))
{
if (g_str_has_suffix (type, ".image"))
return g_strdup ("image/*");
if (g_str_has_suffix (type, ".movie"))
return g_strdup ("video/*");
if (g_str_has_suffix (type, ".text"))
return g_strdup ("text/*");
if (g_str_has_suffix (type, ".audio"))
return g_strdup ("audio/*");
if (g_str_has_suffix (type, ".folder"))
return g_strdup ("inode/directory");
if (g_str_has_suffix (type, ".symlink"))
return g_strdup ("inode/symlink");
if (g_str_has_suffix (type, ".executable"))
return g_strdup ("application/x-executable");
}
uti_str = create_cfstring_from_cstr (type);
mime_str = UTTypeCopyPreferredTagWithClass(uti_str, kUTTagClassMIMEType);
CFRelease (uti_str);
return create_cstr_from_cfstring_with_fallback (mime_str, "application/octet-stream");
}
static gboolean
looks_like_text (const guchar *data,
gsize data_size)
{
gsize i;
guchar c;
for (i = 0; i < data_size; i++)
{
c = data[i];
if (g_ascii_iscntrl (c) && !g_ascii_isspace (c) && c != '\b')
return FALSE;
}
return TRUE;
}
gchar *
g_content_type_guess (const gchar *filename,
const guchar *data,
gsize data_size,
gboolean *result_uncertain)
{
CFStringRef uti = NULL;
gchar *cextension;
CFStringRef extension;
int uncertain = -1;
g_return_val_if_fail (data_size != (gsize) -1, NULL);
if (filename && *filename)
{
gchar *basename = g_path_get_basename (filename);
gchar *dirname = g_path_get_dirname (filename);
gsize i = strlen (filename);
if (filename[i - 1] == '/')
{
if (g_strcmp0 (dirname, "/Volumes") == 0)
{
uti = CFStringCreateCopy (NULL, kUTTypeVolume);
}
else if ((cextension = strrchr (basename, '.')) != NULL)
{
cextension++;
extension = create_cfstring_from_cstr (cextension);
uti = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension,
extension, NULL);
CFRelease (extension);
if (CFStringHasPrefix (uti, CFSTR ("dyn.")))
{
CFRelease (uti);
uti = CFStringCreateCopy (NULL, kUTTypeFolder);
uncertain = TRUE;
}
}
else
{
uti = CFStringCreateCopy (NULL, kUTTypeFolder);
uncertain = TRUE; /* Matches Unix backend */
}
}
else
{
/* GTK needs this... */
if (g_str_has_suffix (basename, ".ui"))
{
uti = CFStringCreateCopy (NULL, kUTTypeXML);
}
else if (g_str_has_suffix (basename, ".txt"))
{
uti = CFStringCreateCopy (NULL, CFSTR ("public.text"));
}
else if ((cextension = strrchr (basename, '.')) != NULL)
{
cextension++;
extension = create_cfstring_from_cstr (cextension);
uti = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension,
extension, NULL);
CFRelease (extension);
}
g_free (basename);
g_free (dirname);
}
}
if (data && (!filename || !uti ||
CFStringCompare (uti, CFSTR ("public.data"), 0) == kCFCompareEqualTo))
{
const char *sniffed_mimetype;
G_LOCK (gio_xdgmime);
sniffed_mimetype = xdg_mime_get_mime_type_for_data (data, data_size, NULL);
G_UNLOCK (gio_xdgmime);
if (sniffed_mimetype != XDG_MIME_TYPE_UNKNOWN)
{
gchar *uti_str = g_content_type_from_mime_type (sniffed_mimetype);
uti = create_cfstring_from_cstr (uti_str);
g_free (uti_str);
}
if (!uti && looks_like_text (data, data_size))
{
if (g_str_has_prefix ((const gchar*)data, "#!/"))
uti = CFStringCreateCopy (NULL, CFSTR ("public.script"));
else
uti = CFStringCreateCopy (NULL, CFSTR ("public.text"));
}
}
if (!uti)
{
/* Generic data type */
uti = CFStringCreateCopy (NULL, CFSTR ("public.data"));
if (result_uncertain)
*result_uncertain = TRUE;
}
else if (result_uncertain)
{
*result_uncertain = uncertain == -1 ? FALSE : uncertain;
}
return create_cstr_from_cfstring (uti);
}
GList *
g_content_types_get_registered (void)
{
/* TODO: UTTypeCreateAllIdentifiersForTag? */
return NULL;
}
gchar **
g_content_type_guess_for_tree (GFile *root)
{
return NULL;
}