| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| // for details. 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:async'; | |
| import 'dart:convert'; | |
| import 'dart:typed_data'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:path/path.dart' as pathos; | |
| import 'package:watcher/watcher.dart' hide Watcher; | |
| /// An in-memory implementation of [ResourceProvider]. | |
| /// Use `/` as a path separator. | |
| class MemoryResourceProvider implements ResourceProvider { | |
| final Map<String, _ResourceData> _pathToData = {}; | |
| final Map<String, String> _pathToLinkedPath = {}; | |
| final Map<String, List<StreamController<WatchEvent>>> _pathToWatchers = {}; | |
| int nextStamp = 0; | |
| final pathos.Context _pathContext; | |
| /// An artificial delay that's waited when initializing watchers. | |
| /// | |
| /// This allows mirroring how the real fs watcher works, where events may be | |
| /// lost between creating the watcher and its `ready` event firing. | |
| /// | |
| /// Like the real file watcher, the `ready` event also requires a listener | |
| /// to be attached before it will fire. | |
| @visibleForTesting | |
| final Duration? delayWatcherInitialization; | |
| /// Paths that should have `PathNotFoundException`s emitted on their watch | |
| /// streams. | |
| @visibleForTesting | |
| final Set<String> emitPathNotFoundExceptionsForPaths = {}; | |
| MemoryResourceProvider({ | |
| pathos.Context? context, | |
| this.delayWatcherInitialization, | |
| }) : _pathContext = context ??= pathos.style == pathos.Style.windows | |
| // On Windows, ensure that the current drive matches | |
| // the drive inserted by ResourceProvider.convertPath | |
| // so that packages are mapped to the correct drive | |
| ? pathos.Context(current: 'C:\\') | |
| : pathos.context; | |
| @override | |
| pathos.Context get pathContext => _pathContext; | |
| /// Converts the given posix [filePath] to conform to this provider's path | |
| /// context. | |
| /// | |
| /// This is a utility method for testing; paths passed in to other methods in | |
| /// this class are never converted automatically. | |
| @Deprecated("Use 'ResourceProviderExtensions.convertPath' directly") | |
| String convertPath(String filePath) { | |
| // This implementation is duplicate with that at | |
| // 'package:analyzer_testing/utilities/extensions/resource_provider.dart'. | |
| // But the analyzer package's lib code _cannot_ depend on the | |
| // analyzer_testing package; hence this duplication until this test-only | |
| // method is removed. | |
| if (pathContext.style == pathos.windows.style) { | |
| if (filePath.startsWith(pathos.posix.separator)) { | |
| filePath = r'C:' + filePath; | |
| } | |
| filePath = filePath.replaceAll( | |
| pathos.posix.separator, | |
| pathos.windows.separator, | |
| ); | |
| } | |
| return filePath; | |
| } | |
| /// Delete the file with the given path. | |
| void deleteFile(String path) { | |
| var data = _pathToData[path]; | |
| if (data is! _FileData) { | |
| throw FileSystemException(path, 'Not a file.'); | |
| } | |
| _pathToData.remove(path); | |
| _removeFromParentFolderData(path); | |
| _notifyWatchers(path, ChangeType.REMOVE); | |
| } | |
| /// Delete the folder with the given path | |
| /// and recursively delete nested files and folders. | |
| void deleteFolder(String path) { | |
| var data = _pathToData[path]; | |
| if (data is! _FolderData) { | |
| throw FileSystemException(path, 'Not a folder.'); | |
| } | |
| for (var childName in data.childNames.toList()) { | |
| var childPath = pathContext.join(path, childName); | |
| var child = getResource(childPath); | |
| if (child is File) { | |
| deleteFile(child.path); | |
| } else if (child is Folder) { | |
| deleteFolder(child.path); | |
| } else { | |
| throw 'failed to delete resource: $child'; | |
| } | |
| } | |
| if (_pathToData[path] != data) { | |
| throw StateError('Unexpected concurrent modification: $path'); | |
| } | |
| if (data.childNames.isNotEmpty) { | |
| throw StateError('Must be empty.'); | |
| } | |
| _pathToData.remove(path); | |
| _removeFromParentFolderData(path); | |
| _notifyWatchers(path, ChangeType.REMOVE); | |
| } | |
| @override | |
| File getFile(String path) { | |
| _ensureAbsoluteAndNormalized(path); | |
| return _MemoryFile(this, path); | |
| } | |
| @override | |
| Folder getFolder(String path) { | |
| _ensureAbsoluteAndNormalized(path); | |
| return _MemoryFolder(this, path); | |
| } | |
| @override | |
| Link getLink(String path) { | |
| _ensureAbsoluteAndNormalized(path); | |
| return _MemoryLink(this, path); | |
| } | |
| @override | |
| Resource getResource(String path) { | |
| _ensureAbsoluteAndNormalized(path); | |
| var data = _pathToData[path]; | |
| return data is _FolderData | |
| ? _MemoryFolder(this, path) | |
| : _MemoryFile(this, path); | |
| } | |
| @override | |
| Folder getStateLocation(String pluginId) { | |
| // ignore: deprecated_member_use_from_same_package | |
| var path = convertPath('/user/home/$pluginId'); | |
| return newFolder(path); | |
| } | |
| void modifyFile(String path, String content) { | |
| var data = _pathToData[path]; | |
| if (data is! _FileData) { | |
| throw FileSystemException(path, 'Not a file.'); | |
| } | |
| var bytes = const Utf8Encoder().convert(content); | |
| _setFileContent(path, bytes); | |
| } | |
| File newFile(String path, String content) { | |
| var bytes = const Utf8Encoder().convert(content); | |
| return newFileWithBytes(path, bytes); | |
| } | |
| File newFileWithBytes(String path, List<int> bytes) { | |
| _ensureAbsoluteAndNormalized(path); | |
| bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); | |
| return _setFileContent(path, bytes); | |
| } | |
| Folder newFolder(String path) { | |
| _newFolder(path); | |
| return _MemoryFolder(this, path); | |
| } | |
| /// Create a link from the [path] to the [target]. | |
| void newLink(String path, String target) { | |
| _ensureAbsoluteAndNormalized(path); | |
| _ensureAbsoluteAndNormalized(target); | |
| _pathToLinkedPath[path] = target; | |
| } | |
| /// Write a representation of the file system on the given [sink]. | |
| void writeOn(StringSink sink) { | |
| List<String> paths = _pathToData.keys.toList(); | |
| paths.sort(); | |
| paths.forEach(sink.writeln); | |
| } | |
| void _addToParentFolderData(_FolderData parentData, String path) { | |
| var childName = pathContext.basename(path); | |
| if (!parentData.childNames.contains(childName)) { | |
| parentData.childNames.add(childName); | |
| } | |
| } | |
| /// The file system abstraction supports only absolute and normalized paths. | |
| /// This method is used to validate any input paths to prevent errors later. | |
| void _ensureAbsoluteAndNormalized(String path) { | |
| if (!pathContext.isAbsolute(path)) { | |
| throw ArgumentError("Path must be absolute : $path"); | |
| } | |
| if (pathContext.normalize(path) != path) { | |
| throw ArgumentError("Path must be normalized : $path"); | |
| } | |
| } | |
| _FolderData _newFolder(String path) { | |
| _ensureAbsoluteAndNormalized(path); | |
| var data = _pathToData[path]; | |
| if (data is _FolderData) { | |
| return data; | |
| } else if (data == null) { | |
| var parentPath = pathContext.dirname(path); | |
| if (parentPath != path) { | |
| var parentData = _newFolder(parentPath); | |
| _addToParentFolderData(parentData, path); | |
| } | |
| var data = _FolderData(); | |
| _pathToData[path] = data; | |
| _notifyWatchers(path, ChangeType.ADD); | |
| return data; | |
| } else { | |
| throw FileSystemException(path, 'Folder expected.'); | |
| } | |
| } | |
| void _notifyWatchers(String path, ChangeType changeType) { | |
| _pathToWatchers.forEach(( | |
| String watcherPath, | |
| List<StreamController<WatchEvent>> streamControllers, | |
| ) { | |
| if (watcherPath == path || pathContext.isWithin(watcherPath, path)) { | |
| for (StreamController<WatchEvent> streamController | |
| in streamControllers) { | |
| streamController.add(WatchEvent(changeType, path)); | |
| } | |
| } | |
| }); | |
| } | |
| void _removeFromParentFolderData(String path) { | |
| var parentPath = pathContext.dirname(path); | |
| var parentData = _pathToData[parentPath] as _FolderData; | |
| var childName = pathContext.basename(path); | |
| parentData.childNames.remove(childName); | |
| } | |
| void _renameFileSync(String path, String newPath) { | |
| var data = _pathToData[path]; | |
| if (data is! _FileData) { | |
| throw FileSystemException(path, 'Not a file.'); | |
| } | |
| if (newPath == path) { | |
| return; | |
| } | |
| var existingNewData = _pathToData[newPath]; | |
| if (existingNewData == null) { | |
| // Nothing to do. | |
| } else if (existingNewData is _FileData) { | |
| deleteFile(newPath); | |
| } else { | |
| throw FileSystemException(newPath, 'Not a file.'); | |
| } | |
| var parentPath = pathContext.dirname(path); | |
| var parentData = _newFolder(parentPath); | |
| _addToParentFolderData(parentData, path); | |
| _pathToData.remove(path); | |
| _pathToData[newPath] = data; | |
| _notifyWatchers(path, ChangeType.REMOVE); | |
| _notifyWatchers(newPath, ChangeType.ADD); | |
| } | |
| String _resolveLinks(String path) { | |
| var parentPath = _pathContext.dirname(path); | |
| if (parentPath == path) { | |
| return path; | |
| } | |
| var canonicalParentPath = _resolveLinks(parentPath); | |
| var baseName = _pathContext.basename(path); | |
| var result = _pathContext.join(canonicalParentPath, baseName); | |
| do { | |
| var linkTarget = _pathToLinkedPath[result]; | |
| if (linkTarget != null) { | |
| result = linkTarget; | |
| } else { | |
| break; | |
| } | |
| } while (true); | |
| return result; | |
| } | |
| File _setFileContent(String path, Uint8List bytes) { | |
| var parentPath = pathContext.dirname(path); | |
| var parentData = _newFolder(parentPath); | |
| _addToParentFolderData(parentData, path); | |
| var exists = _pathToData.containsKey(path); | |
| _pathToData[path] = _FileData(bytes: bytes, timeStamp: nextStamp++); | |
| _notifyWatchers(path, exists ? ChangeType.MODIFY : ChangeType.ADD); | |
| return _MemoryFile(this, path); | |
| } | |
| } | |
| class _FileData extends _ResourceData { | |
| final Uint8List bytes; | |
| final int timeStamp; | |
| _FileData({required this.bytes, required this.timeStamp}); | |
| } | |
| class _FolderData extends _ResourceData { | |
| /// Names (not paths) of direct children. | |
| final List<String> childNames = []; | |
| } | |
| /// An in-memory implementation of [File]. | |
| class _MemoryFile extends _MemoryResource implements File { | |
| _MemoryFile(super.provider, super.path); | |
| @override | |
| bool get exists { | |
| var canonicalPath = provider._resolveLinks(path); | |
| return provider._pathToData[canonicalPath] is _FileData; | |
| } | |
| @override | |
| int get lengthSync { | |
| return readAsBytesSync().length; | |
| } | |
| @override | |
| int get modificationStamp { | |
| var canonicalPath = provider._resolveLinks(path); | |
| var data = provider._pathToData[canonicalPath]; | |
| if (data is! _FileData) { | |
| throw FileSystemException(path, 'File does not exist.'); | |
| } | |
| return data.timeStamp; | |
| } | |
| @override | |
| File copyTo(Folder parentFolder) { | |
| parentFolder.create(); | |
| File destination = parentFolder.getFile(shortName); | |
| destination.writeAsBytesSync(readAsBytesSync()); | |
| return destination; | |
| } | |
| @override | |
| void delete() { | |
| provider.deleteFile(path); | |
| } | |
| @override | |
| bool isOrContains(String path) { | |
| return path == this.path; | |
| } | |
| @override | |
| Uint8List readAsBytesSync() { | |
| var canonicalPath = provider._resolveLinks(path); | |
| var data = provider._pathToData[canonicalPath]; | |
| if (data is! _FileData) { | |
| throw FileSystemException(path, 'File does not exist.'); | |
| } | |
| return data.bytes; | |
| } | |
| @override | |
| String readAsStringSync() { | |
| var bytes = readAsBytesSync(); | |
| return utf8.decode(bytes); | |
| } | |
| @override | |
| File renameSync(String newPath) { | |
| provider._renameFileSync(path, newPath); | |
| return provider.getFile(newPath); | |
| } | |
| @override | |
| File resolveSymbolicLinksSync() { | |
| var canonicalPath = provider._resolveLinks(path); | |
| var result = provider.getFile(canonicalPath); | |
| if (!result.exists) { | |
| throw FileSystemException(path, 'File does not exist.'); | |
| } | |
| return result; | |
| } | |
| @override | |
| void writeAsBytesSync(List<int> bytes) { | |
| bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); | |
| provider._setFileContent(path, bytes); | |
| } | |
| @override | |
| void writeAsStringSync(String content) { | |
| var bytes = const Utf8Encoder().convert(content); | |
| writeAsBytesSync(bytes); | |
| } | |
| } | |
| /// An in-memory implementation of [Folder]. | |
| class _MemoryFolder extends _MemoryResource implements Folder { | |
| _MemoryFolder(super.provider, super.path); | |
| @override | |
| bool get exists { | |
| var canonicalPath = provider._resolveLinks(path); | |
| return provider._pathToData[canonicalPath] is _FolderData; | |
| } | |
| @override | |
| bool get isRoot { | |
| var parentPath = provider.pathContext.dirname(path); | |
| return parentPath == path; | |
| } | |
| @override | |
| String canonicalizePath(String relPath) { | |
| relPath = provider.pathContext.normalize(relPath); | |
| String childPath = provider.pathContext.join(path, relPath); | |
| childPath = provider.pathContext.normalize(childPath); | |
| return childPath; | |
| } | |
| @override | |
| bool contains(String path) { | |
| return provider.pathContext.isWithin(this.path, path); | |
| } | |
| @override | |
| Folder copyTo(Folder parentFolder) { | |
| Folder destination = parentFolder.getFolder(shortName); | |
| destination.create(); | |
| for (Resource child in getChildren()) { | |
| child.copyTo(destination); | |
| } | |
| return destination; | |
| } | |
| @override | |
| void create() { | |
| provider.newFolder(path); | |
| } | |
| @override | |
| void delete() { | |
| provider.deleteFolder(path); | |
| } | |
| @override | |
| Resource getChild(String relPath) { | |
| var path = canonicalizePath(relPath); | |
| return provider.getResource(path); | |
| } | |
| @Deprecated('Use getFile instead.') | |
| @override | |
| _MemoryFile getChildAssumingFile(String relPath) { | |
| return getFile(relPath); | |
| } | |
| @Deprecated('Use getFolder instead.') | |
| @override | |
| _MemoryFolder getChildAssumingFolder(String relPath) { | |
| return getFolder(relPath); | |
| } | |
| @override | |
| List<Resource> getChildren() { | |
| var canonicalPath = provider._resolveLinks(path); | |
| if (canonicalPath != path) { | |
| var target = provider.getFolder(canonicalPath); | |
| var canonicalChildren = target.getChildren(); | |
| return canonicalChildren.map((child) { | |
| var childPath = provider.pathContext.join(path, child.shortName); | |
| if (child is Folder) { | |
| return _MemoryFolder(provider, childPath); | |
| } else { | |
| return _MemoryFile(provider, childPath); | |
| } | |
| }).toList(); | |
| } | |
| var data = provider._pathToData[path]; | |
| if (data is! _FolderData) { | |
| throw FileSystemException(path, 'Folder does not exist.'); | |
| } | |
| var children = <Resource>[]; | |
| for (var childName in data.childNames) { | |
| var childPath = provider.pathContext.join(path, childName); | |
| var child = provider.getResource(childPath); | |
| children.add(child); | |
| } | |
| provider._pathToLinkedPath.forEach((resourcePath, targetPath) { | |
| if (provider.pathContext.dirname(resourcePath) == path) { | |
| var target = provider.getResource(targetPath); | |
| if (target is File) { | |
| children.add(_MemoryFile(provider, resourcePath)); | |
| } else if (target is Folder) { | |
| children.add(_MemoryFolder(provider, resourcePath)); | |
| } | |
| } | |
| }); | |
| return children; | |
| } | |
| @override | |
| _MemoryFile getFile(String relPath) { | |
| var path = canonicalizePath(relPath); | |
| return _MemoryFile(provider, path); | |
| } | |
| @override | |
| _MemoryFolder getFolder(String relPath) { | |
| var path = canonicalizePath(relPath); | |
| return _MemoryFolder(provider, path); | |
| } | |
| @override | |
| bool isOrContains(String path) { | |
| if (path == this.path) { | |
| return true; | |
| } | |
| return contains(path); | |
| } | |
| @override | |
| Folder resolveSymbolicLinksSync() { | |
| var canonicalPath = provider._resolveLinks(path); | |
| var result = provider.getFolder(canonicalPath); | |
| if (!result.exists) { | |
| throw FileSystemException(path, 'Folder does not exist.'); | |
| } | |
| return result; | |
| } | |
| @override | |
| Uri toUri() => provider.pathContext.toUri('$path/'); | |
| } | |
| /// An in-memory implementation of [File]. | |
| class _MemoryLink implements Link { | |
| final MemoryResourceProvider provider; | |
| final String path; | |
| _MemoryLink(this.provider, this.path); | |
| @override | |
| bool get exists { | |
| return provider._pathToLinkedPath.containsKey(path); | |
| } | |
| @override | |
| void create(String target) { | |
| provider.newLink(path, target); | |
| } | |
| } | |
| /// An in-memory implementation of [Resource]. | |
| abstract class _MemoryResource implements Resource { | |
| @override | |
| final MemoryResourceProvider provider; | |
| @override | |
| final String path; | |
| _MemoryResource(this.provider, this.path); | |
| Stream<WatchEvent> get changes => watch().changes; | |
| @override | |
| int get hashCode => path.hashCode; | |
| @override | |
| Folder get parent { | |
| String parentPath = provider.pathContext.dirname(path); | |
| return provider.getFolder(parentPath); | |
| } | |
| @override | |
| String get shortName => provider.pathContext.basename(path); | |
| @override | |
| bool operator ==(Object other) { | |
| if (runtimeType != other.runtimeType) { | |
| return false; | |
| } | |
| return path == (other as _MemoryResource).path; | |
| } | |
| @override | |
| String toString() => path; | |
| @override | |
| Uri toUri() => provider.pathContext.toUri(path); | |
| /// Watch for changes to the files inside this folder (and in any nested | |
| /// folders, including folders reachable via links). | |
| /// | |
| /// If `provider.delayWatcherInitialization` is not `null`, this method will | |
| /// wait for this amount of time before it starts capturing/streaming events | |
| /// to simulate the delay that occurs when initializing a real file system | |
| /// watcher. | |
| @override | |
| ResourceWatcher watch() { | |
| var streamController = StreamController<WatchEvent>.broadcast(); | |
| var ready = Completer<void>(); | |
| /// A helper that sets up the watcher that may be called synchronously | |
| /// or delayed, depending on the value of | |
| /// [provider.delayWatcherInitialization]. | |
| void setupWatcher() { | |
| var watchers = provider._pathToWatchers[path] ??= []; | |
| watchers.add(streamController); | |
| unawaited( | |
| streamController.done.then((_) { | |
| watchers.remove(streamController); | |
| if (watchers.isEmpty) { | |
| provider._pathToWatchers.remove(path); | |
| } | |
| }), | |
| ); | |
| ready.complete(); | |
| if (provider.emitPathNotFoundExceptionsForPaths.contains(path)) { | |
| streamController.addError( | |
| PathNotFoundException( | |
| path, | |
| 'Simulated PathNotFoundException from _MemoryResource', | |
| ), | |
| ); | |
| } | |
| } | |
| var delayWatcherInitialization = provider.delayWatcherInitialization; | |
| if (delayWatcherInitialization != null) { | |
| // Wrap this inside onListen so that (like the real watcher) it will only | |
| // fire after a listener is attached. | |
| streamController.onListen = () { | |
| Future<void>.delayed(delayWatcherInitialization, () { | |
| streamController.onListen = null; | |
| setupWatcher(); | |
| }); | |
| }; | |
| } else { | |
| setupWatcher(); | |
| } | |
| return ResourceWatcher(streamController.stream, () => ready.future); | |
| } | |
| } | |
| class _ResourceData {} |