Line data Source code
1 : // Copyright (c) 2015, 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 : library package_config.packages_file;
6 :
7 : import "package:charcode/ascii.dart";
8 :
9 : import "src/util.dart" show isValidPackageName;
10 :
11 : /// Parses a `.packages` file into a map from package name to base URI.
12 : ///
13 : /// The [source] is the byte content of a `.packages` file, assumed to be
14 : /// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
15 : /// so Latin-1 or Windows-1252 encoding will also work fine.
16 : ///
17 : /// If the file content is available as a string, its [String.codeUnits] can
18 : /// be used as the `source` argument of this function.
19 : ///
20 : /// The [baseLocation] is used as a base URI to resolve all relative
21 : /// URI references against.
22 : /// If the content was read from a file, `baseLocation` should be the
23 : /// location of that file.
24 : ///
25 : /// Returns a simple mapping from package name to package location.
26 : Map<String, Uri> parse(List<int> source, Uri baseLocation) {
27 : int index = 0;
28 0 : Map<String, Uri> result = <String, Uri>{};
29 0 : while (index < source.length) {
30 : bool isComment = false;
31 : int start = index;
32 : int separatorIndex = -1;
33 0 : int end = source.length;
34 0 : int char = source[index++];
35 0 : if (char == $cr || char == $lf) {
36 : continue;
37 : }
38 0 : if (char == $colon) {
39 0 : throw new FormatException("Missing package name", source, index - 1);
40 : }
41 0 : isComment = char == $hash;
42 0 : while (index < source.length) {
43 0 : char = source[index++];
44 0 : if (char == $colon && separatorIndex < 0) {
45 0 : separatorIndex = index - 1;
46 0 : } else if (char == $cr || char == $lf) {
47 0 : end = index - 1;
48 : break;
49 : }
50 : }
51 : if (isComment) continue;
52 0 : if (separatorIndex < 0) {
53 0 : throw new FormatException("No ':' on line", source, index - 1);
54 : }
55 0 : var packageName = new String.fromCharCodes(source, start, separatorIndex);
56 0 : if (!isValidPackageName(packageName)) {
57 0 : throw new FormatException("Not a valid package name", packageName, 0);
58 : }
59 0 : var packageUri = new String.fromCharCodes(source, separatorIndex + 1, end);
60 0 : var packageLocation = Uri.parse(packageUri);
61 0 : packageLocation = baseLocation.resolveUri(packageLocation);
62 0 : if (!packageLocation.path.endsWith('/')) {
63 : packageLocation =
64 0 : packageLocation.replace(path: packageLocation.path + "/");
65 : }
66 0 : if (result.containsKey(packageName)) {
67 0 : throw new FormatException(
68 : "Same package name occured twice.", source, start);
69 : }
70 0 : result[packageName] = packageLocation;
71 : }
72 : return result;
73 : }
74 :
75 : /// Writes the mapping to a [StringSink].
76 : ///
77 : /// If [comment] is provided, the output will contain this comment
78 : /// with `# ` in front of each line.
79 : /// Lines are defined as ending in line feed (`'\n'`). If the final
80 : /// line of the comment doesn't end in a line feed, one will be added.
81 : ///
82 : /// If [baseUri] is provided, package locations will be made relative
83 : /// to the base URI, if possible, before writing.
84 : ///
85 : /// All the keys of [packageMapping] must be valid package names,
86 : /// and the values must be URIs that do not have the `package:` scheme.
87 : void write(StringSink output, Map<String, Uri> packageMapping,
88 : {Uri baseUri, String comment}) {
89 0 : if (baseUri != null && !baseUri.isAbsolute) {
90 0 : throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute");
91 : }
92 :
93 : if (comment != null) {
94 0 : var lines = comment.split('\n');
95 0 : if (lines.last.isEmpty) lines.removeLast();
96 0 : for (var commentLine in lines) {
97 0 : output.write('# ');
98 0 : output.writeln(commentLine);
99 : }
100 : } else {
101 0 : output.write("# generated by package:package_config at ");
102 0 : output.write(new DateTime.now());
103 0 : output.writeln();
104 : }
105 :
106 0 : packageMapping.forEach((String packageName, Uri uri) {
107 : // Validate packageName.
108 0 : if (!isValidPackageName(packageName)) {
109 0 : throw new ArgumentError('"$packageName" is not a valid package name');
110 : }
111 0 : if (uri.scheme == "package") {
112 0 : throw new ArgumentError.value(
113 0 : "Package location must not be a package: URI", uri.toString());
114 : }
115 0 : output.write(packageName);
116 0 : output.write(':');
117 : // If baseUri provided, make uri relative.
118 : if (baseUri != null) {
119 0 : uri = _relativize(uri, baseUri);
120 : }
121 0 : output.write(uri);
122 0 : if (!uri.path.endsWith('/')) {
123 0 : output.write('/');
124 : }
125 0 : output.writeln();
126 : });
127 : }
128 :
129 : /// Attempts to return a relative URI for [uri].
130 : ///
131 : /// The result URI satisfies `baseUri.resolveUri(result) == uri`,
132 : /// but may be relative.
133 : /// The `baseUri` must be absolute.
134 : Uri _relativize(Uri uri, Uri baseUri) {
135 : assert(baseUri.isAbsolute);
136 0 : if (uri.hasQuery || uri.hasFragment) {
137 0 : uri = new Uri(
138 0 : scheme: uri.scheme,
139 0 : userInfo: uri.hasAuthority ? uri.userInfo : null,
140 0 : host: uri.hasAuthority ? uri.host : null,
141 0 : port: uri.hasAuthority ? uri.port : null,
142 0 : path: uri.path);
143 : }
144 :
145 : // Already relative. We assume the caller knows what they are doing.
146 0 : if (!uri.isAbsolute) return uri;
147 :
148 0 : if (baseUri.scheme != uri.scheme) {
149 : return uri;
150 : }
151 :
152 : // If authority differs, we could remove the scheme, but it's not worth it.
153 0 : if (uri.hasAuthority != baseUri.hasAuthority) return uri;
154 0 : if (uri.hasAuthority) {
155 0 : if (uri.userInfo != baseUri.userInfo ||
156 0 : uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
157 0 : uri.port != baseUri.port) {
158 : return uri;
159 : }
160 : }
161 :
162 0 : baseUri = _normalizePath(baseUri);
163 0 : List<String> base = baseUri.pathSegments.toList();
164 0 : if (base.isNotEmpty) {
165 0 : base = new List<String>.from(base)..removeLast();
166 : }
167 0 : uri = _normalizePath(uri);
168 0 : List<String> target = uri.pathSegments.toList();
169 0 : if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
170 : int index = 0;
171 0 : while (index < base.length && index < target.length) {
172 0 : if (base[index] != target[index]) {
173 : break;
174 : }
175 0 : index++;
176 : }
177 0 : if (index == base.length) {
178 0 : if (index == target.length) {
179 0 : return new Uri(path: "./");
180 : }
181 0 : return new Uri(path: target.skip(index).join('/'));
182 0 : } else if (index > 0) {
183 0 : return new Uri(
184 0 : path: '../' * (base.length - index) + target.skip(index).join('/'));
185 : } else {
186 : return uri;
187 : }
188 : }
189 :
190 : // TODO: inline to uri.normalizePath() when we move to 1.11
191 0 : Uri _normalizePath(Uri uri) => new Uri().resolveUri(uri);
|