diff --git a/CMakeLists.txt b/CMakeLists.txt index 7deb1822..679493a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -497,6 +497,7 @@ if (WIBO_ENABLE_FIXTURE_TESTS) wibo_add_fixture_bin(NAME test_fls SOURCES test/test_fls.c) wibo_add_fixture_bin(NAME test_fpu_state SOURCES test/test_fpu_state.c) wibo_add_fixture_bin(NAME test_handleapi SOURCES test/test_handleapi.c) + wibo_add_fixture_bin(NAME test_movefileex SOURCES test/test_movefileex.c) wibo_add_fixture_bin(NAME test_findfile SOURCES test/test_findfile.c) wibo_add_fixture_bin(NAME test_getfileattributesex SOURCES test/test_getfileattributesex.c) wibo_add_fixture_bin(NAME test_locale SOURCES test/test_locale.c) diff --git a/dll/kernel32/fileapi.cpp b/dll/kernel32/fileapi.cpp index 31b5083b..e9dd3044 100644 --- a/dll/kernel32/fileapi.cpp +++ b/dll/kernel32/fileapi.cpp @@ -34,8 +34,38 @@ #include #include +#if defined(__linux__) +#include +#include +#endif + namespace { +// Keep no-replace moves atomic even if another process creates the destination. +int renameFile(const std::filesystem::path &from, const std::filesystem::path &to, bool replace) { + if (replace) + return rename(from.c_str(), to.c_str()); +#if defined(__APPLE__) + return renamex_np(from.c_str(), to.c_str(), RENAME_EXCL); +#else + return syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE); +#endif +} + +DWORD moveFileError(int error, const std::filesystem::path &path) { + if (error == EXDEV) + return ERROR_NOT_SAME_DEVICE; + if (error == EISDIR || error == ENOTEMPTY || error == EPERM) + return ERROR_ACCESS_DENIED; + if (error == ENOENT) { + auto parent = path.parent_path(); + std::error_code ec; + if (!parent.empty() && !std::filesystem::is_directory(parent, ec)) + return ERROR_PATH_NOT_FOUND; + } + return wibo::winErrorFromErrno(error); +} + using random_shorts_engine = std::independent_bits_engine; @@ -1241,6 +1271,90 @@ BOOL WINAPI MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName) { return MoveFileA(from.c_str(), to.c_str()); } +BOOL WINAPI MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, DWORD dwFlags) { + HOST_CONTEXT_GUARD(); + DEBUG_LOG("MoveFileExA(%s, %s, 0x%x)\n", lpExistingFileName ? lpExistingFileName : "(null)", + lpNewFileName ? lpNewFileName : "(null)", dwFlags); + if ((dwFlags & MOVEFILE_DELAY_UNTIL_REBOOT) && (dwFlags & MOVEFILE_COPY_ALLOWED)) { + setLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + // There is no reboot queue or Windows link-tracking service in wibo. + if (dwFlags & (MOVEFILE_DELAY_UNTIL_REBOOT | MOVEFILE_CREATE_HARDLINK | MOVEFILE_FAIL_IF_NOT_TRACKABLE)) { + setLastError(ERROR_NOT_SUPPORTED); + return FALSE; + } + if (!lpExistingFileName || !lpNewFileName || + (dwFlags & ~(MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH))) { + setLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + auto from = files::pathFromWindows(lpExistingFileName); + auto to = files::pathFromWindows(lpNewFileName); + struct stat source{}, destination{}; + if (stat(from.c_str(), &source) != 0) { + setLastError(moveFileError(errno, from)); + return FALSE; + } + bool replace = (dwFlags & MOVEFILE_REPLACE_EXISTING) != 0; + if (stat(to.c_str(), &destination) == 0) { + if (source.st_dev == destination.st_dev && source.st_ino == destination.st_ino) + return TRUE; + if (!replace) { + setLastError(ERROR_ALREADY_EXISTS); + return FALSE; + } + if (S_ISDIR(destination.st_mode) || !(destination.st_mode & S_IWUSR)) { + setLastError(ERROR_ACCESS_DENIED); + return FALSE; + } + } else if (errno != ENOENT) { + setLastError(moveFileError(errno, to)); + return FALSE; + } + if (renameFile(from, to, replace) == 0) + return TRUE; + int error = errno; + if (error != EXDEV || !(dwFlags & MOVEFILE_COPY_ALLOWED) || !S_ISREG(source.st_mode)) { + setLastError(moveFileError(error, to)); + return FALSE; + } + + std::error_code ec; + auto options = replace ? std::filesystem::copy_options::overwrite_existing : std::filesystem::copy_options::none; + if (!std::filesystem::copy_file(from, to, options, ec)) { + setLastError(moveFileError(ec.value(), to)); + return FALSE; + } + // WRITE_THROUGH requires flushing the copy before deleting the original. + if (dwFlags & MOVEFILE_WRITE_THROUGH) { + int fd = open(to.c_str(), O_RDONLY); + if (fd < 0) { + setLastError(moveFileError(errno, to)); + return FALSE; + } + int result = fsync(fd); + error = errno; + close(fd); + if (result != 0) { + setLastError(moveFileError(error, to)); + return FALSE; + } + } + // Windows reports success after a successful copy even if deletion fails. + if (unlink(from.c_str()) != 0) + DEBUG_LOG("MoveFileExA: copied but could not delete source (errno %d)\n", errno); + return TRUE; +} + +BOOL WINAPI MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, DWORD dwFlags) { + HOST_CONTEXT_GUARD(); + DEBUG_LOG("MoveFileExW -> "); + std::string from = lpExistingFileName ? wideStringToString(lpExistingFileName) : ""; + std::string to = lpNewFileName ? wideStringToString(lpNewFileName) : ""; + return MoveFileExA(lpExistingFileName ? from.c_str() : nullptr, lpNewFileName ? to.c_str() : nullptr, dwFlags); +} + DWORD WINAPI SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod) { HOST_CONTEXT_GUARD(); DEBUG_LOG("SetFilePointer(%p, %ld, %p, %u)\n", hFile, static_cast(lDistanceToMove), lpDistanceToMoveHigh, diff --git a/dll/kernel32/fileapi.h b/dll/kernel32/fileapi.h index 0e1d443c..afd8cd17 100644 --- a/dll/kernel32/fileapi.h +++ b/dll/kernel32/fileapi.h @@ -25,6 +25,13 @@ constexpr DWORD OPEN_EXISTING = 3; constexpr DWORD OPEN_ALWAYS = 4; constexpr DWORD TRUNCATE_EXISTING = 5; +constexpr DWORD MOVEFILE_REPLACE_EXISTING = 0x1; +constexpr DWORD MOVEFILE_COPY_ALLOWED = 0x2; +constexpr DWORD MOVEFILE_DELAY_UNTIL_REBOOT = 0x4; +constexpr DWORD MOVEFILE_WRITE_THROUGH = 0x8; +constexpr DWORD MOVEFILE_CREATE_HARDLINK = 0x10; +constexpr DWORD MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x20; + constexpr DWORD FILE_BEGIN = 0; constexpr DWORD FILE_CURRENT = 1; constexpr DWORD FILE_END = 2; @@ -91,6 +98,8 @@ BOOL WINAPI DeleteFileA(LPCSTR lpFileName); BOOL WINAPI DeleteFileW(LPCWSTR lpFileName); BOOL WINAPI MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName); BOOL WINAPI MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName); +BOOL WINAPI MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, DWORD dwFlags); +BOOL WINAPI MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, DWORD dwFlags); DWORD WINAPI SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod); BOOL WINAPI SetFilePointerEx(HANDLE hFile, LARGE_INTEGER liDistanceToMove, PLARGE_INTEGER lpNewFilePointer, DWORD dwMoveMethod); diff --git a/dll/kernel32/memoryapi.cpp b/dll/kernel32/memoryapi.cpp index b43badd3..82090397 100644 --- a/dll/kernel32/memoryapi.cpp +++ b/dll/kernel32/memoryapi.cpp @@ -392,7 +392,21 @@ static LPVOID mapViewOfFileInternal(Pin mapping, DWORD dwDesiredA setLastError(ERROR_INVALID_ADDRESS); return nullptr; } - requestedBase = reinterpret_cast(mapBaseAddr); + uintptr_t mapEnd = alignUp(mapBaseAddr + mapLength, pageSize); + if (mapEnd <= mapBaseAddr || mapEnd > kProcessAddressLimit) { + setLastError(ERROR_INVALID_ADDRESS); + return nullptr; + } + // Fixed views must participate in the same address-space accounting as + // VirtualAlloc, both to reject overlaps and to prevent later allocations + // from replacing the view (VC6 restores PCH data at a fixed address). + wibo::heap::VmStatus reserveStatus = + wibo::heap::reserveViewRange(mapLength, mapBaseAddr, mapEnd, &requestedBase); + if (reserveStatus != wibo::heap::VmStatus::Success) { + setLastError(ERROR_INVALID_ADDRESS); + return nullptr; + } + reservedMapping = true; #ifdef MAP_FIXED_NOREPLACE mapFlags |= MAP_FIXED_NOREPLACE; #else diff --git a/src/errors.h b/src/errors.h index 06638fe1..c79aaeb9 100644 --- a/src/errors.h +++ b/src/errors.h @@ -8,6 +8,7 @@ #define ERROR_ACCESS_DENIED 5 #define ERROR_INVALID_HANDLE 6 #define ERROR_NOT_ENOUGH_MEMORY 8 +#define ERROR_NOT_SAME_DEVICE 17 #define ERROR_NO_MORE_FILES 18 #define ERROR_NO_MORE_ITEMS 259 #define ERROR_FILE_EXISTS 80 diff --git a/test/test_createfilemapping.c b/test/test_createfilemapping.c index 98756877..75d06568 100644 --- a/test/test_createfilemapping.c +++ b/test/test_createfilemapping.c @@ -102,10 +102,46 @@ static void test_zero_size_pagefile_mapping_fails(void) { TEST_CHECK_EQ(ERROR_INVALID_PARAMETER, GetLastError()); } +static void test_fixed_view_reserves_address_space(void) { + const SIZE_T size = 2 * 65536; + uint8_t *base = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE); + TEST_CHECK(base != NULL); + base[0] = 0x42; + HANDLE file = create_temp_file(); + HANDLE mapping = CreateFileMappingA(file, NULL, PAGE_READWRITE, 0, (DWORD)size, NULL); + TEST_CHECK(mapping != NULL); + // A fixed view must not overwrite an existing allocation. + TEST_CHECK(MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base) == NULL); + TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError()); + TEST_CHECK_EQ(0x42, base[0]); + TEST_CHECK(VirtualFree(base, 0, MEM_RELEASE)); + uint8_t *view = MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base); + TEST_CHECK(view == base); + view[0] = 0x24; + view[size - 1] = 0x5a; + TEST_CHECK(MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base) == NULL); + TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError()); + TEST_CHECK(VirtualAlloc(base, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == NULL); + TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError()); + // VC6 reserves another large top-down arena after restoring its PCH view. + uint8_t *arena = VirtualAlloc(NULL, 50 * 1024 * 1024, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE); + TEST_CHECK(arena != NULL); + TEST_CHECK((uintptr_t)arena + 50 * 1024 * 1024 <= (uintptr_t)view || (uintptr_t)arena >= (uintptr_t)view + size); + TEST_CHECK_EQ(0x24, view[0]); + TEST_CHECK_EQ(0x5a, view[size - 1]); + TEST_CHECK(VirtualFree(arena, 0, MEM_RELEASE)); + TEST_CHECK(UnmapViewOfFile(view)); + TEST_CHECK(VirtualAlloc(base, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base); + TEST_CHECK(VirtualFree(base, 0, MEM_RELEASE)); + TEST_CHECK(CloseHandle(mapping)); + TEST_CHECK(CloseHandle(file)); +} + int main(void) { test_readwrite_mapping_extends_file(); test_copy_mapping_does_not_write_file(); test_zero_size_file_mapping_fails(); test_zero_size_pagefile_mapping_fails(); + test_fixed_view_reserves_address_space(); return 0; } diff --git a/test/test_movefileex.c b/test/test_movefileex.c new file mode 100644 index 00000000..18ae28c6 --- /dev/null +++ b/test/test_movefileex.c @@ -0,0 +1,117 @@ +#include "test_assert.h" +#include + +static BOOL move_file(BOOL wide, const char *from, const char *to, DWORD flags) { + if (!wide) + return MoveFileExA(from, to, flags); + WCHAR from_w[MAX_PATH], to_w[MAX_PATH]; + TEST_CHECK(MultiByteToWideChar(CP_ACP, 0, from, -1, from_w, MAX_PATH)); + TEST_CHECK(MultiByteToWideChar(CP_ACP, 0, to, -1, to_w, MAX_PATH)); + return MoveFileExW(from_w, to_w, flags); +} + +static void write_file(const char *name, const char *content) { + HANDLE file = CreateFileA(name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + TEST_CHECK(file != INVALID_HANDLE_VALUE); + DWORD written; + TEST_CHECK(WriteFile(file, content, (DWORD)strlen(content), &written, NULL)); + TEST_CHECK_EQ(strlen(content), written); + TEST_CHECK(CloseHandle(file)); +} + +static void check_file(const char *name, const char *content) { + HANDLE file = CreateFileA(name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + TEST_CHECK(file != INVALID_HANDLE_VALUE); + char buffer[32] = {0}; + DWORD count; + TEST_CHECK(ReadFile(file, buffer, sizeof(buffer) - 1, &count, NULL)); + TEST_CHECK_EQ(strlen(content), count); + TEST_CHECK_STR_EQ(content, buffer); + TEST_CHECK(CloseHandle(file)); +} + +static void check_missing(const char *name) { + TEST_CHECK_EQ(INVALID_FILE_ATTRIBUTES, GetFileAttributesA(name)); + TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError()); +} + +static void test_moves(BOOL wide) { + write_file("source", "new contents"); + TEST_CHECK(move_file(wide, "source", "dest", MOVEFILE_COPY_ALLOWED)); + check_missing("source"); + check_file("dest", "new contents"); + write_file("source", "replacement"); + TEST_CHECK(!move_file(wide, "source", "dest", 0)); + TEST_CHECK_EQ(ERROR_ALREADY_EXISTS, GetLastError()); + check_file("source", "replacement"); + check_file("dest", "new contents"); + TEST_CHECK(move_file(wide, "source", "dest", MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)); + check_missing("source"); + check_file("dest", "replacement"); + TEST_CHECK(move_file(wide, "dest", "dest", MOVEFILE_REPLACE_EXISTING)); + check_file("dest", "replacement"); + TEST_CHECK(!move_file(wide, "missing", "dest", MOVEFILE_REPLACE_EXISTING)); + TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError()); + check_file("dest", "replacement"); + TEST_CHECK(!move_file(wide, "dest", "missing/child", 0)); + TEST_CHECK_EQ(ERROR_PATH_NOT_FOUND, GetLastError()); + TEST_CHECK(CreateDirectoryA("dir", NULL)); + TEST_CHECK(!move_file(wide, "dest", "dir", MOVEFILE_REPLACE_EXISTING)); + TEST_CHECK_EQ(ERROR_ACCESS_DENIED, GetLastError()); + TEST_CHECK(CreateDirectoryA("dir2", NULL)); + TEST_CHECK(!move_file(wide, "dir", "dir2", MOVEFILE_REPLACE_EXISTING)); + TEST_CHECK_EQ(ERROR_ACCESS_DENIED, GetLastError()); + TEST_CHECK(RemoveDirectoryA("dir2")); + TEST_CHECK(move_file(wide, "dir", "dir2", 0)); + TEST_CHECK(RemoveDirectoryA("dir2")); + check_file("dest", "replacement"); + TEST_CHECK(DeleteFileA("dest")); +} + +// Optional destination directory on another volume, for exercising the EXDEV path. +static void test_cross_volume(BOOL wide, const char *directory) { + char dest[MAX_PATH]; + TEST_CHECK(GetTempFileNameA(directory, "wbo", 0, dest)); + TEST_CHECK(DeleteFileA(dest)); + write_file("source", "cross volume"); + TEST_CHECK(!move_file(wide, "source", dest, 0)); + TEST_CHECK_EQ(ERROR_NOT_SAME_DEVICE, GetLastError()); + check_file("source", "cross volume"); + TEST_CHECK(move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)); + check_missing("source"); + check_file(dest, "cross volume"); + write_file("source", "replacement"); + TEST_CHECK(!move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED)); + TEST_CHECK_EQ(ERROR_ALREADY_EXISTS, GetLastError()); + check_file("source", "replacement"); + check_file(dest, "cross volume"); + TEST_CHECK( + move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)); + check_missing("source"); + check_file(dest, "replacement"); + TEST_CHECK(DeleteFileA(dest)); + TEST_CHECK(CreateDirectoryA("source", NULL)); + TEST_CHECK(!move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED)); + DWORD error = GetLastError(); + TEST_CHECK(error == ERROR_NOT_SAME_DEVICE || error == ERROR_ACCESS_DENIED); + TEST_CHECK(RemoveDirectoryA("source")); +} + +int main(int argc, char **argv) { + char original[MAX_PATH], temp[MAX_PATH], fixture[MAX_PATH]; + TEST_CHECK(GetCurrentDirectoryA(MAX_PATH, original)); + TEST_CHECK(GetTempPathA(MAX_PATH, temp)); + TEST_CHECK(GetTempFileNameA(temp, "wbo", 0, fixture)); + TEST_CHECK(DeleteFileA(fixture)); + TEST_CHECK(CreateDirectoryA(fixture, NULL)); + TEST_CHECK(SetCurrentDirectoryA(fixture)); + test_moves(FALSE); + test_moves(TRUE); + if (argc > 1) { + test_cross_volume(FALSE, argv[1]); + test_cross_volume(TRUE, argv[1]); + } + TEST_CHECK(SetCurrentDirectoryA(original)); + TEST_CHECK(RemoveDirectoryA(fixture)); + return 0; +} diff --git a/test/vc6_pch/README.md b/test/vc6_pch/README.md new file mode 100644 index 00000000..88a208f7 --- /dev/null +++ b/test/vc6_pch/README.md @@ -0,0 +1,32 @@ +# VC6 automatic precompiled-header regression + +VC6's `/YX /Fprepro.pch` writes a temporary file and calls +`MoveFileExW(temp, "repro.pch", MOVEFILE_COPY_ALLOWED)` to publish it. +Before the MoveFileEx shim, this aborts on a missing import. +Reuse also needs fixed-address `MapViewOfFileEx` views to reserve their address +range in the allocator. Otherwise VC6's subsequent top-down `VirtualAlloc` +can overwrite the restored PCH view. `test_createfilemapping.exe` checks that +fixed views reject overlapping allocations and remain intact. + +Supply your own VC6 compiler; no proprietary compiler files are included. +Run with Wine first, then the newly built wibo: + +```sh +WINEDEBUG=-all python3 test/vc6_pch/check.py \ + --runner /path/to/wine --compiler /path/to/CL.EXE --output /tmp/pch-wine +python3 test/vc6_pch/check.py \ + --runner build/debug/wibo --compiler /path/to/CL.EXE --output /tmp/pch-wibo +``` + +The check compiles without PCH, creates an automatic PCH, and recompiles with +that PCH. It requires the PCH's hash and modification time to remain unchanged +on reuse, and compares all three objects after excluding only the COFF +header timestamp. It retains the objects and command results in the output +directory so Wine and wibo output can also be compared. + +`test_movefileex.exe` covers the API without requiring VC6. An optional first +argument names a writable directory on another volume to exercise cross-volume +copy, replacement, write-through and directory-move rejection. Run that variant +under Wine first, then wibo with the same destination. Delayed reboot operations +and link tracking are unsupported by the shim and return `ERROR_NOT_SUPPORTED`; +copy combined with delay and unknown flags return `ERROR_INVALID_PARAMETER`. diff --git a/test/vc6_pch/check.py b/test/vc6_pch/check.py new file mode 100644 index 00000000..65c68a0a --- /dev/null +++ b/test/vc6_pch/check.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Manually test VC6 automatic PCH creation/reuse (requires a user-supplied compiler).""" +import argparse +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import tempfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--runner", required=True, type=Path) +parser.add_argument("--compiler", required=True, type=Path) +parser.add_argument("--output", required=True, type=Path) +args = parser.parse_args() +runner, compiler = args.runner.resolve(), args.compiler.resolve() +output = args.output.resolve() +output.mkdir(parents=True, exist_ok=True) +results = [] +with tempfile.TemporaryDirectory(prefix="vc6-pch-") as temporary: + work = Path(temporary) + for name in ("pch.h", "repro.cpp"): + shutil.copyfile(Path(__file__).parent / name, work / name) + pch_state = None + objects = [] + for mode in ("plain", "create", "reuse"): + command = [str(runner), str(compiler), "/nologo", "/O2", "/c", "/Forepro.obj"] + if mode != "plain": + command += ["/YX", "/Fprepro.pch"] + command += ["repro.cpp"] + result = subprocess.run(command, cwd=work, capture_output=True, text=True, timeout=60) + row = dict(mode=mode, command=command, returncode=result.returncode, + stdout=result.stdout, stderr=result.stderr) + results.append(row) + print(json.dumps(row), flush=True) + (output / "results.json").write_text(json.dumps(results, indent=2) + "\n") + if result.returncode: + raise SystemExit(f"{mode} failed") + obj = (work / "repro.obj").read_bytes() + (output / f"{mode}.obj").write_bytes(obj) + # COFF TimeDateStamp is the only field excluded from the comparison. + normalized = obj[:4] + bytes(4) + obj[8:] + objects.append(normalized) + row["normalized_object_sha256"] = hashlib.sha256(normalized).hexdigest() + if mode != "plain": + pch = work / "repro.pch" + state = (pch.stat().st_mtime_ns, hashlib.sha256(pch.read_bytes()).hexdigest()) + if mode == "create": + pch_state = state + if pch.stat().st_size == 0: + raise SystemExit("empty PCH") + elif state != pch_state: + raise SystemExit("PCH was rewritten instead of reused") + row["pch_sha256"] = state[1] + (work / "repro.obj").unlink() + if not all(obj == objects[0] for obj in objects): + raise SystemExit("objects differ beyond the COFF timestamp") +(output / "results.json").write_text(json.dumps(results, indent=2) + "\n") +print("PASS: PCH created and reused; all objects match excluding COFF timestamps") diff --git a/test/vc6_pch/pch.h b/test/vc6_pch/pch.h new file mode 100644 index 00000000..bfb1ee64 --- /dev/null +++ b/test/vc6_pch/pch.h @@ -0,0 +1 @@ +struct Pair { int x; int y; }; diff --git a/test/vc6_pch/repro.cpp b/test/vc6_pch/repro.cpp new file mode 100644 index 00000000..4fdf8985 --- /dev/null +++ b/test/vc6_pch/repro.cpp @@ -0,0 +1,2 @@ +#include "pch.h" +int sum(Pair p) { return p.x + p.y; }