|
| 1 | +// Conformance testee: speaks the protobuf conformance wire protocol on |
| 2 | +// stdin/stdout (4-byte LE length prefix + ConformanceRequest/Response), backed |
| 3 | +// by our reflective Protobuf lib. Run by the official conformance_test_runner. |
| 4 | +// |
| 5 | +// PB_SRC=/path/to/protobuf bun conformance-testee.mjs |
| 6 | +// |
| 7 | +// We implement binary<->binary for proto3 + edition 2023. JSON / JSPB / |
| 8 | +// TEXT_FORMAT and proto2 message types are reported as `skipped`. |
| 9 | +import { readFileSync } from "node:fs"; |
| 10 | +import { Schema } from "./serialization/protobuf/schema.ts"; |
| 11 | + |
| 12 | +const P = process.env.PB_SRC; |
| 13 | +const read = (rel) => readFileSync(`${P}/${rel}`, "utf8"); |
| 14 | + |
| 15 | +const conf = new Schema(read("conformance/conformance.proto")); |
| 16 | +const p3 = new Schema({ "google/protobuf/test_messages_proto3.proto": read("src/google/protobuf/test_messages_proto3.proto") }); |
| 17 | +const ed = new Schema({ "x.proto": read("conformance/test_protos/test_messages_edition2023.proto") }); |
| 18 | +const edP3 = new Schema({ "x.proto": read("editions/golden/test_messages_proto3_editions.proto") }); |
| 19 | +const edP2 = new Schema({ "x.proto": read("editions/golden/test_messages_proto2_editions.proto") }); |
| 20 | + |
| 21 | +const REQ = "conformance.ConformanceRequest"; |
| 22 | +const RESP = "conformance.ConformanceResponse"; |
| 23 | + |
| 24 | +function schemaFor(messageType) { |
| 25 | + // Most-specific package prefix first. |
| 26 | + if (messageType.startsWith("protobuf_test_messages.editions.proto3.")) return edP3; |
| 27 | + if (messageType.startsWith("protobuf_test_messages.editions.proto2.")) return edP2; |
| 28 | + if (messageType.startsWith("protobuf_test_messages.editions.")) return ed; |
| 29 | + if (messageType.startsWith("protobuf_test_messages.proto3.")) return p3; |
| 30 | + return null; // proto2 syntax (or unknown) — unsupported |
| 31 | +} |
| 32 | + |
| 33 | +function handle(reqBytes) { |
| 34 | + const req = conf.parse(REQ, reqBytes); |
| 35 | + const mt = req.messageType ?? ""; |
| 36 | + |
| 37 | + if (mt === "conformance.FailureSet") { |
| 38 | + return conf.build(RESP, { protobufPayload: conf.build("conformance.FailureSet", {}) }); |
| 39 | + } |
| 40 | + |
| 41 | + // We only do binary <-> binary. |
| 42 | + const out = req.requestedOutputFormat; // enum name |
| 43 | + if (req.jsonPayload !== undefined || req.jspbPayload !== undefined || req.textPayload !== undefined) { |
| 44 | + return conf.build(RESP, { skipped: "non-protobuf input unsupported" }); |
| 45 | + } |
| 46 | + if (out !== "PROTOBUF") { |
| 47 | + return conf.build(RESP, { skipped: `output ${out} unsupported` }); |
| 48 | + } |
| 49 | + |
| 50 | + const schema = schemaFor(mt); |
| 51 | + if (!schema) return conf.build(RESP, { skipped: "proto2/unknown message type unsupported" }); |
| 52 | + |
| 53 | + try { |
| 54 | + const msg = schema.parse(mt, req.protobufPayload ?? new Uint8Array(0)); |
| 55 | + try { |
| 56 | + const bytes = schema.build(mt, msg); |
| 57 | + return conf.build(RESP, { protobufPayload: bytes }); |
| 58 | + } catch (e) { |
| 59 | + return conf.build(RESP, { serializeError: String(e?.message ?? e) }); |
| 60 | + } |
| 61 | + } catch (e) { |
| 62 | + const msg = String(e?.message ?? e); |
| 63 | + if (msg.includes("unknown message")) return conf.build(RESP, { skipped: msg }); |
| 64 | + return conf.build(RESP, { parseError: msg }); |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +// --- framed stdin/stdout loop --- |
| 69 | +const reader = Bun.stdin.stream().getReader(); |
| 70 | +const sink = Bun.stdout.writer(); |
| 71 | +let buf = new Uint8Array(0); |
| 72 | + |
| 73 | +function concat(a, b) { |
| 74 | + const out = new Uint8Array(a.length + b.length); |
| 75 | + out.set(a); out.set(b, a.length); |
| 76 | + return out; |
| 77 | +} |
| 78 | +async function readExact(n) { |
| 79 | + while (buf.length < n) { |
| 80 | + const { done, value } = await reader.read(); |
| 81 | + if (done) return null; |
| 82 | + buf = concat(buf, value); |
| 83 | + } |
| 84 | + const out = buf.subarray(0, n); |
| 85 | + buf = buf.slice(n); |
| 86 | + return out; |
| 87 | +} |
| 88 | + |
| 89 | +for (;;) { |
| 90 | + const lenBuf = await readExact(4); |
| 91 | + if (!lenBuf) break; |
| 92 | + const len = new DataView(lenBuf.buffer, lenBuf.byteOffset, 4).getUint32(0, true); |
| 93 | + const reqBytes = await readExact(len); |
| 94 | + if (!reqBytes) break; |
| 95 | + |
| 96 | + let resp; |
| 97 | + try { |
| 98 | + resp = handle(reqBytes.slice()); |
| 99 | + } catch (e) { |
| 100 | + resp = conf.build(RESP, { runtimeError: String(e?.message ?? e) }); |
| 101 | + } |
| 102 | + |
| 103 | + const header = new Uint8Array(4); |
| 104 | + new DataView(header.buffer).setUint32(0, resp.length, true); |
| 105 | + sink.write(header); |
| 106 | + sink.write(resp); |
| 107 | + sink.flush(); |
| 108 | +} |
0 commit comments