|
| 1 | +#!/usr/bin/env node |
| 2 | +import fs from "node:fs" |
| 3 | +import assert from "node:assert/strict" |
| 4 | + |
| 5 | +const geometry = (rows, columns) => { |
| 6 | + const payload = Buffer.alloc(4) |
| 7 | + payload.writeUInt16BE(rows, 0) |
| 8 | + payload.writeUInt16BE(columns, 2) |
| 9 | + return payload |
| 10 | +} |
| 11 | + |
| 12 | +const frame = (type, payload) => { |
| 13 | + const header = Buffer.alloc(5) |
| 14 | + header.writeUInt8(type) |
| 15 | + header.writeUInt32BE(payload.length, 1) |
| 16 | + return Buffer.concat([header, payload]) |
| 17 | +} |
| 18 | + |
| 19 | +const decode = (data, complete) => { |
| 20 | + const packets = [] |
| 21 | + let offset = 0 |
| 22 | + while (offset + 5 <= data.length) { |
| 23 | + const type = data.readUInt8(offset) |
| 24 | + const length = data.readUInt32BE(offset + 1) |
| 25 | + if (length > 32 * 1024 * 1024) throw new Error(`oversized frame: ${length}`) |
| 26 | + if (offset + 5 + length > data.length) break |
| 27 | + packets.push({ type, payload: data.subarray(offset + 5, offset + 5 + length) }) |
| 28 | + offset += 5 + length |
| 29 | + } |
| 30 | + if (complete && offset !== data.length) throw new Error("truncated trailing frame") |
| 31 | + return packets |
| 32 | +} |
| 33 | + |
| 34 | +const snapshotIndexes = (packets) => { |
| 35 | + const indexes = [] |
| 36 | + for (let index = 0; index + 1 < packets.length; index++) { |
| 37 | + if (packets[index].type === 10 && packets[index + 1].type === 5) indexes.push(index) |
| 38 | + } |
| 39 | + return indexes |
| 40 | +} |
| 41 | + |
| 42 | +const parseGeometries = (raw) => raw.split(",").map((entry) => { |
| 43 | + const match = /^(\d+)x(\d+)$/.exec(entry) |
| 44 | + if (!match) throw new Error(`invalid expected geometry: ${entry}`) |
| 45 | + return { rows: Number(match[1]), columns: Number(match[2]) } |
| 46 | +}) |
| 47 | + |
| 48 | +const validateSnapshots = (packets, expectedGeometries, count) => { |
| 49 | + const indexes = snapshotIndexes(packets) |
| 50 | + if (indexes.length < count) throw new Error(`expected ${count} snapshots, got ${indexes.length}`) |
| 51 | + for (let snapshot = 0; snapshot < count; snapshot++) { |
| 52 | + const payload = packets[indexes[snapshot]].payload |
| 53 | + const expected = expectedGeometries[snapshot] |
| 54 | + if (!expected || payload.length !== 4 || payload.readUInt16BE(0) !== expected.rows || |
| 55 | + payload.readUInt16BE(2) !== expected.columns) { |
| 56 | + throw new Error(`snapshot ${snapshot + 1} geometry does not match ${expected?.rows}x${expected?.columns}`) |
| 57 | + } |
| 58 | + } |
| 59 | + return indexes |
| 60 | +} |
| 61 | + |
| 62 | +const validateFinal = (data, expectedGeometries, stdout = Buffer.alloc(0), stderr = Buffer.alloc(0)) => { |
| 63 | + const packets = decode(data, true) |
| 64 | + if (packets.length < 5) throw new Error("too few frames") |
| 65 | + if (packets[0].type !== 10 || packets[1].type !== 5) { |
| 66 | + throw new Error("initial stream does not begin with GEOMETRY, SCREEN") |
| 67 | + } |
| 68 | + const indexes = validateSnapshots(packets, expectedGeometries, expectedGeometries.length) |
| 69 | + if (indexes.length !== expectedGeometries.length) { |
| 70 | + throw new Error(`expected ${expectedGeometries.length} snapshots, got ${indexes.length}`) |
| 71 | + } |
| 72 | + const initial = packets[indexes[0] + 1].payload |
| 73 | + const reconnected = packets[indexes[1] + 1].payload |
| 74 | + const coloredMarker = Buffer.from("\x1b[31mINITIAL_COLOR_61e8") |
| 75 | + if (!initial.includes(coloredMarker) || !reconnected.includes(coloredMarker)) { |
| 76 | + throw new Error("snapshot lost the red SGR state around the initial marker") |
| 77 | + } |
| 78 | + if (!reconnected.includes(Buffer.from("AFTER_DROP_61e8"))) { |
| 79 | + throw new Error("reconnect screen is not the current terminal state") |
| 80 | + } |
| 81 | + const exits = packets.filter((packet) => packet.type === 4) |
| 82 | + if (exits.length !== 1 || packets.at(-1).type !== 4) throw new Error("stream does not end in one EXIT") |
| 83 | + if (!packets.some((packet) => packet.type === 0 && packet.payload.includes(Buffer.from("FINAL_DATA_61e8")))) { |
| 84 | + throw new Error("final terminal DATA was not ordered before EXIT") |
| 85 | + } |
| 86 | + if (packets.some((packet) => ![0, 4, 5, 10].includes(packet.type))) { |
| 87 | + throw new Error("unexpected packet type in machine stream") |
| 88 | + } |
| 89 | + if (stdout.length !== 0) throw new Error("machine attach wrote to stdout") |
| 90 | + const reconnectStatus = Buffer.from("\r\n[reconnecting… — Ctrl-\\ or Ctrl-C to stop]\r\n") |
| 91 | + if (!stderr.equals(reconnectStatus)) { |
| 92 | + throw new Error("machine attach stderr was not exactly one reconnect status") |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +const selfTest = () => { |
| 97 | + const expected = parseGeometries("24x80,13x47") |
| 98 | + const colored = Buffer.from("\x1b[31mINITIAL_COLOR_61e8\x1b[0m") |
| 99 | + const current = Buffer.concat([colored, Buffer.from("\r\nAFTER_DROP_61e8")]) |
| 100 | + const packets = [ |
| 101 | + frame(10, geometry(24, 80)), frame(5, colored), |
| 102 | + frame(10, geometry(13, 47)), frame(5, current), |
| 103 | + frame(0, Buffer.from("FINAL_DATA_61e8")), frame(4, Buffer.alloc(0)), |
| 104 | + ] |
| 105 | + const valid = Buffer.concat(packets) |
| 106 | + const reconnectStatus = Buffer.from("\r\n[reconnecting… — Ctrl-\\ or Ctrl-C to stop]\r\n") |
| 107 | + validateFinal(valid, expected, Buffer.alloc(0), reconnectStatus) |
| 108 | + const uncolored = Buffer.from("\x1b[HINITIAL_COLOR_61e8\x1b[0m") |
| 109 | + const mutations = [ |
| 110 | + () => validateFinal(Buffer.concat([frame(10, geometry(1, 1)), ...packets.slice(1)]), expected), |
| 111 | + () => validateFinal(Buffer.concat([frame(10, geometry(24, 80)), frame(5, uncolored), ...packets.slice(2)]), expected), |
| 112 | + () => validateFinal(Buffer.concat([packets[0], packets[1], packets[2], frame(5, colored), ...packets.slice(4)]), expected), |
| 113 | + () => validateFinal(Buffer.concat([...packets.slice(0, 4), packets[5], packets[4]]), expected), |
| 114 | + () => validateFinal(valid.subarray(0, valid.length - 1), expected), |
| 115 | + () => validateFinal(valid, expected, Buffer.from("unexpected"), reconnectStatus), |
| 116 | + () => validateFinal(valid, expected, Buffer.alloc(0), Buffer.concat([reconnectStatus, Buffer.from("\x1b[32mLEAK")])), |
| 117 | + ] |
| 118 | + for (const mutate of mutations) assert.throws(mutate) |
| 119 | + console.log("ORACLE-MUTATIONS-GREEN-61e8") |
| 120 | +} |
| 121 | + |
| 122 | +if (process.argv[2] === "--self-test") { |
| 123 | + selfTest() |
| 124 | +} else { |
| 125 | + const [path, mode, expectedRaw, geometryRaw, stdoutPath, stderrPath] = process.argv.slice(2) |
| 126 | + if (!path || !mode || !expectedRaw || !geometryRaw) process.exit(2) |
| 127 | + const data = fs.existsSync(path) ? fs.readFileSync(path) : Buffer.alloc(0) |
| 128 | + const expected = Number(expectedRaw) |
| 129 | + const expectedGeometries = parseGeometries(geometryRaw) |
| 130 | + if (mode === "snapshots") { |
| 131 | + validateSnapshots(decode(data, false), expectedGeometries, expected) |
| 132 | + } else if (mode === "final") { |
| 133 | + if (!stdoutPath || !stderrPath) process.exit(2) |
| 134 | + validateFinal(data, expectedGeometries, fs.readFileSync(stdoutPath), fs.readFileSync(stderrPath)) |
| 135 | + console.log("PACKAGED-FD-GREEN-61e8") |
| 136 | + console.log("INITIAL-SNAPSHOT-GREEN-61e8") |
| 137 | + console.log("RECONNECT-SNAPSHOT-GREEN-61e8") |
| 138 | + console.log("FRAMED-TERMINAL-STREAM-GREEN-61e8") |
| 139 | + } else { |
| 140 | + process.exit(2) |
| 141 | + } |
| 142 | +} |
0 commit comments