From 8f499c2eb56f9c3c46fdc60591dc2f7acf899f6f Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 14 Sep 2026 10:43:05 +0800 Subject: [PATCH] Keep browser cursor validation coverage in the existing Web fixture --- .../test/cursor_source_validation_test.dart | 108 ------------------ flutter/test/cursor_web_test.dart | 42 +++++++ 2 files changed, 42 insertions(+), 108 deletions(-) delete mode 100644 flutter/test/cursor_source_validation_test.dart diff --git a/flutter/test/cursor_source_validation_test.dart b/flutter/test/cursor_source_validation_test.dart deleted file mode 100644 index 36f10c0d9..000000000 --- a/flutter/test/cursor_source_validation_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'cursor_test_utils.dart'; - -const _side = 32; -const _rgbaChannels = 4; -const _sourceLimit = 4096; -const _invalidSizes = [ - (0, _side), - (-1, _side), - (_side, 0), - (_side, -1), - (_sourceLimit + 1, 1), - (1, _sourceLimit + 1), - (_sourceLimit * 4, _sourceLimit * 4), -]; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - for (final platform in [ - kPeerPlatformMacOS, - kPeerPlatformWindows, - kPeerPlatformLinux, - ]) { - for (final density in [null, '0', '4']) { - test('cursor source validation $platform density=$density', - () => _checkPackets(platform, density)); - } - } -} - -Future _checkPackets(String platform, String? density) async { - final canvas = CursorTestCanvas(1, style: kRemoteViewStyleAdaptive, scale: 1); - final ffi = CursorTestFFI(canvas)..ffiModel.pi.platform = platform; - final cursor = CursorModel(WeakReference(ffi))..id = 'valid'; - final messages = []; - final originalPrint = debugPrint; - debugPrint = (message, {wrapWidth}) => messages.add(message); - addTearDown(() { - debugPrint = originalPrint; - expect(cursor.parent.target, same(ffi)); - cursor.disposeImages(); - cursor.dispose(); - canvas.dispose(); - }); - await cursor.updateCursorData(_event(density)); - final previous = cursor.cache; - for (final length in [ - 0, - 4, - _side * _side * _rgbaChannels - 1, - _side * _side * _rgbaChannels + 1 - ]) { - await _expectRejected(cursor, _event(density, length: length)); - expect(messages, contains(contains('RGBA length'))); - messages.clear(); - } - for (final (width, height) in _invalidSizes) { - // Invalid JSON detects decoding before geometry validation, without risking - // the oversized allocation if the production size check regresses. - final packet = _event(density, width: width, height: height, length: 4) - ..['colors'] = 'must not decode an invalid source size'; - await _expectRejected(cursor, packet); - expect(messages, contains(contains('source size'))); - messages.clear(); - } - for (final (width, height) in [ - (1, 1), - (_sourceLimit, 1), - (1, _sourceLimit) - ]) { - cursor.id = 'valid'; - await cursor - .updateCursorData(_event(density, width: width, height: height)); - expect((cursor.image!.width, cursor.image!.height), (width, height)); - expect(cursor.cache, isNot(same(previous))); - } -} - -Future _expectRejected( - CursorModel cursor, Map packet) async { - final previous = cursor.cache; - final image = cursor.image; - final hotspot = (cursor.hotx, cursor.hoty); - cursor.id = 'invalid'; - await cursor.updateCursorData({...packet, 'id': 'invalid'}); - expect(cursor.cache, same(previous)); - expect(cursor.image, same(image)); - expect((cursor.hotx, cursor.hoty), hotspot); -} - -Map _event(String? density, - {int width = _side, int height = _side, int? length}) => - { - 'id': 'valid', - 'width': '$width', - 'height': '$height', - 'hotx': '0', - 'hoty': '0', - if (density != null) 'scale': density, - 'colors': jsonEncode( - List.filled(length ?? width * height * _rgbaChannels, 255)), - }; diff --git a/flutter/test/cursor_web_test.dart b/flutter/test/cursor_web_test.dart index 654ace6c2..3423d0742 100644 --- a/flutter/test/cursor_web_test.dart +++ b/flutter/test/cursor_web_test.dart @@ -24,6 +24,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); _alphaTests(); _thinCursorTests(); + _sourceValidationTests(); for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) { for (final dpr in [1.0, 2.0]) { for (final (platform, density) in [ @@ -64,6 +65,47 @@ void main() { }); } +void _sourceValidationTests() { + const side = 48; + const sourceLimit = 4096; + const bytes = side * side * 4; + for (final density in [null, 4.0]) { + test('Web rejects invalid cursor sources and recovers, density=$density', () async { + final canvas = CursorTestCanvas(1, style: kRemoteViewStyleAdaptive, scale: 1); + addTearDown(canvas.dispose); + final cursor = await _loadCursor(CursorTestFFI(canvas), 'source-$density', + source: (side, side), pixelRatio: density); + final cache = cursor.cache; + final image = cursor.image; + final event = { + 'id': 'source-$density', 'width': '$side', 'height': '$side', + 'hotx': '7', 'hoty': '9', + if (density != null) 'scale': '$density', + 'colors': jsonEncode(List.filled(bytes, 255)), + }; + // Invalid JSON proves geometry is checked before decoding or allocation. + const invalidPixels = 'must not decode an invalid source size'; + for (final invalid in [ + {'colors': '[1,2,3,4]'}, + {'colors': jsonEncode(List.filled(bytes + 1, 255))}, + {'width': '0', 'colors': invalidPixels}, + {'height': '-1', 'colors': invalidPixels}, + {'width': '${sourceLimit * 4}', 'height': '${sourceLimit * 4}', + 'colors': invalidPixels}, + {'scale': '1e-300'}, + {'scale': '0.001'}, + ]) { + await cursor.updateCursorData({...event, ...invalid}); + expect(cursor.cache, same(cache)); + expect(cursor.image, same(image)); + expect((cursor.hotx, cursor.hoty), (7, 9)); + } + await cursor.updateCursorData(event); + expect(cursor.cache, isNot(same(cache))); + }); + } +} + void _thinCursorTests() { for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) { for (final zoom in [false, true]) {