1 /** 2 * Functions for retrieving standard paths in cross-platform manner. 3 * 4 */ 5 6 module standardpaths; 7 8 private { 9 import std.process : environment; 10 import std.array; 11 import std.path; 12 import std.file; 13 import std.algorithm : splitter; 14 } 15 16 version(Windows) { 17 private { 18 import std.c.windows.windows; 19 import std.utf; 20 import std.algorithm : canFind; 21 } 22 } else version(OSX) { 23 private { 24 //what to import? 25 } 26 } else version(Posix) { 27 private { 28 import std.stdio : File, StdioException; 29 import std.exception : assumeUnique; 30 import std.conv : octal; 31 } 32 } else { 33 static assert(false, "Unsupported platform"); 34 } 35 36 /** 37 * Locations that can be passed to writablePath and standardPaths functions. 38 * See_Also: 39 * writablePath, standardPaths 40 */ 41 enum StandardPath { 42 Data, ///Location of persisted application data 43 Config, ///Location of configuration files 44 Cache, ///Location of cached data 45 Desktop, ///User's desktop directory 46 Documents, ///User's documents 47 Pictures, ///User's pictures 48 Music, ///User's music 49 Videos, ///User's videos (movies) 50 Download, ///Directory for user's downloaded files 51 Templates, ///Location of templates 52 PublicShare, ///Public share folder 53 Fonts, ///Location of fonts files 54 Applications, ///User's applications 55 } 56 57 /** 58 * Returns: path to user home directory, or an empty string if could not determine home directory. 59 * Note: this function does not provide caching of its results. 60 */ 61 string homeDir() nothrow 62 { 63 try { //environment.get may throw on Windows 64 version(Windows) { 65 //Use GetUserProfileDirectoryW from Userenv.dll? 66 string home = environment.get("USERPROFILE"); 67 if (home.empty) { 68 string homeDrive = environment.get("HOMEDRIVE"); 69 string homePath = environment.get("HOMEPATH"); 70 if (homeDrive.length && homePath.length) { 71 home = homeDrive ~ homePath; 72 } 73 } 74 return home; 75 } else { 76 string home = environment.get("HOME"); 77 return home; 78 } 79 } 80 catch(Exception e) { 81 return null; 82 } 83 } 84 85 /** 86 * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path. 87 * This function does not ensure if the returned path exists and appears to be accessible directory. 88 * Note: this function does not provide caching of its results. 89 */ 90 string writablePath(StandardPath type); 91 92 /** 93 * Returns: array of paths where file of $(U type) belong including one returned by $(B writablePath), or empty array if no paths for $(U type) are defined. 94 * This function does not ensure if all returned paths exist and appear to be accessible directories. 95 * Note: this function does not provide caching of its results. 96 * It may cause performance impact to call this function often since retrieving some paths can be expensive operation. 97 * See_Also: 98 * writablePath 99 */ 100 string[] standardPaths(StandardPath type); 101 102 version(Windows) { 103 private enum pathVarSeparator = ';'; 104 } else version(Posix) { 105 private enum pathVarSeparator = ':'; 106 } 107 108 version(Windows) { 109 110 private { 111 enum { 112 CSIDL_DESKTOP = 0, 113 CSIDL_INTERNET, 114 CSIDL_PROGRAMS, 115 CSIDL_CONTROLS, 116 CSIDL_PRINTERS, 117 CSIDL_PERSONAL, 118 CSIDL_FAVORITES, 119 CSIDL_STARTUP, 120 CSIDL_RECENT, 121 CSIDL_SENDTO, 122 CSIDL_BITBUCKET, 123 CSIDL_STARTMENU, // = 11 124 CSIDL_MYMUSIC = 13, 125 CSIDL_MYVIDEO, // = 14 126 CSIDL_DESKTOPDIRECTORY = 16, 127 CSIDL_DRIVES, 128 CSIDL_NETWORK, 129 CSIDL_NETHOOD, 130 CSIDL_FONTS, 131 CSIDL_TEMPLATES, 132 CSIDL_COMMON_STARTMENU, 133 CSIDL_COMMON_PROGRAMS, 134 CSIDL_COMMON_STARTUP, 135 CSIDL_COMMON_DESKTOPDIRECTORY, 136 CSIDL_APPDATA, 137 CSIDL_PRINTHOOD, 138 CSIDL_LOCAL_APPDATA, 139 CSIDL_ALTSTARTUP, 140 CSIDL_COMMON_ALTSTARTUP, 141 CSIDL_COMMON_FAVORITES, 142 CSIDL_INTERNET_CACHE, 143 CSIDL_COOKIES, 144 CSIDL_HISTORY, 145 CSIDL_COMMON_APPDATA, 146 CSIDL_WINDOWS, 147 CSIDL_SYSTEM, 148 CSIDL_PROGRAM_FILES, 149 CSIDL_MYPICTURES, 150 CSIDL_PROFILE, 151 CSIDL_SYSTEMX86, 152 CSIDL_PROGRAM_FILESX86, 153 CSIDL_PROGRAM_FILES_COMMON, 154 CSIDL_PROGRAM_FILES_COMMONX86, 155 CSIDL_COMMON_TEMPLATES, 156 CSIDL_COMMON_DOCUMENTS, 157 CSIDL_COMMON_ADMINTOOLS, 158 CSIDL_ADMINTOOLS, 159 CSIDL_CONNECTIONS, // = 49 160 CSIDL_COMMON_MUSIC = 53, 161 CSIDL_COMMON_PICTURES, 162 CSIDL_COMMON_VIDEO, 163 CSIDL_RESOURCES, 164 CSIDL_RESOURCES_LOCALIZED, 165 CSIDL_COMMON_OEM_LINKS, 166 CSIDL_CDBURN_AREA, // = 59 167 CSIDL_COMPUTERSNEARME = 61, 168 CSIDL_FLAG_DONT_VERIFY = 0x4000, 169 CSIDL_FLAG_CREATE = 0x8000, 170 CSIDL_FLAG_MASK = 0xFF00 171 } 172 } 173 174 private { 175 alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL); 176 177 version(LinkedShell32) { 178 extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL); 179 GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW; 180 } else { 181 GetSpecialFolderPath ptrSHGetSpecialFolderPath = null; 182 } 183 } 184 185 version(LinkedShell32) {} else { 186 shared static this() 187 { 188 HMODULE lib = LoadLibraryA("Shell32"); 189 if (lib) { 190 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW"); 191 } 192 } 193 } 194 195 196 private string getCSIDLFolder(wchar* path, int csidl) 197 { 198 import core.stdc.wchar_ : wcslen; 199 if (ptrSHGetSpecialFolderPath(null, path, csidl, FALSE)) { 200 size_t len = wcslen(path); 201 return toUTF8(path[0..len]); 202 } 203 return null; 204 } 205 206 string writablePath(StandardPath type) 207 { 208 if (!ptrSHGetSpecialFolderPath) { 209 return null; 210 } 211 212 wchar[MAX_PATH] buf; 213 wchar* path = buf.ptr; 214 215 final switch(type) { 216 case StandardPath.Config: 217 case StandardPath.Data: 218 return getCSIDLFolder(path, CSIDL_LOCAL_APPDATA); 219 case StandardPath.Cache: 220 return buildPath(getCSIDLFolder(path, CSIDL_LOCAL_APPDATA), "cache"); 221 case StandardPath.Desktop: 222 return getCSIDLFolder(path, CSIDL_DESKTOPDIRECTORY); 223 case StandardPath.Documents: 224 return getCSIDLFolder(path, CSIDL_PERSONAL); 225 case StandardPath.Pictures: 226 return getCSIDLFolder(path, CSIDL_MYPICTURES); 227 case StandardPath.Music: 228 return getCSIDLFolder(path, CSIDL_MYMUSIC); 229 case StandardPath.Videos: 230 return getCSIDLFolder(path, CSIDL_MYVIDEO); 231 case StandardPath.Download: 232 return null; 233 case StandardPath.Templates: 234 return getCSIDLFolder(path, CSIDL_TEMPLATES); 235 case StandardPath.PublicShare: 236 return null; 237 case StandardPath.Fonts: 238 return null; 239 case StandardPath.Applications: 240 return getCSIDLFolder(path, CSIDL_PROGRAMS); 241 } 242 } 243 244 string[] standardPaths(StandardPath type) 245 { 246 if (!ptrSHGetSpecialFolderPath) { 247 return null; 248 } 249 250 string commonPath; 251 wchar[MAX_PATH] buf; 252 wchar* path = buf.ptr; 253 254 switch(type) { 255 case StandardPath.Config: 256 case StandardPath.Data: 257 commonPath = getCSIDLFolder(path, CSIDL_COMMON_APPDATA); 258 break; 259 case StandardPath.Desktop: 260 commonPath = getCSIDLFolder(path, CSIDL_COMMON_DESKTOPDIRECTORY); 261 break; 262 case StandardPath.Documents: 263 commonPath = getCSIDLFolder(path, CSIDL_COMMON_DOCUMENTS); 264 break; 265 case StandardPath.Pictures: 266 commonPath = getCSIDLFolder(path, CSIDL_COMMON_PICTURES); 267 break; 268 case StandardPath.Music: 269 commonPath = getCSIDLFolder(path, CSIDL_COMMON_MUSIC); 270 break; 271 case StandardPath.Videos: 272 commonPath = getCSIDLFolder(path, CSIDL_COMMON_VIDEO); 273 break; 274 case StandardPath.Templates: 275 commonPath = getCSIDLFolder(path, CSIDL_COMMON_TEMPLATES); 276 break; 277 case StandardPath.Fonts: 278 commonPath = getCSIDLFolder(path, CSIDL_FONTS); 279 break; 280 case StandardPath.Applications: 281 commonPath = getCSIDLFolder(path, CSIDL_COMMON_PROGRAMS); 282 break; 283 default: 284 break; 285 } 286 287 string[] paths; 288 string userPath = writablePath(type); 289 if (userPath.length) 290 paths ~= userPath; 291 if (commonPath.length) 292 paths ~= commonPath; 293 return paths; 294 } 295 296 private string[] executableExtensions() 297 { 298 static bool filenamesEqual(string first, string second) { 299 return filenameCmp(first, second) == 0; 300 } 301 302 string[] extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array; 303 if (canFind!(filenamesEqual)(extensions, ".exe") == false) { 304 extensions = [".exe", ".com", ".bat", ".cmd"]; 305 } 306 return extensions; 307 } 308 } else version(OSX) { 309 310 string fsPath(short domain, OSType type) 311 { 312 import std.string : fromStringz; 313 314 FSRef fsref; 315 OSErr err = FSFindFolder(domain, type, false, &fsref); 316 if (err) { 317 return null; 318 } else { 319 ubyte[2048] buf; 320 ubyte* path = buf.ptr; 321 if (FSRefMakePath(&fsref, path, path.sizeof) == noErr) { 322 const(char)* cpath = cast(const(char)*)path; 323 return fromStringz(cpath).idup; 324 } else { 325 return null; 326 } 327 } 328 } 329 330 string writablePath(StandardPath type) 331 { 332 final switch(type) { 333 case StandardPath.Config: 334 return fsPath(kUserDomain, kPreferencesFolderType); 335 case StandardPath.Cache: 336 return fsPath(kUserDomain, kCachedDataFolderType); 337 case StandardPath.Data: 338 return fsPath(kUserDomain, kApplicationSupportFolderType); 339 case StandardPath.Desktop: 340 return fsPath(kUserDomain, kDesktopFolderType); 341 case StandardPath.Documents: 342 return fsPath(kUserDomain, kDocumentsFolderType); 343 case StandardPath.Pictures: 344 return fsPath(kUserDomain, kPictureDocumentsFolderType); 345 case StandardPath.Music: 346 return fsPath(kUserDomain, kMusicDocumentsFolderType); 347 case StandardPath.Videos: 348 return fsPath(kUserDomain, kMovieDocumentsFolderType); 349 case StandardPath.Download: 350 return null; 351 case StandardPath.Templates: 352 return null; 353 case StandardPath.PublicShare: 354 return fsPath(kUserDomain, kPublicFolderType ); 355 case StandardPath.Fonts: 356 return fsPath(kUserDomain, kFontsFolderType); 357 case StandardPath.Applications: 358 return fsPath(kUserDomain, kApplicationsFolderType); 359 } 360 } 361 362 string[] standardPaths(StandardPath type) 363 { 364 string commonPath; 365 366 switch(type) { 367 case StandardPath.Fonts: 368 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 369 case StandardPath.Applications: 370 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 371 case StandardPath.Data: 372 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 373 case StandardPath.Cache: 374 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 375 } 376 377 string[] paths; 378 string userPath = writablePath(type); 379 if (userPath.length) 380 paths ~= userPath; 381 if (commonPath.length) 382 paths ~= commonPath; 383 return paths; 384 } 385 386 } else { 387 388 //Concat two paths, but if the first one is empty, then null string is returned. 389 private string homeConcat(string home, string path) nothrow 390 { 391 return home.empty ? null : home ~ path; 392 } 393 394 private string xdgBaseDir(in char[] envvar, string fallback) { 395 string dir = environment.get(envvar); 396 if (!dir.length) { 397 dir = homeConcat(homeDir(), fallback); 398 } 399 return dir; 400 } 401 402 private string xdgUserDir(in char[] key, string fallback) { 403 import std.algorithm : startsWith, countUntil; 404 405 //Read /etc/xdg/user-dirs.defaults for fallbacks? 406 407 string configDir = writablePath(StandardPath.Config); 408 string fileName = configDir ~ "/user-dirs.dirs"; 409 string home = homeDir(); 410 try { 411 auto f = File(fileName, "r"); 412 413 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 414 415 char[] buf; 416 while(f.readln(buf)) { 417 char[] line = buf[0..$-1]; //remove line terminator 418 if (line.startsWith(xdgdir)) { 419 ptrdiff_t index = line.countUntil('='); 420 if (index != -1) { 421 line = line[index+1..$]; 422 if (line.length > 2 && 423 line[0] == '"' && 424 line[$-1] == '"') { 425 line = line[1..$-1]; 426 } 427 428 if (line.startsWith("$HOME")) { 429 return homeConcat(home, assumeUnique(line[5..$])); 430 } 431 if (line.length == 0 || line[0] != '/') { 432 continue; 433 } 434 return assumeUnique(line); 435 } 436 } 437 } 438 } 439 catch(Exception e) { 440 441 } 442 if (fallback.length) 443 return homeConcat(home, fallback); 444 return null; 445 } 446 447 private string[] xdgConfigDirs() { 448 string configDirs = environment.get("XDG_CONFIG_DIRS"); 449 if (configDirs.length) { 450 return splitter(configDirs, pathVarSeparator).array; 451 } else { 452 return ["/etc/xdg"]; 453 } 454 } 455 456 private string[] xdgDataDirs() { 457 string dataDirs = environment.get("XDG_DATA_DIRS"); 458 if (dataDirs.length) { 459 return splitter(dataDirs, pathVarSeparator).array; 460 } else { 461 return ["/usr/local/share/", "/usr/share/"]; 462 } 463 } 464 465 private string[] readFontsConfig(string configFile) nothrow 466 { 467 //Should be changed in future since std.xml is deprecated 468 import std.xml; 469 470 string[] paths; 471 try { 472 string contents = cast(string)read(configFile); 473 check(contents); 474 auto parser = new DocumentParser(contents); 475 parser.onEndTag["dir"] = (in Element xml) 476 { 477 string path = xml.text; 478 479 if (path.length && path[0] == '~') { 480 path = homeConcat(homeDir(), path[1..$]); 481 } else { 482 const(string)* prefix = "prefix" in xml.tag.attr; 483 if (prefix && *prefix == "xdg") { 484 string dataPath = writablePath(StandardPath.Data); 485 if (dataPath.length) { 486 path = buildPath(dataPath, path); 487 } 488 } 489 } 490 if (path.length) { 491 paths ~= path; 492 } 493 }; 494 parser.parse(); 495 } 496 catch(Exception e) { 497 498 } 499 return paths; 500 } 501 502 private string[] fontPaths() nothrow 503 { 504 505 string[] paths; 506 string[] configs = [homeConcat(homeDir(), "/.fonts"), "/etc/fonts/fonts.conf", "/usr/local/etc/fonts/fonts.conf"]; 507 508 foreach(config; configs) { 509 paths ~= readFontsConfig(config); 510 } 511 return paths; 512 } 513 514 /** 515 * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 516 * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string. 517 */ 518 string runtimeDir() 519 { 520 // Do we need it on BSD systems? 521 522 import core.sys.posix.pwd; 523 import core.sys.posix.unistd; 524 import core.sys.posix.sys.stat; 525 import core.sys.posix.sys.types; 526 import core.stdc.errno; 527 import core.stdc.string; 528 529 import std.string : fromStringz, toStringz; 530 import std.stdio : stderr; 531 532 const uid_t uid = getuid(); 533 string runtime = environment.get("XDG_RUNTIME_DIR"); 534 535 mode_t runtimeMode = octal!700; 536 537 if (!runtime.length) { 538 setpwent(); 539 passwd* pw = getpwuid(uid); 540 endpwent(); 541 542 if (pw && pw.pw_name) { 543 runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name)); 544 545 if (!(runtime.exists && runtime.isDir)) { 546 if (mkdir(runtime.toStringz, runtimeMode) != 0) { 547 stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno))); 548 return null; 549 } 550 } 551 } else { 552 stderr.writefln("Failed to get user name to create runtime directory"); 553 return null; 554 } 555 } 556 stat_t statbuf; 557 stat(runtime.toStringz, &statbuf); 558 if (statbuf.st_uid != uid) { 559 stderr.writefln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid); 560 return null; 561 } 562 if ((statbuf.st_mode & octal!777) != runtimeMode) { 563 stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode); 564 return null; 565 } 566 567 return runtime; 568 } 569 570 string writablePath(StandardPath type) 571 { 572 final switch(type) { 573 case StandardPath.Config: 574 return xdgBaseDir("XDG_CONFIG_HOME", "/.config"); 575 case StandardPath.Cache: 576 return xdgBaseDir("XDG_CACHE_HOME", "/.cache"); 577 case StandardPath.Data: 578 return xdgBaseDir("XDG_DATA_HOME", "/.local/share"); 579 case StandardPath.Desktop: 580 return xdgUserDir("DESKTOP", "/Desktop"); 581 case StandardPath.Documents: 582 return xdgUserDir("DOCUMENTS", "/Documents"); 583 case StandardPath.Pictures: 584 return xdgUserDir("PICTURES", "/Pictures"); 585 case StandardPath.Music: 586 return xdgUserDir("MUSIC", "/Music"); 587 case StandardPath.Videos: 588 return xdgUserDir("VIDEOS", "/Videos"); 589 case StandardPath.Download: 590 return xdgUserDir("DOWNLOAD", "/Downloads"); 591 case StandardPath.Templates: 592 return xdgUserDir("TEMPLATES", "/Templates"); 593 case StandardPath.PublicShare: 594 return xdgUserDir("PUBLICSHARE", "/Public"); 595 case StandardPath.Fonts: 596 return homeConcat(homeDir(), "/.fonts"); 597 598 case StandardPath.Applications: 599 return null; 600 } 601 } 602 603 string[] standardPaths(StandardPath type) 604 { 605 string[] paths; 606 607 if (type == StandardPath.Data) { 608 paths = xdgDataDirs(); 609 } else if (type == StandardPath.Config) { 610 paths = xdgConfigDirs(); 611 } else if (type == StandardPath.Fonts) { 612 return fontPaths(); 613 } 614 615 string userPath = writablePath(type); 616 if (userPath.length) { 617 paths = userPath ~ paths; 618 } 619 return paths; 620 } 621 } 622 623 private bool isExecutable(string filePath) { 624 version(Posix) { 625 return (getAttributes(filePath) & octal!100) != 0; 626 } else version(Windows) { 627 //Use GetEffectiveRightsFromAclW? 628 629 const(string)[] exeExtensions = executableExtensions(); 630 foreach(ext; exeExtensions) { 631 if (filePath.extension == ext) 632 return true; 633 } 634 return false; 635 636 } else { 637 static assert(false, "Unsupported platform"); 638 } 639 } 640 641 private string checkExecutable(string filePath) { 642 try { 643 if (filePath.isFile && filePath.isExecutable) { 644 return buildNormalizedPath(filePath); 645 } else { 646 return null; 647 } 648 } 649 catch(FileException e) { 650 return null; 651 } 652 } 653 654 /** 655 * Finds executable by $(B fileName) in the paths specified by $(B paths). 656 * Returns: absolute path to the existing executable file or an empty string if not found. 657 * Params: 658 * fileName = name of executable to search 659 * paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable 660 * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search. 661 */ 662 string findExecutable(string fileName, in string[] paths = []) 663 { 664 if (fileName.isAbsolute()) { 665 return checkExecutable(fileName); 666 } 667 668 const(string)[] searchPaths = paths; 669 if (searchPaths.empty) { 670 string pathVar = environment.get("PATH"); 671 if (pathVar.length) { 672 searchPaths = splitter(pathVar, pathVarSeparator).array; 673 } 674 } 675 676 if (searchPaths.empty) { 677 return null; 678 } 679 680 string toReturn; 681 foreach(string path; searchPaths) { 682 string candidate = buildPath(absolutePath(path), fileName); 683 684 version(Windows) { 685 if (candidate.extension.empty) { 686 foreach(exeExtension; executableExtensions()) { 687 toReturn = checkExecutable(setExtension(candidate, exeExtension)); 688 if (toReturn.length) { 689 return toReturn; 690 } 691 } 692 } 693 } 694 695 toReturn = checkExecutable(candidate); 696 if (toReturn.length) { 697 return toReturn; 698 } 699 } 700 return null; 701 } 702 703