OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 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. |
| 4 |
| 5 /** |
| 6 * This library contains stubs for dartio functionality useful in the browser. |
| 7 * Contents of this file were directly copied from dart:io. |
| 8 */ |
| 9 // TODO(jacobr): remove when there is a subset of dart:io that runs client and |
| 10 // server. b/5818 |
| 11 library dartio_stub; |
| 12 |
| 13 /** |
| 14 * A Path, which is a String interpreted as a sequence of path segments, |
| 15 * which are strings, separated by forward slashes. |
| 16 * Paths are immutable wrappers of a String, that offer member functions for |
| 17 * useful path manipulations and queries. Joining of paths and normalization |
| 18 * interpret '.' and '..' in the usual way. |
| 19 */ |
| 20 abstract class Path { |
| 21 /** |
| 22 * Creates a Path from the String [source]. [source] is used as-is, so if |
| 23 * the string does not consist of segments separated by forward slashes, the |
| 24 * behavior may not be as expected. Paths are immutable. |
| 25 */ |
| 26 factory Path(String source) => new _Path(source); |
| 27 |
| 28 /** |
| 29 * Creates a Path from a String that uses the native filesystem's conventions. |
| 30 * On Windows, this converts '\' to '/', and adds a '/' before a drive letter. |
| 31 * A path starting with '/c:/' (or any other character instead of 'c') is |
| 32 * treated specially. Backwards links ('..') cannot cancel the drive letter. |
| 33 */ |
| 34 factory Path.fromNative(String source) => new _Path.fromNative(source); |
| 35 |
| 36 /** |
| 37 * Is this path the empty string? |
| 38 */ |
| 39 bool get isEmpty; |
| 40 |
| 41 /** |
| 42 * Is this path an absolute path, beginning with a path separator? |
| 43 */ |
| 44 bool get isAbsolute; |
| 45 |
| 46 /** |
| 47 * Does this path end with a path separator? |
| 48 */ |
| 49 bool get hasTrailingSeparator; |
| 50 |
| 51 /** |
| 52 * Does this path contain no consecutive path separators, no segments that |
| 53 * are '.' unless the path is exactly '.', and segments that are '..' only |
| 54 * as the leading segments on a relative path? |
| 55 */ |
| 56 bool get isCanonical; |
| 57 |
| 58 /** |
| 59 * Make a path canonical by dropping segments that are '.', cancelling |
| 60 * segments that are '..' with preceding segments, if possible, |
| 61 * and combining consecutive path separators. Leading '..' segments |
| 62 * are kept on relative paths, and dropped from absolute paths. |
| 63 */ |
| 64 Path canonicalize(); |
| 65 |
| 66 /** |
| 67 * Joins the relative path [further] to this path. Canonicalizes the |
| 68 * resulting joined path using [canonicalize], |
| 69 * interpreting '.' and '..' as directory traversal commands, and removing |
| 70 * consecutive path separators. |
| 71 * |
| 72 * If [further] is an absolute path, an IllegalArgument exception is thrown. |
| 73 * |
| 74 * Examples: |
| 75 * `new Path('/a/b/c').join(new Path('d/e'))` returns the Path object |
| 76 * containing `'a/b/c/d/e'`. |
| 77 * |
| 78 * `new Path('a/b/../c/').join(new Path('d/./e//')` returns the Path |
| 79 * containing `'a/c/d/e/'`. |
| 80 * |
| 81 * `new Path('a/b/c').join(new Path('d/../../e')` returns the Path |
| 82 * containing `'a/b/e'`. |
| 83 * |
| 84 * Note that the join operation does not drop the last segment of the |
| 85 * base path, the way URL joining does. That would be accomplished with |
| 86 * basepath.directoryPath.join(further). |
| 87 * |
| 88 * If you want to avoid joins that traverse |
| 89 * parent directories in the base, you can check whether |
| 90 * `further.canonicalize()` starts with '../' or equals '..'. |
| 91 */ |
| 92 Path join(Path further); |
| 93 |
| 94 |
| 95 /** |
| 96 * Returns a path [:relative:] such that |
| 97 * [:base.join(relative) == this.canonicalize():]. |
| 98 * Throws an exception if such a path is impossible. |
| 99 * For example, if [base] is '../../a/b' and [this] is '.'. |
| 100 * The computation is independent of the file system and current directory. |
| 101 */ |
| 102 Path relativeTo(Path base); |
| 103 |
| 104 /** |
| 105 * Converts a path to a string using the native filesystem's conventions. |
| 106 * |
| 107 * On Windows, converts path separators to backwards slashes, and removes |
| 108 * the leading path separator if the path starts with a drive specification. |
| 109 * For most valid Windows paths, this should be the inverse of the |
| 110 * constructor Path.fromNative. |
| 111 */ |
| 112 String toNativePath(); |
| 113 |
| 114 /** |
| 115 * Returns the path as a string. If this path is constructed using |
| 116 * new Path() or new Path.fromNative() on a non-Windows system, the |
| 117 * returned value is the original string argument to the constructor. |
| 118 */ |
| 119 String toString(); |
| 120 |
| 121 /** |
| 122 * Gets the segments of a Path. Paths beginning or ending with the |
| 123 * path separator do not have leading or terminating empty segments. |
| 124 * Other than that, the segments are just the result of splitting the |
| 125 * path on the path separator. |
| 126 * |
| 127 * new Path('/a/b/c/d').segments() == ['a', 'b', 'c', d']; |
| 128 * new Path(' foo bar //../') == [' foo bar ', '', '..']; |
| 129 */ |
| 130 List<String> segments(); |
| 131 |
| 132 /** |
| 133 * Appends [finalSegment] to a path as a new segment. Adds a path separator |
| 134 * between the path and [finalSegment] if the path does not already end in |
| 135 * a path separator. The path is not canonicalized, and [finalSegment] may |
| 136 * contain path separators. |
| 137 */ |
| 138 Path append(String finalSegment); |
| 139 |
| 140 /** |
| 141 * Drops the final path separator and whatever follows it from this Path, |
| 142 * and returns the resulting Path object. If the only path separator in |
| 143 * this Path is the first character, returns '/' instead of the empty string. |
| 144 * If there is no path separator in the Path, returns the empty string. |
| 145 * |
| 146 * new Path('../images/dot.gif').directoryPath == '../images' |
| 147 * new Path('/usr/geoffrey/www/').directoryPath == '/usr/geoffrey/www' |
| 148 * new Path('lost_file_old').directoryPath == '' |
| 149 * new Path('/src').directoryPath == '/' |
| 150 * Note: new Path('/D:/src').directoryPath == '/D:' |
| 151 */ |
| 152 Path get directoryPath; |
| 153 |
| 154 /** |
| 155 * The part of the path after the last path separator, or the entire path if |
| 156 * it contains no path separator. |
| 157 * |
| 158 * new Path('images/DSC_0027.jpg).filename == 'DSC_0027.jpg' |
| 159 * new Path('users/fred/').filename == '' |
| 160 */ |
| 161 String get filename; |
| 162 |
| 163 /** |
| 164 * The part of [filename] before the last '.', or the entire filename if it |
| 165 * contains no '.'. If [filename] is '.' or '..' it is unchanged. |
| 166 * |
| 167 * new Path('/c:/My Documents/Heidi.txt').filenameWithoutExtension |
| 168 * would return 'Heidi'. |
| 169 * new Path('not what I would call a path').filenameWithoutExtension |
| 170 * would return 'not what I would call a path'. |
| 171 */ |
| 172 String get filenameWithoutExtension; |
| 173 |
| 174 /** |
| 175 * The part of [filename] after the last '.', or '' if [filename] |
| 176 * contains no '.'. If [filename] is '.' or '..', returns ''. |
| 177 * |
| 178 * new Path('tiger.svg').extension == 'svg' |
| 179 * new Path('/src/dart/dart_secrets').extension == '' |
| 180 */ |
| 181 String get extension; |
| 182 } |
| 183 |
| 184 class _Path implements Path { |
| 185 final String _path; |
| 186 |
| 187 _Path(String source) : _path = source; |
| 188 _Path.fromNative(String source) : _path = _clean(source); |
| 189 |
| 190 int hashCode() => _path.hashCode(); |
| 191 |
| 192 static String _clean(String source) { |
| 193 switch (Platform.operatingSystem) { |
| 194 case 'windows': |
| 195 return _cleanWindows(source); |
| 196 default: |
| 197 return source; |
| 198 } |
| 199 } |
| 200 |
| 201 static String _cleanWindows(source) { |
| 202 // Change \ to /. |
| 203 var clean = source.replaceAll('\\', '/'); |
| 204 // Add / before intial [Drive letter]: |
| 205 if (clean.length >= 2 && clean[1] == ':') { |
| 206 clean = '/$clean'; |
| 207 } |
| 208 return clean; |
| 209 } |
| 210 |
| 211 bool get isEmpty => _path.isEmpty(); |
| 212 bool get isAbsolute => _path.startsWith('/'); |
| 213 bool get hasTrailingSeparator => _path.endsWith('/'); |
| 214 |
| 215 String toString() => _path; |
| 216 |
| 217 Path relativeTo(Path base) { |
| 218 // Throws exception if an unimplemented or impossible case is reached. |
| 219 // Returns a path "relative" such that |
| 220 // base.join(relative) == this.canonicalize. |
| 221 // Throws an exception if no such path exists, or the case is not |
| 222 // implemented yet. |
| 223 if (base.isAbsolute && _path.startsWith(base._path)) { |
| 224 if (_path == base._path) return new Path('.'); |
| 225 if (base.hasTrailingSeparator) { |
| 226 return new Path(_path.substring(base._path.length)); |
| 227 } |
| 228 if (_path[base._path.length] == '/') { |
| 229 return new Path(_path.substring(base._path.length + 1)); |
| 230 } |
| 231 } |
| 232 throw new NotImplementedException( |
| 233 "Unimplemented case of Path.relativeTo(base):\n" |
| 234 " Only absolute paths with strict containment are handled at present.\n" |
| 235 " Arguments: $_path.relativeTo($base)"); |
| 236 } |
| 237 |
| 238 Path join(Path further) { |
| 239 if (further.isAbsolute) { |
| 240 throw new ArgumentError( |
| 241 "Path.join called with absolute Path as argument."); |
| 242 } |
| 243 if (isEmpty) { |
| 244 return further.canonicalize(); |
| 245 } |
| 246 if (hasTrailingSeparator) { |
| 247 return new Path('$_path${further._path}').canonicalize(); |
| 248 } |
| 249 return new Path('$_path/${further._path}').canonicalize(); |
| 250 } |
| 251 |
| 252 // Note: The URI RFC names for these operations are normalize, resolve, and |
| 253 // relativize. |
| 254 Path canonicalize() { |
| 255 if (isCanonical) return this; |
| 256 return makeCanonical(); |
| 257 } |
| 258 |
| 259 bool get isCanonical { |
| 260 // Contains no consecutive path separators. |
| 261 // Contains no segments that are '.'. |
| 262 // Absolute paths have no segments that are '..'. |
| 263 // All '..' segments of a relative path are at the beginning. |
| 264 if (isEmpty) return false; // The canonical form of '' is '.'. |
| 265 if (_path == '.') return true; |
| 266 List segs = _path.split('/'); // Don't mask the getter 'segments'. |
| 267 if (segs[0] == '') { // Absolute path |
| 268 segs[0] = null; // Faster than removeRange(). |
| 269 } else { // A canonical relative path may start with .. segments. |
| 270 for (int pos = 0; |
| 271 pos < segs.length && segs[pos] == '..'; |
| 272 ++pos) { |
| 273 segs[pos] = null; |
| 274 } |
| 275 } |
| 276 if (segs.last() == '') segs.removeLast(); // Path ends with /. |
| 277 // No remaining segments can be ., .., or empty. |
| 278 return !segs.some((s) => s == '' || s == '.' || s == '..'); |
| 279 } |
| 280 |
| 281 Path makeCanonical() { |
| 282 bool isAbs = isAbsolute; |
| 283 List segs = segments(); |
| 284 String drive; |
| 285 if (isAbs && |
| 286 !segs.isEmpty() && |
| 287 segs[0].length == 2 && |
| 288 segs[0][1] == ':') { |
| 289 drive = segs[0]; |
| 290 segs.removeRange(0, 1); |
| 291 } |
| 292 List newSegs = []; |
| 293 for (String segment in segs) { |
| 294 switch (segment) { |
| 295 case '..': |
| 296 // Absolute paths drop leading .. markers, including after a drive. |
| 297 if (newSegs.isEmpty()) { |
| 298 if (isAbs) { |
| 299 // Do nothing: drop the segment. |
| 300 } else { |
| 301 newSegs.add('..'); |
| 302 } |
| 303 } else if (newSegs.last() == '..') { |
| 304 newSegs.add('..'); |
| 305 } else { |
| 306 newSegs.removeLast(); |
| 307 } |
| 308 break; |
| 309 case '.': |
| 310 case '': |
| 311 // Do nothing - drop the segment. |
| 312 break; |
| 313 default: |
| 314 newSegs.add(segment); |
| 315 break; |
| 316 } |
| 317 } |
| 318 |
| 319 List segmentsToJoin = []; |
| 320 if (isAbs) { |
| 321 segmentsToJoin.add(''); |
| 322 if (drive != null) { |
| 323 segmentsToJoin.add(drive); |
| 324 } |
| 325 } |
| 326 |
| 327 if (newSegs.isEmpty()) { |
| 328 if (isAbs) { |
| 329 segmentsToJoin.add(''); |
| 330 } else { |
| 331 segmentsToJoin.add('.'); |
| 332 } |
| 333 } else { |
| 334 segmentsToJoin.addAll(newSegs); |
| 335 if (hasTrailingSeparator) { |
| 336 segmentsToJoin.add(''); |
| 337 } |
| 338 } |
| 339 return new Path(Strings.join(segmentsToJoin, '/')); |
| 340 } |
| 341 |
| 342 String toNativePath() { |
| 343 return _path; |
| 344 } |
| 345 |
| 346 List<String> segments() { |
| 347 List result = _path.split('/'); |
| 348 if (isAbsolute) result.removeRange(0, 1); |
| 349 if (hasTrailingSeparator) result.removeLast(); |
| 350 return result; |
| 351 } |
| 352 |
| 353 Path append(String finalSegment) { |
| 354 if (isEmpty) { |
| 355 return new Path(finalSegment); |
| 356 } else if (hasTrailingSeparator) { |
| 357 return new Path('$_path$finalSegment'); |
| 358 } else { |
| 359 return new Path('$_path/$finalSegment'); |
| 360 } |
| 361 } |
| 362 |
| 363 String get filenameWithoutExtension { |
| 364 var name = filename; |
| 365 if (name == '.' || name == '..') return name; |
| 366 int pos = name.lastIndexOf('.'); |
| 367 return (pos < 0) ? name : name.substring(0, pos); |
| 368 } |
| 369 |
| 370 String get extension { |
| 371 var name = filename; |
| 372 int pos = name.lastIndexOf('.'); |
| 373 return (pos < 0) ? '' : name.substring(pos + 1); |
| 374 } |
| 375 |
| 376 Path get directoryPath { |
| 377 int pos = _path.lastIndexOf('/'); |
| 378 if (pos < 0) return new Path(''); |
| 379 while (pos > 0 && _path[pos - 1] == '/') --pos; |
| 380 return new Path((pos > 0) ? _path.substring(0, pos) : '/'); |
| 381 } |
| 382 |
| 383 String get filename { |
| 384 int pos = _path.lastIndexOf('/'); |
| 385 return _path.substring(pos + 1); |
| 386 } |
| 387 } |
| 388 |
| 389 |
OLD | NEW |