diff --git a/.gitignore b/.gitignore index bafa443..b6f1a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ lerna-debug.log yarn-error.log tsconfig.tsbuildinfo .env +*.tgz diff --git a/packages/node-firebird-driver-native/src/lib/attachment.ts b/packages/node-firebird-driver-native/src/lib/attachment.ts index ecd5dc8..b5a4cc3 100644 --- a/packages/node-firebird-driver-native/src/lib/attachment.ts +++ b/packages/node-firebird-driver-native/src/lib/attachment.ts @@ -3,7 +3,7 @@ import { ClientImpl } from './client'; import { StatementImpl } from './statement'; import { TransactionImpl } from './transaction'; import { EventsImpl } from './events'; -import { createDpb } from './fb-util'; +import { createDpb, createCharsetCodec, CharsetCodec } from './fb-util'; import { Blob, @@ -25,8 +25,11 @@ export class AttachmentImpl extends AbstractAttachment { attachmentHandle?: fb.Attachment; + charsetCodec: CharsetCodec; + static async connect(client: ClientImpl, uri: string, options?: ConnectOptions): Promise { const attachment = new AttachmentImpl(client); + attachment.charsetCodec = createCharsetCodec(options?.charset); return await client.statusAction(async (status) => { const dpb = createDpb(options); @@ -41,6 +44,7 @@ export class AttachmentImpl extends AbstractAttachment { options?: CreateDatabaseOptions, ): Promise { const attachment = new AttachmentImpl(client); + attachment.charsetCodec = createCharsetCodec(options?.charset); return await client.statusAction(async (status) => { const dpb = createDpb(options); diff --git a/packages/node-firebird-driver-native/src/lib/statement.ts b/packages/node-firebird-driver-native/src/lib/statement.ts index ce862c3..81d393c 100644 --- a/packages/node-firebird-driver-native/src/lib/statement.ts +++ b/packages/node-firebird-driver-native/src/lib/statement.ts @@ -53,12 +53,18 @@ export class StatementImpl extends AbstractStatement { if (statement.inMetadata) { statement.inBuffer = new Uint8Array(statement.inMetadata.getMessageLengthSync(status)); - statement.dataWriter = createDataWriter(createDescriptors(status, statement.inMetadata)); + statement.dataWriter = createDataWriter( + createDescriptors(status, statement.inMetadata), + attachment.charsetCodec, + ); } if (statement.outMetadata) { statement.outBuffer = new Uint8Array(statement.outMetadata.getMessageLengthSync(status)); - statement.dataReader = createDataReader(createDescriptors(status, statement.outMetadata)); + statement.dataReader = createDataReader( + createDescriptors(status, statement.outMetadata), + attachment.charsetCodec, + ); } return statement; 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..d26a83f 100644 --- a/packages/node-firebird-driver/src/lib/impl/fb-util.ts +++ b/packages/node-firebird-driver/src/lib/impl/fb-util.ts @@ -1,8 +1,5 @@ import * as os from 'os'; const littleEndian = os.endianness() === 'LE'; - -import * as stringDecoder from 'string_decoder'; - import { AbstractAttachment } from './attachment'; import { decodeDate, decodeTime, encodeDate, encodeTime } from './date-time'; import { tzIdToString, tzStringToId } from './time-zones'; @@ -132,9 +129,94 @@ export namespace charSets { export const ascii = 2; } +/** Maps Firebird charset names to TextDecoder encoding labels. */ +const firebirdCharsetMap: Record = { + UTF8: 'utf-8', + NONE: 'utf-8', + ASCII: 'ascii', + WIN1250: 'windows-1250', + WIN1251: 'windows-1251', + WIN1252: 'windows-1252', + WIN1253: 'windows-1253', + WIN1254: 'windows-1254', + WIN1255: 'windows-1255', + WIN1256: 'windows-1256', + WIN1257: 'windows-1257', + WIN1258: 'windows-1258', + ISO8859_1: 'iso-8859-1', + ISO8859_2: 'iso-8859-2', + ISO8859_3: 'iso-8859-3', + ISO8859_4: 'iso-8859-4', + ISO8859_5: 'iso-8859-5', + ISO8859_6: 'iso-8859-6', + ISO8859_7: 'iso-8859-7', + ISO8859_8: 'iso-8859-8', + ISO8859_9: 'iso-8859-9', + ISO8859_13: 'iso-8859-13', + ISO8859_15: 'iso-8859-15', +}; + +/** Resolves a Firebird charset name to a TextDecoder encoding label. */ +export function resolveCharsetEncoding(charset?: string): string { + if (!charset) { + return 'utf-8'; + } + const label = firebirdCharsetMap[charset.toUpperCase()]; + if (!label) { + throw new Error( + `Unsupported Firebird charset: '${charset}'. Supported: ${Object.keys(firebirdCharsetMap).join(', ')}`, + ); + } + return label; +} + +/** Codec for encoding/decoding strings in a specific charset. */ +export interface CharsetCodec { + decode(buffer: Uint8Array): string; + encode(str: string): Buffer; +} + +/** Creates a codec for encoding/decoding strings in the given charset. */ +export function createCharsetCodec(charset?: string): CharsetCodec { + const encoding = resolveCharsetEncoding(charset); + + if (encoding === 'utf-8') { + const decoder = new TextDecoder('utf-8'); + return { + decode: (buffer: Uint8Array) => decoder.decode(buffer), + encode: (str: string) => Buffer.from(str, 'utf8'), + }; + } + + const decoder = new TextDecoder(encoding); + const reverseMap = new Map(); + for (let i = 0; i < 256; i++) { + const char = decoder.decode(new Uint8Array([i])); + reverseMap.set(char, i); + } + + return { + decode: (buffer: Uint8Array) => decoder.decode(buffer), + encode: (str: string) => { + const bytes = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + const b = reverseMap.get(str[i]); + if (b === undefined) { + throw new Error( + `Character '${str[i]}' (U+${str[i].charCodeAt(0).toString(16).padStart(4, '0')}) ` + + `cannot be encoded in charset '${encoding}'.`, + ); + } + bytes[i] = b; + } + return Buffer.from(bytes); + }, + }; +} + export function createDpb(options?: ConnectOptions | CreateDatabaseOptions): Buffer { const code = (c: number) => String.fromCharCode(c); - const charSet = 'utf8'; + const charSet = options?.charset ?? 'utf8'; let ret = `${code(dpb.version1)}${code(dpb.lc_ctype)}${code(charSet.length)}${charSet}`; if (!options) { @@ -306,7 +388,8 @@ export type DataReader = (attachment: Attachment, transaction: Transaction, buff export type ItemReader = (attachment: Attachment, transaction: Transaction, buffer: Uint8Array) => Promise; /** Creates a data reader. */ -export function createDataReader(descriptors: Descriptor[]): DataReader { +export function createDataReader(descriptors: Descriptor[], codec?: CharsetCodec): DataReader { + const charsetCodec = codec ?? createCharsetCodec(); const mappers = new Array(descriptors.length); for (let i = 0; i < descriptors.length; ++i) { @@ -328,9 +411,8 @@ export function createDataReader(descriptors: Descriptor[]): DataReader { case sqlTypes.SQL_VARYING: { //// TODO: none, octets const varLength = dataView.getUint16(descriptor.offset, littleEndian); - const decoder = new stringDecoder.StringDecoder('utf8'); - const buf = Buffer.from(buffer.buffer, descriptor.offset + 2, varLength); - return decoder.end(buf); + const buf = new Uint8Array(buffer.buffer, descriptor.offset + 2, varLength); + return charsetCodec.decode(buf); } /*** @@ -503,7 +585,8 @@ export type ItemWriter = ( ) => Promise; /** Creates a data writer. */ -export function createDataWriter(descriptors: Descriptor[]): DataWriter { +export function createDataWriter(descriptors: Descriptor[], codec?: CharsetCodec): DataWriter { + const charsetCodec = codec ?? createCharsetCodec(); const mappers = new Array(descriptors.length); for (let i = 0; i < descriptors.length; ++i) { @@ -529,7 +612,7 @@ export function createDataWriter(descriptors: Descriptor[]): DataWriter { case sqlTypes.SQL_VARYING: { //// TODO: none, octets const str = value as string; - const strBuffer = Buffer.from(str); + const strBuffer = charsetCodec.encode(str); 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..28ac7da 100644 --- a/packages/node-firebird-driver/src/lib/index.ts +++ b/packages/node-firebird-driver/src/lib/index.ts @@ -45,6 +45,9 @@ export interface ConnectOptions { /** User role. */ role?: string; + /** Connection charset (lc_ctype). Defaults to 'utf8'. */ + charset?: string; + /** Set database read/write mode. */ setDatabaseReadWriteMode?: DatabaseReadWriteMode; }