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 | |