| 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 |
| 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 |
| 39 |
#include <climits> |
| 40 |
#include <pwd.h> |
| 41 |
#include <sys/types.h> |
| 42 |
#include <unistd.h> |
| 43 |
#endif |
| 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" |
| 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 |
|
| 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; |
| 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 |
|
| 134 |
path = getConventionalPath(std::move(path)); |
| 135 |
|
| 136 |
|
| 137 |
if (isDirectory(path)) |
| 138 |
return; |
| 139 |
|
| 140 |
|
| 141 |
|
| 142 |
|
| 143 |
|
| 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 |
|
| 200 |
|
| 201 |
|
| 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; |
| 268 |
} |
| 269 |
if (path[pos1] != '.') { |
| 270 |
|
| 271 |
return path.substr(pos1 + 1); |
| 272 |
} |
| 273 |
if (pos1 == 0) { |
| 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 |
|
| 285 |
|
| 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 |
|
| 364 |
|
| 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); |
| 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 |
|
| 425 |
|
| 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 |
|
| 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"); |
| 507 |
#else |
| 508 |
|
| 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 |
|
| 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(); |
| 554 |
|
| 555 |
#ifdef _WIN32 |
| 556 |
std::string filename2(filename); |
| 557 |
|
| 558 |
|
| 559 |
if (auto pos = filename2.find_last_not_of('/'); pos != std::string::npos) { |
| 560 |
filename2.resize(pos + 1); |
| 561 |
} else { |
| 562 |
|
| 563 |
if (!filename2.empty()) filename2.resize(1); |
| 564 |
} |
| 565 |
|
| 566 |
|
| 567 |
|
| 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; |
| 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 |
|
| 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 |
|
| 657 |
return getNextNumberedFileName(directory, prefix, extension); |
| 658 |
} |
| 659 |
|
| 660 |
std::string filename(argument); |
| 661 |
if (getDirName(filename).empty()) { |
| 662 |
|
| 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 |
|
| 672 |
|
| 673 |
|
| 674 |
|
| 675 |
filename.append(extension.data(), extension.size()); |
| 676 |
} |
| 677 |
return filename; |
| 678 |
} |
| 679 |
|
| 680 |
std::string getTempDir() |
| 681 |
{ |
| 682 |
#ifdef _WIN32 |
| 683 |
|
| 684 |
auto lenWithNull = GetTempPathW(0, nullptr); |
| 685 |
if (lenWithNull == 0) { |
| 686 |
throw FatalError("GetTempPathW failed: ", GetLastError()); |
| 687 |
} |
| 688 |
|
| 689 |
std::wstring s; |
| 690 |
|
| 691 |
s.resize_and_overwrite(lenWithNull - 1, [&](wchar_t* buf, size_t) -> size_t { |
| 692 |
|
| 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 |
} |