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
9namespace 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
518namespace 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