| // 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. | |
| // ignore_for_file: avoid_slow_async_io | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io' as io; | |
| import 'dart:typed_data'; | |
| import 'package:path/path.dart' as path; // flutter_ignore: package_path_import | |
| bool _isDangerousDirectory(String dirPath) { | |
| final String canonical = path.canonicalize(dirPath); | |
| // Check if it is root | |
| if (canonical == '/' || canonical == r'C:\' || (io.Platform.isWindows && canonical.length <= 3)) { | |
| return true; | |
| } | |
| // Check if it is home or parent of home | |
| final String? home = io.Platform.environment['HOME'] ?? io.Platform.environment['USERPROFILE']; | |
| if (home != null) { | |
| final String canonicalHome = path.canonicalize(home); | |
| final String canonicalHomeParent = path.dirname(canonicalHome); | |
| if (canonical == canonicalHome || canonical == canonicalHomeParent) { | |
| return true; | |
| } | |
| // Check if it is Desktop | |
| final String desktop = path.join(canonicalHome, 'Desktop'); | |
| final String canonicalDesktop = path.canonicalize(desktop); | |
| if (canonical == canonicalDesktop) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Canonicalizes [p] and normalizes standard root symlink aliases on macOS. | |
| /// | |
| /// On macOS (Darwin), system root directories `/var`, `/tmp`, and `/etc` are | |
| /// symlinks to `/private/var`, `/private/tmp`, and `/private/etc` respectively | |
| /// (with Darwin per-user `$TMPDIR` located under `/var/folders/...`). | |
| /// | |
| /// Normalizing these prefixes at the string level ensures consistent prefix | |
| /// matching against the resolved temporary directory without performing costly | |
| /// filesystem syscalls (`resolveSymbolicLinksSync`) or failing when validating | |
| /// paths to files/directories that do not exist yet. | |
| String _canonicalize(String p) { | |
| final String canonical = path.canonicalize(p); | |
| if (io.Platform.isMacOS) { | |
| if (canonical.startsWith('/var/') || canonical == '/var') { | |
| return '/private$canonical'; | |
| } | |
| if (canonical.startsWith('/tmp/') || canonical == '/tmp') { | |
| return '/private$canonical'; | |
| } | |
| if (canonical.startsWith('/etc/') || canonical == '/etc') { | |
| return '/private$canonical'; | |
| } | |
| } | |
| return canonical; | |
| } | |
| bool _isAllowedPath(String entityPath) { | |
| final String canonicalEntity = _canonicalize(entityPath); | |
| // Allow system temp | |
| String canonicalTemp; | |
| final io.IOOverrides? currentOverrides = io.IOOverrides.current; | |
| if (currentOverrides is FSGuardIOOverrides) { | |
| canonicalTemp = currentOverrides._canonicalSystemTemp; | |
| } else { | |
| canonicalTemp = _canonicalize(io.Directory.systemTemp.path); | |
| } | |
| if (path.isWithin(canonicalTemp, canonicalEntity) || canonicalEntity == canonicalTemp) { | |
| return true; | |
| } | |
| // Allow modifications inside the Flutter installation root itself | |
| final String? flutterRoot = io.Platform.environment['FLUTTER_ROOT']; | |
| if (flutterRoot != null) { | |
| final String canonicalRoot = path.canonicalize(flutterRoot); | |
| if (!_isDangerousDirectory(canonicalRoot)) { | |
| if (path.isWithin(canonicalRoot, canonicalEntity) || canonicalEntity == canonicalRoot) { | |
| return true; | |
| } | |
| } | |
| } | |
| // Allow modifications inside the directory specified by FLUTTER_TEST_OUTPUTS_DIR | |
| final String? testOutputsDir = io.Platform.environment['FLUTTER_TEST_OUTPUTS_DIR']; | |
| if (testOutputsDir != null) { | |
| final String canonicalOutputs = path.canonicalize(testOutputsDir); | |
| if (!_isDangerousDirectory(canonicalOutputs)) { | |
| if (path.isWithin(canonicalOutputs, canonicalEntity) || canonicalEntity == canonicalOutputs) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool _isGuardDisabled() { | |
| return io.Platform.environment['FLUTTER_TEST_DISABLE_FS_GUARD'] == 'true'; | |
| } | |
| void _checkPath(String targetPath, String entityType, String operation) { | |
| if (_isGuardDisabled()) { | |
| return; | |
| } | |
| if (!_isAllowedPath(targetPath)) { | |
| throw io.FileSystemException( | |
| 'Test attempted to $operation $entityType outside of temp directory: $targetPath. ' | |
| 'This check prevents tests from causing data loss or modifying non-test data on the system. ' | |
| 'To bypass this safety check during local debugging (e.g., to output logs outside the temp directory), ' | |
| 'set the environment variable FLUTTER_TEST_DISABLE_FS_GUARD=true in your shell or IDE.', | |
| targetPath, | |
| ); | |
| } | |
| } | |
| /// A wrapper around [io.File] that prevents modifying operations on paths outside | |
| /// of the system temporary directory during test execution. | |
| class GuardedFile implements io.File { | |
| GuardedFile(this._delegate); | |
| final io.File _delegate; | |
| void _checkModify() { | |
| _checkPath(_delegate.path, 'file', 'modify'); | |
| } | |
| @override | |
| String get path => _delegate.path; | |
| @override | |
| Uri get uri => _delegate.uri; | |
| @override | |
| Future<bool> exists() => _delegate.exists(); | |
| @override | |
| bool existsSync() => _delegate.existsSync(); | |
| @override | |
| Future<io.FileStat> stat() => _delegate.stat(); | |
| @override | |
| io.FileStat statSync() => _delegate.statSync(); | |
| @override | |
| Future<String> resolveSymbolicLinks() => _delegate.resolveSymbolicLinks(); | |
| @override | |
| String resolveSymbolicLinksSync() => _delegate.resolveSymbolicLinksSync(); | |
| @override | |
| io.Directory get parent => GuardedDirectory(_delegate.parent); | |
| @override | |
| io.File get absolute => GuardedFile(_delegate.absolute); | |
| @override | |
| bool get isAbsolute => _delegate.isAbsolute; | |
| @override | |
| Future<int> length() => _delegate.length(); | |
| @override | |
| int lengthSync() => _delegate.lengthSync(); | |
| @override | |
| Future<DateTime> lastAccessed() => _delegate.lastAccessed(); | |
| @override | |
| DateTime lastAccessedSync() => _delegate.lastAccessedSync(); | |
| @override | |
| Future<DateTime> lastModified() => _delegate.lastModified(); | |
| @override | |
| DateTime lastModifiedSync() => _delegate.lastModifiedSync(); | |
| @override | |
| Stream<List<int>> openRead([int? start, int? end]) => _delegate.openRead(start, end); | |
| @override | |
| Future<Uint8List> readAsBytes() => _delegate.readAsBytes(); | |
| @override | |
| Uint8List readAsBytesSync() => _delegate.readAsBytesSync(); | |
| @override | |
| Future<String> readAsString({Encoding encoding = utf8}) => | |
| _delegate.readAsString(encoding: encoding); | |
| @override | |
| String readAsStringSync({Encoding encoding = utf8}) => | |
| _delegate.readAsStringSync(encoding: encoding); | |
| @override | |
| Future<List<String>> readAsLines({Encoding encoding = utf8}) => | |
| _delegate.readAsLines(encoding: encoding); | |
| @override | |
| List<String> readAsLinesSync({Encoding encoding = utf8}) => | |
| _delegate.readAsLinesSync(encoding: encoding); | |
| @override | |
| Stream<io.FileSystemEvent> watch({int events = io.FileSystemEvent.all, bool recursive = false}) => | |
| _delegate.watch(events: events, recursive: recursive); | |
| @override | |
| Future<io.File> create({bool recursive = false, bool exclusive = false}) { | |
| _checkModify(); | |
| return _delegate.create(recursive: recursive, exclusive: exclusive).then((f) => GuardedFile(f)); | |
| } | |
| @override | |
| void createSync({bool recursive = false, bool exclusive = false}) { | |
| _checkModify(); | |
| _delegate.createSync(recursive: recursive, exclusive: exclusive); | |
| } | |
| @override | |
| Future<io.File> rename(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'file', 'rename'); | |
| return _delegate.rename(newPath).then(GuardedFile.new); | |
| } | |
| @override | |
| io.File renameSync(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'file', 'rename'); | |
| return GuardedFile(_delegate.renameSync(newPath)); | |
| } | |
| @override | |
| Future<io.FileSystemEntity> delete({bool recursive = false}) { | |
| _checkModify(); | |
| return _delegate.delete(recursive: recursive); | |
| } | |
| @override | |
| void deleteSync({bool recursive = false}) { | |
| _checkModify(); | |
| _delegate.deleteSync(recursive: recursive); | |
| } | |
| @override | |
| Future<io.File> copy(String newPath) { | |
| _checkPath(newPath, 'file', 'copy'); | |
| return _delegate.copy(newPath).then(GuardedFile.new); | |
| } | |
| @override | |
| io.File copySync(String newPath) { | |
| _checkPath(newPath, 'file', 'copy'); | |
| return GuardedFile(_delegate.copySync(newPath)); | |
| } | |
| @override | |
| Future<void> setLastAccessed(DateTime time) { | |
| _checkModify(); | |
| return _delegate.setLastAccessed(time); | |
| } | |
| @override | |
| void setLastAccessedSync(DateTime time) { | |
| _checkModify(); | |
| _delegate.setLastAccessedSync(time); | |
| } | |
| @override | |
| Future<void> setLastModified(DateTime time) { | |
| _checkModify(); | |
| return _delegate.setLastModified(time); | |
| } | |
| @override | |
| void setLastModifiedSync(DateTime time) { | |
| _checkModify(); | |
| _delegate.setLastModifiedSync(time); | |
| } | |
| @override | |
| Future<io.RandomAccessFile> open({io.FileMode mode = io.FileMode.read}) { | |
| if (mode != io.FileMode.read) { | |
| _checkModify(); | |
| } | |
| return _delegate.open(mode: mode); | |
| } | |
| @override | |
| io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) { | |
| if (mode != io.FileMode.read) { | |
| _checkModify(); | |
| } | |
| return _delegate.openSync(mode: mode); | |
| } | |
| @override | |
| io.IOSink openWrite({io.FileMode mode = io.FileMode.write, Encoding encoding = utf8}) { | |
| _checkModify(); | |
| return _delegate.openWrite(mode: mode, encoding: encoding); | |
| } | |
| @override | |
| Future<io.File> writeAsBytes( | |
| List<int> bytes, { | |
| io.FileMode mode = io.FileMode.write, | |
| bool flush = false, | |
| }) { | |
| _checkModify(); | |
| return _delegate.writeAsBytes(bytes, mode: mode, flush: flush).then((f) => GuardedFile(f)); | |
| } | |
| @override | |
| void writeAsBytesSync( | |
| List<int> bytes, { | |
| io.FileMode mode = io.FileMode.write, | |
| bool flush = false, | |
| }) { | |
| _checkModify(); | |
| _delegate.writeAsBytesSync(bytes, mode: mode, flush: flush); | |
| } | |
| @override | |
| Future<io.File> writeAsString( | |
| String contents, { | |
| io.FileMode mode = io.FileMode.write, | |
| Encoding encoding = utf8, | |
| bool flush = false, | |
| }) { | |
| _checkModify(); | |
| return _delegate | |
| .writeAsString(contents, mode: mode, encoding: encoding, flush: flush) | |
| .then((f) => GuardedFile(f)); | |
| } | |
| @override | |
| void writeAsStringSync( | |
| String contents, { | |
| io.FileMode mode = io.FileMode.write, | |
| Encoding encoding = utf8, | |
| bool flush = false, | |
| }) { | |
| _checkModify(); | |
| _delegate.writeAsStringSync(contents, mode: mode, encoding: encoding, flush: flush); | |
| } | |
| } | |
| /// A wrapper around [io.Directory] that prevents modifying operations on paths outside | |
| /// of the system temporary directory during test execution. | |
| class GuardedDirectory implements io.Directory { | |
| GuardedDirectory(this._delegate); | |
| final io.Directory _delegate; | |
| void _checkModify() { | |
| _checkPath(_delegate.path, 'directory', 'modify'); | |
| } | |
| @override | |
| String get path => _delegate.path; | |
| @override | |
| Uri get uri => _delegate.uri; | |
| @override | |
| Future<bool> exists() => _delegate.exists(); | |
| @override | |
| bool existsSync() => _delegate.existsSync(); | |
| @override | |
| Future<io.FileStat> stat() => _delegate.stat(); | |
| @override | |
| io.FileStat statSync() => _delegate.statSync(); | |
| @override | |
| Future<String> resolveSymbolicLinks() => _delegate.resolveSymbolicLinks(); | |
| @override | |
| String resolveSymbolicLinksSync() => _delegate.resolveSymbolicLinksSync(); | |
| @override | |
| io.Directory get parent => GuardedDirectory(_delegate.parent); | |
| @override | |
| io.Directory get absolute => GuardedDirectory(_delegate.absolute); | |
| @override | |
| bool get isAbsolute => _delegate.isAbsolute; | |
| @override | |
| Stream<io.FileSystemEvent> watch({int events = io.FileSystemEvent.all, bool recursive = false}) => | |
| _delegate.watch(events: events, recursive: recursive); | |
| @override | |
| Stream<io.FileSystemEntity> list({bool recursive = false, bool followLinks = true}) { | |
| return _delegate.list(recursive: recursive, followLinks: followLinks).map((entity) { | |
| if (entity is io.File) { | |
| return GuardedFile(entity); | |
| } | |
| if (entity is io.Directory) { | |
| return GuardedDirectory(entity); | |
| } | |
| if (entity is io.Link) { | |
| return GuardedLink(entity); | |
| } | |
| return entity; | |
| }); | |
| } | |
| @override | |
| List<io.FileSystemEntity> listSync({bool recursive = false, bool followLinks = true}) { | |
| return _delegate.listSync(recursive: recursive, followLinks: followLinks).map((entity) { | |
| if (entity is io.File) { | |
| return GuardedFile(entity); | |
| } | |
| if (entity is io.Directory) { | |
| return GuardedDirectory(entity); | |
| } | |
| if (entity is io.Link) { | |
| return GuardedLink(entity); | |
| } | |
| return entity; | |
| }).toList(); | |
| } | |
| @override | |
| Future<io.Directory> create({bool recursive = false}) { | |
| _checkModify(); | |
| return _delegate.create(recursive: recursive).then((d) => GuardedDirectory(d)); | |
| } | |
| @override | |
| void createSync({bool recursive = false}) { | |
| _checkModify(); | |
| _delegate.createSync(recursive: recursive); | |
| } | |
| @override | |
| Future<io.Directory> createTemp([String? prefix]) { | |
| _checkModify(); | |
| return _delegate.createTemp(prefix).then((d) => GuardedDirectory(d)); | |
| } | |
| @override | |
| io.Directory createTempSync([String? prefix]) { | |
| _checkModify(); | |
| return GuardedDirectory(_delegate.createTempSync(prefix)); | |
| } | |
| @override | |
| Future<io.Directory> rename(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'directory', 'rename'); | |
| return _delegate.rename(newPath).then(GuardedDirectory.new); | |
| } | |
| @override | |
| io.Directory renameSync(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'directory', 'rename'); | |
| return GuardedDirectory(_delegate.renameSync(newPath)); | |
| } | |
| @override | |
| Future<io.FileSystemEntity> delete({bool recursive = false}) { | |
| _checkModify(); | |
| return _delegate.delete(recursive: recursive); | |
| } | |
| @override | |
| void deleteSync({bool recursive = false}) { | |
| _checkModify(); | |
| _delegate.deleteSync(recursive: recursive); | |
| } | |
| } | |
| /// A wrapper around [io.Link] that prevents modifying operations on paths outside | |
| /// of the system temporary directory during test execution. | |
| class GuardedLink implements io.Link { | |
| GuardedLink(this._delegate); | |
| final io.Link _delegate; | |
| void _checkModify() { | |
| _checkPath(_delegate.path, 'link', 'modify'); | |
| } | |
| @override | |
| String get path => _delegate.path; | |
| @override | |
| Uri get uri => _delegate.uri; | |
| @override | |
| Future<bool> exists() => _delegate.exists(); | |
| @override | |
| bool existsSync() => _delegate.existsSync(); | |
| @override | |
| Future<io.FileStat> stat() => _delegate.stat(); | |
| @override | |
| io.FileStat statSync() => _delegate.statSync(); | |
| @override | |
| Future<String> resolveSymbolicLinks() => _delegate.resolveSymbolicLinks(); | |
| @override | |
| String resolveSymbolicLinksSync() => _delegate.resolveSymbolicLinksSync(); | |
| @override | |
| io.Directory get parent => GuardedDirectory(_delegate.parent); | |
| @override | |
| io.Link get absolute => GuardedLink(_delegate.absolute); | |
| @override | |
| bool get isAbsolute => _delegate.isAbsolute; | |
| @override | |
| Stream<io.FileSystemEvent> watch({int events = io.FileSystemEvent.all, bool recursive = false}) => | |
| _delegate.watch(events: events, recursive: recursive); | |
| @override | |
| Future<io.Link> create(String target, {bool recursive = false}) { | |
| _checkModify(); | |
| return _delegate.create(target, recursive: recursive).then((l) => GuardedLink(l)); | |
| } | |
| @override | |
| void createSync(String target, {bool recursive = false}) { | |
| _checkModify(); | |
| _delegate.createSync(target, recursive: recursive); | |
| } | |
| @override | |
| Future<io.Link> update(String target) { | |
| _checkModify(); | |
| return _delegate.update(target).then((l) => GuardedLink(l)); | |
| } | |
| @override | |
| void updateSync(String target) { | |
| _checkModify(); | |
| _delegate.updateSync(target); | |
| } | |
| @override | |
| Future<String> target() => _delegate.target(); | |
| @override | |
| String targetSync() => _delegate.targetSync(); | |
| @override | |
| Future<io.Link> rename(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'link', 'rename'); | |
| return _delegate.rename(newPath).then(GuardedLink.new); | |
| } | |
| @override | |
| io.Link renameSync(String newPath) { | |
| _checkModify(); | |
| _checkPath(newPath, 'link', 'rename'); | |
| return GuardedLink(_delegate.renameSync(newPath)); | |
| } | |
| @override | |
| Future<io.FileSystemEntity> delete({bool recursive = false}) { | |
| _checkModify(); | |
| return _delegate.delete(recursive: recursive); | |
| } | |
| @override | |
| void deleteSync({bool recursive = false}) { | |
| _checkModify(); | |
| _delegate.deleteSync(recursive: recursive); | |
| } | |
| } | |
| /// A custom [io.IOOverrides] class that ensures all file system operations performed | |
| /// by tests are isolated to the system temporary directory by returning guarded | |
| /// wrappers ([GuardedFile], [GuardedDirectory], [GuardedLink]). | |
| final class FSGuardIOOverrides extends io.IOOverrides { | |
| FSGuardIOOverrides() : _parent = io.IOOverrides.current; | |
| final io.IOOverrides? _parent; | |
| late final String _canonicalSystemTemp = () { | |
| final io.Directory rawTemp = _parent != null | |
| ? _parent.getSystemTempDirectory() | |
| : super.getSystemTempDirectory(); | |
| try { | |
| return _canonicalize(rawTemp.resolveSymbolicLinksSync()); | |
| } on Object catch (_) { | |
| return _canonicalize(rawTemp.path); | |
| } | |
| }(); | |
| @override | |
| io.File createFile(String path) { | |
| final io.File rawFile = _parent != null ? _parent.createFile(path) : super.createFile(path); | |
| return GuardedFile(rawFile); | |
| } | |
| @override | |
| io.Directory createDirectory(String path) { | |
| final io.Directory rawDir = _parent != null | |
| ? _parent.createDirectory(path) | |
| : super.createDirectory(path); | |
| return GuardedDirectory(rawDir); | |
| } | |
| @override | |
| io.Link createLink(String path) { | |
| final io.Link rawLink = _parent != null ? _parent.createLink(path) : super.createLink(path); | |
| return GuardedLink(rawLink); | |
| } | |
| @override | |
| io.Directory getCurrentDirectory() { | |
| final io.Directory rawDir = _parent != null | |
| ? _parent.getCurrentDirectory() | |
| : super.getCurrentDirectory(); | |
| return GuardedDirectory(rawDir); | |
| } | |
| @override | |
| io.Directory getSystemTempDirectory() { | |
| final io.Directory rawDir = _parent != null | |
| ? _parent.getSystemTempDirectory() | |
| : super.getSystemTempDirectory(); | |
| return GuardedDirectory(rawDir); | |
| } | |
| @override | |
| void setCurrentDirectory(String path) { | |
| if (_parent != null) { | |
| _parent.setCurrentDirectory(path); | |
| } else { | |
| super.setCurrentDirectory(path); | |
| } | |
| } | |
| @override | |
| Stream<io.FileSystemEvent> fsWatch(String path, int events, bool recursive) { | |
| return _parent != null | |
| ? _parent.fsWatch(path, events, recursive) | |
| : super.fsWatch(path, events, recursive); | |
| } | |
| @override | |
| bool fsWatchIsSupported() { | |
| return _parent != null ? _parent.fsWatchIsSupported() : super.fsWatchIsSupported(); | |
| } | |
| @override | |
| Future<io.FileSystemEntityType> fseGetType(String path, bool followLinks) { | |
| return _parent != null | |
| ? _parent.fseGetType(path, followLinks) | |
| : super.fseGetType(path, followLinks); | |
| } | |
| @override | |
| io.FileSystemEntityType fseGetTypeSync(String path, bool followLinks) { | |
| return _parent != null | |
| ? _parent.fseGetTypeSync(path, followLinks) | |
| : super.fseGetTypeSync(path, followLinks); | |
| } | |
| @override | |
| Future<bool> fseIdentical(String path1, String path2) { | |
| return _parent != null ? _parent.fseIdentical(path1, path2) : super.fseIdentical(path1, path2); | |
| } | |
| @override | |
| bool fseIdenticalSync(String path1, String path2) { | |
| return _parent != null | |
| ? _parent.fseIdenticalSync(path1, path2) | |
| : super.fseIdenticalSync(path1, path2); | |
| } | |
| @override | |
| Future<io.FileStat> stat(String path) { | |
| return _parent != null ? _parent.stat(path) : super.stat(path); | |
| } | |
| @override | |
| io.FileStat statSync(String path) { | |
| return _parent != null ? _parent.statSync(path) : super.statSync(path); | |
| } | |
| } |