Files
docview/tests/test_windows_resources.cpp
2026-09-21 13:41:40 +09:00

109 lines
6.3 KiB
C++

#include "web/resource_policy.h"
#include "web/windows_resource_handles.h"
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
#include <QTest>
#include <winioctl.h>
#include <cstring>
using namespace docview;
namespace {
std::wstring native(const QString &path) { return QDir::toNativeSeparators(path).toStdWString(); }
bool put(const QString &path, const QByteArray &bytes) {
QFile file(path); return file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size();
}
bool junction(const QString &path, const QString &destination) {
if (!QDir().mkpath(path)) return false;
const auto name = native(path), target = std::wstring(L"\\??\\") + native(destination);
winresource::Handle handle(CreateFileW(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
if (!handle.valid()) return false;
struct Header { DWORD tag; WORD bytes, reserved, substituteOffset, substituteBytes, printOffset, printBytes; };
const size_t stringBytes = (target.size() + 1) * sizeof(wchar_t);
std::vector<unsigned char> storage(sizeof(Header) + stringBytes, 0);
auto *header = reinterpret_cast<Header *>(storage.data());
header->tag = IO_REPARSE_TAG_MOUNT_POINT; header->bytes = static_cast<WORD>(8 + stringBytes);
header->substituteBytes = static_cast<WORD>(target.size() * sizeof(wchar_t));
header->printOffset = header->substituteBytes; header->printBytes = 0;
std::memcpy(storage.data() + sizeof(Header), target.c_str(), stringBytes);
DWORD returned = 0;
return DeviceIoControl(handle.get(), FSCTL_SET_REPARSE_POINT, storage.data(), static_cast<DWORD>(storage.size()), nullptr, 0, &returned, nullptr);
}
}
class WindowsResourceTests : public QObject {
Q_OBJECT
private slots:
void pathsAndNativeComponents() {
for (const auto &name : {L"..", L"CON.txt", L"COM1", L"LPT\u00b2.log", L"asset:stream", L"a/b", L"name.", L"name ", L"a\\b"})
QVERIFY(!winresource::safeComponent(name));
QVERIFY(winresource::safeComponent(L"chapter.xhtml"));
for (const auto &root : {QString("C:relative"), QString("//server/share"), QString("//?/C:/tmp")}) {
QVERIFY(!openResource(root, "a")); ResourceWriter writer(root, "a"); QVERIFY(!writer.isValid());
}
}
void readsOnlyPinnedRegularFiles() {
QTemporaryDir root; QVERIFY(root.isValid());
QVERIFY(put(root.filePath("a.txt"), "hello"));
auto file = openResource(root.path(), "a.txt"); QVERIFY(file);
QCOMPARE(file->readAll(), QByteArray("hello"));
const auto name = native(root.filePath("a.txt"));
winresource::Handle mutate(CreateFileW(name.c_str(), GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr));
QVERIFY(!mutate.valid());
QVERIFY(!openResource(root.path(), "a.txt:stream"));
QVERIFY(!openResource(root.path(), "../a.txt"));
}
void atomicPublishAndCancellation() {
QTemporaryDir root; QVERIFY(root.isValid());
{
ResourceWriter writer(root.path(), "nested/book.css"); QVERIFY(writer.isValid());
QVERIFY(writer.write("first")); QVERIFY(!openResource(root.path(), "nested/book.css"));
QVERIFY(writer.write(" second")); QVERIFY(writer.commit()); QVERIFY(!writer.write("late"));
}
auto file = openResource(root.path(), "nested/book.css"); QVERIFY(file);
QCOMPARE(file->readAll(), QByteArray("first second")); file.reset();
{
ResourceWriter writer(root.path(), "nested/cancel.css"); QVERIFY(writer.write("discard"));
QVERIFY(!writer.write(QByteArray(65537, 'x')));
}
QVERIFY(!openResource(root.path(), "nested/cancel.css"));
QCOMPARE(QDir(root.filePath("nested")).entryList(QDir::Files | QDir::Hidden), QStringList{"book.css"});
{
ResourceWriter existing(root.path(), "nested/book.css"); QVERIFY(existing.isValid());
QVERIFY(existing.write("replacement")); QVERIFY(!existing.commit());
}
file = openResource(root.path(), "nested/book.css"); QVERIFY(file);
QCOMPARE(file->readAll(), QByteArray("first second"));
}
void rejectsHardLinksAndJunctions() {
QTemporaryDir root, outside; QVERIFY(root.isValid()); QVERIFY(outside.isValid());
QVERIFY(put(outside.filePath("secret"), "unchanged"));
const auto secret = native(outside.filePath("secret")), link = native(root.filePath("hard"));
QVERIFY2(CreateHardLinkW(link.c_str(), secret.c_str(), nullptr), "An NTFS test volume is required");
QVERIFY(!openResource(root.path(), "hard"));
{ ResourceWriter writer(root.path(), "hard"); QVERIFY(writer.write("bad")); QVERIFY(!writer.commit()); }
QVERIFY2(junction(root.filePath("junction"), outside.path()), "Creating a junction on the NTFS test volume failed");
QVERIFY(!openResource(root.path(), "junction/secret"));
QVERIFY(!openResource(root.filePath("junction"), "secret"));
{ ResourceWriter writer(root.path(), "junction/secret"); QVERIFY(!writer.isValid()); }
QFile unchanged(outside.filePath("secret")); QVERIFY(unchanged.open(QIODevice::ReadOnly));
QCOMPARE(unchanged.readAll(), QByteArray("unchanged")); unchanged.close();
// Remove the junction itself, never recursively visit its destination.
QVERIFY(RemoveDirectoryW(native(root.filePath("junction")).c_str()));
}
void writerPinsEveryAncestorAgainstSwaps() {
QTemporaryDir root; QVERIFY(root.isValid());
QVERIFY(QDir().mkpath(root.filePath("a/b")));
ResourceWriter writer(root.path(), "a/b/result"); QVERIFY(writer.isValid());
QVERIFY(!MoveFileExW(native(root.filePath("a")).c_str(), native(root.filePath("moved")).c_str(), 0));
winresource::Handle mutate(CreateFileW(native(root.filePath("a/b")).c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
QVERIFY(!mutate.valid());
QVERIFY(writer.write("pinned")); QVERIFY(writer.commit());
auto file = openResource(root.path(), "a/b/result"); QVERIFY(file); QCOMPARE(file->readAll(), QByteArray("pinned"));
}
};
QTEST_GUILESS_MAIN(WindowsResourceTests)
#include "test_windows_resources.moc"