// 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:io' as io show Directory, File, FileStat, FileSystemEntity, FileSystemEntityType, Link; import 'package:file/file.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; // flutter_ignore: package_path_import /// A [FileSystem] that wraps the [delegate] file system to create an overlay of /// files from multiple [roots]. /// /// Regular paths or `file:` URIs are resolved directly in the underlying file /// system, but URIs that use a special [scheme] are resolved by searching /// under a set of given roots in order. /// /// For example, consider the following inputs: /// /// - scheme is `multi-root` /// - the set of roots are `/a` and `/b` /// - the underlying file system contains files: /// /root_a/dir/only_a.dart /// /root_a/dir/both.dart /// /root_b/dir/only_b.dart /// /root_b/dir/both.dart /// /other/other.dart /// /// Then: /// /// - file:///other/other.dart is resolved as /other/other.dart /// - multi-root:///dir/only_a.dart is resolved as /root_a/dir/only_a.dart /// - multi-root:///dir/only_b.dart is resolved as /root_b/dir/only_b.dart /// - multi-root:///dir/both.dart is resolved as /root_a/dir/only_a.dart class MultiRootFileSystem extends ForwardingFileSystem { MultiRootFileSystem({ required FileSystem delegate, required String scheme, required List<String> roots, }) : assert(delegate != null), assert(roots.isNotEmpty), _scheme = scheme, _roots = roots.map((String root) => delegate.path.normalize(root)).toList(), super(delegate); @visibleForTesting FileSystem get fileSystem => delegate; final String _scheme; final List<String> _roots; @override File file(dynamic path) => MultiRootFile( fileSystem: this, delegate: delegate.file(_resolve(path)), ); @override Directory directory(dynamic path) => MultiRootDirectory( fileSystem: this, delegate: delegate.directory(_resolve(path)), ); @override Link link(dynamic path) => MultiRootLink( fileSystem: this, delegate: delegate.link(_resolve(path)), ); @override Future<io.FileStat> stat(String path) => delegate.stat(_resolve(path).toString()); @override io.FileStat statSync(String path) => delegate.statSync(_resolve(path).toString()); @override Future<bool> identical(String path1, String path2) => delegate.identical(_resolve(path1).toString(), _resolve(path2).toString()); @override bool identicalSync(String path1, String path2) => delegate.identicalSync(_resolve(path1).toString(), _resolve(path2).toString()); @override Future<io.FileSystemEntityType> type(String path, {bool followLinks = true}) => delegate.type(_resolve(path).toString(), followLinks: followLinks); @override io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) => delegate.typeSync(_resolve(path).toString(), followLinks: followLinks); // Caching the path context here and clearing when the currentDirectory setter // is updated works since the flutter tool restricts usage of dart:io directly // via the forbidden import tests. Otherwise, the path context's current // working directory might get out of sync, leading to unexpected results from // methods like `path.relative`. @override p.Context get path => _cachedPath ??= delegate.path; p.Context? _cachedPath; @override set currentDirectory(dynamic path) { _cachedPath = null; delegate.currentDirectory = path; } /// If the path is a multiroot uri, resolve to the actual path of the /// underlying file system. Otherwise, return as is. dynamic _resolve(dynamic path) { Uri uri; if (path == null) { return null; } else if (path is String) { uri = Uri.parse(path); } else if (path is Uri) { uri = path; } else if (path is FileSystemEntity) { uri = path.uri; } else { throw ArgumentError('Invalid type for "path": ${path?.runtimeType}'); } if (!uri.hasScheme || uri.scheme != _scheme) { return path; } String? firstRootPath; final String relativePath = delegate.path.joinAll(uri.pathSegments); for (final String root in _roots) { final String pathWithRoot = delegate.path.join(root, relativePath); if (delegate.typeSync(pathWithRoot, followLinks: false) != FileSystemEntityType.notFound) { return pathWithRoot; } firstRootPath ??= pathWithRoot; } // If not found, construct the path with the first root. return firstRootPath!; } Uri _toMultiRootUri(Uri uri) { if (uri.scheme != 'file') { return uri; } final p.Context pathContext = delegate.path; final bool isWindows = pathContext.style == p.Style.windows; final String path = uri.toFilePath(windows: isWindows); for (final String root in _roots) { if (path.startsWith('$root${pathContext.separator}')) { String pathWithoutRoot = path.substring(root.length + 1); if (isWindows) { // Convert the path from Windows style pathWithoutRoot = p.url.joinAll(pathContext.split(pathWithoutRoot)); } return Uri.parse('$_scheme:///$pathWithoutRoot'); } } return uri; } @override String toString() => 'MultiRootFileSystem(scheme = $_scheme, roots = $_roots, delegate = $delegate)'; } abstract class MultiRootFileSystemEntity<T extends FileSystemEntity, D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> { MultiRootFileSystemEntity({ required this.fileSystem, required this.delegate, }); @override final D delegate; @override final MultiRootFileSystem fileSystem; @override File wrapFile(io.File delegate) => MultiRootFile( fileSystem: fileSystem, delegate: delegate, ); @override Directory wrapDirectory(io.Directory delegate) => MultiRootDirectory( fileSystem: fileSystem, delegate: delegate, ); @override Link wrapLink(io.Link delegate) => MultiRootLink( fileSystem: fileSystem, delegate: delegate, ); @override Uri get uri => fileSystem._toMultiRootUri(delegate.uri); } class MultiRootFile extends MultiRootFileSystemEntity<File, io.File> with ForwardingFile { MultiRootFile({ required MultiRootFileSystem fileSystem, required io.File delegate, }) : super( fileSystem: fileSystem, delegate: delegate, ); @override String toString() => 'MultiRootFile(fileSystem = $fileSystem, delegate = $delegate)'; } class MultiRootDirectory extends MultiRootFileSystemEntity<Directory, io.Directory> with ForwardingDirectory<Directory> { MultiRootDirectory({ required MultiRootFileSystem fileSystem, required io.Directory delegate, }) : super( fileSystem: fileSystem, delegate: delegate, ); // For the childEntity methods, we first obtain an instance of the entity // from the underlying file system, then invoke childEntity() on it, then // wrap in the ErrorHandling version. @override Directory childDirectory(String basename) => fileSystem.directory(fileSystem.path.join(delegate.path, basename)); @override File childFile(String basename) => fileSystem.file(fileSystem.path.join(delegate.path, basename)); @override Link childLink(String basename) => fileSystem.link(fileSystem.path.join(delegate.path, basename)); @override String toString() => 'MultiRootDirectory(fileSystem = $fileSystem, delegate = $delegate)'; } class MultiRootLink extends MultiRootFileSystemEntity<Link, io.Link> with ForwardingLink { MultiRootLink({ required MultiRootFileSystem fileSystem, required io.Link delegate, }) : super( fileSystem: fileSystem, delegate: delegate, ); @override String toString() => 'MultiRootLink(fileSystem = $fileSystem, delegate = $delegate)'; }