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.string : toStringz; 46 47 static if (is(typeof({import std.string : fromStringz;}))) { 48 import std.string : fromStringz; 49 } else { //own fromStringz implementation for compatibility reasons 50 import std.c.string : strlen; 51 @system pure inout(char)[] fromStringz(inout(char)* cString) { 52 return cString ? cString[0..strlen(cString)] : null; 53 } 54 } 55 56 string verifyIfNeeded(string path, bool shouldVerify) nothrow @trusted 57 { 58 if (path.length && shouldVerify) { 59 bool dirExists; 60 collectException(path.isDir, dirExists); 61 return dirExists ? path : null; 62 } else { 63 return path; 64 } 65 } 66 } 67 } else { 68 static assert(false, "Unsupported platform"); 69 } 70 71 /** 72 * Locations that can be passed to writablePath and standardPaths functions. 73 * See_Also: 74 * writablePath, standardPaths 75 */ 76 enum StandardPath { 77 /** 78 * General location of persisted application data. Every application should have its own subdirectory here. 79 * Note: on Windows it's the same as $(B config) path. 80 */ 81 data, 82 /** 83 * General location of configuration files. Every application should have its own subdirectory here. 84 * Note: on Windows it's the same as $(B data) path. 85 */ 86 config, 87 /** 88 * Location of cached data. 89 * Note: Not available on Windows. 90 */ 91 cache, 92 ///User's desktop directory. 93 desktop, 94 ///User's documents. 95 documents, 96 ///User's pictures. 97 pictures, 98 99 ///User's music. 100 music, 101 102 ///User's videos (movies). 103 videos, 104 105 ///Directory for user's downloaded files. 106 downloads, 107 108 /** 109 * Location of file templates (e.g. office suite document templates). 110 * Note: Not available on OS X. 111 */ 112 templates, 113 114 /** 115 * Public share folder. 116 * Note: Not available on Windows. 117 */ 118 publicShare, 119 /** 120 * Location of fonts files. 121 * 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) 122 */ 123 fonts, 124 /** 125 * User's applications. This has different meaning across platforms. 126 * On Windows it's directory where links to programs for Start menu are stored. 127 * On OS X it's folder where applications are typically put. 128 * On Freedesktop it's directory where .desktop files are put. 129 */ 130 applications, 131 132 /** 133 * Automatically started applications. 134 */ 135 startup 136 } 137 138 /** 139 * Control behavior of functions. 140 * See_Also: writablePath 141 */ 142 enum FolderFlag 143 { 144 none = 0, /// Don't verify that folder exist. 145 create = 1, /// Create if folder does not exist. 146 verify = 2 /// Verify that folder exists. 147 } 148 149 /** 150 * Current user home directory. 151 * Returns: Path to user home directory, or an empty string if could not determine home directory. 152 * Relies on environment variables. 153 * Note: This function does not cache its result. 154 */ 155 string homeDir() nothrow @safe 156 { 157 try { 158 version(Windows) { 159 //Use GetUserProfileDirectoryW from Userenv.dll? 160 string home = environment.get("USERPROFILE"); 161 if (home.empty) { 162 string homeDrive = environment.get("HOMEDRIVE"); 163 string homePath = environment.get("HOMEPATH"); 164 if (homeDrive.length && homePath.length) { 165 home = homeDrive ~ homePath; 166 } 167 } 168 return home; 169 } else { 170 string home = environment.get("HOME"); 171 return home; 172 } 173 } 174 catch (Exception e) { 175 debug { 176 @trusted void writeException(Exception e) nothrow { 177 collectException(stderr.writefln("Error when getting home directory %s", e.msg)); 178 } 179 writeException(e); 180 } 181 return null; 182 } 183 } 184 185 /** 186 * Getting writable paths for various locations. 187 * Returns: Path where files of $(U type) should be written to by current user, or an empty string if could not determine path. 188 * Params: 189 * type = Directory to lookup. 190 * params = Union of $(B FolderFlag)s. 191 * Note: This function does not cache its results. 192 * See_Also: FolderFlag 193 */ 194 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe; 195 196 /** 197 * Getting paths for various locations. 198 * 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). 199 * This function does not ensure if all returned paths exist and appear to be accessible directories. Returned strings are not required to be unique. 200 * Note: This function does cache its results. 201 * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation. 202 * See_Also: 203 * writablePath 204 */ 205 string[] standardPaths(StandardPath type) nothrow @safe; 206 207 208 version(StandardPathsDocs) 209 { 210 /** 211 * Path to runtime user directory on freedesktop platforms. 212 * Returns: User's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 213 * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string. 214 * Note: Deprecated, use xdgRuntimeDir instead. 215 */ 216 deprecated string runtimeDir() nothrow @trusted; 217 218 /** 219 * Path to $(B Roaming) data directory. 220 * Returns: User's Roaming directory. On fail returns an empty string. 221 * Note: This function is Windows only. 222 */ 223 string roamingPath(bool shouldCreate = false) nothrow @safe; 224 225 /** 226 * Location where games may store their saves. 227 * This is common path for games. One should use subfolder for their game saves. 228 * Returns: User's Saved Games directory. On fail returns an empty string. 229 * Note: This function is Windows only. 230 */ 231 string savedGames(bool shouldCreate = false) nothrow @safe; 232 } 233 234 version(Windows) { 235 private enum pathVarSeparator = ';'; 236 } else version(Posix) { 237 private enum pathVarSeparator = ':'; 238 } 239 240 version(Windows) { 241 242 private { 243 enum { 244 CSIDL_DESKTOP = 0, 245 CSIDL_INTERNET, 246 CSIDL_PROGRAMS, 247 CSIDL_CONTROLS, 248 CSIDL_PRINTERS, 249 CSIDL_PERSONAL, 250 CSIDL_FAVORITES, 251 CSIDL_STARTUP, 252 CSIDL_RECENT, 253 CSIDL_SENDTO, 254 CSIDL_BITBUCKET, 255 CSIDL_STARTMENU, // = 11 256 CSIDL_MYMUSIC = 13, 257 CSIDL_MYVIDEO, // = 14 258 CSIDL_DESKTOPDIRECTORY = 16, 259 CSIDL_DRIVES, 260 CSIDL_NETWORK, 261 CSIDL_NETHOOD, 262 CSIDL_FONTS, 263 CSIDL_TEMPLATES, 264 CSIDL_COMMON_STARTMENU, 265 CSIDL_COMMON_PROGRAMS, 266 CSIDL_COMMON_STARTUP, 267 CSIDL_COMMON_DESKTOPDIRECTORY, 268 CSIDL_APPDATA, 269 CSIDL_PRINTHOOD, 270 CSIDL_LOCAL_APPDATA, 271 CSIDL_ALTSTARTUP, 272 CSIDL_COMMON_ALTSTARTUP, 273 CSIDL_COMMON_FAVORITES, 274 CSIDL_INTERNET_CACHE, 275 CSIDL_COOKIES, 276 CSIDL_HISTORY, 277 CSIDL_COMMON_APPDATA, 278 CSIDL_WINDOWS, 279 CSIDL_SYSTEM, 280 CSIDL_PROGRAM_FILES, 281 CSIDL_MYPICTURES, 282 CSIDL_PROFILE, 283 CSIDL_SYSTEMX86, 284 CSIDL_PROGRAM_FILESX86, 285 CSIDL_PROGRAM_FILES_COMMON, 286 CSIDL_PROGRAM_FILES_COMMONX86, 287 CSIDL_COMMON_TEMPLATES, 288 CSIDL_COMMON_DOCUMENTS, 289 CSIDL_COMMON_ADMINTOOLS, 290 CSIDL_ADMINTOOLS, 291 CSIDL_CONNECTIONS, // = 49 292 CSIDL_COMMON_MUSIC = 53, 293 CSIDL_COMMON_PICTURES, 294 CSIDL_COMMON_VIDEO, 295 CSIDL_RESOURCES, 296 CSIDL_RESOURCES_LOCALIZED, 297 CSIDL_COMMON_OEM_LINKS, 298 CSIDL_CDBURN_AREA, // = 59 299 CSIDL_COMPUTERSNEARME = 61, 300 CSIDL_FLAG_DONT_VERIFY = 0x4000, 301 CSIDL_FLAG_CREATE = 0x8000, 302 CSIDL_FLAG_MASK = 0xFF00 303 } 304 305 enum { 306 KF_FLAG_SIMPLE_IDLIST = 0x00000100, 307 KF_FLAG_NOT_PARENT_RELATIVE = 0x00000200, 308 KF_FLAG_DEFAULT_PATH = 0x00000400, 309 KF_FLAG_INIT = 0x00000800, 310 KF_FLAG_NO_ALIAS = 0x00001000, 311 KF_FLAG_DONT_UNEXPAND = 0x00002000, 312 KF_FLAG_DONT_VERIFY = 0x00004000, 313 KF_FLAG_CREATE = 0x00008000, 314 KF_FLAG_NO_APPCONTAINER_REDIRECTION = 0x00010000, 315 KF_FLAG_ALIAS_ONLY = 0x80000000 316 }; 317 318 alias GUID KNOWNFOLDERID; 319 320 enum KNOWNFOLDERID FOLDERID_LocalAppData = {0xf1b32785, 0x6fba, 0x4fcf, [0x9d,0x55,0x7b,0x8e,0x7f,0x15,0x70,0x91]}; 321 enum KNOWNFOLDERID FOLDERID_RoamingAppData = {0x3eb685db, 0x65f9, 0x4cf6, [0xa0,0x3a,0xe3,0xef,0x65,0x72,0x9f,0x3d]}; 322 323 enum KNOWNFOLDERID FOLDERID_Desktop = {0xb4bfcc3a, 0xdb2c, 0x424c, [0xb0,0x29,0x7f,0xe9,0x9a,0x87,0xc6,0x41]}; 324 enum KNOWNFOLDERID FOLDERID_Documents = {0xfdd39ad0, 0x238f, 0x46af, [0xad,0xb4,0x6c,0x85,0x48,0x3,0x69,0xc7]}; 325 enum KNOWNFOLDERID FOLDERID_Downloads = {0x374de290, 0x123f, 0x4565, [0x91,0x64,0x39,0xc4,0x92,0x5e,0x46,0x7b]}; 326 enum KNOWNFOLDERID FOLDERID_Favorites = {0x1777f761, 0x68ad, 0x4d8a, [0x87,0xbd,0x30,0xb7,0x59,0xfa,0x33,0xdd]}; 327 enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]}; 328 enum KNOWNFOLDERID FOLDERID_Music = {0x4bd8d571, 0x6d19, 0x48d3, [0xbe,0x97,0x42,0x22,0x20,0x8,0xe,0x43]}; 329 enum KNOWNFOLDERID FOLDERID_Pictures = {0x33e28130, 0x4e1e, 0x4676, [0x83,0x5a,0x98,0x39,0x5c,0x3b,0xc3,0xbb]}; 330 enum KNOWNFOLDERID FOLDERID_Programs = {0xa77f5d77, 0x2e2b, 0x44c3, [0xa6,0xa2,0xab,0xa6,0x1,0x5,0x4a,0x51]}; 331 enum KNOWNFOLDERID FOLDERID_SavedGames = {0x4c5c32ff, 0xbb9d, 0x43b0, [0xb5,0xb4,0x2d,0x72,0xe5,0x4e,0xaa,0xa4]}; 332 enum KNOWNFOLDERID FOLDERID_Startup = {0xb97d20bb, 0xf46a, 0x4c97, [0xba,0x10,0x5e,0x36,0x8,0x43,0x8,0x54]}; 333 enum KNOWNFOLDERID FOLDERID_Templates = {0xa63293e8, 0x664e, 0x48db, [0xa0,0x79,0xdf,0x75,0x9e,0x5,0x9,0xf7]}; 334 enum KNOWNFOLDERID FOLDERID_Videos = {0x18989b1d, 0x99b5, 0x455b, [0x84,0x1c,0xab,0x7c,0x74,0xe4,0xdd,0xfc]}; 335 336 enum KNOWNFOLDERID FOLDERID_Fonts = {0xfd228cb7, 0xae11, 0x4ae3, [0x86,0x4c,0x16,0xf3,0x91,0xa,0xb8,0xfe]}; 337 enum KNOWNFOLDERID FOLDERID_ProgramData = {0x62ab5d82, 0xfdc1, 0x4dc3, [0xa9,0xdd,0x7,0xd,0x1d,0x49,0x5d,0x97]}; 338 enum KNOWNFOLDERID FOLDERID_CommonPrograms = {0x139d44e, 0x6afe, 0x49f2, [0x86,0x90,0x3d,0xaf,0xca,0xe6,0xff,0xb8]}; 339 enum KNOWNFOLDERID FOLDERID_CommonStartup = {0x82a5ea35, 0xd9cd, 0x47c5, [0x96,0x29,0xe1,0x5d,0x2f,0x71,0x4e,0x6e]}; 340 enum KNOWNFOLDERID FOLDERID_CommonTemplates = {0xb94237e7, 0x57ac, 0x4347, [0x91,0x51,0xb0,0x8c,0x6c,0x32,0xd1,0xf7]}; 341 342 enum KNOWNFOLDERID FOLDERID_PublicDesktop = {0xc4aa340d, 0xf20f, 0x4863, [0xaf,0xef,0xf8,0x7e,0xf2,0xe6,0xba,0x25]}; 343 enum KNOWNFOLDERID FOLDERID_PublicDocuments = {0xed4824af, 0xdce4, 0x45a8, [0x81,0xe2,0xfc,0x79,0x65,0x8,0x36,0x34]}; 344 enum KNOWNFOLDERID FOLDERID_PublicDownloads = {0x3d644c9b, 0x1fb8, 0x4f30, [0x9b,0x45,0xf6,0x70,0x23,0x5f,0x79,0xc0]}; 345 enum KNOWNFOLDERID FOLDERID_PublicMusic = {0x3214fab5, 0x9757, 0x4298, [0xbb,0x61,0x92,0xa9,0xde,0xaa,0x44,0xff]}; 346 enum KNOWNFOLDERID FOLDERID_PublicPictures = {0xb6ebfb86, 0x6907, 0x413c, [0x9a,0xf7,0x4f,0xc2,0xab,0xf0,0x7c,0xc5]}; 347 enum KNOWNFOLDERID FOLDERID_PublicVideos = {0x2400183a, 0x6185, 0x49fb, [0xa2,0xd8,0x4a,0x39,0x2a,0x60,0x2b,0xa3]}; 348 } 349 350 private { 351 extern(Windows) @nogc @system BOOL _dummy_SHGetSpecialFolderPath(HWND, wchar*, int, BOOL) nothrow { return 0; } 352 extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow { return 0; } 353 extern(Windows) @nogc @system void _dummy_CoTaskMemFree(void* pv) nothrow {return;} 354 355 __gshared typeof(&_dummy_SHGetSpecialFolderPath) ptrSHGetSpecialFolderPath = null; 356 __gshared typeof(&_dummy_SHGetKnownFolderPath) ptrSHGetKnownFolderPath = null; 357 __gshared typeof(&_dummy_CoTaskMemFree) ptrCoTaskMemFree = null; 358 359 @nogc @trusted bool hasSHGetSpecialFolderPath() nothrow { 360 return ptrSHGetSpecialFolderPath !is null; 361 } 362 363 @nogc @trusted bool hasSHGetKnownFolderPath() nothrow { 364 return ptrSHGetKnownFolderPath !is null && ptrCoTaskMemFree !is null; 365 } 366 } 367 368 shared static this() 369 { 370 HMODULE shellLib = LoadLibraryA("Shell32"); 371 if (shellLib !is null) { 372 ptrSHGetKnownFolderPath = cast(typeof(ptrSHGetKnownFolderPath))enforce(GetProcAddress(shellLib, "SHGetKnownFolderPath")); 373 if (ptrSHGetKnownFolderPath) { 374 HMODULE ole = LoadLibraryA("Ole32"); 375 if (ole !is null) { 376 ptrCoTaskMemFree = cast(typeof(ptrCoTaskMemFree))enforce(GetProcAddress(ole, "CoTaskMemFree")); 377 if (!ptrCoTaskMemFree) { 378 FreeLibrary(ole); 379 } 380 } 381 } 382 383 if (!hasSHGetKnownFolderPath()) { 384 ptrSHGetSpecialFolderPath = cast(typeof(ptrSHGetSpecialFolderPath))GetProcAddress(shellLib, "SHGetSpecialFolderPathW"); 385 } 386 } 387 } 388 389 private string getCSIDLFolder(int csidl, FolderFlag params = FolderFlag.none) nothrow @trusted { 390 import core.stdc.wchar_ : wcslen; 391 392 if (params & FolderFlag.create) { 393 csidl |= CSIDL_FLAG_CREATE; 394 } 395 if (!(params & FolderFlag.verify)) { 396 csidl |= CSIDL_FLAG_DONT_VERIFY; 397 } 398 wchar[MAX_PATH] path = void; 399 if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) { 400 size_t len = wcslen(path.ptr); 401 try { 402 return toUTF8(path[0..len]); 403 } catch(Exception e) { 404 405 } 406 } 407 return null; 408 } 409 410 private string getKnownFolder(const(KNOWNFOLDERID) folder, FolderFlag params = FolderFlag.none) nothrow @trusted { 411 import core.stdc.wchar_ : wcslen; 412 413 wchar* str; 414 415 DWORD flags = 0; 416 if (params & FolderFlag.create) { 417 flags |= KF_FLAG_CREATE; 418 } 419 if (!(params & FolderFlag.verify)) { 420 flags |= KF_FLAG_DONT_VERIFY; 421 } 422 423 if (hasSHGetKnownFolderPath() && ptrSHGetKnownFolderPath(&folder, flags, null, &str) == S_OK) { 424 scope(exit) ptrCoTaskMemFree(str); 425 try { 426 return str[0..wcslen(str)].toUTF8; 427 } catch(Exception e) { 428 429 } 430 } 431 return null; 432 } 433 434 string roamingPath(FolderFlag params = FolderFlag.none) nothrow @safe 435 { 436 if (hasSHGetKnownFolderPath()) { 437 return getKnownFolder(FOLDERID_RoamingAppData, params); 438 } else if (hasSHGetSpecialFolderPath()) { 439 return getCSIDLFolder(CSIDL_APPDATA, params); 440 } else { 441 return null; 442 } 443 } 444 445 string savedGames(FolderFlag params = FolderFlag.none) nothrow @safe 446 { 447 if (hasSHGetKnownFolderPath()) { 448 return getKnownFolder(FOLDERID_SavedGames, params); 449 } else { 450 return null; 451 } 452 } 453 454 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 455 { 456 if (hasSHGetKnownFolderPath()) { 457 final switch(type) { 458 case StandardPath.config: 459 case StandardPath.data: 460 return getKnownFolder(FOLDERID_LocalAppData, params); 461 case StandardPath.cache: 462 return null; 463 case StandardPath.desktop: 464 return getKnownFolder(FOLDERID_Desktop, params); 465 case StandardPath.documents: 466 return getKnownFolder(FOLDERID_Documents, params); 467 case StandardPath.pictures: 468 return getKnownFolder(FOLDERID_Pictures, params); 469 case StandardPath.music: 470 return getKnownFolder(FOLDERID_Music, params); 471 case StandardPath.videos: 472 return getKnownFolder(FOLDERID_Videos, params); 473 case StandardPath.downloads: 474 return getKnownFolder(FOLDERID_Downloads, params); 475 case StandardPath.templates: 476 return getKnownFolder(FOLDERID_Templates, params); 477 case StandardPath.publicShare: 478 return null; 479 case StandardPath.fonts: 480 return null; 481 case StandardPath.applications: 482 return getKnownFolder(FOLDERID_Programs, params); 483 case StandardPath.startup: 484 return getKnownFolder(FOLDERID_Startup, params); 485 } 486 } else if (hasSHGetSpecialFolderPath()) { 487 final switch(type) { 488 case StandardPath.config: 489 case StandardPath.data: 490 return getCSIDLFolder(CSIDL_LOCAL_APPDATA, params); 491 case StandardPath.cache: 492 return null; 493 case StandardPath.desktop: 494 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY, params); 495 case StandardPath.documents: 496 return getCSIDLFolder(CSIDL_PERSONAL, params); 497 case StandardPath.pictures: 498 return getCSIDLFolder(CSIDL_MYPICTURES, params); 499 case StandardPath.music: 500 return getCSIDLFolder(CSIDL_MYMUSIC, params); 501 case StandardPath.videos: 502 return getCSIDLFolder(CSIDL_MYVIDEO, params); 503 case StandardPath.downloads: 504 return null; 505 case StandardPath.templates: 506 return getCSIDLFolder(CSIDL_TEMPLATES, params); 507 case StandardPath.publicShare: 508 return null; 509 case StandardPath.fonts: 510 return null; 511 case StandardPath.applications: 512 return getCSIDLFolder(CSIDL_PROGRAMS, params); 513 case StandardPath.startup: 514 return getCSIDLFolder(CSIDL_STARTUP, params); 515 } 516 } else { 517 return null; 518 } 519 } 520 521 string[] standardPaths(StandardPath type) nothrow @safe 522 { 523 string commonPath; 524 525 if (hasSHGetKnownFolderPath()) { 526 switch(type) { 527 case StandardPath.config: 528 case StandardPath.data: 529 commonPath = getKnownFolder(FOLDERID_ProgramData); 530 break; 531 case StandardPath.desktop: 532 commonPath = getKnownFolder(FOLDERID_PublicDesktop); 533 break; 534 case StandardPath.documents: 535 commonPath = getKnownFolder(FOLDERID_PublicDocuments); 536 break; 537 case StandardPath.pictures: 538 commonPath = getKnownFolder(FOLDERID_PublicPictures); 539 break; 540 case StandardPath.music: 541 commonPath = getKnownFolder(FOLDERID_PublicMusic); 542 break; 543 case StandardPath.videos: 544 commonPath = getKnownFolder(FOLDERID_PublicVideos); 545 break; 546 case StandardPath.downloads: 547 commonPath = getKnownFolder(FOLDERID_PublicDownloads); 548 break; 549 case StandardPath.templates: 550 commonPath = getKnownFolder(FOLDERID_CommonTemplates); 551 break; 552 case StandardPath.fonts: 553 commonPath = getKnownFolder(FOLDERID_Fonts); 554 break; 555 case StandardPath.applications: 556 commonPath = getKnownFolder(FOLDERID_CommonPrograms); 557 break; 558 case StandardPath.startup: 559 commonPath = getKnownFolder(FOLDERID_CommonStartup); 560 break; 561 default: 562 break; 563 } 564 } else if (hasSHGetSpecialFolderPath()) { 565 switch(type) { 566 case StandardPath.config: 567 case StandardPath.data: 568 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA); 569 break; 570 case StandardPath.desktop: 571 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY); 572 break; 573 case StandardPath.documents: 574 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS); 575 break; 576 case StandardPath.pictures: 577 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES); 578 break; 579 case StandardPath.music: 580 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC); 581 break; 582 case StandardPath.videos: 583 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO); 584 break; 585 case StandardPath.templates: 586 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES); 587 break; 588 case StandardPath.fonts: 589 commonPath = getCSIDLFolder(CSIDL_FONTS); 590 break; 591 case StandardPath.applications: 592 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS); 593 break; 594 case StandardPath.startup: 595 commonPath = getCSIDLFolder(CSIDL_COMMON_STARTUP); 596 break; 597 default: 598 break; 599 } 600 } 601 602 string[] paths; 603 string userPath = writablePath(type); 604 if (userPath.length) 605 paths ~= userPath; 606 if (commonPath.length) 607 paths ~= commonPath; 608 return paths; 609 } 610 611 private const(string)[] executableExtensions() nothrow @trusted 612 { 613 static bool filenamesEqual(string first, string second) nothrow { 614 try { 615 return filenameCmp(first, second) == 0; 616 } catch(Exception e) { 617 return false; 618 } 619 } 620 621 static string[] extensions; 622 if (extensions.empty) { 623 collectException(environment.get("PATHEXT").splitter(pathVarSeparator).array, extensions); 624 if (canFind!(filenamesEqual)(extensions, ".exe") == false) { 625 extensions = [".exe", ".com", ".bat", ".cmd"]; 626 } 627 } 628 629 return extensions; 630 } 631 } else version(OSX) { 632 633 private enum : short { 634 kOnSystemDisk = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/ 635 kOnAppropriateDisk = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/ 636 /* Folder Domains - Carbon only. The constants above can continue to be used, but the folder/volume returned will*/ 637 /* be from one of the domains below.*/ 638 kSystemDomain = -32766, /* Read-only system hierarchy.*/ 639 kLocalDomain = -32765, /* All users of a single machine have access to these resources.*/ 640 kNetworkDomain = -32764, /* All users configured to use a common network server has access to these resources.*/ 641 kUserDomain = -32763, /* Read/write. Resources that are private to the user.*/ 642 kClassicDomain = -32762, /* Domain referring to the currently configured Classic System Folder. Not supported in Mac OS X Leopard and later.*/ 643 kFolderManagerLastDomain = -32760 644 } 645 646 private @nogc int k(string s) nothrow { 647 return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]; 648 } 649 650 private enum { 651 kDesktopFolderType = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */ 652 kTrashFolderType = k("trsh"), /* the trash folder; objects in this folder show up in the trash */ 653 kWhereToEmptyTrashFolderType = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */ 654 kFontsFolderType = k("font"), /* Fonts go here */ 655 kPreferencesFolderType = k("pref"), /* preferences for applications go here */ 656 kSystemPreferencesFolderType = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */ 657 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*/ 658 /* folders such that other users can not access the data inside. On Mac OS X 10.4 and later the data inside the temporary*/ 659 /* items folder is deleted at logout and at boot, but not otherwise. Earlier version of Mac OS X would delete items inside*/ 660 /* the temporary items folder after a period of inaccess. You can ask for a temporary item in a specific domain or on a */ 661 /* particular volume by FSVolumeRefNum. If you want a location for temporary items for a short time, then use either*/ 662 /* ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ). The kUserDomain varient will always be*/ 663 /* on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/ 664 /* 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*/ 665 /* file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/ 666 /* ask for the temporary items folder on the same volume as the file you are safe saving.*/ 667 /* However, be prepared for a failure to find a temporary folder in any domain or on any volume. Some volumes may not have*/ 668 /* a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/ 669 /* a temporary folder for the volume.*/ 670 /* If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/ 671 /* and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/ 672 /* restarts. Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/ 673 /* when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/ 674 kChewableItemsFolderType = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */ 675 kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/ 676 kApplicationsFolderType = k("apps"), /* Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/ 677 kVolumeRootFolderType = k("root"), /* root folder of a volume or domain */ 678 kDomainTopLevelFolderType = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/ 679 kDomainLibraryFolderType = k("dlib"), /* the Library subfolder of a particular domain*/ 680 kUsersFolderType = k("usrs"), /* "Users" folder, usually contains one folder for each user. */ 681 kCurrentUserFolderType = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */ 682 kSharedUserDataFolderType = k("sdat"), /* A Shared folder, readable & writeable by all users */ 683 kCachedDataFolderType = k("cach"), /* Contains various cache files for different clients*/ 684 kDownloadsFolderType = k("down"), /* Refers to the ~/Downloads folder*/ 685 kApplicationSupportFolderType = k("asup"), /* third-party items and folders */ 686 687 688 kDocumentsFolderType = k("docs"), /* User documents are typically put in this folder ( or a subfolder ).*/ 689 kPictureDocumentsFolderType = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/ 690 kMovieDocumentsFolderType = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/ 691 kMusicDocumentsFolderType = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/ 692 kInternetSitesFolderType = k("site"), /* Refers to the "Sites" folder in a users home directory*/ 693 kPublicFolderType = k("pubb"), /* Refers to the "Public" folder in a users home directory*/ 694 695 kDropBoxFolderType = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/ 696 }; 697 698 private { 699 struct FSRef { 700 char[80] hidden; /* private to File Manager*/ 701 }; 702 703 alias int Boolean; 704 alias int OSType; 705 alias short OSErr; 706 alias int OSStatus; 707 708 extern(C) @nogc @system OSErr _dummy_FSFindFolder(short, OSType, Boolean, FSRef*) nothrow { return 0; } 709 extern(C) @nogc @system OSStatus _dummy_FSRefMakePath(const(FSRef)*, char*, uint) nothrow { return 0; } 710 711 __gshared typeof(&_dummy_FSFindFolder) ptrFSFindFolder = null; 712 __gshared typeof(&_dummy_FSRefMakePath) ptrFSRefMakePath = null; 713 } 714 715 shared static this() 716 { 717 enum carbonPath = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore"; 718 719 import core.sys.posix.dlfcn; 720 721 void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL); 722 if (handle) { 723 ptrFSFindFolder = cast(typeof(ptrFSFindFolder))dlsym(handle, "FSFindFolder"); 724 ptrFSRefMakePath = cast(typeof(ptrFSRefMakePath))dlsym(handle, "FSRefMakePath"); 725 } 726 if (ptrFSFindFolder == null || ptrFSRefMakePath == null) { 727 debug collectException(stderr.writeln("Could not load carbon functions")); 728 if (handle) dlclose(handle); 729 } 730 } 731 732 private @nogc @trusted bool isCarbonLoaded() nothrow 733 { 734 return ptrFSFindFolder != null && ptrFSRefMakePath != null; 735 } 736 737 private enum OSErr noErr = 0; 738 739 private string fsPath(short domain, OSType type, bool shouldCreate = false) nothrow @trusted 740 { 741 import std.stdio; 742 FSRef fsref; 743 if (isCarbonLoaded() && ptrFSFindFolder(domain, type, shouldCreate, &fsref) == noErr) { 744 745 char[2048] buf; 746 char* path = buf.ptr; 747 if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) { 748 try { 749 return fromStringz(path).idup; 750 } 751 catch(Exception e) { 752 753 } 754 } 755 } 756 return null; 757 } 758 759 private string writablePathImpl(StandardPath type, bool shouldCreate = false) nothrow @safe 760 { 761 final switch(type) { 762 case StandardPath.config: 763 return fsPath(kUserDomain, kPreferencesFolderType, shouldCreate); 764 case StandardPath.cache: 765 return fsPath(kUserDomain, kCachedDataFolderType, shouldCreate); 766 case StandardPath.data: 767 return fsPath(kUserDomain, kApplicationSupportFolderType, shouldCreate); 768 case StandardPath.desktop: 769 return fsPath(kUserDomain, kDesktopFolderType, shouldCreate); 770 case StandardPath.documents: 771 return fsPath(kUserDomain, kDocumentsFolderType, shouldCreate); 772 case StandardPath.pictures: 773 return fsPath(kUserDomain, kPictureDocumentsFolderType, shouldCreate); 774 case StandardPath.music: 775 return fsPath(kUserDomain, kMusicDocumentsFolderType, shouldCreate); 776 case StandardPath.videos: 777 return fsPath(kUserDomain, kMovieDocumentsFolderType, shouldCreate); 778 case StandardPath.downloads: 779 return fsPath(kUserDomain, kDownloadsFolderType, shouldCreate); 780 case StandardPath.templates: 781 return null; 782 case StandardPath.publicShare: 783 return fsPath(kUserDomain, kPublicFolderType, shouldCreate); 784 case StandardPath.fonts: 785 return fsPath(kUserDomain, kFontsFolderType, shouldCreate); 786 case StandardPath.applications: 787 return fsPath(kUserDomain, kApplicationsFolderType, shouldCreate); 788 case StandardPath.startup: 789 return null; 790 } 791 } 792 793 private string writablePathImpl(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 794 { 795 const bool shouldCreate = (params & FolderFlag.create) != 0; 796 const bool shouldVerify = (params & FolderFlag.verify) != 0; 797 return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify); 798 } 799 800 string[] standardPaths(StandardPath type) nothrow @safe 801 { 802 string commonPath; 803 804 switch(type) { 805 case StandardPath.fonts: 806 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 807 break; 808 case StandardPath.applications: 809 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 810 break; 811 case StandardPath.data: 812 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 813 break; 814 case StandardPath.cache: 815 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 816 break; 817 default: 818 break; 819 } 820 821 string[] paths; 822 string userPath = writablePath(type); 823 if (userPath.length) 824 paths ~= userPath; 825 if (commonPath.length) 826 paths ~= commonPath; 827 return paths; 828 } 829 830 } else { 831 832 static if (!isFreedesktop) { 833 static assert(false, "Unsupported platform"); 834 } else { 835 public import xdgpaths; 836 837 private import std.algorithm : startsWith; 838 private import std.string; 839 private import std.traits; 840 841 //Concat two strings, but if the first one is empty, then null string is returned. 842 private string maybeConcat(string start, string path) nothrow @safe 843 { 844 return start.empty ? null : start ~ path; 845 } 846 847 unittest 848 { 849 assert(maybeConcat(null, "path") == string.init); 850 assert(maybeConcat("path", "/file") == "path/file"); 851 } 852 853 private @trusted string getFromUserDirs(Range)(string xdgdir, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range)) 854 { 855 foreach(line; range) { 856 line = strip(line); 857 auto index = xdgdir.length; 858 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') { 859 line = line[index+1..$]; 860 if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 861 { 862 line = line[1..$-1]; 863 864 if (line.startsWith("$HOME")) { 865 return maybeConcat(home, assumeUnique(line[5..$])); 866 } 867 if (line.length == 0 || line[0] != '/') { 868 continue; 869 } 870 return assumeUnique(line); 871 } 872 } 873 } 874 return null; 875 } 876 877 878 unittest 879 { 880 string content = 881 `# Comment 882 883 XDG_DOCUMENTS_DIR="$HOME/My Documents" 884 XDG_MUSIC_DIR="/data/Music" 885 XDG_VIDEOS_DIR="data/Video" 886 `; 887 string home = "/home/user"; 888 889 assert(getFromUserDirs("XDG_DOCUMENTS_DIR", home, content.splitLines) == "/home/user/My Documents"); 890 assert(getFromUserDirs("XDG_MUSIC_DIR", home, content.splitLines) == "/data/Music"); 891 assert(getFromUserDirs("XDG_DOWNLOAD_DIR", home, content.splitLines).empty); 892 assert(getFromUserDirs("XDG_VIDEOS_DIR", home, content.splitLines).empty); 893 } 894 895 private @trusted string getFromDefaultDirs(Range)(string key, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range)) 896 { 897 foreach(line; range) { 898 line = strip(line); 899 auto index = key.length; 900 if (line.startsWith(key) && line.length > index && line[index] == '=') 901 { 902 line = line[index+1..$]; 903 return home ~ "/" ~ assumeUnique(line); 904 } 905 } 906 return null; 907 } 908 909 unittest 910 { 911 string content = 912 `# Comment 913 914 DOCUMENTS=MyDocuments 915 PICTURES=Images 916 `; 917 string home = "/home/user"; 918 assert(getFromDefaultDirs("DOCUMENTS", home, content.splitLines) == "/home/user/MyDocuments"); 919 assert(getFromDefaultDirs("PICTURES", home, content.splitLines) == "/home/user/Images"); 920 assert(getFromDefaultDirs("VIDEOS", home, content.splitLines).empty); 921 } 922 923 private string xdgUserDir(string key, string fallback = null) nothrow @trusted { 924 string fileName = maybeConcat(writablePath(StandardPath.config), "/user-dirs.dirs"); 925 string home = homeDir(); 926 try { 927 auto f = File(fileName, "r"); 928 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 929 auto path = getFromUserDirs(xdgdir, home, f.byLine()); 930 if (path.length) { 931 return path; 932 } 933 } catch(Exception e) { 934 935 } 936 937 if (home.length) { 938 try { 939 auto f = File("/etc/xdg/user-dirs.defaults", "r"); 940 auto path = getFromDefaultDirs(key, home, f.byLine()); 941 if (path.length) { 942 return path; 943 } 944 } catch (Exception e) { 945 946 } 947 if (fallback.length) { 948 return home ~ fallback; 949 } 950 } 951 return null; 952 } 953 954 private string homeFontsPath() nothrow @trusted { 955 return maybeConcat(homeDir(), "/.fonts"); 956 } 957 958 private string[] fontPaths() nothrow @trusted 959 { 960 enum localShare = "/usr/local/share/fonts"; 961 enum share = "/usr/share/fonts"; 962 963 string homeFonts = homeFontsPath(); 964 if (homeFonts.length) { 965 return [homeFonts, localShare, share]; 966 } else { 967 return [localShare, share]; 968 } 969 } 970 971 private string createIfNeeded(string path, bool shouldCreate) nothrow @trusted 972 { 973 if (path.length && shouldCreate) { 974 bool pathExist; 975 collectException(path.isDir, pathExist); 976 if (pathExist || collectException(mkdirRecurse(path)) is null) { 977 return path; 978 } else { 979 return null; 980 } 981 } else { 982 return path; 983 } 984 } 985 986 private string writablePathImpl(StandardPath type, bool shouldCreate) nothrow @safe 987 { 988 final switch(type) { 989 case StandardPath.config: 990 return xdgConfigHome(null, shouldCreate); 991 case StandardPath.cache: 992 return xdgCacheHome(null, shouldCreate); 993 case StandardPath.data: 994 return xdgDataHome(null, shouldCreate); 995 case StandardPath.desktop: 996 return xdgUserDir("DESKTOP", "/Desktop").createIfNeeded(shouldCreate); 997 case StandardPath.documents: 998 return xdgUserDir("DOCUMENTS").createIfNeeded(shouldCreate); 999 case StandardPath.pictures: 1000 return xdgUserDir("PICTURES").createIfNeeded(shouldCreate); 1001 case StandardPath.music: 1002 return xdgUserDir("MUSIC").createIfNeeded(shouldCreate); 1003 case StandardPath.videos: 1004 return xdgUserDir("VIDEOS").createIfNeeded(shouldCreate); 1005 case StandardPath.downloads: 1006 return xdgUserDir("DOWNLOAD").createIfNeeded(shouldCreate); 1007 case StandardPath.templates: 1008 return xdgUserDir("TEMPLATES", "/Templates").createIfNeeded(shouldCreate); 1009 case StandardPath.publicShare: 1010 return xdgUserDir("PUBLICSHARE", "/Public").createIfNeeded(shouldCreate); 1011 case StandardPath.fonts: 1012 return homeFontsPath().createIfNeeded(shouldCreate); 1013 case StandardPath.applications: 1014 return xdgDataHome("applications", shouldCreate); 1015 case StandardPath.startup: 1016 return xdgConfigHome("autostart", shouldCreate); 1017 } 1018 } 1019 1020 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 1021 { 1022 const bool shouldCreate = (params & FolderFlag.create) != 0; 1023 const bool shouldVerify = (params & FolderFlag.verify) != 0; 1024 return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify); 1025 } 1026 1027 string[] standardPaths(StandardPath type) nothrow @safe 1028 { 1029 string[] paths; 1030 1031 switch(type) { 1032 case StandardPath.data: 1033 return xdgAllDataDirs(); 1034 case StandardPath.config: 1035 return xdgAllConfigDirs(); 1036 case StandardPath.applications: 1037 return xdgAllDataDirs("applications"); 1038 case StandardPath.startup: 1039 return xdgAllConfigDirs("autostart"); 1040 case StandardPath.fonts: 1041 return fontPaths(); 1042 default: 1043 break; 1044 } 1045 1046 string userPath = writablePath(type); 1047 if (userPath.length) { 1048 return [userPath]; 1049 } 1050 return null; 1051 } 1052 } 1053 } 1054 1055 private bool isExecutable(string filePath) nothrow @trusted { 1056 try { 1057 version(Posix) { 1058 import core.sys.posix.unistd; 1059 return access(toStringz(filePath), X_OK) == 0; 1060 } else version(Windows) { 1061 //Use GetEffectiveRightsFromAclW? 1062 1063 string extension = filePath.extension; 1064 const(string)[] exeExtensions = executableExtensions(); 1065 foreach(ext; exeExtensions) { 1066 if (sicmp(extension, ext) == 0) 1067 return true; 1068 } 1069 return false; 1070 1071 } else { 1072 static assert(false, "Unsupported platform"); 1073 } 1074 } catch(Exception e) { 1075 return false; 1076 } 1077 } 1078 1079 private string checkExecutable(string filePath) nothrow @trusted { 1080 try { 1081 if (filePath.isFile && filePath.isExecutable) { 1082 return buildNormalizedPath(filePath); 1083 } else { 1084 return null; 1085 } 1086 } 1087 catch(Exception e) { 1088 return null; 1089 } 1090 } 1091 1092 /** 1093 * System paths where executable files can be found. 1094 * Returns: Range of paths as determined by $(B PATH) environment variable. 1095 * Note: this function does not cache its result 1096 */ 1097 @trusted auto binPaths() 1098 { 1099 string pathVar; 1100 collectException(environment.get("PATH"), pathVar); 1101 return splitter(pathVar, pathVarSeparator).filter!(p => p.length != 0); 1102 } 1103 1104 /// 1105 unittest 1106 { 1107 static if (isFreedesktop) { 1108 import xdgpaths; 1109 import std.algorithm : equal; 1110 1111 auto pathGuard = EnvGuard("PATH"); 1112 1113 environment["PATH"] = ".:/usr/apps:/usr/local/apps:"; 1114 assert(equal(binPaths(), [".", "/usr/apps", "/usr/local/apps"])); 1115 } 1116 } 1117 1118 /** 1119 * Finds executable by $(B fileName) in the paths specified by $(B paths). 1120 * Returns: Absolute path to the existing executable file or an empty string if not found. 1121 * Params: 1122 * fileName = Name of executable to search. If it's absolute path, this function only checks if the file is executable. 1123 * paths = Range of directories where executable should be searched. 1124 * Note: On Windows when fileName extension is omitted, executable extensions will be automatically appended during search. 1125 */ 1126 @trusted nothrow string findExecutable(Range)(string fileName, Range paths) if (isInputRange!Range && is(ElementType!Range : string)) 1127 { 1128 try { 1129 if (fileName.isAbsolute()) { 1130 return checkExecutable(fileName); 1131 } else if (fileName == fileName.baseName) { 1132 string toReturn; 1133 foreach(string path; paths) { 1134 if (path.empty) { 1135 continue; 1136 } 1137 1138 string candidate = buildPath(absolutePath(path), fileName); 1139 1140 version(Windows) { 1141 if (candidate.extension.empty) { 1142 foreach(exeExtension; executableExtensions()) { 1143 toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower())); 1144 if (toReturn.length) { 1145 return toReturn; 1146 } 1147 } 1148 } 1149 } 1150 1151 toReturn = checkExecutable(candidate); 1152 if (toReturn.length) { 1153 return toReturn; 1154 } 1155 } 1156 } 1157 } catch (Exception e) { 1158 1159 } 1160 return null; 1161 } 1162 1163 /** 1164 * ditto, but searches in system paths, determined by $(B PATH) environment variable. 1165 * See_Also: binPaths 1166 */ 1167 @safe string findExecutable(string fileName) nothrow { 1168 try { 1169 return findExecutable(fileName, binPaths()); 1170 } catch(Exception e) { 1171 return null; 1172 } 1173 }