From 18fedf4119a1d6ba7fda53a2209cb59b2b864e30 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 Apr 2026 17:33:45 +0200 Subject: [PATCH] Correctly serialize non ASCII metadata Fixes #6663 --- assets/js/phoenix/serializer.js | 38 +++++++++++++++++++++-------- assets/test/serializer_test.js | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/assets/js/phoenix/serializer.js b/assets/js/phoenix/serializer.js index 3df8eb3ef7..b2f7867d22 100644 --- a/assets/js/phoenix/serializer.js +++ b/assets/js/phoenix/serializer.js @@ -30,28 +30,46 @@ export default { binaryEncode(message){ let {join_ref, ref, event, topic, payload} = message - let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length + let encoder = new TextEncoder() + let joinRefBytes = encoder.encode(join_ref) + let refBytes = encoder.encode(ref) + let topicBytes = encoder.encode(topic) + let eventBytes = encoder.encode(event) + + this.assertFieldSize(joinRefBytes.byteLength, "join_ref") + this.assertFieldSize(refBytes.byteLength, "ref") + this.assertFieldSize(topicBytes.byteLength, "topic") + this.assertFieldSize(eventBytes.byteLength, "event") + + let metaLength = this.META_LENGTH + joinRefBytes.byteLength + refBytes.byteLength + topicBytes.byteLength + eventBytes.byteLength let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength) + let headerBytes = new Uint8Array(header) let view = new DataView(header) let offset = 0 view.setUint8(offset++, this.KINDS.push) // kind - view.setUint8(offset++, join_ref.length) - view.setUint8(offset++, ref.length) - view.setUint8(offset++, topic.length) - view.setUint8(offset++, event.length) - Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0))) - Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0))) - Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0))) - Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0))) + view.setUint8(offset++, joinRefBytes.byteLength) + view.setUint8(offset++, refBytes.byteLength) + view.setUint8(offset++, topicBytes.byteLength) + view.setUint8(offset++, eventBytes.byteLength) + headerBytes.set(joinRefBytes, offset); offset += joinRefBytes.byteLength + headerBytes.set(refBytes, offset); offset += refBytes.byteLength + headerBytes.set(topicBytes, offset); offset += topicBytes.byteLength + headerBytes.set(eventBytes, offset); offset += eventBytes.byteLength var combined = new Uint8Array(header.byteLength + payload.byteLength) - combined.set(new Uint8Array(header), 0) + combined.set(headerBytes, 0) combined.set(new Uint8Array(payload), header.byteLength) return combined.buffer }, + assertFieldSize(size, name){ + if(size > 255){ + throw new Error(`unable to convert ${name} to binary: must be less than or equal to 255 bytes, but is ${size} bytes`) + } + }, + binaryDecode(buffer){ let view = new DataView(buffer) let kind = view.getUint8(0) diff --git a/assets/test/serializer_test.js b/assets/test/serializer_test.js index 78a56e8816..4965340d6e 100644 --- a/assets/test/serializer_test.js +++ b/assets/test/serializer_test.js @@ -81,6 +81,48 @@ describe("binary", () => { }) }) + it("encodes non-ASCII metadata as UTF-8", (done) => { + let buffer = binPayload() + let topic = "room:café" + let event = "π" + let joinRef = "🚀" + let ref = "1" + let encoder = new TextEncoder() + let topicBytes = encoder.encode(topic) + let eventBytes = encoder.encode(event) + let joinRefBytes = encoder.encode(joinRef) + let refBytes = encoder.encode(ref) + + Serializer.encode({join_ref: joinRef, ref: ref, topic: topic, event: event, payload: buffer}, (result) => { + let bytes = new Uint8Array(result) + expect(bytes[0]).toBe(0) // push kind + expect(bytes[1]).toBe(joinRefBytes.byteLength) // 4 bytes for "🚀" + expect(bytes[2]).toBe(refBytes.byteLength) + expect(bytes[3]).toBe(topicBytes.byteLength) // 10 bytes for "room:café" + expect(bytes[4]).toBe(eventBytes.byteLength) // 2 bytes for "π" + + let offset = 5 + expect(Array.from(bytes.slice(offset, offset + joinRefBytes.byteLength))).toEqual(Array.from(joinRefBytes)) + offset += joinRefBytes.byteLength + expect(Array.from(bytes.slice(offset, offset + refBytes.byteLength))).toEqual(Array.from(refBytes)) + offset += refBytes.byteLength + expect(Array.from(bytes.slice(offset, offset + topicBytes.byteLength))).toEqual(Array.from(topicBytes)) + offset += topicBytes.byteLength + expect(Array.from(bytes.slice(offset, offset + eventBytes.byteLength))).toEqual(Array.from(eventBytes)) + offset += eventBytes.byteLength + expect(bytes[offset]).toBe(1) // payload byte + done() + }) + }) + + it("throws when a metadata field exceeds 255 UTF-8 bytes", () => { + let buffer = binPayload() + let big = "a".repeat(256) + expect(() => { + Serializer.encode({join_ref: "0", ref: "1", topic: big, event: "e", payload: buffer}, () => {}) + }).toThrow(/topic/) + }) + it("decodes broadcast", (done) => { let bin = "\x02\x03\ntopsome-event\x01\x01" let buffer = new TextEncoder().encode(bin).buffer