blob: c620a991ef00b7656562e212303fadf9060498c3 [file] [log] [blame]
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'util/archive_exception.dart';
import 'util/crc32.dart';
import 'util/crc64.dart';
import 'util/input_stream.dart';
import 'lzma/lzma_decoder.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<int> decodeBytes(List<int> data, {bool verify = false}) {
return decodeBuffer(InputStream(data), verify: verify);
}
List<int> 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<int> 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) {
var magic = input.readBytes(6).toUint8List();
var 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');
}
var header = input.readBytes(2);
if (header.readByte() != 0) {
throw ArchiveException('Invalid stream flags');
}
streamFlags = header.readByte();
header.reset();
var 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) {
var blockStart = input.position;
var header = input.readBytes(headerLength - 4);
header.skip(1); // Skip length field
var blockFlags = header.readByte();
var nFilters = (blockFlags & 0x3) + 1;
var hasCompressedLength = blockFlags & 0x40 != 0;
var hasUncompressedLength = blockFlags & 0x80 != 0;
int? compressedLength;
if (hasCompressedLength) {
compressedLength = _readMultibyteInteger(header);
}
int? uncompressedLength;
if (hasUncompressedLength) {
uncompressedLength = _readMultibyteInteger(header);
}
var filterIds = <int>[];
var dictionarySize = 0;
for (var i = 0; i < nFilters; i++) {
var id = _readMultibyteInteger(header);
var propertiesLength = _readMultibyteInteger(header);
var properties = header.readBytes(propertiesLength).toUint8List();
if (id == 0x21) {
var v = properties[0];
if (v > 40) {
throw ArchiveException('Invalid LZMA dictionary size');
} else if (v == 40) {
dictionarySize = 1 << 32;
} else if (v % 2 == 0) {
dictionarySize = 1 << ((v ~/ 2) + 12);
} else {
dictionarySize = 1 << (((v - 1) ~/ 2) + 11);
}
}
filterIds.add(id);
}
_readPadding(header);
header.reset();
var crc = input.readUint32();
if (getCrc32(header.toUint8List()) != crc) {
throw ArchiveException('Invalid block CRC checksum');
}
if (filterIds.length != 1 && filterIds.first != 0x21) {
throw ArchiveException('Unsupported filters');
}
var startPosition = input.position;
var startDataLength = data.length;
_readLZMA2(input, dictionarySize);
var actualCompressedLength = input.position - startPosition;
var 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");
}
var paddingSize = _readPadding(input);
// Checksum
var checkType = streamFlags & 0xf;
switch (checkType) {
case 0: // none
break;
case 0x1: // CRC32
var expectedCrc = input.readUint32();
if (verify) {
var 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
var expectedCrc = input.readUint64();
if (verify && isCrc64Supported()) {
var 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
var expectedCrc = input.readBytes(32).toUint8List();
if (verify) {
var 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');
}
var unpaddedLength = input.position - blockStart - paddingSize;
_blockSizes.add(_XZBlockSize(unpaddedLength, uncompressedLength));
}
// Reads LZMA2 data from [input].
void _readLZMA2(InputStreamBase input, int dictionarySize) {
while (true) {
var 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) {
var length = (input.readByte() << 8 | input.readByte()) + 1;
data.add(input.readBytes(length).toUint8List());
} 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
var reset = (control >> 5) & 0x3;
var uncompressedLength = ((control & 0x1f) << 16 |
input.readByte() << 8 |
input.readByte()) +
1;
var 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 * 8;
}
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) {
var startPosition = input.position;
input.skip(1); // Skip index indicator
var nRecords = _readMultibyteInteger(input);
if (nRecords != _blockSizes.length) {
throw ArchiveException('Stream index block count mismatch');
}
for (var i = 0; i < nRecords; i++) {
var unpaddedLength = _readMultibyteInteger(input);
var 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
var indexLength = input.position - startPosition;
input.rewind(indexLength);
var indexData = input.readBytes(indexLength);
var 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) {
var crc = input.readUint32();
var footer = input.readBytes(6);
var 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');
}
var 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');
}
var 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) {
var 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);
}