|
29 | 29 | #include <fstream> |
30 | 30 | #include <iostream> |
31 | 31 | #include <sstream> |
| 32 | +#include <string> |
| 33 | + |
| 34 | +#ifdef _WIN32 |
| 35 | +#include <direct.h> // _mkdir |
| 36 | +#include <windows.h> // GetTempPathW, CreateDirectoryW, RegOpenKeyExA |
| 37 | +#include <winreg.h> // registry constants |
| 38 | +#pragma comment(lib, "Advapi32.lib") // RegOpenKeyExA, RegQueryValueExA, RegCloseKey |
| 39 | + |
| 40 | +// Converts a UTF-16 wide string to a UTF-8 std::string. |
| 41 | +static std::string WcharToUTF8(const std::wstring &wstr) { |
| 42 | + if (wstr.empty()) return std::string(); |
| 43 | + int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, |
| 44 | + NULL, 0, NULL, NULL); |
| 45 | + if (len <= 0) return std::string(); |
| 46 | + std::string str(static_cast<size_t>(len - 1), '\0'); |
| 47 | + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &str[0], len, NULL, NULL); |
| 48 | + return str; |
| 49 | +} |
| 50 | +#else |
| 51 | +#include <cerrno> |
| 52 | +#include <sys/stat.h> // mkdir |
| 53 | +#endif |
32 | 54 |
|
33 | 55 | template <typename T> |
34 | 56 | static bool FloatEquals(const T& a, const T& b) { |
@@ -399,6 +421,190 @@ static bool TestStreamLoadObj() { |
399 | 421 |
|
400 | 422 | const char* gMtlBasePath = "../models/"; |
401 | 423 |
|
| 424 | +// --------------------------------------------------------------------------- |
| 425 | +// Helpers for path-related tests |
| 426 | +// --------------------------------------------------------------------------- |
| 427 | + |
| 428 | +// Creates a single directory level. Returns true on success or if it already exists. |
| 429 | +static bool MakeDir(const std::string& path) { |
| 430 | +#ifdef _WIN32 |
| 431 | + // Use the wide-character API so that paths with non-ASCII characters work. |
| 432 | + std::wstring wpath = UTF8ToWchar(path); |
| 433 | + if (wpath.empty()) return false; |
| 434 | + if (CreateDirectoryW(wpath.c_str(), NULL) != 0) return true; |
| 435 | + return GetLastError() == ERROR_ALREADY_EXISTS; |
| 436 | +#else |
| 437 | + return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST; |
| 438 | +#endif |
| 439 | +} |
| 440 | + |
| 441 | +// Removes a directory and all its contents. |
| 442 | +// NOTE: All callers pass paths that are fully constructed within this test |
| 443 | +// file from hardcoded string literals, so there is no user-controlled input |
| 444 | +// that could be used for command injection. |
| 445 | +static void RemoveTestDir(const std::string& path) { |
| 446 | +#ifdef _WIN32 |
| 447 | + std::string cmd = "rd /s /q \"" + path + "\""; |
| 448 | +#else |
| 449 | + std::string cmd = "rm -rf '" + path + "'"; |
| 450 | +#endif |
| 451 | + if (system(cmd.c_str()) != 0) { /* cleanup failure is non-fatal */ } |
| 452 | +} |
| 453 | + |
| 454 | +// Copies a file in binary mode. The destination path is taken as UTF-8. |
| 455 | +// On Windows, LongPathW(UTF8ToWchar()) is used so that long paths (> MAX_PATH) |
| 456 | +// are handled, exercising the same conversion that tinyobjloader itself uses. |
| 457 | +static bool CopyTestFile(const std::string& src, const std::string& dst) { |
| 458 | + std::ifstream in(src.c_str(), std::ios::binary); |
| 459 | + if (!in) return false; |
| 460 | +#ifdef _WIN32 |
| 461 | + // Apply long-path prefix so that the copy works even for paths > MAX_PATH. |
| 462 | + std::ofstream out(LongPathW(UTF8ToWchar(dst)).c_str(), std::ios::binary); |
| 463 | +#else |
| 464 | + std::ofstream out(dst.c_str(), std::ios::binary); |
| 465 | +#endif |
| 466 | + if (!out) return false; |
| 467 | + out << in.rdbuf(); |
| 468 | + return !out.fail(); |
| 469 | +} |
| 470 | + |
| 471 | +#ifdef _WIN32 |
| 472 | +// Returns true if Windows has the system-wide long path support enabled |
| 473 | +// (HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1). |
| 474 | +static bool IsWindowsLongPathEnabled() { |
| 475 | + HKEY hKey; |
| 476 | + DWORD value = 0; |
| 477 | + DWORD size = sizeof(DWORD); |
| 478 | + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, |
| 479 | + "SYSTEM\\CurrentControlSet\\Control\\FileSystem", 0, |
| 480 | + KEY_READ, &hKey) == ERROR_SUCCESS) { |
| 481 | + RegQueryValueExA(hKey, "LongPathsEnabled", NULL, NULL, |
| 482 | + reinterpret_cast<LPBYTE>(&value), &size); |
| 483 | + RegCloseKey(hKey); |
| 484 | + } |
| 485 | + return value != 0; |
| 486 | +} |
| 487 | +#endif // _WIN32 |
| 488 | + |
| 489 | +// --------------------------------------------------------------------------- |
| 490 | +// Path-related tests |
| 491 | +// --------------------------------------------------------------------------- |
| 492 | + |
| 493 | +// Test: load .obj/.mtl from a directory path containing UTF-8 non-ASCII |
| 494 | +// characters. On Windows our code converts the UTF-8 path to UTF-16 before |
| 495 | +// calling the file API. On Linux, UTF-8 paths are handled natively. |
| 496 | +void test_load_obj_from_utf8_path() { |
| 497 | + // Build a temp directory name that contains the UTF-8 encoded character é |
| 498 | + // (U+00E9, encoded as \xC3\xA9 in UTF-8). |
| 499 | +#ifdef _WIN32 |
| 500 | + wchar_t wtmpbuf[MAX_PATH]; |
| 501 | + GetTempPathW(MAX_PATH, wtmpbuf); |
| 502 | + std::string test_dir = |
| 503 | + WcharToUTF8(wtmpbuf) + "tinyobj_utf8_\xc3\xa9_test\\"; |
| 504 | +#else |
| 505 | + std::string test_dir = "/tmp/tinyobj_utf8_\xc3\xa9_test/"; |
| 506 | +#endif |
| 507 | + |
| 508 | + if (!MakeDir(test_dir)) { |
| 509 | + std::cout << "SKIPPED: Cannot create Unicode temp directory: " << test_dir |
| 510 | + << "\n"; |
| 511 | + return; |
| 512 | + } |
| 513 | + |
| 514 | + const std::string obj_dst = test_dir + "utf8-path-test.obj"; |
| 515 | + const std::string mtl_dst = test_dir + "utf8-path-test.mtl"; |
| 516 | + |
| 517 | + if (!CopyTestFile("../models/utf8-path-test.obj", obj_dst) || |
| 518 | + !CopyTestFile("../models/utf8-path-test.mtl", mtl_dst)) { |
| 519 | + RemoveTestDir(test_dir); |
| 520 | + TEST_CHECK_(false, "Failed to copy test files to Unicode temp directory"); |
| 521 | + return; |
| 522 | + } |
| 523 | + |
| 524 | + tinyobj::ObjReader reader; |
| 525 | + bool ret = reader.ParseFromFile(obj_dst); |
| 526 | + |
| 527 | + RemoveTestDir(test_dir); |
| 528 | + |
| 529 | + if (!reader.Warning().empty()) |
| 530 | + std::cout << "WARN: " << reader.Warning() << "\n"; |
| 531 | + if (!reader.Error().empty()) |
| 532 | + std::cerr << "ERR: " << reader.Error() << "\n"; |
| 533 | + |
| 534 | + TEST_CHECK(ret == true); |
| 535 | + TEST_CHECK(reader.GetShapes().size() == 1); |
| 536 | + TEST_CHECK(reader.GetMaterials().size() == 1); |
| 537 | +} |
| 538 | + |
| 539 | +// Test: load .obj/.mtl from a path whose total length exceeds MAX_PATH (260). |
| 540 | +// On Windows, tinyobjloader prepends the \\?\ extended-length path prefix so |
| 541 | +// that the file can be opened even on systems that have the OS-wide long path |
| 542 | +// support enabled. The test is skipped when that support is not active. |
| 543 | +// On Linux, long paths work natively; this test verifies no regression. |
| 544 | +void test_load_obj_from_long_path() { |
| 545 | +#ifdef _WIN32 |
| 546 | + if (!IsWindowsLongPathEnabled()) { |
| 547 | + std::cout |
| 548 | + << "SKIPPED: Windows long path support (LongPathsEnabled) is not " |
| 549 | + "enabled\n"; |
| 550 | + return; |
| 551 | + } |
| 552 | + wchar_t wtmpbuf[MAX_PATH]; |
| 553 | + GetTempPathW(MAX_PATH, wtmpbuf); |
| 554 | + std::string base = WcharToUTF8(wtmpbuf); // e.g. "C:\Users\...\Temp\" |
| 555 | + const char path_sep = '\\'; |
| 556 | +#else |
| 557 | + std::string base = "/tmp/"; |
| 558 | + const char path_sep = '/'; |
| 559 | +#endif |
| 560 | + |
| 561 | + // Create a two-level directory where the deepest directory name is 250 |
| 562 | + // characters long. Combined with the base path and the filename |
| 563 | + // "utf8-path-test.obj" (18 chars) the total file path comfortably exceeds |
| 564 | + // MAX_PATH (260) on all supported platforms. |
| 565 | + std::string test_root = base + "tinyobj_lp_test" + path_sep; |
| 566 | + std::string long_subdir = test_root + std::string(250, 'a') + path_sep; |
| 567 | + std::string obj_path = long_subdir + "utf8-path-test.obj"; |
| 568 | + |
| 569 | + // obj_path must exceed MAX_PATH for the test to be meaningful. |
| 570 | + // (On a typical Windows installation it is ~320 chars; on Linux ~287 chars.) |
| 571 | + if (obj_path.size() <= 260) { |
| 572 | + std::cout << "SKIPPED: generated path (" << obj_path.size() |
| 573 | + << " chars) does not exceed MAX_PATH=260\n"; |
| 574 | + return; |
| 575 | + } |
| 576 | + |
| 577 | + if (!MakeDir(test_root) || !MakeDir(long_subdir)) { |
| 578 | + RemoveTestDir(test_root); |
| 579 | + std::cout << "SKIPPED: Cannot create long-path temp directory: " |
| 580 | + << long_subdir << "\n"; |
| 581 | + return; |
| 582 | + } |
| 583 | + |
| 584 | + if (!CopyTestFile("../models/utf8-path-test.obj", |
| 585 | + long_subdir + "utf8-path-test.obj") || |
| 586 | + !CopyTestFile("../models/utf8-path-test.mtl", |
| 587 | + long_subdir + "utf8-path-test.mtl")) { |
| 588 | + RemoveTestDir(test_root); |
| 589 | + TEST_CHECK_(false, "Failed to copy test files to long-path directory"); |
| 590 | + return; |
| 591 | + } |
| 592 | + |
| 593 | + tinyobj::ObjReader reader; |
| 594 | + bool ret = reader.ParseFromFile(obj_path); |
| 595 | + |
| 596 | + RemoveTestDir(test_root); |
| 597 | + |
| 598 | + if (!reader.Warning().empty()) |
| 599 | + std::cout << "WARN: " << reader.Warning() << "\n"; |
| 600 | + if (!reader.Error().empty()) |
| 601 | + std::cerr << "ERR: " << reader.Error() << "\n"; |
| 602 | + |
| 603 | + TEST_CHECK(ret == true); |
| 604 | + TEST_CHECK(reader.GetShapes().size() == 1); |
| 605 | + TEST_CHECK(reader.GetMaterials().size() == 1); |
| 606 | +} |
| 607 | + |
402 | 608 | void test_cornell_box() { |
403 | 609 | TEST_CHECK(true == TestLoadObj("../models/cornell_box.obj", gMtlBasePath)); |
404 | 610 | } |
@@ -1747,6 +1953,8 @@ TEST_LIST = { |
1747 | 1953 | test_default_kd_for_multiple_materials_issue391}, |
1748 | 1954 | {"test_removeUtf8Bom", test_removeUtf8Bom}, |
1749 | 1955 | {"test_loadObj_with_BOM", test_loadObj_with_BOM}, |
| 1956 | + {"test_load_obj_from_utf8_path", test_load_obj_from_utf8_path}, |
| 1957 | + {"test_load_obj_from_long_path", test_load_obj_from_long_path}, |
1750 | 1958 | {"test_loadObjWithCallback_with_BOM", test_loadObjWithCallback_with_BOM}, |
1751 | 1959 | {"test_texcoord_w_component", test_texcoord_w_component}, |
1752 | 1960 | {"test_texcoord_w_mixed_component", test_texcoord_w_mixed_component}, |
|
0 commit comments