mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-29 12:15:39 +00:00
feat(relinker): frame red-zone leaves without a frame pointer by moving their rsp operands
660 lines
40 KiB
C++
660 lines
40 KiB
C++
#include "TestProgress.hpp"
|
|
#include <relinker/pipeline/Conversion.hpp>
|
|
#include <relinker/patching/GamePatch.hpp>
|
|
#include <relinker/parsing/ElfReader.hpp>
|
|
#include <io/BufferUtils.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
using namespace Relinker;
|
|
using namespace Relinker::GamePatch;
|
|
|
|
void require(const bool condition, const std::string& message) {
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
template<typename TAction>
|
|
void rejects(TAction action, const std::string& fragment) {
|
|
try {
|
|
action();
|
|
} catch (const Domain::RelinkerException& error) {
|
|
require(std::string(error.what()).find(fragment) != std::string::npos, "wrong error, expected `" + fragment + "` in: " + error.what());
|
|
return;
|
|
}
|
|
throw std::runtime_error("accepted: expected `" + fragment + "`");
|
|
}
|
|
|
|
std::string header(const std::string& body) {
|
|
return "format = 1\n" + body;
|
|
}
|
|
|
|
void rejectsText(const std::string& text, const std::size_t line, const std::string& fragment = {}) {
|
|
rejects([&] { ParsePatchFile(text); }, "line " + std::to_string(line) + ":");
|
|
if (!fragment.empty()) rejects([&] { ParsePatchFile(text); }, fragment);
|
|
}
|
|
|
|
PatchEntry entry(const std::string& target, const std::uint64_t address, std::vector<std::uint8_t> expected, std::vector<std::uint8_t> value, const std::string& name = {}) {
|
|
PatchEntry result;
|
|
result.Target = target;
|
|
result.VirtualAddress = address;
|
|
result.Expected = std::move(expected);
|
|
result.Value = std::move(value);
|
|
result.Name = name;
|
|
result.Line = 1;
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::uint8_t> twoSegmentFixture() {
|
|
std::vector<std::uint8_t> bytes(0x3000);
|
|
const std::array<std::uint8_t, 7> magic{0x7f, 'E', 'L', 'F', 2, 1, 1};
|
|
std::copy(magic.begin(), magic.end(), bytes.begin());
|
|
Io::WriteU16(bytes, 16, 3);
|
|
Io::WriteU16(bytes, 18, 62);
|
|
Io::WriteU32(bytes, 20, 1);
|
|
Io::WriteU64(bytes, 32, 64);
|
|
Io::WriteU16(bytes, 52, 64);
|
|
Io::WriteU16(bytes, 54, 56);
|
|
Io::WriteU16(bytes, 56, 2);
|
|
const auto segment = [&](const std::size_t index, const std::uint64_t offset, const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize) {
|
|
const auto at = 64 + index * 56;
|
|
Io::WriteU32(bytes, at, 1);
|
|
Io::WriteU32(bytes, at + 4, 6);
|
|
Io::WriteU64(bytes, at + 8, offset);
|
|
Io::WriteU64(bytes, at + 16, address);
|
|
Io::WriteU64(bytes, at + 24, address);
|
|
Io::WriteU64(bytes, at + 32, fileSize);
|
|
Io::WriteU64(bytes, at + 40, memorySize);
|
|
Io::WriteU64(bytes, at + 48, 8);
|
|
};
|
|
segment(0, 0, 0, 0x1100, 0x1100);
|
|
segment(1, 0x2000, 0x1100, 0x100, 0x300);
|
|
for (std::size_t index = 0; index < 0x100; ++index) {
|
|
bytes[0x1000 + index] = static_cast<std::uint8_t>(index);
|
|
bytes[0x2000 + index] = static_cast<std::uint8_t>(0x80 + index);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
std::vector<std::uint8_t> conversionFixture() {
|
|
std::vector<std::uint8_t> bytes(0x3000);
|
|
const std::array<std::uint8_t, 7> magic{0x7f, 'E', 'L', 'F', 2, 1, 1};
|
|
std::copy(magic.begin(), magic.end(), bytes.begin());
|
|
Io::WriteU16(bytes, 16, 3);
|
|
Io::WriteU16(bytes, 18, 62);
|
|
Io::WriteU32(bytes, 20, 1);
|
|
Io::WriteU64(bytes, 24, 0x1000);
|
|
Io::WriteU64(bytes, 32, 64);
|
|
Io::WriteU16(bytes, 52, 64);
|
|
Io::WriteU16(bytes, 54, 56);
|
|
Io::WriteU16(bytes, 56, 8);
|
|
Io::WriteU16(bytes, 58, 64);
|
|
const auto programHeader = [&](std::size_t index, unsigned type, unsigned flags, unsigned offset, unsigned size) {
|
|
const auto at = 64 + index * 56;
|
|
Io::WriteU32(bytes, at, type);
|
|
Io::WriteU32(bytes, at + 4, flags);
|
|
Io::WriteU64(bytes, at + 8, offset);
|
|
Io::WriteU64(bytes, at + 16, offset);
|
|
Io::WriteU64(bytes, at + 24, offset);
|
|
Io::WriteU64(bytes, at + 32, size);
|
|
Io::WriteU64(bytes, at + 40, size);
|
|
Io::WriteU64(bytes, at + 48, 8);
|
|
};
|
|
programHeader(0, 1, 5, 0x1000, 8);
|
|
programHeader(1, 1, 6, 0x2000, 0x1000);
|
|
for (std::size_t index = 3; index < 8; ++index) programHeader(index, 0x6fffff01, 0, 0, 0);
|
|
bytes[0x1000] = 0xc3;
|
|
const std::array<std::pair<std::uint64_t, std::uint64_t>, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}};
|
|
programHeader(2, 2, 6, 0x2000, tags.size() * 16);
|
|
for (std::size_t index = 0; index < tags.size(); ++index) {
|
|
Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first);
|
|
Io::WriteU64(bytes, 0x2008 + index * 16, tags[index].second);
|
|
}
|
|
Io::WriteU32(bytes, 0x2680, 1);
|
|
Io::WriteU32(bytes, 0x2684, 1);
|
|
return bytes;
|
|
}
|
|
|
|
ConversionInput conversionInput(const std::vector<std::uint8_t>& executable) {
|
|
ConversionInput result;
|
|
result.Executable = executable;
|
|
result.InputPath = "virtual/title/custom-name.bin";
|
|
result.OutputPath = "virtual/output/title";
|
|
result.ModuleRoot = "virtual/title";
|
|
result.Modules.push_back({"virtual/title/sce_module/plugin.prx", conversionFixture()});
|
|
return result;
|
|
}
|
|
|
|
void grammarAccepted() {
|
|
const auto file = ParsePatchFile(
|
|
"# leading comment\n"
|
|
"format = 1 # trailing comment\n"
|
|
"relinker_version = \"x\"\n"
|
|
"relinker_commit = \"y\"\n"
|
|
"config = \"z\"\n"
|
|
"config_sha256 = \"w\"\n"
|
|
"main_target = \"eboot.elf\"\n"
|
|
"\n"
|
|
"[[patch]]\n"
|
|
"name = \"pool_size\"\n"
|
|
"target = \"eboot.elf\"\n"
|
|
"va = 0x1234\n"
|
|
"type = \"i32\"\n"
|
|
"expected = 0x10000000\n"
|
|
"value = 0x18000000\n"
|
|
"max = 0x7f000000\n"
|
|
"align = 0x1000\n"
|
|
"description = \"has # inside\"\n"
|
|
"\n"
|
|
"[[patch]]\n"
|
|
"target = \"sce_module/libc.prx\"\n"
|
|
"va = 4096\n"
|
|
"expected = \"00ff\"\n"
|
|
"value = \"AB12\"\n");
|
|
require(file.MainTarget == std::string("eboot.elf"), "main_target was not read");
|
|
require(file.Entries.size() == 2, "wrong entry count");
|
|
const auto& typed = file.Entries[0];
|
|
require(typed.Name == "pool_size" && typed.Target == "eboot.elf" && typed.VirtualAddress == 0x1234, "typed entry fields");
|
|
require(typed.Expected == std::vector<std::uint8_t>({0, 0, 0, 0x10}) && typed.Value == std::vector<std::uint8_t>({0, 0, 0, 0x18}), "typed encoding is not little-endian");
|
|
require(typed.Description == "has # inside" && typed.Line == 9, "description or line");
|
|
const auto& raw = file.Entries[1];
|
|
require(raw.Name.empty() && raw.VirtualAddress == 4096, "raw entry fields");
|
|
require(raw.Expected == std::vector<std::uint8_t>({0x00, 0xff}) && raw.Value == std::vector<std::uint8_t>({0xab, 0x12}), "raw hex bytes");
|
|
require(!ParsePatchFile("format = 1\n").MainTarget && ParsePatchFile("format = 1\n").Entries.empty(), "empty file");
|
|
}
|
|
|
|
void grammarRejected() {
|
|
rejects([] { ParsePatchFile(""); }, "format = 1");
|
|
rejectsText("format = 2\n", 1, "unsupported format");
|
|
rejectsText(header("flavour = 1\n"), 2, "unknown top-level key");
|
|
rejectsText(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\ncolour = 1\n"), 7, "unknown key");
|
|
rejectsText(header("[[patch]]\ntarget = \"a\"\ntarget = \"b\"\n"), 4, "duplicate key");
|
|
rejectsText(header("[patch]\n"), 2, "unsupported table header");
|
|
rejectsText(header("[other]\n"), 2, "unsupported table header");
|
|
rejectsText(header("[[patch]]\ndescription = \"a\\nb\"\n"), 3, "escapes");
|
|
rejectsText(header("[[patch]]\ndescription = [1, 2]\n"), 3, "arrays");
|
|
rejectsText(header("[[patch]]\ndescription = 'x'\n"), 3, "single-quoted");
|
|
rejectsText(header("[[patch]]\ndescription = { a = 1 }\n"), 3, "inline tables");
|
|
rejectsText(header("[[patch]]\ndescription = \"\"\"x\"\"\"\n"), 3, "multiline");
|
|
rejectsText(header("[[patch]]\nva = 1.5\n"), 3, "floats");
|
|
rejectsText(header("[[patch]]\nva = true\n"), 3, "booleans");
|
|
rejectsText(header("[[patch]]\nsub.key = 1\n"), 3, "dotted keys");
|
|
rejectsText(header("[[patch]]\n\"quoted\" = 1\n"), 3, "quoted keys");
|
|
rejectsText(header("[[patch]]\nva\n"), 3, "key = value");
|
|
rejectsText(header("[[patch]]\nva = 1_000\n"), 3);
|
|
rejectsText(header("[[patch]]\nva = \"unterminated\n"), 3, "unterminated");
|
|
}
|
|
|
|
std::string typedEntry(const std::string& type, const std::string& expected, const std::string& value, const std::string& extra = {}) {
|
|
return header("[[patch]]\ntarget = \"a\"\nva = 1\ntype = \"" + type + "\"\nexpected = " + expected + "\nvalue = " + value + "\n" + extra);
|
|
}
|
|
|
|
void integerRanges() {
|
|
struct Case { const char* Type; const char* Fits; const char* Overflows; std::size_t Width; };
|
|
for (const auto& c : {Case{"u8", "255", "256", 1}, Case{"u16", "65535", "65536", 2}, Case{"u32", "0xffffffff", "0x100000000", 4}, Case{"u64", "0xffffffffffffffff", "0x10000000000000000", 8},
|
|
Case{"i8", "127", "128", 1}, Case{"i16", "32767", "32768", 2}, Case{"i32", "0x7fffffff", "0x80000000", 4}, Case{"i64", "0x7fffffffffffffff", "0x8000000000000000", 8}}) {
|
|
const auto accepted = ParsePatchFile(typedEntry(c.Type, "0", c.Fits));
|
|
require(accepted.Entries.front().Value.size() == c.Width, std::string("width of ") + c.Type);
|
|
rejects([&] { ParsePatchFile(typedEntry(c.Type, "0", c.Overflows)); }, "line 7");
|
|
}
|
|
require(ParsePatchFile(typedEntry("i32", "-1", "-2147483648")).Entries.front().Value == std::vector<std::uint8_t>({0, 0, 0, 0x80}), "i32 minimum");
|
|
require(ParsePatchFile(typedEntry("i8", "-128", "-1")).Entries.front().Value == std::vector<std::uint8_t>({0xff}), "i8 -1");
|
|
require(ParsePatchFile(typedEntry("i64", "-1", "-1")).Entries.front().Expected == std::vector<std::uint8_t>(8, 0xff), "i64 -1");
|
|
rejects([] { ParsePatchFile(typedEntry("i8", "0", "-129")); }, "line 7");
|
|
rejects([] { ParsePatchFile(typedEntry("u8", "0", "-1")); }, "line 7");
|
|
rejects([] { ParsePatchFile(typedEntry("i16", "40000", "0")); }, "line 6");
|
|
rejects([] { ParsePatchFile(typedEntry("f32", "0", "0")); }, "unknown type");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "\"00\"", "1")); }, "integer");
|
|
}
|
|
|
|
void boundsAndWidth() {
|
|
ParsePatchFile(typedEntry("u32", "0", "8", "min = 8\nmax = 8\nalign = 8\n"));
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "7", "min = 8\n")); }, "below min");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "9", "max = 8\n")); }, "above max");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "12", "align = 8\n")); }, "multiple of align");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "8", "align = 0\n")); }, "align must be positive");
|
|
ParsePatchFile(typedEntry("i32", "0", "-2097152", "align = 2097152\n"));
|
|
rejects([] { ParsePatchFile(typedEntry("i32", "0", "-1048576", "align = 2097152\n")); }, "multiple of align");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "768MiB")); }, "line 7");
|
|
rejects([] { ParsePatchFile(typedEntry("u32", "0", "007")); }, "leading zeros");
|
|
ParsePatchFile(typedEntry("u32", "0", "0"));
|
|
ParsePatchFile(typedEntry("i32", "-0", "10"));
|
|
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\nmin = 1\n")); }, "requires a type");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\n")); }, "same width");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"0\"\nvalue = \"1\"\n")); }, "even-length");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"zz\"\nvalue = \"00\"\n")); }, "non-hex");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "missing key `target`");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = -1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "va must be");
|
|
}
|
|
|
|
std::string namedEntry(const std::string& name, const std::string& extra = {}) {
|
|
return header("[[patch]]\nname = \"" + name + "\"\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n" + extra);
|
|
}
|
|
|
|
void names() {
|
|
require(ParsePatchFile(namedEntry("pool_size_2")).Entries.front().Name == "pool_size_2", "valid name");
|
|
for (const char* bad : {"Upper", "has-dash", "has space", "", "dot.ted", "\xc3\xa9"}) rejects([&] { ParsePatchFile(namedEntry(bad)); }, "line 3: name must be");
|
|
rejects([] { ParsePatchFile(header("[[patch]]\nname = 5\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "line 3: name must be");
|
|
rejects([] { ParsePatchFile(namedEntry("same") + "\n[[patch]]\nname = \"same\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n"); }, "line 10: duplicate name");
|
|
ParsePatchFile(namedEntry("one") + "\n[[patch]]\nname = \"two\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n");
|
|
}
|
|
|
|
void ranges() {
|
|
const ElfReader reader(twoSegmentFixture());
|
|
const auto resolved = ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {9, 9}), entry("t", 0x1100, {0x80}, {1}), entry("t", 0x11fc, {0xfc, 0xfd, 0xfe, 0xff}, {0, 0, 0, 0})});
|
|
require(resolved[0].Offset == 0x1000 && resolved[0].Length == 2, "first segment translation");
|
|
require(resolved[1].Offset == 0x2000, "second segment translation");
|
|
require(resolved[2].Offset == 0x20fc, "second segment end");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x10fe, {0, 0, 0, 0}, {1, 1, 1, 1})}); }, "one PT_LOAD");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x1200, {0}, {1})}); }, "one PT_LOAD");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x11fe, {0, 0, 0}, {1, 1, 1})}); }, "one PT_LOAD");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x5000, {0}, {1})}); }, "one PT_LOAD");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x1000, {0, 1, 2}, {1, 1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})}); }, "overlaps");
|
|
ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})});
|
|
}
|
|
|
|
void headerRange() {
|
|
const ElfReader reader(twoSegmentFixture());
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x0, {0x7f}, {0})}); }, "program header table");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0x40, {0}, {1})}); }, "program header table");
|
|
rejects([&] { ResolveRanges(reader, {entry("t", 0xaf, {0}, {1})}); }, "program header table");
|
|
const auto ok = ResolveRanges(reader, {entry("t", 0xb0, {0}, {1})});
|
|
require(ok.front().Offset == 0xb0, "range after the header table was rejected");
|
|
}
|
|
|
|
void applyAndMismatch() {
|
|
auto bytes = twoSegmentFixture();
|
|
const auto original = bytes;
|
|
const std::vector<PatchEntry> entries{entry("t", 0x1000, {0, 1}, {9, 8}, "first"), entry("t", 0x1100, {0x80}, {1})};
|
|
const auto patched = PatchTarget(bytes, "t", entries);
|
|
require(patched.Applied.size() == 2 && patched.Applied[0].Offset == 0x1000 && patched.Applied[0].Name == "first", "applied records");
|
|
require(bytes[0x1000] == 9 && bytes[0x1001] == 8 && bytes[0x2000] == 1, "values were not written");
|
|
for (std::size_t index = 0; index < bytes.size(); ++index)
|
|
if (index != 0x1000 && index != 0x1001 && index != 0x2000) require(bytes[index] == original[index], "byte outside the patch changed");
|
|
|
|
bytes = original;
|
|
rejects([&] { PatchTarget(bytes, "t", {entry("t", 0x1000, {0, 1}, {9, 8}), entry("t", 0x1100, {0x81}, {1})}); }, "expected 81 found 80");
|
|
require(bytes == original, "a mismatch wrote bytes");
|
|
require(PatchTarget(bytes, "other", entries).Applied.empty() && bytes == original, "entry hit another target");
|
|
}
|
|
|
|
void programmaticWidths() {
|
|
auto bytes = twoSegmentFixture();
|
|
const auto original = bytes;
|
|
const ElfReader reader(original);
|
|
const std::vector<PatchEntry> invalid{
|
|
entry("t", 0x1000, {0}, {9, 8}),
|
|
entry("t", 0x1000, {0, 1}, {9}),
|
|
entry("t", 0x1000, {}, {}),
|
|
entry("t", 0x1000, {}, {9}),
|
|
entry("t", 0x1000, {0}, {}),
|
|
};
|
|
for (const auto& patch : invalid) {
|
|
const std::vector<PatchEntry> entries{entry("t", 0x1100, {0x80}, {1}), patch};
|
|
rejects([&] { ResolveRanges(reader, entries); }, "same nonzero width");
|
|
rejects([&] { PatchTarget(bytes, "t", entries); }, "same nonzero width");
|
|
require(bytes == original, "an invalid patch width wrote bytes");
|
|
rejects([&] { ApplyPatches(bytes, "t", entries, {{0x2000, 1, 0x1100}, {0x1000, patch.Expected.size(), 0x1000}}); }, "same nonzero width");
|
|
require(bytes == original, "direct application of an invalid patch width wrote bytes");
|
|
for (const bool outputMode : {false, true}) {
|
|
PatchFile file;
|
|
file.MainTarget = "t";
|
|
file.Entries = entries;
|
|
rejects([&] { VerifyPatches(file, {{"t", original}}, {}, outputMode); }, "same nonzero width");
|
|
}
|
|
}
|
|
}
|
|
|
|
void applicationRangeWidths() {
|
|
const std::vector<std::uint8_t> original{0, 1, 2, 3};
|
|
auto bytes = original;
|
|
const std::vector<PatchEntry> entries{entry("t", 0x1000, {0}, {9}), entry("t", 0x1002, {2, 3}, {8, 7})};
|
|
for (const std::uint64_t length : {0u, 1u, 3u}) {
|
|
rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, length, 0x1002}}); }, "range length does not match");
|
|
require(bytes == original, "a mismatched range length wrote bytes");
|
|
}
|
|
rejects([&] { ApplyPatches(bytes, "t", {entry("t", 0x1003, {3, 4}, {8, 7})}, {{3, 1, 0x1003}}); }, "range length does not match");
|
|
require(bytes == original, "a truncated comparison range wrote bytes");
|
|
for (const std::uint64_t offset : std::array<std::uint64_t, 4>{3, 4, 5, UINT64_MAX}) {
|
|
rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {offset, 2, 0x1002}}); }, "range exceeds the image");
|
|
require(bytes == original, "an out-of-bounds patch wrote bytes");
|
|
}
|
|
rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}}); }, "ranges do not match");
|
|
require(bytes == original, "a missing range wrote bytes");
|
|
const auto applied = ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, 2, 0x1002}});
|
|
require(bytes == std::vector<std::uint8_t>({9, 1, 8, 7}) && applied.size() == 2, "valid boundary patches failed");
|
|
require(ApplyPatches(bytes, "t", {}, {}).empty(), "empty patch list was rejected");
|
|
}
|
|
|
|
void overlapWithWriters() {
|
|
const std::vector<PatchedRange> patched{{0x1010, 2, 0x401010}};
|
|
const auto reject = [&](const WriterSites& sites, const std::string& writer) { rejects([&] { CheckOverlaps("t", patched, sites); }, writer); };
|
|
const std::vector<Codegen::Amd64OnlySubstitutionReport> inPlace{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::InPlace}};
|
|
reject({.Reports = inPlace}, "Intel in-place report");
|
|
const std::vector<Codegen::Amd64OnlySubstitutionReport> kept{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::Kept}};
|
|
CheckOverlaps("t", patched, {.Reports = kept});
|
|
const std::vector<Codegen::Amd64OnlySubstitutionReport> distant{{"VPSHUFB", 0x1000, 4, 4, Codegen::Amd64OnlyLowering::InPlace}};
|
|
CheckOverlaps("t", patched, {.Reports = distant});
|
|
const std::vector<Codegen::TrampolineSite> absorbed{{0x1008, 0x401008, 0x10, {}, {}, 0}};
|
|
reject({.Trampolines = absorbed}, "Intel trampoline site");
|
|
const std::vector<Codegen::TrampolineSite> shortSite{{0x1008, 0x401008, 4, {}, {}, 0}};
|
|
CheckOverlaps("t", patched, {.Trampolines = shortSite});
|
|
const std::vector<RelinkPatch> relink{{0x1011, {1, 2, 3}}};
|
|
reject({.RelinkPatches = relink}, "relinker patch");
|
|
const std::vector<RelinkPatch> relinkAfter{{0x1012, {1, 2, 3}}};
|
|
CheckOverlaps("t", patched, {.RelinkPatches = relinkAfter});
|
|
const std::vector<WriterRange> trap{{0x1011, 2, "syscall trap"}};
|
|
reject({.Traps = trap}, "syscall trap");
|
|
const std::vector<WriterRange> trapBefore{{0x100e, 2, "syscall trap"}};
|
|
CheckOverlaps("t", patched, {.Traps = trapBefore});
|
|
const std::vector<WriterRange> window{{0x100f, 4, "window"}};
|
|
reject({.Traps = window}, "window");
|
|
const std::vector<Codegen::TrampolineSite> frame{{0x1011, 0x401011, 5, {}, {}, 0}};
|
|
reject({.RedZoneSites = frame}, "red-zone frame site");
|
|
const std::vector<Codegen::TrampolineSite> frameBefore{{0x100b, 0x40100b, 5, {}, {}, 0}};
|
|
CheckOverlaps("t", patched, {.RedZoneSites = frameBefore});
|
|
const std::vector<Codegen::RedZoneDisplacement> moved{{0x100f, {0x10, 0, 0, 0}}};
|
|
reject({.RedZoneDisplacements = moved}, "red-zone moved displacement");
|
|
const std::vector<Codegen::RedZoneDisplacement> movedBefore{{0x100f, {0x10}}};
|
|
CheckOverlaps("t", patched, {.RedZoneDisplacements = movedBefore});
|
|
}
|
|
|
|
void conversion() {
|
|
TestProgress progress;
|
|
const Conversion converter(progress);
|
|
const auto executable = conversionFixture();
|
|
ConversionOptions plain;
|
|
plain.TargetName = "custom-name.bin";
|
|
const auto baseline = converter.Convert(conversionInput(executable), plain);
|
|
require(baseline.AppliedPatches.empty(), "patches applied without entries");
|
|
|
|
ConversionOptions patched = plain;
|
|
patched.Patches = {entry("custom-name.bin", 0x1001, {0}, {0x90}, "nop"), entry("sce_module/plugin.prx", 0x1002, {0}, {0x41})};
|
|
const auto output = converter.Convert(conversionInput(executable), patched);
|
|
require(output.AppliedPatches.size() == 2, "executable and module patches were not both applied");
|
|
require(output.AppliedPatches[0].Target == "custom-name.bin" && output.AppliedPatches[1].Target == "sce_module/plugin.prx", "applied targets");
|
|
require(output.Executable != baseline.Executable && output.Modules.front().Bytes != baseline.Modules.front().Bytes, "patches changed nothing");
|
|
require(progress.Messages.find("Patch: custom-name.bin [nop] va=0x1001 off=0x1001 00 -> 90\n") != std::string::npos, "progress line: " + progress.Messages);
|
|
require(progress.Messages.find("Patch: sce_module/plugin.prx va=0x1002 off=0x1002 00 -> 41\n") != std::string::npos, "module progress line");
|
|
|
|
ConversionOptions other = patched;
|
|
other.TargetName = "other.bin";
|
|
auto different = executable;
|
|
different[0x1005] = 0x90;
|
|
const auto second = converter.Convert(conversionInput(different), other);
|
|
require(second.Modules.size() == output.Modules.size() && second.Modules.front().Bytes == output.Modules.front().Bytes, "shared modules differ between executables");
|
|
require(second.AppliedPatches.size() == 1, "an executable entry hit another target");
|
|
|
|
ConversionOptions failing = plain;
|
|
failing.Patches = {entry("custom-name.bin", 0x1001, {7}, {1})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00");
|
|
failing.Patches = {entry("sce_module/plugin.prx", 0x1002, {7}, {1})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00");
|
|
failing.Patches = {entry("custom-name.bin", 0x1000, {0xc3, 0}, {0, 0}), entry("custom-name.bin", 0x1001, {0}, {1})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "overlaps");
|
|
for (const auto target : {TargetSystem::Linux, TargetSystem::Windows, TargetSystem::MacOs}) {
|
|
failing.Target = target;
|
|
for (const char* name : {"custom-name.bin", "sce_module/plugin.prx"}) {
|
|
failing.Patches = {entry(name, 0x1001, {0}, {9, 8})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "same nonzero width");
|
|
}
|
|
}
|
|
|
|
const auto repeated = converter.Convert(conversionInput(executable), plain);
|
|
require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output");
|
|
}
|
|
|
|
std::vector<std::uint8_t> framedLeafFixture() {
|
|
auto bytes = conversionFixture();
|
|
Io::WriteU64(bytes, 64 + 32, 0x80);
|
|
Io::WriteU64(bytes, 64 + 40, 0x80);
|
|
std::fill(bytes.begin() + 0x1000, bytes.begin() + 0x1080, 0xcc);
|
|
const std::vector<std::uint8_t> caller{0xe8, 0x3b, 0x00, 0x00, 0x00, 0xc3};
|
|
const std::vector<std::uint8_t> leaf{0x53, 0x41, 0x56, 0x41, 0x57, 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x8b, 0x04, 0x24, 0x48, 0x8b, 0x44, 0x24, 0x08, 0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xc3};
|
|
std::copy(caller.begin(), caller.end(), bytes.begin() + 0x1000);
|
|
std::copy(leaf.begin(), leaf.end(), bytes.begin() + 0x1040);
|
|
const auto ehFrameHeader = 64 + 7 * 56;
|
|
Io::WriteU32(bytes, ehFrameHeader, 0x6474e550);
|
|
Io::WriteU32(bytes, ehFrameHeader + 4, 4);
|
|
for (const auto field : {8, 16, 24}) Io::WriteU64(bytes, ehFrameHeader + field, 0x2900);
|
|
Io::WriteU64(bytes, ehFrameHeader + 32, 32);
|
|
Io::WriteU64(bytes, ehFrameHeader + 40, 32);
|
|
Io::WriteU64(bytes, ehFrameHeader + 48, 4);
|
|
Io::WriteU32(bytes, 0x2800, 9);
|
|
const std::vector<std::uint8_t> cie{1, 0, 1, 0x78, 16};
|
|
std::copy(cie.begin(), cie.end(), bytes.begin() + 0x2808);
|
|
Io::WriteU32(bytes, 0x2840, 20);
|
|
Io::WriteU32(bytes, 0x2844, 0x44);
|
|
Io::WriteU64(bytes, 0x2848, 0x1040);
|
|
Io::WriteU64(bytes, 0x2850, leaf.size());
|
|
bytes[0x2900] = 1;
|
|
bytes[0x2902] = 3;
|
|
Io::WriteU64(bytes, 0x2904, 0x2800);
|
|
Io::WriteU32(bytes, 0x290c, 1);
|
|
Io::WriteU64(bytes, 0x2910, 0x1040);
|
|
Io::WriteU64(bytes, 0x2918, 0x2840);
|
|
return bytes;
|
|
}
|
|
|
|
void framedLeafPatches() {
|
|
TestProgress progress;
|
|
const Conversion converter(progress);
|
|
const auto executable = framedLeafFixture();
|
|
ConversionOptions options;
|
|
options.TargetName = "custom-name.bin";
|
|
options.Target = TargetSystem::Windows;
|
|
options.FrameRedZone = true;
|
|
converter.Convert(conversionInput(executable), options);
|
|
require(progress.Messages.find("1 framed (1 without a frame pointer, 1 rsp operands moved, 2 in 1 operand stubs; 1 exits)") != std::string::npos, "the fixture's leaf was not framed with a moved displacement and an operand stub: " + progress.Messages);
|
|
options.Patches = {entry("custom-name.bin", 0x1010, {0xcc}, {0x90})};
|
|
require(converter.Convert(conversionInput(executable), options).AppliedPatches.size() == 1, "a patch outside the framed leaf was not applied");
|
|
options.Patches = {entry("custom-name.bin", 0x1049, {0xf0}, {0xe8})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), options); }, "overlaps red-zone moved displacement at off=0x1049 length 1");
|
|
options.Patches = {entry("custom-name.bin", 0x104c, {0x04}, {0x0c})};
|
|
rejects([&] { converter.Convert(conversionInput(executable), options); }, "overlaps red-zone frame site at off=0x104a length 9");
|
|
}
|
|
|
|
void targets() {
|
|
const std::vector<PatchEntry> entries{entry("a.elf", 1, {0}, {1}), entry("sce_module/libc.prx", 1, {0}, {1})};
|
|
MatchTargets(entries, {"a.elf", "b.elf", "sce_module/libc.prx"});
|
|
rejects([&] { MatchTargets(entries, {"a.elf", "b.elf"}); }, "unmatched patch target: sce_module/libc.prx");
|
|
rejects([&] { MatchTargets(entries, {"a.elf", "a.elf", "sce_module/libc.prx"}); }, "ambiguous patch target: a.elf");
|
|
MatchTargets({}, {});
|
|
}
|
|
|
|
void verify() {
|
|
auto bytes = twoSegmentFixture();
|
|
const std::vector<VerifyTarget> executables{{"main.elf", bytes}};
|
|
auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n"
|
|
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n"
|
|
"[[patch]]\ntarget = \"sce_module/libc.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n"));
|
|
const auto before = bytes;
|
|
const std::vector<VerifyTarget> modules{{"sce_module/libc.prx", twoSegmentFixture()}};
|
|
auto input = VerifyPatches(file, executables, modules, false);
|
|
require(input.size() == 3 && input[0].Status == VerifyStatus::Applicable && input[0].Name == "one" && input[0].Offset == 0x1000, "input applicable");
|
|
require(input[1].Status == VerifyStatus::Mismatch && input[1].Detail.find("expected ff found 80") != std::string::npos, "input mismatch");
|
|
require(input[2].Status == VerifyStatus::Applicable, "module entry in input mode");
|
|
require(bytes == before && executables.front().Bytes == before, "verify wrote bytes");
|
|
|
|
auto patchedBytes = twoSegmentFixture();
|
|
patchedBytes[0x1000] = 9;
|
|
patchedBytes[0x1001] = 8;
|
|
patchedBytes[0x2000] = 1;
|
|
const std::vector<VerifyTarget> outputs{{"main.elf", patchedBytes}};
|
|
auto output = VerifyPatches(file, outputs, {}, true);
|
|
require(output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "output applied");
|
|
require(output[2].Status == VerifyStatus::NotChecked && output[2].Detail == "module, or executable not named with --executable", "output module not-checked");
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"main.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: main.elf");
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"bin/a.elf", patchedBytes}, {"bin/a.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: bin/a.elf");
|
|
output = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, true);
|
|
require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("not applied") != std::string::npos, "unpatched output");
|
|
|
|
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "unmatched patch target: typo.prx");
|
|
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "--verify-output");
|
|
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target");
|
|
auto exec = twoSegmentFixture();
|
|
Io::WriteU16(exec, 16, 2);
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "has no linux_base");
|
|
const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n"
|
|
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n"));
|
|
require(based.LinuxBase == 0x400000, "linux_base was not read");
|
|
auto rebased = patchedBytes;
|
|
Io::WriteU16(rebased, 16, 2);
|
|
for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000);
|
|
output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true);
|
|
require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied");
|
|
require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address");
|
|
output = VerifyPatches(based, {{"main.elf", exec}}, {}, true);
|
|
require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied");
|
|
rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000");
|
|
rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base");
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead");
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead");
|
|
rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead");
|
|
|
|
auto sce = twoSegmentFixture();
|
|
Io::WriteU16(sce, 16, 0xfe10);
|
|
require(VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", sce}}, {}, false).front().Status == VerifyStatus::Applicable,
|
|
"input mode must accept any e_type");
|
|
|
|
require(OutputExecutablePath("out/app.elf", "bin/tool.elf") == std::filesystem::path("out/bin.tool.elf.elf"), "output executable name");
|
|
require(OutputExecutablePath("out/app.exe", "./a/b/c.bin") == std::filesystem::path("out/a.b.c.bin.exe"), "output executable extension");
|
|
for (const char* bad : {"../x", "/abs/x", "dir/", ""}) rejects([&] { OutputExecutablePath("out/app.elf", bad); }, "--executable must be a file");
|
|
}
|
|
|
|
void rangeFailureInVerify() {
|
|
const auto file = ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1200\nexpected = \"00\"\nvalue = \"01\"\n"));
|
|
const auto results = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, false);
|
|
require(results.front().Status == VerifyStatus::Mismatch && results.front().Detail.find("PT_LOAD") != std::string::npos, "range failure should be a mismatch");
|
|
}
|
|
|
|
void manifest() {
|
|
const AppliedPatch first{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "set \"x\" later", "alpha"};
|
|
const AppliedPatch second{"sce_module/libc.prx", 0x2000, 0x2000, {5}, {6}, "", ""};
|
|
AppliedPatch duplicate = first;
|
|
const auto merged = AggregateManifest({{first, second}, {duplicate, second}});
|
|
require(merged.size() == 2, "records were not deduplicated");
|
|
rejects([&] { WriteManifest(merged, "main.elf", ""); }, "Patch manifest cannot hold the description: set \"x\" later");
|
|
|
|
const AppliedPatch plain{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "plain description", "alpha"};
|
|
const auto text = WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "");
|
|
const auto parsed = ParsePatchFile(text);
|
|
require(parsed.MainTarget == std::string("main.elf"), "main_target did not round-trip");
|
|
require(parsed.Entries.size() == 2, "entry count did not round-trip");
|
|
require(parsed.Entries[0].Name == "alpha" && parsed.Entries[0].Target == "main.elf" && parsed.Entries[0].VirtualAddress == 0x1000, "first entry");
|
|
require(parsed.Entries[0].Expected == plain.Expected && parsed.Entries[0].Value == plain.Value && parsed.Entries[0].Description == "plain description", "first bytes");
|
|
require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry");
|
|
require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text");
|
|
require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic");
|
|
require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base");
|
|
const auto based = WriteManifest({plain}, "main.elf", "", 0x400000);
|
|
require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip");
|
|
rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer");
|
|
rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer");
|
|
}
|
|
|
|
void reservedNames() {
|
|
for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"})
|
|
rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved");
|
|
ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n"));
|
|
}
|
|
|
|
void relocationOverlap() {
|
|
std::vector<std::uint8_t> table(48);
|
|
Io::WriteU64(table, 0, 0x2010);
|
|
Io::WriteU64(table, 24, 0x2100);
|
|
const std::vector<PatchedRange> hit{{0x1010, 2, 0x2017}};
|
|
const std::vector<PatchedRange> adjacent{{0x1010, 2, 0x2018}};
|
|
const std::vector<PatchedRange> before{{0x1010, 2, 0x200f}};
|
|
rejects([&] { CheckOverlaps("t", hit, {.RelaData = table}); }, "RelaData relocation target");
|
|
rejects([&] { CheckOverlaps("t", hit, {.RelaPltData = table}); }, "RelaPltData relocation target");
|
|
rejects([&] { CheckOverlaps("t", {{0x1010, 1, 0x2100}}, {.RelaData = table}); }, "relocation target at va=0x2100");
|
|
rejects([&] { CheckOverlaps("t", before, {.RelaPltData = table}); }, "RelaPltData");
|
|
CheckOverlaps("t", adjacent, {.RelaData = table, .RelaPltData = table});
|
|
CheckOverlaps("t", {{0x1010, 2, 0x200c}}, {.RelaData = table});
|
|
}
|
|
|
|
void manifestConfig() {
|
|
const AppliedPatch patch{"main.elf", 0x1000, 0x1000, {0}, {1}, "", ""};
|
|
const auto config = [&](const std::string& path) { return WriteManifest({patch}, "main.elf", path); };
|
|
require(config("").find("config =") == std::string::npos, "empty config was recorded");
|
|
require(config("dir/p.toml").find("config = \"dir/p.toml\"\n") != std::string::npos, "forward-slash path");
|
|
rejects([&] { config("dir/p\x7f.toml"); }, "Patch manifest cannot hold the config");
|
|
rejects([&] { config("dir/p\x01.toml"); }, "Patch manifest cannot hold the config: p\x01.toml");
|
|
const auto quoted = config("we\"ird/p.toml");
|
|
require(quoted.find("config = \"p.toml\"\n") != std::string::npos, "quote path was not reduced to the file name");
|
|
require(ParsePatchFile(quoted).Entries.size() == 1, "manifest with a reduced config did not parse");
|
|
rejects([&] { config("dir/a\"b.toml"); }, "Patch manifest cannot hold the config");
|
|
const auto hold = [&](AppliedPatch bad, const std::string& field) { rejects([&] { WriteManifest({bad}, "main.elf", ""); }, "Patch manifest cannot hold the " + field + ": "); };
|
|
auto bad = patch;
|
|
bad.Name = "a\"b";
|
|
hold(bad, "name");
|
|
bad = patch;
|
|
bad.Name = "a\\b";
|
|
hold(bad, "name");
|
|
bad = patch;
|
|
bad.Target = "a\"b";
|
|
hold(bad, "target");
|
|
bad = patch;
|
|
bad.Description = "a\"b";
|
|
hold(bad, "description");
|
|
}
|
|
|
|
void typedManifest() {
|
|
const auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"neg\"\ntarget = \"main.elf\"\nva = 0x1000\ntype = \"i32\"\nexpected = -1\nvalue = 0x18000000\nmin = -5\nmax = 0x7f000000\nalign = 0x1000\n"
|
|
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\ntype = \"u8\"\nexpected = 128\nvalue = 1\n"
|
|
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1104\nexpected = \"84\"\nvalue = \"02\"\n"));
|
|
auto bytes = twoSegmentFixture();
|
|
Io::WriteU32(bytes, 0x1000, 0xffffffff);
|
|
const auto before = bytes;
|
|
const auto patched = PatchTarget(bytes, "main.elf", file.Entries);
|
|
require(patched.Applied.size() == 3 && patched.Applied[0].Type == "i32" && patched.Applied[0].Min && patched.Applied[0].Min->Negative, "typed fields are not carried");
|
|
const auto text = WriteManifest(AggregateManifest({patched.Applied}), "main.elf", "");
|
|
require(text.find("type = \"i32\"") != std::string::npos && text.find("expected = -1\n") != std::string::npos && text.find("min = -5\n") != std::string::npos
|
|
&& text.find("max = 0x7f000000\n") != std::string::npos && text.find("align = 0x1000\n") != std::string::npos && text.find("value = 0x18000000\n") != std::string::npos, "typed manifest text: " + text);
|
|
require(text.find("expected = \"84\"") != std::string::npos, "raw entry stayed raw");
|
|
const auto reparsed = ParsePatchFile(text);
|
|
require(reparsed.Entries.size() == 3 && reparsed.Entries[0].Name == "neg" && reparsed.Entries[0].Type == "i32", "typed round-trip");
|
|
for (std::size_t index = 0; index < 3; ++index)
|
|
require(reparsed.Entries[index].Expected == file.Entries[index].Expected && reparsed.Entries[index].Value == file.Entries[index].Value, "round-trip bytes");
|
|
auto again = before;
|
|
PatchTarget(again, "main.elf", reparsed.Entries);
|
|
require(again == bytes, "re-applying the manifest gives different bytes");
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
grammarAccepted();
|
|
grammarRejected();
|
|
integerRanges();
|
|
boundsAndWidth();
|
|
names();
|
|
reservedNames();
|
|
relocationOverlap();
|
|
typedManifest();
|
|
manifestConfig();
|
|
ranges();
|
|
headerRange();
|
|
applyAndMismatch();
|
|
programmaticWidths();
|
|
applicationRangeWidths();
|
|
overlapWithWriters();
|
|
conversion();
|
|
framedLeafPatches();
|
|
targets();
|
|
verify();
|
|
rangeFailureInVerify();
|
|
manifest();
|
|
} catch (const std::exception& error) {
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|