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