333 lines
21 KiB
C++
333 lines
21 KiB
C++
#pragma once
|
|
#include <bcrypt.h>
|
|
#include <map>
|
|
#include <winternl.h>
|
|
|
|
// Host-owned transaction channel. No workspace handles cross into an AppContainer.
|
|
namespace workspace_write {
|
|
static size_t fileLimit = 8 * 1024 * 1024;
|
|
using CreateFileFn = NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, ULONG, ULONG, ULONG, PVOID, ULONG);
|
|
using SetFileFn = NTSTATUS(NTAPI*)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS);
|
|
using ErrorFn = ULONG(NTAPI*)(NTSTATUS);
|
|
template<class Function> static Function nativeFunction(const char* name) {
|
|
const auto address = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), name);
|
|
require(address != nullptr, "Resolve native transaction API");
|
|
Function result = nullptr;
|
|
static_assert(sizeof(result) == sizeof(address)); memcpy(&result, &address, sizeof(result));
|
|
return result;
|
|
}
|
|
static bool nativeOk(NTSTATUS status) {
|
|
if (status >= 0) return true;
|
|
SetLastError(nativeFunction<ErrorFn>("RtlNtStatusToDosError")(status));
|
|
return false;
|
|
}
|
|
static HANDLE openRelative(HANDLE parent, const std::wstring& name, ACCESS_MASK access, bool directory, bool create = false) {
|
|
UNICODE_STRING text = {}; text.Buffer = const_cast<PWSTR>(name.data());
|
|
text.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t)); text.MaximumLength = text.Length;
|
|
OBJECT_ATTRIBUTES attributes = {}; attributes.Length = sizeof(attributes); attributes.RootDirectory = parent;
|
|
attributes.ObjectName = &text; attributes.Attributes = 0x1040; // OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE
|
|
IO_STATUS_BLOCK status = {}; HANDLE handle = INVALID_HANDLE_VALUE;
|
|
const ULONG options = 0x00200000 | 0x20 | (directory ? 1 : 0); // OPEN_REPARSE_POINT, synchronous, optional directory
|
|
const auto result = nativeFunction<CreateFileFn>("NtCreateFile")(&handle, access | SYNCHRONIZE, &attributes, &status, nullptr,
|
|
FILE_ATTRIBUTE_NORMAL, directory ? FILE_SHARE_READ | FILE_SHARE_WRITE : FILE_SHARE_READ, create ? 2 : 1, options, nullptr, 0);
|
|
return nativeOk(result) ? handle : INVALID_HANDLE_VALUE;
|
|
}
|
|
struct Root {
|
|
std::vector<std::unique_ptr<Handle>> held;
|
|
fs::path path;
|
|
HANDLE handle = nullptr;
|
|
std::wstring rootFinal;
|
|
std::string rootIdentity;
|
|
explicit Root(const std::wstring& input) {
|
|
if (input.size() < 3 || input.size() > 1024 || !iswalpha(input[0]) || input[1] != L':' || input[2] != L'\\') throw std::runtime_error("Workspace requires a local drive path");
|
|
path = input.substr(0, 3);
|
|
if (GetDriveTypeW(path.c_str()) != DRIVE_FIXED) throw std::runtime_error("Workspace requires a fixed local volume");
|
|
auto first = std::make_unique<Handle>(CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
|
|
require(first->value != INVALID_HANDLE_VALUE, "Pin transaction volume");
|
|
wchar_t filesystem[32] = {};
|
|
require(GetVolumeInformationByHandleW(first->value, nullptr, 0, nullptr, nullptr, nullptr, filesystem, 32) != FALSE, "Inspect transaction volume");
|
|
if (_wcsicmp(filesystem, L"NTFS") != 0) throw std::runtime_error("Workspace transaction requires NTFS");
|
|
handle = first->value; held.push_back(std::move(first));
|
|
if (input.size() > 3) for (const auto& name : workspace_read::parts(input.substr(3))) {
|
|
auto next = std::make_unique<Handle>(openRelative(handle, name, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
|
|
require(next->value != INVALID_HANDLE_VALUE, "Pin transaction root ancestor");
|
|
workspace_read::information(next->value);
|
|
handle = next->value; held.push_back(std::move(next)); path /= name;
|
|
}
|
|
rootFinal = workspace_read::finalPath(handle); rootIdentity = workspace_read::identity(handle);
|
|
}
|
|
std::wstring relative(HANDLE target) const {
|
|
const auto actual = workspace_read::finalPath(target);
|
|
if (actual.size() <= rootFinal.size() || _wcsnicmp(actual.c_str(), rootFinal.c_str(), rootFinal.size()) != 0 || actual[rootFinal.size()] != L'\\') throw std::runtime_error("Transaction handle escaped root");
|
|
return actual.substr(rootFinal.size() + 1);
|
|
}
|
|
};
|
|
static std::string hex(const BYTE* bytes, size_t size) {
|
|
static const char* digits = "0123456789abcdef";
|
|
std::string result; result.reserve(size * 2);
|
|
for (size_t index = 0; index < size; ++index) { result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15]; }
|
|
return result;
|
|
}
|
|
static std::vector<BYTE> unhex(const std::string& text, size_t limit) {
|
|
if (text.size() % 2 || text.size() > limit * 2) throw std::runtime_error("Invalid transaction field length");
|
|
std::vector<BYTE> result; result.reserve(text.size() / 2);
|
|
auto digit = [](char ch) -> BYTE {
|
|
if (ch >= '0' && ch <= '9') return static_cast<BYTE>(ch - '0');
|
|
if (ch >= 'a' && ch <= 'f') return static_cast<BYTE>(ch - 'a' + 10);
|
|
throw std::runtime_error("Invalid transaction hex field");
|
|
};
|
|
for (size_t index = 0; index < text.size(); index += 2) result.push_back(static_cast<BYTE>((digit(text[index]) << 4) | digit(text[index + 1])));
|
|
return result;
|
|
}
|
|
static std::wstring pathField(const std::string& text) {
|
|
const auto bytes = unhex(text, 4096);
|
|
const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), nullptr, 0);
|
|
if (size <= 0 || size > 1024) throw std::runtime_error("Invalid transaction path encoding");
|
|
std::wstring result(size, 0);
|
|
require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(bytes.data()), static_cast<int>(bytes.size()), result.data(), size) == size, "Decode transaction path");
|
|
return result;
|
|
}
|
|
static std::wstring key(std::wstring value) {
|
|
std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) { return static_cast<wchar_t>(towlower(ch)); });
|
|
return value;
|
|
}
|
|
static std::string hash(const std::vector<BYTE>& bytes) {
|
|
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
|
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0) throw std::runtime_error("Open transaction SHA256");
|
|
BYTE result[32] = {};
|
|
const auto status = BCryptHash(algorithm, nullptr, 0, const_cast<BYTE*>(bytes.data()), static_cast<ULONG>(bytes.size()), result, sizeof(result));
|
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
if (status < 0) throw std::runtime_error("Hash transaction contents");
|
|
return hex(result, sizeof(result));
|
|
}
|
|
static std::vector<BYTE> contents(HANDLE handle) {
|
|
const auto info = workspace_read::information(handle);
|
|
const auto size = (static_cast<ULONGLONG>(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
|
|
if ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || size > fileLimit) throw std::runtime_error("Transaction file type or size denied");
|
|
std::vector<BYTE> bytes(static_cast<size_t>(size)); DWORD count = 0;
|
|
if (!bytes.empty()) require(ReadFile(handle, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr) && count == bytes.size(), "Read pinned transaction file");
|
|
return bytes;
|
|
}
|
|
static void reply(const std::string& text) {
|
|
const auto line = text + '\n'; DWORD count = 0;
|
|
require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), line.data(), static_cast<DWORD>(line.size()), &count, nullptr) && count == line.size(), "Write transaction response");
|
|
}
|
|
struct Session {
|
|
struct MissingParent : std::runtime_error { MissingParent() : std::runtime_error("Transaction parent missing") {} };
|
|
Root root;
|
|
std::map<std::wstring, std::unique_ptr<Handle>> directories;
|
|
std::map<std::wstring, std::string> known;
|
|
explicit Session(const std::wstring& path, const std::string& expected) : root(path) {
|
|
if (expected != "-" && expected != root.rootIdentity) throw std::runtime_error("Workspace root identity changed");
|
|
}
|
|
void validate(HANDLE handle, const std::wstring& relative, bool directory) {
|
|
for (const auto& ancestor : root.held) workspace_read::information(ancestor->value);
|
|
const auto info = workspace_read::information(handle);
|
|
if (directory != ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)) throw std::runtime_error("Transaction target type changed");
|
|
if (_wcsicmp(root.relative(handle).c_str(), relative.c_str()) != 0) throw std::runtime_error("Transaction final name changed");
|
|
}
|
|
HANDLE parent(const std::vector<std::wstring>& names) {
|
|
HANDLE parentHandle = root.handle; std::wstring relative;
|
|
workspace_read::information(parentHandle);
|
|
for (size_t index = 0; index + 1 < names.size(); ++index) {
|
|
if (!relative.empty()) relative += L'\\'; relative += names[index];
|
|
const auto name = key(relative);
|
|
if (directories.count(name)) { parentHandle = directories[name]->value; workspace_read::information(parentHandle); continue; }
|
|
if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit");
|
|
auto handle = std::make_unique<Handle>(openRelative(parentHandle, names[index], FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
|
|
if (handle->value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) throw MissingParent();
|
|
require(handle->value != INVALID_HANDLE_VALUE, "Pin transaction ancestor");
|
|
validate(handle->value, relative, true);
|
|
parentHandle = handle->value;
|
|
directories.emplace(name, std::move(handle));
|
|
}
|
|
return parentHandle;
|
|
}
|
|
void unpin(const std::wstring& relative) {
|
|
const auto prefix = key(relative);
|
|
for (auto entry = directories.begin(); entry != directories.end();) {
|
|
if (entry->first == prefix || entry->first.compare(0, prefix.size() + 1, prefix + L'\\') == 0) entry = directories.erase(entry);
|
|
else ++entry;
|
|
}
|
|
}
|
|
std::string execute(const std::vector<std::string>& fields) {
|
|
if (fields.size() != 5) throw std::runtime_error("Invalid transaction frame");
|
|
const auto& operation = fields[0]; const auto relative = pathField(fields[1]);
|
|
const auto names = workspace_read::parts(relative);
|
|
const auto name = key(relative);
|
|
HANDLE directory = nullptr;
|
|
try { directory = parent(names); }
|
|
catch (const MissingParent&) {
|
|
if (operation == "cleanup") return "{\"ok\":true}";
|
|
if (operation == "read" || operation == "stat") return "{\"ok\":true,\"kind\":\"missing\"}";
|
|
throw;
|
|
}
|
|
const auto absolute = names.back();
|
|
const auto& nonce = fields[4];
|
|
if (operation == "write" || operation == "cleanup") {
|
|
if (nonce.size() != 32 || nonce.find_first_not_of("0123456789abcdef") != std::string::npos) throw std::runtime_error("Invalid transaction mutation identity");
|
|
}
|
|
const auto temporaryName = L".ycode-sandbox-" + std::wstring(nonce.begin(), nonce.end());
|
|
const auto temporary = temporaryName + L"-new.tmp";
|
|
const auto previous = temporaryName + L"-old.tmp";
|
|
if (operation == "cleanup") {
|
|
const auto separator = fields[2].find(':');
|
|
if (separator == std::string::npos) throw std::runtime_error("Invalid transaction cleanup hashes");
|
|
const std::pair<fs::path, std::string> candidates[] = {{previous, fields[2].substr(0, separator)}, {temporary, fields[2].substr(separator + 1)}};
|
|
for (const auto& candidate : candidates) {
|
|
Handle stale(openRelative(directory, candidate.first.wstring(), GENERIC_READ | DELETE, false));
|
|
if (stale.value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) continue;
|
|
require(stale.value != INVALID_HANDLE_VALUE, "Open transaction recovery scratch");
|
|
validate(stale.value, (fs::path(relative).parent_path() / candidate.first.filename()).wstring(), false);
|
|
if (hash(contents(stale.value)) != candidate.second) throw std::runtime_error("Transaction scratch changed externally");
|
|
FILE_DISPOSITION_INFO disposition = {TRUE};
|
|
require(SetFileInformationByHandle(stale.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove transaction recovery scratch");
|
|
}
|
|
return "{\"ok\":true}";
|
|
}
|
|
if (operation == "mkdir") {
|
|
Handle created(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true, true));
|
|
require(created.value != INVALID_HANDLE_VALUE, "Create pinned transaction directory");
|
|
validate(created.value, relative, true);
|
|
const auto identity = workspace_read::identity(created.value);
|
|
directories[name] = std::make_unique<Handle>(created.value); created.value = nullptr;
|
|
known.erase(name);
|
|
return "{\"ok\":true,\"identity\":\"" + identity + "\"}";
|
|
}
|
|
if (operation == "rmdir") unpin(relative);
|
|
const bool mutation = operation == "write" || operation == "unlink" || operation == "rmdir";
|
|
Handle target(openRelative(directory, absolute, GENERIC_READ | (mutation ? DELETE : 0), operation == "rmdir"));
|
|
const DWORD error = target.value == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
|
|
const bool missing = error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
|
|
if (target.value == INVALID_HANDLE_VALUE && !missing) { SetLastError(error); require(false, "Open pinned transaction target"); }
|
|
if (operation == "read" || operation == "stat") {
|
|
if (missing) { known.erase(name); return "{\"ok\":true,\"kind\":\"missing\"}"; }
|
|
const bool isDirectory = (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
validate(target.value, relative, isDirectory);
|
|
const auto identity = workspace_read::identity(target.value); known[name] = identity;
|
|
if (isDirectory && !directories.count(name)) {
|
|
if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit");
|
|
auto pinned = std::make_unique<Handle>(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true));
|
|
require(pinned->value != INVALID_HANDLE_VALUE, "Pin inspected transaction directory");
|
|
validate(pinned->value, relative, true);
|
|
if (workspace_read::identity(pinned->value) != identity) throw std::runtime_error("Inspected transaction directory identity changed");
|
|
directories[name] = std::move(pinned);
|
|
}
|
|
if (operation == "read" && isDirectory) throw std::runtime_error("Transaction target is a directory");
|
|
std::string result = "{\"ok\":true,\"kind\":\"" + std::string(isDirectory ? "directory" : "file") + "\",\"identity\":\"" + identity + "\"";
|
|
if (operation == "read") result += ",\"data\":\"" + workspace_read::base64(contents(target.value)) + "\"";
|
|
return result + '}';
|
|
}
|
|
if (operation == "list") {
|
|
if (missing) throw std::runtime_error("Transaction directory missing");
|
|
validate(target.value, relative, true);
|
|
std::string result = "{\"ok\":true,\"entries\":["; bool first = true; size_t count = 0;
|
|
std::vector<BYTE> buffer(65536);
|
|
for (;;) {
|
|
if (!GetFileInformationByHandleEx(target.value, FileIdBothDirectoryInfo, buffer.data(), static_cast<DWORD>(buffer.size()))) {
|
|
if (GetLastError() == ERROR_NO_MORE_FILES) break;
|
|
require(false, "Enumerate transaction directory");
|
|
}
|
|
size_t offset = 0;
|
|
for (;;) {
|
|
const auto entry = reinterpret_cast<FILE_ID_BOTH_DIR_INFO*>(buffer.data() + offset);
|
|
if (entry->FileNameLength > buffer.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName)) throw std::runtime_error("Invalid transaction directory record");
|
|
const std::wstring child(entry->FileName, entry->FileNameLength / sizeof(wchar_t));
|
|
if (child != L"." && child != L"..") {
|
|
if (++count > 10000 || !workspace_read::component(child)) throw std::runtime_error("Transaction directory entry denied");
|
|
if (!first) result += ','; first = false;
|
|
result += "\"" + jsonText(child) + "\"";
|
|
}
|
|
if (entry->NextEntryOffset == 0) break;
|
|
if (entry->NextEntryOffset < sizeof(FILE_ID_BOTH_DIR_INFO) || entry->NextEntryOffset > buffer.size() - offset - sizeof(FILE_ID_BOTH_DIR_INFO)) throw std::runtime_error("Invalid transaction directory offset");
|
|
offset += entry->NextEntryOffset;
|
|
}
|
|
}
|
|
return result + "]}";
|
|
}
|
|
if (!mutation) throw std::runtime_error("Unknown transaction operation");
|
|
if (!missing) {
|
|
validate(target.value, relative, operation == "rmdir");
|
|
if (operation != "rmdir" && (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_READONLY)) throw std::runtime_error("Transaction read-only file denied");
|
|
const auto identity = workspace_read::identity(target.value);
|
|
if (known.count(name) && known[name] != identity) throw std::runtime_error("Transaction target identity changed");
|
|
if (operation == "rmdir") {
|
|
if (fields[2] != identity) throw std::runtime_error("Transaction directory identity changed");
|
|
} else if (fields[2] != hash(contents(target.value))) throw std::runtime_error("Transaction contents changed");
|
|
} else if (operation != "write" || fields[2] != "-") throw std::runtime_error("Transaction target disappeared");
|
|
if (operation == "unlink" || operation == "rmdir") {
|
|
FILE_DISPOSITION_INFO disposition = {TRUE};
|
|
require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Delete pinned transaction target");
|
|
known.erase(name);
|
|
return "{\"ok\":true}";
|
|
}
|
|
const auto bytes = unhex(fields[3], fileLimit);
|
|
Handle output(openRelative(directory, temporary, GENERIC_WRITE | DELETE | FILE_READ_ATTRIBUTES, false, true));
|
|
require(output.value != INVALID_HANDLE_VALUE, "Create transaction replacement");
|
|
bool published = false, moved = false;
|
|
auto renameHandle = [directory](HANDLE handle, const fs::path& destination) {
|
|
const auto name = destination.wstring();
|
|
std::vector<BYTE> storage(offsetof(FILE_RENAME_INFO, FileName) + name.size() * sizeof(wchar_t));
|
|
auto rename = reinterpret_cast<FILE_RENAME_INFO*>(storage.data());
|
|
rename->ReplaceIfExists = FALSE;
|
|
rename->RootDirectory = directory; rename->FileNameLength = static_cast<DWORD>(name.size() * sizeof(wchar_t));
|
|
memcpy(rename->FileName, name.data(), rename->FileNameLength);
|
|
IO_STATUS_BLOCK status = {};
|
|
return nativeOk(nativeFunction<SetFileFn>("NtSetInformationFile")(handle, &status, rename, static_cast<ULONG>(storage.size()), static_cast<FILE_INFORMATION_CLASS>(10)));
|
|
};
|
|
try {
|
|
DWORD count = 0;
|
|
if (!bytes.empty()) require(WriteFile(output.value, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr) && count == bytes.size(), "Write transaction replacement");
|
|
require(FlushFileBuffers(output.value) != FALSE, "Flush transaction replacement");
|
|
validate(output.value, (fs::path(relative).parent_path() / fs::path(temporary).filename()).wstring(), false);
|
|
const auto replacementIdentity = workspace_read::identity(output.value);
|
|
// A no-overwrite handoff keeps the original handle exclusively pinned.
|
|
// A competing basename creation fails publication instead of losing data.
|
|
if (!missing) { require(renameHandle(target.value, previous), "Stage pinned transaction original"); moved = true; }
|
|
require(renameHandle(output.value, absolute), "Publish pinned transaction replacement");
|
|
published = true;
|
|
known[name] = replacementIdentity;
|
|
if (moved) {
|
|
FILE_DISPOSITION_INFO disposition = {TRUE};
|
|
require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove pinned transaction original");
|
|
}
|
|
} catch (...) {
|
|
FILE_DISPOSITION_INFO disposition = {TRUE};
|
|
if (!published) SetFileInformationByHandle(output.value, FileDispositionInfo, &disposition, sizeof(disposition));
|
|
if (moved && !published && !renameHandle(target.value, absolute)) throw std::runtime_error("Transaction handoff needs recovery; competing target preserved");
|
|
throw;
|
|
}
|
|
return "{\"ok\":true}";
|
|
}
|
|
};
|
|
static int run(const std::wstring& root, const std::string& expected, bool gitMetadata = false) {
|
|
if (gitMetadata) fileLimit = 128 * 1024 * 1024;
|
|
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace transaction is host-only");
|
|
workspace_read::Lifetime lifetime(600000);
|
|
Session session(root, expected);
|
|
reply("{\"protocol\":1,\"rootIdentity\":\"" + session.root.rootIdentity + "\"}");
|
|
std::string line; size_t requests = 0; char buffer[16384]; DWORD count = 0;
|
|
while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &count, nullptr) && count) {
|
|
for (DWORD index = 0; index < count; ++index) {
|
|
if (buffer[index] != '\n') {
|
|
if (line.size() >= fileLimit * 2 + 16384) throw std::runtime_error("Transaction frame limit");
|
|
line += buffer[index]; continue;
|
|
}
|
|
if (++requests > 100000) throw std::runtime_error("Transaction request limit");
|
|
std::vector<std::string> fields; size_t start = 0;
|
|
for (;;) { const auto end = line.find('\t', start); fields.push_back(line.substr(start, end == std::string::npos ? end : end - start)); if (end == std::string::npos) break; start = end + 1; }
|
|
try { reply(session.execute(fields)); }
|
|
catch (const std::exception& error) {
|
|
std::string message;
|
|
for (char ch : std::string(error.what())) { if (ch == '\\' || ch == '"') message += '\\'; message += ch; }
|
|
reply("{\"ok\":false,\"error\":\"" + message + "\"}");
|
|
}
|
|
line.clear();
|
|
}
|
|
}
|
|
if (!line.empty()) throw std::runtime_error("Truncated transaction frame");
|
|
return 0;
|
|
}
|
|
}
|