From 7393cab586e01d9d1c6834242390751d3c3f9a66 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 17 Feb 2026 12:12:13 -0300 Subject: [PATCH] Add ConnectOption charSetForNONE --- .gitignore | 3 +- .../src/lib/attachment.ts | 2 + .../src/lib/fb-util.ts | 1 + packages/node-firebird-driver/package.json | 8 +++ .../src/lib/impl/attachment.ts | 1 + .../src/lib/impl/encoding.ts | 58 +++++++++++++++++ .../src/lib/impl/fb-util.ts | 17 ++--- .../node-firebird-driver/src/lib/index.ts | 6 ++ .../node-firebird-driver/src/test/tests.ts | 62 +++++++++++++++++++ yarn.lock | 5 ++ 10 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 packages/node-firebird-driver/src/lib/impl/encoding.ts diff --git a/.gitignore b/.gitignore index bafa443..cd19e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules/ .yarn/install-state.gz dist/ -lerna-debug.log -yarn-error.log +*.log tsconfig.tsbuildinfo .env diff --git a/packages/node-firebird-driver-native/src/lib/attachment.ts b/packages/node-firebird-driver-native/src/lib/attachment.ts index ecd5dc8..83a5e1f 100644 --- a/packages/node-firebird-driver-native/src/lib/attachment.ts +++ b/packages/node-firebird-driver-native/src/lib/attachment.ts @@ -27,6 +27,7 @@ export class AttachmentImpl extends AbstractAttachment { static async connect(client: ClientImpl, uri: string, options?: ConnectOptions): Promise { const attachment = new AttachmentImpl(client); + attachment.charSetForNONE = options?.charSetForNONE ?? 'utf8'; return await client.statusAction(async (status) => { const dpb = createDpb(options); @@ -41,6 +42,7 @@ export class AttachmentImpl extends AbstractAttachment { options?: CreateDatabaseOptions, ): Promise { const attachment = new AttachmentImpl(client); + attachment.charSetForNONE = options?.charSetForNONE ?? 'utf8'; return await client.statusAction(async (status) => { const dpb = createDpb(options); diff --git a/packages/node-firebird-driver-native/src/lib/fb-util.ts b/packages/node-firebird-driver-native/src/lib/fb-util.ts index 33d6e1d..49ea980 100644 --- a/packages/node-firebird-driver-native/src/lib/fb-util.ts +++ b/packages/node-firebird-driver-native/src/lib/fb-util.ts @@ -73,6 +73,7 @@ export function createDescriptors(status: fb.Status, metadata?: fb.MessageMetada ret.push({ type: metadata.getTypeSync(status, i), subType: metadata.getSubTypeSync(status, i), + charSet: metadata.getCharSetSync(status, i), nullOffset: metadata.getNullOffsetSync(status, i), offset: metadata.getOffsetSync(status, i), length: metadata.getLengthSync(status, i), diff --git a/packages/node-firebird-driver/package.json b/packages/node-firebird-driver/package.json index ae41f25..1320621 100644 --- a/packages/node-firebird-driver/package.json +++ b/packages/node-firebird-driver/package.json @@ -31,5 +31,13 @@ "typings": "./dist/lib/index.d.ts", "dependencies": { "@types/node": "^22.13.10" + }, + "peerDependencies": { + "iconv-lite": "^0.7.2" + }, + "peerDependenciesMeta": { + "iconv-lite": { + "optional": true + } } } diff --git a/packages/node-firebird-driver/src/lib/impl/attachment.ts b/packages/node-firebird-driver/src/lib/impl/attachment.ts index fe9b113..3408076 100644 --- a/packages/node-firebird-driver/src/lib/impl/attachment.ts +++ b/packages/node-firebird-driver/src/lib/impl/attachment.ts @@ -22,6 +22,7 @@ export abstract class AbstractAttachment implements Attachment { events = new Set(); statements = new Set(); transactions = new Set(); + charSetForNONE = 'utf8'; /** Default transaction options. */ defaultTransactionOptions: TransactionOptions; diff --git a/packages/node-firebird-driver/src/lib/impl/encoding.ts b/packages/node-firebird-driver/src/lib/impl/encoding.ts new file mode 100644 index 0000000..bd624b2 --- /dev/null +++ b/packages/node-firebird-driver/src/lib/impl/encoding.ts @@ -0,0 +1,58 @@ +interface IconvLiteModule { + encodingExists(encoding: string): boolean; + decode(buffer: Buffer, encoding: string): string; + encode(value: string, encoding: string): Buffer; +} + +let iconvLite: IconvLiteModule | undefined; +let iconvLiteLoadAttempted = false; + +function getIconvLite(encoding: string): IconvLiteModule { + if (!iconvLiteLoadAttempted) { + iconvLiteLoadAttempted = true; + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + iconvLite = require('iconv-lite') as IconvLiteModule; + } catch { + iconvLite = undefined; + } + } + + if (!iconvLite) { + throw new Error( + `Encoding '${encoding}' in charSetForNONE requires optional dependency 'iconv-lite'. ` + + `Install it with: yarn add iconv-lite`, + ); + } + + return iconvLite; +} + +export function decodeString(bytes: Buffer, encoding: string): string { + if (Buffer.isEncoding(encoding as BufferEncoding)) { + return bytes.toString(encoding as BufferEncoding); + } + + const iconvLiteModule = getIconvLite(encoding); + + if (iconvLiteModule.encodingExists(encoding)) { + return iconvLiteModule.decode(bytes, encoding); + } + + throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`); +} + +export function encodeString(value: string, encoding: string): Buffer { + if (Buffer.isEncoding(encoding as BufferEncoding)) { + return Buffer.from(value, encoding as BufferEncoding); + } + + const iconvLiteModule = getIconvLite(encoding); + + if (iconvLiteModule.encodingExists(encoding)) { + return iconvLiteModule.encode(value, encoding); + } + + throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`); +} diff --git a/packages/node-firebird-driver/src/lib/impl/fb-util.ts b/packages/node-firebird-driver/src/lib/impl/fb-util.ts index 1089262..4d0e41f 100644 --- a/packages/node-firebird-driver/src/lib/impl/fb-util.ts +++ b/packages/node-firebird-driver/src/lib/impl/fb-util.ts @@ -1,9 +1,8 @@ import * as os from 'os'; const littleEndian = os.endianness() === 'LE'; -import * as stringDecoder from 'string_decoder'; - import { AbstractAttachment } from './attachment'; +import { decodeString, encodeString } from './encoding'; import { decodeDate, decodeTime, encodeDate, encodeTime } from './date-time'; import { tzIdToString, tzStringToId } from './time-zones'; import { AbstractTransaction } from './transaction'; @@ -129,6 +128,7 @@ export namespace cancelType { } export namespace charSets { + export const none = 0; export const ascii = 2; } @@ -296,6 +296,7 @@ export function getPortableInteger(buffer: Uint8Array, length: number) { export interface Descriptor { type: number; subType: number; + charSet: number; length: number; scale: number; offset: number; @@ -326,11 +327,11 @@ export function createDataReader(descriptors: Descriptor[]): DataReader { switch (descriptor.type) { // SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder. case sqlTypes.SQL_VARYING: { - //// TODO: none, octets + // TODO: octets const varLength = dataView.getUint16(descriptor.offset, littleEndian); - const decoder = new stringDecoder.StringDecoder('utf8'); + const encoding = descriptor.charSet === charSets.none ? attachment.charSetForNONE : 'utf8'; const buf = Buffer.from(buffer.buffer, descriptor.offset + 2, varLength); - return decoder.end(buf); + return decodeString(buf, encoding); } /*** @@ -527,9 +528,11 @@ export function createDataWriter(descriptors: Descriptor[]): DataWriter { switch (descriptor.type) { // SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder. case sqlTypes.SQL_VARYING: { - //// TODO: none, octets + //// TODO: octets const str = value as string; - const strBuffer = Buffer.from(str); + const attached = attachment as AbstractAttachment; + const encoding = descriptor.charSet === charSets.none ? attached.charSetForNONE : 'utf8'; + const strBuffer = encodeString(str, encoding); const bytesArray = Uint8Array.from(strBuffer); diff --git a/packages/node-firebird-driver/src/lib/index.ts b/packages/node-firebird-driver/src/lib/index.ts index 5a3da33..0e2ef0e 100644 --- a/packages/node-firebird-driver/src/lib/index.ts +++ b/packages/node-firebird-driver/src/lib/index.ts @@ -47,6 +47,12 @@ export interface ConnectOptions { /** Set database read/write mode. */ setDatabaseReadWriteMode?: DatabaseReadWriteMode; + + /** + * Node.js character set encoding used for Firebird NONE charset columns/parameters. + * Requires iconv-lite package. + */ + charSetForNONE?: string; } /** DatabaseReadWriteMode enum */ diff --git a/packages/node-firebird-driver/src/test/tests.ts b/packages/node-firebird-driver/src/test/tests.ts index 0e8e5b2..3a1a22c 100644 --- a/packages/node-firebird-driver/src/test/tests.ts +++ b/packages/node-firebird-driver/src/test/tests.ts @@ -973,6 +973,68 @@ export function runCommonTests(client: Client) { await attachment.dropDatabase(); }); + test('NONE charset uses charSetForNONE for read/write', async () => { + const filename = getTempFile('ResultSet-none-charset.fdb'); + + { + const attachment = await client.createDatabase(filename); + const transaction = await attachment.startTransaction(); + await attachment.execute(transaction, 'create table t1 (id integer, x varchar(10) character set none)'); + await transaction.commitRetaining(); + await attachment.execute( + transaction, + "insert into t1 (id, x) values (1, cast(x'B99C9F' as varchar(3) character set none))", + ); + await attachment.execute( + transaction, + "insert into t1 (id, x) values (2, cast(x'B1B6BC' as varchar(3) character set none))", + ); + await transaction.commit(); + await attachment.disconnect(); + } + + { + const attachment = await client.connect(filename); + const transaction = await attachment.startTransaction(); + const row1 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1'); + const row2 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2'); + expect(row1[0]).toBe(Buffer.from('b99c9f', 'hex').toString('utf8')); + expect(row2[0]).toBe(Buffer.from('b1b6bc', 'hex').toString('utf8')); + await transaction.commit(); + await attachment.disconnect(); + } + + { + const attachment = await client.connect(filename, { charSetForNONE: 'windows-1250' }); + const transaction = await attachment.startTransaction(); + const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1'); + expect(row[0]).toBe('ąśź'); + await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [101, 'ąśź']); + const check = await attachment.executeSingleton( + transaction, + "select count(*) from t1 where id = 101 and x = cast(x'B99C9F' as varchar(3) character set none)", + ); + expect(check[0]).toBe(1); + await transaction.commit(); + await attachment.disconnect(); + } + + { + const attachment = await client.connect(filename, { charSetForNONE: 'iso-8859-2' }); + const transaction = await attachment.startTransaction(); + const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2'); + expect(row[0]).toBe('ąśź'); + await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [201, 'ąśź']); + const check = await attachment.executeSingleton( + transaction, + "select count(*) from t1 where id = 201 and x = cast(x'B1B6BC' as varchar(3) character set none)", + ); + expect(check[0]).toBe(1); + await transaction.commit(); + await attachment.dropDatabase(); + } + }); + test('#fetch() with fetchSize', async () => { const attachment = await client.createDatabase(getTempFile('ResultSet-fetch-with-fetchSize.fdb')); const transaction = await attachment.startTransaction(); diff --git a/yarn.lock b/yarn.lock index e5bfc74..ee234a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6691,6 +6691,11 @@ __metadata: resolution: "node-firebird-driver@workspace:packages/node-firebird-driver" dependencies: "@types/node": "npm:^22.13.10" + peerDependencies: + iconv-lite: ^0.7.2 + peerDependenciesMeta: + iconv-lite: + optional: true languageName: unknown linkType: soft