Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Added `Body.size` for exposing known body byte lengths without consuming the
body.
- Fixed `Blob` byte snapshot semantics so byte-backed parts and read buffers are
copied consistently across native, `dart:io`, and js wrappers.

## 0.5.0

Expand Down
15 changes: 12 additions & 3 deletions lib/src/fetch/blob.js.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:typed_data';

import 'package:block/block.dart' as block;
import 'package:web/web.dart' as web;

Expand Down Expand Up @@ -25,10 +27,17 @@ class Blob extends native.Blob implements block.Block {

static Object _normalizePart(native.BlobPart part) {
return switch (part) {
final Blob blob => blob,
final native.Blob blob => blob,
final ByteBuffer buffer => Uint8List.fromList(buffer.asUint8List()),
final ByteData data => Uint8List.fromList(
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
),
final Uint8List bytes => Uint8List.fromList(bytes),
final List<int> bytes => Uint8List.fromList(bytes),
final String text => text,
final Blob blob => native.blobBacking(blob),
final native.Blob blob => native.blobBacking(blob),
final web.Blob blob => blob,
_ => native.Blob([part]),
_ => native.blobBacking(native.Blob([part])),
};
}
}
24 changes: 19 additions & 5 deletions lib/src/fetch/blob.native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ class Blob implements block.Block {
@override
int get size => _host.size;

Future<Uint8List> bytes() => _host.arrayBuffer();
Future<Uint8List> bytes() => arrayBuffer();

@override
Future<Uint8List> arrayBuffer() => _host.arrayBuffer();
Future<Uint8List> arrayBuffer() async {
return Uint8List.fromList(await _host.arrayBuffer());
}

@override
Future<String> text() => _host.text();
Expand All @@ -55,7 +57,13 @@ class Blob implements block.Block {
throw ArgumentError.value(chunkSize, 'chunkSize', 'Must be > 0');
}

return _host.stream(chunkSize: chunkSize);
return _stream(chunkSize);
}

Stream<Uint8List> _stream(int chunkSize) async* {
await for (final chunk in _host.stream(chunkSize: chunkSize)) {
yield Uint8List.fromList(chunk);
}
}

@override
Expand All @@ -74,8 +82,8 @@ class Blob implements block.Block {
return switch (part) {
final Blob blob => blob._host,
final block.Block blockPart => blockPart,
final ByteBuffer buffer => ByteData.sublistView(buffer.asUint8List()),
final ByteData data => ByteData.sublistView(
final ByteBuffer buffer => Uint8List.fromList(buffer.asUint8List()),
final ByteData data => Uint8List.fromList(
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
),
final Uint8List bytes => Uint8List.fromList(bytes),
Expand All @@ -90,6 +98,12 @@ class Blob implements block.Block {
}
}

/// Returns the backing block for Blob-part normalization.
///
/// Platform wrappers use this to preserve Blob byte-sequence semantics without
/// calling overridable read methods on Blob subclasses.
block.Block blobBacking(Blob blob) => blob._host;

/// Normalizes Blob/File MIME type inputs using the File API rules.
String normalizeBlobType(String type) {
StringBuffer? buffer;
Expand Down
103 changes: 103 additions & 0 deletions test/blob_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:ht/src/fetch/blob.dart' as platform_blob;
import 'package:test/test.dart';

void main() {
group('Blob', () {
test('snapshots byte parts at construction', () async {
final typedBytes = Uint8List.fromList(utf8.encode('ab'));
final bufferBytes = Uint8List.fromList(utf8.encode('cd'));
final dataBytes = Uint8List.fromList(utf8.encode('-ef-'));
final data = ByteData.sublistView(dataBytes, 1, 3);
final listBytes = <int>[...utf8.encode('gh')];

final blob = platform_blob.Blob(<platform_blob.BlobPart>[
typedBytes,
bufferBytes.buffer,
data,
listBytes,
]);

typedBytes[0] = 'x'.codeUnitAt(0);
bufferBytes[0] = 'y'.codeUnitAt(0);
dataBytes[1] = 'z'.codeUnitAt(0);
listBytes[0] = 'w'.codeUnitAt(0);

expect(await blob.text(), 'abcdefgh');

final singleBufferBytes = Uint8List.fromList(utf8.encode('ij'));
final singleBufferBlob = platform_blob.Blob(<platform_blob.BlobPart>[
singleBufferBytes.buffer,
]);
singleBufferBytes[0] = 'x'.codeUnitAt(0);
expect(await singleBufferBlob.text(), 'ij');

final singleDataBytes = Uint8List.fromList(utf8.encode('-kl-'));
final singleDataBlob = platform_blob.Blob(<platform_blob.BlobPart>[
ByteData.sublistView(singleDataBytes, 1, 3),
]);
singleDataBytes[1] = 'y'.codeUnitAt(0);
expect(await singleDataBlob.text(), 'kl');
});

test('arrayBuffer and bytes return defensive copies', () async {
final blob = platform_blob.Blob(<platform_blob.BlobPart>[
Uint8List.fromList(utf8.encode('abc')),
]);

final buffer = await blob.arrayBuffer();
buffer[0] = 'x'.codeUnitAt(0);

expect(await blob.text(), 'abc');

final bytes = await blob.bytes();
bytes[1] = 'y'.codeUnitAt(0);

expect(await blob.text(), 'abc');
});

test('stream chunks do not expose mutable backing', () async {
final blob = platform_blob.Blob(<platform_blob.BlobPart>[
Uint8List.fromList(utf8.encode('abcd')),
]);

expect(() => blob.stream(chunkSize: 0), throwsArgumentError);

final chunks = await blob.stream(chunkSize: 2).toList();
chunks.first[0] = 'x'.codeUnitAt(0);

expect(await blob.text(), 'abcd');
expect(
await blob.stream(chunkSize: 2).map(utf8.decode).toList(),
<String>['ab', 'cd'],
);
});

test('Blob parts use Blob backing instead of read overrides', () async {
final blob = platform_blob.Blob(<platform_blob.BlobPart>[
_ReadOverridingBlob(),
]);

expect(await blob.text(), 'base');
});
});
}

class _ReadOverridingBlob extends platform_blob.Blob {
_ReadOverridingBlob() : super(<platform_blob.BlobPart>['base'], 'text/plain');

@override
Future<Uint8List> arrayBuffer() async {
return Uint8List.fromList(utf8.encode('override'));
}

@override
Stream<Uint8List> stream({int chunkSize = 16 * 1024}) {
return Stream<Uint8List>.value(Uint8List.fromList(utf8.encode('override')));
}

@override
Future<String> text() async => 'override';
}