Files
ReSkate/Engine/Core/Debug/multipart.cpp
T
2026-10-02 14:40:41 -05:00

89 lines
5.8 KiB
C++

#include "multipart.h"
#include "upload.h"
#include <algorithm>
#include <format>
#include <limits>
namespace dingosdk::backtrace {
namespace {
bool stream_file(HANDLE file, DWORD bytes, const std::function<bool(const void*, DWORD)>& sink) {
std::array<char, 65536> buffer{};
while (bytes) {
DWORD read{};
const auto wanted = std::min<DWORD>(bytes, static_cast<DWORD>(buffer.size()));
if (!ReadFile(file, buffer.data(), wanted, &read, nullptr) || read != wanted || !sink(buffer.data(), read)) return false;
bytes -= read;
}
return true;
}
bool file_length(HANDLE file, std::uintmax_t maximum, DWORD& bytes) {
LARGE_INTEGER length{};
if (file == INVALID_HANDLE_VALUE || !GetFileSizeEx(file, &length) || length.QuadPart < 0 ||
static_cast<std::uintmax_t>(length.QuadPart) > maximum) return false;
bytes = static_cast<DWORD>(length.QuadPart);
return true;
}
}
std::filesystem::path log_attachment_path(const std::filesystem::path& dump) { return dump.wstring() + L".log"; }
LogSnapshot snapshot_log(const std::filesystem::path& source, const std::filesystem::path& dump) noexcept {
try {
Handle input(CreateFileW(source.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
LARGE_INTEGER length{};
if (input.value == INVALID_HANDLE_VALUE || !GetFileSizeEx(input.value, &length) || length.QuadPart < 0)
return LogSnapshot::unavailable;
const bool truncated = length.QuadPart > max_log_bytes;
const auto bytes = static_cast<DWORD>(std::min<LONGLONG>(length.QuadPart, max_log_bytes));
LARGE_INTEGER start{}; start.QuadPart = length.QuadPart - bytes;
if (!SetFilePointerEx(input.value, start, nullptr, FILE_BEGIN)) return LogSnapshot::unavailable;
const auto destination = log_attachment_path(dump);
Handle output(CreateFileW(destination.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
if (output.value == INVALID_HANDLE_VALUE) return LogSnapshot::unavailable;
const auto saved = stream_file(input.value, bytes, [&](const void* data, DWORD count) {
DWORD written{};
return WriteFile(output.value, data, count, &written, nullptr) && written == count;
}) && FlushFileBuffers(output.value);
CloseHandle(output.release());
if (!saved) { DeleteFileW(destination.c_str()); return LogSnapshot::unavailable; }
return truncated ? LogSnapshot::tail : LogSnapshot::complete;
} catch (...) { return LogSnapshot::unavailable; }
}
MultipartReport::MultipartReport(const std::filesystem::path& dump, const Json& attributes, const std::string& boundary) {
if (!attributes.is_object() || boundary.empty() || boundary.size() > 70 ||
boundary.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") != std::string::npos) {
error_ = ERROR_INVALID_PARAMETER; return;
}
dump_.value = CreateFileW(dump.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!file_length(dump_.value, max_dump_bytes, dump_bytes_) || !dump_bytes_) { error_ = ERROR_FILE_INVALID; return; }
for (const auto& [key, value] : attributes.items()) {
if (key.empty() || key.size() > 128 || key.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") != std::string::npos ||
!value.is_string() || value.string().size() > 4096 || value.string().find(boundary) != std::string::npos) {
error_ = ERROR_INVALID_DATA; return;
}
prefix_ += std::format("--{}\r\nContent-Disposition: form-data; name=\"{}\"\r\n\r\n{}\r\n", boundary, key, value.string());
if (prefix_.size() > 65536) { error_ = ERROR_INVALID_DATA; return; }
}
prefix_ += std::format("--{}\r\nContent-Disposition: form-data; name=\"upload_file_minidump\"; filename=\"crash.dmp\"\r\nContent-Type: application/octet-stream\r\n\r\n", boundary);
const auto metadata = attributes.dump();
if (metadata.size() > 65536) { error_ = ERROR_INVALID_DATA; return; }
attachments_ = std::format("\r\n--{}\r\nContent-Disposition: form-data; name=\"attachment_report.json\"; filename=\"report.json\"\r\nContent-Type: application/json\r\n\r\n{}", boundary, metadata);
const auto log_status = attributes.value("attachment.log", "unavailable");
if (log_status == "complete" || log_status == "tail") {
log_.value = CreateFileW(log_attachment_path(dump).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
// If a queued attachment becomes unreadable, retain the report for retry
// instead of confirming an upload that silently omitted its log.
if (!file_length(log_.value, max_log_bytes, log_bytes_)) { error_ = ERROR_FILE_INVALID; return; }
attachments_ += std::format("\r\n--{}\r\nContent-Disposition: form-data; name=\"attachment_ReSkate.log\"; filename=\"ReSkate.log\"\r\nContent-Type: text/plain\r\n\r\n", boundary);
}
suffix_ = std::format("\r\n--{}--\r\n", boundary);
const auto total = prefix_.size() + static_cast<std::uint64_t>(dump_bytes_) + attachments_.size() + log_bytes_ + suffix_.size();
if (total > std::numeric_limits<DWORD>::max()) { error_ = ERROR_FILE_TOO_LARGE; return; }
size_ = static_cast<DWORD>(total);
}
bool MultipartReport::write(const std::function<bool(const void*, DWORD)>& sink) {
return !error_ && sink(prefix_.data(), static_cast<DWORD>(prefix_.size())) && stream_file(dump_.value, dump_bytes_, sink) &&
sink(attachments_.data(), static_cast<DWORD>(attachments_.size())) && stream_file(log_.value, log_bytes_, sink) &&
sink(suffix_.data(), static_cast<DWORD>(suffix_.size()));
}
}