Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Core/FileSystems/DirectoryFileSystem.cpp
3186 views
1
// Copyright (c) 2012- PPSSPP Project.
2
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, version 2.0 or later versions.
6
7
// This program is distributed in the hope that it will be useful,
8
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
// GNU General Public License 2.0 for more details.
11
12
// A copy of the GPL 2.0 should have been included with the program.
13
// If not, see http://www.gnu.org/licenses/
14
15
// Official git repository and contact information can be found at
16
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17
18
#include "ppsspp_config.h"
19
20
#include <algorithm>
21
#include <ctime>
22
#include <limits>
23
24
#include "Common/Data/Text/I18n.h"
25
#include "Common/Data/Encoding/Utf8.h"
26
#include "Common/Serialize/Serializer.h"
27
#include "Common/Serialize/SerializeFuncs.h"
28
#include "Common/StringUtils.h"
29
#include "Common/System/OSD.h"
30
#include "Common/File/FileUtil.h"
31
#include "Common/File/DiskFree.h"
32
#include "Common/File/VFS/VFS.h"
33
#include "Common/SysError.h"
34
#include "Core/FileSystems/DirectoryFileSystem.h"
35
#include "Core/HLE/sceKernel.h"
36
#include "Core/HW/MemoryStick.h"
37
#include "Core/CoreTiming.h"
38
#include "Core/System.h"
39
#include "Core/Replay.h"
40
#include "Core/Reporting.h"
41
#include "Core/ELF/ParamSFO.h"
42
43
#ifdef _WIN32
44
#include "Common/CommonWindows.h"
45
#include <sys/stat.h>
46
#if PPSSPP_PLATFORM(UWP)
47
#include <fileapifromapp.h>
48
#endif
49
#undef FILE_OPEN
50
#else
51
#include <dirent.h>
52
#include <unistd.h>
53
#include <sys/stat.h>
54
#if defined(__ANDROID__)
55
#include <sys/types.h>
56
#include <sys/vfs.h>
57
#define statvfs statfs
58
#else
59
#include <sys/statvfs.h>
60
#endif
61
#include <ctype.h>
62
#include <fcntl.h>
63
#endif
64
65
DirectoryFileSystem::DirectoryFileSystem(IHandleAllocator *_hAlloc, const Path & _basePath, FileSystemFlags _flags) : basePath(_basePath), flags(_flags) {
66
File::CreateFullPath(basePath);
67
68
static const std::string_view mixedCase = "wJpCzSBNnZfxSgoS";
69
static const std::string_view upperCase = "WJPCZSBNNZFXSGOS";
70
71
// Check for case sensitivity
72
bool checkSucceeded = false;
73
File::CreateEmptyFile(basePath / mixedCase);
74
if (File::Exists(basePath / mixedCase)) {
75
checkSucceeded = true;
76
if (!File::Exists(basePath / upperCase)) {
77
flags |= FileSystemFlags::CASE_SENSITIVE;
78
}
79
}
80
File::Delete(basePath / mixedCase);
81
82
INFO_LOG(Log::IO, "Is file system case sensitive? %s (base: '%s') (checkOK: %d)", (flags & FileSystemFlags::CASE_SENSITIVE) ? "yes" : "no", _basePath.c_str(), checkSucceeded);
83
84
hAlloc = _hAlloc;
85
}
86
87
DirectoryFileSystem::~DirectoryFileSystem() {
88
CloseAll();
89
}
90
91
// TODO(scoped): Merge the two below functions somehow.
92
93
Path DirectoryFileHandle::GetLocalPath(const Path &basePath, std::string localPath) const {
94
if (localPath.empty())
95
return basePath;
96
97
if (localPath[0] == '/')
98
localPath.erase(0, 1);
99
100
if (fileSystemFlags_ & FileSystemFlags::STRIP_PSP) {
101
if (localPath == "PSP") {
102
localPath = "/";
103
} else if (startsWithNoCase(localPath, "PSP/")) {
104
localPath = localPath.substr(4);
105
}
106
}
107
108
return basePath / localPath;
109
}
110
111
Path DirectoryFileSystem::GetLocalPath(std::string internalPath) const {
112
if (internalPath.empty())
113
return basePath;
114
115
if (internalPath[0] == '/')
116
internalPath.erase(0, 1);
117
118
if (flags & FileSystemFlags::STRIP_PSP) {
119
if (internalPath == "PSP") {
120
internalPath = "/";
121
} else if (startsWithNoCase(internalPath, "PSP/")) {
122
internalPath = internalPath.substr(4);
123
}
124
}
125
126
return basePath / internalPath;
127
}
128
129
bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, FileAccess access, u32 &error) {
130
error = 0;
131
132
if (access == FILEACCESS_NONE) {
133
error = SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT;
134
return false;
135
}
136
137
if (fileSystemFlags_ & FileSystemFlags::CASE_SENSITIVE) {
138
if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {
139
DEBUG_LOG(Log::FileSystem, "Checking case for path %s", fileName.c_str());
140
if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {
141
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
142
return false; // or go on and attempt (for a better error code than just 0?)
143
}
144
}
145
}
146
// else we try fopen first (in case we're lucky) before simulating case insensitivity
147
148
Path fullName = GetLocalPath(basePath, fileName);
149
150
// On the PSP, truncating doesn't lose data. If you seek later, you'll recover it.
151
// This is abnormal, so we deviate from the PSP's behavior and truncate on write/close.
152
// This means it's incorrectly not truncated before the write.
153
if (access & FILEACCESS_TRUNCATE) {
154
needsTrunc_ = 0;
155
}
156
157
//TODO: tests, should append seek to end of file? seeking in a file opened for append?
158
#if PPSSPP_PLATFORM(WINDOWS)
159
// Convert parameters to Windows permissions and access
160
DWORD desired = 0;
161
DWORD sharemode = 0;
162
DWORD openmode = 0;
163
if (access & FILEACCESS_READ) {
164
desired |= GENERIC_READ;
165
sharemode |= FILE_SHARE_READ;
166
}
167
if (access & FILEACCESS_WRITE) {
168
desired |= GENERIC_WRITE;
169
sharemode |= FILE_SHARE_WRITE | FILE_SHARE_READ;
170
}
171
if (access & FILEACCESS_CREATE) {
172
if (access & FILEACCESS_EXCL) {
173
openmode = CREATE_NEW;
174
} else {
175
openmode = OPEN_ALWAYS;
176
}
177
} else {
178
openmode = OPEN_EXISTING;
179
}
180
181
// Let's do it!
182
#if PPSSPP_PLATFORM(UWP)
183
hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);
184
#else
185
hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);
186
#endif
187
bool success = hFile != INVALID_HANDLE_VALUE;
188
if (!success) {
189
DWORD w32err = GetLastError();
190
191
if (w32err == ERROR_SHARING_VIOLATION) {
192
// Sometimes, the file is locked for write, let's try again.
193
sharemode |= FILE_SHARE_WRITE;
194
#if PPSSPP_PLATFORM(UWP)
195
hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);
196
#else
197
hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);
198
#endif
199
success = hFile != INVALID_HANDLE_VALUE;
200
if (!success) {
201
w32err = GetLastError();
202
}
203
}
204
205
if (w32err == ERROR_DISK_FULL || w32err == ERROR_NOT_ENOUGH_QUOTA) {
206
// This is returned when the disk is full.
207
auto err = GetI18NCategory(I18NCat::ERRORS);
208
g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");
209
error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;
210
} else if (!success) {
211
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
212
}
213
}
214
#else
215
if (fullName.Type() == PathType::CONTENT_URI) {
216
// Convert flags. Don't want to share this type, bad dependency.
217
u32 flags = File::OPEN_NONE;
218
if (access & FILEACCESS_READ)
219
flags |= File::OPEN_READ;
220
if (access & FILEACCESS_WRITE)
221
flags |= File::OPEN_WRITE;
222
if (access & FILEACCESS_APPEND)
223
flags |= File::OPEN_APPEND;
224
if (access & FILEACCESS_CREATE)
225
flags |= File::OPEN_CREATE;
226
// Important: don't pass TRUNCATE here, the PSP truncates weirdly. See #579.
227
// See above about truncate behavior. Just add READ to preserve data here.
228
if (access & FILEACCESS_TRUNCATE)
229
flags |= File::OPEN_READ;
230
231
int fd = File::OpenFD(fullName, (File::OpenFlag)flags);
232
// Try to detect reads/writes to PSP/GAME to avoid them in replays.
233
if (fullName.FilePathContainsNoCase("PSP/GAME/")) {
234
inGameDir_ = true;
235
}
236
hFile = fd;
237
if (fd != -1) {
238
// Success
239
return true;
240
} else {
241
ERROR_LOG(Log::FileSystem, "File::OpenFD returned an error");
242
// TODO: Need better error codes from OpenFD so we can distinguish
243
// disk full. Just set not found for now.
244
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
245
return false;
246
}
247
}
248
249
int flags = 0;
250
if (access & FILEACCESS_APPEND) {
251
flags |= O_APPEND;
252
}
253
if ((access & FILEACCESS_READ) && (access & FILEACCESS_WRITE)) {
254
flags |= O_RDWR;
255
} else if (access & FILEACCESS_READ) {
256
flags |= O_RDONLY;
257
} else if (access & FILEACCESS_WRITE) {
258
flags |= O_WRONLY;
259
}
260
if (access & FILEACCESS_CREATE) {
261
flags |= O_CREAT;
262
}
263
if (access & FILEACCESS_EXCL) {
264
flags |= O_EXCL;
265
}
266
267
hFile = open(fullName.c_str(), flags, 0666);
268
bool success = hFile != -1;
269
#endif
270
271
if (fileSystemFlags_ & FileSystemFlags::CASE_SENSITIVE) {
272
if (!success && !(access & FILEACCESS_CREATE)) {
273
if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {
274
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
275
return false;
276
}
277
fullName = GetLocalPath(basePath, fileName);
278
DEBUG_LOG(Log::FileSystem, "Case may have been incorrect, second try opening %s (%s)", fullName.c_str(), fileName.c_str());
279
280
// And try again with the correct case this time
281
#if PPSSPP_PLATFORM(UWP)
282
// Should never get here.
283
#elif PPSSPP_PLATFORM(WINDOWS)
284
// Unlikely to get here, heh.
285
hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);
286
success = hFile != INVALID_HANDLE_VALUE;
287
#else
288
hFile = open(fullName.c_str(), flags, 0666);
289
success = hFile != -1;
290
#endif
291
}
292
}
293
294
#if !PPSSPP_PLATFORM(WINDOWS)
295
if (success) {
296
// Reject directories, even if we succeed in opening them.
297
// TODO: Might want to do this stat first...
298
struct stat st;
299
if (fstat(hFile, &st) == 0 && S_ISDIR(st.st_mode)) {
300
close(hFile);
301
errno = EISDIR;
302
success = false;
303
}
304
} else if (errno == ENOSPC) {
305
// This is returned when the disk is full.
306
auto err = GetI18NCategory(I18NCat::ERRORS);
307
g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");
308
error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;
309
} else {
310
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
311
}
312
#endif
313
314
// Try to detect reads/writes to PSP/GAME to avoid them in replays.
315
if (fullName.FilePathContainsNoCase("PSP/GAME/")) {
316
inGameDir_ = true;
317
}
318
if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {
319
MemoryStick_NotifyWrite();
320
}
321
322
return success;
323
}
324
325
size_t DirectoryFileHandle::Read(u8* pointer, s64 size)
326
{
327
size_t bytesRead = 0;
328
if (needsTrunc_ != -1) {
329
// If the file was marked to be truncated, pretend there's nothing.
330
// On a PSP. it actually is truncated, but the data wasn't erased.
331
off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);
332
if (needsTrunc_ <= off) {
333
return replay_ ? ReplayApplyDiskRead(pointer, 0, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : 0;
334
}
335
if (needsTrunc_ < off + size) {
336
size = needsTrunc_ - off;
337
}
338
}
339
if (size > 0) {
340
#ifdef _WIN32
341
::ReadFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesRead, 0);
342
#else
343
bytesRead = read(hFile, pointer, size);
344
#endif
345
}
346
return replay_ ? ReplayApplyDiskRead(pointer, (uint32_t)bytesRead, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : bytesRead;
347
}
348
349
size_t DirectoryFileHandle::Write(const u8* pointer, s64 size)
350
{
351
size_t bytesWritten = 0;
352
bool diskFull = false;
353
354
#ifdef _WIN32
355
BOOL success = ::WriteFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesWritten, 0);
356
if (success == FALSE) {
357
DWORD err = GetLastError();
358
diskFull = err == ERROR_DISK_FULL || err == ERROR_NOT_ENOUGH_QUOTA;
359
}
360
#else
361
bytesWritten = write(hFile, pointer, size);
362
if (bytesWritten == (size_t)-1) {
363
diskFull = errno == ENOSPC;
364
}
365
#endif
366
if (needsTrunc_ != -1) {
367
off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);
368
if (needsTrunc_ < off) {
369
needsTrunc_ = off;
370
}
371
}
372
373
if (replay_) {
374
bytesWritten = ReplayApplyDiskWrite(pointer, (uint64_t)bytesWritten, (uint64_t)size, &diskFull, inGameDir_, CoreTiming::GetGlobalTimeUs());
375
}
376
377
MemoryStick_NotifyWrite();
378
379
if (diskFull) {
380
ERROR_LOG(Log::FileSystem, "Disk full");
381
auto err = GetI18NCategory(I18NCat::ERRORS);
382
g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");
383
// We only return an error when the disk is actually full.
384
// When writing this would cause the disk to be full, so it wasn't written, we return 0.
385
Path saveFolder = GetSysDirectory(DIRECTORY_SAVEDATA);
386
int64_t space;
387
if (free_disk_space(saveFolder, space)) {
388
if (space < size) {
389
// Sign extend to a 64-bit value.
390
return (size_t)(s64)(s32)SCE_KERNEL_ERROR_ERRNO_DEVICE_NO_FREE_SPACE;
391
}
392
}
393
}
394
return bytesWritten;
395
}
396
397
size_t DirectoryFileHandle::Seek(s32 position, FileMove type)
398
{
399
if (needsTrunc_ != -1) {
400
// If the file is "currently truncated" move to the end based on that position.
401
// The actual, underlying file hasn't been truncated (yet.)
402
if (type == FILEMOVE_END) {
403
type = FILEMOVE_BEGIN;
404
position = (s32)(needsTrunc_ + position);
405
}
406
}
407
408
size_t result;
409
#ifdef _WIN32
410
DWORD moveMethod = 0;
411
switch (type) {
412
case FILEMOVE_BEGIN: moveMethod = FILE_BEGIN; break;
413
case FILEMOVE_CURRENT: moveMethod = FILE_CURRENT; break;
414
case FILEMOVE_END: moveMethod = FILE_END; break;
415
}
416
417
LARGE_INTEGER distance;
418
distance.QuadPart = position;
419
LARGE_INTEGER cursor;
420
SetFilePointerEx(hFile, distance, &cursor, moveMethod);
421
result = (size_t)cursor.QuadPart;
422
#else
423
int moveMethod = 0;
424
switch (type) {
425
case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break;
426
case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break;
427
case FILEMOVE_END: moveMethod = SEEK_END; break;
428
}
429
result = lseek(hFile, position, moveMethod);
430
#endif
431
432
return replay_ ? (size_t)ReplayApplyDisk64(ReplayAction::FILE_SEEK, result, CoreTiming::GetGlobalTimeUs()) : result;
433
}
434
435
void DirectoryFileHandle::Close() {
436
if (needsTrunc_ != -1) {
437
#ifdef _WIN32
438
Seek((s32)needsTrunc_, FILEMOVE_BEGIN);
439
if (SetEndOfFile(hFile) == 0) {
440
ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);
441
}
442
#elif !PPSSPP_PLATFORM(SWITCH)
443
// Note: it's not great that Switch cannot truncate appropriately...
444
if (ftruncate(hFile, (off_t)needsTrunc_) != 0) {
445
ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);
446
}
447
#endif
448
}
449
450
#ifdef _WIN32
451
if (hFile != (HANDLE)-1)
452
CloseHandle(hFile);
453
#else
454
if (hFile != -1)
455
close(hFile);
456
#endif
457
}
458
459
void DirectoryFileSystem::CloseAll() {
460
for (auto iter = entries.begin(); iter != entries.end(); ++iter) {
461
INFO_LOG(Log::FileSystem, "DirectoryFileSystem::CloseAll(): Force closing %d (%s)", (int)iter->first, iter->second.guestFilename.c_str());
462
iter->second.hFile.Close();
463
}
464
entries.clear();
465
}
466
467
bool DirectoryFileSystem::MkDir(const std::string &dirname) {
468
bool result;
469
if (flags & FileSystemFlags::CASE_SENSITIVE) {
470
// Must fix case BEFORE attempting, because MkDir would create
471
// duplicate (different case) directories
472
std::string fixedCase = dirname;
473
if (!FixPathCase(basePath, fixedCase, FPC_PARTIAL_ALLOWED)) {
474
result = false;
475
} else {
476
result = File::CreateFullPath(GetLocalPath(fixedCase));
477
}
478
} else {
479
result = File::CreateFullPath(GetLocalPath(dirname));
480
}
481
MemoryStick_NotifyWrite();
482
return ReplayApplyDisk(ReplayAction::MKDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;
483
}
484
485
bool DirectoryFileSystem::RmDir(const std::string &dirname) {
486
Path fullName = GetLocalPath(dirname);
487
488
if (flags & FileSystemFlags::CASE_SENSITIVE) {
489
// Maybe we're lucky?
490
if (File::DeleteDirRecursively(fullName)) {
491
MemoryStick_NotifyWrite();
492
return (bool)ReplayApplyDisk(ReplayAction::RMDIR, true, CoreTiming::GetGlobalTimeUs());
493
}
494
495
// Nope, fix case and try again. Should we try again?
496
std::string fullPath = dirname;
497
if (!FixPathCase(basePath, fullPath, FPC_FILE_MUST_EXIST))
498
return (bool)ReplayApplyDisk(ReplayAction::RMDIR, false, CoreTiming::GetGlobalTimeUs());
499
500
fullName = GetLocalPath(fullPath);
501
}
502
503
bool result = File::DeleteDirRecursively(fullName);
504
MemoryStick_NotifyWrite();
505
return ReplayApplyDisk(ReplayAction::RMDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;
506
}
507
508
int DirectoryFileSystem::RenameFile(const std::string &from, const std::string &to) {
509
std::string fullTo = to;
510
511
// Rename ignores the path (even if specified) on to.
512
size_t chop_at = to.find_last_of('/');
513
if (chop_at != to.npos)
514
fullTo = to.substr(chop_at + 1);
515
516
// Now put it in the same directory as from.
517
size_t dirname_end = from.find_last_of('/');
518
if (dirname_end != from.npos)
519
fullTo = from.substr(0, dirname_end + 1) + fullTo;
520
521
// At this point, we should check if the paths match and give an already exists error.
522
if (from == fullTo)
523
return ReplayApplyDisk(ReplayAction::FILE_RENAME, SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS, CoreTiming::GetGlobalTimeUs());
524
525
Path fullFrom = GetLocalPath(from);
526
527
if (flags & FileSystemFlags::CASE_SENSITIVE) {
528
// In case TO should overwrite a file with different case. Check error code?
529
if (!FixPathCase(basePath, fullTo, FPC_PATH_MUST_EXIST))
530
return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());
531
}
532
533
Path fullToPath = GetLocalPath(fullTo);
534
535
bool retValue = File::Rename(fullFrom, fullToPath);
536
537
if (flags & FileSystemFlags::CASE_SENSITIVE) {
538
if (!retValue) {
539
// May have failed due to case sensitivity on FROM, so try again. Check error code?
540
std::string fullFromPath = from;
541
if (!FixPathCase(basePath, fullFromPath, FPC_FILE_MUST_EXIST))
542
return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());
543
fullFrom = GetLocalPath(fullFromPath);
544
545
retValue = File::Rename(fullFrom, fullToPath);
546
}
547
}
548
549
// TODO: Better error codes.
550
int result = retValue ? 0 : (int)SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS;
551
MemoryStick_NotifyWrite();
552
return ReplayApplyDisk(ReplayAction::FILE_RENAME, result, CoreTiming::GetGlobalTimeUs());
553
}
554
555
bool DirectoryFileSystem::RemoveFile(const std::string &filename) {
556
Path localPath = GetLocalPath(filename);
557
558
bool retValue = File::Delete(localPath);
559
560
if (flags & FileSystemFlags::CASE_SENSITIVE) {
561
if (!retValue) {
562
// May have failed due to case sensitivity, so try again. Try even if it fails?
563
std::string fullNamePath = filename;
564
if (!FixPathCase(basePath, fullNamePath, FPC_FILE_MUST_EXIST))
565
return (bool)ReplayApplyDisk(ReplayAction::FILE_REMOVE, false, CoreTiming::GetGlobalTimeUs());
566
localPath = GetLocalPath(fullNamePath);
567
568
retValue = File::Delete(localPath);
569
}
570
}
571
572
MemoryStick_NotifyWrite();
573
return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0;
574
}
575
576
int DirectoryFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {
577
OpenFileEntry entry;
578
entry.hFile.fileSystemFlags_ = flags;
579
u32 err = 0;
580
bool success = entry.hFile.Open(basePath, filename, (FileAccess)(access & FILEACCESS_PSP_FLAGS), err);
581
if (err == 0 && !success) {
582
err = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
583
}
584
585
err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs());
586
if (err != 0) {
587
std::string errorString;
588
int logError;
589
#ifdef _WIN32
590
auto win32err = GetLastError();
591
logError = (int)win32err;
592
errorString = GetStringErrorMsg(win32err);
593
#else
594
logError = (int)errno;
595
#endif
596
if (!(access & FILEACCESS_PPSSPP_QUIET)) {
597
ERROR_LOG(Log::FileSystem, "DirectoryFileSystem::OpenFile('%s'): FAILED, %d - access = %d '%s'", filename.c_str(), logError, (int)(access & FILEACCESS_PSP_FLAGS), errorString.c_str());
598
}
599
return err;
600
} else {
601
#ifdef _WIN32
602
if (access & FILEACCESS_APPEND) {
603
entry.hFile.Seek(0, FILEMOVE_END);
604
}
605
#endif
606
607
u32 newHandle = hAlloc->GetNewHandle();
608
609
entry.guestFilename = filename;
610
entry.access = (FileAccess)(access & FILEACCESS_PSP_FLAGS);
611
612
entries[newHandle] = entry;
613
614
return newHandle;
615
}
616
}
617
618
void DirectoryFileSystem::CloseFile(u32 handle) {
619
EntryMap::iterator iter = entries.find(handle);
620
if (iter != entries.end()) {
621
hAlloc->FreeHandle(handle);
622
iter->second.hFile.Close();
623
entries.erase(iter);
624
} else {
625
//This shouldn't happen...
626
ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);
627
}
628
}
629
630
bool DirectoryFileSystem::OwnsHandle(u32 handle) {
631
EntryMap::iterator iter = entries.find(handle);
632
return (iter != entries.end());
633
}
634
635
int DirectoryFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {
636
return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;
637
}
638
639
PSPDevType DirectoryFileSystem::DevType(u32 handle) {
640
return PSPDevType::FILE;
641
}
642
643
size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {
644
int ignored;
645
return ReadFile(handle, pointer, size, ignored);
646
}
647
648
size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {
649
EntryMap::iterator iter = entries.find(handle);
650
if (iter != entries.end()) {
651
if (size < 0) {
652
ERROR_LOG(Log::FileSystem, "Invalid read for %lld bytes from disk %s", size, iter->second.guestFilename.c_str());
653
return 0;
654
}
655
656
size_t bytesRead = iter->second.hFile.Read(pointer,size);
657
return bytesRead;
658
} else {
659
// This shouldn't happen...
660
ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);
661
return 0;
662
}
663
}
664
665
size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {
666
int ignored;
667
return WriteFile(handle, pointer, size, ignored);
668
}
669
670
size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {
671
EntryMap::iterator iter = entries.find(handle);
672
if (iter != entries.end()) {
673
size_t bytesWritten = iter->second.hFile.Write(pointer,size);
674
return bytesWritten;
675
} else {
676
//This shouldn't happen...
677
ERROR_LOG(Log::FileSystem,"Cannot write to file that hasn't been opened: %08x", handle);
678
return 0;
679
}
680
}
681
682
size_t DirectoryFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {
683
EntryMap::iterator iter = entries.find(handle);
684
if (iter != entries.end()) {
685
return iter->second.hFile.Seek(position,type);
686
} else {
687
//This shouldn't happen...
688
ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);
689
return 0;
690
}
691
}
692
693
PSPFileInfo DirectoryFileSystem::GetFileInfo(std::string filename) {
694
PSPFileInfo x;
695
x.name = filename;
696
697
File::FileInfo info;
698
Path fullName = GetLocalPath(filename);
699
if (!File::GetFileInfo(fullName, &info)) {
700
if (flags & FileSystemFlags::CASE_SENSITIVE) {
701
if (!FixPathCase(basePath, filename, FPC_FILE_MUST_EXIST))
702
return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
703
fullName = GetLocalPath(filename);
704
705
if (!File::GetFileInfo(fullName, &info))
706
return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
707
} else {
708
return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
709
}
710
}
711
712
x.type = info.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
713
x.exists = true;
714
715
if (x.type != FILETYPE_DIRECTORY) {
716
x.size = info.size;
717
}
718
x.access = info.access;
719
time_t atime = info.atime;
720
time_t ctime = info.ctime;
721
time_t mtime = info.mtime;
722
723
localtime_r((time_t*)&atime, &x.atime);
724
localtime_r((time_t*)&ctime, &x.ctime);
725
localtime_r((time_t*)&mtime, &x.mtime);
726
727
return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
728
}
729
730
PSPFileInfo DirectoryFileSystem::GetFileInfoByHandle(u32 handle) {
731
WARN_LOG(Log::FileSystem, "GetFileInfoByHandle not yet implemented for DirectoryFileSystem");
732
return PSPFileInfo();
733
}
734
735
#ifdef _WIN32
736
#define FILETIME_FROM_UNIX_EPOCH_US 11644473600000000ULL
737
738
static void tmFromFiletime(tm &dest, const FILETIME &src) {
739
u64 from_1601_us = (((u64) src.dwHighDateTime << 32ULL) + (u64) src.dwLowDateTime) / 10ULL;
740
u64 from_1970_us = from_1601_us - FILETIME_FROM_UNIX_EPOCH_US;
741
742
time_t t = (time_t) (from_1970_us / 1000000UL);
743
localtime_s(&dest, &t);
744
}
745
#endif
746
747
// This simulates a bug in the PSP VFAT driver.
748
//
749
// Windows NT VFAT optimizes valid DOS filenames that are in lowercase.
750
// The PSP VFAT driver doesn't support this optimization, and behaves like Windows 98.
751
// Some homebrew depends on this bug in the PSP firmware.
752
//
753
// This essentially tries to simulate the "Windows 98 world view" on modern operating systems.
754
// Essentially all lowercase files are seen as UPPERCASE.
755
//
756
// Note: PSP-created files would stay lowercase, but this uppercases them too.
757
// Hopefully no PSP games read directories after they create files in them...
758
static std::string SimulateVFATBug(std::string filename) {
759
// These are the characters allowed in DOS filenames.
760
static const char * const FAT_UPPER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&'(){}-_`~";
761
static const char * const FAT_LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&'(){}-_`~";
762
static const char * const LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz";
763
764
// To avoid logging/comparing, skip all this if it has no lowercase chars to begin with.
765
size_t lowerchar = filename.find_first_of(LOWER_CHARS);
766
if (lowerchar == filename.npos) {
767
return filename;
768
}
769
770
bool apply_hack = false;
771
size_t dot_pos = filename.find('.');
772
if (dot_pos == filename.npos && filename.length() <= 8) {
773
size_t badchar = filename.find_first_not_of(FAT_LOWER_CHARS);
774
if (badchar == filename.npos) {
775
// It's all lowercase. Convert to upper.
776
apply_hack = true;
777
}
778
} else {
779
// There's a separate flag for each, so we compare separately.
780
// But they both have to either be all upper or lowercase.
781
std::string base = filename.substr(0, dot_pos);
782
std::string ext = filename.substr(dot_pos + 1);
783
784
// The filename must be short enough to fit.
785
if (base.length() <= 8 && ext.length() <= 3) {
786
size_t base_non_lower = base.find_first_not_of(FAT_LOWER_CHARS);
787
size_t base_non_upper = base.find_first_not_of(FAT_UPPER_CHARS);
788
size_t ext_non_lower = ext.find_first_not_of(FAT_LOWER_CHARS);
789
size_t ext_non_upper = ext.find_first_not_of(FAT_UPPER_CHARS);
790
791
// As long as neither is mixed, we apply the hack.
792
bool base_apply_hack = base_non_lower == base.npos || base_non_upper == base.npos;
793
bool ext_apply_hack = ext_non_lower == ext.npos || ext_non_upper == ext.npos;
794
apply_hack = base_apply_hack && ext_apply_hack;
795
}
796
}
797
798
if (apply_hack) {
799
VERBOSE_LOG(Log::FileSystem, "Applying VFAT hack to filename: %s", filename.c_str());
800
// In this situation, NT would write UPPERCASE, and just set a flag to say "actually lowercase".
801
// That VFAT flag isn't read by the PSP firmware, so let's pretend to "not read it."
802
std::transform(filename.begin(), filename.end(), filename.begin(), toupper);
803
}
804
805
return filename;
806
}
807
808
bool DirectoryFileSystem::ComputeRecursiveDirSizeIfFast(const std::string &path, int64_t *size) {
809
Path localPath = GetLocalPath(path);
810
811
int64_t sizeTemp = File::ComputeRecursiveDirectorySize(localPath);
812
if (sizeTemp >= 0) {
813
*size = sizeTemp;
814
return true;
815
} else {
816
return false;
817
}
818
}
819
820
std::vector<PSPFileInfo> DirectoryFileSystem::GetDirListing(const std::string &path, bool *exists) {
821
std::vector<PSPFileInfo> myVector;
822
823
std::vector<File::FileInfo> files;
824
Path localPath = GetLocalPath(path);
825
const int flags = File::GETFILES_GETHIDDEN | File::GETFILES_GET_NAVIGATION_ENTRIES;
826
bool success = File::GetFilesInDir(localPath, &files, nullptr, flags);
827
828
if (this->flags & FileSystemFlags::CASE_SENSITIVE) {
829
if (!success) {
830
// TODO: Case sensitivity should be checked on a file system basis, right?
831
std::string fixedPath = path;
832
if (FixPathCase(basePath, fixedPath, FPC_FILE_MUST_EXIST)) {
833
// May have failed due to case sensitivity, try again
834
localPath = GetLocalPath(fixedPath);
835
success = File::GetFilesInDir(localPath, &files, nullptr, flags);
836
}
837
}
838
}
839
840
if (!success) {
841
if (exists)
842
*exists = false;
843
return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());
844
}
845
846
bool hideISOFiles = PSP_CoreParameter().compat.flags().HideISOFiles;
847
848
// Then apply transforms to match PSP idiosynchrasies, as we convert the entries.
849
for (auto &file : files) {
850
PSPFileInfo entry;
851
if (Flags() & FileSystemFlags::SIMULATE_FAT32) {
852
entry.name = SimulateVFATBug(file.name);
853
} else {
854
entry.name = file.name;
855
}
856
if (hideISOFiles) {
857
if (endsWithNoCase(entry.name, ".cso") || endsWithNoCase(entry.name, ".iso") || endsWithNoCase(entry.name, ".chd")) { // chd not really necessary, but let's hide them too.
858
// Workaround for DJ Max Portable, see compat.ini.
859
continue;
860
} else if (file.isDirectory) {
861
if (endsWithNoCase(path, "SAVEDATA")) {
862
// Don't let it see savedata from other games, it can misinterpret stuff.
863
std::string gameID = g_paramSFO.GetDiscID();
864
if (entry.name.size() > 2 && !startsWithNoCase(entry.name, gameID)) {
865
continue;
866
}
867
} else if (file.name == "GAME" || file.name == "TEXTURES" || file.name == "PPSSPP_STATE" || file.name == "PLUGINS" || file.name == "SYSTEM" || equalsNoCase(file.name, "Cheats")) {
868
// The game scans these folders on startup which can take time. Skip them.
869
continue;
870
}
871
}
872
}
873
if (file.name == "..") {
874
entry.size = 4096;
875
} else {
876
entry.size = file.size;
877
}
878
if (file.isDirectory) {
879
entry.type = FILETYPE_DIRECTORY;
880
} else {
881
entry.type = FILETYPE_NORMAL;
882
}
883
entry.access = file.access;
884
entry.exists = file.exists;
885
886
localtime_r((time_t*)&file.atime, &entry.atime);
887
localtime_r((time_t*)&file.ctime, &entry.ctime);
888
localtime_r((time_t*)&file.mtime, &entry.mtime);
889
890
myVector.push_back(entry);
891
}
892
893
if (this->flags & FileSystemFlags::STRIP_PSP) {
894
if (path == "/") {
895
// Artificially add the /PSP directory to the root listing.
896
PSPFileInfo pspInfo{};
897
pspInfo.name = "PSP";
898
pspInfo.type = FILETYPE_DIRECTORY;
899
pspInfo.size = 4096;
900
pspInfo.access = 0x777;
901
pspInfo.exists = true;
902
myVector.push_back(pspInfo);
903
}
904
}
905
906
if (exists)
907
*exists = true;
908
return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());
909
}
910
911
u64 DirectoryFileSystem::FreeDiskSpace(const std::string &path) {
912
int64_t result = 0;
913
if (free_disk_space(GetLocalPath(path), result)) {
914
return ReplayApplyDisk64(ReplayAction::FREESPACE, (uint64_t)result, CoreTiming::GetGlobalTimeUs());
915
}
916
917
if (flags & FileSystemFlags::CASE_SENSITIVE) {
918
std::string fixedCase = path;
919
if (FixPathCase(basePath, fixedCase, FPC_FILE_MUST_EXIST)) {
920
// May have failed due to case sensitivity, try again.
921
if (free_disk_space(GetLocalPath(fixedCase), result)) {
922
return ReplayApplyDisk64(ReplayAction::FREESPACE, result, CoreTiming::GetGlobalTimeUs());
923
}
924
}
925
}
926
927
// Just assume they're swimming in free disk space if we don't know otherwise.
928
return ReplayApplyDisk64(ReplayAction::FREESPACE, std::numeric_limits<u64>::max(), CoreTiming::GetGlobalTimeUs());
929
}
930
931
void DirectoryFileSystem::DoState(PointerWrap &p) {
932
auto s = p.Section("DirectoryFileSystem", 0, 2);
933
if (!s)
934
return;
935
936
// Savestate layout:
937
// u32: number of entries
938
// per-entry:
939
// u32: handle number
940
// std::string filename (in guest's terms, untranslated)
941
// enum FileAccess file access mode
942
// u32 seek position
943
// s64 current truncate position (v2+ only)
944
945
u32 num = (u32) entries.size();
946
Do(p, num);
947
948
if (p.mode == p.MODE_READ) {
949
CloseAll();
950
u32 key;
951
OpenFileEntry entry;
952
entry.hFile.fileSystemFlags_ = flags;
953
for (u32 i = 0; i < num; i++) {
954
Do(p, key);
955
Do(p, entry.guestFilename);
956
Do(p, entry.access);
957
u32 err;
958
bool brokenFile = false;
959
if (!entry.hFile.Open(basePath,entry.guestFilename,entry.access, err)) {
960
ERROR_LOG(Log::FileSystem, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str());
961
brokenFile = true;
962
}
963
u32 position;
964
Do(p, position);
965
if (position != entry.hFile.Seek(position, FILEMOVE_BEGIN)) {
966
ERROR_LOG(Log::FileSystem, "Failed to restore seek position while loading state: %s", entry.guestFilename.c_str());
967
brokenFile = true;
968
}
969
if (s >= 2) {
970
Do(p, entry.hFile.needsTrunc_);
971
}
972
// Let's hope that things don't go that badly with the file mysteriously auto-closed.
973
// Better than not loading the save state at all, hopefully.
974
if (!brokenFile) {
975
entries[key] = entry;
976
}
977
}
978
} else {
979
for (auto iter = entries.begin(); iter != entries.end(); ++iter) {
980
u32 key = iter->first;
981
Do(p, key);
982
Do(p, iter->second.guestFilename);
983
Do(p, iter->second.access);
984
u32 position = (u32)iter->second.hFile.Seek(0, FILEMOVE_CURRENT);
985
Do(p, position);
986
Do(p, iter->second.hFile.needsTrunc_);
987
}
988
}
989
}
990
991
992
993
VFSFileSystem::VFSFileSystem(IHandleAllocator *_hAlloc, std::string _basePath) : basePath(_basePath) {
994
hAlloc = _hAlloc;
995
}
996
997
VFSFileSystem::~VFSFileSystem() {
998
for (auto iter = entries.begin(); iter != entries.end(); ++iter) {
999
delete [] iter->second.fileData;
1000
}
1001
entries.clear();
1002
}
1003
1004
std::string VFSFileSystem::GetLocalPath(const std::string &localPath) const {
1005
return basePath + localPath;
1006
}
1007
1008
bool VFSFileSystem::MkDir(const std::string &dirname) {
1009
// NOT SUPPORTED - READ ONLY
1010
return false;
1011
}
1012
1013
bool VFSFileSystem::RmDir(const std::string &dirname) {
1014
// NOT SUPPORTED - READ ONLY
1015
return false;
1016
}
1017
1018
int VFSFileSystem::RenameFile(const std::string &from, const std::string &to) {
1019
// NOT SUPPORTED - READ ONLY
1020
return -1;
1021
}
1022
1023
bool VFSFileSystem::RemoveFile(const std::string &filename) {
1024
// NOT SUPPORTED - READ ONLY
1025
return false;
1026
}
1027
1028
int VFSFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {
1029
if (access != FILEACCESS_READ) {
1030
ERROR_LOG(Log::FileSystem, "VFSFileSystem only supports plain reading");
1031
return SCE_KERNEL_ERROR_ERRNO_INVALID_FLAG;
1032
}
1033
1034
std::string fullName = GetLocalPath(filename);
1035
const char *fullNameC = fullName.c_str();
1036
VERBOSE_LOG(Log::FileSystem, "VFSFileSystem actually opening %s (%s)", fullNameC, filename.c_str());
1037
1038
size_t size;
1039
u8 *data = g_VFS.ReadFile(fullNameC, &size);
1040
if (!data) {
1041
ERROR_LOG(Log::FileSystem, "VFSFileSystem failed to open %s", filename.c_str());
1042
return SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
1043
}
1044
1045
OpenFileEntry entry;
1046
entry.fileData = data;
1047
entry.size = size;
1048
entry.seekPos = 0;
1049
u32 newHandle = hAlloc->GetNewHandle();
1050
entries[newHandle] = entry;
1051
return newHandle;
1052
}
1053
1054
PSPFileInfo VFSFileSystem::GetFileInfo(std::string filename) {
1055
PSPFileInfo x;
1056
x.name = filename;
1057
1058
std::string fullName = GetLocalPath(filename);
1059
File::FileInfo fo;
1060
if (g_VFS.GetFileInfo(fullName.c_str(), &fo)) {
1061
x.exists = fo.exists;
1062
if (x.exists) {
1063
x.size = fo.size;
1064
x.type = fo.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
1065
x.access = fo.isWritable ? 0666 : 0444;
1066
}
1067
} else {
1068
x.exists = false;
1069
}
1070
return x;
1071
}
1072
1073
PSPFileInfo VFSFileSystem::GetFileInfoByHandle(u32 handle) {
1074
return PSPFileInfo();
1075
}
1076
1077
1078
void VFSFileSystem::CloseFile(u32 handle) {
1079
EntryMap::iterator iter = entries.find(handle);
1080
if (iter != entries.end()) {
1081
delete [] iter->second.fileData;
1082
entries.erase(iter);
1083
} else {
1084
//This shouldn't happen...
1085
ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);
1086
}
1087
}
1088
1089
bool VFSFileSystem::OwnsHandle(u32 handle) {
1090
EntryMap::iterator iter = entries.find(handle);
1091
return (iter != entries.end());
1092
}
1093
1094
int VFSFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {
1095
return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;
1096
}
1097
1098
PSPDevType VFSFileSystem::DevType(u32 handle) {
1099
return PSPDevType::FILE;
1100
}
1101
1102
size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {
1103
int ignored;
1104
return ReadFile(handle, pointer, size, ignored);
1105
}
1106
1107
size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {
1108
DEBUG_LOG(Log::FileSystem,"VFSFileSystem::ReadFile %08x %p %i", handle, pointer, (u32)size);
1109
EntryMap::iterator iter = entries.find(handle);
1110
if (iter != entries.end())
1111
{
1112
if(iter->second.seekPos + size > iter->second.size)
1113
size = iter->second.size - iter->second.seekPos;
1114
if(size < 0) size = 0;
1115
size_t bytesRead = size;
1116
memcpy(pointer, iter->second.fileData + iter->second.seekPos, size);
1117
iter->second.seekPos += size;
1118
return bytesRead;
1119
} else {
1120
ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);
1121
return 0;
1122
}
1123
}
1124
1125
size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {
1126
int ignored;
1127
return WriteFile(handle, pointer, size, ignored);
1128
}
1129
1130
size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {
1131
// NOT SUPPORTED - READ ONLY
1132
return 0;
1133
}
1134
1135
size_t VFSFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {
1136
EntryMap::iterator iter = entries.find(handle);
1137
if (iter != entries.end()) {
1138
switch (type) {
1139
case FILEMOVE_BEGIN: iter->second.seekPos = position; break;
1140
case FILEMOVE_CURRENT: iter->second.seekPos += position; break;
1141
case FILEMOVE_END: iter->second.seekPos = iter->second.size + position; break;
1142
}
1143
return iter->second.seekPos;
1144
} else {
1145
//This shouldn't happen...
1146
ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);
1147
return 0;
1148
}
1149
}
1150
1151
std::vector<PSPFileInfo> VFSFileSystem::GetDirListing(const std::string &path, bool *exists) {
1152
std::vector<PSPFileInfo> myVector;
1153
// TODO
1154
if (exists)
1155
*exists = false;
1156
return myVector;
1157
}
1158
1159
void VFSFileSystem::DoState(PointerWrap &p) {
1160
// Note: used interchangeably with DirectoryFileSystem for flash0:, so we use the same name.
1161
auto s = p.Section("DirectoryFileSystem", 0, 2);
1162
if (!s)
1163
return;
1164
1165
u32 num = (u32) entries.size();
1166
Do(p, num);
1167
1168
if (num != 0) {
1169
p.SetError(p.ERROR_WARNING);
1170
ERROR_LOG(Log::FileSystem, "FIXME: Open files during savestate, could go badly.");
1171
}
1172
}
1173
1174