Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 1 | // 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 Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 5 | // 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. |
| 32 | Map<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 Meldgaard | 6aeb179 | 2022-06-07 14:27:16 +0200 | [diff] [blame] | 83 | packageLocation.replace(path: "${packageLocation.path}/"); |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 84 | } |
| 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. |
| 113 | void write(StringSink output, Map<String, Uri> packageMapping, |
Sarah Zakarias | bb7e25b | 2021-09-22 14:25:30 +0200 | [diff] [blame] | 114 | {Uri? baseUri, String? comment, bool allowDefaultPackage = false}) { |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 115 | 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 Meldgaard | 6aeb179 | 2022-06-07 14:27:16 +0200 | [diff] [blame] | 165 | uri = uri.replace(path: '${uri.path}/'); |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 166 | } |
| 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). |
| 174 | const String _validPackageNameCharacters = |
| 175 | r" ! $ &'()*+,-. 0123456789 ; = " |
| 176 | r"@ABCDEFGHIJKLMNOPQRSTUVWXYZ _ abcdefghijklmnopqrstuvwxyz ~ "; |
| 177 | |
| 178 | /// Tests whether something is a valid Dart package name. |
| 179 | bool 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. |
| 191 | int 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 `/`. |
| 211 | String 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 Meldgaard | 6aeb179 | 2022-06-07 14:27:16 +0200 | [diff] [blame] | 254 | var badChar = "U+${badCharCode.toRadixString(16).padLeft(4, '0')}"; |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 255 | 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 `/`. |
| 270 | bool 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. |
| 280 | bool 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. |
| 292 | int 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 Zakarias | bb7e25b | 2021-09-22 14:25:30 +0200 | [diff] [blame] | 318 | Uri relativizeUri(Uri uri, Uri? baseUri) { |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 319 | 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. |
| 382 | Uri _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. |
| 440 | const int $lf = 0x0a; |
| 441 | |
| 442 | /// "Carriage return" control character. |
| 443 | const int $cr = 0x0d; |
| 444 | |
| 445 | /// Space character. |
| 446 | const int $space = 0x20; |
| 447 | |
| 448 | /// Character `#`. |
| 449 | const int $hash = 0x23; |
| 450 | |
| 451 | /// Character `.`. |
| 452 | const int $dot = 0x2e; |
| 453 | |
| 454 | /// Character `:`. |
| 455 | const int $colon = 0x3a; |
| 456 | |
| 457 | /// Character `?`. |
| 458 | const int $question = 0x3f; |
| 459 | |
| 460 | /// Character `{`. |
| 461 | const 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. |
| 467 | abstract class PackageConfigError { |
| 468 | PackageConfigError._(); |
| 469 | } |
| 470 | |
| 471 | class PackageConfigArgumentError extends ArgumentError |
| 472 | implements PackageConfigError { |
Sarah Zakarias | bb7e25b | 2021-09-22 14:25:30 +0200 | [diff] [blame] | 473 | PackageConfigArgumentError(Object? value, String name, String message) |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 474 | : super.value(value, name, message); |
| 475 | |
| 476 | PackageConfigArgumentError.from(ArgumentError error) |
| 477 | : super.value(error.invalidValue, error.name, error.message); |
| 478 | } |
| 479 | |
| 480 | class PackageConfigFormatException extends FormatException |
| 481 | implements PackageConfigError { |
Sarah Zakarias | bb7e25b | 2021-09-22 14:25:30 +0200 | [diff] [blame] | 482 | PackageConfigFormatException(String message, Object? source, [int? offset]) |
Sigurd Meldgaard | 392a3cb | 2021-01-05 16:22:13 +0100 | [diff] [blame] | 483 | : super(message, source, offset); |
| 484 | |
| 485 | PackageConfigFormatException.from(FormatException exception) |
| 486 | : super(exception.message, exception.source, exception.offset); |
| 487 | } |