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