OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library watcher.path_dart; | |
6 | |
7 import 'dart:collection'; | 5 import 'dart:collection'; |
8 | 6 |
9 import 'package:path/path.dart' as p; | 7 import 'package:path/path.dart' as p; |
10 | 8 |
11 /// A set of paths, organized into a directory hierarchy. | 9 /// A set of paths, organized into a directory hierarchy. |
12 /// | 10 /// |
13 /// When a path is [add]ed, it creates an implicit directory structure above | 11 /// When a path is [add]ed, it creates an implicit directory structure above |
14 /// that path. Directories can be inspected using [containsDir] and removed | 12 /// that path. Directories can be inspected using [containsDir] and removed |
15 /// using [remove]. If they're removed, their contents are removed as well. | 13 /// using [remove]. If they're removed, their contents are removed as well. |
16 /// | 14 /// |
17 /// The paths in the set are normalized so that they all begin with [root]. | 15 /// The paths in the set are normalized so that they all begin with [root]. |
18 class PathSet { | 16 class PathSet { |
19 /// The root path, which all paths in the set must be under. | 17 /// The root path, which all paths in the set must be under. |
20 final String root; | 18 final String root; |
21 | 19 |
22 /// The path set's directory hierarchy. | 20 /// The path set's directory hierarchy. |
23 /// | 21 /// |
24 /// Each level of this hierarchy has the same structure: a map from strings to | 22 /// Each entry represents a directory or file. It may be a file or directory |
25 /// other maps, which are further levels of the hierarchy. A map with no | 23 /// that was explicitly added, or a parent directory that was implicitly |
26 /// elements indicates a path that was added to the set that has no paths | 24 /// added in order to add a child. |
27 /// beneath it. Such a path should not be treated as a directory by | 25 final _Entry _entries = new _Entry(); |
28 /// [containsDir]. | |
29 final _entries = new Map<String, Map>(); | |
30 | |
31 /// The set of paths that were explicitly added to this set. | |
32 /// | |
33 /// This is needed to disambiguate a directory that was explicitly added to | |
34 /// the set from a directory that was implicitly added by adding a path | |
35 /// beneath it. | |
36 final _paths = new Set<String>(); | |
37 | 26 |
38 PathSet(this.root); | 27 PathSet(this.root); |
39 | 28 |
40 /// Adds [path] to the set. | 29 /// Adds [path] to the set. |
41 void add(String path) { | 30 void add(String path) { |
42 path = _normalize(path); | 31 path = _normalize(path); |
43 _paths.add(path); | |
44 | 32 |
45 var parts = _split(path); | 33 var parts = p.split(path); |
46 var dir = _entries; | 34 var entry = _entries; |
47 for (var part in parts) { | 35 for (var part in parts) { |
48 dir = dir.putIfAbsent(part, () => {}); | 36 entry = entry.contents.putIfAbsent(part, () => new _Entry()); |
49 } | 37 } |
| 38 |
| 39 entry.isExplicit = true; |
50 } | 40 } |
51 | 41 |
52 /// Removes [path] and any paths beneath it from the set and returns the | 42 /// Removes [path] and any paths beneath it from the set and returns the |
53 /// removed paths. | 43 /// removed paths. |
54 /// | 44 /// |
55 /// Even if [path] itself isn't in the set, if it's a directory containing | 45 /// Even if [path] itself isn't in the set, if it's a directory containing |
56 /// paths that are in the set those paths will be removed and returned. | 46 /// paths that are in the set those paths will be removed and returned. |
57 /// | 47 /// |
58 /// If neither [path] nor any paths beneath it are in the set, returns an | 48 /// If neither [path] nor any paths beneath it are in the set, returns an |
59 /// empty set. | 49 /// empty set. |
60 Set<String> remove(String path) { | 50 Set<String> remove(String path) { |
61 path = _normalize(path); | 51 path = _normalize(path); |
62 var parts = new Queue.from(_split(path)); | 52 var parts = new Queue.from(p.split(path)); |
63 | 53 |
64 // Remove the children of [dir], as well as [dir] itself if necessary. | 54 // Remove the children of [dir], as well as [dir] itself if necessary. |
65 // | 55 // |
66 // [partialPath] is the path to [dir], and a prefix of [path]; the remaining | 56 // [partialPath] is the path to [dir], and a prefix of [path]; the remaining |
67 // components of [path] are in [parts]. | 57 // components of [path] are in [parts]. |
68 recurse(dir, partialPath) { | 58 Set<String> recurse(dir, partialPath) { |
69 if (parts.length > 1) { | 59 if (parts.length > 1) { |
70 // If there's more than one component left in [path], recurse down to | 60 // If there's more than one component left in [path], recurse down to |
71 // the next level. | 61 // the next level. |
72 var part = parts.removeFirst(); | 62 var part = parts.removeFirst(); |
73 var entry = dir[part]; | 63 var entry = dir.contents[part]; |
74 if (entry == null || entry.isEmpty) return new Set(); | 64 if (entry == null || entry.contents.isEmpty) return new Set(); |
75 | 65 |
76 partialPath = p.join(partialPath, part); | 66 partialPath = p.join(partialPath, part); |
77 var paths = recurse(entry, partialPath); | 67 var paths = recurse(entry, partialPath); |
78 // After removing this entry's children, if it has no more children and | 68 // After removing this entry's children, if it has no more children and |
79 // it's not in the set in its own right, remove it as well. | 69 // it's not in the set in its own right, remove it as well. |
80 if (entry.isEmpty && !_paths.contains(partialPath)) dir.remove(part); | 70 if (entry.contents.isEmpty && !entry.isExplicit) { |
| 71 dir.contents.remove(part); |
| 72 } |
81 return paths; | 73 return paths; |
82 } | 74 } |
83 | 75 |
84 // If there's only one component left in [path], we should remove it. | 76 // If there's only one component left in [path], we should remove it. |
85 var entry = dir.remove(parts.first); | 77 var entry = dir.contents.remove(parts.first); |
86 if (entry == null) return new Set(); | 78 if (entry == null) return new Set(); |
87 | 79 |
88 if (entry.isEmpty) { | 80 if (entry.contents.isEmpty) { |
89 _paths.remove(path); | 81 return new Set.from([p.join(root, path)]); |
90 return new Set.from([path]); | |
91 } | 82 } |
92 | 83 |
93 var set = _removePathsIn(entry, path); | 84 var set = _explicitPathsWithin(entry, path); |
94 if (_paths.contains(path)) { | 85 if (entry.isExplicit) { |
95 _paths.remove(path); | 86 set.add(p.join(root, path)); |
96 set.add(path); | |
97 } | 87 } |
| 88 |
98 return set; | 89 return set; |
99 } | 90 } |
100 | 91 |
101 return recurse(_entries, root); | 92 return recurse(_entries, root); |
102 } | 93 } |
103 | 94 |
104 /// Recursively removes and returns all paths in [dir]. | 95 /// Recursively lists all of the explicit paths within [dir]. |
105 /// | 96 /// |
106 /// [root] should be the path to [dir]. | 97 /// [dirPath] should be the path to [dir]. |
107 Set<String> _removePathsIn(Map dir, String root) { | 98 Set<String> _explicitPathsWithin(_Entry dir, String dirPath) { |
108 var removedPaths = new Set(); | 99 var paths = new Set<String>(); |
109 recurse(dir, path) { | 100 recurse(dir, path) { |
110 dir.forEach((name, entry) { | 101 dir.contents.forEach((name, entry) { |
111 var entryPath = p.join(path, name); | 102 var entryPath = p.join(path, name); |
112 if (_paths.remove(entryPath)) removedPaths.add(entryPath); | 103 if (entry.isExplicit) paths.add(p.join(root, entryPath)); |
| 104 |
113 recurse(entry, entryPath); | 105 recurse(entry, entryPath); |
114 }); | 106 }); |
115 } | 107 } |
116 | 108 |
117 recurse(dir, root); | 109 recurse(dir, dirPath); |
118 return removedPaths; | 110 return paths; |
119 } | 111 } |
120 | 112 |
121 /// Returns whether [this] contains [path]. | 113 /// Returns whether [this] contains [path]. |
122 /// | 114 /// |
123 /// This only returns true for paths explicitly added to [this]. | 115 /// This only returns true for paths explicitly added to [this]. |
124 /// Implicitly-added directories can be inspected using [containsDir]. | 116 /// Implicitly-added directories can be inspected using [containsDir]. |
125 bool contains(String path) => _paths.contains(_normalize(path)); | 117 bool contains(String path) { |
| 118 path = _normalize(path); |
| 119 var entry = _entries; |
| 120 |
| 121 for (var part in p.split(path)) { |
| 122 entry = entry.contents[part]; |
| 123 if (entry == null) return false; |
| 124 } |
| 125 |
| 126 return entry.isExplicit; |
| 127 } |
126 | 128 |
127 /// Returns whether [this] contains paths beneath [path]. | 129 /// Returns whether [this] contains paths beneath [path]. |
128 bool containsDir(String path) { | 130 bool containsDir(String path) { |
129 path = _normalize(path); | 131 path = _normalize(path); |
130 var dir = _entries; | 132 var entry = _entries; |
131 | 133 |
132 for (var part in _split(path)) { | 134 for (var part in p.split(path)) { |
133 dir = dir[part]; | 135 entry = entry.contents[part]; |
134 if (dir == null) return false; | 136 if (entry == null) return false; |
135 } | 137 } |
136 | 138 |
137 return !dir.isEmpty; | 139 return !entry.contents.isEmpty; |
138 } | 140 } |
139 | 141 |
140 /// Returns a [Set] of all paths in [this]. | 142 /// All of the paths explicitly added to this set. |
141 Set<String> toSet() => _paths.toSet(); | 143 List<String> get paths { |
| 144 var result = <String>[]; |
| 145 |
| 146 recurse(dir, path) { |
| 147 for (var name in dir.contents.keys) { |
| 148 var entry = dir.contents[name]; |
| 149 var entryPath = p.join(path, name); |
| 150 if (entry.isExplicit) result.add(entryPath); |
| 151 recurse(entry, entryPath); |
| 152 } |
| 153 } |
| 154 |
| 155 recurse(_entries, root); |
| 156 return result; |
| 157 } |
142 | 158 |
143 /// Removes all paths from [this]. | 159 /// Removes all paths from [this]. |
144 void clear() { | 160 void clear() { |
145 _paths.clear(); | 161 _entries.contents.clear(); |
146 _entries.clear(); | |
147 } | 162 } |
148 | 163 |
149 String toString() => _paths.toString(); | |
150 | |
151 /// Returns a normalized version of [path]. | 164 /// Returns a normalized version of [path]. |
152 /// | 165 /// |
153 /// This removes any extra ".." or "."s and ensure that the returned path | 166 /// This removes any extra ".." or "."s and ensure that the returned path |
154 /// begins with [root]. It's an error if [path] isn't within [root]. | 167 /// begins with [root]. It's an error if [path] isn't within [root]. |
155 String _normalize(String path) { | 168 String _normalize(String path) { |
156 var relative = p.relative(p.normalize(path), from: root); | 169 assert(p.isWithin(root, path)); |
157 var parts = p.split(relative); | 170 |
158 // TODO(nweiz): replace this with [p.isWithin] when that exists (issue | 171 return p.relative(p.normalize(path), from: root); |
159 // 14980). | |
160 if (!p.isRelative(relative) || parts.first == '..' || parts.first == '.') { | |
161 throw new ArgumentError('Path "$path" is not inside "$root".'); | |
162 } | |
163 return p.join(root, relative); | |
164 } | 172 } |
| 173 } |
165 | 174 |
166 /// Returns the segments of [path] beneath [root]. | 175 /// A virtual file system entity tracked by the [PathSet]. |
167 List<String> _split(String path) => p.split(p.relative(path, from: root)); | 176 /// |
| 177 /// It may have child entries in [contents], which implies it's a directory. |
| 178 class _Entry { |
| 179 /// The child entries contained in this directory. |
| 180 final Map<String, _Entry> contents = {}; |
| 181 |
| 182 /// If this entry was explicitly added as a leaf file system entity, this |
| 183 /// will be true. |
| 184 /// |
| 185 /// Otherwise, it represents a parent directory that was implicitly added |
| 186 /// when added some child of it. |
| 187 bool isExplicit = false; |
168 } | 188 } |
OLD | NEW |