163 lines
5.9 KiB
C++
163 lines
5.9 KiB
C++
#include "files_text_document.h"
|
|
|
|
#include <objbase.h>
|
|
#include <winioctl.h>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
|
|
namespace fs = std::filesystem;
|
|
using ks::files::TextDocument;
|
|
|
|
namespace
|
|
{
|
|
void check(bool condition, const char *message)
|
|
{
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
struct Fixture
|
|
{
|
|
fs::path directory;
|
|
Fixture()
|
|
{
|
|
GUID id{};
|
|
wchar_t name[40]{};
|
|
check(SUCCEEDED(CoCreateGuid(&id)) && StringFromGUID2(id, name, 40), "Create fixture ID");
|
|
directory = fs::temp_directory_path() / (std::wstring(L"ks-files-text-") + name);
|
|
fs::create_directory(directory);
|
|
}
|
|
~Fixture()
|
|
{
|
|
std::error_code error;
|
|
fs::remove_all(directory, error);
|
|
}
|
|
};
|
|
|
|
void writeFile(const fs::path &path, const std::string &bytes)
|
|
{
|
|
std::ofstream output(path, std::ios::binary);
|
|
output.write(bytes.data(), bytes.size());
|
|
check(static_cast<bool>(output), "Write fixture");
|
|
}
|
|
|
|
std::string readFile(const fs::path &path)
|
|
{
|
|
std::ifstream input(path, std::ios::binary);
|
|
check(static_cast<bool>(input), "Read fixture");
|
|
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
|
}
|
|
|
|
void editsAcrossPages(const fs::path &file)
|
|
{
|
|
const std::string first = std::string(TextDocument::pageBytes - 2, 'a') + "\r\n";
|
|
const std::string middle = std::string(TextDocument::pageBytes - 2, 'b') + "\r\n";
|
|
const std::string last = "last\r\n";
|
|
const std::string bom = "\xef\xbb\xbf";
|
|
writeFile(file, bom + first + middle + last);
|
|
TextDocument document(file);
|
|
check(document.pages() == 3, "Expected three pages");
|
|
check(document.readPage(1) == middle, "Read middle page");
|
|
document.stage(2, "last changed\r\n");
|
|
check(!document.modifiedWith(2, last), "Restoring the only modified page leaves no changes");
|
|
check(document.modifiedWith(0, first), "An original page must not hide changes to other pages");
|
|
document.stage(0, "first changed\n");
|
|
check(document.readPage(2) == "last changed\r\n", "Read staged page");
|
|
check(document.save(), "Replace and reopen");
|
|
check(!document.modified(), "Save clears staged page edits");
|
|
check(readFile(file) == bom + "first changed\n" + middle + "last changed\r\n",
|
|
"Save must preserve untouched bytes and the BOM");
|
|
document.stage(0, "saved again\n");
|
|
check(document.save(), "Save again through the same document");
|
|
}
|
|
|
|
void unicodeBoundaries(const fs::path &file)
|
|
{
|
|
const auto chunk = TextDocument::pageBytes;
|
|
const std::string emoji = "\xf0\x9f\x98\x80";
|
|
writeFile(file, std::string(chunk - 1, 'a') + emoji + "tail");
|
|
{
|
|
TextDocument document(file, false);
|
|
check(document.boundary(1) == chunk + 3, "UTF-8 split within a code point");
|
|
check(document.readPage(0).ends_with(emoji) && document.readPage(1) == "tail", "Read split UTF-8");
|
|
}
|
|
for (const bool little : {true, false})
|
|
{
|
|
const std::string bom = little ? "\xff\xfe" : "\xfe\xff";
|
|
const std::string letter(little ? "a\0" : "\0a", 2);
|
|
const std::string pair(little ? "\x3d\xd8\x00\xde" : "\xd8\x3d\xde\x00", 4);
|
|
std::string first;
|
|
for (size_t n = 0; n < chunk / 2 - 1; ++n) first += letter;
|
|
first += pair;
|
|
writeFile(file, bom + first + letter);
|
|
TextDocument document(file);
|
|
check(document.readPage(0).ends_with(emoji) && document.readPage(1) == "a", "Read split UTF-16");
|
|
document.stage(1, emoji);
|
|
check(document.save() && readFile(file) == bom + first + pair, "Preserve UTF-16 byte order");
|
|
}
|
|
}
|
|
|
|
void rejectExternalReplacement(const fs::path &file)
|
|
{
|
|
writeFile(file, "original");
|
|
TextDocument document(file);
|
|
document.stage(0, "editor changes");
|
|
auto later = file;
|
|
later += L".later";
|
|
auto original = file;
|
|
original += L".original";
|
|
writeFile(later, "external changes");
|
|
check(MoveFileExW(file.c_str(), original.c_str(), 0) && MoveFileExW(later.c_str(), file.c_str(), 0),
|
|
"Replace file externally");
|
|
bool rejected{};
|
|
try { document.save(); }
|
|
catch (const std::exception &) { rejected = true; }
|
|
check(rejected && document.modified() && readFile(file) == "external changes",
|
|
"Preserve external file and staged changes after conflict");
|
|
}
|
|
|
|
void sparseFile(const fs::path &file)
|
|
{
|
|
constexpr uint64_t offset = 5ULL << 30;
|
|
const auto handle = CreateFileW(file.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
check(handle != INVALID_HANDLE_VALUE, "Create sparse fixture");
|
|
DWORD count{};
|
|
if (!DeviceIoControl(handle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &count, nullptr))
|
|
{
|
|
CloseHandle(handle);
|
|
std::cout << "Sparse fixture skipped on this file system.\n";
|
|
return;
|
|
}
|
|
LARGE_INTEGER position{};
|
|
position.QuadPart = offset;
|
|
const bool written = SetFilePointerEx(handle, position, nullptr, FILE_BEGIN) &&
|
|
WriteFile(handle, "last", 4, &count, nullptr) && count == 4;
|
|
CloseHandle(handle);
|
|
check(written, "Write beyond the 32-bit range");
|
|
TextDocument document(file);
|
|
const auto index = document.pages() - 1;
|
|
check(document.size() == offset + 4 && document.boundary(index) == offset, "Use 64-bit offsets");
|
|
check(document.readPage(index) == "last", "Read past 4 GiB");
|
|
document.stage(index, "edited");
|
|
check(document.readPage(index) == "edited", "Stage an edit past 4 GiB");
|
|
}
|
|
}
|
|
|
|
int main()
|
|
{
|
|
try
|
|
{
|
|
const Fixture fixture;
|
|
editsAcrossPages(fixture.directory / L"pages.txt");
|
|
unicodeBoundaries(fixture.directory / L"unicode.txt");
|
|
rejectExternalReplacement(fixture.directory / L"external.txt");
|
|
sparseFile(fixture.directory / L"5gib.log");
|
|
std::cout << "Files text document regressions passed.\n";
|
|
}
|
|
catch (const std::exception &error)
|
|
{
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|