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