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