Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Common/File/Path.cpp
3186 views
1
#include "ppsspp_config.h"
2
3
#include <algorithm> // for std::search
4
#include <cctype>
5
#include <cstring>
6
7
#include "Common/File/Path.h"
8
#include "Common/File/AndroidContentURI.h"
9
#include "Common/File/FileUtil.h"
10
#include "Common/StringUtils.h"
11
#include "Common/Log.h"
12
#include "Common/Data/Encoding/Utf8.h"
13
14
#include "android/jni/app-android.h"
15
16
#if PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__)
17
#include "UWP/UWPHelpers/StorageManager.h"
18
#endif
19
20
#if HOST_IS_CASE_SENSITIVE
21
#include <dirent.h>
22
#include <unistd.h>
23
#include <sys/stat.h>
24
#endif
25
26
Path::Path(std::string_view str) {
27
Init(str);
28
}
29
30
#if PPSSPP_PLATFORM(WINDOWS)
31
Path::Path(const std::wstring &str) {
32
type_ = PathType::NATIVE;
33
Init(ConvertWStringToUTF8(str));
34
}
35
#endif
36
37
void Path::Init(std::string_view str) {
38
if (str.empty()) {
39
type_ = PathType::UNDEFINED;
40
path_.clear();
41
} else if (startsWith(str, "http://") || startsWith(str, "https://")) {
42
type_ = PathType::HTTP;
43
path_ = str;
44
} else if (Android_IsContentUri(str)) {
45
// Content URIs on non scoped-storage (and possibly other cases?) can contain
46
// "raw:/" URIs inside. This happens when picking the Download folder using the folder browser
47
// on Android 9.
48
// Here's an example:
49
// content://com.android.providers.downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp
50
//
51
// Since this is a legacy use case, I think it's safe enough to just detect this
52
// and flip it to a NATIVE url and hope for the best.
53
AndroidContentURI uri(str);
54
if (startsWith(uri.FilePath(), "raw:/")) {
55
INFO_LOG(Log::System, "Raw path detected: %s", uri.FilePath().c_str());
56
path_ = uri.FilePath().substr(4);
57
type_ = PathType::NATIVE;
58
} else {
59
// A normal content URI path.
60
type_ = PathType::CONTENT_URI;
61
path_ = str;
62
}
63
} else {
64
type_ = PathType::NATIVE;
65
path_ = str;
66
}
67
68
#if PPSSPP_PLATFORM(WINDOWS)
69
// Flip all the slashes around. We flip them back on ToWString().
70
for (size_t i = 0; i < path_.size(); i++) {
71
if (path_[i] == '\\') {
72
path_[i] = '/';
73
}
74
}
75
#endif
76
77
// Don't pop_back if it's just "/".
78
if (type_ == PathType::NATIVE && path_.size() > 1 && path_.back() == '/') {
79
path_.pop_back();
80
}
81
}
82
83
// We always use forward slashes internally, we convert to backslash only when
84
// converted to a wstring.
85
Path Path::operator /(std::string_view subdir) const {
86
if (type_ == PathType::CONTENT_URI) {
87
AndroidContentURI uri(path_);
88
return Path(uri.WithComponent(subdir).ToString());
89
}
90
91
// Direct string manipulation.
92
93
if (subdir.empty()) {
94
return Path(path_);
95
}
96
std::string fullPath = path_;
97
if (subdir.front() != '/' && (fullPath.empty() || fullPath.back() != '/')) {
98
fullPath += "/";
99
}
100
fullPath += subdir;
101
// Prevent adding extra slashes.
102
if (fullPath.back() == '/') {
103
fullPath.pop_back();
104
}
105
return Path(fullPath);
106
}
107
108
void Path::operator /=(std::string_view subdir) {
109
*this = *this / subdir;
110
}
111
112
Path Path::WithExtraExtension(std::string_view ext) const {
113
if (type_ == PathType::CONTENT_URI) {
114
AndroidContentURI uri(path_);
115
return Path(uri.WithExtraExtension(ext).ToString());
116
}
117
118
_dbg_assert_(!ext.empty() && ext[0] == '.');
119
return Path(path_ + std::string(ext));
120
}
121
122
Path Path::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension) const {
123
if (type_ == PathType::CONTENT_URI) {
124
AndroidContentURI uri(path_);
125
return Path(uri.WithReplacedExtension(oldExtension, newExtension).ToString());
126
}
127
128
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
129
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
130
if (endsWithNoCase(path_, oldExtension)) {
131
std::string newPath = path_.substr(0, path_.size() - oldExtension.size());
132
return Path(newPath + newExtension);
133
} else {
134
return Path(*this);
135
}
136
}
137
138
Path Path::WithReplacedExtension(const std::string &newExtension) const {
139
if (type_ == PathType::CONTENT_URI) {
140
AndroidContentURI uri(path_);
141
return Path(uri.WithReplacedExtension(newExtension).ToString());
142
}
143
144
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
145
if (path_.empty()) {
146
return Path(*this);
147
}
148
std::string extension = GetFileExtension();
149
std::string newPath = path_.substr(0, path_.size() - extension.size()) + newExtension;
150
return Path(newPath);
151
}
152
153
std::string Path::GetFilename() const {
154
if (type_ == PathType::CONTENT_URI) {
155
AndroidContentURI uri(path_);
156
return uri.GetLastPart();
157
}
158
size_t pos = path_.rfind('/');
159
if (pos != std::string::npos) {
160
return path_.substr(pos + 1);
161
}
162
return path_;
163
}
164
165
static std::string GetExtFromString(std::string_view str) {
166
size_t pos = str.rfind(".");
167
if (pos == std::string::npos) {
168
return "";
169
}
170
size_t slash_pos = str.rfind("/");
171
if (slash_pos != std::string::npos && slash_pos > pos) {
172
// Don't want to detect "df/file" from "/as.df/file"
173
return "";
174
}
175
std::string ext(str.substr(pos));
176
for (size_t i = 0; i < ext.size(); i++) {
177
ext[i] = tolower(ext[i]);
178
}
179
return ext;
180
}
181
182
std::string Path::GetFileExtension() const {
183
if (type_ == PathType::CONTENT_URI) {
184
AndroidContentURI uri(path_);
185
return uri.GetFileExtension();
186
}
187
return GetExtFromString(path_);
188
}
189
190
std::string Path::GetDirectory() const {
191
if (type_ == PathType::CONTENT_URI) {
192
// Unclear how meaningful this is.
193
AndroidContentURI uri(path_);
194
uri.NavigateUp();
195
return uri.ToString();
196
}
197
198
size_t pos = path_.rfind('/');
199
if (type_ == PathType::HTTP) {
200
// Things are a bit different for HTTP, because we probably ended with /.
201
if (pos + 1 == path_.size()) {
202
pos = path_.rfind('/', pos - 1);
203
if (pos != path_.npos && pos > 8) {
204
return path_.substr(0, pos + 1);
205
}
206
}
207
}
208
209
if (pos != std::string::npos) {
210
if (pos == 0) {
211
return "/"; // We're at the root.
212
}
213
return path_.substr(0, pos);
214
#if PPSSPP_PLATFORM(WINDOWS)
215
} else if (path_.size() == 2 && path_[1] == ':') {
216
// Windows fake-root.
217
return "/";
218
#endif
219
} else {
220
// There could be a ':', too. Unlike the slash, let's include that
221
// in the returned directory.
222
size_t c_pos = path_.rfind(':');
223
if (c_pos != std::string::npos) {
224
return path_.substr(0, c_pos + 1);
225
}
226
}
227
// No directory components, we're a relative path.
228
return path_;
229
}
230
231
bool Path::FilePathContainsNoCase(std::string_view needle) const {
232
std::string haystack;
233
if (type_ == PathType::CONTENT_URI) {
234
haystack = AndroidContentURI(path_).FilePath();
235
} else {
236
haystack = path_;
237
}
238
auto pred = [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); };
239
auto found = std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), pred);
240
return found != haystack.end();
241
}
242
243
bool Path::StartsWith(const Path &other) const {
244
if (other.empty()) {
245
return true;
246
}
247
if (type_ != other.type_) {
248
// Bad
249
return false;
250
}
251
return startsWith(path_, other.path_);
252
}
253
254
bool Path::StartsWithGlobalAndNotEqual(const Path &other) const {
255
if (other.empty()) {
256
return true;
257
}
258
if (type_ != other.type_) {
259
// Bad
260
return false;
261
}
262
if (type_ == PathType::CONTENT_URI) {
263
AndroidContentURI a(path_);
264
AndroidContentURI b(other.path_);
265
std::string aLast = a.GetLastPart();
266
std::string bLast = b.GetLastPart();
267
if (aLast == bLast) {
268
return false;
269
}
270
return CountChar(aLast, '/') != CountChar(bLast, '/') && startsWith(aLast, bLast);
271
}
272
return *this != other && StartsWith(other);
273
}
274
275
const std::string &Path::ToString() const {
276
return path_;
277
}
278
279
#if PPSSPP_PLATFORM(WINDOWS)
280
std::wstring Path::ToWString() const {
281
std::wstring w = ConvertUTF8ToWString(path_);
282
for (size_t i = 0; i < w.size(); i++) {
283
if (w[i] == '/') {
284
w[i] = '\\';
285
}
286
}
287
return w;
288
}
289
std::string Path::ToCString() const {
290
std::string w = path_;
291
for (size_t i = 0; i < w.size(); i++) {
292
if (w[i] == '/') {
293
w[i] = '\\';
294
}
295
}
296
return w;
297
}
298
#endif
299
300
std::string Path::ToVisualString(const char *relativeRoot) const {
301
if (type_ == PathType::CONTENT_URI) {
302
return AndroidContentURI(path_).ToVisualString();
303
#if PPSSPP_PLATFORM(WINDOWS)
304
} else if (type_ == PathType::NATIVE) {
305
#if PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__)
306
return GetPreviewPath(path_);
307
#else
308
// It can be useful to show the path as relative to the memstick
309
if (relativeRoot) {
310
std::string root = ReplaceAll(relativeRoot, "/", "\\");
311
std::string path = ReplaceAll(path_, "/", "\\");
312
if (startsWithNoCase(path, root)) {
313
return path.substr(root.size());
314
} else {
315
return path;
316
}
317
} else {
318
return ReplaceAll(path_, "/", "\\");
319
}
320
#endif
321
#else
322
if (relativeRoot) {
323
std::string root = relativeRoot;
324
if (startsWithNoCase(path_, root)) {
325
return path_.substr(root.size());
326
} else {
327
return path_;
328
}
329
} else {
330
return path_;
331
}
332
#endif
333
} else {
334
return path_;
335
}
336
}
337
338
bool Path::CanNavigateUp() const {
339
if (type_ == PathType::CONTENT_URI) {
340
return AndroidContentURI(path_).CanNavigateUp();
341
} else if (type_ == PathType::HTTP) {
342
size_t rootSlash = path_.find_first_of('/', strlen("https://"));
343
if (rootSlash == path_.npos || path_.size() == rootSlash + 1) {
344
// This means, "http://server" or "http://server/". Can't go up.
345
return false;
346
}
347
}
348
if (path_ == "/" || path_.empty()) {
349
return false;
350
}
351
return true;
352
}
353
354
Path Path::NavigateUp() const {
355
if (type_ == PathType::CONTENT_URI) {
356
AndroidContentURI uri(path_);
357
uri.NavigateUp();
358
return Path(uri.ToString());
359
}
360
std::string dir = GetDirectory();
361
return Path(dir);
362
}
363
364
Path Path::GetRootVolume() const {
365
if (!IsAbsolute()) {
366
// Can't do anything
367
return Path(path_);
368
}
369
370
if (type_ == PathType::CONTENT_URI) {
371
AndroidContentURI uri(path_);
372
AndroidContentURI rootPath = uri.WithRootFilePath("");
373
return Path(rootPath.ToString());
374
}
375
376
#if PPSSPP_PLATFORM(WINDOWS)
377
if (path_[1] == ':') {
378
// Windows path with drive letter
379
std::string path = path_.substr(0, 2);
380
return Path(path);
381
}
382
// Support UNC and device paths.
383
if (path_[0] == '/' && path_[1] == '/') {
384
size_t next = 2;
385
if ((path_[2] == '.' || path_[2] == '?') && path_[3] == '/') {
386
// Device path, or "\\.\UNC" path, skip the dot and consider the device the root.
387
next = 4;
388
}
389
390
size_t len = path_.find_first_of('/', next);
391
return Path(path_.substr(0, len));
392
}
393
#endif
394
return Path("/");
395
}
396
397
bool Path::IsAbsolute() const {
398
if (type_ == PathType::CONTENT_URI) {
399
// These don't exist in relative form.
400
return true;
401
}
402
403
if (path_.empty())
404
return true;
405
else if (path_.front() == '/')
406
return true;
407
#if PPSSPP_PLATFORM(WINDOWS)
408
else if (path_.size() > 3 && path_[1] == ':')
409
return true; // Windows path with drive letter
410
#endif
411
else
412
return false;
413
}
414
415
bool Path::ComputePathTo(const Path &other, std::string &path) const {
416
if (other == *this) {
417
path.clear();
418
return true;
419
}
420
421
if (!other.StartsWith(*this)) {
422
// Can't do this. Should return an error.
423
return false;
424
}
425
426
if (*this == other) {
427
// Equal, the path is empty.
428
path.clear();
429
return true;
430
}
431
432
if (type_ == PathType::CONTENT_URI) {
433
AndroidContentURI a(path_);
434
AndroidContentURI b(other.path_);
435
if (a.RootPath() != b.RootPath()) {
436
// No common root, can't do anything
437
return false;
438
}
439
return a.ComputePathTo(b, path);
440
} else if (path_ == "/") {
441
path = other.path_.substr(1);
442
return true;
443
} else {
444
path = other.path_.substr(path_.size() + 1);
445
return true;
446
}
447
}
448
449
static bool FixFilenameCase(const std::string &path, std::string &filename) {
450
#if _WIN32
451
// We don't support case-insensitive file systems on Windows.
452
return true;
453
#else
454
455
// Are we lucky?
456
if (File::Exists(Path(path + filename)))
457
return true;
458
459
size_t filenameSize = filename.size(); // size in bytes, not characters
460
for (size_t i = 0; i < filenameSize; i++)
461
{
462
filename[i] = tolower(filename[i]);
463
}
464
465
//TODO: lookup filename in cache for "path"
466
struct dirent *result = NULL;
467
468
DIR *dirp = opendir(path.c_str());
469
if (!dirp)
470
return false;
471
472
bool retValue = false;
473
474
while ((result = readdir(dirp)))
475
{
476
if (strlen(result->d_name) != filenameSize)
477
continue;
478
479
size_t i;
480
for (i = 0; i < filenameSize; i++)
481
{
482
if (filename[i] != tolower(result->d_name[i]))
483
break;
484
}
485
486
if (i < filenameSize)
487
continue;
488
489
filename = result->d_name;
490
retValue = true;
491
}
492
493
closedir(dirp);
494
495
return retValue;
496
#endif
497
}
498
499
bool FixPathCase(const Path &realBasePath, std::string &path, FixPathCaseBehavior behavior) {
500
#if _WIN32
501
return true;
502
#else
503
504
if (realBasePath.Type() == PathType::CONTENT_URI) {
505
// Nothing to do. These are already case insensitive, I think.
506
return true;
507
}
508
509
std::string basePath = realBasePath.ToString();
510
511
size_t len = path.size();
512
513
if (len == 0)
514
return true;
515
516
if (path[len - 1] == '/')
517
{
518
len--;
519
520
if (len == 0)
521
return true;
522
}
523
524
std::string fullPath;
525
fullPath.reserve(basePath.size() + len + 1);
526
fullPath.append(basePath);
527
528
size_t start = 0;
529
while (start < len)
530
{
531
size_t i = path.find('/', start);
532
if (i == std::string::npos)
533
i = len;
534
535
if (i > start)
536
{
537
std::string component = path.substr(start, i - start);
538
539
// Fix case and stop on nonexistant path component
540
if (FixFilenameCase(fullPath, component) == false) {
541
// Still counts as success if partial matches allowed or if this
542
// is the last component and only the ones before it are required
543
return (behavior == FPC_PARTIAL_ALLOWED || (behavior == FPC_PATH_MUST_EXIST && i >= len));
544
}
545
546
path.replace(start, i - start, component);
547
548
fullPath.append(1, '/');
549
fullPath.append(component);
550
}
551
552
start = i + 1;
553
}
554
555
return true;
556
#endif
557
}
558
559