| 1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
| 2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
| 3 | #pragma once |
| 4 | |
| 5 | #include "Prerequisites/BsPlatformDefines.h" |
| 6 | #include "String/BsString.h" |
| 7 | #include "Utility/BsUtil.h" |
| 8 | |
| 9 | namespace bs |
| 10 | { |
| 11 | /** @addtogroup Filesystem |
| 12 | * @{ |
| 13 | */ |
| 14 | |
| 15 | /** |
| 16 | * Class for storing and manipulating file paths. Paths may be parsed from and to raw strings according to various |
| 17 | * platform specific path types. |
| 18 | * |
| 19 | * @note |
| 20 | * In order to allow the system to easily distinguish between file and directory paths, try to ensure that all directory |
| 21 | * paths end with a separator (\ or / depending on platform). System won't fail if you don't but it will be easier to |
| 22 | * misuse. |
| 23 | */ |
| 24 | class BS_UTILITY_EXPORT Path |
| 25 | { |
| 26 | public: |
| 27 | enum class PathType |
| 28 | { |
| 29 | Windows, |
| 30 | Unix, |
| 31 | Default |
| 32 | }; |
| 33 | |
| 34 | public: |
| 35 | Path() = default; |
| 36 | |
| 37 | /** |
| 38 | * Constructs a path by parsing the provided path string. Throws exception if provided path is not valid. |
| 39 | * |
| 40 | * @param[in] pathStr String containing the path. Ideally this should be an UTF-8 encoded string in order to |
| 41 | * support non-ANSI characters in the path. |
| 42 | * @param[in] type If set to default path will be parsed according to the rules of the platform the application |
| 43 | * is being compiled to. Otherwise it will be parsed according to provided type. |
| 44 | */ |
| 45 | Path(const String& pathStr, PathType type = PathType::Default); |
| 46 | |
| 47 | /** |
| 48 | * Constructs a path by parsing the provided path null terminated string. Throws exception if provided path is |
| 49 | * not valid. |
| 50 | * |
| 51 | * @param[in] pathStr Null-terminated string containing the path. Ideally this should be an UTF-8 encoded string |
| 52 | * in order to support non-ANSI characters in the path. |
| 53 | * @param[in] type If set to default path will be parsed according to the rules of the platform the application |
| 54 | * is being compiled to. Otherwise it will be parsed according to provided type. |
| 55 | */ |
| 56 | Path(const char* pathStr, PathType type = PathType::Default); |
| 57 | Path(const Path& other); |
| 58 | |
| 59 | /** |
| 60 | * Assigns a path by parsing the provided path string. Path will be parsed according to the rules of the platform |
| 61 | * the application is being compiled to. |
| 62 | */ |
| 63 | Path& operator= (const String& pathStr); |
| 64 | |
| 65 | /** |
| 66 | * Assigns a path by parsing the provided path null terminated string. Path will be parsed according to the rules |
| 67 | * of the platform the application is being compiled to. |
| 68 | */ |
| 69 | Path& operator= (const char* pathStr); |
| 70 | |
| 71 | Path& operator= (const Path& path); |
| 72 | |
| 73 | /** |
| 74 | * Compares two paths and returns true if they match. Comparison is case insensitive and paths will be compared |
| 75 | * as-is, without canonization. |
| 76 | */ |
| 77 | bool operator== (const Path& path) const { return equals(path); } |
| 78 | |
| 79 | /** |
| 80 | * Compares two paths and returns true if they don't match. Comparison is case insensitive and paths will be |
| 81 | * compared as-is, without canonization. |
| 82 | */ |
| 83 | bool operator!= (const Path& path) const { return !equals(path); } |
| 84 | |
| 85 | /** Gets a directory name with the specified index from the path. */ |
| 86 | const String& operator[] (UINT32 idx) const { return getDirectory(idx); } |
| 87 | |
| 88 | /** Swap internal data with another Path object. */ |
| 89 | void swap(Path& path); |
| 90 | |
| 91 | /** Create a path from another Path object. */ |
| 92 | void assign(const Path& path); |
| 93 | |
| 94 | /** |
| 95 | * Constructs a path by parsing the provided path string. Throws exception if provided path is not valid. |
| 96 | * |
| 97 | * @param[in] pathStr String containing the path. |
| 98 | * @param[in] type If set to default path will be parsed according to the rules of the platform the application |
| 99 | * is being compiled to. Otherwise it will be parsed according to provided type. |
| 100 | */ |
| 101 | void assign(const String& pathStr, PathType type = PathType::Default); |
| 102 | |
| 103 | /** |
| 104 | * Constructs a path by parsing the provided path null terminated string. Throws exception if provided path is not |
| 105 | * valid. |
| 106 | * |
| 107 | * @param[in] pathStr Null-terminated string containing the path. |
| 108 | * @param[in] type If set to default path will be parsed according to the rules of the platform the |
| 109 | * application is being compiled to. Otherwise it will be parsed according to provided |
| 110 | * type. |
| 111 | */ |
| 112 | void assign(const char* pathStr, PathType type = PathType::Default); |
| 113 | |
| 114 | /** |
| 115 | * Converts the path in a string according to platform path rules. |
| 116 | * |
| 117 | * @param[in] type If set to default path will be parsed according to the rules of the platform the application is |
| 118 | * being compiled to. Otherwise it will be parsed according to provided type. |
| 119 | * @return String representing the path using the UTF8 string encoding. |
| 120 | */ |
| 121 | String toString(PathType type = PathType::Default) const; |
| 122 | |
| 123 | /** |
| 124 | * Converts the path to either a string or a wstring, doing The Right Thing for the current platform. |
| 125 | * |
| 126 | * This method is equivalent to toWString() on Windows, and to toString() elsewhere. |
| 127 | */ |
| 128 | #if BS_PLATFORM == BS_PLATFORM_WIN32 |
| 129 | WString toPlatformString() const; |
| 130 | #else |
| 131 | String toPlatformString() const { return toString(); } |
| 132 | #endif |
| 133 | |
| 134 | /** Checks is the path a directory (contains no file-name). */ |
| 135 | bool isDirectory() const { return mFilename.empty(); } |
| 136 | |
| 137 | /** Checks does the path point to a file. */ |
| 138 | bool isFile() const { return !mFilename.empty(); } |
| 139 | |
| 140 | /** Checks is the contained path absolute. */ |
| 141 | bool isAbsolute() const { return mIsAbsolute; } |
| 142 | |
| 143 | /** |
| 144 | * Returns parent path. If current path points to a file the parent path will be the folder where the file is located. |
| 145 | * Or if it contains a directory the parent will be the parent directory. If no parent exists, same path will be |
| 146 | * returned. |
| 147 | */ |
| 148 | Path getParent() const; |
| 149 | |
| 150 | /** |
| 151 | * Returns an absolute path by appending the current path to the provided base. If path was already absolute no |
| 152 | * changes are made and copy of current path is returned. If base is not absolute, then the returned path will be |
| 153 | * made relative to base, but will not be absolute. |
| 154 | */ |
| 155 | Path getAbsolute(const Path& base) const; |
| 156 | |
| 157 | /** |
| 158 | * Returns a relative path by making the current path relative to the provided base. Base must be a part of the |
| 159 | * current path. If base is not a part of the path no changes are made and a copy of the current path is returned. |
| 160 | */ |
| 161 | Path getRelative(const Path& base) const; |
| 162 | |
| 163 | /** |
| 164 | * Returns the path as a path to directory. If path was pointing to a file, the filename is removed, otherwise no |
| 165 | * changes are made and exact copy is returned. |
| 166 | */ |
| 167 | Path getDirectory() const; |
| 168 | |
| 169 | /** |
| 170 | * Makes the path the parent of the current path. If current path points to a file the parent path will be the |
| 171 | * folder where the file is located. Or if it contains a directory the parent will be the parent directory. If no |
| 172 | * parent exists, same path will be returned. |
| 173 | */ |
| 174 | Path& makeParent(); |
| 175 | |
| 176 | /** |
| 177 | * Makes the current path absolute by appending it to base. If path was already absolute no changes are made and |
| 178 | * copy of current path is returned. If base is not absolute, then the returned path will be made relative to base, |
| 179 | * but will not be absolute. |
| 180 | */ |
| 181 | Path& makeAbsolute(const Path& base); |
| 182 | |
| 183 | /** |
| 184 | * Makes the current path relative to the provided base. Base must be a part of the current path. If base is not |
| 185 | * a part of the path no changes are made and a copy of the current path is returned. |
| 186 | */ |
| 187 | Path& makeRelative(const Path& base); |
| 188 | |
| 189 | /** Appends another path to the end of this path. */ |
| 190 | Path& append(const Path& path); |
| 191 | |
| 192 | /** |
| 193 | * Checks if the current path contains the provided path. Comparison is case insensitive and paths will be compared |
| 194 | * as-is, without canonization. |
| 195 | */ |
| 196 | bool includes(const Path& child) const; |
| 197 | |
| 198 | /** |
| 199 | * Compares two paths and returns true if they match. Comparison is case insensitive and paths will be compared |
| 200 | * as-is, without canonization. |
| 201 | */ |
| 202 | bool equals(const Path& other) const; |
| 203 | |
| 204 | /** Change or set the filename in the path. */ |
| 205 | void setFilename(const String& filename) { mFilename = filename; } |
| 206 | |
| 207 | /** |
| 208 | * Change or set the base name in the path. Base name changes the filename by changing its base to the provided |
| 209 | * value but keeping extension intact. |
| 210 | */ |
| 211 | void setBasename(const String& basename); |
| 212 | |
| 213 | /** |
| 214 | * Change or set the extension of the filename in the path. |
| 215 | * |
| 216 | * @param[in] extension Extension with a leading ".". |
| 217 | */ |
| 218 | void setExtension(const String& extension); |
| 219 | |
| 220 | /** Returns a filename with extension. */ |
| 221 | const String& getFilename() const { return mFilename; } |
| 222 | |
| 223 | /** |
| 224 | * Returns a filename in the path. |
| 225 | * |
| 226 | * @param[in] extension If true, returned filename will contain an extension. |
| 227 | */ |
| 228 | String getFilename(bool extension) const; |
| 229 | |
| 230 | /** Returns file extension with the leading ".". */ |
| 231 | String getExtension() const; |
| 232 | |
| 233 | /** Gets the number of directories in the path. */ |
| 234 | UINT32 getNumDirectories() const { return (UINT32)mDirectories.size(); } |
| 235 | |
| 236 | /** Gets a directory name with the specified index from the path. */ |
| 237 | const String& getDirectory(UINT32 idx) const; |
| 238 | |
| 239 | /** Returns path device (for example drive, volume, etc.) if one exists in the path. */ |
| 240 | const String& getDevice() const { return mDevice; } |
| 241 | |
| 242 | /** Returns path node (for example network name) if one exists in the path. */ |
| 243 | const String& getNode() const { return mNode; } |
| 244 | |
| 245 | /** |
| 246 | * Gets last element in the path, filename if it exists, otherwise the last directory. If no directories exist |
| 247 | * returns device or node. |
| 248 | */ |
| 249 | const String& getTail() const; |
| 250 | |
| 251 | /** Clears the path to nothing. */ |
| 252 | void clear(); |
| 253 | |
| 254 | /** Returns true if no path has been set. */ |
| 255 | bool isEmpty() const { return mDirectories.empty() && mFilename.empty() && mDevice.empty() && mNode.empty(); } |
| 256 | |
| 257 | /** Concatenates two paths. */ |
| 258 | Path operator+ (const Path& rhs) const; |
| 259 | |
| 260 | /** Concatenates two paths. */ |
| 261 | Path& operator+= (const Path& rhs); |
| 262 | |
| 263 | /** Compares two path elements (filenames, directory names, etc.). */ |
| 264 | static bool comparePathElem(const String& left, const String& right); |
| 265 | |
| 266 | /** Combines two paths and returns the result. Right path should be relative. */ |
| 267 | static Path combine(const Path& left, const Path& right); |
| 268 | |
| 269 | /** Strips invalid characters from the provided string and replaces them with empty spaces. */ |
| 270 | static void stripInvalid(String& path); |
| 271 | |
| 272 | static const Path BLANK; |
| 273 | private: |
| 274 | /** |
| 275 | * Constructs a path by parsing the provided raw string data. Throws exception if provided path is not valid. |
| 276 | * |
| 277 | * @param[in] pathStr String containing the path. |
| 278 | * @param[in] numChars Number of character in the provided path string. |
| 279 | * @param[in] type If set to default path will be parsed according to the rules of the platform the |
| 280 | * application is being compiled to. Otherwise it will be parsed according to provided |
| 281 | * type. |
| 282 | */ |
| 283 | void assign(const char* pathStr, UINT32 numChars, PathType type = PathType::Default); |
| 284 | |
| 285 | /** Parses a Windows path and stores the parsed data internally. Throws an exception if parsing fails. */ |
| 286 | template<class T> |
| 287 | void parseWindows(const T* pathStr, UINT32 numChars) |
| 288 | { |
| 289 | clear(); |
| 290 | |
| 291 | UINT32 idx = 0; |
| 292 | BasicStringStream<T> tempStream; |
| 293 | |
| 294 | if (idx < numChars) |
| 295 | { |
| 296 | if (pathStr[idx] == '\\' || pathStr[idx] == '/') |
| 297 | { |
| 298 | mIsAbsolute = true; |
| 299 | idx++; |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | if (idx < numChars) |
| 304 | { |
| 305 | // Path starts with a node, a drive letter or is relative |
| 306 | if (mIsAbsolute && (pathStr[idx] == '\\' || pathStr[idx] == '/')) // Node |
| 307 | { |
| 308 | idx++; |
| 309 | |
| 310 | tempStream.str(BasicString<T>()); |
| 311 | tempStream.clear(); |
| 312 | while (idx < numChars && pathStr[idx] != '\\' && pathStr[idx] != '/') |
| 313 | tempStream << pathStr[idx++]; |
| 314 | |
| 315 | setNode(tempStream.str()); |
| 316 | |
| 317 | if (idx < numChars) |
| 318 | idx++; |
| 319 | } |
| 320 | else // A drive letter or not absolute |
| 321 | { |
| 322 | T drive = pathStr[idx]; |
| 323 | idx++; |
| 324 | |
| 325 | if (idx < numChars && pathStr[idx] == ':') |
| 326 | { |
| 327 | if (mIsAbsolute || !((drive >= 'a' && drive <= 'z') || (drive >= 'A' && drive <= 'Z'))) |
| 328 | throwInvalidPathException(BasicString<T>(pathStr, numChars)); |
| 329 | |
| 330 | mIsAbsolute = true; |
| 331 | setDevice(String(1, drive)); |
| 332 | |
| 333 | idx++; |
| 334 | |
| 335 | if (idx >= numChars || (pathStr[idx] != '\\' && pathStr[idx] != '/')) |
| 336 | throwInvalidPathException(BasicString<T>(pathStr, numChars)); |
| 337 | |
| 338 | idx++; |
| 339 | } |
| 340 | else |
| 341 | idx--; |
| 342 | } |
| 343 | |
| 344 | while (idx < numChars) |
| 345 | { |
| 346 | tempStream.str(BasicString<T>()); |
| 347 | tempStream.clear(); |
| 348 | while (idx < numChars && pathStr[idx] != '\\' && pathStr[idx] != '/') |
| 349 | { |
| 350 | tempStream << pathStr[idx]; |
| 351 | idx++; |
| 352 | } |
| 353 | |
| 354 | if (idx < numChars) |
| 355 | pushDirectory(tempStream.str()); |
| 356 | else |
| 357 | setFilename(tempStream.str()); |
| 358 | |
| 359 | idx++; |
| 360 | } |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /** Parses a Unix path and stores the parsed data internally. Throws an exception if parsing fails. */ |
| 365 | template<class T> |
| 366 | void parseUnix(const T* pathStr, UINT32 numChars) |
| 367 | { |
| 368 | clear(); |
| 369 | |
| 370 | UINT32 idx = 0; |
| 371 | BasicStringStream<T> tempStream; |
| 372 | |
| 373 | if (idx < numChars) |
| 374 | { |
| 375 | if (pathStr[idx] == '/') |
| 376 | { |
| 377 | mIsAbsolute = true; |
| 378 | idx++; |
| 379 | } |
| 380 | else if (pathStr[idx] == '~') |
| 381 | { |
| 382 | idx++; |
| 383 | if (idx >= numChars || pathStr[idx] == '/') |
| 384 | { |
| 385 | pushDirectory(String("~" )); |
| 386 | mIsAbsolute = true; |
| 387 | } |
| 388 | else |
| 389 | idx--; |
| 390 | } |
| 391 | |
| 392 | while (idx < numChars) |
| 393 | { |
| 394 | tempStream.str(BasicString<T>()); |
| 395 | tempStream.clear(); |
| 396 | while (idx < numChars && pathStr[idx] != '/') |
| 397 | { |
| 398 | tempStream << pathStr[idx]; |
| 399 | idx++; |
| 400 | } |
| 401 | |
| 402 | if (idx < numChars) |
| 403 | { |
| 404 | if (mDirectories.empty()) |
| 405 | { |
| 406 | BasicString<T> deviceStr = tempStream.str(); |
| 407 | if (!deviceStr.empty() && *(deviceStr.rbegin()) == ':') |
| 408 | { |
| 409 | setDevice(deviceStr.substr(0, deviceStr.length() - 1)); |
| 410 | mIsAbsolute = true; |
| 411 | } |
| 412 | else |
| 413 | { |
| 414 | pushDirectory(deviceStr); |
| 415 | } |
| 416 | } |
| 417 | else |
| 418 | { |
| 419 | pushDirectory(tempStream.str()); |
| 420 | } |
| 421 | } |
| 422 | else |
| 423 | { |
| 424 | setFilename(tempStream.str()); |
| 425 | } |
| 426 | |
| 427 | idx++; |
| 428 | } |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | void setNode(const String& node) { mNode = node; } |
| 433 | void setDevice(const String& device) { mDevice = device; } |
| 434 | |
| 435 | /** Build a Windows path string from internal path data. */ |
| 436 | String buildWindows() const; |
| 437 | |
| 438 | /** Build a Unix path string from internal path data. */ |
| 439 | String buildUnix() const; |
| 440 | |
| 441 | /** Add new directory to the end of the path. */ |
| 442 | void pushDirectory(const String& dir); |
| 443 | |
| 444 | /** Helper method that throws invalid path exception. */ |
| 445 | void throwInvalidPathException(const String& path) const; |
| 446 | private: |
| 447 | friend struct RTTIPlainType<Path>; // For serialization |
| 448 | friend struct ::std::hash<bs::Path>; |
| 449 | |
| 450 | Vector<String> mDirectories; |
| 451 | String mDevice; |
| 452 | String mFilename; |
| 453 | String mNode; |
| 454 | bool mIsAbsolute = false; |
| 455 | }; |
| 456 | |
| 457 | /** @cond SPECIALIZATIONS */ |
| 458 | |
| 459 | /** |
| 460 | * RTTIPlainType specialization for Path that allows paths be serialized as value types. |
| 461 | * |
| 462 | * @see RTTIPlainType |
| 463 | */ |
| 464 | template<> struct RTTIPlainType<Path> |
| 465 | { |
| 466 | enum { id = TID_Path }; enum { hasDynamicSize = 1 }; |
| 467 | |
| 468 | static void toMemory(const Path& data, char* memory) |
| 469 | { |
| 470 | UINT32 size = getDynamicSize(data); |
| 471 | memcpy(memory, &size, sizeof(UINT32)); |
| 472 | memory += sizeof(UINT32); |
| 473 | |
| 474 | memory = rttiWriteElem(data.mDevice, memory); |
| 475 | memory = rttiWriteElem(data.mNode, memory); |
| 476 | memory = rttiWriteElem(data.mFilename, memory); |
| 477 | memory = rttiWriteElem(data.mIsAbsolute, memory); |
| 478 | rttiWriteElem(data.mDirectories, memory); |
| 479 | } |
| 480 | |
| 481 | static UINT32 fromMemory(Path& data, char* memory) |
| 482 | { |
| 483 | UINT32 size; |
| 484 | memcpy(&size, memory, sizeof(UINT32)); |
| 485 | memory += sizeof(UINT32); |
| 486 | |
| 487 | memory = rttiReadElem(data.mDevice, memory); |
| 488 | memory = rttiReadElem(data.mNode, memory); |
| 489 | memory = rttiReadElem(data.mFilename, memory); |
| 490 | memory = rttiReadElem(data.mIsAbsolute, memory); |
| 491 | rttiReadElem(data.mDirectories, memory); |
| 492 | |
| 493 | return size; |
| 494 | } |
| 495 | |
| 496 | static UINT32 getDynamicSize(const Path& data) |
| 497 | { |
| 498 | UINT64 dataSize = rttiGetElemSize(data.mDevice) + rttiGetElemSize(data.mNode) + rttiGetElemSize(data.mFilename) + |
| 499 | rttiGetElemSize(data.mIsAbsolute) + rttiGetElemSize(data.mDirectories) + sizeof(UINT32); |
| 500 | |
| 501 | #if BS_DEBUG_MODE |
| 502 | if (dataSize > std::numeric_limits<UINT32>::max()) |
| 503 | { |
| 504 | __string_throwDataOverflowException(); |
| 505 | } |
| 506 | #endif |
| 507 | |
| 508 | return (UINT32)dataSize; |
| 509 | } |
| 510 | }; |
| 511 | |
| 512 | /** @endcond */ |
| 513 | /** @} */ |
| 514 | } |
| 515 | |
| 516 | /** @cond STDLIB */ |
| 517 | |
| 518 | namespace std |
| 519 | { |
| 520 | /** Hash value generator for Path. */ |
| 521 | template<> |
| 522 | struct hash<bs::Path> |
| 523 | { |
| 524 | size_t operator()(const bs::Path& path) const |
| 525 | { |
| 526 | size_t hash = 0; |
| 527 | bs::bs_hash_combine(hash, path.mFilename); |
| 528 | bs::bs_hash_combine(hash, path.mDevice); |
| 529 | bs::bs_hash_combine(hash, path.mNode); |
| 530 | |
| 531 | for (auto& dir : path.mDirectories) |
| 532 | bs::bs_hash_combine(hash, dir); |
| 533 | |
| 534 | return hash; |
| 535 | } |
| 536 | }; |
| 537 | } |
| 538 | |
| 539 | /** @endcond */ |
| 540 | |