CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
hrydgard

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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