From d38e30d65a627b956e08e8ee2ab2a27c7dd95a74 Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Mon, 15 Dec 2025 06:46:18 -0800 Subject: [PATCH] [dart2wasm] Fix bug in JS typed data `.sublist()` implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue https://github.com/flutter/flutter/issues/179853 Change-Id: I97ce8d7177dfdd612714fafbc2a6b13555d9bba1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/468300 Reviewed-by: Ömer Ağacan Commit-Queue: Martin Kustermann --- .../_internal/wasm/lib/js_typed_array.dart | 140 ++++++---------- tests/web/wasm/regression_179853_test.dart | 154 ++++++++++++++++++ 2 files changed, 208 insertions(+), 86 deletions(-) create mode 100644 tests/web/wasm/regression_179853_test.dart diff --git a/sdk/lib/_internal/wasm/lib/js_typed_array.dart b/sdk/lib/_internal/wasm/lib/js_typed_array.dart index b74f1caff50..3d8cc457e58 100644 --- a/sdk/lib/_internal/wasm/lib/js_typed_array.dart +++ b/sdk/lib/_internal/wasm/lib/js_typed_array.dart @@ -891,14 +891,12 @@ final class JSUint8ArrayImpl extends JSIntegerArrayBase @override JSUint8ArrayImpl sublist(int start, [int? end]) { - final newOffset = offsetInBytes + start; - final newEnd = RangeErrorUtils.checkValidRange( - newOffset, - end, - lengthInBytes, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + start; + final newLengthInBytes = newEnd - start; + return JSUint8ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - final newLength = newEnd - newOffset; - return JSUint8ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -999,14 +997,12 @@ final class JSInt8ArrayImpl extends JSIntegerArrayBase @override JSInt8ArrayImpl sublist(int start, [int? end]) { - final newOffset = offsetInBytes + start; - final newEnd = RangeErrorUtils.checkValidRange( - newOffset, - end, - lengthInBytes, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + start; + final newLengthInBytes = newEnd - start; + return JSInt8ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - final newLength = newEnd - newOffset; - return JSInt8ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1109,15 +1105,11 @@ final class JSUint8ClampedArrayImpl extends JSIntegerArrayBase @override JSUint8ClampedArrayImpl sublist(int start, [int? end]) { - final newOffset = offsetInBytes + start; - final newEnd = RangeErrorUtils.checkValidRange( - newOffset, - end, - lengthInBytes, - ); - final newLength = newEnd - newOffset; + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + start; + final newLengthInBytes = newEnd - start; return JSUint8ClampedArrayImpl._( - buffer.cloneAsDataView(newOffset, newLength), + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); } @@ -1211,15 +1203,12 @@ final class JSUint16ArrayImpl extends JSIntegerArrayBase @override JSUint16ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 2); - final int newEnd = end == null ? lengthInBytes : end * 2; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 2, - newEnd ~/ 2, - lengthInBytes ~/ 2, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 2 * start; + final newLengthInBytes = 2 * (newEnd - start); + return JSUint16ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSUint16ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1327,15 +1316,12 @@ final class JSInt16ArrayImpl extends JSIntegerArrayBase @override JSInt16ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 2); - final int newEnd = end == null ? lengthInBytes : end * 2; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 2, - newEnd ~/ 2, - lengthInBytes ~/ 2, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 2 * start; + final newLengthInBytes = 2 * (newEnd - start); + return JSInt16ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSInt16ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1443,15 +1429,12 @@ final class JSUint32ArrayImpl extends JSIntegerArrayBase @override JSUint32ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 4); - final int newEnd = end == null ? lengthInBytes : end * 4; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 4, - newEnd ~/ 4, - lengthInBytes ~/ 4, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 4 * start; + final newLengthInBytes = 4 * (newEnd - start); + return JSUint32ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSUint32ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1559,15 +1542,12 @@ final class JSInt32ArrayImpl extends JSIntegerArrayBase @override JSInt32ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 4); - final int newEnd = end == null ? lengthInBytes : end * 4; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 4, - newEnd ~/ 4, - lengthInBytes ~/ 4, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 4 * start; + final newLengthInBytes = 4 * (newEnd - start); + return JSInt32ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSInt32ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1756,15 +1736,12 @@ final class JSBigUint64ArrayImpl extends JSIntegerArrayBase @override JSBigUint64ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 8); - final int newEnd = end == null ? lengthInBytes : end * 8; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 8, - newEnd ~/ 8, - lengthInBytes ~/ 8, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 8 * start; + final newLengthInBytes = 8 * (newEnd - start); + return JSBigUint64ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSBigUint64ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -1860,15 +1837,12 @@ final class JSBigInt64ArrayImpl extends JSIntegerArrayBase @override JSBigInt64ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 8); - final int newEnd = end == null ? lengthInBytes : end * 8; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 8, - newEnd ~/ 8, - lengthInBytes ~/ 8, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 8 * start; + final newLengthInBytes = 8 * (newEnd - start); + return JSBigInt64ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSBigInt64ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -2321,15 +2295,12 @@ final class JSFloat32ArrayImpl extends JSFloatArrayBase @override JSFloat32ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 4); - final int newEnd = end == null ? lengthInBytes : end * 4; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 4, - newEnd ~/ 4, - lengthInBytes ~/ 4, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 4 * start; + final newLengthInBytes = 4 * (newEnd - start); + return JSFloat32ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSFloat32ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override @@ -2438,15 +2409,12 @@ final class JSFloat64ArrayImpl extends JSFloatArrayBase @override JSFloat64ArrayImpl sublist(int start, [int? end]) { - final int newOffset = offsetInBytes + (start * 8); - final int newEnd = end == null ? lengthInBytes : end * 8; - final int newLength = newEnd - newOffset; - RangeErrorUtils.checkValidRange( - newOffset ~/ 8, - newEnd ~/ 8, - lengthInBytes ~/ 8, + final newEnd = RangeErrorUtils.checkValidRange(start, end, length); + final newOffsetInBytes = offsetInBytes + 8 * start; + final newLengthInBytes = 8 * (newEnd - start); + return JSFloat64ArrayImpl._( + buffer.cloneAsDataView(newOffsetInBytes, newLengthInBytes), ); - return JSFloat64ArrayImpl._(buffer.cloneAsDataView(newOffset, newLength)); } @override diff --git a/tests/web/wasm/regression_179853_test.dart b/tests/web/wasm/regression_179853_test.dart new file mode 100644 index 00000000000..2080a0c8b30 --- /dev/null +++ b/tests/web/wasm/regression_179853_test.dart @@ -0,0 +1,154 @@ +// Copyright (c) 2025, 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:js_interop'; +import 'dart:typed_data'; + +import 'package:expect/expect.dart'; + +void main() { + const size = 128; + for (final buffer in createBuffers(128)) { + Expect.equals(size, buffer.lengthInBytes); + initData(buffer, size); + forEachTypedData((viewConstructor, lengthFun, sublistFun, elementSize) { + final sizeInElements = size ~/ elementSize; + + final view = viewConstructor( + buffer, + /*offsetInBytes=*/ elementSize, + /*length=*/ sizeInElements - 2, + ); + print( + 'view: ${view.runtimeType} offsetInBytes:${view.offsetInBytes} len=${lengthFun(view)}', + ); + Expect.equals(elementSize, view.offsetInBytes); + Expect.equals(sizeInElements - 2, lengthFun(view)); + Expect.equals((sizeInElements - 2) * elementSize, view.lengthInBytes); + verifyData(view, elementSize, size - 2 * elementSize); + + // This creates a new list. + final sublist = sublistFun( + view, + /*start=*/ 1, + /*end=*/ sizeInElements - 3, + ); + Expect.equals(0, sublist.offsetInBytes); + Expect.equals(sizeInElements - 4, lengthFun(sublist)); + Expect.equals((sizeInElements - 4) * elementSize, sublist.lengthInBytes); + + verifyData(sublist, 2 * elementSize, size - 4 * elementSize); + }); + } +} + +void forEachTypedData( + void Function( + TypedData Function(ByteBuffer, [int, int?]), + int Function(TypedData), + TypedData Function(TypedData, int, int?), + int, + ) + fun, +) { + fun( + Uint8List.view, + (td) => (td as Uint8List).length, + (td, start, end) => (td as Uint8List).sublist(start, end), + 1, + ); + fun( + Int8List.view, + (td) => (td as Int8List).length, + (td, start, end) => (td as Int8List).sublist(start, end), + 1, + ); + fun( + Uint8ClampedList.view, + (td) => (td as Uint8ClampedList).length, + (td, start, end) => (td as Uint8ClampedList).sublist(start, end), + 1, + ); + fun( + Uint16List.view, + (td) => (td as Uint16List).length, + (td, start, end) => (td as Uint16List).sublist(start, end), + 2, + ); + fun( + Int16List.view, + (td) => (td as Int16List).length, + (td, start, end) => (td as Int16List).sublist(start, end), + 2, + ); + fun( + Uint32List.view, + (td) => (td as Uint32List).length, + (td, start, end) => (td as Uint32List).sublist(start, end), + 4, + ); + fun( + Int32List.view, + (td) => (td as Int32List).length, + (td, start, end) => (td as Int32List).sublist(start, end), + 4, + ); + fun( + Float32List.view, + (td) => (td as Float32List).length, + (td, start, end) => (td as Float32List).sublist(start, end), + 4, + ); + fun( + Float64List.view, + (td) => (td as Float64List).length, + (td, start, end) => (td as Float64List).sublist(start, end), + 8, + ); +} + +List createBuffers(int size) { + return [ + // JS backed. + JSArrayBuffer(size).toDart, + JSUint8Array.withLength(size).toDart.buffer, + JSInt8Array.withLength(size).toDart.buffer, + JSUint8ClampedArray.withLength(size).toDart.buffer, + JSUint16Array.withLength(size ~/ 2).toDart.buffer, + JSInt16Array.withLength(size ~/ 2).toDart.buffer, + JSUint32Array.withLength(size ~/ 4).toDart.buffer, + JSInt32Array.withLength(size ~/ 4).toDart.buffer, + JSFloat32Array.withLength(size ~/ 4).toDart.buffer, + JSFloat64Array.withLength(size ~/ 8).toDart.buffer, + + // Dart backed. + Uint8List(size).buffer, + Int8List(size).buffer, + Uint8ClampedList(size).buffer, + Uint16List(size ~/ 2).buffer, + Int16List(size ~/ 2).buffer, + Uint32List(size ~/ 4).buffer, + Int32List(size ~/ 4).buffer, + Int64List(size ~/ 8).buffer, + Float32List(size ~/ 4).buffer, + Float64List(size ~/ 8).buffer, + Float32x4List(size ~/ 16).buffer, + Float64x2List(size ~/ 16).buffer, + ]; +} + +void initData(ByteBuffer buffer, int length) { + final td = buffer.asUint8List(); + for (int i = 0; i < length; i++) { + td[i] = i; + } +} + +void verifyData(TypedData typedData, int offset, int length) { + final td = typedData.buffer.asUint8List(typedData.offsetInBytes); + print('verifyData($offset, $length) '); + for (int i = 0; i < length; i++) { + Expect.equals(offset + i, td[i]); + } +}