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 alias typeof(&RegOpenKeyExW) func_RegOpenKeyEx; 232 alias typeof(&RegQueryValueExW) func_RegQueryValueEx; 233 alias typeof(&RegCloseKey) func_RegCloseKey; 234 235 __gshared func_RegOpenKeyEx ptrRegOpenKeyEx; 236 __gshared func_RegQueryValueEx ptrRegQueryValueEx; 237 __gshared func_RegCloseKey ptrRegCloseKey; 238 239 @nogc @trusted bool hasSHGetSpecialFolderPath() nothrow { 240 return ptrSHGetSpecialFolderPath != null; 241 } 242 243 @nogc @trusted bool isAdvApiLoaded() nothrow { 244 return ptrRegOpenKeyEx && ptrRegQueryValueEx && ptrRegCloseKey; 245 } 246 } 247 248 shared static this() 249 { 250 version(LinkedShell32) {} else { 251 HMODULE shellLib = LoadLibraryA("Shell32"); 252 if (shellLib) { 253 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(shellLib, "SHGetSpecialFolderPathW"); 254 } 255 } 256 257 HMODULE advApi = LoadLibraryA("Advapi32.dll"); 258 if (advApi) { 259 ptrRegOpenKeyEx = cast(func_RegOpenKeyEx)GetProcAddress(advApi, "RegOpenKeyExW"); 260 ptrRegQueryValueEx = cast(func_RegQueryValueEx)GetProcAddress(advApi, "RegQueryValueExW"); 261 ptrRegCloseKey = cast(func_RegCloseKey)GetProcAddress(advApi, "RegCloseKey"); 262 } 263 } 264 265 private string getShellFolder(const(wchar)* key) nothrow @trusted 266 { 267 HKEY hKey; 268 if (isAdvApiLoaded()) { 269 auto result = ptrRegOpenKeyEx(HKEY_CURRENT_USER, 270 "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\0"w.ptr, 271 0, 272 KEY_QUERY_VALUE, 273 &hKey 274 ); 275 scope(exit) ptrRegCloseKey(hKey); 276 277 if (result == ERROR_SUCCESS) { 278 DWORD type; 279 BYTE[MAX_PATH*wchar.sizeof] buf = void; 280 DWORD length = cast(DWORD)buf.length; 281 result = ptrRegQueryValueEx(hKey, key, null, &type, buf.ptr, &length); 282 if (result == ERROR_SUCCESS && type == REG_SZ && (length % 2 == 0)) { 283 auto str = cast(wstring)buf[0..length]; 284 try { 285 return toUTF8(str); 286 } catch(Exception e) { 287 288 } 289 } 290 } 291 } 292 293 return null; 294 } 295 296 297 private string getCSIDLFolder(int csidl) nothrow @trusted 298 { 299 import core.stdc.wchar_ : wcslen; 300 301 wchar[MAX_PATH] path = void; 302 if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) { 303 size_t len = wcslen(path.ptr); 304 try { 305 return toUTF8(path[0..len]); 306 } catch(Exception e) { 307 308 } 309 } 310 return null; 311 } 312 313 /// Path to $(B Roaming) data directory. This function is Windows only. 314 string roamingPath() nothrow @safe 315 { 316 return getCSIDLFolder(CSIDL_APPDATA); 317 } 318 319 string writablePath(StandardPath type) nothrow @safe 320 { 321 final switch(type) { 322 case StandardPath.Config: 323 case StandardPath.Data: 324 return getCSIDLFolder(CSIDL_LOCAL_APPDATA); 325 case StandardPath.Cache: 326 { 327 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA); 328 if (path.length) { 329 return buildPath(path, "cache"); 330 } 331 return null; 332 } 333 case StandardPath.Desktop: 334 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY); 335 case StandardPath.Documents: 336 return getCSIDLFolder(CSIDL_PERSONAL); 337 case StandardPath.Pictures: 338 return getCSIDLFolder(CSIDL_MYPICTURES); 339 case StandardPath.Music: 340 return getCSIDLFolder(CSIDL_MYMUSIC); 341 case StandardPath.Videos: 342 return getCSIDLFolder(CSIDL_MYVIDEO); 343 case StandardPath.Download: 344 return getShellFolder("{374DE290-123F-4565-9164-39C4925E467B}\0"w.ptr); 345 case StandardPath.Templates: 346 return getCSIDLFolder(CSIDL_TEMPLATES); 347 case StandardPath.PublicShare: 348 return null; 349 case StandardPath.Fonts: 350 return null; 351 case StandardPath.Applications: 352 return getCSIDLFolder(CSIDL_PROGRAMS); 353 } 354 } 355 356 string[] standardPaths(StandardPath type) nothrow @safe 357 { 358 string commonPath; 359 360 switch(type) { 361 case StandardPath.Config: 362 case StandardPath.Data: 363 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA); 364 break; 365 case StandardPath.Desktop: 366 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY); 367 break; 368 case StandardPath.Documents: 369 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS); 370 break; 371 case StandardPath.Pictures: 372 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES); 373 break; 374 case StandardPath.Music: 375 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC); 376 break; 377 case StandardPath.Videos: 378 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO); 379 break; 380 case StandardPath.Templates: 381 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES); 382 break; 383 case StandardPath.Fonts: 384 commonPath = getCSIDLFolder(CSIDL_FONTS); 385 break; 386 case StandardPath.Applications: 387 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS); 388 break; 389 default: 390 break; 391 } 392 393 string[] paths; 394 string userPath = writablePath(type); 395 if (userPath.length) 396 paths ~= userPath; 397 if (commonPath.length) 398 paths ~= commonPath; 399 return paths; 400 } 401 402 private string[] executableExtensions() nothrow @trusted 403 { 404 static bool filenamesEqual(string first, string second) nothrow { 405 try { 406 return filenameCmp(first, second) == 0; 407 } catch(Exception e) { 408 return false; 409 } 410 } 411 412 string[] extensions; 413 try { 414 extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array; 415 if (canFind!(filenamesEqual)(extensions, ".exe") == false) { 416 extensions = []; 417 } 418 } catch (Exception e) { 419 420 } 421 if (extensions.empty) { 422 extensions = [".exe", ".com", ".bat", ".cmd"]; 423 } 424 return extensions; 425 } 426 } else version(OSX) { 427 428 private enum : short { 429 kOnSystemDisk = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/ 430 kOnAppropriateDisk = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/ 431 /* Folder Domains - Carbon only. The constants above can continue to be used, but the folder/volume returned will*/ 432 /* be from one of the domains below.*/ 433 kSystemDomain = -32766, /* Read-only system hierarchy.*/ 434 kLocalDomain = -32765, /* All users of a single machine have access to these resources.*/ 435 kNetworkDomain = -32764, /* All users configured to use a common network server has access to these resources.*/ 436 kUserDomain = -32763, /* Read/write. Resources that are private to the user.*/ 437 kClassicDomain = -32762, /* Domain referring to the currently configured Classic System Folder. Not supported in Mac OS X Leopard and later.*/ 438 kFolderManagerLastDomain = -32760 439 } 440 441 private @nogc int k(string s) nothrow { 442 return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]; 443 } 444 445 private enum { 446 kDesktopFolderType = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */ 447 kTrashFolderType = k("trsh"), /* the trash folder; objects in this folder show up in the trash */ 448 kWhereToEmptyTrashFolderType = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */ 449 kFontsFolderType = k("font"), /* Fonts go here */ 450 kPreferencesFolderType = k("pref"), /* preferences for applications go here */ 451 kSystemPreferencesFolderType = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */ 452 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*/ 453 /* folders such that other users can not access the data inside. On Mac OS X 10.4 and later the data inside the temporary*/ 454 /* items folder is deleted at logout and at boot, but not otherwise. Earlier version of Mac OS X would delete items inside*/ 455 /* the temporary items folder after a period of inaccess. You can ask for a temporary item in a specific domain or on a */ 456 /* particular volume by FSVolumeRefNum. If you want a location for temporary items for a short time, then use either*/ 457 /* ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ). The kUserDomain varient will always be*/ 458 /* on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/ 459 /* 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*/ 460 /* file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/ 461 /* ask for the temporary items folder on the same volume as the file you are safe saving.*/ 462 /* However, be prepared for a failure to find a temporary folder in any domain or on any volume. Some volumes may not have*/ 463 /* a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/ 464 /* a temporary folder for the volume.*/ 465 /* If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/ 466 /* and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/ 467 /* restarts. Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/ 468 /* when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/ 469 kChewableItemsFolderType = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */ 470 kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/ 471 kApplicationsFolderType = k("apps"), /* Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/ 472 kVolumeRootFolderType = k("root"), /* root folder of a volume or domain */ 473 kDomainTopLevelFolderType = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/ 474 kDomainLibraryFolderType = k("dlib"), /* the Library subfolder of a particular domain*/ 475 kUsersFolderType = k("usrs"), /* "Users" folder, usually contains one folder for each user. */ 476 kCurrentUserFolderType = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */ 477 kSharedUserDataFolderType = k("sdat"), /* A Shared folder, readable & writeable by all users */ 478 kCachedDataFolderType = k("cach"), /* Contains various cache files for different clients*/ 479 kDownloadsFolderType = k("down"), /* Refers to the ~/Downloads folder*/ 480 kApplicationSupportFolderType = k("asup"), /* third-party items and folders */ 481 482 483 kDocumentsFolderType = k("docs"), /* User documents are typically put in this folder ( or a subfolder ).*/ 484 kPictureDocumentsFolderType = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/ 485 kMovieDocumentsFolderType = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/ 486 kMusicDocumentsFolderType = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/ 487 kInternetSitesFolderType = k("site"), /* Refers to the "Sites" folder in a users home directory*/ 488 kPublicFolderType = k("pubb"), /* Refers to the "Public" folder in a users home directory*/ 489 490 kDropBoxFolderType = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/ 491 }; 492 493 private { 494 struct FSRef { 495 char[80] hidden; /* private to File Manager*/ 496 }; 497 498 alias int Boolean; 499 alias int OSType; 500 alias int OSerr; 501 502 extern(C) @nogc @system int dummy(short, int, int, FSRef*) nothrow { return 0; } 503 extern(C) @nogc @system int dummy2(const(FSRef)*, char*, uint) nothrow { return 0; } 504 505 alias da_FSFindFolder = typeof(&dummy); 506 alias da_FSRefMakePath = typeof(&dummy2); 507 508 __gshared da_FSFindFolder ptrFSFindFolder = null; 509 __gshared da_FSRefMakePath ptrFSRefMakePath = null; 510 } 511 512 shared static this() 513 { 514 enum carbonPath = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore"; 515 516 import core.sys.posix.dlfcn; 517 518 void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL); 519 if (handle) { 520 ptrFSFindFolder = cast(da_FSFindFolder)dlsym(handle, "FSFindFolder"); 521 ptrFSRefMakePath = cast(da_FSRefMakePath)dlsym(handle, "FSRefMakePath"); 522 } 523 if (ptrFSFindFolder == null || ptrFSRefMakePath == null) { 524 debug collectException(stderr.writeln("Could not load carbon functions")); 525 } 526 } 527 528 @nogc @trusted bool isCarbonLoaded() nothrow 529 { 530 return ptrFSFindFolder != null && ptrFSRefMakePath != null; 531 } 532 533 enum noErr = 0; 534 535 string fsPath(short domain, OSType type) nothrow @trusted 536 { 537 import std.stdio; 538 FSRef fsref; 539 if (isCarbonLoaded() && ptrFSFindFolder(domain, type, false, &fsref) == noErr) { 540 541 char[2048] buf; 542 char* path = buf.ptr; 543 if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) { 544 try { 545 546 return fromStringz(path).idup; 547 } 548 catch(Exception e) { 549 550 } 551 } 552 } 553 return null; 554 } 555 556 string writablePath(StandardPath type) nothrow @safe 557 { 558 final switch(type) { 559 case StandardPath.Config: 560 return fsPath(kUserDomain, kPreferencesFolderType); 561 case StandardPath.Cache: 562 return fsPath(kUserDomain, kCachedDataFolderType); 563 case StandardPath.Data: 564 return fsPath(kUserDomain, kApplicationSupportFolderType); 565 case StandardPath.Desktop: 566 return fsPath(kUserDomain, kDesktopFolderType); 567 case StandardPath.Documents: 568 return fsPath(kUserDomain, kDocumentsFolderType); 569 case StandardPath.Pictures: 570 return fsPath(kUserDomain, kPictureDocumentsFolderType); 571 case StandardPath.Music: 572 return fsPath(kUserDomain, kMusicDocumentsFolderType); 573 case StandardPath.Videos: 574 return fsPath(kUserDomain, kMovieDocumentsFolderType); 575 case StandardPath.Download: 576 return fsPath(kUserDomain, kDownloadsFolderType); 577 case StandardPath.Templates: 578 return null; 579 case StandardPath.PublicShare: 580 return fsPath(kUserDomain, kPublicFolderType ); 581 case StandardPath.Fonts: 582 return fsPath(kUserDomain, kFontsFolderType); 583 case StandardPath.Applications: 584 return fsPath(kUserDomain, kApplicationsFolderType); 585 } 586 } 587 588 string[] standardPaths(StandardPath type) nothrow @safe 589 { 590 string commonPath; 591 592 switch(type) { 593 case StandardPath.Fonts: 594 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 595 break; 596 case StandardPath.Applications: 597 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 598 break; 599 case StandardPath.Data: 600 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 601 break; 602 case StandardPath.Cache: 603 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 604 break; 605 default: 606 break; 607 } 608 609 string[] paths; 610 string userPath = writablePath(type); 611 if (userPath.length) 612 paths ~= userPath; 613 if (commonPath.length) 614 paths ~= commonPath; 615 return paths; 616 } 617 618 } else version(Posix) { 619 620 //Concat two strings, but if the first one is empty, then null string is returned. 621 private string maybeConcat(string start, string path) nothrow @safe 622 { 623 return start.empty ? null : start ~ path; 624 } 625 626 private string xdgBaseDir(string envvar, string fallback) nothrow @trusted { 627 string dir; 628 collectException(environment.get(envvar), dir); 629 if (!dir.length) { 630 dir = maybeConcat(homeDir(), fallback); 631 } 632 return dir; 633 } 634 635 private string xdgUserDir(string key, string fallback = null) nothrow @trusted { 636 import std.algorithm : startsWith; 637 import std.string : strip; 638 639 string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs"); 640 string home = homeDir(); 641 try { 642 auto f = File(fileName, "r"); 643 644 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 645 646 char[] buf; 647 while(f.readln(buf)) { 648 char[] line = strip(buf); 649 auto index = xdgdir.length; 650 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') { 651 line = line[index+1..$]; 652 if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 653 { 654 line = line[1..$-1]; 655 656 if (line.startsWith("$HOME")) { 657 return maybeConcat(home, assumeUnique(line[5..$])); 658 } 659 if (line.length == 0 || line[0] != '/') { 660 continue; 661 } 662 return assumeUnique(line); 663 } 664 } 665 } 666 } catch(Exception e) { 667 668 } 669 670 if (home.length) { 671 try { 672 auto f = File("/etc/xdg/user-dirs.defaults", "r"); 673 char[] buf; 674 while(f.readln(buf)) { 675 char[] line = strip(buf); 676 auto index = key.length; 677 if (line.startsWith(key) && line.length > index && line[index] == '=') 678 { 679 line = line[index+1..$]; 680 return home ~ "/" ~ assumeUnique(line); 681 } 682 } 683 } catch (Exception e) { 684 685 } 686 if (fallback.length) { 687 return home ~ fallback; 688 } 689 } 690 return null; 691 } 692 693 private string[] xdgConfigDirs() nothrow @trusted { 694 try { 695 string configDirs = environment.get("XDG_CONFIG_DIRS"); 696 if (configDirs.length) { 697 return splitter(configDirs, pathVarSeparator).array; 698 } 699 } 700 catch(Exception e) { 701 702 } 703 return ["/etc/xdg"]; 704 } 705 706 private string[] xdgDataDirs() nothrow @trusted { 707 try { 708 string dataDirs = environment.get("XDG_DATA_DIRS"); 709 if (dataDirs.length) { 710 return splitter(dataDirs, pathVarSeparator).array; 711 } 712 } catch(Exception e) { 713 714 } 715 return ["/usr/local/share", "/usr/share"]; 716 } 717 718 719 version(fontsconf) { 720 private string[] readFontsConfig(string configFile) nothrow @trusted 721 { 722 //Should be changed in future since std.xml is deprecated 723 import std.xml; 724 725 string[] paths; 726 try { 727 string contents = cast(string)read(configFile); 728 check(contents); 729 auto parser = new DocumentParser(contents); 730 parser.onEndTag["dir"] = (in Element xml) 731 { 732 string path = xml.text; 733 734 if (path.length && path[0] == '~') { 735 path = maybeConcat(homeDir(), path[1..$]); 736 } else { 737 const(string)* prefix = "prefix" in xml.tag.attr; 738 if (prefix && *prefix == "xdg") { 739 string dataPath = writablePath(StandardPath.Data); 740 if (dataPath.length) { 741 path = buildPath(dataPath, path); 742 } 743 } 744 } 745 if (path.length) { 746 paths ~= path; 747 } 748 }; 749 parser.parse(); 750 } 751 catch(Exception e) { 752 753 } 754 return paths; 755 } 756 757 private string[] fontPaths() nothrow @trusted 758 { 759 string[] paths; 760 761 string homeConfig = homeFontsConfig(); 762 if (homeConfig.length) { 763 paths ~= readFontsConfig(homeConfig); 764 } 765 766 enum configs = ["/etc/fonts/fonts.conf", //path on linux 767 "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd 768 foreach(config; configs) { 769 paths ~= readFontsConfig(config); 770 } 771 return paths; 772 } 773 774 private string homeFontsConfig() nothrow @trusted { 775 return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf"); 776 } 777 778 private string homeFontsPath() nothrow @trusted { 779 string[] paths = readFontsConfig(homeFontsConfig()); 780 if (paths.length) 781 return paths[0]; 782 return null; 783 } 784 785 } else { 786 private string homeFontsPath() nothrow @trusted { 787 return maybeConcat(homeDir(), "/.fonts"); 788 } 789 790 private string[] fontPaths() nothrow @trusted 791 { 792 enum localShare = "/usr/local/share/fonts"; 793 enum share = "/usr/share/fonts"; 794 795 string homeFonts = homeFontsPath(); 796 if (homeFonts.length) { 797 return [homeFonts, localShare, share]; 798 } else { 799 return [localShare, share]; 800 } 801 } 802 803 } 804 805 806 807 /** 808 * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 809 * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string. 810 * Note: this function is defined only on $(B Posix) systems (except for OS X) 811 */ 812 string runtimeDir() nothrow @trusted 813 { 814 // Do we need it on BSD systems? 815 816 import core.sys.posix.pwd; 817 import core.sys.posix.unistd; 818 import core.sys.posix.sys.stat; 819 import core.sys.posix.sys.types; 820 import core.stdc.errno; 821 import core.stdc.string; 822 823 try { //one try to rule them all and for compatibility reasons 824 const uid_t uid = getuid(); 825 string runtime; 826 collectException(environment.get("XDG_RUNTIME_DIR"), runtime); 827 828 mode_t runtimeMode = octal!700; 829 830 if (!runtime.length) { 831 setpwent(); 832 passwd* pw = getpwuid(uid); 833 endpwent(); 834 835 try { 836 if (pw && pw.pw_name) { 837 runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name)); 838 839 if (!(runtime.exists && runtime.isDir)) { 840 if (mkdir(runtime.toStringz, runtimeMode) != 0) { 841 debug stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno))); 842 return null; 843 } 844 } 845 } else { 846 debug stderr.writeln("Failed to get user name to create runtime directory"); 847 return null; 848 } 849 } catch(Exception e) { 850 debug collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg)); 851 return null; 852 } 853 } 854 stat_t statbuf; 855 stat(runtime.toStringz, &statbuf); 856 if (statbuf.st_uid != uid) { 857 debug collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid)); 858 return null; 859 } 860 if ((statbuf.st_mode & octal!777) != runtimeMode) { 861 debug collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode)); 862 return null; 863 } 864 865 return runtime; 866 } catch (Exception e) { 867 debug collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg)); 868 return null; 869 } 870 } 871 872 string writablePath(StandardPath type) nothrow @safe 873 { 874 final switch(type) { 875 case StandardPath.Config: 876 return xdgBaseDir("XDG_CONFIG_HOME", "/.config"); 877 case StandardPath.Cache: 878 return xdgBaseDir("XDG_CACHE_HOME", "/.cache"); 879 case StandardPath.Data: 880 return xdgBaseDir("XDG_DATA_HOME", "/.local/share"); 881 case StandardPath.Desktop: 882 return xdgUserDir("DESKTOP", "/Desktop"); 883 case StandardPath.Documents: 884 return xdgUserDir("DOCUMENTS"); 885 case StandardPath.Pictures: 886 return xdgUserDir("PICTURES"); 887 case StandardPath.Music: 888 return xdgUserDir("MUSIC"); 889 case StandardPath.Videos: 890 return xdgUserDir("VIDEOS"); 891 case StandardPath.Download: 892 return xdgUserDir("DOWNLOAD"); 893 case StandardPath.Templates: 894 return xdgUserDir("TEMPLATES", "/Templates"); 895 case StandardPath.PublicShare: 896 return xdgUserDir("PUBLICSHARE", "/Public"); 897 case StandardPath.Fonts: 898 return homeFontsPath(); 899 case StandardPath.Applications: 900 return maybeConcat(writablePath(StandardPath.Data), "/applications"); 901 } 902 } 903 904 string[] standardPaths(StandardPath type) nothrow @safe 905 { 906 string[] paths; 907 908 switch(type) { 909 case StandardPath.Data: 910 paths = xdgDataDirs(); 911 break; 912 case StandardPath.Config: 913 paths = xdgConfigDirs(); 914 break; 915 case StandardPath.Applications: 916 { 917 paths = xdgDataDirs(); 918 foreach(ref path; paths) { 919 path ~= "/applications"; 920 } 921 } 922 break; 923 case StandardPath.Fonts: 924 return fontPaths(); 925 default: 926 break; 927 } 928 929 string userPath = writablePath(type); 930 if (userPath.length) { 931 paths = userPath ~ paths; 932 } 933 return paths; 934 } 935 } else { 936 static assert(false, "Unsupported platform"); 937 } 938 939 private bool isExecutable(string filePath) nothrow @trusted { 940 try { 941 version(Posix) { 942 import core.sys.posix.unistd; 943 return access(toStringz(filePath), X_OK) == 0; 944 } else version(Windows) { 945 //Use GetEffectiveRightsFromAclW? 946 947 string extension = filePath.extension; 948 const(string)[] exeExtensions = executableExtensions(); 949 foreach(ext; exeExtensions) { 950 if (sicmp(extension, ext) == 0) 951 return true; 952 } 953 return false; 954 955 } else { 956 static assert(false, "Unsupported platform"); 957 } 958 } catch(Exception e) { 959 return false; 960 } 961 } 962 963 private string checkExecutable(string filePath) nothrow @trusted { 964 try { 965 if (filePath.isFile && filePath.isExecutable) { 966 return buildNormalizedPath(filePath); 967 } else { 968 return null; 969 } 970 } 971 catch(Exception e) { 972 return null; 973 } 974 } 975 976 /** 977 * Finds executable by $(B fileName) in the paths specified by $(B paths). 978 * Returns: absolute path to the existing executable file or an empty string if not found. 979 * Params: 980 * fileName = name of executable to search 981 * paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable 982 * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search. 983 */ 984 string findExecutable(string fileName, in string[] paths = []) nothrow @safe 985 { 986 @trusted string[] getEnvPaths() { //trusted function for compatibility with older compilers 987 string pathVar = environment.get("PATH"); 988 if (pathVar.length) { 989 return splitter(pathVar, pathVarSeparator).array; 990 } else { 991 return null; 992 } 993 } 994 995 try { 996 if (fileName.isAbsolute()) { 997 return checkExecutable(fileName); 998 } 999 1000 const(string)[] searchPaths = paths; 1001 if (searchPaths.empty) { 1002 searchPaths = getEnvPaths(); 1003 } 1004 1005 string toReturn; 1006 foreach(string path; searchPaths) { 1007 string candidate = buildPath(absolutePath(path), fileName); 1008 1009 version(Windows) { 1010 if (candidate.extension.empty) { 1011 foreach(exeExtension; executableExtensions()) { 1012 toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower())); 1013 if (toReturn.length) { 1014 return toReturn; 1015 } 1016 } 1017 } 1018 } 1019 1020 toReturn = checkExecutable(candidate); 1021 if (toReturn.length) { 1022 return toReturn; 1023 } 1024 } 1025 } catch (Exception e) { 1026 1027 } 1028 return null; 1029 } 1030 1031