blob: b05084ce2f3c0b6ae04530a68c1b9fb6e8ae24f8 [file] [log] [blame]
// 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;
_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));
}
} else if (base.isAbsolute && isAbsolute) {
List<String> baseSegments = base.canonicalize().segments();
List<String> pathSegments = canonicalize().segments();
int common = 0;
int length = min(pathSegments.length, baseSegments.length);
while (common < length && pathSegments[common] == baseSegments[common]) {
common++;
}
final sb = new StringBuffer();
for (int i = common + 1; i < baseSegments.length; i++) {
sb.add('../');
}
if (base.hasTrailingSeparator) {
sb.add('../');
}
for (int i = common; i < pathSegments.length - 1; i++) {
sb.add('${pathSegments[i]}/');
}
sb.add('${pathSegments.last()}');
if (hasTrailingSeparator) {
sb.add('/');
}
return new Path(sb.toString());
}
throw new NotImplementedException(
"Unimplemented case of Path.relativeTo(base):\n"
" Only absolute paths are handled at present.\n"
" Arguments: $_path.relativeTo($base)");
}
Path join(Path further) {
if (further.isAbsolute) {
throw new ArgumentError(
"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.length > 3 &&
nativePath.startsWith('/') &&
nativePath[2] == ':') {
nativePath = nativePath.substring(1);
}
nativePath = nativePath.replaceAll('/', '\\');
return nativePath;
}
return _path;
}
List<String> 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);
}
}