blob: 71c86d28f95ee202a106e6ba90e87d3ad9e206da [file] [log] [blame]
// 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 'package:file/file.dart';
import 'package:file/src/io.dart' as io;
import 'clock.dart';
import 'common.dart';
import 'memory_file_stat.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 {
/// 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) {
DirectoryNode 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(DirectoryNode? parent) : super(parent) {
int 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(DirectoryNode? parent) : 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 parent) : 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) {
Uint8List 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 parent, this.target)
: assert(target.isNotEmpty),
super(parent);
/// 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? tailVisitor(DirectoryNode parent, String childName, Node? child)?,
}) {
Node? 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 {
Node? node = referentOrNull;
return node == null ? MemoryFileStat.notFound : node.stat;
} finally {
_reentrant = false;
}
}
}