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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ lerna-debug.log
yarn-error.log
tsconfig.tsbuildinfo
.env
*.tgz
6 changes: 5 additions & 1 deletion packages/node-firebird-driver-native/src/lib/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,8 +25,11 @@ export class AttachmentImpl extends AbstractAttachment {

attachmentHandle?: fb.Attachment;

charsetCodec: CharsetCodec;

static async connect(client: ClientImpl, uri: string, options?: ConnectOptions): Promise<AttachmentImpl> {
const attachment = new AttachmentImpl(client);
attachment.charsetCodec = createCharsetCodec(options?.charset);

return await client.statusAction(async (status) => {
const dpb = createDpb(options);
Expand All @@ -41,6 +44,7 @@ export class AttachmentImpl extends AbstractAttachment {
options?: CreateDatabaseOptions,
): Promise<AttachmentImpl> {
const attachment = new AttachmentImpl(client);
attachment.charsetCodec = createCharsetCodec(options?.charset);

return await client.statusAction(async (status) => {
const dpb = createDpb(options);
Expand Down
10 changes: 8 additions & 2 deletions packages/node-firebird-driver-native/src/lib/statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
103 changes: 93 additions & 10 deletions packages/node-firebird-driver/src/lib/impl/fb-util.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -132,9 +129,94 @@ export namespace charSets {
export const ascii = 2;
}

/** Maps Firebird charset names to TextDecoder encoding labels. */
const firebirdCharsetMap: Record<string, string> = {
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<string, number>();
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) {
Expand Down Expand Up @@ -306,7 +388,8 @@ export type DataReader = (attachment: Attachment, transaction: Transaction, buff
export type ItemReader = (attachment: Attachment, transaction: Transaction, buffer: Uint8Array) => Promise<any>;

/** 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<ItemReader>(descriptors.length);

for (let i = 0; i < descriptors.length; ++i) {
Expand All @@ -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);
}

/***
Expand Down Expand Up @@ -503,7 +585,8 @@ export type ItemWriter = (
) => Promise<void>;

/** 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<ItemWriter>(descriptors.length);

for (let i = 0; i < descriptors.length; ++i) {
Expand All @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions packages/node-firebird-driver/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down