// Copyright (c) 2012, 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. class _Path implements Path { final String _path; const _Path(String source) : _path = source; _Path.fromNative(String source) : _path = _clean(source); int hashCode() => _path.hashCode(); static String _clean(String source) { switch (Platform.operatingSystem) { case 'windows': return _cleanWindows(source); default: return source; } } static String _cleanWindows(source) { // Change \ to /. var clean = source.replaceAll('\\', '/'); // Add / before intial [Drive letter]: if (clean.length >= 2 && clean[1] == ':') { clean = '/$clean'; } return clean; } bool get isEmpty() => _path.isEmpty(); bool get isAbsolute() => _path.startsWith('/'); bool get hasTrailingSeparator() => _path.endsWith('/'); String toString() => _path; Path relativeTo(Path base) { // Throws exception if an unimplemented or impossible case is reached. // Returns a path "relative" such that // base.join(relative) == this.canonicalize. // Throws an exception if no such path exists, or the case is not // implemented yet. if (base.isAbsolute && _path.startsWith(base._path)) { if (_path == base._path) return new Path('.'); if (base.hasTrailingSeparator) { return new Path(_path.substring(base._path.length)); } if (_path[base._path.length] == '/') { return new Path(_path.substring(base._path.length + 1)); } } throw new NotImplementedException( "Unimplemented case of Path.relativeTo(base):\n" " Only absolute paths with strict containment are handled at present.\n" " Arguments: $_path.relativeTo($base)"); } Path join(Path further) { if (further.isAbsolute) { throw new IllegalArgumentException( "Path.join called with absolute Path as argument."); } if (isEmpty) { return further.canonicalize(); } if (hasTrailingSeparator) { return new Path('$_path${further._path}').canonicalize(); } return new Path('$_path/${further._path}').canonicalize(); } // Note: The URI RFC names for these operations are normalize, resolve, and // relativize. Path canonicalize() { if (isCanonical) return this; return makeCanonical(); } bool get isCanonical() { // Contains no consecutive path separators. // Contains no segments that are '.'. // Absolute paths have no segments that are '..'. // All '..' segments of a relative path are at the beginning. if (isEmpty) return false; // The canonical form of '' is '.'. if (_path == '.') return true; List segs = _path.split('/'); // Don't mask the getter 'segments'. if (segs[0] == '') { // Absolute path segs[0] = null; // Faster than removeRange(). } else { // A canonical relative path may start with .. segments. for (int pos = 0; pos < segs.length && segs[pos] == '..'; ++pos) { segs[pos] = null; } } if (segs.last() == '') segs.removeLast(); // Path ends with /. // No remaining segments can be ., .., or empty. return !segs.some((s) => s == '' || s == '.' || s == '..'); } Path makeCanonical() { bool isAbs = isAbsolute; List segs = segments(); String drive; if (isAbs && !segs.isEmpty() && segs[0].length == 2 && segs[0][1] == ':') { drive = segs[0]; segs.removeRange(0, 1); } List newSegs = []; for (String segment in segs) { switch (segment) { case '..': // Absolute paths drop leading .. markers, including after a drive. if (newSegs.isEmpty()) { if (isAbs) { // Do nothing: drop the segment. } else { newSegs.add('..'); } } else if (newSegs.last() == '..') { newSegs.add('..'); } else { newSegs.removeLast(); } break; case '.': case '': // Do nothing - drop the segment. break; default: newSegs.add(segment); break; } } List segmentsToJoin = []; if (isAbs) { segmentsToJoin.add(''); if (drive != null) { segmentsToJoin.add(drive); } } if (newSegs.isEmpty()) { if (isAbs) { segmentsToJoin.add(''); } else { segmentsToJoin.add('.'); } } else { segmentsToJoin.addAll(newSegs); if (hasTrailingSeparator) { segmentsToJoin.add(''); } } return new Path(Strings.join(segmentsToJoin, '/')); } String toNativePath() { if (Platform.operatingSystem == 'windows') { String nativePath = _path; // Drop '/' before a drive letter. if (nativePath.startsWith('/') && nativePath[2] == ':') { nativePath = nativePath.substring(1); } nativePath = nativePath.replaceAll('/', '\\'); return nativePath; } return _path; } List segments() { List result = _path.split('/'); if (isAbsolute) result.removeRange(0, 1); if (hasTrailingSeparator) result.removeLast(); return result; } Path append(String finalSegment) { if (isEmpty) { return new Path(finalSegment); } else if (hasTrailingSeparator) { return new Path('$_path$finalSegment'); } else { return new Path('$_path/$finalSegment'); } } String get filenameWithoutExtension() { var name = filename; if (name == '.' || name == '..') return name; int pos = name.lastIndexOf('.'); return (pos < 0) ? name : name.substring(0, pos); } String get extension() { var name = filename; int pos = name.lastIndexOf('.'); return (pos < 0) ? '' : name.substring(pos + 1); } Path get directoryPath() { int pos = _path.lastIndexOf('/'); if (pos < 0) return new Path(''); while (pos > 0 && _path[pos - 1] == '/') --pos; return new Path((pos > 0) ? _path.substring(0, pos) : '/'); } String get filename() { int pos = _path.lastIndexOf('/'); return _path.substring(pos + 1); } }