1 /** 2 * Finding associations between MIME types and applications. 3 * Authors: 4 * $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov) 5 * Copyright: 6 * Roman Chistokhodov, 2016 7 * License: 8 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 9 * See_Also: 10 * $(LINK2 https://www.freedesktop.org/wiki/Specifications/mime-apps-spec/, MIME Applications Associations) 11 */ 12 13 module mimeapps; 14 15 private { 16 import std.algorithm; 17 import std.array; 18 import std.exception; 19 import std.file; 20 import std.path; 21 import std.traits; 22 23 import xdgpaths; 24 import isfreedesktop; 25 } 26 27 public import desktopfile.file; 28 29 private @trusted auto parseMimeTypeName(String)(String name) if (isSomeString!String && is(ElementEncodingType!String : char)) 30 { 31 alias Tuple!(String, "media", String, "subtype") MimeTypeName; 32 33 String media; 34 String subtype; 35 36 size_t i; 37 for (i=0; i<name.length; ++i) { 38 if (name[i] == '/') { 39 media = name[0..i]; 40 subtype = name[i+1..$]; 41 break; 42 } 43 } 44 45 return MimeTypeName(media, subtype); 46 } 47 48 unittest 49 { 50 auto t = parseMimeTypeName("text/plain"); 51 assert(t.media == "text" && t.subtype == "plain"); 52 53 t = parseMimeTypeName("not mime type"); 54 assert(t.media == string.init && t.subtype == string.init); 55 } 56 57 private @nogc @trusted bool allSymbolsAreValid(const(char)[] name) nothrow pure 58 { 59 import std.ascii : isAlpha, isDigit; 60 for (size_t i=0; i<name.length; ++i) { 61 char c = name[i]; 62 if (!(c.isAlpha || c.isDigit || c == '-' || c == '+' || c == '.' || c == '_')) { 63 return false; 64 } 65 } 66 return true; 67 } 68 69 private @nogc @safe bool isValidMimeTypeName(const(char)[] name) nothrow pure 70 { 71 auto t = parseMimeTypeName(name); 72 return t.media.length && t.subtype.length && allSymbolsAreValid(t.media) && allSymbolsAreValid(t.subtype); 73 } 74 75 unittest 76 { 77 assert( isValidMimeTypeName("text/plain")); 78 assert( isValidMimeTypeName("text/plain2")); 79 assert( isValidMimeTypeName("text/vnd.type")); 80 assert( isValidMimeTypeName("x-scheme-handler/http")); 81 assert(!isValidMimeTypeName("not mime type")); 82 assert(!isValidMimeTypeName("not()/valid")); 83 assert(!isValidMimeTypeName("not/valid{}")); 84 assert(!isValidMimeTypeName("text/")); 85 assert(!isValidMimeTypeName("/plain")); 86 assert(!isValidMimeTypeName("/")); 87 } 88 89 private @trusted void validateMimeType(string mimeType) { 90 if (!isValidMimeTypeName(mimeType)) { 91 throw new Exception("Invalid MIME type name"); 92 } 93 } 94 95 static if (isFreedesktop) 96 { 97 /** 98 * Find all known mimeapps.list files locations. Found paths are not checked for existence. 99 * Returns: Paths of mimeapps.list files in the system. 100 * Note: This function is available only on Freedesktop. 101 * See_Also: $(LINK2 https://specifications.freedesktop.org/mime-apps-spec/latest/ar01s02.html, File name and location) 102 */ 103 @safe string[] mimeAppsListPaths() nothrow 104 { 105 enum mimeAppsList = "mimeapps.list"; 106 enum applicationsMimeAppsList = "applications/mimeapps.list"; 107 string configHome = xdgConfigHome(mimeAppsList); 108 string appHome = xdgDataHome(applicationsMimeAppsList); 109 110 string[] configPaths = xdgConfigDirs(mimeAppsList); 111 string[] appPaths = xdgDataDirs(applicationsMimeAppsList); 112 113 string[] toReturn; 114 if (configHome.length) { 115 toReturn ~= configHome; 116 } 117 if (appHome.length) { 118 toReturn ~= appHome; 119 } 120 return toReturn ~ configPaths ~ appPaths; 121 } 122 123 /** 124 * Find all known mimeinfo.cache files locations. Found paths are not checked for existence. 125 * Returns: Paths of mimeinfo.cache files in the system. 126 * Note: This function is available only on Freedesktop. 127 */ 128 @safe string[] mimeInfoCachePaths() nothrow 129 { 130 return xdgAllDataDirs("applications/mimeinfo.cache"); 131 } 132 } 133 134 /** 135 * IniLikeGroup subclass for easy access to the list of applications associated with given type. 136 */ 137 final class MimeAppsGroup : IniLikeGroup 138 { 139 protected @nogc @safe this(string groupName) nothrow { 140 super(groupName); 141 } 142 143 /** 144 * Split string list of desktop ids into range. 145 */ 146 static @trusted auto splitApps(string apps) { 147 return std.algorithm.splitter(apps, ";").filter!(s => !s.empty); 148 } 149 150 /// 151 unittest 152 { 153 assert(splitApps("kde4-kate.desktop;kde4-kwrite.desktop;geany.desktop;").equal(["kde4-kate.desktop", "kde4-kwrite.desktop", "geany.desktop"])); 154 } 155 156 /** 157 * List applications for given mimeType. 158 * Returns: Range of $(B Desktop id)s for mimeType. 159 */ 160 @safe auto appsForMimeType(string mimeType) const { 161 return splitApps(value(mimeType)); 162 } 163 164 protected: 165 @trusted override void validateKeyValue(string key, string value) const { 166 validateMimeType(key); 167 } 168 } 169 170 /** 171 * Class represenation of single mimeapps.list file containing information about MIME type associations and default applications. 172 */ 173 final class MimeAppsListFile : IniLikeFile 174 { 175 /** 176 * Read mimeapps.list file. 177 * Throws: 178 * $(B ErrnoException) if file could not be opened. 179 * $(B IniLikeException) if error occured while reading the file or "MIME Cache" group is missing. 180 */ 181 @trusted this(string fileName) 182 { 183 this(iniLikeFileReader(fileName), fileName); 184 } 185 186 /** 187 * Read MIME type associations from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. 188 * Throws: 189 * $(B IniLikeException) if error occured while parsing or "MIME Cache" group is missing. 190 */ 191 this(IniLikeReader)(IniLikeReader reader, string fileName = null) 192 { 193 super(reader, fileName); 194 } 195 196 /** 197 * Access "Desktop Applications" group. 198 * Returns: MimeAppsGroup for "Desktop Applications" group or null if file does not have such group. 199 */ 200 @safe inout(MimeAppsGroup) defaultApplications() nothrow inout { 201 return _defaultApps; 202 } 203 204 /** 205 * Access "Added Associations" group. 206 * Returns: MimeAppsGroup for "Added Associations" group or null if file does not have such group. 207 */ 208 @safe inout(MimeAppsGroup) addedAssociations() nothrow inout { 209 return _addedApps; 210 } 211 212 /** 213 * Access "Removed Associations" group. 214 * Returns: MimeAppsGroup for "Removed Associations" group or null if file does not have such group. 215 */ 216 @safe inout(MimeAppsGroup) removedAssociations() nothrow inout { 217 return _removedApps; 218 } 219 220 protected: 221 @trusted override void addCommentForGroup(string comment, IniLikeGroup currentGroup, string groupName) { 222 return; 223 } 224 225 @trusted override void addKeyValueForGroup(string key, string value, IniLikeGroup currentGroup, string groupName) 226 { 227 if (currentGroup) { 228 if (currentGroup.contains(key)) { 229 return; 230 } 231 currentGroup[key] = value; 232 } 233 } 234 235 @trusted override IniLikeGroup createGroup(string groupName) 236 { 237 auto existent = group(groupName); 238 if (existent !is null) { 239 return existent; 240 } else { 241 if (groupName == "Default Applications") { 242 _defaultApps = new MimeAppsGroup(groupName); 243 return _defaultApps; 244 } else if (groupName == "Added Associations") { 245 _addedApps = new MimeAppsGroup(groupName); 246 return _addedApps; 247 } else if (groupName == "Removed Associations") { 248 _removedApps = new MimeAppsGroup(groupName); 249 return _removedApps; 250 } else { 251 return null; 252 } 253 } 254 } 255 256 private: 257 MimeAppsGroup _addedApps; 258 MimeAppsGroup _removedApps; 259 MimeAppsGroup _defaultApps; 260 } 261 262 /// 263 unittest 264 { 265 string content = 266 `[Added Associations] 267 text/plain=geany.desktop;kde4-kwrite.desktop; 268 image/png=kde4-gwenview.desktop;gthumb.desktop; 269 270 [Removed Associations] 271 text/plain=libreoffice-writer.desktop; 272 273 [Default Applications] 274 text/plain=kde4-kate.desktop 275 x-scheme-handler/http=chromium.desktop;iceweasel.desktop; 276 `; 277 auto mimeAppsList = new MimeAppsListFile(iniLikeStringReader(content)); 278 assert(mimeAppsList.addedAssociations() !is null); 279 assert(mimeAppsList.removedAssociations() !is null); 280 assert(mimeAppsList.defaultApplications() !is null); 281 282 assert(mimeAppsList.addedAssociations().appsForMimeType("text/plain").equal(["geany.desktop", "kde4-kwrite.desktop"])); 283 assert(mimeAppsList.removedAssociations().appsForMimeType("text/plain").equal(["libreoffice-writer.desktop"])); 284 assert(mimeAppsList.defaultApplications().appsForMimeType("x-scheme-handler/http").equal(["chromium.desktop", "iceweasel.desktop"])); 285 } 286 287 /** 288 * Class represenation of single mimeinfo.cache file containing information about MIME type associations. 289 */ 290 final class MimeInfoCacheFile : IniLikeFile 291 { 292 /** 293 * Read MIME Cache from file. 294 * Throws: 295 * $(B ErrnoException) if file could not be opened. 296 * $(B IniLikeException) if error occured while reading the file or "MIME Cache" group is missing. 297 */ 298 @trusted this(string fileName) 299 { 300 this(iniLikeFileReader(fileName), fileName); 301 } 302 303 /** 304 * Constructs MimeInfoCacheFile with empty MIME Cache group. 305 */ 306 @safe this() { 307 super(); 308 addGroup("MIME Cache"); 309 } 310 311 /// 312 unittest 313 { 314 auto micf = new MimeInfoCacheFile(); 315 assert(micf.mimeCache() !is null); 316 } 317 318 /** 319 * Read MIME Cache from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. 320 * Throws: 321 * $(B IniLikeException) if error occured while parsing or "MIME Cache" group is missing. 322 */ 323 this(IniLikeReader)(IniLikeReader reader, string fileName = null) 324 { 325 super(reader, fileName); 326 enforce(_mimeCache !is null, new IniLikeException("No \"MIME Cache\" group", 0)); 327 } 328 329 /** 330 * Access "MIME Cache" group. 331 */ 332 @safe inout(MimeAppsGroup) mimeCache() nothrow inout { 333 return _mimeCache; 334 } 335 336 /** 337 * Alias for easy access to "MIME Cache" group. 338 */ 339 alias mimeCache this; 340 341 protected: 342 @trusted override void addCommentForGroup(string comment, IniLikeGroup currentGroup, string groupName) { 343 return; 344 } 345 346 @trusted override void addKeyValueForGroup(string key, string value, IniLikeGroup currentGroup, string groupName) 347 { 348 if (currentGroup) { 349 if (currentGroup.contains(key)) { 350 return; 351 } 352 currentGroup[key] = value; 353 } 354 } 355 356 @trusted override IniLikeGroup createGroup(string groupName) 357 { 358 auto existent = group(groupName); 359 if (existent !is null) { 360 return existent; 361 } else { 362 if (groupName == "MIME Cache") { 363 _mimeCache = new MimeAppsGroup(groupName); 364 return _mimeCache; 365 } else { 366 return null; 367 } 368 } 369 } 370 private: 371 MimeAppsGroup _mimeCache; 372 } 373 374 /// 375 unittest 376 { 377 string content = 378 `[Some group] 379 Key=Value 380 `; 381 assertThrown!IniLikeException(new MimeInfoCacheFile(iniLikeStringReader(content))); 382 383 content = 384 `[MIME Cache] 385 text/plain=geany.desktop;kde4-kwrite.desktop; 386 image/png=kde4-gwenview.desktop;gthumb.desktop; 387 `; 388 389 auto mimeInfoCache = new MimeInfoCacheFile(iniLikeStringReader(content)); 390 assert(mimeInfoCache.appsForMimeType("text/plain").equal(["geany.desktop", "kde4-kwrite.desktop"])); 391 assert(mimeInfoCache.appsForMimeType("image/png").equal(["kde4-gwenview.desktop", "gthumb.desktop"])); 392 assert(mimeInfoCache.appsForMimeType("application/nonexistent").empty); 393 394 content = 395 `[MIME Cache] 396 text/plain=geany.desktop; 397 notmimetype=value 398 `; 399 assertThrown!IniLikeException(new MimeInfoCacheFile(iniLikeStringReader(content))); 400 } 401 402 /** 403 * Create MimeAppsListFile objects for paths. 404 * Returns: Array of MimeAppsListFile objects read from paths. If some could not be read it's not included in the results. 405 */ 406 @trusted MimeAppsListFile[] mimeAppsListFiles(const(string)[] paths) nothrow 407 { 408 return paths.map!(function(string path) { 409 MimeAppsListFile file; 410 collectException(new MimeAppsListFile(path), file); 411 return file; 412 }).filter!(file => file !is null).array; 413 } 414 415 static if (isFreedesktop) 416 { 417 /** 418 * ditto, but automatically read MimeAppsListFile objects from determined system paths. 419 * Note: Available only on Freedesktop. 420 */ 421 @safe MimeAppsListFile[] mimeAppsListFiles() nothrow { 422 return mimeAppsListFiles(mimeAppsListPaths()); 423 } 424 } 425 426 /** 427 * Create MimeInfoCacheFile objects for paths. 428 * Returns: Array of MimeInfoCacheFile objects read from paths. If some could not be read it's not included in the results. 429 */ 430 @trusted MimeInfoCacheFile[] mimeInfoCacheFiles(const(string)[] paths) nothrow 431 { 432 return paths.map!(function(string path) { 433 MimeInfoCacheFile file; 434 collectException(new MimeInfoCacheFile(path), file); 435 return file; 436 }).filter!(file => file !is null).array; 437 } 438 439 static if (isFreedesktop) 440 { 441 /** 442 * ditto, but automatically read MimeInfoCacheFile objects from determined system paths. 443 * Note: Available only on Freedesktop. 444 */ 445 @safe MimeInfoCacheFile[] mimeInfoCacheFiles() nothrow { 446 return mimeInfoCacheFiles(mimeInfoCachePaths()); 447 } 448 } 449 450 /** 451 * Interface for desktop file provider. 452 * See_Also: findAssociatedApplications, findKnownAssociatedApplications, findDefaultApplication 453 */ 454 interface IDesktopFileProvider 455 { 456 /** 457 * Retrieve DesktopFile by desktopId 458 * Returns: Found DesktopFile or null if not found. 459 */ 460 const(DesktopFile) getByDesktopId(string desktopId); 461 462 /** 463 * Update internal information, e.g. re-read cached .desktop files if needed. 464 */ 465 void update(); 466 } 467 468 /** 469 * Implementation of desktop file provider. 470 */ 471 class DesktopFileProvider : IDesktopFileProvider 472 { 473 private: 474 import std.datetime : SysTime; 475 476 static struct DesktopFileItem 477 { 478 DesktopFile desktopFile; 479 SysTime time; 480 string baseDir; 481 SysTime baseDirTime; 482 } 483 484 static struct BaseDirItem 485 { 486 string path; 487 SysTime time; 488 bool valid; 489 } 490 491 public: 492 /** 493 * Construct using applicationsPaths. Automatically calls update. 494 * Params: 495 * applicationsPaths = Paths of applications/ directories where .desktop files are stored. 496 * options = Options used to read desktop files. 497 * These should be all known paths even if they don't exist at the time. 498 */ 499 @trusted this(const(string)[] applicationsPaths, DesktopFile.ReadOptions options = DesktopFile.defaultReadOptions) { 500 _baseDirItems = applicationsPaths.map!(p => BaseDirItem(p, SysTime.init, false)).array; 501 _readOptions = options; 502 update(); 503 } 504 505 override const(DesktopFile) getByDesktopId(string desktopId) 506 { 507 auto itemIn = desktopId in _cache; 508 if (itemIn) { 509 return itemIn.desktopFile; 510 } else { 511 auto foundItem = getDesktopFileItem(desktopId); 512 if (foundItem.desktopFile !is null) { 513 _cache[desktopId] = foundItem; 514 return foundItem.desktopFile; 515 } 516 } 517 return null; 518 } 519 520 override void update() 521 { 522 foreach (ref item; _baseDirItems) { 523 try { 524 SysTime accessTime; 525 getTimes(item.path, accessTime, item.time); 526 item.valid = item.path.isDir(); 527 } catch(Exception e) { 528 item.valid = false; 529 } 530 } 531 532 foreach(desktopId, item; _cache) { 533 SysTime modifyTime; 534 BaseDirItem baseDirItem; 535 string filePath = findDesktopFilePath(desktopId, modifyTime, baseDirItem); 536 537 if (filePath.length) { 538 if (item.time != modifyTime || item.baseDir != baseDirItem.path) { 539 try { 540 auto desktopFile = new DesktopFile(filePath, _readOptions); 541 _cache[desktopId] = DesktopFileItem(desktopFile, modifyTime, baseDirItem.path, baseDirItem.time); 542 } catch(Exception e) { 543 _cache.remove(desktopId); 544 } 545 } 546 } else { 547 _cache.remove(desktopId); 548 } 549 } 550 } 551 552 private: 553 DesktopFileItem getDesktopFileItem(string desktopId) 554 { 555 SysTime modifyTime; 556 BaseDirItem baseDirItem; 557 string filePath = findDesktopFilePath(desktopId, modifyTime, baseDirItem); 558 if (filePath.length) { 559 try { 560 auto desktopFile = new DesktopFile(filePath, _readOptions); 561 return DesktopFileItem(desktopFile, modifyTime, baseDirItem.path, baseDirItem.time); 562 } catch(Exception e) { 563 return DesktopFileItem.init; 564 } 565 } 566 return DesktopFileItem.init; 567 } 568 569 string findDesktopFilePath(string desktopId, out SysTime modifyTime, out BaseDirItem dirItem) 570 { 571 foreach(baseDirItem; _baseDirItems) { 572 if (!baseDirItem.valid) { 573 continue; 574 } 575 576 auto filePath = buildPath(baseDirItem.path, desktopId); 577 bool fileExists = filePath.exists; 578 if (!fileExists && filePath.canFind('-')) { 579 //try subdirectory 580 filePath = buildPath(baseDirItem.path, desktopId.replace("-", "/")); 581 fileExists = filePath.exists; 582 } 583 if (fileExists) { 584 try { 585 SysTime accessTime; 586 getTimes(filePath, accessTime, modifyTime); 587 dirItem = baseDirItem; 588 return filePath; 589 } catch(Exception e) { 590 return null; 591 } 592 } 593 } 594 return null; 595 } 596 597 DesktopFileItem[string] _cache; 598 BaseDirItem[] _baseDirItems; 599 DesktopFile.ReadOptions _readOptions; 600 } 601 602 private enum FindAssocFlag { 603 none = 0, 604 onlyFirst = 1, 605 ignoreRemovedGroup = 2 606 } 607 608 private const(DesktopFile)[] findAssociatedApplicationsImpl(ListRange, CacheRange)(string mimeType, ListRange mimeAppsListFiles, CacheRange mimeInfoCacheFiles, IDesktopFileProvider desktopFileProvider, FindAssocFlag flag = FindAssocFlag.none) 609 { 610 string[] removed; 611 const(DesktopFile)[] desktopFiles; 612 foreach(mimeAppsListFile; mimeAppsListFiles) { 613 if (mimeAppsListFile is null) { 614 continue; 615 } 616 617 auto removedAppsGroup = mimeAppsListFile.removedAssociations(); 618 if (removedAppsGroup !is null && !(flag & FindAssocFlag.ignoreRemovedGroup)) { 619 removed ~= removedAppsGroup.appsForMimeType(mimeType).array; 620 } 621 auto addedAppsGroup = mimeAppsListFile.addedAssociations(); 622 if (addedAppsGroup !is null) { 623 foreach(desktopId; addedAppsGroup.appsForMimeType(mimeType)) { 624 if (removed.canFind(desktopId)) { 625 continue; 626 } 627 auto desktopFile = desktopFileProvider.getByDesktopId(desktopId); 628 if (desktopFile) { 629 if (flag & FindAssocFlag.onlyFirst) { 630 return [desktopFile]; 631 } 632 desktopFiles ~= desktopFile; 633 } 634 removed ~= desktopId; 635 } 636 } 637 } 638 639 foreach(mimeInfoCacheFile; mimeInfoCacheFiles) { 640 if (mimeInfoCacheFile is null) { 641 continue; 642 } 643 644 foreach(desktopId; mimeInfoCacheFile.appsForMimeType(mimeType)) { 645 if (removed.canFind(desktopId)) { 646 continue; 647 } 648 auto desktopFile = desktopFileProvider.getByDesktopId(desktopId); 649 if (desktopFile) { 650 if (flag & FindAssocFlag.onlyFirst) { 651 return [desktopFile]; 652 } 653 desktopFiles ~= desktopFile; 654 } 655 removed ~= desktopId; 656 } 657 } 658 659 return desktopFiles; 660 } 661 662 /** 663 * Find associated applications for mimeType. 664 * Params: 665 * mimeType = MIME type or uri scheme handler in question. 666 * mimeAppsListFiles = Range of MimeAppsListFile objects to use in searching. 667 * mimeInfoCacheFiles = Range of MimeInfoCacheFile objects to use in searching. 668 * desktopFileProvider = desktop file provider instance. 669 * Returns: Array of found $(B DesktopFile) object capable of opening file of given MIME type or url of given scheme. 670 * See_Also: $(LINK2 https://specifications.freedesktop.org/mime-apps-spec/latest/ar01s03.html, Adding/removing associations) 671 */ 672 const(DesktopFile)[] findAssociatedApplications(ListRange, CacheRange)(string mimeType, ListRange mimeAppsListFiles, CacheRange mimeInfoCacheFiles, IDesktopFileProvider desktopFileProvider) 673 if(isForwardRange!ListRange && is(ElementType!ListRange : const(MimeAppsListFile)) 674 && isForwardRange!CacheRange && is(ElementType!CacheRange : const(MimeInfoCacheFile))) 675 in { 676 assert(desktopFileProvider !is null); 677 } 678 body { 679 return findAssociatedApplicationsImpl(mimeType, mimeAppsListFiles, mimeInfoCacheFiles, desktopFileProvider); 680 } 681 682 /** 683 * Find all known associated applications for mimeType, including explicitly removed by user. 684 * Params: 685 * mimeType = MIME type or uri scheme handler in question. 686 * mimeAppsListFiles = Range of MimeAppsListFile objects to use in searching. 687 * mimeInfoCacheFiles = Range of MimeInfoCacheFile objects to use in searching. 688 * desktopFileProvider = desktop file provider instance. 689 * Returns: Array of found $(B DesktopFile) object capable of opening file of given MIME type or url of given scheme. 690 */ 691 const(DesktopFile)[] findKnownAssociatedApplications(ListRange, CacheRange)(string mimeType, ListRange mimeAppsListFiles, CacheRange mimeInfoCacheFiles, IDesktopFileProvider desktopFileProvider) 692 { 693 return findAssociatedApplicationsImpl(mimeType, mimeAppsListFiles, mimeInfoCacheFiles, desktopFileProvider, FindAssocFlag.ignoreRemovedGroup); 694 } 695 696 /** 697 * Find default application for mimeType. 698 * Params: 699 * mimeType = MIME type or uri scheme handler in question. 700 * mimeAppsListFiles = Range of MimeAppsListFile objects to use in searching. 701 * mimeInfoCacheFiles = Range of MimeInfoCacheFile objects to use in searching. 702 * desktopFileProvider = desktop file provider instance. Must be non-null. 703 * Returns: Found $(B DesktopFile) or null if not found. 704 * Note: In real world you probably will need to call this function on parent MIME type if it fails for original mimeType. 705 * See_Also: $(LINK2 https://specifications.freedesktop.org/mime-apps-spec/latest/ar01s04.html, Default Application) 706 */ 707 const(DesktopFile) findDefaultApplication(ListRange, CacheRange)(string mimeType, ListRange mimeAppsListFiles, CacheRange mimeInfoCacheFiles, IDesktopFileProvider desktopFileProvider) 708 if(isForwardRange!ListRange && is(ElementType!ListRange : const(MimeAppsListFile)) 709 && isForwardRange!CacheRange && is(ElementType!CacheRange : const(MimeInfoCacheFile))) 710 in { 711 assert(desktopFileProvider !is null); 712 } 713 body { 714 foreach(mimeAppsListFile; mimeAppsListFiles) { 715 if (mimeAppsListFile is null) { 716 continue; 717 } 718 auto defaultAppsGroup = mimeAppsListFile.defaultApplications(); 719 if (defaultAppsGroup !is null) { 720 foreach(desktopId; defaultAppsGroup.appsForMimeType(mimeType)) { 721 auto desktopFile = desktopFileProvider.getByDesktopId(desktopId); 722 if (desktopFile !is null) { 723 return desktopFile; 724 } 725 } 726 } 727 } 728 729 auto desktopFiles = findAssociatedApplicationsImpl(mimeType, mimeAppsListFiles, mimeInfoCacheFiles, desktopFileProvider, FindAssocFlag.onlyFirst); 730 return desktopFiles.length ? desktopFiles.front : null; 731 }