clank-together/test/helpers.js
Clank Together 836610da8f Clank Together: coordinator, sandboxed verifier and agent client
Coordinator (accounts with approval, API tokens, tasks with leases, sandboxed MSVC verification against
the original exe, peer review by three agents for non-identical code, human review, serial merge queue),
the `td` contributor CLI with an MCP server for agents, pinned Ghidra integration, deployment (Caddy,
systemd, nftables, Forgejo) and the wowdecomp 15890 migration tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
2026-10-18 01:29:16 +02:00

231 lines
7.4 KiB
JavaScript

// Builders for synthetic COFF objects and PE images used by the tests.
export function buildCoff({ sections, symbols }) {
const strings = [];
let strSize = 4;
const addString = (s) => {
const off = strSize;
strings.push(Buffer.from(s + '\0', 'latin1'));
strSize += s.length + 1;
return off;
};
const nameField = (name, isSymbol) => {
const b = Buffer.alloc(8);
if (name.length <= 8) b.write(name, 'latin1');
else if (isSymbol) b.writeUInt32LE(addString(name), 4);
else b.write('/' + addString(name), 'latin1');
return b;
};
const headerSize = 20 + sections.length * 40;
let p = headerSize;
const layout = sections.map((s) => {
const dataPtr = p;
p += s.data.length;
const relocPtr = p;
p += (s.relocs?.length ?? 0) * 10;
return { dataPtr, relocPtr };
});
const symPtr = p;
const header = Buffer.alloc(20);
header.writeUInt16LE(0x14c, 0);
header.writeUInt16LE(sections.length, 2);
header.writeUInt32LE(symPtr, 8);
header.writeUInt32LE(symbols.length, 12);
const secHeaders = sections.map((s, i) => {
const h = Buffer.alloc(40);
nameField(s.name, false).copy(h, 0);
h.writeUInt32LE(s.data.length, 16);
h.writeUInt32LE(layout[i].dataPtr, 20);
h.writeUInt32LE(s.relocs?.length ? layout[i].relocPtr : 0, 24);
h.writeUInt16LE(s.relocs?.length ?? 0, 32);
h.writeUInt32LE(s.characteristics ?? 0x60501020, 36);
return h;
});
const body = [];
sections.forEach((s) => {
body.push(s.data);
for (const r of s.relocs ?? []) {
const b = Buffer.alloc(10);
b.writeUInt32LE(r.offset, 0);
b.writeUInt32LE(r.symbolIndex, 4);
b.writeUInt16LE(r.type, 8);
body.push(b);
}
});
const symTable = symbols.map((s) => {
const b = Buffer.alloc(18);
nameField(s.name, true).copy(b, 0);
b.writeUInt32LE(s.value ?? 0, 8);
b.writeInt16LE(s.section, 12);
b.writeUInt16LE(s.type ?? 0, 14);
b.writeUInt8(s.storageClass ?? 2, 16);
return b;
});
const strTable = Buffer.concat([Buffer.alloc(4), ...strings]);
strTable.writeUInt32LE(strSize, 0);
return Buffer.concat([header, ...secHeaders, ...body, ...symTable, strTable]);
}
/**
* @param {{imageBase?: number, sections: Array<{name, rva, data, characteristics?}>, baseRelocs?: number[]}} p
* baseRelocs are virtual addresses of HIGHLOW fixups.
*/
export function buildPe({ imageBase = 0x400000, sections, baseRelocs = [] }) {
const all = [...sections];
if (baseRelocs.length) {
const pages = new Map();
for (const va of baseRelocs) {
const rva = va - imageBase;
const page = rva & ~0xfff;
if (!pages.has(page)) pages.set(page, []);
pages.get(page).push(rva & 0xfff);
}
const blocks = [];
for (const [page, offs] of pages) {
if (offs.length % 2) offs.push(-1);
const b = Buffer.alloc(8 + offs.length * 2);
b.writeUInt32LE(page, 0);
b.writeUInt32LE(b.length, 4);
offs.forEach((o, i) => b.writeUInt16LE(o < 0 ? 0 : (3 << 12) | o, 8 + i * 2));
blocks.push(b);
}
const last = all[all.length - 1];
const rva = Math.ceil((last.rva + last.data.length) / 0x1000) * 0x1000 + 0x1000;
all.push({ name: '.reloc', rva, data: Buffer.concat(blocks), characteristics: 0x42000040, isReloc: true });
}
const peOff = 0x40;
const optSize = 224;
const secTable = peOff + 4 + 20 + optSize;
let raw = Math.ceil((secTable + all.length * 40) / 0x200) * 0x200;
const placed = all.map((s) => {
const rawSize = Math.ceil(s.data.length / 0x200) * 0x200;
const r = { ...s, rawPtr: raw, rawSize };
raw += rawSize;
return r;
});
const buf = Buffer.alloc(raw);
buf.writeUInt16LE(0x5a4d, 0);
buf.writeUInt32LE(peOff, 0x3c);
buf.writeUInt32LE(0x4550, peOff);
const coff = peOff + 4;
buf.writeUInt16LE(0x14c, coff);
buf.writeUInt16LE(placed.length, coff + 2);
buf.writeUInt16LE(optSize, coff + 16);
const opt = coff + 20;
buf.writeUInt16LE(0x10b, opt);
buf.writeUInt32LE(0x1000, opt + 16);
buf.writeUInt32LE(imageBase, opt + 28);
buf.writeUInt32LE(16, opt + 92);
const reloc = placed.find((s) => s.isReloc);
if (reloc) {
buf.writeUInt32LE(reloc.rva, opt + 96 + 5 * 8);
buf.writeUInt32LE(reloc.data.length, opt + 100 + 5 * 8);
}
placed.forEach((s, i) => {
const o = secTable + i * 40;
buf.write(s.name, o, 'latin1');
buf.writeUInt32LE(s.data.length, o + 8);
buf.writeUInt32LE(s.rva, o + 12);
buf.writeUInt32LE(s.rawSize, o + 16);
buf.writeUInt32LE(s.rawPtr, o + 20);
buf.writeUInt32LE(s.characteristics ?? 0x60000020, o + 36);
s.data.copy(buf, s.rawPtr);
});
return buf;
}
/**
* A tiny "game": Player_Update at 0x401000 calls Callee at 0x401100 and reads
* g_val at 0x405000.
*
* 55 push ebp
* 8b ec mov ebp, esp
* e8 rel32 call Callee
* a1 imm32 mov eax, [g_val]
* 5d pop ebp
* c3 ret
*/
export const FN_ADDR = 0x401000;
export const CALLEE_ADDR = 0x401100;
export const GLOBAL_ADDR = 0x405000;
export function fixture({ objPatch } = {}) {
const text = Buffer.alloc(0x200, 0xcc);
const code = Buffer.from([0x55, 0x8b, 0xec, 0xe8, 0, 0, 0, 0, 0xa1, 0, 0, 0, 0, 0x5d, 0xc3]);
code.copy(text, 0);
text.writeInt32LE(CALLEE_ADDR - (FN_ADDR + 8), 4);
text.writeUInt32LE(GLOBAL_ADDR, 9);
Buffer.from([0xc3]).copy(text, CALLEE_ADDR - FN_ADDR);
const data = Buffer.alloc(0x10);
const pe = buildPe({
sections: [
{ name: '.text', rva: 0x1000, data: text },
{ name: '.data', rva: 0x5000, data, characteristics: 0xc0000040 },
],
baseRelocs: [FN_ADDR + 9],
});
const objCode = Buffer.from(code);
objPatch?.(objCode);
const obj = buildCoff({
sections: [
{
name: '.text$mn',
data: objCode,
characteristics: 0x60501020, // code, COMDAT
relocs: [
{ offset: 4, symbolIndex: 2, type: 0x14 },
{ offset: 9, symbolIndex: 3, type: 0x06 },
],
},
],
symbols: [
{ name: '.text$mn', section: 1, storageClass: 3 },
{ name: '?Update@Player@@QAEXXZ', section: 1, type: 0x20, storageClass: 2 },
{ name: '?Callee@@YAXXZ', section: 0, type: 0x20, storageClass: 2 },
{ name: '?g_val@@3HA', section: 0, storageClass: 2 },
],
});
const listing = [
'; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01',
'PUBLIC\t?Update@Player@@QAEXXZ\t\t\t\t; Player::Update',
'EXTRN\t?Callee@@YAXXZ:PROC\t\t\t\t; Callee',
'EXTRN\t?g_val@@3HA:DWORD\t\t\t\t; g_val',
'; Function compile flags: /Ogtp',
'; File z:\\job\\src\\player.cpp',
';\tCOMDAT ?Update@Player@@QAEXXZ',
'_TEXT\tSEGMENT',
'?Update@Player@@QAEXXZ PROC\t\t\t\t; Player::Update, COMDAT',
'',
'; 5 : {',
'',
' 00000\t55\t\t push\t ebp',
' 00001\t8b ec\t\t mov\t ebp, esp',
'',
'; 6 : Callee();',
'',
' 00003\te8 00 00 00 00\t call\t ?Callee@@YAXXZ\t\t; Callee',
'',
'; 7 : return g_val;',
'',
' 00008\ta1 00 00 00 00\t mov\t eax, DWORD PTR ?g_val@@3HA ; g_val',
'',
'; 8 : }',
'',
' 0000d\t5d\t\t pop\t ebp',
' 0000e\tc3\t\t ret\t 0',
'?Update@Player@@QAEXXZ ENDP\t\t\t\t; Player::Update',
'_TEXT\tENDS',
'END',
].join('\r\n');
const source = ['#include "player.h"', '', '// FUNCTION: fn_00001', 'int Player::Update()', '{', ' Callee();', ' return g_val;', '}', ''].join('\n');
return { pe, obj, listing, source };
}