// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; namespace SixLabors.Fonts.Unicode { /// /// Builder class to manipulate and generate a trie. /// This is useful for ICU data in primitive types. /// Provides a compact way to store information that is indexed by Unicode /// values, such as character properties, types, keyboard values, etc. /// This is very useful when you have a block of Unicode data that contains significant /// values while the rest of the Unicode data is unused in the application or /// when you have a lot of redundance, such as where all 21,000 Han ideographs /// have the same value. However, lookup is much faster than a hash table. /// A trie of any primitive data type serves two purposes: /// /// internal class UnicodeTrieBuilder { // These have been kept in the original format for now to aid porting // and testing. #pragma warning disable SA1310 // Field names should not contain underscore // Shift size for getting the index-1 table offset. internal const int UTRIE2_SHIFT_1 = 6 + 5; // Shift size for getting the index-2 table offset. internal const int UTRIE2_SHIFT_2 = 5; // Difference between the two shift sizes, // for getting an index-1 offset from an index-2 offset. 6=11-5 private const int UTRIE2_SHIFT_1_2 = UTRIE2_SHIFT_1 - UTRIE2_SHIFT_2; // Number of index-1 entries for the BMP. 32=0x20 // This part of the index-1 table is omitted from the serialized form. internal const int UTRIE2_OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> UTRIE2_SHIFT_1; // Number of code points per index-1 table entry. 2048=0x800 private const int UTRIE2_CP_PER_INDEX_1_ENTRY = 1 << UTRIE2_SHIFT_1; // Start with allocation of 16k data entries. private const int INITIAL_DATA_LENGTH = 1 << 14; // Grow about 8x each time. private const int UNEWTRIE2_MEDIUM_DATA_LENGTH = 1 << 17; private const int INDEX_1_LENGTH = 0x110000 >> UTRIE2_SHIFT_1; // Number of entries in a data block. 32=0x20 private const int UTRIE2_DATA_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_2; // Mask for getting the lower bits for the in-data-block offset. internal const int UTRIE2_DATA_MASK = UTRIE2_DATA_BLOCK_LENGTH - 1; // Shift size for shifting left the index array values. // Increases possible data size with 16-bit index values at the cost // of compactability. // This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY. internal const int UTRIE2_INDEX_SHIFT = 2; // The alignment size of a data block. Also the granularity for compaction. internal const int UTRIE2_DATA_GRANULARITY = 1 << UTRIE2_INDEX_SHIFT; // The BMP part of the index-2 table is fixed and linear and starts at offset 0. // Length=2048=0x800=0x10000>>UTRIE2_SHIFT_2. private const int UTRIE2_INDEX_2_OFFSET = 0; // The part of the index-2 table for U+D800..U+DBFF stores values for // lead surrogate code _units_ not code _points_. // Values for lead surrogate code _points_ are indexed with this portion of the table. // Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.) internal const int UTRIE2_LSCP_INDEX_2_OFFSET = 0x10000 >> UTRIE2_SHIFT_2; private const int UTRIE2_LSCP_INDEX_2_LENGTH = 0x400 >> UTRIE2_SHIFT_2; // Count the lengths of both BMP pieces. 2080=0x820 private const int UTRIE2_INDEX_2_BMP_LENGTH = UTRIE2_LSCP_INDEX_2_OFFSET + UTRIE2_LSCP_INDEX_2_LENGTH; // The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820. // Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2. private const int UTRIE2_UTF8_2B_INDEX_2_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH; private const int UTRIE2_UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; // U+0800 is the first code point after 2-byte UTF-8 // The index-1 table, only used for supplementary code points, at offset 2112=0x840. // Variable length, for code points up to highStart, where the last single-value range starts. // Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1. // (For 0x100000 supplementary code points U+10000..U+10ffff.) // // The part of the index-2 table for supplementary code points starts // after this index-1 table. // // Both the index-1 table and the following part of the index-2 table // are omitted completely if there is only BMP data. internal const int UTRIE2_INDEX_1_OFFSET = UTRIE2_UTF8_2B_INDEX_2_OFFSET + UTRIE2_UTF8_2B_INDEX_2_LENGTH; private const int UTRIE2_MAX_INDEX_1_LENGTH = 0x100000 >> UTRIE2_SHIFT_1; // Maximum length of the build-time index-2 array. // Maximum number of Unicode code points (0x110000) shifted right by UTRIE2_SHIFT_2, // plus the part of the index-2 table for lead surrogate code points, // plus the build-time index gap, // plus the null index-2 block. private const int UNEWTRIE2_MAX_INDEX_2_LENGTH = (0x110000 >> UTRIE2_SHIFT_2) + UTRIE2_LSCP_INDEX_2_LENGTH + UNEWTRIE2_INDEX_GAP_LENGTH + UTRIE2_INDEX_2_BLOCK_LENGTH; private const int UNEWTRIE2_INDEX_1_LENGTH = 0x110000 >> UTRIE2_SHIFT_1; // Number of entries in an index-2 block. 64=0x40 private const int UTRIE2_INDEX_2_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_1_2; // Mask for getting the lower bits for the in-index-2-block offset. internal const int UTRIE2_INDEX_2_MASK = UTRIE2_INDEX_2_BLOCK_LENGTH - 1; // At build time, leave a gap in the index-2 table, // at least as long as the maximum lengths of the 2-byte UTF-8 index-2 table // and the supplementary index-1 table. // Round up to UTRIE2_INDEX_2_BLOCK_LENGTH for proper compacting. private const int UNEWTRIE2_INDEX_GAP_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH; private const int UNEWTRIE2_INDEX_GAP_LENGTH = (UTRIE2_UTF8_2B_INDEX_2_LENGTH + UTRIE2_MAX_INDEX_1_LENGTH + UTRIE2_INDEX_2_MASK) & ~UTRIE2_INDEX_2_MASK; // Maximum length of the build-time data array. // One entry per 0x110000 code points, plus the illegal-UTF-8 block and the null block, // plus values for the 0x400 surrogate code units. private const int UNEWTRIE2_MAX_DATA_LENGTH = 0x110000 + 0x40 + 0x40 + 0x400; // The illegal-UTF-8 data block follows the ASCII block, at offset 128=0x80. // Used with linear access for single bytes 0..0xbf for simple error handling. // Length 64=0x40, not UTRIE2_DATA_BLOCK_LENGTH. private const int UTRIE2_BAD_UTF8_DATA_OFFSET = 0x80; // The start of non-linear-ASCII data blocks, at offset 192=0xc0. private const int UTRIE2_DATA_START_OFFSET = 0xc0; // The null data block. // Length 64=0x40 even if UTRIE2_DATA_BLOCK_LENGTH is smaller, // to work with 6-bit trail bytes from 2-byte UTF-8. private const int UNEWTRIE2_DATA_NULL_OFFSET = UTRIE2_DATA_START_OFFSET; // The null index-2 block, following the gap in the index-2 table. private const int UNEWTRIE2_INDEX_2_NULL_OFFSET = UNEWTRIE2_INDEX_GAP_OFFSET + UNEWTRIE2_INDEX_GAP_LENGTH; // The start of allocated index-2 blocks. private const int UNEWTRIE2_INDEX_2_START_OFFSET = UNEWTRIE2_INDEX_2_NULL_OFFSET + UTRIE2_INDEX_2_BLOCK_LENGTH; // The start of allocated data blocks. private const int UNEWTRIE2_DATA_START_OFFSET = UNEWTRIE2_DATA_NULL_OFFSET + 0x40; // The start of data blocks for U+0800 and above. // Below, compaction uses a block length of 64 for 2-byte UTF-8. // From here on, compaction uses UTRIE2_DATA_BLOCK_LENGTH. // Data values for 0x780 code points beyond ASCII. private const int UNEWTRIE2_DATA_0800_OFFSET = UNEWTRIE2_DATA_START_OFFSET + 0x780; // Maximum length of the runtime index array. // Limited by its own 16-bit index values, and by uint16_t UTrie2Header.indexLength. // (The actual maximum length is lower, // (0x110000>>UTRIE2_SHIFT_2)+UTRIE2_UTF8_2B_INDEX_2_LENGTH+UTRIE2_MAX_INDEX_1_LENGTH.) private const int UTRIE2_MAX_INDEX_LENGTH = 0xffff; // Maximum length of the runtime data array. // Limited by 16-bit index values that are left-shifted by UTRIE2_INDEX_SHIFT, // and by uint16_t UTrie2Header.shiftedDataLength. private const int UTRIE2_MAX_DATA_LENGTH = 0xffff << UTRIE2_INDEX_SHIFT; #pragma warning restore SA1310 // Field names should not contain underscore private readonly uint initialValue; private readonly uint errorValue; private int highStart; private uint[] data; private int dataCapacity; private readonly int[] index1; private readonly int[] index2; private int firstFreeBlock; private bool isCompacted; private readonly int[] map; private int dataNullOffset; private int dataLength; private int index2NullOffset; private int index2Length; /// /// Initializes a new instance of the class. /// /// The initial value that is set for all code points. /// The value for out-of-range code points and illegal UTF-8. public UnicodeTrieBuilder(uint initialValue = 0, uint errorValue = 0) { this.initialValue = initialValue; this.errorValue = errorValue; this.highStart = 0x110000; this.index1 = new int[INDEX_1_LENGTH]; this.index2 = new int[UNEWTRIE2_MAX_INDEX_2_LENGTH]; this.data = new uint[INITIAL_DATA_LENGTH]; this.dataCapacity = INITIAL_DATA_LENGTH; this.firstFreeBlock = 0; this.isCompacted = false; // Multi-purpose per-data-block table. // // Before compacting: // // Per-data-block reference counters/free-block list. // 0: unused // >0: reference counter (number of index-2 entries pointing here) // <0: next free data block in free-block list // // While compacting: // // Map of adjusted indexes, used in compactData() and compactIndex2(). // Maps from original indexes to new ones. this.map = new int[UNEWTRIE2_MAX_DATA_LENGTH >> UTRIE2_SHIFT_2]; // preallocate and reset // - ASCII // - the bad-UTF-8-data block // - the null data block int i; for (i = 0; i < 0x80; ++i) { this.data[i] = initialValue; } for (; i < 0xc0; ++i) { this.data[i] = errorValue; } for (i = UNEWTRIE2_DATA_NULL_OFFSET; i < UNEWTRIE2_DATA_START_OFFSET; ++i) { this.data[i] = initialValue; } this.dataNullOffset = UNEWTRIE2_DATA_NULL_OFFSET; this.dataLength = UNEWTRIE2_DATA_START_OFFSET; // set the index-2 indexes for the 2=0x80>>UTRIE2_SHIFT_2 ASCII data blocks int j; for (i = 0, j = 0; j < 0x80; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) { this.index2[i] = j; this.map[i] = 1; } // reference counts for the bad-UTF-8-data block */ for (; j < 0xc0; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) { this.map[i] = 0; } // Reference counts for the null data block: all blocks except for the ASCII blocks. // Plus 1 so that we don't drop this block during compaction. // Plus as many as needed for lead surrogate code points. // i==newdataNullOffset this.map[i++] = (0x110000 >> UTRIE2_SHIFT_2) - (0x80 >> UTRIE2_SHIFT_2) + 1 + UTRIE2_LSCP_INDEX_2_LENGTH; j += UTRIE2_DATA_BLOCK_LENGTH; for (; j < UNEWTRIE2_DATA_START_OFFSET; ++i, j += UTRIE2_DATA_BLOCK_LENGTH) { this.map[i] = 0; } // set the remaining indexes in the BMP index-2 block // to the null data block for (i = 0x80 >> UTRIE2_SHIFT_2; i < UTRIE2_INDEX_2_BMP_LENGTH; ++i) { this.index2[i] = UNEWTRIE2_DATA_NULL_OFFSET; } // Fill the index gap with impossible values so that compaction // does not overlap other index-2 blocks with the gap. for (i = 0; i < UNEWTRIE2_INDEX_GAP_LENGTH; ++i) { this.index2[UNEWTRIE2_INDEX_GAP_OFFSET + i] = -1; } // set the indexes in the null index-2 block for (i = 0; i < UTRIE2_INDEX_2_BLOCK_LENGTH; ++i) { this.index2[UNEWTRIE2_INDEX_2_NULL_OFFSET + i] = UNEWTRIE2_DATA_NULL_OFFSET; } this.index2NullOffset = UNEWTRIE2_INDEX_2_NULL_OFFSET; this.index2Length = UNEWTRIE2_INDEX_2_START_OFFSET; // set the index-1 indexes for the linear index-2 block for (i = 0, j = 0; i < UTRIE2_OMITTED_BMP_INDEX_1_LENGTH; ++i, j += UTRIE2_INDEX_2_BLOCK_LENGTH) { this.index1[i] = j; } // set the remaining index-1 indexes to the null index-2 block for (; i < UNEWTRIE2_INDEX_1_LENGTH; ++i) { this.index1[i] = UNEWTRIE2_INDEX_2_NULL_OFFSET; } // Preallocate and reset data for U+0080..U+07ff, // for 2-byte UTF-8 which will be compacted in 64-blocks // even if UTRIE2_DATA_BLOCK_LENGTH is smaller. for (i = 0x80; i < 0x800; i += UTRIE2_DATA_BLOCK_LENGTH) { this.Set(i, initialValue); } } /// /// Gets the value for a code point as stored in the trie. /// /// The code point. /// The value. public uint Get(int c) => this.Get(c, true); /// /// Sets a value for a given code point. /// /// The code point. /// The value. /// Invalid codepoint. /// Already compacted. public void Set(int codePoint, uint value) { if (codePoint is < 0 or > 0x10ffff) { throw new ArgumentOutOfRangeException(nameof(codePoint)); } if (this.isCompacted) { throw new InvalidOperationException("Already compacted"); } int block = this.GetDataBlock(codePoint, true); this.data[block + (codePoint & UTRIE2_DATA_MASK)] = value; } /// /// Set a value in a range of code points [start..end]. /// All code points c with start <= c <= end will get the value if /// is or if the old value is the /// initial value. /// /// The first code point to get the value. /// The last code point to get the value (inclusive). /// The value. /// Whether old non-initial values are to be overwritten. /// Invalid codepoint. /// Already compacted. public void SetRange(int start, int end, uint value, bool overwrite) { if ((start > 0x10ffff) || (end > 0x10ffff) || start > end) { throw new ArgumentOutOfRangeException(nameof(start)); } if (this.isCompacted) { throw new InvalidOperationException("Already compacted"); } if (!overwrite && value == this.initialValue) { return; // Nothing to do. } int block; int rest; int repeatBlock; int limit = end + 1; if ((start & UTRIE2_DATA_MASK) != 0) { int nextStart; // set partial block at [start..following block boundary[ block = this.GetDataBlock(start, true); nextStart = (start + UTRIE2_DATA_MASK) & ~UTRIE2_DATA_MASK; if (nextStart <= limit) { this.FillBlock(block, start & UTRIE2_DATA_MASK, UTRIE2_DATA_BLOCK_LENGTH, value, this.initialValue, overwrite); start = nextStart; } else { this.FillBlock(block, start & UTRIE2_DATA_MASK, limit & UTRIE2_DATA_MASK, value, this.initialValue, overwrite); return; } } // number of positions in the last, partial block rest = limit & UTRIE2_DATA_MASK; // round down limit to a block boundary limit &= ~UTRIE2_DATA_MASK; // iterate over all-value blocks if (value == this.initialValue) { repeatBlock = this.dataNullOffset; } else { repeatBlock = -1; } while (start < limit) { int i2; bool setRepeatBlock = false; if (value == this.initialValue && this.IsInNullBlock(start, true)) { start += UTRIE2_DATA_BLOCK_LENGTH; // nothing to do continue; } // get index value i2 = this.GetIndex2Block(start, true); i2 += (start >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; block = this.index2[i2]; if (this.IsWritableBlock(block)) { // already allocated if (overwrite && block >= UNEWTRIE2_DATA_0800_OFFSET) { // We overwrite all values, and it's not a // protected (ASCII-linear or 2-byte UTF-8) block: // replace with the repeatBlock. setRepeatBlock = true; } else { // !overwrite, or protected block: just write the values into this block this.FillBlock(block, 0, UTRIE2_DATA_BLOCK_LENGTH, value, this.initialValue, overwrite); } } else if (this.data[block] != value && (overwrite || block == this.dataNullOffset)) { // Set the repeatBlock instead of the null block or previous repeat block: // // If !isWritableBlock() then all entries in the block have the same value // because it's the null block or a range block (the repeatBlock from a previous // call to utrie2_setRange32()). // No other blocks are used multiple times before compacting. // // The null block is the only non-writable block with the initialValue because // of the repeatBlock initialization above. (If value==initialValue, then // the repeatBlock will be the null data block.) // // We set our repeatBlock if the desired value differs from the block's value, // and if we overwrite any data or if the data is all initial values // (which is the same as the block being the null block, see above). setRepeatBlock = true; } if (setRepeatBlock) { if (repeatBlock >= 0) { this.SetIndex2Entry(i2, repeatBlock); } else { // create and set and fill the repeatBlock repeatBlock = this.GetDataBlock(start, true); this.WriteBlock(repeatBlock, value); } } start += UTRIE2_DATA_BLOCK_LENGTH; } if (rest > 0) { // set partial block at [last block boundary..limit[ block = this.GetDataBlock(start, true); this.FillBlock(block, 0, rest, value, this.initialValue, overwrite); } } /// /// Compacts the data and populates an optimized readonly Trie. /// /// The . /// Trie data is too large. public UnicodeTrie Freeze() { int allIndexesLength, i; if (!this.isCompacted) { this.CompactTrie(); } if (this.highStart <= 0x10000) { allIndexesLength = UTRIE2_INDEX_1_OFFSET; } else { allIndexesLength = this.index2Length; } int dataMove = allIndexesLength; // are indexLength and dataLength within limits? if ((allIndexesLength > UTRIE2_MAX_INDEX_LENGTH) // for unshifted indexLength || ((dataMove + this.dataNullOffset) > 0xffff) // for unshifted dataNullOffset || ((dataMove + UNEWTRIE2_DATA_0800_OFFSET) > 0xffff) // for unshifted 2-byte UTF-8 index-2 values || ((dataMove + this.dataLength) > UTRIE2_MAX_DATA_LENGTH)) { // for shiftedDataLength throw new InvalidOperationException("Trie data is too large."); } // calculate the sizes of, and allocate, the index and data arrays int indexLength = allIndexesLength + this.dataLength; uint[] data32 = new uint[indexLength]; // write the index-2 array values shifted right by UTRIE2_INDEX_SHIFT, after adding dataMove int destIdx = 0; for (i = 0; i < UTRIE2_INDEX_2_BMP_LENGTH; i++) { data32[destIdx++] = (uint)((this.index2[i] + dataMove) >> UTRIE2_INDEX_SHIFT); } // write UTF-8 2-byte index-2 values, not right-shifted for (i = 0; i < 0xc2 - 0xc0; i++) { // C0..C1 data32[destIdx++] = (uint)(dataMove + UTRIE2_BAD_UTF8_DATA_OFFSET); } for (; i < 0xe0 - 0xc0; i++) { // C2..DF data32[destIdx++] = (uint)(dataMove + this.index2[i << (6 - UTRIE2_SHIFT_2)]); } if (this.highStart > 0x10000) { int index1Length = (this.highStart - 0x10000) >> UTRIE2_SHIFT_1; int index2Offset = UTRIE2_INDEX_2_BMP_LENGTH + UTRIE2_UTF8_2B_INDEX_2_LENGTH + index1Length; // write 16-bit index-1 values for supplementary code points for (i = 0; i < index1Length; i++) { data32[destIdx++] = (uint)(UTRIE2_INDEX_2_OFFSET + this.index1[i + UTRIE2_OMITTED_BMP_INDEX_1_LENGTH]); } // write the index-2 array values for supplementary code points, // shifted right by INDEX_SHIFT, after adding dataMove for (i = 0; i < this.index2Length - index2Offset; i++) { data32[destIdx++] = (uint)((dataMove + this.index2[index2Offset + i]) >> UTRIE2_INDEX_SHIFT); } } // write 16-bit data values for (i = 0; i < this.dataLength; i++) { data32[destIdx++] = this.data[i]; } return new UnicodeTrie(data32, this.highStart, this.errorValue); } private uint Get(int c, bool fromLSCP) { if (c is < 0 or > 0x10ffff) { return this.errorValue; } int i2; int block; if (c >= this.highStart && (!U_IS_LEAD(c) || fromLSCP)) { return this.data[this.dataLength - UTRIE2_DATA_GRANULARITY]; } if (U_IS_LEAD(c) && fromLSCP) { i2 = UTRIE2_LSCP_INDEX_2_OFFSET - (0xd800 >> UTRIE2_SHIFT_2) + (c >> UTRIE2_SHIFT_2); } else { i2 = this.index1[c >> UTRIE2_SHIFT_1] + ((c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK); } block = this.index2[i2]; return this.data[block + (c & UTRIE2_DATA_MASK)]; } private int GetDataBlock(int c, bool forLSCP) { int i2 = this.GetIndex2Block(c, forLSCP); i2 += (c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; int oldBlock = this.index2[i2]; if (this.IsWritableBlock(oldBlock)) { return oldBlock; } // allocate a new data block int newBlock = this.AllocDataBlock(oldBlock); this.SetIndex2Entry(i2, newBlock); return newBlock; } private int GetIndex2Block(int c, bool forLSCP) { if (U_IS_LEAD(c) && forLSCP) { return UTRIE2_LSCP_INDEX_2_OFFSET; } int i1 = c >> UTRIE2_SHIFT_1; int i2 = this.index1[i1]; if (i2 == this.index2NullOffset) { i2 = this.AllocIndex2Block(); this.index1[i1] = i2; } return i2; } /// /// Is this code point a lead surrogate (U+d800..U+dbff)? /// /// The code point. /// The . private static bool U_IS_LEAD(int c) => (c & 0xfffffc00) == 0xd800; private bool IsWritableBlock(int block) => block != this.dataNullOffset && this.map[block >> UTRIE2_SHIFT_2] == 1; private bool IsInNullBlock(int c, bool forLSCP) { int i2, block; if (U_IS_LEAD(c) && forLSCP) { i2 = UTRIE2_LSCP_INDEX_2_OFFSET - (0xd800 >> UTRIE2_SHIFT_2) + (c >> UTRIE2_SHIFT_2); } else { i2 = this.index1[c >> UTRIE2_SHIFT_1] + ((c >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK); } block = this.index2[i2]; return block == this.dataNullOffset; } private void SetIndex2Entry(int i2, int block) { int oldBlock; // increment first, in case block==oldBlock! ++this.map[block >> UTRIE2_SHIFT_2]; oldBlock = this.index2[i2]; if (--this.map[oldBlock >> UTRIE2_SHIFT_2] == 0) { this.ReleaseDataBlock(oldBlock); } this.index2[i2] = block; } // call when the block's reference counter reaches 0 private void ReleaseDataBlock(int block) { // put this block at the front of the free-block chain this.map[block >> UTRIE2_SHIFT_2] = -this.firstFreeBlock; this.firstFreeBlock = block; } private int AllocDataBlock(int copyBlock) { int newBlock, newTop; if (this.firstFreeBlock != 0) { // get the first free block newBlock = this.firstFreeBlock; this.firstFreeBlock = -this.map[newBlock >> UTRIE2_SHIFT_2]; } else { // get a new block from the high end newBlock = this.dataLength; newTop = newBlock + UTRIE2_DATA_BLOCK_LENGTH; if (newTop > this.dataCapacity) { // out of memory in the data array. int capacity; uint[] newData; if (this.dataCapacity < UNEWTRIE2_MEDIUM_DATA_LENGTH) { capacity = UNEWTRIE2_MEDIUM_DATA_LENGTH; } else if (this.dataCapacity < UNEWTRIE2_MAX_DATA_LENGTH) { capacity = UNEWTRIE2_MAX_DATA_LENGTH; } else { // Should never occur. // Either UNEWTRIE2_MAX_DATA_LENGTH is incorrect, // or the code writes more values than should be possible. throw new InvalidOperationException(nameof(capacity)); } newData = new uint[capacity]; Array.Copy(this.data, newData, this.dataLength); this.data = newData; this.dataCapacity = capacity; } this.dataLength = newTop; } Array.Copy(this.data, copyBlock, this.data, newBlock, UTRIE2_DATA_BLOCK_LENGTH); this.map[newBlock >> UTRIE2_SHIFT_2] = 0; return newBlock; } private int AllocIndex2Block() { int newBlock, newTop; newBlock = this.index2Length; newTop = newBlock + UTRIE2_INDEX_2_BLOCK_LENGTH; if (newTop > this.index2.Length) { // Should never occur. // Either UTRIE2_MAX_BUILD_TIME_INDEX_LENGTH is incorrect, // or the code writes more values than should be possible. throw new InvalidOperationException(nameof(newTop)); } this.index2Length = newTop; Array.Copy(this.index2, this.index2NullOffset, this.index2, newBlock, UTRIE2_INDEX_2_BLOCK_LENGTH); return newBlock; } private int FindSameIndex2Block(int index2Length, int otherBlock) { // ensure that we do not even partially get past index2Length index2Length -= UTRIE2_INDEX_2_BLOCK_LENGTH; for (int block = 0; block <= index2Length; ++block) { if (Equal(this.index2, block, otherBlock, UTRIE2_INDEX_2_BLOCK_LENGTH)) { return block; } } return -1; } private int FindSameDataBlock(int dataLength, int otherBlock, int blockLength) { // ensure that we do not even partially get past dataLength dataLength -= blockLength; for (int block = 0; block <= dataLength; block += UTRIE2_DATA_GRANULARITY) { if (Equal(this.data, block, otherBlock, blockLength)) { return block; } } return -1; } // Find the start of the last range in the trie by enumerating backward. // Indexes for supplementary code points higher than this will be omitted. private int FindHighStart(uint highValue) { uint[] data32; uint value, initialValue; int c, prev; int i1, i2, j, i2Block, prevI2Block, index2NullOffset, block, prevBlock, nullBlock; data32 = this.data; initialValue = this.initialValue; index2NullOffset = this.index2NullOffset; nullBlock = this.dataNullOffset; /* set variables for previous range */ if (highValue == initialValue) { prevI2Block = index2NullOffset; prevBlock = nullBlock; } else { prevI2Block = -1; prevBlock = -1; } prev = 0x110000; // enumerate index-2 blocks i1 = UNEWTRIE2_INDEX_1_LENGTH; c = prev; while (c > 0) { i2Block = this.index1[--i1]; if (i2Block == prevI2Block) { // the index-2 block is the same as // the previous one, and filled with highValue c -= UTRIE2_CP_PER_INDEX_1_ENTRY; continue; } prevI2Block = i2Block; if (i2Block == index2NullOffset) { // this is the null index-2 block if (highValue != initialValue) { return c; } c -= UTRIE2_CP_PER_INDEX_1_ENTRY; } else { // enumerate data blocks for one index-2 block for (i2 = UTRIE2_INDEX_2_BLOCK_LENGTH; i2 > 0;) { block = this.index2[i2Block + --i2]; if (block == prevBlock) { // the block is the same as the previous one, and filled with highValue c -= UTRIE2_DATA_BLOCK_LENGTH; continue; } prevBlock = block; if (block == nullBlock) { // this is the null data block if (highValue != initialValue) { return c; } c -= UTRIE2_DATA_BLOCK_LENGTH; } else { for (j = UTRIE2_DATA_BLOCK_LENGTH; j > 0;) { value = data32[block + --j]; if (value != highValue) { return c; } --c; } } } } } // deliver last range return 0; } // initialValue is ignored if overwrite=TRUE private void FillBlock(int block, int start, int limit, uint value, uint initialValue, bool overwrite) { int pLimit = block + limit; block += start; if (overwrite) { while (block < pLimit) { this.data[block++] = value; } } else { while (block < pLimit) { if (this.data[block] == initialValue) { this.data[block] = value; } ++block; } } } private void WriteBlock(int block, uint value) { int limit = block + UTRIE2_DATA_BLOCK_LENGTH; while (block < limit) { this.data[block++] = value; } } private void CompactTrie() { // find highStart and round it up uint highValue = this.Get(0x10ffff); int localHighStart = this.FindHighStart(highValue); localHighStart = (localHighStart + (UTRIE2_CP_PER_INDEX_1_ENTRY - 1)) & ~(UTRIE2_CP_PER_INDEX_1_ENTRY - 1); if (localHighStart == 0x110000) { highValue = this.errorValue; } // Set highStart only after Get(trie, highStart). // Otherwise Get(highStart) would try to read the highValue. this.highStart = localHighStart; if (localHighStart < 0x110000) { // Blank out [highStart..10ffff] to release associated data blocks. int suppHighStart = this.highStart <= 0x10000 ? 0x10000 : this.highStart; this.SetRange(suppHighStart, 0x10ffff, this.initialValue, true); } this.CompactData(); if (this.highStart > 0x10000) { this.CompactIndex2(); } // Store the highValue in the data array and round up the dataLength. // Must be done after compactData() because that assumes that dataLength // is a multiple of UTRIE2_DATA_BLOCK_LENGTH. this.data[this.dataLength++] = highValue; while ((this.dataLength & (UTRIE2_DATA_GRANULARITY - 1)) != 0) { this.data[this.dataLength++] = this.initialValue; } this.isCompacted = true; } // Compact a build-time trie. // // The compaction // - removes blocks that are identical with earlier ones // - overlaps adjacent blocks as much as possible (if overlap==TRUE) // - moves blocks in steps of the data granularity // - moves and overlaps blocks that overlap with multiple values in the overlap region // // It does not // - try to move and overlap blocks that are not already adjacent private void CompactData() { int start, newStart, movedStart; int blockLength, overlap; int i, mapIndex, blockCount; // do not compact linear-ASCII data newStart = UTRIE2_DATA_START_OFFSET; for (start = 0, i = 0; start < newStart; start += UTRIE2_DATA_BLOCK_LENGTH, ++i) { this.map[i] = start; } // Start with a block length of 64 for 2-byte UTF-8, // then switch to UTRIE2_DATA_BLOCK_LENGTH. blockLength = 64; blockCount = blockLength >> UTRIE2_SHIFT_2; for (start = newStart; start < this.dataLength;) { // start: index of first entry of current block // newStart: index where the current block is to be moved // (right after current end of already-compacted data) if (start == UNEWTRIE2_DATA_0800_OFFSET) { blockLength = UTRIE2_DATA_BLOCK_LENGTH; blockCount = 1; } // skip blocks that are not used if (this.map[start >> UTRIE2_SHIFT_2] <= 0) { // advance start to the next block start += blockLength; // leave newStart with the previous block! continue; } // search for an identical block if ((movedStart = this.FindSameDataBlock(newStart, start, blockLength)) >= 0) { // found an identical block, set the other block's index value for the current block for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) { this.map[mapIndex++] = movedStart; movedStart += UTRIE2_DATA_BLOCK_LENGTH; } // advance start to the next block start += blockLength; // leave newStart with the previous block! continue; } // see if the beginning of this block can be overlapped with the end of the previous block // look for maximum overlap (modulo granularity) with the previous, adjacent block overlap = blockLength - UTRIE2_DATA_GRANULARITY; while (overlap > 0 && !Equal(this.data, newStart - overlap, start, overlap)) { overlap -= UTRIE2_DATA_GRANULARITY; } if (overlap > 0 || newStart < start) { // some overlap, or just move the whole block movedStart = newStart - overlap; for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) { this.map[mapIndex++] = movedStart; movedStart += UTRIE2_DATA_BLOCK_LENGTH; } // move the non-overlapping indexes to their new positions start += overlap; for (i = blockLength - overlap; i > 0; --i) { this.data[newStart++] = this.data[start++]; } } else { // no overlap && newStart==start for (i = blockCount, mapIndex = start >> UTRIE2_SHIFT_2; i > 0; --i) { this.map[mapIndex++] = start; start += UTRIE2_DATA_BLOCK_LENGTH; } newStart = start; } } // now adjust the index-2 table for (i = 0; i < this.index2Length; ++i) { if (i == UNEWTRIE2_INDEX_GAP_OFFSET) { // Gap indexes are invalid (-1). Skip over the gap. i += UNEWTRIE2_INDEX_GAP_LENGTH; } this.index2[i] = this.map[this.index2[i] >> UTRIE2_SHIFT_2]; } this.dataNullOffset = this.map[this.dataNullOffset >> UTRIE2_SHIFT_2]; // ensure dataLength alignment while ((newStart & (UTRIE2_DATA_GRANULARITY - 1)) != 0) { this.data[newStart++] = this.initialValue; } this.dataLength = newStart; } private void CompactIndex2() { int i, start, newStart, movedStart, overlap; // do not compact linear-BMP index-2 blocks newStart = UTRIE2_INDEX_2_BMP_LENGTH; for (start = 0, i = 0; start < newStart; start += UTRIE2_INDEX_2_BLOCK_LENGTH, ++i) { this.map[i] = start; } // Reduce the index table gap to what will be needed at runtime. newStart += UTRIE2_UTF8_2B_INDEX_2_LENGTH + ((this.highStart - 0x10000) >> UTRIE2_SHIFT_1); for (start = UNEWTRIE2_INDEX_2_NULL_OFFSET; start < this.index2Length;) { // start: index of first entry of current block // newStart: index where the current block is to be moved // (right after current end of already-compacted data) // // search for an identical block if ((movedStart = this.FindSameIndex2Block(newStart, start)) >= 0) { // found an identical block, set the other block's index value for the current block this.map[start >> UTRIE2_SHIFT_1_2] = movedStart; // advance start to the next block start += UTRIE2_INDEX_2_BLOCK_LENGTH; // leave newStart with the previous block! continue; } // see if the beginning of this block can be overlapped with the end of the previous block // look for maximum overlap with the previous, adjacent block for (overlap = UTRIE2_INDEX_2_BLOCK_LENGTH - 1; overlap > 0 && !Equal(this.index2, newStart - overlap, start, overlap); --overlap) { } if (overlap > 0 || newStart < start) { // some overlap, or just move the whole block this.map[start >> UTRIE2_SHIFT_1_2] = newStart - overlap; // move the non-overlapping indexes to their new positions start += overlap; for (i = UTRIE2_INDEX_2_BLOCK_LENGTH - overlap; i > 0; --i) { this.index2[newStart++] = this.index2[start++]; } } else { // no overlap && newStart==start this.map[start >> UTRIE2_SHIFT_1_2] = start; start += UTRIE2_INDEX_2_BLOCK_LENGTH; newStart = start; } } // now adjust the index-1 table for (i = 0; i < UNEWTRIE2_INDEX_1_LENGTH; ++i) { this.index1[i] = this.map[this.index1[i] >> UTRIE2_SHIFT_1_2]; } this.index2NullOffset = this.map[this.index2NullOffset >> UTRIE2_SHIFT_1_2]; // Ensure data table alignment: // Needs to be granularity-aligned for 16-bit trie // (so that dataMove will be down-shiftable), // and 2-aligned for uint32_t data. while ((newStart & ((UTRIE2_DATA_GRANULARITY - 1) | 1)) != 0) { // Arbitrary value: 0x3fffc not possible for real data. this.index2[newStart++] = 0xffff << UTRIE2_INDEX_SHIFT; } this.index2Length = newStart; } private static bool Equal(uint[] a, int s, int t, int length) { for (int i = 0; i < length; i++) { if (a[s + i] != a[t + i]) { return false; } } return true; } private static bool Equal(int[] a, int s, int t, int length) { for (int i = 0; i < length; i++) { if (a[s + i] != a[t + i]) { return false; } } return true; } } }