diff --git a/src/lib/__tests__/packetBuilder.spec.ts b/src/lib/__tests__/packetBuilder.spec.ts new file mode 100644 index 00000000..c4a69a76 --- /dev/null +++ b/src/lib/__tests__/packetBuilder.spec.ts @@ -0,0 +1,120 @@ +import type { ISerializableCommand } from '../../commands' +import { ProtocolVersion } from '../../enums' +import { PacketBuilder } from '../packetBuilder' + +class FakeCommand implements ISerializableCommand { + static readonly rawName: string = 'FAKE' + + constructor(public readonly length: number, public readonly value: number = 1) {} + + public get lengthWithHeader(): number { + return this.length + 8 + } + + serialize = jest.fn((_version: ProtocolVersion): Buffer => { + return Buffer.alloc(this.length, this.value) + }) +} + +describe('PacketBuilder', () => { + it('No commands', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + expect(builder.getPackets()).toHaveLength(0) + }) + + it('Single command', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(10) + builder.addCommand(cmd) + + expect(builder.getPackets()).toHaveLength(1) + expect(builder.getPackets()).toHaveLength(1) // Ensure that calling it twice doesnt affect the output + expect(builder.getPackets()[0]).toHaveLength(cmd.lengthWithHeader) + }) + + it('Once finished cant add more commands', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(10) + builder.addCommand(cmd) + + expect(builder.getPackets()).toHaveLength(1) + + expect(() => builder.addCommand(cmd)).toThrow('finished') + }) + + it('Repeated command', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(10) + for (let i = 0; i < 5; i++) { + builder.addCommand(cmd) + } + + expect(builder.getPackets()).toHaveLength(1) + expect(builder.getPackets()[0]).toHaveLength(cmd.lengthWithHeader * 5) + }) + + it('Repeated command spanning multiple packets', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(10) + for (let i = 0; i < 60; i++) { + builder.addCommand(cmd) + } + + expect(cmd.lengthWithHeader).toBe(18) + expect(builder.getPackets()).toHaveLength(3) + + expect(builder.getPackets()[0]).toHaveLength(cmd.lengthWithHeader * 27) + expect(builder.getPackets()[1]).toHaveLength(cmd.lengthWithHeader * 27) + expect(builder.getPackets()[2]).toHaveLength(cmd.lengthWithHeader * 6) + }) + + it('Command too large to fit a packets', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(501) + expect(() => builder.addCommand(cmd)).toThrow('too large') + }) + + it('Command same size as packet', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const cmd = new FakeCommand(500 - 8) + expect(cmd.lengthWithHeader).toBe(500) + + builder.addCommand(cmd) + expect(builder.getPackets()).toHaveLength(1) + expect(builder.getPackets()[0]).toHaveLength(cmd.lengthWithHeader) + }) + + it('Commands of mixed sizes', () => { + const builder = new PacketBuilder(500, ProtocolVersion.V8_1_1) + + const largeCmd = new FakeCommand(400) + const mediumCmd = new FakeCommand(80) + const smallCmd = new FakeCommand(10) + + // packet 0: + builder.addCommand(mediumCmd) + builder.addCommand(smallCmd) + + // packet 1: + builder.addCommand(largeCmd) + builder.addCommand(mediumCmd) + + // packet 2: + builder.addCommand(smallCmd) + builder.addCommand(smallCmd) + builder.addCommand(largeCmd) + + expect(builder.getPackets()).toHaveLength(3) + expect(builder.getPackets()[0]).toHaveLength(mediumCmd.lengthWithHeader + smallCmd.lengthWithHeader) + expect(builder.getPackets()[1]).toHaveLength(largeCmd.lengthWithHeader + mediumCmd.lengthWithHeader) + expect(builder.getPackets()[2]).toHaveLength( + smallCmd.lengthWithHeader + smallCmd.lengthWithHeader + largeCmd.lengthWithHeader + ) + }) +}) diff --git a/src/lib/packetBuilder.ts b/src/lib/packetBuilder.ts index 3a9945ab..d4db48ed 100644 --- a/src/lib/packetBuilder.ts +++ b/src/lib/packetBuilder.ts @@ -7,6 +7,7 @@ export class PacketBuilder { readonly #completedBuffers: Buffer[] = [] + #finished = false #currentPacketBuffer: Buffer #currentPacketFilled: number @@ -19,16 +20,17 @@ export class PacketBuilder { } public addCommand(cmd: ISerializableCommand): void { + if (this.#finished) throw new Error('Packets have been finished') + if (typeof cmd.serialize !== 'function') { throw new Error(`Command ${cmd.constructor.name} is not serializable`) } - const payload = cmd.serialize(this.#protocolVersion) - const rawName: string = (cmd.constructor as any).rawName + const payload = cmd.serialize(this.#protocolVersion) const totalLength = payload.length + 8 - if (totalLength >= this.#maxPacketSize) { + if (totalLength > this.#maxPacketSize) { throw new Error(`Comamnd ${cmd.constructor.name} is too large for a single packet`) } @@ -37,11 +39,9 @@ export class PacketBuilder { this.#finishBuffer() } - // Command name + // Add to packet this.#currentPacketBuffer.writeUInt16BE(payload.length + 8, this.#currentPacketFilled + 0) this.#currentPacketBuffer.write(rawName, this.#currentPacketFilled + 4, 4) - - // Body payload.copy(this.#currentPacketBuffer, this.#currentPacketFilled + 8) this.#currentPacketFilled += totalLength @@ -50,11 +50,13 @@ export class PacketBuilder { public getPackets(): Buffer[] { this.#finishBuffer(true) + this.#finished = true + return this.#completedBuffers } #finishBuffer(skipCreateNext?: boolean) { - if (this.#currentPacketFilled === 0) return + if (this.#currentPacketFilled === 0 || this.#finished) return this.#completedBuffers.push(this.#currentPacketBuffer.subarray(0, this.#currentPacketFilled))