blob: c00c01ac26d919779366667a5e7239de202a0e85 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:web/web.dart' as web;
/// Web implementation of gzipDecoder using the browser's `DecompressionStream`.
Converter<List<int>, List<int>> get gzipDecoder => const BrowserGZipDecoder();
/// A [Converter] that decompresses gzip-compressed data using the browser's
/// `DecompressionStream` API.
class BrowserGZipDecoder extends Converter<List<int>, List<int>> {
const BrowserGZipDecoder();
@override
List<int> convert(List<int> input) {
// The browser's DecompressionStream API is asynchronous, so we cannot
// implement this synchronously.
throw UnsupportedError(
'Synchronous gzip decoding is not supported on the web.',
);
}
@override
Stream<List<int>> bind(Stream<List<int>> stream) {
final controller = StreamController<List<int>>();
unawaited(_pipe(stream, controller));
return controller.stream;
}
bool _isHarmlessPaddingError(Object e) {
// Some .tar.gz may contain harmless padding, which DecrompressionStream in
// the browsers are sensitive to. Example:
// https://pub.dev/api/archives/lints-1.0.1.tar.gz
final errorMessage = e.toString().toLowerCase();
// Match the specific EOF padding errors from V8, SpiderMonkey, and WebKit
return errorMessage.contains('junk found') ||
errorMessage.contains('unexpected input') ||
errorMessage.contains('extra bytes');
}
Future<void> _pipe(
Stream<List<int>> stream,
StreamController<List<int>> controller,
) async {
final decompressionStream = web.DecompressionStream('gzip');
final writer = decompressionStream.writable.getWriter();
final reader =
decompressionStream.readable.getReader()
as web.ReadableStreamDefaultReader;
final readFuture = () async {
try {
while (true) {
final result = await reader.read().toDart;
if (result.done) break;
final value = result.value as JSUint8Array;
controller.add(value.toDart);
}
} catch (e, st) {
if (_isHarmlessPaddingError(e)) {
// Ignore trailing junk
return;
}
controller.addError(e, st);
} finally {
reader.releaseLock();
}
}();
try {
await for (final chunk in stream) {
final bytes = chunk is Uint8List ? chunk : Uint8List.fromList(chunk);
await writer.write(bytes.toJS).toDart;
}
await writer.close().toDart;
} catch (e, st) {
if (_isHarmlessPaddingError(e)) {
// Ignore trailing junk
} else {
controller.addError(e, st);
}
} finally {
await readFuture;
await controller.close();
}
}
}