pascalx.io.charset.pas

Переключить прокрутку окна
Загрузить этот исходный код

{
    pascalx.io.charset — поддержка кодировок текста.

    Copyright © 2021, 2026 Малик Разработчик

    Это свободная программа: вы можете перераспространять её и/или изменять
    её на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она будет полезной,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <https://www.gnu.org/licenses/>.
}

unit pascalx.io.charset;

    {$MODE OBJFPC}

interface

    {%region} uses
        {$IFDEF WINDOWS}
        windows,
        {$ENDIF}
        pascalx.lang,
        pascalx.io,
        pascalx.io.extension,
        pascalx.io.bytearray;
    {%endregion}

    {$WARN 3018 OFF} { позволить конструкторы с любой видимостью }

    {$TYPEINFO ON}
    {$CALLING REGISTER}

    const UNIT_NAME = 'pascalx.io.charset';

    {%region} type
        CharacterDecodingErrorHandler   = interface;
        CharacterEncodingErrorHandler   = interface;
        CharactersBuffer                = class;
        CharactersStorage               = class;
        CharDecoder                     = class;
        CharEncoder                     = class;
        Charset                         = class;
        CharsetException                = class;
        CharacterDecodingException      = class;
        CharacterEncodingException      = class;
        RegisteredCharsetNameException  = class;
        UnsupportedCharsetNameException = class;

        ErrorAction = (
            eaIgnore,
            eaReport,
            eaReplace,
            eaException
        );

        Charset_Array1d = specialize DynamicArray<Charset>;

        Charset_Collection1d = specialize Collection<Charset>;

        CharacterDecodingErrorHandler = interface(RawInterface) ['{9BEC84B9-E33B-4EA9-8CB3-83AD3B102358}']
            procedure decodingError(decoder: CharDecoder);
        end;

        CharacterEncodingErrorHandler = interface(RawInterface) ['{9BEC84B9-E33B-4EA9-8CB3-83AD3B102359}']
            procedure encodingError(encoder: CharEncoder);
        end;

        CharactersBuffer = class sealed(&Object, UCharWriter, Extendable)
        private
            fldLength: int;
            fldContent: uchar_Array1d;
            procedure checkBounds(beginIndex, endIndex: int);
            function charAt(index: int): uchar;
            function madeLarger(curLength, newLength: int): uchar_Array1d;
        public
            procedure close();
            procedure write(ucharData: int);
            procedure write(const src: uchar_Array1d);
            procedure write(const src: uchar_Array1d; offset, length: int);
            procedure flush();
            procedure getChars(beginIndex, endIndex: int; const dst: uchar_Array1d; offset: int);
            procedure copyInto(const dst: uchar_Array1d; offset: int);
            procedure discard();
            procedure discard(charsQuantity: int);
            function toString(): AnsiString; override;
            function getExtensions(): Extension_Array1d;
            function getExtension(const typ: ShortString): Extension;
            function toUCharArray(): uchar_Array1d;
            property chars[index: int]: uchar read charAt; default;
            property length: int read fldLength;
        end;

        CharactersStorage = class sealed(&Object, UCharReader, Extension, LimitedSizeExtension, Extendable)
        private
            fldPosition: int;
            fldLength: int;
            fldContent: uchar_Array1d;
        public
            constructor create(const content: UnicodeString);
            constructor create(const content: uchar_Array1d);
            constructor create(const content: uchar_Array1d; length: int);
            constructor create(const content: uchar_Array1d; length, position: int);
            procedure close();
            function read(): int;
            function read(const dst: uchar_Array1d): int;
            function read(const dst: uchar_Array1d; offset, length: int): int;
            function skip(ucharsQuantity: long): long;
            function available(): long;
            function getExtensions(): Extension_Array1d;
            function getExtension(const typ: ShortString): Extension;
            property length: int read fldLength;
            property position: int read fldPosition;
        end;

        CharDecoder = class abstract(&Object)
        private
            class function toStream(src: ByteReader; length: int): ByteArrayInputStream; static;
        private
            fldGivedChars: long;
            fldAcceptedBytes: long;
            fldErrorAction: ErrorAction;
            fldErrorHandler: CharacterDecodingErrorHandler;
            fldCountersMonitor: Mutex;
            fldCharset: Charset;
            procedure setErrorAction(newErrorAction: ErrorAction);
        protected
            procedure decode(src: ByteReader; length: int; dst: UCharWriter); virtual; abstract;
            procedure report();
        public
            constructor create(charset: Charset);
            destructor destroy; override;
            procedure reset();
            procedure decodeToWriter(dst: UCharWriter; const src: byte_Array1d);
            procedure decodeToWriter(dst: UCharWriter; const src: byte_Array1d; offset, length: int);
            procedure decodeToWriter(dst: UCharWriter; src: ByteReader; length: int);
            function givedChars(): long;
            function acceptedBytes(): long;
            function decode(const src: byte_Array1d): uchar_Array1d;
            function decode(const src: byte_Array1d; offset, length: int): uchar_Array1d;
            function decode(src: ByteReader; length: int): uchar_Array1d;
        published
            property charset: Charset read fldCharset;
            property errorAction: ErrorAction read fldErrorAction write setErrorAction;
            property errorHandler: CharacterDecodingErrorHandler read fldErrorHandler write fldErrorHandler;
        end;

        CharEncoder = class abstract(&Object)
        private
            class function toStream(src: UCharReader; length: int): CharactersStorage; static;
        private
            fldGivedBytes: long;
            fldAcceptedChars: long;
            fldErrorAction: ErrorAction;
            fldErrorHandler: CharacterEncodingErrorHandler;
            fldCountersMonitor: Mutex;
            fldCharset: Charset;
            procedure setErrorAction(newErrorAction: ErrorAction);
        protected
            procedure encode(src: UCharReader; length: int; dst: ByteWriter); virtual; abstract;
            procedure report();
        public
            constructor create(charset: Charset);
            destructor destroy; override;
            procedure reset();
            procedure encodeToWriter(dst: ByteWriter; const src: UnicodeString);
            procedure encodeToWriter(dst: ByteWriter; const src: uchar_Array1d);
            procedure encodeToWriter(dst: ByteWriter; const src: uchar_Array1d; offset, length: int);
            procedure encodeToWriter(dst: ByteWriter; src: UCharReader; length: int);
            function canEncode(src: uchar): boolean; virtual;
            function canEncode(const src: UnicodeString): boolean;
            function canEncode(const src: uchar_Array1d): boolean;
            function canEncode(const src: uchar_Array1d; offset, length: int): boolean;
            function givedBytes(): long;
            function acceptedChars(): long;
            function encode(const src: UnicodeString): byte_Array1d;
            function encode(const src: uchar_Array1d): byte_Array1d;
            function encode(const src: uchar_Array1d; offset, length: int): byte_Array1d;
            function encode(src: UCharReader; length: int): byte_Array1d;
        published
            property charset: Charset read fldCharset;
            property errorAction: ErrorAction read fldErrorAction write setErrorAction;
            property errorHandler: CharacterEncodingErrorHandler read fldErrorHandler write fldErrorHandler;
        end;

        Charset = class abstract(&Object)
        public
            class procedure register(charsetRef: Charset); static;
            class function isSupported(const charsetName: AnsiString): boolean; static;
            class function enumerate(): Charset_Collection1d; static;
            class function getDefault(): Charset; static;
            class function getConsoleInput(): Charset; static;
            class function getConsoleOutput(): Charset; static;
            class function get(const charsetName: AnsiString): Charset; static;
        private
            fldName: AnsiString;
            fldAliases: AnsiString_Array1d;
        protected
            constructor create(const name: AnsiString; const aliases: AnsiString_Array1d);
        public
            function newDecoder(): CharDecoder; virtual; abstract;
            function newEncoder(): CharEncoder; virtual; abstract;
            function contains(charset: Charset): boolean; virtual;
            function displayName(): UnicodeString; virtual;
            function equals(anot: TObject): boolean; override; final;
            function getHashCode(): long; override; final;
            function toString(): AnsiString; override; final;
            function isRegistered(): boolean;
            function isName(const charsetName: AnsiString): boolean;
            function decode(const src: byte_Array1d): uchar_Array1d;
            function decode(const src: byte_Array1d; offset, length: int): uchar_Array1d;
            function encode(const src: uchar_Array1d): byte_Array1d;
            function encode(const src: uchar_Array1d; offset, length: int): byte_Array1d;
            function aliases(): AnsiString_Array1d;
        published
            property name: AnsiString read fldName;
        end;

        CharsetException = class(InvalidDataFormatException);

        CharacterDecodingException = class(CharsetException);

        CharacterEncodingException = class(CharsetException);

        RegisteredCharsetNameException = class(Exception);

        UnsupportedCharsetNameException = class(IOException);
    {%endregion}

implementation

    {%region} uses
        pascalx.io.charset.unicode,
        pascalx.lang.table;
    {%endregion}

    {$R *.res}

    {$TYPEINFO OFF}
    {$CALLING REGISTER}

    {%region} type
        CharsetCollection = class;

        HashtableOfAnsiStringToCharset = specialize HashtableOfAnsiString<Charset>;

        CharsetCollection = class sealed(RefCountObject, Charset_Collection1d)
        private
            fldLength: int;
            fldCharsets: Charset_Array1d;
        public
            constructor create(const charsets: Charset_Array1d; length: int);
            procedure copyInto(const dstArray; dstOffset: int);
            function getLength(): int;
            function componentAt(index: int): Charset;
            function toArray(): Charset_Array1d;
        end;
    {%endregion}

    {%region} var
        charsetRegisteredCount: int;
        charsetRegisteredArray: Charset_Array1d;
        charsetRegisteredTable: HashtableOfAnsiStringToCharset;
        charsetMonitor: Mutex;
        charsetDefault: Charset;
        {$IFDEF WINDOWS}
        charsetConsoleInput: Charset;
        charsetConsoleOutput: Charset;
        {$ENDIF}
    {%endregion}

    {%region  routines — charset}
        procedure charsetRegisteredArrayFinalize();
        var
            index: int;
        begin
            for index := charsetRegisteredCount - 1 downto 0 do begin
                charsetRegisteredArray[index].free();
            end;
            charsetRegisteredCount := 0;
            charsetRegisteredArray := nil;
            charsetRegisteredTable := nil;
            charsetMonitor := nil;
            charsetDefault := nil;
            {$IFDEF WINDOWS}
            charsetConsoleInput := nil;
            charsetConsoleOutput := nil;
            {$ENDIF}
        end;
    {%endregion}

    {%region  CharactersBuffer }
        procedure CharactersBuffer.checkBounds(beginIndex, endIndex: int);
        var
            locLength: int;
        begin
            locLength := fldLength;
            if ((beginIndex or endIndex) < 0) or (beginIndex > locLength) or (endIndex > locLength) or (beginIndex > endIndex) then begin
                raise IndexOutOfBoundsException.create(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'out-of-bounds.index'));
            end;
        end;

        function CharactersBuffer.charAt(index: int): uchar;
        begin
            if (index < 0) or (index >= fldLength) then begin
                raise IndexOutOfBoundsException.create(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'out-of-bounds.index'));
            end;
            result := fldContent[index];
        end;

        function CharactersBuffer.madeLarger(curLength, newLength: int): uchar_Array1d;
        var
            curCapacity: int;
            newCapacity: int;
            curContent: uchar_Array1d;
            newContent: uchar_Array1d;
        begin
            if newLength < 0 then begin
                raise BufferTooLargeError.create(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, '!error.buffer-too-large'));
            end;
            newContent := fldContent;
            curContent := newContent;
            curCapacity := system.length(curContent);
            if curCapacity <= 0 then begin
                if newLength >= CoInt.MAX_VALUE - $3f then begin
                    newCapacity := CoInt.MAX_VALUE;
                end else begin
                    newCapacity := newLength + $3f;
                end;
                newContent := &Array.newUChar1d(newCapacity);
                fldContent := newContent;
            end else
            if newLength > curCapacity then begin
                newCapacity := (curCapacity shl 1) or 1;
                if newCapacity < 0 then newCapacity := CoInt.MAX_VALUE;
                if newCapacity < newLength then begin
                    if newLength >= CoInt.MAX_VALUE - $3f then begin
                        newCapacity := CoInt.MAX_VALUE;
                    end else begin
                        newCapacity := newLength + $3f;
                    end;
                end;
                newContent := &Array.newUChar1d(newCapacity);
                &Array.copyPrimitives(curContent, 0, newContent, 0, curLength);
                fldContent := newContent;
            end;
            fldLength := newLength;
            result := newContent;
        end;

        procedure CharactersBuffer.close();
        begin
            destroy;
        end;

        procedure CharactersBuffer.write(ucharData: int);
        var
            curLength: int;
            newLength: int;
            locContent: uchar_Array1d;
        begin
            curLength := fldLength;
            newLength := curLength + 1;
            locContent := madeLarger(curLength, newLength);
            locContent[curLength] := uchar(ucharData);
        end;

        procedure CharactersBuffer.write(const src: uchar_Array1d);
        begin
            write(src, 0, system.length(src));
        end;

        procedure CharactersBuffer.write(const src: uchar_Array1d; offset, length: int);
        var
            curLength: int;
            newLength: int;
            locContent: uchar_Array1d;
        begin
            &Array.checkBounds(src, offset, length);
            if length > 0 then begin
                curLength := fldLength;
                newLength := curLength + length;
                locContent := madeLarger(curLength, newLength);
                &Array.copyPrimitives(src, offset, locContent, curLength, length);
            end;
        end;

        procedure CharactersBuffer.flush();
        begin
        end;

        procedure CharactersBuffer.getChars(beginIndex, endIndex: int; const dst: uchar_Array1d; offset: int);
        var
            locLength: int;
        begin
            checkBounds(beginIndex, endIndex);
            locLength := endIndex - beginIndex;
            &Array.checkBounds(dst, offset, locLength);
            &Array.copyPrimitives(fldContent, beginIndex, dst, offset, locLength);
        end;

        procedure CharactersBuffer.copyInto(const dst: uchar_Array1d; offset: int);
        var
            locLength: int;
        begin
            locLength := fldLength;
            &Array.checkBounds(dst, offset, locLength);
            &Array.copyPrimitives(fldContent, 0, dst, offset, locLength);
        end;

        procedure CharactersBuffer.discard();
        begin
            fldLength := 0;
        end;

        procedure CharactersBuffer.discard(charsQuantity: int);
        var
            locLength: int;
            locContent: uchar_Array1d;
        begin
            if charsQuantity > 0 then begin
                locLength := fldLength;
                if charsQuantity >= locLength then begin
                    fldLength := 0;
                    exit;
                end;
                locContent := fldContent;
                dec(locLength, charsQuantity);
                &Array.copyPrimitives(locContent, charsQuantity, locContent, 0, locLength);
                fldLength := locLength;
            end;
        end;

        function CharactersBuffer.toString(): AnsiString;
        begin
            result := (UnicodeString.create(fldContent, 0, fldLength)).toUTF8();
        end;

        function CharactersBuffer.getExtensions(): Extension_Array1d;
        begin
            result := nil;
        end;

        function CharactersBuffer.getExtension(const typ: ShortString): Extension;
        begin
            result := nil;
        end;

        function CharactersBuffer.toUCharArray(): uchar_Array1d;
        var
            locLength: int;
            locContent: uchar_Array1d;
        begin
            locLength := fldLength;
            locContent := &Array.newUChar1d(locLength);
            &Array.copyPrimitives(fldContent, 0, locContent, 0, locLength);
            result := locContent;
        end;
    {%endregion}

    {%region  CharactersStorage }
        constructor CharactersStorage.create(const content: UnicodeString);
        begin
            create(content.toUCharArray());
        end;

        constructor CharactersStorage.create(const content: uchar_Array1d);
        begin
            inherited create();
            fldLength := system.length(content);
            fldContent := content;
        end;

        constructor CharactersStorage.create(const content: uchar_Array1d; length: int);
        var
            alength: int;
        begin
            inherited create();
            if length > 0 then begin
                alength := system.length(content);
                if length < alength then begin
                    fldLength := length;
                end else begin
                    fldLength := alength;
                end;
                fldContent := content;
            end;
        end;

        constructor CharactersStorage.create(const content: uchar_Array1d; length, position: int);
        var
            alength: int;
            slength: int;
        begin
            inherited create();
            if length > 0 then begin
                alength := system.length(content);
                if length < alength then begin
                    slength := length;
                end else begin
                    slength := alength;
                end;
                fldLength := slength;
                if position < 0 then begin
                    fldPosition := 0;
                end else
                if position < slength then begin
                    fldPosition := position;
                end else begin
                    fldPosition := slength;
                end;
                fldContent := content;
            end;
        end;

        procedure CharactersStorage.close();
        begin
            destroy;
        end;

        function CharactersStorage.read(): int;
        var
            locPosition: int;
        begin
            locPosition := fldPosition;
            if locPosition >= fldLength then begin
                result := -1;
                exit;
            end;
            fldPosition := locPosition + 1;
            result := int(fldContent[locPosition]);
        end;

        function CharactersStorage.read(const dst: uchar_Array1d): int;
        begin
            result := read(dst, 0, system.length(dst));
        end;

        function CharactersStorage.read(const dst: uchar_Array1d; offset, length: int): int;
        var
            locPosition: int;
            locRemainder: int;
        begin
            &Array.checkBounds(dst, offset, length);
            if length <= 0 then begin
                result := 0;
                exit;
            end;
            locPosition := fldPosition;
            locRemainder := fldLength - locPosition;
            if locRemainder <= 0 then begin
                result := -1;
                exit;
            end;
            if length > locRemainder then length := locRemainder;
            &Array.copyPrimitives(fldContent, locPosition, dst, offset, length);
            fldPosition := locPosition + length;
            result := length;
        end;

        function CharactersStorage.skip(ucharsQuantity: long): long;
        var
            locLength: int;
            locPosition: int;
            locRemainder: int;
        begin
            if ucharsQuantity <= 0 then begin
                result := 0;
                exit;
            end;
            if ucharsQuantity < CoInt.MAX_VALUE then begin
                locLength := int(ucharsQuantity);
            end else begin
                locLength := CoInt.MAX_VALUE;
            end;
            locPosition := fldPosition;
            locRemainder := fldLength - locPosition;
            if locLength > locRemainder then locLength := locRemainder;
            fldPosition := locPosition + locLength;
            result := locLength;
        end;

        function CharactersStorage.available(): long;
        begin
            result := fldLength - fldPosition;
        end;

        function CharactersStorage.getExtensions(): Extension_Array1d;
        begin
            result := [ self as Extension ];
        end;

        function CharactersStorage.getExtension(const typ: ShortString): Extension;
        var
            ext: Extension;
        begin
            ext := self;
            if Lang.isInstance(ext, typ) then begin
                result := Extension(Lang.cast(ext, typ));
                exit;
            end;
            result := nil;
        end;
    {%endregion}

    {%region  CharDecoder }
        class function CharDecoder.toStream(src: ByteReader; length: int): ByteArrayInputStream;
        var
            aarray: byte_Array1d;
        begin
            aarray := &Array.newByte1d(length);
            length := src.read(aarray, 0, length);
            result := ByteArrayInputStream.create(aarray, length);
        end;

        procedure CharDecoder.setErrorAction(newErrorAction: ErrorAction);
        begin
            if (newErrorAction < eaIgnore) or (newErrorAction > eaException) then newErrorAction := eaReplace;
            fldErrorAction := newErrorAction;
        end;

        procedure CharDecoder.report();
        var
            handler: CharacterDecodingErrorHandler;
        begin
            handler := fldErrorHandler;
            if handler <> nil then handler.decodingError(self);
        end;

        constructor CharDecoder.create(charset: Charset);
        begin
            inherited create();
            fldErrorAction := eaReplace;
            fldCountersMonitor := Mutex.create();
            fldCharset := charset;
        end;

        destructor CharDecoder.destroy;
        begin
            fldCountersMonitor.free();
            inherited destroy;
        end;

        procedure CharDecoder.reset();
        var
            cmon: Mutex;
        begin
            cmon := fldCountersMonitor;
            cmon.beginSynchronized();
            try
                fldGivedChars := 0;
                fldAcceptedBytes := 0;
            finally
                cmon.endSynchronized();
            end;
        end;

        procedure CharDecoder.decodeToWriter(dst: UCharWriter; const src: byte_Array1d);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(decode(src, 0, length(src)));
        end;

        procedure CharDecoder.decodeToWriter(dst: UCharWriter; const src: byte_Array1d; offset, length: int);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(decode(src, offset, length));
        end;

        procedure CharDecoder.decodeToWriter(dst: UCharWriter; src: ByteReader; length: int);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(decode(src, length));
        end;

        function CharDecoder.givedChars(): long;
        begin
            result := fldGivedChars;
        end;

        function CharDecoder.acceptedBytes(): long;
        begin
            result := fldAcceptedBytes;
        end;

        function CharDecoder.decode(const src: byte_Array1d): uchar_Array1d;
        begin
            result := decode(src, 0, length(src));
        end;

        function CharDecoder.decode(const src: byte_Array1d; offset, length: int): uchar_Array1d;
        var
            deltaGivedChars: long;
            deltaAcceptedBytes: long;
            buffer: CharactersBuffer;
            stream: ByteArrayInputStream;
            cmon: Mutex;
        begin
            &Array.checkBounds(src, offset, length);
            if length <= 0 then begin
                result := &Array.newUChar1d(0);
                exit;
            end;
            buffer := CharactersBuffer.create();
            try
                stream := ByteArrayInputStream.create(src, offset + length, offset);
                try
                    decode(stream, length, buffer);
                    deltaAcceptedBytes := stream.position() - offset;
                finally
                    stream.free();
                end;
                deltaGivedChars := buffer.length;
                cmon := fldCountersMonitor;
                cmon.beginSynchronized();
                try
                    inc(fldGivedChars, deltaGivedChars);
                    inc(fldAcceptedBytes, deltaAcceptedBytes);
                finally
                    cmon.endSynchronized();
                end;
                result := buffer.toUCharArray();
            finally
                buffer.free();
            end;
        end;

        function CharDecoder.decode(src: ByteReader; length: int): uchar_Array1d;
        var
            offset: long;
            deltaGivedChars: long;
            deltaAcceptedBytes: long;
            seekable: SeekExtension;
            buffer: CharactersBuffer;
            stream: ByteArrayInputStream;
            cmon: Mutex;
        begin
            if src = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('src') ]));
            end;
            if length <= 0 then begin
                result := &Array.newUChar1d(0);
                exit;
            end;
            buffer := CharactersBuffer.create();
            try
                if Lang.isInstance(src, ByteArrayInputStream) then begin
                    stream := ByteArrayInputStream(Lang.cast(src, ByteArrayInputStream));
                    offset := stream.position();
                    decode(src, length, buffer);
                    deltaAcceptedBytes := stream.position() - offset;
                end else begin
                    seekable := SeekExtension(src.getExtension(SeekExtension));
                    if seekable <> nil then begin
                        offset := seekable.position();
                        decode(src, length, buffer);
                        deltaAcceptedBytes := seekable.position() - offset;
                    end else begin
                        stream := toStream(src, length);
                        try
                            decode(stream, length, buffer);
                            deltaAcceptedBytes := stream.position();
                        finally
                            stream.free();
                        end;
                    end;
                end;
                deltaGivedChars := buffer.length;
                cmon := fldCountersMonitor;
                cmon.beginSynchronized();
                try
                    inc(fldGivedChars, deltaGivedChars);
                    inc(fldAcceptedBytes, deltaAcceptedBytes);
                finally
                    cmon.endSynchronized();
                end;
                result := buffer.toUCharArray();
            finally
                buffer.free();
            end;
        end;
    {%endregion}

    {%region  CharEncoder }
        class function CharEncoder.toStream(src: UCharReader; length: int): CharactersStorage;
        var
            aarray: uchar_Array1d;
        begin
            aarray := &Array.newUChar1d(length);
            length := src.read(aarray, 0, length);
            result := CharactersStorage.create(aarray, length);
        end;

        procedure CharEncoder.setErrorAction(newErrorAction: ErrorAction);
        begin
            if (newErrorAction < eaIgnore) or (newErrorAction > eaException) then newErrorAction := eaReplace;
            fldErrorAction := newErrorAction;
        end;

        procedure CharEncoder.report();
        var
            handler: CharacterEncodingErrorHandler;
        begin
            handler := fldErrorHandler;
            if handler <> nil then handler.encodingError(self);
        end;

        constructor CharEncoder.create(charset: Charset);
        begin
            inherited create();
            fldErrorAction := eaReplace;
            fldCountersMonitor := Mutex.create();
            fldCharset := charset;
        end;

        destructor CharEncoder.destroy;
        begin
            fldCountersMonitor.free();
            inherited destroy;
        end;

        procedure CharEncoder.reset();
        var
            cmon: Mutex;
        begin
            cmon := fldCountersMonitor;
            cmon.beginSynchronized();
            try
                fldGivedBytes := 0;
                fldAcceptedChars := 0;
            finally
                cmon.endSynchronized();
            end;
        end;

        procedure CharEncoder.encodeToWriter(dst: ByteWriter; const src: UnicodeString);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(encode(src));
        end;

        procedure CharEncoder.encodeToWriter(dst: ByteWriter; const src: uchar_Array1d);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(encode(src, 0, length(src)));
        end;

        procedure CharEncoder.encodeToWriter(dst: ByteWriter; const src: uchar_Array1d; offset, length: int);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(encode(src, offset, length));
        end;

        procedure CharEncoder.encodeToWriter(dst: ByteWriter; src: UCharReader; length: int);
        begin
            if dst = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('dst') ]));
            end;
            dst.write(encode(src, length));
        end;

        function CharEncoder.canEncode(src: uchar): boolean;
        begin
            result := true;
        end;

        function CharEncoder.canEncode(const src: UnicodeString): boolean;
        var
            idx: int;
        begin
            for idx := 0 to length(src) - 1 do if not canEncode(src[idx + 1]) then begin
                result := false;
                exit;
            end;
            result := true;
        end;

        function CharEncoder.canEncode(const src: uchar_Array1d): boolean;
        begin
            result := canEncode(src, 0, length(src));
        end;

        function CharEncoder.canEncode(const src: uchar_Array1d; offset, length: int): boolean;
        var
            idx: int;
        begin
            &Array.checkBounds(src, offset, length);
            for idx := offset to offset + length - 1 do if not canEncode(src[idx]) then begin
                result := false;
                exit;
            end;
            result := true;
        end;

        function CharEncoder.givedBytes(): long;
        begin
            result := fldGivedBytes;
        end;

        function CharEncoder.acceptedChars(): long;
        begin
            result := fldAcceptedChars;
        end;

        function CharEncoder.encode(const src: UnicodeString): byte_Array1d;
        var
            length: int;
            deltaGivedBytes: long;
            deltaAcceptedChars: long;
            buffer: ByteArrayOutputStream;
            stream: CharactersStorage;
            cmon: Mutex;
        begin
            length := system.length(src);
            if length <= 0 then begin
                result := &Array.newByte1d(0);
                exit;
            end;
            buffer := ByteArrayOutputStream.create();
            try
                stream := CharactersStorage.create(src);
                try
                    encode(stream, length, buffer);
                    deltaAcceptedChars := stream.position;
                finally
                    stream.free();
                end;
                deltaGivedBytes := buffer.position();
                cmon := fldCountersMonitor;
                cmon.beginSynchronized();
                try
                    inc(fldGivedBytes, deltaGivedBytes);
                    inc(fldAcceptedChars, deltaAcceptedChars);
                finally
                    cmon.endSynchronized();
                end;
                result := buffer.toByteArray();
            finally
                buffer.free();
            end;
        end;

        function CharEncoder.encode(const src: uchar_Array1d): byte_Array1d;
        begin
            result := encode(src, 0, length(src));
        end;

        function CharEncoder.encode(const src: uchar_Array1d; offset, length: int): byte_Array1d;
        var
            deltaGivedBytes: long;
            deltaAcceptedChars: long;
            buffer: ByteArrayOutputStream;
            stream: CharactersStorage;
            cmon: Mutex;
        begin
            &Array.checkBounds(src, offset, length);
            if length <= 0 then begin
                result := &Array.newByte1d(0);
                exit;
            end;
            buffer := ByteArrayOutputStream.create();
            try
                stream := CharactersStorage.create(src, offset + length, offset);
                try
                    encode(stream, length, buffer);
                    deltaAcceptedChars := stream.position - offset;
                finally
                    stream.free();
                end;
                deltaGivedBytes := buffer.position();
                cmon := fldCountersMonitor;
                cmon.beginSynchronized();
                try
                    inc(fldGivedBytes, deltaGivedBytes);
                    inc(fldAcceptedChars, deltaAcceptedChars);
                finally
                    cmon.endSynchronized();
                end;
                result := buffer.toByteArray();
            finally
                buffer.free();
            end;
        end;

        function CharEncoder.encode(src: UCharReader; length: int): byte_Array1d;
        var
            offset: long;
            deltaGivedBytes: long;
            deltaAcceptedChars: long;
            seekable: SeekExtension;
            buffer: ByteArrayOutputStream;
            stream: CharactersStorage;
            cmon: Mutex;
        begin
            if src = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('src') ]));
            end;
            if length <= 0 then begin
                result := &Array.newByte1d(0);
                exit;
            end;
            buffer := ByteArrayOutputStream.create();
            try
                if Lang.isInstance(src, CharactersStorage) then begin
                    stream := CharactersStorage(Lang.cast(src, CharactersStorage));
                    offset := stream.position;
                    encode(src, length, buffer);
                    deltaAcceptedChars := stream.position - offset;
                end else begin
                    seekable := SeekExtension(src.getExtension(SeekExtension));
                    if seekable <> nil then begin
                        offset := seekable.position();
                        encode(src, length, buffer);
                        deltaAcceptedChars := seekable.position() - offset;
                    end else begin
                        stream := toStream(src, length);
                        try
                            encode(stream, length, buffer);
                            deltaAcceptedChars := stream.position;
                        finally
                            stream.free();
                        end;
                    end;
                end;
                deltaGivedBytes := buffer.position();
                cmon := fldCountersMonitor;
                cmon.beginSynchronized();
                try
                    inc(fldGivedBytes, deltaGivedBytes);
                    inc(fldAcceptedChars, deltaAcceptedChars);
                finally
                    cmon.endSynchronized();
                end;
                result := buffer.toByteArray();
            finally
                buffer.free();
            end;
        end;
    {%endregion}

    {%region  Charset }
        class procedure Charset.register(charsetRef: Charset);
        label
            break_label0;
        var
            index: int;
            count: int;
            charsetName: AnsiString;
            currentName: AnsiString;
            currentAliases: AnsiString_Array1d;
            charsetRegisteredData: Charset_Array1d;
            charsetRegisteredCopy: Charset_Array1d;
        begin
            if charsetRef = nil then begin
                raise NullPointerException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'null-pointer.argument'), [ CoAnsiString.create('charsetRef') ]));
            end;
            charsetName := charsetRef.fldName;
            charsetMonitor.beginSynchronized();
            try
                currentName := charsetName;
                if charsetRegisteredTable.contains(currentName) then begin
                    charsetName := currentName;
                    goto break_label0;
                end;
                currentAliases := charsetRef.fldAliases;
                for index := length(currentAliases) - 1 downto 0 do begin
                    currentName := currentAliases[index];
                    if charsetRegisteredTable.contains(currentName) then begin
                        charsetName := currentName;
                        goto break_label0;
                    end;
                end;
                count := charsetRegisteredCount;
                charsetRegisteredData := charsetRegisteredArray;
                if count = CoInt.MAX_VALUE then begin
                    raise BufferTooLargeError.create(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, '!error.buffer-too-large'));
                end;
                if count = length(charsetRegisteredData) then begin
                    charsetRegisteredCopy := Charset_Array1d(&Array.newTObject1d((count shl 1) or 1));
                    &Array.copyObjects(charsetRegisteredData, 0, charsetRegisteredCopy, 0, count);
                    charsetRegisteredData := charsetRegisteredCopy;
                    charsetRegisteredArray := charsetRegisteredData;
                end;
                charsetRegisteredData[count] := charsetRef;
                charsetRegisteredCount := count + 1;
                charsetRegisteredTable[charsetName] := charsetRef;
                for index := length(currentAliases) - 1 downto 0 do begin
                    charsetRegisteredTable[currentAliases[index]] := charsetRef;
                end;
                charsetName := '';
                break_label0:
            finally
                charsetMonitor.endSynchronized();
            end;
            if length(charsetName) > 0 then begin
                raise RegisteredCharsetNameException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.io.charset.UNIT_NAME, 'registered-charset-name'), [
                    CoAnsiString.create(charsetName)
                ]));
            end;
        end;

        class function Charset.isSupported(const charsetName: AnsiString): boolean;
        begin
            charsetMonitor.beginSynchronized();
            try
                result := charsetRegisteredTable.contains(charsetName);
            finally
                charsetMonitor.endSynchronized();
            end;
        end;

        class function Charset.enumerate(): Charset_Collection1d;
        begin
            result := CharsetCollection.create(charsetRegisteredArray, charsetRegisteredCount);
        end;

        class function Charset.getDefault(): Charset;
        begin
            result := charsetDefault;
        end;

        class function Charset.getConsoleInput(): Charset;
        {$IFDEF WINDOWS}
        var
            charsetName: AnsiString;
        {$ENDIF}
        begin
            {$IFDEF WINDOWS}
            if charsetConsoleInput = nil then begin
                charsetName := 'CP-' + CoInt.toString(int(windows.getConsoleCP()));
                charsetMonitor.beginSynchronized();
                try
                    if charsetConsoleInput = nil then begin
                        charsetConsoleInput := charsetRegisteredTable[charsetName];
                    end;
                finally
                    charsetMonitor.endSynchronized();
                end;
                if charsetConsoleInput = nil then begin
                    raise UnsupportedCharsetNameException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.io.charset.UNIT_NAME, 'unsupported-charset-name'), [
                        CoAnsiString.create(charsetName)
                    ]));
                end;
            end;
            result := charsetConsoleInput;
            {$ELSE}
            result := charsetDefault;
            {$ENDIF}
        end;

        class function Charset.getConsoleOutput(): Charset;
        {$IFDEF WINDOWS}
        var
            charsetName: AnsiString;
        {$ENDIF}
        begin
            {$IFDEF WINDOWS}
            if charsetConsoleOutput = nil then begin
                charsetName := 'CP-' + CoInt.toString(int(windows.getConsoleOutputCP()));
                charsetMonitor.beginSynchronized();
                try
                    if charsetConsoleOutput = nil then begin
                        charsetConsoleOutput := charsetRegisteredTable[charsetName];
                    end;
                finally
                    charsetMonitor.endSynchronized();
                end;
                if charsetConsoleOutput = nil then begin
                    raise UnsupportedCharsetNameException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.io.charset.UNIT_NAME, 'unsupported-charset-name'), [
                        CoAnsiString.create(charsetName)
                    ]));
                end;
            end;
            result := charsetConsoleOutput;
            {$ELSE}
            result := charsetDefault;
            {$ENDIF}
        end;

        class function Charset.get(const charsetName: AnsiString): Charset;
        var
            charsetFound: Charset;
        begin
            charsetMonitor.beginSynchronized();
            try
                charsetFound := charsetRegisteredTable[charsetName];
            finally
                charsetMonitor.endSynchronized();
            end;
            if charsetFound = nil then begin
                raise UnsupportedCharsetNameException.create(AnsiString.format(AResource.readUnitResourceAsAnsiString(pascalx.io.charset.UNIT_NAME, 'unsupported-charset-name'), [
                    CoAnsiString.create(charsetName)
                ]));
            end;
            result := charsetFound;
        end;

        constructor Charset.create(const name: AnsiString; const aliases: AnsiString_Array1d);
        label
            continue_label0;
        var
            ali: int;
            alj: int;
            sourcesLength: int;
            aliasesLength: int;
            aliasInstance: AnsiString;
            aliasesData: AnsiString_Array1d;
            aliasesCopy: AnsiString_Array1d;
        begin
            inherited create();
            aliasesLength := length(aliases);
            aliasesData := &Array.newAnsiString1d(aliasesLength);
            sourcesLength := aliasesLength;
            aliasesLength := 0;
            for ali := 0 to sourcesLength - 1 do begin
                aliasInstance := aliases[ali];
                if (length(aliasInstance) <= 0) or (aliasInstance = name) then goto continue_label0;
                for alj := aliasesLength - 1 downto 0 do if aliasInstance = aliasesData[alj] then goto continue_label0;
                aliasesData[aliasesLength] := aliasInstance;
                inc(aliasesLength);
                continue_label0:
            end;
            if aliasesLength <> length(aliasesData) then begin
                aliasesCopy := &Array.newAnsiString1d(aliasesLength);
                &Array.copyStrings(aliasesData, 0, aliasesCopy, 0, aliasesLength);
                aliasesData := aliasesCopy;
            end;
            fldName := name;
            fldAliases := aliasesData;
        end;

        function Charset.contains(charset: Charset): boolean;
        begin
            result := false;
        end;

        function Charset.displayName(): UnicodeString;
        begin
            result := fldName.toUTF16();
        end;

        function Charset.equals(anot: TObject): boolean;
        begin
            result := (anot = self) or (anot is Charset) and (Charset(anot).fldName = fldName);
        end;

        function Charset.getHashCode(): long;
        begin
            result := (CoAnsiString.create(fldName) as RefCountInterface).getHashCode();
        end;

        function Charset.toString(): AnsiString;
        begin
            result := 'charset ' + fldName;
        end;

        function Charset.isRegistered(): boolean;
        begin
            charsetMonitor.beginSynchronized();
            try
                result := charsetRegisteredTable[fldName] = self;
            finally
                charsetMonitor.endSynchronized();
            end;
        end;

        function Charset.isName(const charsetName: AnsiString): boolean;
        var
            index: int;
            locAliases: AnsiString_Array1d;
        begin
            if fldName = charsetName then begin
                result := true;
                exit;
            end;
            locAliases := fldAliases;
            for index := length(locAliases) - 1 downto 0 do if locAliases[index] = charsetName then begin
                result := true;
                exit;
            end;
            result := false;
        end;

        function Charset.decode(const src: byte_Array1d): uchar_Array1d;
        begin
            try
                with newDecoder() do try
                    result := decode(src);
                finally
                    free();
                end;
            except
                on e: RuntimeException do begin
                    raise;
                end;
                on e: Exception do begin
                    raise UnsupportedOperationException.create(e.message, e.helpContext);
                end;
            end;
        end;

        function Charset.decode(const src: byte_Array1d; offset, length: int): uchar_Array1d;
        begin
            try
                with newDecoder() do try
                    result := decode(src, offset, length);
                finally
                    free();
                end;
            except
                on e: RuntimeException do begin
                    raise;
                end;
                on e: Exception do begin
                    raise UnsupportedOperationException.create(e.message, e.helpContext);
                end;
            end;
        end;

        function Charset.encode(const src: uchar_Array1d): byte_Array1d;
        begin
            try
                with newEncoder() do try
                    result := encode(src);
                finally
                    free();
                end;
            except
                on e: RuntimeException do begin
                    raise;
                end;
                on e: Exception do begin
                    raise UnsupportedOperationException.create(e.message, e.helpContext);
                end;
            end;
        end;

        function Charset.encode(const src: uchar_Array1d; offset, length: int): byte_Array1d;
        begin
            try
                with newEncoder() do try
                    result := encode(src, offset, length);
                finally
                    free();
                end;
            except
                on e: RuntimeException do begin
                    raise;
                end;
                on e: Exception do begin
                    raise UnsupportedOperationException.create(e.message, e.helpContext);
                end;
            end;
        end;

        function Charset.aliases(): AnsiString_Array1d;
        var
            length: int;
            aliasesData: AnsiString_Array1d;
            aliasesCopy: AnsiString_Array1d;
        begin
            aliasesData := fldAliases;
            length := system.length(aliasesData);
            aliasesCopy := &Array.newAnsiString1d(length);
            &Array.copyStrings(aliasesData, 0, aliasesCopy, 0, length);
            result := aliasesCopy;
        end;
    {%endregion}

    {%region  CharsetCollection }
        constructor CharsetCollection.create(const charsets: Charset_Array1d; length: int);
        begin
            inherited create();
            fldLength := length;
            fldCharsets := charsets;
        end;

        procedure CharsetCollection.copyInto(const dstArray; dstOffset: int);
        begin
            &Array.copyObjects(fldCharsets, 0, dstArray, dstOffset, fldLength);
        end;

        function CharsetCollection.getLength(): int;
        begin
            result := fldLength;
        end;

        function CharsetCollection.componentAt(index: int): Charset;
        begin
            if (index < 0) or (index >= fldLength) then begin
                raise ArrayIndexOutOfBoundsException.create(AResource.readUnitResourceAsAnsiString(pascalx.lang.UNIT_NAME, 'out-of-bounds.array-index'));
            end;
            result := fldCharsets[index];
        end;

        function CharsetCollection.toArray(): Charset_Array1d;
        var
            length: int;
            charsetsData: Charset_Array1d;
            charsetsCopy: Charset_Array1d;
        begin
            charsetsData := fldCharsets;
            length := fldLength;
            charsetsCopy := Charset_Array1d(&Array.newTObject1d(length));
            &Array.copyObjects(charsetsData, 0, charsetsCopy, 0, length);
            result := charsetsCopy;
        end;
    {%endregion}

    {%region} initialization
        Lang.registerInterfaces([
            typeInfo(CharacterDecodingErrorHandler),
            typeInfo(CharacterEncodingErrorHandler)
        ]);

        charsetRegisteredArray := Charset_Array1d(&Array.newTObject1d($0f));
        charsetRegisteredTable := HashtableOfAnsiStringToCharset.create();
        charsetMonitor := Mutex.create();

        charsetDefault := UTF8.create();
        Charset.register(charsetDefault);
        Charset.register(UTF16BE.create());
        Charset.register(UTF16LE.create());
    {%endregion}

    {%region} finalization
        charsetMonitor.free();
        charsetRegisteredTable.free();
        charsetRegisteredArrayFinalize();
    {%endregion}

end.