|  | // Copyright (c) 2012, 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 'dart:async'; | 
|  | import 'dart:convert'; | 
|  | import 'dart:io' as io; | 
|  | import 'dart:io'; | 
|  | import 'dart:typed_data'; | 
|  |  | 
|  | import 'package:collection/collection.dart' show IterableExtension, maxBy; | 
|  | import 'package:crypto/crypto.dart'; | 
|  | import 'package:http/http.dart' as http; | 
|  | import 'package:meta/meta.dart'; | 
|  | import 'package:path/path.dart' as p; | 
|  | import 'package:pub_semver/pub_semver.dart'; | 
|  | import 'package:stack_trace/stack_trace.dart'; | 
|  |  | 
|  | import '../authentication/client.dart'; | 
|  | import '../crc32c.dart'; | 
|  | import '../exceptions.dart'; | 
|  | import '../http.dart'; | 
|  | import '../io.dart'; | 
|  | import '../language_version.dart'; | 
|  | import '../log.dart' as log; | 
|  | import '../package.dart'; | 
|  | import '../package_name.dart'; | 
|  | import '../pubspec.dart'; | 
|  | import '../rate_limited_scheduler.dart'; | 
|  | import '../source.dart'; | 
|  | import '../system_cache.dart'; | 
|  | import '../utils.dart'; | 
|  | import 'cached.dart'; | 
|  | import 'root.dart'; | 
|  |  | 
|  | const contentHashesDocumentationUrl = 'https://dart.dev/go/content-hashes'; | 
|  |  | 
|  | /// Validates and normalizes a [hostedUrl] which is pointing to a pub server. | 
|  | /// | 
|  | /// A [hostedUrl] is a URL pointing to a _hosted pub server_ as defined by the | 
|  | /// [repository-spec-v2][1]. The default value is `pub.dev`, and can be | 
|  | /// overwritten using `PUB_HOSTED_URL`. It can also specified for individual | 
|  | /// hosted-dependencies in `pubspec.yaml`, and for the root package using the | 
|  | /// `publish_to` key. | 
|  | /// | 
|  | /// The [hostedUrl] is always normalized to a [Uri] with path that ends in slash | 
|  | /// unless the path is merely `/`, in which case we normalize to the bare | 
|  | /// domain. | 
|  | /// | 
|  | /// We change `https://pub.dartlang.org` to `https://pub.dev`, this  maintains | 
|  | /// backwards compatibility with `pubspec.lock`-files which contain | 
|  | /// `https://pub.dartlang.org`. | 
|  | /// | 
|  | /// Throws [FormatException] if there is anything wrong with [hostedUrl]. | 
|  | /// | 
|  | /// [1]: ../../../doc/repository-spec-v2.md | 
|  | Uri validateAndNormalizeHostedUrl(String hostedUrl) { | 
|  | Uri u; | 
|  | try { | 
|  | u = Uri.parse(hostedUrl); | 
|  | } on FormatException catch (e) { | 
|  | throw FormatException( | 
|  | 'invalid url: ${e.message}', | 
|  | e.source, | 
|  | e.offset, | 
|  | ); | 
|  | } | 
|  | if (!u.hasScheme || (u.scheme != 'http' && u.scheme != 'https')) { | 
|  | throw FormatException('url scheme must be https:// or http://', hostedUrl); | 
|  | } | 
|  | if (!u.hasAuthority || u.host == '') { | 
|  | throw FormatException('url must have a hostname', hostedUrl); | 
|  | } | 
|  | if (u.userInfo != '') { | 
|  | throw FormatException('user-info is not supported in url', hostedUrl); | 
|  | } | 
|  | if (u.hasQuery) { | 
|  | throw FormatException('querystring is not supported in url', hostedUrl); | 
|  | } | 
|  | if (u.hasFragment) { | 
|  | throw FormatException('fragment is not supported in url', hostedUrl); | 
|  | } | 
|  | u = u.normalizePath(); | 
|  | // If we have a path of only `/` | 
|  | if (u.path == '/') { | 
|  | u = u.replace(path: ''); | 
|  | } | 
|  | // If there is a path, and it doesn't end in a slash we normalize to slash | 
|  | if (u.path.isNotEmpty && !u.path.endsWith('/')) { | 
|  | u = u.replace(path: '${u.path}/'); | 
|  | } | 
|  | // pub.dev and pub.dartlang.org are identical. | 
|  | // | 
|  | // We rewrite here to avoid caching both, and to avoid having different | 
|  | // credentials for these two. | 
|  | // | 
|  | // Changing this to pub.dev raises the following concerns: | 
|  | // | 
|  | //  1. It would blow through users caches. | 
|  | //  2. It would cause conflicts for users checking pubspec.lock into git, if | 
|  | //     using different versions of the dart-sdk / pub client. | 
|  | //  3. It might cause other problems (investigation needed) for pubspec.lock | 
|  | //     across different versions of the dart-sdk / pub client. | 
|  | //  4. It would expand the API surface we're committed to supporting | 
|  | //     long-term. | 
|  | // | 
|  | // Clearly, a bit of investigation is necessary before we update this to | 
|  | // pub.dev, it might be attractive to do next time we change the server API. | 
|  | if (u == Uri.parse('https://pub.dartlang.org')) { | 
|  | log.fine('Using https://pub.dev instead of https://pub.dartlang.org.'); | 
|  | u = Uri.parse('https://pub.dev'); | 
|  | } | 
|  | if (runningFromTest && | 
|  | u == Uri.parse('https://pub.dev') && | 
|  | Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL')) { | 
|  | u = Uri.parse(Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']!); | 
|  | } | 
|  | return u; | 
|  | } | 
|  |  | 
|  | /// A package source that gets packages from a package hosting site that uses | 
|  | /// the same API as pub.dev. | 
|  | class HostedSource extends CachedSource { | 
|  | static HostedSource instance = HostedSource._(); | 
|  |  | 
|  | HostedSource._(); | 
|  |  | 
|  | @override | 
|  | final name = 'hosted'; | 
|  | @override | 
|  | final hasMultipleVersions = true; | 
|  |  | 
|  | static String pubDevUrl = 'https://pub.dev'; | 
|  | static String pubDartlangUrl = 'https://pub.dartlang.org'; | 
|  |  | 
|  | static bool isPubDevUrl(String url) { | 
|  | final parsedUrl = Uri.parse(url); | 
|  | if (parsedUrl.scheme != 'http' && parsedUrl.scheme != 'https') { | 
|  | // A non http(s) url is not pub.dev. | 
|  | return false; | 
|  | } | 
|  | if (parsedUrl.host.isEmpty) { | 
|  | // The empty host is not pub.dev. | 
|  | return false; | 
|  | } | 
|  | final origin = parsedUrl.origin; | 
|  | // Allow the defaultHostedUrl to be overriden when running from tests | 
|  | if (runningFromTest && | 
|  | io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] != null) { | 
|  | return origin == io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']; | 
|  | } | 
|  | return origin == pubDevUrl || origin == pubDartlangUrl; | 
|  | } | 
|  |  | 
|  | static bool isFromPubDev(PackageId id) { | 
|  | final description = id.description.description; | 
|  | return description is HostedDescription && isPubDevUrl(description.url); | 
|  | } | 
|  |  | 
|  | /// Gets the default URL for the package server for hosted dependencies. | 
|  | late final String defaultUrl = () { | 
|  | // Changing this to pub.dev raises the following concerns: | 
|  | // | 
|  | //  1. It would blow through users caches. | 
|  | //  2. It would cause conflicts for users checking pubspec.lock into git, if | 
|  | //     using different versions of the dart-sdk / pub client. | 
|  | //  3. It might cause other problems (investigation needed) for pubspec.lock | 
|  | //     across different versions of the dart-sdk / pub client. | 
|  | //  4. It would expand the API surface we're committed to supporting | 
|  | //     long-term. | 
|  | // | 
|  | // Clearly, a bit of investigation is necessary before we update this to | 
|  | // pub.dev, it might be attractive to do next time we change the server API. | 
|  | try { | 
|  | var defaultHostedUrl = 'https://pub.dev'; | 
|  | // Allow the defaultHostedUrl to be overriden when running from tests | 
|  | if (runningFromTest) { | 
|  | defaultHostedUrl = | 
|  | io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] ?? | 
|  | defaultHostedUrl; | 
|  | } | 
|  | return validateAndNormalizeHostedUrl( | 
|  | io.Platform.environment['PUB_HOSTED_URL'] ?? defaultHostedUrl, | 
|  | ).toString(); | 
|  | } on FormatException catch (e) { | 
|  | throw ConfigException( | 
|  | 'Invalid `PUB_HOSTED_URL="${e.source}"`: ${e.message}', | 
|  | ); | 
|  | } | 
|  | }(); | 
|  |  | 
|  | /// Whether extra metadata headers should be sent for HTTP requests to a given | 
|  | /// [url]. | 
|  | static bool shouldSendAdditionalMetadataFor(Uri url) { | 
|  | if (runningFromTest && Platform.environment.containsKey('PUB_HOSTED_URL')) { | 
|  | if (url.origin != Platform.environment['PUB_HOSTED_URL']) { | 
|  | return false; | 
|  | } | 
|  | } else { | 
|  | if (!HostedSource.isPubDevUrl(url.toString())) return false; | 
|  | } | 
|  |  | 
|  | if (Platform.environment.containsKey('CI') && | 
|  | Platform.environment['CI'] != 'false') { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /// Returns a reference to a hosted package named [name]. | 
|  | /// | 
|  | /// If [url] is passed, it's the URL of the pub server from which the package | 
|  | /// should be downloaded. [url] will be normalized and validated using | 
|  | /// [validateAndNormalizeHostedUrl]. This can throw a [FormatException]. | 
|  | PackageRef refFor(String name, {String? url}) { | 
|  | final d = HostedDescription(name, url ?? defaultUrl); | 
|  | return PackageRef(name, d); | 
|  | } | 
|  |  | 
|  | /// Ensures that [description] is a valid hosted package description. | 
|  | /// | 
|  | /// Simple hosted dependencies only consist of a plain string, which is | 
|  | /// resolved against the default host. In this case, [description] will be | 
|  | /// null. | 
|  | /// | 
|  | /// Hosted dependencies may also specify a custom host from which the package | 
|  | /// is fetched. There are two syntactic forms of those dependencies: | 
|  | /// | 
|  | ///  1. With an url and an optional name in a map: `hosted: {url: <url>}` | 
|  | ///  2. With a direct url: `hosted: <url>` | 
|  | @override | 
|  | PackageRef parseRef( | 
|  | String name, | 
|  | Object? description, { | 
|  | required Description containingDescription, | 
|  | required LanguageVersion languageVersion, | 
|  | }) { | 
|  | return PackageRef( | 
|  | name, | 
|  | _parseDescription(name, description, languageVersion), | 
|  | ); | 
|  | } | 
|  |  | 
|  | @override | 
|  | PackageId parseId( | 
|  | String name, | 
|  | Version version, | 
|  | Object? description, { | 
|  | String? containingDir, | 
|  | }) { | 
|  | // Old pub versions only wrote `description: <pkg>` into the lock file. | 
|  | if (description is String) { | 
|  | if (description != name) { | 
|  | throw const FormatException( | 
|  | 'The description should be the same as the name', | 
|  | ); | 
|  | } | 
|  | return PackageId( | 
|  | name, | 
|  | version, | 
|  | ResolvedHostedDescription( | 
|  | HostedDescription(name, defaultUrl), | 
|  | sha256: null, | 
|  | ), | 
|  | ); | 
|  | } | 
|  | if (description is! Map) { | 
|  | throw const FormatException( | 
|  | 'The description should be a string or a map.', | 
|  | ); | 
|  | } | 
|  | final url = description['url']; | 
|  | if (url is! String) { | 
|  | throw const FormatException('The url should be a string.'); | 
|  | } | 
|  | final sha256 = description['sha256']; | 
|  | if (sha256 != null && sha256 is! String) { | 
|  | throw const FormatException('The sha256 should be a string.'); | 
|  | } | 
|  | final foundName = description['name']; | 
|  | if (foundName is! String) { | 
|  | throw const FormatException('The name should be a string.'); | 
|  | } | 
|  | if (foundName != name) { | 
|  | throw FormatException('The name should be $name'); | 
|  | } | 
|  | return PackageId( | 
|  | name, | 
|  | version, | 
|  | ResolvedHostedDescription( | 
|  | HostedDescription(name, url), | 
|  | sha256: _parseContentHash(sha256 as String?), | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Decodes a sha256 hash from a lock-file or package-listing. | 
|  | /// It is expected to be a hex-encoded String of length 64. | 
|  | /// | 
|  | /// Throws a [FormatException] if the string cannot be decoded. | 
|  | Uint8List? _parseContentHash(String? encoded) { | 
|  | if (encoded == null) return null; | 
|  | if (encoded.length != 64) { | 
|  | throw const FormatException('Content-hash has incorrect length'); | 
|  | } | 
|  | try { | 
|  | return hexDecode(encoded); | 
|  | } on FormatException catch (e) { | 
|  | return throw FormatException( | 
|  | 'Badly formatted content-hash: ${e.message}', | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Parses the description for a package. | 
|  | /// | 
|  | /// If the package parses correctly, this returns a (name, url) pair. If not, | 
|  | /// this throws a descriptive FormatException. | 
|  | HostedDescription _parseDescription( | 
|  | String packageName, | 
|  | Object? description, | 
|  | LanguageVersion languageVersion, | 
|  | ) { | 
|  | if (description == null) { | 
|  | // Simple dependency without a `hosted` block, use the default server. | 
|  | return HostedDescription(packageName, defaultUrl); | 
|  | } | 
|  |  | 
|  | final canUseShorthandSyntax = languageVersion.supportsShorterHostedSyntax; | 
|  |  | 
|  | if (description is String) { | 
|  | // Old versions of pub (pre Dart 2.15) interpret `hosted: foo` as | 
|  | // `hosted: {name: foo, url: <default>}`. | 
|  | // For later versions, we treat it as `hosted: {name: <inferred>, | 
|  | // url: foo}` if a user opts in by raising their min SDK environment. | 
|  | // | 
|  | // Since the old behavior is very rarely used and we want to show a | 
|  | // helpful error message if the new syntax is used without raising the SDK | 
|  | // environment, we throw an error if something that looks like a URI is | 
|  | // used as a package name. | 
|  | if (canUseShorthandSyntax) { | 
|  | return HostedDescription(packageName, description); | 
|  | } else { | 
|  | if (_looksLikePackageName.hasMatch(description)) { | 
|  | // Valid use of `hosted: package` dependency with an old SDK | 
|  | // environment. | 
|  | return HostedDescription(description, defaultUrl); | 
|  | } else { | 
|  | const shorterSyntaxVersion = | 
|  | LanguageVersion.firstVersionWithShorterHostedSyntax; | 
|  | throw FormatException( | 
|  | 'Using `hosted: <url>` is only supported with a minimum SDK ' | 
|  | 'constraint of $shorterSyntaxVersion.', | 
|  | ); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (description is! Map) { | 
|  | throw const FormatException( | 
|  | 'The description must be a package name or map.', | 
|  | ); | 
|  | } | 
|  |  | 
|  | var name = description['name']; | 
|  | if (canUseShorthandSyntax) name ??= packageName; | 
|  |  | 
|  | if (name is! String) { | 
|  | const shorterSyntaxVersion = | 
|  | LanguageVersion.firstVersionWithShorterHostedSyntax; | 
|  | throw FormatException( | 
|  | "The 'name' key must have a string value without " | 
|  | 'a minimum Dart SDK constraint of $shorterSyntaxVersion.0 or higher.', | 
|  | ); | 
|  | } | 
|  |  | 
|  | final u = description['url']; | 
|  | if (u != null && u is! String) { | 
|  | throw const FormatException("The 'url' key must be a string value."); | 
|  | } | 
|  | final url = u ?? defaultUrl; | 
|  |  | 
|  | if (languageVersion.forbidsUnknownDescriptionKeys) { | 
|  | for (final key in description.keys) { | 
|  | if (!['url', 'name'].contains(key)) { | 
|  | throw FormatException( | 
|  | 'Unknown key "$key" in description.', | 
|  | ); | 
|  | } | 
|  | } | 
|  | } | 
|  | return HostedDescription(name, url as String); | 
|  | } | 
|  |  | 
|  | static final RegExp _looksLikePackageName = | 
|  | RegExp(r'^[a-zA-Z_]+[a-zA-Z0-9_]*$'); | 
|  |  | 
|  | late final RateLimitedScheduler<_RefAndCache, List<_VersionInfo>> _scheduler = | 
|  | RateLimitedScheduler( | 
|  | _fetchVersions, | 
|  | maxConcurrentOperations: 10, | 
|  | ); | 
|  |  | 
|  | List<_VersionInfo> _versionInfoFromPackageListing( | 
|  | Map body, | 
|  | PackageRef ref, | 
|  | Uri location, | 
|  | SystemCache cache, | 
|  | ) { | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final versions = body['versions']; | 
|  | if (versions is! List) { | 
|  | throw const FormatException('versions must be a list'); | 
|  | } | 
|  | return versions.map((map) { | 
|  | if (map is! Map) { | 
|  | throw const FormatException('versions list element must be a map'); | 
|  | } | 
|  | final pubspecData = map['pubspec']; | 
|  | if (pubspecData is! Map) { | 
|  | throw const FormatException('pubspec must be a map'); | 
|  | } | 
|  | final pubspec = Pubspec.fromMap( | 
|  | pubspecData, | 
|  | cache.sources, | 
|  | expectedName: ref.name, | 
|  | location: location, | 
|  | containingDescription: description, | 
|  | ); | 
|  | final archiveSha256 = map['archive_sha256']; | 
|  | if (archiveSha256 != null && archiveSha256 is! String) { | 
|  | throw const FormatException('archive_sha256 must be a String'); | 
|  | } | 
|  | final archiveUrl = map['archive_url']; | 
|  | if (archiveUrl is! String) { | 
|  | throw const FormatException('archive_url must be a String'); | 
|  | } | 
|  | final isDiscontinued = body['isDiscontinued'] ?? false; | 
|  | if (isDiscontinued is! bool) { | 
|  | throw const FormatException('isDiscontinued must be a bool'); | 
|  | } | 
|  | final replacedBy = body['replacedBy']; | 
|  | if (replacedBy is! String?) { | 
|  | throw const FormatException('replacedBy must be a String'); | 
|  | } | 
|  | final retracted = map['retracted'] ?? false; | 
|  | if (retracted is! bool) { | 
|  | throw const FormatException('retracted must be a bool'); | 
|  | } | 
|  | DateTime? advisoriesDate; | 
|  | final advisoriesUpdated = body['advisoriesUpdated']; | 
|  | if (advisoriesUpdated != null) { | 
|  | if (advisoriesUpdated is! String) { | 
|  | throw const FormatException('advisoriesUpdated must be a String'); | 
|  | } | 
|  | advisoriesDate = DateTime.parse(advisoriesUpdated); | 
|  | } | 
|  |  | 
|  | final status = PackageStatus( | 
|  | isDiscontinued: isDiscontinued, | 
|  | discontinuedReplacedBy: replacedBy, | 
|  | isRetracted: retracted, | 
|  | advisoriesUpdated: advisoriesDate, | 
|  | ); | 
|  | return _VersionInfo( | 
|  | pubspec.version, | 
|  | pubspec, | 
|  | Uri.parse(archiveUrl), | 
|  | status, | 
|  | _parseContentHash(archiveSha256 as String?), | 
|  | ); | 
|  | }).toList(); | 
|  | } | 
|  |  | 
|  | Future<List<_VersionInfo>> _fetchVersionsNoPrefetching( | 
|  | PackageRef ref, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final description = ref.description; | 
|  |  | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final packageName = description.packageName; | 
|  | final hostedUrl = description.url; | 
|  | final url = _listVersionsUrl(ref); | 
|  | log.io('Get versions from $url.'); | 
|  |  | 
|  | final String bodyText; | 
|  | final dynamic body; | 
|  | final List<_VersionInfo> result; | 
|  | try { | 
|  | // TODO(sigurdm): Implement cancellation of requests. This probably | 
|  | // requires resolution of: https://github.com/dart-lang/http/issues/424. | 
|  | bodyText = await withAuthenticatedClient(cache, Uri.parse(hostedUrl), | 
|  | (client) async { | 
|  | return await retryForHttp( | 
|  | 'fetching versions for "$packageName" from "$url"', () async { | 
|  | final request = http.Request('GET', url); | 
|  | request.attachPubApiHeaders(); | 
|  | request.attachMetadataHeaders(); | 
|  | final response = await client.fetch(request); | 
|  | return response.body; | 
|  | }); | 
|  | }); | 
|  | final decoded = jsonDecode(bodyText); | 
|  | if (decoded is! Map<String, dynamic>) { | 
|  | throw const FormatException('version listing must be a mapping'); | 
|  | } | 
|  | body = decoded; | 
|  | result = _versionInfoFromPackageListing( | 
|  | body as Map<String, dynamic>, | 
|  | ref, | 
|  | url, | 
|  | cache, | 
|  | ); | 
|  | } on Exception catch (error, stackTrace) { | 
|  | _throwFriendlyError(error, stackTrace, packageName, hostedUrl); | 
|  | } | 
|  |  | 
|  | // Cache the response on disk. | 
|  | // Don't cache overly big responses. | 
|  | if (bodyText.length < 1000 * 1024) { | 
|  | await _cacheVersionListingResponse(body, ref, cache); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | Future<List<_VersionInfo>> _fetchVersions(_RefAndCache refAndCache) async { | 
|  | final ref = refAndCache.ref; | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final preschedule = | 
|  | Zone.current[_prefetchingKey] as void Function(_RefAndCache)?; | 
|  |  | 
|  | /// Prefetch the dependencies of the latest version, we are likely to need | 
|  | /// them later. | 
|  | void prescheduleDependenciesOfLatest( | 
|  | List<_VersionInfo>? listing, | 
|  | SystemCache cache, | 
|  | ) { | 
|  | if (listing == null || listing.isEmpty) return; | 
|  | final latestVersion = | 
|  | maxBy<_VersionInfo, Version>(listing, (e) => e.version)!; | 
|  | final dependencies = latestVersion.pubspec.dependencies.values; | 
|  | unawaited( | 
|  | withDependencyType(DependencyType.none, () async { | 
|  | for (final packageRange in dependencies) { | 
|  | if (packageRange.source is HostedSource) { | 
|  | preschedule!(_RefAndCache(packageRange.toRef(), cache)); | 
|  | } | 
|  | } | 
|  | }), | 
|  | ); | 
|  | } | 
|  |  | 
|  | final cache = refAndCache.cache; | 
|  | if (preschedule != null) { | 
|  | /// If we have a cached response - preschedule dependencies of that. | 
|  | prescheduleDependenciesOfLatest( | 
|  | await _cachedVersionListingResponse(ref, cache), | 
|  | cache, | 
|  | ); | 
|  | } | 
|  | final result = await _fetchVersionsNoPrefetching(ref, cache); | 
|  |  | 
|  | if (preschedule != null) { | 
|  | // Preschedule the dependencies from the actual response. | 
|  | // This might overlap with those from the cached response. But the | 
|  | // scheduler ensures each listing will be fetched at most once. | 
|  | prescheduleDependenciesOfLatest(result, cache); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | Future<List<Advisory>?> _fetchAdvisories( | 
|  | PackageRef ref, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  |  | 
|  | final packageName = description.packageName; | 
|  | final hostedUrl = description.url; | 
|  |  | 
|  | final url = _listAdvisoriesUrl(ref); | 
|  | log.io('Fetching security advisories from $url.'); | 
|  |  | 
|  | final String bodyText; | 
|  | final Map<String, dynamic> body; | 
|  | final List<Advisory>? result; | 
|  | try { | 
|  | bodyText = await withAuthenticatedClient(cache, Uri.parse(hostedUrl), | 
|  | (client) async { | 
|  | return await retryForHttp( | 
|  | 'fetching advisories for "$packageName" from "$url"', () async { | 
|  | final request = http.Request('GET', url); | 
|  | request.attachPubApiHeaders(); | 
|  | request.attachMetadataHeaders(); | 
|  | final response = await client.fetch(request); | 
|  | return response.body; | 
|  | }); | 
|  | }); | 
|  | final decoded = jsonDecode(bodyText); | 
|  | if (decoded is! Map<String, dynamic>) { | 
|  | throw const FormatException('security advisories must be a mapping'); | 
|  | } | 
|  | body = decoded; | 
|  | result = _extractAdvisoryDetailsForPackage(decoded, ref.name); | 
|  | } on FormatException catch (error, stackTrace) { | 
|  | log.warning( | 
|  | 'Failed to decode advisories for $packageName from $hostedUrl.\n' | 
|  | '$error\n' | 
|  | '${Chain.forTrace(stackTrace)}'); | 
|  | return null; | 
|  | } on PubHttpResponseException catch (error, stackTrace) { | 
|  | if (isPubDevUrl(hostedUrl)) { | 
|  | fail( | 
|  | 'Failed to fetch advisories for "$packageName" from "$hostedUrl".\n', | 
|  | error, | 
|  | stackTrace, | 
|  | ); | 
|  | } else { | 
|  | log.warning( | 
|  | 'Warning: Unable to fetch advisories for "$packageName" ' | 
|  | 'from "$hostedUrl".\n', | 
|  | ); | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Cache the response on disk. | 
|  | // Don't cache overly big responses. | 
|  | if (bodyText.length < 5000 * 1024) { | 
|  | await _cacheAdvisoriesResponse(body, ref, cache); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /// Extracts relevant details from [body] about advisories affecting the | 
|  | /// package named [packageName]. The details are used when generating output | 
|  | /// for runs such as `dart pub upgrade`. | 
|  | List<Advisory>? _extractAdvisoryDetailsForPackage( | 
|  | Map body, | 
|  | String packageName, | 
|  | ) { | 
|  | final advisoriesList = <Advisory>[]; | 
|  |  | 
|  | final advisories = body['advisories']; | 
|  | if (advisories is! List) { | 
|  | throw const FormatException('advisories must be a list'); | 
|  | } | 
|  |  | 
|  | final advisoriesUpdated = body['advisoriesUpdated']; | 
|  | if (advisoriesUpdated is! String) { | 
|  | throw const FormatException('advisoriesUpdated must be a String'); | 
|  | } | 
|  |  | 
|  | for (final advisory in advisories) { | 
|  | if (advisory is! Map) { | 
|  | throw const FormatException('advisory must be a map'); | 
|  | } | 
|  |  | 
|  | final databaseSpecific = advisory['database_specific']; | 
|  | if (databaseSpecific is! Map?) { | 
|  | throw const FormatException('database_specific must be a map or null'); | 
|  | } | 
|  |  | 
|  | final pubDisplayUrl = databaseSpecific?['pub_display_url']; | 
|  | if (pubDisplayUrl is! String?) { | 
|  | throw const FormatException('pub_display_url must be a String or null'); | 
|  | } | 
|  |  | 
|  | final id = advisory['id']; | 
|  | if (id is! String) { | 
|  | throw const FormatException('id must be a String'); | 
|  | } | 
|  |  | 
|  | final summary = advisory['summary']; | 
|  | if (summary is! String) { | 
|  | throw const FormatException('summary must be a String'); | 
|  | } | 
|  |  | 
|  | final aliasIDs = <String>[]; | 
|  | final aliases = advisory['aliases']; | 
|  | if (aliases is! List) { | 
|  | throw const FormatException('aliases must be a list'); | 
|  | } | 
|  |  | 
|  | for (final id in aliases) { | 
|  | if (id is! String) { | 
|  | throw const FormatException('alias IDs must be a string'); | 
|  | } | 
|  | aliasIDs.add(id); | 
|  | } | 
|  |  | 
|  | final affectedPackages = advisory['affected']; | 
|  | if (affectedPackages is! List) { | 
|  | throw const FormatException('affectedPackages must be a list'); | 
|  | } | 
|  |  | 
|  | bool matchesNameAndEcosystem( | 
|  | dynamic affectedPackage, | 
|  | String name, | 
|  | String ecosystem, | 
|  | ) { | 
|  | if (affectedPackage is! Map) { | 
|  | throw const FormatException('`affected` must be a map'); | 
|  | } | 
|  | final package = affectedPackage['package']; | 
|  | if (package is! Map) { | 
|  | throw const FormatException('`package` must be a map'); | 
|  | } | 
|  | final affectedName = package['name']; | 
|  | if (affectedName is! String) { | 
|  | throw const FormatException('package name must be a String'); | 
|  | } | 
|  | if (affectedName == name) { | 
|  | final affectedEcosystem = package['ecosystem']; | 
|  | if (affectedEcosystem is! String) { | 
|  | throw const FormatException('ecosystem must be a String'); | 
|  | } | 
|  | return affectedEcosystem.toLowerCase() == ecosystem; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | for (final affectedPackage in affectedPackages) { | 
|  | if (affectedPackage is! Map) { | 
|  | throw const FormatException('`affected` must be a list of maps'); | 
|  | } | 
|  | if (matchesNameAndEcosystem(affectedPackage, packageName, 'pub')) { | 
|  | final affectedVersions = <String>{}; | 
|  | final versions = affectedPackage['versions']; | 
|  | if (versions is! List) { | 
|  | throw const FormatException('package versions must be a list'); | 
|  | } | 
|  |  | 
|  | for (final v in versions) { | 
|  | if (v is! String) { | 
|  | throw const FormatException( | 
|  | 'package version elements must be a string', | 
|  | ); | 
|  | } | 
|  | affectedVersions.add(v); | 
|  | } | 
|  |  | 
|  | advisoriesList.add( | 
|  | Advisory( | 
|  | id: id, | 
|  | affectedVersions: affectedVersions, | 
|  | aliases: aliasIDs, | 
|  | summary: summary, | 
|  | pubDisplayUrl: pubDisplayUrl, | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return advisoriesList; | 
|  | } | 
|  |  | 
|  | Future<List<Advisory>?> _getAdvisories( | 
|  | PackageId id, | 
|  | SystemCache cache, | 
|  | Duration? maxAge, | 
|  | ) async { | 
|  | final advisoriesUpdated = | 
|  | (await status(id.toRef(), id.version, cache, maxAge: maxAge)) | 
|  | .advisoriesUpdated; | 
|  | if (advisoriesUpdated == null) return null; | 
|  |  | 
|  | Future<List<Advisory>?> readAdvisoriesFromCache() async { | 
|  | final advisoriesCachePath = _advisoriesCachePath(id.toRef(), cache); | 
|  | final stat = io.File(advisoriesCachePath).statSync(); | 
|  |  | 
|  | if (stat.type == io.FileSystemEntityType.file) { | 
|  | if (advisoriesUpdated.isAfter(stat.modified)) { | 
|  | tryDeleteEntry(advisoriesCachePath); | 
|  | return null; | 
|  | } | 
|  |  | 
|  | try { | 
|  | final doc = jsonDecode(readTextFile(advisoriesCachePath)); | 
|  | if (doc is! Map) { | 
|  | throw const FormatException('Broken cached advisories response'); | 
|  | } | 
|  |  | 
|  | final cachedAdvisoriesUpdated = doc['advisoriesUpdated']; | 
|  | if (cachedAdvisoriesUpdated is! String) { | 
|  | throw const FormatException('Broken cached advisories response'); | 
|  | } | 
|  | final parsedCacheAdvisoriesUpdated = | 
|  | DateTime.parse(cachedAdvisoriesUpdated); | 
|  | final advisoriesUpdated = | 
|  | (await status(id.toRef(), id.version, cache)).advisoriesUpdated; | 
|  |  | 
|  | if ( | 
|  | // We could not obtain the timestamp of latest advisory update. | 
|  | advisoriesUpdated == null || | 
|  | // The cached entry is too old. | 
|  | advisoriesUpdated.isAfter(parsedCacheAdvisoriesUpdated)) { | 
|  | tryDeleteEntry(advisoriesCachePath); | 
|  | } else { | 
|  | return _extractAdvisoryDetailsForPackage(doc, id.toRef().name); | 
|  | } | 
|  | } on io.IOException { | 
|  | // Could not read the file. Delete if it exists. | 
|  | tryDeleteEntry(advisoriesCachePath); | 
|  | } on FormatException catch (e) { | 
|  | tryDeleteEntry(advisoriesCachePath); | 
|  | log.fine('Failed to read cached advisories: $e'); | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | return await readAdvisoriesFromCache() ?? | 
|  | await _fetchAdvisories(id.toRef(), cache); | 
|  | } | 
|  |  | 
|  | /// An in-memory cache to store the cached version listing loaded from | 
|  | /// [_versionListingCachePath]. | 
|  | /// | 
|  | /// Invariant: Entries in this cache are the parsed version of the exact same | 
|  | /// information cached on disk. I.e. if the entry is present in this cache, | 
|  | /// there will not be a newer version on disk. | 
|  | final Map<PackageRef, (DateTime, List<_VersionInfo>)> _responseCache = {}; | 
|  |  | 
|  | /// If a cached version listing response for [ref] exists on disk and is less | 
|  | /// than [maxAge] old it is parsed and returned. | 
|  | /// | 
|  | /// Otherwise deletes a cached response if it exists and returns `null`. | 
|  | /// | 
|  | /// If [maxAge] is not given, we will try to get the cached version no matter | 
|  | /// how old it is. | 
|  | Future<List<_VersionInfo>?> _cachedVersionListingResponse( | 
|  | PackageRef ref, | 
|  | SystemCache cache, { | 
|  | Duration? maxAge, | 
|  | }) async { | 
|  | final cachedInfo = _responseCache[ref]; | 
|  | if (cachedInfo != null) { | 
|  | final (cacheTimestamp, versionInfo) = cachedInfo; | 
|  | final cacheAge = DateTime.now().difference(cacheTimestamp); | 
|  | if (maxAge == null || maxAge > cacheAge) { | 
|  | // The cached value is not too old. | 
|  | return versionInfo; | 
|  | } | 
|  | } | 
|  | final cachePath = _versionListingCachePath(ref, cache); | 
|  | final stat = io.File(cachePath).statSync(); | 
|  | final now = DateTime.now(); | 
|  | if (stat.type == io.FileSystemEntityType.file) { | 
|  | if (maxAge == null || now.difference(stat.modified) < maxAge) { | 
|  | try { | 
|  | final cachedDoc = jsonDecode(readTextFile(cachePath)); | 
|  | if (cachedDoc is! Map) { | 
|  | throw const FormatException( | 
|  | 'Broken cached version listing response', | 
|  | ); | 
|  | } | 
|  | final timestamp = cachedDoc['_fetchedAt']; | 
|  | if (timestamp is! String) { | 
|  | throw const FormatException( | 
|  | 'Broken cached version listing response', | 
|  | ); | 
|  | } | 
|  | final parsedTimestamp = DateTime.parse(timestamp); | 
|  | final cacheAge = DateTime.now().difference(parsedTimestamp); | 
|  | if (maxAge != null && cacheAge > maxAge) { | 
|  | // Too old according to internal timestamp - delete. | 
|  | tryDeleteEntry(cachePath); | 
|  | } else { | 
|  | final res = _versionInfoFromPackageListing( | 
|  | cachedDoc, | 
|  | ref, | 
|  | Uri.file(cachePath), | 
|  | cache, | 
|  | ); | 
|  | _responseCache[ref] = (parsedTimestamp, res); | 
|  | return res; | 
|  | } | 
|  | } on io.IOException { | 
|  | // Could not read the file. Delete if it exists. | 
|  | tryDeleteEntry(cachePath); | 
|  | } on FormatException { | 
|  | // Decoding error - bad file or bad timestamp. Delete the file. | 
|  | tryDeleteEntry(cachePath); | 
|  | } | 
|  | } else { | 
|  | // File too old | 
|  | tryDeleteEntry(cachePath); | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | Future<void> _cacheAdvisoriesResponse( | 
|  | Map<String, dynamic> body, | 
|  | PackageRef ref, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final path = _advisoriesCachePath(ref, cache); | 
|  | try { | 
|  | ensureDir(p.dirname(path)); | 
|  |  | 
|  | writeTextFile( | 
|  | path, | 
|  | jsonEncode( | 
|  | <String, dynamic>{ | 
|  | ...body, | 
|  | }, | 
|  | ), | 
|  | ); | 
|  | } on io.IOException catch (e) { | 
|  | // Not being able to write this cache is not fatal. Just move on... | 
|  | log.fine('Failed writing cache file. $e'); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Saves the (decoded) response from package-listing of [ref]. | 
|  | Future<void> _cacheVersionListingResponse( | 
|  | Map<String, dynamic> body, | 
|  | PackageRef ref, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final path = _versionListingCachePath(ref, cache); | 
|  | try { | 
|  | ensureDir(p.dirname(path)); | 
|  | await writeTextFileAsync( | 
|  | path, | 
|  | jsonEncode( | 
|  | <String, dynamic>{ | 
|  | ...body, | 
|  | '_fetchedAt': DateTime.now().toIso8601String(), | 
|  | }, | 
|  | ), | 
|  | ); | 
|  | // Delete the entry in the in-memory cache to maintain the invariant that | 
|  | // cached information in memory is the same as that on the disk. | 
|  | _responseCache.remove(ref); | 
|  | } on io.IOException catch (e) { | 
|  | // Not being able to write this cache is not fatal. Just move on... | 
|  | log.fine('Failed writing cache file. $e'); | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<PackageStatus> status( | 
|  | PackageRef ref, | 
|  | Version version, | 
|  | SystemCache cache, { | 
|  | Duration? maxAge, | 
|  | }) async { | 
|  | // If we don't have the specific version we return the empty response, since | 
|  | // it is more or less harmless.. | 
|  | // | 
|  | // This can happen if the connection is broken, or the server is faulty. | 
|  | // We want to avoid a crash | 
|  | // | 
|  | // TODO(sigurdm): Consider representing the non-existence of the | 
|  | // package-version in the return value. | 
|  | return (await _versionInfo(ref, version, cache, maxAge: maxAge))?.status ?? | 
|  | PackageStatus(); | 
|  | } | 
|  |  | 
|  | Future<_VersionInfo?> _versionInfo( | 
|  | PackageRef ref, | 
|  | Version version, | 
|  | SystemCache cache, { | 
|  | Duration? maxAge, | 
|  | }) async { | 
|  | if (cache.isOffline) { | 
|  | // Do we have a cached version response on disk? | 
|  | final versionListing = await _cachedVersionListingResponse(ref, cache); | 
|  |  | 
|  | if (versionListing == null) { | 
|  | return null; | 
|  | } | 
|  | return versionListing.firstWhereOrNull((l) => l.version == version); | 
|  | } | 
|  | // Did we already get info for this package? | 
|  | var versionListing = _scheduler.peek(_RefAndCache(ref, cache)); | 
|  | if (maxAge != null) { | 
|  | // Do we have a cached version response on disk? | 
|  | versionListing ??= | 
|  | await _cachedVersionListingResponse(ref, cache, maxAge: maxAge); | 
|  | } | 
|  | // Otherwise retrieve the info from the host. | 
|  | versionListing ??= await _scheduler | 
|  | .schedule(_RefAndCache(ref, cache)) | 
|  | // Failures retrieving the listing here should just be ignored. | 
|  | .catchError( | 
|  | (_) async => <_VersionInfo>[], | 
|  | test: (error) => error is Exception, | 
|  | ); | 
|  |  | 
|  | return versionListing.firstWhereOrNull((l) => l.version == version); | 
|  | } | 
|  |  | 
|  | // The path to .cache directory inside the pub cache | 
|  | String _cacheDirPath(PackageRef ref, SystemCache cache) { | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final dir = _urlToDirectory(description.url); | 
|  | // Use a dot-dir because older versions of pub won't choke on that | 
|  | // name when iterating the cache (it is not listed by [listDir]). | 
|  | return p.join( | 
|  | cache.rootDirForSource(this), | 
|  | dir, | 
|  | _versionListingDirectory, | 
|  | ); | 
|  | } | 
|  |  | 
|  | static const _versionListingDirectory = '.cache'; | 
|  |  | 
|  | // The path where the response from the package-listing api is cached. | 
|  | String _versionListingCachePath(PackageRef ref, SystemCache cache) { | 
|  | return p.join( | 
|  | _cacheDirPath(ref, cache), | 
|  | '${ref.name}-versions.json', | 
|  | ); | 
|  | } | 
|  |  | 
|  | // The path where the response from the advisories api is cached. | 
|  | String _advisoriesCachePath(PackageRef ref, SystemCache cache) { | 
|  | return p.join( | 
|  | _cacheDirPath(ref, cache), | 
|  | '${ref.name}-advisories.json', | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Downloads a list of all versions of a package that are available from the | 
|  | /// site. | 
|  | @override | 
|  | Future<List<PackageId>> doGetVersions( | 
|  | PackageRef ref, | 
|  | Duration? maxAge, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | if (cache.isOffline) { | 
|  | final url = description.url; | 
|  | final root = cache.rootDirForSource(HostedSource.instance); | 
|  | final dir = p.join(root, _urlToDirectory(url)); | 
|  | log.io('Finding versions of ${ref.name} in $dir'); | 
|  | List<PackageId> offlineVersions; | 
|  | if (dirExists(dir)) { | 
|  | offlineVersions = listDir(dir) | 
|  | .where(_looksLikePackageDir) | 
|  | .map((entry) => _idForBasename(p.basename(entry), url)) | 
|  | .where((id) => id.name == ref.name && id.version != Version.none) | 
|  | .toList(); | 
|  | } else { | 
|  | offlineVersions = []; | 
|  | } | 
|  |  | 
|  | // If there are no versions in the cache, report a clearer error. | 
|  | if (offlineVersions.isEmpty) { | 
|  | throw PackageNotFoundException( | 
|  | 'could not find package ${ref.name} in cache', | 
|  | hint: 'Try again without --offline!', | 
|  | ); | 
|  | } | 
|  |  | 
|  | return offlineVersions; | 
|  | } | 
|  | var versionListing = _scheduler.peek(_RefAndCache(ref, cache)); | 
|  | if (maxAge != null) { | 
|  | // Do we have a cached version response on disk? | 
|  | versionListing ??= | 
|  | await _cachedVersionListingResponse(ref, cache, maxAge: maxAge); | 
|  | } | 
|  | versionListing ??= await _scheduler.schedule(_RefAndCache(ref, cache)); | 
|  | return versionListing | 
|  | .map( | 
|  | (i) => PackageId( | 
|  | ref.name, | 
|  | i.version, | 
|  | ResolvedHostedDescription( | 
|  | ref.description as HostedDescription, | 
|  | sha256: i.archiveSha256, | 
|  | ), | 
|  | ), | 
|  | ) | 
|  | .toList(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<List<Advisory>?>? getAdvisoriesForPackage( | 
|  | PackageId id, | 
|  | SystemCache cache, | 
|  | Duration? maxAge, | 
|  | ) { | 
|  | return _getAdvisories(id, cache, maxAge); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<List<Advisory>?>? getAdvisoriesForPackageVersion( | 
|  | PackageId id, | 
|  | SystemCache cache, | 
|  | Duration? maxAge, | 
|  | ) async { | 
|  | final advisories = await getAdvisoriesForPackage(id, cache, maxAge); | 
|  | if (advisories == null) return null; | 
|  |  | 
|  | return advisories | 
|  | .where( | 
|  | (advisory) => advisory.affectedVersions | 
|  | .contains(id.version.canonicalizedVersion), | 
|  | ) | 
|  | .toList(); | 
|  | } | 
|  |  | 
|  | /// Parses [ref] into its hosted description and package name components. | 
|  | (HostedDescription, String) _parseRef(PackageRef ref) { | 
|  | final description = ref.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final package = Uri.encodeComponent(ref.name); | 
|  | return (description, package); | 
|  | } | 
|  |  | 
|  | /// Parses [ref] and computes the api url for listing versions of the given | 
|  | /// package. | 
|  | Uri _listVersionsUrl(PackageRef ref) { | 
|  | final (description, package) = _parseRef(ref); | 
|  | return Uri.parse(description.url).resolve('api/packages/$package'); | 
|  | } | 
|  |  | 
|  | /// Parses [ref] and computes the api url for getting security advisories for | 
|  | /// a given package. | 
|  | Uri _listAdvisoriesUrl(PackageRef ref) { | 
|  | final (description, package) = _parseRef(ref); | 
|  | return Uri.parse(description.url) | 
|  | .resolve('api/packages/$package/advisories'); | 
|  | } | 
|  |  | 
|  | /// Retrieves the pubspec for a specific version of a package that is | 
|  | /// available from the site. | 
|  | @override | 
|  | Future<Pubspec> describeUncached(PackageId id, SystemCache cache) async { | 
|  | if (cache.isOffline) { | 
|  | throw PackageNotFoundException( | 
|  | '${id.name} ${id.version} is not available in cache', | 
|  | hint: 'Try again without --offline!', | 
|  | ); | 
|  | } | 
|  | final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache)); | 
|  | final url = _listVersionsUrl(id.toRef()); | 
|  | return versions.firstWhereOrNull((i) => i.version == id.version)?.pubspec ?? | 
|  | (throw PackageNotFoundException('Could not find package $id at $url')); | 
|  | } | 
|  |  | 
|  | /// Downloads the package identified by [id] to the system cache if needed. | 
|  | /// | 
|  | /// Validates that the content hash of [id] corresponds to what is already in | 
|  | /// cache, if not the file is redownloaded. | 
|  | @override | 
|  | Future<DownloadPackageResult> downloadToSystemCache( | 
|  | PackageId id, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | var didUpdate = false; | 
|  | final packageDir = getDirectoryInCache(id, cache); | 
|  |  | 
|  | // Use the content-hash from the version-info to compare with what we | 
|  | // already downloaded. | 
|  | // | 
|  | // The content-hash from [id] will be compared with that when the lockfile | 
|  | // is written. | 
|  | // | 
|  | // We allow the version-listing to be a few days outdated in order for `pub | 
|  | // get` with an existing working resolution and everything in cache to be | 
|  | // fast. | 
|  | final versionInfo = await _versionInfo( | 
|  | id.toRef(), | 
|  | id.version, | 
|  | cache, | 
|  | maxAge: const Duration(days: 3), | 
|  | ); | 
|  |  | 
|  | final expectedContentHash = versionInfo?.archiveSha256 ?? | 
|  | // Handling of legacy server - we use the hash from the id (typically | 
|  | // from the lockfile) to compare to the existing download. | 
|  | (id.description as ResolvedHostedDescription).sha256; | 
|  | Uint8List? contentHash; | 
|  | if (!fileExists(hashPath(id, cache))) { | 
|  | if (dirExists(packageDir) && !cache.isOffline) { | 
|  | log.fine( | 
|  | 'Cache entry for ${id.name}-${id.version} has no content-hash ' | 
|  | '- redownloading.', | 
|  | ); | 
|  | deleteEntry(packageDir); | 
|  | } | 
|  | } else if (expectedContentHash == null) { | 
|  | // Can happen with a legacy server combined with a legacy lock file. | 
|  | log.fine( | 
|  | 'Content-hash of ${id.name}-${id.version} not known from resolution.', | 
|  | ); | 
|  | } else { | 
|  | final hashFromCache = sha256FromCache(id, cache); | 
|  | if (!fixedTimeBytesEquals(hashFromCache, expectedContentHash)) { | 
|  | log.warning( | 
|  | 'Cached version of ${id.name}-${id.version} has wrong hash ' | 
|  | '- redownloading.', | 
|  | ); | 
|  | if (cache.isOffline) { | 
|  | fail('Cannot redownload while offline. Try again without --offline.'); | 
|  | } | 
|  | deleteEntry(packageDir); | 
|  | } else { | 
|  | contentHash = hashFromCache; | 
|  | } | 
|  | } | 
|  | if (dirExists(packageDir)) { | 
|  | contentHash ??= sha256FromCache(id, cache); | 
|  | } else { | 
|  | didUpdate = true; | 
|  | if (cache.isOffline) { | 
|  | fail( | 
|  | 'Missing package ${id.name}-${id.version}. ' | 
|  | 'Try again without --offline.', | 
|  | ); | 
|  | } | 
|  | contentHash = await _download(id, packageDir, cache); | 
|  | } | 
|  | return DownloadPackageResult( | 
|  | PackageId( | 
|  | id.name, | 
|  | id.version, | 
|  | (id.description as ResolvedHostedDescription).withSha256(contentHash), | 
|  | ), | 
|  | didUpdate: didUpdate, | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// The system cache directory for the hosted source contains subdirectories | 
|  | /// for each separate repository URL that's used on the system. | 
|  | /// | 
|  | /// Each of these subdirectories then contains a subdirectory for each | 
|  | /// package downloaded from that site. | 
|  | @override | 
|  | String getDirectoryInCache(PackageId id, SystemCache cache) { | 
|  | final description = id.description.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final rootDir = cache.rootDirForSource(this); | 
|  |  | 
|  | final dir = _urlToDirectory(description.url); | 
|  | return p.join(rootDir, dir, '${id.name}-${id.version}'); | 
|  | } | 
|  |  | 
|  | /// The system cache directory for the hosted source contains subdirectories | 
|  | /// for each separate repository URL that's used on the system. | 
|  | /// | 
|  | /// Parallel to this there is a `hosted-hashes` directory with a stored hash | 
|  | /// of all downloaded packages. | 
|  | String hashPath(PackageId id, SystemCache cache) { | 
|  | final description = id.description.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | final rootDir = cache.rootDir; | 
|  |  | 
|  | final serverDir = _urlToDirectory(description.url); | 
|  | return p.join( | 
|  | rootDir, | 
|  | 'hosted-hashes', | 
|  | serverDir, | 
|  | '${id.name}-${id.version}.sha256', | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Loads the hash at `hashPath(id)`. | 
|  | Uint8List? sha256FromCache(PackageId id, SystemCache cache) { | 
|  | try { | 
|  | return _parseContentHash(readTextFile(hashPath(id, cache))); | 
|  | } on io.IOException { | 
|  | return null; | 
|  | } on FormatException catch (e) { | 
|  | log.fine('Bad content-hash in cache: $e, ignoring cache entry'); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Re-downloads all packages that have been previously downloaded into the | 
|  | /// system cache from any server. | 
|  | @override | 
|  | Future<Iterable<RepairResult>> repairCachedPackages(SystemCache cache) async { | 
|  | final rootDir = cache.rootDirForSource(this); | 
|  | if (!dirExists(rootDir)) return []; | 
|  |  | 
|  | return (await Future.wait( | 
|  | listDir(rootDir).map((serverDir) async { | 
|  | final directory = p.basename(serverDir); | 
|  | late final String url; | 
|  | try { | 
|  | url = _directoryToUrl(directory); | 
|  | } on FormatException { | 
|  | log.error('Unable to detect hosted url from directory: $directory'); | 
|  | // If _directoryToUrl can't interpret a directory name, | 
|  | // we just silently ignore it and hope it's because it | 
|  | // comes from a newer version of pub. | 
|  | // | 
|  | // This is most likely because someone manually modified PUB_CACHE. | 
|  | return <RepairResult>[]; | 
|  | } | 
|  |  | 
|  | final results = <RepairResult>[]; | 
|  | final packages = <Package>[]; | 
|  | for (var entry in listDir(serverDir)) { | 
|  | try { | 
|  | packages.add( | 
|  | Package.load( | 
|  | entry, | 
|  | loadPubspec: Pubspec.loadRootWithSources(cache.sources), | 
|  | ), | 
|  | ); | 
|  | } catch (error, stackTrace) { | 
|  | log.error('Failed to load package', error, stackTrace); | 
|  | final id = _idForBasename( | 
|  | p.basename(entry), | 
|  | url, | 
|  | ); | 
|  | results.add( | 
|  | RepairResult( | 
|  | id.name, | 
|  | id.version, | 
|  | this, | 
|  | success: false, | 
|  | ), | 
|  | ); | 
|  | tryDeleteEntry(entry); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Delete the cached package listings. | 
|  | tryDeleteEntry(p.join(serverDir, _versionListingDirectory)); | 
|  |  | 
|  | packages.sort(Package.orderByNameAndVersion); | 
|  |  | 
|  | return results | 
|  | ..addAll( | 
|  | await Future.wait( | 
|  | packages.map((package) async { | 
|  | final id = PackageId( | 
|  | package.name, | 
|  | package.version, | 
|  | ResolvedHostedDescription( | 
|  | HostedDescription._(package.name, url), | 
|  | sha256: null, | 
|  | ), | 
|  | ); | 
|  | try { | 
|  | deleteEntry(package.dir); | 
|  | await _download(id, package.dir, cache); | 
|  | return RepairResult(id.name, id.version, this, success: true); | 
|  | } catch (error, stackTrace) { | 
|  | var message = 'Failed to repair ${log.bold(package.name)} ' | 
|  | '${package.version}'; | 
|  | if (url != defaultUrl) message += ' from $url'; | 
|  | log.error('$message. Error:\n$error'); | 
|  | log.fine(stackTrace.toString()); | 
|  |  | 
|  | tryDeleteEntry(package.dir); | 
|  | return RepairResult( | 
|  | id.name, | 
|  | id.version, | 
|  | this, | 
|  | success: false, | 
|  | ); | 
|  | } | 
|  | }), | 
|  | ), | 
|  | ); | 
|  | }), | 
|  | )) | 
|  | .expand((x) => x); | 
|  | } | 
|  |  | 
|  | /// Returns the best-guess package ID for [basename], which should be a | 
|  | /// subdirectory in a hosted cache. | 
|  | PackageId _idForBasename(String basename, String url) { | 
|  | final components = split1(basename, '-'); | 
|  | var version = Version.none; | 
|  | if (components.length > 1) { | 
|  | try { | 
|  | version = Version.parse(components.last); | 
|  | } on FormatException { | 
|  | // Default to Version.none. | 
|  | } | 
|  | } | 
|  | final name = components.first; | 
|  | return PackageId( | 
|  | name, | 
|  | version, | 
|  | ResolvedHostedDescription(HostedDescription(name, url), sha256: null), | 
|  | ); | 
|  | } | 
|  |  | 
|  | bool _looksLikePackageDir(String path) { | 
|  | final components = split1(p.basename(path), '-'); | 
|  | if (components.length < 2) return false; | 
|  | try { | 
|  | Version.parse(components.last); | 
|  | } on FormatException { | 
|  | return false; | 
|  | } | 
|  | return dirExists(path); | 
|  | } | 
|  |  | 
|  | /// Gets all of the packages that have been downloaded into the system cache | 
|  | /// from the default server. | 
|  | @override | 
|  | List<Package> getCachedPackages(SystemCache cache) { | 
|  | final root = cache.rootDirForSource(HostedSource.instance); | 
|  | final cacheDir = | 
|  | p.join(root, _urlToDirectory(HostedSource.instance.defaultUrl)); | 
|  | if (!dirExists(cacheDir)) return []; | 
|  |  | 
|  | return listDir(cacheDir) | 
|  | .where(_looksLikePackageDir) | 
|  | .map((entry) { | 
|  | try { | 
|  | return Package.load( | 
|  | entry, | 
|  | loadPubspec: Pubspec.loadRootWithSources(cache.sources), | 
|  | ); | 
|  | } catch (error, stackTrace) { | 
|  | log.fine('Failed to load package from $entry:\n' | 
|  | '$error\n' | 
|  | '${Chain.forTrace(stackTrace)}'); | 
|  | return null; | 
|  | } | 
|  | }) | 
|  | .nonNulls | 
|  | .toList(); | 
|  | } | 
|  |  | 
|  | Future<void> downloadInto( | 
|  | PackageId id, | 
|  | String destPath, | 
|  | SystemCache cache, | 
|  | ) => | 
|  | _download(id, destPath, cache); | 
|  |  | 
|  | /// Downloads package [id] from the archive_url and unpacks it into | 
|  | /// [destPath]. | 
|  | /// | 
|  | /// If there is no archive_url, try to fetch it from | 
|  | /// `$server/packages/$package/versions/$version.tar.gz` where server comes | 
|  | /// from `id.description`. | 
|  | /// | 
|  | /// Returns the content-hash of the downloaded archive. | 
|  | Future<Uint8List> _download( | 
|  | PackageId id, | 
|  | String destPath, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | final description = id.description.description; | 
|  | if (description is! HostedDescription) { | 
|  | throw ArgumentError('Wrong source'); | 
|  | } | 
|  | // We never want to use a cached `archive_url`, so we never attempt to load | 
|  | // the version listing from cache. Besides in most cases we already have | 
|  | // downloaded a fresh copy of the version listing response in the in-memory | 
|  | // cache, so looking in the file-system is pointless. | 
|  | // | 
|  | // We avoid using cached `archive_url` values because the `archive_url` for | 
|  | // a custom package server may include a temporary signature in the | 
|  | // query-string as is the case with signed S3 URLs. And we wish to allow for | 
|  | // such URLs to be used. | 
|  | final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache)); | 
|  | final versionInfo = | 
|  | versions.firstWhereOrNull((i) => i.version == id.version); | 
|  | final packageName = id.name; | 
|  | final version = id.version; | 
|  | late final Uint8List contentHash; | 
|  | if (versionInfo == null) { | 
|  | throw PackageNotFoundException( | 
|  | 'Package $packageName has no version $version', | 
|  | ); | 
|  | } | 
|  |  | 
|  | final archiveUrl = versionInfo.archiveUrl; | 
|  | log.io('Get package from $archiveUrl.'); | 
|  | log.fine('Downloading ${log.bold(id.name)} ${id.version}...'); | 
|  |  | 
|  | // Download and extract the archive to a temp directory. | 
|  | return await withTempDir((tempDirForArchive) async { | 
|  | final fileName = '$packageName-$version.tar.gz'; | 
|  | final archivePath = p.join(tempDirForArchive, fileName); | 
|  |  | 
|  | Stream<List<int>> validateSha256( | 
|  | Stream<List<int>> stream, | 
|  | Digest? expectedHash, | 
|  | ) async* { | 
|  | final output = _SingleValueSink<Digest>(); | 
|  | final input = sha256.startChunkedConversion(output); | 
|  | await for (final v in stream) { | 
|  | input.add(v); | 
|  | yield v; | 
|  | } | 
|  | input.close(); | 
|  | final actualHash = output.value; | 
|  | if (expectedHash != null && output.value != expectedHash) { | 
|  | log.fine( | 
|  | 'Expected content-hash for ${id.name}-${id.version} $expectedHash ' | 
|  | 'actual: ${output.value}.', | 
|  | ); | 
|  | throw PackageIntegrityException(''' | 
|  | Downloaded archive for ${id.name}-${id.version} had wrong content-hash. | 
|  |  | 
|  | This indicates a problem on the package repository: `${description.url}`. | 
|  |  | 
|  | See $contentHashesDocumentationUrl. | 
|  | '''); | 
|  | } | 
|  | contentHash = Uint8List.fromList(actualHash.bytes); | 
|  | writeHash(id, cache, contentHash); | 
|  | } | 
|  |  | 
|  | // It is important that we do not compare against id.description.sha256, | 
|  | // as we need to check against the newly fetched version listing to ensure | 
|  | // that content changes result in updated lockfiles, not failure to | 
|  | // download. | 
|  | final expectedSha256 = versionInfo.archiveSha256; | 
|  |  | 
|  | try { | 
|  | await withAuthenticatedClient(cache, Uri.parse(description.url), | 
|  | (client) async { | 
|  | // In addition to HTTP errors, this will retry crc32c/sha256 errors as | 
|  | // well because [PackageIntegrityException] subclasses | 
|  | // [PubHttpException]. | 
|  | await retryForHttp('downloading "$archiveUrl"', () async { | 
|  | final request = http.Request('GET', archiveUrl); | 
|  | request.attachMetadataHeaders(); | 
|  | final response = await client.fetchAsStream(request); | 
|  |  | 
|  | Stream<List<int>> stream = response.stream; | 
|  | final expectedCrc32c = _parseCrc32c(response.headers, fileName); | 
|  | if (expectedCrc32c != null) { | 
|  | stream = _validateCrc32c( | 
|  | response.stream, | 
|  | expectedCrc32c, | 
|  | id, | 
|  | archiveUrl, | 
|  | ); | 
|  | } | 
|  | stream = validateSha256( | 
|  | stream, | 
|  | (expectedSha256 == null) ? null : Digest(expectedSha256), | 
|  | ); | 
|  |  | 
|  | // We download the archive to disk instead of streaming it directly | 
|  | // into the tar unpacking. This simplifies stream handling. | 
|  | // Package:tar cancels the stream when it reaches end-of-archive, | 
|  | // and cancelling a http stream makes it not reusable. There are | 
|  | // ways around this, and we might revisit this later. | 
|  | await createFileFromStream(stream, archivePath); | 
|  | }); | 
|  | }); | 
|  | } on Exception catch (error, stackTrace) { | 
|  | _throwFriendlyError(error, stackTrace, id.name, description.url); | 
|  | } | 
|  |  | 
|  | final tempDir = cache.createTempDir(); | 
|  | try { | 
|  | try { | 
|  | await extractTarGz(readBinaryFileAsStream(archivePath), tempDir); | 
|  | } on FormatException catch (e) { | 
|  | dataError('Failed to extract `$archivePath`: ${e.message}.'); | 
|  | } | 
|  | ensureDir(p.dirname(destPath)); | 
|  | } catch (e) { | 
|  | deleteEntry(tempDir); | 
|  | rethrow; | 
|  | } | 
|  | // Now that the get has succeeded, move it to the real location in the | 
|  | // cache. | 
|  | // | 
|  | // If this fails with a "directory not empty" exception we assume that | 
|  | // another pub process has installed the same package version while we | 
|  | // downloaded. | 
|  | tryRenameDir(tempDir, destPath); | 
|  | return contentHash; | 
|  | }); | 
|  | } | 
|  |  | 
|  | /// Writes the contenthash for [id] in the cache. | 
|  | void writeHash(PackageId id, SystemCache cache, List<int> bytes) { | 
|  | final path = hashPath(id, cache); | 
|  | ensureDir(p.dirname(path)); | 
|  | writeTextFile( | 
|  | path, | 
|  | hexEncode(bytes), | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Installs a tar.gz file in [archivePath] as if it was downloaded from a | 
|  | /// package repository. | 
|  | /// | 
|  | /// The name, version and repository are decided from the pubspec.yaml that | 
|  | /// must be present in the archive. | 
|  | Future<PackageId> preloadPackage( | 
|  | String archivePath, | 
|  | SystemCache cache, | 
|  | ) async { | 
|  | // Extract to a temp-folder and do atomic rename to preserve the integrity | 
|  | // of the cache. | 
|  | late final Uint8List contentHash; | 
|  |  | 
|  | final tempDir = cache.createTempDir(); | 
|  | final PackageId id; | 
|  | try { | 
|  | try { | 
|  | // We read the file twice, once to compute the hash, and once to extract | 
|  | // the archive. | 
|  | // | 
|  | // It would be desirable to read the file only once, but the tar | 
|  | // extraction closes the stream early making things tricky to get right. | 
|  | contentHash = Uint8List.fromList( | 
|  | (await sha256.bind(readBinaryFileAsStream(archivePath)).first).bytes, | 
|  | ); | 
|  | await extractTarGz(readBinaryFileAsStream(archivePath), tempDir); | 
|  | } on FormatException catch (e) { | 
|  | dataError('Failed to extract `$archivePath`: ${e.message}.'); | 
|  | } | 
|  | if (!fileExists(p.join(tempDir, 'pubspec.yaml'))) { | 
|  | fail( | 
|  | 'Found no `pubspec.yaml` in $archivePath. ' | 
|  | 'Is it a valid pub package archive?', | 
|  | ); | 
|  | } | 
|  | final Pubspec pubspec; | 
|  | try { | 
|  | pubspec = Pubspec.load( | 
|  | tempDir, | 
|  | cache.sources, | 
|  | containingDescription: | 
|  | // Dummy description. As we never use the dependencies, they don't | 
|  | // need to be resolved. | 
|  | RootDescription('.'), | 
|  | ); | 
|  | final errors = pubspec.dependencyErrors; | 
|  | if (errors.isNotEmpty) { | 
|  | throw errors.first; | 
|  | } | 
|  | } on Exception catch (e) { | 
|  | fail('Failed to load `pubspec.yaml` from `$archivePath`: $e.'); | 
|  | } | 
|  | // Reconstruct the PackageId from the extracted pubspec.yaml. | 
|  | id = PackageId( | 
|  | pubspec.name, | 
|  | pubspec.version, | 
|  | ResolvedHostedDescription( | 
|  | HostedDescription(pubspec.name, defaultUrl), | 
|  | sha256: contentHash, | 
|  | ), | 
|  | ); | 
|  | } catch (e) { | 
|  | deleteEntry(tempDir); | 
|  | rethrow; | 
|  | } | 
|  | final packageDir = getDirectoryInCache(id, cache); | 
|  | if (dirExists(packageDir)) { | 
|  | log.fine( | 
|  | 'Cache entry for ${id.name}-${id.version} already exists. Replacing.', | 
|  | ); | 
|  | deleteEntry(packageDir); | 
|  | } | 
|  | tryRenameDir(tempDir, packageDir); | 
|  | writeHash(id, cache, contentHash); | 
|  | return id; | 
|  | } | 
|  |  | 
|  | /// When an error occurs trying to read something about [package] from | 
|  | /// [hostedUrl], this tries to translate into a more user friendly error | 
|  | /// message. | 
|  | /// | 
|  | /// Always throws an error, either the original one or a better one. | 
|  | static Never _throwFriendlyError( | 
|  | Exception error, | 
|  | StackTrace stackTrace, | 
|  | String package, | 
|  | String hostedUrl, | 
|  | ) { | 
|  | if (error is PubHttpResponseException) { | 
|  | if (error.response.statusCode == 404) { | 
|  | throw PackageNotFoundException( | 
|  | 'could not find package $package at $hostedUrl', | 
|  | innerError: error, | 
|  | innerTrace: stackTrace, | 
|  | ); | 
|  | } | 
|  |  | 
|  | fail( | 
|  | '${error.response.statusCode} ${error.response.reasonPhrase} trying ' | 
|  | 'to find package $package at $hostedUrl.', | 
|  | error, | 
|  | stackTrace, | 
|  | ); | 
|  | } else if (error is io.SocketException) { | 
|  | fail( | 
|  | 'Got socket error trying to find package $package at $hostedUrl.', | 
|  | error, | 
|  | stackTrace, | 
|  | ); | 
|  | } else if (error is io.TlsException) { | 
|  | fail( | 
|  | 'Got TLS error trying to find package $package at $hostedUrl.', | 
|  | error, | 
|  | stackTrace, | 
|  | ); | 
|  | } else if (error is AuthenticationException) { | 
|  | String? hint; | 
|  | var message = 'authentication failed'; | 
|  |  | 
|  | assert(error.statusCode == 401 || error.statusCode == 403); | 
|  | if (error.statusCode == 401) { | 
|  | hint = '$hostedUrl package repository requested authentication!\n' | 
|  | 'You can provide credentials using:\n' | 
|  | '    dart pub token add $hostedUrl'; | 
|  | } | 
|  | if (error.statusCode == 403) { | 
|  | hint = 'Insufficient permissions to the resource at the $hostedUrl ' | 
|  | 'package repository.\nYou can modify credentials using:\n' | 
|  | '    dart pub token add $hostedUrl'; | 
|  | message = 'authorization failed'; | 
|  | } | 
|  |  | 
|  | if (error.serverMessage?.isNotEmpty == true && hint != null) { | 
|  | hint += '\n${error.serverMessage}'; | 
|  | } | 
|  |  | 
|  | throw PackageNotFoundException(message, hint: hint); | 
|  | } else if (error is FormatException) { | 
|  | throw PackageNotFoundException( | 
|  | 'Got badly formatted response trying to find ' | 
|  | 'package $package at $hostedUrl', | 
|  | innerError: error, | 
|  | innerTrace: stackTrace, | 
|  | hint: 'Check that "$hostedUrl" is a valid package repository.', | 
|  | ); | 
|  | } else { | 
|  | // Otherwise re-throw the original exception. | 
|  | throw error; | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Enables speculative prefetching of dependencies of packages queried with | 
|  | /// [doGetVersions]. | 
|  | Future<T> withPrefetching<T>(Future<T> Function() callback) async { | 
|  | return await _scheduler.withPrescheduling((preschedule) async { | 
|  | return await runZoned( | 
|  | callback, | 
|  | zoneValues: {_prefetchingKey: preschedule}, | 
|  | ); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /// Key for storing the current prefetch function in the current [Zone]. | 
|  | static const _prefetchingKey = #_prefetch; | 
|  | } | 
|  |  | 
|  | /// The [PackageRef.description] for a [HostedSource], storing the | 
|  | /// [packageName] and resolved [url] of the package server. | 
|  | class HostedDescription extends Description { | 
|  | final String packageName; | 
|  | final String url; | 
|  |  | 
|  | HostedDescription._(this.packageName, this.url); | 
|  |  | 
|  | // This can be used to construct a description with any specific url. | 
|  | factory HostedDescription.raw(String packageName, String url) => | 
|  | HostedDescription._(packageName, url); | 
|  |  | 
|  | factory HostedDescription(String packageName, String url) => | 
|  | HostedDescription._( | 
|  | packageName, | 
|  | validateAndNormalizeHostedUrl(url).toString(), | 
|  | ); | 
|  |  | 
|  | @override | 
|  | int get hashCode => Object.hash(packageName, url); | 
|  |  | 
|  | @override | 
|  | bool operator ==(Object other) { | 
|  | return other is HostedDescription && | 
|  | other.packageName == packageName && | 
|  | other.url == url; | 
|  | } | 
|  |  | 
|  | @override | 
|  | String format() => 'on $url'; | 
|  |  | 
|  | @override | 
|  | Object? serializeForPubspec({ | 
|  | required String? containingDir, | 
|  | required LanguageVersion languageVersion, | 
|  | }) { | 
|  | if (url == source.defaultUrl) { | 
|  | return null; | 
|  | } | 
|  | if (languageVersion >= | 
|  | LanguageVersion.firstVersionWithShorterHostedSyntax) { | 
|  | return url; | 
|  | } | 
|  | return {'url': url, 'name': packageName}; | 
|  | } | 
|  |  | 
|  | @override | 
|  | HostedSource get source => HostedSource.instance; | 
|  | } | 
|  |  | 
|  | class ResolvedHostedDescription extends ResolvedDescription { | 
|  | @override | 
|  | HostedDescription get description => super.description as HostedDescription; | 
|  |  | 
|  | /// The content hash of the package archive (the `tar.gz` file) of the | 
|  | /// PackageId described by this. | 
|  | /// | 
|  | /// This can be obtained in several ways: | 
|  | /// * Reported from a server in the archive_sha256 field. (will be null if the | 
|  | ///   server does not report this.) | 
|  | /// * Obtained from a pubspec.lock (will be null for legacy lock-files). | 
|  | /// * Read from the | 
|  | ///   `<PUB_CACHE>/hosted-hashes/<server>/<package>-<version>.sha256` file. | 
|  | ///   (will be null if the file doesn't exist for corrupt or legacy caches). | 
|  | final Uint8List? sha256; | 
|  |  | 
|  | ResolvedHostedDescription( | 
|  | HostedDescription super.description, { | 
|  | required this.sha256, | 
|  | }); | 
|  |  | 
|  | @override | 
|  | Object? serializeForLockfile({required String? containingDir}) { | 
|  | final hash = sha256; | 
|  | return { | 
|  | 'name': description.packageName, | 
|  | 'url': description.url, | 
|  | if (hash != null) 'sha256': hexEncode(hash), | 
|  | }; | 
|  | } | 
|  |  | 
|  | @override | 
|  | // We do not include the sha256 in the hashCode because of the equality | 
|  | // semantics. | 
|  | int get hashCode => description.hashCode; | 
|  |  | 
|  | @override | 
|  | bool operator ==(Object other) { | 
|  | return other is ResolvedHostedDescription && | 
|  | other.description == description && | 
|  | // A [sha256] of `null` means that we don't know the hash yet. | 
|  | // Therefore we have to assume it is equal to any known value. | 
|  | (sha256 == null || | 
|  | other.sha256 == null || | 
|  | fixedTimeBytesEquals(sha256, other.sha256)); | 
|  | } | 
|  |  | 
|  | ResolvedHostedDescription withSha256(Uint8List? newSha256) => | 
|  | ResolvedHostedDescription(description, sha256: newSha256); | 
|  | } | 
|  |  | 
|  | /// Information about a package version retrieved from /api/packages/$package< | 
|  | class _VersionInfo { | 
|  | final Pubspec pubspec; | 
|  | final Uri archiveUrl; | 
|  | final Version version; | 
|  |  | 
|  | /// The sha256 digest of the archive according to the package-repository. | 
|  | final Uint8List? archiveSha256; | 
|  | final PackageStatus status; | 
|  |  | 
|  | _VersionInfo( | 
|  | this.version, | 
|  | this.pubspec, | 
|  | this.archiveUrl, | 
|  | this.status, | 
|  | this.archiveSha256, | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Information about a security advisory affecting a package. The information | 
|  | /// is retrieved from /api/packages/$package/advisories | 
|  | class Advisory { | 
|  | String id; | 
|  | Set<String> affectedVersions; | 
|  | List<String> aliases; | 
|  | String summary; | 
|  | String? pubDisplayUrl; | 
|  | Advisory({ | 
|  | required this.id, | 
|  | required this.affectedVersions, | 
|  | required this.summary, | 
|  | this.aliases = const [], | 
|  | this.pubDisplayUrl, | 
|  | }); | 
|  |  | 
|  | String get displayHandle => pubDisplayUrl ?? id; | 
|  | } | 
|  |  | 
|  | /// Given a URL, returns a "normalized" string to be used as a directory name | 
|  | /// for packages downloaded from the server at that URL. | 
|  | /// | 
|  | /// This normalization strips off the scheme (which is presumed to be HTTP or | 
|  | /// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it | 
|  | /// incorrectly: it uses the character's *decimal* ASCII value instead of hex. | 
|  | /// | 
|  | /// This could cause an ambiguity since some characters get encoded as three | 
|  | /// digits and others two. It's possible for one to be a prefix of the other. | 
|  | /// In practice, the set of characters that are encoded don't happen to have | 
|  | /// any collisions, so the encoding is reversible. | 
|  | /// | 
|  | /// This behavior is a bug, but is being preserved for compatibility. | 
|  | String _urlToDirectory(String hostedUrl) { | 
|  | // Normalize all loopback URLs to "localhost". | 
|  | final url = hostedUrl.replaceAllMapped( | 
|  | RegExp(r'^(https?://)(127\.0\.0\.1|\[::1\]|localhost)?'), (match) { | 
|  | // Don't include the scheme for HTTPS URLs. This makes the directory names | 
|  | // nice for the default and most recommended scheme. We also don't include | 
|  | // it for localhost URLs, since they're always known to be HTTP. | 
|  | final localhost = match[2] == null ? '' : 'localhost'; | 
|  | final scheme = | 
|  | match[1] == 'https://' || localhost.isNotEmpty ? '' : match[1]; | 
|  | return '$scheme$localhost'; | 
|  | }); | 
|  | return replace( | 
|  | url, | 
|  | RegExp(r'[<>:"\\/|?*%]'), | 
|  | (match) => '%${match[0]!.codeUnitAt(0)}', | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Given a directory name in the system cache, returns the URL of the server | 
|  | /// whose packages it contains. | 
|  | /// | 
|  | /// See [_urlToDirectory] for details on the mapping. Note that because the | 
|  | /// directory name does not preserve the scheme, this has to guess at it. It | 
|  | /// chooses "http" for loopback URLs (mainly to support the pub tests) and | 
|  | /// "https" for all others. | 
|  | String _directoryToUrl(String directory) { | 
|  | // Decode the pseudo-URL-encoded characters. | 
|  | const chars = '<>:"\\/|?*%'; | 
|  | for (var i = 0; i < chars.length; i++) { | 
|  | final c = chars.substring(i, i + 1); | 
|  | directory = directory.replaceAll('%${c.codeUnitAt(0)}', c); | 
|  | } | 
|  |  | 
|  | // If the URL has an explicit scheme, use that. | 
|  | if (directory.contains('://')) { | 
|  | return Uri.parse(directory).toString(); | 
|  | } | 
|  |  | 
|  | // Otherwise, default to http for localhost and https for everything else. | 
|  | final scheme = | 
|  | isLoopback(directory.replaceAll(RegExp(':.*'), '')) ? 'http' : 'https'; | 
|  | return Uri.parse('$scheme://$directory').toString(); | 
|  | } | 
|  |  | 
|  | // TODO(sigurdm): This is quite inelegant. | 
|  | class _RefAndCache { | 
|  | final PackageRef ref; | 
|  | final SystemCache cache; | 
|  | _RefAndCache(this.ref, this.cache); | 
|  |  | 
|  | @override | 
|  | int get hashCode => ref.hashCode; | 
|  | @override | 
|  | bool operator ==(Object other) => other is _RefAndCache && other.ref == ref; | 
|  | } | 
|  |  | 
|  | /// A sink that can only have `add` called once, and that can retrieve the | 
|  | /// value. | 
|  | class _SingleValueSink<T> implements Sink<T> { | 
|  | late final T value; | 
|  |  | 
|  | @override | 
|  | void add(T data) { | 
|  | value = data; | 
|  | } | 
|  |  | 
|  | @override | 
|  | void close() {} | 
|  | } | 
|  |  | 
|  | @visibleForTesting | 
|  | const checksumHeaderName = 'x-goog-hash'; | 
|  |  | 
|  | /// Adds a checksum validation "tap" to the response stream and returns a | 
|  | /// wrapped `Stream` object, which should be used to consume the incoming data. | 
|  | /// | 
|  | /// As chunks are received, a CRC32C checksum is updated. | 
|  | /// Once the download is completed, the final checksum is compared with | 
|  | /// the one present in the checksum response header. | 
|  | /// | 
|  | /// Throws [PackageIntegrityException] if there is a checksum mismatch. | 
|  | Stream<List<int>> _validateCrc32c( | 
|  | Stream<List<int>> stream, | 
|  | int expectedChecksum, | 
|  | PackageId id, | 
|  | Uri archiveUrl, | 
|  | ) async* { | 
|  | final crc32c = Crc32c(); | 
|  |  | 
|  | await for (final chunk in stream) { | 
|  | crc32c.update(chunk); | 
|  | yield chunk; | 
|  | } | 
|  |  | 
|  | final actualChecksum = crc32c.finalize(); | 
|  |  | 
|  | log.fine( | 
|  | 'Computed checksum $actualChecksum for ${id.name} ${id.version} with ' | 
|  | 'expected CRC32C of $expectedChecksum.'); | 
|  |  | 
|  | if (actualChecksum != expectedChecksum) { | 
|  | throw PackageIntegrityException( | 
|  | 'Package archive for ${id.name} ${id.version} downloaded from ' | 
|  | '"$archiveUrl" has "x-goog-hash: crc32c=$expectedChecksum", which ' | 
|  | 'doesn\'t match the checksum of the archive downloaded.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Parses response [headers] and returns the archive's CRC32C checksum. | 
|  | /// | 
|  | /// In most cases, GCS provides both MD5 and CRC32C checksums in its response | 
|  | /// headers. It uses the header name "x-goog-hash" for these values. It has | 
|  | /// been documented and observed that GCS will send multiple response headers | 
|  | /// with the same "x-goog-hash" token as the key. | 
|  | /// https://cloud.google.com/storage/docs/xml-api/reference-headers#xgooghash | 
|  | /// | 
|  | /// Additionally, when the Dart http client encounters multiple response | 
|  | /// headers with the same key, it concatenates their values with a comma | 
|  | /// before inserting a single item with that key and concatenated value into | 
|  | /// its response "headers" Map. | 
|  | /// See https://github.com/dart-lang/http/issues/24 | 
|  | /// https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/base_response.dart#L29 | 
|  | /// | 
|  | /// Throws [PackageIntegrityException] if the CRC32C checksum cannot be parsed. | 
|  | int? _parseCrc32c(Map<String, String> headers, String fileName) { | 
|  | final checksumHeader = headers[checksumHeaderName]; | 
|  | if (checksumHeader == null) return null; | 
|  |  | 
|  | final parts = checksumHeader.split(','); | 
|  | for (final part in parts) { | 
|  | if (part.startsWith('crc32c=')) { | 
|  | final undecoded = part.substring('crc32c='.length); | 
|  |  | 
|  | try { | 
|  | final bytes = base64Decode(undecoded); | 
|  |  | 
|  | // CRC32C must be 32 bits, or 4 bytes. | 
|  | if (bytes.length != 4) { | 
|  | throw FormatException('CRC32C checksum has invalid length', bytes); | 
|  | } | 
|  |  | 
|  | return ByteData.view(bytes.buffer).getUint32(0); | 
|  | } on FormatException catch (e, s) { | 
|  | log.exception(e, s); | 
|  | throw PackageIntegrityException( | 
|  | 'Package archive "$fileName" has a malformed CRC32C checksum in ' | 
|  | 'its response headers'); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return null; | 
|  | } |