1 /** 2 * Functions for retrieving standard paths in cross-platform manner. 3 * 4 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 5 */ 6 7 module standardpaths; 8 9 private { 10 import std.process : environment; 11 import std.array; 12 import std.path; 13 import std.file; 14 import std.algorithm : splitter, canFind; 15 import std.exception; 16 17 debug { 18 import std.stdio : stderr; 19 } 20 } 21 22 version(Windows) { 23 private { 24 import std.c.windows.windows; 25 import std.utf; 26 import std.uni : toLower, sicmp; 27 } 28 } else version(Posix) { 29 private { 30 import std.stdio : File, StdioException; 31 import std.conv : octal; 32 import std.string : toStringz, fromStringz; 33 } 34 } else { 35 static assert(false, "Unsupported platform"); 36 } 37 38 /** 39 * Locations that can be passed to writablePath and standardPaths functions. 40 * See_Also: 41 * writablePath, standardPaths 42 */ 43 enum StandardPath { 44 /** 45 * Location of persisted application data. 46 */ 47 Data, 48 /** 49 * Location of configuration files. 50 * Note: on Windows it's the same as $(B Data) path. 51 */ 52 Config, 53 /** 54 * Location of cached data. 55 * Note: on Windows it's the same as $(B Data)/cache. 56 */ 57 Cache, 58 Desktop, ///User's desktop directory 59 Documents, ///User's documents 60 Pictures, ///User's pictures 61 Music, ///User's music 62 Videos, ///User's videos (movies) 63 64 /** 65 * Directory for user's downloaded files. 66 * Note: currently always return null on Windows. 67 */ 68 Download, 69 Templates, ///Location of templates. 70 71 /** 72 * Public share folder. 73 * Note: available only on systems with xdg-user-dirs (Linux, FreeBSD) 74 */ 75 PublicShare, 76 /** 77 * Location of fonts files. 78 * Note: don't relie on this on freedesktop. Better consider using $(LINK2 http://www.freedesktop.org/wiki/Software/fontconfig/, fontconfig library) 79 */ 80 Fonts, 81 Applications, ///User's applications. 82 } 83 84 /** 85 * Returns: path to user home directory, or an empty string if could not determine home directory. 86 * Relies on environment variables. 87 * Note: this function does not provide caching of its result. 88 */ 89 string homeDir() nothrow @safe 90 { 91 try { 92 version(Windows) { 93 //Use GetUserProfileDirectoryW from Userenv.dll? 94 string home = environment.get("USERPROFILE"); 95 if (home.empty) { 96 string homeDrive = environment.get("HOMEDRIVE"); 97 string homePath = environment.get("HOMEPATH"); 98 if (homeDrive.length && homePath.length) { 99 home = homeDrive ~ homePath; 100 } 101 } 102 return home; 103 } else { 104 string home = environment.get("HOME"); 105 return home; 106 } 107 } 108 catch (Exception e) { 109 debug collectException(stderr.writefln("Error when getting home directory %s", e.msg)); 110 return null; 111 } 112 } 113 114 /** 115 * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path. 116 * This function does not ensure if the returned path exists and appears to be accessible directory. 117 * Note: this function does not provide caching of its results. 118 */ 119 string writablePath(StandardPath type) nothrow @safe; 120 121 /** 122 * Returns: array of paths where files of $(U type) belong including one returned by $(B writablePath), or an empty array if no paths are defined for $(U type). 123 * This function does not ensure if all returned paths exist and appear to be accessible directories. 124 * Note: this function does not provide caching of its results. Also returned strings are not required to be unique. 125 * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation. 126 * See_Also: 127 * writablePath 128 */ 129 string[] standardPaths(StandardPath type) nothrow @safe; 130 131 version(Windows) { 132 private enum pathVarSeparator = ';'; 133 } else version(Posix) { 134 private enum pathVarSeparator = ':'; 135 } 136 137 version(Windows) { 138 139 private { 140 enum { 141 CSIDL_DESKTOP = 0, 142 CSIDL_INTERNET, 143 CSIDL_PROGRAMS, 144 CSIDL_CONTROLS, 145 CSIDL_PRINTERS, 146 CSIDL_PERSONAL, 147 CSIDL_FAVORITES, 148 CSIDL_STARTUP, 149 CSIDL_RECENT, 150 CSIDL_SENDTO, 151 CSIDL_BITBUCKET, 152 CSIDL_STARTMENU, // = 11 153 CSIDL_MYMUSIC = 13, 154 CSIDL_MYVIDEO, // = 14 155 CSIDL_DESKTOPDIRECTORY = 16, 156 CSIDL_DRIVES, 157 CSIDL_NETWORK, 158 CSIDL_NETHOOD, 159 CSIDL_FONTS, 160 CSIDL_TEMPLATES, 161 CSIDL_COMMON_STARTMENU, 162 CSIDL_COMMON_PROGRAMS, 163 CSIDL_COMMON_STARTUP, 164 CSIDL_COMMON_DESKTOPDIRECTORY, 165 CSIDL_APPDATA, 166 CSIDL_PRINTHOOD, 167 CSIDL_LOCAL_APPDATA, 168 CSIDL_ALTSTARTUP, 169 CSIDL_COMMON_ALTSTARTUP, 170 CSIDL_COMMON_FAVORITES, 171 CSIDL_INTERNET_CACHE, 172 CSIDL_COOKIES, 173 CSIDL_HISTORY, 174 CSIDL_COMMON_APPDATA, 175 CSIDL_WINDOWS, 176 CSIDL_SYSTEM, 177 CSIDL_PROGRAM_FILES, 178 CSIDL_MYPICTURES, 179 CSIDL_PROFILE, 180 CSIDL_SYSTEMX86, 181 CSIDL_PROGRAM_FILESX86, 182 CSIDL_PROGRAM_FILES_COMMON, 183 CSIDL_PROGRAM_FILES_COMMONX86, 184 CSIDL_COMMON_TEMPLATES, 185 CSIDL_COMMON_DOCUMENTS, 186 CSIDL_COMMON_ADMINTOOLS, 187 CSIDL_ADMINTOOLS, 188 CSIDL_CONNECTIONS, // = 49 189 CSIDL_COMMON_MUSIC = 53, 190 CSIDL_COMMON_PICTURES, 191 CSIDL_COMMON_VIDEO, 192 CSIDL_RESOURCES, 193 CSIDL_RESOURCES_LOCALIZED, 194 CSIDL_COMMON_OEM_LINKS, 195 CSIDL_CDBURN_AREA, // = 59 196 CSIDL_COMPUTERSNEARME = 61, 197 CSIDL_FLAG_DONT_VERIFY = 0x4000, 198 CSIDL_FLAG_CREATE = 0x8000, 199 CSIDL_FLAG_MASK = 0xFF00 200 } 201 } 202 203 private { 204 alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL) nothrow @nogc @system; 205 206 version(LinkedShell32) { 207 extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL) nothrow @nogc @system; 208 __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW; 209 } else { 210 __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = null; 211 } 212 213 bool hasSHGetSpecialFolderPath() nothrow @nogc @trusted { 214 return ptrSHGetSpecialFolderPath != null; 215 } 216 } 217 218 version(LinkedShell32) {} else { 219 shared static this() 220 { 221 HMODULE lib = LoadLibraryA("Shell32"); 222 if (lib) { 223 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW"); 224 } 225 } 226 } 227 228 229 private string getCSIDLFolder(int csidl) nothrow @trusted 230 { 231 import core.stdc.wchar_ : wcslen; 232 233 wchar[MAX_PATH] path = void; 234 if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) { 235 size_t len = wcslen(path.ptr); 236 try { 237 return toUTF8(path[0..len]); 238 } catch(Exception e) { 239 240 } 241 } 242 return null; 243 } 244 245 /// Path to $(B Roaming) data directory. This function is Windows only. 246 string roamingPath() nothrow @safe 247 { 248 return getCSIDLFolder(CSIDL_APPDATA); 249 } 250 251 string writablePath(StandardPath type) nothrow @safe 252 { 253 final switch(type) { 254 case StandardPath.Config: 255 case StandardPath.Data: 256 return getCSIDLFolder(CSIDL_LOCAL_APPDATA); 257 case StandardPath.Cache: 258 { 259 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA); 260 if (path.length) { 261 return buildPath(path, "cache"); 262 } 263 return null; 264 } 265 case StandardPath.Desktop: 266 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY); 267 case StandardPath.Documents: 268 return getCSIDLFolder(CSIDL_PERSONAL); 269 case StandardPath.Pictures: 270 return getCSIDLFolder(CSIDL_MYPICTURES); 271 case StandardPath.Music: 272 return getCSIDLFolder(CSIDL_MYMUSIC); 273 case StandardPath.Videos: 274 return getCSIDLFolder(CSIDL_MYVIDEO); 275 case StandardPath.Download: 276 return null; 277 case StandardPath.Templates: 278 return getCSIDLFolder(CSIDL_TEMPLATES); 279 case StandardPath.PublicShare: 280 return null; 281 case StandardPath.Fonts: 282 return null; 283 case StandardPath.Applications: 284 return getCSIDLFolder(CSIDL_PROGRAMS); 285 } 286 } 287 288 string[] standardPaths(StandardPath type) nothrow @safe 289 { 290 string commonPath; 291 292 switch(type) { 293 case StandardPath.Config: 294 case StandardPath.Data: 295 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA); 296 break; 297 case StandardPath.Desktop: 298 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY); 299 break; 300 case StandardPath.Documents: 301 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS); 302 break; 303 case StandardPath.Pictures: 304 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES); 305 break; 306 case StandardPath.Music: 307 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC); 308 break; 309 case StandardPath.Videos: 310 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO); 311 break; 312 case StandardPath.Templates: 313 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES); 314 break; 315 case StandardPath.Fonts: 316 commonPath = getCSIDLFolder(CSIDL_FONTS); 317 break; 318 case StandardPath.Applications: 319 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS); 320 break; 321 default: 322 break; 323 } 324 325 string[] paths; 326 string userPath = writablePath(type); 327 if (userPath.length) 328 paths ~= userPath; 329 if (commonPath.length) 330 paths ~= commonPath; 331 return paths; 332 } 333 334 private string[] executableExtensions() nothrow @trusted 335 { 336 static bool filenamesEqual(string first, string second) nothrow { 337 try { 338 return filenameCmp(first, second) == 0; 339 } catch(Exception e) { 340 return false; 341 } 342 } 343 344 string[] extensions; 345 try { 346 extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array; 347 if (canFind!(filenamesEqual)(extensions, ".exe") == false) { 348 extensions = []; 349 } 350 } catch (Exception e) { 351 352 } 353 if (extensions.empty) { 354 extensions = [".exe", ".com", ".bat", ".cmd"]; 355 } 356 return extensions; 357 } 358 } else version(OSX) { 359 360 private enum : short { 361 kOnSystemDisk = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/ 362 kOnAppropriateDisk = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/ 363 /* Folder Domains - Carbon only. The constants above can continue to be used, but the folder/volume returned will*/ 364 /* be from one of the domains below.*/ 365 kSystemDomain = -32766, /* Read-only system hierarchy.*/ 366 kLocalDomain = -32765, /* All users of a single machine have access to these resources.*/ 367 kNetworkDomain = -32764, /* All users configured to use a common network server has access to these resources.*/ 368 kUserDomain = -32763, /* Read/write. Resources that are private to the user.*/ 369 kClassicDomain = -32762, /* Domain referring to the currently configured Classic System Folder. Not supported in Mac OS X Leopard and later.*/ 370 kFolderManagerLastDomain = -32760 371 } 372 373 private int k(string s) nothrow @nogc { 374 return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]; 375 } 376 377 private enum { 378 kDesktopFolderType = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */ 379 kTrashFolderType = k("trsh"), /* the trash folder; objects in this folder show up in the trash */ 380 kWhereToEmptyTrashFolderType = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */ 381 kFontsFolderType = k("font"), /* Fonts go here */ 382 kPreferencesFolderType = k("pref"), /* preferences for applications go here */ 383 kSystemPreferencesFolderType = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */ 384 kTemporaryFolderType = k("temp"), /* On Mac OS X, each user has their own temporary items folder, and the Folder Manager attempts to set permissions of these*/ 385 /* folders such that other users can not access the data inside. On Mac OS X 10.4 and later the data inside the temporary*/ 386 /* items folder is deleted at logout and at boot, but not otherwise. Earlier version of Mac OS X would delete items inside*/ 387 /* the temporary items folder after a period of inaccess. You can ask for a temporary item in a specific domain or on a */ 388 /* particular volume by FSVolumeRefNum. If you want a location for temporary items for a short time, then use either*/ 389 /* ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ). The kUserDomain varient will always be*/ 390 /* on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/ 391 /* will probably be on the local hard drive in case the user's home is a network volume ). If you want a location for a temporary*/ 392 /* file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/ 393 /* ask for the temporary items folder on the same volume as the file you are safe saving.*/ 394 /* However, be prepared for a failure to find a temporary folder in any domain or on any volume. Some volumes may not have*/ 395 /* a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/ 396 /* a temporary folder for the volume.*/ 397 /* If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/ 398 /* and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/ 399 /* restarts. Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/ 400 /* when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/ 401 kChewableItemsFolderType = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */ 402 kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/ 403 kApplicationsFolderType = k("apps"), /* Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/ 404 kVolumeRootFolderType = k("root"), /* root folder of a volume or domain */ 405 kDomainTopLevelFolderType = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/ 406 kDomainLibraryFolderType = k("dlib"), /* the Library subfolder of a particular domain*/ 407 kUsersFolderType = k("usrs"), /* "Users" folder, usually contains one folder for each user. */ 408 kCurrentUserFolderType = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */ 409 kSharedUserDataFolderType = k("sdat"), /* A Shared folder, readable & writeable by all users */ 410 kCachedDataFolderType = k("cach"), /* Contains various cache files for different clients*/ 411 kDownloadsFolderType = k("down"), /* Refers to the ~/Downloads folder*/ 412 kApplicationSupportFolderType = k("asup"), /* third-party items and folders */ 413 414 415 kDocumentsFolderType = k("docs"), /* User documents are typically put in this folder ( or a subfolder ).*/ 416 kPictureDocumentsFolderType = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/ 417 kMovieDocumentsFolderType = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/ 418 kMusicDocumentsFolderType = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/ 419 kInternetSitesFolderType = k("site"), /* Refers to the "Sites" folder in a users home directory*/ 420 kPublicFolderType = k("pubb"), /* Refers to the "Public" folder in a users home directory*/ 421 422 kDropBoxFolderType = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/ 423 }; 424 425 private { 426 struct FSRef { 427 char[80] hidden; /* private to File Manager*/ 428 }; 429 430 alias int Boolean; 431 alias int OSType; 432 alias int OSerr; 433 434 alias da_FSFindFolder = extern(C) int function(short, int, int, FSRef*) nothrow @nogc @system; 435 alias da_FSRefMakePath = extern(C) int function(const(FSRef)*, char*, uint) nothrow @nogc @system; 436 437 __gshared da_FSFindFolder ptrFSFindFolder = null; 438 __gshared da_FSRefMakePath ptrFSRefMakePath = null; 439 } 440 441 shared static this() 442 { 443 enum carbonPath = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore"; 444 445 import core.sys.posix.dlfcn; 446 447 void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL); 448 if (handle) { 449 ptrFSFindFolder = cast(da_FSFindFolder)dlsym(handle, "FSFindFolder"); 450 ptrFSRefMakePath = cast(da_FSRefMakePath)dlsym(handle, "FSRefMakePath"); 451 } 452 if (ptrFSFindFolder == null || ptrFSRefMakePath == null) { 453 debug collectException(stderr.writeln("Could not load carbon functions")); 454 } 455 } 456 457 bool isCarbonLoaded() @trusted @nogc nothrow 458 { 459 return ptrFSFindFolder != null && ptrFSRefMakePath != null; 460 } 461 462 enum noErr = 0; 463 464 string fsPath(short domain, OSType type) nothrow @trusted 465 { 466 import std.stdio; 467 FSRef fsref; 468 if (isCarbonLoaded() && ptrFSFindFolder(domain, type, false, &fsref) == noErr) { 469 470 char[2048] buf; 471 char* path = buf.ptr; 472 if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) { 473 try { 474 475 return fromStringz(path).idup; 476 } 477 catch(Exception e) { 478 479 } 480 } 481 } 482 return null; 483 } 484 485 string writablePath(StandardPath type) nothrow @safe 486 { 487 final switch(type) { 488 case StandardPath.Config: 489 return fsPath(kUserDomain, kPreferencesFolderType); 490 case StandardPath.Cache: 491 return fsPath(kUserDomain, kCachedDataFolderType); 492 case StandardPath.Data: 493 return fsPath(kUserDomain, kApplicationSupportFolderType); 494 case StandardPath.Desktop: 495 return fsPath(kUserDomain, kDesktopFolderType); 496 case StandardPath.Documents: 497 return fsPath(kUserDomain, kDocumentsFolderType); 498 case StandardPath.Pictures: 499 return fsPath(kUserDomain, kPictureDocumentsFolderType); 500 case StandardPath.Music: 501 return fsPath(kUserDomain, kMusicDocumentsFolderType); 502 case StandardPath.Videos: 503 return fsPath(kUserDomain, kMovieDocumentsFolderType); 504 case StandardPath.Download: 505 return fsPath(kUserDomain, kDownloadsFolderType); 506 case StandardPath.Templates: 507 return null; 508 case StandardPath.PublicShare: 509 return fsPath(kUserDomain, kPublicFolderType ); 510 case StandardPath.Fonts: 511 return fsPath(kUserDomain, kFontsFolderType); 512 case StandardPath.Applications: 513 return fsPath(kUserDomain, kApplicationsFolderType); 514 } 515 } 516 517 string[] standardPaths(StandardPath type) nothrow @safe 518 { 519 string commonPath; 520 521 switch(type) { 522 case StandardPath.Fonts: 523 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 524 break; 525 case StandardPath.Applications: 526 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 527 break; 528 case StandardPath.Data: 529 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 530 break; 531 case StandardPath.Cache: 532 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 533 break; 534 default: 535 break; 536 } 537 538 string[] paths; 539 string userPath = writablePath(type); 540 if (userPath.length) 541 paths ~= userPath; 542 if (commonPath.length) 543 paths ~= commonPath; 544 return paths; 545 } 546 547 } else version(Posix) { 548 549 //Concat two strings, but if the first one is empty, then null string is returned. 550 private string maybeConcat(string start, string path) nothrow @safe 551 { 552 return start.empty ? null : start ~ path; 553 } 554 555 private string xdgBaseDir(in char[] envvar, string fallback) nothrow @trusted { 556 string dir; 557 collectException(environment.get(envvar), dir); 558 if (!dir.length) { 559 dir = maybeConcat(homeDir(), fallback); 560 } 561 return dir; 562 } 563 564 private string xdgUserDir(in char[] key, string fallback = null) nothrow @trusted { 565 import std.algorithm : startsWith; 566 import std.string : strip; 567 568 string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs"); 569 string home = homeDir(); 570 try { 571 auto f = File(fileName, "r"); 572 573 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 574 575 char[] buf; 576 while(f.readln(buf)) { 577 char[] line = strip(buf); 578 auto index = xdgdir.length; 579 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') { 580 line = line[index+1..$]; 581 if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 582 { 583 line = line[1..$-1]; 584 585 if (line.startsWith("$HOME")) { 586 return maybeConcat(home, assumeUnique(line[5..$])); 587 } 588 if (line.length == 0 || line[0] != '/') { 589 continue; 590 } 591 return assumeUnique(line); 592 } 593 } 594 } 595 } catch(Exception e) { 596 597 } 598 599 if (home.length) { 600 try { 601 auto f = File("/etc/xdg/user-dirs.defaults", "r"); 602 char[] buf; 603 while(f.readln(buf)) { 604 char[] line = strip(buf); 605 auto index = key.length; 606 if (line.startsWith(key) && line.length > index && line[index] == '=') 607 { 608 line = line[index+1..$]; 609 return home ~ "/" ~ assumeUnique(line); 610 } 611 } 612 } catch (Exception e) { 613 614 } 615 if (fallback.length) { 616 return home ~ fallback; 617 } 618 } 619 return null; 620 } 621 622 private string[] xdgConfigDirs() nothrow @trusted { 623 try { 624 string configDirs = environment.get("XDG_CONFIG_DIRS"); 625 if (configDirs.length) { 626 return splitter(configDirs, pathVarSeparator).array; 627 } 628 } 629 catch(Exception e) { 630 631 } 632 return ["/etc/xdg"]; 633 } 634 635 private string[] xdgDataDirs() nothrow @trusted { 636 try { 637 string dataDirs = environment.get("XDG_DATA_DIRS"); 638 if (dataDirs.length) { 639 return splitter(dataDirs, pathVarSeparator).array; 640 } 641 } catch(Exception e) { 642 643 } 644 return ["/usr/local/share", "/usr/share"]; 645 } 646 647 648 version(fontsconf) { 649 private string[] readFontsConfig(string configFile) nothrow @trusted 650 { 651 //Should be changed in future since std.xml is deprecated 652 import std.xml; 653 654 string[] paths; 655 try { 656 string contents = cast(string)read(configFile); 657 check(contents); 658 auto parser = new DocumentParser(contents); 659 parser.onEndTag["dir"] = (in Element xml) 660 { 661 string path = xml.text; 662 663 if (path.length && path[0] == '~') { 664 path = maybeConcat(homeDir(), path[1..$]); 665 } else { 666 const(string)* prefix = "prefix" in xml.tag.attr; 667 if (prefix && *prefix == "xdg") { 668 string dataPath = writablePath(StandardPath.Data); 669 if (dataPath.length) { 670 path = buildPath(dataPath, path); 671 } 672 } 673 } 674 if (path.length) { 675 paths ~= path; 676 } 677 }; 678 parser.parse(); 679 } 680 catch(Exception e) { 681 682 } 683 return paths; 684 } 685 686 private string[] fontPaths() nothrow @trusted 687 { 688 string[] paths; 689 690 string homeConfig = homeFontsConfig(); 691 if (homeConfig.length) { 692 paths ~= readFontsConfig(homeConfig); 693 } 694 695 enum configs = ["/etc/fonts/fonts.conf", //path on linux 696 "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd 697 foreach(config; configs) { 698 paths ~= readFontsConfig(config); 699 } 700 return paths; 701 } 702 703 private string homeFontsConfig() nothrow @trusted { 704 return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf"); 705 } 706 707 private string homeFontsPath() nothrow @trusted { 708 string[] paths = readFontsConfig(homeFontsConfig()); 709 if (paths.length) 710 return paths[0]; 711 return null; 712 } 713 714 } else { 715 private string homeFontsPath() nothrow @trusted { 716 return maybeConcat(homeDir(), "/.fonts"); 717 } 718 719 private string[] fontPaths() nothrow @trusted 720 { 721 enum localShare = "/usr/local/share/fonts"; 722 enum share = "/usr/share/fonts"; 723 724 string homeFonts = homeFontsPath(); 725 if (homeFonts.length) { 726 return [homeFonts, localShare, share]; 727 } else { 728 return [localShare, share]; 729 } 730 } 731 732 } 733 734 735 736 /** 737 * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 738 * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string. 739 * Note: this function is defined only on $(B Posix) systems (except for OS X) 740 */ 741 string runtimeDir() nothrow @trusted 742 { 743 // Do we need it on BSD systems? 744 745 import core.sys.posix.pwd; 746 import core.sys.posix.unistd; 747 import core.sys.posix.sys.stat; 748 import core.sys.posix.sys.types; 749 import core.stdc.errno; 750 import core.stdc.string; 751 752 const uid_t uid = getuid(); 753 string runtime; 754 collectException(environment.get("XDG_RUNTIME_DIR"), runtime); 755 756 mode_t runtimeMode = octal!700; 757 758 if (!runtime.length) { 759 setpwent(); 760 passwd* pw = getpwuid(uid); 761 endpwent(); 762 763 try { 764 if (pw && pw.pw_name) { 765 runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name)); 766 767 if (!(runtime.exists && runtime.isDir)) { 768 if (mkdir(runtime.toStringz, runtimeMode) != 0) { 769 debug stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno))); 770 return null; 771 } 772 } 773 } else { 774 debug stderr.writeln("Failed to get user name to create runtime directory"); 775 return null; 776 } 777 } catch(Exception e) { 778 debug collectException(stderr.writeln("Error when creating runtime directory", e.msg)); 779 return null; 780 } 781 } 782 stat_t statbuf; 783 stat(runtime.toStringz, &statbuf); 784 if (statbuf.st_uid != uid) { 785 debug collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid)); 786 return null; 787 } 788 if ((statbuf.st_mode & octal!777) != runtimeMode) { 789 debug collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode)); 790 return null; 791 } 792 793 return runtime; 794 } 795 796 string writablePath(StandardPath type) nothrow @safe 797 { 798 final switch(type) { 799 case StandardPath.Config: 800 return xdgBaseDir("XDG_CONFIG_HOME", "/.config"); 801 case StandardPath.Cache: 802 return xdgBaseDir("XDG_CACHE_HOME", "/.cache"); 803 case StandardPath.Data: 804 return xdgBaseDir("XDG_DATA_HOME", "/.local/share"); 805 case StandardPath.Desktop: 806 return xdgUserDir("DESKTOP", "/Desktop"); 807 case StandardPath.Documents: 808 return xdgUserDir("DOCUMENTS"); 809 case StandardPath.Pictures: 810 return xdgUserDir("PICTURES"); 811 case StandardPath.Music: 812 return xdgUserDir("MUSIC"); 813 case StandardPath.Videos: 814 return xdgUserDir("VIDEOS"); 815 case StandardPath.Download: 816 return xdgUserDir("DOWNLOAD"); 817 case StandardPath.Templates: 818 return xdgUserDir("TEMPLATES", "/Templates"); 819 case StandardPath.PublicShare: 820 return xdgUserDir("PUBLICSHARE", "/Public"); 821 case StandardPath.Fonts: 822 return homeFontsPath(); 823 case StandardPath.Applications: 824 return maybeConcat(writablePath(StandardPath.Data), "/applications"); 825 } 826 } 827 828 string[] standardPaths(StandardPath type) nothrow @safe 829 { 830 string[] paths; 831 832 switch(type) { 833 case StandardPath.Data: 834 paths = xdgDataDirs(); 835 break; 836 case StandardPath.Config: 837 paths = xdgConfigDirs(); 838 break; 839 case StandardPath.Applications: 840 { 841 paths = xdgDataDirs(); 842 foreach(ref path; paths) { 843 path ~= "/applications"; 844 } 845 } 846 break; 847 case StandardPath.Fonts: 848 return fontPaths(); 849 default: 850 break; 851 } 852 853 string userPath = writablePath(type); 854 if (userPath.length) { 855 paths = userPath ~ paths; 856 } 857 return paths; 858 } 859 } else { 860 static assert(false, "Unsupported platform"); 861 } 862 863 private bool isExecutable(string filePath) nothrow @trusted { 864 try { 865 version(Posix) { 866 import core.sys.posix.unistd; 867 return access(toStringz(filePath), X_OK) == 0; 868 } else version(Windows) { 869 //Use GetEffectiveRightsFromAclW? 870 871 string extension = filePath.extension; 872 const(string)[] exeExtensions = executableExtensions(); 873 foreach(ext; exeExtensions) { 874 if (sicmp(extension, ext) == 0) 875 return true; 876 } 877 return false; 878 879 } else { 880 static assert(false, "Unsupported platform"); 881 } 882 } catch(Exception e) { 883 return false; 884 } 885 } 886 887 private string checkExecutable(string filePath) nothrow @trusted { 888 try { 889 if (filePath.isFile && filePath.isExecutable) { 890 return buildNormalizedPath(filePath); 891 } else { 892 return null; 893 } 894 } 895 catch(Exception e) { 896 return null; 897 } 898 } 899 900 /** 901 * Finds executable by $(B fileName) in the paths specified by $(B paths). 902 * Returns: absolute path to the existing executable file or an empty string if not found. 903 * Params: 904 * fileName = name of executable to search 905 * paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable 906 * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search. 907 */ 908 string findExecutable(string fileName, in string[] paths = []) nothrow @safe 909 { 910 try { 911 if (fileName.isAbsolute()) { 912 return checkExecutable(fileName); 913 } 914 915 const(string)[] searchPaths = paths; 916 if (searchPaths.empty) { 917 string pathVar = environment.get("PATH"); 918 if (pathVar.length) { 919 searchPaths = splitter(pathVar, pathVarSeparator).array; 920 } 921 } 922 923 if (searchPaths.empty) { 924 return null; 925 } 926 927 string toReturn; 928 foreach(string path; searchPaths) { 929 string candidate = buildPath(absolutePath(path), fileName); 930 931 version(Windows) { 932 if (candidate.extension.empty) { 933 foreach(exeExtension; executableExtensions()) { 934 toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower())); 935 if (toReturn.length) { 936 return toReturn; 937 } 938 } 939 } 940 } 941 942 toReturn = checkExecutable(candidate); 943 if (toReturn.length) { 944 return toReturn; 945 } 946 } 947 } catch (Exception e) { 948 949 } 950 return null; 951 } 952 953