import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'lzma/lzma_decoder.dart'; import 'util/archive_exception.dart'; import 'util/crc32.dart'; import 'util/crc64.dart'; import 'util/input_stream.dart'; // The XZ specification can be found at // https://tukaani.org/xz/xz-file-format.txt. /// Decompress data with the xz format decoder. class XZDecoder { List decodeBytes(List data, {bool verify = false}) { return decodeBuffer(InputStream(data), verify: verify); } List decodeBuffer(InputStreamBase input, {bool verify = false}) { var decoder = _XZStreamDecoder(verify: verify); return decoder.decode(input); } } /// Decodes an XZ stream. class _XZStreamDecoder { // True if checksums are confirmed. final bool verify; // Decoded data. final data = BytesBuilder(); // LZMA decoder. final decoder = LzmaDecoder(); // Stream flags, which are sent in both the header and the footer. var streamFlags = 0; // Block sizes. final _blockSizes = <_XZBlockSize>[]; _XZStreamDecoder({this.verify = false}); // Decode this stream and return the uncompressed data. List decode(InputStreamBase input) { _readStreamHeader(input); while (true) { var blockHeader = input.peekBytes(1).readByte(); if (blockHeader == 0) { var indexSize = _readStreamIndex(input); _readStreamFooter(input, indexSize); return data.takeBytes(); } var blockLength = (blockHeader + 1) * 4; _readBlock(input, blockLength); } } // Reads an XZ steam header from [input]. void _readStreamHeader(InputStreamBase input) { final magic = input.readBytes(6).toUint8List(); final magicIsValid = magic[0] == 253 && magic[1] == 55 /* '7' */ && magic[2] == 122 /* 'z' */ && magic[3] == 88 /* 'X' */ && magic[4] == 90 /* 'Z' */ && magic[5] == 0; if (!magicIsValid) { throw ArchiveException('Invalid XZ stream header signature'); } final header = input.readBytes(2); if (header.readByte() != 0) { throw ArchiveException('Invalid stream flags'); } streamFlags = header.readByte(); header.reset(); final crc = input.readUint32(); if (getCrc32(header.toUint8List()) != crc) { throw ArchiveException('Invalid stream header CRC checksum'); } } // Reads a data block from [input]. void _readBlock(InputStreamBase input, int headerLength) { final blockStart = input.position; final header = input.readBytes(headerLength - 4); header.skip(1); // Skip length field final blockFlags = header.readByte(); final nFilters = (blockFlags & 0x3) + 1; final hasCompressedLength = blockFlags & 0x40 != 0; final hasUncompressedLength = blockFlags & 0x80 != 0; int? compressedLength; if (hasCompressedLength) { compressedLength = _readMultibyteInteger(header); } int? uncompressedLength; if (hasUncompressedLength) { uncompressedLength = _readMultibyteInteger(header); } final filters = []; var dictionarySize = 0; for (var i = 0; i < nFilters; i++) { final id = _readMultibyteInteger(header); final propertiesLength = _readMultibyteInteger(header); final properties = header.readBytes(propertiesLength).toUint8List(); if (id == 0x03) { // delta filter final distance = properties[0]; filters.add(id); filters.add(distance); } else if (id == 0x21) { // lzma2 filter final v = properties[0]; if (v > 40) { throw ArchiveException('Invalid LZMA dictionary size'); } else if (v == 40) { dictionarySize = 0xffffffff; } else { final mantissa = 2 | (v & 0x1); final exponent = (v >> 1) + 11; dictionarySize = mantissa << exponent; } filters.add(id); filters.add(dictionarySize); } else { filters.add(id); filters.add(0); } } _readPadding(header); header.reset(); final crc = input.readUint32(); if (getCrc32(header.toUint8List()) != crc) { throw ArchiveException('Invalid block CRC checksum'); } if (filters.length != 2 && filters.first != 0x21) { throw ArchiveException('Unsupported filters'); } final startPosition = input.position; final startDataLength = data.length; _readLZMA2(input, dictionarySize); final actualCompressedLength = input.position - startPosition; final actualUncompressedLength = data.length - startDataLength; if (compressedLength != null && compressedLength != actualCompressedLength) { throw ArchiveException("Compressed data doesn't match expected length"); } uncompressedLength ??= actualUncompressedLength; if (uncompressedLength != actualUncompressedLength) { throw ArchiveException("Uncompressed data doesn't match expected length"); } final paddingSize = _readPadding(input); // Checksum final checkType = streamFlags & 0xf; switch (checkType) { case 0: // none break; case 0x1: // CRC32 final expectedCrc = input.readUint32(); if (verify) { final actualCrc = getCrc32(data.toBytes().sublist(startDataLength)); if (actualCrc != expectedCrc) { throw ArchiveException('CRC32 check failed'); } } break; case 0x2: case 0x3: input.skip(4); if (verify) { throw ArchiveException('Unknown check type $checkType'); } break; case 0x4: // CRC64 final expectedCrc = input.readUint64(); if (verify && isCrc64Supported()) { final actualCrc = getCrc64(data.toBytes().sublist(startDataLength)); if (actualCrc != expectedCrc) { throw ArchiveException('CRC64 check failed'); } } break; case 0x5: case 0x6: input.skip(8); if (verify) { throw ArchiveException('Unknown check type $checkType'); } break; case 0x7: case 0x8: case 0x9: input.skip(16); if (verify) { throw ArchiveException('Unknown check type $checkType'); } break; case 0xa: // SHA-256 final expectedCrc = input.readBytes(32).toUint8List(); if (verify) { final actualCrc = sha256.convert(data.toBytes().sublist(startDataLength)).bytes; for (var i = 0; i < 32; i++) { if (actualCrc[i] != expectedCrc[i]) { throw ArchiveException('SHA-256 check failed'); } } } break; case 0xb: case 0xc: input.skip(32); if (verify) { throw ArchiveException('Unknown check type $checkType'); } break; case 0xd: case 0xe: case 0xf: input.skip(64); if (verify) { throw ArchiveException('Unknown check type $checkType'); } break; default: throw ArchiveException('Unknown block check type $checkType'); } final unpaddedLength = input.position - blockStart - paddingSize; _blockSizes.add(_XZBlockSize(unpaddedLength, uncompressedLength)); } // Reads LZMA2 data from [input]. void _readLZMA2(InputStreamBase input, int dictionarySize) { while (true) { final control = input.readByte(); // Control values: // 00000000 - end marker // 00000001 - reset dictionary and uncompresed data // 00000010 - uncompressed data // 1rrxxxxx - LZMA data with reset (r) and high bits of size field (x) if (control & 0x80 == 0) { if (control == 0) { decoder.reset(resetDictionary: true); return; } else if (control == 1) { final length = (input.readByte() << 8 | input.readByte()) + 1; data.add(input.readBytes(length).toUint8List()); } else if (control == 2) { // uncompressed data final length = (input.readByte() << 8 | input.readByte()) + 1; data.add(decoder.decodeUncompressed(input.readBytes(length), length)); } else { throw ArchiveException('Unknown LZMA2 control code $control'); } } else { // Reset flags: // 0 - reset nothing // 1 - reset state // 2 - reset state, properties // 3 - reset state, properties and dictionary final reset = (control >> 5) & 0x3; final uncompressedLength = ((control & 0x1f) << 16 | input.readByte() << 8 | input.readByte()) + 1; final compressedLength = (input.readByte() << 8 | input.readByte()) + 1; int? literalContextBits; int? literalPositionBits; int? positionBits; if (reset >= 2) { // The three LZMA decoder properties are combined into a single number. var properties = input.readByte(); positionBits = properties ~/ 45; properties -= positionBits * 45; literalPositionBits = properties ~/ 9; literalContextBits = properties - literalPositionBits * 9; } if (reset > 0) { decoder.reset( literalContextBits: literalContextBits, literalPositionBits: literalPositionBits, positionBits: positionBits, resetDictionary: reset == 3); } data.add(decoder.decode( input.readBytes(compressedLength), uncompressedLength)); } } } // Reads an XZ stream index from [input]. // Returns the length of the index in bytes. int _readStreamIndex(InputStreamBase input) { final startPosition = input.position; input.skip(1); // Skip index indicator final nRecords = _readMultibyteInteger(input); if (nRecords != _blockSizes.length) { throw ArchiveException('Stream index block count mismatch'); } for (var i = 0; i < nRecords; i++) { final unpaddedLength = _readMultibyteInteger(input); final uncompressedLength = _readMultibyteInteger(input); if (_blockSizes[i].unpaddedLength != unpaddedLength) { throw ArchiveException('Stream index compressed length mismatch'); } if (_blockSizes[i].uncompressedLength != uncompressedLength) { throw ArchiveException('Stream index uncompressed length mismatch'); } } _readPadding(input); // Re-read for CRC calculation final indexLength = input.position - startPosition; input.rewind(indexLength); final indexData = input.readBytes(indexLength); final crc = input.readUint32(); if (getCrc32(indexData.toUint8List()) != crc) { throw ArchiveException('Invalid stream index CRC checksum'); } return indexLength + 4; } // Reads an XZ stream footer from [input] and check the index size matches // [indexSize]. void _readStreamFooter(InputStreamBase input, int indexSize) { final crc = input.readUint32(); final footer = input.readBytes(6); final backwardSize = (footer.readUint32() + 1) * 4; if (backwardSize != indexSize) { throw ArchiveException('Stream footer has invalid index size'); } if (footer.readByte() != 0) { throw ArchiveException('Invalid stream flags'); } final footerFlags = footer.readByte(); if (footerFlags != streamFlags) { throw ArchiveException("Stream footer flags don't match header flags"); } footer.reset(); if (getCrc32(footer.toUint8List()) != crc) { throw ArchiveException('Invalid stream footer CRC checksum'); } final magic = input.readBytes(2).toUint8List(); if (magic[0] != 89 /* 'Y' */ && magic[1] != 90 /* 'Z' */) { throw ArchiveException('Invalid XZ stream footer signature'); } } // Reads a multibyte integer from [input]. int _readMultibyteInteger(InputStreamBase input) { var value = 0; var shift = 0; while (true) { final data = input.readByte(); value |= (data & 0x7f) << shift; if (data & 0x80 == 0) { return value; } shift += 7; } } // Reads padding from [input] until the read position is aligned to a 4 byte // boundary. The padding bytes are confirmed to be zeros. // Returns he number of padding bytes. int _readPadding(InputStreamBase input) { var count = 0; while (input.position % 4 != 0) { if (input.readByte() != 0) { throw ArchiveException('Non-zero padding byte'); } count++; } return count; } } // Information about a block size. class _XZBlockSize { // The block size excluding padding. final int unpaddedLength; // The size of the data in the block when uncompressed. final int uncompressedLength; const _XZBlockSize(this.unpaddedLength, this.uncompressedLength); }