• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* GIO - GLib Input, Output and Streaming Library
2 *
3 * Copyright (C) 2014 Patrick Griffis
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General
16 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20#include "config.h"
21
22#include "gcontenttype.h"
23#include "gicon.h"
24#include "gthemedicon.h"
25
26#include <CoreServices/CoreServices.h>
27
28#define XDG_PREFIX _gio_xdg
29#include "xdgmime/xdgmime.h"
30
31/* We lock this mutex whenever we modify global state in this module.  */
32G_LOCK_DEFINE_STATIC (gio_xdgmime);
33
34
35/*< internal >
36 * create_cfstring_from_cstr:
37 * @cstr: a #gchar
38 *
39 * Converts a cstr to a utf8 cfstring
40 * It must be CFReleased()'d.
41 *
42 */
43static CFStringRef
44create_cfstring_from_cstr (const gchar *cstr)
45{
46  return CFStringCreateWithCString (NULL, cstr, kCFStringEncodingUTF8);
47}
48
49/*< internal >
50 * create_cstr_from_cfstring:
51 * @str: a #CFStringRef
52 *
53 * Converts a cfstring to a utf8 cstring.
54 * The incoming cfstring is released for you.
55 * The returned string must be g_free()'d.
56 *
57 */
58static gchar *
59create_cstr_from_cfstring (CFStringRef str)
60{
61  g_return_val_if_fail (str != NULL, NULL);
62
63  CFIndex length = CFStringGetLength (str);
64  CFIndex maxlen = CFStringGetMaximumSizeForEncoding (length, kCFStringEncodingUTF8);
65  gchar *buffer = g_malloc (maxlen + 1);
66  Boolean success = CFStringGetCString (str, (char *) buffer, maxlen,
67                                        kCFStringEncodingUTF8);
68  CFRelease (str);
69  if (success)
70    return buffer;
71  else
72    {
73      g_free (buffer);
74      return NULL;
75    }
76}
77
78/*< internal >
79 * create_cstr_from_cfstring_with_fallback:
80 * @str: a #CFStringRef
81 * @fallback: a #gchar
82 *
83 * Tries to convert a cfstring to a utf8 cstring.
84 * If @str is NULL or conversion fails @fallback is returned.
85 * The incoming cfstring is released for you.
86 * The returned string must be g_free()'d.
87 *
88 */
89static gchar *
90create_cstr_from_cfstring_with_fallback (CFStringRef  str,
91                                         const gchar *fallback)
92{
93  gchar *cstr = NULL;
94
95  if (str)
96    cstr = create_cstr_from_cfstring (str);
97  if (!cstr)
98    return g_strdup (fallback);
99
100  return cstr;
101}
102
103/*< private >*/
104void
105g_content_type_set_mime_dirs (const gchar * const *dirs)
106{
107  /* noop on macOS */
108}
109
110/*< private >*/
111const gchar * const *
112g_content_type_get_mime_dirs (void)
113{
114  const gchar * const *mime_dirs = { NULL };
115  return mime_dirs;
116}
117
118gboolean
119g_content_type_equals (const gchar *type1,
120                       const gchar *type2)
121{
122  CFStringRef str1, str2;
123  gboolean ret;
124
125  g_return_val_if_fail (type1 != NULL, FALSE);
126  g_return_val_if_fail (type2 != NULL, FALSE);
127
128  if (g_ascii_strcasecmp (type1, type2) == 0)
129    return TRUE;
130
131  str1 = create_cfstring_from_cstr (type1);
132  str2 = create_cfstring_from_cstr (type2);
133
134  ret = UTTypeEqual (str1, str2);
135
136  CFRelease (str1);
137  CFRelease (str2);
138
139  return ret;
140}
141
142gboolean
143g_content_type_is_a (const gchar *ctype,
144                     const gchar *csupertype)
145{
146  CFStringRef type, supertype;
147  gboolean ret;
148
149  g_return_val_if_fail (ctype != NULL, FALSE);
150  g_return_val_if_fail (csupertype != NULL, FALSE);
151
152  type = create_cfstring_from_cstr (ctype);
153  supertype = create_cfstring_from_cstr (csupertype);
154
155  ret = UTTypeConformsTo (type, supertype);
156
157  CFRelease (type);
158  CFRelease (supertype);
159
160  return ret;
161}
162
163gboolean
164g_content_type_is_mime_type (const gchar *type,
165                             const gchar *mime_type)
166{
167  gchar *content_type;
168  gboolean ret;
169
170  g_return_val_if_fail (type != NULL, FALSE);
171  g_return_val_if_fail (mime_type != NULL, FALSE);
172
173  content_type = g_content_type_from_mime_type (mime_type);
174  ret = g_content_type_is_a (type, content_type);
175  g_free (content_type);
176
177  return ret;
178}
179
180gboolean
181g_content_type_is_unknown (const gchar *type)
182{
183  g_return_val_if_fail (type != NULL, FALSE);
184
185  /* Should dynamic types be considered "unknown"? */
186  if (g_str_has_prefix (type, "dyn."))
187    return TRUE;
188  /* application/octet-stream */
189  else if (g_strcmp0 (type, "public.data") == 0)
190    return TRUE;
191
192  return FALSE;
193}
194
195gchar *
196g_content_type_get_description (const gchar *type)
197{
198  CFStringRef str;
199  CFStringRef desc_str;
200
201  g_return_val_if_fail (type != NULL, NULL);
202
203  str = create_cfstring_from_cstr (type);
204  desc_str = UTTypeCopyDescription (str);
205
206  CFRelease (str);
207  return create_cstr_from_cfstring_with_fallback (desc_str, "unknown");
208}
209
210/* <internal>
211 * _get_generic_icon_name_from_mime_type
212 *
213 * This function produces a generic icon name from a @mime_type.
214 * If no generic icon name is found in the xdg mime database, the
215 * generic icon name is constructed.
216 *
217 * Background:
218 * generic-icon elements specify the icon to use as a generic icon for this
219 * particular mime-type, given by the name attribute. This is used if there
220 * is no specific icon (see icon for how these are found). These are used
221 * for categories of similar types (like spreadsheets or archives) that can
222 * use a common icon. The Icon Naming Specification lists a set of such
223 * icon names. If this element is not specified then the mimetype is used
224 * to generate the generic icon by using the top-level media type
225 * (e.g. "video" in "video/ogg") and appending "-x-generic"
226 * (i.e. "video-x-generic" in the previous example).
227 *
228 * From: https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-0.18.html
229 */
230
231static gchar *
232_get_generic_icon_name_from_mime_type (const gchar *mime_type)
233{
234  const gchar *xdg_icon_name;
235  gchar *icon_name;
236
237  G_LOCK (gio_xdgmime);
238  xdg_icon_name = xdg_mime_get_generic_icon (mime_type);
239  G_UNLOCK (gio_xdgmime);
240
241  if (xdg_icon_name == NULL)
242    {
243      const char *p;
244      const char *suffix = "-x-generic";
245      gsize prefix_len;
246
247      p = strchr (mime_type, '/');
248      if (p == NULL)
249        prefix_len = strlen (mime_type);
250      else
251        prefix_len = p - mime_type;
252
253      icon_name = g_malloc (prefix_len + strlen (suffix) + 1);
254      memcpy (icon_name, mime_type, prefix_len);
255      memcpy (icon_name + prefix_len, suffix, strlen (suffix));
256      icon_name[prefix_len + strlen (suffix)] = 0;
257    }
258  else
259    {
260      icon_name = g_strdup (xdg_icon_name);
261    }
262
263  return icon_name;
264}
265
266
267static GIcon *
268g_content_type_get_icon_internal (const gchar *uti,
269                                  gboolean     symbolic)
270{
271  char *mimetype_icon;
272  char *mime_type;
273  char *generic_mimetype_icon = NULL;
274  char *q;
275  char *icon_names[6];
276  int n = 0;
277  GIcon *themed_icon;
278  const char  *xdg_icon;
279  int i;
280
281  g_return_val_if_fail (uti != NULL, NULL);
282
283  mime_type = g_content_type_get_mime_type (uti);
284
285  G_LOCK (gio_xdgmime);
286  xdg_icon = xdg_mime_get_icon (mime_type);
287  G_UNLOCK (gio_xdgmime);
288
289  if (xdg_icon)
290    icon_names[n++] = g_strdup (xdg_icon);
291
292  mimetype_icon = g_strdup (mime_type);
293  while ((q = strchr (mimetype_icon, '/')) != NULL)
294    *q = '-';
295
296  icon_names[n++] = mimetype_icon;
297
298  generic_mimetype_icon = _get_generic_icon_name_from_mime_type (mime_type);
299
300  if (generic_mimetype_icon)
301    icon_names[n++] = generic_mimetype_icon;
302
303  if (symbolic)
304    {
305      for (i = 0; i < n; i++)
306        {
307          icon_names[n + i] = icon_names[i];
308          icon_names[i] = g_strconcat (icon_names[i], "-symbolic", NULL);
309        }
310
311      n += n;
312    }
313
314  themed_icon = g_themed_icon_new_from_names (icon_names, n);
315
316  for (i = 0; i < n; i++)
317    g_free (icon_names[i]);
318
319  g_free(mime_type);
320
321  return themed_icon;
322}
323
324GIcon *
325g_content_type_get_icon (const gchar *type)
326{
327  return g_content_type_get_icon_internal (type, FALSE);
328}
329
330GIcon *
331g_content_type_get_symbolic_icon (const gchar *type)
332{
333  return g_content_type_get_icon_internal (type, TRUE);
334}
335
336gchar *
337g_content_type_get_generic_icon_name (const gchar *type)
338{
339  return NULL;
340}
341
342gboolean
343g_content_type_can_be_executable (const gchar *type)
344{
345  CFStringRef uti;
346  gboolean ret = FALSE;
347
348  g_return_val_if_fail (type != NULL, FALSE);
349
350  uti = create_cfstring_from_cstr (type);
351
352  if (UTTypeConformsTo (uti, kUTTypeApplication))
353    ret = TRUE;
354  else if (UTTypeConformsTo (uti, CFSTR("public.executable")))
355    ret = TRUE;
356  else if (UTTypeConformsTo (uti, CFSTR("public.script")))
357    ret = TRUE;
358  /* Our tests assert that all text can be executable... */
359  else if (UTTypeConformsTo (uti, CFSTR("public.text")))
360      ret = TRUE;
361
362  CFRelease (uti);
363  return ret;
364}
365
366gchar *
367g_content_type_from_mime_type (const gchar *mime_type)
368{
369  CFStringRef mime_str;
370  CFStringRef uti_str;
371
372  g_return_val_if_fail (mime_type != NULL, NULL);
373
374  /* Their api does not handle globs but they are common. */
375  if (g_str_has_suffix (mime_type, "*"))
376    {
377      if (g_str_has_prefix (mime_type, "audio"))
378        return g_strdup ("public.audio");
379      if (g_str_has_prefix (mime_type, "image"))
380        return g_strdup ("public.image");
381      if (g_str_has_prefix (mime_type, "text"))
382        return g_strdup ("public.text");
383      if (g_str_has_prefix (mime_type, "video"))
384        return g_strdup ("public.movie");
385    }
386
387  /* Some exceptions are needed for gdk-pixbuf.
388   * This list is not exhaustive.
389   */
390  if (g_str_has_prefix (mime_type, "image"))
391    {
392      if (g_str_has_suffix (mime_type, "x-icns"))
393        return g_strdup ("com.apple.icns");
394      if (g_str_has_suffix (mime_type, "x-tga"))
395        return g_strdup ("com.truevision.tga-image");
396      if (g_str_has_suffix (mime_type, "x-ico"))
397        return g_strdup ("com.microsoft.ico ");
398    }
399
400  /* These are also not supported...
401   * Used in glocalfileinfo.c
402   */
403  if (g_str_has_prefix (mime_type, "inode"))
404    {
405      if (g_str_has_suffix (mime_type, "directory"))
406        return g_strdup ("public.folder");
407      if (g_str_has_suffix (mime_type, "symlink"))
408        return g_strdup ("public.symlink");
409    }
410
411  /* This is correct according to the Apple docs:
412     https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
413  */
414  if (strcmp (mime_type, "text/plain") == 0)
415    return g_strdup ("public.text");
416
417  /* Non standard type */
418  if (strcmp (mime_type, "application/x-executable") == 0)
419    return g_strdup ("public.executable");
420
421  mime_str = create_cfstring_from_cstr (mime_type);
422  uti_str = UTTypeCreatePreferredIdentifierForTag (kUTTagClassMIMEType, mime_str, NULL);
423
424  CFRelease (mime_str);
425  return create_cstr_from_cfstring_with_fallback (uti_str, "public.data");
426}
427
428gchar *
429g_content_type_get_mime_type (const gchar *type)
430{
431  CFStringRef uti_str;
432  CFStringRef mime_str;
433
434  g_return_val_if_fail (type != NULL, NULL);
435
436  /* We must match the additions above
437   * so conversions back and forth work.
438   */
439  if (g_str_has_prefix (type, "public"))
440    {
441      if (g_str_has_suffix (type, ".image"))
442        return g_strdup ("image/*");
443      if (g_str_has_suffix (type, ".movie"))
444        return g_strdup ("video/*");
445      if (g_str_has_suffix (type, ".text"))
446        return g_strdup ("text/*");
447      if (g_str_has_suffix (type, ".audio"))
448        return g_strdup ("audio/*");
449      if (g_str_has_suffix (type, ".folder"))
450        return g_strdup ("inode/directory");
451      if (g_str_has_suffix (type, ".symlink"))
452        return g_strdup ("inode/symlink");
453      if (g_str_has_suffix (type, ".executable"))
454        return g_strdup ("application/x-executable");
455    }
456
457  uti_str = create_cfstring_from_cstr (type);
458  mime_str = UTTypeCopyPreferredTagWithClass(uti_str, kUTTagClassMIMEType);
459
460  CFRelease (uti_str);
461  return create_cstr_from_cfstring_with_fallback (mime_str, "application/octet-stream");
462}
463
464static gboolean
465looks_like_text (const guchar *data,
466                 gsize         data_size)
467{
468  gsize i;
469  guchar c;
470
471  for (i = 0; i < data_size; i++)
472    {
473      c = data[i];
474      if (g_ascii_iscntrl (c) && !g_ascii_isspace (c) && c != '\b')
475        return FALSE;
476    }
477  return TRUE;
478}
479
480gchar *
481g_content_type_guess (const gchar  *filename,
482                      const guchar *data,
483                      gsize         data_size,
484                      gboolean     *result_uncertain)
485{
486  CFStringRef uti = NULL;
487  gchar *cextension;
488  CFStringRef extension;
489  int uncertain = -1;
490
491  g_return_val_if_fail (data_size != (gsize) -1, NULL);
492
493  if (filename && *filename)
494    {
495      gchar *basename = g_path_get_basename (filename);
496      gchar *dirname = g_path_get_dirname (filename);
497      gsize i = strlen (filename);
498
499      if (filename[i - 1] == '/')
500        {
501          if (g_strcmp0 (dirname, "/Volumes") == 0)
502            {
503              uti = CFStringCreateCopy (NULL, kUTTypeVolume);
504            }
505          else if ((cextension = strrchr (basename, '.')) != NULL)
506            {
507              cextension++;
508              extension = create_cfstring_from_cstr (cextension);
509              uti = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension,
510                                                           extension, NULL);
511              CFRelease (extension);
512
513              if (CFStringHasPrefix (uti, CFSTR ("dyn.")))
514                {
515                  CFRelease (uti);
516                  uti = CFStringCreateCopy (NULL, kUTTypeFolder);
517                  uncertain = TRUE;
518                }
519            }
520          else
521            {
522              uti = CFStringCreateCopy (NULL, kUTTypeFolder);
523              uncertain = TRUE; /* Matches Unix backend */
524            }
525        }
526      else
527        {
528          /* GTK needs this... */
529          if (g_str_has_suffix (basename, ".ui"))
530            {
531              uti = CFStringCreateCopy (NULL, kUTTypeXML);
532            }
533          else if (g_str_has_suffix (basename, ".txt"))
534            {
535              uti = CFStringCreateCopy (NULL, CFSTR ("public.text"));
536            }
537          else if ((cextension = strrchr (basename, '.')) != NULL)
538            {
539              cextension++;
540              extension = create_cfstring_from_cstr (cextension);
541              uti = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension,
542                                                           extension, NULL);
543              CFRelease (extension);
544            }
545          g_free (basename);
546          g_free (dirname);
547        }
548    }
549  if (data && (!filename || !uti ||
550               CFStringCompare (uti, CFSTR ("public.data"), 0) == kCFCompareEqualTo))
551    {
552      const char *sniffed_mimetype;
553      G_LOCK (gio_xdgmime);
554      sniffed_mimetype = xdg_mime_get_mime_type_for_data (data, data_size, NULL);
555      G_UNLOCK (gio_xdgmime);
556      if (sniffed_mimetype != XDG_MIME_TYPE_UNKNOWN)
557        {
558          gchar *uti_str = g_content_type_from_mime_type (sniffed_mimetype);
559          uti = create_cfstring_from_cstr (uti_str);
560          g_free (uti_str);
561        }
562      if (!uti && looks_like_text (data, data_size))
563        {
564          if (g_str_has_prefix ((const gchar*)data, "#!/"))
565            uti = CFStringCreateCopy (NULL, CFSTR ("public.script"));
566          else
567            uti = CFStringCreateCopy (NULL, CFSTR ("public.text"));
568        }
569    }
570
571  if (!uti)
572    {
573      /* Generic data type */
574      uti = CFStringCreateCopy (NULL, CFSTR ("public.data"));
575      if (result_uncertain)
576        *result_uncertain = TRUE;
577    }
578  else if (result_uncertain)
579    {
580      *result_uncertain = uncertain == -1 ? FALSE : uncertain;
581    }
582
583  return create_cstr_from_cfstring (uti);
584}
585
586GList *
587g_content_types_get_registered (void)
588{
589  /* TODO: UTTypeCreateAllIdentifiersForTag? */
590  return NULL;
591}
592
593gchar **
594g_content_type_guess_for_tree (GFile *root)
595{
596  return NULL;
597}
598