// Copyright 2014 The Flutter Authors. 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:convert'; import 'dart:typed_data'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:standard_message_codec/standard_message_codec.dart'; import '../src/common.dart'; import '../src/package_config.dart'; void main() { Future>> extractAssetManifestJsonFromBundle( ManifestAssetBundle bundle, ) async { final String manifestJson = utf8.decode( await bundle.entries['AssetManifest.json']!.contentsAsBytes(), ); final Map parsedJson = json.decode(manifestJson) as Map; final Iterable keys = parsedJson.keys; final Map> parsedManifest = >{ for (final String key in keys) key: List.from(parsedJson[key] as List), }; return parsedManifest; } Future> extractAssetManifestSmcBinFromBundle( ManifestAssetBundle bundle, ) async { final List manifest = await bundle.entries['AssetManifest.bin']!.contentsAsBytes(); final ByteData asByteData = ByteData.view(Uint8List.fromList(manifest).buffer); final Map decoded = const StandardMessageCodec().decodeMessage(asByteData)! as Map; return decoded; } group('AssetBundle asset variants (with Unix-style paths)', () { late Platform platform; late FileSystem fs; late String flutterRoot; setUp(() { platform = FakePlatform(); fs = MemoryFileSystem.test(); flutterRoot = Cache.defaultFlutterRoot( platform: platform, fileSystem: fs, userMessages: UserMessages(), ); writePackageConfigFiles(directory: fs.currentDirectory, mainLibName: 'test'); }); void createPubspec({required List assets}) { fs.file('pubspec.yaml').writeAsStringSync(''' name: test dependencies: flutter: sdk: flutter flutter: assets: ${assets.map((String entry) => ' - $entry').join('\n')} '''); } testWithoutContext( 'Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async { createPubspec(assets: ['assets/', 'assets/notAVariant/']); const String image = 'assets/image.jpg'; const String image2xVariant = 'assets/2x/image.jpg'; const String imageNonVariant = 'assets/notAVariant/image.jpg'; final List assets = [image, image2xVariant, imageNonVariant]; for (final String asset in assets) { final File assetFile = fs.file(asset); assetFile.createSync(recursive: true); assetFile.writeAsStringSync(asset); } final ManifestAssetBundle bundle = ManifestAssetBundle( logger: BufferLogger.test(), fileSystem: fs, platform: platform, flutterRoot: flutterRoot, ); await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), ); final Map> jsonManifest = await extractAssetManifestJsonFromBundle( bundle, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( bundle, ); final Map>> expectedAssetManifest = >>{ image: >[ {'asset': image}, {'asset': image2xVariant, 'dpr': 2.0}, ], imageNonVariant: >[ {'asset': imageNonVariant}, ], }; expect(smcBinManifest, equals(expectedAssetManifest)); expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest))); }, ); testWithoutContext( 'Asset directories have their subdirectories searched for asset variants', () async { createPubspec(assets: ['assets/', 'assets/folder/']); const String topLevelImage = 'assets/image.jpg'; const String secondLevelImage = 'assets/folder/secondLevel.jpg'; const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg'; final List assets = [topLevelImage, secondLevelImage, secondLevel2xVariant]; for (final String asset in assets) { final File assetFile = fs.file(asset); assetFile.createSync(recursive: true); assetFile.writeAsStringSync(asset); } final ManifestAssetBundle bundle = ManifestAssetBundle( logger: BufferLogger.test(), fileSystem: fs, flutterRoot: flutterRoot, platform: platform, ); await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), ); final Map> jsonManifest = await extractAssetManifestJsonFromBundle( bundle, ); expect(jsonManifest, hasLength(2)); expect(jsonManifest[topLevelImage], equals([topLevelImage])); expect( jsonManifest[secondLevelImage], equals([secondLevelImage, secondLevel2xVariant]), ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( bundle, ); final Map>> expectedAssetManifest = >>{ topLevelImage: >[ {'asset': topLevelImage}, ], secondLevelImage: >[ {'asset': secondLevelImage}, {'asset': secondLevel2xVariant, 'dpr': 2.0}, ], }; expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest))); expect(smcBinManifest, equals(expectedAssetManifest)); }, ); testWithoutContext('Asset paths should never be URI-encoded', () async { createPubspec(assets: ['assets/normalFolder/']); const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg'; const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg'; final List assets = [image, imageVariant]; for (final String asset in assets) { final File assetFile = fs.file(asset); assetFile.createSync(recursive: true); assetFile.writeAsStringSync(asset); } final ManifestAssetBundle bundle = ManifestAssetBundle( logger: BufferLogger.test(), fileSystem: fs, platform: platform, flutterRoot: flutterRoot, ); await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), ); final Map> jsonManifest = await extractAssetManifestJsonFromBundle( bundle, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( bundle, ); final Map>> expectedAssetManifest = >>{ image: >[ {'asset': image}, {'asset': imageVariant, 'dpr': 3.0}, ], }; expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest))); expect(smcBinManifest, equals(expectedAssetManifest)); }); testWithoutContext('Main assets are not included if the file does not exist', () async { createPubspec(assets: ['assets/image.png']); // We intentionally do not add a 'assets/image.png'. const String imageVariant = 'assets/2x/image.png'; final List assets = [imageVariant]; for (final String asset in assets) { final File assetFile = fs.file(asset); assetFile.createSync(recursive: true); assetFile.writeAsStringSync(asset); } final ManifestAssetBundle bundle = ManifestAssetBundle( logger: BufferLogger.test(), fileSystem: fs, platform: platform, flutterRoot: flutterRoot, ); await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), ); final Map>> expectedManifest = >>{ 'assets/image.png': >[ {'asset': imageVariant, 'dpr': 2.0}, ], }; final Map> jsonManifest = await extractAssetManifestJsonFromBundle( bundle, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( bundle, ); expect(jsonManifest, equals(_assetManifestBinToJson(expectedManifest))); expect(smcBinManifest, equals(expectedManifest)); }); }); group('AssetBundle asset variants (with Windows-style filepaths)', () { late final Platform platform; late final FileSystem fs; late final String flutterRoot; setUp(() { platform = FakePlatform(operatingSystem: 'windows'); fs = MemoryFileSystem.test(style: FileSystemStyle.windows); flutterRoot = Cache.defaultFlutterRoot( platform: platform, fileSystem: fs, userMessages: UserMessages(), ); writePackageConfigFiles(directory: fs.currentDirectory, mainLibName: 'test'); fs.file('pubspec.yaml').writeAsStringSync(''' name: test dependencies: flutter: sdk: flutter flutter: assets: - assets/ - assets/somewhereElse/ '''); }); testWithoutContext('Variant detection works with windows-style filepaths', () async { const List assets = [ r'assets\foo.jpg', r'assets\2x\foo.jpg', r'assets\somewhereElse\bar.jpg', r'assets\somewhereElse\2x\bar.jpg', ]; for (final String asset in assets) { final File assetFile = fs.file(asset); assetFile.createSync(recursive: true); assetFile.writeAsStringSync(asset); } final ManifestAssetBundle bundle = ManifestAssetBundle( logger: BufferLogger.test(), fileSystem: fs, platform: platform, flutterRoot: flutterRoot, ); await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), ); final Map>> expectedAssetManifest = >>{ 'assets/foo.jpg': >[ {'asset': 'assets/foo.jpg'}, {'asset': 'assets/2x/foo.jpg', 'dpr': 2.0}, ], 'assets/somewhereElse/bar.jpg': >[ {'asset': 'assets/somewhereElse/bar.jpg'}, {'asset': 'assets/somewhereElse/2x/bar.jpg', 'dpr': 2.0}, ], }; final Map> jsonManifest = await extractAssetManifestJsonFromBundle( bundle, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( bundle, ); expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest))); expect(smcBinManifest, equals(expectedAssetManifest)); }); }); } Map _assetManifestBinToJson(Map manifest) { List convertList(List variants) => variants.map((Object variant) => (variant as Map)['asset']!).toList(); return manifest.map( (Object key, Object value) => MapEntry(key, convertList(value as List)), ); }