blob: e9f7d32d25c294ea697cb702752e63ac21112584 [file] [log] [blame]
// Copyright (c) 2013, 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.
library watcher.path_dart;
import 'dart:collection';
import 'package:path/path.dart' as p;
/// A set of paths, organized into a directory hierarchy.
///
/// When a path is [add]ed, it creates an implicit directory structure above
/// that path. Directories can be inspected using [containsDir] and removed
/// using [remove]. If they're removed, their contents are removed as well.
///
/// The paths in the set are normalized so that they all begin with [root].
class PathSet {
/// The root path, which all paths in the set must be under.
final String root;
/// The path set's directory hierarchy.
///
/// Each level of this hierarchy has the same structure: a map from strings to
/// other maps, which are further levels of the hierarchy. A map with no
/// elements indicates a path that was added to the set that has no paths
/// beneath it. Such a path should not be treated as a directory by
/// [containsDir].
final _entries = new Map<String, Map>();
/// The set of paths that were explicitly added to this set.
///
/// This is needed to disambiguate a directory that was explicitly added to
/// the set from a directory that was implicitly added by adding a path
/// beneath it.
final _paths = new Set<String>();
PathSet(this.root);
/// Adds [path] to the set.
void add(String path) {
path = _normalize(path);
_paths.add(path);
var parts = _split(path);
var dir = _entries;
for (var part in parts) {
dir = dir.putIfAbsent(part, () => {});
}
}
/// Removes [path] and any paths beneath it from the set and returns the
/// removed paths.
///
/// Even if [path] itself isn't in the set, if it's a directory containing
/// paths that are in the set those paths will be removed and returned.
///
/// If neither [path] nor any paths beneath it are in the set, returns an
/// empty set.
Set<String> remove(String path) {
path = _normalize(path);
var parts = new Queue.from(_split(path));
// Remove the children of [dir], as well as [dir] itself if necessary.
//
// [partialPath] is the path to [dir], and a prefix of [path]; the remaining
// components of [path] are in [parts].
recurse(dir, partialPath) {
if (parts.length > 1) {
// If there's more than one component left in [path], recurse down to
// the next level.
var part = parts.removeFirst();
var entry = dir[part];
if (entry == null || entry.isEmpty) return new Set();
partialPath = p.join(partialPath, part);
var paths = recurse(entry, partialPath);
// After removing this entry's children, if it has no more children and
// it's not in the set in its own right, remove it as well.
if (entry.isEmpty && !_paths.contains(partialPath)) dir.remove(part);
return paths;
}
// If there's only one component left in [path], we should remove it.
var entry = dir.remove(parts.first);
if (entry == null) return new Set();
if (entry.isEmpty) {
_paths.remove(path);
return new Set.from([path]);
}
var set = _removePathsIn(entry, path);
if (_paths.contains(path)) {
_paths.remove(path);
set.add(path);
}
return set;
}
return recurse(_entries, root);
}
/// Recursively removes and returns all paths in [dir].
///
/// [root] should be the path to [dir].
Set<String> _removePathsIn(Map dir, String root) {
var removedPaths = new Set();
recurse(dir, path) {
dir.forEach((name, entry) {
var entryPath = p.join(path, name);
if (_paths.remove(entryPath)) removedPaths.add(entryPath);
recurse(entry, entryPath);
});
}
recurse(dir, root);
return removedPaths;
}
/// Returns whether [this] contains [path].
///
/// This only returns true for paths explicitly added to [this].
/// Implicitly-added directories can be inspected using [containsDir].
bool contains(String path) => _paths.contains(_normalize(path));
/// Returns whether [this] contains paths beneath [path].
bool containsDir(String path) {
path = _normalize(path);
var dir = _entries;
for (var part in _split(path)) {
dir = dir[part];
if (dir == null) return false;
}
return !dir.isEmpty;
}
/// Returns a [Set] of all paths in [this].
Set<String> toSet() => _paths.toSet();
/// Removes all paths from [this].
void clear() {
_paths.clear();
_entries.clear();
}
String toString() => _paths.toString();
/// Returns a normalized version of [path].
///
/// This removes any extra ".." or "."s and ensure that the returned path
/// begins with [root]. It's an error if [path] isn't within [root].
String _normalize(String path) {
var relative = p.relative(p.normalize(path), from: root);
var parts = p.split(relative);
// TODO(nweiz): replace this with [p.isWithin] when that exists (issue
// 14980).
if (!p.isRelative(relative) || parts.first == '..' || parts.first == '.') {
throw new ArgumentError('Path "$path" is not inside "$root".');
}
return p.join(root, relative);
}
/// Returns the segments of [path] beneath [root].
List<String> _split(String path) => p.split(p.relative(path, from: root));
}