Unverified Commit ee12d7c3 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] check for permission issues when copying file (#63540)

parent 39be8a40
......@@ -194,6 +194,17 @@ class ErrorHandlingFile
);
}
@override
RandomAccessFile openSync({FileMode mode = FileMode.read}) {
return _runSync<RandomAccessFile>(
() => delegate.openSync(
mode: mode,
),
platform: _platform,
failureMessage: 'Flutter failed to open a file at "${delegate.path}"',
);
}
@override
String toString() => delegate.toString();
}
......@@ -385,9 +396,17 @@ void _handleWindowsException(FileSystemException e, String message) {
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
const int kAccessDenied = 5;
final int errorCode = e.osError?.errorCode ?? 0;
// Catch errors and bail when:
switch (errorCode) {
case kAccessDenied:
throwToolExit(
'$message. The flutter tool cannot access the file.\n'
'Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.'
);
break;
case kDeviceFull:
throwToolExit(
'$message. The target device is full.'
......
......@@ -32,18 +32,19 @@ import '../runner/flutter_command.dart';
import '../template.dart';
const List<String> _kAvailablePlatforms = <String>[
'ios',
'android',
'windows',
'linux',
'macos',
'web',
];
'ios',
'android',
'windows',
'linux',
'macos',
'web',
];
const String _kNoPlatformsErrorMessage = '''
The plugin project was generated without specifying the `--platforms` flag, no new platforms are added.
To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same
directory. You can also find detailed instructions on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
directory. You can also find detailed instructions on how to add platforms in the `pubspec.yaml`
at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
''';
class CreateCommand extends FlutterCommand {
......@@ -790,7 +791,10 @@ https://flutter.dev/docs/development/packages-and-plugins/developing-packages#pl
final Template template = await Template.fromName(
templateName,
fileSystem: globals.fs,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
templateManifest: templateManifest,
pub: pub,
);
return template.render(directory, context, overwriteExisting: overwrite);
}
......
......@@ -254,6 +254,8 @@ class IdeConfigCommand extends FlutterCommand {
null,
fileSystem: globals.fs,
templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
);
return template.render(
globals.fs.directory(dirPath),
......
......@@ -15,6 +15,7 @@ import 'base/file_system.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'bundle.dart' as bundle;
import 'dart/pub.dart';
import 'features.dart';
import 'flutter_manifest.dart';
import 'globals.dart' as globals;
......@@ -657,7 +658,14 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject {
}
Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(path, fileSystem: globals.fs, templateManifest: null);
final Template template = await Template.fromName(
path,
fileSystem: globals.fs,
templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
pub: pub,
);
template.render(
target,
<String, dynamic>{
......@@ -810,7 +818,14 @@ to migrate your project.
}
Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(path, fileSystem: globals.fs, templateManifest: null);
final Template template = await Template.fromName(
path,
fileSystem: globals.fs,
templateManifest: null,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
pub: pub,
);
template.render(
target,
<String, dynamic>{
......
......@@ -8,10 +8,11 @@ import 'package:package_config/package_config_types.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/template.dart';
import 'cache.dart';
import 'dart/package_map.dart';
import 'dart/pub.dart';
import 'globals.dart' as globals hide fs;
/// Expands templates in a directory to a destination. All files that must
/// undergo template expansion should end with the '.tmpl' extension. All files
......@@ -32,8 +33,12 @@ import 'globals.dart' as globals hide fs;
class Template {
Template(Directory templateSource, Directory baseDir, this.imageSourceDir, {
@required FileSystem fileSystem,
@required Logger logger,
@required TemplateRenderer templateRenderer,
@required Set<Uri> templateManifest,
}) : _fileSystem = fileSystem,
_logger = logger,
_templateRenderer = templateRenderer,
_templateManifest = templateManifest {
_templateFilePaths = <String, String>{};
......@@ -42,14 +47,9 @@ class Template {
}
final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true);
for (final FileSystemEntity entity in templateFiles) {
if (entity is! File) {
// We are only interesting in template *file* URIs.
continue;
}
for (final FileSystemEntity entity in templateFiles.whereType<File>()) {
if (_templateManifest != null && !_templateManifest.contains(Uri.file(entity.absolute.path))) {
globals.logger.printTrace('Skipping ${entity.absolute.path}, missing from the template manifest.');
_logger.printTrace('Skipping ${entity.absolute.path}, missing from the template manifest.');
// Skip stale files in the flutter_tools directory.
continue;
}
......@@ -68,20 +68,28 @@ class Template {
static Future<Template> fromName(String name, {
@required FileSystem fileSystem,
@required Set<Uri> templateManifest,
@required Logger logger,
@required TemplateRenderer templateRenderer,
@required Pub pub,
}) async {
// All named templates are placed in the 'templates' directory
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
final Directory imageDir = await _templateImageDirectory(name, fileSystem);
final Directory imageDir = await _templateImageDirectory(name, fileSystem, logger, pub);
return Template(
templateDir,
templateDir, imageDir,
fileSystem: fileSystem,
logger: logger,
templateRenderer: templateRenderer,
templateManifest: templateManifest,
);
}
final FileSystem _fileSystem;
final Logger _logger;
final Set<Uri> _templateManifest;
final TemplateRenderer _templateRenderer;
static const String templateExtension = '.tmpl';
static const String copyTemplateExtension = '.copy.tmpl';
static const String imageTemplateExtension = '.img.tmpl';
......@@ -102,7 +110,7 @@ class Template {
try {
destination.createSync(recursive: true);
} on FileSystemException catch (err) {
globals.printError(err.toString());
_logger.printError(err.toString());
throwToolExit('Failed to flutter create at ${destination.path}.');
return 0;
}
......@@ -198,22 +206,22 @@ class Template {
if (overwriteExisting) {
finalDestinationFile.deleteSync(recursive: true);
if (printStatusWhenWriting) {
globals.printStatus(' $relativePathForLogging (overwritten)');
_logger.printStatus(' $relativePathForLogging (overwritten)');
}
} else {
// The file exists but we cannot overwrite it, move on.
if (printStatusWhenWriting) {
globals.printTrace(' $relativePathForLogging (existing - skipped)');
_logger.printTrace(' $relativePathForLogging (existing - skipped)');
}
return;
}
} else {
if (printStatusWhenWriting) {
globals.printStatus(' $relativePathForLogging (created)');
_logger.printStatus(' $relativePathForLogging (created)');
}
}
fileCount++;
fileCount += 1;
finalDestinationFile.createSync(recursive: true);
final File sourceFile = _fileSystem.file(absoluteSourcePath);
......@@ -222,6 +230,7 @@ class Template {
// not need mustache rendering but needs to be directly copied.
if (sourceFile.path.endsWith(copyTemplateExtension)) {
_validateReadPermissions(sourceFile);
sourceFile.copySync(finalDestinationFile.path);
return;
......@@ -233,6 +242,7 @@ class Template {
if (sourceFile.path.endsWith(imageTemplateExtension)) {
final File imageSourceFile = _fileSystem.file(_fileSystem.path.join(
imageSourceDir.path, relativeDestinationPath.replaceAll(imageTemplateExtension, '')));
_validateReadPermissions(imageSourceFile);
imageSourceFile.copySync(finalDestinationFile.path);
return;
......@@ -242,8 +252,9 @@ class Template {
// rendering via mustache.
if (sourceFile.path.endsWith(templateExtension)) {
_validateReadPermissions(sourceFile);
final String templateContents = sourceFile.readAsStringSync();
final String renderedContents = globals.templateRenderer.renderString(templateContents, context);
final String renderedContents = _templateRenderer.renderString(templateContents, context);
finalDestinationFile.writeAsStringSync(renderedContents);
......@@ -252,12 +263,20 @@ class Template {
// Step 5: This file does not end in .tmpl but is in a directory that
// does. Directly copy the file to the destination.
_validateReadPermissions(sourceFile);
sourceFile.copySync(finalDestinationFile.path);
});
return fileCount;
}
/// Attempt open/close the file to ensure that read permissions are correct.
///
/// If this fails with a certain error code, the [ErrorHandlingFileSystem] will
/// trigger a tool exit with a better message.
void _validateReadPermissions(File file) {
file.openSync().closeSync();
}
}
Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
......@@ -268,25 +287,25 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
// Returns the directory containing the 'name' template directory in
// flutter_template_images, to resolve image placeholder against.
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) async {
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Logger logger, Pub pub) async {
final String toolPackagePath = fileSystem.path.join(
Cache.flutterRoot, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName);
// Ensure that .packgaes is present.
if (!fileSystem.file(packageFilePath).existsSync()) {
await _ensurePackageDependencies(toolPackagePath);
await _ensurePackageDependencies(toolPackagePath, pub);
}
PackageConfig packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath),
logger: globals.logger,
logger: logger,
);
Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
// Ensure that the template image package is present.
if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) {
await _ensurePackageDependencies(toolPackagePath);
await _ensurePackageDependencies(toolPackagePath, pub);
packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath),
logger: globals.logger,
logger: logger,
);
imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
}
......@@ -298,7 +317,7 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) as
// Runs 'pub get' for the given path to ensure that .packages is created and
// all dependencies are present.
Future<void> _ensurePackageDependencies(String packagePath) async {
Future<void> _ensurePackageDependencies(String packagePath, Pub pub) async {
await pub.get(
context: PubContext.pubGet,
directory: packagePath,
......
......@@ -59,6 +59,9 @@ void setupWriteMocks({
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.openSync(
mode: anyNamed('mode'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void setupCreateTempMocks({
......@@ -79,6 +82,7 @@ void main() {
group('throws ToolExit on Windows', () {
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
const int kUserPermissionDenied = 5;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
......@@ -91,6 +95,28 @@ void main() {
when(mockFileSystem.path).thenReturn(MockPathContext());
});
testWithoutContext('when access is denied', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final File file = fs.file('file');
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
expect(() => file.openSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when writing to a full device', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
......
......@@ -389,3 +389,24 @@ class TestFlutterCommandRunner extends FlutterCommandRunner {
);
}
}
/// A file system that allows preconfiguring certain entities.
///
/// This is useful for inserting mocks/entities which throw errors or
/// have other behavior that is not easily configured through the
/// filesystem interface.
class ConfiguredFileSystem extends ForwardingFileSystem {
ConfiguredFileSystem(FileSystem delegate, {@required this.entities}) : super(delegate);
final Map<String, FileSystemEntity> entities;
@override
File file(dynamic path) {
return (entities[path] as File) ?? super.file(path);
}
@override
Directory directory(dynamic path) {
return (entities[path] as Directory) ?? super.directory(path);
}
}
......@@ -3,29 +3,26 @@
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/template.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/template.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart';
import 'src/common.dart';
import 'src/testbed.dart';
void main() {
Testbed testbed;
setUp(() {
testbed = Testbed();
});
test('Template.render throws ToolExit when FileSystem exception is raised', () => testbed.run(() {
testWithoutContext('Template.render throws ToolExit when FileSystem exception is raised', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final Template template = Template(
globals.fs.directory('examples'),
globals.fs.currentDirectory,
fileSystem.directory('examples'),
fileSystem.currentDirectory,
null,
fileSystem: globals.fs,
fileSystem: fileSystem,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
templateManifest: null,
);
final MockDirectory mockDirectory = MockDirectory();
......@@ -33,9 +30,33 @@ void main() {
expect(() => template.render(mockDirectory, <String, Object>{}),
throwsToolExit());
}));
});
test('Template.render replaces .img.tmpl files with files from the image source', () => testbed.run(() {
testWithoutContext('Template.render attempts to read byte from template file before copying', () {
final MemoryFileSystem baseFileSystem = MemoryFileSystem.test();
baseFileSystem.file('templates/foo.copy.tmpl').createSync(recursive: true);
final ConfiguredFileSystem fileSystem = ConfiguredFileSystem(
baseFileSystem,
entities: <String, FileSystemEntity>{
'/templates/foo.copy.tmpl': FakeFile('/templates/foo.copy.tmpl'),
},
);
final Template template = Template(
fileSystem.directory('templates'),
fileSystem.currentDirectory,
null,
fileSystem: fileSystem,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
templateManifest: null,
);
expect(() => template.render(fileSystem.directory('out'), <String, Object>{}),
throwsA(isA<FileSystemException>()));
});
testWithoutContext('Template.render replaces .img.tmpl files with files from the image source', () {
final MemoryFileSystem fileSystem = MemoryFileSystem();
final Directory templateDir = fileSystem.directory('templates');
final Directory imageSourceDir = fileSystem.directory('template_images');
......@@ -52,43 +73,77 @@ void main() {
imageSourceDir,
fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
);
template.render(destination, <String, Object>{});
final File destinationImage = destination.childFile(imageName);
expect(destinationImage.existsSync(), true);
expect(destinationImage, exists);
expect(destinationImage.readAsBytesSync(), equals(sourceImage.readAsBytesSync()));
}));
});
test('Template.fromName runs pub get if .packages is missing', () => testbed.run(() async {
testWithoutContext('Template.fromName runs pub get if .packages is missing', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
when(pub.get(
context: PubContext.pubGet,
directory: anyNamed('directory'),
generateSyntheticPackage: false,
)).thenThrow(UnsupportedError(''));
Cache.flutterRoot = '/flutter';
// Attempting to run pub in a test throws.
await expectLater(Template.fromName('app', fileSystem: fileSystem, templateManifest: null),
throwsUnsupportedError);
}));
await expectLater(Template.fromName(
'app',
fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
pub: pub,
templateRenderer: FakeTemplateRenderer(),
),
throwsUnsupportedError,
);
});
test('Template.fromName runs pub get if .packages is missing flutter_template_images', () => testbed.run(() async {
testWithoutContext('Template.fromName runs pub get if .packages is missing flutter_template_images', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
when(pub.get(
context: PubContext.pubGet,
directory: anyNamed('directory'),
generateSyntheticPackage: false,
)).thenThrow(UnsupportedError(''));
Cache.flutterRoot = '/flutter';
final File packagesFile = fileSystem.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childFile('.packages');
.childDirectory('packages')
.childDirectory('flutter_tools')
.childFile('.packages');
packagesFile.createSync(recursive: true);
// Attempting to run pub in a test throws.
await expectLater(Template.fromName('app', fileSystem: fileSystem, templateManifest: null),
throwsUnsupportedError);
}));
await expectLater(
Template.fromName(
'app',
fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
pub: pub,
templateRenderer: FakeTemplateRenderer(),
),
throwsUnsupportedError,
);
});
test('Template.fromName runs pub get if flutter_template_images directory is missing', () => testbed.run(() async {
testWithoutContext('Template.fromName runs pub get if flutter_template_images directory is missing', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem();
final MockPub pub = MockPub();
Cache.flutterRoot = '/flutter';
final File packagesFile = fileSystem.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childFile('.packages');
.childDirectory('packages')
.childDirectory('flutter_tools')
.childFile('.packages');
packagesFile.createSync(recursive: true);
packagesFile.writeAsStringSync('\n');
......@@ -101,11 +156,35 @@ void main() {
packagesFile.writeAsStringSync('flutter_template_images:file:///flutter_template_images');
});
await Template.fromName('app', fileSystem: fileSystem, templateManifest: null);
}, overrides: <Type, Generator>{
Pub: () => MockPub(),
}));
await Template.fromName(
'app',
fileSystem: fileSystem,
templateManifest: null,
logger: BufferLogger.test(),
templateRenderer: FakeTemplateRenderer(),
pub: pub,
);
});
}
class MockPub extends Mock implements Pub {}
class MockDirectory extends Mock implements Directory {}
class FakeFile extends Fake implements File {
FakeFile(this.path);
@override
final String path;
@override
int lengthSync() {
throw const FileSystemException('', '', OSError('', 5));
}
}
class FakeTemplateRenderer extends TemplateRenderer {
@override
String renderString(String template, dynamic context, {bool htmlEscapeValues = false}) {
return '';
}
}
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