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