blob: 25f27b9a2ff53e6f430ebc9d51d67a08027b7d40 [file] [log] [blame]
// Copyright (c) 2015, 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 package_config.packagemap;
class Packages {
static const int _EQUALS = 0x3d;
static const int _CR = 0x0d;
static const int _NL = 0x0a;
static const int _NUMBER_SIGN = 0x23;
final Map<String, Uri> packageMapping;
Packages(this.packageMapping);
/// Resolves a URI to a non-package URI.
///
/// If [uri] is a `package:` URI, the location is resolved wrt. the
/// [packageMapping].
/// Otherwise the original URI is returned.
Uri resolve(Uri uri) {
if (uri.scheme.toLowerCase() != "package") {
return uri;
}
if (uri.hasAuthority) {
throw new ArgumentError.value(uri, "uri", "Must not have authority");
}
if (uri.path.startsWith("/")) {
throw new ArgumentError.value(
uri, "uri", "Path must not start with '/'.");
}
// Normalizes the path by removing '.' and '..' segments.
uri = uri.normalizePath();
String path = uri.path;
var slashIndex = path.indexOf('/');
String packageName;
String rest;
if (slashIndex < 0) {
packageName = path;
rest = "";
} else {
packageName = path.substring(0, slashIndex);
rest = path.substring(slashIndex + 1);
}
Uri packageLocation = packageMapping[packageName];
if (packageLocation == null) {
throw new ArgumentError.value(
uri, "uri", "Unknown package name: $packageName");
}
return packageLocation.resolveUri(new Uri(path: rest));
}
/// Parses a `packages.cfg` file into a `Packages` object.
///
/// The [baseLocation] is used as a base URI to resolve all relative
/// URI references against.
///
/// The `Packages` object allows resolving package: URIs and writing
/// the mapping back to a file or string.
/// The [packageMapping] will contain a simple mapping from package name
/// to package location.
static Packages parse(String source, Uri baseLocation) {
int index = 0;
Map<String, Uri> result = <String, Uri>{};
while (index < source.length) {
bool isComment = false;
int start = index;
int eqIndex = -1;
int end = source.length;
int char = source.codeUnitAt(index++);
if (char == _CR || char == _NL) {
continue;
}
if (char == _EQUALS) {
throw new FormatException("Missing package name", source, index - 1);
}
isComment = char == _NUMBER_SIGN;
while (index < source.length) {
char = source.codeUnitAt(index++);
if (char == _EQUALS && eqIndex < 0) {
eqIndex = index - 1;
} else if (char == _NL || char == _CR) {
end = index - 1;
break;
}
}
if (isComment) continue;
if (eqIndex < 0) {
throw new FormatException("No '=' on line", source, index - 1);
}
_checkIdentifier(source, start, eqIndex);
var packageName = source.substring(start, eqIndex);
var packageLocation = Uri.parse(source, eqIndex + 1, end);
if (!packageLocation.path.endsWith('/')) {
packageLocation =
packageLocation.replace(path: packageLocation.path + "/");
}
packageLocation = baseLocation.resolveUri(packageLocation);
if (result.containsKey(packageName)) {
throw new FormatException(
"Same package name occured twice.", source, start);
}
result[packageName] = packageLocation;
}
return new Packages(result);
}
/**
* Writes the mapping to a [StringSink].
*
* If [comment] is provided, the output will contain this comment
* with `#` in front of each line.
*
* If [baseUri] is provided, package locations will be made relative
* to the base URI, if possible, before writing.
*/
void write(StringSink output, {Uri baseUri, String comment}) {
if (baseUri != null && !baseUri.isAbsolute) {
throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute");
}
if (comment != null) {
for (var commentLine in comment.split('\n')) {
output.write('#');
output.writeln(commentLine);
}
} else {
output.write("# generated by package:packagecfg at ");
output.write(new DateTime.now());
output.writeln();
}
packageMapping.forEach((String packageName, Uri uri) {
// Validate packageName.
_checkIdentifier(packageName, 0, packageName.length);
output.write(packageName);
output.write('=');
// If baseUri provided, make uri relative.
if (baseUri != null) {
uri = relativize(uri, baseUri);
}
output.write(uri);
if (!uri.path.endsWith('/')) {
output.write('/');
}
output.writeln();
});
}
String toString() {
StringBuffer buffer = new StringBuffer();
write(buffer);
return buffer.toString();
}
static Uri relativize(Uri uri, Uri baseUri) {
if (uri.hasQuery || uri.hasFragment) {
uri = new Uri(
scheme: uri.scheme,
userInfo: uri.hasAuthority ? uri.userInfo : null,
host: uri.hasAuthority ? uri.host : null,
port: uri.hasAuthority ? uri.port : null,
path: uri.path);
}
if (!baseUri.isAbsolute) {
throw new ArgumentError("Base uri '$baseUri' must be absolute.");
}
// Already relative.
if (!uri.isAbsolute) return uri;
if (baseUri.scheme.toLowerCase() != uri.scheme.toLowerCase()) {
return uri;
}
// If authority differs, we could remove the scheme, but it's not worth it.
if (uri.hasAuthority != baseUri.hasAuthority) return uri;
if (uri.hasAuthority) {
if (uri.userInfo != baseUri.userInfo ||
uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
uri.port != baseUri.port) {
return uri;
}
}
baseUri = baseUri.normalizePath();
List<String> base = baseUri.pathSegments.toList();
if (base.isNotEmpty) {
base = new List<String>.from(base)..removeLast();
}
uri = uri.normalizePath();
List<String> target = uri.pathSegments.toList();
int index = 0;
while (index < base.length && index < target.length) {
if (base[index] != target[index]) {
break;
}
index++;
}
if (index == base.length) {
return new Uri(path: target.skip(index).join('/'));
} else if (index > 0) {
return new Uri(
path: '../' * (base.length - index) + target.skip(index).join('/'));
} else {
return uri;
}
}
static bool _checkIdentifier(String string, int start, int end) {
const int a = 0x61;
const int z = 0x7a;
const int _ = 0x5f;
const int $ = 0x24;
if (start == end) return false;
for (int i = start; i < end; i++) {
var char = string.codeUnitAt(i);
if (char == _ || char == $) continue;
if ((char ^ 0x30) <= 9 && i > 0) continue;
char |= 0x20; // Lower-case letters.
if (char >= a && char <= z) continue;
throw new FormatException("Not an identifier", string, i);
}
return true;
}
}