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#include <Debug/BsDebug.h>
4#include "Prerequisites/BsPrerequisitesUtil.h"
5#include "Error/BsException.h"
6#include "String/BsUnicode.h"
7
8namespace bs
9{
10 const Path Path::BLANK = Path();
11
12 Path::Path(const String& pathStr, PathType type)
13 {
14 assign(pathStr, type);
15 }
16
17 Path::Path(const char* pathStr, PathType type)
18 {
19 assign(pathStr);
20 }
21
22 Path::Path(const Path& other)
23 {
24 assign(other);
25 }
26
27 Path& Path::operator= (const Path& path)
28 {
29 assign(path);
30 return *this;
31 }
32
33 Path& Path::operator= (const String& pathStr)
34 {
35 assign(pathStr);
36 return *this;
37 }
38
39 Path& Path::operator= (const char* pathStr)
40 {
41 assign(pathStr);
42 return *this;
43 }
44
45 void Path::swap(Path& path)
46 {
47 std::swap(mDirectories, path.mDirectories);
48 std::swap(mFilename, path.mFilename);
49 std::swap(mDevice, path.mDevice);
50 std::swap(mNode, path.mNode);
51 std::swap(mIsAbsolute, path.mIsAbsolute);
52 }
53
54 void Path::assign(const Path& path)
55 {
56 mDirectories = path.mDirectories;
57 mFilename = path.mFilename;
58 mDevice = path.mDevice;
59 mNode = path.mNode;
60 mIsAbsolute = path.mIsAbsolute;
61 }
62
63 void Path::assign(const String& pathStr, PathType type)
64 {
65 assign(pathStr.data(), (UINT32)pathStr.length(), type);
66 }
67
68 void Path::assign(const char* pathStr, PathType type)
69 {
70 assign(pathStr, (UINT32)strlen(pathStr), type);
71 }
72
73 void Path::assign(const char* pathStr, UINT32 numChars, PathType type)
74 {
75 switch (type)
76 {
77 case PathType::Windows:
78 parseWindows(pathStr, numChars);
79 break;
80 case PathType::Unix:
81 parseUnix(pathStr, numChars);
82 break;
83 default:
84#if BS_PLATFORM == BS_PLATFORM_WIN32
85 parseWindows(pathStr, numChars);
86#elif BS_PLATFORM == BS_PLATFORM_OSX || BS_PLATFORM == BS_PLATFORM_LINUX
87 parseUnix(pathStr, numChars);
88#else
89 static_assert(false, "Unsupported platform for path.");
90#endif
91 break;
92 }
93 }
94
95#if BS_PLATFORM == BS_PLATFORM_WIN32
96 WString Path::toPlatformString() const
97 {
98 return UTF8::toWide(toString());
99 }
100#endif
101
102 String Path::toString(PathType type) const
103 {
104 switch (type)
105 {
106 case PathType::Windows:
107 return buildWindows();
108 case PathType::Unix:
109 return buildUnix();
110 default:
111#if BS_PLATFORM == BS_PLATFORM_WIN32
112 return buildWindows();
113#elif BS_PLATFORM == BS_PLATFORM_OSX || BS_PLATFORM == BS_PLATFORM_LINUX
114 return buildUnix();
115#else
116 static_assert(false, "Unsupported platform for path.");
117#endif
118 break;
119 }
120 }
121
122 Path Path::getParent() const
123 {
124 Path copy = *this;
125 copy.makeParent();
126
127 return copy;
128 }
129
130 Path Path::getAbsolute(const Path& base) const
131 {
132 Path copy = *this;
133 copy.makeAbsolute(base);
134
135 return copy;
136 }
137
138 Path Path::getRelative(const Path& base) const
139 {
140 Path copy = *this;
141 copy.makeRelative(base);
142
143 return copy;
144 }
145
146 Path Path::getDirectory() const
147 {
148 Path copy = *this;
149 copy.mFilename.clear();
150
151 return copy;
152 }
153
154 Path& Path::makeParent()
155 {
156 if (mFilename.empty())
157 {
158 if (mDirectories.empty())
159 {
160 if (!mIsAbsolute)
161 mDirectories.push_back("..");
162 }
163 else
164 {
165 if (mDirectories.back() == "..")
166 mDirectories.push_back("..");
167 else
168 mDirectories.pop_back();
169 }
170 }
171 else
172 {
173 mFilename.clear();
174 }
175
176 return *this;
177 }
178
179 Path& Path::makeAbsolute(const Path& base)
180 {
181 if (mIsAbsolute)
182 return *this;
183
184 Path absDir = base.getDirectory();
185 if (base.isFile())
186 absDir.pushDirectory(base.mFilename);
187
188 for (auto& dir : mDirectories)
189 absDir.pushDirectory(dir);
190
191 absDir.setFilename(mFilename);
192 *this = absDir;
193
194 return *this;
195 }
196
197 Path& Path::makeRelative(const Path& base)
198 {
199 if (!base.includes(*this))
200 return *this;
201
202 mDirectories.erase(mDirectories.begin(), mDirectories.begin() + base.mDirectories.size());
203
204 // Sometimes a directory name can be interpreted as a file and we're okay with that. Check for that
205 // special case.
206 if (base.isFile())
207 {
208 if (mDirectories.size() > 0)
209 mDirectories.erase(mDirectories.begin());
210 else
211 mFilename = "";
212 }
213
214 mDevice = "";
215 mNode = "";
216 mIsAbsolute = false;
217
218 return *this;
219 }
220
221 bool Path::includes(const Path& child) const
222 {
223 if (mDevice != child.mDevice)
224 return false;
225
226 if (mNode != child.mNode)
227 return false;
228
229 auto iterParent = mDirectories.begin();
230 auto iterChild = child.mDirectories.begin();
231
232 for (; iterParent != mDirectories.end(); ++iterChild, ++iterParent)
233 {
234 if (iterChild == child.mDirectories.end())
235 return false;
236
237 if (!comparePathElem(*iterChild, *iterParent))
238 return false;
239 }
240
241 if (!mFilename.empty())
242 {
243 if (iterChild == child.mDirectories.end())
244 {
245 if (child.mFilename.empty())
246 return false;
247
248 if (!comparePathElem(child.mFilename, mFilename))
249 return false;
250 }
251 else
252 {
253 if (!comparePathElem(*iterChild, mFilename))
254 return false;
255 }
256 }
257
258 return true;
259 }
260
261 bool Path::equals(const Path& other) const
262 {
263 if (mIsAbsolute != other.mIsAbsolute)
264 return false;
265
266 if (mIsAbsolute)
267 {
268 if (!comparePathElem(mDevice, other.mDevice))
269 return false;
270 }
271
272 if (!comparePathElem(mNode, other.mNode))
273 return false;
274
275 UINT32 myNumElements = (UINT32)mDirectories.size();
276 UINT32 otherNumElements = (UINT32)other.mDirectories.size();
277
278 if (!mFilename.empty())
279 myNumElements++;
280
281 if (!other.mFilename.empty())
282 otherNumElements++;
283
284 if (myNumElements != otherNumElements)
285 return false;
286
287 if(myNumElements > 0)
288 {
289 auto iterMe = mDirectories.begin();
290 auto iterOther = other.mDirectories.begin();
291
292 for(UINT32 i = 0; i < (myNumElements - 1); i++, ++iterMe, ++iterOther)
293 {
294 if (!comparePathElem(*iterMe, *iterOther))
295 return false;
296 }
297
298 if (!mFilename.empty())
299 {
300 if (!other.mFilename.empty())
301 {
302 if (!comparePathElem(mFilename, other.mFilename))
303 return false;
304 }
305 else
306 {
307 if (!comparePathElem(mFilename, *iterOther))
308 return false;
309 }
310 }
311 else
312 {
313 if (!other.mFilename.empty())
314 {
315 if (!comparePathElem(*iterMe, other.mFilename))
316 return false;
317 }
318 else
319 {
320 if (!comparePathElem(*iterMe, *iterOther))
321 return false;
322 }
323 }
324 }
325
326 return true;
327 }
328
329 Path& Path::append(const Path& path)
330 {
331 if (!mFilename.empty())
332 pushDirectory(mFilename);
333
334 for (auto& dir : path.mDirectories)
335 pushDirectory(dir);
336
337 mFilename = path.mFilename;
338
339 return *this;
340 }
341
342 void Path::setBasename(const String& basename)
343 {
344 mFilename = basename + getExtension();
345 }
346
347 void Path::setExtension(const String& extension)
348 {
349 StringStream stream;
350 stream << getFilename(false);
351 stream << extension;
352
353 mFilename = stream.str();
354 }
355
356 String Path::getFilename(bool extension) const
357 {
358 if (extension)
359 return mFilename;
360 else
361 {
362 String::size_type pos = mFilename.rfind('.');
363 if (pos != String::npos)
364 return mFilename.substr(0, pos);
365 else
366 return mFilename;
367 }
368 }
369
370 String Path::getExtension() const
371 {
372 String::size_type pos = mFilename.rfind('.');
373 if (pos != String::npos)
374 return mFilename.substr(pos);
375 else
376 return String();
377 }
378
379 const String& Path::getDirectory(UINT32 idx) const
380 {
381 if (idx >= (UINT32)mDirectories.size())
382 {
383 BS_EXCEPT(InvalidParametersException, "Index out of range: " + bs::toString(idx) + ". Valid range: [0, " +
384 bs::toString((UINT32)mDirectories.size() - 1) + "]");
385 }
386
387 return mDirectories[idx];
388 }
389
390 const String& Path::getTail() const
391 {
392 if (isFile())
393 return mFilename;
394 else if (mDirectories.size() > 0)
395 return mDirectories.back();
396 else
397 return StringUtil::BLANK;
398 }
399
400 void Path::clear()
401 {
402 mDirectories.clear();
403 mDevice.clear();
404 mFilename.clear();
405 mNode.clear();
406 mIsAbsolute = false;
407 }
408
409 void Path::throwInvalidPathException(const String& path) const
410 {
411 BS_EXCEPT(InvalidParametersException, "Incorrectly formatted path provided: " + path);
412 }
413
414 String Path::buildWindows() const
415 {
416 StringStream result;
417 if (!mNode.empty())
418 {
419 result << "\\\\";
420 result << mNode;
421 result << "\\";
422 }
423 else if (!mDevice.empty())
424 {
425 result << mDevice;
426 result << ":\\";
427 }
428 else if (mIsAbsolute)
429 {
430 result << "\\";
431 }
432
433 for (auto& dir : mDirectories)
434 {
435 result << dir;
436 result << "\\";
437 }
438
439 result << mFilename;
440 return result.str();
441 }
442
443 String Path::buildUnix() const
444 {
445 StringStream result;
446 auto dirIter = mDirectories.begin();
447
448 if (!mDevice.empty())
449 {
450 result << "/";
451 result << mDevice;
452 result << ":/";
453 }
454 else if (mIsAbsolute)
455 {
456 if (dirIter != mDirectories.end() && *dirIter == "~")
457 {
458 result << "~";
459 dirIter++;
460 }
461
462 result << "/";
463 }
464
465 for (; dirIter != mDirectories.end(); ++dirIter)
466 {
467 result << *dirIter;
468 result << "/";
469 }
470
471 result << mFilename;
472 return result.str();
473 }
474
475 Path Path::operator+ (const Path& rhs) const
476 {
477 return Path::combine(*this, rhs);
478 }
479
480 Path& Path::operator+= (const Path& rhs)
481 {
482 return append(rhs);
483 }
484
485 bool Path::comparePathElem(const String& left, const String& right)
486 {
487 // Note: Might be more efficient to perform toLower character by character, and return as soon as comparison
488 // fails. Instead of this way where we're allocating two temporary strings with dynamic memory. Although that
489 // approach is problematic as well because UTF8 case conversion requires external library calls which might not
490 // support single character conversion, so it might end up being less efficient.
491 return UTF8::toLower(left) == UTF8::toLower(right);
492 }
493
494 Path Path::combine(const Path& left, const Path& right)
495 {
496 Path output = left;
497 return output.append(right);
498 }
499
500 void Path::stripInvalid(String& path)
501 {
502 String illegalChars = "\\/:?\"<>|";
503
504 for(auto& entry : path)
505 {
506 if(illegalChars.find(entry) != String::npos)
507 entry = ' ';
508 }
509 }
510
511 void Path::pushDirectory(const String& dir)
512 {
513 if (!dir.empty() && dir != ".")
514 {
515 if (dir == "..")
516 {
517 if (!mDirectories.empty() && mDirectories.back() != "..")
518 mDirectories.pop_back();
519 else
520 mDirectories.push_back(dir);
521 }
522 else
523 mDirectories.push_back(dir);
524 }
525 }
526}
527