blob: 21ffb070f570db8c0fa57505236b3b35c3f4e4da [file] [log] [blame]
// 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:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/source_resource.dart';
import 'package:path/path.dart' as pathos;
import 'package:watcher/watcher.dart';
/// 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;
MemoryResourceProvider({pathos.Context? context})
: _pathContext = context ??= pathos.style == pathos.Style.windows
// On Windows, ensure that the current drive matches
// the drive inserted by MemoryResourceProvider.convertPath
// so that packages are mapped to the correct drive
? pathos.Context(current: 'C:\\')
: pathos.context;
@override
pathos.Context get pathContext => _pathContext;
/// Convert the given posix [path] 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.
String convertPath(String path) {
if (pathContext.style == pathos.windows.style) {
if (path.startsWith(pathos.posix.separator)) {
path = r'C:' + path;
}
path = path.replaceAll(pathos.posix.separator, pathos.windows.separator);
}
return path;
}
/// 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
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) {
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.');
}
_pathToData[path] = _FileData(
bytes: utf8.encode(content) as Uint8List,
timeStamp: nextStamp++,
);
_notifyWatchers(path, ChangeType.MODIFY);
}
File newFile(
String path,
String content, [
@Deprecated('This parameter is not used and will be removed') int? stamp,
]) {
var bytes = utf8.encode(content) as Uint8List;
// ignore: deprecated_member_use_from_same_package
return newFileWithBytes(path, bytes, stamp);
}
File newFileWithBytes(
String path,
List<int> bytes, [
@Deprecated('This parameter is not used and will be removed') int? stamp,
]) {
_ensureAbsoluteAndNormalized(path);
bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);
var parentPath = pathContext.dirname(path);
var parentData = _newFolder(parentPath);
_addToParentFolderData(parentData, path);
_pathToData[path] = _FileData(
bytes: bytes,
timeStamp: nextStamp++,
);
_notifyWatchers(path, ChangeType.ADD);
return _MemoryFile(this, path);
}
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;
}
void _setFileContent(String path, Uint8List bytes) {
var parentPath = pathContext.dirname(path);
var parentData = _newFolder(parentPath);
_addToParentFolderData(parentData, path);
_pathToData[path] = _FileData(
bytes: bytes,
timeStamp: nextStamp++,
);
_notifyWatchers(path, ChangeType.MODIFY);
}
}
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(MemoryResourceProvider provider, String path)
: super(provider, 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.getChildAssumingFile(shortName);
destination.writeAsBytesSync(readAsBytesSync());
return destination;
}
@override
Source createSource([Uri? uri]) {
uri ??= provider.pathContext.toUri(path);
return FileSource(this, uri);
}
@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 = utf8.encode(content) as Uint8List;
writeAsBytesSync(bytes);
}
}
/// An in-memory implementation of [Folder].
class _MemoryFolder extends _MemoryResource implements Folder {
_MemoryFolder(MemoryResourceProvider provider, String path)
: super(provider, 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.getChildAssumingFolder(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);
}
@override
_MemoryFile getChildAssumingFile(String relPath) {
var path = canonicalizePath(relPath);
return _MemoryFile(provider, path);
}
@override
_MemoryFolder getChildAssumingFolder(String relPath) {
var path = canonicalizePath(relPath);
return _MemoryFolder(provider, path);
}
@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
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 [Resource].
abstract class _MemoryResource implements Resource {
@override
final MemoryResourceProvider provider;
@override
final String path;
_MemoryResource(this.provider, this.path);
Stream<WatchEvent> get changes {
StreamController<WatchEvent> streamController =
StreamController<WatchEvent>();
if (!provider._pathToWatchers.containsKey(path)) {
provider._pathToWatchers[path] = <StreamController<WatchEvent>>[];
}
provider._pathToWatchers[path]!.add(streamController);
streamController.done.then((_) {
provider._pathToWatchers[path]!.remove(streamController);
if (provider._pathToWatchers[path]!.isEmpty) {
provider._pathToWatchers.remove(path);
}
});
return streamController.stream;
}
@override
int get hashCode => path.hashCode;
@override
Folder get parent2 {
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);
}
class _ResourceData {}