Unverified Commit 83bdde2b authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Catch file system exceptions when trying to parse user-provided asset file paths (#142214)

Fixes #141211
parent 38879dae
...@@ -958,8 +958,8 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -958,8 +958,8 @@ class ManifestAssetBundle implements AssetBundle {
} on UnsupportedError catch (e) { } on UnsupportedError catch (e) {
throwToolExit( throwToolExit(
'Unable to search for asset files in directory path "${assetUri.path}". ' 'Unable to search for asset files in directory path "${assetUri.path}". '
'Please ensure that this is valid URI that points to a directory ' 'Please ensure that this entry in pubspec.yaml is a valid file path.\n'
'that is available on the local file system.\nError details:\n$e'); 'Error details:\n$e');
} }
if (!_fileSystem.directory(directoryPath).existsSync()) { if (!_fileSystem.directory(directoryPath).existsSync()) {
...@@ -1296,17 +1296,25 @@ class _AssetDirectoryCache { ...@@ -1296,17 +1296,25 @@ class _AssetDirectoryCache {
final Map<String, List<File>> _variantsPerFolder = <String, List<File>>{}; final Map<String, List<File>> _variantsPerFolder = <String, List<File>>{};
List<String> variantsFor(String assetPath) { List<String> variantsFor(String assetPath) {
final String directory = _fileSystem.path.dirname(assetPath); final String directoryName = _fileSystem.path.dirname(assetPath);
if (!_fileSystem.directory(directory).existsSync()) { try {
if (!_fileSystem.directory(directoryName).existsSync()) {
return const <String>[]; return const <String>[];
} }
} on FileSystemException catch (e) {
throwToolExit(
'Unable to check the existence of asset file "$assetPath". '
'Ensure that the asset file is declared as a valid local file system path.\n'
'Details: $e',
);
}
if (_cache.containsKey(assetPath)) { if (_cache.containsKey(assetPath)) {
return _cache[assetPath]!; return _cache[assetPath]!;
} }
if (!_variantsPerFolder.containsKey(directory)) { if (!_variantsPerFolder.containsKey(directoryName)) {
_variantsPerFolder[directory] = _fileSystem.directory(directory) _variantsPerFolder[directoryName] = _fileSystem.directory(directoryName)
.listSync() .listSync()
.whereType<Directory>() .whereType<Directory>()
.where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename)) .where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename))
...@@ -1315,7 +1323,7 @@ class _AssetDirectoryCache { ...@@ -1315,7 +1323,7 @@ class _AssetDirectoryCache {
.toList(); .toList();
} }
final File assetFile = _fileSystem.file(assetPath); final File assetFile = _fileSystem.file(assetPath);
final List<File> potentialVariants = _variantsPerFolder[directory]!; final List<File> potentialVariants = _variantsPerFolder[directoryName]!;
final String basename = assetFile.basename; final String basename = assetFile.basename;
return _cache[assetPath] = <String>[ return _cache[assetPath] = <String>[
// It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png), // It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png),
......
// 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 '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 '../src/common.dart';
void main() {
Future<ManifestAssetBundle> buildBundleWithFlavor(String? flavor, {
required Logger logger,
required FileSystem fileSystem,
required Platform platform,
}) async {
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: logger,
fileSystem: fileSystem,
platform: platform,
flutterRoot: Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fileSystem,
userMessages: UserMessages(),
),
splitDeferredAssets: true,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
flavor: flavor,
);
return bundle;
}
testWithoutContext('correctly bundles assets given a simple asset manifest with flavors', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('assets', 'common', 'image.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('assets', 'vanilla', 'ice-cream.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('assets', 'strawberry', 'ice-cream.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('assets', 'orange', 'ice-cream.png')).createSync(recursive: true);
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- assets/common/
- path: assets/vanilla/
flavors:
- vanilla
- path: assets/strawberry/
flavors:
- strawberry
- path: assets/orange/ice-cream.png
flavors:
- orange
''');
ManifestAssetBundle bundle;
bundle = await buildBundleWithFlavor(
null,
logger: logger,
fileSystem: fileSystem,
platform: platform,
);
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png')));
bundle = await buildBundleWithFlavor(
'strawberry',
logger: logger,
fileSystem: fileSystem,
platform: platform,
);
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, contains('assets/strawberry/ice-cream.png'));
expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png')));
bundle = await buildBundleWithFlavor(
'orange',
logger: logger,
fileSystem: fileSystem,
platform: platform,
);
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png')));
expect(bundle.entries.keys, contains('assets/orange/ice-cream.png'));
});
testWithoutContext('throws a tool exit when a non-flavored folder contains a flavored asset', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('assets', 'unflavored.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('assets', 'vanillaOrange.png')).createSync(recursive: true);
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- assets/
- path: assets/vanillaOrange.png
flavors:
- vanilla
- orange
''');
expect(
buildBundleWithFlavor(
null,
logger: logger,
fileSystem: fileSystem,
platform: platform,
),
throwsToolExit(message: 'Multiple assets entries include the file '
'"assets/vanillaOrange.png", but they specify different lists of flavors.\n'
'An entry with the path "assets/" does not specify any flavors.\n'
'An entry with the path "assets/vanillaOrange.png" specifies the flavor(s): "vanilla", "orange".\n\n'
'Consider organizing assets with different flavors into different directories.'),
);
});
testWithoutContext('throws a tool exit when a flavored folder contains a flavorless asset', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('vanilla', 'flavorless.png')).createSync(recursive: true);
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- path: vanilla/
flavors:
- vanilla
- vanilla/flavorless.png
''');
expect(
buildBundleWithFlavor(
null,
logger: logger,
fileSystem: fileSystem,
platform: platform,
),
throwsToolExit(message: 'Multiple assets entries include the file '
'"vanilla/flavorless.png", but they specify different lists of flavors.\n'
'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n'
'An entry with the path "vanilla/flavorless.png" does not specify any flavors.\n\n'
'Consider organizing assets with different flavors into different directories.'),
);
});
testWithoutContext('tool exits when two file-explicit entries give the same asset different flavors', () {
final MemoryFileSystem fileSystem = MemoryFileSystem();
fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform();
fileSystem.file('.packages').createSync();
fileSystem.file('orange.png').createSync(recursive: true);
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- path: orange.png
flavors:
- orange
- path: orange.png
flavors:
- mango
''');
expect(
buildBundleWithFlavor(
null,
logger: logger,
fileSystem: fileSystem,
platform: platform,
),
throwsToolExit(message: 'Multiple assets entries include the file '
'"orange.png", but they specify different lists of flavors.\n'
'An entry with the path "orange.png" specifies the flavor(s): "orange".\n'
'An entry with the path "orange.png" specifies the flavor(s): "mango".'),
);
});
testWithoutContext('throws ToolExit when flavor from file-level declaration has different flavor from containing folder flavor declaration', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform();
fileSystem.file('.packages').createSync();
fileSystem.file(fileSystem.path.join('vanilla', 'actually-strawberry.png')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true);
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- path: vanilla/
flavors:
- vanilla
- path: vanilla/actually-strawberry.png
flavors:
- strawberry
''');
expect(
buildBundleWithFlavor(
null,
logger: logger,
fileSystem: fileSystem,
platform: platform,
),
throwsToolExit(message: 'Multiple assets entries include the file '
'"vanilla/actually-strawberry.png", but they specify different lists of flavors.\n'
'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n'
'An entry with the path "vanilla/actually-strawberry.png" '
'specifies the flavor(s): "strawberry".'),
);
});
}
...@@ -24,18 +24,16 @@ import 'package:standard_message_codec/standard_message_codec.dart'; ...@@ -24,18 +24,16 @@ import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
const String shaderLibDir = '/./shader_lib';
void main() { void main() {
group('AssetBundle.build', () { const String shaderLibDir = '/./shader_lib';
late Logger logger;
group('AssetBundle.build (using context)', () {
late FileSystem testFileSystem; late FileSystem testFileSystem;
late Platform platform; late Platform platform;
setUp(() async { setUp(() async {
testFileSystem = MemoryFileSystem(); testFileSystem = MemoryFileSystem();
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
logger = BufferLogger.test();
platform = FakePlatform(); platform = FakePlatform();
}); });
...@@ -153,30 +151,6 @@ flutter: ...@@ -153,30 +151,6 @@ flutter:
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('throws ToolExit when directory entry contains invalid characters', () async {
testFileSystem.file('.packages').createSync();
testFileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- https://mywebsite.com/images/
''');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
expect(() => bundle.build(packagesPath: '.packages'), throwsToolExit(
message: 'Unable to search for asset files in directory path "https%3A//mywebsite.com/images/". '
'Please ensure that this is valid URI that points to a directory that is '
'available on the local file system.\n'
'Error details:\n'
'Unsupported operation: Illegal character in path: https:',
));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(operatingSystem: 'windows'),
});
testUsingContext('handle removal of wildcard directories', () async { testUsingContext('handle removal of wildcard directories', () async {
globals.fs.file(globals.fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true); globals.fs.file(globals.fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true);
final File pubspec = globals.fs.file('pubspec.yaml') final File pubspec = globals.fs.file('pubspec.yaml')
...@@ -361,182 +335,100 @@ flutter: ...@@ -361,182 +335,100 @@ flutter:
Platform: () => platform, Platform: () => platform,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
});
group('flavors feature', () { group('AssetBundle.build', () {
Future<ManifestAssetBundle> buildBundleWithFlavor(String? flavor) async { testWithoutContext('throws ToolExit when directory entry contains invalid characters (Windows only)', () async {
final ManifestAssetBundle bundle = ManifestAssetBundle( final MemoryFileSystem fileSystem = MemoryFileSystem(style: FileSystemStyle.windows);
logger: logger, final BufferLogger logger = BufferLogger.test();
fileSystem: testFileSystem, final FakePlatform platform = FakePlatform(operatingSystem: 'windows');
platform: platform, final String flutterRoot = Cache.defaultFlutterRoot(
flutterRoot: Cache.defaultFlutterRoot(
platform: platform, platform: platform,
fileSystem: testFileSystem, fileSystem: fileSystem,
userMessages: UserMessages(), userMessages: UserMessages(),
),
splitDeferredAssets: true,
); );
await bundle.build( fileSystem.file('.packages').createSync();
packagesPath: '.packages', fileSystem.file('pubspec.yaml')
flutterProject: FlutterProject.fromDirectoryTest(testFileSystem.currentDirectory),
flavor: flavor,
);
return bundle;
}
testWithoutContext('correctly bundles assets given a simple asset manifest with flavors', () async {
testFileSystem.file('.packages').createSync();
testFileSystem.file(testFileSystem.path.join('assets', 'common', 'image.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('assets', 'vanilla', 'ice-cream.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('assets', 'strawberry', 'ice-cream.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('assets', 'orange', 'ice-cream.png')).createSync(recursive: true);
testFileSystem.file('pubspec.yaml')
..createSync() ..createSync()
..writeAsStringSync(r''' ..writeAsStringSync(r'''
name: example name: example
flutter: flutter:
assets: assets:
- assets/common/ - https://mywebsite.com/images/
- path: assets/vanilla/ ''');
flavors: final ManifestAssetBundle bundle = ManifestAssetBundle(
- vanilla logger: logger,
- path: assets/strawberry/ fileSystem: fileSystem,
flavors: platform: platform,
- strawberry flutterRoot: flutterRoot,
- path: assets/orange/ice-cream.png
flavors:
- orange
''');
ManifestAssetBundle bundle;
bundle = await buildBundleWithFlavor(null);
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png')));
bundle = await buildBundleWithFlavor('strawberry');
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, contains('assets/strawberry/ice-cream.png'));
expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png')));
bundle = await buildBundleWithFlavor('orange');
expect(bundle.entries.keys, contains('assets/common/image.png'));
expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png')));
expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png')));
expect(bundle.entries.keys, contains('assets/orange/ice-cream.png'));
});
testWithoutContext('throws a tool exit when a non-flavored folder contains a flavored asset', () async {
testFileSystem.file('.packages').createSync();
testFileSystem.file(testFileSystem.path.join('assets', 'unflavored.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('assets', 'vanillaOrange.png')).createSync(recursive: true);
testFileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- assets/
- path: assets/vanillaOrange.png
flavors:
- vanilla
- orange
''');
expect(
buildBundleWithFlavor(null),
throwsToolExit(message: 'Multiple assets entries include the file '
'"assets/vanillaOrange.png", but they specify different lists of flavors.\n'
'An entry with the path "assets/" does not specify any flavors.\n'
'An entry with the path "assets/vanillaOrange.png" specifies the flavor(s): "vanilla", "orange".\n\n'
'Consider organizing assets with different flavors into different directories.'),
); );
});
testWithoutContext('throws a tool exit when a flavored folder contains a flavorless asset', () async {
testFileSystem.file('.packages').createSync();
testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('vanilla', 'flavorless.png')).createSync(recursive: true);
testFileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- path: vanilla/
flavors:
- vanilla
- vanilla/flavorless.png
''');
expect( expect(
buildBundleWithFlavor(null), () => bundle.build(
throwsToolExit(message: 'Multiple assets entries include the file ' packagesPath: '.packages',
'"vanilla/flavorless.png", but they specify different lists of flavors.\n' flutterProject: FlutterProject.fromDirectoryTest(
'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' fileSystem.currentDirectory,
'An entry with the path "vanilla/flavorless.png" does not specify any flavors.\n\n' ),
'Consider organizing assets with different flavors into different directories.'), ),
throwsToolExit(
message: 'Unable to search for asset files in directory path "https%3A//mywebsite.com/images/". '
'Please ensure that this entry in pubspec.yaml is a valid file path.\n'
'Error details:\n'
'Unsupported operation: Illegal character in path: https:',
),
); );
}); });
testWithoutContext('tool exits when two file-explicit entries give the same asset different flavors', () { testWithoutContext('throws ToolExit when file entry contains invalid characters (Windows only)', () async {
testFileSystem.file('.packages').createSync(); final FileSystem fileSystem = MemoryFileSystem(
testFileSystem.file('orange.png').createSync(recursive: true); style: FileSystemStyle.windows,
testFileSystem.file('pubspec.yaml') opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.exists && context == r'C:\http:\\website.com') {
throw const FileSystemException(
r"FileSystemException: Exists failed, path = 'C:\http:\\website.com' "
'(OS Error: The filename, directory name, or volume label syntax is '
'incorrect., errno = 123)',
);
}
},
);
final BufferLogger logger = BufferLogger.test();
final FakePlatform platform = FakePlatform(operatingSystem: 'windows');
final String flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fileSystem,
userMessages: UserMessages(),
);
fileSystem.file('.packages').createSync();
fileSystem.file('pubspec.yaml')
..createSync() ..createSync()
..writeAsStringSync(r''' ..writeAsStringSync(r'''
name: example name: example
flutter: flutter:
assets: assets:
- path: orange.png - http://website.com/hi.png
flavors: ''');
- orange final ManifestAssetBundle bundle = ManifestAssetBundle(
- path: orange.png logger: logger,
flavors: fileSystem: fileSystem,
- mango platform: platform,
'''); flutterRoot: flutterRoot,
expect(
buildBundleWithFlavor(null),
throwsToolExit(message: 'Multiple assets entries include the file '
'"orange.png", but they specify different lists of flavors.\n'
'An entry with the path "orange.png" specifies the flavor(s): "orange".\n'
'An entry with the path "orange.png" specifies the flavor(s): "mango".'),
); );
});
testWithoutContext('throws ToolExit when flavor from file-level declaration has different flavor from containing folder flavor declaration', () async {
testFileSystem.file('.packages').createSync();
testFileSystem.file(testFileSystem.path.join('vanilla', 'actually-strawberry.png')).createSync(recursive: true);
testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true);
testFileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
assets:
- path: vanilla/
flavors:
- vanilla
- path: vanilla/actually-strawberry.png
flavors:
- strawberry
''');
expect( expect(
buildBundleWithFlavor(null), () => bundle.build(
throwsToolExit(message: 'Multiple assets entries include the file ' packagesPath: '.packages',
'"vanilla/actually-strawberry.png", but they specify different lists of flavors.\n' flutterProject: FlutterProject.fromDirectoryTest(
'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' fileSystem.currentDirectory,
'An entry with the path "vanilla/actually-strawberry.png" ' ),
'specifies the flavor(s): "strawberry".'), ),
throwsToolExit(
message: 'Unable to check the existence of asset file ',
),
); );
}); });
}); });
});
group('AssetBundle.build (web builds)', () { group('AssetBundle.build (web builds)', () {
late FileSystem testFileSystem; late FileSystem testFileSystem;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment