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