Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/pg-protocol/src/buffer-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ export class Writer {
return this
}

// Write an Int32 byte-length prefix immediately followed by the string's UTF-8
// bytes. Postgres' Bind wire format prefixes every parameter with its length,
// and doing it in one method computes Buffer.byteLength ONCE — the previous
// `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned the string
// three times (byteLength for the prefix, byteLength again inside addString,
// then the encode), which is costly for large text parameters.
public addInt32PrefixedString(string: string): Writer {
const len = Buffer.byteLength(string)
this.ensure(4 + len)
const buffer = this.buffer
let offset = this.offset
buffer[offset++] = (len >>> 24) & 0xff
buffer[offset++] = (len >>> 16) & 0xff
buffer[offset++] = (len >>> 8) & 0xff
buffer[offset++] = (len >>> 0) & 0xff
buffer.write(string, offset, 'utf-8')
this.offset = offset + len
return this
}

public add(otherBuffer: Buffer): Writer {
this.ensure(otherBuffer.length)
otherBuffer.copy(this.buffer, this.offset)
Expand Down
22 changes: 22 additions & 0 deletions packages/pg-protocol/src/outbound-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@ describe('serializer', () => {
.join(true, 'B')
assert.deepEqual(actual, expectedBuffer)
})

it('encodes a multi-byte string param with its UTF-8 byte length, not char length', function () {
// Guards the single-pass addInt32PrefixedString write path: the Int32
// length prefix must be the UTF-8 byte count, not String.length. 'héllo中🎉'
// is 7 code points / 8 UTF-16 code units but 13 UTF-8 bytes.
const value = 'héllo中🎉'
const bytes = Buffer.from(value, 'utf8')
assert.notEqual(bytes.length, value.length) // sanity: the divergence we're testing
const actual = serialize.bind({ values: [value] })
const expectedBuffer = new BufferList()
.addCString('') // portal
.addCString('') // statement
.addInt16(1) // param format code count
.addInt16(0) // format code for the one value (text)
.addInt16(1) // value count
.addInt32(bytes.length) // 13 — the UTF-8 byte length, NOT value.length (8)
.add(bytes)
.addInt16(1) // result format code count
.addInt16(0) // result format (text)
.join(true, 'B')
assert.deepEqual(actual, expectedBuffer)
})
})

it('with custom valueMapper', function () {
Expand Down
6 changes: 3 additions & 3 deletions packages/pg-protocol/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const password = (password: string): Buffer => {

const sendSASLInitialResponseMessage = function (mechanism: string, initialResponse: string): Buffer {
// 0x70 = 'p'
writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse)
writer.addCString(mechanism).addInt32PrefixedString(initialResponse)

return writer.flush(code.startup)
}
Expand Down Expand Up @@ -135,8 +135,8 @@ const writeValues = function (values: any[], valueMapper?: ValueMapper): void {
} else {
// add the param type (string) to the writer
writer.addInt16(ParamType.STRING)
paramWriter.addInt32(Buffer.byteLength(mappedVal))
paramWriter.addString(mappedVal)
// length prefix + UTF-8 bytes in one pass (Buffer.byteLength computed once)
paramWriter.addInt32PrefixedString(mappedVal)
}
}
}
Expand Down
Loading