| // Copyright (c) 2017, 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:typed_data'; | |
| import '../../interface.dart'; | |
| import '../../io.dart' as io; | |
| import 'clock.dart'; | |
| import 'common.dart'; | |
| import 'memory_file_stat.dart'; | |
| import 'operations.dart'; | |
| import 'style.dart'; | |
| /// Visitor callback for use with [NodeBasedFileSystem.findNode]. | |
| /// | |
| /// [parent] is the parent node of the current path segment and is guaranteed | |
| /// to be non-null. | |
| /// | |
| /// [childName] is the basename of the entity at the current path segment. It | |
| /// is guaranteed to be non-null. | |
| /// | |
| /// [childNode] is the node at the current path segment. It will be | |
| /// non-null only if such an entity exists. The return value of this callback | |
| /// will be used as the value of this node, which allows this callback to | |
| /// do things like recursively create or delete folders. | |
| /// | |
| /// [currentSegment] is the index of the current segment within the overall | |
| /// path that's being walked by [NodeBasedFileSystem.findNode]. | |
| /// | |
| /// [finalSegment] is the index of the final segment that will be walked by | |
| /// [NodeBasedFileSystem.findNode]. | |
| typedef SegmentVisitor = Node? Function( | |
| DirectoryNode parent, | |
| String childName, | |
| Node? childNode, | |
| int currentSegment, | |
| int finalSegment, | |
| ); | |
| /// A [FileSystem] whose internal structure is made up of a tree of [Node] | |
| /// instances, rooted at a single node. | |
| abstract class NodeBasedFileSystem implements StyleableFileSystem { | |
| /// An optional handle to hook into common file system operations. | |
| void Function(String context, FileSystemOp operation) get opHandle; | |
| /// The root node. | |
| RootNode? get root; | |
| /// The path of the current working directory. | |
| String get cwd; | |
| /// The clock to use when finding the current time (e.g. to set the creation | |
| /// time of a new node). | |
| Clock get clock; | |
| /// Gets the backing node of the entity at the specified path. If the tail | |
| /// element of the path does not exist, this will return null. If the tail | |
| /// element cannot be reached because its directory does not exist, a | |
| /// [io.FileSystemException] will be thrown. | |
| /// | |
| /// If [path] is a relative path, it will be resolved relative to | |
| /// [reference], or the current working directory ([cwd]) if [reference] is | |
| /// null. If [path] is an absolute path, [reference] will be ignored. | |
| /// | |
| /// If the last element in [path] represents a symbolic link, this will | |
| /// return the [LinkNode] node for the link (it will not return the | |
| /// node to which the link points), unless [followTailLink] is true. | |
| /// Directory links in the _middle_ of the path will be followed in order to | |
| /// find the node regardless of the value of [followTailLink]. | |
| /// | |
| /// If [segmentVisitor] is specified, it will be invoked for every path | |
| /// segment visited along the way starting where the reference (root folder | |
| /// if the path is absolute) is the parent. For each segment, the return value | |
| /// of [segmentVisitor] will be used as the backing node of that path | |
| /// segment, thus allowing callers to create nodes on demand in the | |
| /// specified path. Note that `..` and `.` segments may cause the visitor to | |
| /// get invoked with the same node multiple times. When [segmentVisitor] is | |
| /// invoked, for each path segment that resolves to a link node, the visitor | |
| /// will visit the actual link node if [visitLinks] is true; otherwise it | |
| /// will visit the target of the link node. | |
| /// | |
| /// If [pathWithSymlinks] is specified, the path to the node with symbolic | |
| /// links explicitly broken out will be appended to the buffer. `..` and `.` | |
| /// path segments will *not* be resolved and are left to the caller. | |
| Node? findNode( | |
| String path, { | |
| Node reference, | |
| SegmentVisitor segmentVisitor, | |
| bool visitLinks = false, | |
| List<String> pathWithSymlinks, | |
| bool followTailLink = false, | |
| }); | |
| } | |
| /// A class that represents the actual storage of an existent file system | |
| /// entity (whereas classes [File], [Directory], and [Link] represent less | |
| /// concrete entities that may or may not yet exist). | |
| /// | |
| /// This data structure is loosely based on a Unix-style file system inode | |
| /// (hence the name). | |
| abstract class Node { | |
| /// Constructs a new [Node] as a child of the specified parent. | |
| Node(this._parent) { | |
| if (_parent == null && !isRoot) { | |
| throw const io.FileSystemException('All nodes must have a parent.'); | |
| } | |
| } | |
| DirectoryNode? _parent; | |
| /// Gets the directory that holds this node. | |
| DirectoryNode get parent => _parent!; | |
| /// Reparents this node to live in the specified directory. | |
| set parent(DirectoryNode parent) { | |
| var ancestor = parent; | |
| while (!ancestor.isRoot) { | |
| if (ancestor == this) { | |
| throw const io.FileSystemException( | |
| 'A directory cannot be its own ancestor.'); | |
| } | |
| ancestor = ancestor.parent; | |
| } | |
| _parent = parent; | |
| } | |
| /// Returns the type of the file system entity that this node represents. | |
| io.FileSystemEntityType get type; | |
| /// Returns the POSIX stat information for this file system object. | |
| io.FileStat get stat; | |
| /// Returns the closest directory in the ancestry hierarchy starting with | |
| /// this node. For directory nodes, it returns the node itself; for other | |
| /// nodes, it returns the parent node. | |
| DirectoryNode get directory => _parent!; | |
| /// Tells if this node is a root node. | |
| bool get isRoot => false; | |
| /// Returns the file system responsible for this node. | |
| NodeBasedFileSystem get fs => _parent!.fs; | |
| } | |
| /// Base class that represents the backing for those nodes that have | |
| /// substance (namely, node types that will not redirect to other types when | |
| /// you call [stat] on them). | |
| abstract class RealNode extends Node { | |
| /// Constructs a new [RealNode] as a child of the specified [parent]. | |
| RealNode(super.parent) { | |
| var now = clock.now.millisecondsSinceEpoch; | |
| changed = now; | |
| modified = now; | |
| accessed = now; | |
| } | |
| /// See [NodeBasedFileSystem.clock]. | |
| Clock get clock => parent.clock; | |
| /// Last changed time in milliseconds since the Epoch. | |
| late int changed; | |
| /// Last modified time in milliseconds since the Epoch. | |
| late int modified; | |
| /// Last accessed time in milliseconds since the Epoch. | |
| late int accessed; | |
| /// Bitmask representing the file read/write/execute mode. | |
| int mode = 0x777; | |
| @override | |
| io.FileStat get stat { | |
| return MemoryFileStat( | |
| DateTime.fromMillisecondsSinceEpoch(changed), | |
| DateTime.fromMillisecondsSinceEpoch(modified), | |
| DateTime.fromMillisecondsSinceEpoch(accessed), | |
| type, | |
| mode, | |
| size, | |
| ); | |
| } | |
| /// The size of the file system entity in bytes. | |
| int get size; | |
| /// Updates the last modified time of the node. | |
| void touch() { | |
| modified = clock.now.millisecondsSinceEpoch; | |
| } | |
| } | |
| /// Class that represents the backing for an in-memory directory. | |
| class DirectoryNode extends RealNode { | |
| /// Constructs a new [DirectoryNode] as a child of the specified [parent]. | |
| DirectoryNode(super.parent); | |
| /// Child nodes, indexed by their basename. | |
| final Map<String, Node> children = <String, Node>{}; | |
| @override | |
| io.FileSystemEntityType get type => io.FileSystemEntityType.directory; | |
| @override | |
| DirectoryNode get directory => this; | |
| @override | |
| int get size => 0; | |
| } | |
| /// Class that represents the backing for the root of the in-memory file system. | |
| class RootNode extends DirectoryNode { | |
| /// Constructs a new [RootNode] tied to the specified file system. | |
| RootNode(this.fs) | |
| : assert(fs.root == null), | |
| super(null); | |
| @override | |
| final NodeBasedFileSystem fs; | |
| @override | |
| Clock get clock => fs.clock; | |
| @override | |
| DirectoryNode get parent => this; | |
| @override | |
| bool get isRoot => true; | |
| @override | |
| set parent(DirectoryNode parent) => | |
| throw UnsupportedError('Cannot set the parent of the root directory.'); | |
| } | |
| /// Class that represents the backing for an in-memory regular file. | |
| class FileNode extends RealNode { | |
| /// Constructs a new [FileNode] as a child of the specified [parent]. | |
| FileNode(DirectoryNode super.parent); | |
| /// File contents in bytes. | |
| Uint8List get content => _content; | |
| Uint8List _content = Uint8List(0); | |
| @override | |
| io.FileSystemEntityType get type => io.FileSystemEntityType.file; | |
| @override | |
| int get size => _content.length; | |
| /// Appends the specified bytes to the end of this node's [content]. | |
| void write(List<int> bytes) { | |
| var existing = _content; | |
| _content = Uint8List(existing.length + bytes.length); | |
| _content.setRange(0, existing.length, existing); | |
| _content.setRange(existing.length, _content.length, bytes); | |
| } | |
| /// Truncates this node's [content] to the specified length. | |
| /// | |
| /// [length] must be in the range \[0, [size]\]. | |
| void truncate(int length) { | |
| assert(length >= 0); | |
| assert(length <= _content.length); | |
| _content = _content.sublist(0, length); | |
| } | |
| /// Clears the [content] of the node. | |
| void clear() { | |
| _content = Uint8List(0); | |
| } | |
| /// Copies data from [source] into this node. The [modified] and [changed] | |
| /// fields will be reset as opposed to copied to indicate that this file | |
| /// has been modified and changed. | |
| void copyFrom(FileNode source) { | |
| modified = changed = clock.now.millisecondsSinceEpoch; | |
| accessed = source.accessed; | |
| mode = source.mode; | |
| _content = Uint8List.fromList(source.content); | |
| } | |
| } | |
| /// Class that represents the backing for an in-memory symbolic link. | |
| class LinkNode extends Node { | |
| /// Constructs a new [LinkNode] as a child of the specified [parent] and | |
| /// linking to the specified [target] path. | |
| LinkNode(DirectoryNode super.parent, this.target) : assert(target.isNotEmpty); | |
| /// The path to which this link points. | |
| String target; | |
| /// A marker used to detect circular link references. | |
| bool _reentrant = false; | |
| /// Gets the node backing for this link's target. Throws a | |
| /// [FileSystemException] if this link references a non-existent file | |
| /// system entity. | |
| /// | |
| /// If [tailVisitor] is specified, it will be invoked for the tail path | |
| /// segment of this link's target, and its return value will be used as the | |
| /// return value of this method. If the tail path segment of this link's | |
| /// target cannot be traversed into, a [FileSystemException] will be thrown, | |
| /// and [tailVisitor] will not be invoked. | |
| Node getReferent({ | |
| Node? Function(DirectoryNode parent, String childName, Node? child)? | |
| tailVisitor, | |
| }) { | |
| var referent = fs.findNode( | |
| target, | |
| reference: this, | |
| segmentVisitor: ( | |
| DirectoryNode parent, | |
| String childName, | |
| Node? child, | |
| int currentSegment, | |
| int finalSegment, | |
| ) { | |
| if (tailVisitor != null && currentSegment == finalSegment) { | |
| child = tailVisitor(parent, childName, child); | |
| } | |
| return child; | |
| }, | |
| ); | |
| checkExists(referent, () => target); | |
| return referent!; | |
| } | |
| /// Gets the node backing for this link's target, or null if this link | |
| /// references a non-existent file system entity. | |
| Node? get referentOrNull { | |
| try { | |
| return getReferent(); | |
| } on io.FileSystemException { | |
| return null; | |
| } | |
| } | |
| @override | |
| io.FileSystemEntityType get type => io.FileSystemEntityType.link; | |
| @override | |
| io.FileStat get stat { | |
| if (_reentrant) { | |
| return MemoryFileStat.notFound; | |
| } | |
| _reentrant = true; | |
| try { | |
| var node = referentOrNull; | |
| return node == null ? MemoryFileStat.notFound : node.stat; | |
| } finally { | |
| _reentrant = false; | |
| } | |
| } | |
| } |