| /* 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 don’t know of an appropriate equivalent for application/x-zerosize, but |
| * historically GLib has returned public.text for zero-sized files, so let’s |
| * 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; |
| } |