diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index 77dc4abba..63d2a39fa 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -4,7 +4,7 @@ using System.Linq; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.BZip2MT.InputStream; using SharpCompress.Compressors.Deflate; using SharpCompress.Compressors.Deflate64; using SharpCompress.Compressors.Explode; @@ -122,7 +122,7 @@ protected Stream CreateDecompressionStream(Stream stream, ZipCompressionMethod m } case ZipCompressionMethod.BZip2: { - return new BZip2Stream(stream, CompressionMode.Decompress, false); + return new BZip2ParallelInputStream(stream); } case ZipCompressionMethod.LZMA: { diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockCompressor.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockCompressor.cs new file mode 100644 index 000000000..d8e703f44 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockCompressor.cs @@ -0,0 +1,270 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// Compresses and writes a single BZip2 block + /// + /// Block encoding consists of the following stages: + /// 1. Run-Length Encoding[1] - write() + /// 2. Burrows Wheeler Transform - close() (through BZip2DivSufSort) + /// 3. Write block header - close() + /// 4. Move To Front Transform - close() (through BZip2HuffmanStageEncoder) + /// 5. Run-Length Encoding[2] - close() (through BZip2HuffmanStageEncoder) + /// 6. Create and write Huffman tables - close() (through BZip2HuffmanStageEncoder) + /// 7. Huffman encode and write data - close() (through BZip2HuffmanStageEncoder) + /// + internal class BZip2BlockCompressor + { + // The stream to which compressed BZip2 data is written + private readonly IBZip2BitOutputStream bitOutputStream; + + // CRC builder for the block + private readonly CRC32 crc = new CRC32(); + + // The RLE'd block data + private readonly byte[] block; + + // Current length of the data within the block array + private int blockLength; + + // A limit beyond which new data will not be accepted into the block + private readonly int blockLengthLimit; + + // The values that are present within the RLE'd block data. For each index, true if that + // value is present within the data, otherwise false + private readonly bool[] blockValuesPresent = new bool[256]; + + // The Burrows Wheeler Transformed block data + private readonly int[] bwtBlock; + + // The current RLE value being accumulated (undefined when rleLength is 0) + private int rleCurrentValue = -1; + + // The repeat count of the current RLE value + private int rleLength; + + /// + /// Determines if any bytes have been written to the block. + /// True if one or more bytes has been written to the block, otherwise false. + /// + public bool IsEmpty + { + get { return ((this.blockLength == 0) && (this.rleLength == 0)); } + } + + /// + /// Gets the CRC of the completed block. Only valid after calling Close(). + /// + public uint CRC + { + get { return this.crc.CRC; } + } + + /// + /// Public constructor + /// + /// The BZip2BitOutputStream to which compressed BZip2 data is written + /// The declared block size in bytes. Up to this many bytes will be accepted + /// into the block after Run-Length Encoding is applied + public BZip2BlockCompressor(IBZip2BitOutputStream bitOutputStream, int blockSize) + { + this.bitOutputStream = bitOutputStream; + + // One extra byte is added to allow for the block wrap applied in close() + this.block = new byte[blockSize + 1]; + this.bwtBlock = new int[blockSize + 1]; + this.blockLengthLimit = blockSize - 6; // 5 bytes for one RLE run plus one byte - see Write(int) + } + + /// + /// Writes a byte to the block, accumulating to an RLE run where possible + /// + /// The byte to write + /// True if the byte was written, or false if the block is already full + public bool Write(int value) + { + if (this.blockLength > this.blockLengthLimit) + return false; + + if (this.rleLength == 0) + { + this.rleCurrentValue = value; + this.rleLength = 1; + } + else if (this.rleCurrentValue != value) + { + // This path commits us to write 6 bytes - one RLE run (5 bytes) plus one extra + this.WriteRun(this.rleCurrentValue & 0xff, this.rleLength); + this.rleCurrentValue = value; + this.rleLength = 1; + } + else + { + if (this.rleLength == 254) + { + this.WriteRun(this.rleCurrentValue & 0xff, 255); + this.rleLength = 0; + } + else + { + this.rleLength++; + } + } + + return true; + } + + /// + /// Writes an array to the block + /// + /// The array to write + /// The offset within the input data to write from + /// The number of bytes of input data to write + /// The actual number of input bytes written. May be less than the number requested, or + /// zero if the block is already full + public int Write(byte[] data, int offset, int length) + { + var written = 0; + + while (length-- > 0) + { + if (!this.Write(data[offset++])) + break; + written++; + } + + return written; + } + + /// Compresses and writes out the block. + /// Exception on any I/O error writing the data + public void CloseBlock() + { + // If an RLE run is in progress, write it out + if (this.rleLength > 0) + this.WriteRun(this.rleCurrentValue & 0xff, this.rleLength); + + // Apply a one byte block wrap required by the BWT implementation + this.block[this.blockLength] = this.block[0]; + + // Perform the Burrows Wheeler Transform + var divSufSort = new BZip2DivSufSort(this.block, this.bwtBlock, this.blockLength); + var bwtStartPointer = divSufSort.BWT(); + + // Write out the block header + this.bitOutputStream.WriteBits(24, BZip2Constants.BLOCK_HEADER_MARKER_1); + this.bitOutputStream.WriteBits(24, BZip2Constants.BLOCK_HEADER_MARKER_2); + this.bitOutputStream.WriteInteger(this.crc.CRC); + this.bitOutputStream.WriteBoolean(false); // Randomised block flag. We never create randomised blocks + this.bitOutputStream.WriteBits(24, (uint)bwtStartPointer); + + // Write out the symbol map + this.WriteSymbolMap(); + + // Perform the Move To Front Transform and Run-Length Encoding[2] stages + var mtfEncoder = new BZip2MTFAndRLE2StageEncoder( + this.bwtBlock, + this.blockLength, + this.blockValuesPresent + ); + mtfEncoder.Encode(); + + // Perform the Huffman Encoding stage and write out the encoded data + var huffmanEncoder = new BZip2HuffmanStageEncoder( + this.bitOutputStream, + mtfEncoder.MtfBlock, + mtfEncoder.MtfLength, + mtfEncoder.MtfAlphabetSize, + mtfEncoder.MtfSymbolFrequencies + ); + huffmanEncoder.Encode(); + } + + /// + /// Write the Huffman symbol to output byte map + /// + /// on any I/O error writing the data + private void WriteSymbolMap() + { + var condensedInUse = new bool[16]; + + for (var i = 0; i < 16; i++) + { + for (int j = 0, k = i << 4; j < 16; j++, k++) + { + if (this.blockValuesPresent[k]) + { + condensedInUse[i] = true; + } + } + } + + for (var i = 0; i < 16; i++) + { + this.bitOutputStream.WriteBoolean(condensedInUse[i]); + } + + for (var i = 0; i < 16; i++) + { + if (condensedInUse[i]) + { + for (int j = 0, k = i * 16; j < 16; j++, k++) + { + this.bitOutputStream.WriteBoolean(this.blockValuesPresent[k]); + } + } + } + } + + /// + /// Writes an RLE run to the block array, updating the block CRC and present values array as required + /// + /// The value to write + /// The run length of the value to write + private void WriteRun(int value, int runLength) + { + this.blockValuesPresent[value] = true; + this.crc.UpdateCrc(value, runLength); + + var byteValue = (byte)value; + switch (runLength) + { + case 1: + this.block[this.blockLength] = byteValue; + this.blockLength = this.blockLength + 1; + break; + + case 2: + this.block[this.blockLength] = byteValue; + this.block[this.blockLength + 1] = byteValue; + this.blockLength = this.blockLength + 2; + break; + + case 3: + this.block[this.blockLength] = byteValue; + this.block[this.blockLength + 1] = byteValue; + this.block[this.blockLength + 2] = byteValue; + this.blockLength = this.blockLength + 3; + break; + + default: + runLength -= 4; + this.blockValuesPresent[runLength] = true; + this.block[this.blockLength] = byteValue; + this.block[this.blockLength + 1] = byteValue; + this.block[this.blockLength + 2] = byteValue; + this.block[this.blockLength + 3] = byteValue; + this.block[this.blockLength + 4] = (byte)runLength; + this.blockLength = this.blockLength + 5; + break; + } + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockDecompressor.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockDecompressor.cs new file mode 100644 index 000000000..b1c4bb2c4 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2BlockDecompressor.cs @@ -0,0 +1,980 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// Reads and decompresses a single BZip2 block + /// + /// Block decoding consists of the following stages: + /// 1. Read block header - BZip2BlockDecompressor() + /// 2. Read Huffman tables - readHuffmanTables() + /// 3. Read and decode Huffman encoded data - decodeHuffmanData() + /// 4. Run-Length Decoding[2] - decodeHuffmanData() + /// 5. Inverse Move To Front Transform - decodeHuffmanData() + /// 6. Inverse Burrows Wheeler Transform - initialiseInverseBWT() + /// 7. Run-Length Decoding[1] - read() + /// 8. Optional Block De-Randomisation - read() (through decodeNextBWTByte()) + /// + internal class BZip2BlockDecompressor + { + /// + /// The BZip2 specification originally included the optional addition of a slight pseudo-random + /// perturbation to the input data, in order to work around the block sorting algorithm's non- + /// optimal performance on some types of input. The current mainline bzip2 does not require this + /// and will not create randomised blocks, but compatibility is still required for old data (and + /// third party compressors that haven't caught up). When decompressing a randomised block, for + /// each value N in this array, a 1 will be XOR'd onto the output of the Burrows-Wheeler + /// transform stage after N bytes, then the next N taken from the following entry. + /// + private static readonly int[] RNUMS = + { + 619, + 720, + 127, + 481, + 931, + 816, + 813, + 233, + 566, + 247, + 985, + 724, + 205, + 454, + 863, + 491, + 741, + 242, + 949, + 214, + 733, + 859, + 335, + 708, + 621, + 574, + 73, + 654, + 730, + 472, + 419, + 436, + 278, + 496, + 867, + 210, + 399, + 680, + 480, + 51, + 878, + 465, + 811, + 169, + 869, + 675, + 611, + 697, + 867, + 561, + 862, + 687, + 507, + 283, + 482, + 129, + 807, + 591, + 733, + 623, + 150, + 238, + 59, + 379, + 684, + 877, + 625, + 169, + 643, + 105, + 170, + 607, + 520, + 932, + 727, + 476, + 693, + 425, + 174, + 647, + 73, + 122, + 335, + 530, + 442, + 853, + 695, + 249, + 445, + 515, + 909, + 545, + 703, + 919, + 874, + 474, + 882, + 500, + 594, + 612, + 641, + 801, + 220, + 162, + 819, + 984, + 589, + 513, + 495, + 799, + 161, + 604, + 958, + 533, + 221, + 400, + 386, + 867, + 600, + 782, + 382, + 596, + 414, + 171, + 516, + 375, + 682, + 485, + 911, + 276, + 98, + 553, + 163, + 354, + 666, + 933, + 424, + 341, + 533, + 870, + 227, + 730, + 475, + 186, + 263, + 647, + 537, + 686, + 600, + 224, + 469, + 68, + 770, + 919, + 190, + 373, + 294, + 822, + 808, + 206, + 184, + 943, + 795, + 384, + 383, + 461, + 404, + 758, + 839, + 887, + 715, + 67, + 618, + 276, + 204, + 918, + 873, + 777, + 604, + 560, + 951, + 160, + 578, + 722, + 79, + 804, + 96, + 409, + 713, + 940, + 652, + 934, + 970, + 447, + 318, + 353, + 859, + 672, + 112, + 785, + 645, + 863, + 803, + 350, + 139, + 93, + 354, + 99, + 820, + 908, + 609, + 772, + 154, + 274, + 580, + 184, + 79, + 626, + 630, + 742, + 653, + 282, + 762, + 623, + 680, + 81, + 927, + 626, + 789, + 125, + 411, + 521, + 938, + 300, + 821, + 78, + 343, + 175, + 128, + 250, + 170, + 774, + 972, + 275, + 999, + 639, + 495, + 78, + 352, + 126, + 857, + 956, + 358, + 619, + 580, + 124, + 737, + 594, + 701, + 612, + 669, + 112, + 134, + 694, + 363, + 992, + 809, + 743, + 168, + 974, + 944, + 375, + 748, + 52, + 600, + 747, + 642, + 182, + 862, + 81, + 344, + 805, + 988, + 739, + 511, + 655, + 814, + 334, + 249, + 515, + 897, + 955, + 664, + 981, + 649, + 113, + 974, + 459, + 893, + 228, + 433, + 837, + 553, + 268, + 926, + 240, + 102, + 654, + 459, + 51, + 686, + 754, + 806, + 760, + 493, + 403, + 415, + 394, + 687, + 700, + 946, + 670, + 656, + 610, + 738, + 392, + 760, + 799, + 887, + 653, + 978, + 321, + 576, + 617, + 626, + 502, + 894, + 679, + 243, + 440, + 680, + 879, + 194, + 572, + 640, + 724, + 926, + 56, + 204, + 700, + 707, + 151, + 457, + 449, + 797, + 195, + 791, + 558, + 945, + 679, + 297, + 59, + 87, + 824, + 713, + 663, + 412, + 693, + 342, + 606, + 134, + 108, + 571, + 364, + 631, + 212, + 174, + 643, + 304, + 329, + 343, + 97, + 430, + 751, + 497, + 314, + 983, + 374, + 822, + 928, + 140, + 206, + 73, + 263, + 980, + 736, + 876, + 478, + 430, + 305, + 170, + 514, + 364, + 692, + 829, + 82, + 855, + 953, + 676, + 246, + 369, + 970, + 294, + 750, + 807, + 827, + 150, + 790, + 288, + 923, + 804, + 378, + 215, + 828, + 592, + 281, + 565, + 555, + 710, + 82, + 896, + 831, + 547, + 261, + 524, + 462, + 293, + 465, + 502, + 56, + 661, + 821, + 976, + 991, + 658, + 869, + 905, + 758, + 745, + 193, + 768, + 550, + 608, + 933, + 378, + 286, + 215, + 979, + 792, + 961, + 61, + 688, + 793, + 644, + 986, + 403, + 106, + 366, + 905, + 644, + 372, + 567, + 466, + 434, + 645, + 210, + 389, + 550, + 919, + 135, + 780, + 773, + 635, + 389, + 707, + 100, + 626, + 958, + 165, + 504, + 920, + 176, + 193, + 713, + 857, + 265, + 203, + 50, + 668, + 108, + 645, + 990, + 626, + 197, + 510, + 357, + 358, + 850, + 858, + 364, + 936, + 638, + }; + + // Maximum possible number of Huffman table selectors + private const int HUFFMAN_MAXIMUM_SELECTORS = + (900000 / BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH) + 1; + + // Provides bits of input to decode + private readonly IBZip2BitInputStream bitInputStream; + + // Calculates the block CRC from the fully decoded bytes of the block + private readonly CRC32 crc = new CRC32(); + + // The CRC of the current block as read from the block header + private readonly uint blockCRC; + + // true if the current block is randomised, otherwise false + private readonly bool blockRandomised; + + // + // Huffman Decoding stage + // + + // The end-of-block Huffman symbol. Decoding of the block ends when this is encountered + private int huffmanEndOfBlockSymbol; + + /// + /// A map from Huffman symbol index to output character. Some types of data (e.g. ASCII text) + /// may contain only a limited number of byte values; Huffman symbols are only allocated to + /// those values that actually occur in the uncompressed data. + /// + private readonly byte[] huffmanSymbolMap = new byte[256]; + + // + // Move To Front stage + // + + /// + /// Counts of each byte value within the bwtTransformedArray data. Collected at the Move + /// To Front stage, consumed by the Inverse Burrows Wheeler Transform stage + /// + private readonly int[] bwtByteCounts = new int[256]; + + /// + /// The Burrows-Wheeler Transform processed data. Read at the Move To Front stage, consumed by the + /// Inverse Burrows Wheeler Transform stage + /// + private byte[] bwtBlock; + + // + // Inverse Burrows-Wheeler Transform stage + // + + /// + /// At each position contains the union of :- + /// An output character (8 bits) + /// A pointer from each position to its successor (24 bits, left shifted 8 bits) + /// As the pointer cannot exceed the maximum block size of 900k, 24 bits is more than enough to + /// hold it; Folding the character data into the spare bits while performing the inverse BWT, + /// when both pieces of information are available, saves a large number of memory accesses in + /// the final decoding stages. + /// + private int[] bwtMergedPointers = Array.Empty(); + + // The current merged pointer into the Burrow-Wheeler Transform array + private int bwtCurrentMergedPointer; + + /// + /// The actual length in bytes of the current block at the Inverse Burrows Wheeler Transform + /// stage (before final Run-Length Decoding) + /// + private int bwtBlockLength; + + // The number of output bytes that have been decoded up to the Inverse Burrows Wheeler Transform stage + private int bwtBytesDecoded; + + // + // Run-Length Encoding and Random Perturbation stage + // + + // The most recently RLE decoded byte + private int rleLastDecodedByte = -1; + + /// + /// The number of previous identical output bytes decoded. After 4 identical bytes, the next byte + /// decoded is an RLE repeat count + /// + private int rleAccumulator; + + // The RLE repeat count of the current decoded byte. When this reaches zero, a new byte is decoded + private int rleRepeat; + + // If the current block is randomised, the position within the RNUMS randomisation array + private int randomIndex; + + // If the current block is randomised, the remaining count at the current RNUMS position + private int randomCount = BZip2BlockDecompressor.RNUMS[0] - 1; + + // Minimum number of alternative Huffman tables + public const int HUFFMAN_MINIMUM_TABLES = 2; + + // Maximum number of alternative Huffman tables + public const int HUFFMAN_MAXIMUM_TABLES = 6; + + /// + /// Read and decode the block's Huffman tables + /// @return A decoder for the Huffman stage that uses the decoded tables + /// + /// if the input stream reaches EOF before all table data has been read + private BZip2HuffmanStageDecoder ReadHuffmanTables() + { + var tableCodeLengths = new byte[ + BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES, + BZip2MTFAndRLE2StageEncoder.HUFFMAN_MAXIMUM_ALPHABET_SIZE + ]; + + // Read Huffman symbol to output byte map + uint huffmanUsedRanges = this.bitInputStream.ReadBits(16); + int huffmanSymbolCount = 0; + + for (int i = 0; i < 16; i++) + { + if ((huffmanUsedRanges & ((1 << 15) >> i)) == 0) + continue; + for (int j = 0, k = i << 4; j < 16; j++, k++) + { + if (this.bitInputStream.ReadBoolean()) + { + this.huffmanSymbolMap[huffmanSymbolCount++] = (byte)k; + } + } + } + var endOfBlockSymbol = huffmanSymbolCount + 1; + this.huffmanEndOfBlockSymbol = endOfBlockSymbol; + + // Read total number of tables and selectors + uint totalTables = this.bitInputStream.ReadBits(3); + uint totalSelectors = this.bitInputStream.ReadBits(15); + if ( + (totalTables < BZip2BlockDecompressor.HUFFMAN_MINIMUM_TABLES) + || (totalTables > BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES) + || (totalSelectors < 1) + || (totalSelectors > BZip2BlockDecompressor.HUFFMAN_MAXIMUM_SELECTORS) + ) + { + throw new IOException("BZip2 block Huffman tables invalid"); + } + + // Read and decode MTFed Huffman selector list + var tableMTF = new MoveToFront(); + var selectors = new byte[totalSelectors]; + for (var selector = 0; selector < totalSelectors; selector++) + { + selectors[selector] = tableMTF.IndexToFront((int)this.bitInputStream.ReadUnary()); + } + + // Read the Canonical Huffman code lengths for each table + for (var table = 0; table < totalTables; table++) + { + int currentLength = (int)this.bitInputStream.ReadBits(5); + for (var i = 0; i <= endOfBlockSymbol; i++) + { + while (this.bitInputStream.ReadBoolean()) + { + currentLength += this.bitInputStream.ReadBoolean() ? -1 : 1; + } + tableCodeLengths[table, i] = (byte)currentLength; + } + } + + return new BZip2HuffmanStageDecoder( + this.bitInputStream, + endOfBlockSymbol + 1, + tableCodeLengths, + selectors + ); + } + + /// + /// Reads the Huffman encoded data from the input stream, performs Run-Length Decoding and + /// applies the Move To Front transform to reconstruct the Burrows-Wheeler Transform array + /// + /// The Huffman decoder through which symbols are read + /// if an end-of-block symbol was not decoded within the declared block size + private void DecodeHuffmanData(BZip2HuffmanStageDecoder huffmanDecoder) + { + var symbolMTF = new MoveToFront(); + int _bwtBlockLength = 0; + int repeatCount = 0; + int repeatIncrement = 1; + int mtfValue = 0; + + while (true) + { + var nextSymbol = huffmanDecoder.NextSymbol(); + + if (nextSymbol == BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNA) + { + repeatCount += repeatIncrement; + repeatIncrement <<= 1; + } + else if (nextSymbol == BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNB) + { + repeatCount += repeatIncrement << 1; + repeatIncrement <<= 1; + } + else + { + byte nextByte; + if (repeatCount > 0) + { + if (_bwtBlockLength + repeatCount > this.bwtBlock.Length) + throw new IOException("BZip2 block exceeds declared block size"); + + nextByte = this.huffmanSymbolMap[mtfValue]; + this.bwtByteCounts[nextByte & 0xff] += repeatCount; + while (--repeatCount >= 0) + { + this.bwtBlock[_bwtBlockLength++] = nextByte; + } + + repeatCount = 0; + repeatIncrement = 1; + } + + if (nextSymbol == this.huffmanEndOfBlockSymbol) + break; + + if (_bwtBlockLength >= this.bwtBlock.Length) + throw new IOException("BZip2 block exceeds declared block size"); + + mtfValue = symbolMTF.IndexToFront(nextSymbol - 1) & 0xff; + + nextByte = this.huffmanSymbolMap[mtfValue]; + this.bwtByteCounts[nextByte & 0xff]++; + this.bwtBlock[_bwtBlockLength++] = nextByte; + } + } + + this.bwtBlockLength = _bwtBlockLength; + } + + /// + /// Set up the Inverse Burrows-Wheeler Transform merged pointer array + /// + /// The start pointer into the BWT array + /// if the given start pointer is invalid + private void InitialiseInverseBWT(uint bwtStartPointer) + { + var _bwtMergedPointers = new int[this.bwtBlockLength]; + var characterBase = new int[256]; + + if ((bwtStartPointer < 0) || (bwtStartPointer >= this.bwtBlockLength)) + throw new IOException("BZip2 start pointer invalid"); + + // Cumulatise character counts + Array.ConstrainedCopy(this.bwtByteCounts, 0, characterBase, 1, 255); + for (int i = 2; i <= 255; i++) + { + characterBase[i] += characterBase[i - 1]; + } + + // Merged-Array Inverse Burrows-Wheeler Transform + // Combining the output characters and forward pointers into a single array here, where we + // have already read both of the corresponding values, cuts down on memory accesses in the + // final walk through the array + for (int i = 0; i < this.bwtBlockLength; i++) + { + int value = this.bwtBlock[i] & 0xff; + _bwtMergedPointers[characterBase[value]++] = (i << 8) + value; + } + + //this.bwtBlock = null; + this.bwtMergedPointers = _bwtMergedPointers; + this.bwtCurrentMergedPointer = _bwtMergedPointers[bwtStartPointer]; + } + + /// + /// Decodes a byte from the Burrows-Wheeler Transform stage. If the block has randomisation + /// applied, reverses the randomisation + /// + /// The decoded byte + private int decodeNextBWTByte() + { + int nextDecodedByte = this.bwtCurrentMergedPointer & 0xff; + this.bwtCurrentMergedPointer = this.bwtMergedPointers[ + this.bwtCurrentMergedPointer >> 8 + ]; + + if (this.blockRandomised) + { + if (--this.randomCount == 0) + { + nextDecodedByte ^= 1; + this.randomIndex = (this.randomIndex + 1) % 512; + this.randomCount = BZip2BlockDecompressor.RNUMS[this.randomIndex]; + } + } + + this.bwtBytesDecoded++; + + return nextDecodedByte; + } + + /// + /// Public constructor + /// + /// The BZip2BitInputStream to read from + /// The maximum decoded size of the block + /// If the block could not be decoded + public BZip2BlockDecompressor(IBZip2BitInputStream bitInputStream, uint blockSize) + { + this.bitInputStream = bitInputStream; + this.bwtBlock = new byte[blockSize]; + + // Read block header + this.blockCRC = this.bitInputStream.ReadInteger(); //.ReadBits(32); + this.blockRandomised = this.bitInputStream.ReadBoolean(); + uint bwtStartPointer = this.bitInputStream.ReadBits(24); + + // Read block data and decode through to the Inverse Burrows Wheeler Transform stage + var huffmanDecoder = this.ReadHuffmanTables(); + this.DecodeHuffmanData(huffmanDecoder); + this.InitialiseInverseBWT(bwtStartPointer); + } + + /// + /// Decodes a byte from the final Run-Length Encoding stage, pulling a new byte from the + /// Burrows-Wheeler Transform stage when required + /// + /// The decoded byte, or -1 if there are no more bytes + public int Read() + { + while (this.rleRepeat < 1) + { + if (this.bwtBytesDecoded == this.bwtBlockLength) + return -1; + + int nextByte = this.decodeNextBWTByte(); + if (nextByte != this.rleLastDecodedByte) + { + // New byte, restart accumulation + this.rleLastDecodedByte = nextByte; + this.rleRepeat = 1; + this.rleAccumulator = 1; + this.crc.UpdateCrc(nextByte); + } + else + { + if (++this.rleAccumulator == 4) + { + // Accumulation complete, start repetition + int _rleRepeat = this.decodeNextBWTByte() + 1; + this.rleRepeat = _rleRepeat; + this.rleAccumulator = 0; + this.crc.UpdateCrc(nextByte, _rleRepeat); + } + else + { + this.rleRepeat = 1; + this.crc.UpdateCrc(nextByte); + } + } + } + + this.rleRepeat--; + + return this.rleLastDecodedByte; + } + + /// + /// Decodes multiple bytes from the final Run-Length Encoding stage, pulling new bytes from the + /// Burrows-Wheeler Transform stage when required + /// + /// The array to write to + /// The starting position within the array + /// The number of bytes to read + /// The number of bytes actually read, or -1 if there are no bytes left in the block + public int Read(byte[] destination, int offset, int length) + { + int i; + for (i = 0; i < length; i++, offset++) + { + var decoded = this.Read(); + if (decoded == -1) + return (i == 0) ? -1 : i; + + destination[offset] = (byte)decoded; + } + return i; + } + + public int Read(Stream x, int maxLength) + { + int i; + for (i = 0; i < maxLength; i++) + { + var decoded = this.Read(); + if (decoded == -1) + return (i == 0) ? -1 : i; + + x.WriteByte((byte)decoded); + } + return i; + } + + public int ReadAll(Stream x) + { + int count = 0; + while (true) + { + var decoded = this.Read(); + if (decoded == -1) + return (count == 0) ? -1 : count; + count++; + x.WriteByte((byte)decoded); + } + } + + /// + /// Verify and return the block CRC. This method may only be called after all of the block's bytes have been read + /// + /// The block CRC + /// if the CRC verification failed + public uint CheckCrc() + { + if (this.blockCRC != this.crc.CRC) + throw new IOException("BZip2 block CRC error"); + + return this.crc.CRC; + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2DivSufSort.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2DivSufSort.cs new file mode 100644 index 000000000..6d52a1ab6 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2DivSufSort.cs @@ -0,0 +1,2981 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// + /// DivSufSort suffix array generator + /// Based on libdivsufsort 1.2.3 patched to support BZip2 + /// + /// + /// This is a simple conversion of the original C with two minor bugfixes applied(see "BUGFIX" + /// comments within the class). Documentation within the class is largely absent. + /// + internal class BZip2DivSufSort + { + private class StackEntry + { + public readonly int a; + public readonly int b; + public readonly int c; + public readonly int d; + + public StackEntry(int a, int b, int c, int d) + { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + } + + private class PartitionResult + { + public readonly int first; + public readonly int last; + + public PartitionResult(int first, int last) + { + this.first = first; + this.last = last; + } + } + + private class TRBudget + { + private int budget; + public int chance; + + public bool update(int size, int n) + { + this.budget -= n; + if (this.budget <= 0) + { + if (--this.chance == 0) + { + return false; + } + this.budget += size; + } + + return true; + } + + public TRBudget(int budget, int chance) + { + this.budget = budget; + this.chance = chance; + } + } + + private const int STACK_SIZE = 64; + private const int BUCKET_A_SIZE = 256; + private const int BUCKET_B_SIZE = 65536; + private const int SS_BLOCKSIZE = 1024; + private const int INSERTIONSORT_THRESHOLD = 8; + + private static readonly int[] log2table = + { + -1, + 0, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + }; + + private readonly int[] SA; + private readonly byte[] T; + private readonly int n; + + /// + /// Public constructor + /// + /// T The input array + /// SA The output array + /// The length of the input data + public BZip2DivSufSort(byte[] T, int[] SA, int n) + { + this.T = T; + this.SA = SA; + this.n = n; + } + + /// + /// Performs a Burrows Wheeler Transform on the input array + /// + /// the index of the first character of the input array within the output array + public int BWT() + { + if (this.n == 0) + return 0; + + if (this.n == 1) + { + this.SA[0] = this.T[0]; + return 0; + } + + var bucketA = new int[BZip2DivSufSort.BUCKET_A_SIZE]; + var bucketB = new int[BZip2DivSufSort.BUCKET_B_SIZE]; + + int m = this.sortTypeBstar(bucketA, bucketB); + return 0 < m ? this.constructBWT(bucketA, bucketB) : 0; + } + + // ReSharper disable LoopVariableIsNeverChangedInsideLoop + private static void swapElements(int[] array1, int index1, int[] array2, int index2) + { + var temp = array1[index1]; + array1[index1] = array2[index2]; + array2[index2] = temp; + } + + private int ssCompare(int p1, int p2, int depth) + { + int U1n, + U2n; // pointers within T + int U1, + U2; + + for ( + U1 = depth + this.SA[p1], U2 = depth + this.SA[p2], U1n = this.SA[p1 + 1] + 2, U2n = + this.SA[p2 + 1] + 2; + (U1 < U1n) && (U2 < U2n) && (this.T[U1] == this.T[U2]); + ++U1, ++U2 + ) { } + + return U1 < U1n + ? (U2 < U2n ? (this.T[U1] & 0xff) - (this.T[U2] & 0xff) : 1) + : (U2 < U2n ? -1 : 0); + } + + private int ssCompareLast(int PA, int p1, int p2, int depth, int size) + { + int U1, + U2, + U1n, + U2n; + + for ( + U1 = depth + this.SA[p1], U2 = depth + this.SA[p2], U1n = size, U2n = + this.SA[(p2 + 1)] + 2; + (U1 < U1n) && (U2 < U2n) && (this.T[U1] == this.T[U2]); + ++U1, ++U2 + ) { } + + if (U1 < U1n) + return (U2 < U2n) ? (this.T[U1] & 0xff) - (this.T[U2] & 0xff) : 1; + + if (U2 == U2n) + return 1; + + for ( + U1 = U1 % size, U1n = this.SA[PA] + 2; + (U1 < U1n) && (U2 < U2n) && (this.T[U1] == this.T[U2]); + ++U1, ++U2 + ) { } + + return U1 < U1n + ? (U2 < U2n ? (this.T[U1] & 0xff) - (this.T[U2] & 0xff) : 1) + : (U2 < U2n ? -1 : 0); + } + + private void ssInsertionSort(int PA, int first, int last, int depth) + { + int i; // pointer within SA + + for (i = last - 2; first <= i; --i) + { + int j; // pointer within SA + int t; + int r; + for ( + t = this.SA[i], j = i + 1; + 0 < (r = this.ssCompare(PA + t, PA + this.SA[j], depth)); + + ) + { + do + { + this.SA[j - 1] = this.SA[j]; + } while ((++j < last) && (this.SA[j] < 0)); + if (last <= j) + { + break; + } + } + if (r == 0) + { + this.SA[j] = ~this.SA[j]; + } + this.SA[j - 1] = t; + } + } + + private void ssFixdown(int Td, int PA, int sa, int i, int size) + { + int j, + k; + int v; + int c; + + for ( + v = this.SA[sa + i], c = (this.T[Td + this.SA[PA + v]]) & 0xff; + (j = 2 * i + 1) < size; + this.SA[sa + i] = this.SA[sa + k], i = k + ) + { + int d = this.T[Td + this.SA[PA + this.SA[sa + (k = j++)]]] & 0xff; + int e; + if (d < (e = this.T[Td + this.SA[PA + this.SA[sa + j]]] & 0xff)) + { + k = j; + d = e; + } + if (d <= c) + break; + } + this.SA[sa + i] = v; + } + + private void ssHeapSort(int Td, int PA, int sa, int size) + { + int i; + + int m = size; + if ((size % 2) == 0) + { + m--; + if ( + (this.T[Td + this.SA[PA + this.SA[sa + (m / 2)]]] & 0xff) + < (this.T[Td + this.SA[PA + this.SA[sa + m]]] & 0xff) + ) + { + swapElements(this.SA, sa + m, this.SA, sa + (m / 2)); + } + } + + for (i = m / 2 - 1; 0 <= i; --i) + { + this.ssFixdown(Td, PA, sa, i, m); + } + + if ((size % 2) == 0) + { + swapElements(this.SA, sa, this.SA, sa + m); + this.ssFixdown(Td, PA, sa, 0, m); + } + + for (i = m - 1; 0 < i; --i) + { + int t = this.SA[sa]; + this.SA[sa] = this.SA[sa + i]; + this.ssFixdown(Td, PA, sa, 0, i); + this.SA[sa + i] = t; + } + } + + private int ssMedian3(int Td, int PA, int v1, int v2, int v3) + { + var T_v1 = this.T[Td + this.SA[PA + this.SA[v1]]] & 0xff; + var T_v2 = this.T[Td + this.SA[PA + this.SA[v2]]] & 0xff; + var T_v3 = this.T[Td + this.SA[PA + this.SA[v3]]] & 0xff; + + if (T_v1 > T_v2) + { + var temp = v1; + v1 = v2; + v2 = temp; + var T_vtemp = T_v1; + T_v1 = T_v2; + T_v2 = T_vtemp; + } + if (T_v2 > T_v3) + { + return T_v1 > T_v3 ? v1 : v3; + } + return v2; + } + + private int ssMedian5(int Td, int PA, int v1, int v2, int v3, int v4, int v5) + { + var T_v1 = this.T[Td + this.SA[PA + this.SA[v1]]] & 0xff; + var T_v2 = this.T[Td + this.SA[PA + this.SA[v2]]] & 0xff; + var T_v3 = this.T[Td + this.SA[PA + this.SA[v3]]] & 0xff; + var T_v4 = this.T[Td + this.SA[PA + this.SA[v4]]] & 0xff; + var T_v5 = this.T[Td + this.SA[PA + this.SA[v5]]] & 0xff; + int temp; + int T_vtemp; + + // ReSharper disable RedundantAssignment + if (T_v2 > T_v3) + { + temp = v2; + v2 = v3; + v3 = temp; + T_vtemp = T_v2; + T_v2 = T_v3; + T_v3 = T_vtemp; + } + if (T_v4 > T_v5) + { + temp = v4; + v4 = v5; + v5 = temp; + T_vtemp = T_v4; + T_v4 = T_v5; + T_v5 = T_vtemp; + } + if (T_v2 > T_v4) + { + temp = v2; + v2 = v4; + v4 = temp; + T_vtemp = T_v2; + T_v2 = T_v4; + T_v4 = T_vtemp; + temp = v3; + v3 = v5; + v5 = temp; + T_vtemp = T_v3; + T_v3 = T_v5; + T_v5 = T_vtemp; + } + if (T_v1 > T_v3) + { + temp = v1; + v1 = v3; + v3 = temp; + T_vtemp = T_v1; + T_v1 = T_v3; + T_v3 = T_vtemp; + } + if (T_v1 > T_v4) + { + temp = v1; + v1 = v4; + v4 = temp; + T_vtemp = T_v1; + T_v1 = T_v4; + T_v4 = T_vtemp; + temp = v3; + v3 = v5; + v5 = temp; + T_vtemp = T_v3; + T_v3 = T_v5; + T_v5 = T_vtemp; + } + // ReSharper restore RedundantAssignment + + return T_v3 > T_v4 ? v4 : v3; + } + + private int ssPivot(int Td, int PA, int first, int last) + { + int t = last - first; + int middle = first + t / 2; + + if (t <= 512) + { + if (t <= 32) + { + return this.ssMedian3(Td, PA, first, middle, last - 1); + } + t >>= 2; + return this.ssMedian5(Td, PA, first, first + t, middle, last - 1 - t, last - 1); + } + t >>= 3; + return this.ssMedian3( + Td, + PA, + this.ssMedian3(Td, PA, first, first + t, first + (t << 1)), + this.ssMedian3(Td, PA, middle - t, middle, middle + t), + this.ssMedian3(Td, PA, last - 1 - (t << 1), last - 1 - t, last - 1) + ); + } + + private static int ssLog(int x) + { + return ((x & 0xff00) != 0) + ? 8 + BZip2DivSufSort.log2table[(x >> 8) & 0xff] + : BZip2DivSufSort.log2table[x & 0xff]; + } + + private int ssSubstringPartition(int PA, int first, int last, int depth) + { + int a, + b; + + for (a = first - 1, b = last; ; ) + { + for ( + ; + (++a < b) + && ( + (this.SA[PA + this.SA[a]] + depth) >= (this.SA[PA + this.SA[a] + 1] + 1) + ); + + ) + { + this.SA[a] = ~this.SA[a]; + } + for ( + ; + (a < --b) + && ( + (this.SA[PA + this.SA[b]] + depth) < (this.SA[PA + this.SA[b] + 1] + 1) + ); + + ) { } + if (b <= a) + { + break; + } + int t = ~this.SA[b]; + this.SA[b] = this.SA[a]; + this.SA[a] = t; + } + if (first < a) + this.SA[first] = ~this.SA[first]; + + return a; + } + + private void ssMultiKeyIntroSort(int PA, int first, int last, int depth) + { + var stack = new StackEntry[BZip2DivSufSort.STACK_SIZE]; + + int ssize; + int limit; + int x = 0; + + for (ssize = 0, limit = ssLog(last - first); ; ) + { + if ((last - first) <= BZip2DivSufSort.INSERTIONSORT_THRESHOLD) + { + if (1 < (last - first)) + { + this.ssInsertionSort(PA, first, last, depth); + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + last = entry.b; + depth = entry.c; + limit = entry.d; + continue; + } + + int Td = depth; + if (limit-- == 0) + { + this.ssHeapSort(Td, PA, first, last - first); + } + int a; + int v; + if (limit < 0) + { + for ( + a = first + 1, v = this.T[Td + this.SA[PA + this.SA[first]]] & 0xff; + a < last; + ++a + ) + { + if ((x = (this.T[Td + this.SA[PA + this.SA[a]]] & 0xff)) != v) + { + if (1 < (a - first)) + { + break; + } + v = x; + first = a; + } + } + if ((this.T[Td + this.SA[PA + this.SA[first]] - 1] & 0xff) < v) + { + first = this.ssSubstringPartition(PA, first, a, depth); + } + if ((a - first) <= (last - a)) + { + if (1 < (a - first)) + { + stack[ssize++] = new StackEntry(a, last, depth, -1); + last = a; + depth += 1; + limit = ssLog(a - first); + } + else + { + first = a; + limit = -1; + } + } + else + { + if (1 < (last - a)) + { + stack[ssize++] = new StackEntry(first, a, depth + 1, ssLog(a - first)); + first = a; + limit = -1; + } + else + { + last = a; + depth += 1; + limit = ssLog(a - first); + } + } + continue; + } + + a = this.ssPivot(Td, PA, first, last); + v = this.T[Td + this.SA[PA + this.SA[a]]] & 0xff; + swapElements(this.SA, first, this.SA, a); + + int b; + for ( + b = first; + (++b < last) && ((x = (this.T[Td + this.SA[PA + this.SA[b]]] & 0xff)) == v); + + ) { } + if (((a = b) < last) && (x < v)) + { + for ( + ; + (++b < last) && ((x = (this.T[Td + this.SA[PA + this.SA[b]]] & 0xff)) <= v); + + ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + } + int c; + for ( + c = last; + (b < --c) && ((x = (this.T[Td + this.SA[PA + this.SA[c]]] & 0xff)) == v); + + ) { } + int d; + if ((b < (d = c)) && (x > v)) + { + for ( + ; + (b < --c) && ((x = (this.T[Td + this.SA[PA + this.SA[c]]] & 0xff)) >= v); + + ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + for (; b < c; ) + { + swapElements(this.SA, b, this.SA, c); + for ( + ; + (++b < c) && ((x = (this.T[Td + this.SA[PA + this.SA[b]]] & 0xff)) <= v); + + ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + for ( + ; + (b < --c) && ((x = (this.T[Td + this.SA[PA + this.SA[c]]] & 0xff)) >= v); + + ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + + if (a <= d) + { + c = b - 1; + + int s; + int t; + if ((s = a - first) > (t = b - a)) + { + s = t; + } + int e; + int f; + for (e = first, f = b - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + if ((s = d - c) > (t = last - d - 1)) + { + s = t; + } + for (e = b, f = last - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + + a = first + (b - a); + c = last - (d - c); + b = + (v <= (this.T[Td + this.SA[PA + this.SA[a]] - 1] & 0xff)) + ? a + : this.ssSubstringPartition(PA, a, c, depth); + + if ((a - first) <= (last - c)) + { + if ((last - c) <= (c - b)) + { + stack[ssize++] = new StackEntry(b, c, depth + 1, ssLog(c - b)); + stack[ssize++] = new StackEntry(c, last, depth, limit); + last = a; + } + else if ((a - first) <= (c - b)) + { + stack[ssize++] = new StackEntry(c, last, depth, limit); + stack[ssize++] = new StackEntry(b, c, depth + 1, ssLog(c - b)); + last = a; + } + else + { + stack[ssize++] = new StackEntry(c, last, depth, limit); + stack[ssize++] = new StackEntry(first, a, depth, limit); + first = b; + last = c; + depth += 1; + limit = ssLog(c - b); + } + } + else + { + if ((a - first) <= (c - b)) + { + stack[ssize++] = new StackEntry(b, c, depth + 1, ssLog(c - b)); + stack[ssize++] = new StackEntry(first, a, depth, limit); + first = c; + } + else if ((last - c) <= (c - b)) + { + stack[ssize++] = new StackEntry(first, a, depth, limit); + stack[ssize++] = new StackEntry(b, c, depth + 1, ssLog(c - b)); + first = c; + } + else + { + stack[ssize++] = new StackEntry(first, a, depth, limit); + stack[ssize++] = new StackEntry(c, last, depth, limit); + first = b; + last = c; + depth += 1; + limit = ssLog(c - b); + } + } + } + else + { + limit += 1; + if ((this.T[Td + this.SA[PA + this.SA[first]] - 1] & 0xff) < v) + { + first = this.ssSubstringPartition(PA, first, last, depth); + limit = ssLog(last - first); + } + depth += 1; + } + } + } + + private static void ssBlockSwap( + int[] array1, + int first1, + int[] array2, + int first2, + int size + ) + { + int a, + b; + int i; + for (i = size, a = first1, b = first2; 0 < i; --i, ++a, ++b) + { + swapElements(array1, a, array2, b); + } + } + + private void ssMergeForward( + int PA, + int[] buf, + int bufoffset, + int first, + int middle, + int last, + int depth + ) + { + int i, + j, + k; + int t; + + int bufend = bufoffset + (middle - first) - 1; + ssBlockSwap(buf, bufoffset, this.SA, first, middle - first); + + for (t = this.SA[first], i = first, j = bufoffset, k = middle; ; ) + { + int r = this.ssCompare(PA + buf[j], PA + this.SA[k], depth); + if (r < 0) + { + do + { + this.SA[i++] = buf[j]; + if (bufend <= j) + { + buf[j] = t; + return; + } + buf[j++] = this.SA[i]; + } while (buf[j] < 0); + } + else if (r > 0) + { + do + { + this.SA[i++] = this.SA[k]; + this.SA[k++] = this.SA[i]; + if (last <= k) + { + while (j < bufend) + { + this.SA[i++] = buf[j]; + buf[j++] = this.SA[i]; + } + this.SA[i] = buf[j]; + buf[j] = t; + return; + } + } while (this.SA[k] < 0); + } + else + { + this.SA[k] = ~this.SA[k]; + do + { + this.SA[i++] = buf[j]; + if (bufend <= j) + { + buf[j] = t; + return; + } + buf[j++] = this.SA[i]; + } while (buf[j] < 0); + + do + { + this.SA[i++] = this.SA[k]; + this.SA[k++] = this.SA[i]; + if (last <= k) + { + while (j < bufend) + { + this.SA[i++] = buf[j]; + buf[j++] = this.SA[i]; + } + this.SA[i] = buf[j]; + buf[j] = t; + return; + } + } while (this.SA[k] < 0); + } + } + } + + private void ssMergeBackward( + int PA, + int[] buf, + int bufoffset, + int first, + int middle, + int last, + int depth + ) + { + int p1, + p2; + int i, + j, + k; + int t; + + int bufend = bufoffset + (last - middle); + ssBlockSwap(buf, bufoffset, this.SA, middle, last - middle); + + int x = 0; + if (buf[bufend - 1] < 0) + { + x |= 1; + p1 = PA + ~buf[bufend - 1]; + } + else + { + p1 = PA + buf[bufend - 1]; + } + if (this.SA[middle - 1] < 0) + { + x |= 2; + p2 = PA + ~this.SA[middle - 1]; + } + else + { + p2 = PA + this.SA[middle - 1]; + } + for (t = this.SA[last - 1], i = last - 1, j = bufend - 1, k = middle - 1; ; ) + { + int r = this.ssCompare(p1, p2, depth); + if (r > 0) + { + if ((x & 1) != 0) + { + do + { + this.SA[i--] = buf[j]; + buf[j--] = this.SA[i]; + } while (buf[j] < 0); + x ^= 1; + } + this.SA[i--] = buf[j]; + if (j <= bufoffset) + { + buf[j] = t; + return; + } + buf[j--] = this.SA[i]; + + if (buf[j] < 0) + { + x |= 1; + p1 = PA + ~buf[j]; + } + else + { + p1 = PA + buf[j]; + } + } + else if (r < 0) + { + if ((x & 2) != 0) + { + do + { + this.SA[i--] = this.SA[k]; + this.SA[k--] = this.SA[i]; + } while (this.SA[k] < 0); + x ^= 2; + } + this.SA[i--] = this.SA[k]; + this.SA[k--] = this.SA[i]; + if (k < first) + { + while (bufoffset < j) + { + this.SA[i--] = buf[j]; + buf[j--] = this.SA[i]; + } + this.SA[i] = buf[j]; + buf[j] = t; + return; + } + + if (this.SA[k] < 0) + { + x |= 2; + p2 = PA + ~this.SA[k]; + } + else + { + p2 = PA + this.SA[k]; + } + } + else + { + if ((x & 1) != 0) + { + do + { + this.SA[i--] = buf[j]; + buf[j--] = this.SA[i]; + } while (buf[j] < 0); + x ^= 1; + } + this.SA[i--] = ~buf[j]; + if (j <= bufoffset) + { + buf[j] = t; + return; + } + buf[j--] = this.SA[i]; + + if ((x & 2) != 0) + { + do + { + this.SA[i--] = this.SA[k]; + this.SA[k--] = this.SA[i]; + } while (this.SA[k] < 0); + x ^= 2; + } + this.SA[i--] = this.SA[k]; + this.SA[k--] = this.SA[i]; + if (k < first) + { + while (bufoffset < j) + { + this.SA[i--] = buf[j]; + buf[j--] = this.SA[i]; + } + this.SA[i] = buf[j]; + buf[j] = t; + return; + } + + if (buf[j] < 0) + { + x |= 1; + p1 = PA + ~buf[j]; + } + else + { + p1 = PA + buf[j]; + } + if (this.SA[k] < 0) + { + x |= 2; + p2 = PA + ~this.SA[k]; + } + else + { + p2 = PA + this.SA[k]; + } + } + } + } + + private static int getIDX(int a) + { + return (0 <= a) ? a : ~a; + } + + private void ssMergeCheckEqual(int PA, int depth, int a) + { + if ( + (0 <= this.SA[a]) + && (this.ssCompare(PA + getIDX(this.SA[a - 1]), PA + this.SA[a], depth) == 0) + ) + { + this.SA[a] = ~this.SA[a]; + } + } + + private void ssMerge( + int PA, + int first, + int middle, + int last, + int[] buf, + int bufoffset, + int bufsize, + int depth + ) + { + var stack = new StackEntry[BZip2DivSufSort.STACK_SIZE]; + + int ssize; + int check; + + for (check = 0, ssize = 0; ; ) + { + if ((last - middle) <= bufsize) + { + if ((first < middle) && (middle < last)) + { + this.ssMergeBackward(PA, buf, bufoffset, first, middle, last, depth); + } + + if ((check & 1) != 0) + { + this.ssMergeCheckEqual(PA, depth, first); + } + if ((check & 2) != 0) + { + this.ssMergeCheckEqual(PA, depth, last); + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + middle = entry.b; + last = entry.c; + check = entry.d; + continue; + } + + if ((middle - first) <= bufsize) + { + if (first < middle) + { + this.ssMergeForward(PA, buf, bufoffset, first, middle, last, depth); + } + if ((check & 1) != 0) + { + this.ssMergeCheckEqual(PA, depth, first); + } + if ((check & 2) != 0) + { + this.ssMergeCheckEqual(PA, depth, last); + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + middle = entry.b; + last = entry.c; + check = entry.d; + continue; + } + + int m; + int len; + int half; + for ( + m = 0, len = Math.Min(middle - first, last - middle), half = len >> 1; + 0 < len; + len = half, half >>= 1 + ) + { + if ( + this.ssCompare( + PA + getIDX(this.SA[middle + m + half]), + PA + getIDX(this.SA[middle - m - half - 1]), + depth + ) < 0 + ) + { + m += half + 1; + half -= (len & 1) ^ 1; + } + } + + if (0 < m) + { + ssBlockSwap(this.SA, middle - m, this.SA, middle, m); + int j; + int i = j = middle; + int next = 0; + if ((middle + m) < last) + { + if (this.SA[middle + m] < 0) + { + for (; this.SA[i - 1] < 0; --i) { } + this.SA[middle + m] = ~this.SA[middle + m]; + } + for (j = middle; this.SA[j] < 0; ++j) { } + next = 1; + } + if ((i - first) <= (last - j)) + { + stack[ssize++] = new StackEntry( + j, + middle + m, + last, + (check & 2) | (next & 1) + ); + middle -= m; + last = i; + check = (check & 1); + } + else + { + if ((i == middle) && (middle == j)) + { + next <<= 1; + } + stack[ssize++] = new StackEntry( + first, + middle - m, + i, + (check & 1) | (next & 2) + ); + first = j; + middle += m; + check = (check & 2) | (next & 1); + } + } + else + { + if ((check & 1) != 0) + { + this.ssMergeCheckEqual(PA, depth, first); + } + this.ssMergeCheckEqual(PA, depth, middle); + if ((check & 2) != 0) + { + this.ssMergeCheckEqual(PA, depth, last); + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + middle = entry.b; + last = entry.c; + check = entry.d; + } + } + } + + private void subStringSort( + int PA, + int first, + int last, + int[] buf, + int bufoffset, + int bufsize, + int depth, + bool lastsuffix, + int size + ) + { + int a; + int i; + int k; + + if (lastsuffix) + ++first; + + for ( + a = first, i = 0; + (a + BZip2DivSufSort.SS_BLOCKSIZE) < last; + a += BZip2DivSufSort.SS_BLOCKSIZE, ++i + ) + { + this.ssMultiKeyIntroSort(PA, a, a + BZip2DivSufSort.SS_BLOCKSIZE, depth); + int[] curbuf = this.SA; + int curbufoffset = a + BZip2DivSufSort.SS_BLOCKSIZE; + int curbufsize = last - (a + BZip2DivSufSort.SS_BLOCKSIZE); + if (curbufsize <= bufsize) + { + curbufsize = bufsize; + curbuf = buf; + curbufoffset = bufoffset; + } + int b; + int j; + for ( + b = a, k = BZip2DivSufSort.SS_BLOCKSIZE, j = i; + (j & 1) != 0; + b -= k, k <<= 1, j >>= 1 + ) + this.ssMerge(PA, b - k, b, b + k, curbuf, curbufoffset, curbufsize, depth); + } + + this.ssMultiKeyIntroSort(PA, a, last, depth); + + for (k = BZip2DivSufSort.SS_BLOCKSIZE; i != 0; k <<= 1, i >>= 1) + { + if ((i & 1) != 0) + { + this.ssMerge(PA, a - k, a, last, buf, bufoffset, bufsize, depth); + a -= k; + } + } + + if (lastsuffix) + { + int r; + for ( + a = first, i = this.SA[first - 1], r = 1; + (a < last) + && ( + (this.SA[a] < 0) + || ( + 0 + < (r = this.ssCompareLast(PA, PA + i, PA + this.SA[a], depth, size)) + ) + ); + ++a + ) + { + this.SA[a - 1] = this.SA[a]; + } + if (r == 0) + { + this.SA[a] = ~this.SA[a]; + } + this.SA[a - 1] = i; + } + } + + private int trGetC(int ISA, int ISAd, int ISAn, int p) + { + return ( + ((ISAd + p) < ISAn) + ? this.SA[ISAd + p] + : this.SA[ISA + ((ISAd - ISA + p) % (ISAn - ISA))] + ); + } + + private void trFixdown(int ISA, int ISAd, int ISAn, int sa, int i, int size) + { + int j, + k; + int v; + int c; + + for ( + v = this.SA[sa + i], c = this.trGetC(ISA, ISAd, ISAn, v); + (j = 2 * i + 1) < size; + this.SA[sa + i] = this.SA[sa + k], i = k + ) + { + k = j++; + int d = this.trGetC(ISA, ISAd, ISAn, this.SA[sa + k]); + int e; + if (d < (e = this.trGetC(ISA, ISAd, ISAn, this.SA[sa + j]))) + { + k = j; + d = e; + } + if (d <= c) + { + break; + } + } + this.SA[sa + i] = v; + } + + private void trHeapSort(int ISA, int ISAd, int ISAn, int sa, int size) + { + int i; + + int m = size; + if ((size % 2) == 0) + { + m--; + if ( + this.trGetC(ISA, ISAd, ISAn, this.SA[sa + (m / 2)]) + < this.trGetC(ISA, ISAd, ISAn, this.SA[sa + m]) + ) + { + swapElements(this.SA, sa + m, this.SA, sa + (m / 2)); + } + } + + for (i = m / 2 - 1; 0 <= i; --i) + { + this.trFixdown(ISA, ISAd, ISAn, sa, i, m); + } + + if ((size % 2) == 0) + { + swapElements(this.SA, sa + 0, this.SA, sa + m); + this.trFixdown(ISA, ISAd, ISAn, sa, 0, m); + } + + for (i = m - 1; 0 < i; --i) + { + int t = this.SA[sa + 0]; + this.SA[sa + 0] = this.SA[sa + i]; + this.trFixdown(ISA, ISAd, ISAn, sa, 0, i); + this.SA[sa + i] = t; + } + } + + private void trInsertionSort(int ISA, int ISAd, int ISAn, int first, int last) + { + int a; + + for (a = first + 1; a < last; ++a) + { + int b; + int t; + int r; + for ( + t = this.SA[a], b = a - 1; + 0 + > ( + r = + this.trGetC(ISA, ISAd, ISAn, t) + - this.trGetC(ISA, ISAd, ISAn, this.SA[b]) + ); + + ) + { + do + { + this.SA[b + 1] = this.SA[b]; + } while ((first <= --b) && (this.SA[b] < 0)); + if (b < first) + { + break; + } + } + if (r == 0) + { + this.SA[b] = ~this.SA[b]; + } + this.SA[b + 1] = t; + } + } + + private static int trLog(int x) + { + return ((x & 0xffff0000) != 0) + ? ( + ((x & 0xff000000) != 0) + ? 24 + BZip2DivSufSort.log2table[(x >> 24) & 0xff] + : 16 + BZip2DivSufSort.log2table[(x >> 16) & 0xff] + ) + : ( + ((x & 0x0000ff00) != 0) + ? 8 + BZip2DivSufSort.log2table[(x >> 8) & 0xff] + : 0 + BZip2DivSufSort.log2table[(x >> 0) & 0xff] + ); + } + + private int trMedian3(int ISA, int ISAd, int ISAn, int v1, int v2, int v3) + { + var SA_v1 = this.trGetC(ISA, ISAd, ISAn, this.SA[v1]); + var SA_v2 = this.trGetC(ISA, ISAd, ISAn, this.SA[v2]); + var SA_v3 = this.trGetC(ISA, ISAd, ISAn, this.SA[v3]); + + if (SA_v1 > SA_v2) + { + var temp = v1; + v1 = v2; + v2 = temp; + var SA_vtemp = SA_v1; + SA_v1 = SA_v2; + SA_v2 = SA_vtemp; + } + if (SA_v2 > SA_v3) + { + return SA_v1 > SA_v3 ? v1 : v3; + } + + return v2; + } + + private int trMedian5(int ISA, int ISAd, int ISAn, int v1, int v2, int v3, int v4, int v5) + { + var SA_v1 = this.trGetC(ISA, ISAd, ISAn, this.SA[v1]); + var SA_v2 = this.trGetC(ISA, ISAd, ISAn, this.SA[v2]); + var SA_v3 = this.trGetC(ISA, ISAd, ISAn, this.SA[v3]); + var SA_v4 = this.trGetC(ISA, ISAd, ISAn, this.SA[v4]); + var SA_v5 = this.trGetC(ISA, ISAd, ISAn, this.SA[v5]); + int temp; + int SA_vtemp; + + // ReSharper disable RedundantAssignment + if (SA_v2 > SA_v3) + { + temp = v2; + v2 = v3; + v3 = temp; + SA_vtemp = SA_v2; + SA_v2 = SA_v3; + SA_v3 = SA_vtemp; + } + if (SA_v4 > SA_v5) + { + temp = v4; + v4 = v5; + v5 = temp; + SA_vtemp = SA_v4; + SA_v4 = SA_v5; + SA_v5 = SA_vtemp; + } + if (SA_v2 > SA_v4) + { + temp = v2; + v2 = v4; + v4 = temp; + SA_vtemp = SA_v2; + SA_v2 = SA_v4; + SA_v4 = SA_vtemp; + temp = v3; + v3 = v5; + v5 = temp; + SA_vtemp = SA_v3; + SA_v3 = SA_v5; + SA_v5 = SA_vtemp; + } + if (SA_v1 > SA_v3) + { + temp = v1; + v1 = v3; + v3 = temp; + SA_vtemp = SA_v1; + SA_v1 = SA_v3; + SA_v3 = SA_vtemp; + } + if (SA_v1 > SA_v4) + { + temp = v1; + v1 = v4; + v4 = temp; + SA_vtemp = SA_v1; + SA_v1 = SA_v4; + SA_v4 = SA_vtemp; + temp = v3; + v3 = v5; + v5 = temp; + SA_vtemp = SA_v3; + SA_v3 = SA_v5; + SA_v5 = SA_vtemp; + } + // ReSharper restore RedundantAssignment + + return SA_v3 > SA_v4 ? v4 : v3; + } + + private int trPivot(int ISA, int ISAd, int ISAn, int first, int last) + { + int t = last - first; + int middle = first + t / 2; + + if (t <= 512) + { + if (t <= 32) + { + return this.trMedian3(ISA, ISAd, ISAn, first, middle, last - 1); + } + t >>= 2; + return this.trMedian5( + ISA, + ISAd, + ISAn, + first, + first + t, + middle, + last - 1 - t, + last - 1 + ); + } + t >>= 3; + return this.trMedian3( + ISA, + ISAd, + ISAn, + this.trMedian3(ISA, ISAd, ISAn, first, first + t, first + (t << 1)), + this.trMedian3(ISA, ISAd, ISAn, middle - t, middle, middle + t), + this.trMedian3(ISA, ISAd, ISAn, last - 1 - (t << 1), last - 1 - t, last - 1) + ); + } + + private void lsUpdateGroup(int ISA, int first, int last) + { + int a; + + for (a = first; a < last; ++a) + { + int b; + if (0 <= this.SA[a]) + { + b = a; + do + { + this.SA[ISA + this.SA[a]] = a; + } while ((++a < last) && (0 <= this.SA[a])); + this.SA[b] = b - a; + if (last <= a) + { + break; + } + } + b = a; + do + { + this.SA[a] = ~this.SA[a]; + } while (this.SA[++a] < 0); + int t = a; + do + { + this.SA[ISA + this.SA[b]] = t; + } while (++b <= a); + } + } + + private void lsIntroSort(int ISA, int ISAd, int ISAn, int first, int last) + { + var stack = new StackEntry[BZip2DivSufSort.STACK_SIZE]; + + int limit; + int x = 0; + int ssize; + + for (ssize = 0, limit = trLog(last - first); ; ) + { + if ((last - first) <= BZip2DivSufSort.INSERTIONSORT_THRESHOLD) + { + if (1 < (last - first)) + { + this.trInsertionSort(ISA, ISAd, ISAn, first, last); + this.lsUpdateGroup(ISA, first, last); + } + else if ((last - first) == 1) + { + this.SA[first] = -1; + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + last = entry.b; + limit = entry.c; + continue; + } + + int a; + int b; + if (limit-- == 0) + { + this.trHeapSort(ISA, ISAd, ISAn, first, last - first); + for (a = last - 1; first < a; a = b) + { + for ( + x = this.trGetC(ISA, ISAd, ISAn, this.SA[a]), b = a - 1; + (first <= b) && (this.trGetC(ISA, ISAd, ISAn, this.SA[b]) == x); + --b + ) + { + this.SA[b] = ~this.SA[b]; + } + } + this.lsUpdateGroup(ISA, first, last); + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + last = entry.b; + limit = entry.c; + continue; + } + + a = this.trPivot(ISA, ISAd, ISAn, first, last); + swapElements(this.SA, first, this.SA, a); + int v = this.trGetC(ISA, ISAd, ISAn, this.SA[first]); + + for ( + b = first; + (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) == v); + + ) { } + if (((a = b) < last) && (x < v)) + { + for (; (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + } + int c; + for (c = last; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) == v); ) + { } + int d; + if ((b < (d = c)) && (x > v)) + { + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + for (; b < c; ) + { + swapElements(this.SA, b, this.SA, c); + for (; (++b < c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + + if (a <= d) + { + c = b - 1; + + int s; + int t; + if ((s = a - first) > (t = b - a)) + { + s = t; + } + int e; + int f; + for (e = first, f = b - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + if ((s = d - c) > (t = last - d - 1)) + { + s = t; + } + for (e = b, f = last - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + + a = first + (b - a); + b = last - (d - c); + + for (c = first, v = a - 1; c < a; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + if (b < last) + { + for (c = a, v = b - 1; c < b; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + } + if ((b - a) == 1) + { + this.SA[a] = -1; + } + + if ((a - first) <= (last - b)) + { + if (first < a) + { + stack[ssize++] = new StackEntry(b, last, limit, 0); + last = a; + } + else + { + first = b; + } + } + else + { + if (b < last) + { + stack[ssize++] = new StackEntry(first, a, limit, 0); + first = b; + } + else + { + last = a; + } + } + } + else + { + if (ssize == 0) + return; + var entry = stack[--ssize]; + first = entry.a; + last = entry.b; + limit = entry.c; + } + } + } + + private void lsSort(int ISA, int x, int depth) + { + int ISAd; + + for (ISAd = ISA + depth; -x < this.SA[0]; ISAd += (ISAd - ISA)) + { + int first = 0; + int skip = 0; + int last; + int t; + do + { + if ((t = this.SA[first]) < 0) + { + first -= t; + skip += t; + } + else + { + if (skip != 0) + { + this.SA[first + skip] = skip; + skip = 0; + } + last = this.SA[ISA + t] + 1; + this.lsIntroSort(ISA, ISAd, ISA + x, first, last); + first = last; + } + } while (first < x); + + if (skip != 0) + this.SA[first + skip] = skip; + + if (x < (ISAd - ISA)) + { + first = 0; + do + { + if ((t = this.SA[first]) < 0) + first -= t; + else + { + last = this.SA[ISA + t] + 1; + int i; + for (i = first; i < last; ++i) + this.SA[ISA + this.SA[i]] = i; + first = last; + } + } while (first < x); + break; + } + } + } + + private PartitionResult trPartition(int ISA, int ISAd, int ISAn, int first, int last, int v) + { + int a, + b, + c, + d; + var x = 0; + + for ( + b = first - 1; + (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) == v); + + ) { } + if (((a = b) < last) && (x < v)) + { + for (; (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + } + for (c = last; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) == v); ) { } + if ((b < (d = c)) && (x > v)) + { + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + for (; b < c; ) + { + swapElements(this.SA, b, this.SA, c); + for (; (++b < c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + + if (a <= d) + { + c = b - 1; + int t; + int s; + if ((s = a - first) > (t = b - a)) + s = t; + + int e; + int f; + for (e = first, f = b - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + if ((s = d - c) > (t = last - d - 1)) + { + s = t; + } + for (e = b, f = last - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + first += (b - a); + last -= (d - c); + } + + return new PartitionResult(first, last); + } + + private void trCopy(int ISA, int ISAn, int first, int a, int b, int last, int depth) + { + int c, + d, + e; + int s; + int v = b - 1; + + for (c = first, d = a - 1; c <= d; ++c) + { + if ((s = this.SA[c] - depth) < 0) + { + s += ISAn - ISA; + } + if (this.SA[ISA + s] == v) + { + this.SA[++d] = s; + this.SA[ISA + s] = d; + } + } + for (c = last - 1, e = d + 1, d = b; e < d; --c) + { + if ((s = this.SA[c] - depth) < 0) + { + s += ISAn - ISA; + } + if (this.SA[ISA + s] == v) + { + this.SA[--d] = s; + this.SA[ISA + s] = d; + } + } + } + + private void trIntroSort( + int ISA, + int ISAd, + int ISAn, + int first, + int last, + TRBudget budget, + int size + ) + { + var stack = new StackEntry[BZip2DivSufSort.STACK_SIZE]; + + int s; + int x = 0; + int limit; + int ssize; + + for (ssize = 0, limit = trLog(last - first); ; ) + { + int a; + int b; + int c; + int v; + int next; + if (limit < 0) + { + if (limit == -1) + { + if (!budget.update(size, last - first)) + break; + var result = this.trPartition(ISA, ISAd - 1, ISAn, first, last, last - 1); + a = result.first; + b = result.last; + if ((first < a) || (b < last)) + { + if (a < last) + { + for (c = first, v = a - 1; c < a; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + } + if (b < last) + { + for (c = a, v = b - 1; c < b; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + } + + stack[ssize++] = new StackEntry(0, a, b, 0); + stack[ssize++] = new StackEntry(ISAd - 1, first, last, -2); + if ((a - first) <= (last - b)) + { + if (1 < (a - first)) + { + stack[ssize++] = new StackEntry(ISAd, b, last, trLog(last - b)); + last = a; + limit = trLog(a - first); + } + else if (1 < (last - b)) + { + first = b; + limit = trLog(last - b); + } + else + { + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + } + else + { + if (1 < (last - b)) + { + stack[ssize++] = new StackEntry( + ISAd, + first, + a, + trLog(a - first) + ); + first = b; + limit = trLog(last - b); + } + else if (1 < (a - first)) + { + last = a; + limit = trLog(a - first); + } + else + { + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + } + } + else + { + for (c = first; c < last; ++c) + { + this.SA[ISA + this.SA[c]] = c; + } + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + } + else if (limit == -2) + { + a = stack[--ssize].b; + b = stack[ssize].c; + this.trCopy(ISA, ISAn, first, a, b, last, ISAd - ISA); + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + else + { + if (0 <= this.SA[first]) + { + a = first; + do + { + this.SA[ISA + this.SA[a]] = a; + } while ((++a < last) && (0 <= this.SA[a])); + first = a; + } + if (first < last) + { + a = first; + do + { + this.SA[a] = ~this.SA[a]; + } while (this.SA[++a] < 0); + next = + (this.SA[ISA + this.SA[a]] != this.SA[ISAd + this.SA[a]]) + ? trLog(a - first + 1) + : -1; + if (++a < last) + { + for (b = first, v = a - 1; b < a; ++b) + { + this.SA[ISA + this.SA[b]] = v; + } + } + + if ((a - first) <= (last - a)) + { + stack[ssize++] = new StackEntry(ISAd, a, last, -3); + ISAd += 1; + last = a; + limit = next; + } + else + { + if (1 < (last - a)) + { + stack[ssize++] = new StackEntry(ISAd + 1, first, a, next); + first = a; + limit = -3; + } + else + { + ISAd += 1; + last = a; + limit = next; + } + } + } + else + { + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + } + continue; + } + + if ((last - first) <= BZip2DivSufSort.INSERTIONSORT_THRESHOLD) + { + if (!budget.update(size, last - first)) + break; + this.trInsertionSort(ISA, ISAd, ISAn, first, last); + limit = -3; + continue; + } + + if (limit-- == 0) + { + if (!budget.update(size, last - first)) + break; + this.trHeapSort(ISA, ISAd, ISAn, first, last - first); + for (a = last - 1; first < a; a = b) + { + for ( + x = this.trGetC(ISA, ISAd, ISAn, this.SA[a]), b = a - 1; + (first <= b) && (this.trGetC(ISA, ISAd, ISAn, this.SA[b]) == x); + --b + ) + { + this.SA[b] = ~this.SA[b]; + } + } + limit = -3; + continue; + } + + a = this.trPivot(ISA, ISAd, ISAn, first, last); + + swapElements(this.SA, first, this.SA, a); + v = this.trGetC(ISA, ISAd, ISAn, this.SA[first]); + for ( + b = first; + (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) == v); + + ) { } + if (((a = b) < last) && (x < v)) + { + for (; (++b < last) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + } + for (c = last; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) == v); ) + { } + int d; + if ((b < (d = c)) && (x > v)) + { + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + for (; b < c; ) + { + swapElements(this.SA, b, this.SA, c); + for (; (++b < c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[b])) <= v); ) + { + if (x == v) + { + swapElements(this.SA, b, this.SA, a); + ++a; + } + } + for (; (b < --c) && ((x = this.trGetC(ISA, ISAd, ISAn, this.SA[c])) >= v); ) + { + if (x == v) + { + swapElements(this.SA, c, this.SA, d); + --d; + } + } + } + + if (a <= d) + { + c = b - 1; + + int t; + if ((s = a - first) > (t = b - a)) + { + s = t; + } + int e; + int f; + for (e = first, f = b - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + if ((s = d - c) > (t = last - d - 1)) + { + s = t; + } + for (e = b, f = last - s; 0 < s; --s, ++e, ++f) + { + swapElements(this.SA, e, this.SA, f); + } + + a = first + (b - a); + b = last - (d - c); + next = (this.SA[ISA + this.SA[a]] != v) ? trLog(b - a) : -1; + + for (c = first, v = a - 1; c < a; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + if (b < last) + { + for (c = a, v = b - 1; c < b; ++c) + { + this.SA[ISA + this.SA[c]] = v; + } + } + + if ((a - first) <= (last - b)) + { + if ((last - b) <= (b - a)) + { + if (1 < (a - first)) + { + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + last = a; + } + else if (1 < (last - b)) + { + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + first = b; + } + else if (1 < (b - a)) + { + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + if (ssize == 0) + return; + var entry = stack[--ssize]; + ISAd = entry.a; + first = entry.b; + last = entry.c; + limit = entry.d; + } + } + else if ((a - first) <= (b - a)) + { + if (1 < (a - first)) + { + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + last = a; + } + else if (1 < (b - a)) + { + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + first = b; + } + } + else + { + if (1 < (b - a)) + { + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + last = a; + } + } + } + else + { + if ((a - first) <= (b - a)) + { + if (1 < (last - b)) + { + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + first = b; + } + else if (1 < (a - first)) + { + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + last = a; + } + else if (1 < (b - a)) + { + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + stack[ssize++] = new StackEntry(ISAd, first, last, limit); + } + } + else if ((last - b) <= (b - a)) + { + if (1 < (last - b)) + { + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + stack[ssize++] = new StackEntry(ISAd + 1, a, b, next); + first = b; + } + else if (1 < (b - a)) + { + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + last = a; + } + } + else + { + if (1 < (b - a)) + { + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + stack[ssize++] = new StackEntry(ISAd, b, last, limit); + ISAd += 1; + first = a; + last = b; + limit = next; + } + else + { + stack[ssize++] = new StackEntry(ISAd, first, a, limit); + first = b; + } + } + } + } + else + { + if (!budget.update(size, last - first)) + break; // BUGFIX : Added to prevent an infinite loop in the original code + limit += 1; + ISAd += 1; + } + } + + for (s = 0; s < ssize; ++s) + { + if (stack[s].d == -3) + { + this.lsUpdateGroup(ISA, stack[s].b, stack[s].c); + } + } + } + + private void trSort(int ISA, int x, int depth) + { + int first = 0; + + if (-x < this.SA[0]) + { + var budget = new TRBudget(x, trLog(x) * 2 / 3 + 1); + do + { + int t; + if ((t = this.SA[first]) < 0) + { + first -= t; + } + else + { + int last = this.SA[ISA + t] + 1; + if (1 < (last - first)) + { + this.trIntroSort(ISA, ISA + depth, ISA + x, first, last, budget, x); + if (budget.chance == 0) + { + // Switch to Larsson-Sadakane sorting algorithm. + if (0 < first) + { + this.SA[0] = -first; + } + this.lsSort(ISA, x, depth); + break; + } + } + first = last; + } + } while (first < x); + } + } + + private static int BUCKET_B(int c0, int c1) + { + return (c1 << 8) | c0; + } + + private static int BUCKET_BSTAR(int c0, int c1) + { + return (c0 << 8) | c1; + } + + private int sortTypeBstar(int[] bucketA, int[] bucketB) + { + var tempbuf = new int[256]; + + int i, + j, + k, + t; + int c0, + c1; + int flag; + + for (i = 1, flag = 1; i < this.n; ++i) + { + if (this.T[i - 1] != this.T[i]) + { + if ((this.T[i - 1] & 0xff) > (this.T[i] & 0xff)) + { + flag = 0; + } + break; + } + } + i = this.n - 1; + int m = this.n; + + int ti, + ti1, + t0; + if ( + ((ti = (this.T[i] & 0xff)) < (t0 = (this.T[0] & 0xff))) + || ((this.T[i] == this.T[0]) && (flag != 0)) + ) + { + if (flag == 0) + { + ++bucketB[BUCKET_BSTAR(ti, t0)]; + this.SA[--m] = i; + } + else + { + ++bucketB[BUCKET_B(ti, t0)]; + } + for ( + --i; + (0 <= i) && ((ti = (this.T[i] & 0xff)) <= (ti1 = (this.T[i + 1] & 0xff))); + --i + ) + { + ++bucketB[BUCKET_B(ti, ti1)]; + } + } + + for (; 0 <= i; ) + { + do + { + ++bucketA[this.T[i] & 0xff]; + } while ((0 <= --i) && ((this.T[i] & 0xff) >= (this.T[i + 1] & 0xff))); + + if (0 <= i) + { + ++bucketB[BUCKET_BSTAR(this.T[i] & 0xff, this.T[i + 1] & 0xff)]; + this.SA[--m] = i; + for ( + --i; + (0 <= i) && ((ti = (this.T[i] & 0xff)) <= (ti1 = (this.T[i + 1] & 0xff))); + --i + ) + { + ++bucketB[BUCKET_B(ti, ti1)]; + } + } + } + m = this.n - m; + if (m == 0) + { + for (i = 0; i < this.n; ++i) + { + this.SA[i] = i; + } + return 0; + } + + for (c0 = 0, i = -1, j = 0; c0 < 256; ++c0) + { + t = i + bucketA[c0]; + bucketA[c0] = i + j; + i = t + bucketB[BUCKET_B(c0, c0)]; + for (c1 = c0 + 1; c1 < 256; ++c1) + { + j += bucketB[BUCKET_BSTAR(c0, c1)]; + bucketB[(c0 << 8) | c1] = j; + i += bucketB[BUCKET_B(c0, c1)]; + } + } + + int PAb = this.n - m; + int ISAb = m; + for (i = m - 2; 0 <= i; --i) + { + t = this.SA[PAb + i]; + c0 = this.T[t] & 0xff; + c1 = this.T[t + 1] & 0xff; + this.SA[--bucketB[BUCKET_BSTAR(c0, c1)]] = i; + } + t = this.SA[PAb + m - 1]; + c0 = this.T[t] & 0xff; + c1 = this.T[t + 1] & 0xff; + this.SA[--bucketB[BUCKET_BSTAR(c0, c1)]] = m - 1; + + int[] buf = this.SA; + int bufoffset = m; + int bufsize = this.n - (2 * m); + if (bufsize <= 256) + { + buf = tempbuf; + bufoffset = 0; + bufsize = 256; + } + + for (c0 = 255, j = m; 0 < j; --c0) + { + for (c1 = 255; c0 < c1; j = i, --c1) + { + i = bucketB[BUCKET_BSTAR(c0, c1)]; + if (1 < (j - i)) + { + this.subStringSort( + PAb, + i, + j, + buf, + bufoffset, + bufsize, + 2, + this.SA[i] == (m - 1), + this.n + ); + } + } + } + + for (i = m - 1; 0 <= i; --i) + { + if (0 <= this.SA[i]) + { + j = i; + do + { + this.SA[ISAb + this.SA[i]] = i; + } while ((0 <= --i) && (0 <= this.SA[i])); + this.SA[i + 1] = i - j; + if (i <= 0) + { + break; + } + } + j = i; + do + { + this.SA[ISAb + (this.SA[i] = ~this.SA[i])] = j; + } while (this.SA[--i] < 0); + this.SA[ISAb + this.SA[i]] = j; + } + + this.trSort(ISAb, m, 1); + + i = this.n - 1; + j = m; + if ( + ((this.T[i] & 0xff) < (this.T[0] & 0xff)) + || ((this.T[i] == this.T[0]) && (flag != 0)) + ) + { + if (flag == 0) + { + this.SA[this.SA[ISAb + --j]] = i; + } + for (--i; (0 <= i) && ((this.T[i] & 0xff) <= (this.T[i + 1] & 0xff)); --i) { } + } + for (; 0 <= i; ) + { + for (--i; (0 <= i) && ((this.T[i] & 0xff) >= (this.T[i + 1] & 0xff)); --i) { } + if (0 <= i) + { + this.SA[this.SA[ISAb + --j]] = i; + for (--i; (0 <= i) && ((this.T[i] & 0xff) <= (this.T[i + 1] & 0xff)); --i) { } + } + } + + for (c0 = 255, i = this.n - 1, k = m - 1; 0 <= c0; --c0) + { + for (c1 = 255; c0 < c1; --c1) + { + t = i - bucketB[BUCKET_B(c0, c1)]; + bucketB[BUCKET_B(c0, c1)] = i + 1; + + for (i = t, j = bucketB[BUCKET_BSTAR(c0, c1)]; j <= k; --i, --k) + { + this.SA[i] = this.SA[k]; + } + } + t = i - bucketB[BUCKET_B(c0, c0)]; + bucketB[BUCKET_B(c0, c0)] = i + 1; + if (c0 < 255) + { + bucketB[BUCKET_BSTAR(c0, c0 + 1)] = t + 1; + } + i = bucketA[c0]; + } + + return m; + } + + private int constructBWT(int[] bucketA, int[] bucketB) + { + int i; + int t = 0; + int s, + s1; + int c0, + c1, + c2 = 0; + var orig = -1; + + for (c1 = 254; 0 <= c1; --c1) + { + int j; + for ( + i = bucketB[BUCKET_BSTAR(c1, c1 + 1)], j = bucketA[c1 + 1], t = 0, c2 = -1; + i <= j; + --j + ) + { + if (0 <= (s1 = s = this.SA[j])) + { + if (--s < 0) + s = this.n - 1; + + if ((c0 = (this.T[s] & 0xff)) <= c1) + { + this.SA[j] = ~s1; + if ((0 < s) && ((this.T[s - 1] & 0xff) > c0)) + { + s = ~s; + } + if (c2 == c0) + { + this.SA[--t] = s; + } + else + { + if (0 <= c2) + bucketB[BUCKET_B(c2, c1)] = t; + + this.SA[t = bucketB[BUCKET_B(c2 = c0, c1)] - 1] = s; + } + } + } + else + { + this.SA[j] = ~s; + } + } + } + + for (i = 0; i < this.n; ++i) + { + if (0 <= (s1 = s = this.SA[i])) + { + if (--s < 0) + s = this.n - 1; + + if ((c0 = (this.T[s] & 0xff)) >= (this.T[s + 1] & 0xff)) + { + if ((0 < s) && ((this.T[s - 1] & 0xff) < c0)) + s = ~s; + + if (c0 == c2) + { + this.SA[++t] = s; + } + else + { + if (c2 != -1) // BUGFIX: Original code can write to bucketA[-1] + bucketA[c2] = t; + this.SA[t = bucketA[c2 = c0] + 1] = s; + } + } + } + else + { + s1 = ~s1; + } + + if (s1 == 0) + { + this.SA[i] = this.T[this.n - 1]; + orig = i; + } + else + { + this.SA[i] = this.T[s1 - 1]; + } + } + + return orig; + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageDecoder.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageDecoder.cs new file mode 100644 index 000000000..20a302799 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageDecoder.cs @@ -0,0 +1,186 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// + /// A decoder for the BZip2 Huffman coding stage + /// + internal class BZip2HuffmanStageDecoder + { + // The BZip2BitInputStream from which Huffman codes are read + private readonly IBZip2BitInputStream bitInputStream; + + // The longest Huffman code length accepted by the decoder + private const int HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH = 23; + + // The Huffman table number to use for each group of 50 symbols + private readonly byte[] selectors; + + // The minimum code length for each Huffman table + private readonly int[] minimumLengths = new int[ + BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES + ]; + + /// + /// An array of values for each Huffman table that must be subtracted from the numerical value of + /// a Huffman code of a given bit length to give its canonical code index + /// + private readonly int[,] codeBases = new int[ + BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES, + BZip2HuffmanStageDecoder.HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH + 2 + ]; + + /// + /// An array of values for each Huffman table that gives the highest numerical value of a Huffman + /// code of a given bit length + /// + private readonly int[,] codeLimits = new int[ + BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES, + BZip2HuffmanStageDecoder.HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH + 1 + ]; + + // A mapping for each Huffman table from canonical code index to output symbol + private readonly int[,] codeSymbols = new int[ + BZip2BlockDecompressor.HUFFMAN_MAXIMUM_TABLES, + BZip2MTFAndRLE2StageEncoder.HUFFMAN_MAXIMUM_ALPHABET_SIZE + ]; + + // The Huffman table for the current group + private int currentTable; + + // The index of the current group within the selectors array + private int groupIndex = -1; + + // The byte position within the current group. A new group is selected every 50 decoded bytes + private int groupPosition = -1; + + /// + /// Public constructor + /// + /// The BZip2BitInputStream from which Huffman codes are read + /// The total number of codes (uniform for each table) + /// The Canonical Huffman code lengths for each table + /// The Huffman table number to use for each group of 50 symbols + public BZip2HuffmanStageDecoder( + IBZip2BitInputStream bitInputStream, + int alphabetSize, + byte[,] tableCodeLengths, + byte[] selectors + ) + { + this.bitInputStream = bitInputStream; + this.selectors = selectors; + this.currentTable = this.selectors[0]; + this.CreateHuffmanDecodingTables(alphabetSize, tableCodeLengths); + } + + /// + /// Decodes and returns the next symbol + /// + /// The decoded symbol + /// if the end of the input stream is reached while decoding + public int NextSymbol() + { + // Move to next group selector if required + if (((++this.groupPosition % BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH) == 0)) + { + this.groupIndex++; + if (this.groupIndex == this.selectors.Length) + throw new IOException("Error decoding BZip2 block"); + + this.currentTable = this.selectors[this.groupIndex] & 0xff; + } + + var codeLength = this.minimumLengths[this.currentTable]; + + // Starting with the minimum bit length for the table, read additional bits one at a time + // until a complete code is recognised + for ( + uint codeBits = this.bitInputStream.ReadBits(codeLength); + codeLength <= BZip2HuffmanStageDecoder.HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH; + codeLength++ + ) + { + if (codeBits <= this.codeLimits[this.currentTable, codeLength]) + { + // Convert the code to a symbol index and return + return this.codeSymbols[ + this.currentTable, + codeBits - this.codeBases[this.currentTable, codeLength] + ]; + } + codeBits = (codeBits << 1) | this.bitInputStream.ReadBits(1); + } + + // A valid code was not recognised + throw new IOException("Error decoding BZip2 block"); + } + + /// + /// Constructs Huffman decoding tables from lists of Canonical Huffman code lengths + /// + /// The total number of codes (uniform for each table) + /// The Canonical Huffman code lengths for each table + private void CreateHuffmanDecodingTables(int alphabetSize, byte[,] tableCodeLengths) + { + for (int table = 0; table < tableCodeLengths.GetLength(0); table++) + { + int minimumLength = BZip2HuffmanStageDecoder.HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH; + int maximumLength = 0; + + // Find the minimum and maximum code length for the table + for (int i = 0; i < alphabetSize; i++) + { + maximumLength = Math.Max(tableCodeLengths[table, i], maximumLength); + minimumLength = Math.Min(tableCodeLengths[table, i], minimumLength); + } + this.minimumLengths[table] = minimumLength; + + // Calculate the first output symbol for each code length + for (int i = 0; i < alphabetSize; i++) + { + this.codeBases[table, tableCodeLengths[table, i] + 1]++; + } + for ( + int i = 1; + i < BZip2HuffmanStageDecoder.HUFFMAN_DECODE_MAXIMUM_CODE_LENGTH + 2; + i++ + ) + { + this.codeBases[table, i] += this.codeBases[table, i - 1]; + } + + // Calculate the first and last Huffman code for each code length (codes at a given length are sequential in value) + for (int i = minimumLength, code = 0; i <= maximumLength; i++) + { + int base1 = code; + code += this.codeBases[table, i + 1] - this.codeBases[table, i]; + this.codeBases[table, i] = base1 - this.codeBases[table, i]; + this.codeLimits[table, i] = code - 1; + code <<= 1; + } + + // Populate the mapping from canonical code index to output symbol + for ( + int bitLength = minimumLength, codeIndex = 0; + bitLength <= maximumLength; + bitLength++ + ) + { + for (int symbol = 0; symbol < alphabetSize; symbol++) + { + if (tableCodeLengths[table, symbol] == bitLength) + this.codeSymbols[table, codeIndex++] = symbol; + } + } + } + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageEncoder.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageEncoder.cs new file mode 100644 index 000000000..9f83d67a5 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/BZip2HuffmanStageEncoder.cs @@ -0,0 +1,387 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// + /// An encoder for the BZip2 Huffman encoding stage + /// + internal class BZip2HuffmanStageEncoder + { + // Used in initial Huffman table generation + private const int HUFFMAN_HIGH_SYMBOL_COST = 15; + + // The longest Huffman code length created by the encoder + private const int HUFFMAN_ENCODE_MAXIMUM_CODE_LENGTH = 20; + + // Number of symbols decoded after which a new Huffman table is selected + public const int HUFFMAN_GROUP_RUN_LENGTH = 50; + + // The BZip2BitOutputStream to which the Huffman tables and data is written + private readonly IBZip2BitOutputStream bitOutputStream; + + // The output of the Move To Front Transform and Run Length Encoding[2] stages + private readonly ushort[] mtfBlock; + + // The actual number of values contained in the mtfBlock array + private readonly int mtfLength; + + // The number of unique values in the mtfBlock array + private readonly int mtfAlphabetSize; + + // The global frequencies of values within the mtfBlock array + private readonly int[] mtfSymbolFrequencies; + + // The Canonical Huffman code lengths for each table + private readonly int[,] huffmanCodeLengths; + + // Merged code symbols for each table. The value at each position is ((code length << 24) | code) + private readonly int[,] huffmanMergedCodeSymbols; + + // The selectors for each segment + private readonly byte[] selectors; + + /// + /// Public constructor + /// + /// The BZip2BitOutputStream to write to + /// The MTF block data + /// The actual length of the MTF block + /// The size of the MTF block's alphabet + /// The frequencies the MTF block's symbols + public BZip2HuffmanStageEncoder( + IBZip2BitOutputStream bitOutputStream, + ushort[] mtfBlock, + int mtfLength, + int mtfAlphabetSize, + int[] mtfSymbolFrequencies + ) + { + this.bitOutputStream = bitOutputStream; + this.mtfBlock = mtfBlock; + this.mtfSymbolFrequencies = mtfSymbolFrequencies; + this.mtfAlphabetSize = mtfAlphabetSize; + this.mtfLength = mtfLength; + + var totalTables = selectTableCount(mtfLength); + + this.huffmanCodeLengths = new int[totalTables, mtfAlphabetSize]; + this.huffmanMergedCodeSymbols = new int[totalTables, mtfAlphabetSize]; + this.selectors = new byte[ + (mtfLength + BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH - 1) + / BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH + ]; + } + + /// + /// Encodes and writes the block data + /// + /// on any I/O error writing the data + public void Encode() + { + // Create optimised selector list and Huffman tables + this.generateHuffmanOptimisationSeeds(); + for (var i = 3; i >= 0; i--) + { + this.optimiseSelectorsAndHuffmanTables(i == 0); + } + this.assignHuffmanCodeSymbols(); + + // Write out the tables and the block data encoded with them + this.writeSelectorsAndHuffmanTables(); + this.writeBlockData(); + } + + /// + /// Selects an appropriate table count for a given MTF length + /// + /// The length to select a table count for + /// The selected table count + private static int selectTableCount(int mtfLength) + { + if (mtfLength >= 2400) + return 6; + if (mtfLength >= 1200) + return 5; + if (mtfLength >= 600) + return 4; + return mtfLength >= 200 ? 3 : 2; + } + + /// + /// Generate a Huffman code length table for a given list of symbol frequencies + /// + /// The total number of symbols + /// The frequencies of the symbols + /// The array to which the generated code lengths should be written + /// + private static void generateHuffmanCodeLengths( + int alphabetSize, + int[,] symbolFrequencies, + int[,] codeLengths, + int index + ) + { + var mergedFrequenciesAndIndices = new int[alphabetSize]; + var sortedFrequencies = new int[alphabetSize]; + + // The Huffman allocator needs its input symbol frequencies to be sorted, but we need to return code lengths in the same order as the + // corresponding frequencies are passed in + + // The symbol frequency and index are merged into a single array of integers - frequency in the high 23 bits, index in the low 9 bits. + // 2^23 = 8,388,608 which is higher than the maximum possible frequency for one symbol in a block + // 2^9 = 512 which is higher than the maximum possible alphabet size (== 258) + // Sorting this array simultaneously sorts the frequencies and leaves a lookup that can be used to cheaply invert the sort + for (int i = 0; i < alphabetSize; i++) + mergedFrequenciesAndIndices[i] = (symbolFrequencies[index, i] << 9) | i; + + Array.Sort(mergedFrequenciesAndIndices); + for (int i = 0; i < alphabetSize; i++) + sortedFrequencies[i] = mergedFrequenciesAndIndices[i] >> 9; + + // Allocate code lengths - the allocation is in place, so the code lengths will be in the sortedFrequencies array afterwards + HuffmanAllocator.AllocateHuffmanCodeLengths( + sortedFrequencies, + BZip2HuffmanStageEncoder.HUFFMAN_ENCODE_MAXIMUM_CODE_LENGTH + ); + + // Reverse the sort to place the code lengths in the same order as the symbols whose frequencies were passed in + for (int i = 0; i < alphabetSize; i++) + codeLengths[index, mergedFrequenciesAndIndices[i] & 0x1ff] = sortedFrequencies[i]; + } + + /// + /// Generate initial Huffman code length tables, giving each table a different low cost section + /// of the alphabet that is roughly equal in overall cumulative frequency. Note that the initial + /// tables are invalid for actual Huffman code generation, and only serve as the seed for later + /// iterative optimisation in optimiseSelectorsAndHuffmanTables(int) + /// + private void generateHuffmanOptimisationSeeds() + { + int totalTables = this.huffmanCodeLengths.GetLength(0); + int remainingLength = this.mtfLength; + int lowCostEnd = -1; + + for (int i = 0; i < totalTables; i++) + { + int targetCumulativeFrequency = remainingLength / (totalTables - i); + int lowCostStart = lowCostEnd + 1; + int actualCumulativeFrequency = 0; + + while ( + (actualCumulativeFrequency < targetCumulativeFrequency) + && (lowCostEnd < (this.mtfAlphabetSize - 1)) + ) + { + actualCumulativeFrequency += this.mtfSymbolFrequencies[++lowCostEnd]; + } + + if ( + (lowCostEnd > lowCostStart) + && (i != 0) + && (i != (totalTables - 1)) + && (((totalTables - i) & 1) == 0) + ) + actualCumulativeFrequency -= this.mtfSymbolFrequencies[lowCostEnd--]; + + for (var j = 0; j < this.mtfAlphabetSize; j++) + { + if ((j < lowCostStart) || (j > lowCostEnd)) + this.huffmanCodeLengths[i, j] = + BZip2HuffmanStageEncoder.HUFFMAN_HIGH_SYMBOL_COST; + } + + remainingLength -= actualCumulativeFrequency; + } + } + + /// + /// Co-optimise the selector list and the alternative Huffman table code lengths. This method is + /// called repeatedly in the hope that the total encoded size of the selectors, the Huffman code + /// lengths and the block data encoded with them will converge towards a minimum.
+ /// If the data is highly incompressible, it is possible that the total encoded size will + /// instead diverge (increase) slightly.
+ ///
+ /// If true, write out the (final) chosen selectors + private void optimiseSelectorsAndHuffmanTables(bool storeSelectors) + { + int totalTables = this.huffmanCodeLengths.GetLength(0); + var tableFrequencies = new int[totalTables, this.mtfAlphabetSize]; + + int selectorIndex = 0; + + // Find the best table for each group of 50 block bytes based on the current Huffman code lengths + for (int groupStart = 0; groupStart < this.mtfLength; ) + { + int groupEnd = + Math.Min( + groupStart + BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH, + this.mtfLength + ) - 1; + + // Calculate the cost of this group when encoded by each table + var cost = new int[totalTables]; + for (int i = groupStart; i <= groupEnd; i++) + { + int value = this.mtfBlock[i]; + for (var j = 0; j < totalTables; j++) + { + cost[j] += this.huffmanCodeLengths[j, value]; + } + } + + // Find the table with the least cost for this group + byte bestTable = 0; + var bestCost = cost[0]; + for (byte i = 1; i < totalTables; i++) + { + var tableCost = cost[i]; + if (tableCost < bestCost) + { + bestCost = tableCost; + bestTable = i; + } + } + + // Accumulate symbol frequencies for the table chosen for this block + for (int i = groupStart; i <= groupEnd; i++) + { + tableFrequencies[bestTable, this.mtfBlock[i]]++; + } + + // Store a selector indicating the table chosen for this block + if (storeSelectors) + { + this.selectors[selectorIndex++] = bestTable; + } + + groupStart = groupEnd + 1; + } + + // Generate new Huffman code lengths based on the frequencies for each table accumulated in this iteration + for (int i = 0; i < totalTables; i++) + { + generateHuffmanCodeLengths( + this.mtfAlphabetSize, + tableFrequencies, + this.huffmanCodeLengths, + i + ); + } + } + + // Assigns Canonical Huffman codes based on the calculated lengths + private void assignHuffmanCodeSymbols() + { + int totalTables = this.huffmanCodeLengths.GetLength(0); + + for (int i = 0; i < totalTables; i++) + { + int minimumLength = 32; + int maximumLength = 0; + for (var j = 0; j < this.mtfAlphabetSize; j++) + { + var length = this.huffmanCodeLengths[i, j]; + if (length > maximumLength) + { + maximumLength = length; + } + if (length < minimumLength) + { + minimumLength = length; + } + } + + int code = 0; + for (int j = minimumLength; j <= maximumLength; j++) + { + for (var k = 0; k < this.mtfAlphabetSize; k++) + { + if ((this.huffmanCodeLengths[i, k] & 0xff) == j) + { + this.huffmanMergedCodeSymbols[i, k] = (j << 24) | code; + code++; + } + } + code <<= 1; + } + } + } + + /// + /// Write out the selector list and Huffman tables + /// + /// on any I/O error writing the data + private void writeSelectorsAndHuffmanTables() + { + int totalSelectors = this.selectors.Length; + int totalTables = this.huffmanCodeLengths.GetLength(0); + + this.bitOutputStream.WriteBits(3, (uint)totalTables); + this.bitOutputStream.WriteBits(15, (uint)totalSelectors); + + // Write the selectors + var selectorMTF = new MoveToFront(); + for (int i = 0; i < totalSelectors; i++) + { + this.bitOutputStream.WriteUnary(selectorMTF.ValueToFront(this.selectors[i])); + } + + // Write the Huffman tables + for (int i = 0; i < totalTables; i++) + { + var currentLength = this.huffmanCodeLengths[i, 0]; + + this.bitOutputStream.WriteBits(5, (uint)currentLength); + + for (var j = 0; j < this.mtfAlphabetSize; j++) + { + var codeLength = this.huffmanCodeLengths[i, j]; + var value = (currentLength < codeLength) ? 2u : 3u; + var delta = Math.Abs(codeLength - currentLength); + while (delta-- > 0) + { + this.bitOutputStream.WriteBits(2, value); + } + this.bitOutputStream.WriteBoolean(false); + currentLength = codeLength; + } + } + } + + /// + /// Writes out the encoded block data + /// + /// on any I/O error writing the data + private void writeBlockData() + { + int selectorIndex = 0; + + for (int mtfIndex = 0; mtfIndex < this.mtfLength; ) + { + int groupEnd = + Math.Min( + mtfIndex + BZip2HuffmanStageEncoder.HUFFMAN_GROUP_RUN_LENGTH, + this.mtfLength + ) - 1; + + int index = this.selectors[selectorIndex++]; + + while (mtfIndex <= groupEnd) + { + var mergedCodeSymbol = this.huffmanMergedCodeSymbols[ + index, + this.mtfBlock[mtfIndex++] + ]; + this.bitOutputStream.WriteBits(mergedCodeSymbol >> 24, (uint)mergedCodeSymbol); + } + } + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/Bzip2MTFAndRLE2StageEncoder.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/Bzip2MTFAndRLE2StageEncoder.cs new file mode 100644 index 000000000..5985c6f4a --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/Bzip2MTFAndRLE2StageEncoder.cs @@ -0,0 +1,168 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// An encoder for the BZip2 Move To Front Transform and Run-Length Encoding[2] stages + /// + /// An encoder for the BZip2 Move To Front Transform and Run-Length Encoding[2] stages. + /// Although conceptually these two stages are separate, it is computationally efficient to perform them in one pass. + /// + internal class BZip2MTFAndRLE2StageEncoder + { + // The Burrows-Wheeler transformed block + private readonly int[] bwtBlock; + + // Actual length of the data in the bwtBlock array + private readonly int bwtLength; + + // At each position, true if the byte value with that index is present within the block, otherwise false + private readonly bool[] bwtValuesInUse; + + // The output of the Move To Front Transform and Run-Length Encoding[2] stages + private readonly ushort[] mtfBlock; + + // The global frequencies of values within the mtfBlock array + private readonly int[] mtfSymbolFrequencies = new int[ + BZip2MTFAndRLE2StageEncoder.HUFFMAN_MAXIMUM_ALPHABET_SIZE + ]; + + // Maximum possible Huffman alphabet size + public const int HUFFMAN_MAXIMUM_ALPHABET_SIZE = 258; + + // Huffman symbol used for run-length encoding + public const ushort RLE_SYMBOL_RUNA = 0; + + // Huffman symbol used for run-length encoding + public const ushort RLE_SYMBOL_RUNB = 1; + + // Gets the encoded MTF block + public ushort[] MtfBlock + { + get { return this.mtfBlock; } + } + + // Gets The actual length of the MTF block + public int MtfLength { get; private set; } + + // Gets the size of the MTF block's alphabet + public int MtfAlphabetSize { get; private set; } + + // Gets the frequencies of the MTF block's symbols + public int[] MtfSymbolFrequencies + { + get { return this.mtfSymbolFrequencies; } + } + + /// + /// Public constructor + /// + /// The Burrows Wheeler Transformed block data + /// The actual length of the BWT data + /// The values that are present within the BWT data. For each index, + /// true if that value is present within the data, otherwise false + public BZip2MTFAndRLE2StageEncoder(int[] bwtBlock, int bwtLength, bool[] bwtValuesPresent) + { + this.bwtBlock = bwtBlock; + this.bwtLength = bwtLength; + this.bwtValuesInUse = bwtValuesPresent; + this.mtfBlock = new ushort[bwtLength + 1]; + } + + // Performs the Move To Front transform and Run Length Encoding[1] stages + public void Encode() + { + var huffmanSymbolMap = new byte[256]; + var symbolMTF = new MoveToFront(); + + int totalUniqueValues = 0; + for (var i = 0; i < 256; i++) + { + if (this.bwtValuesInUse[i]) + huffmanSymbolMap[i] = (byte)totalUniqueValues++; + } + + int endOfBlockSymbol = totalUniqueValues + 1; + int mtfIndex = 0; + int repeatCount = 0; + int totalRunAs = 0; + int totalRunBs = 0; + + for (var i = 0; i < this.bwtLength; i++) + { + // Move To Front + int mtfPosition = symbolMTF.ValueToFront(huffmanSymbolMap[this.bwtBlock[i] & 0xff]); + + // Run Length Encode + if (mtfPosition == 0) + { + repeatCount++; + } + else + { + if (repeatCount > 0) + { + repeatCount--; + while (true) + { + if ((repeatCount & 1) == 0) + { + this.mtfBlock[mtfIndex++] = + BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNA; + totalRunAs++; + } + else + { + this.mtfBlock[mtfIndex++] = + BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNB; + totalRunBs++; + } + + if (repeatCount <= 1) + break; + + repeatCount = (repeatCount - 2) >> 1; + } + repeatCount = 0; + } + + this.mtfBlock[mtfIndex++] = (char)(mtfPosition + 1); + this.mtfSymbolFrequencies[mtfPosition + 1]++; + } + } + + if (repeatCount > 0) + { + repeatCount--; + while (true) + { + if ((repeatCount & 1) == 0) + { + this.mtfBlock[mtfIndex++] = BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNA; + totalRunAs++; + } + else + { + this.mtfBlock[mtfIndex++] = BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNB; + totalRunBs++; + } + + if (repeatCount <= 1) + break; + + repeatCount = (repeatCount - 2) >> 1; + } + } + + this.mtfBlock[mtfIndex] = (ushort)endOfBlockSymbol; + this.mtfSymbolFrequencies[endOfBlockSymbol]++; + this.mtfSymbolFrequencies[BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNA] += totalRunAs; + this.mtfSymbolFrequencies[BZip2MTFAndRLE2StageEncoder.RLE_SYMBOL_RUNB] += totalRunBs; + + this.MtfLength = mtfIndex + 1; + this.MtfAlphabetSize = endOfBlockSymbol + 1; + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/CRC32.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/CRC32.cs new file mode 100644 index 000000000..31594e263 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/CRC32.cs @@ -0,0 +1,298 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// + /// A CRC32 calculator + /// + internal class CRC32 + { + /// The CRC lookup table + private static readonly uint[] Crc32Lookup = + { + 0x00000000, + 0x04c11db7, + 0x09823b6e, + 0x0d4326d9, + 0x130476dc, + 0x17c56b6b, + 0x1a864db2, + 0x1e475005, + 0x2608edb8, + 0x22c9f00f, + 0x2f8ad6d6, + 0x2b4bcb61, + 0x350c9b64, + 0x31cd86d3, + 0x3c8ea00a, + 0x384fbdbd, + 0x4c11db70, + 0x48d0c6c7, + 0x4593e01e, + 0x4152fda9, + 0x5f15adac, + 0x5bd4b01b, + 0x569796c2, + 0x52568b75, + 0x6a1936c8, + 0x6ed82b7f, + 0x639b0da6, + 0x675a1011, + 0x791d4014, + 0x7ddc5da3, + 0x709f7b7a, + 0x745e66cd, + 0x9823b6e0, + 0x9ce2ab57, + 0x91a18d8e, + 0x95609039, + 0x8b27c03c, + 0x8fe6dd8b, + 0x82a5fb52, + 0x8664e6e5, + 0xbe2b5b58, + 0xbaea46ef, + 0xb7a96036, + 0xb3687d81, + 0xad2f2d84, + 0xa9ee3033, + 0xa4ad16ea, + 0xa06c0b5d, + 0xd4326d90, + 0xd0f37027, + 0xddb056fe, + 0xd9714b49, + 0xc7361b4c, + 0xc3f706fb, + 0xceb42022, + 0xca753d95, + 0xf23a8028, + 0xf6fb9d9f, + 0xfbb8bb46, + 0xff79a6f1, + 0xe13ef6f4, + 0xe5ffeb43, + 0xe8bccd9a, + 0xec7dd02d, + 0x34867077, + 0x30476dc0, + 0x3d044b19, + 0x39c556ae, + 0x278206ab, + 0x23431b1c, + 0x2e003dc5, + 0x2ac12072, + 0x128e9dcf, + 0x164f8078, + 0x1b0ca6a1, + 0x1fcdbb16, + 0x018aeb13, + 0x054bf6a4, + 0x0808d07d, + 0x0cc9cdca, + 0x7897ab07, + 0x7c56b6b0, + 0x71159069, + 0x75d48dde, + 0x6b93dddb, + 0x6f52c06c, + 0x6211e6b5, + 0x66d0fb02, + 0x5e9f46bf, + 0x5a5e5b08, + 0x571d7dd1, + 0x53dc6066, + 0x4d9b3063, + 0x495a2dd4, + 0x44190b0d, + 0x40d816ba, + 0xaca5c697, + 0xa864db20, + 0xa527fdf9, + 0xa1e6e04e, + 0xbfa1b04b, + 0xbb60adfc, + 0xb6238b25, + 0xb2e29692, + 0x8aad2b2f, + 0x8e6c3698, + 0x832f1041, + 0x87ee0df6, + 0x99a95df3, + 0x9d684044, + 0x902b669d, + 0x94ea7b2a, + 0xe0b41de7, + 0xe4750050, + 0xe9362689, + 0xedf73b3e, + 0xf3b06b3b, + 0xf771768c, + 0xfa325055, + 0xfef34de2, + 0xc6bcf05f, + 0xc27dede8, + 0xcf3ecb31, + 0xcbffd686, + 0xd5b88683, + 0xd1799b34, + 0xdc3abded, + 0xd8fba05a, + 0x690ce0ee, + 0x6dcdfd59, + 0x608edb80, + 0x644fc637, + 0x7a089632, + 0x7ec98b85, + 0x738aad5c, + 0x774bb0eb, + 0x4f040d56, + 0x4bc510e1, + 0x46863638, + 0x42472b8f, + 0x5c007b8a, + 0x58c1663d, + 0x558240e4, + 0x51435d53, + 0x251d3b9e, + 0x21dc2629, + 0x2c9f00f0, + 0x285e1d47, + 0x36194d42, + 0x32d850f5, + 0x3f9b762c, + 0x3b5a6b9b, + 0x0315d626, + 0x07d4cb91, + 0x0a97ed48, + 0x0e56f0ff, + 0x1011a0fa, + 0x14d0bd4d, + 0x19939b94, + 0x1d528623, + 0xf12f560e, + 0xf5ee4bb9, + 0xf8ad6d60, + 0xfc6c70d7, + 0xe22b20d2, + 0xe6ea3d65, + 0xeba91bbc, + 0xef68060b, + 0xd727bbb6, + 0xd3e6a601, + 0xdea580d8, + 0xda649d6f, + 0xc423cd6a, + 0xc0e2d0dd, + 0xcda1f604, + 0xc960ebb3, + 0xbd3e8d7e, + 0xb9ff90c9, + 0xb4bcb610, + 0xb07daba7, + 0xae3afba2, + 0xaafbe615, + 0xa7b8c0cc, + 0xa379dd7b, + 0x9b3660c6, + 0x9ff77d71, + 0x92b45ba8, + 0x9675461f, + 0x8832161a, + 0x8cf30bad, + 0x81b02d74, + 0x857130c3, + 0x5d8a9099, + 0x594b8d2e, + 0x5408abf7, + 0x50c9b640, + 0x4e8ee645, + 0x4a4ffbf2, + 0x470cdd2b, + 0x43cdc09c, + 0x7b827d21, + 0x7f436096, + 0x7200464f, + 0x76c15bf8, + 0x68860bfd, + 0x6c47164a, + 0x61043093, + 0x65c52d24, + 0x119b4be9, + 0x155a565e, + 0x18197087, + 0x1cd86d30, + 0x029f3d35, + 0x065e2082, + 0x0b1d065b, + 0x0fdc1bec, + 0x3793a651, + 0x3352bbe6, + 0x3e119d3f, + 0x3ad08088, + 0x2497d08d, + 0x2056cd3a, + 0x2d15ebe3, + 0x29d4f654, + 0xc5a92679, + 0xc1683bce, + 0xcc2b1d17, + 0xc8ea00a0, + 0xd6ad50a5, + 0xd26c4d12, + 0xdf2f6bcb, + 0xdbee767c, + 0xe3a1cbc1, + 0xe760d676, + 0xea23f0af, + 0xeee2ed18, + 0xf0a5bd1d, + 0xf464a0aa, + 0xf9278673, + 0xfde69bc4, + 0x89b8fd09, + 0x8d79e0be, + 0x803ac667, + 0x84fbdbd0, + 0x9abc8bd5, + 0x9e7d9662, + 0x933eb0bb, + 0x97ffad0c, + 0xafb010b1, + 0xab710d06, + 0xa6322bdf, + 0xa2f33668, + 0xbcb4666d, + 0xb8757bda, + 0xb5365d03, + 0xb1f740b4, + }; + + /// The current CRC + private uint crc = 0xffffffff; + + /// Gets the current CRC + public uint CRC => ~this.crc; + + /// Updates the CRC with a single byte + /// The value to update the CRC with + public void UpdateCrc(int value) + { + this.crc = (this.crc << 8) ^ CRC32.Crc32Lookup[((this.crc >> 24) ^ value) & 0xff]; + } + + /// Update the CRC with a sequence of identical bytes + /// The value to update the CRC with + /// The number of bytes + public void UpdateCrc(int value, int count) + { + while (count-- > 0) + { + this.crc = (this.crc << 8) ^ CRC32.Crc32Lookup[((this.crc >> 24) ^ value) & 0xff]; + } + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/HuffmanAllocator.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/HuffmanAllocator.cs new file mode 100644 index 000000000..bc1eea761 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/HuffmanAllocator.cs @@ -0,0 +1,220 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// An in-place, length restricted Canonical Huffman code length allocator + /// + /// Based on the algorithm proposed by R.L.Milidiú, A.A.Pessoa and E.S.Laber + /// in "In-place Length-Restricted Prefix Coding" (see: http://www-di.inf.puc-rio.br/~laber/public/spire98.ps) + /// and incorporating additional ideas from the implementation of "shcodec" by Simakov Alexander + /// (see: http://webcenter.ru/~xander/) + /// + internal static class HuffmanAllocator + { + /// Allocates Canonical Huffman code lengths in place based on a sorted frequency array + /// On input, a sorted array of symbol frequencies; On output, an array of Canonical Huffman code lenghts + /// The maximum code length. Must be at least ceil(log2(array.length)) + public static void AllocateHuffmanCodeLengths(int[] array, int maximumLength) + { + switch (array.Length) + { + case 2: + array[1] = 1; + break; + case 1: + array[0] = 1; + return; + } + + // Pass 1 : Set extended parent pointers + SetExtendedParentPointers(array); + + // Pass 2 : Find number of nodes to relocate in order to achieve maximum code length + int nodesToRelocate = FindNodesToRelocate(array, maximumLength); + + // Pass 3 : Generate code lengths + if ((array[0] % array.Length) >= nodesToRelocate) + { + AllocateNodeLengths(array); + } + else + { + var insertDepth = maximumLength - SignificantBits(nodesToRelocate - 1); + AllocateNodeLengthsWithRelocation(array, nodesToRelocate, insertDepth); + } + } + + /// + /// + /// + /// The code length array + /// The input position + /// The number of internal nodes to be relocated + /// The smallest k such that nodesToMove <= k <= i and i <= (array[k] % array.length) + private static int First(int[] array, int i, int nodesToMove) + { + int length = array.Length; + int limit = i; + int k = array.Length - 2; + + while ((i >= nodesToMove) && ((array[i] % length) > limit)) + { + k = i; + i -= (limit - i + 1); + } + i = Math.Max(nodesToMove - 1, i); + + while (k > (i + 1)) + { + var temp = (i + k) >> 1; + if ((array[temp] % length) > limit) + k = temp; + else + i = temp; + } + + return k; + } + + /// + /// Fills the code array with extended parent pointers + /// + /// The code length array + private static void SetExtendedParentPointers(int[] array) + { + int length = array.Length; + + array[0] += array[1]; + + for (int headNode = 0, tailNode = 1, topNode = 2; tailNode < (length - 1); tailNode++) + { + int temp; + if ((topNode >= length) || (array[headNode] < array[topNode])) + { + temp = array[headNode]; + array[headNode++] = tailNode; + } + else + { + temp = array[topNode++]; + } + + if ( + (topNode >= length) + || ((headNode < tailNode) && (array[headNode] < array[topNode])) + ) + { + temp += array[headNode]; + array[headNode++] = tailNode + length; + } + else + { + temp += array[topNode++]; + } + + array[tailNode] = temp; + } + } + + /// + /// Finds the number of nodes to relocate in order to achieve a given code length limit + /// + /// The code length array + /// The maximum bit length for the generated codes + /// The number of nodes to relocate + private static int FindNodesToRelocate(int[] array, int maximumLength) + { + int currentNode = array.Length - 2; + + for ( + var currentDepth = 1; + (currentDepth < (maximumLength - 1)) && (currentNode > 1); + currentDepth++ + ) + currentNode = First(array, currentNode - 1, 0); + + return currentNode; + } + + /// + /// A final allocation pass with no code length limit + /// + /// The code length array + private static void AllocateNodeLengths(int[] array) + { + int firstNode = array.Length - 2; + int nextNode = array.Length - 1; + + for (int currentDepth = 1, availableNodes = 2; availableNodes > 0; currentDepth++) + { + int lastNode = firstNode; + firstNode = First(array, lastNode - 1, 0); + + for (var i = availableNodes - (lastNode - firstNode); i > 0; i--) + array[nextNode--] = currentDepth; + + availableNodes = (lastNode - firstNode) << 1; + } + } + + /// + /// A final allocation pass that relocates nodes in order to achieve a maximum code length limit + /// + /// The code length array + /// The number of internal nodes to be relocated + /// The depth at which to insert relocated nodes + private static void AllocateNodeLengthsWithRelocation( + int[] array, + int nodesToMove, + int insertDepth + ) + { + int firstNode = array.Length - 2; + int nextNode = array.Length - 1; + int currentDepth = (insertDepth == 1) ? 2 : 1; + int nodesLeftToMove = (insertDepth == 1) ? nodesToMove - 2 : nodesToMove; + + for (int availableNodes = currentDepth << 1; availableNodes > 0; currentDepth++) + { + int lastNode = firstNode; + firstNode = + (firstNode <= nodesToMove) + ? firstNode + : First(array, lastNode - 1, nodesToMove); + + int offset = 0; + if (currentDepth >= insertDepth) + { + offset = Math.Min(nodesLeftToMove, 1 << (currentDepth - insertDepth)); + } + else if (currentDepth == (insertDepth - 1)) + { + offset = 1; + if ((array[firstNode]) == lastNode) + firstNode++; + } + + for (var i = availableNodes - (lastNode - firstNode + offset); i > 0; i--) + array[nextNode--] = currentDepth; + + nodesLeftToMove -= offset; + availableNodes = (lastNode - firstNode + offset) << 1; + } + } + + private static int SignificantBits(int x) + { + int n; + for (n = 0; x > 0; n++) + { + x >>= 1; + } + return n; + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Algorithm/MoveToFront.cs b/src/SharpCompress/Compressors/BZip2MT/Algorithm/MoveToFront.cs new file mode 100644 index 000000000..a9fa23251 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Algorithm/MoveToFront.cs @@ -0,0 +1,61 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System; + +namespace SharpCompress.Compressors.BZip2MT.Algorithm +{ + /// + /// A 256 entry Move To Front transform + /// + internal class MoveToFront + { + /// The Move To Front list + private readonly byte[] mtf; + + /// Public constructor + public MoveToFront() + { + this.mtf = new byte[256]; + for (int i = 0; i < 256; i++) + this.mtf[i] = (byte)i; + } + + /// Moves a value to the head of the MTF list (forward Move To Front transform) + /// The value to move + /// The position the value moved from + public int ValueToFront(byte value) + { + int index = 0; + byte temp = this.mtf[0]; + + if (value == temp) + return index; + + this.mtf[0] = value; + while (temp != value) + { + index++; + byte temp2 = temp; + temp = this.mtf[index]; + this.mtf[index] = temp2; + } + + return index; + } + + /// Gets the value from a given index and moves it to the front of the MTF list (inverse Move To Front transform) + /// The index to move + /// The value at the given index + public byte IndexToFront(int index) + { + byte value = this.mtf[index]; + Array.ConstrainedCopy(this.mtf, 0, this.mtf, 1, index); + this.mtf[0] = value; + + return value; + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStream.cs b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStream.cs new file mode 100644 index 000000000..d9f9e73de --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStream.cs @@ -0,0 +1,114 @@ +// Bzip2 library for .net +// By Jaime Olivares +// Location: http://github.com/jaime-olivares/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 + +using System.IO; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.InputStream +{ + /// + /// Implements a bit-wise input stream + /// + internal class BZip2BitInputStream : IBZip2BitInputStream + { + // The stream from which bits are read + private Stream _inputStream; + + // A buffer of bits read from the input stream that have not yet been returned + private uint _bitBuffer; + + // The number of bits currently buffered in bitBuffer + private int _bitCount; + + /// Public constructor + /// The input stream to wrap + public BZip2BitInputStream(Stream inputStream) => this._inputStream = inputStream; + + public void Dispose() + { + // do nothing, the BZip2BitInputStream is not the owner of the _inputStream so don't dispose it + } + + /// Reads a single bit from the wrapped input stream + /// true if the bit read was 1, otherwise false + /// if no more bits are available in the input stream + public bool ReadBoolean() + { + if (this._bitCount > 0) + { + this._bitCount--; + } + else + { + int byteRead = this._inputStream.ReadByte(); + + if (byteRead < 0) + throw new IOException("Insufficient data"); + + this._bitBuffer = (this._bitBuffer << 8) | (uint)byteRead; + this._bitCount += 7; + } + + return ((this._bitBuffer & (1 << this._bitCount))) != 0; + } + + /// Reads a zero-terminated unary number from the wrapped input stream + /// The unary number + /// if no more bits are available in the input stream + public uint ReadUnary() + { + for (uint unaryCount = 0; ; unaryCount++) + { + if (this._bitCount > 0) + { + this._bitCount--; + } + else + { + var byteRead = this._inputStream.ReadByte(); + + if (byteRead < 0) + throw new IOException("Insufficient data"); + + this._bitBuffer = (this._bitBuffer << 8) | (uint)byteRead; + this._bitCount += 7; + } + + if (((this._bitBuffer & (1 << this._bitCount))) == 0) + return unaryCount; + } + } + + /// Reads up to 32 bits from the wrapped input stream + /// The number of bits to read (maximum 32) + /// The bits requested, right-aligned within the integer + /// if no more bits are available in the input stream + public uint ReadBits(int count) + { + if (this._bitCount < count) + { + while (this._bitCount < count) + { + int byteRead = this._inputStream.ReadByte(); + + if (byteRead < 0) + throw new IOException("Insufficient data"); + + this._bitBuffer = (this._bitBuffer << 8) | (uint)byteRead; + this._bitCount += 8; + } + } + + this._bitCount -= count; + + return (uint)((this._bitBuffer >> this._bitCount) & ((1 << count) - 1)); + } + + /// Reads 32 bits of input as an integer + /// The integer read + /// if 32 bits are not available in the input stream + public uint ReadInteger() => (this.ReadBits(16) << 16) | (this.ReadBits(16)); + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStreamSplitter.cs b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStreamSplitter.cs new file mode 100644 index 000000000..6e8c70159 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2BitInputStreamSplitter.cs @@ -0,0 +1,196 @@ +// Added by drone1400, December 2025 +// Location: https://github.com/drone1400/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.InputStream +{ + /// + /// Helper class for splitting a BZip2 stream into multiple Memory Streams made up of individual BZip2 blocks. + /// The spitting is done by looking for the magic block header 6 byte sequence. + /// The output MemoryStreams bits are left aligned. + /// + internal class BZip2BitInputStreamSplitter : IDisposable + { + // The stream from which bits are read + private BZip2BitInputStream? _bitInputStream; + + // internal temporary buffer + private MemoryStream? _buffer = null; + + private int _blockLevel = 0; + private int _blockSizeBytes = 0; + private uint _finalCrc = 0; + private ulong _crtVal = 0x00; + private int _bitCount = 0; + + public bool IsStreamComplete => this._bitInputStream is null; + public int BlockLevel => this._blockLevel; + public int BlockSizeBytes => this._blockSizeBytes; + public uint FinalCrc => this._finalCrc; + + public BZip2BitInputStreamSplitter( + Stream inputStream, + InputStreamHeaderCheckType inputStreamHeaderCheck = + InputStreamHeaderCheckType.FULL_HEADER, + int manualBlockLevel = 9 + ) + { + this._bitInputStream = new BZip2BitInputStream(inputStream); + this.Initialize(inputStreamHeaderCheck, manualBlockLevel); + } + + private void Initialize(InputStreamHeaderCheckType inputStreamHeaderCheck, int blockLevel) + { + if (this._bitInputStream is null) + return; + + /* Read the stream header */ + try + { + switch (inputStreamHeaderCheck) + { + case InputStreamHeaderCheckType.FULL_HEADER: + { + uint marker1 = this._bitInputStream.ReadBits(16); + uint marker2 = this._bitInputStream.ReadBits(8); + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if ( + marker1 != BZip2Constants.STREAM_START_MARKER_1 + || marker2 != BZip2Constants.STREAM_START_MARKER_2 + || blockLevel < 1 + || blockLevel > 9 + ) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + case InputStreamHeaderCheckType.NO_BZ: + { + uint marker2 = this._bitInputStream.ReadBits(8); + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if ( + marker2 != BZip2Constants.STREAM_START_MARKER_2 + || blockLevel < 1 + || blockLevel > 9 + ) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + case InputStreamHeaderCheckType.NO_BZH: + { + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if (blockLevel < 1 || blockLevel > 9) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + } + + this._blockLevel = blockLevel; + this._blockSizeBytes = blockLevel * 100000; + + this._crtVal = this._bitInputStream.ReadBits(24); + this._crtVal <<= 24; + this._crtVal |= this._bitInputStream.ReadBits(24); + + // expecting the first block header immediately after + if (this._crtVal != BZip2Constants.BLOCK_HEADER_MARKER) + { + throw new IOException("BZip2 stream format error"); + } + + this._buffer = new MemoryStream(this._blockSizeBytes + 100000); + } + catch (IOException) + { + // If the stream header was not valid, stop trying to read more data + this._bitInputStream = null; + this._buffer = null; + throw; + } + } + + /// + /// Finds the next BZip2 block and returns an in memory copy of it + /// + /// MemoryStream + public MemoryStream? CopyNextBlock() + { + void FlushByte() + { + this._buffer.WriteByte((byte)(this._crtVal >> 48)); + this._crtVal &= 0x0000FFFF_FFFFFFFF; + this._bitCount = 0; + } + + void FlushPartialByte() + { + byte partialByte = (byte)(this._crtVal >> 48); + partialByte <<= (8 - this._bitCount); + this._buffer.WriteByte(partialByte); + + this._crtVal &= 0x0000FFFF_FFFFFFFF; + this._bitCount = 0; + } + + if (this._bitInputStream is null || this._buffer is null) + return null; + + while (true) + { + this._crtVal <<= 1; + this._crtVal |= this._bitInputStream.ReadBits(1); + ulong testVal = this._crtVal & 0x0000FFFF_FFFFFFFF; + this._bitCount++; + if (this._bitCount == 8) + { + FlushByte(); + } + + if (testVal == BZip2Constants.STREAM_END_MARKER) + { + // we found the magic bit sequence for end of stream, that means we're done! + if (this._bitCount > 0) + { + FlushPartialByte(); + } + + // read the 32 bit CRC at the end of the file + this._finalCrc = this._bitInputStream.ReadInteger(); + + MemoryStream retBuffer = this._buffer; + + // clear buffer and input stream to mark that we are done processing blocks for good + this._buffer = null; + this._bitInputStream = null; + return retBuffer; + } + + if (testVal == BZip2Constants.BLOCK_HEADER_MARKER) + { + // we found the magic bit sequence! + if (this._bitCount > 0) + { + FlushPartialByte(); + } + MemoryStream retBuffer = this._buffer; + this._buffer = new MemoryStream(this._blockSizeBytes); + return retBuffer; + } + } + } + + public void Dispose() + { + this._bitInputStream?.Dispose(); + this._buffer?.Dispose(); + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2InputStream.cs new file mode 100644 index 000000000..2950e925b --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2InputStream.cs @@ -0,0 +1,293 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Algorithm; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.InputStream +{ + /// An InputStream wrapper that decompresses BZip2 data + /// Instances of this class are not threadsafe + public class BZip2InputStream : Stream + { + // The stream from which compressed BZip2 data is read and decoded + private Stream _inputStream; + + // True if the underlying stream will be closed with the current Stream + private readonly bool _isOwner; + + // An InputStream wrapper that provides bit-level reads + private BZip2BitInputStream _bitInputStream; + + // (@code true} if the end of the compressed stream has been reached, otherwise false + private bool _streamComplete; + + /// + /// The declared block size of the stream (before final run-length decoding). The final block + /// will usually be smaller, but no block in the stream has to be exactly this large, and an + /// encoder could in theory choose to mix blocks of any size up to this value. Its function is + /// therefore as a hint to the decompressor as to how much working space is sufficient to + /// decompress blocks in a given stream + /// + private uint _streamBlockSize; + + // The merged CRC of all blocks decompressed so far + private uint _streamCrc; + + // The decompressor for the current block + private BZip2BlockDecompressor? _blockDecompressor; + + /// Public constructor + /// The InputStream to wrap + /// if true, will close the stream when done + /// + /// Used when is NoHeader + public BZip2InputStream( + Stream inputStream, + bool isOwner = true, + InputStreamHeaderCheckType inputStreamHeaderCheck = + InputStreamHeaderCheckType.FULL_HEADER, + int manualBlockLevel = 9 + ) + { + this._inputStream = inputStream; + this._bitInputStream = new BZip2BitInputStream(inputStream); + this._isOwner = isOwner; + + // initialize stream immediately + this.InitializeStream(inputStreamHeaderCheck, manualBlockLevel); + // prepare first block + this.InitializeNextBlock(); + } + + #region Implementation of abstract members of Stream + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public override void Flush() => + throw new NotSupportedException( + $"{nameof(BZip2InputStream)} does not support 'Flush()' method." + ); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException( + $"{nameof(BZip2InputStream)} does not support 'Seek(long offset, SeekOrigin origin)' method." + ); + + public override void SetLength(long value) => + throw new NotSupportedException( + $"{nameof(BZip2InputStream)} does not support 'SetLength(long value)' method." + ); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException( + $"{nameof(BZip2InputStream)} does not support 'Write(byte[] buffer, int offset, int count)' method." + ); + + public override bool CanRead => this._inputStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => this._inputStream.Length; + public override long Position + { + get => this._inputStream.Position; + set => + throw new NotSupportedException( + $"{nameof(BZip2InputStream)} does not support Set operation for property 'Position'." + ); + } + + public override int ReadByte() + { + var nextByte = this._blockDecompressor?.Read() ?? -1; + + // if current block has reached its end, prepare next block and try reading again + if (nextByte == -1) + { + if (this.InitializeNextBlock()) + { + nextByte = this._blockDecompressor?.Read() ?? -1; + } + } + + return nextByte; + } + + public override int Read(byte[] destination, int offset, int length) + { + int bytesRead = this._blockDecompressor?.Read(destination, offset, length) ?? 0; + + // if current block has reached its end, prepare next block and try reading again + if (bytesRead == -1) + { + if (this.InitializeNextBlock()) + { + bytesRead = this._blockDecompressor?.Read(destination, offset, length) ?? 0; + } + } + + if (bytesRead == -1) + bytesRead = 0; + + return bytesRead; + } + + // overriding Dispose instead of Close as recommended in https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.close?view=net-6.0 + protected override void Dispose(bool disposing) + { + this._streamComplete = true; + this._blockDecompressor = null; + + this._bitInputStream.Dispose(); + + if (this._isOwner) + { + //this._inputStream.Close(); + this._inputStream.Dispose(); + } + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #endregion + + /// Reads the stream header and checks that the data appears to be a valid BZip2 stream + /// if the stream header is not valid + private void InitializeStream( + InputStreamHeaderCheckType inputStreamHeaderCheck, + int blockLevel + ) + { + /* If the stream has been explicitly closed, throw an exception */ + if (this._bitInputStream is null) + throw new IOException("Stream closed"); + + // If we're already at the end of the stream, do nothing + if (this._streamComplete) + return; + + // Read the stream header + try + { + switch (inputStreamHeaderCheck) + { + case InputStreamHeaderCheckType.FULL_HEADER: + { + uint marker1 = this._bitInputStream.ReadBits(16); + uint marker2 = this._bitInputStream.ReadBits(8); + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if ( + marker1 != BZip2Constants.STREAM_START_MARKER_1 + || marker2 != BZip2Constants.STREAM_START_MARKER_2 + || blockLevel < 1 + || blockLevel > 9 + ) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + case InputStreamHeaderCheckType.NO_BZ: + { + uint marker2 = this._bitInputStream.ReadBits(8); + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if ( + marker2 != BZip2Constants.STREAM_START_MARKER_2 + || blockLevel < 1 + || blockLevel > 9 + ) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + case InputStreamHeaderCheckType.NO_BZH: + { + blockLevel = ((int)this._bitInputStream.ReadBits(8) - '0'); + if (blockLevel < 1 || blockLevel > 9) + { + throw new IOException("Invalid BZip2 header"); + } + break; + } + } + + this._streamBlockSize = (uint)(blockLevel * 100000); + } + catch (IOException) + { + // If the stream header was not valid, stop trying to read more data + this._streamComplete = true; + throw; + } + } + + /// Prepares a new block for decompression if any remain in the stream + /// If a previous block has completed, its CRC is checked and merged into the stream CRC. + /// If the previous block was the final block in the stream, the stream CRC is validated + /// true if a block was successfully initialised, or false if the end of file marker was encountered + /// If either the block or stream CRC check failed, if the following data is + /// not a valid block-header or end-of-file marker, or if the following block could not be decoded + private bool InitializeNextBlock() + { + // If we're already at the end of the stream, do nothing + if (this._streamComplete) + return false; + + // If a block is complete, check the block CRC and integrate it into the stream CRC + if (this._blockDecompressor != null) + { + uint blockCrc = this._blockDecompressor.CheckCrc(); + this._streamCrc = ((this._streamCrc << 1) | (this._streamCrc >> 31)) ^ blockCrc; + } + + // Read block-header or end-of-stream marker + uint marker1 = this._bitInputStream.ReadBits(24); + uint marker2 = this._bitInputStream.ReadBits(24); + + if ( + marker1 == BZip2Constants.BLOCK_HEADER_MARKER_1 + && marker2 == BZip2Constants.BLOCK_HEADER_MARKER_2 + ) + { + // Initialise a new block + try + { + this._blockDecompressor = new BZip2BlockDecompressor( + this._bitInputStream, + this._streamBlockSize + ); + } + catch (IOException) + { + // If the block could not be decoded, stop trying to read more data + this._streamComplete = true; + throw; + } + return true; + } + if ( + marker1 == BZip2Constants.STREAM_END_MARKER_1 + && marker2 == BZip2Constants.STREAM_END_MARKER_2 + ) + { + // Read and verify the end-of-stream CRC + this._streamComplete = true; + uint storedCombinedCrc = this._bitInputStream.ReadInteger(); // .ReadBits(32); + + if (storedCombinedCrc != this._streamCrc) + throw new IOException("BZip2 stream CRC error"); + + return false; + } + + // If what was read is not a valid block-header or end-of-stream marker, the stream is broken + this._streamComplete = true; + throw new IOException("BZip2 stream format error"); + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputDataBlock.cs b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputDataBlock.cs new file mode 100644 index 000000000..27742b331 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputDataBlock.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Algorithm; + +namespace SharpCompress.Compressors.BZip2MT.InputStream +{ + internal class BZip2ParallelInputDataBlock : IDisposable + { + private int _blockId = 0; + private MemoryStream? _inputBlockBuffer; + private MemoryStream? _outputBuffer; + private int _outputBufferSize; + private int _blockSizeBytes; + private bool _isCrcOk = false; + private uint _crcValue; + + public long Length => this._outputBuffer?.Length ?? 0; + public long Position => this._outputBuffer?.Position ?? 0; + public bool IsDone => this.Position >= this.Length; + public int BlockId => this._blockId; + public bool IsCrcOk => this._isCrcOk; + public uint CrcValue => this._crcValue; + + public BZip2ParallelInputDataBlock( + int blockId, + MemoryStream inputBlockBuffer, + int blockSizeBytes, + int outputBufferSize + ) + { + this._blockId = blockId; + this._inputBlockBuffer = inputBlockBuffer; + this._blockSizeBytes = blockSizeBytes; + this._outputBufferSize = outputBufferSize; + } + + public void Dispose() + { + this._inputBlockBuffer?.Dispose(); + this._outputBuffer?.Dispose(); + } + + /// + /// Decompresses the full block + /// + public void Decompress() + { + if (this._inputBlockBuffer is null) + throw new IOException("Attempted to Decompress the same block twice"); + + this._outputBuffer = new MemoryStream(this._outputBufferSize); + + // note: we need to skip the first 6 magic bytes... + this._inputBlockBuffer.Position = 6; + using BZip2BitInputStream inputStream = new BZip2BitInputStream(this._inputBlockBuffer); + BZip2BlockDecompressor blockDecompressor = new BZip2BlockDecompressor( + inputStream, + (uint)this._blockSizeBytes + ); + + int readCount = blockDecompressor.ReadAll(this._outputBuffer); + + // reset buffer position + this._outputBuffer.Position = 0; + + try + { + this._crcValue = blockDecompressor.CheckCrc(); + this._isCrcOk = true; + } + catch (Exception) + { + this._crcValue = 0; + this._isCrcOk = false; + } + + this._inputBlockBuffer.Dispose(); + this._inputBlockBuffer = null; + } + + /// + /// reads a single byte from the output buffer + /// + /// + public int Read() + { + if (this._outputBuffer is null) + throw new IOException( + "Attempted to read decompressed data before decompressing block" + ); + + if (this._isCrcOk == false) + return -1; + if (this._outputBuffer.Position < this._outputBuffer.Length) + return this._outputBuffer.ReadByte(); + return -1; + } + + /// + /// Reads a number of bytes from the output buffer into the destination buffer + /// + /// destination buffer + /// starting position within destination buffer + /// maximum number of bytes to read + /// + public int Read(byte[] destination, int offset, int length) + { + if (this._outputBuffer is null) + throw new IOException( + "Attempted to read decompressed data before decompressing block" + ); + + if (this._isCrcOk == false) + return 0; + + return this._outputBuffer.Read(destination, offset, length); + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputStream.cs b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputStream.cs new file mode 100644 index 000000000..d2fdd10a0 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/InputStream/BZip2ParallelInputStream.cs @@ -0,0 +1,398 @@ +// Added by drone1400, July 2025 +// Location: https://github.com/drone1400/bzip2 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.InputStream +{ + /// An InputStream wrapper that decompresses BZip2 data using multiple threads + /// Instances of this class are not threadsafe + public class BZip2ParallelInputStream : Stream + { + #region Private fields + + // block size fields + private readonly int _blockLevel; + private readonly int _blockSizeBytes; + + private int _mtNextInputBlockId = 0; + private int _mtNextOutputBlockId = 0; + + // yikes! sounds bad right? + // if one of the worker threads, this is set to true and any further attempt to + // write/flush/close the stream will throw an exception + private bool _unsafeFatalException = false; + private Exception? _lastWorkerException = null; + + // dictionary of processed blocks + private readonly Dictionary _mtDecodedBlocks = + new Dictionary(); + + private int _mtPendingBlocks = 0; + private readonly int _mtMaxPendingBlocks; + + private readonly int _mtWorkerBufferSize; + + private readonly object _syncRootProcesing = new object(); + + // The stream from which compressed BZip2 data is read and decoded + private Stream _inputStream; + + // True if the underlying stream will be closed with the current Stream + private readonly bool _isOwner; + + // An InputStream wrapper that provides bit-level reads + private BZip2BitInputStreamSplitter _inputStreamSplitter; + + // The merged CRC of all blocks decompressed so far + private uint _streamCrc; + + #endregion + + #region Public methods + + /// Public constructor + /// The InputStream to wrap + /// True if the underlying stream will be closed with the current Stream + /// + /// Used when is NoHeader + /// Maximum number of blocks to read/decompress at the same time + /// Minimum buffer size for each worker thread during decompression, in bytes. + public BZip2ParallelInputStream( + Stream inputStream, + bool isOwner = true, + InputStreamHeaderCheckType inputStreamHeaderCheck = + InputStreamHeaderCheckType.FULL_HEADER, + int manualBlockLevel = 9, + int maxPendingBlocks = 0, + int workerBufferSize = 12582912 + ) + { + if (maxPendingBlocks == 0) + { + maxPendingBlocks = Environment.ProcessorCount; + } + this._mtMaxPendingBlocks = maxPendingBlocks; + this._mtWorkerBufferSize = workerBufferSize; + + // initialize stream + this._inputStream = inputStream; + this._isOwner = isOwner; + this._inputStreamSplitter = new BZip2BitInputStreamSplitter( + inputStream, + inputStreamHeaderCheck, + manualBlockLevel + ); + + this._blockLevel = this._inputStreamSplitter.BlockLevel; + this._blockSizeBytes = this._inputStreamSplitter.BlockSizeBytes; + } + + #endregion + + /// + /// Decompress a block of data + /// + /// + /// if compressing the block somehow fails... + private void MultiThreadWorkerAction(object? blockData) + { + try + { + if (blockData is BZip2ParallelInputDataBlock dataBlock) + { + // do the work + dataBlock.Decompress(); + + // queue up the finished decompressed block data + lock (this._syncRootProcesing) + { + this._mtDecodedBlocks.Add(dataBlock.BlockId, dataBlock); + } + } + } + catch (Exception ex) + { + // set this without any locks... + this._lastWorkerException = ex; + this._unsafeFatalException = true; + + throw new IOException( + "BZip2 Processing thread somehow crashed... See inner exception for details!", + ex + ); + } + finally + { + lock (this._syncRootProcesing) + { + this._mtPendingBlocks--; + } + } + } + + /// + /// If there are not too many pending blocks being processed, reads another block from the input stream and adds it to the work queue. + /// + /// True if the data block was queued up, false otherwise + /// if one of the worker threads or writing to the final bit stream experienced an exception + /// Should only be called from within or + private bool TryToQueueUpAnotherBlock() + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the decompression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + if (this._inputStreamSplitter.IsStreamComplete) + { + return false; + } + + lock (this._syncRootProcesing) + { + if (this._mtPendingBlocks >= this._mtMaxPendingBlocks) + return false; + } + + // extract the next block... + MemoryStream? ms = this._inputStreamSplitter.CopyNextBlock(); + + // this should be impossible if this._inputStreamSplitter.IsStreamComplete is not true + if (ms is null) + return false; + + lock (this._syncRootProcesing) + { + this._mtPendingBlocks++; + BZip2ParallelInputDataBlock data = new BZip2ParallelInputDataBlock( + this._mtNextInputBlockId++, + ms, + this._blockSizeBytes, + this._mtWorkerBufferSize + ); + ThreadPool.QueueUserWorkItem(this.MultiThreadWorkerAction, data); + } + + return true; + } + + /// + /// Checks if we are done processing the stream. This is done by checing if there are any pending blocks to be decoded or read. + /// Also by checking if the input stream splitter helper has reached the end of stream markers. + /// + /// True if we are done, false otherwise + /// If there's a CRC error at end of the stream. + /// /// Should only be called from within or + private bool CheckIfCompletelyDone() + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the decompression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + lock (this._syncRootProcesing) + { + if ( + this._mtPendingBlocks == 0 + && this._mtDecodedBlocks.Count == 0 + && this._inputStreamSplitter.IsStreamComplete + ) + { + if (this._inputStreamSplitter.FinalCrc != this._streamCrc) + { + throw new IOException("BZip2 stream CRC error"); + } + // we are compeltely done, can no longer do anything! + return true; + } + } + + return false; + } + + #region Implementation of abstract members of Stream + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public override void Flush() => + throw new NotSupportedException( + $"{nameof(BZip2ParallelInputStream)} does not support 'Flush()' method." + ); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelInputStream)} does not support 'Seek(long offset, SeekOrigin origin)' method." + ); + + public override void SetLength(long value) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelInputStream)} does not support 'SetLength(long value)' method." + ); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelInputStream)} does not support 'Write(byte[] buffer, int offset, int count)' method." + ); + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => this._inputStream.Length; + public override long Position + { + get => this._inputStream.Position; + set => + throw new NotSupportedException( + $"{nameof(BZip2ParallelInputStream)} does not support Set operation for property 'Position'." + ); + } + + public override int ReadByte() + { + while (true) + { + // check if the next output block is done + BZip2ParallelInputDataBlock? data = null; + lock (this._syncRootProcesing) + { + if (this._mtDecodedBlocks.ContainsKey(this._mtNextOutputBlockId)) + { + data = this._mtDecodedBlocks[this._mtNextOutputBlockId]; + } + } + + if (data != null) + { + if (data.IsCrcOk == false) + { + this._unsafeFatalException = true; + throw new IOException("BZip2 block decompression CRC error..."); + } + + if (data.IsDone == false) + { + // can read next byte + return data.Read(); + } + + // decompressor is done! + lock (this._syncRootProcesing) + { + uint blockCrc = data.CrcValue; + this._streamCrc = + ((this._streamCrc << 1) | (this._streamCrc >> 31)) ^ blockCrc; + + this._mtDecodedBlocks.Remove(this._mtNextOutputBlockId); + this._mtNextOutputBlockId++; + + // continue to processing next block that is already decoded, skip queueing up more blocks... + continue; + } + } + + // queue up the next block + bool queueSuccessful = this.TryToQueueUpAnotherBlock(); + + // check if we are completely done + if (this.CheckIfCompletelyDone()) + return -1; + + if (!queueSuccessful) + { + Thread.Sleep(1); + } + } + } + + public override int Read(byte[] destination, int offset, int length) + { + int totalRead = 0; + int remaining = length; + + while (true) + { + // check if the next output block is done + BZip2ParallelInputDataBlock? data = null; + lock (this._syncRootProcesing) + { + if (this._mtDecodedBlocks.ContainsKey(this._mtNextOutputBlockId)) + { + data = this._mtDecodedBlocks[this._mtNextOutputBlockId]; + } + } + + if (data != null) + { + if (data.IsCrcOk == false) + { + this._unsafeFatalException = true; + throw new IOException("BZip2 block decompression CRC error..."); + } + + int readCount = data.Read(destination, offset, remaining); + remaining -= readCount; + offset += readCount; + totalRead += readCount; + + if (totalRead >= length) + return totalRead; + + if (data.IsDone) + { + // decompressor is done! + lock (this._syncRootProcesing) + { + uint blockCrc = data.CrcValue; + this._streamCrc = + ((this._streamCrc << 1) | (this._streamCrc >> 31)) ^ blockCrc; + + this._mtDecodedBlocks.Remove(this._mtNextOutputBlockId); + this._mtNextOutputBlockId++; + + // continue to processing next block that is already decoded, skip queueing up more blocks... + continue; + } + } + } + + // queue up the next block + bool queueSuccessful = this.TryToQueueUpAnotherBlock(); + + // check if we are completely done + if (this.CheckIfCompletelyDone()) + return totalRead; + + if (!queueSuccessful) + { + Thread.Sleep(1); + } + } + } + + // overriding Dispose instead of Close as recommended in https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.close?view=net-6.0 + protected override void Dispose(bool disposing) + { + this._mtDecodedBlocks.Clear(); + + if (this._isOwner) + { + //this._inputStream.Close(); + this._inputStream.Dispose(); + } + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #endregion + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Interface/BZip2Constants.cs b/src/SharpCompress/Compressors/BZip2MT/Interface/BZip2Constants.cs new file mode 100644 index 000000000..3a6d24067 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Interface/BZip2Constants.cs @@ -0,0 +1,45 @@ +// Added by drone1400, December 2025 +// Location: https://github.com/drone1400/bzip2 + +namespace SharpCompress.Compressors.BZip2MT.Interface +{ + internal static class BZip2Constants + { + public const uint STREAM_START_MARKER = 0x425A68; + public const ulong STREAM_END_MARKER = 0x177245385090; + public const ulong BLOCK_HEADER_MARKER = 0x314159265359; + + /// The first 2 bytes of a Bzip2 marker, BZ + public const uint STREAM_START_MARKER_1 = 0x425a; + + /// The 'h' that distinguishes BZip from BZip2 + public const uint STREAM_START_MARKER_2 = 0x68; + + /// First three bytes of the end of stream marker + public const uint STREAM_END_MARKER_1 = 0x177245; + + /// Last three bytes of the end of stream marker + public const uint STREAM_END_MARKER_2 = 0x385090; + + // First three bytes of the block header marker + public const uint BLOCK_HEADER_MARKER_1 = 0x314159; + + // Last three bytes of the block header marker + public const uint BLOCK_HEADER_MARKER_2 = 0x265359; + } + + public enum InputStreamHeaderCheckType + { + // check for full header (ex: BZh9 ) + FULL_HEADER, + + // skips BZ part of header + NO_BZ, + + // skips BZh part of header + NO_BZH, + + // skips BZh and block level, aka the whole header + NO_HEADER, + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitInputStream.cs b/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitInputStream.cs new file mode 100644 index 000000000..1da856cbe --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitInputStream.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace SharpCompress.Compressors.BZip2MT.Interface +{ + internal interface IBZip2BitInputStream : IDisposable + { + /// Reads a single bit from the wrapped input stream + /// true if the bit read was 1, otherwise false + /// if no more bits are available in the input stream + public bool ReadBoolean(); + + /// Reads a zero-terminated unary number from the wrapped input stream + /// The unary number + /// if no more bits are available in the input stream + public uint ReadUnary(); + + /// Reads up to 32 bits from the wrapped input stream + /// The number of bits to read (maximum 32) + /// The bits requested, right-aligned within the integer + /// if no more bits are available in the input stream + public uint ReadBits(int count); + + /// Reads 32 bits of input as an integer + /// The integer read + /// if 32 bits are not available in the input stream + public uint ReadInteger(); + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitOutputStream.cs b/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitOutputStream.cs new file mode 100644 index 000000000..6aa5651da --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/Interface/IBZip2BitOutputStream.cs @@ -0,0 +1,48 @@ +// Added by drone1400, July 2022 +// Location: https://github.com/drone1400/bzip2 + +using System; + +namespace SharpCompress.Compressors.BZip2MT.Interface +{ + /// + /// Interface for a stream wrapper that implements bit-wise write operations + /// + internal interface IBZip2BitOutputStream : IDisposable + { + /// + /// Writes a single bit to the wrapped output stream + /// + /// The bit to write + /// if an error occurs writing to the stream + public void WriteBoolean(bool value); + + /// + /// Writes a zero-terminated unary number to the wrapped output stream + /// + /// The number to write (must be non-negative) + /// if an error occurs writing to the stream + public void WriteUnary(int value); + + /// + /// Writes up to 24 bits to the wrapped output stream + /// + /// The number of bits to write (maximum 24) + /// The bits to write + /// if an error occurs writing to the stream + public void WriteBits(int count, uint value); + + /// + /// Writes an integer as 32 bits of output + /// + /// The integer to write + /// if an error occurs writing to the stream + public void WriteInteger(uint value); + + /// + /// Writes any remaining bits to the output stream, zero padding to a whole byte as required + /// + /// if an error occurs writing to the stream + public void Flush(); + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2BitOutputStream.cs b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2BitOutputStream.cs new file mode 100644 index 000000000..3e6fccb3f --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2BitOutputStream.cs @@ -0,0 +1,93 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System.IO; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.OutputStream +{ + /// Implements a bit-wise output stream + /// + /// Allows the writing of single bit booleans, unary numbers, bit + /// strings of arbitrary length(up to 24 bits), and bit aligned 32-bit integers.A single byte at a + /// time is written to the wrapped stream when sufficient bits have been accumulated + /// + internal class BZip2BitOutputStream : IBZip2BitOutputStream + { + // The stream to which bits are written + private readonly Stream _outputStream; + + // A buffer of bits waiting to be written to the output stream + private uint _bitBuffer; + + // The number of bits currently buffered in bitBuffer + private int _bitCount; + + /// + /// Public constructor + /// + /// The OutputStream to wrap + public BZip2BitOutputStream(Stream outputStream) => this._outputStream = outputStream; + + public void Dispose() + { + // do nothing, the BZip2BitOutputStream is not the owner of the _outputStream so don't dispose it + } + + #region IBZip2BitOutputStream implementation + + public void WriteBoolean(bool value) + { + this._bitCount++; + this._bitBuffer |= ((value ? 1u : 0u) << (32 - this._bitCount)); + + if (this._bitCount == 8) + { + this._outputStream.WriteByte((byte)(this._bitBuffer >> 24)); + this._bitBuffer = 0; + this._bitCount = 0; + } + } + + public void WriteUnary(int value) + { + while (value-- > 0) + { + this.WriteBoolean(true); + } + this.WriteBoolean(false); + } + + public void WriteBits(int count, uint value) + { + this._bitBuffer |= ((value << (32 - count)) >> this._bitCount); + this._bitCount += count; + + while (this._bitCount >= 8) + { + this._outputStream.WriteByte((byte)(this._bitBuffer >> 24)); + this._bitBuffer <<= 8; + this._bitCount -= 8; + } + } + + public void WriteInteger(uint value) + { + this.WriteBits(16, (value >> 16) & 0xffff); + this.WriteBits(16, value & 0xffff); + } + + public void Flush() + { + if (this._bitCount > 0) + { + this.WriteBits(8 - this._bitCount, 0); + } + } + + #endregion + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2OutputStream.cs new file mode 100644 index 000000000..4288ce2a6 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2OutputStream.cs @@ -0,0 +1,205 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Algorithm; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.OutputStream +{ + /// An OutputStream wrapper that compresses BZip2 data + /// Instances of this class are not threadsafe + public class BZip2OutputStream : Stream + { + // The stream to which compressed BZip2 data is written + private Stream _outputStream; + + // An OutputStream wrapper that provides bit-level writes + private readonly BZip2BitOutputStream _bitOutputStream; + + // (@code true} if the compressed stream has been finished, otherwise false + private bool _streamFinished; + + // The declared maximum block size of the stream (before final run-length decoding) + private readonly int _streamBlockSize; + + // The merged CRC of all blocks compressed so far + private uint _streamCrc; + + // The compressor for the current block + private BZip2BlockCompressor? _blockCompressor; + + // True if the underlying stream will be closed with the current Stream + private bool _isOwner; + + /// Public constructor + /// The output stream to write to + /// The BZip2 block size as a multiple of 100,000 bytes (minimum 1, maximum 9) + /// True if the underlying stream will be closed with the current Stream + /// On any I/O error writing to the output stream + /// Larger block sizes require more memory for both compression and decompression, + /// but give better compression ratios. 9 will usually be the best value to use + public BZip2OutputStream( + Stream outputStream, + bool isOwner = true, + int blockSizeMultiplier = 9 + ) + { + if (outputStream is null) + throw new ArgumentException("Null output stream"); + + if ((blockSizeMultiplier < 1) || (blockSizeMultiplier > 9)) + throw new ArgumentException("Invalid BZip2 block size" + blockSizeMultiplier); + + this._streamBlockSize = blockSizeMultiplier * 100000; + this._outputStream = outputStream; + this._bitOutputStream = new BZip2BitOutputStream(this._outputStream); + this._isOwner = isOwner; + + this._bitOutputStream.WriteBits(16, BZip2Constants.STREAM_START_MARKER_1); + this._bitOutputStream.WriteBits(8, BZip2Constants.STREAM_START_MARKER_2); + this._bitOutputStream.WriteBits(8, (uint)('0' + blockSizeMultiplier)); + + this.InitialiseNextBlock(); + } + + #region Implementation of abstract members of Stream + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public override void Flush() => + throw new NotSupportedException( + $"{nameof(BZip2OutputStream)} does not support 'Flush()' method." + ); + + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException( + $"{nameof(BZip2OutputStream)} does not support 'Read(byte[] buffer, int offset, int count)' method." + ); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException( + $"{nameof(BZip2OutputStream)} does not support 'Seek(long offset, SeekOrigin origin)' method." + ); + + public override void SetLength(long value) => + throw new NotSupportedException( + $"{nameof(BZip2OutputStream)} does not support 'SetLength(long value)' method." + ); + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => this._outputStream.CanWrite; + + public override long Length => this._outputStream.Length; + + public override long Position + { + get => this._outputStream.Position; + set => + throw new NotSupportedException( + $"{nameof(BZip2OutputStream)} does not support Set operation for property 'Position'." + ); + } + + public override void WriteByte(byte value) + { + if (this._outputStream is null) + throw new IOException("Stream closed"); + + if (this._blockCompressor is null || this._streamFinished) + throw new IOException("Write beyond end of stream"); + + if (!this._blockCompressor.Write(value & 0xff)) + { + this.CloseBlock(); + this.InitialiseNextBlock(); + this._blockCompressor.Write(value & 0xff); + } + } + + public override void Write(byte[] data, int offset, int length) + { + if (this._outputStream is null) + throw new IOException("Stream closed"); + + if (this._blockCompressor is null || this._streamFinished) + throw new IOException("Write beyond end of stream"); + + while (length > 0) + { + int bytesWritten; + if ((bytesWritten = this._blockCompressor.Write(data, offset, length)) < length) + { + this.CloseBlock(); + this.InitialiseNextBlock(); + } + offset += bytesWritten; + length -= bytesWritten; + } + } + + // overriding Dispose instead of Close as recommended in https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.close?view=net-6.0 + protected override void Dispose(bool disposing) + { + this.Finish(); + if (this._isOwner) + { + this._outputStream.Dispose(); + } + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #endregion + + /// Initialises a new block for compression + private void InitialiseNextBlock() => + this._blockCompressor = new BZip2BlockCompressor( + this._bitOutputStream, + this._streamBlockSize + ); + + /// Compress and write out the block currently in progress + /// If no bytes have been written to the block, it is discarded + /// On any I/O error writing to the output stream + private void CloseBlock() + { + if (this._blockCompressor is null || this._blockCompressor.IsEmpty) + return; + + this._blockCompressor.CloseBlock(); + this._streamCrc = + ((this._streamCrc << 1) | (this._streamCrc >> 31)) ^ this._blockCompressor.CRC; + } + + /// Compresses and writes out any as yet unwritten data, then writes the end of the BZip2 stream + /// The underlying OutputStream is not closed + /// On any I/O error writing to the output stream + private void Finish() + { + if (!this._streamFinished) + { + this._streamFinished = true; + try + { + this.CloseBlock(); + this._bitOutputStream.WriteBits(24, BZip2Constants.STREAM_END_MARKER_1); + this._bitOutputStream.WriteBits(24, BZip2Constants.STREAM_END_MARKER_2); + this._bitOutputStream.WriteInteger(this._streamCrc); + this._bitOutputStream.Flush(); + this._outputStream.Flush(); + } + finally + { + this._blockCompressor = null; + } + } + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputDataBlock.cs b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputDataBlock.cs new file mode 100644 index 000000000..58de1843d --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputDataBlock.cs @@ -0,0 +1,176 @@ +// Added by drone1400, July 2022 +// Location: https://github.com/drone1400/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Algorithm; +using SharpCompress.Compressors.BZip2MT.Interface; + +namespace SharpCompress.Compressors.BZip2MT.OutputStream +{ + /// A collection of bit output data + /// + /// Allows the writing of single bit booleans, unary numbers, bit + /// strings of arbitrary length(up to 24 bits), and bit aligned 32-bit integers.A single byte at a + /// time is written to a list of structures that serves as a buffer for use in parallelized + /// execution of block compression + /// + internal class BZip2ParallelOutputDataBlock : IBZip2BitOutputStream + { + private BZip2BitOutputStream _internalBitStream; + private MemoryStream _buffer; + private long _bitCount = 0; + + /// + /// Compressed block CRC to be stored here when block is finished + /// + public uint BlockCrc => this._blockCrc; + private uint _blockCrc = 0; + + /// + /// Indicates that the compression block is full + /// + public bool IsFull => this._isFull; + private bool _isFull = false; + + /// + /// Block numeric id for distinguishing blocks + /// + public int BlockId => this._blockId; + private int _blockId; + + /// + /// Number of bytes loaded into the block compressor + /// + public int LoadedBytes => this._loadedBytes; + private int _loadedBytes = 0; + private readonly BZip2BlockCompressor _compressor; + + /// + /// Public constructor + /// + /// block size in bytes, also initial internal buffer list capacity + /// Block number id, used to distinguish blocks in multithreadding + public BZip2ParallelOutputDataBlock(int blockSizeBytes, int blockId) + { + this._buffer = new MemoryStream(blockSizeBytes + 100000); + this._internalBitStream = new BZip2BitOutputStream(this._buffer); + this._blockId = blockId; + this._compressor = new BZip2BlockCompressor(this, blockSizeBytes); + } + + public void Dispose() + { + this._internalBitStream.Dispose(); + this._buffer.Dispose(); + } + + /// + /// Loads a byte into the 's first RLE stage + /// + /// Byte + /// True if byte was loaded, false if byte could not be loaded because block compressor is full + public bool LoadByte(byte value) + { + if (this._compressor.Write(value)) + { + this._loadedBytes++; + return true; + } + + // could not load the byte, means block is full + this._isFull = true; + + return false; + } + + /// + /// Loads bytes from a buffer into the 's first RLE stage + /// + /// Byte buffer + /// Byte buffer offset + /// Number of bytes to load + /// Number of bytes actually loaded + public int LoadBytes(byte[] buff, int offset, int length) + { + int count = this._compressor.Write(buff, offset, length); + this._loadedBytes += count; + if (count < length) + { + // could not load all the bytes, means block is full + this._isFull = true; + } + return count; + } + + /// + /// Starts the actual compression + /// + public void CompressBytes() + { + this._compressor.CloseBlock(); + this._blockCrc = this._compressor.CRC; + } + + /// + /// Writes all the buffer data to the real + /// + /// The real bit output stream + /// if an error occurs writing to the stream + public void WriteToRealOutputStream(BZip2BitOutputStream stream) + { + this._internalBitStream.Flush(); + + this._buffer.Position = 0; + while (this._bitCount >= 8) + { + int b = this._buffer.ReadByte(); + stream.WriteBits(8, (uint)b); + this._bitCount -= 8; + } + + if (this._bitCount > 0) + { + int b = this._buffer.ReadByte(); + b = (b >> (8 - (int)this._bitCount)); + stream.WriteBits((int)this._bitCount, (uint)b); + this._bitCount = 0; + } + } + + #region IBZip2BitOutputStream implementation + + public void WriteBoolean(bool value) + { + this._bitCount++; + this._internalBitStream.WriteBoolean(value); + } + + public void WriteUnary(int value) + { + while (value-- > 0) + { + this._bitCount++; + this._internalBitStream.WriteBoolean(true); + } + this._bitCount++; + this._internalBitStream.WriteBoolean(false); + } + + public void WriteBits(int count, uint value) + { + this._bitCount += count; + this._internalBitStream.WriteBits(count, value); + } + + public void WriteInteger(uint value) + { + this._bitCount += 32; + this._internalBitStream.WriteInteger(value); + } + + public void Flush() => this._internalBitStream.Flush(); + + #endregion + } +} diff --git a/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputStream.cs b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputStream.cs new file mode 100644 index 000000000..9e370b800 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2MT/OutputStream/BZip2ParallelOutputStream.cs @@ -0,0 +1,425 @@ +// Added by drone1400, July 2022 +// Location: https://github.com/drone1400/bzip2 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace SharpCompress.Compressors.BZip2MT.OutputStream +{ + /// An OutputStream wrapper that compresses BZip2 data using multiple threads + /// Instances of this class are not threadsafe + public class BZip2ParallelOutputStream : Stream + { + // block size fields + private readonly int _compressBlockSize; + private readonly int _blockLevel; + + private int _mtNextInputBlockId = 0; + private int _mtNextOutputBlockId = 0; + + // flag indicating input stream is finished + private bool _mtStreamIsFinished = false; + + // yikes! sounds bad right? + // if one of the worker threads, this is set to true and any furhter attempt to + // write/flush/close the stream will throw an exception + private bool _unsafeFatalException = false; + private Exception? _lastWorkerException = null; + + // dictionary of processed blocks + private readonly Dictionary _mtProcessedBlocks = + new Dictionary(); + + private int _mtPendingBlocks = 0; + + private readonly object _syncRootProcesing = new object(); + private readonly object _syncRootOutputStream = new object(); + + // The output stream + private readonly Stream _outputStream; + + // The bit output stream + private readonly BZip2BitOutputStream _bitStream; + + // The merged CRC of all blocks compressed so far + private uint _streamCrc = 0; + + // + private BZip2ParallelOutputDataBlock _currentBlockBuffer; + + // True if the underlying stream will be closed with the current Stream + private readonly bool _isOwner; + + // for debug purposes... + // private int _debugMaxMetaBufferDataPairCount = 0; + + /// + /// Public constructor + /// + /// The output stream in which to write the compressed data + /// True if the underlying stream will be closed with the current Stream + /// The BZip2 block size as a multiple of 100,000 bytes (minimum 1, maximum 9) + /// For best performance, compressor thread number should be equal to CPU core count, but results may vary + public BZip2ParallelOutputStream(Stream output, bool isOwner = true, int blockLevel = 9) + { + this._outputStream = output; + this._bitStream = new BZip2BitOutputStream(output); + + if (blockLevel < 1) + blockLevel = 1; + if (blockLevel > 9) + blockLevel = 9; + this._blockLevel = blockLevel; + + // evaluate thread count + // if (compressorThreads < 1) compressorThreads = 1; + // if (compressorThreads > ABSOLUTE_MAX_THREADS) compressorThreads = ABSOLUTE_MAX_THREADS; + // this._mtCompressorThreads = compressorThreads; + + // supposedly a block can only expand 1.25x, so 0.8 of normal block size should always be safe... + this._compressBlockSize = 100000 * this._blockLevel; + + // initialize initial meta buffer + this._currentBlockBuffer = new BZip2ParallelOutputDataBlock( + this._compressBlockSize, + this._mtNextInputBlockId++ + ); + + this._isOwner = isOwner; + + // write the bz2 header... + this.WriteBz2Header(); + } + + /// + /// Writes the next pending Output data block to the output bit stream + /// + /// True if a block was written, false if didn't find any pending blocks + /// + private bool TryWriteOutputBlockAndIncrementId() + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the compression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + try + { + BZip2ParallelOutputDataBlock? currentOutput = null; + + lock (this._syncRootProcesing) + { + // check if the next output block can be extracted + if (this._mtProcessedBlocks.ContainsKey(this._mtNextOutputBlockId)) + { + currentOutput = this._mtProcessedBlocks[this._mtNextOutputBlockId]; + this._mtProcessedBlocks.Remove(this._mtNextOutputBlockId); + } + + // check if we got anything to write + if (currentOutput is null) + return false; + + this._mtNextOutputBlockId++; + } + + lock (this._syncRootOutputStream) + { + // update file CRC + this._streamCrc = + ((this._streamCrc << 1) | (this._streamCrc >> 31)) ^ currentOutput.BlockCrc; + currentOutput.WriteToRealOutputStream(this._bitStream); + } + return true; + } + catch (Exception ex) + { + // set this without any locks... + this._lastWorkerException = ex; + this._unsafeFatalException = true; + + // rethrow exception, hopefully something catches it?... + throw new IOException( + "BZip2 error writing output data! See inner exception for details!", + ex + ); + } + } + + /// + /// Compresses a block of data + /// + /// + /// if compressing the block somehow fails... + private void MultiThreadWorkerAction(object? blockData) + { + try + { + if (blockData is BZip2ParallelOutputDataBlock buffer) + { + // do the work + buffer.CompressBytes(); + + // queue up the finished compressed block data + lock (this._syncRootProcesing) + { + this._mtProcessedBlocks.Add(buffer.BlockId, buffer); + } + } + } + catch (Exception ex) + { + // set this without any locks... + this._lastWorkerException = ex; + this._unsafeFatalException = true; + + throw new IOException( + "BZip2 Processing thread somehow crashed... See inner exception for details!", + ex + ); + } + finally + { + lock (this._syncRootProcesing) + { + this._mtPendingBlocks--; + } + } + } + + /// + /// Writes the BZ2 header to the bit stream + /// + private void WriteBz2Header() + { + lock (this._syncRootOutputStream) + { + // write BZIP file header + this._bitStream.WriteBits(8, 0x42); // B + this._bitStream.WriteBits(8, 0x5A); // Z + this._bitStream.WriteBits(8, 0x68); // h + this._bitStream.WriteBits(8, (uint)(0x30 + this._blockLevel)); // block level digit + } + } + + /// + /// Writes the BZ2 footer tot he bit stream and flushes the stream + /// + private void WriteBz2FooterAndFlush() + { + lock (this._syncRootOutputStream) + { + // end magic + this._bitStream.WriteBits(8, 0x17); + this._bitStream.WriteBits(8, 0x72); + this._bitStream.WriteBits(8, 0x45); + this._bitStream.WriteBits(8, 0x38); + this._bitStream.WriteBits(8, 0x50); + this._bitStream.WriteBits(8, 0x90); + + // write combined CRC + this._bitStream.WriteBits(16, (this._streamCrc >> 16) & 0xFFFF); + this._bitStream.WriteBits(16, this._streamCrc & 0xFFFF); + + // flush all remaining bits + this._bitStream.Flush(); + this._outputStream.Flush(); + } + } + + /// + /// Queues up the current data block if it is not empty + /// + /// True if the data block was queued up, false if the current block is empty + /// if one of the worker threads or writing to the final bit stream experienced an exception + private bool TryEnqueueCurrentBlockBuffer() + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the compression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + // make sure current block has data + if (this._currentBlockBuffer.LoadedBytes <= 0) + return false; + + lock (this._syncRootProcesing) + { + this._mtPendingBlocks++; + ThreadPool.QueueUserWorkItem( + this.MultiThreadWorkerAction, + this._currentBlockBuffer + ); + this._currentBlockBuffer = new BZip2ParallelOutputDataBlock( + this._compressBlockSize, + this._mtNextInputBlockId++ + ); + } + + return true; + } + + /// + /// Waits for any pending data blocks to be written to the bit stream and writes the BZ2 footer + /// + /// If there was an exception in a worker thread or when writing to the final bitstream + private void FinishBitstream() + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the compression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + lock (this._syncRootProcesing) + { + if (this._mtStreamIsFinished) + return; + this._mtStreamIsFinished = true; + } + + // check if there is still data left to write + this.TryEnqueueCurrentBlockBuffer(); + + // decrement next input block since no longer getting another block + this._mtNextInputBlockId--; + + while (true) + { + if (this._unsafeFatalException) + { + throw new IOException( + "One of the compression threads somehow failed... This should never happen.", + this._lastWorkerException + ); + } + + lock (this._syncRootProcesing) + { + if (this._mtNextInputBlockId == this._mtNextOutputBlockId) + { + // all done, can safely exit + break; + } + } + + // try writing output block and keep doing so while successful + while (this.TryWriteOutputBlockAndIncrementId()) { } + Thread.Sleep(1); + } + + // sanity check, this should be impossible and should be caught by some test right?... + lock (this._syncRootProcesing) + { + if (this._mtPendingBlocks != 0 || this._mtProcessedBlocks.Count > 0) + { + throw new IOException("BZip2 dispose operation sanity check failed!..."); + } + } + + // finally, write the footer! + this.WriteBz2FooterAndFlush(); + + //Console.WriteLine($"Max number of data pair entries was {this._debugMaxMetaBufferDataPairCount}"); + } + + #region Implementation of abstract members of Stream + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + // overriding Dispose instead of Close as recommended in https://docs.microsoft.com/en-us/dotnet/api/system.io.stream.close?view=net-6.0 + protected override void Dispose(bool disposing) + { + this.FinishBitstream(); + base.Dispose(disposing); + if (this._isOwner) + { + this._outputStream.Close(); + } + } + + public override void Flush() + { + // try writing output block and keep doing so while successful + while (this.TryWriteOutputBlockAndIncrementId()) { } + } + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelOutputStream)} does not support 'Seek(long offset, SeekOrigin origin)' method." + ); + + public override void SetLength(long value) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelOutputStream)} does not support 'SetLength(long value)' method." + ); + + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException( + $"{nameof(BZip2ParallelOutputStream)} does not support 'Read(byte[] buffer, int offset, int count)' method." + ); + + public override void WriteByte(byte value) + { + if (!this._currentBlockBuffer.LoadByte(value)) + { + // byte could not be loaded, this happens when current block buffer is full + + this.TryEnqueueCurrentBlockBuffer(); + this._currentBlockBuffer.LoadByte(value); + + // try writing output block and keep doing so while successful + while (this.TryWriteOutputBlockAndIncrementId()) { } + } + } + + public override void Write(byte[] data, int offset, int length) + { + while (length > 0) + { + if (!this._currentBlockBuffer.IsFull) + { + int count = this._currentBlockBuffer.LoadBytes(data, offset, length); + offset += count; + length -= count; + } + + if (this._currentBlockBuffer.IsFull) + { + this.TryEnqueueCurrentBlockBuffer(); + } + + // try writing output block and keep doing so while successful + while (this.TryWriteOutputBlockAndIncrementId()) { } + } + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => this._outputStream.CanWrite; + public override long Length => this._outputStream.Length; + + public override long Position + { + get => this._outputStream.Position; + set => + throw new NotSupportedException( + $"{nameof(BZip2ParallelOutputStream)} does not support Set operation for property 'Position'." + ); + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #endregion + } +} diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index d32020fd7..64dd2d611 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -6,7 +6,8 @@ using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.BZip2MT; +using SharpCompress.Compressors.BZip2MT.InputStream; using SharpCompress.Compressors.Deflate; using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.Lzw; @@ -120,8 +121,8 @@ public TestOption( new(CompressionType.None, (stream) => true, (stream) => stream, ["tar"], false), // We always do a test for IsTarFile later new( CompressionType.BZip2, - BZip2Stream.IsBZip2, - (stream) => new BZip2Stream(stream, CompressionMode.Decompress, false), + Compressors.BZip2.BZip2Stream.IsBZip2, + (stream) => new BZip2ParallelInputStream(stream), ["tar.bz2", "tb2", "tbz", "tbz2", "tz2"] ), new( diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index c92188856..3efe0c277 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -6,7 +6,7 @@ using SharpCompress.Common; using SharpCompress.Common.Tar; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.BZip2MT.InputStream; using SharpCompress.Compressors.Deflate; using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.Lzw; @@ -34,7 +34,7 @@ protected override Stream RequestInitialStream() var stream = base.RequestInitialStream(); return compressionType switch { - CompressionType.BZip2 => new BZip2Stream(stream, CompressionMode.Decompress, false), + CompressionType.BZip2 => new BZip2ParallelInputStream(stream), CompressionType.GZip => new GZipStream(stream, CompressionMode.Decompress), CompressionType.ZStandard => new ZStandardStream(stream), CompressionType.LZip => new LZipStream(stream, CompressionMode.Decompress), @@ -74,10 +74,10 @@ public static TarReader Open(Stream stream, ReaderOptions? options = null) } ((IStreamStack)rewindableStream).StackSeek(pos); - if (BZip2Stream.IsBZip2(rewindableStream)) + if (Compressors.BZip2.BZip2Stream.IsBZip2(rewindableStream)) { ((IStreamStack)rewindableStream).StackSeek(pos); - var testStream = new BZip2Stream(rewindableStream, CompressionMode.Decompress, false); + var testStream = new BZip2ParallelInputStream(rewindableStream); if (TarArchive.IsTarFile(testStream)) { ((IStreamStack)rewindableStream).StackSeek(pos); diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index 96346e2bc..e542654cf 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -6,6 +6,8 @@ using SharpCompress.Common.Tar.Headers; using SharpCompress.Compressors; using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.BZip2MT; +using SharpCompress.Compressors.BZip2MT.OutputStream; using SharpCompress.Compressors.Deflate; using SharpCompress.Compressors.LZMA; using SharpCompress.IO; @@ -35,7 +37,7 @@ public TarWriter(Stream destination, TarWriterOptions options) break; case CompressionType.BZip2: { - destination = new BZip2Stream(destination, CompressionMode.Compress, false); + destination = new BZip2ParallelOutputStream(destination); } break; case CompressionType.GZip: diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index f867c8a91..8580424c1 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -9,7 +9,7 @@ using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.BZip2MT.OutputStream; using SharpCompress.Compressors.Deflate; using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.PPMd; @@ -441,7 +441,7 @@ private Stream GetWriteStream(Stream writeStream) } case ZipCompressionMethod.BZip2: { - return new BZip2Stream(counting, CompressionMode.Compress, false); + return new BZip2ParallelOutputStream(counting); } case ZipCompressionMethod.LZMA: { diff --git a/tests/SharpCompress.Test/BZip2MT/BZip2MTReaderTests.cs b/tests/SharpCompress.Test/BZip2MT/BZip2MTReaderTests.cs new file mode 100644 index 000000000..3321c3260 --- /dev/null +++ b/tests/SharpCompress.Test/BZip2MT/BZip2MTReaderTests.cs @@ -0,0 +1,28 @@ +using System.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Tar; +using Xunit; +namespace SharpCompress.Test.BZip2MT; + +public class BZip2MTReaderTests : ReaderTests +{ + [Fact] + public void BZip2MT_Reader_Factory_ValidTarBz2File() + { + Stream stream = new MemoryStream( + new byte[] + { + 0x42, 0x5a, 0x68, 0x39, 0x31, 0x41, 0x59, 0x26, 0x53, 0x59, 0xd2, 0xeb, 0x13, 0xbb, 0x00, 0x00, + 0x2f, 0xdf, 0x81, 0xce, 0x00, 0x40, 0x01, 0x6f, 0x80, 0x20, 0x00, 0x00, 0x20, 0x60, 0x00, 0x1e, + 0x40, 0x02, 0x00, 0x04, 0x00, 0xa0, 0x00, 0x54, 0x42, 0x8f, 0xd5, 0x18, 0x00, 0x03, 0x20, 0x94, + 0x53, 0xc1, 0x40, 0x03, 0x40, 0x5a, 0xbe, 0xa5, 0x09, 0x81, 0x03, 0x52, 0x89, 0x46, 0x40, 0x53, + 0x65, 0xab, 0x3b, 0xb4, 0xe7, 0x20, 0x9c, 0x48, 0x6f, 0x0f, 0xa3, 0x4c, 0x3d, 0xc1, 0x58, 0x06, + 0xdc, 0x7a, 0xc8, 0x99, 0x22, 0x82, 0xc6, 0xd1, 0x55, 0x15, 0x8d, 0x33, 0xe8, 0xbb, 0x92, 0x29, + 0xc2, 0x84, 0x86, 0x97, 0x58, 0x9d, 0xd8, + } + ); + var reader = ReaderFactory.Open(stream); + + Assert.IsType(typeof(TarReader), reader); + } +} diff --git a/tests/SharpCompress.Test/BZip2MT/BZip2MTStreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2MT/BZip2MTStreamAsyncTests.cs new file mode 100644 index 000000000..b6e8c6922 --- /dev/null +++ b/tests/SharpCompress.Test/BZip2MT/BZip2MTStreamAsyncTests.cs @@ -0,0 +1,201 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Compressors.BZip2MT.InputStream; +using SharpCompress.Compressors.BZip2MT.OutputStream; +using Xunit; +namespace SharpCompress.Test.BZip2MT; + +public class BZip2MTStreamAsyncTests +{ + private byte[] CreateTestData(int size) + { + var data = new byte[size]; + // Create compressible data with repetitive pattern + for (int i = 0; i < size; i++) + { + data[i] = (byte)('A' + (i % 26)); + } + return data; + } + + [Fact] + public async Task BZip2MTCompressDecompressAsyncTest() + { + var testData = this.CreateTestData(10000); + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2ParallelOutputStream(memoryStream) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + bzip2Stream.Close(); + } + compressed = memoryStream.ToArray(); + } + + // Verify compression occurred + Assert.True(compressed.Length > 0); + Assert.True(compressed.Length < testData.Length); + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2ParallelInputStream(memoryStream) + ) + { + decompressed = new byte[testData.Length]; + var totalRead = 0; + int bytesRead; + while ( + ( + bytesRead = await bzip2Stream.ReadAsync( + decompressed, + totalRead, + testData.Length - totalRead + ) + ) > 0 + ) + { + totalRead += bytesRead; + } + } + } + + // Verify decompression + Assert.Equal(testData, decompressed); + } + + [Fact] + public async Task BZip2MTReadAsyncWithCancellationTest() + { + var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2ParallelOutputStream(memoryStream) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + bzip2Stream.Close(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress with cancellation support + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2ParallelInputStream(memoryStream) + ) + { + var buffer = new byte[1024]; + using var cts = new System.Threading.CancellationTokenSource(); + + // Read should complete without cancellation + var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + Assert.True(bytesRead > 0); + } + } + } + + [Fact] + public async Task BZip2MTMultipleAsyncWritesTest() + { + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2ParallelOutputStream(memoryStream, false) + ) + { + var data1 = Encoding.ASCII.GetBytes("Hello "); + var data2 = Encoding.ASCII.GetBytes("World"); + var data3 = Encoding.ASCII.GetBytes("!"); + + await bzip2Stream.WriteAsync(data1, 0, data1.Length); + await bzip2Stream.WriteAsync(data2, 0, data2.Length); + await bzip2Stream.WriteAsync(data3, 0, data3.Length); + + bzip2Stream.Close(); + } + + var compressed = memoryStream.ToArray(); + Assert.True(compressed.Length > 0); + + // Decompress and verify + using (var readStream = new MemoryStream(compressed)) + { + // reset memory stream position + memoryStream.Position = 0; + + using ( + var bzip2Stream = new BZip2ParallelInputStream(memoryStream) + ) + { + var result = new StringBuilder(); + var buffer = new byte[256]; + int bytesRead; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + result.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead)); + } + + Assert.Equal("Hello World!", result.ToString()); + } + } + } + } + + [Fact] + public async Task BZip2MTLargeDataAsyncTest() + { + var largeData = this.CreateTestData(100000); + + // Compress + byte[] compressed; + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2ParallelOutputStream(memoryStream) + ) + { + await bzip2Stream.WriteAsync(largeData, 0, largeData.Length); + bzip2Stream.Close(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2ParallelInputStream(memoryStream) + ) + { + decompressed = new byte[largeData.Length]; + var totalRead = 0; + int bytesRead; + var buffer = new byte[4096]; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + Array.Copy(buffer, 0, decompressed, totalRead, bytesRead); + totalRead += bytesRead; + } + } + } + + // Verify + Assert.Equal(largeData, decompressed); + } +} diff --git a/tests/SharpCompress.Test/BZip2MT/TestCommon.cs b/tests/SharpCompress.Test/BZip2MT/TestCommon.cs new file mode 100644 index 000000000..e50142952 --- /dev/null +++ b/tests/SharpCompress.Test/BZip2MT/TestCommon.cs @@ -0,0 +1,206 @@ +using System; +using System.Diagnostics; +using System.IO; +using SharpCompress.Compressors.BZip2MT.InputStream; +using SharpCompress.Compressors.BZip2MT.OutputStream; +using Xunit; +using Xunit.Abstractions; +namespace SharpCompress.Test.BZip2MT +{ + public static class TestCommon + { + + /// + /// Common test routine for Multi Threaded compression + Single Threaded decompression + /// + /// + /// Input stream, must be seekable + /// If true, will use Parallel Compressor + /// If true, will use Parallel Decompressor + /// Maximum number of threads to use for compression, if 0 will use Environment.ProcessorCount + /// Size for temporary output memory buffer + /// Size for temporary copy buffer + /// If true, will save inputStream data to a file + /// (Time Compression, Time Decompression) + public static (double, double) GenericTest(ITestOutputHelper console, Stream inputStream, bool compressMultiThread, bool decompressMultiThread, int outputBufferSize = 8388608, int copyBufferSize = 8388608, bool saveFileOnFail = false) + { + void DebugSaveInputStream() + { + string randomFile = Path.GetRandomFileName(); + console.WriteLine($" Saving input data to {randomFile}"); + using FileStream fs = new FileStream(randomFile, FileMode.Create, FileAccess.Write); + inputStream.Position = 0; + inputStream.CopyTo(fs); + fs.Flush(); + fs.Close(); + } + + double timeCompress = 0; + double timeDecompress = 0; + + using MemoryStream output = new MemoryStream(outputBufferSize); + using MemoryStream outputDecompressed = new MemoryStream(outputBufferSize); + + try + { + // compress input + Stopwatch swCompress = new Stopwatch(); + swCompress.Start(); + using Stream compressor = compressMultiThread + ? new BZip2ParallelOutputStream(output, false, 9) + : new BZip2OutputStream(output, false, 9); + inputStream.CopyTo(compressor, copyBufferSize); + compressor.Close(); + swCompress.Stop(); + timeCompress = swCompress.ElapsedMilliseconds; + console.WriteLine($" {timeCompress} ms {(compressMultiThread ? "MT": "ST")} compression time... "); + + // reset output position + output.Position = 0; + + // decompress output + Stopwatch SwDecompress = new Stopwatch(); + SwDecompress.Start(); + using Stream decompressor = decompressMultiThread + ? new BZip2ParallelInputStream(output, false) + : new BZip2InputStream(output, false); + decompressor.CopyTo(outputDecompressed, copyBufferSize); + SwDecompress.Stop(); + timeDecompress = SwDecompress.ElapsedMilliseconds; + console.WriteLine($" {timeDecompress} ms {(decompressMultiThread ? "MT": "ST")} decompression time"); + } catch (Exception ex) + { + if (saveFileOnFail) + { + DebugSaveInputStream(); + } + + Assert.Fail($"Exception was thrown... {ex}"); + } + + if (inputStream.Length != outputDecompressed.Length) + { + Assert.Fail($"Decompressed stream length mismatch, expecting {inputStream.Length}, got {outputDecompressed.Length}"); + } + + inputStream.Position = 0; + outputDecompressed.Position = 0; + + for (int i = 0; i < inputStream.Length; i++) + { + int expect = inputStream.ReadByte(); + int value = outputDecompressed.ReadByte(); + if ( expect != value) + { + if (saveFileOnFail) + { + DebugSaveInputStream(); + Assert.Fail($"bytes differ at position {i}, expected {expect}, got {value}"); + } + } + } + + return (timeCompress, timeDecompress); + } + + public enum RandomDataMode + { + SingleByteValue, + RandomBytes, + RandomBytesRepeat, + } + + public enum TestMode + { + CMT_DST, // multi thread compress, single thread decompress + CST_DST, // single thread compress, single thread decompress + CMT_DMT, // multi thread compress, multi thread decompress + CST_DMT, // single thread compress, multi thread decompress + } + + public static void RandomLongTest_X(ITestOutputHelper console, int repeat, RandomDataMode dataMode, TestMode testMode, int len = 9000000) + { + Random random = new Random(); + + int copyBufferSize = 8388608; + int outBufferSize = 8388608; + int repeatStreaks = 64; + + double totalCompressionTimeMs = 0; + double totalDecompressionTimeMs = 0; + + for (int r = 0; r < repeat; r++) + { + byte[] bigBuffer = new byte[len]; + switch (dataMode) + { + case RandomDataMode.RandomBytes: + { + random.NextBytes(bigBuffer); + break; + } + case RandomDataMode.RandomBytesRepeat: + { + random.NextBytes(bigBuffer); + + int offset = 0; + for (int rs = 0; rs < repeatStreaks; rs++) + { + int newoffset = random.Next(0, (len - 10000) / repeatStreaks); + offset += newoffset; + int count = random.Next(0, 512); + byte val = bigBuffer[offset++]; + for (int i = 0; i < count; i++) + { + bigBuffer[offset++] = val; + } + } + break; + } + case RandomDataMode.SingleByteValue: + { + byte value = (byte)(random.Next() & 0xFF); + for (int i = 0; i < len; i++) + { + bigBuffer[i] = value; + } + break; + } + } + + MemoryStream ms = new MemoryStream(bigBuffer); + double timeC = 0, timeD = 0; + switch (testMode) + { + default: throw new ArgumentException("Unknown test mode"); + case TestMode.CMT_DMT: + { + (timeC, timeD) = GenericTest(console, ms, true, true, outBufferSize, copyBufferSize, true); + break; + } + case TestMode.CST_DMT: + { + (timeC, timeD) = GenericTest(console, ms, false, true, outBufferSize, copyBufferSize, true); + break; + } + case TestMode.CST_DST: + { + (timeC, timeD) = GenericTest(console, ms, false, false, outBufferSize, copyBufferSize, true); + break; + } + case TestMode.CMT_DST: + { + (timeC, timeD) = GenericTest(console, ms, true, false, outBufferSize, copyBufferSize, true); + break; + } + } + + totalCompressionTimeMs += timeC; + totalDecompressionTimeMs += timeD; + } + console.WriteLine($"DONE {testMode}"); + console.WriteLine($"AVERAGE {totalCompressionTimeMs / repeat} ms compression time... "); + console.WriteLine($"AVERAGE {totalDecompressionTimeMs / repeat} ms decompression time... "); + } + } +} diff --git a/tests/SharpCompress.Test/BZip2MT/Tests.cs b/tests/SharpCompress.Test/BZip2MT/Tests.cs new file mode 100644 index 000000000..02db3b341 --- /dev/null +++ b/tests/SharpCompress.Test/BZip2MT/Tests.cs @@ -0,0 +1,139 @@ +// Bzip2 library for .net +// Modified by drone1400 +// Location: https://github.com/drone1400/bzip2 +// Ported from the Java implementation by Matthew Francis: https://github.com/MateuszBartosiewicz/bzip2 +// Modified from the .net implementation by Jaime Olivares: http://github.com/jaime-olivares/bzip2 + +using System; +using System.IO; +using SharpCompress.Compressors.BZip2MT.Algorithm; +using SharpCompress.Compressors.BZip2MT.InputStream; +using SharpCompress.Compressors.BZip2MT.OutputStream; +using Xunit; +using Xunit.Abstractions; +namespace SharpCompress.Test.BZip2MT +{ + + /// + /// Unit test for the BZip2 compression library + /// + public class Tests + { + + private const int BufferSizeLarge = 10000000; // Almost 10 Mb + private const int BufferSizeSmall = 100000; // Around 100 Kb + private static byte[] Buffer = new byte[Tests.BufferSizeLarge]; + + private readonly ITestOutputHelper _console; + private readonly Random _random; + + /// + /// Fills the test buffer with random values + /// + public Tests(ITestOutputHelper console) + { + this._random = new Random(); + this._console = console; + + this._random.NextBytes(Tests.Buffer); + } + + /// + /// Performs a CRC check and compare against well-known results + /// The buffer has different values + /// + [Fact] + public void CrcAlgorithmDifferentValues() + { + byte[] buffer = + { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA + }; + + var crc = new CRC32(); + for (int i = 0; i < buffer.Length; i++) + crc.UpdateCrc(buffer[i]); + + Assert.Equal(0x8AEE127A, crc.CRC); + } + + /// + /// Performs a CRC check and compare against well-known results + /// The buffer has different values + /// + [Fact] + public void CrcAlgorithmSameValues() + { + var crc = new CRC32(); + crc.UpdateCrc(0x55, 10); + Assert.Equal(0xA1E07747, crc.CRC); + } + + /// + /// Compresses the full buffer and checks for a reasonable compressed size + /// + [Fact] + public void CompressSmokeLarge() + { + var input = new MemoryStream(Tests.Buffer); + var output = new MemoryStream(); + + var compressor = new BZip2OutputStream(output, false); + input.CopyTo(compressor); + compressor.Close(); + + // Estimated size between inputSize*0.5 and inputSize*1.1 + Assert.True(output.Length > Tests.BufferSizeLarge * 0.5); + Assert.True(output.Length < Tests.BufferSizeLarge * 1.1); + } + + /// + /// Compresses a portion of the buffer and checks for a reasonable compressed size + /// + [Fact] + public void CompressSmokeSmall() + { + var input = new MemoryStream(Tests.Buffer, 0, Tests.BufferSizeSmall); + var output = new MemoryStream(); + + var compressor = new BZip2OutputStream(output, false); + input.CopyTo(compressor); + compressor.Close(); + + // Estimated size between inputSize*0.5 and inputSize*1.1 + Assert.True(output.Length > Tests.BufferSizeSmall * 0.5); + Assert.True(output.Length < Tests.BufferSizeSmall * 1.1); + } + + /// + /// Compresses and decompresses a long random buffer + /// + [Fact] + public void CompressAndDecompress() + { + var input = new MemoryStream(Tests.Buffer); + var output = new MemoryStream(); + + var compressor = new BZip2OutputStream(output, false); + input.CopyTo(compressor); + compressor.Close(); + + Assert.True(output.Length > 4); + + output.Position = 0; + var output2 = new MemoryStream(); + var decompressor = new BZip2InputStream(output, false); + decompressor.CopyTo(output2); + + Assert.Equal(Tests.Buffer.Length, output2.Length); + output2.Position = 0; + for (int i = 0; i < Tests.Buffer.Length; i++) + { + if (Tests.Buffer[i] != (byte)output2.ReadByte()) + { + Assert.Fail($"bytes differ at position {i}"); + } + } + } + } +} diff --git a/tests/SharpCompress.Test/BZip2MT/TestsRandomData.cs b/tests/SharpCompress.Test/BZip2MT/TestsRandomData.cs new file mode 100644 index 000000000..5deec2dce --- /dev/null +++ b/tests/SharpCompress.Test/BZip2MT/TestsRandomData.cs @@ -0,0 +1,53 @@ +using Xunit; +using Xunit.Abstractions; +namespace SharpCompress.Test.BZip2MT +{ + public class TestsRandomData + { + private readonly ITestOutputHelper _console; + protected int _repeatCount = 1; + + public TestsRandomData(ITestOutputHelper console) + { + this._console = console; + } + + [Fact] + public void RandomSingleByteLongTest_CST_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.SingleByteValue, TestCommon.TestMode.CST_DST, 100000000); + [Fact] + public void RandomLongTest_CST_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytes, TestCommon.TestMode.CST_DST); + [Fact] + public void RandomLongTestWithRepeatedValues_CST_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytesRepeat, TestCommon.TestMode.CST_DST); + [Fact] + public void RandomSingleByteLongTest_CMT_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.SingleByteValue, TestCommon.TestMode.CMT_DST, 100000000); + [Fact] + public void RandomLongTest_CMT_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytes, TestCommon.TestMode.CMT_DST); + [Fact] + public void RandomLongTestWithRepeatedValues_CMT_DST() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytesRepeat, TestCommon.TestMode.CMT_DST); + + [Fact] + public void RandomSingleByteLongTest_CST_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.SingleByteValue, TestCommon.TestMode.CST_DMT, 100000000); + [Fact] + public void RandomLongTest_CST_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytes, TestCommon.TestMode.CST_DMT); + [Fact] + public void RandomLongTestWithRepeatedValues_CST_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytesRepeat, TestCommon.TestMode.CST_DMT); + [Fact] + public void RandomSingleByteLongTest_CMT_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.SingleByteValue, TestCommon.TestMode.CMT_DMT, 100000000); + [Fact] + public void RandomLongTest_CMT_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytes, TestCommon.TestMode.CMT_DMT); + [Fact] + public void RandomLongTestWithRepeatedValues_CMT_DMT() => + TestCommon.RandomLongTest_X(this._console, this._repeatCount, TestCommon.RandomDataMode.RandomBytesRepeat, TestCommon.TestMode.CMT_DMT); + } +}