blob: ac57b4f15753e3eb00c81fafdf13cae45c5055d6 [file] [log] [blame]
// Copyright (c) 2019, 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.
import "package_config_impl.dart";
import "package:charcode/ascii.dart";
import "util.dart" show isValidPackageName, relativizeUri;
import "errors.dart";
/// Parses a `.packages` file into a [PackageConfig].
///
/// The [source] is the byte content of a `.packages` file, assumed to be
/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
/// so Latin-1 or Windows-1252 encoding will also work fine.
///
/// If the file content is available as a string, its [String.codeUnits] can
/// be used as the `source` argument of this function.
///
/// The [baseLocation] is used as a base URI to resolve all relative
/// URI references against.
/// If the content was read from a file, `baseLocation` should be the
/// location of that file.
///
/// Returns a simple package configuration where each package's
/// [Package.packageUriRoot] is the same as its [Package.root]
/// and it has no [Package.languageVersion].
PackageConfig parse(List<int> source, Uri baseLocation) {
if (baseLocation.isScheme("package")) {
throw PackageConfigArgumentError(
baseLocation, "baseLocation", "Must not be a package: URI");
}
int index = 0;
List<Package> packages = [];
Set<String> packageNames = {};
while (index < source.length) {
bool isComment = false;
int start = index;
int separatorIndex = -1;
int end = source.length;
int char = source[index++];
if (char == $cr || char == $lf) {
continue;
}
if (char == $colon) {
throw PackageConfigFormatException(
"Missing package name", source, index - 1);
}
isComment = char == $hash;
while (index < source.length) {
char = source[index++];
if (char == $colon && separatorIndex < 0) {
separatorIndex = index - 1;
} else if (char == $cr || char == $lf) {
end = index - 1;
break;
}
}
if (isComment) continue;
if (separatorIndex < 0) {
throw PackageConfigFormatException("No ':' on line", source, index - 1);
}
var packageName = String.fromCharCodes(source, start, separatorIndex);
if (!isValidPackageName(packageName)) {
throw PackageConfigFormatException(
"Not a valid package name", packageName, 0);
}
var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
Uri packageLocation = baseLocation.resolve(packageValue);
if (packageLocation.isScheme("package")) {
throw PackageConfigFormatException(
"Package URI as location for package", source, separatorIndex + 1);
}
if (packageLocation.hasQuery || packageLocation.hasFragment) {
throw PackageConfigFormatException(
"Location URI must not have query or fragment", source, start);
}
if (!packageLocation.path.endsWith('/')) {
packageLocation =
packageLocation.replace(path: packageLocation.path + "/");
}
if (packageNames.contains(packageName)) {
throw PackageConfigFormatException(
"Same package name occured more than once", source, start);
}
packages.add(SimplePackage(
packageName, packageLocation, packageLocation, null, null));
packageNames.add(packageName);
}
return SimplePackageConfig(1, packages, null);
}
/// Writes the configuration to a [StringSink].
///
/// If [comment] is provided, the output will contain this comment
/// with `# ` in front of each line.
/// Lines are defined as ending in line feed (`'\n'`). If the final
/// line of the comment doesn't end in a line feed, one will be added.
///
/// If [baseUri] is provided, package locations will be made relative
/// to the base URI, if possible, before writing.
///
/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
/// empty string mapping to the _default package name_.
///
/// All the keys of [packageMapping] must be valid package names,
/// and the values must be URIs that do not have the `package:` scheme.
void write(StringSink output, PackageConfig config,
{Uri baseUri, String comment}) {
if (baseUri != null && !baseUri.isAbsolute) {
throw PackageConfigArgumentError(baseUri, "baseUri", "Must be absolute");
}
if (comment != null) {
var lines = comment.split('\n');
if (lines.last.isEmpty) lines.removeLast();
for (var commentLine in lines) {
output.write('# ');
output.writeln(commentLine);
}
} else {
output.write("# generated by package:package_config at ");
output.write(DateTime.now());
output.writeln();
}
for (var package in config.packages) {
var packageName = package.name;
var uri = package.packageUriRoot;
// Validate packageName.
if (!isValidPackageName(packageName)) {
throw PackageConfigArgumentError(
config, "config", '"$packageName" is not a valid package name');
}
if (uri.scheme == "package") {
throw PackageConfigArgumentError(
config, "config", "Package location must not be a package URI: $uri");
}
output.write(packageName);
output.write(':');
// If baseUri provided, make uri relative.
if (baseUri != null) {
uri = relativizeUri(uri, baseUri);
}
if (!uri.path.endsWith('/')) {
uri = uri.replace(path: uri.path + '/');
}
output.write(uri);
output.writeln();
}
}