decodeBytes method

Uint8List decodeBytes(
  1. List<int> data, {
  2. bool verify = false,
  3. bool throwOnError = false,
  4. int? maxOutputSize,
  5. XZMultithreadOptions<Uint8List>? multithread,
})

Decompress the given bytes with the xz format.

A malformed or truncated archive yields whatever was decoded before the failure, with nothing to say that it is not the whole file. Set throwOnError to get an ArchiveException instead, which is the only way this method can report a failure: unlike decodeStream it has no return value to spare for one.

verify checks the checksum stored with each block, catching damage that decodes without complaint. It costs time and changes only whether a failure is noticed, never what a successful decode returns. A block whose check does not match is a failure like any other, so without throwOnError the result is still the bytes up to and including it, exactly as decodeStream would have left them in its output.

throwOnError governs how a bad archive is reported and nothing else. Arguments that cannot be honoured throw ArgumentError whichever way it is set, and so does maxPreallocateSize on this class.

Pass multithread to decode on isolates. The call then returns immediately, the return value is an empty list, and the result arrives through XZMultithreadOptions.onDone:

final completer = Completer<Uint8List>();
XZDecoder().decodeBytes(compressed,
    multithread: XZMultithreadOptions(onDone: completer.complete));
final data = await completer.future;

throwOnError keeps working there. The exception cannot be thrown at the caller, whose stack is long gone by then, so it is handed to XZMultithreadOptions.onError instead, and XZMultithreadOptions.onDone is not called at all. Without throwOnError a failed decode reaches XZMultithreadOptions.onDone as the partial output, exactly as it is returned here.

Multithreading only pays off for an archive written in several blocks, which is what xz --block-size=... produces; a single block archive cannot be split and is simply decoded on one isolate. Measured on a 1.1 GB archive of six 192 MB blocks:

call peak memory time
no multithread 3.0 GB 16.7 s
with multithread 3.9 GB 8.0 s
with multithread, six workers 4.4 GB 5.1 s

This is the more expensive of the two methods, and by some distance: the archive and the decoded output both stay in memory, and every worker holds a copy of the block it is decoding, because bytes handed over cannot be read from anywhere else. decodeStream over an InputFileStream and an OutputFileStream decodes the same archive in the same time using well under a third of the memory, and is worth preferring whenever the data is on disk anyway.

The default XZMultithreadOptions.memoryBudget allowed three workers here; raising it buys the six worker row.

With maxOutputSize, an ArchiveException is thrown as soon as the decoded data would pass that many bytes, whatever throwOnError is. With multithread it goes to XZMultithreadOptions.onError.

Implementation

Uint8List decodeBytes(List<int> data,
    {bool verify = false,
    bool throwOnError = false,
    int? maxOutputSize,
    XZMultithreadOptions<Uint8List>? multithread}) {
  final bytes = data is Uint8List ? data : Uint8List.fromList(data);
  _checkMaxOutputSize(maxOutputSize);

  if (multithread == null) {
    return _decodeBytes(bytes, verify, throwOnError, maxOutputSize);
  }
  _checkOptions(multithread, throwOnError);

  if (!xzIsolatesSupported) {
    // No isolates here, so this blocks the caller, but the result is still
    // delivered the way the caller asked for it.
    _report(
        multithread,
        () => _decodeBytes(bytes, verify, throwOnError, maxOutputSize),
        Uint8List(0));
    return Uint8List(0);
  }

  _reportAsync(
      multithread,
      () => _decodeBytesOnIsolates(
          bytes, verify, throwOnError, maxOutputSize, multithread),
      Uint8List(0));
  return Uint8List(0);
}