blob: d5c79895ecbd4fa374e6e50d0359e64cf00d99fb [file] [log] [blame]
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +01001// Copyright (c) 2012, 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
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +01005// ignore_for_file: prefer_single_quotes
6
7// This code is copied from an older version of package:package_config - and
8// kept here until we completely abandon the old .packages file.
9// See: https://github.com/dart-lang/package_config/blob/04b9abec2627dfaf9b7ec39c31a3b03f06ed9be7/lib/packages_file.dart
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/// If [allowDefaultPackage] is set to true, an entry with an empty package name
26/// is accepted. This entry does not correspond to a package, but instead
27/// represents a *default package* which non-package libraries may be considered
28/// part of in some cases. The value of that entry must be a valid package name.
29///
30/// Returns a simple mapping from package name to package location.
31/// If default package is allowed, the map maps the empty string to the default package's name.
32Map<String, Uri> parse(List<int> source, Uri baseLocation,
33 {bool allowDefaultPackage = false}) {
34 var index = 0;
35 var result = <String, Uri>{};
36 while (index < source.length) {
37 var isComment = false;
38 var start = index;
39 var separatorIndex = -1;
40 var end = source.length;
41 var char = source[index++];
42 if (char == $cr || char == $lf) {
43 continue;
44 }
45 if (char == $colon) {
46 if (!allowDefaultPackage) {
47 throw FormatException("Missing package name", source, index - 1);
48 }
49 separatorIndex = index - 1;
50 }
51 isComment = char == $hash;
52 while (index < source.length) {
53 char = source[index++];
54 if (char == $colon && separatorIndex < 0) {
55 separatorIndex = index - 1;
56 } else if (char == $cr || char == $lf) {
57 end = index - 1;
58 break;
59 }
60 }
61 if (isComment) continue;
62 if (separatorIndex < 0) {
63 throw FormatException("No ':' on line", source, index - 1);
64 }
65 var packageName = String.fromCharCodes(source, start, separatorIndex);
66 if (packageName.isEmpty
67 ? !allowDefaultPackage
68 : !isValidPackageName(packageName)) {
69 throw FormatException("Not a valid package name", packageName, 0);
70 }
71 var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
72 Uri packageLocation;
73 if (packageName.isEmpty) {
74 if (!isValidPackageName(packageValue)) {
75 throw FormatException(
76 "Default package entry value is not a valid package name");
77 }
78 packageLocation = Uri(path: packageValue);
79 } else {
80 packageLocation = baseLocation.resolve(packageValue);
81 if (!packageLocation.path.endsWith('/')) {
82 packageLocation =
Sigurd Meldgaard6aeb1792022-06-07 14:27:16 +020083 packageLocation.replace(path: "${packageLocation.path}/");
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +010084 }
85 }
86 if (result.containsKey(packageName)) {
87 if (packageName.isEmpty) {
88 throw FormatException(
89 "More than one default package entry", source, start);
90 }
91 throw FormatException("Same package name occured twice", source, start);
92 }
93 result[packageName] = packageLocation;
94 }
95 return result;
96}
97
98/// Writes the mapping to a [StringSink].
99///
100/// If [comment] is provided, the output will contain this comment
101/// with `# ` in front of each line.
102/// Lines are defined as ending in line feed (`'\n'`). If the final
103/// line of the comment doesn't end in a line feed, one will be added.
104///
105/// If [baseUri] is provided, package locations will be made relative
106/// to the base URI, if possible, before writing.
107///
108/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
109/// empty string mapping to the _default package name_.
110///
111/// All the keys of [packageMapping] must be valid package names,
112/// and the values must be URIs that do not have the `package:` scheme.
113void write(StringSink output, Map<String, Uri> packageMapping,
Sarah Zakariasbb7e25b2021-09-22 14:25:30 +0200114 {Uri? baseUri, String? comment, bool allowDefaultPackage = false}) {
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100115 ArgumentError.checkNotNull(allowDefaultPackage, 'allowDefaultPackage');
116
117 if (baseUri != null && !baseUri.isAbsolute) {
118 throw ArgumentError.value(baseUri, "baseUri", "Must be absolute");
119 }
120
121 if (comment != null) {
122 var lines = comment.split('\n');
123 if (lines.last.isEmpty) lines.removeLast();
124 for (var commentLine in lines) {
125 output.write('# ');
126 output.writeln(commentLine);
127 }
128 } else {
129 output.write("# generated by package:package_config at ");
130 output.write(DateTime.now());
131 output.writeln();
132 }
133
134 packageMapping.forEach((String packageName, Uri uri) {
135 // If [packageName] is empty then [uri] is the _default package name_.
136 if (allowDefaultPackage && packageName.isEmpty) {
137 final defaultPackageName = uri.toString();
138 if (!isValidPackageName(defaultPackageName)) {
139 throw ArgumentError.value(
140 defaultPackageName,
141 'defaultPackageName',
142 '"$defaultPackageName" is not a valid package name',
143 );
144 }
145 output.write(':');
146 output.write(defaultPackageName);
147 output.writeln();
148 return;
149 }
150 // Validate packageName.
151 if (!isValidPackageName(packageName)) {
152 throw ArgumentError('"$packageName" is not a valid package name');
153 }
154 if (uri.scheme == "package") {
155 throw ArgumentError.value(
156 "Package location must not be a package: URI", uri.toString());
157 }
158 output.write(packageName);
159 output.write(':');
160 // If baseUri provided, make uri relative.
161 if (baseUri != null) {
162 uri = _relativize(uri, baseUri);
163 }
164 if (!uri.path.endsWith('/')) {
Sigurd Meldgaard6aeb1792022-06-07 14:27:16 +0200165 uri = uri.replace(path: '${uri.path}/');
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100166 }
167 output.write(uri);
168 output.writeln();
169 });
170}
171
172// All ASCII characters that are valid in a package name, with space
173// for all the invalid ones (including space).
174const String _validPackageNameCharacters =
175 r" ! $ &'()*+,-. 0123456789 ; = "
176 r"@ABCDEFGHIJKLMNOPQRSTUVWXYZ _ abcdefghijklmnopqrstuvwxyz ~ ";
177
178/// Tests whether something is a valid Dart package name.
179bool isValidPackageName(String string) {
180 return checkPackageName(string) < 0;
181}
182
183/// Check if a string is a valid package name.
184///
185/// Valid package names contain only characters in [_validPackageNameCharacters]
186/// and must contain at least one non-'.' character.
187///
188/// Returns `-1` if the string is valid.
189/// Otherwise returns the index of the first invalid character,
190/// or `string.length` if the string contains no non-'.' character.
191int checkPackageName(String string) {
192 // Becomes non-zero if any non-'.' character is encountered.
193 var nonDot = 0;
194 for (var i = 0; i < string.length; i++) {
195 var c = string.codeUnitAt(i);
196 if (c > 0x7f || _validPackageNameCharacters.codeUnitAt(c) <= $space) {
197 return i;
198 }
199 nonDot += c ^ $dot;
200 }
201 if (nonDot == 0) return string.length;
202 return -1;
203}
204
205/// Validate that a [Uri] is a valid `package:` URI.
206///
207/// Used to validate user input.
208///
209/// Returns the package name extracted from the package URI,
210/// which is the path segment between `package:` and the first `/`.
211String checkValidPackageUri(Uri packageUri, String name) {
212 if (packageUri.scheme != "package") {
213 throw PackageConfigArgumentError(packageUri, name, "Not a package: URI");
214 }
215 if (packageUri.hasAuthority) {
216 throw PackageConfigArgumentError(
217 packageUri, name, "Package URIs must not have a host part");
218 }
219 if (packageUri.hasQuery) {
220 // A query makes no sense if resolved to a file: URI.
221 throw PackageConfigArgumentError(
222 packageUri, name, "Package URIs must not have a query part");
223 }
224 if (packageUri.hasFragment) {
225 // We could leave the fragment after the URL when resolving,
226 // but it would be odd if "package:foo/foo.dart#1" and
227 // "package:foo/foo.dart#2" were considered different libraries.
228 // Keep the syntax open in case we ever get multiple libraries in one file.
229 throw PackageConfigArgumentError(
230 packageUri, name, "Package URIs must not have a fragment part");
231 }
232 if (packageUri.path.startsWith('/')) {
233 throw PackageConfigArgumentError(
234 packageUri, name, "Package URIs must not start with a '/'");
235 }
236 var firstSlash = packageUri.path.indexOf('/');
237 if (firstSlash == -1) {
238 throw PackageConfigArgumentError(packageUri, name,
239 "Package URIs must start with the package name followed by a '/'");
240 }
241 var packageName = packageUri.path.substring(0, firstSlash);
242 var badIndex = checkPackageName(packageName);
243 if (badIndex >= 0) {
244 if (packageName.isEmpty) {
245 throw PackageConfigArgumentError(
246 packageUri, name, "Package names mus be non-empty");
247 }
248 if (badIndex == packageName.length) {
249 throw PackageConfigArgumentError(packageUri, name,
250 "Package names must contain at least one non-'.' character");
251 }
252 assert(badIndex < packageName.length);
253 var badCharCode = packageName.codeUnitAt(badIndex);
Sigurd Meldgaard6aeb1792022-06-07 14:27:16 +0200254 var badChar = "U+${badCharCode.toRadixString(16).padLeft(4, '0')}";
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100255 if (badCharCode >= 0x20 && badCharCode <= 0x7e) {
256 // Printable character.
257 badChar = "'${packageName[badIndex]}' ($badChar)";
258 }
259 throw PackageConfigArgumentError(
260 packageUri, name, "Package names must not contain $badChar");
261 }
262 return packageName;
263}
264
265/// Checks whether URI is just an absolute directory.
266///
267/// * It must have a scheme.
268/// * It must not have a query or fragment.
269/// * The path must end with `/`.
270bool isAbsoluteDirectoryUri(Uri uri) {
271 if (uri.hasQuery) return false;
272 if (uri.hasFragment) return false;
273 if (!uri.hasScheme) return false;
274 var path = uri.path;
275 if (!path.endsWith("/")) return false;
276 return true;
277}
278
279/// Whether the former URI is a prefix of the latter.
280bool isUriPrefix(Uri prefix, Uri path) {
281 assert(!prefix.hasFragment);
282 assert(!prefix.hasQuery);
283 assert(!path.hasQuery);
284 assert(!path.hasFragment);
285 assert(prefix.path.endsWith('/'));
286 return path.toString().startsWith(prefix.toString());
287}
288
289/// Finds the first non-JSON-whitespace character in a file.
290///
291/// Used to heuristically detect whether a file is a JSON file or an .ini file.
292int firstNonWhitespaceChar(List<int> bytes) {
293 for (var i = 0; i < bytes.length; i++) {
294 var char = bytes[i];
295 if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
296 return char;
297 }
298 }
299 return -1;
300}
301
302/// Attempts to return a relative path-only URI for [uri].
303///
304/// First removes any query or fragment part from [uri].
305///
306/// If [uri] is already relative (has no scheme), it's returned as-is.
307/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
308/// as the [uri] instead.
309///
310/// If the [uri] has a scheme or authority part which differs from
311/// the [baseUri], or if there is no overlap in the paths of the
312/// two URIs at all, the [uri] is returned as-is.
313///
314/// Otherwise the result is a path-only URI which satsifies
315/// `baseUri.resolveUri(result) == uri`,
316///
317/// The `baseUri` must be absolute.
Sarah Zakariasbb7e25b2021-09-22 14:25:30 +0200318Uri relativizeUri(Uri uri, Uri? baseUri) {
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100319 if (baseUri == null) return uri;
320 assert(baseUri.isAbsolute);
321 if (uri.hasQuery || uri.hasFragment) {
322 uri = Uri(
323 scheme: uri.scheme,
324 userInfo: uri.hasAuthority ? uri.userInfo : null,
325 host: uri.hasAuthority ? uri.host : null,
326 port: uri.hasAuthority ? uri.port : null,
327 path: uri.path);
328 }
329
330 // Already relative. We assume the caller knows what they are doing.
331 if (!uri.isAbsolute) return uri;
332
333 if (baseUri.scheme != uri.scheme) {
334 return uri;
335 }
336
337 // If authority differs, we could remove the scheme, but it's not worth it.
338 if (uri.hasAuthority != baseUri.hasAuthority) return uri;
339 if (uri.hasAuthority) {
340 if (uri.userInfo != baseUri.userInfo ||
341 uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
342 uri.port != baseUri.port) {
343 return uri;
344 }
345 }
346
347 baseUri = baseUri.normalizePath();
348 var base = [...baseUri.pathSegments];
349 if (base.isNotEmpty) base.removeLast();
350 uri = uri.normalizePath();
351 var target = [...uri.pathSegments];
352 if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
353 var index = 0;
354 while (index < base.length && index < target.length) {
355 if (base[index] != target[index]) {
356 break;
357 }
358 index++;
359 }
360 if (index == base.length) {
361 if (index == target.length) {
362 return Uri(path: "./");
363 }
364 return Uri(path: target.skip(index).join('/'));
365 } else if (index > 0) {
366 var buffer = StringBuffer();
367 for (var n = base.length - index; n > 0; --n) {
368 buffer.write("../");
369 }
370 buffer.writeAll(target.skip(index), "/");
371 return Uri(path: buffer.toString());
372 } else {
373 return uri;
374 }
375}
376
377/// Attempts to return a relative URI for [uri].
378///
379/// The result URI satisfies `baseUri.resolveUri(result) == uri`,
380/// but may be relative.
381/// The `baseUri` must be absolute.
382Uri _relativize(Uri uri, Uri baseUri) {
383 assert(baseUri.isAbsolute);
384 if (uri.hasQuery || uri.hasFragment) {
385 uri = Uri(
386 scheme: uri.scheme,
387 userInfo: uri.hasAuthority ? uri.userInfo : null,
388 host: uri.hasAuthority ? uri.host : null,
389 port: uri.hasAuthority ? uri.port : null,
390 path: uri.path);
391 }
392
393 // Already relative. We assume the caller knows what they are doing.
394 if (!uri.isAbsolute) return uri;
395
396 if (baseUri.scheme != uri.scheme) {
397 return uri;
398 }
399
400 // If authority differs, we could remove the scheme, but it's not worth it.
401 if (uri.hasAuthority != baseUri.hasAuthority) return uri;
402 if (uri.hasAuthority) {
403 if (uri.userInfo != baseUri.userInfo ||
404 uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
405 uri.port != baseUri.port) {
406 return uri;
407 }
408 }
409
410 baseUri = baseUri.normalizePath();
411 var base = baseUri.pathSegments.toList();
412 if (base.isNotEmpty) {
413 base = List<String>.from(base)..removeLast();
414 }
415 uri = uri.normalizePath();
416 var target = uri.pathSegments.toList();
417 if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
418 var index = 0;
419 while (index < base.length && index < target.length) {
420 if (base[index] != target[index]) {
421 break;
422 }
423 index++;
424 }
425 if (index == base.length) {
426 if (index == target.length) {
427 return Uri(path: "./");
428 }
429 return Uri(path: target.skip(index).join('/'));
430 } else if (index > 0) {
431 return Uri(
432 path: '../' * (base.length - index) + target.skip(index).join('/'));
433 } else {
434 return uri;
435 }
436}
437
438// Character constants used by this package.
439/// "Line feed" control character.
440const int $lf = 0x0a;
441
442/// "Carriage return" control character.
443const int $cr = 0x0d;
444
445/// Space character.
446const int $space = 0x20;
447
448/// Character `#`.
449const int $hash = 0x23;
450
451/// Character `.`.
452const int $dot = 0x2e;
453
454/// Character `:`.
455const int $colon = 0x3a;
456
457/// Character `?`.
458const int $question = 0x3f;
459
460/// Character `{`.
461const int $lbrace = 0x7b;
462
463/// General superclass of most errors and exceptions thrown by this package.
464///
465/// Only covers errors thrown while parsing package configuration files.
466/// Programming errors and I/O exceptions are not covered.
467abstract class PackageConfigError {
468 PackageConfigError._();
469}
470
471class PackageConfigArgumentError extends ArgumentError
472 implements PackageConfigError {
Sarah Zakariasbb7e25b2021-09-22 14:25:30 +0200473 PackageConfigArgumentError(Object? value, String name, String message)
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100474 : super.value(value, name, message);
475
476 PackageConfigArgumentError.from(ArgumentError error)
477 : super.value(error.invalidValue, error.name, error.message);
478}
479
480class PackageConfigFormatException extends FormatException
481 implements PackageConfigError {
Sarah Zakariasbb7e25b2021-09-22 14:25:30 +0200482 PackageConfigFormatException(String message, Object? source, [int? offset])
Sigurd Meldgaard392a3cb2021-01-05 16:22:13 +0100483 : super(message, source, offset);
484
485 PackageConfigFormatException.from(FormatException exception)
486 : super(exception.message, exception.source, exception.offset);
487}