OXYGENHUB
◆ main openmsx / src / file / FileOperations.cc
17.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "FileOperations.hh"
2
3
#include "FileException.hh"
4
#include "ReadDir.hh"
5
6
#include "StringOp.hh"
7
#include "narrow.hh"
8
#include "one_of.hh"
9
#include "strCat.hh"
10
#include "unistdp.hh"
11
12
#include "build-info.hh"
13
14
#include <algorithm>
15
#include <array>
16
#include <cassert>
17
#include <cerrno>
18
#include <cstdlib>
19
#include <filesystem>
20
#include <format>
21
#include <iterator>
22
#include <stdexcept>
23
24
#ifdef	_WIN32
25
#ifndef _WIN32_IE
26
#define _WIN32_IE 0x0500	// For SHGetSpecialFolderPathW with MinGW
27
#endif
28
#include "utf8_checked.hh"
29
#include <windows.h>
30
#include <shlobj.h>
31
#include <shellapi.h>
32
#include <io.h>
33
#include <direct.h>
34
#include <ctype.h>
35
#include <cstdlib>
36
#include <cstring>
37
#include <algorithm>
38
#else // ifdef _WIN32_ ...
39
#include <climits>
40
#include <pwd.h>
41
#include <sys/types.h>
42
#include <unistd.h>
43
#endif // ifdef _WIN32_ ... else ...
44
45
#ifdef PATH_MAX
46
#define MAXPATHLEN PATH_MAX
47
#elifdef MAX_PATH
48
#define MAXPATHLEN MAX_PATH
49
#else
50
#define MAXPATHLEN 4096
51
#endif
52
53
#ifdef __APPLE__
54
#include "FileOperationsMac.hh"
55
#endif
56
57
#ifndef _MSC_VER
58
#include <dirent.h>
59
#endif
60
61
#if PLATFORM_ANDROID
62
#include "SDL_system.h" // for GetExternalStorage stuff
63
#endif
64
65
#ifdef _WIN32
66
using namespace utf8;
67
#endif
68
69
namespace openmsx::FileOperations {
70
71
bool needsTildeExpansion(std::string_view path)
72
{
73
	return !path.empty() && (path[0] == '~');
74
}
75
76
std::string expandTilde(std::string path)
77
{
78
	if (!needsTildeExpansion(path)) {
79
		return path;
80
	}
81
	auto pos = path.find_first_of('/');
82
	std::string_view user = ((path.size() == 1) || (pos == 1))
83
	                ? std::string_view{}
84
	                : std::string_view(path).substr(1, (pos == std::string::npos) ? pos : pos - 1);
85
	std::string result = getUserHomeDir(user);
86
	if (result.empty()) {
87
		// failed to find homedir, return the path unchanged
88
		return path;
89
	}
90
	if (pos == std::string_view::npos) {
91
		return result;
92
	}
93
	if (result.back() != '/') {
94
		result += '/';
95
	}
96
	std::string_view last = std::string_view(path).substr(pos + 1);
97
	result.append(last.data(), last.size());
98
	return result;
99
}
100
101
void mkdir(zstring_view path, mode_t mode)
102
{
103
#ifdef _WIN32
104
	(void)&mode; // Suppress C4100 VC++ warning
105
	if ((path == "/") || path.ends_with(':') || path.ends_with(":/")) {
106
		return;
107
	}
108
	int result = _wmkdir(utf8to16(getNativePath(std::string(path))).c_str());
109
#else
110
	int result = ::mkdir(path.c_str(), mode);
111
#endif
112
	if (result && (errno != EEXIST)) {
113
		throw FileException("Error creating dir ", path);
114
	}
115
}
116
117
static bool isUNCPath(std::string_view path)
118
{
119
#ifdef _WIN32
120
	return path.starts_with("//") || path.starts_with("\\\\");
121
#else
122
	(void)path;
123
	return false;
124
#endif
125
}
126
127
void mkdirp(std::string path)
128
{
129
	if (path.empty()) {
130
		return;
131
	}
132
133
	// We may receive platform-specific paths here, so conventionalize early
134
	path = getConventionalPath(std::move(path));
135
136
	// If the directory already exists, don't try to recreate it
137
	if (isDirectory(path))
138
		return;
139
140
	// If the path is a UNC path (e.g. \\server\share) then the first two paths in the loop below will be \ and \\server
141
	// If the path is an absolute path (e.g. c:\foo\bar) then the first path in the loop will be C:
142
	// None of those are valid directory paths, so we skip over them and don't call mkdir.
143
	// Relative paths are fine, since each segment in the path is significant.
144
	int skip = isUNCPath(path) ? 2 :
145
		   isAbsolutePath(path) ? 1 : 0;
146
	std::string::size_type pos = 0;
147
	do {
148
		pos = path.find_first_of('/', pos + 1);
149
		if (skip) {
150
			skip--;
151
			continue;
152
		}
153
		mkdir(path.substr(0, pos), 0755);
154
	} while (pos != std::string::npos);
155
156
	if (!isDirectory(path)) {
157
		throw FileException("Error creating dir ", path);
158
	}
159
}
160
161
int unlink(zstring_view path)
162
{
163
#ifdef _WIN32
164
	return _wunlink(utf8to16(path).c_str());
165
#else
166
	return ::unlink(path.c_str());
167
#endif
168
}
169
170
int rmdir(zstring_view path)
171
{
172
#ifdef _WIN32
173
	return _wrmdir(utf8to16(path).c_str());
174
#else
175
	return ::rmdir(path.c_str());
176
#endif
177
}
178
179
namespace fs = std::filesystem;
180
181
static fs::path makeFsPath(zstring_view path)
182
{
183
	#ifdef _WIN32
184
	    return {utf8to16(path)};
185
	#else
186
	    return {path.view()};
187
	#endif
188
}
189
190
int deleteRecursive(zstring_view path)
191
{
192
	std::error_code ec;
193
	fs::remove_all(makeFsPath(path), ec);
194
	return ec ? -1 : 0;
195
}
196
197
FILE_t openFile(zstring_view filename, zstring_view mode)
198
{
199
	// Mode must contain a 'b' character. On unix this doesn't make any
200
	// difference. But on windows this is required to open the file
201
	// in binary mode.
202
	assert(mode.contains('b'));
203
#ifdef _WIN32
204
	return FILE_t(_wfopen(utf8to16(filename).c_str(),
205
	                      utf8to16(mode).c_str()));
206
#else
207
	return FILE_t(fopen(filename.c_str(), mode.c_str()));
208
#endif
209
}
210
211
void openOfStream(std::ofstream& stream, zstring_view filename)
212
{
213
#ifdef _WIN32
214
	stream.open(utf8to16(filename).c_str());
215
#else
216
	stream.open(filename.c_str());
217
#endif
218
}
219
220
void openOfStream(std::ofstream& stream, zstring_view filename,
221
                  std::ios_base::openmode mode)
222
{
223
#ifdef _WIN32
224
	stream.open(utf8to16(filename).c_str(), mode);
225
#else
226
	stream.open(filename.c_str(), mode);
227
#endif
228
}
229
230
std::string_view getFilename(std::string_view path)
231
{
232
	if (auto pos = path.rfind('/'); pos != std::string_view::npos) {
233
		return path.substr(pos + 1);
234
	}
235
	return path;
236
}
237
238
std::string_view getDirName(std::string_view path)
239
{
240
	if (auto pos = path.rfind('/'); pos != std::string_view::npos) {
241
		return path.substr(0, pos + 1);
242
	}
243
	return {};
244
}
245
246
std::string_view getExtension(std::string_view path)
247
{
248
	std::string_view filename = getFilename(path);
249
	if (auto pos = filename.rfind('.'); pos != std::string_view::npos) {
250
		return filename.substr(pos);
251
	}
252
	return {};
253
}
254
255
std::string_view stripExtension(std::string_view path)
256
{
257
	if (auto pos = path.rfind('.'); pos != std::string_view::npos) {
258
		return path.substr(0, pos);
259
	}
260
	return path;
261
}
262
263
std::string_view stem(std::string_view path)
264
{
265
	auto pos1 = path.find_last_of("/.");
266
	if (pos1 == std::string_view::npos) {
267
		return path; // No '/' or '.'
268
	}
269
	if (path[pos1] != '.') {
270
		// filename without extension
271
		return path.substr(pos1 + 1);
272
	}
273
	if (pos1 == 0) { // (the only) '.' is at the start
274
		return {};
275
	}
276
277
	auto pos2 = path.find_last_of('/', pos1 - 1);
278
	if (pos2 == std::string_view::npos) {
279
		return path.substr(0, pos1);
280
	}
281
	return path.substr(pos2 + 1, pos1 - pos2 - 1);
282
}
283
284
// Characters that are not allowed (or not desirable) in a filename.
285
// Keep in sync with 'utils::filename_clean' in share/scripts/_utils.tcl.
286
static constexpr std::string_view disallowedInFilename =
287
#ifdef _WIN32
288
	R"(/\?*:|"<>+[])";
289
#else
290
	"/";
291
#endif
292
293
std::string cleanFilename(std::string_view filename)
294
{
295
	std::string result{filename};
296
	std::ranges::replace_if(result, [](char c) {
297
		// UNIX does allow 0x01-0x1f and 0x7f, but we consider them
298
		// undesirable. Note that bytes >= 0x80 are left alone, so
299
		// UTF-8 multi-byte sequences pass through unchanged.
300
		auto u = static_cast<unsigned char>(c);
301
		return (u < 0x20) || (u == 0x7f) || disallowedInFilename.contains(c);
302
	}, '_');
303
	return result;
304
}
305
306
std::string join(std::string_view part1, std::string_view part2)
307
{
308
	if (part1.empty() || isAbsolutePath(part2)) {
309
		return std::string(part2);
310
	}
311
	if (part1.back() == '/') {
312
		return strCat(part1, part2);
313
	}
314
	return strCat(part1, '/', part2);
315
}
316
std::string join(std::string_view part1, std::string_view part2, std::string_view part3)
317
{
318
	return join(part1, join(part2, part3));
319
}
320
321
std::string join(std::string_view part1, std::string_view part2,
322
            std::string_view part3, std::string_view part4)
323
{
324
	return join(part1, join(part2, join(part3, part4)));
325
}
326
327
#ifdef _WIN32
328
std::string getNativePath(zstring_view path)
329
{
330
	auto result = std::string(path);
331
	std::ranges::replace(result, '/', '\\');
332
	return result;
333
}
334
335
std::string getConventionalPath(std::string path)
336
{
337
	std::ranges::replace(path, '\\', '/');
338
	return path;
339
}
340
#endif
341
342
std::string getCurrentWorkingDirectory()
343
{
344
#ifdef _WIN32
345
	std::array<wchar_t, MAXPATHLEN> bufW;
346
	const wchar_t* result = _wgetcwd(bufW.data(), narrow<int>(bufW.size()));
347
	if (!result) {
348
		throw FileException("Couldn't get current working directory.");
349
	}
350
	return utf16to8(result);
351
#else
352
	std::array<char, MAXPATHLEN> buf;
353
	char* result = getcwd(buf.data(), buf.size());
354
	if (!result) {
355
		throw FileException("Couldn't get current working directory.");
356
	}
357
	return buf.data();
358
#endif
359
}
360
361
std::string getAbsolutePath(std::string_view path)
362
{
363
	// In rare cases getCurrentWorkingDirectory() can throw,
364
	// so only call it when really necessary.
365
	if (isAbsolutePath(path)) {
366
		return std::string(path);
367
	}
368
	return join(getCurrentWorkingDirectory(), path);
369
}
370
371
bool isAbsolutePath(std::string_view path)
372
{
373
	if (isUNCPath(path)) return true;
374
#ifdef _WIN32
375
	if ((path.size() >= 3) && (path[1] == ':') && (path[2] == one_of('/', '\\'))) {
376
		char drive = tolower(path[0]);
377
		if (('a' <= drive) && (drive <= 'z')) {
378
			return true;
379
		}
380
	}
381
#endif
382
	return !path.empty() && (path[0] == '/');
383
}
384
385
std::string getUserHomeDir(std::string_view username)
386
{
387
#ifdef _WIN32
388
	(void)(&username); // ignore parameter, avoid warning
389
390
	std::array<wchar_t, MAXPATHLEN + 1> bufW;
391
	if (!SHGetSpecialFolderPathW(nullptr, bufW.data(), CSIDL_PERSONAL, TRUE)) {
392
		throw FatalError(
393
			"SHGetSpecialFolderPathW failed: ", GetLastError());
394
	}
395
396
	return getConventionalPath(utf16to8(bufW.data()));
397
#else
398
	const char* dir = nullptr;
399
	struct passwd* pw = nullptr;
400
	if (username.empty()) {
401
		dir = getenv("HOME");
402
		if (!dir) {
403
			pw = getpwuid(getuid());
404
		}
405
	} else {
406
		pw = getpwnam(std::string(username).c_str());
407
	}
408
	if (pw) {
409
		dir = pw->pw_dir;
410
	}
411
	return dir ? dir : std::string{};
412
#endif
413
}
414
415
const std::string& getUserOpenMSXDir()
416
{
417
	static const std::string OPENMSX_DIR = []() -> std::string {
418
		if (const char* home = getenv("OPENMSX_HOME")) {
419
			return home;
420
		}
421
#ifdef _WIN32
422
		return expandTilde("~/openMSX");
423
#elif PLATFORM_ANDROID
424
		// TODO: do something to query whether the storage is available
425
		// via SDL_AndroidGetExternalStorageState
426
		return strCat(SDL_AndroidGetExternalStoragePath(), "/openMSX");
427
#else
428
		return expandTilde("~/.openMSX");
429
#endif
430
	}();
431
	return OPENMSX_DIR;
432
}
433
434
std::string getUserOpenMSXDir(std::string_view subdir)
435
{
436
	return join(getUserOpenMSXDir(), subdir);
437
}
438
439
const std::string& getUserDataDir()
440
{
441
	static std::optional<std::string> result;
442
	if (!result) {
443
		const char* const NAME = "OPENMSX_USER_DATA";
444
		const char* value = getenv(NAME);
445
		result = value ? value : getUserOpenMSXDir("share");
446
	}
447
	return *result;
448
}
449
450
const std::string& getSystemDataDir()
451
{
452
	static std::optional<std::string> result;
453
	if (!result) result = []() -> std::string {
454
		if (const char* value = getenv("OPENMSX_SYSTEM_DATA")) {
455
			return value;
456
		}
457
#ifdef _WIN32
458
		std::array<wchar_t, MAXPATHLEN + 1> bufW;
459
		if (int res = GetModuleFileNameW(nullptr, bufW.data(), DWORD(bufW.size()));
460
		    !res) {
461
			throw FatalError(
462
				"Cannot detect openMSX directory. GetModuleFileNameW failed: ",
463
				GetLastError());
464
		}
465
466
		std::string filename = utf16to8(bufW.data());
467
		auto pos = filename.find_last_of('\\');
468
		if (pos == std::string::npos) {
469
			throw FatalError("openMSX is not in directory!?");
470
		}
471
		return getConventionalPath(filename.substr(0, pos)) + "/share";
472
#elifdef __APPLE__
473
		return findResourceDir("share");
474
#elif PLATFORM_ANDROID
475
		return getAbsolutePath("openmsx_system");
476
#else
477
		// defined in build-info.hh (default /opt/openMSX/share)
478
		return DATADIR;
479
#endif
480
	}();
481
	return *result;
482
}
483
484
const std::string& getSystemDocDir()
485
{
486
	static std::optional<std::string> result;
487
	if (!result) result = []() -> std::string {
488
#ifdef _WIN32
489
		std::array<wchar_t, MAXPATHLEN + 1> bufW;
490
		if (int res = GetModuleFileNameW(nullptr, bufW.data(), DWORD(bufW.size()));
491
		    !res) {
492
			throw FatalError(
493
				"Cannot detect openMSX directory. GetModuleFileNameW failed: ",
494
				GetLastError());
495
		}
496
497
		std::string filename = utf16to8(bufW.data());
498
		auto pos = filename.find_last_of('\\');
499
		if (pos == std::string::npos) {
500
			throw FatalError("openMSX is not in directory!?");
501
		}
502
		return getConventionalPath(filename.substr(0, pos)) + "/doc";
503
#elifdef __APPLE__
504
		return findResourceDir("doc");
505
#elif PLATFORM_ANDROID
506
		return getAbsolutePath("openmsx_system"); // TODO: currently no docs are installed on Android
507
#else
508
		// defined in build-info.hh (default /opt/openMSX/doc)
509
		return DOCDIR;
510
#endif
511
	}();
512
	return *result;
513
}
514
515
#ifdef _WIN32
516
static bool driveExists(char driveLetter)
517
{
518
	std::array<char, 3> buf = {driveLetter, ':', 0};
519
	return GetFileAttributesA(buf.data()) != INVALID_FILE_ATTRIBUTES;
520
}
521
#endif
522
523
#ifdef _WIN32
524
std::string expandCurrentDirFromDrive(std::string path)
525
{
526
	std::string result = path;
527
	if (((path.size() == 2) && (path[1] == ':')) ||
528
		((path.size() >= 3) && (path[1] == ':') && (path[2] != '/'))) {
529
		// get current directory for this drive
530
		unsigned char drive = tolower(path[0]);
531
		if (('a' <= drive) && (drive <= 'z')) {
532
			std::array<wchar_t, MAXPATHLEN + 1> bufW;
533
			if (driveExists(drive) &&
534
				_wgetdcwd(drive - 'a' + 1, bufW.data(), MAXPATHLEN)) {
535
				result = getConventionalPath(utf16to8(bufW.data()));
536
				if (result.back() != '/') {
537
					result += '/';
538
				}
539
				if (path.size() > 2) {
540
					auto tmp = std::string_view(path).substr(2);
541
					result.append(tmp.data(), tmp.size());
542
				}
543
			}
544
		}
545
	}
546
	return result;
547
}
548
#endif
549
550
std::optional<Stat> getStat(zstring_view filename)
551
{
552
	std::optional<Stat> st;
553
	st.emplace(); // allocate relatively large 'struct stat' in the return slot
554
555
#ifdef _WIN32
556
	std::string filename2(filename);
557
	// workaround for VC++: strip trailing slashes (but keep it if it's the
558
	// only character in the path)
559
	if (auto pos = filename2.find_last_not_of('/'); pos != std::string::npos) {
560
		filename2.resize(pos + 1);
561
	} else {
562
		// string was either empty or a (sequence of) '/' character(s)
563
		if (!filename2.empty()) filename2.resize(1);
564
	}
565
	// For some reason, the default _wstat is 32-bit, even though we do not
566
	// seem to define _USE_32BIT_TIME anywhere... so use explicit 64-bit
567
	// call.
568
	if (_wstat64(utf8to16(filename2).c_str(), &*st)) {
569
		st.reset();
570
	}
571
#else
572
	if (stat(filename.c_str(), &*st)) {
573
		st.reset();
574
	}
575
#endif
576
577
	return st; // we count on NRVO to eliminate memcpy of 'struct stat'
578
}
579
580
bool isRegularFile(const Stat& st)
581
{
582
	return S_ISREG(st.st_mode);
583
}
584
bool isRegularFile(zstring_view filename)
585
{
586
	auto st = getStat(filename);
587
	return st && isRegularFile(*st);
588
}
589
590
bool isDirectory(const Stat& st)
591
{
592
	return S_ISDIR(st.st_mode);
593
}
594
595
bool isDirectory(zstring_view directory)
596
{
597
	auto st = getStat(directory);
598
	return st && isDirectory(*st);
599
}
600
601
bool exists(zstring_view filename)
602
{
603
	return static_cast<bool>(getStat(filename));
604
}
605
606
static unsigned getNextNum(const dirent* d, std::string_view prefix, std::string_view extension,
607
                           unsigned nofDigits)
608
{
609
	auto extensionLen = extension.size();
610
	auto prefixLen = prefix.size();
611
	std::string_view name(d->d_name);
612
613
	if ((name.size() != (prefixLen + nofDigits + extensionLen)) ||
614
	    (!name.starts_with(prefix)) ||
615
	    (name.substr(prefixLen + nofDigits, extensionLen) != extension)) {
616
		return 0;
617
	}
618
	return StringOp::stringToBase<10, unsigned>(name.substr(prefixLen, nofDigits)).value_or(0);
619
}
620
621
std::string getNextNumberedFileName(
622
	std::string_view directory, std::string_view prefix, std::string_view extension, bool addSeparator)
623
{
624
	std::string newPrefix;
625
	if (addSeparator) {
626
		newPrefix = strCat(prefix, (prefix.contains(' ') ? ' ' : '_'));
627
		prefix = newPrefix;
628
	}
629
630
	const unsigned nofDigits = 4;
631
632
	unsigned max_num = 0;
633
634
	std::string dirName = getUserOpenMSXDir(directory);
635
	try {
636
		mkdirp(dirName);
637
	} catch (FileException&) {
638
		// ignore
639
	}
640
641
	ReadDir dir(dirName);
642
	while (auto* d = dir.getEntry()) {
643
		max_num = std::max(max_num, getNextNum(d, prefix, extension, nofDigits));
644
	}
645
646
	return std::format("{}{:0{}}{}",
647
		FileOperations::join(dirName, prefix),
648
		max_num + 1, nofDigits, extension);
649
}
650
651
std::string parseCommandFileArgument(
652
	std::string_view argument, std::string_view directory,
653
	std::string_view prefix,   std::string_view extension)
654
{
655
	if (argument.empty()) {
656
		// directory is also created when needed
657
		return getNextNumberedFileName(directory, prefix, extension);
658
	}
659
660
	std::string filename(argument);
661
	if (getDirName(filename).empty()) {
662
		// no dir given, use standard dir (and create it)
663
		std::string dir = getUserOpenMSXDir(directory);
664
		mkdirp(dir);
665
		filename = join(dir, filename);
666
	} else {
667
		filename = expandTilde(std::move(filename));
668
	}
669
670
	if (!filename.ends_with(extension) && !exists(filename)) {
671
		// Expected extension not already given, append it. But only
672
		// when the filename without extension doesn't already exist.
673
		// Without this exception stuff like 'soundlog start /dev/null'
674
		// reports an error " ... error opening file /dev/null.wav."
675
		filename.append(extension.data(), extension.size());
676
	}
677
	return filename;
678
}
679
680
std::string getTempDir()
681
{
682
#ifdef _WIN32
683
	// Get required buffer size including null terminator.
684
	auto lenWithNull = GetTempPathW(0, nullptr);
685
	if (lenWithNull == 0) {
686
		throw FatalError("GetTempPathW failed: ", GetLastError());
687
	}
688
689
	std::wstring s;
690
	// Allocate space for characters only (without null terminator)
691
	s.resize_and_overwrite(lenWithNull - 1, [&](wchar_t* buf, size_t) -> size_t {
692
		// Returns length excluding the null terminator.
693
		return GetTempPathW(lenWithNull, buf);
694
	});
695
696
	if ((s.size() > 1) && s.ends_with(L'\\')) s.pop_back();
697
698
	return utf16to8(s);
699
#elif PLATFORM_ANDROID
700
	return getSystemDataDir() + "/tmp";
701
#else
702
	const char* result = nullptr;
703
	if (!result) result = getenv("TMPDIR");
704
	if (!result) result = getenv("TMP");
705
	if (!result) result = getenv("TEMP");
706
	if (!result) {
707
		result = "/tmp";
708
	}
709
	return result;
710
#endif
711
}
712
713
FILE_t openUniqueFile(const std::string& directory, std::string& filename)
714
{
715
#ifdef _WIN32
716
	std::wstring directoryW = utf8to16(directory);
717
	std::array<wchar_t, MAX_PATH> filenameW;
718
	if (!GetTempFileNameW(directoryW.c_str(), L"msx", 0, filenameW.data())) {
719
		throw FileException("GetTempFileNameW failed: ", GetLastError());
720
	}
721
	filename = utf16to8(filenameW.data());
722
	return FILE_t(_wfopen(filenameW.data(), L"wb"));
723
#else
724
	filename = directory + "/XXXXXX";
725
	auto oldMask = umask(S_IRWXO | S_IRWXG);
726
	int fd = mkstemp(filename.data());
727
	umask(oldMask);
728
	if (fd == -1) {
729
		throw FileException("Couldnt get temp file name");
730
	}
731
	return FILE_t(fdopen(fd, "wb"));
732
#endif
733
}
734
735
} // namespace openmsx::FileOperations