Skip to content
Closed
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
54 changes: 51 additions & 3 deletions packages/pg-protocol/src/buffer-reader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
const fromCharCode = String.fromCharCode

// Longest value decoded without calling into buffer.toString(). Above this
// length the per-call cost of toString() is cheaper than decoding in JS.
const MAX_ASCII_LENGTH = 16

// Most values in a result set are short: ids, flags, small numbers, dates. For
// those the fixed cost of the C++ call behind buffer.toString() is bigger than
// the decoding itself, so build the string from the char codes instead.
// Returns undefined when the bytes are not ASCII, so the caller falls back to
// buffer.toString().
// prettier-ignore
const decodeAscii = (b: Buffer, i: number, length: number): string | undefined => {
let bits = 0
for (let k = 0; k < length; k++) {
bits |= b[i + k]
}
if (bits > 127) {
return undefined
}
switch (length) {
case 0: return ''
case 1: return fromCharCode(b[i])
case 2: return fromCharCode(b[i], b[i + 1])
case 3: return fromCharCode(b[i], b[i + 1], b[i + 2])
case 4: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3])
case 5: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4])
case 6: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5])
case 7: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6])
case 8: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7])
case 9: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8])
case 10: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9])
case 11: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10])
case 12: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11])
case 13: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12])
case 14: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13])
case 15: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13], b[i + 14])
case 16: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13], b[i + 14], b[i + 15])
}
return undefined
}

export class BufferReader {
private buffer: Buffer = Buffer.allocUnsafe(0)

Expand Down Expand Up @@ -36,9 +78,15 @@ export class BufferReader {
}

public string(length: number): string {
const result = this.buffer.toString(this.encoding, this.offset, this.offset + length)
this.offset += length
return result
const start = this.offset
this.offset = start + length
if (length <= MAX_ASCII_LENGTH) {
const ascii = decodeAscii(this.buffer, start, length)
if (ascii !== undefined) {
return ascii
}
}
return this.buffer.toString(this.encoding, start, this.offset)
}

public cstring(): string {
Expand Down
20 changes: 20 additions & 0 deletions packages/pg-protocol/src/inbound-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,26 @@ describe('PgPacketStream', function () {
fields: ['test'],
})
})

it('decodes values of any length and encoding', function () {
// short ascii values take a different decoding path than long or
// multi byte ones, so check both sides of it
const values = [
...Array.from({ length: 20 }, (_, i) => 'x'.repeat(i)),
'\u00e9',
'citt\u00e0',
'\u00fcn\u00efc\u00f6d\u00e9 w\u00f6rld',
'\u4e2d\u6587\u5b57\u7b26\u4e32',
'\ud83c\udf89',
'a\ud83c\udf89b',
null,
]
const fields: (string | null)[] = []
new Parser().parse(buffers.dataRow(values), (msg) => {
fields.push(...(msg as any).fields)
})
assert.deepStrictEqual(fields, values)
})
})

describe('notice message', function () {
Expand Down
Loading